@toon-protocol/relay 2.0.1 → 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/README.md +34 -2
- package/dist/{chunk-FXQSNOCG.js → chunk-QZQRHQEQ.js} +356 -22
- package/dist/chunk-QZQRHQEQ.js.map +1 -0
- package/dist/chunk-SMT6G3XD.js +92 -0
- package/dist/chunk-SMT6G3XD.js.map +1 -0
- package/dist/cli.js +58 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +412 -7
- package/dist/index.js +25 -3
- package/dist/index.js.map +1 -1
- package/dist/verify-worker.d.ts +2 -0
- package/dist/verify-worker.js +17 -0
- package/dist/verify-worker.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-FXQSNOCG.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,17 @@ interface RelayServerConfig {
|
|
|
15
15
|
port: number;
|
|
16
16
|
/** Host/IP to bind to (default: '0.0.0.0'). Set to '127.0.0.1' for hidden service mode. */
|
|
17
17
|
host?: string;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Maximum concurrent WebSocket connections (default: 4096; relay#90).
|
|
20
|
+
*
|
|
21
|
+
* Each connection costs one file descriptor plus a few KB of handler
|
|
22
|
+
* state, so the practical ceiling is fd-limit-shaped, not memory-shaped.
|
|
23
|
+
* 4096 supports several hundred-listener huddles at once (the stock 100
|
|
24
|
+
* made >100 listeners impossible) while leaving comfortable headroom
|
|
25
|
+
* under docker's default nofile limit (1048576) AND still fitting under a
|
|
26
|
+
* conservative 8192 ulimit; on a classic 1024 soft limit the startup
|
|
27
|
+
* fd-limit check logs a warning (see NostrRelayServer.start).
|
|
28
|
+
*/
|
|
19
29
|
maxConnections?: number;
|
|
20
30
|
/** Maximum subscriptions per connection (default: 20) */
|
|
21
31
|
maxSubscriptionsPerConnection?: number;
|
|
@@ -71,6 +81,7 @@ declare class RelayError extends Error {
|
|
|
71
81
|
declare class SqliteEventStore implements EventStore {
|
|
72
82
|
private db;
|
|
73
83
|
private insertStmt;
|
|
84
|
+
private insertOrIgnoreStmt;
|
|
74
85
|
private getStmt;
|
|
75
86
|
private deleteByPubkeyKindStmt;
|
|
76
87
|
private deleteByPubkeyKindDTagStmt;
|
|
@@ -129,6 +140,139 @@ declare class SqliteEventStore implements EventStore {
|
|
|
129
140
|
*/
|
|
130
141
|
declare function matchFilter(event: NostrEvent, filter: Filter): boolean;
|
|
131
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Event-signature verification for the paid-write hot path (relay#85).
|
|
145
|
+
*
|
|
146
|
+
* nostr-tools' `verifyEvent` runs BIP-340 schnorr verification in pure-JS
|
|
147
|
+
* noble at ~1.3ms per event -- synchronously, on the relay's single Node
|
|
148
|
+
* event loop. Post-#84 that verify IS the write-path ceiling: the devnet
|
|
149
|
+
* relay caps at ~240-260 events/s aggregate, CPU-bound (connector#685
|
|
150
|
+
* Phase G measurements).
|
|
151
|
+
*
|
|
152
|
+
* This module verifies with libsecp256k1 compiled to WASM (`tiny-secp256k1`,
|
|
153
|
+
* ~0.20ms per event on the same workload -- ~7x faster) and keeps noble as a
|
|
154
|
+
* fallback: if the WASM module fails to load or fails a startup self-test on
|
|
155
|
+
* a known-good/known-bad vector pair, verification transparently degrades to
|
|
156
|
+
* `verifyEvent` from nostr-tools. The relay never hard-fails because of the
|
|
157
|
+
* fast path. WASM was chosen over a native addon deliberately: it needs no
|
|
158
|
+
* platform toolchain in the Docker build, and the `secp256k1` native-addon
|
|
159
|
+
* package exposes no schnorr API at all.
|
|
160
|
+
*
|
|
161
|
+
* Semantics match nostr-tools `verifyEvent` -- serialize per NIP-01, SHA-256,
|
|
162
|
+
* compare against `event.id`, then BIP-340-verify `event.sig` -- with one
|
|
163
|
+
* deliberate improvement: a structurally invalid event returns `false` here
|
|
164
|
+
* (nostr-tools' `getEventHash` throws instead). `verifyEventSignature` never
|
|
165
|
+
* throws.
|
|
166
|
+
*
|
|
167
|
+
* @module
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Which verify implementation is active. Surfaced so the launcher can log it
|
|
172
|
+
* once at startup (the noble fallback is a silent ~7x throughput loss
|
|
173
|
+
* otherwise).
|
|
174
|
+
*/
|
|
175
|
+
declare const verifyImplementation: 'libsecp256k1-wasm' | 'noble-pure-js';
|
|
176
|
+
/**
|
|
177
|
+
* Verify ONLY that `event.id` is the correct NIP-01 SHA-256 hash of the
|
|
178
|
+
* event's serialized form. Does NOT check the schnorr signature and does NOT
|
|
179
|
+
* stamp nostr-tools' `verifiedSymbol` cache (an id check is not a signature
|
|
180
|
+
* verdict).
|
|
181
|
+
*
|
|
182
|
+
* This is the integrity floor for the paid-ephemeral skip-verify path
|
|
183
|
+
* (relay#85): when the relay skips schnorr for payment-gated ephemeral kinds,
|
|
184
|
+
* it still refuses events whose id does not match their content, so a paid
|
|
185
|
+
* writer cannot make the relay broadcast a frame whose bytes disagree with
|
|
186
|
+
* the id that clients index/verify by.
|
|
187
|
+
*
|
|
188
|
+
* Never throws; structurally invalid events return false.
|
|
189
|
+
*
|
|
190
|
+
* @param event - The event whose id to check.
|
|
191
|
+
* @returns True iff `event.id` equals the NIP-01 SHA-256 of the event.
|
|
192
|
+
*/
|
|
193
|
+
declare function verifyEventId(event: NostrEvent): boolean;
|
|
194
|
+
/**
|
|
195
|
+
* Verify a Nostr event's id and BIP-340 signature.
|
|
196
|
+
*
|
|
197
|
+
* Drop-in replacement for nostr-tools' `verifyEvent` on the write hot path:
|
|
198
|
+
* same accept/reject semantics for well-formed events, ~7x faster via WASM
|
|
199
|
+
* libsecp256k1 when available, noble pure-JS otherwise. Never throws;
|
|
200
|
+
* structurally invalid events return false.
|
|
201
|
+
*
|
|
202
|
+
* @param event - The event to verify.
|
|
203
|
+
* @returns True iff the id matches the NIP-01 hash and the signature is valid.
|
|
204
|
+
*/
|
|
205
|
+
declare function verifyEventSignature(event: NostrEvent): boolean;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Worker-thread pool for event-signature verification (relay#85).
|
|
209
|
+
*
|
|
210
|
+
* Post-#87 the WASM verify takes ~0.2ms per event -- but it still runs ON the
|
|
211
|
+
* single Node event loop, which also carries every WebSocket broadcast.
|
|
212
|
+
* Agent writers make persistent-kind write rates potentially bursty and high;
|
|
213
|
+
* a verify burst on the loop is exactly the kind of stall that shows up as
|
|
214
|
+
* tail jitter on ephemeral (huddle-frame) latency. The pool moves persistent
|
|
215
|
+
* -kind schnorr verification onto worker threads so verify bursts cannot
|
|
216
|
+
* stall the loop, at the cost of one thread-hop per verified event.
|
|
217
|
+
*
|
|
218
|
+
* Shape:
|
|
219
|
+
* - `size` workers (default `max(0, os.cpus().length - 1)`); each worker
|
|
220
|
+
* imports `verify-event.ts` and therefore instantiates its OWN WASM
|
|
221
|
+
* libsecp256k1 (same self-test + noble fallback semantics as inline).
|
|
222
|
+
* - `size: 0` -- automatic on 1-core boxes, and the explicit config escape
|
|
223
|
+
* hatch (TOON_VERIFY_WORKERS=0) -- keeps the current inline path: `verify`
|
|
224
|
+
* resolves synchronously-computed results, no threads are created.
|
|
225
|
+
* - Dispatch is least-busy; per-call results resolve independently.
|
|
226
|
+
* ORDERING: results for CONCURRENT calls may settle out of submission
|
|
227
|
+
* order. The write path stays correct because the upstream connector
|
|
228
|
+
* serializes each BTP session's POSTs (next request only after the
|
|
229
|
+
* previous response) -- pinned by tests in write-handler.test.ts.
|
|
230
|
+
* - Worker failure degrades transparently: pending and future verifies fall
|
|
231
|
+
* back to the inline implementation (the pool never hard-fails a write).
|
|
232
|
+
* - Hand-rolled on `node:worker_threads` -- ~100 lines beats a piscina
|
|
233
|
+
* dependency in a package whose runtime deps are deliberately minimal.
|
|
234
|
+
*
|
|
235
|
+
* @module
|
|
236
|
+
*/
|
|
237
|
+
|
|
238
|
+
/** A pool that verifies event signatures off the main event loop. */
|
|
239
|
+
interface VerifyPool {
|
|
240
|
+
/**
|
|
241
|
+
* Verify an event's id + BIP-340 signature. Never rejects; invalid or
|
|
242
|
+
* structurally broken events resolve `false`. Semantics match
|
|
243
|
+
* `verifyEventSignature`, including stamping the nostr-tools
|
|
244
|
+
* verified-event cache symbol on the caller's object.
|
|
245
|
+
*/
|
|
246
|
+
verify(event: NostrEvent): Promise<boolean>;
|
|
247
|
+
/** Live worker count (0 = inline path). */
|
|
248
|
+
readonly size: number;
|
|
249
|
+
/** Terminate all workers. Idempotent. Pending verifies resolve inline. */
|
|
250
|
+
destroy(): Promise<void>;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Default pool size: one worker per CPU minus one core reserved for the
|
|
254
|
+
* event loop (WS fan-out + HTTP). On a 1-core box this is 0 -- the inline
|
|
255
|
+
* path -- because a worker would only add thread-hop overhead while
|
|
256
|
+
* competing for the same core.
|
|
257
|
+
*/
|
|
258
|
+
declare function defaultVerifyWorkers(): number;
|
|
259
|
+
/** Options for {@link createVerifyPool}. */
|
|
260
|
+
interface VerifyPoolOptions {
|
|
261
|
+
/** Worker count (default {@link defaultVerifyWorkers}; 0 = inline). */
|
|
262
|
+
size?: number;
|
|
263
|
+
/**
|
|
264
|
+
* Called with the wall-clock milliseconds of each verify -- including
|
|
265
|
+
* pool queue + thread-hop time, i.e. the latency a write actually paid.
|
|
266
|
+
* The launcher wires this into the /metrics registry.
|
|
267
|
+
*/
|
|
268
|
+
onMeasure?: (ms: number) => void;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Create a verify pool. See the module doc for semantics; see
|
|
272
|
+
* {@link VerifyPoolOptions} for knobs.
|
|
273
|
+
*/
|
|
274
|
+
declare function createVerifyPool(options?: VerifyPoolOptions): VerifyPool;
|
|
275
|
+
|
|
132
276
|
/**
|
|
133
277
|
* Represents an active subscription from a client.
|
|
134
278
|
*/
|
|
@@ -138,6 +282,21 @@ interface Subscription {
|
|
|
138
282
|
/** Filters applied to this subscription */
|
|
139
283
|
filters: Filter[];
|
|
140
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Build a NIP-01 EVENT frame from an ALREADY-SERIALIZED event payload,
|
|
287
|
+
* splicing in the (JSON-escaped) subscription id.
|
|
288
|
+
*
|
|
289
|
+
* Byte-identical to `JSON.stringify(['EVENT', subscriptionId, event])` --
|
|
290
|
+
* pinned by tests -- but lets the broadcast fan-out serialize the event
|
|
291
|
+
* ONCE and reuse the string across every matching subscriber (relay#91:
|
|
292
|
+
* 500 subscribers previously meant 500 identical `JSON.stringify(event)`
|
|
293
|
+
* calls per frame, measured pinning a core in the s500 benchmark run).
|
|
294
|
+
*
|
|
295
|
+
* @param subscriptionId - The per-subscriber NIP-01 subscription id.
|
|
296
|
+
* @param eventJson - `JSON.stringify(event)` output to reuse.
|
|
297
|
+
* @returns The full EVENT frame string for the wire.
|
|
298
|
+
*/
|
|
299
|
+
declare function serializeEventFrame(subscriptionId: string, eventJson: string): string;
|
|
141
300
|
/**
|
|
142
301
|
* Handles NIP-01 messages for a single WebSocket connection.
|
|
143
302
|
*/
|
|
@@ -170,8 +329,15 @@ declare class ConnectionHandler {
|
|
|
170
329
|
/**
|
|
171
330
|
* Push a new event to all matching subscriptions on this connection.
|
|
172
331
|
* Used when events are stored outside the WebSocket flow (e.g., via ILP).
|
|
332
|
+
*
|
|
333
|
+
* @param event - The event to fan out (used for filter matching).
|
|
334
|
+
* @param eventJson - Optional pre-serialized `JSON.stringify(event)`.
|
|
335
|
+
* `NostrRelayServer.broadcastEvent` serializes the event ONCE and passes
|
|
336
|
+
* it here so a 500-subscriber fan-out costs one serialization, not 500
|
|
337
|
+
* (relay#91). When omitted (direct callers), the event is serialized
|
|
338
|
+
* on first matching send.
|
|
173
339
|
*/
|
|
174
|
-
notifyNewEvent(event: NostrEvent): void;
|
|
340
|
+
notifyNewEvent(event: NostrEvent, eventJson?: string): void;
|
|
175
341
|
/**
|
|
176
342
|
* Clean up all subscriptions for this connection.
|
|
177
343
|
*/
|
|
@@ -188,14 +354,24 @@ declare class ConnectionHandler {
|
|
|
188
354
|
* with the event as a plain JSON object — so any standard nostr client can
|
|
189
355
|
* parse it and verify `id`/`sig` from the wire bytes (#46). Never re-encode
|
|
190
356
|
* the event (TOON text, double-JSON-stringify, etc.) at this boundary.
|
|
357
|
+
* serializeEventFrame is byte-identical to the full JSON.stringify
|
|
358
|
+
* envelope (pinned by tests).
|
|
191
359
|
*/
|
|
192
360
|
private sendEvent;
|
|
193
361
|
private sendEose;
|
|
194
362
|
private sendOk;
|
|
195
363
|
private sendNotice;
|
|
364
|
+
/** Send a message: pre-serialized frames go out as-is (relay#91). */
|
|
196
365
|
private send;
|
|
197
366
|
}
|
|
198
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Read this process's soft "Max open files" limit from /proc/self/limits.
|
|
370
|
+
* Returns null off-Linux or on any parse failure (the check is advisory).
|
|
371
|
+
*
|
|
372
|
+
* @internal Exported for unit testing.
|
|
373
|
+
*/
|
|
374
|
+
declare function readOpenFilesSoftLimit(read?: (path: string) => string): number | null;
|
|
199
375
|
/**
|
|
200
376
|
* A NIP-01 compliant Nostr relay WebSocket server.
|
|
201
377
|
* Handles client connections and routes messages to ConnectionHandlers.
|
|
@@ -227,6 +403,12 @@ declare class NostrRelayServer {
|
|
|
227
403
|
* Broadcast an event to all connected clients with matching subscriptions.
|
|
228
404
|
* Call this after storing an event outside the WebSocket flow (e.g., via ILP)
|
|
229
405
|
* so that discovery subscribers are notified.
|
|
406
|
+
*
|
|
407
|
+
* Serialize-once fan-out (relay#91): the event payload is stringified ONE
|
|
408
|
+
* time here and reused for every matching subscriber -- only the small
|
|
409
|
+
* per-subscription `["EVENT",<subId>,...]` envelope is spliced per send.
|
|
410
|
+
* Previously each of N subscribers re-serialized the identical event
|
|
411
|
+
* (N=500 pinned a core doing 500 identical stringifies per frame).
|
|
230
412
|
*/
|
|
231
413
|
broadcastEvent(event: NostrEvent): void;
|
|
232
414
|
private handleConnection;
|
|
@@ -325,7 +507,9 @@ declare class RelaySubscriber {
|
|
|
325
507
|
* - `POST /write` (TOON_BLS_PORT, default 3100): accepts `{ event }` as JSON,
|
|
326
508
|
* trusts the injected `X-TOON-Payer`/`-Amount`/`-Chain` headers WITHOUT
|
|
327
509
|
* re-validating payment, verifies only the event's own signature for
|
|
328
|
-
* integrity
|
|
510
|
+
* integrity (paid ephemeral kinds skip schnorr by default and keep the
|
|
511
|
+
* id check -- relay#85, see `verifyEphemeral`), and stores it.
|
|
512
|
+
* `GET /health` and `GET /metrics` live on the same port.
|
|
329
513
|
* - Free NIP-01 WebSocket reads (TOON_RELAY_PORT, default 7100).
|
|
330
514
|
*
|
|
331
515
|
* `startRelay()` returns a `RelayInstance` with an explicit `.stop()` for
|
|
@@ -354,6 +538,25 @@ interface RelayConfig {
|
|
|
354
538
|
* port to localhost only (e.g. when an upstream proxy handles inbound).
|
|
355
539
|
*/
|
|
356
540
|
host?: string;
|
|
541
|
+
/**
|
|
542
|
+
* Bind host for the HTTP write/health listener (default: 0.0.0.0).
|
|
543
|
+
*
|
|
544
|
+
* The write port MUST only be reachable via the payment-gating connector
|
|
545
|
+
* (see `verifyEphemeral`): in the canonical compose deploy that is enforced
|
|
546
|
+
* by NOT host-publishing the port (docker `expose:`, never `ports:` --
|
|
547
|
+
* note that docker `ports:` publishes bypass ufw). When the relay runs
|
|
548
|
+
* directly on a host, bind this to a loopback/internal address instead.
|
|
549
|
+
* A non-internal bind while the ephemeral verify skip is active logs a
|
|
550
|
+
* prominent startup warning (never a hard failure -- topologies vary).
|
|
551
|
+
*/
|
|
552
|
+
writeHost?: string;
|
|
553
|
+
/**
|
|
554
|
+
* Maximum concurrent WebSocket read connections (default: 4096; env:
|
|
555
|
+
* TOON_MAX_CONNECTIONS). Connections beyond the cap are closed with 1013.
|
|
556
|
+
* Fd-limit-shaped, not memory-shaped -- see RelayServerConfig
|
|
557
|
+
* .maxConnections for the sizing reasoning (relay#90).
|
|
558
|
+
*/
|
|
559
|
+
maxConnections?: number;
|
|
357
560
|
/** Data directory for the file-backed SQLite store (default: ./data). */
|
|
358
561
|
dataDir?: string;
|
|
359
562
|
/**
|
|
@@ -365,6 +568,33 @@ interface RelayConfig {
|
|
|
365
568
|
eventStore?: EventStore;
|
|
366
569
|
/** Skip event-signature verification on `POST /write` (default: false). */
|
|
367
570
|
devMode?: boolean;
|
|
571
|
+
/**
|
|
572
|
+
* Run FULL schnorr verification on ephemeral kinds (default: false -- the
|
|
573
|
+
* paid-ephemeral verify skip is ON by default, relay#85).
|
|
574
|
+
*
|
|
575
|
+
* The default skip is safe ONLY because `POST /write` is payment-gated by
|
|
576
|
+
* the upstream connector and clients verify every signature themselves;
|
|
577
|
+
* the SHA-256 event-id check always runs. Community operators fronting the
|
|
578
|
+
* write port with anything other than a payment-gating connector should
|
|
579
|
+
* set this to true (env: TOON_VERIFY_EPHEMERAL=true). See
|
|
580
|
+
* `WriteHandlerConfig.verifyEphemeral` for the full invariant.
|
|
581
|
+
*/
|
|
582
|
+
verifyEphemeral?: boolean;
|
|
583
|
+
/**
|
|
584
|
+
* Worker-thread verify pool size for persistent-kind signature
|
|
585
|
+
* verification (default: `max(0, os.cpus().length - 1)`; env:
|
|
586
|
+
* TOON_VERIFY_WORKERS). `0` -- automatic on 1-core boxes -- is the inline
|
|
587
|
+
* escape hatch: verification runs synchronously on the event loop as
|
|
588
|
+
* before. Workers keep bursty agent-writer verify load from stalling the
|
|
589
|
+
* loop and jittering ephemeral frame latency (relay#85).
|
|
590
|
+
*/
|
|
591
|
+
verifyWorkers?: number;
|
|
592
|
+
/**
|
|
593
|
+
* Log one line per accepted `POST /write` (default: false). Per-event
|
|
594
|
+
* console I/O is measurable tail jitter on the write hot path (relay#85),
|
|
595
|
+
* so this is a debug switch, not an access log.
|
|
596
|
+
*/
|
|
597
|
+
logWrites?: boolean;
|
|
368
598
|
}
|
|
369
599
|
/**
|
|
370
600
|
* Resolved configuration with all defaults applied.
|
|
@@ -373,8 +603,13 @@ interface ResolvedRelayConfig {
|
|
|
373
603
|
relayPort: number;
|
|
374
604
|
blsPort: number;
|
|
375
605
|
host: string;
|
|
606
|
+
writeHost: string;
|
|
607
|
+
maxConnections: number;
|
|
376
608
|
dataDir: string;
|
|
377
609
|
devMode: boolean;
|
|
610
|
+
verifyEphemeral: boolean;
|
|
611
|
+
verifyWorkers: number;
|
|
612
|
+
logWrites: boolean;
|
|
378
613
|
}
|
|
379
614
|
/**
|
|
380
615
|
* A running TOON relay node instance returned by `startRelay()`.
|
|
@@ -411,6 +646,34 @@ interface RelaySubscription {
|
|
|
411
646
|
/** Whether this subscription is still active. */
|
|
412
647
|
isActive(): boolean;
|
|
413
648
|
}
|
|
649
|
+
/**
|
|
650
|
+
* Whether `host` is a bind address that cannot be reached from the public
|
|
651
|
+
* internet directly: loopback, RFC1918 private, IPv6 unique-local/link-local.
|
|
652
|
+
* `0.0.0.0` / `::` (all interfaces) and public addresses return false.
|
|
653
|
+
*
|
|
654
|
+
* Used by the startup exposure guard: with the paid-ephemeral verify skip
|
|
655
|
+
* active, the write port must only be reachable via the payment-gating
|
|
656
|
+
* connector. Note a "false" here is not proof of exposure -- inside a
|
|
657
|
+
* container, 0.0.0.0 is required for the connector to dial the compose
|
|
658
|
+
* network and the port is kept private by not host-publishing it -- which is
|
|
659
|
+
* why the guard warns instead of failing.
|
|
660
|
+
*
|
|
661
|
+
* @internal Exported for unit testing.
|
|
662
|
+
*/
|
|
663
|
+
declare function isInternalBindHost(host: string): boolean;
|
|
664
|
+
/**
|
|
665
|
+
* Log the prominent write-port exposure warning when the paid-ephemeral
|
|
666
|
+
* verify skip is active and the write listener binds a non-internal
|
|
667
|
+
* interface. Deliberately a warning, not a hard failure: in the canonical
|
|
668
|
+
* compose deploy the port binds 0.0.0.0 inside the container and is private
|
|
669
|
+
* because it is never host-published (docker `expose:`, not `ports:`).
|
|
670
|
+
*
|
|
671
|
+
* @internal Exported for unit testing.
|
|
672
|
+
*/
|
|
673
|
+
declare function warnIfWritePortExposed(writeHost: string, blsPort: number, options: {
|
|
674
|
+
verifyEphemeral: boolean;
|
|
675
|
+
devMode: boolean;
|
|
676
|
+
}): boolean;
|
|
414
677
|
/**
|
|
415
678
|
* Start a TOON relay node with the given configuration.
|
|
416
679
|
*
|
|
@@ -464,6 +727,87 @@ interface HealthResponse {
|
|
|
464
727
|
*/
|
|
465
728
|
declare function createHealthResponse(config: HealthConfig): HealthResponse;
|
|
466
729
|
|
|
730
|
+
/**
|
|
731
|
+
* Metrics for the relay's HTTP telemetry surface (relay#85).
|
|
732
|
+
*
|
|
733
|
+
* Served as JSON from `GET /metrics` on the write/health port, next to
|
|
734
|
+
* `/health`. Two families, chosen because they are the TRIGGER METRICS for
|
|
735
|
+
* future scaling decisions (2026-08-02 benchmarking, toon-meta
|
|
736
|
+
* proto/spacetimedb-relay RESULTS.md):
|
|
737
|
+
*
|
|
738
|
+
* - **Event-loop delay**: the single Node loop carries every WS broadcast;
|
|
739
|
+
* loop lag IS ephemeral (huddle-frame) tail latency. Sustained p99 growth
|
|
740
|
+
* here is the signal to shed load or scale out.
|
|
741
|
+
* - **Per-event verify time**: wall-clock ms per signature verification as
|
|
742
|
+
* the write actually paid it (including verify-pool queue + thread-hop
|
|
743
|
+
* when workers are enabled). Growth here is the signal to resize the
|
|
744
|
+
* verify pool (TOON_VERIFY_WORKERS) or move boxes.
|
|
745
|
+
*
|
|
746
|
+
* The registry is deliberately dependency-free: `monitorEventLoopDelay`
|
|
747
|
+
* from node:perf_hooks plus a fixed-size ring of recent verify durations
|
|
748
|
+
* (percentiles over the last {@link VERIFY_WINDOW} samples -- bounded
|
|
749
|
+
* memory, O(window log window) only when a snapshot is requested).
|
|
750
|
+
*
|
|
751
|
+
* @module
|
|
752
|
+
*/
|
|
753
|
+
/** Aggregates for one duration family, in milliseconds. */
|
|
754
|
+
interface DurationStats {
|
|
755
|
+
/** Total samples recorded since startup. */
|
|
756
|
+
count: number;
|
|
757
|
+
/** Mean over ALL samples since startup. */
|
|
758
|
+
meanMs: number;
|
|
759
|
+
/** Max over ALL samples since startup. */
|
|
760
|
+
maxMs: number;
|
|
761
|
+
/** Median over the most recent window (up to {@link VERIFY_WINDOW}). */
|
|
762
|
+
p50Ms: number;
|
|
763
|
+
/** 99th percentile over the most recent window. */
|
|
764
|
+
p99Ms: number;
|
|
765
|
+
}
|
|
766
|
+
/** The `GET /metrics` response shape. */
|
|
767
|
+
interface MetricsSnapshot {
|
|
768
|
+
timestamp: number;
|
|
769
|
+
/**
|
|
770
|
+
* Event-loop delay in ms (node:perf_hooks monitorEventLoopDelay since the
|
|
771
|
+
* last snapshot reset -- lifetime of the process unless noted). `mean`,
|
|
772
|
+
* `p50`, `p99`, `max` -- loop lag is ephemeral-frame tail latency.
|
|
773
|
+
*/
|
|
774
|
+
eventLoopDelayMs: {
|
|
775
|
+
mean: number;
|
|
776
|
+
p50: number;
|
|
777
|
+
p99: number;
|
|
778
|
+
max: number;
|
|
779
|
+
};
|
|
780
|
+
/** Per-event signature-verify timing (trigger metric for pool sizing). */
|
|
781
|
+
verify: {
|
|
782
|
+
/** Active implementation: 'libsecp256k1-wasm' or 'noble-pure-js'. */
|
|
783
|
+
implementation: string;
|
|
784
|
+
/** Verify-pool worker count (0 = inline on the event loop). */
|
|
785
|
+
workers: number;
|
|
786
|
+
} & DurationStats;
|
|
787
|
+
}
|
|
788
|
+
/** Live registry behind `GET /metrics`. */
|
|
789
|
+
interface MetricsRegistry {
|
|
790
|
+
/** Record one verify duration in milliseconds. */
|
|
791
|
+
recordVerify(ms: number): void;
|
|
792
|
+
/** Build the current snapshot (cheap; safe to poll). */
|
|
793
|
+
snapshot(): MetricsSnapshot;
|
|
794
|
+
/** Update the reported worker count (pool may degrade at runtime). */
|
|
795
|
+
setVerifyWorkers(workers: number): void;
|
|
796
|
+
/** Disable the loop-delay histogram (call on relay stop). */
|
|
797
|
+
stop(): void;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Create the metrics registry. One per relay instance; the launcher wires
|
|
801
|
+
* `recordVerify` into the verify pool's `onMeasure` and serves `snapshot()`
|
|
802
|
+
* from `GET /metrics`.
|
|
803
|
+
*
|
|
804
|
+
* @param info - Static verify metadata surfaced in the snapshot.
|
|
805
|
+
*/
|
|
806
|
+
declare function createMetricsRegistry(info: {
|
|
807
|
+
verifyImplementation: string;
|
|
808
|
+
verifyWorkers: number;
|
|
809
|
+
}): MetricsRegistry;
|
|
810
|
+
|
|
467
811
|
/**
|
|
468
812
|
* Write handler for @toon-protocol/relay.
|
|
469
813
|
*
|
|
@@ -480,9 +824,26 @@ declare function createHealthResponse(config: HealthConfig): HealthResponse;
|
|
|
480
824
|
* Flow:
|
|
481
825
|
* 1. Parse JSON body `{ event }` -> 400 on malformed/missing event
|
|
482
826
|
* 2. Capture trusted X-TOON-Payer / X-TOON-Amount / X-TOON-Chain headers
|
|
483
|
-
* 3. Verify the event signature (skipped in devMode) -> 422 on invalid sig
|
|
484
|
-
*
|
|
485
|
-
*
|
|
827
|
+
* 3. Verify the event signature (skipped in devMode) -> 422 on invalid sig.
|
|
828
|
+
* Verification uses the fast WASM libsecp256k1 path (crypto/verify-event)
|
|
829
|
+
* rather than noble pure-JS: post-#84 the synchronous ~1.3ms noble verify
|
|
830
|
+
* on the single event loop WAS the write-path ceiling (~240-260 events/s
|
|
831
|
+
* aggregate, relay#85 / connector#685 Phase G).
|
|
832
|
+
* PAID-EPHEMERAL EXCEPTION (relay#85, decision 2026-08-02): for ephemeral
|
|
833
|
+
* kinds (20000 <= kind < 30000) the schnorr verification is SKIPPED by
|
|
834
|
+
* default -- only the SHA-256 id check runs -> 422 on id mismatch. See
|
|
835
|
+
* the loud comment at the verify step for why this is safe, and
|
|
836
|
+
* `verifyEphemeral` to turn full verification back on.
|
|
837
|
+
* 4. Store the event in the EventStore -- unless its kind is ephemeral
|
|
838
|
+
* (NIP-16: 20000 <= kind < 30000), which is delivered live and never
|
|
839
|
+
* persisted. Skipping the store here is not only NIP-16 semantics: the
|
|
840
|
+
* synchronous per-event disk write was the serialization point that
|
|
841
|
+
* capped the whole paid-write pipeline at ~150 events/s globally
|
|
842
|
+
* (connector#685), and ephemeral traffic -- audio frames -- is exactly
|
|
843
|
+
* the traffic that hits that path hardest.
|
|
844
|
+
* 5. Fire the optional onStored callback (ephemeral events included: it is
|
|
845
|
+
* the live-broadcast hook, and ephemeral events exist only as that
|
|
846
|
+
* broadcast)
|
|
486
847
|
* 6. Respond 200 with the event id, storedAt timestamp, and echoed headers
|
|
487
848
|
*
|
|
488
849
|
* @module
|
|
@@ -496,8 +857,52 @@ interface WriteHandlerConfig {
|
|
|
496
857
|
eventStore: EventStore;
|
|
497
858
|
/** Whether dev mode is enabled (skips Schnorr signature verification). */
|
|
498
859
|
devMode: boolean;
|
|
860
|
+
/**
|
|
861
|
+
* Run FULL schnorr verification on ephemeral kinds too (default: false --
|
|
862
|
+
* i.e. the paid-ephemeral verify skip is ON by default).
|
|
863
|
+
*
|
|
864
|
+
* ! SECURITY INVARIANT -- READ BEFORE TOUCHING !
|
|
865
|
+
* The default skip is safe ONLY because this write surface is payment-gated:
|
|
866
|
+
* every request reaching `POST /write` has already passed the upstream
|
|
867
|
+
* connector's claim gate (payment IS the admission/spam gate), and the
|
|
868
|
+
* protocol rule is that clients trust the signature chain and verify every
|
|
869
|
+
* event themselves -- never the relay. Relay-side schnorr on ephemeral
|
|
870
|
+
* frames is therefore pure spam defense that payment already provides;
|
|
871
|
+
* forging a speaker costs real money to emit frames every client discards.
|
|
872
|
+
* The SHA-256 id check is ALWAYS kept (see handleWrite).
|
|
873
|
+
*
|
|
874
|
+
* If you ever add a FREE (non-payment-gated) ephemeral write lane, it MUST
|
|
875
|
+
* NOT reuse this skip -- free spam with valid-looking ids would be
|
|
876
|
+
* broadcast to every subscriber. Community operators who front this port
|
|
877
|
+
* with anything other than a payment-gating connector should set
|
|
878
|
+
* `verifyEphemeral: true` (TOON_VERIFY_EPHEMERAL=true).
|
|
879
|
+
*/
|
|
880
|
+
verifyEphemeral?: boolean;
|
|
881
|
+
/**
|
|
882
|
+
* Signature verifier for non-skipped (persistent-kind) events. Defaults to
|
|
883
|
+
* the inline `verifyEventSignature`; the launcher injects the worker-pool
|
|
884
|
+
* verifier (`crypto/verify-pool.ts`) so verify bursts run off the event
|
|
885
|
+
* loop (relay#85). May resolve asynchronously -- the handler awaits it.
|
|
886
|
+
*
|
|
887
|
+
* ORDERING NOTE: an async verifier means CONCURRENT requests can complete
|
|
888
|
+
* out of arrival order. Per-session write ordering is enforced UPSTREAM:
|
|
889
|
+
* the connector serializes each BTP session's POSTs (it does not send the
|
|
890
|
+
* next request until the previous response arrives), so sequential
|
|
891
|
+
* same-session writes can never reorder here. Pinned by the ordering test
|
|
892
|
+
* in write-handler.test.ts -- do not weaken that contract upstream without
|
|
893
|
+
* revisiting this.
|
|
894
|
+
*/
|
|
895
|
+
verifyEvent?: (event: NostrEvent) => Promise<boolean> | boolean;
|
|
499
896
|
/** Optional callback fired after an event is successfully stored. */
|
|
500
897
|
onStored?: (event: NostrEvent) => void;
|
|
898
|
+
/**
|
|
899
|
+
* Log one line per accepted write (default: false). Off by default because
|
|
900
|
+
* per-event console I/O on the single event loop is measurable tail jitter
|
|
901
|
+
* at huddle frame rates (relay#85, connector#685 Phase G): every write's
|
|
902
|
+
* log line goes through docker's json-file driver, i.e. residual per-event
|
|
903
|
+
* disk I/O that #84 did not remove.
|
|
904
|
+
*/
|
|
905
|
+
logWrites?: boolean;
|
|
501
906
|
}
|
|
502
907
|
/**
|
|
503
908
|
* Write handler instance.
|
|
@@ -514,4 +919,4 @@ interface WriteHandler {
|
|
|
514
919
|
*/
|
|
515
920
|
declare function createWriteHandler(config: WriteHandlerConfig): WriteHandler;
|
|
516
921
|
|
|
517
|
-
export { ConnectionHandler, DEFAULT_RELAY_CONFIG, type EventStore, type HealthConfig, type HealthResponse, InMemoryEventStore, NostrRelayServer, type RelayConfig, RelayError, type RelayInstance, type RelayServerConfig, RelaySubscriber, type RelaySubscriberConfig, type RelaySubscription, type ResolvedRelayConfig, SqliteEventStore, type Subscription, ToonDecodeError, ToonEncodeError, VERSION, type WriteHandler, type WriteHandlerConfig, createHealthResponse, createWriteHandler, decodeEventFromToon, encodeEventToToon, matchFilter, startRelay };
|
|
922
|
+
export { ConnectionHandler, DEFAULT_RELAY_CONFIG, type DurationStats, type EventStore, type HealthConfig, type HealthResponse, InMemoryEventStore, type MetricsRegistry, type MetricsSnapshot, NostrRelayServer, type RelayConfig, RelayError, type RelayInstance, type RelayServerConfig, RelaySubscriber, type RelaySubscriberConfig, type RelaySubscription, type ResolvedRelayConfig, SqliteEventStore, type Subscription, ToonDecodeError, ToonEncodeError, VERSION, type VerifyPool, type VerifyPoolOptions, type WriteHandler, type WriteHandlerConfig, createHealthResponse, createMetricsRegistry, createVerifyPool, createWriteHandler, decodeEventFromToon, defaultVerifyWorkers, encodeEventToToon, isInternalBindHost, matchFilter, readOpenFilesSoftLimit, serializeEventFrame, startRelay, verifyEventId, verifyEventSignature, verifyImplementation, warnIfWritePortExposed };
|
package/dist/index.js
CHANGED
|
@@ -8,10 +8,22 @@ import {
|
|
|
8
8
|
SqliteEventStore,
|
|
9
9
|
VERSION,
|
|
10
10
|
createHealthResponse,
|
|
11
|
+
createMetricsRegistry,
|
|
12
|
+
createVerifyPool,
|
|
11
13
|
createWriteHandler,
|
|
14
|
+
defaultVerifyWorkers,
|
|
15
|
+
isInternalBindHost,
|
|
12
16
|
matchFilter,
|
|
13
|
-
|
|
14
|
-
|
|
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";
|
|
15
27
|
|
|
16
28
|
// src/toon/codec.ts
|
|
17
29
|
import { encode, decode } from "@toon-format/toon";
|
|
@@ -117,10 +129,20 @@ export {
|
|
|
117
129
|
ToonEncodeError,
|
|
118
130
|
VERSION,
|
|
119
131
|
createHealthResponse,
|
|
132
|
+
createMetricsRegistry,
|
|
133
|
+
createVerifyPool,
|
|
120
134
|
createWriteHandler,
|
|
121
135
|
decodeEventFromToon,
|
|
136
|
+
defaultVerifyWorkers,
|
|
122
137
|
encodeEventToToon,
|
|
138
|
+
isInternalBindHost,
|
|
123
139
|
matchFilter,
|
|
124
|
-
|
|
140
|
+
readOpenFilesSoftLimit,
|
|
141
|
+
serializeEventFrame,
|
|
142
|
+
startRelay,
|
|
143
|
+
verifyEventId,
|
|
144
|
+
verifyEventSignature,
|
|
145
|
+
verifyImplementation,
|
|
146
|
+
warnIfWritePortExposed
|
|
125
147
|
};
|
|
126
148
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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":"
|
|
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,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.
|
|
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": {
|