@toon-protocol/relay 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @toon-protocol/relay
2
+
3
+ ILP-gated Nostr relay with Business Logic Server.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @toon-protocol/relay
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import {
15
+ NostrRelayServer,
16
+ BusinessLogicServer,
17
+ PricingService,
18
+ SqliteEventStore,
19
+ } from '@toon-protocol/relay';
20
+ ```
21
+
22
+ ## License
23
+
24
+ MIT
@@ -0,0 +1,469 @@
1
+ import { NostrEvent } from 'nostr-tools/pure';
2
+ import { Filter } from 'nostr-tools/filter';
3
+ import { WebSocket } from 'ws';
4
+ export { ToonDecodeError, ToonEncodeError, decodeEventFromToon, encodeEventToToon } from '@toon-protocol/core';
5
+ import { Hono } from 'hono';
6
+ import { SimplePool } from 'nostr-tools/pool';
7
+
8
+ /**
9
+ * Configuration options for the Nostr relay.
10
+ */
11
+ interface RelayConfig {
12
+ /** Port to listen on (default: 7000) */
13
+ port: number;
14
+ /** Maximum concurrent connections (default: 100) */
15
+ maxConnections?: number;
16
+ /** Maximum subscriptions per connection (default: 20) */
17
+ maxSubscriptionsPerConnection?: number;
18
+ /** Maximum filters per subscription (default: 10) */
19
+ maxFiltersPerSubscription?: number;
20
+ /** Path to SQLite database file (default: ':memory:' for in-memory) */
21
+ databasePath?: string;
22
+ }
23
+ /**
24
+ * Default relay configuration values.
25
+ */
26
+ declare const DEFAULT_RELAY_CONFIG: Required<RelayConfig>;
27
+
28
+ /**
29
+ * Interface for event storage backends.
30
+ */
31
+ interface EventStore {
32
+ /** Store an event by its ID */
33
+ store(event: NostrEvent): void;
34
+ /** Retrieve a single event by ID */
35
+ get(id: string): NostrEvent | undefined;
36
+ /** Query events matching any of the provided filters */
37
+ query(filters: Filter[]): NostrEvent[];
38
+ /** Close the storage backend (optional) */
39
+ close?(): void;
40
+ }
41
+ /**
42
+ * In-memory implementation of EventStore.
43
+ * Events are stored in a Map keyed by event ID.
44
+ */
45
+ declare class InMemoryEventStore implements EventStore {
46
+ private events;
47
+ store(event: NostrEvent): void;
48
+ get(id: string): NostrEvent | undefined;
49
+ query(filters: Filter[]): NostrEvent[];
50
+ /**
51
+ * Close the storage backend (no-op for in-memory store).
52
+ */
53
+ close(): void;
54
+ }
55
+
56
+ /**
57
+ * Custom error class for relay storage errors.
58
+ */
59
+ declare class RelayError extends Error {
60
+ code: string;
61
+ constructor(message: string, code: string);
62
+ }
63
+ /**
64
+ * SQLite implementation of EventStore.
65
+ * Persists events to a SQLite database file.
66
+ */
67
+ declare class SqliteEventStore implements EventStore {
68
+ private db;
69
+ private insertStmt;
70
+ private getStmt;
71
+ private deleteByPubkeyKindStmt;
72
+ private deleteByPubkeyKindDTagStmt;
73
+ private getByPubkeyKindStmt;
74
+ private getByPubkeyKindDTagStmt;
75
+ /**
76
+ * Create a new SqliteEventStore.
77
+ * @param dbPath - Path to the database file. Use ':memory:' for in-memory database.
78
+ */
79
+ constructor(dbPath?: string);
80
+ /**
81
+ * Store an event in the database.
82
+ * Handles replaceable and parameterized replaceable events according to NIP-01.
83
+ */
84
+ store(event: NostrEvent): void;
85
+ /**
86
+ * Store a replaceable event (kinds 10000-19999).
87
+ * Only keeps the latest event per pubkey+kind.
88
+ */
89
+ private storeReplaceableEvent;
90
+ /**
91
+ * Store a parameterized replaceable event (kinds 30000-39999).
92
+ * Only keeps the latest event per pubkey+kind+d-tag.
93
+ */
94
+ private storeParameterizedReplaceableEvent;
95
+ /**
96
+ * Retrieve an event by its ID.
97
+ */
98
+ get(id: string): NostrEvent | undefined;
99
+ /**
100
+ * Query events matching any of the provided filters.
101
+ */
102
+ query(filters: Filter[]): NostrEvent[];
103
+ /**
104
+ * Build SQL query from filters.
105
+ */
106
+ private buildQuerySql;
107
+ /**
108
+ * Close the database connection.
109
+ */
110
+ close(): void;
111
+ }
112
+
113
+ /**
114
+ * Check if an event matches a single filter according to NIP-01 rules.
115
+ *
116
+ * Matching rules:
117
+ * - All specified fields must match (AND logic)
118
+ * - `ids` and `authors` support prefix matching
119
+ * - Tag filters (#e, #p, etc.) match events with corresponding tags
120
+ * - Empty filter matches all events
121
+ *
122
+ * @param event - The Nostr event to check
123
+ * @param filter - The filter to match against
124
+ * @returns true if the event matches the filter
125
+ */
126
+ declare function matchFilter(event: NostrEvent, filter: Filter): boolean;
127
+
128
+ /**
129
+ * Represents an active subscription from a client.
130
+ */
131
+ interface Subscription {
132
+ /** Unique subscription identifier from the client */
133
+ id: string;
134
+ /** Filters applied to this subscription */
135
+ filters: Filter[];
136
+ }
137
+ /**
138
+ * Handles NIP-01 messages for a single WebSocket connection.
139
+ */
140
+ declare class ConnectionHandler {
141
+ private ws;
142
+ private eventStore;
143
+ private subscriptions;
144
+ private config;
145
+ constructor(ws: WebSocket, eventStore: EventStore, config?: Partial<RelayConfig>);
146
+ /**
147
+ * Handle an incoming message from the WebSocket.
148
+ */
149
+ handleMessage(data: string): void;
150
+ /**
151
+ * Handle a REQ message to create/update a subscription.
152
+ */
153
+ private handleReq;
154
+ /**
155
+ * Handle an EVENT message to store an event.
156
+ * Accepts events for free (no payment required).
157
+ */
158
+ private handleEvent;
159
+ /**
160
+ * Handle a CLOSE message to terminate a subscription.
161
+ */
162
+ private handleClose;
163
+ /**
164
+ * Push a new event to all matching subscriptions on this connection.
165
+ * Used when events are stored outside the WebSocket flow (e.g., via ILP).
166
+ */
167
+ notifyNewEvent(event: NostrEvent): void;
168
+ /**
169
+ * Clean up all subscriptions for this connection.
170
+ */
171
+ cleanup(): void;
172
+ /**
173
+ * Get the number of active subscriptions.
174
+ */
175
+ getSubscriptionCount(): number;
176
+ private sendEvent;
177
+ private sendEose;
178
+ private sendOk;
179
+ private sendNotice;
180
+ private send;
181
+ }
182
+
183
+ /**
184
+ * A NIP-01 compliant Nostr relay WebSocket server.
185
+ * Handles client connections and routes messages to ConnectionHandlers.
186
+ */
187
+ declare class NostrRelayServer {
188
+ private eventStore;
189
+ private wss;
190
+ private handlers;
191
+ private config;
192
+ constructor(config: Partial<RelayConfig> | undefined, eventStore: EventStore);
193
+ /**
194
+ * Start the WebSocket server.
195
+ */
196
+ start(): Promise<void>;
197
+ /**
198
+ * Stop the WebSocket server and close all connections.
199
+ */
200
+ stop(): Promise<void>;
201
+ /**
202
+ * Get the port the server is listening on.
203
+ * Returns 0 if the server is not started.
204
+ */
205
+ getPort(): number;
206
+ /**
207
+ * Get the number of connected clients.
208
+ */
209
+ getClientCount(): number;
210
+ /**
211
+ * Broadcast an event to all connected clients with matching subscriptions.
212
+ * Call this after storing an event outside the WebSocket flow (e.g., via ILP)
213
+ * so that discovery subscribers are notified.
214
+ */
215
+ broadcastEvent(event: NostrEvent): void;
216
+ private handleConnection;
217
+ }
218
+
219
+ /**
220
+ * Configuration for the Pricing Service.
221
+ */
222
+ interface PricingConfig {
223
+ /** Base price per byte for event storage */
224
+ basePricePerByte: bigint;
225
+ /** Optional price overrides by event kind */
226
+ kindOverrides?: Map<number, bigint>;
227
+ }
228
+ /**
229
+ * Error class for pricing-specific errors.
230
+ */
231
+ declare class PricingError extends RelayError {
232
+ constructor(message: string, code?: string);
233
+ }
234
+
235
+ /**
236
+ * Service for calculating event storage prices with kind-based overrides.
237
+ */
238
+ declare class PricingService {
239
+ private readonly basePricePerByte;
240
+ private readonly kindOverrides;
241
+ constructor(config: PricingConfig);
242
+ /**
243
+ * Calculate price for a Nostr event.
244
+ *
245
+ * @param event - The Nostr event to price
246
+ * @returns The calculated price as bigint
247
+ */
248
+ calculatePrice(event: NostrEvent): bigint;
249
+ /**
250
+ * Calculate price from raw TOON bytes and event kind.
251
+ *
252
+ * @param bytes - The TOON-encoded event bytes
253
+ * @param kind - The event kind number
254
+ * @returns The calculated price as bigint
255
+ */
256
+ calculatePriceFromBytes(bytes: Uint8Array, kind: number): bigint;
257
+ /**
258
+ * Get the effective price per byte for a given kind.
259
+ *
260
+ * @param kind - The event kind number
261
+ * @returns The price per byte (kind override if exists, otherwise base price)
262
+ */
263
+ getPricePerByte(kind: number): bigint;
264
+ }
265
+
266
+ /**
267
+ * Load pricing configuration from environment variables.
268
+ *
269
+ * Environment variables:
270
+ * - RELAY_BASE_PRICE_PER_BYTE: Base price per byte (default: "10")
271
+ * - RELAY_KIND_OVERRIDES: JSON object mapping kind to price (optional)
272
+ * Format: {"1":"5","30023":"100"}
273
+ *
274
+ * @returns PricingConfig loaded from environment
275
+ * @throws PricingError if env vars contain invalid values
276
+ */
277
+ declare function loadPricingConfigFromEnv(): PricingConfig;
278
+ /**
279
+ * Load pricing configuration from a JSON file.
280
+ *
281
+ * File format:
282
+ * {
283
+ * "basePricePerByte": "10",
284
+ * "kindOverrides": {
285
+ * "0": "0",
286
+ * "1": "5",
287
+ * "30023": "100"
288
+ * }
289
+ * }
290
+ *
291
+ * @param path - Path to the JSON config file
292
+ * @returns PricingConfig loaded from file
293
+ * @throws PricingError if file cannot be read or contains invalid values
294
+ */
295
+ declare function loadPricingConfigFromFile(path: string): PricingConfig;
296
+
297
+ /**
298
+ * Validate that a string is a valid Nostr pubkey format.
299
+ * @param pubkey - The pubkey to validate
300
+ * @returns true if valid 64-character lowercase hex string
301
+ */
302
+ declare function isValidPubkey(pubkey: string): boolean;
303
+ /**
304
+ * Configuration for the Business Logic Server.
305
+ */
306
+ interface BlsConfig {
307
+ /** Base price per byte for event storage (used for simple pricing) */
308
+ basePricePerByte: bigint;
309
+ /** Optional PricingService for kind-based pricing overrides */
310
+ pricingService?: PricingService;
311
+ /** Optional owner pubkey - events from this pubkey bypass payment */
312
+ ownerPubkey?: string;
313
+ }
314
+ /**
315
+ * Incoming packet request from ILP connector.
316
+ */
317
+ interface HandlePacketRequest {
318
+ /** Payment amount as string (parsed to bigint) */
319
+ amount: string;
320
+ /** ILP destination address */
321
+ destination: string;
322
+ /** Base64-encoded TOON Nostr event */
323
+ data: string;
324
+ /** Source ILP address */
325
+ sourceAccount?: string;
326
+ }
327
+ /**
328
+ * Response for accepted packet.
329
+ */
330
+ interface HandlePacketAcceptResponse {
331
+ accept: true;
332
+ /** @deprecated Connector computes fulfillment from SHA256(toon_bytes). Will be removed in a future version. */
333
+ fulfillment?: string;
334
+ metadata?: {
335
+ eventId: string;
336
+ storedAt: number;
337
+ };
338
+ }
339
+ /**
340
+ * Response for rejected packet.
341
+ */
342
+ interface HandlePacketRejectResponse {
343
+ accept: false;
344
+ /** ILP error code (F00, F06, etc.) */
345
+ code: string;
346
+ /** Human-readable error message */
347
+ message: string;
348
+ metadata?: {
349
+ required?: string;
350
+ received?: string;
351
+ };
352
+ }
353
+ /**
354
+ * Union type for packet response.
355
+ */
356
+ type HandlePacketResponse = HandlePacketAcceptResponse | HandlePacketRejectResponse;
357
+ /**
358
+ * ILP error code constants.
359
+ */
360
+ declare const ILP_ERROR_CODES: {
361
+ readonly BAD_REQUEST: "F00";
362
+ readonly INSUFFICIENT_AMOUNT: "F06";
363
+ readonly INTERNAL_ERROR: "T00";
364
+ };
365
+ /**
366
+ * BLS-specific error class.
367
+ */
368
+ declare class BlsError extends RelayError {
369
+ constructor(message: string, code?: string);
370
+ }
371
+
372
+ /**
373
+ * Generate a fulfillment from an event ID.
374
+ * The fulfillment is SHA-256(eventId) encoded as base64.
375
+ *
376
+ * Note: The sender must use SHA256(SHA256(eventId)) as the condition
377
+ * in their ILP Prepare packet.
378
+ *
379
+ * @deprecated Connector computes fulfillment from SHA256(toon_bytes). BLS should not generate fulfillment.
380
+ */
381
+ declare function generateFulfillment(eventId: string): string;
382
+ /**
383
+ * Business Logic Server for ILP payment verification.
384
+ *
385
+ * Handles payment requests from an ILP connector, verifying that the
386
+ * payment amount meets the required price for storing the included
387
+ * Nostr event.
388
+ */
389
+ declare class BusinessLogicServer {
390
+ private config;
391
+ private eventStore;
392
+ private app;
393
+ constructor(config: BlsConfig, eventStore: EventStore);
394
+ /**
395
+ * Set up HTTP routes.
396
+ */
397
+ private setupRoutes;
398
+ /**
399
+ * Process a packet request.
400
+ *
401
+ * This method is public to support direct connector integration in embedded mode,
402
+ * where the connector calls this method directly via setPacketHandler() instead
403
+ * of making HTTP requests.
404
+ */
405
+ handlePacket(request: HandlePacketRequest): HandlePacketAcceptResponse | HandlePacketRejectResponse;
406
+ /**
407
+ * Get the Hono app instance for testing or composition.
408
+ */
409
+ getApp(): Hono;
410
+ /**
411
+ * Start the HTTP server on the specified port.
412
+ */
413
+ start(port: number): void;
414
+ }
415
+
416
+ /**
417
+ * Subscribe to upstream relays and propagate events into the local EventStore.
418
+ *
419
+ * Follows the same lifecycle pattern as core's discovery tracker and SocialPeerDiscovery:
420
+ * - Accept optional SimplePool for testability
421
+ * - start() returns { unsubscribe } cleanup handle
422
+ * - isUnsubscribed guard prevents processing after teardown
423
+ */
424
+
425
+ /**
426
+ * Configuration for RelaySubscriber.
427
+ */
428
+ interface RelaySubscriberConfig {
429
+ /** Upstream relay URLs to subscribe to */
430
+ relayUrls: string[];
431
+ /** Nostr filter for which events to pull (e.g. kinds, authors) */
432
+ filter: Filter;
433
+ /** Verify event signatures before storing (default: true) */
434
+ verifySignatures?: boolean;
435
+ }
436
+ /**
437
+ * Subscribes to upstream Nostr relays and stores received events
438
+ * in the local EventStore. Useful for relay-to-relay event propagation.
439
+ */
440
+ declare class RelaySubscriber {
441
+ private readonly config;
442
+ private readonly eventStore;
443
+ private readonly pool;
444
+ private started;
445
+ /**
446
+ * @param config - Subscriber configuration
447
+ * @param eventStore - Storage backend to write events into
448
+ * @param pool - Optional SimplePool instance (creates new one if not provided)
449
+ */
450
+ constructor(config: RelaySubscriberConfig, eventStore: EventStore, pool?: SimplePool);
451
+ /**
452
+ * Start subscribing to the configured upstream relays.
453
+ *
454
+ * @returns Handle with unsubscribe() to stop the subscription
455
+ * @throws Error if already started
456
+ */
457
+ start(): {
458
+ unsubscribe: () => void;
459
+ };
460
+ }
461
+
462
+ /**
463
+ * @toon-protocol/relay
464
+ *
465
+ * ILP-gated Nostr relay with Business Logic Server.
466
+ */
467
+ declare const VERSION = "0.1.0";
468
+
469
+ export { type BlsConfig, BlsError, BusinessLogicServer, ConnectionHandler, DEFAULT_RELAY_CONFIG, type EventStore, type HandlePacketAcceptResponse, type HandlePacketRejectResponse, type HandlePacketRequest, type HandlePacketResponse, ILP_ERROR_CODES, InMemoryEventStore, NostrRelayServer, type PricingConfig, PricingError, PricingService, type RelayConfig, RelayError, RelaySubscriber, type RelaySubscriberConfig, SqliteEventStore, type Subscription, VERSION, generateFulfillment, isValidPubkey, loadPricingConfigFromEnv, loadPricingConfigFromFile, matchFilter };