@toon-protocol/relay 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,16 +6,117 @@ import {
6
6
  RelayError,
7
7
  RelaySubscriber,
8
8
  SqliteEventStore,
9
- ToonDecodeError,
10
- ToonEncodeError,
11
9
  VERSION,
12
10
  createHealthResponse,
11
+ createMetricsRegistry,
12
+ createVerifyPool,
13
13
  createWriteHandler,
14
- decodeEventFromToon,
15
- encodeEventToToon,
14
+ defaultVerifyWorkers,
15
+ isInternalBindHost,
16
16
  matchFilter,
17
- startRelay
18
- } from "./chunk-745ADETR.js";
17
+ readOpenFilesSoftLimit,
18
+ serializeEventFrame,
19
+ startRelay,
20
+ warnIfWritePortExposed
21
+ } from "./chunk-QZQRHQEQ.js";
22
+ import {
23
+ verifyEventId,
24
+ verifyEventSignature,
25
+ verifyImplementation
26
+ } from "./chunk-SMT6G3XD.js";
27
+
28
+ // src/toon/codec.ts
29
+ import { encode, decode } from "@toon-format/toon";
30
+ var ToonEncodeError = class extends Error {
31
+ code = "TOON_ENCODE_ERROR";
32
+ constructor(message, cause) {
33
+ super(message, { cause });
34
+ this.name = "ToonEncodeError";
35
+ }
36
+ };
37
+ var ToonDecodeError = class extends Error {
38
+ code = "TOON_DECODE_ERROR";
39
+ constructor(message, cause) {
40
+ super(message, { cause });
41
+ this.name = "ToonDecodeError";
42
+ }
43
+ };
44
+ function encodeEventToToon(event) {
45
+ return new TextEncoder().encode(encodeEventToToonString(event));
46
+ }
47
+ function encodeEventToToonString(event) {
48
+ try {
49
+ return encode(event);
50
+ } catch (error) {
51
+ throw new ToonEncodeError(
52
+ `Failed to encode event to TOON: ${error instanceof Error ? error.message : String(error)}`,
53
+ error instanceof Error ? error : void 0
54
+ );
55
+ }
56
+ }
57
+ function isValidHex(value, length) {
58
+ return typeof value === "string" && value.length === length && /^[0-9a-f]+$/i.test(value);
59
+ }
60
+ function validateNostrEvent(obj) {
61
+ if (typeof obj !== "object" || obj === null) {
62
+ throw new ToonDecodeError("Decoded value is not an object");
63
+ }
64
+ const event = obj;
65
+ if (!isValidHex(event["id"], 64)) {
66
+ throw new ToonDecodeError(
67
+ "Invalid event id: must be a 64-character hex string"
68
+ );
69
+ }
70
+ if (!isValidHex(event["pubkey"], 64)) {
71
+ throw new ToonDecodeError(
72
+ "Invalid event pubkey: must be a 64-character hex string"
73
+ );
74
+ }
75
+ if (typeof event["kind"] !== "number" || !Number.isInteger(event["kind"])) {
76
+ throw new ToonDecodeError("Invalid event kind: must be an integer");
77
+ }
78
+ if (typeof event["content"] !== "string") {
79
+ throw new ToonDecodeError("Invalid event content: must be a string");
80
+ }
81
+ const tags = event["tags"];
82
+ if (!Array.isArray(tags)) {
83
+ throw new ToonDecodeError("Invalid event tags: must be an array");
84
+ }
85
+ for (let i = 0; i < tags.length; i++) {
86
+ const tag = tags[i];
87
+ if (!Array.isArray(tag)) {
88
+ throw new ToonDecodeError(`Invalid event tags[${i}]: must be an array`);
89
+ }
90
+ for (let j = 0; j < tag.length; j++) {
91
+ if (typeof tag[j] !== "string") {
92
+ throw new ToonDecodeError(
93
+ `Invalid event tags[${i}][${j}]: must be a string`
94
+ );
95
+ }
96
+ }
97
+ }
98
+ if (typeof event["created_at"] !== "number" || !Number.isInteger(event["created_at"])) {
99
+ throw new ToonDecodeError("Invalid event created_at: must be an integer");
100
+ }
101
+ if (!isValidHex(event["sig"], 128)) {
102
+ throw new ToonDecodeError(
103
+ "Invalid event sig: must be a 128-character hex string"
104
+ );
105
+ }
106
+ }
107
+ function decodeEventFromToon(data) {
108
+ let decoded;
109
+ try {
110
+ decoded = decode(new TextDecoder().decode(data));
111
+ } catch (error) {
112
+ throw new ToonDecodeError(
113
+ `Failed to decode TOON data: ${error instanceof Error ? error.message : String(error)}`,
114
+ error instanceof Error ? error : void 0
115
+ );
116
+ }
117
+ validateNostrEvent(decoded);
118
+ return decoded;
119
+ }
19
120
  export {
20
121
  ConnectionHandler,
21
122
  DEFAULT_RELAY_CONFIG,
@@ -28,10 +129,20 @@ export {
28
129
  ToonEncodeError,
29
130
  VERSION,
30
131
  createHealthResponse,
132
+ createMetricsRegistry,
133
+ createVerifyPool,
31
134
  createWriteHandler,
32
135
  decodeEventFromToon,
136
+ defaultVerifyWorkers,
33
137
  encodeEventToToon,
138
+ isInternalBindHost,
34
139
  matchFilter,
35
- startRelay
140
+ readOpenFilesSoftLimit,
141
+ serializeEventFrame,
142
+ startRelay,
143
+ verifyEventId,
144
+ verifyEventSignature,
145
+ verifyImplementation,
146
+ warnIfWritePortExposed
36
147
  };
37
148
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
1
+ {"version":3,"sources":["../src/toon/codec.ts"],"sourcesContent":["/**\n * TOON event codec.\n *\n * Encodes/decodes Nostr events to and from the TOON text format. Vendored from\n * `@toon-protocol/core` so the relay depends only on the lightweight\n * `@toon-format/toon` encoder rather than core's full transitive tree (which\n * pulls Arweave / web3 wallet stacks the relay does not use). Same MIT\n * license / org.\n *\n * NOTE: this codec is NOT used on the relay's NIP-01 read surface. Outbound\n * EVENT frames are canonical NIP-01 JSON (see\n * `websocket/ConnectionHandler.sendEvent`, #46) so that standard nostr clients\n * can parse events and verify signatures from the wire. The codec remains\n * exported for library consumers that exchange TOON-text events elsewhere.\n *\n * @module\n */\n\nimport { encode, decode } from '@toon-format/toon';\nimport type { NostrEvent } from 'nostr-tools/pure';\n\n/** Thrown when a Nostr event cannot be encoded to TOON. */\nexport class ToonEncodeError extends Error {\n readonly code = 'TOON_ENCODE_ERROR';\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = 'ToonEncodeError';\n }\n}\n\n/** Thrown when TOON data cannot be decoded into a valid Nostr event. */\nexport class ToonDecodeError extends Error {\n readonly code = 'TOON_DECODE_ERROR';\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = 'ToonDecodeError';\n }\n}\n\n/** Encode a Nostr event to TOON bytes (UTF-8). */\nexport function encodeEventToToon(event: NostrEvent): Uint8Array {\n return new TextEncoder().encode(encodeEventToToonString(event));\n}\n\n/** Encode a Nostr event to a TOON string. */\nexport function encodeEventToToonString(event: NostrEvent): string {\n try {\n return encode(event);\n } catch (error) {\n throw new ToonEncodeError(\n `Failed to encode event to TOON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\nfunction isValidHex(value: unknown, length: number): boolean {\n return (\n typeof value === 'string' &&\n value.length === length &&\n /^[0-9a-f]+$/i.test(value)\n );\n}\n\nfunction validateNostrEvent(obj: unknown): asserts obj is NostrEvent {\n if (typeof obj !== 'object' || obj === null) {\n throw new ToonDecodeError('Decoded value is not an object');\n }\n const event = obj as Record<string, unknown>;\n if (!isValidHex(event['id'], 64)) {\n throw new ToonDecodeError(\n 'Invalid event id: must be a 64-character hex string'\n );\n }\n if (!isValidHex(event['pubkey'], 64)) {\n throw new ToonDecodeError(\n 'Invalid event pubkey: must be a 64-character hex string'\n );\n }\n if (typeof event['kind'] !== 'number' || !Number.isInteger(event['kind'])) {\n throw new ToonDecodeError('Invalid event kind: must be an integer');\n }\n if (typeof event['content'] !== 'string') {\n throw new ToonDecodeError('Invalid event content: must be a string');\n }\n const tags = event['tags'];\n if (!Array.isArray(tags)) {\n throw new ToonDecodeError('Invalid event tags: must be an array');\n }\n for (let i = 0; i < tags.length; i++) {\n const tag: unknown = tags[i];\n if (!Array.isArray(tag)) {\n throw new ToonDecodeError(`Invalid event tags[${i}]: must be an array`);\n }\n for (let j = 0; j < tag.length; j++) {\n if (typeof tag[j] !== 'string') {\n throw new ToonDecodeError(\n `Invalid event tags[${i}][${j}]: must be a string`\n );\n }\n }\n }\n if (\n typeof event['created_at'] !== 'number' ||\n !Number.isInteger(event['created_at'])\n ) {\n throw new ToonDecodeError('Invalid event created_at: must be an integer');\n }\n if (!isValidHex(event['sig'], 128)) {\n throw new ToonDecodeError(\n 'Invalid event sig: must be a 128-character hex string'\n );\n }\n}\n\n/** Decode TOON bytes into a validated Nostr event. */\nexport function decodeEventFromToon(data: Uint8Array): NostrEvent {\n let decoded: unknown;\n try {\n decoded = decode(new TextDecoder().decode(data));\n } catch (error) {\n throw new ToonDecodeError(\n `Failed to decode TOON data: ${\n error instanceof Error ? error.message : String(error)\n }`,\n error instanceof Error ? error : undefined\n );\n }\n validateNostrEvent(decoded);\n return decoded;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,17 @@
1
+ import {
2
+ verifyEventSignature
3
+ } from "./chunk-SMT6G3XD.js";
4
+
5
+ // src/crypto/verify-worker.ts
6
+ import { parentPort } from "worker_threads";
7
+ if (!parentPort) {
8
+ throw new Error("verify-worker must be started as a worker thread");
9
+ }
10
+ var port = parentPort;
11
+ port.on("message", (message) => {
12
+ port.postMessage({
13
+ seq: message.seq,
14
+ ok: verifyEventSignature(message.event)
15
+ });
16
+ });
17
+ //# sourceMappingURL=verify-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/crypto/verify-worker.ts"],"sourcesContent":["/**\n * Worker-thread entrypoint for the verify pool (relay#85).\n *\n * Each worker imports `verify-event.ts` in its own thread, so each worker\n * instantiates its OWN WASM libsecp256k1 module (with the same load-time\n * self-test and transparent noble fallback as the inline path -- WASM\n * instances are not shareable across threads anyway).\n *\n * Protocol: the parent posts `{ seq, event }`; the worker replies\n * `{ seq, ok }`. `verifyEventSignature` never throws, so every request gets\n * exactly one reply.\n *\n * Built as its own tsup entry (`dist/verify-worker.js`) because\n * `worker_threads` needs a real JS file on disk, not a bundled import.\n *\n * @module\n */\n\nimport { parentPort } from 'node:worker_threads';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport { verifyEventSignature } from './verify-event.js';\n\nif (!parentPort) {\n throw new Error('verify-worker must be started as a worker thread');\n}\n\nconst port = parentPort;\nport.on('message', (message: { seq: number; event: NostrEvent }) => {\n port.postMessage({\n seq: message.seq,\n ok: verifyEventSignature(message.event),\n });\n});\n"],"mappings":";;;;;AAkBA,SAAS,kBAAkB;AAI3B,IAAI,CAAC,YAAY;AACf,QAAM,IAAI,MAAM,kDAAkD;AACpE;AAEA,IAAM,OAAO;AACb,KAAK,GAAG,WAAW,CAAC,YAAgD;AAClE,OAAK,YAAY;AAAA,IACf,KAAK,QAAQ;AAAA,IACb,IAAI,qBAAqB,QAAQ,KAAK;AAAA,EACxC,CAAC;AACH,CAAC;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toon-protocol/relay",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Nostr relay app: free NIP-01 reads + HTTP POST /write",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -43,6 +43,7 @@
43
43
  "better-sqlite3": "^11.0.0",
44
44
  "hono": "^4.11.10",
45
45
  "nostr-tools": "^2.20.0",
46
+ "tiny-secp256k1": "^2.2.4",
46
47
  "ws": "^8.0.0"
47
48
  },
48
49
  "devDependencies": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/version.ts","../src/types.ts","../src/filters/matchFilter.ts","../src/storage/InMemoryEventStore.ts","../src/storage/SqliteEventStore.ts","../src/toon/codec.ts","../src/websocket/ConnectionHandler.ts","../src/websocket/NostrRelayServer.ts","../src/subscriber/RelaySubscriber.ts","../src/launcher/handlers/write-handler.ts","../src/launcher/health.ts","../src/launcher/relay.ts"],"sourcesContent":["/** Package version, surfaced on `GET /health`. */\nexport const VERSION = '0.1.0';\n","/**\n * Configuration options for the Nostr relay.\n */\nexport interface RelayServerConfig {\n /** Port to listen on (default: 7000) */\n port: number;\n /** Host/IP to bind to (default: '0.0.0.0'). Set to '127.0.0.1' for hidden service mode. */\n host?: string;\n /** Maximum concurrent connections (default: 100) */\n maxConnections?: number;\n /** Maximum subscriptions per connection (default: 20) */\n maxSubscriptionsPerConnection?: number;\n /** Maximum filters per subscription (default: 10) */\n maxFiltersPerSubscription?: number;\n /** Path to SQLite database file (default: ':memory:' for in-memory) */\n databasePath?: string;\n}\n\n/**\n * Default relay configuration values.\n */\nexport const DEFAULT_RELAY_CONFIG: Required<RelayServerConfig> = {\n port: 7000,\n host: '0.0.0.0',\n maxConnections: 100,\n maxSubscriptionsPerConnection: 20,\n maxFiltersPerSubscription: 10,\n databasePath: ':memory:',\n};\n","import type { NostrEvent } from 'nostr-tools/pure';\nimport type { Filter } from 'nostr-tools/filter';\n\n/**\n * Check if an event matches a single filter according to NIP-01 rules.\n *\n * Matching rules:\n * - All specified fields must match (AND logic)\n * - `ids` and `authors` support prefix matching\n * - Tag filters (#e, #p, etc.) match events with corresponding tags\n * - Empty filter matches all events\n *\n * @param event - The Nostr event to check\n * @param filter - The filter to match against\n * @returns true if the event matches the filter\n */\nexport function matchFilter(event: NostrEvent, filter: Filter): boolean {\n // Empty filter matches everything\n if (Object.keys(filter).length === 0) {\n return true;\n }\n\n // Check ids (prefix matching)\n if (filter.ids !== undefined && filter.ids.length > 0) {\n const matches = filter.ids.some((id) => event.id.startsWith(id));\n if (!matches) return false;\n }\n\n // Check authors (prefix matching)\n if (filter.authors !== undefined && filter.authors.length > 0) {\n const matches = filter.authors.some((author) =>\n event.pubkey.startsWith(author)\n );\n if (!matches) return false;\n }\n\n // Check kinds (exact matching)\n if (filter.kinds !== undefined && filter.kinds.length > 0) {\n if (!filter.kinds.includes(event.kind)) return false;\n }\n\n // Check since (created_at >= since)\n if (filter.since !== undefined) {\n if (event.created_at < filter.since) return false;\n }\n\n // Check until (created_at <= until)\n if (filter.until !== undefined) {\n if (event.created_at > filter.until) return false;\n }\n\n // Check tag filters (#e, #p, and generic #<single-letter>)\n for (const key of Object.keys(filter)) {\n if (key.startsWith('#') && key.length === 2) {\n const tagName = key.slice(1);\n const filterValues = filter[key as `#${string}`];\n\n if (filterValues !== undefined && filterValues.length > 0) {\n // Find matching tags in the event\n const eventTagValues = event.tags\n .filter((tag) => tag[0] === tagName)\n .map((tag) => tag[1]);\n\n // At least one filter value must match an event tag value\n const hasMatch = filterValues.some((v) => eventTagValues.includes(v));\n if (!hasMatch) return false;\n }\n }\n }\n\n return true;\n}\n","import type { NostrEvent } from 'nostr-tools/pure';\nimport type { Filter } from 'nostr-tools/filter';\nimport { matchFilter } from '../filters/index.js';\n\n/**\n * Interface for event storage backends.\n */\nexport interface EventStore {\n /** Store an event by its ID */\n store(event: NostrEvent): void;\n /** Retrieve a single event by ID */\n get(id: string): NostrEvent | undefined;\n /** Query events matching any of the provided filters */\n query(filters: Filter[]): NostrEvent[];\n /** Close the storage backend (optional) */\n close?(): void;\n}\n\n/**\n * In-memory implementation of EventStore.\n * Events are stored in a Map keyed by event ID.\n */\nexport class InMemoryEventStore implements EventStore {\n private events = new Map<string, NostrEvent>();\n\n store(event: NostrEvent): void {\n this.events.set(event.id, event);\n }\n\n get(id: string): NostrEvent | undefined {\n return this.events.get(id);\n }\n\n query(filters: Filter[]): NostrEvent[] {\n // Get all events\n const allEvents = Array.from(this.events.values());\n\n // If no filters provided, return all events sorted by created_at desc\n if (filters.length === 0) {\n return allEvents.sort((a, b) => b.created_at - a.created_at);\n }\n\n // Find events matching ANY filter (OR logic between filters)\n const matchingEvents: NostrEvent[] = [];\n\n for (const event of allEvents) {\n for (const filter of filters) {\n if (matchFilter(event, filter)) {\n matchingEvents.push(event);\n break; // Only add once even if matches multiple filters\n }\n }\n }\n\n // Sort by created_at descending\n matchingEvents.sort((a, b) => b.created_at - a.created_at);\n\n // Apply limit from first filter that has one (NIP-01 semantics)\n const limitFilter = filters.find((f) => f.limit !== undefined);\n if (limitFilter?.limit !== undefined) {\n return matchingEvents.slice(0, limitFilter.limit);\n }\n\n return matchingEvents;\n }\n\n /**\n * Close the storage backend (no-op for in-memory store).\n */\n close(): void {\n // No-op for in-memory store\n }\n}\n","import Database from 'better-sqlite3';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { Filter } from 'nostr-tools/filter';\nimport type { EventStore } from './InMemoryEventStore.js';\n\n/**\n * SQL schema for the events table.\n */\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n pubkey TEXT NOT NULL,\n kind INTEGER NOT NULL,\n content TEXT NOT NULL,\n tags TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n sig TEXT NOT NULL,\n received_at INTEGER NOT NULL\n)\n`;\n\n/**\n * SQL for creating indexes on the events table.\n */\nconst INDEX_SQL = [\n 'CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey)',\n 'CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind)',\n 'CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at)',\n 'CREATE INDEX IF NOT EXISTS idx_events_pubkey_kind ON events(pubkey, kind)',\n];\n\n/**\n * Initialize the database schema.\n */\nfunction initializeSchema(db: Database.Database): void {\n db.exec(SCHEMA_SQL);\n for (const indexSql of INDEX_SQL) {\n db.exec(indexSql);\n }\n}\n\n/**\n * Custom error class for relay storage errors.\n */\nexport class RelayError extends Error {\n constructor(\n message: string,\n public code: string\n ) {\n super(message);\n this.name = 'RelayError';\n }\n}\n\n/**\n * Check if an event kind is in the replaceable range (10000-19999).\n * Excludes TOON-specific parameterized kinds 10032-10099.\n */\nfunction isReplaceableKind(kind: number): boolean {\n return kind >= 10000 && kind <= 19999 && !(kind >= 10032 && kind <= 10099);\n}\n\n/**\n * Check if an event kind is in the parameterized replaceable range.\n * Covers NIP-33 (30000-39999) and TOON-specific kinds (10032-10099).\n */\nfunction isParameterizedReplaceableKind(kind: number): boolean {\n return (kind >= 30000 && kind <= 39999) || (kind >= 10032 && kind <= 10099);\n}\n\n/**\n * Get the 'd' tag value from an event's tags array.\n */\nfunction getDTagValue(tags: string[][]): string {\n const dTag = tags.find((tag) => tag[0] === 'd');\n return dTag?.[1] ?? '';\n}\n\n/**\n * SQLite implementation of EventStore.\n * Persists events to a SQLite database file.\n */\nexport class SqliteEventStore implements EventStore {\n private db: Database.Database;\n private insertStmt: Database.Statement;\n private getStmt: Database.Statement;\n private deleteByPubkeyKindStmt: Database.Statement;\n private deleteByPubkeyKindDTagStmt: Database.Statement;\n private getByPubkeyKindStmt: Database.Statement;\n private getByPubkeyKindDTagStmt: Database.Statement;\n\n /**\n * Create a new SqliteEventStore.\n * @param dbPath - Path to the database file. Use ':memory:' for in-memory database.\n */\n constructor(dbPath = ':memory:') {\n try {\n this.db = new Database(dbPath);\n initializeSchema(this.db);\n\n // Prepare statements for better performance\n this.insertStmt = this.db.prepare(`\n INSERT OR REPLACE INTO events (id, pubkey, kind, content, tags, created_at, sig, received_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n this.getStmt = this.db.prepare('SELECT * FROM events WHERE id = ?');\n\n this.deleteByPubkeyKindStmt = this.db.prepare(\n 'DELETE FROM events WHERE pubkey = ? AND kind = ?'\n );\n\n this.deleteByPubkeyKindDTagStmt = this.db.prepare(\n \"DELETE FROM events WHERE pubkey = ? AND kind = ? AND json_extract(tags, '$') LIKE ?\"\n );\n\n this.getByPubkeyKindStmt = this.db.prepare(\n 'SELECT id, created_at FROM events WHERE pubkey = ? AND kind = ?'\n );\n\n this.getByPubkeyKindDTagStmt = this.db.prepare(\n 'SELECT id, created_at FROM events WHERE pubkey = ? AND kind = ? AND tags LIKE ?'\n );\n } catch (error) {\n throw new RelayError(\n `Failed to initialize database: ${error instanceof Error ? error.message : String(error)}`,\n 'STORAGE_ERROR'\n );\n }\n }\n\n /**\n * Store an event in the database.\n * Handles replaceable and parameterized replaceable events according to NIP-01.\n */\n store(event: NostrEvent): void {\n try {\n const tagsJson = JSON.stringify(event.tags);\n const receivedAt = Math.floor(Date.now() / 1000);\n\n if (isReplaceableKind(event.kind)) {\n // Replaceable event (10000-19999)\n this.storeReplaceableEvent(event, tagsJson, receivedAt);\n } else if (isParameterizedReplaceableKind(event.kind)) {\n // Parameterized replaceable event (30000-39999)\n this.storeParameterizedReplaceableEvent(event, tagsJson, receivedAt);\n } else {\n // Regular event - INSERT OR IGNORE to handle duplicates\n const insertOrIgnore = this.db.prepare(`\n INSERT OR IGNORE INTO events (id, pubkey, kind, content, tags, created_at, sig, received_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n insertOrIgnore.run(\n event.id,\n event.pubkey,\n event.kind,\n event.content,\n tagsJson,\n event.created_at,\n event.sig,\n receivedAt\n );\n }\n } catch (error) {\n if (error instanceof RelayError) {\n throw error;\n }\n throw new RelayError(\n `Failed to store event: ${error instanceof Error ? error.message : String(error)}`,\n 'STORAGE_ERROR'\n );\n }\n }\n\n /**\n * Store a replaceable event (kinds 10000-19999).\n * Only keeps the latest event per pubkey+kind.\n */\n private storeReplaceableEvent(\n event: NostrEvent,\n tagsJson: string,\n receivedAt: number\n ): void {\n const existing = this.getByPubkeyKindStmt.get(event.pubkey, event.kind) as\n | { id: string; created_at: number }\n | undefined;\n\n if (existing) {\n // Only replace if new event is newer, or same time with lower id\n if (\n event.created_at > existing.created_at ||\n (event.created_at === existing.created_at && event.id < existing.id)\n ) {\n // Use transaction for atomicity\n const transaction = this.db.transaction(() => {\n this.deleteByPubkeyKindStmt.run(event.pubkey, event.kind);\n this.insertStmt.run(\n event.id,\n event.pubkey,\n event.kind,\n event.content,\n tagsJson,\n event.created_at,\n event.sig,\n receivedAt\n );\n });\n transaction();\n }\n // If existing event is newer or same, don't replace\n } else {\n // No existing event, just insert\n this.insertStmt.run(\n event.id,\n event.pubkey,\n event.kind,\n event.content,\n tagsJson,\n event.created_at,\n event.sig,\n receivedAt\n );\n }\n }\n\n /**\n * Store a parameterized replaceable event (kinds 30000-39999).\n * Only keeps the latest event per pubkey+kind+d-tag.\n */\n private storeParameterizedReplaceableEvent(\n event: NostrEvent,\n tagsJson: string,\n receivedAt: number\n ): void {\n const dTagValue = getDTagValue(event.tags);\n\n // For empty d-tag value, we need to match events that either:\n // 1. Have [\"d\", \"\"] in tags\n // 2. Have no d-tag at all (tags doesn't contain \"d\" as first element)\n let existing: { id: string; created_at: number } | undefined;\n\n if (dTagValue === '') {\n // Query for events with same pubkey and kind, then filter in code\n const candidates = this.db\n .prepare(\n 'SELECT id, created_at, tags FROM events WHERE pubkey = ? AND kind = ?'\n )\n .all(event.pubkey, event.kind) as {\n id: string;\n created_at: number;\n tags: string;\n }[];\n\n // Find one with empty or missing d-tag\n for (const candidate of candidates) {\n const candidateTags = JSON.parse(candidate.tags) as string[][];\n const candidateDTagValue = getDTagValue(candidateTags);\n if (candidateDTagValue === '') {\n existing = { id: candidate.id, created_at: candidate.created_at };\n break;\n }\n }\n } else {\n const dTagPattern = `%[\"d\",\"${dTagValue}\"%`;\n existing = this.getByPubkeyKindDTagStmt.get(\n event.pubkey,\n event.kind,\n dTagPattern\n ) as { id: string; created_at: number } | undefined;\n }\n\n if (existing) {\n // Only replace if new event is newer, or same time with lower id\n if (\n event.created_at > existing.created_at ||\n (event.created_at === existing.created_at && event.id < existing.id)\n ) {\n // Use transaction for atomicity - delete by ID for safety\n const transaction = this.db.transaction(() => {\n this.db.prepare('DELETE FROM events WHERE id = ?').run(existing.id);\n this.insertStmt.run(\n event.id,\n event.pubkey,\n event.kind,\n event.content,\n tagsJson,\n event.created_at,\n event.sig,\n receivedAt\n );\n });\n transaction();\n }\n // If existing event is newer or same, don't replace\n } else {\n // No existing event, just insert\n this.insertStmt.run(\n event.id,\n event.pubkey,\n event.kind,\n event.content,\n tagsJson,\n event.created_at,\n event.sig,\n receivedAt\n );\n }\n }\n\n /**\n * Retrieve an event by its ID.\n */\n get(id: string): NostrEvent | undefined {\n try {\n const row = this.getStmt.get(id) as\n | {\n id: string;\n pubkey: string;\n kind: number;\n content: string;\n tags: string;\n created_at: number;\n sig: string;\n }\n | undefined;\n\n if (!row) {\n return undefined;\n }\n\n return {\n id: row.id,\n pubkey: row.pubkey,\n kind: row.kind,\n content: row.content,\n tags: JSON.parse(row.tags) as string[][],\n created_at: row.created_at,\n sig: row.sig,\n };\n } catch (error) {\n throw new RelayError(\n `Failed to get event: ${error instanceof Error ? error.message : String(error)}`,\n 'STORAGE_ERROR'\n );\n }\n }\n\n /**\n * Query events matching any of the provided filters.\n */\n query(filters: Filter[]): NostrEvent[] {\n try {\n const { sql, params } = this.buildQuerySql(filters);\n const stmt = this.db.prepare(sql);\n const rows = stmt.all(...params) as {\n id: string;\n pubkey: string;\n kind: number;\n content: string;\n tags: string;\n created_at: number;\n sig: string;\n }[];\n\n return rows.map((row) => ({\n id: row.id,\n pubkey: row.pubkey,\n kind: row.kind,\n content: row.content,\n tags: JSON.parse(row.tags) as string[][],\n created_at: row.created_at,\n sig: row.sig,\n }));\n } catch (error) {\n throw new RelayError(\n `Failed to query events: ${error instanceof Error ? error.message : String(error)}`,\n 'STORAGE_ERROR'\n );\n }\n }\n\n /**\n * Build SQL query from filters.\n */\n private buildQuerySql(filters: Filter[]): { sql: string; params: unknown[] } {\n if (filters.length === 0) {\n return {\n sql: 'SELECT * FROM events ORDER BY created_at DESC',\n params: [],\n };\n }\n\n const conditions: string[] = [];\n const params: unknown[] = [];\n\n for (const filter of filters) {\n const filterConditions: string[] = [];\n\n if (filter.ids?.length) {\n // Prefix matching with LIKE\n const idConditions = filter.ids.map(() => 'id LIKE ?');\n filterConditions.push(`(${idConditions.join(' OR ')})`);\n params.push(...filter.ids.map((id) => `${id}%`));\n }\n\n if (filter.authors?.length) {\n const authorConditions = filter.authors.map(() => 'pubkey LIKE ?');\n filterConditions.push(`(${authorConditions.join(' OR ')})`);\n params.push(...filter.authors.map((a) => `${a}%`));\n }\n\n if (filter.kinds?.length) {\n filterConditions.push(\n `kind IN (${filter.kinds.map(() => '?').join(', ')})`\n );\n params.push(...filter.kinds);\n }\n\n if (filter.since !== undefined) {\n filterConditions.push('created_at >= ?');\n params.push(filter.since);\n }\n\n if (filter.until !== undefined) {\n filterConditions.push('created_at <= ?');\n params.push(filter.until);\n }\n\n // Handle tag filters (#e, #p, etc.)\n for (const [key, values] of Object.entries(filter)) {\n if (key.startsWith('#') && Array.isArray(values) && values.length > 0) {\n const tagName = key.slice(1);\n const tagConditions = values.map(() => `tags LIKE ?`);\n filterConditions.push(`(${tagConditions.join(' OR ')})`);\n params.push(...values.map((v) => `%[\"${tagName}\",\"${v}\"%`));\n }\n }\n\n if (filterConditions.length > 0) {\n conditions.push(`(${filterConditions.join(' AND ')})`);\n }\n }\n\n let sql = 'SELECT * FROM events';\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' OR ')}`;\n }\n sql += ' ORDER BY created_at DESC';\n\n // Apply limit from first filter that specifies it\n const limitFilter = filters.find((f) => f.limit !== undefined);\n if (limitFilter?.limit !== undefined) {\n sql += ' LIMIT ?';\n params.push(limitFilter.limit);\n }\n\n return { sql, params };\n }\n\n /**\n * Close the database connection.\n */\n close(): void {\n this.db.close();\n }\n}\n","/**\n * TOON event codec.\n *\n * Encodes/decodes Nostr events to and from the TOON wire format used on this\n * relay's NIP-01 read surface. Vendored from `@toon-protocol/core` so the relay\n * depends only on the lightweight `@toon-format/toon` encoder rather than core's\n * full transitive tree (which pulls Arweave / web3 wallet stacks the relay does\n * not use). Same MIT license / org.\n *\n * @module\n */\n\nimport { encode, decode } from '@toon-format/toon';\nimport type { NostrEvent } from 'nostr-tools/pure';\n\n/** Thrown when a Nostr event cannot be encoded to TOON. */\nexport class ToonEncodeError extends Error {\n readonly code = 'TOON_ENCODE_ERROR';\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = 'ToonEncodeError';\n }\n}\n\n/** Thrown when TOON data cannot be decoded into a valid Nostr event. */\nexport class ToonDecodeError extends Error {\n readonly code = 'TOON_DECODE_ERROR';\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = 'ToonDecodeError';\n }\n}\n\n/** Encode a Nostr event to TOON bytes (UTF-8). */\nexport function encodeEventToToon(event: NostrEvent): Uint8Array {\n return new TextEncoder().encode(encodeEventToToonString(event));\n}\n\n/** Encode a Nostr event to a TOON string. */\nexport function encodeEventToToonString(event: NostrEvent): string {\n try {\n return encode(event);\n } catch (error) {\n throw new ToonEncodeError(\n `Failed to encode event to TOON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\nfunction isValidHex(value: unknown, length: number): boolean {\n return (\n typeof value === 'string' &&\n value.length === length &&\n /^[0-9a-f]+$/i.test(value)\n );\n}\n\nfunction validateNostrEvent(obj: unknown): asserts obj is NostrEvent {\n if (typeof obj !== 'object' || obj === null) {\n throw new ToonDecodeError('Decoded value is not an object');\n }\n const event = obj as Record<string, unknown>;\n if (!isValidHex(event['id'], 64)) {\n throw new ToonDecodeError(\n 'Invalid event id: must be a 64-character hex string'\n );\n }\n if (!isValidHex(event['pubkey'], 64)) {\n throw new ToonDecodeError(\n 'Invalid event pubkey: must be a 64-character hex string'\n );\n }\n if (typeof event['kind'] !== 'number' || !Number.isInteger(event['kind'])) {\n throw new ToonDecodeError('Invalid event kind: must be an integer');\n }\n if (typeof event['content'] !== 'string') {\n throw new ToonDecodeError('Invalid event content: must be a string');\n }\n const tags = event['tags'];\n if (!Array.isArray(tags)) {\n throw new ToonDecodeError('Invalid event tags: must be an array');\n }\n for (let i = 0; i < tags.length; i++) {\n const tag: unknown = tags[i];\n if (!Array.isArray(tag)) {\n throw new ToonDecodeError(`Invalid event tags[${i}]: must be an array`);\n }\n for (let j = 0; j < tag.length; j++) {\n if (typeof tag[j] !== 'string') {\n throw new ToonDecodeError(\n `Invalid event tags[${i}][${j}]: must be a string`\n );\n }\n }\n }\n if (\n typeof event['created_at'] !== 'number' ||\n !Number.isInteger(event['created_at'])\n ) {\n throw new ToonDecodeError('Invalid event created_at: must be an integer');\n }\n if (!isValidHex(event['sig'], 128)) {\n throw new ToonDecodeError(\n 'Invalid event sig: must be a 128-character hex string'\n );\n }\n}\n\n/** Decode TOON bytes into a validated Nostr event. */\nexport function decodeEventFromToon(data: Uint8Array): NostrEvent {\n let decoded: unknown;\n try {\n decoded = decode(new TextDecoder().decode(data));\n } catch (error) {\n throw new ToonDecodeError(\n `Failed to decode TOON data: ${\n error instanceof Error ? error.message : String(error)\n }`,\n error instanceof Error ? error : undefined\n );\n }\n validateNostrEvent(decoded);\n return decoded;\n}\n","import type { WebSocket } from 'ws';\nimport type { Filter } from 'nostr-tools/filter';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { EventStore } from '../storage/index.js';\nimport type { RelayServerConfig } from '../types.js';\nimport { DEFAULT_RELAY_CONFIG } from '../types.js';\nimport { 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 * Write handler for @toon-protocol/relay.\n *\n * Exposes a plain-HTTP write surface that accepts an event-as-JSON, trusts\n * (but does NOT validate) injected payment headers, verifies ONLY the event\n * signature for integrity, and stores the event.\n *\n * This handler is intentionally decoupled from any payment layer: it contains\n * no claim/settlement/ILP logic and imports none of it. Payment validation is\n * the upstream terminator's concern; by the time a request reaches this surface\n * the trusted `X-TOON-*` headers are assumed already proven. The handler\n * captures them purely for the response echo and a log line.\n *\n * Flow:\n * 1. Parse JSON body `{ event }` -> 400 on malformed/missing event\n * 2. Capture trusted X-TOON-Payer / X-TOON-Amount / X-TOON-Chain headers\n * 3. Verify the event signature (skipped in devMode) -> 422 on invalid sig\n * 4. Store the event in the EventStore\n * 5. Fire the optional onStored callback\n * 6. Respond 200 with the event id, storedAt timestamp, and echoed headers\n *\n * @module\n */\n\nimport type { Context } from 'hono';\nimport { verifyEvent } from 'nostr-tools/pure';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { EventStore } from '../../storage/index.js';\n\n/**\n * Configuration for the write handler.\n */\nexport interface WriteHandlerConfig {\n /** Event store backend used to persist accepted events. */\n eventStore: EventStore;\n /** Whether dev mode is enabled (skips Schnorr signature verification). */\n devMode: boolean;\n /** Optional callback fired after an event is successfully stored. */\n onStored?: (event: NostrEvent) => void;\n}\n\n/**\n * Write handler instance.\n */\nexport interface WriteHandler {\n /** Handle a plain-HTTP write request. */\n handleWrite(c: Context): Promise<Response>;\n}\n\n/**\n * Create a write handler.\n *\n * @param config - Handler configuration.\n * @returns A WriteHandler with a handleWrite method.\n */\nexport function createWriteHandler(\n config: WriteHandlerConfig\n): WriteHandler {\n return {\n async handleWrite(c: Context): Promise<Response> {\n // --- Parse request body ---\n let body: { event?: NostrEvent };\n try {\n body = (await c.req.json()) as { event?: NostrEvent };\n } catch {\n return c.json({ error: 'Invalid request body' }, 400);\n }\n\n if (!body.event) {\n return c.json({ error: 'Missing required field: event' }, 400);\n }\n\n const event = body.event;\n\n // --- Capture trusted payment headers (NOT validated here) ---\n const payer = c.req.header('X-TOON-Payer');\n const amount = c.req.header('X-TOON-Amount');\n const chain = c.req.header('X-TOON-Chain');\n\n console.log(\n `[write] event=${event.id} payer=${payer ?? '-'} amount=${amount ?? '-'} chain=${chain ?? '-'}`\n );\n\n // --- Verify event signature (integrity only; skipped in devMode) ---\n if (!config.devMode && !verifyEvent(event)) {\n return c.json({ error: 'Invalid event signature' }, 422);\n }\n\n // --- Store the event ---\n config.eventStore.store(event);\n\n // --- Fire the optional stored callback ---\n config.onStored?.(event);\n\n // --- Build response (echo trusted headers) ---\n return c.json(\n {\n eventId: event.id,\n storedAt: Math.floor(Date.now() / 1000),\n payer,\n amount,\n chain,\n },\n 200\n );\n },\n };\n}\n","/**\n * Health response for the relay's HTTP server.\n *\n * The relay is a plain read/write app (no payment, connector, or settlement\n * layer), so the health response is deliberately minimal: liveness plus the\n * node's identity and version. It is served from `GET /health` on the write\n * port and is the target of the container healthcheck.\n *\n * @module\n */\n\nimport { VERSION } from '../version.js';\n\n/** Configuration for building a health response. */\nexport interface HealthConfig {\n /** Node's Nostr pubkey (64-char hex). */\n pubkey: string;\n}\n\n/** The health response shape. */\nexport interface HealthResponse {\n status: 'healthy';\n pubkey: string;\n capabilities: string[];\n version: string;\n timestamp: number;\n}\n\n/**\n * Build a health response for the relay.\n *\n * Pure function: takes a config and returns the response object, so it is\n * trivially unit-testable and reusable.\n *\n * @param config - Health configuration (the node's pubkey).\n * @returns The health response object.\n */\nexport function createHealthResponse(config: HealthConfig): HealthResponse {\n return {\n status: 'healthy',\n pubkey: config.pubkey,\n capabilities: ['relay'],\n version: VERSION,\n timestamp: Date.now(),\n };\n}\n","/**\n * startRelay() -- Programmatic API for starting a TOON relay node.\n *\n * The relay is a plain HTTP/WebSocket app. It does NOT speak ILP and contains\n * no payment, connector, settlement, or pricing logic: payment is enforced\n * entirely upstream by an external terminator (see the connector repo). By the\n * time a write reaches this process it is already proven paid, so the relay\n * simply stores the event and serves reads.\n *\n * Two surfaces:\n *\n * - `POST /write` (TOON_BLS_PORT, default 3100): accepts `{ event }` as JSON,\n * trusts the injected `X-TOON-Payer`/`-Amount`/`-Chain` headers WITHOUT\n * re-validating payment, verifies only the event's own signature for\n * integrity, and stores it. `GET /health` lives on the same port.\n * - Free NIP-01 WebSocket reads (TOON_RELAY_PORT, default 7100).\n *\n * `startRelay()` returns a `RelayInstance` with an explicit `.stop()` for\n * lifecycle control (the CLI wraps this with process-signal handling).\n *\n * @module\n */\n\nimport { mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { serve, type ServerType } from '@hono/node-server';\nimport { Hono, type Context } from 'hono';\nimport { getPublicKey } from 'nostr-tools/pure';\nimport { privateKeyFromSeedWords } from 'nostr-tools/nip06';\nimport type { Filter } from 'nostr-tools/filter';\nimport { SqliteEventStore } from '../storage/index.js';\nimport type { EventStore } from '../storage/index.js';\nimport { NostrRelayServer } from '../websocket/index.js';\nimport { RelaySubscriber } from '../subscriber/index.js';\nimport { createWriteHandler } from './handlers/write-handler.js';\nimport { createHealthResponse } from './health.js';\n\n// ---------- Configuration ----------\n\n/**\n * Configuration for starting a TOON relay node via `startRelay()`.\n *\n * Exactly one of `mnemonic` or `secretKey` must be provided -- it is the node's\n * Nostr identity (surfaced on `/health`).\n */\nexport interface RelayConfig {\n // --- Identity (exactly one required) ---\n\n /** 12-word or 24-word BIP-39 mnemonic phrase (NIP-06 derivation). */\n mnemonic?: string;\n /** 32-byte secp256k1 secret key. */\n secretKey?: Uint8Array;\n\n // --- Network ---\n\n /** WebSocket relay (read) port (default: 7100). */\n relayPort?: number;\n /** HTTP write/health port (default: 3100). */\n blsPort?: number;\n /**\n * WebSocket bind host (default: 0.0.0.0). Set to `127.0.0.1` to bind the read\n * port to localhost only (e.g. when an upstream proxy handles inbound).\n */\n host?: string;\n\n // --- Storage ---\n\n /** Data directory for the file-backed SQLite store (default: ./data). */\n dataDir?: string;\n /**\n * Pre-built EventStore. When provided, the relay uses it instead of building\n * the default file-backed `SqliteEventStore` under `dataDir` (useful for\n * tests via `InMemoryEventStore`, or to share a store when embedding). The\n * caller owns its lifecycle when supplied.\n */\n eventStore?: EventStore;\n\n // --- Development ---\n\n /** Skip event-signature verification on `POST /write` (default: false). */\n devMode?: boolean;\n}\n\n/**\n * Resolved configuration with all defaults applied.\n */\nexport interface ResolvedRelayConfig {\n relayPort: number;\n blsPort: number;\n host: string;\n dataDir: string;\n devMode: boolean;\n}\n\n/**\n * A running TOON relay node instance returned by `startRelay()`.\n */\nexport interface RelayInstance {\n /** Whether the relay is currently running. */\n isRunning(): boolean;\n\n /** Gracefully stop the relay and release all resources. */\n stop(): Promise<void>;\n\n /**\n * Subscribe to a remote Nostr relay. Received events are stored in this\n * node's EventStore. Returns a handle for lifecycle management.\n *\n * @param relayUrl - WebSocket URL of the relay to subscribe to.\n * @param filter - Nostr filter (kinds, authors, etc.).\n * @returns A RelaySubscription handle.\n * @throws If the relay is not running.\n */\n subscribe(relayUrl: string, filter: Filter): RelaySubscription;\n\n /** The node's Nostr x-only public key (64-char hex). */\n pubkey: string;\n\n /** The resolved configuration with all defaults applied. */\n config: ResolvedRelayConfig;\n}\n\n/**\n * Handle for managing an outbound subscription to a remote Nostr relay.\n * Returned by `RelayInstance.subscribe()`.\n */\nexport interface RelaySubscription {\n /** Close the subscription and disconnect from the relay. */\n close(): void;\n /** The relay URL this subscription is connected to. */\n relayUrl: string;\n /** Whether this subscription is still active. */\n isActive(): boolean;\n}\n\n// ---------- Identity ----------\n\n/**\n * Derive the node's Nostr identity from the config. Exactly one of `mnemonic`\n * or `secretKey` must be set.\n *\n * @internal\n */\nfunction deriveIdentity(config: RelayConfig): {\n secretKey: Uint8Array;\n pubkey: string;\n} {\n const hasMnemonic = config.mnemonic !== undefined;\n const hasSecretKey = config.secretKey !== undefined;\n\n if (hasMnemonic && hasSecretKey) {\n throw new Error(\n 'RelayConfig: provide either mnemonic or secretKey, not both'\n );\n }\n if (!hasMnemonic && !hasSecretKey) {\n throw new Error('RelayConfig: one of mnemonic or secretKey is required');\n }\n\n const secretKey = hasMnemonic\n ? privateKeyFromSeedWords(config.mnemonic as string)\n : (config.secretKey as Uint8Array);\n\n return { secretKey, pubkey: getPublicKey(secretKey) };\n}\n\n// ---------- Subscription Helper ----------\n\n/**\n * Create a subscription to a remote Nostr relay, storing received events\n * in the local EventStore. Returns a RelaySubscription handle.\n *\n * @internal Exported for unit testing only. Use `RelayInstance.subscribe()` instead.\n */\nexport function createSubscription(\n relayUrl: string,\n filter: Filter,\n eventStore: EventStore,\n activeSubscriptions: Set<RelaySubscription>\n): RelaySubscription {\n // Validate WebSocket URL scheme to provide clear errors and prevent\n // non-WebSocket URLs from reaching SimplePool.\n // nosemgrep: javascript.lang.security.detect-insecure-websocket.detect-insecure-websocket -- validation check, not a connection\n if (!relayUrl.startsWith('ws://') && !relayUrl.startsWith('wss://')) {\n throw new Error(\n 'Invalid relay URL -- must use WebSocket scheme (ws or wss)'\n );\n }\n\n const subscriber = new RelaySubscriber(\n { relayUrls: [relayUrl], filter },\n eventStore\n );\n const handle = subscriber.start();\n\n let active = true;\n const subscription: RelaySubscription = {\n close() {\n if (!active) return;\n active = false;\n handle.unsubscribe();\n activeSubscriptions.delete(subscription);\n },\n relayUrl,\n isActive() {\n return active;\n },\n };\n\n activeSubscriptions.add(subscription);\n return subscription;\n}\n\n// ---------- Main API ----------\n\n/**\n * Start a TOON relay node with the given configuration.\n *\n * Wires the event store, the HTTP write/health server, and the NIP-01\n * WebSocket read server, then returns a `RelayInstance` for lifecycle control.\n *\n * @param config - Node configuration. One of `mnemonic`/`secretKey` is required.\n * @returns A running RelayInstance.\n * @throws If both or neither of mnemonic/secretKey are provided.\n *\n * @example\n * ```typescript\n * const relay = await startRelay({ secretKey });\n * // ... POST /write on 3100, read NIP-01 on 7100 ...\n * await relay.stop();\n * ```\n */\nexport async function startRelay(config: RelayConfig): Promise<RelayInstance> {\n // --- 1. Identity ---\n const identity = deriveIdentity(config);\n\n // --- 2. Resolve config ---\n const relayPort = config.relayPort ?? 7100;\n const blsPort = config.blsPort ?? 3100;\n const host = config.host ?? '0.0.0.0';\n const dataDir = config.dataDir ?? './data';\n const devMode = config.devMode ?? false;\n\n const resolvedConfig: ResolvedRelayConfig = {\n relayPort,\n blsPort,\n host,\n dataDir,\n devMode,\n };\n\n // --- 3. Event store ---\n // Use the injected store as-is, or build a file-backed SqliteEventStore.\n let eventStore: EventStore;\n if (config.eventStore) {\n eventStore = config.eventStore;\n } else {\n mkdirSync(dataDir, { recursive: true });\n eventStore = new SqliteEventStore(join(dataDir, 'events.db'));\n }\n\n // --- 4. WebSocket read server (created first so /write can broadcast) ---\n const wsRelay = new NostrRelayServer({ port: relayPort, host }, eventStore);\n\n // --- 5. HTTP write/health server ---\n const app = new Hono();\n\n app.get('/health', (c: Context) =>\n c.json(createHealthResponse({ pubkey: identity.pubkey }))\n );\n\n // POST /write: trust the upstream terminator's injected payment headers,\n // verify only the event signature, store, and broadcast to live WS readers.\n const writeHandler = createWriteHandler({\n eventStore,\n devMode,\n onStored: (event) => {\n try {\n wsRelay.broadcastEvent(event);\n } catch {\n // Non-broadcastable payloads -- ignore.\n }\n },\n });\n app.post('/write', (c: Context) => writeHandler.handleWrite(c));\n\n // Resolve once the HTTP server is actually listening so callers (and tests)\n // never race a not-yet-bound port.\n const blsServer: ServerType = await new Promise<ServerType>((resolve) => {\n const server = serve({ fetch: app.fetch, port: blsPort }, () =>\n resolve(server)\n );\n });\n\n // --- 6. Start the WS read server ---\n await wsRelay.start();\n\n // --- 7. Lifecycle ---\n let running = true;\n const activeSubscriptions = new Set<RelaySubscription>();\n\n const instance: RelayInstance = {\n isRunning() {\n return running;\n },\n\n subscribe(subscribeRelayUrl: string, filter: Filter): RelaySubscription {\n if (!running) {\n throw new Error('Cannot subscribe: relay is not running');\n }\n return createSubscription(\n subscribeRelayUrl,\n filter,\n eventStore,\n activeSubscriptions\n );\n },\n\n async stop() {\n if (!running) return;\n running = false;\n\n for (const sub of activeSubscriptions) {\n sub.close();\n }\n activeSubscriptions.clear();\n\n await wsRelay.stop();\n blsServer.close();\n\n // Only close a store we created; an injected store is the caller's.\n if (!config.eventStore) {\n eventStore.close?.();\n }\n },\n\n pubkey: identity.pubkey,\n config: resolvedConfig,\n };\n\n return instance;\n}\n"],"mappings":"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","names":["verifyEvent"]}