@toon-protocol/relay 1.3.4 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -93
- package/dist/chunk-FXQSNOCG.js +939 -0
- package/dist/chunk-FXQSNOCG.js.map +1 -0
- package/dist/cli.js +38 -216
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +121 -1005
- package/dist/index.js +72 -424
- package/dist/index.js.map +1 -1
- package/package.json +3 -8
- package/dist/chunk-NYKVCNJL.js +0 -2103
- package/dist/chunk-NYKVCNJL.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/version.ts","../src/types.ts","../src/filters/matchFilter.ts","../src/storage/InMemoryEventStore.ts","../src/storage/SqliteEventStore.ts","../src/websocket/ConnectionHandler.ts","../src/websocket/NostrRelayServer.ts","../src/subscriber/RelaySubscriber.ts","../src/launcher/handlers/write-handler.ts","../src/launcher/health.ts","../src/launcher/relay.ts"],"sourcesContent":["/** Package version, surfaced on `GET /health`. */\nexport const VERSION = '0.1.0';\n","/**\n * Configuration options for the Nostr relay.\n */\nexport interface RelayServerConfig {\n /** Port to listen on (default: 7000) */\n port: number;\n /** Host/IP to bind to (default: '0.0.0.0'). Set to '127.0.0.1' for hidden service mode. */\n host?: string;\n /** Maximum concurrent connections (default: 100) */\n maxConnections?: number;\n /** Maximum subscriptions per connection (default: 20) */\n maxSubscriptionsPerConnection?: number;\n /** Maximum filters per subscription (default: 10) */\n maxFiltersPerSubscription?: number;\n /** Path to SQLite database file (default: ':memory:' for in-memory) */\n databasePath?: string;\n}\n\n/**\n * Default relay configuration values.\n */\nexport const DEFAULT_RELAY_CONFIG: Required<RelayServerConfig> = {\n port: 7000,\n host: '0.0.0.0',\n maxConnections: 100,\n maxSubscriptionsPerConnection: 20,\n maxFiltersPerSubscription: 10,\n databasePath: ':memory:',\n};\n","import type { NostrEvent } from 'nostr-tools/pure';\nimport type { Filter } from 'nostr-tools/filter';\n\n/**\n * Check if an event matches a single filter according to NIP-01 rules.\n *\n * Matching rules:\n * - All specified fields must match (AND logic)\n * - `ids` and `authors` support prefix matching\n * - Tag filters (#e, #p, etc.) match events with corresponding tags\n * - Empty filter matches all events\n *\n * @param event - The Nostr event to check\n * @param filter - The filter to match against\n * @returns true if the event matches the filter\n */\nexport function matchFilter(event: NostrEvent, filter: Filter): boolean {\n // Empty filter matches everything\n if (Object.keys(filter).length === 0) {\n return true;\n }\n\n // Check ids (prefix matching)\n if (filter.ids !== undefined && filter.ids.length > 0) {\n const matches = filter.ids.some((id) => event.id.startsWith(id));\n if (!matches) return false;\n }\n\n // Check authors (prefix matching)\n if (filter.authors !== undefined && filter.authors.length > 0) {\n const matches = filter.authors.some((author) =>\n event.pubkey.startsWith(author)\n );\n if (!matches) return false;\n }\n\n // Check kinds (exact matching)\n if (filter.kinds !== undefined && filter.kinds.length > 0) {\n if (!filter.kinds.includes(event.kind)) return false;\n }\n\n // Check since (created_at >= since)\n if (filter.since !== undefined) {\n if (event.created_at < filter.since) return false;\n }\n\n // Check until (created_at <= until)\n if (filter.until !== undefined) {\n if (event.created_at > filter.until) return false;\n }\n\n // Check tag filters (#e, #p, and generic #<single-letter>)\n for (const key of Object.keys(filter)) {\n if (key.startsWith('#') && key.length === 2) {\n const tagName = key.slice(1);\n const filterValues = filter[key as `#${string}`];\n\n if (filterValues !== undefined && filterValues.length > 0) {\n // Find matching tags in the event\n const eventTagValues = event.tags\n .filter((tag) => tag[0] === tagName)\n .map((tag) => tag[1]);\n\n // At least one filter value must match an event tag value\n const hasMatch = filterValues.some((v) => eventTagValues.includes(v));\n if (!hasMatch) return false;\n }\n }\n }\n\n return true;\n}\n","import type { NostrEvent } from 'nostr-tools/pure';\nimport type { Filter } from 'nostr-tools/filter';\nimport { matchFilter } from '../filters/index.js';\n\n/**\n * Interface for event storage backends.\n */\nexport interface EventStore {\n /** Store an event by its ID */\n store(event: NostrEvent): void;\n /** Retrieve a single event by ID */\n get(id: string): NostrEvent | undefined;\n /** Query events matching any of the provided filters */\n query(filters: Filter[]): NostrEvent[];\n /** Close the storage backend (optional) */\n close?(): void;\n}\n\n/**\n * In-memory implementation of EventStore.\n * Events are stored in a Map keyed by event ID.\n */\nexport class InMemoryEventStore implements EventStore {\n private events = new Map<string, NostrEvent>();\n\n store(event: NostrEvent): void {\n this.events.set(event.id, event);\n }\n\n get(id: string): NostrEvent | undefined {\n return this.events.get(id);\n }\n\n query(filters: Filter[]): NostrEvent[] {\n // Get all events\n const allEvents = Array.from(this.events.values());\n\n // If no filters provided, return all events sorted by created_at desc\n if (filters.length === 0) {\n return allEvents.sort((a, b) => b.created_at - a.created_at);\n }\n\n // Find events matching ANY filter (OR logic between filters)\n const matchingEvents: NostrEvent[] = [];\n\n for (const event of allEvents) {\n for (const filter of filters) {\n if (matchFilter(event, filter)) {\n matchingEvents.push(event);\n break; // Only add once even if matches multiple filters\n }\n }\n }\n\n // Sort by created_at descending\n matchingEvents.sort((a, b) => b.created_at - a.created_at);\n\n // Apply limit from first filter that has one (NIP-01 semantics)\n const limitFilter = filters.find((f) => f.limit !== undefined);\n if (limitFilter?.limit !== undefined) {\n return matchingEvents.slice(0, limitFilter.limit);\n }\n\n return matchingEvents;\n }\n\n /**\n * Close the storage backend (no-op for in-memory store).\n */\n close(): void {\n // No-op for in-memory store\n }\n}\n","import Database from 'better-sqlite3';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { Filter } from 'nostr-tools/filter';\nimport type { EventStore } from './InMemoryEventStore.js';\n\n/**\n * SQL schema for the events table.\n */\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n pubkey TEXT NOT NULL,\n kind INTEGER NOT NULL,\n content TEXT NOT NULL,\n tags TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n sig TEXT NOT NULL,\n received_at INTEGER NOT NULL\n)\n`;\n\n/**\n * SQL for creating indexes on the events table.\n */\nconst INDEX_SQL = [\n 'CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey)',\n 'CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind)',\n 'CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at)',\n 'CREATE INDEX IF NOT EXISTS idx_events_pubkey_kind ON events(pubkey, kind)',\n];\n\n/**\n * Initialize the database schema.\n */\nfunction initializeSchema(db: Database.Database): void {\n db.exec(SCHEMA_SQL);\n for (const indexSql of INDEX_SQL) {\n db.exec(indexSql);\n }\n}\n\n/**\n * Custom error class for relay storage errors.\n */\nexport class RelayError extends Error {\n constructor(\n message: string,\n public code: string\n ) {\n super(message);\n this.name = 'RelayError';\n }\n}\n\n/**\n * Check if an event kind is in the replaceable range (10000-19999).\n * Excludes TOON-specific parameterized kinds 10032-10099.\n */\nfunction isReplaceableKind(kind: number): boolean {\n return kind >= 10000 && kind <= 19999 && !(kind >= 10032 && kind <= 10099);\n}\n\n/**\n * Check if an event kind is in the parameterized replaceable range.\n * Covers NIP-33 (30000-39999) and TOON-specific kinds (10032-10099).\n */\nfunction isParameterizedReplaceableKind(kind: number): boolean {\n return (kind >= 30000 && kind <= 39999) || (kind >= 10032 && kind <= 10099);\n}\n\n/**\n * Get the 'd' tag value from an event's tags array.\n */\nfunction getDTagValue(tags: string[][]): string {\n const dTag = tags.find((tag) => tag[0] === 'd');\n return dTag?.[1] ?? '';\n}\n\n/**\n * SQLite implementation of EventStore.\n * Persists events to a SQLite database file.\n */\nexport class SqliteEventStore implements EventStore {\n private db: Database.Database;\n private insertStmt: Database.Statement;\n private getStmt: Database.Statement;\n private deleteByPubkeyKindStmt: Database.Statement;\n private deleteByPubkeyKindDTagStmt: Database.Statement;\n private getByPubkeyKindStmt: Database.Statement;\n private getByPubkeyKindDTagStmt: Database.Statement;\n\n /**\n * Create a new SqliteEventStore.\n * @param dbPath - Path to the database file. Use ':memory:' for in-memory database.\n */\n constructor(dbPath = ':memory:') {\n try {\n this.db = new Database(dbPath);\n initializeSchema(this.db);\n\n // Prepare statements for better performance\n this.insertStmt = this.db.prepare(`\n INSERT OR REPLACE INTO events (id, pubkey, kind, content, tags, created_at, sig, received_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n this.getStmt = this.db.prepare('SELECT * FROM events WHERE id = ?');\n\n this.deleteByPubkeyKindStmt = this.db.prepare(\n 'DELETE FROM events WHERE pubkey = ? AND kind = ?'\n );\n\n this.deleteByPubkeyKindDTagStmt = this.db.prepare(\n \"DELETE FROM events WHERE pubkey = ? AND kind = ? AND json_extract(tags, '$') LIKE ?\"\n );\n\n this.getByPubkeyKindStmt = this.db.prepare(\n 'SELECT id, created_at FROM events WHERE pubkey = ? AND kind = ?'\n );\n\n this.getByPubkeyKindDTagStmt = this.db.prepare(\n 'SELECT id, created_at FROM events WHERE pubkey = ? AND kind = ? AND tags LIKE ?'\n );\n } catch (error) {\n throw new RelayError(\n `Failed to initialize database: ${error instanceof Error ? error.message : String(error)}`,\n 'STORAGE_ERROR'\n );\n }\n }\n\n /**\n * Store an event in the database.\n * Handles replaceable and parameterized replaceable events according to NIP-01.\n */\n store(event: NostrEvent): void {\n try {\n const tagsJson = JSON.stringify(event.tags);\n const receivedAt = Math.floor(Date.now() / 1000);\n\n if (isReplaceableKind(event.kind)) {\n // Replaceable event (10000-19999)\n this.storeReplaceableEvent(event, tagsJson, receivedAt);\n } else if (isParameterizedReplaceableKind(event.kind)) {\n // Parameterized replaceable event (30000-39999)\n this.storeParameterizedReplaceableEvent(event, tagsJson, receivedAt);\n } else {\n // Regular event - INSERT OR IGNORE to handle duplicates\n const insertOrIgnore = this.db.prepare(`\n INSERT OR IGNORE INTO events (id, pubkey, kind, content, tags, created_at, sig, received_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n insertOrIgnore.run(\n event.id,\n event.pubkey,\n event.kind,\n event.content,\n tagsJson,\n event.created_at,\n event.sig,\n receivedAt\n );\n }\n } catch (error) {\n if (error instanceof RelayError) {\n throw error;\n }\n throw new RelayError(\n `Failed to store event: ${error instanceof Error ? error.message : String(error)}`,\n 'STORAGE_ERROR'\n );\n }\n }\n\n /**\n * Store a replaceable event (kinds 10000-19999).\n * Only keeps the latest event per pubkey+kind.\n */\n private storeReplaceableEvent(\n event: NostrEvent,\n tagsJson: string,\n receivedAt: number\n ): void {\n const existing = this.getByPubkeyKindStmt.get(event.pubkey, event.kind) as\n | { id: string; created_at: number }\n | undefined;\n\n if (existing) {\n // Only replace if new event is newer, or same time with lower id\n if (\n event.created_at > existing.created_at ||\n (event.created_at === existing.created_at && event.id < existing.id)\n ) {\n // Use transaction for atomicity\n const transaction = this.db.transaction(() => {\n this.deleteByPubkeyKindStmt.run(event.pubkey, event.kind);\n this.insertStmt.run(\n event.id,\n event.pubkey,\n event.kind,\n event.content,\n tagsJson,\n event.created_at,\n event.sig,\n receivedAt\n );\n });\n transaction();\n }\n // If existing event is newer or same, don't replace\n } else {\n // No existing event, just insert\n this.insertStmt.run(\n event.id,\n event.pubkey,\n event.kind,\n event.content,\n tagsJson,\n event.created_at,\n event.sig,\n receivedAt\n );\n }\n }\n\n /**\n * Store a parameterized replaceable event (kinds 30000-39999).\n * Only keeps the latest event per pubkey+kind+d-tag.\n */\n private storeParameterizedReplaceableEvent(\n event: NostrEvent,\n tagsJson: string,\n receivedAt: number\n ): void {\n const dTagValue = getDTagValue(event.tags);\n\n // For empty d-tag value, we need to match events that either:\n // 1. Have [\"d\", \"\"] in tags\n // 2. Have no d-tag at all (tags doesn't contain \"d\" as first element)\n let existing: { id: string; created_at: number } | undefined;\n\n if (dTagValue === '') {\n // Query for events with same pubkey and kind, then filter in code\n const candidates = this.db\n .prepare(\n 'SELECT id, created_at, tags FROM events WHERE pubkey = ? AND kind = ?'\n )\n .all(event.pubkey, event.kind) as {\n id: string;\n created_at: number;\n tags: string;\n }[];\n\n // Find one with empty or missing d-tag\n for (const candidate of candidates) {\n const candidateTags = JSON.parse(candidate.tags) as string[][];\n const candidateDTagValue = getDTagValue(candidateTags);\n if (candidateDTagValue === '') {\n existing = { id: candidate.id, created_at: candidate.created_at };\n break;\n }\n }\n } else {\n const dTagPattern = `%[\"d\",\"${dTagValue}\"%`;\n existing = this.getByPubkeyKindDTagStmt.get(\n event.pubkey,\n event.kind,\n dTagPattern\n ) as { id: string; created_at: number } | undefined;\n }\n\n if (existing) {\n // Only replace if new event is newer, or same time with lower id\n if (\n event.created_at > existing.created_at ||\n (event.created_at === existing.created_at && event.id < existing.id)\n ) {\n // Use transaction for atomicity - delete by ID for safety\n const transaction = this.db.transaction(() => {\n this.db.prepare('DELETE FROM events WHERE id = ?').run(existing.id);\n this.insertStmt.run(\n event.id,\n event.pubkey,\n event.kind,\n event.content,\n tagsJson,\n event.created_at,\n event.sig,\n receivedAt\n );\n });\n transaction();\n }\n // If existing event is newer or same, don't replace\n } else {\n // No existing event, just insert\n this.insertStmt.run(\n event.id,\n event.pubkey,\n event.kind,\n event.content,\n tagsJson,\n event.created_at,\n event.sig,\n receivedAt\n );\n }\n }\n\n /**\n * Retrieve an event by its ID.\n */\n get(id: string): NostrEvent | undefined {\n try {\n const row = this.getStmt.get(id) as\n | {\n id: string;\n pubkey: string;\n kind: number;\n content: string;\n tags: string;\n created_at: number;\n sig: string;\n }\n | undefined;\n\n if (!row) {\n return undefined;\n }\n\n return {\n id: row.id,\n pubkey: row.pubkey,\n kind: row.kind,\n content: row.content,\n tags: JSON.parse(row.tags) as string[][],\n created_at: row.created_at,\n sig: row.sig,\n };\n } catch (error) {\n throw new RelayError(\n `Failed to get event: ${error instanceof Error ? error.message : String(error)}`,\n 'STORAGE_ERROR'\n );\n }\n }\n\n /**\n * Query events matching any of the provided filters.\n */\n query(filters: Filter[]): NostrEvent[] {\n try {\n const { sql, params } = this.buildQuerySql(filters);\n const stmt = this.db.prepare(sql);\n const rows = stmt.all(...params) as {\n id: string;\n pubkey: string;\n kind: number;\n content: string;\n tags: string;\n created_at: number;\n sig: string;\n }[];\n\n return rows.map((row) => ({\n id: row.id,\n pubkey: row.pubkey,\n kind: row.kind,\n content: row.content,\n tags: JSON.parse(row.tags) as string[][],\n created_at: row.created_at,\n sig: row.sig,\n }));\n } catch (error) {\n throw new RelayError(\n `Failed to query events: ${error instanceof Error ? error.message : String(error)}`,\n 'STORAGE_ERROR'\n );\n }\n }\n\n /**\n * Build SQL query from filters.\n */\n private buildQuerySql(filters: Filter[]): { sql: string; params: unknown[] } {\n if (filters.length === 0) {\n return {\n sql: 'SELECT * FROM events ORDER BY created_at DESC',\n params: [],\n };\n }\n\n const conditions: string[] = [];\n const params: unknown[] = [];\n\n for (const filter of filters) {\n const filterConditions: string[] = [];\n\n if (filter.ids?.length) {\n // Prefix matching with LIKE\n const idConditions = filter.ids.map(() => 'id LIKE ?');\n filterConditions.push(`(${idConditions.join(' OR ')})`);\n params.push(...filter.ids.map((id) => `${id}%`));\n }\n\n if (filter.authors?.length) {\n const authorConditions = filter.authors.map(() => 'pubkey LIKE ?');\n filterConditions.push(`(${authorConditions.join(' OR ')})`);\n params.push(...filter.authors.map((a) => `${a}%`));\n }\n\n if (filter.kinds?.length) {\n filterConditions.push(\n `kind IN (${filter.kinds.map(() => '?').join(', ')})`\n );\n params.push(...filter.kinds);\n }\n\n if (filter.since !== undefined) {\n filterConditions.push('created_at >= ?');\n params.push(filter.since);\n }\n\n if (filter.until !== undefined) {\n filterConditions.push('created_at <= ?');\n params.push(filter.until);\n }\n\n // Handle tag filters (#e, #p, etc.)\n for (const [key, values] of Object.entries(filter)) {\n if (key.startsWith('#') && Array.isArray(values) && values.length > 0) {\n const tagName = key.slice(1);\n const tagConditions = values.map(() => `tags LIKE ?`);\n filterConditions.push(`(${tagConditions.join(' OR ')})`);\n params.push(...values.map((v) => `%[\"${tagName}\",\"${v}\"%`));\n }\n }\n\n if (filterConditions.length > 0) {\n conditions.push(`(${filterConditions.join(' AND ')})`);\n }\n }\n\n let sql = 'SELECT * FROM events';\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' OR ')}`;\n }\n sql += ' ORDER BY created_at DESC';\n\n // Apply limit from first filter that specifies it\n const limitFilter = filters.find((f) => f.limit !== undefined);\n if (limitFilter?.limit !== undefined) {\n sql += ' LIMIT ?';\n params.push(limitFilter.limit);\n }\n\n return { sql, params };\n }\n\n /**\n * Close the database connection.\n */\n close(): void {\n this.db.close();\n }\n}\n","import type { WebSocket } from 'ws';\nimport type { Filter } from 'nostr-tools/filter';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { EventStore } from '../storage/index.js';\nimport type { RelayServerConfig } from '../types.js';\nimport { DEFAULT_RELAY_CONFIG } from '../types.js';\nimport { matchFilter } from '../filters/index.js';\n\n/**\n * Represents an active subscription from a client.\n */\nexport interface Subscription {\n /** Unique subscription identifier from the client */\n id: string;\n /** Filters applied to this subscription */\n filters: Filter[];\n}\n\n/**\n * Handles NIP-01 messages for a single WebSocket connection.\n */\nexport class ConnectionHandler {\n private subscriptions = new Map<string, Subscription>();\n private config: Required<RelayServerConfig>;\n\n constructor(\n private ws: WebSocket,\n private eventStore: EventStore,\n config: Partial<RelayServerConfig> = {}\n ) {\n this.config = { ...DEFAULT_RELAY_CONFIG, ...config };\n }\n\n /**\n * Handle an incoming message from the WebSocket.\n */\n handleMessage(data: string): void {\n console.log(`[ConnectionHandler] Received message:`, data.slice(0, 150));\n let message: unknown[];\n\n try {\n const parsed = JSON.parse(data);\n if (!Array.isArray(parsed)) {\n this.sendNotice('error: invalid message format, expected JSON array');\n return;\n }\n message = parsed;\n } catch {\n this.sendNotice('error: invalid JSON');\n return;\n }\n\n const messageType = message[0];\n console.log(`[ConnectionHandler] Message type: ${messageType}`);\n\n if (messageType === 'REQ') {\n const subscriptionId = message[1];\n const filters = message.slice(2) as Filter[];\n this.handleReq(subscriptionId as string, filters);\n } else if (messageType === 'EVENT') {\n const event = message[1];\n this.handleEvent(event as NostrEvent);\n } else if (messageType === 'CLOSE') {\n const subscriptionId = message[1];\n this.handleClose(subscriptionId as string);\n } else {\n this.sendNotice(`error: unknown message type: ${messageType}`);\n }\n }\n\n /**\n * Handle a REQ message to create/update a subscription.\n */\n private handleReq(subscriptionId: string, filters: Filter[]): void {\n // Validate subscription ID\n if (typeof subscriptionId !== 'string' || subscriptionId.length === 0) {\n this.sendNotice('error: invalid subscription id');\n return;\n }\n\n // Check subscription limit (only for new subscriptions)\n if (!this.subscriptions.has(subscriptionId)) {\n if (\n this.subscriptions.size >= this.config.maxSubscriptionsPerConnection\n ) {\n this.sendNotice('error: too many subscriptions');\n return;\n }\n }\n\n // Check filter limit\n if (filters.length > this.config.maxFiltersPerSubscription) {\n this.sendNotice('error: too many filters');\n return;\n }\n\n // Store the subscription\n this.subscriptions.set(subscriptionId, {\n id: subscriptionId,\n filters,\n });\n\n // Query matching events\n console.log(\n `[ConnectionHandler] REQ: ${subscriptionId}, filters:`,\n JSON.stringify(filters).slice(0, 100)\n );\n const events = this.eventStore.query(filters);\n console.log(\n `[ConnectionHandler] Query returned ${events.length} events for ${subscriptionId}`\n );\n\n // Send matching events\n for (const event of events) {\n console.log(\n `[ConnectionHandler] Sending event ${event.id.slice(0, 16)}... to ${subscriptionId}`\n );\n this.sendEvent(subscriptionId, event);\n }\n\n // Send EOSE\n console.log(`[ConnectionHandler] Sending EOSE for ${subscriptionId}`);\n this.sendEose(subscriptionId);\n }\n\n /**\n * Handle an EVENT message from a WebSocket client.\n *\n * Rejects all external writes — the relay is ILP-gated (pay to write).\n * Events are only stored through the ILP packet handler which calls\n * eventStore.store() directly and then broadcastEvent() to notify subscribers.\n */\n private handleEvent(event: NostrEvent): void {\n this.sendOk(event.id, false, 'restricted: writes require ILP payment');\n }\n\n /**\n * Handle a CLOSE message to terminate a subscription.\n */\n private handleClose(subscriptionId: string): void {\n // Silently remove subscription (no error if it doesn't exist per NIP-01)\n this.subscriptions.delete(subscriptionId);\n }\n\n /**\n * Push a new event to all matching subscriptions on this connection.\n * Used when events are stored outside the WebSocket flow (e.g., via ILP).\n */\n notifyNewEvent(event: NostrEvent): void {\n for (const sub of this.subscriptions.values()) {\n const matches = sub.filters.some((f) => matchFilter(event, f));\n if (matches) {\n this.sendEvent(sub.id, event);\n }\n }\n }\n\n /**\n * Clean up all subscriptions for this connection.\n */\n cleanup(): void {\n this.subscriptions.clear();\n }\n\n /**\n * Get the number of active subscriptions.\n */\n getSubscriptionCount(): number {\n return this.subscriptions.size;\n }\n\n /**\n * Emit an outbound NIP-01 EVENT frame.\n *\n * The event MUST go on the wire as canonical NIP-01 JSON —\n * `[\"EVENT\", <subId>, {id, pubkey, created_at, kind, tags, content, sig}]`\n * with the event as a plain JSON object — so any standard nostr client can\n * parse it and verify `id`/`sig` from the wire bytes (#46). Never re-encode\n * the event (TOON text, double-JSON-stringify, etc.) at this boundary.\n */\n private sendEvent(subscriptionId: string, event: NostrEvent): void {\n this.send(['EVENT', subscriptionId, event]);\n }\n\n private sendEose(subscriptionId: string): void {\n this.send(['EOSE', subscriptionId]);\n }\n\n private sendOk(eventId: string, success: boolean, message: string): void {\n this.send(['OK', eventId, success, message]);\n }\n\n private sendNotice(message: string): void {\n this.send(['NOTICE', message]);\n }\n\n private send(message: unknown[]): void {\n if (this.ws.readyState === 1) {\n // OPEN\n this.ws.send(JSON.stringify(message));\n }\n }\n}\n","import type { WebSocket } from 'ws';\nimport { WebSocketServer } from 'ws';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { EventStore } from '../storage/index.js';\nimport type { RelayServerConfig } from '../types.js';\nimport { DEFAULT_RELAY_CONFIG } from '../types.js';\nimport { ConnectionHandler } from './ConnectionHandler.js';\n\n/**\n * A NIP-01 compliant Nostr relay WebSocket server.\n * Handles client connections and routes messages to ConnectionHandlers.\n */\nexport class NostrRelayServer {\n private wss: WebSocketServer | null = null;\n private handlers = new Map<WebSocket, ConnectionHandler>();\n private config: Required<RelayServerConfig>;\n\n constructor(\n config: Partial<RelayServerConfig> = {},\n private eventStore: EventStore\n ) {\n this.config = { ...DEFAULT_RELAY_CONFIG, ...config };\n }\n\n /**\n * Start the WebSocket server.\n */\n async start(): Promise<void> {\n return new Promise((resolve, reject) => {\n try {\n this.wss = new WebSocketServer({\n port: this.config.port,\n host: this.config.host,\n });\n\n this.wss.on('connection', (ws: WebSocket) => {\n this.handleConnection(ws);\n });\n\n this.wss.on('error', (error: Error) => {\n console.error('[NostrRelayServer] Server error:', error.message);\n });\n\n this.wss.on('listening', () => {\n const address = this.wss?.address();\n if (address && typeof address === 'object') {\n console.log(`[NostrRelayServer] Listening on port ${address.port}`);\n }\n resolve();\n });\n } catch (error) {\n reject(error);\n }\n });\n }\n\n /**\n * Stop the WebSocket server and close all connections.\n */\n async stop(): Promise<void> {\n return new Promise((resolve) => {\n if (!this.wss) {\n resolve();\n return;\n }\n\n // Clean up all connection handlers\n for (const [ws, handler] of this.handlers) {\n handler.cleanup();\n ws.close();\n }\n this.handlers.clear();\n\n this.wss.close(() => {\n this.wss = null;\n resolve();\n });\n });\n }\n\n /**\n * Get the port the server is listening on.\n * Returns 0 if the server is not started.\n */\n getPort(): number {\n if (!this.wss) return 0;\n const address = this.wss.address();\n if (address && typeof address === 'object') {\n return address.port;\n }\n return 0;\n }\n\n /**\n * Get the number of connected clients.\n */\n getClientCount(): number {\n return this.handlers.size;\n }\n\n /**\n * Broadcast an event to all connected clients with matching subscriptions.\n * Call this after storing an event outside the WebSocket flow (e.g., via ILP)\n * so that discovery subscribers are notified.\n */\n broadcastEvent(event: NostrEvent): void {\n for (const handler of this.handlers.values()) {\n handler.notifyNewEvent(event);\n }\n }\n\n private handleConnection(ws: WebSocket): void {\n // Check max connections\n if (this.handlers.size >= this.config.maxConnections) {\n ws.close(1013, 'max connections reached');\n return;\n }\n\n console.log('[NostrRelayServer] Client connected');\n\n const handler = new ConnectionHandler(ws, this.eventStore, this.config);\n this.handlers.set(ws, handler);\n\n ws.on('message', (data: Buffer | string) => {\n const message = typeof data === 'string' ? data : data.toString();\n handler.handleMessage(message);\n });\n\n ws.on('close', () => {\n console.log('[NostrRelayServer] Client disconnected');\n handler.cleanup();\n this.handlers.delete(ws);\n });\n\n ws.on('error', (error: Error) => {\n console.error('[NostrRelayServer] Client error:', error.message);\n handler.cleanup();\n this.handlers.delete(ws);\n });\n }\n}\n","/**\n * Subscribe to upstream relays and propagate events into the local EventStore.\n *\n * Follows the same lifecycle pattern as core's discovery tracker and SocialPeerDiscovery:\n * - Accept optional SimplePool for testability\n * - start() returns { unsubscribe } cleanup handle\n * - isUnsubscribed guard prevents processing after teardown\n */\n\nimport { SimplePool } from 'nostr-tools/pool';\nimport { verifyEvent } from 'nostr-tools/pure';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { Filter } from 'nostr-tools/filter';\nimport type { EventStore } from '../storage/index.js';\n\n/**\n * Configuration for RelaySubscriber.\n */\nexport interface RelaySubscriberConfig {\n /** Upstream relay URLs to subscribe to */\n relayUrls: string[];\n /** Nostr filter for which events to pull (e.g. kinds, authors) */\n filter: Filter;\n /** Verify event signatures before storing (default: true) */\n verifySignatures?: boolean;\n}\n\n/**\n * Subscribes to upstream Nostr relays and stores received events\n * in the local EventStore. Useful for relay-to-relay event propagation.\n */\nexport class RelaySubscriber {\n private readonly config: RelaySubscriberConfig;\n private readonly eventStore: EventStore;\n private readonly pool: SimplePool;\n private started = false;\n\n /**\n * @param config - Subscriber configuration\n * @param eventStore - Storage backend to write events into\n * @param pool - Optional SimplePool instance (creates new one if not provided)\n */\n constructor(\n config: RelaySubscriberConfig,\n eventStore: EventStore,\n pool?: SimplePool\n ) {\n this.config = config;\n this.eventStore = eventStore;\n this.pool = pool ?? new SimplePool();\n }\n\n /**\n * Start subscribing to the configured upstream relays.\n *\n * @returns Handle with unsubscribe() to stop the subscription\n * @throws Error if already started\n */\n start(): { unsubscribe: () => void } {\n if (this.started) {\n throw new Error('RelaySubscriber already started');\n }\n this.started = true;\n\n const shouldVerify = this.config.verifySignatures !== false;\n let isUnsubscribed = false;\n\n const subCloser = this.pool.subscribeMany(\n this.config.relayUrls,\n this.config.filter,\n {\n onevent: (event: NostrEvent) => {\n if (isUnsubscribed) return;\n\n if (shouldVerify && !verifyEvent(event)) {\n return;\n }\n\n try {\n this.eventStore.store(event);\n } catch (error) {\n console.warn(\n '[RelaySubscriber] Failed to store event:',\n error instanceof Error ? error.message : 'Unknown error'\n );\n }\n },\n }\n );\n\n return {\n unsubscribe: () => {\n if (!isUnsubscribed) {\n isUnsubscribed = true;\n subCloser.close();\n this.started = false;\n }\n },\n };\n }\n}\n","/**\n * Write handler for @toon-protocol/relay.\n *\n * Exposes a plain-HTTP write surface that accepts an event-as-JSON, trusts\n * (but does NOT validate) injected payment headers, verifies ONLY the event\n * signature for integrity, and stores the event.\n *\n * This handler is intentionally decoupled from any payment layer: it contains\n * no claim/settlement/ILP logic and imports none of it. Payment validation is\n * the upstream terminator's concern; by the time a request reaches this surface\n * the trusted `X-TOON-*` headers are assumed already proven. The handler\n * captures them purely for the response echo and a log line.\n *\n * Flow:\n * 1. Parse JSON body `{ event }` -> 400 on malformed/missing event\n * 2. Capture trusted X-TOON-Payer / X-TOON-Amount / X-TOON-Chain headers\n * 3. Verify the event signature (skipped in devMode) -> 422 on invalid sig\n * 4. Store the event in the EventStore\n * 5. Fire the optional onStored callback\n * 6. Respond 200 with the event id, storedAt timestamp, and echoed headers\n *\n * @module\n */\n\nimport type { Context } from 'hono';\nimport { verifyEvent } from 'nostr-tools/pure';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { EventStore } from '../../storage/index.js';\n\n/**\n * Configuration for the write handler.\n */\nexport interface WriteHandlerConfig {\n /** Event store backend used to persist accepted events. */\n eventStore: EventStore;\n /** Whether dev mode is enabled (skips Schnorr signature verification). */\n devMode: boolean;\n /** Optional callback fired after an event is successfully stored. */\n onStored?: (event: NostrEvent) => void;\n}\n\n/**\n * Write handler instance.\n */\nexport interface WriteHandler {\n /** Handle a plain-HTTP write request. */\n handleWrite(c: Context): Promise<Response>;\n}\n\n/**\n * Create a write handler.\n *\n * @param config - Handler configuration.\n * @returns A WriteHandler with a handleWrite method.\n */\nexport function createWriteHandler(\n config: WriteHandlerConfig\n): WriteHandler {\n return {\n async handleWrite(c: Context): Promise<Response> {\n // --- Parse request body ---\n let body: { event?: NostrEvent };\n try {\n body = (await c.req.json()) as { event?: NostrEvent };\n } catch {\n return c.json({ error: 'Invalid request body' }, 400);\n }\n\n if (!body.event) {\n return c.json({ error: 'Missing required field: event' }, 400);\n }\n\n const event = body.event;\n\n // --- Capture trusted payment headers (NOT validated here) ---\n const payer = c.req.header('X-TOON-Payer');\n const amount = c.req.header('X-TOON-Amount');\n const chain = c.req.header('X-TOON-Chain');\n\n console.log(\n `[write] event=${event.id} payer=${payer ?? '-'} amount=${amount ?? '-'} chain=${chain ?? '-'}`\n );\n\n // --- Verify event signature (integrity only; skipped in devMode) ---\n if (!config.devMode && !verifyEvent(event)) {\n return c.json({ error: 'Invalid event signature' }, 422);\n }\n\n // --- Store the event ---\n config.eventStore.store(event);\n\n // --- Fire the optional stored callback ---\n config.onStored?.(event);\n\n // --- Build response (echo trusted headers) ---\n return c.json(\n {\n eventId: event.id,\n storedAt: Math.floor(Date.now() / 1000),\n payer,\n amount,\n chain,\n },\n 200\n );\n },\n };\n}\n","/**\n * Health response for the relay's HTTP server.\n *\n * The relay is a plain read/write app (no payment, connector, or settlement\n * layer), so the health response is deliberately minimal: liveness plus the\n * node's identity and version. It is served from `GET /health` on the write\n * port and is the target of the container healthcheck.\n *\n * @module\n */\n\nimport { VERSION } from '../version.js';\n\n/** Configuration for building a health response. */\nexport interface HealthConfig {\n /** Node's Nostr pubkey (64-char hex). */\n pubkey: string;\n}\n\n/** The health response shape. */\nexport interface HealthResponse {\n status: 'healthy';\n pubkey: string;\n capabilities: string[];\n version: string;\n timestamp: number;\n}\n\n/**\n * Build a health response for the relay.\n *\n * Pure function: takes a config and returns the response object, so it is\n * trivially unit-testable and reusable.\n *\n * @param config - Health configuration (the node's pubkey).\n * @returns The health response object.\n */\nexport function createHealthResponse(config: HealthConfig): HealthResponse {\n return {\n status: 'healthy',\n pubkey: config.pubkey,\n capabilities: ['relay'],\n version: VERSION,\n timestamp: Date.now(),\n };\n}\n","/**\n * startRelay() -- Programmatic API for starting a TOON relay node.\n *\n * The relay is a plain HTTP/WebSocket app. It does NOT speak ILP and contains\n * no payment, connector, settlement, or pricing logic: payment is enforced\n * entirely upstream by an external terminator (see the connector repo). By the\n * time a write reaches this process it is already proven paid, so the relay\n * simply stores the event and serves reads.\n *\n * Two surfaces:\n *\n * - `POST /write` (TOON_BLS_PORT, default 3100): accepts `{ event }` as JSON,\n * trusts the injected `X-TOON-Payer`/`-Amount`/`-Chain` headers WITHOUT\n * re-validating payment, verifies only the event's own signature for\n * integrity, and stores it. `GET /health` lives on the same port.\n * - Free NIP-01 WebSocket reads (TOON_RELAY_PORT, default 7100).\n *\n * `startRelay()` returns a `RelayInstance` with an explicit `.stop()` for\n * lifecycle control (the CLI wraps this with process-signal handling).\n *\n * @module\n */\n\nimport { mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { serve, type ServerType } from '@hono/node-server';\nimport { Hono, type Context } from 'hono';\nimport { getPublicKey } from 'nostr-tools/pure';\nimport { privateKeyFromSeedWords } from 'nostr-tools/nip06';\nimport type { Filter } from 'nostr-tools/filter';\nimport { SqliteEventStore } from '../storage/index.js';\nimport type { EventStore } from '../storage/index.js';\nimport { NostrRelayServer } from '../websocket/index.js';\nimport { RelaySubscriber } from '../subscriber/index.js';\nimport { createWriteHandler } from './handlers/write-handler.js';\nimport { createHealthResponse } from './health.js';\n\n// ---------- Configuration ----------\n\n/**\n * Configuration for starting a TOON relay node via `startRelay()`.\n *\n * Exactly one of `mnemonic` or `secretKey` must be provided -- it is the node's\n * Nostr identity (surfaced on `/health`).\n */\nexport interface RelayConfig {\n // --- Identity (exactly one required) ---\n\n /** 12-word or 24-word BIP-39 mnemonic phrase (NIP-06 derivation). */\n mnemonic?: string;\n /** 32-byte secp256k1 secret key. */\n secretKey?: Uint8Array;\n\n // --- Network ---\n\n /** WebSocket relay (read) port (default: 7100). */\n relayPort?: number;\n /** HTTP write/health port (default: 3100). */\n blsPort?: number;\n /**\n * WebSocket bind host (default: 0.0.0.0). Set to `127.0.0.1` to bind the read\n * port to localhost only (e.g. when an upstream proxy handles inbound).\n */\n host?: string;\n\n // --- Storage ---\n\n /** Data directory for the file-backed SQLite store (default: ./data). */\n dataDir?: string;\n /**\n * Pre-built EventStore. When provided, the relay uses it instead of building\n * the default file-backed `SqliteEventStore` under `dataDir` (useful for\n * tests via `InMemoryEventStore`, or to share a store when embedding). The\n * caller owns its lifecycle when supplied.\n */\n eventStore?: EventStore;\n\n // --- Development ---\n\n /** Skip event-signature verification on `POST /write` (default: false). */\n devMode?: boolean;\n}\n\n/**\n * Resolved configuration with all defaults applied.\n */\nexport interface ResolvedRelayConfig {\n relayPort: number;\n blsPort: number;\n host: string;\n dataDir: string;\n devMode: boolean;\n}\n\n/**\n * A running TOON relay node instance returned by `startRelay()`.\n */\nexport interface RelayInstance {\n /** Whether the relay is currently running. */\n isRunning(): boolean;\n\n /** Gracefully stop the relay and release all resources. */\n stop(): Promise<void>;\n\n /**\n * Subscribe to a remote Nostr relay. Received events are stored in this\n * node's EventStore. Returns a handle for lifecycle management.\n *\n * @param relayUrl - WebSocket URL of the relay to subscribe to.\n * @param filter - Nostr filter (kinds, authors, etc.).\n * @returns A RelaySubscription handle.\n * @throws If the relay is not running.\n */\n subscribe(relayUrl: string, filter: Filter): RelaySubscription;\n\n /** The node's Nostr x-only public key (64-char hex). */\n pubkey: string;\n\n /** The resolved configuration with all defaults applied. */\n config: ResolvedRelayConfig;\n}\n\n/**\n * Handle for managing an outbound subscription to a remote Nostr relay.\n * Returned by `RelayInstance.subscribe()`.\n */\nexport interface RelaySubscription {\n /** Close the subscription and disconnect from the relay. */\n close(): void;\n /** The relay URL this subscription is connected to. */\n relayUrl: string;\n /** Whether this subscription is still active. */\n isActive(): boolean;\n}\n\n// ---------- Identity ----------\n\n/**\n * Derive the node's Nostr identity from the config. Exactly one of `mnemonic`\n * or `secretKey` must be set.\n *\n * @internal\n */\nfunction deriveIdentity(config: RelayConfig): {\n secretKey: Uint8Array;\n pubkey: string;\n} {\n const hasMnemonic = config.mnemonic !== undefined;\n const hasSecretKey = config.secretKey !== undefined;\n\n if (hasMnemonic && hasSecretKey) {\n throw new Error(\n 'RelayConfig: provide either mnemonic or secretKey, not both'\n );\n }\n if (!hasMnemonic && !hasSecretKey) {\n throw new Error('RelayConfig: one of mnemonic or secretKey is required');\n }\n\n const secretKey = hasMnemonic\n ? privateKeyFromSeedWords(config.mnemonic as string)\n : (config.secretKey as Uint8Array);\n\n return { secretKey, pubkey: getPublicKey(secretKey) };\n}\n\n// ---------- Subscription Helper ----------\n\n/**\n * Create a subscription to a remote Nostr relay, storing received events\n * in the local EventStore. Returns a RelaySubscription handle.\n *\n * @internal Exported for unit testing only. Use `RelayInstance.subscribe()` instead.\n */\nexport function createSubscription(\n relayUrl: string,\n filter: Filter,\n eventStore: EventStore,\n activeSubscriptions: Set<RelaySubscription>\n): RelaySubscription {\n // Validate WebSocket URL scheme to provide clear errors and prevent\n // non-WebSocket URLs from reaching SimplePool.\n // nosemgrep: javascript.lang.security.detect-insecure-websocket.detect-insecure-websocket -- validation check, not a connection\n if (!relayUrl.startsWith('ws://') && !relayUrl.startsWith('wss://')) {\n throw new Error(\n 'Invalid relay URL -- must use WebSocket scheme (ws or wss)'\n );\n }\n\n const subscriber = new RelaySubscriber(\n { relayUrls: [relayUrl], filter },\n eventStore\n );\n const handle = subscriber.start();\n\n let active = true;\n const subscription: RelaySubscription = {\n close() {\n if (!active) return;\n active = false;\n handle.unsubscribe();\n activeSubscriptions.delete(subscription);\n },\n relayUrl,\n isActive() {\n return active;\n },\n };\n\n activeSubscriptions.add(subscription);\n return subscription;\n}\n\n// ---------- Main API ----------\n\n/**\n * Start a TOON relay node with the given configuration.\n *\n * Wires the event store, the HTTP write/health server, and the NIP-01\n * WebSocket read server, then returns a `RelayInstance` for lifecycle control.\n *\n * @param config - Node configuration. One of `mnemonic`/`secretKey` is required.\n * @returns A running RelayInstance.\n * @throws If both or neither of mnemonic/secretKey are provided.\n *\n * @example\n * ```typescript\n * const relay = await startRelay({ secretKey });\n * // ... POST /write on 3100, read NIP-01 on 7100 ...\n * await relay.stop();\n * ```\n */\nexport async function startRelay(config: RelayConfig): Promise<RelayInstance> {\n // --- 1. Identity ---\n const identity = deriveIdentity(config);\n\n // --- 2. Resolve config ---\n const relayPort = config.relayPort ?? 7100;\n const blsPort = config.blsPort ?? 3100;\n const host = config.host ?? '0.0.0.0';\n const dataDir = config.dataDir ?? './data';\n const devMode = config.devMode ?? false;\n\n const resolvedConfig: ResolvedRelayConfig = {\n relayPort,\n blsPort,\n host,\n dataDir,\n devMode,\n };\n\n // --- 3. Event store ---\n // Use the injected store as-is, or build a file-backed SqliteEventStore.\n let eventStore: EventStore;\n if (config.eventStore) {\n eventStore = config.eventStore;\n } else {\n mkdirSync(dataDir, { recursive: true });\n eventStore = new SqliteEventStore(join(dataDir, 'events.db'));\n }\n\n // --- 4. WebSocket read server (created first so /write can broadcast) ---\n const wsRelay = new NostrRelayServer({ port: relayPort, host }, eventStore);\n\n // --- 5. HTTP write/health server ---\n const app = new Hono();\n\n app.get('/health', (c: Context) =>\n c.json(createHealthResponse({ pubkey: identity.pubkey }))\n );\n\n // POST /write: trust the upstream terminator's injected payment headers,\n // verify only the event signature, store, and broadcast to live WS readers.\n const writeHandler = createWriteHandler({\n eventStore,\n devMode,\n onStored: (event) => {\n try {\n wsRelay.broadcastEvent(event);\n } catch {\n // Non-broadcastable payloads -- ignore.\n }\n },\n });\n app.post('/write', (c: Context) => writeHandler.handleWrite(c));\n\n // Resolve once the HTTP server is actually listening so callers (and tests)\n // never race a not-yet-bound port.\n const blsServer: ServerType = await new Promise<ServerType>((resolve) => {\n const server = serve({ fetch: app.fetch, port: blsPort }, () =>\n resolve(server)\n );\n });\n\n // --- 6. Start the WS read server ---\n await wsRelay.start();\n\n // --- 7. Lifecycle ---\n let running = true;\n const activeSubscriptions = new Set<RelaySubscription>();\n\n const instance: RelayInstance = {\n isRunning() {\n return running;\n },\n\n subscribe(subscribeRelayUrl: string, filter: Filter): RelaySubscription {\n if (!running) {\n throw new Error('Cannot subscribe: relay is not running');\n }\n return createSubscription(\n subscribeRelayUrl,\n filter,\n eventStore,\n activeSubscriptions\n );\n },\n\n async stop() {\n if (!running) return;\n running = false;\n\n for (const sub of activeSubscriptions) {\n sub.close();\n }\n activeSubscriptions.clear();\n\n await wsRelay.stop();\n blsServer.close();\n\n // Only close a store we created; an injected store is the caller's.\n if (!config.eventStore) {\n eventStore.close?.();\n }\n },\n\n pubkey: identity.pubkey,\n config: resolvedConfig,\n };\n\n return instance;\n}\n"],"mappings":";AACO,IAAM,UAAU;;;ACoBhB,IAAM,uBAAoD;AAAA,EAC/D,MAAM;AAAA,EACN,MAAM;AAAA,EACN,gBAAgB;AAAA,EAChB,+BAA+B;AAAA,EAC/B,2BAA2B;AAAA,EAC3B,cAAc;AAChB;;;ACZO,SAAS,YAAY,OAAmB,QAAyB;AAEtE,MAAI,OAAO,KAAK,MAAM,EAAE,WAAW,GAAG;AACpC,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,QAAQ,UAAa,OAAO,IAAI,SAAS,GAAG;AACrD,UAAM,UAAU,OAAO,IAAI,KAAK,CAAC,OAAO,MAAM,GAAG,WAAW,EAAE,CAAC;AAC/D,QAAI,CAAC,QAAS,QAAO;AAAA,EACvB;AAGA,MAAI,OAAO,YAAY,UAAa,OAAO,QAAQ,SAAS,GAAG;AAC7D,UAAM,UAAU,OAAO,QAAQ;AAAA,MAAK,CAAC,WACnC,MAAM,OAAO,WAAW,MAAM;AAAA,IAChC;AACA,QAAI,CAAC,QAAS,QAAO;AAAA,EACvB;AAGA,MAAI,OAAO,UAAU,UAAa,OAAO,MAAM,SAAS,GAAG;AACzD,QAAI,CAAC,OAAO,MAAM,SAAS,MAAM,IAAI,EAAG,QAAO;AAAA,EACjD;AAGA,MAAI,OAAO,UAAU,QAAW;AAC9B,QAAI,MAAM,aAAa,OAAO,MAAO,QAAO;AAAA,EAC9C;AAGA,MAAI,OAAO,UAAU,QAAW;AAC9B,QAAI,MAAM,aAAa,OAAO,MAAO,QAAO;AAAA,EAC9C;AAGA,aAAW,OAAO,OAAO,KAAK,MAAM,GAAG;AACrC,QAAI,IAAI,WAAW,GAAG,KAAK,IAAI,WAAW,GAAG;AAC3C,YAAM,UAAU,IAAI,MAAM,CAAC;AAC3B,YAAM,eAAe,OAAO,GAAmB;AAE/C,UAAI,iBAAiB,UAAa,aAAa,SAAS,GAAG;AAEzD,cAAM,iBAAiB,MAAM,KAC1B,OAAO,CAAC,QAAQ,IAAI,CAAC,MAAM,OAAO,EAClC,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;AAGtB,cAAM,WAAW,aAAa,KAAK,CAAC,MAAM,eAAe,SAAS,CAAC,CAAC;AACpE,YAAI,CAAC,SAAU,QAAO;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;;;ACjDO,IAAM,qBAAN,MAA+C;AAAA,EAC5C,SAAS,oBAAI,IAAwB;AAAA,EAE7C,MAAM,OAAyB;AAC7B,SAAK,OAAO,IAAI,MAAM,IAAI,KAAK;AAAA,EACjC;AAAA,EAEA,IAAI,IAAoC;AACtC,WAAO,KAAK,OAAO,IAAI,EAAE;AAAA,EAC3B;AAAA,EAEA,MAAM,SAAiC;AAErC,UAAM,YAAY,MAAM,KAAK,KAAK,OAAO,OAAO,CAAC;AAGjD,QAAI,QAAQ,WAAW,GAAG;AACxB,aAAO,UAAU,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AAAA,IAC7D;AAGA,UAAM,iBAA+B,CAAC;AAEtC,eAAW,SAAS,WAAW;AAC7B,iBAAW,UAAU,SAAS;AAC5B,YAAI,YAAY,OAAO,MAAM,GAAG;AAC9B,yBAAe,KAAK,KAAK;AACzB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,mBAAe,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AAGzD,UAAM,cAAc,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,MAAS;AAC7D,QAAI,aAAa,UAAU,QAAW;AACpC,aAAO,eAAe,MAAM,GAAG,YAAY,KAAK;AAAA,IAClD;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AAAA,EAEd;AACF;;;ACxEA,OAAO,cAAc;AAQrB,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBnB,IAAM,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKA,SAAS,iBAAiB,IAA6B;AACrD,KAAG,KAAK,UAAU;AAClB,aAAW,YAAY,WAAW;AAChC,OAAG,KAAK,QAAQ;AAAA,EAClB;AACF;AAKO,IAAM,aAAN,cAAyB,MAAM;AAAA,EACpC,YACE,SACO,MACP;AACA,UAAM,OAAO;AAFN;AAGP,SAAK,OAAO;AAAA,EACd;AACF;AAMA,SAAS,kBAAkB,MAAuB;AAChD,SAAO,QAAQ,OAAS,QAAQ,SAAS,EAAE,QAAQ,SAAS,QAAQ;AACtE;AAMA,SAAS,+BAA+B,MAAuB;AAC7D,SAAQ,QAAQ,OAAS,QAAQ,SAAW,QAAQ,SAAS,QAAQ;AACvE;AAKA,SAAS,aAAa,MAA0B;AAC9C,QAAM,OAAO,KAAK,KAAK,CAAC,QAAQ,IAAI,CAAC,MAAM,GAAG;AAC9C,SAAO,OAAO,CAAC,KAAK;AACtB;AAMO,IAAM,mBAAN,MAA6C;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMR,YAAY,SAAS,YAAY;AAC/B,QAAI;AACF,WAAK,KAAK,IAAI,SAAS,MAAM;AAC7B,uBAAiB,KAAK,EAAE;AAGxB,WAAK,aAAa,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,OAGjC;AAED,WAAK,UAAU,KAAK,GAAG,QAAQ,mCAAmC;AAElE,WAAK,yBAAyB,KAAK,GAAG;AAAA,QACpC;AAAA,MACF;AAEA,WAAK,6BAA6B,KAAK,GAAG;AAAA,QACxC;AAAA,MACF;AAEA,WAAK,sBAAsB,KAAK,GAAG;AAAA,QACjC;AAAA,MACF;AAEA,WAAK,0BAA0B,KAAK,GAAG;AAAA,QACrC;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,YAAM,IAAI;AAAA,QACR,kCAAkC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QACxF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAyB;AAC7B,QAAI;AACF,YAAM,WAAW,KAAK,UAAU,MAAM,IAAI;AAC1C,YAAM,aAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAE/C,UAAI,kBAAkB,MAAM,IAAI,GAAG;AAEjC,aAAK,sBAAsB,OAAO,UAAU,UAAU;AAAA,MACxD,WAAW,+BAA+B,MAAM,IAAI,GAAG;AAErD,aAAK,mCAAmC,OAAO,UAAU,UAAU;AAAA,MACrE,OAAO;AAEL,cAAM,iBAAiB,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,SAGtC;AACD,uBAAe;AAAA,UACb,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN;AAAA,UACA,MAAM;AAAA,UACN,MAAM;AAAA,UACN;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,UAAI,iBAAiB,YAAY;AAC/B,cAAM;AAAA,MACR;AACA,YAAM,IAAI;AAAA,QACR,0BAA0B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QAChF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,sBACN,OACA,UACA,YACM;AACN,UAAM,WAAW,KAAK,oBAAoB,IAAI,MAAM,QAAQ,MAAM,IAAI;AAItE,QAAI,UAAU;AAEZ,UACE,MAAM,aAAa,SAAS,cAC3B,MAAM,eAAe,SAAS,cAAc,MAAM,KAAK,SAAS,IACjE;AAEA,cAAM,cAAc,KAAK,GAAG,YAAY,MAAM;AAC5C,eAAK,uBAAuB,IAAI,MAAM,QAAQ,MAAM,IAAI;AACxD,eAAK,WAAW;AAAA,YACd,MAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM;AAAA,YACN;AAAA,YACA,MAAM;AAAA,YACN,MAAM;AAAA,YACN;AAAA,UACF;AAAA,QACF,CAAC;AACD,oBAAY;AAAA,MACd;AAAA,IAEF,OAAO;AAEL,WAAK,WAAW;AAAA,QACd,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN;AAAA,QACA,MAAM;AAAA,QACN,MAAM;AAAA,QACN;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,mCACN,OACA,UACA,YACM;AACN,UAAM,YAAY,aAAa,MAAM,IAAI;AAKzC,QAAI;AAEJ,QAAI,cAAc,IAAI;AAEpB,YAAM,aAAa,KAAK,GACrB;AAAA,QACC;AAAA,MACF,EACC,IAAI,MAAM,QAAQ,MAAM,IAAI;AAO/B,iBAAW,aAAa,YAAY;AAClC,cAAM,gBAAgB,KAAK,MAAM,UAAU,IAAI;AAC/C,cAAM,qBAAqB,aAAa,aAAa;AACrD,YAAI,uBAAuB,IAAI;AAC7B,qBAAW,EAAE,IAAI,UAAU,IAAI,YAAY,UAAU,WAAW;AAChE;AAAA,QACF;AAAA,MACF;AAAA,IACF,OAAO;AACL,YAAM,cAAc,UAAU,SAAS;AACvC,iBAAW,KAAK,wBAAwB;AAAA,QACtC,MAAM;AAAA,QACN,MAAM;AAAA,QACN;AAAA,MACF;AAAA,IACF;AAEA,QAAI,UAAU;AAEZ,UACE,MAAM,aAAa,SAAS,cAC3B,MAAM,eAAe,SAAS,cAAc,MAAM,KAAK,SAAS,IACjE;AAEA,cAAM,cAAc,KAAK,GAAG,YAAY,MAAM;AAC5C,eAAK,GAAG,QAAQ,iCAAiC,EAAE,IAAI,SAAS,EAAE;AAClE,eAAK,WAAW;AAAA,YACd,MAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM;AAAA,YACN;AAAA,YACA,MAAM;AAAA,YACN,MAAM;AAAA,YACN;AAAA,UACF;AAAA,QACF,CAAC;AACD,oBAAY;AAAA,MACd;AAAA,IAEF,OAAO;AAEL,WAAK,WAAW;AAAA,QACd,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN;AAAA,QACA,MAAM;AAAA,QACN,MAAM;AAAA,QACN;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,IAAoC;AACtC,QAAI;AACF,YAAM,MAAM,KAAK,QAAQ,IAAI,EAAE;AAY/B,UAAI,CAAC,KAAK;AACR,eAAO;AAAA,MACT;AAEA,aAAO;AAAA,QACL,IAAI,IAAI;AAAA,QACR,QAAQ,IAAI;AAAA,QACZ,MAAM,IAAI;AAAA,QACV,SAAS,IAAI;AAAA,QACb,MAAM,KAAK,MAAM,IAAI,IAAI;AAAA,QACzB,YAAY,IAAI;AAAA,QAChB,KAAK,IAAI;AAAA,MACX;AAAA,IACF,SAAS,OAAO;AACd,YAAM,IAAI;AAAA,QACR,wBAAwB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QAC9E;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAiC;AACrC,QAAI;AACF,YAAM,EAAE,KAAK,OAAO,IAAI,KAAK,cAAc,OAAO;AAClD,YAAM,OAAO,KAAK,GAAG,QAAQ,GAAG;AAChC,YAAM,OAAO,KAAK,IAAI,GAAG,MAAM;AAU/B,aAAO,KAAK,IAAI,CAAC,SAAS;AAAA,QACxB,IAAI,IAAI;AAAA,QACR,QAAQ,IAAI;AAAA,QACZ,MAAM,IAAI;AAAA,QACV,SAAS,IAAI;AAAA,QACb,MAAM,KAAK,MAAM,IAAI,IAAI;AAAA,QACzB,YAAY,IAAI;AAAA,QAChB,KAAK,IAAI;AAAA,MACX,EAAE;AAAA,IACJ,SAAS,OAAO;AACd,YAAM,IAAI;AAAA,QACR,2BAA2B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QACjF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,cAAc,SAAuD;AAC3E,QAAI,QAAQ,WAAW,GAAG;AACxB,aAAO;AAAA,QACL,KAAK;AAAA,QACL,QAAQ,CAAC;AAAA,MACX;AAAA,IACF;AAEA,UAAM,aAAuB,CAAC;AAC9B,UAAM,SAAoB,CAAC;AAE3B,eAAW,UAAU,SAAS;AAC5B,YAAM,mBAA6B,CAAC;AAEpC,UAAI,OAAO,KAAK,QAAQ;AAEtB,cAAM,eAAe,OAAO,IAAI,IAAI,MAAM,WAAW;AACrD,yBAAiB,KAAK,IAAI,aAAa,KAAK,MAAM,CAAC,GAAG;AACtD,eAAO,KAAK,GAAG,OAAO,IAAI,IAAI,CAAC,OAAO,GAAG,EAAE,GAAG,CAAC;AAAA,MACjD;AAEA,UAAI,OAAO,SAAS,QAAQ;AAC1B,cAAM,mBAAmB,OAAO,QAAQ,IAAI,MAAM,eAAe;AACjE,yBAAiB,KAAK,IAAI,iBAAiB,KAAK,MAAM,CAAC,GAAG;AAC1D,eAAO,KAAK,GAAG,OAAO,QAAQ,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC;AAAA,MACnD;AAEA,UAAI,OAAO,OAAO,QAAQ;AACxB,yBAAiB;AAAA,UACf,YAAY,OAAO,MAAM,IAAI,MAAM,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,QACpD;AACA,eAAO,KAAK,GAAG,OAAO,KAAK;AAAA,MAC7B;AAEA,UAAI,OAAO,UAAU,QAAW;AAC9B,yBAAiB,KAAK,iBAAiB;AACvC,eAAO,KAAK,OAAO,KAAK;AAAA,MAC1B;AAEA,UAAI,OAAO,UAAU,QAAW;AAC9B,yBAAiB,KAAK,iBAAiB;AACvC,eAAO,KAAK,OAAO,KAAK;AAAA,MAC1B;AAGA,iBAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,MAAM,GAAG;AAClD,YAAI,IAAI,WAAW,GAAG,KAAK,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS,GAAG;AACrE,gBAAM,UAAU,IAAI,MAAM,CAAC;AAC3B,gBAAM,gBAAgB,OAAO,IAAI,MAAM,aAAa;AACpD,2BAAiB,KAAK,IAAI,cAAc,KAAK,MAAM,CAAC,GAAG;AACvD,iBAAO,KAAK,GAAG,OAAO,IAAI,CAAC,MAAM,MAAM,OAAO,MAAM,CAAC,IAAI,CAAC;AAAA,QAC5D;AAAA,MACF;AAEA,UAAI,iBAAiB,SAAS,GAAG;AAC/B,mBAAW,KAAK,IAAI,iBAAiB,KAAK,OAAO,CAAC,GAAG;AAAA,MACvD;AAAA,IACF;AAEA,QAAI,MAAM;AACV,QAAI,WAAW,SAAS,GAAG;AACzB,aAAO,UAAU,WAAW,KAAK,MAAM,CAAC;AAAA,IAC1C;AACA,WAAO;AAGP,UAAM,cAAc,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,MAAS;AAC7D,QAAI,aAAa,UAAU,QAAW;AACpC,aAAO;AACP,aAAO,KAAK,YAAY,KAAK;AAAA,IAC/B;AAEA,WAAO,EAAE,KAAK,OAAO;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,GAAG,MAAM;AAAA,EAChB;AACF;;;AC5bO,IAAM,oBAAN,MAAwB;AAAA,EAI7B,YACU,IACA,YACR,SAAqC,CAAC,GACtC;AAHQ;AACA;AAGR,SAAK,SAAS,EAAE,GAAG,sBAAsB,GAAG,OAAO;AAAA,EACrD;AAAA,EATQ,gBAAgB,oBAAI,IAA0B;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA,EAaR,cAAc,MAAoB;AAChC,YAAQ,IAAI,yCAAyC,KAAK,MAAM,GAAG,GAAG,CAAC;AACvE,QAAI;AAEJ,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,UAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,aAAK,WAAW,oDAAoD;AACpE;AAAA,MACF;AACA,gBAAU;AAAA,IACZ,QAAQ;AACN,WAAK,WAAW,qBAAqB;AACrC;AAAA,IACF;AAEA,UAAM,cAAc,QAAQ,CAAC;AAC7B,YAAQ,IAAI,qCAAqC,WAAW,EAAE;AAE9D,QAAI,gBAAgB,OAAO;AACzB,YAAM,iBAAiB,QAAQ,CAAC;AAChC,YAAM,UAAU,QAAQ,MAAM,CAAC;AAC/B,WAAK,UAAU,gBAA0B,OAAO;AAAA,IAClD,WAAW,gBAAgB,SAAS;AAClC,YAAM,QAAQ,QAAQ,CAAC;AACvB,WAAK,YAAY,KAAmB;AAAA,IACtC,WAAW,gBAAgB,SAAS;AAClC,YAAM,iBAAiB,QAAQ,CAAC;AAChC,WAAK,YAAY,cAAwB;AAAA,IAC3C,OAAO;AACL,WAAK,WAAW,gCAAgC,WAAW,EAAE;AAAA,IAC/D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,UAAU,gBAAwB,SAAyB;AAEjE,QAAI,OAAO,mBAAmB,YAAY,eAAe,WAAW,GAAG;AACrE,WAAK,WAAW,gCAAgC;AAChD;AAAA,IACF;AAGA,QAAI,CAAC,KAAK,cAAc,IAAI,cAAc,GAAG;AAC3C,UACE,KAAK,cAAc,QAAQ,KAAK,OAAO,+BACvC;AACA,aAAK,WAAW,+BAA+B;AAC/C;AAAA,MACF;AAAA,IACF;AAGA,QAAI,QAAQ,SAAS,KAAK,OAAO,2BAA2B;AAC1D,WAAK,WAAW,yBAAyB;AACzC;AAAA,IACF;AAGA,SAAK,cAAc,IAAI,gBAAgB;AAAA,MACrC,IAAI;AAAA,MACJ;AAAA,IACF,CAAC;AAGD,YAAQ;AAAA,MACN,4BAA4B,cAAc;AAAA,MAC1C,KAAK,UAAU,OAAO,EAAE,MAAM,GAAG,GAAG;AAAA,IACtC;AACA,UAAM,SAAS,KAAK,WAAW,MAAM,OAAO;AAC5C,YAAQ;AAAA,MACN,sCAAsC,OAAO,MAAM,eAAe,cAAc;AAAA,IAClF;AAGA,eAAW,SAAS,QAAQ;AAC1B,cAAQ;AAAA,QACN,qCAAqC,MAAM,GAAG,MAAM,GAAG,EAAE,CAAC,UAAU,cAAc;AAAA,MACpF;AACA,WAAK,UAAU,gBAAgB,KAAK;AAAA,IACtC;AAGA,YAAQ,IAAI,wCAAwC,cAAc,EAAE;AACpE,SAAK,SAAS,cAAc;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,YAAY,OAAyB;AAC3C,SAAK,OAAO,MAAM,IAAI,OAAO,wCAAwC;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAY,gBAA8B;AAEhD,SAAK,cAAc,OAAO,cAAc;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAAe,OAAyB;AACtC,eAAW,OAAO,KAAK,cAAc,OAAO,GAAG;AAC7C,YAAM,UAAU,IAAI,QAAQ,KAAK,CAAC,MAAM,YAAY,OAAO,CAAC,CAAC;AAC7D,UAAI,SAAS;AACX,aAAK,UAAU,IAAI,IAAI,KAAK;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACd,SAAK,cAAc,MAAM;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,uBAA+B;AAC7B,WAAO,KAAK,cAAc;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,UAAU,gBAAwB,OAAyB;AACjE,SAAK,KAAK,CAAC,SAAS,gBAAgB,KAAK,CAAC;AAAA,EAC5C;AAAA,EAEQ,SAAS,gBAA8B;AAC7C,SAAK,KAAK,CAAC,QAAQ,cAAc,CAAC;AAAA,EACpC;AAAA,EAEQ,OAAO,SAAiB,SAAkB,SAAuB;AACvE,SAAK,KAAK,CAAC,MAAM,SAAS,SAAS,OAAO,CAAC;AAAA,EAC7C;AAAA,EAEQ,WAAW,SAAuB;AACxC,SAAK,KAAK,CAAC,UAAU,OAAO,CAAC;AAAA,EAC/B;AAAA,EAEQ,KAAK,SAA0B;AACrC,QAAI,KAAK,GAAG,eAAe,GAAG;AAE5B,WAAK,GAAG,KAAK,KAAK,UAAU,OAAO,CAAC;AAAA,IACtC;AAAA,EACF;AACF;;;ACzMA,SAAS,uBAAuB;AAWzB,IAAM,mBAAN,MAAuB;AAAA,EAK5B,YACE,SAAqC,CAAC,GAC9B,YACR;AADQ;AAER,SAAK,SAAS,EAAE,GAAG,sBAAsB,GAAG,OAAO;AAAA,EACrD;AAAA,EATQ,MAA8B;AAAA,EAC9B,WAAW,oBAAI,IAAkC;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAYR,MAAM,QAAuB;AAC3B,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAI;AACF,aAAK,MAAM,IAAI,gBAAgB;AAAA,UAC7B,MAAM,KAAK,OAAO;AAAA,UAClB,MAAM,KAAK,OAAO;AAAA,QACpB,CAAC;AAED,aAAK,IAAI,GAAG,cAAc,CAAC,OAAkB;AAC3C,eAAK,iBAAiB,EAAE;AAAA,QAC1B,CAAC;AAED,aAAK,IAAI,GAAG,SAAS,CAAC,UAAiB;AACrC,kBAAQ,MAAM,oCAAoC,MAAM,OAAO;AAAA,QACjE,CAAC;AAED,aAAK,IAAI,GAAG,aAAa,MAAM;AAC7B,gBAAM,UAAU,KAAK,KAAK,QAAQ;AAClC,cAAI,WAAW,OAAO,YAAY,UAAU;AAC1C,oBAAQ,IAAI,wCAAwC,QAAQ,IAAI,EAAE;AAAA,UACpE;AACA,kBAAQ;AAAA,QACV,CAAC;AAAA,MACH,SAAS,OAAO;AACd,eAAO,KAAK;AAAA,MACd;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC1B,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAI,CAAC,KAAK,KAAK;AACb,gBAAQ;AACR;AAAA,MACF;AAGA,iBAAW,CAAC,IAAI,OAAO,KAAK,KAAK,UAAU;AACzC,gBAAQ,QAAQ;AAChB,WAAG,MAAM;AAAA,MACX;AACA,WAAK,SAAS,MAAM;AAEpB,WAAK,IAAI,MAAM,MAAM;AACnB,aAAK,MAAM;AACX,gBAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAkB;AAChB,QAAI,CAAC,KAAK,IAAK,QAAO;AACtB,UAAM,UAAU,KAAK,IAAI,QAAQ;AACjC,QAAI,WAAW,OAAO,YAAY,UAAU;AAC1C,aAAO,QAAQ;AAAA,IACjB;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAyB;AACvB,WAAO,KAAK,SAAS;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,eAAe,OAAyB;AACtC,eAAW,WAAW,KAAK,SAAS,OAAO,GAAG;AAC5C,cAAQ,eAAe,KAAK;AAAA,IAC9B;AAAA,EACF;AAAA,EAEQ,iBAAiB,IAAqB;AAE5C,QAAI,KAAK,SAAS,QAAQ,KAAK,OAAO,gBAAgB;AACpD,SAAG,MAAM,MAAM,yBAAyB;AACxC;AAAA,IACF;AAEA,YAAQ,IAAI,qCAAqC;AAEjD,UAAM,UAAU,IAAI,kBAAkB,IAAI,KAAK,YAAY,KAAK,MAAM;AACtE,SAAK,SAAS,IAAI,IAAI,OAAO;AAE7B,OAAG,GAAG,WAAW,CAAC,SAA0B;AAC1C,YAAM,UAAU,OAAO,SAAS,WAAW,OAAO,KAAK,SAAS;AAChE,cAAQ,cAAc,OAAO;AAAA,IAC/B,CAAC;AAED,OAAG,GAAG,SAAS,MAAM;AACnB,cAAQ,IAAI,wCAAwC;AACpD,cAAQ,QAAQ;AAChB,WAAK,SAAS,OAAO,EAAE;AAAA,IACzB,CAAC;AAED,OAAG,GAAG,SAAS,CAAC,UAAiB;AAC/B,cAAQ,MAAM,oCAAoC,MAAM,OAAO;AAC/D,cAAQ,QAAQ;AAChB,WAAK,SAAS,OAAO,EAAE;AAAA,IACzB,CAAC;AAAA,EACH;AACF;;;ACnIA,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAqBrB,IAAM,kBAAN,MAAsB;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACT,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlB,YACE,QACA,YACA,MACA;AACA,SAAK,SAAS;AACd,SAAK,aAAa;AAClB,SAAK,OAAO,QAAQ,IAAI,WAAW;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,QAAqC;AACnC,QAAI,KAAK,SAAS;AAChB,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AACA,SAAK,UAAU;AAEf,UAAM,eAAe,KAAK,OAAO,qBAAqB;AACtD,QAAI,iBAAiB;AAErB,UAAM,YAAY,KAAK,KAAK;AAAA,MAC1B,KAAK,OAAO;AAAA,MACZ,KAAK,OAAO;AAAA,MACZ;AAAA,QACE,SAAS,CAAC,UAAsB;AAC9B,cAAI,eAAgB;AAEpB,cAAI,gBAAgB,CAAC,YAAY,KAAK,GAAG;AACvC;AAAA,UACF;AAEA,cAAI;AACF,iBAAK,WAAW,MAAM,KAAK;AAAA,UAC7B,SAAS,OAAO;AACd,oBAAQ;AAAA,cACN;AAAA,cACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,YAC3C;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,aAAa,MAAM;AACjB,YAAI,CAAC,gBAAgB;AACnB,2BAAiB;AACjB,oBAAU,MAAM;AAChB,eAAK,UAAU;AAAA,QACjB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;AC3EA,SAAS,eAAAA,oBAAmB;AA8BrB,SAAS,mBACd,QACc;AACd,SAAO;AAAA,IACL,MAAM,YAAY,GAA+B;AAE/C,UAAI;AACJ,UAAI;AACF,eAAQ,MAAM,EAAE,IAAI,KAAK;AAAA,MAC3B,QAAQ;AACN,eAAO,EAAE,KAAK,EAAE,OAAO,uBAAuB,GAAG,GAAG;AAAA,MACtD;AAEA,UAAI,CAAC,KAAK,OAAO;AACf,eAAO,EAAE,KAAK,EAAE,OAAO,gCAAgC,GAAG,GAAG;AAAA,MAC/D;AAEA,YAAM,QAAQ,KAAK;AAGnB,YAAM,QAAQ,EAAE,IAAI,OAAO,cAAc;AACzC,YAAM,SAAS,EAAE,IAAI,OAAO,eAAe;AAC3C,YAAM,QAAQ,EAAE,IAAI,OAAO,cAAc;AAEzC,cAAQ;AAAA,QACN,iBAAiB,MAAM,EAAE,UAAU,SAAS,GAAG,WAAW,UAAU,GAAG,UAAU,SAAS,GAAG;AAAA,MAC/F;AAGA,UAAI,CAAC,OAAO,WAAW,CAACA,aAAY,KAAK,GAAG;AAC1C,eAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,GAAG,GAAG;AAAA,MACzD;AAGA,aAAO,WAAW,MAAM,KAAK;AAG7B,aAAO,WAAW,KAAK;AAGvB,aAAO,EAAE;AAAA,QACP;AAAA,UACE,SAAS,MAAM;AAAA,UACf,UAAU,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,UACtC;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ACtEO,SAAS,qBAAqB,QAAsC;AACzE,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,QAAQ,OAAO;AAAA,IACf,cAAc,CAAC,OAAO;AAAA,IACtB,SAAS;AAAA,IACT,WAAW,KAAK,IAAI;AAAA,EACtB;AACF;;;ACtBA,SAAS,iBAAiB;AAC1B,SAAS,YAAY;AACrB,SAAS,aAA8B;AACvC,SAAS,YAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,+BAA+B;AAmHxC,SAAS,eAAe,QAGtB;AACA,QAAM,cAAc,OAAO,aAAa;AACxC,QAAM,eAAe,OAAO,cAAc;AAE1C,MAAI,eAAe,cAAc;AAC/B,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,eAAe,CAAC,cAAc;AACjC,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AAEA,QAAM,YAAY,cACd,wBAAwB,OAAO,QAAkB,IAChD,OAAO;AAEZ,SAAO,EAAE,WAAW,QAAQ,aAAa,SAAS,EAAE;AACtD;AAUO,SAAS,mBACd,UACA,QACA,YACA,qBACmB;AAInB,MAAI,CAAC,SAAS,WAAW,OAAO,KAAK,CAAC,SAAS,WAAW,QAAQ,GAAG;AACnE,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aAAa,IAAI;AAAA,IACrB,EAAE,WAAW,CAAC,QAAQ,GAAG,OAAO;AAAA,IAChC;AAAA,EACF;AACA,QAAM,SAAS,WAAW,MAAM;AAEhC,MAAI,SAAS;AACb,QAAM,eAAkC;AAAA,IACtC,QAAQ;AACN,UAAI,CAAC,OAAQ;AACb,eAAS;AACT,aAAO,YAAY;AACnB,0BAAoB,OAAO,YAAY;AAAA,IACzC;AAAA,IACA;AAAA,IACA,WAAW;AACT,aAAO;AAAA,IACT;AAAA,EACF;AAEA,sBAAoB,IAAI,YAAY;AACpC,SAAO;AACT;AAqBA,eAAsB,WAAW,QAA6C;AAE5E,QAAM,WAAW,eAAe,MAAM;AAGtC,QAAM,YAAY,OAAO,aAAa;AACtC,QAAM,UAAU,OAAO,WAAW;AAClC,QAAM,OAAO,OAAO,QAAQ;AAC5B,QAAM,UAAU,OAAO,WAAW;AAClC,QAAM,UAAU,OAAO,WAAW;AAElC,QAAM,iBAAsC;AAAA,IAC1C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAIA,MAAI;AACJ,MAAI,OAAO,YAAY;AACrB,iBAAa,OAAO;AAAA,EACtB,OAAO;AACL,cAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AACtC,iBAAa,IAAI,iBAAiB,KAAK,SAAS,WAAW,CAAC;AAAA,EAC9D;AAGA,QAAM,UAAU,IAAI,iBAAiB,EAAE,MAAM,WAAW,KAAK,GAAG,UAAU;AAG1E,QAAM,MAAM,IAAI,KAAK;AAErB,MAAI;AAAA,IAAI;AAAA,IAAW,CAAC,MAClB,EAAE,KAAK,qBAAqB,EAAE,QAAQ,SAAS,OAAO,CAAC,CAAC;AAAA,EAC1D;AAIA,QAAM,eAAe,mBAAmB;AAAA,IACtC;AAAA,IACA;AAAA,IACA,UAAU,CAAC,UAAU;AACnB,UAAI;AACF,gBAAQ,eAAe,KAAK;AAAA,MAC9B,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,CAAC;AACD,MAAI,KAAK,UAAU,CAAC,MAAe,aAAa,YAAY,CAAC,CAAC;AAI9D,QAAM,YAAwB,MAAM,IAAI,QAAoB,CAAC,YAAY;AACvE,UAAM,SAAS;AAAA,MAAM,EAAE,OAAO,IAAI,OAAO,MAAM,QAAQ;AAAA,MAAG,MACxD,QAAQ,MAAM;AAAA,IAChB;AAAA,EACF,CAAC;AAGD,QAAM,QAAQ,MAAM;AAGpB,MAAI,UAAU;AACd,QAAM,sBAAsB,oBAAI,IAAuB;AAEvD,QAAM,WAA0B;AAAA,IAC9B,YAAY;AACV,aAAO;AAAA,IACT;AAAA,IAEA,UAAU,mBAA2B,QAAmC;AACtE,UAAI,CAAC,SAAS;AACZ,cAAM,IAAI,MAAM,wCAAwC;AAAA,MAC1D;AACA,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,OAAO;AACX,UAAI,CAAC,QAAS;AACd,gBAAU;AAEV,iBAAW,OAAO,qBAAqB;AACrC,YAAI,MAAM;AAAA,MACZ;AACA,0BAAoB,MAAM;AAE1B,YAAM,QAAQ,KAAK;AACnB,gBAAU,MAAM;AAGhB,UAAI,CAAC,OAAO,YAAY;AACtB,mBAAW,QAAQ;AAAA,MACrB;AAAA,IACF;AAAA,IAEA,QAAQ,SAAS;AAAA,IACjB,QAAQ;AAAA,EACV;AAEA,SAAO;AACT;","names":["verifyEvent"]}
|
package/dist/cli.js
CHANGED
|
@@ -1,78 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startRelay
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FXQSNOCG.js";
|
|
5
5
|
|
|
6
6
|
// src/launcher/cli.ts
|
|
7
7
|
import { parseArgs } from "util";
|
|
8
8
|
function printHelp() {
|
|
9
9
|
console.log(
|
|
10
10
|
`
|
|
11
|
-
Usage:
|
|
11
|
+
Usage: relay [options]
|
|
12
12
|
|
|
13
13
|
Options:
|
|
14
|
-
--mnemonic <words> BIP-39 mnemonic (12 or 24 words)
|
|
14
|
+
--mnemonic <words> BIP-39 mnemonic (12 or 24 words; NIP-06 derivation)
|
|
15
15
|
--secret-key <hex> 32-byte secret key in hex
|
|
16
|
-
--relay-port <port> WebSocket
|
|
17
|
-
--bls-port <port>
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
|
|
21
|
-
URL and routes everything outside the local prefix
|
|
22
|
-
through it. --ilp-address becomes REQUIRED and must
|
|
23
|
-
fall under the parent's prefix.
|
|
24
|
-
--parent-peer-id <id> BTP peer id to register the parent under (default: apex)
|
|
25
|
-
--parent-auth-token <t> Auth token for the parent peer (default: empty / no-auth)
|
|
26
|
-
--ilp-address <addr> ILP address for this node (default: g.toon.<pubkey>;
|
|
27
|
-
REQUIRED when --connector-url is set, e.g. g.townhouse.<self>)
|
|
28
|
-
--node-id <id> Stable nodeId for the embedded connector (default: toon-<pubkey>)
|
|
29
|
-
--known-peers <json> Known peers as JSON array
|
|
30
|
-
--dev-mode Enable dev mode (skip verification)
|
|
31
|
-
--x402-enabled Enable x402 /publish endpoint (default: false)
|
|
32
|
-
--oblivious-mode Run as a payment-oblivious relay (default: false).
|
|
33
|
-
No embedded connector is created and no x402/ILP
|
|
34
|
-
settlement code runs; exposes POST /write (event-as-
|
|
35
|
-
JSON) trusting injected X-TOON-* headers. Free NIP-01
|
|
36
|
-
WS reads are unchanged. Mutually exclusive with
|
|
37
|
-
--connector-url.
|
|
38
|
-
--discovery <mode> Discovery mode: 'seed-list' or 'genesis' (default: 'genesis')
|
|
39
|
-
--seed-relays <urls> Comma-separated public Nostr relay URLs for seed discovery
|
|
40
|
-
--publish-seed-entry Publish this node as a seed relay entry (default: false)
|
|
41
|
-
--external-relay-url <url> External WebSocket URL of this relay
|
|
16
|
+
--relay-port <port> WebSocket read port (default: 7100)
|
|
17
|
+
--bls-port <port> HTTP write/health port (default: 3100)
|
|
18
|
+
--host <host> WebSocket bind host (default: 0.0.0.0)
|
|
19
|
+
--data-dir <path> Data directory for the SQLite store (default: ./data)
|
|
20
|
+
--dev-mode Skip event-signature verification on POST /write
|
|
42
21
|
--help Show this help message
|
|
43
22
|
|
|
44
23
|
Environment Variables:
|
|
45
|
-
TOON_MNEMONIC
|
|
46
|
-
TOON_SECRET_KEY
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
TOON_ILP_ADDRESS Same as --ilp-address (required with TOON_CONNECTOR_URL)
|
|
54
|
-
TOON_NODE_ID Same as --node-id
|
|
55
|
-
TOON_KNOWN_PEERS Same as --known-peers
|
|
56
|
-
TOON_DEV_MODE Same as --dev-mode (set to "true")
|
|
57
|
-
TOON_X402_ENABLED Same as --x402-enabled (set to "true")
|
|
58
|
-
TOON_OBLIVIOUS_MODE Same as --oblivious-mode (set to "true")
|
|
59
|
-
TOON_DISCOVERY Same as --discovery
|
|
60
|
-
TOON_SEED_RELAYS Same as --seed-relays
|
|
61
|
-
TOON_PUBLISH_SEED_ENTRY Same as --publish-seed-entry (set to "true")
|
|
62
|
-
TOON_EXTERNAL_RELAY_URL Same as --external-relay-url
|
|
63
|
-
TOON_FEE_PER_EVENT Fee per event in ILP units (overrides basePricePerByte)
|
|
64
|
-
TOON_SETTLEMENT_PRIVATE_KEY EVM private key (0x-prefixed 32-byte hex) for the
|
|
65
|
-
embedded connector's ClaimReceiver / chainProviders.
|
|
66
|
-
Defaults to the identity-derived secp256k1 hex.
|
|
67
|
-
TOON_PARENT_EVM_ADDRESS EVM treasury address advertised to the parent
|
|
68
|
-
connector as the peer entry's evmAddress (used by the
|
|
69
|
-
apex's PerPacketClaimService when opening a settlement
|
|
70
|
-
channel toward this child).
|
|
24
|
+
TOON_MNEMONIC Same as --mnemonic
|
|
25
|
+
TOON_SECRET_KEY Same as --secret-key
|
|
26
|
+
NOSTR_SECRET_KEY Alias for TOON_SECRET_KEY (identity); TOON_SECRET_KEY wins
|
|
27
|
+
TOON_RELAY_PORT Same as --relay-port
|
|
28
|
+
TOON_BLS_PORT Same as --bls-port
|
|
29
|
+
TOON_HOST Same as --host
|
|
30
|
+
TOON_DATA_DIR Same as --data-dir
|
|
31
|
+
TOON_DEV_MODE Same as --dev-mode (set to "true")
|
|
71
32
|
|
|
72
33
|
Security:
|
|
73
|
-
Prefer TOON_MNEMONIC
|
|
74
|
-
over --mnemonic / --secret-key CLI flags. CLI arguments are visible
|
|
75
|
-
other users on the system via process listings (e.g. ps aux). See CWE-214.
|
|
34
|
+
Prefer TOON_MNEMONIC / TOON_SECRET_KEY / NOSTR_SECRET_KEY environment
|
|
35
|
+
variables over --mnemonic / --secret-key CLI flags. CLI arguments are visible
|
|
36
|
+
to other users on the system via process listings (e.g. ps aux). See CWE-214.
|
|
76
37
|
`.trim()
|
|
77
38
|
);
|
|
78
39
|
}
|
|
@@ -83,20 +44,9 @@ function parseCli() {
|
|
|
83
44
|
"secret-key": { type: "string" },
|
|
84
45
|
"relay-port": { type: "string" },
|
|
85
46
|
"bls-port": { type: "string" },
|
|
47
|
+
host: { type: "string" },
|
|
86
48
|
"data-dir": { type: "string" },
|
|
87
|
-
"connector-url": { type: "string" },
|
|
88
|
-
"parent-peer-id": { type: "string" },
|
|
89
|
-
"parent-auth-token": { type: "string" },
|
|
90
|
-
"ilp-address": { type: "string" },
|
|
91
|
-
"node-id": { type: "string" },
|
|
92
|
-
"known-peers": { type: "string" },
|
|
93
49
|
"dev-mode": { type: "boolean" },
|
|
94
|
-
"x402-enabled": { type: "boolean" },
|
|
95
|
-
"oblivious-mode": { type: "boolean" },
|
|
96
|
-
discovery: { type: "string" },
|
|
97
|
-
"seed-relays": { type: "string" },
|
|
98
|
-
"publish-seed-entry": { type: "boolean" },
|
|
99
|
-
"external-relay-url": { type: "string" },
|
|
100
50
|
help: { type: "boolean" }
|
|
101
51
|
},
|
|
102
52
|
strict: true,
|
|
@@ -117,7 +67,7 @@ function parseCli() {
|
|
|
117
67
|
);
|
|
118
68
|
}
|
|
119
69
|
const mnemonic = values.mnemonic ?? process.env["TOON_MNEMONIC"] ?? void 0;
|
|
120
|
-
const secretKeyHex = values["secret-key"] ?? process.env["TOON_SECRET_KEY"] ?? void 0;
|
|
70
|
+
const secretKeyHex = values["secret-key"] ?? process.env["TOON_SECRET_KEY"] ?? process.env["NOSTR_SECRET_KEY"] ?? void 0;
|
|
121
71
|
let secretKey;
|
|
122
72
|
if (secretKeyHex) {
|
|
123
73
|
if (secretKeyHex.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(secretKeyHex)) {
|
|
@@ -126,14 +76,15 @@ function parseCli() {
|
|
|
126
76
|
}
|
|
127
77
|
secretKey = Uint8Array.from(Buffer.from(secretKeyHex, "hex"));
|
|
128
78
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
79
|
+
if (!mnemonic && !secretKey) {
|
|
80
|
+
console.error(
|
|
81
|
+
"Error: one of --mnemonic (or TOON_MNEMONIC) or --secret-key (or TOON_SECRET_KEY / NOSTR_SECRET_KEY) is required"
|
|
82
|
+
);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
if (mnemonic && secretKey) {
|
|
135
86
|
console.error(
|
|
136
|
-
"Error:
|
|
87
|
+
"Error: provide either a mnemonic or a secret key, not both"
|
|
137
88
|
);
|
|
138
89
|
process.exit(1);
|
|
139
90
|
}
|
|
@@ -149,162 +100,33 @@ function parseCli() {
|
|
|
149
100
|
console.error("Error: --bls-port must be an integer between 1 and 65535");
|
|
150
101
|
process.exit(1);
|
|
151
102
|
}
|
|
103
|
+
const host = values.host ?? process.env["TOON_HOST"] ?? void 0;
|
|
152
104
|
const dataDir = values["data-dir"] ?? process.env["TOON_DATA_DIR"] ?? void 0;
|
|
153
105
|
const devMode = values["dev-mode"] ?? (process.env["TOON_DEV_MODE"] === "true" ? true : void 0);
|
|
154
|
-
const x402Enabled = values["x402-enabled"] ?? (process.env["TOON_X402_ENABLED"] === "true" ? true : void 0);
|
|
155
|
-
const obliviousMode = values["oblivious-mode"] ?? (process.env["TOON_OBLIVIOUS_MODE"] === "true" ? true : void 0);
|
|
156
|
-
const knownPeersJson = values["known-peers"] ?? process.env["TOON_KNOWN_PEERS"] ?? void 0;
|
|
157
|
-
let knownPeers;
|
|
158
|
-
if (knownPeersJson) {
|
|
159
|
-
try {
|
|
160
|
-
const parsed = JSON.parse(knownPeersJson);
|
|
161
|
-
if (Array.isArray(parsed)) {
|
|
162
|
-
knownPeers = parsed.filter(
|
|
163
|
-
(p) => typeof p === "object" && p !== null && typeof p["pubkey"] === "string" && typeof p["btpEndpoint"] === "string"
|
|
164
|
-
).map((p) => ({
|
|
165
|
-
pubkey: p["pubkey"],
|
|
166
|
-
relayUrl: p["relayUrl"] || "ws://localhost:7100",
|
|
167
|
-
btpEndpoint: p["btpEndpoint"]
|
|
168
|
-
}));
|
|
169
|
-
}
|
|
170
|
-
} catch {
|
|
171
|
-
console.error("Error: --known-peers must be valid JSON");
|
|
172
|
-
process.exit(1);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
if (!mnemonic && !secretKey) {
|
|
176
|
-
console.error(
|
|
177
|
-
"Error: one of --mnemonic (or TOON_MNEMONIC) or --secret-key (or TOON_SECRET_KEY) is required"
|
|
178
|
-
);
|
|
179
|
-
process.exit(1);
|
|
180
|
-
}
|
|
181
|
-
const discoveryStr = values.discovery ?? process.env["TOON_DISCOVERY"] ?? void 0;
|
|
182
|
-
let discoveryMode;
|
|
183
|
-
if (discoveryStr) {
|
|
184
|
-
if (discoveryStr !== "seed-list" && discoveryStr !== "genesis") {
|
|
185
|
-
console.error('Error: --discovery must be "seed-list" or "genesis"');
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
188
|
-
discoveryMode = discoveryStr;
|
|
189
|
-
}
|
|
190
|
-
const seedRelaysStr = values["seed-relays"] ?? process.env["TOON_SEED_RELAYS"] ?? void 0;
|
|
191
|
-
const seedRelaysArr = seedRelaysStr ? seedRelaysStr.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
192
|
-
if (seedRelaysArr) {
|
|
193
|
-
for (const url of seedRelaysArr) {
|
|
194
|
-
if (!url.startsWith("ws://") && !url.startsWith("wss://")) {
|
|
195
|
-
console.error(
|
|
196
|
-
"Error: --seed-relays contains invalid URL -- must use WebSocket scheme (ws or wss)"
|
|
197
|
-
);
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
const publishSeedEntry = values["publish-seed-entry"] ?? (process.env["TOON_PUBLISH_SEED_ENTRY"] === "true" ? true : void 0);
|
|
203
|
-
const externalRelayUrl = values["external-relay-url"] ?? process.env["TOON_EXTERNAL_RELAY_URL"] ?? void 0;
|
|
204
|
-
const feePerEventStr = process.env["TOON_FEE_PER_EVENT"] ?? void 0;
|
|
205
|
-
const feePerEvent = feePerEventStr ? parseInt(feePerEventStr, 10) : void 0;
|
|
206
|
-
if (feePerEvent !== void 0 && (Number.isNaN(feePerEvent) || feePerEvent < 0)) {
|
|
207
|
-
console.error("Error: TOON_FEE_PER_EVENT must be a non-negative integer");
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
const btpEndpoint = process.env["TOON_BTP_ENDPOINT"] ?? void 0;
|
|
211
|
-
const assetCode = process.env["TOON_ASSET_CODE"] ?? void 0;
|
|
212
|
-
const assetScaleStr = process.env["TOON_ASSET_SCALE"] ?? void 0;
|
|
213
|
-
const assetScale = assetScaleStr ? parseInt(assetScaleStr, 10) : void 0;
|
|
214
|
-
if (assetScale !== void 0 && (Number.isNaN(assetScale) || assetScale < 0 || assetScale > 18)) {
|
|
215
|
-
console.error("Error: TOON_ASSET_SCALE must be an integer in 0..18");
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
218
|
-
const settlementPrivateKey = process.env["TOON_SETTLEMENT_PRIVATE_KEY"] ?? void 0;
|
|
219
|
-
if (settlementPrivateKey !== void 0 && !/^0x[0-9a-fA-F]{64}$/.test(settlementPrivateKey)) {
|
|
220
|
-
console.error(
|
|
221
|
-
"Error: TOON_SETTLEMENT_PRIVATE_KEY must be a 0x-prefixed 32-byte hex string"
|
|
222
|
-
);
|
|
223
|
-
process.exit(1);
|
|
224
|
-
}
|
|
225
|
-
const parentEvmAddress = process.env["TOON_PARENT_EVM_ADDRESS"] ?? void 0;
|
|
226
|
-
if (parentEvmAddress !== void 0 && !/^0x[0-9a-fA-F]{40}$/.test(parentEvmAddress)) {
|
|
227
|
-
console.error(
|
|
228
|
-
"Error: TOON_PARENT_EVM_ADDRESS must be a 0x-prefixed 20-byte hex address"
|
|
229
|
-
);
|
|
230
|
-
process.exit(1);
|
|
231
|
-
}
|
|
232
|
-
let chainRpcUrls;
|
|
233
|
-
let tokenNetworks;
|
|
234
|
-
let preferredTokens;
|
|
235
|
-
let settlementAddresses;
|
|
236
|
-
const supportedChainsStr = process.env["SUPPORTED_CHAINS"];
|
|
237
|
-
if (supportedChainsStr) {
|
|
238
|
-
const chains = supportedChainsStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
239
|
-
for (const chain of chains) {
|
|
240
|
-
const key = chain.replace(/:/g, "_").toUpperCase();
|
|
241
|
-
const addr = process.env[`SETTLEMENT_ADDRESS_${key}`];
|
|
242
|
-
if (addr) (settlementAddresses ??= {})[chain] = addr;
|
|
243
|
-
const rpc = process.env[`CHAIN_RPC_URL_${key}`];
|
|
244
|
-
if (rpc) (chainRpcUrls ??= {})[chain] = rpc;
|
|
245
|
-
const tokenNet = process.env[`TOKEN_NETWORK_${key}`];
|
|
246
|
-
if (tokenNet) (tokenNetworks ??= {})[chain] = tokenNet;
|
|
247
|
-
const token = process.env[`PREFERRED_TOKEN_${key}`];
|
|
248
|
-
if (token) (preferredTokens ??= {})[chain] = token;
|
|
249
|
-
if (!addr) {
|
|
250
|
-
console.warn(
|
|
251
|
-
`[Town] Warning: chain "${chain}" listed in SUPPORTED_CHAINS but no SETTLEMENT_ADDRESS_${key} env var found`
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
106
|
const config = {
|
|
257
|
-
...connectorUrl && { connectorUrl },
|
|
258
|
-
...chainRpcUrls && { chainRpcUrls },
|
|
259
|
-
...tokenNetworks && { tokenNetworks },
|
|
260
|
-
...preferredTokens && { preferredTokens },
|
|
261
|
-
...settlementAddresses && { settlementAddresses },
|
|
262
|
-
...parentPeerId && { parentPeerId },
|
|
263
|
-
...parentAuthToken !== void 0 && { parentAuthToken },
|
|
264
|
-
...ilpAddress && { ilpAddress },
|
|
265
|
-
...nodeId && { nodeId },
|
|
266
107
|
...mnemonic && { mnemonic },
|
|
267
108
|
...secretKey && { secretKey },
|
|
268
109
|
...relayPort !== void 0 && { relayPort },
|
|
269
110
|
...blsPort !== void 0 && { blsPort },
|
|
111
|
+
...host && { host },
|
|
270
112
|
...dataDir && { dataDir },
|
|
271
|
-
...
|
|
272
|
-
...devMode !== void 0 && { devMode },
|
|
273
|
-
...x402Enabled !== void 0 && { x402Enabled },
|
|
274
|
-
...obliviousMode !== void 0 && { obliviousMode },
|
|
275
|
-
...discoveryMode && { discovery: discoveryMode },
|
|
276
|
-
...seedRelaysArr && { seedRelays: seedRelaysArr },
|
|
277
|
-
...publishSeedEntry !== void 0 && { publishSeedEntry },
|
|
278
|
-
...externalRelayUrl && { externalRelayUrl },
|
|
279
|
-
...feePerEvent !== void 0 && { feePerEvent },
|
|
280
|
-
...btpEndpoint && { btpEndpoint },
|
|
281
|
-
...assetCode && { assetCode },
|
|
282
|
-
...assetScale !== void 0 && { assetScale },
|
|
283
|
-
...settlementPrivateKey && { settlementPrivateKey },
|
|
284
|
-
...parentEvmAddress && { parentEvmAddress }
|
|
113
|
+
...devMode !== void 0 && { devMode }
|
|
285
114
|
};
|
|
286
115
|
return config;
|
|
287
116
|
}
|
|
288
117
|
async function main() {
|
|
289
118
|
const config = parseCli();
|
|
290
119
|
console.log("\n" + "=".repeat(50));
|
|
291
|
-
console.log("TOON
|
|
120
|
+
console.log("TOON Relay Starting");
|
|
292
121
|
console.log("=".repeat(50) + "\n");
|
|
293
122
|
const instance = await startRelay(config);
|
|
294
123
|
console.log("\n" + "=".repeat(50));
|
|
295
|
-
console.log("TOON
|
|
124
|
+
console.log("TOON Relay Ready");
|
|
296
125
|
console.log("=".repeat(50));
|
|
297
|
-
console.log(` Pubkey:
|
|
298
|
-
console.log(`
|
|
299
|
-
console.log(`
|
|
300
|
-
console.log(`
|
|
301
|
-
console.log(` ILP Address: ${instance.config.ilpAddress}`);
|
|
302
|
-
if (instance.config.connectorUrl) {
|
|
303
|
-
console.log(` Parent BTP: ${instance.config.connectorUrl}`);
|
|
304
|
-
console.log(` Parent Peer: ${instance.config.parentPeerId}`);
|
|
305
|
-
}
|
|
306
|
-
console.log(` Peers: ${instance.bootstrapResult.peerCount}`);
|
|
307
|
-
console.log(` Channels: ${instance.bootstrapResult.channelCount}`);
|
|
126
|
+
console.log(` Pubkey: ${instance.pubkey}`);
|
|
127
|
+
console.log(` Reads: ws://localhost:${instance.config.relayPort}`);
|
|
128
|
+
console.log(` Writes: http://localhost:${instance.config.blsPort}/write`);
|
|
129
|
+
console.log(` Health: http://localhost:${instance.config.blsPort}/health`);
|
|
308
130
|
console.log("=".repeat(50) + "\n");
|
|
309
131
|
const shutdown = async (signal) => {
|
|
310
132
|
console.log(`
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/launcher/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * CLI entrypoint for @toon-protocol/relay.\n *\n * Thin wrapper around startRelay() that parses CLI flags and environment\n * variables, then delegates all logic to town.ts.\n *\n * Usage:\n * npx @toon-protocol/relay --mnemonic \"abandon abandon ...\" \\\n * --connector-url \"ws://apex.example:3001\" \\\n * --ilp-address \"g.townhouse.alice\"\n *\n * Environment variables override defaults; CLI flags override environment variables.\n */\n\nimport { parseArgs } from 'node:util';\nimport { startRelay } from './town.js';\nimport type { RelayConfig, RelayInstance } from './town.js';\n\n// ---------- CLI Parsing ----------\n\nfunction printHelp(): void {\n console.log(\n `\nUsage: toon-town [options]\n\nOptions:\n --mnemonic <words> BIP-39 mnemonic (12 or 24 words)\n --secret-key <hex> 32-byte secret key in hex\n --relay-port <port> WebSocket relay port (default: 7100)\n --bls-port <port> BLS HTTP port (default: 3100)\n --data-dir <path> Data directory (default: ./data)\n --connector-url <url> Parent connector BTP URL (e.g. ws://apex:3001).\n When set, the embedded connector peers with this\n URL and routes everything outside the local prefix\n through it. --ilp-address becomes REQUIRED and must\n fall under the parent's prefix.\n --parent-peer-id <id> BTP peer id to register the parent under (default: apex)\n --parent-auth-token <t> Auth token for the parent peer (default: empty / no-auth)\n --ilp-address <addr> ILP address for this node (default: g.toon.<pubkey>;\n REQUIRED when --connector-url is set, e.g. g.townhouse.<self>)\n --node-id <id> Stable nodeId for the embedded connector (default: toon-<pubkey>)\n --known-peers <json> Known peers as JSON array\n --dev-mode Enable dev mode (skip verification)\n --x402-enabled Enable x402 /publish endpoint (default: false)\n --oblivious-mode Run as a payment-oblivious relay (default: false).\n No embedded connector is created and no x402/ILP\n settlement code runs; exposes POST /write (event-as-\n JSON) trusting injected X-TOON-* headers. Free NIP-01\n WS reads are unchanged. Mutually exclusive with\n --connector-url.\n --discovery <mode> Discovery mode: 'seed-list' or 'genesis' (default: 'genesis')\n --seed-relays <urls> Comma-separated public Nostr relay URLs for seed discovery\n --publish-seed-entry Publish this node as a seed relay entry (default: false)\n --external-relay-url <url> External WebSocket URL of this relay\n --help Show this help message\n\nEnvironment Variables:\n TOON_MNEMONIC Same as --mnemonic\n TOON_SECRET_KEY Same as --secret-key\n TOON_RELAY_PORT Same as --relay-port\n TOON_BLS_PORT Same as --bls-port\n TOON_DATA_DIR Same as --data-dir\n TOON_CONNECTOR_URL Same as --connector-url (parent BTP URL)\n TOON_PARENT_PEER_ID Same as --parent-peer-id\n TOON_PARENT_AUTH_TOKEN Same as --parent-auth-token\n TOON_ILP_ADDRESS Same as --ilp-address (required with TOON_CONNECTOR_URL)\n TOON_NODE_ID Same as --node-id\n TOON_KNOWN_PEERS Same as --known-peers\n TOON_DEV_MODE Same as --dev-mode (set to \"true\")\n TOON_X402_ENABLED Same as --x402-enabled (set to \"true\")\n TOON_OBLIVIOUS_MODE Same as --oblivious-mode (set to \"true\")\n TOON_DISCOVERY Same as --discovery\n TOON_SEED_RELAYS Same as --seed-relays\n TOON_PUBLISH_SEED_ENTRY Same as --publish-seed-entry (set to \"true\")\n TOON_EXTERNAL_RELAY_URL Same as --external-relay-url\n TOON_FEE_PER_EVENT Fee per event in ILP units (overrides basePricePerByte)\n TOON_SETTLEMENT_PRIVATE_KEY EVM private key (0x-prefixed 32-byte hex) for the\n embedded connector's ClaimReceiver / chainProviders.\n Defaults to the identity-derived secp256k1 hex.\n TOON_PARENT_EVM_ADDRESS EVM treasury address advertised to the parent\n connector as the peer entry's evmAddress (used by the\n apex's PerPacketClaimService when opening a settlement\n channel toward this child).\n\nSecurity:\n Prefer TOON_MNEMONIC or TOON_SECRET_KEY environment variables\n over --mnemonic / --secret-key CLI flags. CLI arguments are visible to\n other users on the system via process listings (e.g. ps aux). See CWE-214.\n`.trim()\n );\n}\n\nfunction parseCli(): RelayConfig {\n const { values } = parseArgs({\n options: {\n mnemonic: { type: 'string' },\n 'secret-key': { type: 'string' },\n 'relay-port': { type: 'string' },\n 'bls-port': { type: 'string' },\n 'data-dir': { type: 'string' },\n 'connector-url': { type: 'string' },\n 'parent-peer-id': { type: 'string' },\n 'parent-auth-token': { type: 'string' },\n 'ilp-address': { type: 'string' },\n 'node-id': { type: 'string' },\n 'known-peers': { type: 'string' },\n 'dev-mode': { type: 'boolean' },\n 'x402-enabled': { type: 'boolean' },\n 'oblivious-mode': { type: 'boolean' },\n discovery: { type: 'string' },\n 'seed-relays': { type: 'string' },\n 'publish-seed-entry': { type: 'boolean' },\n 'external-relay-url': { type: 'string' },\n help: { type: 'boolean' },\n },\n strict: true,\n allowPositionals: false,\n });\n\n if (values.help) {\n printHelp();\n process.exit(0);\n }\n\n // Resolve: CLI flags override env vars\n\n // Warn about process-listing exposure (CWE-214) when secrets are passed via CLI flags\n if (values.mnemonic) {\n console.warn(\n 'Warning: --mnemonic is visible in process listings. ' +\n 'Prefer TOON_MNEMONIC environment variable for production use.'\n );\n }\n if (values['secret-key']) {\n console.warn(\n 'Warning: --secret-key is visible in process listings. ' +\n 'Prefer TOON_SECRET_KEY environment variable for production use.'\n );\n }\n\n const mnemonic = values.mnemonic ?? process.env['TOON_MNEMONIC'] ?? undefined;\n\n const secretKeyHex =\n values['secret-key'] ?? process.env['TOON_SECRET_KEY'] ?? undefined;\n\n let secretKey: Uint8Array | undefined;\n if (secretKeyHex) {\n if (secretKeyHex.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(secretKeyHex)) {\n console.error('Error: --secret-key must be a 64-character hex string');\n process.exit(1);\n }\n secretKey = Uint8Array.from(Buffer.from(secretKeyHex, 'hex'));\n }\n\n const connectorUrl =\n values['connector-url'] ?? process.env['TOON_CONNECTOR_URL'] ?? undefined;\n\n const parentPeerId =\n values['parent-peer-id'] ?? process.env['TOON_PARENT_PEER_ID'] ?? undefined;\n\n const parentAuthToken =\n values['parent-auth-token'] ??\n process.env['TOON_PARENT_AUTH_TOKEN'] ??\n undefined;\n\n const ilpAddress =\n values['ilp-address'] ?? process.env['TOON_ILP_ADDRESS'] ?? undefined;\n\n const nodeId = values['node-id'] ?? process.env['TOON_NODE_ID'] ?? undefined;\n\n if (connectorUrl && !ilpAddress) {\n console.error(\n 'Error: --ilp-address (or TOON_ILP_ADDRESS) is required when ' +\n '--connector-url is set; it must fall under the parent connector prefix ' +\n '(e.g. g.townhouse.<self>)'\n );\n process.exit(1);\n }\n\n const relayPortStr =\n values['relay-port'] ?? process.env['TOON_RELAY_PORT'] ?? undefined;\n const relayPort = relayPortStr ? parseInt(relayPortStr, 10) : undefined;\n if (\n relayPort !== undefined &&\n (Number.isNaN(relayPort) || relayPort <= 0 || relayPort > 65535)\n ) {\n console.error('Error: --relay-port must be an integer between 1 and 65535');\n process.exit(1);\n }\n\n const blsPortStr =\n values['bls-port'] ?? process.env['TOON_BLS_PORT'] ?? undefined;\n const blsPort = blsPortStr ? parseInt(blsPortStr, 10) : undefined;\n if (\n blsPort !== undefined &&\n (Number.isNaN(blsPort) || blsPort <= 0 || blsPort > 65535)\n ) {\n console.error('Error: --bls-port must be an integer between 1 and 65535');\n process.exit(1);\n }\n\n const dataDir =\n values['data-dir'] ?? process.env['TOON_DATA_DIR'] ?? undefined;\n\n const devMode =\n values['dev-mode'] ??\n (process.env['TOON_DEV_MODE'] === 'true' ? true : undefined);\n\n const x402Enabled =\n values['x402-enabled'] ??\n (process.env['TOON_X402_ENABLED'] === 'true' ? true : undefined);\n\n const obliviousMode =\n values['oblivious-mode'] ??\n (process.env['TOON_OBLIVIOUS_MODE'] === 'true' ? true : undefined);\n\n const knownPeersJson =\n values['known-peers'] ?? process.env['TOON_KNOWN_PEERS'] ?? undefined;\n\n let knownPeers:\n | { pubkey: string; relayUrl: string; btpEndpoint: string }[]\n | undefined;\n if (knownPeersJson) {\n try {\n const parsed: unknown = JSON.parse(knownPeersJson);\n if (Array.isArray(parsed)) {\n knownPeers = (parsed as unknown[])\n .filter(\n (p): p is Record<string, unknown> =>\n typeof p === 'object' &&\n p !== null &&\n typeof (p as Record<string, unknown>)['pubkey'] === 'string' &&\n typeof (p as Record<string, unknown>)['btpEndpoint'] === 'string'\n )\n .map((p) => ({\n pubkey: p['pubkey'] as string,\n relayUrl: (p['relayUrl'] as string) || 'ws://localhost:7100',\n btpEndpoint: p['btpEndpoint'] as string,\n }));\n }\n } catch {\n console.error('Error: --known-peers must be valid JSON');\n process.exit(1);\n }\n }\n\n if (!mnemonic && !secretKey) {\n console.error(\n 'Error: one of --mnemonic (or TOON_MNEMONIC) or --secret-key (or TOON_SECRET_KEY) is required'\n );\n process.exit(1);\n }\n\n // Discovery mode\n const discoveryStr =\n values.discovery ?? process.env['TOON_DISCOVERY'] ?? undefined;\n let discoveryMode: 'seed-list' | 'genesis' | undefined;\n if (discoveryStr) {\n if (discoveryStr !== 'seed-list' && discoveryStr !== 'genesis') {\n console.error('Error: --discovery must be \"seed-list\" or \"genesis\"');\n process.exit(1);\n }\n discoveryMode = discoveryStr;\n }\n\n // Seed relays (comma-separated list of public Nostr relay URLs)\n const seedRelaysStr =\n values['seed-relays'] ?? process.env['TOON_SEED_RELAYS'] ?? undefined;\n const seedRelaysArr = seedRelaysStr\n ? seedRelaysStr\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n : undefined;\n\n // Validate seed relay URLs have WebSocket scheme (CWE-20)\n if (seedRelaysArr) {\n for (const url of seedRelaysArr) {\n // nosemgrep: javascript.lang.security.detect-insecure-websocket.detect-insecure-websocket -- validation check, not a connection\n if (!url.startsWith('ws://') && !url.startsWith('wss://')) {\n console.error(\n 'Error: --seed-relays contains invalid URL -- must use WebSocket scheme (ws or wss)'\n );\n process.exit(1);\n }\n }\n }\n\n // Publish seed entry flag\n const publishSeedEntry =\n values['publish-seed-entry'] ??\n (process.env['TOON_PUBLISH_SEED_ENTRY'] === 'true' ? true : undefined);\n\n // External relay URL\n const externalRelayUrl =\n values['external-relay-url'] ??\n process.env['TOON_EXTERNAL_RELAY_URL'] ??\n undefined;\n\n // Fee per event (overrides basePricePerByte)\n const feePerEventStr = process.env['TOON_FEE_PER_EVENT'] ?? undefined;\n const feePerEvent = feePerEventStr ? parseInt(feePerEventStr, 10) : undefined;\n if (\n feePerEvent !== undefined &&\n (Number.isNaN(feePerEvent) || feePerEvent < 0)\n ) {\n console.error('Error: TOON_FEE_PER_EVENT must be a non-negative integer');\n process.exit(1);\n }\n\n // Public BTP endpoint advertised in this town's kind:10032 (so clients learn\n // how to reach the apex to route packets to g.townhouse.town). Set by the\n // Townhouse orchestrator from the apex's .anyone / direct URL.\n const btpEndpoint = process.env['TOON_BTP_ENDPOINT'] ?? undefined;\n\n // Settlement asset advertised in kind:10032.\n const assetCode = process.env['TOON_ASSET_CODE'] ?? undefined;\n const assetScaleStr = process.env['TOON_ASSET_SCALE'] ?? undefined;\n const assetScale = assetScaleStr ? parseInt(assetScaleStr, 10) : undefined;\n if (\n assetScale !== undefined &&\n (Number.isNaN(assetScale) || assetScale < 0 || assetScale > 18)\n ) {\n console.error('Error: TOON_ASSET_SCALE must be an integer in 0..18');\n process.exit(1);\n }\n\n // Settlement private key — controls the embedded connector's ClaimReceiver\n // signer. CLI flag intentionally omitted (process listings would expose the\n // key via `ps`, CWE-214). Env-only.\n const settlementPrivateKey =\n process.env['TOON_SETTLEMENT_PRIVATE_KEY'] ?? undefined;\n if (\n settlementPrivateKey !== undefined &&\n !/^0x[0-9a-fA-F]{64}$/.test(settlementPrivateKey)\n ) {\n console.error(\n 'Error: TOON_SETTLEMENT_PRIVATE_KEY must be a 0x-prefixed 32-byte hex string'\n );\n process.exit(1);\n }\n\n // Parent EVM address advertised to the apex peer. Public address — safe to\n // ship via env. Validated as 0x + 40 hex chars (ERC-55 mixed-case allowed).\n const parentEvmAddress = process.env['TOON_PARENT_EVM_ADDRESS'] ?? undefined;\n if (\n parentEvmAddress !== undefined &&\n !/^0x[0-9a-fA-F]{40}$/.test(parentEvmAddress)\n ) {\n console.error(\n 'Error: TOON_PARENT_EVM_ADDRESS must be a 0x-prefixed 20-byte hex address'\n );\n process.exit(1);\n }\n\n // Multi-chain settlement advertisement (additive; opt-in via SUPPORTED_CHAINS).\n //\n // The default single-EVM-chain path (TOON_CHAIN/TOON_RPC_URL) is unchanged.\n // When SUPPORTED_CHAINS is set, parse per-chain env vars so the node can\n // advertise additional chains (notably `solana:devnet`) in kind:10032 with a\n // chain-native settlement recipient. Env key convention mirrors the SDK\n // entrypoint (docker/src/shared.ts): \"solana:devnet\" -> \"SOLANA_DEVNET\".\n // SETTLEMENT_ADDRESS_<KEY> recipient address advertised for the chain\n // CHAIN_RPC_URL_<KEY> RPC URL for the chain\n // TOKEN_NETWORK_<KEY> payment-channel program / token-network address\n // PREFERRED_TOKEN_<KEY> preferred token (e.g. USDC mint)\n let chainRpcUrls: Record<string, string> | undefined;\n let tokenNetworks: Record<string, string> | undefined;\n let preferredTokens: Record<string, string> | undefined;\n let settlementAddresses: Record<string, string> | undefined;\n const supportedChainsStr = process.env['SUPPORTED_CHAINS'];\n if (supportedChainsStr) {\n const chains = supportedChainsStr\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n for (const chain of chains) {\n const key = chain.replace(/:/g, '_').toUpperCase();\n const addr = process.env[`SETTLEMENT_ADDRESS_${key}`];\n if (addr) (settlementAddresses ??= {})[chain] = addr;\n const rpc = process.env[`CHAIN_RPC_URL_${key}`];\n if (rpc) (chainRpcUrls ??= {})[chain] = rpc;\n const tokenNet = process.env[`TOKEN_NETWORK_${key}`];\n if (tokenNet) (tokenNetworks ??= {})[chain] = tokenNet;\n const token = process.env[`PREFERRED_TOKEN_${key}`];\n if (token) (preferredTokens ??= {})[chain] = token;\n if (!addr) {\n console.warn(\n `[Town] Warning: chain \"${chain}\" listed in SUPPORTED_CHAINS but no SETTLEMENT_ADDRESS_${key} env var found`\n );\n }\n }\n }\n\n const config: RelayConfig = {\n ...(connectorUrl && { connectorUrl }),\n ...(chainRpcUrls && { chainRpcUrls }),\n ...(tokenNetworks && { tokenNetworks }),\n ...(preferredTokens && { preferredTokens }),\n ...(settlementAddresses && { settlementAddresses }),\n ...(parentPeerId && { parentPeerId }),\n ...(parentAuthToken !== undefined && { parentAuthToken }),\n ...(ilpAddress && { ilpAddress }),\n ...(nodeId && { nodeId }),\n ...(mnemonic && { mnemonic }),\n ...(secretKey && { secretKey }),\n ...(relayPort !== undefined && { relayPort }),\n ...(blsPort !== undefined && { blsPort }),\n ...(dataDir && { dataDir }),\n ...(knownPeers && { knownPeers }),\n ...(devMode !== undefined && { devMode }),\n ...(x402Enabled !== undefined && { x402Enabled }),\n ...(obliviousMode !== undefined && { obliviousMode }),\n ...(discoveryMode && { discovery: discoveryMode }),\n ...(seedRelaysArr && { seedRelays: seedRelaysArr }),\n ...(publishSeedEntry !== undefined && { publishSeedEntry }),\n ...(externalRelayUrl && { externalRelayUrl }),\n ...(feePerEvent !== undefined && { feePerEvent }),\n ...(btpEndpoint && { btpEndpoint }),\n ...(assetCode && { assetCode }),\n ...(assetScale !== undefined && { assetScale }),\n ...(settlementPrivateKey && { settlementPrivateKey }),\n ...(parentEvmAddress && { parentEvmAddress }),\n };\n\n return config;\n}\n\n// ---------- Main ----------\n\nasync function main(): Promise<void> {\n const config = parseCli();\n\n console.log('\\n' + '='.repeat(50));\n console.log('TOON Town Starting');\n console.log('='.repeat(50) + '\\n');\n\n const instance: RelayInstance = await startRelay(config);\n\n console.log('\\n' + '='.repeat(50));\n console.log('TOON Town Ready');\n console.log('='.repeat(50));\n console.log(` Pubkey: ${instance.pubkey}`);\n console.log(` EVM Address: ${instance.evmAddress}`);\n console.log(` Relay: ws://localhost:${instance.config.relayPort}`);\n console.log(` BLS: http://localhost:${instance.config.blsPort}`);\n console.log(` ILP Address: ${instance.config.ilpAddress}`);\n if (instance.config.connectorUrl) {\n console.log(` Parent BTP: ${instance.config.connectorUrl}`);\n console.log(` Parent Peer: ${instance.config.parentPeerId}`);\n }\n console.log(` Peers: ${instance.bootstrapResult.peerCount}`);\n console.log(` Channels: ${instance.bootstrapResult.channelCount}`);\n console.log('='.repeat(50) + '\\n');\n\n // Wire graceful shutdown\n const shutdown = async (signal: string): Promise<void> => {\n console.log(`\\n[Shutdown] Received ${signal}`);\n await instance.stop();\n console.log('[Shutdown] Complete');\n process.exit(0);\n };\n\n process.on('SIGINT', () => {\n shutdown('SIGINT').catch(console.error);\n });\n process.on('SIGTERM', () => {\n shutdown('SIGTERM').catch(console.error);\n });\n}\n\nmain().catch((error: unknown) => {\n console.error('[Fatal] Startup error:', error);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/launcher/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * CLI entrypoint for @toon-protocol/relay.\n *\n * Thin wrapper around startRelay() that parses CLI flags and environment\n * variables, then delegates to relay.ts. The relay is a plain read/write app:\n * free NIP-01 WebSocket reads plus an HTTP `POST /write` surface. Payment is\n * enforced upstream by an external terminator, so there are no connector,\n * ILP, chain, or pricing options here.\n *\n * Usage:\n * relay --secret-key <hex>\n * NOSTR_SECRET_KEY=<hex> relay\n *\n * Environment variables override defaults; CLI flags override environment variables.\n */\n\nimport { parseArgs } from 'node:util';\nimport { startRelay } from './relay.js';\nimport type { RelayConfig, RelayInstance } from './relay.js';\n\n// ---------- CLI Parsing ----------\n\nfunction printHelp(): void {\n console.log(\n `\nUsage: relay [options]\n\nOptions:\n --mnemonic <words> BIP-39 mnemonic (12 or 24 words; NIP-06 derivation)\n --secret-key <hex> 32-byte secret key in hex\n --relay-port <port> WebSocket read port (default: 7100)\n --bls-port <port> HTTP write/health port (default: 3100)\n --host <host> WebSocket bind host (default: 0.0.0.0)\n --data-dir <path> Data directory for the SQLite store (default: ./data)\n --dev-mode Skip event-signature verification on POST /write\n --help Show this help message\n\nEnvironment Variables:\n TOON_MNEMONIC Same as --mnemonic\n TOON_SECRET_KEY Same as --secret-key\n NOSTR_SECRET_KEY Alias for TOON_SECRET_KEY (identity); TOON_SECRET_KEY wins\n TOON_RELAY_PORT Same as --relay-port\n TOON_BLS_PORT Same as --bls-port\n TOON_HOST Same as --host\n TOON_DATA_DIR Same as --data-dir\n TOON_DEV_MODE Same as --dev-mode (set to \"true\")\n\nSecurity:\n Prefer TOON_MNEMONIC / TOON_SECRET_KEY / NOSTR_SECRET_KEY environment\n variables over --mnemonic / --secret-key CLI flags. CLI arguments are visible\n to other users on the system via process listings (e.g. ps aux). See CWE-214.\n`.trim()\n );\n}\n\nfunction parseCli(): RelayConfig {\n const { values } = parseArgs({\n options: {\n mnemonic: { type: 'string' },\n 'secret-key': { type: 'string' },\n 'relay-port': { type: 'string' },\n 'bls-port': { type: 'string' },\n host: { type: 'string' },\n 'data-dir': { type: 'string' },\n 'dev-mode': { type: 'boolean' },\n help: { type: 'boolean' },\n },\n strict: true,\n allowPositionals: false,\n });\n\n if (values.help) {\n printHelp();\n process.exit(0);\n }\n\n // Warn about process-listing exposure (CWE-214) when secrets are passed via CLI flags\n if (values.mnemonic) {\n console.warn(\n 'Warning: --mnemonic is visible in process listings. ' +\n 'Prefer TOON_MNEMONIC environment variable for production use.'\n );\n }\n if (values['secret-key']) {\n console.warn(\n 'Warning: --secret-key is visible in process listings. ' +\n 'Prefer TOON_SECRET_KEY environment variable for production use.'\n );\n }\n\n const mnemonic = values.mnemonic ?? process.env['TOON_MNEMONIC'] ?? undefined;\n\n // Identity secret key. NOSTR_SECRET_KEY is accepted as an alias for\n // TOON_SECRET_KEY so the container honors the same identity env the connector\n // compose uses. TOON_SECRET_KEY wins when both are set.\n const secretKeyHex =\n values['secret-key'] ??\n process.env['TOON_SECRET_KEY'] ??\n process.env['NOSTR_SECRET_KEY'] ??\n undefined;\n\n let secretKey: Uint8Array | undefined;\n if (secretKeyHex) {\n if (secretKeyHex.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(secretKeyHex)) {\n console.error('Error: --secret-key must be a 64-character hex string');\n process.exit(1);\n }\n secretKey = Uint8Array.from(Buffer.from(secretKeyHex, 'hex'));\n }\n\n if (!mnemonic && !secretKey) {\n console.error(\n 'Error: one of --mnemonic (or TOON_MNEMONIC) or --secret-key ' +\n '(or TOON_SECRET_KEY / NOSTR_SECRET_KEY) is required'\n );\n process.exit(1);\n }\n if (mnemonic && secretKey) {\n console.error(\n 'Error: provide either a mnemonic or a secret key, not both'\n );\n process.exit(1);\n }\n\n const relayPortStr =\n values['relay-port'] ?? process.env['TOON_RELAY_PORT'] ?? undefined;\n const relayPort = relayPortStr ? parseInt(relayPortStr, 10) : undefined;\n if (\n relayPort !== undefined &&\n (Number.isNaN(relayPort) || relayPort <= 0 || relayPort > 65535)\n ) {\n console.error('Error: --relay-port must be an integer between 1 and 65535');\n process.exit(1);\n }\n\n const blsPortStr =\n values['bls-port'] ?? process.env['TOON_BLS_PORT'] ?? undefined;\n const blsPort = blsPortStr ? parseInt(blsPortStr, 10) : undefined;\n if (\n blsPort !== undefined &&\n (Number.isNaN(blsPort) || blsPort <= 0 || blsPort > 65535)\n ) {\n console.error('Error: --bls-port must be an integer between 1 and 65535');\n process.exit(1);\n }\n\n const host = values.host ?? process.env['TOON_HOST'] ?? undefined;\n\n const dataDir =\n values['data-dir'] ?? process.env['TOON_DATA_DIR'] ?? undefined;\n\n const devMode =\n values['dev-mode'] ??\n (process.env['TOON_DEV_MODE'] === 'true' ? true : undefined);\n\n const config: RelayConfig = {\n ...(mnemonic && { mnemonic }),\n ...(secretKey && { secretKey }),\n ...(relayPort !== undefined && { relayPort }),\n ...(blsPort !== undefined && { blsPort }),\n ...(host && { host }),\n ...(dataDir && { dataDir }),\n ...(devMode !== undefined && { devMode }),\n };\n\n return config;\n}\n\n// ---------- Main ----------\n\nasync function main(): Promise<void> {\n const config = parseCli();\n\n console.log('\\n' + '='.repeat(50));\n console.log('TOON Relay Starting');\n console.log('='.repeat(50) + '\\n');\n\n const instance: RelayInstance = await startRelay(config);\n\n console.log('\\n' + '='.repeat(50));\n console.log('TOON Relay Ready');\n console.log('='.repeat(50));\n console.log(` Pubkey: ${instance.pubkey}`);\n console.log(` Reads: ws://localhost:${instance.config.relayPort}`);\n console.log(` Writes: http://localhost:${instance.config.blsPort}/write`);\n console.log(` Health: http://localhost:${instance.config.blsPort}/health`);\n console.log('='.repeat(50) + '\\n');\n\n // Wire graceful shutdown\n const shutdown = async (signal: string): Promise<void> => {\n console.log(`\\n[Shutdown] Received ${signal}`);\n await instance.stop();\n console.log('[Shutdown] Complete');\n process.exit(0);\n };\n\n process.on('SIGINT', () => {\n shutdown('SIGINT').catch(console.error);\n });\n process.on('SIGTERM', () => {\n shutdown('SIGTERM').catch(console.error);\n });\n}\n\nmain().catch((error: unknown) => {\n console.error('[Fatal] Startup error:', error);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|