feat: relay command

This commit is contained in:
Alejandro Gómez
2025-12-11 00:09:26 +01:00
parent 929365a813
commit e926272686
16 changed files with 1064 additions and 5 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Logs
logs
*.log
*.tsbuildinfo
npm-debug.log*
yarn-debug.log*
yarn-error.log*

25
TODO.md Normal file
View File

@@ -0,0 +1,25 @@
# TODO
## Known Issues
### RTL Support in Rich Text
**Priority**: Medium
**File**: `src/components/nostr/RichText/Text.tsx`
Current RTL implementation is partial and has limitations:
- RTL text direction works (`dir` attribute on spans)
- RTL text alignment (right-align) doesn't work properly with inline elements
- Mixed LTR/RTL content with inline elements (hashtags, mentions) creates layout conflicts
**The core problem**:
- Inline elements (hashtags, mentions) need inline flow to stay on same line
- RTL alignment requires block-level containers
- These two requirements conflict
**Potential solutions to explore**:
1. Line-aware rendering at RichText component level (parse and group by lines)
2. CSS-based approach with unicode-bidi and direction properties
3. Separate rendering paths for pure RTL content vs mixed content
4. Accept partial RTL support and document limitations
**Test case**: Arabic text with hashtags on same line should display properly with right-alignment.

View File

@@ -103,6 +103,9 @@ export default function Home() {
case "decode":
content = <DecodeViewer args={window.props.args} />;
break;
case "relay":
content = <RelayViewer url={window.props.url} />;
break;
default:
content = (
<div className="p-4 text-muted-foreground">

View File

@@ -1,5 +1,4 @@
import { useState } from "react";
import { Copy, Check } from "lucide-react";
import { Check, Copy } from "lucide-react";
import {
Dialog,
DialogContent,

View File

@@ -0,0 +1,46 @@
import { getNIPInfo } from "../lib/nip-icons";
import { useGrimoire } from "@/core/state";
export interface NIPBadgeProps {
nipNumber: number;
className?: string;
showName?: boolean;
}
/**
* NIPBadge - Reusable component for displaying NIP badges
* Shows icon, number, optional name, and links to NIP page
*/
export function NIPBadge({
nipNumber,
className = "",
showName = true,
}: NIPBadgeProps) {
const { addWindow } = useGrimoire();
const nipInfo = getNIPInfo(nipNumber);
const name = nipInfo?.name || `NIP-${nipNumber}`;
const description =
nipInfo?.description || `Nostr Implementation Possibility ${nipNumber}`;
const openNIP = () => {
const paddedNum = nipNumber.toString().padStart(2, "0");
addWindow(
"nip",
{ number: paddedNum },
nipInfo ? `NIP ${paddedNum} - ${nipInfo?.name}` : `NIP ${paddedNum}`,
);
};
return (
<button
onClick={openNIP}
className={`flex items-center gap-1.5 rounded-md border bg-card px-2.5 py-1.5 text-sm hover:bg-muted/20 cursor-crosshair ${className}`}
title={description}
>
<span className="font-mono font-medium">{nipNumber}</span>
{showName && nipInfo && (
<span className="text-muted-foreground">· {name}</span>
)}
</button>
);
}

View File

@@ -0,0 +1,88 @@
import { Copy, Check, Server } from "lucide-react";
import { useRelayInfo } from "../hooks/useRelayInfo";
import { useCopy } from "../hooks/useCopy";
import { Button } from "./ui/button";
import { UserName } from "./nostr/UserName";
import { NIPBadge } from "./NIPBadge";
export interface RelayViewerProps {
url: string;
}
export function RelayViewer({ url }: RelayViewerProps) {
const info = useRelayInfo(url);
const { copy, copied } = useCopy();
return (
<div className="flex flex-col gap-6 p-6">
{/* Header */}
<div className="flex items-center gap-4">
{info?.icon ? (
<img src={info.icon} alt={info.name || url} className="size-16" />
) : (
<Server className="size-16 text-muted-foreground" />
)}
<div className="flex-1">
<h2 className="text-2xl font-bold">
{info?.name || "Unknown Relay"}
</h2>
<div className="flex items-center gap-2 text-xs font-mono text-muted-foreground">
{url}
<Button
variant="ghost"
size="icon"
className="size-5"
onClick={() => copy(url)}
>
{copied ? (
<Check className="size-3" />
) : (
<Copy className="size-3" />
)}
</Button>
</div>
{info?.description && (
<p className="text-sm mt-2">{info.description}</p>
)}
</div>
</div>
{/* Operator */}
{(info?.contact || info?.pubkey) && (
<div>
<h3 className="mb-2 font-semibold text-sm">Operator</h3>
<div className="space-y-2 text-sm">
{info.contact && info.contact.length == 64 && (
<UserName pubkey={info.contact} />
)}
{info.pubkey && info.pubkey.length === 64 && (
<UserName pubkey={info.pubkey} />
)}
</div>
</div>
)}
{/* Software */}
{(info?.software || info?.version) && (
<div>
<h3 className="mb-2 font-semibold text-sm">Software</h3>
<span className="text-sm text-muted-foreground">
{info.software || info.version}
</span>
</div>
)}
{/* Supported NIPs */}
{info?.supported_nips && info.supported_nips.length > 0 && (
<div>
<h3 className="mb-3 font-semibold text-sm">Supported NIPs</h3>
<div className="flex flex-wrap gap-2">
{info.supported_nips.map((num: number) => (
<NIPBadge key={num} nipNumber={num} showName={true} />
))}
</div>
</div>
)}
</div>
);
}

51
src/hooks/useRelayInfo.ts Normal file
View File

@@ -0,0 +1,51 @@
import { useLiveQuery } from "dexie-react-hooks";
import { useEffect } from "react";
import { RelayInformation } from "../types/nip11";
import { fetchRelayInfo } from "../lib/nip11";
import db from "../services/db";
/**
* React hook to fetch and cache relay information (NIP-11)
* @param wsUrl - WebSocket URL of the relay (ws:// or wss://)
* @returns Relay information or undefined if not yet loaded
*/
export function useRelayInfo(wsUrl: string | undefined): RelayInformation | undefined {
const cached = useLiveQuery(
() => (wsUrl ? db.relayInfo.get(wsUrl) : undefined),
[wsUrl],
);
useEffect(() => {
if (!wsUrl) return;
if (cached) return;
// Fetch relay info if not in cache
fetchRelayInfo(wsUrl).then((info) => {
if (info) {
db.relayInfo.put({
url: wsUrl,
info,
fetchedAt: Date.now(),
});
}
});
}, [cached, wsUrl]);
return cached?.info;
}
/**
* React hook to check if a relay supports a specific NIP
* @param wsUrl - WebSocket URL of the relay
* @param nipNumber - NIP number to check (e.g., 42 for NIP-42)
* @returns true if supported, false if not, undefined if not yet loaded
*/
export function useRelaySupportsNip(
wsUrl: string | undefined,
nipNumber: number,
): boolean | undefined {
const info = useRelayInfo(wsUrl);
if (!info) return undefined;
return info.supported_nips?.includes(nipNumber) ?? false;
}

577
src/lib/nip-icons.ts Normal file
View File

@@ -0,0 +1,577 @@
/**
* NIP Icon Mapping
* Maps NIP numbers to Lucide icons for visual representation
*/
import {
FileText,
Lock,
Hash,
MessageSquare,
Tag,
Image,
Link,
Zap,
Key,
Shield,
Search,
Calendar,
Users,
Mail,
Gift,
Flag,
AlertCircle,
Globe,
Server,
Database,
Eye,
Heart,
Star,
Bookmark,
Share2,
Filter,
Coins,
Video,
Music,
Code,
ShoppingCart,
GitBranch,
Package,
Wallet,
Radio,
Compass,
Gamepad2,
type LucideIcon,
} from "lucide-react";
export interface NIPInfo {
number: number;
name: string;
description: string;
icon: LucideIcon;
deprecated?: boolean;
}
export const NIP_METADATA: Record<number | string, NIPInfo> = {
// Core Protocol
1: {
number: 1,
name: "Basic Protocol",
description: "Basic protocol flow description",
icon: FileText,
},
2: {
number: 2,
name: "Follow List",
description: "Contact list and petnames",
icon: Users,
},
4: {
number: 4,
name: "Encrypted DMs",
description: "Encrypted direct messages",
icon: Mail,
deprecated: true,
},
5: {
number: 5,
name: "Mapping Nostr keys to DNS",
description: "Mapping Nostr keys to DNS-based internet identifiers",
icon: Globe,
},
6: {
number: 6,
name: "Key Derivation",
description: "Basic key derivation from mnemonic seed phrase",
icon: Key,
},
7: {
number: 7,
name: "window.nostr",
description: "window.nostr capability for web browsers",
icon: Globe,
},
8: {
number: 8,
name: "Mentions",
description: "Handling mentions",
icon: Tag,
deprecated: true,
},
9: {
number: 9,
name: "Event Deletion",
description: "Event deletion",
icon: AlertCircle,
},
10: {
number: 10,
name: "Conventions",
description: "Conventions for clients' use of e and p tags",
icon: Tag,
},
11: {
number: 11,
name: "Relay Info",
description: "Relay information document",
icon: Server,
},
13: {
number: 13,
name: "Proof of Work",
description: "Proof of work",
icon: Zap,
},
14: {
number: 14,
name: "Subject Tag",
description: "Subject tag in text events",
icon: Tag,
},
15: {
number: 15,
name: "Marketplace",
description: "Marketplace (for resilient marketplaces)",
icon: ShoppingCart,
},
17: {
number: 17,
name: "Private DMs",
description: "Private Direct Messages",
icon: Lock,
},
18: { number: 18, name: "Reposts", description: "Reposts", icon: Share2 },
19: {
number: 19,
name: "bech32 Entities",
description: "bech32-encoded entities",
icon: Hash,
},
21: {
number: 21,
name: "nostr: URI",
description: "nostr: URI scheme",
icon: Link,
},
22: {
number: 22,
name: "Comment",
description: "Comment",
icon: MessageSquare,
},
23: {
number: 23,
name: "Long-form",
description: "Long-form content",
icon: FileText,
},
24: {
number: 24,
name: "Extra Metadata",
description: "Extra metadata fields and tags",
icon: Tag,
},
25: { number: 25, name: "Reactions", description: "Reactions", icon: Heart },
26: {
number: 26,
name: "Delegated Signing",
description: "Delegated event signing",
icon: Key,
deprecated: true,
},
27: {
number: 27,
name: "Text Note References",
description: "Text note references",
icon: Link,
},
28: {
number: 28,
name: "Public Chat",
description: "Public chat",
icon: MessageSquare,
},
29: {
number: 29,
name: "Relay Groups",
description: "Relay-based groups",
icon: Users,
},
30: {
number: 30,
name: "Custom Emoji",
description: "Custom emoji",
icon: Gift,
},
31: {
number: 31,
name: "Unknown Events",
description: "Dealing with unknown event kinds",
icon: AlertCircle,
},
32: { number: 32, name: "Labeling", description: "Labeling", icon: Tag },
34: { number: 34, name: "Git", description: "git stuff", icon: GitBranch },
35: { number: 35, name: "Torrents", description: "Torrents", icon: Share2 },
36: {
number: 36,
name: "Sensitive Content",
description: "Sensitive content warnings",
icon: Eye,
},
37: {
number: 37,
name: "Draft Events",
description: "Draft Events",
icon: FileText,
},
38: {
number: 38,
name: "User Status",
description: "User statuses",
icon: Flag,
},
39: {
number: 39,
name: "External Identity",
description: "External identities in profiles",
icon: Globe,
},
40: {
number: 40,
name: "Expiration",
description: "Expiration timestamp",
icon: Calendar,
},
42: {
number: 42,
name: "Authentication",
description: "Authentication of clients to relays",
icon: Shield,
},
43: {
number: 43,
name: "Relay Access",
description: "Fast Authentication and Relay Access",
icon: Server,
},
44: {
number: 44,
name: "Encrypted Payloads",
description: "Encrypted Payloads (Versioned)",
icon: Lock,
},
45: {
number: 45,
name: "Event Counts",
description: "Counting results",
icon: Hash,
},
46: {
number: 46,
name: "Remote Signing",
description: "Nostr connect protocol",
icon: Key,
},
47: {
number: 47,
name: "Wallet Connect",
description: "Wallet connect",
icon: Wallet,
},
48: { number: 48, name: "Proxy Tags", description: "Proxy tags", icon: Tag },
49: {
number: 49,
name: "Private Key Encryption",
description: "Private key encryption",
icon: Lock,
},
50: {
number: 50,
name: "Search",
description: "Search capability",
icon: Search,
},
51: { number: 51, name: "Lists", description: "Lists", icon: Filter },
52: {
number: 52,
name: "Calendar Events",
description: "Calendar Events",
icon: Calendar,
},
53: {
number: 53,
name: "Live Activities",
description: "Live Activities",
icon: Radio,
},
54: { number: 54, name: "Wiki", description: "Wiki", icon: FileText },
55: {
number: 55,
name: "Android Signer",
description: "Android Signer Application",
icon: Key,
},
56: { number: 56, name: "Reporting", description: "Reporting", icon: Flag },
57: {
number: 57,
name: "Lightning Zaps",
description: "Lightning zaps",
icon: Zap,
},
58: { number: 58, name: "Badges", description: "Badges", icon: Star },
59: { number: 59, name: "Gift Wrap", description: "Gift Wrap", icon: Gift },
60: {
number: 60,
name: "Cashu Wallet",
description: "Cashu Wallet",
icon: Wallet,
},
61: { number: 61, name: "Nutzaps", description: "Nutzaps", icon: Zap },
62: {
number: 62,
name: "Request to Vanish",
description: "Request to Vanish",
icon: Eye,
},
64: { number: 64, name: "Chess", description: "Chess (PGN)", icon: Gamepad2 },
65: {
number: 65,
name: "Relay List",
description: "Relay list metadata",
icon: Server,
},
66: {
number: 66,
name: "Relay Discovery",
description: "Relay Discovery",
icon: Compass,
},
68: {
number: 68,
name: "Picture-first",
description: "Picture-first feeds",
icon: Image,
},
69: {
number: 69,
name: "P2P Order",
description: "Peer-to-peer Order events",
icon: ShoppingCart,
},
70: {
number: 70,
name: "Protected Events",
description: "Protected Events",
icon: Shield,
},
71: {
number: 71,
name: "Video Events",
description: "Video Events",
icon: Video,
},
72: {
number: 72,
name: "Moderation",
description: "Moderated communities",
icon: Shield,
},
73: {
number: 73,
name: "External Content IDs",
description: "External Content IDs",
icon: Link,
},
75: { number: 75, name: "Zap Goals", description: "Zap Goals", icon: Zap },
77: {
number: 77,
name: "Negentropy",
description: "Negentropy Protocol Sync",
icon: Server,
},
78: {
number: 78,
name: "App Data",
description: "Application-specific data",
icon: Database,
},
84: {
number: 84,
name: "Highlights",
description: "Highlights",
icon: Bookmark,
},
86: {
number: 86,
name: "Relay Management",
description: "Relay Management API",
icon: Server,
},
87: {
number: 87,
name: "Ecash Mints",
description: "Ecash Mint Discoverability",
icon: Coins,
},
88: { number: 88, name: "Polls", description: "Polls", icon: Filter },
89: {
number: 89,
name: "App Handlers",
description: "Recommended application handlers",
icon: Package,
},
90: {
number: 90,
name: "Data Vending",
description: "Data Vending Machines",
icon: Database,
},
92: {
number: 92,
name: "Media Attachments",
description: "Media Attachments",
icon: Image,
},
94: {
number: 94,
name: "File Metadata",
description: "File metadata",
icon: Image,
},
96: {
number: 96,
name: "HTTP File Storage",
description: "HTTP File Storage Integration",
icon: Server,
deprecated: true,
},
98: {
number: 98,
name: "HTTP Auth",
description: "HTTP authentication",
icon: Lock,
},
99: {
number: 99,
name: "Classified Listings",
description: "Classified listings",
icon: Tag,
},
// Hex NIPs (A0-EE)
A0: {
number: 0xa0,
name: "Voice Messages",
description: "Voice Messages",
icon: Music,
},
B0: {
number: 0xb0,
name: "Web Bookmarks",
description: "Web Bookmarks",
icon: Bookmark,
},
B7: { number: 0xb7, name: "Blossom", description: "Blossom", icon: Package },
BE: {
number: 0xbe,
name: "BLE",
description: "BLE Communications",
icon: Radio,
},
C0: {
number: 0xc0,
name: "Code Snippets",
description: "Code Snippets",
icon: Code,
},
C7: {
number: 0xc7,
name: "Chats",
description: "Chats",
icon: MessageSquare,
},
EE: {
number: 0xee,
name: "E2EE MLS",
description: "E2EE Messaging (MLS)",
icon: Lock,
},
"7D": {
number: 0x7d,
name: "Threads",
description: "Threads",
icon: MessageSquare,
},
};
/**
* Get NIP metadata by number (handles both decimal and hex)
*/
export function getNIPInfo(nipNumber: number | string): NIPInfo | undefined {
// Try direct lookup
if (NIP_METADATA[nipNumber]) {
return NIP_METADATA[nipNumber];
}
// Try hex conversion for numbers > 99
if (typeof nipNumber === "number" && nipNumber > 99) {
const hexKey = nipNumber.toString(16).toUpperCase();
return NIP_METADATA[hexKey];
}
return undefined;
}
/**
* Get all supported NIPs with their metadata, excluding deprecated ones
*/
export function getSupportedNIPsInfo(
nipNumbers: number[],
includeDeprecated: boolean = false,
): (NIPInfo | { number: number; name: string; icon: LucideIcon })[] {
return nipNumbers
.map((num) => {
const info = getNIPInfo(num);
if (info) {
// Skip deprecated NIPs unless explicitly included
if (!includeDeprecated && info.deprecated) {
return null;
}
return info;
}
// Fallback for unknown NIPs
return {
number: num,
name: `NIP-${num}`,
icon: FileText,
};
})
.filter(
(
nip,
): nip is NIPInfo | { number: number; name: string; icon: LucideIcon } =>
nip !== null,
);
}
/**
* Group NIPs by a single consolidated category
* For relay viewer, we show all NIPs in one list
*/
export function groupNIPsByCategory(
nipNumbers: number[],
): Record<string, NIPInfo[]> {
const grouped: Record<string, NIPInfo[]> = {
"Supported NIPs": [],
};
nipNumbers.forEach((num) => {
const info = getNIPInfo(num);
// Skip deprecated NIPs
if (info && !info.deprecated) {
grouped["Supported NIPs"].push(info);
}
});
return grouped;
}

100
src/lib/nip11.ts Normal file
View File

@@ -0,0 +1,100 @@
import { RelayInformation } from "../types/nip11";
import db from "../services/db";
/**
* NIP-11: Relay Information Document
* https://github.com/nostr-protocol/nips/blob/master/11.md
*/
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
/**
* Fetch relay information document
* NIP-11 specifies: GET request with Accept: application/nostr+json header
*/
export async function fetchRelayInfo(
wsUrl: string,
): Promise<RelayInformation | null> {
try {
// Convert ws:// or wss:// to https://
const httpUrl = wsUrl.replace(/^ws(s)?:/, "https:");
const response = await fetch(httpUrl, {
headers: { Accept: "application/nostr+json" },
});
if (!response.ok) return null;
return (await response.json()) as RelayInformation;
} catch (error) {
console.warn(`NIP-11: Failed to fetch ${wsUrl}:`, error);
return null;
}
}
/**
* Get relay information with caching (fetches if needed)
*/
export async function getRelayInfo(
wsUrl: string,
): Promise<RelayInformation | null> {
const cached = await db.relayInfo.get(wsUrl);
const isExpired = !cached || Date.now() - cached.fetchedAt > CACHE_DURATION;
if (!isExpired) return cached.info;
const info = await fetchRelayInfo(wsUrl);
if (info) {
await db.relayInfo.put({ url: wsUrl, info, fetchedAt: Date.now() });
}
return info;
}
/**
* Get cached relay info only (no network request)
*/
export async function getCachedRelayInfo(
wsUrl: string,
): Promise<RelayInformation | null> {
const cached = await db.relayInfo.get(wsUrl);
return cached?.info ?? null;
}
/**
* Fetch multiple relays in parallel
*/
export async function getRelayInfoBatch(
wsUrls: string[],
): Promise<Map<string, RelayInformation>> {
const results = new Map<string, RelayInformation>();
const infos = await Promise.all(wsUrls.map((url) => getRelayInfo(url)));
infos.forEach((info, i) => {
if (info) results.set(wsUrls[i], info);
});
return results;
}
/**
* Clear relay info cache
*/
export async function clearRelayInfoCache(wsUrl?: string): Promise<void> {
if (wsUrl) {
await db.relayInfo.delete(wsUrl);
} else {
await db.relayInfo.clear();
}
}
/**
* Check if relay supports a specific NIP
*/
export async function relaySupportsNip(
wsUrl: string,
nipNumber: number,
): Promise<boolean> {
const info = await getRelayInfo(wsUrl);
return info?.supported_nips?.includes(nipNumber) ?? false;
}

View File

@@ -0,0 +1,24 @@
/**
* Nostr validation utilities for hex strings and identifiers
*/
/**
* Check if a string is a valid 64-character hex pubkey
*/
export function isValidHexPubkey(value: string): boolean {
return /^[0-9a-f]{64}$/i.test(value);
}
/**
* Check if a string is a valid 64-character hex event ID
*/
export function isValidHexEventId(value: string): boolean {
return /^[0-9a-f]{64}$/i.test(value);
}
/**
* Normalize hex string to lowercase
*/
export function normalizeHex(value: string): string {
return value.toLowerCase();
}

36
src/lib/relay-parser.ts Normal file
View File

@@ -0,0 +1,36 @@
export interface ParsedRelayCommand {
url: string;
}
/**
* Parse RELAY command arguments
*
* Examples:
* relay wss://relay.damus.io
* relay relay.primal.net
* relay nos.lol
*/
export function parseRelayCommand(args: string[]): ParsedRelayCommand {
if (args.length < 1) {
throw new Error("Usage: RELAY <url>");
}
let url = args[0];
// Auto-add wss:// protocol if not present
if (!url.startsWith("ws://") && !url.startsWith("wss://")) {
url = `wss://${url}`;
}
// Validate URL format
try {
const parsedUrl = new URL(url);
if (!parsedUrl.protocol.startsWith("ws")) {
throw new Error("Relay must be a WebSocket URL (ws:// or wss://)");
}
} catch {
throw new Error(`Invalid relay URL: ${url}`);
}
return { url };
}

View File

@@ -1,5 +1,6 @@
import { ProfileContent } from "applesauce-core/helpers";
import { Dexie, Table } from "dexie";
import { RelayInformation } from "../types/nip11";
export interface Profile extends ProfileContent {
pubkey: string;
@@ -17,17 +18,25 @@ export interface Nip {
fetchedAt: number;
}
export interface RelayInfo {
url: string;
info: RelayInformation;
fetchedAt: number;
}
class GrimoireDb extends Dexie {
profiles!: Table<Profile>;
nip05!: Table<Nip05>;
nips!: Table<Nip>;
relayInfo!: Table<RelayInfo>;
constructor(name: string) {
super(name);
this.version(3).stores({
this.version(4).stores({
profiles: "&pubkey",
nip05: "&nip05",
nips: "&id",
relayInfo: "&url",
});
}
}

View File

@@ -10,7 +10,8 @@ export type AppId =
| "open"
| "profile"
| "encode"
| "decode";
| "decode"
| "relay";
export interface WindowInstance {
id: string;

View File

@@ -3,6 +3,7 @@ import type { AppId } from "./app";
import { parseOpenCommand } from "@/lib/open-parser";
import { parseProfileCommand } from "@/lib/profile-parser";
import { parseRelayCommand } from "@/lib/relay-parser";
import { resolveNip05Batch } from "@/lib/nip05";
export interface ManPageEntry {

99
src/types/nip11.ts Normal file
View File

@@ -0,0 +1,99 @@
/**
* NIP-11: Relay Information Document
* https://github.com/nostr-protocol/nips/blob/master/11.md
*/
export interface RelayInformation {
/** DNS name of the relay */
name?: string;
/** Description of the relay in plain text */
description?: string;
/** Public key of the relay administrator */
pubkey?: string;
/** Administrative contact for the relay */
contact?: string;
/** List of NIPs supported by this relay */
supported_nips?: number[];
/** Software version running the relay */
software?: string;
/** Software version identifier */
version?: string;
/** Relay limitations and policies */
limitation?: RelayLimitation;
/** Payment information for paid relays */
payments_url?: string;
/** Relay usage fees */
fees?: RelayFees;
/** URL to the relay's icon */
icon?: string;
}
export interface RelayLimitation {
/** Maximum length of the content field */
max_message_length?: number;
/** Maximum number of subscriptions per WebSocket connection */
max_subscriptions?: number;
/** Maximum number of filters per subscription */
max_filters?: number;
/** Maximum length of subscription ID */
max_subid_length?: number;
/** Minimum prefix length for search filters */
min_prefix?: number;
/** Maximum number of elements in various arrays */
max_limit?: number;
/** Minimum POW difficulty for events */
min_pow_difficulty?: number;
/** Whether authentication is required */
auth_required?: boolean;
/** Whether payment is required */
payment_required?: boolean;
/** Restricted write access */
restricted_writes?: boolean;
/** Created at lower limit (oldest events accepted) */
created_at_lower_limit?: number;
/** Created at upper limit (newest events accepted) */
created_at_upper_limit?: number;
}
export interface RelayFees {
/** Admission fee structure */
admission?: Array<{ amount: number; unit: string }>;
/** Subscription fee structure */
subscription?: Array<{ amount: number; unit: string; period?: number }>;
/** Publication fee structure */
publication?: Array<{ kinds?: number[]; amount: number; unit: string }>;
}
export interface CachedRelayInfo {
/** Relay URL (websocket) */
url: string;
/** Relay information document */
info: RelayInformation;
/** Timestamp when the info was fetched */
fetchedAt: number;
}

View File

@@ -1 +0,0 @@
{"root":["./src/main.tsx","./src/root.tsx","./src/vite-env.d.ts","./src/components/command.tsx","./src/components/commandlauncher.tsx","./src/components/decodeviewer.tsx","./src/components/encodeviewer.tsx","./src/components/eventdetailviewer.tsx","./src/components/grimoirewelcome.tsx","./src/components/home.tsx","./src/components/jsonviewer.tsx","./src/components/kindbadge.tsx","./src/components/kindrenderer.tsx","./src/components/manpage.tsx","./src/components/markdown.tsx","./src/components/nipbadge.tsx","./src/components/niprenderer.tsx","./src/components/profileviewer.tsx","./src/components/relayviewer.tsx","./src/components/reqviewer.tsx","./src/components/tabbar.tsx","./src/components/timestamp.tsx","./src/components/winviewer.tsx","./src/components/windowtoolbar.tsx","./src/components/nostr/embeddedevent.tsx","./src/components/nostr/feed.tsx","./src/components/nostr/mediadialog.tsx","./src/components/nostr/mediaembed.tsx","./src/components/nostr/quotedevent.tsx","./src/components/nostr/richtext.tsx","./src/components/nostr/username.tsx","./src/components/nostr/index.ts","./src/components/nostr/nip05.tsx","./src/components/nostr/npub.tsx","./src/components/nostr/relay-pool.tsx","./src/components/nostr/user-menu.tsx","./src/components/nostr/linkpreview/audiolink.tsx","./src/components/nostr/linkpreview/imagelink.tsx","./src/components/nostr/linkpreview/plainlink.tsx","./src/components/nostr/linkpreview/videolink.tsx","./src/components/nostr/linkpreview/index.ts","./src/components/nostr/richtext/emoji.tsx","./src/components/nostr/richtext/eventembed.tsx","./src/components/nostr/richtext/gallery.tsx","./src/components/nostr/richtext/hashtag.tsx","./src/components/nostr/richtext/link.tsx","./src/components/nostr/richtext/mention.tsx","./src/components/nostr/richtext/text.tsx","./src/components/nostr/richtext/index.ts","./src/components/nostr/kinds/baseeventrenderer.tsx","./src/components/nostr/kinds/kind0detailrenderer.tsx","./src/components/nostr/kinds/kind0renderer.tsx","./src/components/nostr/kinds/kind1063renderer.tsx","./src/components/nostr/kinds/kind1renderer.tsx","./src/components/nostr/kinds/kind20renderer.tsx","./src/components/nostr/kinds/kind21renderer.tsx","./src/components/nostr/kinds/kind22renderer.tsx","./src/components/nostr/kinds/kind30023detailrenderer.tsx","./src/components/nostr/kinds/kind30023renderer.tsx","./src/components/nostr/kinds/kind3renderer.tsx","./src/components/nostr/kinds/kind6renderer.tsx","./src/components/nostr/kinds/kind7renderer.tsx","./src/components/nostr/kinds/kind9735renderer.tsx","./src/components/nostr/kinds/kind9802detailrenderer.tsx","./src/components/nostr/kinds/kind9802renderer.tsx","./src/components/nostr/kinds/index.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/constants/kinds.ts","./src/constants/nips.ts","./src/core/logic.ts","./src/core/state.ts","./src/hooks/useaccountsync.ts","./src/hooks/usecopy.ts","./src/hooks/uselocale.ts","./src/hooks/usenip.ts","./src/hooks/usenip05.ts","./src/hooks/usenostrevent.ts","./src/hooks/useprofile.ts","./src/hooks/userelayconnection.ts","./src/hooks/userelayinfo.ts","./src/hooks/usereqtimeline.ts","./src/hooks/usetimeline.ts","./src/lib/decode-parser.ts","./src/lib/encode-parser.ts","./src/lib/imeta.ts","./src/lib/nip-icons.ts","./src/lib/nip-kinds.ts","./src/lib/nip05.ts","./src/lib/nip11.ts","./src/lib/nostr-utils.ts","./src/lib/nostr-validation.ts","./src/lib/open-parser.ts","./src/lib/profile-parser.ts","./src/lib/relay-parser.ts","./src/lib/req-parser.ts","./src/lib/utils.ts","./src/services/accounts.ts","./src/services/db.ts","./src/services/event-store.ts","./src/services/loaders.ts","./src/services/relay-pool.ts","./src/types/app.ts","./src/types/man.ts","./src/types/nip11.ts","./src/types/nostr.ts","./src/types/profile.ts"],"version":"5.6.3"}