@toon-protocol/relay 1.3.3 → 1.3.4

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/filters/matchFilter.ts","../src/storage/InMemoryEventStore.ts","../src/storage/SqliteEventStore.ts","../src/toon/index.ts","../src/websocket/ConnectionHandler.ts","../src/websocket/NostrRelayServer.ts","../src/subscriber/RelaySubscriber.ts","../src/launcher/handlers/event-storage-handler.ts","../src/launcher/handlers/x402-pricing.ts","../src/launcher/handlers/x402-types.ts","../src/launcher/handlers/x402-preflight.ts","../src/launcher/handlers/x402-settlement.ts","../src/launcher/handlers/x402-publish-handler.ts","../src/launcher/health.ts","../src/launcher/town.ts","../src/launcher/handlers/oblivious-write-handler.ts"],"sourcesContent":["/**\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","export {\n encodeEventToToon,\n encodeEventToToonString,\n ToonEncodeError,\n} from '@toon-protocol/core';\nexport { decodeEventFromToon, ToonDecodeError } from '@toon-protocol/core';\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 { encodeEventToToonString } from '../toon/index.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 private sendEvent(subscriptionId: string, event: NostrEvent): void {\n this.send(['EVENT', subscriptionId, encodeEventToToonString(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 * Event storage handler for @toon-protocol/relay.\n *\n * Stores incoming Nostr events in the EventStore after decoding from TOON.\n * This is the \"default\" handler for the relay -- it processes all event kinds\n * except those handled by kind-specific handlers.\n *\n * The handler is intentionally simple (~15 lines of logic). The SDK pipeline\n * handles signature verification, pricing validation, and self-write bypass\n * before the handler is invoked. The handler only needs to:\n * 1. ctx.decode() -- lazy-decode the TOON payload into a NostrEvent\n * 2. eventStore.store(event) -- persist the event\n * 3. ctx.accept({ eventId, storedAt }) -- accept the ILP packet\n */\n\nimport type { EventStore } from '../../storage/index.js';\nimport type {\n Handler,\n HandlerContext,\n HandlerResponse,\n} from '@toon-protocol/sdk';\n\n/**\n * Configuration for the event storage handler.\n *\n * Minimal by design -- the handler's only job is decode + store + accept.\n * Pricing, verification, and self-write bypass are SDK pipeline concerns.\n */\nexport interface EventStorageHandlerConfig {\n /** Event store backend (e.g., SqliteEventStore from @toon-protocol/relay). */\n eventStore: EventStore;\n}\n\n/**\n * Creates an event storage handler that decodes TOON payloads and stores\n * Nostr events in the configured EventStore.\n *\n * Errors from `ctx.decode()` or `eventStore.store()` are not caught here --\n * they propagate to the SDK's dispatch error boundary, which converts\n * unhandled exceptions to `{ accept: false, code: 'T00', message: 'Internal error' }`.\n *\n * @param config - Handler configuration with the event store backend.\n * @returns A handler function compatible with `node.onDefault(handler)`.\n */\nexport function createEventStorageHandler(\n config: EventStorageHandlerConfig\n): Handler {\n const { eventStore } = config;\n\n return async (ctx: HandlerContext): Promise<HandlerResponse> => {\n // Decode the TOON payload into a structured NostrEvent\n const event = ctx.decode();\n\n // Store the event (EventStore handles replaceable events, duplicates, etc.)\n eventStore.store(event);\n\n // Accept the packet with event metadata\n return ctx.accept({ eventId: event.id, storedAt: Date.now() });\n };\n}\n","/**\n * x402 pricing calculator with multi-hop routing buffer.\n *\n * Computes the all-in USDC price for publishing a Nostr event via the\n * x402 HTTP on-ramp. The price includes a configurable routing buffer\n * (default 10%) to cover multi-hop overhead -- intermediate relays charge\n * their own per-byte fees.\n *\n * @module\n */\n\n/**\n * Configuration for the x402 pricing calculator.\n */\nexport interface X402PricingConfig {\n /** Base price per byte in ILP/USDC micro-units (e.g., 10n). */\n basePricePerByte: bigint;\n /** Routing buffer percentage (default: 10, meaning 10%). */\n routingBufferPercent: number;\n}\n\n/**\n * Calculate the all-in x402 price for a TOON payload.\n *\n * Formula:\n * basePrice = basePricePerByte * toonLength\n * buffer = basePrice * routingBufferPercent / 100\n * total = basePrice + buffer\n *\n * The routing buffer covers multi-hop overhead. 10% default is a\n * conservative estimate per Party Mode Decision 8.\n *\n * @param config - Pricing configuration with base price and buffer percent.\n * @param toonLength - Length of the TOON-encoded payload in bytes.\n * @returns Total price in USDC micro-units.\n */\nexport function calculateX402Price(\n config: X402PricingConfig,\n toonLength: number\n): bigint {\n // Guard against misconfigured routing buffer that could undercharge or\n // produce nonsensical prices. Clamp to [0, 200] (0% to 200% buffer).\n const clampedBuffer = Math.max(0, Math.min(200, config.routingBufferPercent));\n const basePrice = config.basePricePerByte * BigInt(toonLength);\n const buffer = (basePrice * BigInt(clampedBuffer)) / 100n;\n return basePrice + buffer;\n}\n","/**\n * EIP-3009 types and constants for the x402 publish endpoint.\n *\n * EIP-3009 (`transferWithAuthorization`) allows gasless USDC transfers:\n * the user signs an off-chain authorization, and the facilitator (node\n * operator) submits it on-chain, paying gas. The user pays only the\n * USDC transfer amount.\n *\n * @module\n */\n\nimport type { NostrEvent } from 'nostr-tools/pure';\n\n/**\n * EIP-3009 `transferWithAuthorization` signed authorization.\n *\n * The user signs this off-chain (EIP-712 typed data). The facilitator\n * submits the signature on-chain to execute the USDC transfer.\n */\nexport interface Eip3009Authorization {\n /** Sender's EVM address ('0x...'). */\n from: string;\n /** Recipient's EVM address ('0x...' -- facilitator). */\n to: string;\n /** USDC amount in micro-units (bigint). */\n value: bigint;\n /** Unix timestamp: authorization valid after this time. */\n validAfter: number;\n /** Unix timestamp: authorization expires at this time. */\n validBefore: number;\n /** 32-byte nonce ('0x...' hex string). */\n nonce: string;\n /** ECDSA recovery id (27 or 28). */\n v: number;\n /** ECDSA r component ('0x...' 32 bytes). */\n r: string;\n /** ECDSA s component ('0x...' 32 bytes). */\n s: string;\n}\n\n/**\n * EIP-712 typed data structure for `transferWithAuthorization`.\n *\n * This is the type definition used for off-chain signature verification\n * and on-chain contract calls.\n *\n * NOTE: The EIP-712 domain for USDC's `transferWithAuthorization` is\n * different from the EIP-712 domain for TokenNetwork's balance proofs.\n * The x402 handler must use the USDC contract's domain.\n */\nexport const EIP_3009_TYPES = {\n TransferWithAuthorization: [\n { name: 'from', type: 'address' },\n { name: 'to', type: 'address' },\n { name: 'value', type: 'uint256' },\n { name: 'validAfter', type: 'uint256' },\n { name: 'validBefore', type: 'uint256' },\n { name: 'nonce', type: 'bytes32' },\n ],\n} as const;\n\n/**\n * EIP-712 domain separator for USDC's `transferWithAuthorization`.\n *\n * Uses the USDC contract's name and version, NOT the TokenNetwork's.\n */\nexport const USDC_EIP712_DOMAIN = {\n name: 'USD Coin',\n version: '2',\n} as const;\n\n/**\n * Minimal EventStore interface for destination reachability checks.\n * Uses structural typing to avoid importing @toon-protocol/relay directly.\n * The query method accepts Filter[] (array) per the relay's EventStore interface.\n */\nexport interface EventStoreLike {\n query(filters: { kinds?: number[]; authors?: string[] }[]): unknown[];\n}\n\n/**\n * Request body for the x402 `/publish` endpoint.\n *\n * The client sends a signed Nostr event and a destination ILP address.\n * The handler TOON-encodes the event before routing.\n */\nexport interface X402PublishRequest {\n /** Signed Nostr event. */\n event: NostrEvent;\n /** Target ILP address (e.g., \"g.toon.target-relay\"). */\n destination: string;\n}\n\n/**\n * Response body for a successful x402 `/publish` request (HTTP 200).\n */\nexport interface X402PublishResponse {\n /** Nostr event ID (64-char hex). */\n eventId: string;\n /** On-chain settlement transaction hash. */\n settlementTxHash: string;\n /** Whether the ILP PREPARE was fulfilled or rejected by the destination. */\n deliveryStatus: 'fulfilled' | 'rejected';\n /** Always false -- no refunds on REJECT per protocol design. */\n refundInitiated: false;\n}\n\n/**\n * Response body for the 402 pricing negotiation.\n */\nexport interface X402PricingResponse {\n /** Price in USDC micro-units (as string for BigInt serialization). */\n amount: string;\n /** Node operator's EVM address that will receive the USDC. */\n facilitatorAddress: string;\n /** Payment network identifier. */\n paymentNetwork: 'eip-3009';\n /** EVM chain ID. */\n chainId: number;\n /** USDC contract address on this chain. */\n usdcAddress: string;\n}\n\n/**\n * Minimal USDC ABI for EIP-3009 operations.\n *\n * Includes only the functions needed by the x402 handler:\n * - `balanceOf`: Read sender's USDC balance (pre-flight check #2)\n * - `authorizationState`: Check nonce freshness (pre-flight check #3)\n * - `transferWithAuthorization`: Execute gasless USDC transfer (settlement)\n */\nexport const USDC_ABI = [\n {\n name: 'balanceOf',\n type: 'function',\n stateMutability: 'view',\n inputs: [{ name: 'account', type: 'address' }],\n outputs: [{ name: '', type: 'uint256' }],\n },\n {\n name: 'authorizationState',\n type: 'function',\n stateMutability: 'view',\n inputs: [\n { name: 'authorizer', type: 'address' },\n { name: 'nonce', type: 'bytes32' },\n ],\n outputs: [{ name: '', type: 'bool' }],\n },\n {\n name: 'transferWithAuthorization',\n type: 'function',\n stateMutability: 'nonpayable',\n inputs: [\n { name: 'from', type: 'address' },\n { name: 'to', type: 'address' },\n { name: 'value', type: 'uint256' },\n { name: 'validAfter', type: 'uint256' },\n { name: 'validBefore', type: 'uint256' },\n { name: 'nonce', type: 'bytes32' },\n { name: 'v', type: 'uint8' },\n { name: 'r', type: 'bytes32' },\n { name: 's', type: 'bytes32' },\n ],\n outputs: [],\n },\n] as const;\n","/**\n * Pre-flight validation pipeline for the x402 publish endpoint.\n *\n * Implements 6 free checks that run before any on-chain transaction,\n * preventing gas griefing (E3-R008). All checks are either pure\n * cryptography or read-only RPC calls (no gas cost).\n *\n * Check order (cheapest to most expensive):\n * 1. EIP-3009 signature verification (off-chain, ~1ms)\n * 2. USDC balance check (eth_call, ~50ms)\n * 3. Nonce freshness check (eth_call, ~50ms)\n * 4. TOON shallow parse (pure computation, ~0.1ms)\n * 5. Schnorr signature verification (pure crypto, ~2ms)\n * 6. Destination reachability check (local lookup, ~0.1ms)\n *\n * @module\n */\n\nimport { verifyTypedData } from 'viem';\nimport type { PublicClient } from 'viem';\nimport { shallowParseToon } from '@toon-protocol/core/toon';\nimport type { ToonRoutingMeta } from '@toon-protocol/core/toon';\nimport type { ChainPreset } from '@toon-protocol/core';\nimport type { Eip3009Authorization, EventStoreLike } from './x402-types.js';\nimport { EIP_3009_TYPES, USDC_EIP712_DOMAIN, USDC_ABI } from './x402-types.js';\n\n/**\n * Result of running the pre-flight validation pipeline.\n */\nexport interface PreflightResult {\n /** Whether all checks passed. */\n passed: boolean;\n /** Which check failed (only set if passed is false). */\n failedCheck?: string;\n /** List of check names that were executed. */\n checksPerformed: string[];\n}\n\n/**\n * Callback for Schnorr signature verification.\n * Returns true if the signature is valid.\n */\nexport type SchnorrVerifyFn = (meta: ToonRoutingMeta) => Promise<boolean>;\n\n/**\n * Configuration for the pre-flight validation pipeline.\n */\nexport interface PreflightConfig {\n /** Resolved chain configuration. */\n chainConfig: ChainPreset;\n /** Base price per byte for pricing validation. */\n basePricePerByte: bigint;\n /** This node's Nostr public key. */\n ownPubkey: string;\n /** Whether dev mode is enabled (skips Schnorr verification). */\n devMode: boolean;\n /** viem public client for read-only contract calls (optional, for testing). */\n publicClient?: PublicClient;\n /** EventStore for destination reachability check (optional). */\n eventStore?: EventStoreLike;\n /** Schnorr verification callback (optional, uses SDK verification pipeline). */\n schnorrVerify?: SchnorrVerifyFn;\n}\n\n/**\n * Run the 6-stage pre-flight validation pipeline.\n *\n * All checks are free (no gas cost). If any check fails, execution\n * stops immediately and no on-chain transaction is attempted.\n *\n * @param authorization - EIP-3009 signed authorization from the client.\n * @param toonData - Base64-encoded TOON payload.\n * @param destination - Target ILP address.\n * @param config - Pre-flight configuration.\n * @returns PreflightResult indicating success or which check failed.\n */\nexport async function runPreflight(\n authorization: Eip3009Authorization,\n toonData: string,\n destination: string,\n config: PreflightConfig\n): Promise<PreflightResult> {\n const checksPerformed: string[] = [];\n\n // --- Check 1: EIP-3009 signature verification (off-chain) ---\n checksPerformed.push('eip3009-signature');\n try {\n const domain = {\n ...USDC_EIP712_DOMAIN,\n chainId: config.chainConfig.chainId,\n verifyingContract: config.chainConfig.usdcAddress as `0x${string}`,\n };\n\n const valid = await verifyTypedData({\n address: authorization.from as `0x${string}`,\n domain,\n types: EIP_3009_TYPES,\n primaryType: 'TransferWithAuthorization',\n message: {\n from: authorization.from as `0x${string}`,\n to: authorization.to as `0x${string}`,\n value: authorization.value,\n validAfter: BigInt(authorization.validAfter),\n validBefore: BigInt(authorization.validBefore),\n nonce: authorization.nonce as `0x${string}`,\n },\n signature: encodeSignature(authorization),\n });\n\n if (!valid) {\n return {\n passed: false,\n failedCheck: 'eip3009-signature',\n checksPerformed,\n };\n }\n } catch {\n return { passed: false, failedCheck: 'eip3009-signature', checksPerformed };\n }\n\n // --- Check 2: USDC balance check (read-only eth_call) ---\n checksPerformed.push('usdc-balance');\n if (config.publicClient) {\n try {\n const balance = await config.publicClient.readContract({\n address: config.chainConfig.usdcAddress as `0x${string}`,\n abi: USDC_ABI,\n functionName: 'balanceOf',\n args: [authorization.from as `0x${string}`],\n });\n if ((balance as bigint) < authorization.value) {\n return { passed: false, failedCheck: 'usdc-balance', checksPerformed };\n }\n } catch {\n return { passed: false, failedCheck: 'usdc-balance', checksPerformed };\n }\n }\n\n // --- Check 3: Nonce freshness check (read-only eth_call) ---\n checksPerformed.push('nonce-freshness');\n if (config.publicClient) {\n try {\n const used = await config.publicClient.readContract({\n address: config.chainConfig.usdcAddress as `0x${string}`,\n abi: USDC_ABI,\n functionName: 'authorizationState',\n args: [\n authorization.from as `0x${string}`,\n authorization.nonce as `0x${string}`,\n ],\n });\n if (used) {\n return {\n passed: false,\n failedCheck: 'nonce-freshness',\n checksPerformed,\n };\n }\n } catch {\n return { passed: false, failedCheck: 'nonce-freshness', checksPerformed };\n }\n }\n\n // --- Check 4: TOON shallow parse ---\n checksPerformed.push('toon-shallow-parse');\n let toonMeta: ToonRoutingMeta;\n try {\n const toonBytes = Buffer.from(toonData, 'base64');\n toonMeta = shallowParseToon(toonBytes);\n } catch {\n return {\n passed: false,\n failedCheck: 'toon-shallow-parse',\n checksPerformed,\n };\n }\n\n // --- Check 5: Schnorr signature verification ---\n checksPerformed.push('schnorr-signature');\n if (!config.devMode && config.schnorrVerify) {\n try {\n const valid = await config.schnorrVerify(toonMeta);\n if (!valid) {\n return {\n passed: false,\n failedCheck: 'schnorr-signature',\n checksPerformed,\n };\n }\n } catch {\n return {\n passed: false,\n failedCheck: 'schnorr-signature',\n checksPerformed,\n };\n }\n }\n\n // --- Check 6: Destination reachability check ---\n checksPerformed.push('destination-reachability');\n if (config.eventStore) {\n try {\n const events = config.eventStore.query([{ kinds: [10032] }]);\n // A destination is reachable if we have at least one kind:10032 peer\n // info event (which means the connector has peers that may route the\n // packet). Without any peer info, no ILP routes exist.\n if (events.length === 0) {\n return {\n passed: false,\n failedCheck: 'destination-reachability',\n checksPerformed,\n };\n }\n } catch {\n return {\n passed: false,\n failedCheck: 'destination-reachability',\n checksPerformed,\n };\n }\n }\n\n return { passed: true, checksPerformed };\n}\n\n/**\n * Encode an EIP-3009 authorization's v, r, s components into a\n * compact signature hex string for viem's verifyTypedData.\n */\nfunction encodeSignature(auth: Eip3009Authorization): `0x${string}` {\n // r (32 bytes) + s (32 bytes) + v (1 byte)\n const r = auth.r.startsWith('0x') ? auth.r.slice(2) : auth.r;\n const s = auth.s.startsWith('0x') ? auth.s.slice(2) : auth.s;\n const v = auth.v.toString(16).padStart(2, '0');\n return `0x${r}${s}${v}`;\n}\n","/**\n * EIP-3009 on-chain settlement module for the x402 publish endpoint.\n *\n * Executes `transferWithAuthorization` on the USDC contract to settle\n * the gasless USDC transfer from the client to the facilitator (node\n * operator). The facilitator pays gas; the client pays only USDC.\n *\n * Settlement atomicity (E3-R006):\n * - If settlement fails (revert), no ILP PREPARE is constructed.\n * - If settlement succeeds but ILP PREPARE is rejected, no refund.\n *\n * @module\n */\n\nimport type { WalletClient, PublicClient } from 'viem';\nimport type { ChainPreset } from '@toon-protocol/core';\nimport type { Eip3009Authorization } from './x402-types.js';\nimport { USDC_ABI } from './x402-types.js';\n\n/**\n * Result of an EIP-3009 settlement attempt.\n */\nexport interface X402SettlementResult {\n /** Whether the on-chain transaction succeeded. */\n success: boolean;\n /** Transaction hash (only set on success). */\n txHash?: string;\n /** Error message (only set on failure). */\n error?: string;\n}\n\n/**\n * @deprecated Use X402SettlementResult instead.\n */\nexport type SettlementResult = X402SettlementResult;\n\n/**\n * Configuration for the settlement module.\n *\n * Named `X402SettlementConfig` to avoid collision with\n * `SettlementConfig` from `@toon-protocol/core` (bootstrap).\n */\nexport interface X402SettlementConfig {\n /** Resolved chain configuration. */\n chainConfig: ChainPreset;\n /** viem wallet client for the facilitator (submits the tx, pays gas). */\n walletClient: WalletClient;\n /** viem public client for waiting on transaction receipts. */\n publicClient?: PublicClient;\n}\n\n/**\n * Settle an EIP-3009 `transferWithAuthorization` on-chain.\n *\n * Submits the client's signed authorization to the USDC contract.\n * The facilitator (node operator) pays gas for the transaction.\n *\n * @param authorization - Signed EIP-3009 authorization from the client.\n * @param config - Settlement configuration with wallet client.\n * @returns SettlementResult indicating success/failure.\n */\n/**\n * @deprecated Use X402SettlementConfig instead.\n */\nexport type SettlementConfig = X402SettlementConfig;\n\nexport async function settleEip3009(\n authorization: Eip3009Authorization,\n config: X402SettlementConfig\n): Promise<X402SettlementResult> {\n try {\n const hash = await config.walletClient.writeContract({\n address: config.chainConfig.usdcAddress as `0x${string}`,\n abi: USDC_ABI,\n functionName: 'transferWithAuthorization',\n args: [\n authorization.from as `0x${string}`,\n authorization.to as `0x${string}`,\n authorization.value,\n BigInt(authorization.validAfter),\n BigInt(authorization.validBefore),\n authorization.nonce as `0x${string}`,\n authorization.v,\n authorization.r as `0x${string}`,\n authorization.s as `0x${string}`,\n ],\n chain: null, // Use the wallet client's configured chain\n account: config.walletClient.account ?? null,\n });\n\n // Optionally wait for receipt if public client is available\n if (config.publicClient) {\n const receipt = await config.publicClient.waitForTransactionReceipt({\n hash,\n });\n if (receipt.status === 'reverted') {\n return {\n success: false,\n error: 'Transaction reverted on-chain',\n };\n }\n }\n\n return {\n success: true,\n txHash: hash,\n };\n } catch (error: unknown) {\n const message =\n error instanceof Error ? error.message : 'Unknown settlement error';\n return {\n success: false,\n error: message,\n };\n }\n}\n","/**\n * x402 publish handler for the TOON protocol.\n *\n * Implements the HTTP-native payment on-ramp via the x402 protocol pattern.\n * Allows any HTTP client (AI agents, browsers, CLI tools) to publish Nostr\n * events to the network by paying USDC, without understanding ILP or\n * running an ILP client.\n *\n * Flow:\n * 1. Client sends request without X-PAYMENT header -> 402 with pricing\n * 2. Client signs EIP-3009 auth and retries with X-PAYMENT header\n * 3. Handler runs 6 free pre-flight checks\n * 4. Handler settles USDC on-chain via transferWithAuthorization\n * 5. Handler constructs ILP PREPARE via shared buildIlpPrepare()\n * 6. Handler routes PREPARE through connector\n * 7. Handler returns 200 with event ID and tx hash\n *\n * @module\n */\n\nimport type { Context } from 'hono';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport { buildIlpPrepare, encodeEventToToon } from '@toon-protocol/core';\nimport type {\n ChainPreset,\n IlpClient,\n BuildIlpPrepareParams,\n} from '@toon-protocol/core';\nimport { calculateX402Price } from './x402-pricing.js';\nimport { runPreflight } from './x402-preflight.js';\nimport type { PreflightConfig } from './x402-preflight.js';\nimport { settleEip3009 } from './x402-settlement.js';\nimport type {\n X402SettlementConfig,\n X402SettlementResult,\n} from './x402-settlement.js';\nimport type {\n Eip3009Authorization,\n EventStoreLike,\n X402PublishRequest,\n X402PublishResponse,\n X402PricingResponse,\n} from './x402-types.js';\nimport type { WalletClient, PublicClient } from 'viem';\n\n/**\n * Configuration for the x402 publish handler.\n */\nexport interface X402HandlerConfig {\n /** Whether x402 is enabled for this node. */\n x402Enabled: boolean;\n /** Resolved chain configuration. */\n chainConfig: ChainPreset;\n /** Base price per byte in USDC micro-units. */\n basePricePerByte: bigint;\n /** Routing buffer percentage for multi-hop overhead (default: 10). */\n routingBufferPercent: number;\n /** Facilitator's EVM address (receives USDC payments). */\n facilitatorAddress: string;\n /** This node's Nostr public key. */\n ownPubkey: string;\n /** Whether dev mode is enabled (skips Schnorr verification). */\n devMode: boolean;\n /** ILP client for sending PREPARE packets. */\n ilpClient?: IlpClient;\n /** Event store for destination reachability check. */\n eventStore?: EventStoreLike;\n /** TOON encoder function (defaults to core's encodeEventToToon). */\n toonEncoder?: (event: NostrEvent) => Uint8Array;\n /** viem wallet client for on-chain settlement (facilitator pays gas). */\n walletClient?: WalletClient;\n /** viem public client for read-only contract calls. */\n publicClient?: PublicClient;\n /** Override settle function (for testing). */\n settle?: (\n auth: Eip3009Authorization,\n config: X402SettlementConfig\n ) => Promise<X402SettlementResult>;\n /** Override pre-flight function (for testing). */\n runPreflightFn?: typeof runPreflight;\n}\n\n/**\n * x402 publish handler instance.\n */\nexport interface X402Handler {\n /** Handle a /publish request (both 402 pricing and paid publish). */\n handlePublish: (c: Context) => Promise<Response>;\n}\n\n/**\n * Create an x402 publish handler.\n *\n * Returns a handler that processes both the 402 pricing negotiation\n * (no X-PAYMENT header) and the paid publish flow (with X-PAYMENT header).\n *\n * @param config - Handler configuration.\n * @returns X402Handler with handlePublish method.\n */\nexport function createX402Handler(config: X402HandlerConfig): X402Handler {\n const encoder = config.toonEncoder ?? encodeEventToToon;\n\n // Validate facilitator address at construction time (fail fast)\n if (\n config.x402Enabled &&\n (!config.facilitatorAddress ||\n !/^0x[0-9a-fA-F]{40}$/.test(config.facilitatorAddress))\n ) {\n throw new Error(\n 'x402 enabled but facilitatorAddress is not a valid EVM address'\n );\n }\n\n return {\n async handlePublish(c: Context): Promise<Response> {\n // --- Gate: x402 disabled ---\n if (!config.x402Enabled) {\n return c.json({ error: 'x402 not enabled' }, 404);\n }\n\n // --- Parse request body ---\n let body: X402PublishRequest;\n try {\n body = (await c.req.json()) as X402PublishRequest;\n } catch {\n return c.json({ error: 'Invalid request body' }, 400);\n }\n\n if (!body.event || !body.destination) {\n return c.json(\n { error: 'Missing required fields: event, destination' },\n 400\n );\n }\n\n // Validate destination ILP address format (must start with 'g.' per ILP)\n if (\n typeof body.destination !== 'string' ||\n !body.destination.startsWith('g.')\n ) {\n return c.json(\n { error: 'Invalid destination: must be a global ILP address (g.*)' },\n 400\n );\n }\n\n // --- TOON-encode the event ---\n let toonBytes: Uint8Array;\n try {\n toonBytes = encoder(body.event);\n } catch {\n return c.json({ error: 'Failed to TOON-encode event' }, 400);\n }\n\n const toonBase64 = Buffer.from(toonBytes).toString('base64');\n\n // --- Check for X-PAYMENT header ---\n const paymentHeader = c.req.header('X-PAYMENT');\n\n if (!paymentHeader) {\n // --- 402 Pricing Response ---\n const price = calculateX402Price(\n {\n basePricePerByte: config.basePricePerByte,\n routingBufferPercent: config.routingBufferPercent,\n },\n toonBytes.length\n );\n\n const pricing: X402PricingResponse = {\n amount: String(price),\n facilitatorAddress: config.facilitatorAddress,\n paymentNetwork: 'eip-3009',\n chainId: config.chainConfig.chainId,\n usdcAddress: config.chainConfig.usdcAddress,\n };\n\n return c.json(pricing, 402);\n }\n\n // --- Parse EIP-3009 authorization from X-PAYMENT header ---\n let authorization: Eip3009Authorization;\n try {\n const parsed: unknown = JSON.parse(paymentHeader);\n authorization = parseAuthorization(parsed);\n } catch {\n return c.json({ error: 'Invalid X-PAYMENT header' }, 400);\n }\n\n // --- Pre-flight validation (6 free checks) ---\n const preflightConfig: PreflightConfig = {\n chainConfig: config.chainConfig,\n basePricePerByte: config.basePricePerByte,\n ownPubkey: config.ownPubkey,\n devMode: config.devMode,\n publicClient: config.publicClient,\n eventStore: config.eventStore,\n };\n\n try {\n const preflightFn = config.runPreflightFn ?? runPreflight;\n const preflightResult = await preflightFn(\n authorization,\n toonBase64,\n body.destination,\n preflightConfig\n );\n\n if (!preflightResult.passed) {\n return c.json(\n {\n error: `Pre-flight check failed: ${preflightResult.failedCheck}`,\n failedCheck: preflightResult.failedCheck,\n },\n 400\n );\n }\n } catch {\n // CWE-209: generic error, log details server-side\n console.error('[x402] Pre-flight error');\n return c.json({ error: 'Internal server error' }, 500);\n }\n\n // --- On-chain settlement ---\n let settlementResult: X402SettlementResult;\n try {\n const settleFn = config.settle ?? settleEip3009;\n\n // Guard: walletClient is required for real settlement\n if (!config.settle && !config.walletClient) {\n console.error('[x402] Settlement error: walletClient not configured');\n return c.json({ error: 'Internal server error' }, 500);\n }\n\n const settlementConfig: X402SettlementConfig = {\n chainConfig: config.chainConfig,\n walletClient: config.walletClient as WalletClient,\n publicClient: config.publicClient,\n };\n\n settlementResult = await settleFn(authorization, settlementConfig);\n } catch {\n // CWE-209: generic error\n console.error('[x402] Settlement error');\n return c.json({ error: 'Internal server error' }, 500);\n }\n\n if (!settlementResult.success) {\n // Log the full error server-side; return generic message to client\n // to avoid leaking on-chain revert reasons (CWE-209).\n console.error(\n '[x402] Settlement failed:',\n settlementResult.error ?? 'unknown'\n );\n return c.json({ error: 'Settlement failed' }, 400);\n }\n\n // --- Construct and send ILP PREPARE ---\n const amount = config.basePricePerByte * BigInt(toonBytes.length);\n\n const prepareParams: BuildIlpPrepareParams = {\n destination: body.destination,\n amount,\n data: toonBytes,\n };\n\n const prepare = buildIlpPrepare(prepareParams);\n\n let deliveryStatus: 'fulfilled' | 'rejected' = 'rejected';\n\n if (config.ilpClient) {\n try {\n const ilpResult = await config.ilpClient.sendIlpPacket(prepare);\n deliveryStatus = ilpResult.accepted ? 'fulfilled' : 'rejected';\n } catch {\n // ILP send failed, but settlement already succeeded.\n // No refund per protocol design.\n deliveryStatus = 'rejected';\n }\n }\n\n // --- Build response ---\n const response: X402PublishResponse = {\n eventId: body.event.id,\n settlementTxHash: settlementResult.txHash ?? '',\n deliveryStatus,\n refundInitiated: false,\n };\n\n return c.json(response, 200);\n },\n };\n}\n\n/**\n * Parse and validate an EIP-3009 authorization from the X-PAYMENT header.\n *\n * @param parsed - Parsed JSON from the header.\n * @returns Validated Eip3009Authorization.\n * @throws If required fields are missing or invalid.\n */\n/**\n * Validate a hex string has the expected format: 0x-prefixed, correct length,\n * and contains only valid hex characters.\n */\nfunction isValidHex(value: string, expectedLength: number): boolean {\n if (value.length !== expectedLength) return false;\n if (!value.startsWith('0x')) return false;\n return /^0x[0-9a-fA-F]+$/.test(value);\n}\n\nfunction parseAuthorization(parsed: unknown): Eip3009Authorization {\n if (typeof parsed !== 'object' || parsed === null) {\n throw new Error('Authorization must be an object');\n }\n\n const obj = parsed as Record<string, unknown>;\n\n const from = obj['from'];\n const to = obj['to'];\n const value = obj['value'];\n const validAfter = obj['validAfter'];\n const validBefore = obj['validBefore'];\n const nonce = obj['nonce'];\n const v = obj['v'];\n const r = obj['r'];\n const s = obj['s'];\n\n // EVM addresses: 0x + 40 hex chars = 42 total\n if (typeof from !== 'string' || !isValidHex(from, 42)) {\n throw new Error('Invalid from address');\n }\n if (typeof to !== 'string' || !isValidHex(to, 42)) {\n throw new Error('Invalid to address');\n }\n // bytes32 nonce: 0x + 64 hex chars = 66 total\n if (typeof nonce !== 'string' || !isValidHex(nonce, 66)) {\n throw new Error('Invalid nonce');\n }\n // bytes32 r and s: 0x + 64 hex chars = 66 total\n if (typeof r !== 'string' || !isValidHex(r, 66)) {\n throw new Error('Invalid r');\n }\n if (typeof s !== 'string' || !isValidHex(s, 66)) {\n throw new Error('Invalid s');\n }\n // v must be 27 or 28 (standard ECDSA recovery id)\n if (typeof v !== 'number' || (v !== 27 && v !== 28)) {\n throw new Error('Invalid v');\n }\n\n // Validate validAfter and validBefore are numeric (prevent NaN propagation)\n const parsedValidAfter = Number(validAfter);\n const parsedValidBefore = Number(validBefore);\n if (Number.isNaN(parsedValidAfter) || parsedValidAfter < 0) {\n throw new Error('Invalid validAfter');\n }\n if (Number.isNaN(parsedValidBefore) || parsedValidBefore < 0) {\n throw new Error('Invalid validBefore');\n }\n\n // Validate value is a non-negative numeric value\n const valueStr = String(value);\n let valueBigInt: bigint;\n try {\n valueBigInt = BigInt(valueStr);\n } catch {\n throw new Error('Invalid value');\n }\n if (valueBigInt < 0n) {\n throw new Error('Invalid value: must be non-negative');\n }\n\n return {\n from,\n to,\n value: valueBigInt,\n validAfter: parsedValidAfter,\n validBefore: parsedValidBefore,\n nonce,\n v,\n r,\n s,\n };\n}\n","/**\n * Enriched health response for TOON relay nodes (Story 3.6).\n *\n * Provides a pure function `createHealthResponse()` that builds a comprehensive\n * health JSON object combining static configuration (pricing, chain, version,\n * capabilities) with live runtime state (phase, peerCount, channelCount).\n *\n * The response mirrors kind:10035 service discovery event fields but adds\n * runtime-only fields that cannot be known at event publish time.\n *\n * @module\n */\n\nimport { VERSION } from '@toon-protocol/core';\nimport type { BootstrapPhase } from '@toon-protocol/core';\n\n/** TEE attestation state for the health response (enforcement guideline 12). */\nexport interface TeeHealthInfo {\n /** Whether a valid attestation has been published. */\n attested: boolean;\n /** Enclave type identifier (e.g., 'aws-nitro', 'marlin-oyster'). */\n enclaveType: string;\n /** Unix timestamp of the last attestation event. */\n lastAttestation: number;\n /** Platform Configuration Register 0 (SHA-384 hex, 96 chars). */\n pcr0: string;\n /** Attestation validity state. */\n state: 'valid' | 'stale' | 'unattested';\n}\n\n/** Configuration for building a health response. */\nexport interface HealthConfig {\n /** Current bootstrap phase. */\n phase: BootstrapPhase;\n /** Node's Nostr pubkey (64-char hex). */\n pubkey: string;\n /** Node's ILP address. */\n ilpAddress: string;\n /** Number of registered peers. */\n peerCount: number;\n /** Number of discovered (not yet registered) peers. */\n discoveredPeerCount: number;\n /** Number of open payment channels. */\n channelCount: number;\n /**\n * Base price per byte (bigint from config, converted to number via Number()).\n * Values exceeding Number.MAX_SAFE_INTEGER (2^53 - 1) will lose precision.\n */\n basePricePerByte: bigint;\n /** Whether x402 is enabled. */\n x402Enabled: boolean;\n /** Chain preset name. */\n chain: string;\n /**\n * TEE attestation info.\n * Omit entirely when not running in a TEE (enforcement guideline 12).\n */\n tee?: TeeHealthInfo;\n}\n\n/** The enriched health response shape. */\nexport interface HealthResponse {\n status: 'healthy';\n phase: BootstrapPhase;\n pubkey: string;\n ilpAddress: string;\n peerCount: number;\n discoveredPeerCount: number;\n channelCount: number;\n pricing: {\n basePricePerByte: number;\n currency: 'USDC';\n };\n x402?: {\n enabled: true;\n endpoint: string;\n };\n /**\n * TEE attestation info. Only present when running in a TEE enclave.\n * Entirely absent when not in TEE (enforcement guideline 12 --\n * never `{ attested: false }`, simply omit the field).\n */\n tee?: TeeHealthInfo;\n capabilities: string[];\n chain: string;\n version: string;\n sdk: true;\n timestamp: number;\n}\n\n/**\n * Build an enriched health response from the given configuration.\n *\n * This is a pure function -- it takes a config object and returns a response\n * object. No Hono context or HTTP request is needed, making it easy to unit\n * test and reuse across entrypoints.\n *\n * The `x402` field is entirely omitted when x402 is disabled (AC #2).\n * This matches the same omission semantics used in kind:10035 events.\n *\n * @param config - Health configuration with runtime state and static config.\n * @returns The enriched health response object.\n */\nexport function createHealthResponse(config: HealthConfig): HealthResponse {\n const response: HealthResponse = {\n status: 'healthy',\n phase: config.phase,\n pubkey: config.pubkey,\n ilpAddress: config.ilpAddress,\n peerCount: config.peerCount,\n discoveredPeerCount: config.discoveredPeerCount,\n channelCount: config.channelCount,\n pricing: {\n basePricePerByte: Number(config.basePricePerByte),\n currency: 'USDC',\n },\n capabilities: config.x402Enabled ? ['relay', 'x402'] : ['relay'],\n chain: config.chain,\n version: VERSION,\n sdk: true,\n timestamp: Date.now(),\n };\n\n if (config.x402Enabled) {\n response.x402 = {\n enabled: true,\n endpoint: '/publish',\n };\n }\n\n // TEE attestation info (enforcement guideline 12: omit entirely when not in TEE)\n if (config.tee) {\n response.tee = config.tee;\n }\n\n return response;\n}\n","/**\n * startRelay() -- Programmatic API for starting a TOON relay node.\n *\n * This module wraps the same SDK components used by docker/src/entrypoint-sdk.ts\n * into a single function call with a typed configuration object. Both\n * `startRelay()` and the Docker entrypoint compose the same pipeline:\n *\n * Identity -> Verification -> Pricing -> HandlerRegistry -> BLS + Relay + Bootstrap\n *\n * The key difference is lifecycle management: the Docker entrypoint uses\n * process-level signals (SIGINT/SIGTERM), while `startRelay()` returns a\n * `RelayInstance` with an explicit `.stop()` method.\n *\n * ## Deployment Modes\n *\n * The town node ALWAYS runs an embedded `ConnectorNode` so that packets\n * destined for its own ILP address can be routed locally (the connector and\n * the BLS handler must share a process for the round-trip to work).\n *\n * - **Standalone embedded** (no `connectorUrl`): A self-routing embedded\n * connector with no upstream peers. Useful for genesis nodes and tests.\n * - **Embedded with parent** (`connectorUrl` set): The embedded connector\n * is configured with `connectorUrl` as a parent BTP peer, plus a self-route\n * for local delivery and a default-route to the parent for everything else.\n * - **Pre-built embedded** (`connector`): Pass a fully constructed\n * `EmbeddableConnectorLike`. The town does not modify it.\n *\n * `connector` and `connectorUrl` are mutually exclusive — provide at most one.\n *\n * - **Oblivious** (`obliviousMode: true`, default `false`): the relay runs as a\n * payment-oblivious app behind an external terminator. No embedded connector\n * is created; no x402/EIP-3009/ILP-settlement code runs. The node exposes\n * `POST /write` (event-as-JSON), trusting injected `X-TOON-Payer`/`-Amount`/\n * `-Chain` headers without re-validating payment. Free NIP-01 WS reads are\n * unchanged. Mutually exclusive with `connector`/`connectorUrl`. The embedded\n * modes above remain the DEFAULT and are unchanged when `obliviousMode` is\n * false.\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 {\n HandlerRegistry,\n createVerificationPipeline,\n createPricingValidator,\n createHandlerContext,\n fromMnemonic,\n fromSecretKey,\n} from '@toon-protocol/sdk';\nimport type {\n HandlePacketAcceptResponse,\n HandlePacketRejectResponse,\n NodeIdentity,\n} from '@toon-protocol/sdk';\nimport { createEventStorageHandler } from './handlers/event-storage-handler.js';\nimport { createX402Handler } from './handlers/x402-publish-handler.js';\nimport { createObliviousWriteHandler } from './handlers/oblivious-write-handler.js';\nimport { createHealthResponse } from './health.js';\nimport {\n BootstrapService,\n createDiscoveryTracker,\n ILP_PEER_INFO_KIND,\n createDirectIlpClient,\n createDirectConnectorAdmin,\n createDirectChannelClient,\n SocialPeerDiscovery,\n buildIlpPeerInfoEvent,\n resolveChainConfig,\n SeedRelayDiscovery,\n publishSeedRelayEntry,\n buildServiceDiscoveryEvent,\n VERSION,\n} from '@toon-protocol/core';\nimport type {\n ServiceDiscoveryContent,\n SkillDescriptor,\n} from '@toon-protocol/core';\nimport type {\n ConnectorChannelClient,\n BootstrapEvent,\n IlpPeerInfo,\n HandlePacketRequest,\n ConnectorAdminClient,\n IlpClient,\n SettlementConfig,\n EmbeddableConnectorLike,\n} from '@toon-protocol/core';\nimport {\n shallowParseToon,\n decodeEventFromToon,\n encodeEventToToon,\n} from '@toon-protocol/core/toon';\nimport { SqliteEventStore } from '../storage/index.js';\nimport { NostrRelayServer } from '../websocket/index.js';\nimport { RelaySubscriber } from '../subscriber/index.js';\nimport type { EventStore } from '../storage/index.js';\nimport type { Filter } from 'nostr-tools/filter';\nimport {\n ConnectorNode,\n createLogger as createConnectorLogger,\n} from '@toon-protocol/connector';\nimport type { ConnectorConfig } from '@toon-protocol/connector';\nimport {\n createPublicClient,\n createWalletClient,\n defineChain,\n http,\n} from 'viem';\nimport type { WalletClient, PublicClient } from 'viem';\nimport { privateKeyToAccount } from 'viem/accounts';\n\n// ---------- SDK Pipeline Constants ----------\nconst MAX_PAYLOAD_BASE64_LENGTH = 1_048_576;\n\n// ---------- Public Types ----------\n\n/**\n * Configuration for starting a TOON relay node via `startRelay()`.\n *\n * Exactly one of `mnemonic` or `secretKey` must be provided.\n * `connector` and `connectorUrl` are mutually exclusive — provide at most one.\n *\n * - When neither is provided, a standalone embedded `ConnectorNode` is built\n * with only a self-route (no upstream peers).\n * - When `connectorUrl` is set, the embedded connector is configured with\n * that URL as a parent BTP peer plus a default-route to it. `ilpAddress`\n * becomes REQUIRED in this mode and must fall under the parent's prefix\n * (e.g. `g.townhouse.<self>`).\n * - When `connector` is set, the caller-supplied `EmbeddableConnectorLike`\n * is used as-is; town does not configure peers, routes, or settlement on it.\n */\nexport interface RelayConfig {\n // --- Identity (exactly one required) ---\n\n /** 12-word or 24-word BIP-39 mnemonic phrase. */\n mnemonic?: string;\n /** 32-byte secp256k1 secret key. */\n secretKey?: Uint8Array;\n\n // --- Connector ---\n\n /**\n * Pre-built embedded connector. Mutually exclusive with `connectorUrl`.\n * When provided, town does not modify the connector — peers, routes, and\n * settlement are the caller's responsibility.\n */\n connector?: EmbeddableConnectorLike;\n /**\n * Pre-built EventStore. When provided, town uses it instead of constructing\n * the default file-backed `SqliteEventStore` under `dataDir`. Useful for\n * tests (inject an `InMemoryEventStore`) and for embedding the relay with a\n * shared store. The caller owns its lifecycle when supplied.\n */\n eventStore?: EventStore;\n /**\n * Parent connector BTP URL (e.g. `ws://apex.example:3001`). When set, the\n * embedded connector is built with this URL as a parent peer and a default\n * `g.` route to that peer; `ilpAddress` MUST also be set and fall under the\n * parent's prefix. Mutually exclusive with `connector`.\n */\n connectorUrl?: string;\n /** BTP peer id to use for the parent connector (default: `'apex'`). */\n parentPeerId?: string;\n /** BTP auth token for the parent peer (default: empty string -- no-auth). */\n parentAuthToken?: string;\n /** Stable nodeId for the embedded connector (default: `toon-<pubkeyShort>`). */\n nodeId?: string;\n\n /**\n * Run as a payment-oblivious relay (default `false`). When `true`, the relay\n * runs as a payment-oblivious app behind an external terminator: no embedded\n * connector is created and no x402/EIP-3009/ILP-settlement code runs. The\n * node exposes `POST /write` (event-as-JSON), trusting injected\n * `X-TOON-Payer`/`X-TOON-Amount`/`X-TOON-Chain` headers without re-validating\n * payment. Free NIP-01 WS reads are unchanged. Mutually exclusive with\n * `connector`/`connectorUrl`. Embedded modes remain the default and unchanged\n * when this is `false`. Overridable via the `TOON_OBLIVIOUS_MODE` env var.\n */\n obliviousMode?: boolean;\n\n /** BTP server port for the embedded connector (default: 3000). */\n btpServerPort?: number;\n\n /**\n * EVM private key for settlement infrastructure on the embedded connector.\n * If not set, the identity's secp256k1 key is reused.\n */\n settlementPrivateKey?: string;\n\n /**\n * EVM treasury address advertised to the parent connector for the\n * embedded-with-parent peer entry. The apex's PerPacketClaimService uses\n * this as the `peerAddress` when the apex opens a payment channel toward\n * this child. Only meaningful when `connectorUrl` is set. When omitted,\n * the parent peer entry has no `evmAddress` and the apex's channel-open\n * call must supply `peerAddress` explicitly.\n */\n parentEvmAddress?: string;\n\n // --- Network ---\n\n /** WebSocket relay port (default: 7100). */\n relayPort?: number;\n /** BLS HTTP server port (default: 3100). */\n blsPort?: number;\n /**\n * ILP address for this node. Default `g.toon.<pubkeyShort>` is used only\n * when no parent connector is configured. When `connectorUrl` is set this\n * field is REQUIRED and must fall under the parent's address prefix.\n */\n ilpAddress?: string;\n /** BTP WebSocket endpoint (default: ws://localhost:3000). */\n btpEndpoint?: string;\n\n // --- Pricing ---\n\n /** Base price per byte in ILP units (default: 10n). */\n basePricePerByte?: bigint;\n /** Routing buffer percentage for x402 multi-hop overhead (default: 10). */\n routingBufferPercent?: number;\n\n // --- x402 ---\n\n /** Enable x402 /publish endpoint (default: false). */\n x402Enabled?: boolean;\n /** Facilitator EVM address for x402 payments. Defaults to the node's EVM address. */\n facilitatorAddress?: string;\n\n // --- Peers ---\n\n /** Known peers to bootstrap with. */\n knownPeers?: { pubkey: string; relayUrl: string; btpEndpoint: string }[];\n\n // --- Chain / Settlement ---\n\n /** Chain preset name (default: 'anvil'). See resolveChainConfig(). */\n chain?: string;\n /** Chain ID -> RPC URL mapping (e.g., { 'evm:base:31337': 'http://localhost:8545' }). */\n chainRpcUrls?: Record<string, string>;\n /** Chain ID -> TokenNetwork contract address. */\n tokenNetworks?: Record<string, string>;\n /** Chain ID -> preferred token address. */\n preferredTokens?: Record<string, string>;\n /**\n * Chain ID -> settlement (recipient) address advertised in kind:10032.\n *\n * By default every supported chain advertises the identity's EVM address.\n * That is wrong for non-EVM chains (e.g. `solana:devnet`), whose settlement\n * recipient must be a chain-native address (a base58 Solana pubkey). Provide\n * a per-chain override here to advertise a chain-native recipient; chains\n * absent from this map keep the EVM-address default.\n *\n * NOTE (Phase-2 Stage 2 gate): advertising a Solana recipient is necessary\n * but NOT sufficient for a settleable Solana loop — the client must also open\n * a real on-chain Solana payment-channel PDA and sign over that PDA. See the\n * Stage-2 PR description / gate report.\n */\n settlementAddresses?: Record<string, string>;\n\n // --- Storage ---\n\n /** Data directory path (default: ./data). */\n dataDir?: string;\n\n // --- Development ---\n\n /** Enable dev mode (skip verification). Default: false. */\n devMode?: boolean;\n\n // --- Discovery ---\n\n /** Discovery mode: 'seed-list' for production, 'genesis' for dev (default: 'genesis'). */\n discovery?: 'seed-list' | 'genesis';\n /** Public Nostr relay URLs for seed relay discovery (used when discovery: 'seed-list'). */\n seedRelays?: string[];\n /** Whether to publish this node as a seed relay entry (default: false). */\n publishSeedEntry?: boolean;\n /** External WebSocket URL of this relay (required if publishSeedEntry is true). */\n externalRelayUrl?: string;\n\n // --- Transport Privacy ---\n\n /**\n * Ator hidden service configuration for the relay.\n *\n * When enabled, the relay binds to localhost only (ator handles inbound routing)\n * and publishes the `.anon` address in seed relay discovery events.\n *\n * - `enabled: false` (default): Relay binds to `0.0.0.0`, no privacy overlay.\n * - `enabled: true`: Relay binds to `127.0.0.1`, publishes `anonAddress` for discovery.\n */\n ator?: {\n enabled: boolean;\n /** The `.anon` hidden service address for this relay (e.g., \"wss://abc123.anon:443\"). */\n anonAddress?: string;\n /** SOCKS5 proxy URL for outbound connections (default: \"socks5h://127.0.0.1:9050\"). */\n socksProxy?: string;\n };\n\n // --- DVM ---\n\n /**\n * Optional DVM skill descriptor to include in service discovery events.\n * When provided, the service discovery event will include the `skill` field.\n * Typically computed by `node.getSkillDescriptor()` from the SDK.\n */\n skill?: SkillDescriptor;\n\n // --- Advanced ---\n\n /** Enable ArDrive peer lookup (default: false). */\n ardriveEnabled?: boolean;\n /** Public Nostr relay URLs for social discovery. */\n relayUrls?: string[];\n /** Asset code for ILP (default: 'USD'). */\n assetCode?: string;\n /** Asset scale for ILP (default: 6). */\n assetScale?: number;\n\n // --- Fee Override ---\n\n /**\n * Fee per event in ILP units (overrides basePricePerByte when set).\n * When provided, sets basePricePerByte to this value. Used by the\n * Townhouse orchestrator via TOON_FEE_PER_EVENT env var.\n */\n feePerEvent?: number;\n\n /**\n * NIP-40 time-to-live for this node's kind:10032 announcement, in seconds\n * (default 3600). The node re-publishes its announcement at half this\n * interval so a live apex stays fresh while an offline one expires, letting\n * clients skip its unreachable BTP endpoint (issue #261). Set to 0 to disable\n * the expiration tag and the heartbeat (non-expiring announcement). Override\n * via the `TOON_ANNOUNCEMENT_TTL_SECONDS` env var.\n */\n announcementTtlSeconds?: number;\n}\n\n/**\n * Resolved configuration with all defaults applied. All fields are non-optional\n * (ports, pricing, paths have been filled in).\n */\nexport interface ResolvedRelayConfig {\n relayPort: number;\n blsPort: number;\n ilpAddress: string;\n btpEndpoint: string;\n /** Stable nodeId of the embedded connector. */\n nodeId: string;\n /** Parent connector URL when peering with one (omitted otherwise). */\n connectorUrl?: string;\n /** Parent BTP peer id (only meaningful when connectorUrl is set). */\n parentPeerId?: string;\n basePricePerByte: bigint;\n routingBufferPercent: number;\n x402Enabled: boolean;\n knownPeers: { pubkey: string; relayUrl: string; btpEndpoint: string }[];\n dataDir: string;\n devMode: boolean;\n ardriveEnabled: boolean;\n relayUrls: string[];\n assetCode: string;\n assetScale: number;\n /** Discovery mode: 'seed-list' for production, 'genesis' for dev. */\n discovery: 'seed-list' | 'genesis';\n /** Public Nostr relay URLs for seed relay discovery. */\n seedRelays: string[];\n /** Whether to publish this node as a seed relay entry. */\n publishSeedEntry: boolean;\n /** External WebSocket URL of this relay (for seed entry publishing). */\n externalRelayUrl?: string;\n /** Chain preset name (e.g., 'anvil', 'arbitrum-one'). */\n chain: string;\n /** Whether the relay is running in payment-oblivious mode (no connector). */\n obliviousMode: boolean;\n}\n\n/**\n * A running TOON relay node instance returned by `startRelay()`.\n *\n * Provides lifecycle control (stop), identity info, and bootstrap results.\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 the\n * Town'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 town 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 node's EVM address (0x-prefixed). */\n evmAddress: string;\n\n /** The resolved configuration with all defaults applied. */\n config: ResolvedRelayConfig;\n\n /** Bootstrap results from the startup phase. */\n bootstrapResult: {\n peerCount: number;\n channelCount: number;\n };\n\n /** Discovery mode used by this instance. */\n discoveryMode: 'seed-list' | 'genesis';\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// ---------- 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 (consistency with BTP URL\n // validation convention in project-context.md).\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 // Track last-seen timestamp for future reconnection with `since:` filter.\n // Currently unused -- SimplePool handles reconnection internally.\n // eslint-disable-next-line prefer-const -- will be reassigned in future story\n let _lastSeenTimestamp = 0;\n void _lastSeenTimestamp;\n\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 * Composes the full SDK pipeline (identity, verification, pricing, handlers)\n * and starts the relay WebSocket server, BLS HTTP server, bootstrap service,\n * and relay monitor. Returns a `RelayInstance` for lifecycle management.\n *\n * The town node ALWAYS runs an embedded `ConnectorNode`. Three configurations\n * are supported:\n * - No connector args: standalone embedded connector with self-route only.\n * - `connectorUrl`: embedded connector configured with that URL as a parent\n * BTP peer plus a default `g.` route to it. `ilpAddress` is REQUIRED here.\n * - `connector`: pass a pre-built `EmbeddableConnectorLike`; town does not\n * modify it.\n *\n * @param config - Node configuration. One of `mnemonic`/`secretKey` is required;\n * `connector` and `connectorUrl` are mutually exclusive.\n * @returns A running RelayInstance.\n * @throws If both or neither of mnemonic/secretKey are provided.\n * @throws If both connector and connectorUrl are provided.\n * @throws If connectorUrl is set without an explicit ilpAddress.\n *\n * @example\n * ```typescript\n * // Standalone (no parent)\n * const town = await startRelay({ mnemonic: 'abandon ...' });\n *\n * // Embedded with parent\n * const town = await startRelay({\n * mnemonic: 'abandon ...',\n * connectorUrl: 'ws://apex.example:3001',\n * parentPeerId: 'apex',\n * parentAuthToken: '',\n * ilpAddress: 'g.townhouse.alice',\n * });\n * ```\n */\nexport async function startRelay(config: RelayConfig): Promise<RelayInstance> {\n // --- 1. Validate identity ---\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 // --- 1b. Validate connector mode ---\n const hasConnector = config.connector !== undefined;\n const hasConnectorUrl = config.connectorUrl !== undefined;\n\n if (hasConnector && hasConnectorUrl) {\n throw new Error(\n 'RelayConfig: provide either connector or connectorUrl, not both'\n );\n }\n\n // Oblivious mode: payment-oblivious relay behind an external terminator. No\n // embedded connector is created, so it is mutually exclusive with the\n // connector/connectorUrl embedded modes. Env wins only when neither is set.\n const obliviousMode =\n config.obliviousMode ?? process.env['TOON_OBLIVIOUS_MODE'] === 'true';\n\n if (obliviousMode && (hasConnector || hasConnectorUrl)) {\n throw new Error(\n 'RelayConfig: obliviousMode is mutually exclusive with connector/connectorUrl ' +\n '(an oblivious relay runs no embedded connector)'\n );\n }\n\n // When peering with a parent, the operator MUST set ilpAddress so it falls\n // under the parent's prefix (e.g. g.townhouse.<self>). The default\n // g.toon.<pubkey> address would not be routable from the parent.\n if (hasConnectorUrl && config.ilpAddress === undefined) {\n throw new Error(\n 'RelayConfig: ilpAddress is required when connectorUrl is set ' +\n '(must fall under the parent connector prefix, e.g. g.townhouse.<self>)'\n );\n }\n\n // --- 2. Derive identity ---\n const identity: NodeIdentity = hasMnemonic\n ? fromMnemonic(config.mnemonic as string)\n : fromSecretKey(config.secretKey as Uint8Array);\n\n // --- 3. Resolve config with defaults ---\n const relayPort = config.relayPort ?? 7100;\n const blsPort = config.blsPort ?? 3100;\n const pubkeyShort = identity.pubkey.slice(0, 16);\n const ilpAddress = config.ilpAddress ?? `g.toon.${pubkeyShort}`;\n // When no public BTP endpoint is configured (operator hasn't set one, or the\n // apex .anyone hostname isn't resolved yet), advertise an EMPTY btpEndpoint\n // rather than a loopback URL. A loopback default (`ws://localhost:3000`) leaks\n // into a network-visible kind:10032 and is unreachable from outside the Docker\n // network — clients that faithfully dial it fail (issue #259). An empty value\n // is rejected gracefully by client discovery instead of misdirecting it.\n const btpEndpoint = config.btpEndpoint ?? '';\n const nodeId = config.nodeId ?? `toon-${pubkeyShort}`;\n const parentPeerId = config.parentPeerId ?? 'apex';\n const parentAuthToken = config.parentAuthToken ?? '';\n const connectorUrl = config.connectorUrl;\n const basePricePerByte =\n config.feePerEvent !== undefined\n ? BigInt(config.feePerEvent)\n : (config.basePricePerByte ?? 10n);\n const routingBufferPercent = config.routingBufferPercent ?? 10;\n // x402 settles payments on-chain inside this process. In oblivious mode the\n // process is payment-oblivious (an external terminator gates writes), so\n // force x402 off regardless of what the caller passed.\n const x402Enabled = obliviousMode ? false : (config.x402Enabled ?? false);\n const knownPeers = [...(config.knownPeers ?? [])];\n const dataDir = config.dataDir ?? './data';\n const devMode = config.devMode ?? false;\n const ardriveEnabled = config.ardriveEnabled ?? false;\n const relayUrls = config.relayUrls ?? [`ws://localhost:${relayPort}`];\n const assetCode = config.assetCode ?? 'USD';\n const assetScale = config.assetScale ?? 6;\n const discovery = config.discovery ?? 'genesis';\n // NIP-40 TTL for the kind:10032 announcement (issue #261). Env wins over the\n // config field; default 1h. A non-finite/negative value falls back to the\n // default, and 0 disables expiration + the heartbeat (non-expiring event).\n const announcementTtlSeconds = (() => {\n const fromEnv = process.env['TOON_ANNOUNCEMENT_TTL_SECONDS'];\n const raw =\n fromEnv !== undefined && fromEnv !== ''\n ? Number(fromEnv)\n : config.announcementTtlSeconds;\n if (raw === undefined) return 3600;\n if (!Number.isFinite(raw) || raw < 0) return 3600;\n return Math.floor(raw);\n })();\n const seedRelays = config.seedRelays ?? [];\n const publishSeedEntryFlag = config.publishSeedEntry ?? false;\n // Use ator .anon address as externalRelayUrl when ator is enabled and no explicit URL set\n const externalRelayUrl =\n config.externalRelayUrl ??\n (config.ator?.enabled && config.ator.anonAddress\n ? config.ator.anonAddress\n : undefined);\n\n // --- 3b. Resolve chain preset early (needed for resolvedConfig and settlement) ---\n // Relay-only sentinel: when the operator (or the Townhouse network resolver in\n // `custom` mode with no EVM provider) sets the chain to `'none'`, the node runs\n // as a pure relay — no settlement chain is resolved, so no ethers provider is\n // ever constructed and the node connects straight to its parent connector.\n // `resolveChainConfig` reads `TOON_CHAIN` itself (env wins over the parameter),\n // so we mirror that precedence here to detect the sentinel before it throws on\n // an unknown chain name.\n // Oblivious mode forces relay-only: no settlement chain, no provider, no\n // channels — the process never touches the payment layer. The `'none'`\n // sentinel (or oblivious mode) selects the relay-only branch below.\n const requestedChain = process.env['TOON_CHAIN'] || config.chain;\n const relayOnly = requestedChain === 'none' || obliviousMode;\n if (relayOnly) {\n console.log('[Town] connector.relay_only', {\n reason: 'no settlement chain configured (chain=none)',\n });\n }\n const chainConfig = relayOnly\n ? {\n name: 'none',\n chainId: 0,\n rpcUrl: '',\n usdcAddress: '',\n tokenNetworkAddress: '',\n registryAddress: '',\n }\n : resolveChainConfig(config.chain);\n const chainKey = `evm:base:${chainConfig.chainId}`;\n\n const resolvedConfig: ResolvedRelayConfig = {\n relayPort,\n blsPort,\n ilpAddress,\n btpEndpoint,\n nodeId,\n ...(connectorUrl && { connectorUrl, parentPeerId }),\n basePricePerByte,\n routingBufferPercent,\n x402Enabled,\n knownPeers,\n dataDir,\n devMode,\n ardriveEnabled,\n relayUrls,\n assetCode,\n assetScale,\n discovery,\n seedRelays,\n publishSeedEntry: publishSeedEntryFlag,\n ...(externalRelayUrl && { externalRelayUrl }),\n chain: chainConfig.name,\n obliviousMode,\n };\n\n // --- 3c. Auto-create embedded connector when no pre-built one was supplied ---\n // Skipped entirely in oblivious mode: a payment-oblivious relay runs no\n // embedded connector (an external terminator gates writes).\n let autoCreatedConnector: ConnectorNode | null = null;\n if (!hasConnector && !obliviousMode) {\n const btpServerPort = config.btpServerPort ?? 3000;\n const connectorLogger = createConnectorLogger(\n nodeId,\n (process.env['TOON_CONNECTOR_LOG_LEVEL'] as\n | 'debug'\n | 'info'\n | 'warn'\n | 'error'\n | undefined) ?? 'warn'\n );\n\n // Routes: always self-route for local delivery; add a parent default route\n // when peering. Local delivery is triggered by `nextHop === nodeId` (or\n // the literal 'local'); the connector's packet-handler.ts then auto-skips\n // settlement fees for local hops.\n const routes: {\n prefix: string;\n nextHop: string;\n priority?: number;\n }[] = [{ prefix: ilpAddress, nextHop: nodeId, priority: 100 }];\n\n // Peers: only the parent, when configured.\n const peers: {\n id: string;\n url: string;\n authToken: string;\n relation?: 'parent' | 'peer' | 'child';\n evmAddress?: string;\n }[] = [];\n\n if (hasConnectorUrl) {\n peers.push({\n id: parentPeerId,\n url: connectorUrl as string,\n authToken: parentAuthToken,\n // Tag the upstream as our PARENT so the embedded connector's\n // relation-aware logic applies (toon-protocol/connector#78): a child\n // skips the inbound per-packet-claim requirement for PREPAREs forwarded\n // by its parent (the parent settles in aggregate and attaches no\n // per-packet claim to a child). Without this the peer defaults to\n // 'peer' and the child F06-rejects every parent-forwarded paid packet.\n // NOTE: `parentPeerId` MUST equal the parent connector's nodeId (its BTP\n // auth identity), since the connector keys peerRelations by the\n // auth-declared peerId of the inbound session — not a local alias.\n relation: 'parent',\n // When the operator publishes their EVM treasury address to the\n // parent, the apex can open a settlement channel toward this child\n // without needing to discover the address via kind:10032. The\n // connector schema treats this field as optional metadata.\n ...(config.parentEvmAddress && { evmAddress: config.parentEvmAddress }),\n });\n // Connector's isValidILPAddress rejects trailing dots; RoutingTable\n // adds the delimiter at match time, so 'g' matches 'g.foo' correctly.\n routes.push({ prefix: 'g', nextHop: parentPeerId, priority: 0 });\n }\n\n // chainProviders entry — wires the embedded ConnectorNode's ClaimReceiver\n // so it can verify per-packet claims signed by the apex (image >=3.4.0).\n // Same secp256k1 key derives Nostr identity AND EVM treasury account.\n // We only build the entry when the chain preset has all settlement\n // addresses populated; presets like arbitrum-one have empty registry/\n // tokenNetwork strings, in which case we degrade gracefully (no warn-fail).\n const hasSettlementAddresses =\n !!chainConfig.rpcUrl &&\n !!chainConfig.registryAddress &&\n !!chainConfig.tokenNetworkAddress &&\n !!chainConfig.usdcAddress;\n\n let chainProvidersEntry: {\n chainType: 'evm';\n chainId: string;\n rpcUrl: string;\n registryAddress: string;\n tokenAddress: string;\n keyId: string;\n } | null = null;\n if (hasSettlementAddresses) {\n // chainId here is the connector's `evm:<numeric>` form, NOT the\n // chainKey ('evm:base:<numeric>') used for settlement maps.\n // When operator supplies `settlementPrivateKey`, prefer it over the\n // identity-derived hex — this lets the embedded connector's\n // ClaimReceiver use a funded EVM account (e.g. Anvil deterministic\n // privkey) distinct from the Nostr identity.\n const keyHex =\n config.settlementPrivateKey ??\n `0x${Buffer.from(identity.secretKey).toString('hex')}`;\n if (!/^0x[0-9a-fA-F]{64}$/.test(keyHex)) {\n throw new Error(\n `RelayConfig.settlementPrivateKey must be a 0x-prefixed 32-byte hex string (got length ${keyHex.length}); cannot wire chainProviders for ${chainConfig.name}`\n );\n }\n chainProvidersEntry = {\n chainType: 'evm',\n chainId: `evm:${chainConfig.chainId}`,\n rpcUrl: chainConfig.rpcUrl,\n registryAddress: chainConfig.registryAddress,\n tokenAddress: chainConfig.usdcAddress,\n keyId: keyHex,\n };\n } else {\n console.warn('[Town] connector.chain_providers_skipped', {\n chain: chainConfig.name,\n reason: 'missing settlement addresses',\n });\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const connectorConfig: any = {\n nodeId,\n btpServerPort,\n environment: 'development' as const,\n deploymentMode: 'embedded' as const,\n peers,\n routes,\n localDelivery: { enabled: false },\n // Children don't expose an admin API — the apex parent is the\n // operator-facing surface. Disabling avoids a hard runtime dep on\n // express in the town docker bundle.\n adminApi: { enabled: false },\n // Belt-and-braces: zero connector forwarding fee. Combined with the\n // packet-handler's automatic skip for local-delivery hops this keeps the\n // child fee surface flat regardless of peering topology.\n settlement: {\n connectorFeePercentage: 0,\n } as unknown as NonNullable<ConnectorConfig['settlement']>,\n ...(chainProvidersEntry && { chainProviders: [chainProvidersEntry] }),\n };\n // Ator/SOCKS5 transport propagation also covers the parent dial when\n // running inside a hidden-service deployment.\n if (config.ator?.enabled && config.ator.anonAddress) {\n connectorConfig.transport = {\n type: 'socks5',\n socksProxy: config.ator.socksProxy ?? 'socks5h://127.0.0.1:9050',\n externalUrl: config.ator.anonAddress,\n managed: false,\n };\n }\n autoCreatedConnector = new ConnectorNode(connectorConfig, connectorLogger);\n }\n\n // Effective connector: user-provided or auto-created. Null in oblivious mode\n // (no embedded connector); connector-dependent builders below are skipped.\n const effectiveConnector: EmbeddableConnectorLike | null =\n config.connector ??\n (autoCreatedConnector as unknown as EmbeddableConnectorLike | null);\n\n // --- 4. Create data directory ---\n mkdirSync(dataDir, { recursive: true });\n\n // --- 5. EventStore ---\n // Caller-supplied store wins (tests/embedding); otherwise default to the\n // file-backed SqliteEventStore under dataDir.\n const dbPath = join(dataDir, 'events.db');\n const eventStore: EventStore =\n config.eventStore ?? new SqliteEventStore(dbPath);\n\n // --- 5b. Auto-populate settlement defaults from chain preset ---\n\n // Auto-populate settlement fields from chain preset when not explicitly set.\n // Explicit config values always win over chain preset defaults.\n const effectiveChainRpcUrls =\n config.chainRpcUrls ??\n (relayOnly ? undefined : { [chainKey]: chainConfig.rpcUrl });\n const effectivePreferredTokens =\n config.preferredTokens ??\n (relayOnly ? undefined : { [chainKey]: chainConfig.usdcAddress });\n const effectiveTokenNetworks =\n config.tokenNetworks ??\n (chainConfig.tokenNetworkAddress\n ? { [chainKey]: chainConfig.tokenNetworkAddress }\n : undefined);\n\n // --- 6. Settlement configuration ---\n let channelClient: ConnectorChannelClient | undefined;\n let settlementInfo: SettlementConfig | undefined;\n\n const hasSettlement =\n effectiveChainRpcUrls ||\n effectiveTokenNetworks ||\n effectivePreferredTokens ||\n config.settlementAddresses;\n\n if (hasSettlement) {\n const supportedChains = Array.from(\n new Set([\n ...Object.keys(effectiveChainRpcUrls ?? {}),\n ...Object.keys(effectiveTokenNetworks ?? {}),\n ...Object.keys(effectivePreferredTokens ?? {}),\n ...Object.keys(config.settlementAddresses ?? {}),\n ])\n );\n\n // Build settlement addresses. Each chain defaults to the identity's EVM\n // address, but a per-chain override (config.settlementAddresses) wins so\n // non-EVM chains can advertise a chain-native recipient (e.g. the apex's\n // base58 Solana pubkey for `solana:devnet`).\n const settlementAddresses: Record<string, string> = {};\n for (const chain of supportedChains) {\n settlementAddresses[chain] =\n config.settlementAddresses?.[chain] ?? identity.evmAddress;\n }\n\n settlementInfo = {\n supportedChains,\n settlementAddresses,\n preferredTokens: effectivePreferredTokens,\n tokenNetworks: effectiveTokenNetworks,\n };\n\n if (\n effectiveConnector?.openChannel &&\n effectiveConnector.getChannelState\n ) {\n channelClient = createDirectChannelClient(\n effectiveConnector as Required<\n Pick<EmbeddableConnectorLike, 'openChannel' | 'getChannelState'>\n >\n );\n }\n }\n\n // --- 7. Connector admin client ---\n // Skipped in oblivious mode (no connector to administer).\n const adminClient: ConnectorAdminClient | undefined = effectiveConnector\n ? createDirectConnectorAdmin(effectiveConnector)\n : undefined;\n\n // --- 8. SDK Pipeline ---\n const verifier = createVerificationPipeline({ devMode });\n\n const pricer = createPricingValidator({\n basePricePerByte,\n ownPubkey: identity.pubkey,\n });\n\n const registry = new HandlerRegistry();\n registry.onDefault(createEventStorageHandler({ eventStore }));\n\n const toonDecoder = (toon: string) => {\n const bytes = Buffer.from(toon, 'base64');\n return decodeEventFromToon(bytes);\n };\n\n const handlePacket = async (\n request: HandlePacketRequest\n ): Promise<HandlePacketAcceptResponse | HandlePacketRejectResponse> => {\n // Stage 1: Size check\n if (request.data.length > MAX_PAYLOAD_BASE64_LENGTH) {\n return { accept: false, code: 'F08', message: 'Payload too large' };\n }\n\n // Stage 2: Shallow TOON parse\n const toonBytes = Buffer.from(request.data, 'base64');\n let meta;\n try {\n meta = shallowParseToon(toonBytes);\n } catch {\n return { accept: false, code: 'F06', message: 'Invalid TOON payload' };\n }\n\n // Stage 3: Schnorr verification\n const verifyResult = await verifier.verify(meta, request.data);\n if (!verifyResult.verified) {\n if (verifyResult.rejection) {\n return verifyResult.rejection;\n }\n return { accept: false, code: 'F06', message: 'Verification failed' };\n }\n\n // Stage 4: Pricing validation\n let amount: bigint;\n try {\n amount = BigInt(request.amount);\n } catch {\n return {\n accept: false,\n code: 'T00',\n message: 'Invalid payment amount',\n };\n }\n const priceResult = pricer.validate(meta, amount);\n if (!priceResult.accepted) {\n if (priceResult.rejection) {\n return priceResult.rejection;\n }\n return {\n accept: false,\n code: 'F04',\n message: 'Pricing validation failed',\n };\n }\n\n // Stage 5: Handler dispatch\n const ctx = createHandlerContext({\n toon: request.data,\n meta,\n amount,\n destination: request.destination,\n toonDecoder,\n });\n\n try {\n const result = await registry.dispatch(ctx);\n // Broadcast stored events to WebSocket subscribers so the live event feed\n // in the Townhouse dashboard reflects newly accepted events in real time.\n if (result.accept) {\n try {\n const event = decodeEventFromToon(toonBytes);\n wsRelayRef.current?.broadcastEvent(event);\n } catch {\n // Non-Nostr payloads (e.g. kind:10032 ILP info) may fail decode — ignore.\n }\n }\n return result;\n } catch (err: unknown) {\n const errMsg = err instanceof Error ? err.message : 'Unknown error';\n console.error('[Town] Handler dispatch failed:', errMsg);\n return { accept: false, code: 'T00', message: 'Internal error' };\n }\n };\n\n // --- 9. Bootstrap service setup ---\n const bootstrapService = new BootstrapService(\n {\n knownPeers,\n ardriveEnabled,\n defaultRelayUrl: `ws://localhost:${relayPort}`,\n ...(settlementInfo && { settlementInfo }),\n ownIlpAddress: ilpAddress,\n toonEncoder: encodeEventToToon,\n toonDecoder: decodeEventFromToon,\n basePricePerByte,\n },\n identity.secretKey,\n {\n ilpAddress,\n btpEndpoint,\n assetCode,\n assetScale,\n }\n );\n\n let peerCount = 0;\n let channelCount = 0;\n // discoveryTracker is created after the embedded connector starts; this ref\n // lets the health handler respond safely before initialization completes\n // (returns 0 counts until ready).\n const discoveryTrackerRef: {\n current?: ReturnType<typeof createDiscoveryTracker>;\n } = {};\n\n // wsRelay is created in step 11 (after BLS server). This ref lets handlePacket\n // broadcast newly stored events to WebSocket subscribers without a forward reference.\n const wsRelayRef: { current?: NostrRelayServer } = {};\n\n // --- 10. BLS HTTP Server ---\n const app = new Hono();\n app.get('/health', (c: Context) => {\n const bootstrapPhase = bootstrapService.getPhase();\n const dt = discoveryTrackerRef.current;\n return c.json(\n createHealthResponse({\n phase: bootstrapPhase,\n pubkey: identity.pubkey,\n ilpAddress,\n peerCount: (dt ? dt.getPeerCount() : 0) + peerCount,\n discoveredPeerCount: dt ? dt.getDiscoveredCount() : 0,\n channelCount,\n basePricePerByte,\n x402Enabled,\n chain: chainConfig.name,\n })\n );\n });\n\n // The ILP localDelivery write surface is mounted ONLY in embedded mode. In\n // oblivious mode the relay exposes POST /write instead (see below), and\n // /handle-packet is left unmounted (404).\n if (!obliviousMode) {\n app.post('/handle-packet', async (c: Context) => {\n try {\n const body = (await c.req.json()) as HandlePacketRequest;\n if (\n body.amount === undefined ||\n body.amount === null ||\n body.destination === undefined ||\n body.destination === null ||\n body.data === undefined ||\n body.data === null\n ) {\n return c.json(\n { accept: false, code: 'F00', message: 'Missing required fields' },\n 400\n );\n }\n const result = await handlePacket(body);\n // Feed accepted kind:10032 events to discovery tracker for peer discovery\n if (result.accept) {\n try {\n const toonBytes = Buffer.from(body.data, 'base64');\n const decoded = decodeEventFromToon(toonBytes);\n if (decoded && decoded.kind === ILP_PEER_INFO_KIND) {\n discoveryTrackerRef.current?.processEvent(decoded);\n }\n } catch {\n /* decode failed, ignore */\n }\n }\n return c.json(result, result.accept ? 200 : 400);\n } catch (error: unknown) {\n // Log the full error server-side for debugging, but return a generic\n // message to the caller to avoid leaking internal details (CWE-209).\n console.error('[Town] handle-packet error:', error);\n return c.json(\n { accept: false, code: 'T00', message: 'Internal server error' },\n 500\n );\n }\n });\n }\n\n // --- 10b. ILP client (created before x402 handler so it can be wired in) ---\n // Skipped in oblivious mode (no connector to send ILP packets through).\n const ilpClient: IlpClient | undefined = effectiveConnector\n ? createDirectIlpClient(effectiveConnector, {\n toonDecoder: (bytes: Uint8Array) => decodeEventFromToon(bytes),\n })\n : undefined;\n\n // --- 10c. viem clients for x402 settlement (conditional) ---\n let x402WalletClient: WalletClient | undefined;\n let x402PublicClient: PublicClient | undefined;\n\n if (x402Enabled) {\n // Derive EVM private key from node identity (same secp256k1 key)\n // Best-effort zeroing of intermediate Buffer; hex string is immutable\n // and cannot be zeroed (JS limitation, same as fromMnemonic pattern).\n let keyBuffer: Buffer | undefined;\n try {\n // Buffer.from(TypedArray) copies the data — identity.secretKey is not aliased.\n keyBuffer = Buffer.from(identity.secretKey);\n const privateKeyHex = `0x${keyBuffer.toString('hex')}` as `0x${string}`;\n const account = privateKeyToAccount(privateKeyHex);\n const viemChain = defineChain({\n id: chainConfig.chainId,\n name: chainConfig.name,\n nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },\n rpcUrls: { default: { http: [] } },\n });\n\n x402PublicClient = createPublicClient({\n chain: viemChain,\n transport: http(chainConfig.rpcUrl),\n });\n x402WalletClient = createWalletClient({\n account,\n chain: viemChain,\n transport: http(chainConfig.rpcUrl),\n });\n } catch (error: unknown) {\n throw new Error(\n `x402 initialization failed: could not derive EVM account from identity key: ${error instanceof Error ? error.message : String(error)}`\n );\n } finally {\n if (keyBuffer) {\n keyBuffer.fill(0);\n }\n }\n }\n\n // --- 10d. Write surface routing ---\n // Embedded mode (default): mount the x402 /publish route as before. Oblivious\n // mode: mount POST /write (plain-HTTP, payment-oblivious) and skip /publish.\n if (obliviousMode) {\n // The oblivious write handler stores the event, then mirrors the SAME\n // post-store side effects used by the embedded handlePacket closure:\n // 1. broadcast to WS subscribers so live readers see new events, and\n // 2. feed accepted kind:10032 events to the discovery tracker.\n const obliviousHandler = createObliviousWriteHandler({\n eventStore,\n devMode,\n onStored: (event) => {\n // Mirror handlePacket's WS broadcast (town.ts step 8).\n try {\n wsRelayRef.current?.broadcastEvent(event);\n } catch {\n // Non-broadcastable payloads — ignore (matches embedded behavior).\n }\n // Mirror the discovery-tracker feed for kind:10032 (ILP peer info).\n if (event.kind === ILP_PEER_INFO_KIND) {\n discoveryTrackerRef.current?.processEvent(event);\n }\n },\n });\n app.post('/write', (c: Context) => obliviousHandler.handleWrite(c));\n } else {\n const x402Handler = createX402Handler({\n x402Enabled,\n chainConfig,\n basePricePerByte,\n routingBufferPercent,\n facilitatorAddress: config.facilitatorAddress ?? identity.evmAddress,\n ownPubkey: identity.pubkey,\n devMode,\n eventStore,\n ilpClient,\n walletClient: x402WalletClient,\n publicClient: x402PublicClient,\n });\n\n // Register /publish for both GET and POST methods\n app.get('/publish', (c: Context) => x402Handler.handlePublish(c));\n app.post('/publish', (c: Context) => x402Handler.handlePublish(c));\n }\n\n const blsServer: ServerType = serve({\n fetch: app.fetch,\n port: blsPort,\n });\n\n // --- 11. WebSocket Relay ---\n // When ator is enabled, bind to localhost only (hidden service handles inbound routing)\n const relayHost = config.ator?.enabled ? '127.0.0.1' : undefined;\n const wsRelay = new NostrRelayServer(\n { port: relayPort, host: relayHost },\n eventStore\n );\n wsRelayRef.current = wsRelay;\n await wsRelay.start();\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n // --- 12. Running state ---\n let running = true;\n\n // --- 13. Bootstrap ---\n // Connector-dependent wiring is skipped in oblivious mode (no connector).\n if (adminClient) {\n bootstrapService.setConnectorAdmin(adminClient);\n }\n if (channelClient) {\n bootstrapService.setChannelClient(channelClient);\n }\n\n if (ilpClient) {\n bootstrapService.setIlpClient(ilpClient);\n }\n\n bootstrapService.on((event: BootstrapEvent) => {\n switch (event.type) {\n case 'bootstrap:peer-registered':\n peerCount++;\n break;\n case 'bootstrap:channel-opened':\n channelCount++;\n break;\n case 'bootstrap:ready':\n // Phase update handled automatically\n break;\n }\n });\n\n // Wire the packet handler directly to the embedded connector.\n if (effectiveConnector?.setPacketHandler) {\n effectiveConnector.setPacketHandler(async (request) => {\n const result = await handlePacket(request as HandlePacketRequest);\n // Feed accepted kind:10032 events to discovery tracker\n if (result.accept && discoveryTrackerRef.current) {\n try {\n const toonBytes = Buffer.from(\n (request as HandlePacketRequest).data,\n 'base64'\n );\n const decoded = decodeEventFromToon(toonBytes);\n if (decoded && decoded.kind === ILP_PEER_INFO_KIND) {\n discoveryTrackerRef.current.processEvent(decoded);\n }\n } catch {\n /* decode failed, ignore */\n }\n }\n return result;\n });\n }\n\n // Start the auto-created connector before bootstrap. Pre-built connectors\n // are the caller's responsibility to start.\n if (autoCreatedConnector) {\n await autoCreatedConnector.start();\n }\n\n // Create DiscoveryTracker\n const discoveryTracker = createDiscoveryTracker({\n secretKey: identity.secretKey,\n settlementInfo,\n });\n if (adminClient) {\n discoveryTracker.setConnectorAdmin(adminClient);\n }\n if (channelClient) {\n discoveryTracker.setChannelClient(channelClient);\n }\n // Wire discovery tracker ref (used by health handler and embedded packet handler)\n discoveryTrackerRef.current = discoveryTracker;\n\n // --- 13b. Seed Relay Discovery (when discovery: 'seed-list') ---\n // Runs before bootstrap to populate knownPeers from seed relay list.\n let seedRelayDiscovery: SeedRelayDiscovery | undefined;\n if (discovery === 'seed-list' && seedRelays.length > 0) {\n seedRelayDiscovery = new SeedRelayDiscovery({\n publicRelays: seedRelays,\n });\n\n try {\n const seedResult = await seedRelayDiscovery.discover();\n // Convert discovered peers to KnownPeer[] format and merge with config\n const seedPeers = seedResult.discoveredPeers\n .filter((info) => info.pubkey)\n .map((info) => ({\n pubkey: info.pubkey as string,\n relayUrl:\n seedResult.connectedUrls[0] ?? `ws://localhost:${relayPort}`,\n btpEndpoint: info.btpEndpoint,\n }));\n\n // Merge with existing knownPeers (config peers take priority)\n const existingPubkeys = new Set(knownPeers.map((p) => p.pubkey));\n for (const seedPeer of seedPeers) {\n if (!existingPubkeys.has(seedPeer.pubkey)) {\n knownPeers.push(seedPeer);\n }\n }\n\n console.log(\n `[Town] Seed relay discovery: found ${seedPeers.length} peers from ${seedResult.connectedUrls.length} seed relay(s)`\n );\n } catch (seedError: unknown) {\n const msg =\n seedError instanceof Error ? seedError.message : 'Unknown error';\n console.warn(`[Town] Seed relay discovery failed: ${msg}`);\n // Continue with any knownPeers from config\n }\n }\n\n // Handle for the kind:10032 liveness heartbeat (issue #261); cleared in stop().\n let announcementHeartbeat: ReturnType<typeof setInterval> | undefined;\n\n try {\n const results = await bootstrapService.bootstrap();\n\n // Self-write: publish own kind:10032\n const ownIlpInfo: IlpPeerInfo = {\n ilpAddress,\n btpEndpoint,\n assetCode,\n assetScale,\n // Advertise the publish price (per byte, in ILP base units) so clients can\n // compute the amount to attach before sending — derived from feePerEvent /\n // basePricePerByte. Previously omitted, leaving peers to assume free.\n feePerByte: String(basePricePerByte),\n // Public Nostr relay URL for FREE reads, so clients discover where to\n // subscribe (separate from btpEndpoint, which is the pay-to-write path).\n // Set when the operator exposes the relay publicly (HS .anyone or direct).\n ...(externalRelayUrl && { relayUrl: externalRelayUrl }),\n ...(settlementInfo?.supportedChains && {\n supportedChains: settlementInfo.supportedChains,\n }),\n ...(settlementInfo?.settlementAddresses && {\n settlementAddresses: settlementInfo.settlementAddresses,\n }),\n ...(settlementInfo?.preferredTokens && {\n preferredTokens: settlementInfo.preferredTokens,\n }),\n ...(settlementInfo?.tokenNetworks && {\n tokenNetworks: settlementInfo.tokenNetworks,\n }),\n };\n\n // Build + store + propagate a fresh kind:10032 each time. Re-signing yields\n // a new created_at and NIP-40 expiration window, so a live apex's\n // announcement stays unexpired while an offline one lapses (issue #261).\n const publishOwnAnnouncement = () => {\n try {\n const ilpInfoEvent = buildIlpPeerInfoEvent(\n ownIlpInfo,\n identity.secretKey,\n announcementTtlSeconds > 0\n ? { ttlSeconds: announcementTtlSeconds }\n : {}\n );\n eventStore.store(ilpInfoEvent);\n\n // Publish to genesis relay via ILP if we have bootstrap peers\n const firstPeer = knownPeers[0];\n const genesisResult = results[0];\n if (ilpClient && firstPeer && genesisResult) {\n const genesisIlpAddress = genesisResult.peerInfo.ilpAddress;\n const toonBytes = encodeEventToToon(ilpInfoEvent);\n const base64Toon = Buffer.from(toonBytes).toString('base64');\n const ilpAmount = String(BigInt(toonBytes.length) * basePricePerByte);\n\n ilpClient\n .sendIlpPacket({\n destination: genesisIlpAddress,\n amount: ilpAmount,\n data: base64Toon,\n })\n .catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : 'Unknown';\n console.warn('[Town] Failed to publish via ILP:', msg);\n });\n }\n } catch (error: unknown) {\n console.warn('[Town] Failed to publish ILP info:', error);\n }\n };\n\n publishOwnAnnouncement();\n\n // Liveness heartbeat: re-announce at half the TTL so there is always an\n // unexpired kind:10032 on the relay while we are up. When the node stops,\n // the heartbeat stops and the last announcement expires after the TTL,\n // signalling clients to stop dialing this (now-unreachable) apex (#261).\n if (announcementTtlSeconds > 0) {\n const heartbeatMs = Math.max(\n 1,\n Math.floor((announcementTtlSeconds * 1000) / 2)\n );\n announcementHeartbeat = setInterval(publishOwnAnnouncement, heartbeatMs);\n // Don't let the heartbeat keep the process alive on its own.\n announcementHeartbeat.unref?.();\n }\n\n // Self-write: publish own kind:10035 (Service Discovery)\n try {\n const serviceDiscoveryContent: ServiceDiscoveryContent = {\n serviceType: 'relay',\n ilpAddress,\n pricing: {\n basePricePerByte: Number(basePricePerByte),\n currency: 'USDC',\n },\n supportedKinds: [1, 10032, 10035, 10036],\n capabilities: x402Enabled ? ['relay', 'x402'] : ['relay'],\n chain: chainConfig.name,\n version: VERSION,\n };\n\n // Only include x402 field when enabled (AC #3: omit entirely when disabled)\n if (x402Enabled) {\n serviceDiscoveryContent.x402 = {\n enabled: true,\n endpoint: '/publish',\n };\n }\n\n // Include skill descriptor when DVM capabilities are configured (Story 5.4)\n if (config.skill) {\n serviceDiscoveryContent.skill = config.skill;\n }\n\n const serviceDiscoveryEvent = buildServiceDiscoveryEvent(\n serviceDiscoveryContent,\n identity.secretKey\n );\n eventStore.store(serviceDiscoveryEvent);\n\n // Publish to peers via ILP (fire-and-forget, same pattern as kind:10032)\n const firstPeer = knownPeers[0];\n const genesisResult = results[0];\n if (ilpClient && firstPeer && genesisResult) {\n const genesisIlpAddress = genesisResult.peerInfo.ilpAddress;\n const sdToonBytes = encodeEventToToon(serviceDiscoveryEvent);\n const sdBase64Toon = Buffer.from(sdToonBytes).toString('base64');\n const sdIlpAmount = String(\n BigInt(sdToonBytes.length) * basePricePerByte\n );\n\n ilpClient\n .sendIlpPacket({\n destination: genesisIlpAddress,\n amount: sdIlpAmount,\n data: sdBase64Toon,\n })\n .catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : 'Unknown';\n console.warn(\n '[Town] Failed to publish service discovery via ILP:',\n msg\n );\n });\n }\n } catch (error: unknown) {\n console.warn('[Town] Failed to publish service discovery:', error);\n }\n\n // Exclude already-bootstrapped peers from discovery\n const bootstrapPeerPubkeys = results.map((r) => r.knownPeer.pubkey);\n discoveryTracker.addExcludedPubkeys(bootstrapPeerPubkeys);\n } catch (error: unknown) {\n console.error('[Town] Bootstrap failed:', error);\n }\n\n // --- 13c. Publish seed relay entry (after bootstrap complete) ---\n if (publishSeedEntryFlag && !externalRelayUrl) {\n console.warn(\n '[Town] publishSeedEntry is true but externalRelayUrl is not set -- skipping seed relay entry publication'\n );\n }\n if (publishSeedEntryFlag && externalRelayUrl && seedRelays.length > 0) {\n publishSeedRelayEntry({\n secretKey: identity.secretKey,\n relayUrl: externalRelayUrl,\n publicRelays: seedRelays,\n })\n .then(({ publishedTo, eventId }) => {\n console.log(\n `[Town] Published seed relay entry to ${publishedTo} relay(s), eventId: ${eventId}`\n );\n })\n .catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : 'Unknown error';\n console.warn(`[Town] Failed to publish seed relay entry: ${msg}`);\n });\n }\n\n // Social discovery\n const socialDiscovery = new SocialPeerDiscovery(\n { relayUrls },\n identity.secretKey\n );\n const socialSubscription = socialDiscovery.start();\n\n // --- 14. Outbound subscription tracking ---\n const activeSubscriptions = new Set<RelaySubscription>();\n\n // --- 15. Build RelayInstance ---\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: town is not running');\n }\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 // Stop the kind:10032 liveness heartbeat so the announcement lapses (#261)\n if (announcementHeartbeat) {\n clearInterval(announcementHeartbeat);\n announcementHeartbeat = undefined;\n }\n\n // Close outbound subscriptions first\n for (const sub of activeSubscriptions) {\n sub.close();\n }\n activeSubscriptions.clear();\n\n if (socialSubscription) {\n socialSubscription.unsubscribe();\n }\n\n // Close seed relay discovery connections\n if (seedRelayDiscovery) {\n await seedRelayDiscovery.close();\n }\n\n await wsRelay.stop();\n blsServer.close();\n\n // Stop auto-created connector\n if (autoCreatedConnector) {\n await autoCreatedConnector.stop();\n }\n\n // Close the EventStore (optional method on the EventStore interface)\n eventStore.close?.();\n },\n\n pubkey: identity.pubkey,\n evmAddress: identity.evmAddress,\n config: resolvedConfig,\n bootstrapResult: {\n peerCount,\n channelCount,\n },\n discoveryMode: discovery,\n };\n\n return instance;\n}\n\n// ---------- Deprecated aliases ----------\n// The launcher API was renamed from `startTown`/`Town*` to `startRelay`/`Relay*`\n// when @toon-protocol/town was merged into @toon-protocol/relay. The old names\n// are retained as aliases so existing callers keep working.\n\n/**\n * @deprecated Use {@link startRelay} instead. Retained for backwards\n * compatibility after the town → relay package merge.\n */\nexport const startTown = startRelay;\n\n/**\n * @deprecated Use {@link RelayConfig} instead.\n */\nexport type TownConfig = RelayConfig;\n\n/**\n * @deprecated Use {@link RelayInstance} instead.\n */\nexport type TownInstance = RelayInstance;\n\n/**\n * @deprecated Use {@link ResolvedRelayConfig} instead.\n */\nexport type ResolvedTownConfig = ResolvedRelayConfig;\n\n/**\n * @deprecated Use {@link RelaySubscription} instead.\n */\nexport type TownSubscription = RelaySubscription;\n","/**\n * Payment-oblivious 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 the payment layer: it contains\n * no claim/settlement/ILP/x402/EIP-3009 logic and imports none of it. Payment\n * validation is the connector's concern; by the time a request reaches this\n * surface, the trusted `X-TOON-*` headers are assumed already proven by an\n * upstream gate. The handler captures them purely for the response echo and a\n * 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 payment-oblivious write handler.\n */\nexport interface ObliviousWriteHandlerConfig {\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 * Payment-oblivious write handler instance.\n */\nexport interface ObliviousWriteHandler {\n /** Handle a plain-HTTP write request. */\n handleWrite(c: Context): Promise<Response>;\n}\n\n/**\n * Create a payment-oblivious write handler.\n *\n * @param config - Handler configuration.\n * @returns An ObliviousWriteHandler with a handleWrite method.\n */\nexport function createObliviousWriteHandler(\n config: ObliviousWriteHandlerConfig\n): ObliviousWriteHandler {\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 `[oblivious-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"],"mappings":"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","names":["encodeEventToToon","encodeEventToToon","verifyEvent","VERSION","shallowParseToon","decodeEventFromToon","encodeEventToToon","decodeEventFromToon","shallowParseToon","encodeEventToToon","VERSION"]}
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startRelay
4
- } from "./chunk-ZKWFGHZ7.js";
4
+ } from "./chunk-NYKVCNJL.js";
5
5
 
6
6
  // src/launcher/cli.ts
7
7
  import { parseArgs } from "util";
@@ -29,6 +29,12 @@ Options:
29
29
  --known-peers <json> Known peers as JSON array
30
30
  --dev-mode Enable dev mode (skip verification)
31
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.
32
38
  --discovery <mode> Discovery mode: 'seed-list' or 'genesis' (default: 'genesis')
33
39
  --seed-relays <urls> Comma-separated public Nostr relay URLs for seed discovery
34
40
  --publish-seed-entry Publish this node as a seed relay entry (default: false)
@@ -49,6 +55,7 @@ Environment Variables:
49
55
  TOON_KNOWN_PEERS Same as --known-peers
50
56
  TOON_DEV_MODE Same as --dev-mode (set to "true")
51
57
  TOON_X402_ENABLED Same as --x402-enabled (set to "true")
58
+ TOON_OBLIVIOUS_MODE Same as --oblivious-mode (set to "true")
52
59
  TOON_DISCOVERY Same as --discovery
53
60
  TOON_SEED_RELAYS Same as --seed-relays
54
61
  TOON_PUBLISH_SEED_ENTRY Same as --publish-seed-entry (set to "true")
@@ -85,6 +92,7 @@ function parseCli() {
85
92
  "known-peers": { type: "string" },
86
93
  "dev-mode": { type: "boolean" },
87
94
  "x402-enabled": { type: "boolean" },
95
+ "oblivious-mode": { type: "boolean" },
88
96
  discovery: { type: "string" },
89
97
  "seed-relays": { type: "string" },
90
98
  "publish-seed-entry": { type: "boolean" },
@@ -144,6 +152,7 @@ function parseCli() {
144
152
  const dataDir = values["data-dir"] ?? process.env["TOON_DATA_DIR"] ?? void 0;
145
153
  const devMode = values["dev-mode"] ?? (process.env["TOON_DEV_MODE"] === "true" ? true : void 0);
146
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);
147
156
  const knownPeersJson = values["known-peers"] ?? process.env["TOON_KNOWN_PEERS"] ?? void 0;
148
157
  let knownPeers;
149
158
  if (knownPeersJson) {
@@ -262,6 +271,7 @@ function parseCli() {
262
271
  ...knownPeers && { knownPeers },
263
272
  ...devMode !== void 0 && { devMode },
264
273
  ...x402Enabled !== void 0 && { x402Enabled },
274
+ ...obliviousMode !== void 0 && { obliviousMode },
265
275
  ...discoveryMode && { discovery: discoveryMode },
266
276
  ...seedRelaysArr && { seedRelays: seedRelaysArr },
267
277
  ...publishSeedEntry !== void 0 && { publishSeedEntry },