@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/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
- /** Maximum concurrent connections (default: 100) */
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, and stores it. `GET /health` lives on the same port.
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
- * 4. Store the event in the EventStore
485
- * 5. Fire the optional onStored callback
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
- startRelay
14
- } from "./chunk-FXQSNOCG.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";
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
- startRelay
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":"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","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.1",
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": {