@toon-protocol/relay 2.0.1 → 2.1.0

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 CHANGED
@@ -1,12 +1,14 @@
1
1
  # @toon-protocol/relay
2
2
 
3
- A Nostr relay app: free NIP-01 WebSocket reads plus an HTTP `POST /write`
4
- surface for storing events.
3
+ A Nostr relay app: free NIP-01 WebSocket reads, an HTTP `POST /write` surface
4
+ for storing events, and a free `POST /write-ephemeral` lane for presence and
5
+ typing traffic.
5
6
 
6
- The relay contains **no ILP, connector, settlement, or pricing logic**. Payment
7
- is enforced entirely upstream by an external terminator by the time a write
8
- reaches this process it is already proven paid, so the relay simply stores the
9
- event and serves reads.
7
+ The relay contains **no payment, settlement, or ILP logic**. Payment is
8
+ enforced upstream by a terminating connector; by the time a write reaches this
9
+ process it is already paid for, so the relay verifies the event signature,
10
+ stores it, and serves reads. To run one behind a payment proxy, see the
11
+ [repository README](https://github.com/toon-protocol/relay#readme).
10
12
 
11
13
  ## Install
12
14
 
@@ -14,89 +16,141 @@ event and serves reads.
14
16
  npm install @toon-protocol/relay
15
17
  ```
16
18
 
17
- ## Run (CLI)
19
+ ## Run
18
20
 
19
21
  ```bash
20
22
  NOSTR_SECRET_KEY=<64-char-hex> npx @toon-protocol/relay
21
- # reads: ws://localhost:7100
22
- # writes: http://localhost:3100/write
23
- # health: http://localhost:3100/health
23
+ # reads: ws://localhost:7100
24
+ # writes: http://localhost:3100/write
25
+ # ephemeral: http://localhost:3100/write-ephemeral
26
+ # health: http://localhost:3100/health
24
27
  ```
25
28
 
26
- | Env var | Default | Description |
27
- |---------|---------|-------------|
28
- | `TOON_SECRET_KEY` / `NOSTR_SECRET_KEY` | — | 64-char hex identity key (one of these or `TOON_MNEMONIC` is required) |
29
- | `TOON_MNEMONIC` | | BIP-39 mnemonic (NIP-06 derivation) |
30
- | `TOON_RELAY_PORT` | `7100` | WebSocket read port |
31
- | `TOON_BLS_PORT` | `3100` | HTTP write/health port |
32
- | `TOON_DATA_DIR` | `./data` | SQLite data directory |
33
- | `TOON_DEV_MODE` | `false` | Skip event-signature verification on `POST /write` |
29
+ An identity key is required: one of `--secret-key` / `TOON_SECRET_KEY` /
30
+ `NOSTR_SECRET_KEY` (64 hex characters), or `--mnemonic` / `TOON_MNEMONIC`
31
+ (BIP-39, NIP-06 derivation). Prefer the environment variables a secret
32
+ passed as a flag is visible in process listings, and the CLI warns when you do
33
+ it.
34
+
35
+ | Flag | Environment variable | Default | Description |
36
+ | ------------------------------------ | --------------------------------------- | --------- | --------------------------------------------------------------------------- |
37
+ | `--secret-key` | `TOON_SECRET_KEY`, `NOSTR_SECRET_KEY` | — | 64-hex identity key |
38
+ | `--mnemonic` | `TOON_MNEMONIC` | — | BIP-39 mnemonic (NIP-06) |
39
+ | `--relay-port` | `TOON_RELAY_PORT` | `7100` | WebSocket read port |
40
+ | `--bls-port` | `TOON_BLS_PORT` | `3100` | HTTP write / health / metrics port |
41
+ | `--host` | `TOON_HOST` | `0.0.0.0` | read-port bind address |
42
+ | `--write-host` | `TOON_WRITE_HOST` | `0.0.0.0` | write-port bind address (see [exposure](#the-write-port-must-stay-private)) |
43
+ | `--data-dir` | `TOON_DATA_DIR` | `./data` | SQLite directory |
44
+ | `--dev-mode` | `TOON_DEV_MODE` | `false` | skip signature verification entirely — smoke tests only |
45
+ | `--verify-ephemeral` | `TOON_VERIFY_EPHEMERAL` | `false` | full verification on paid ephemeral kinds too |
46
+ | `--verify-workers` | `TOON_VERIFY_WORKERS` | CPUs − 1 | verify-pool threads; `0` verifies on the event loop |
47
+ | `--max-connections` | `TOON_MAX_CONNECTIONS` | `4096` | concurrent WS reads (one file descriptor each) |
48
+ | `--log-writes` | `TOON_LOG_WRITES` | `false` | one log line per accepted write |
49
+ | `--no-enforce-expiration` | `TOON_ENFORCE_EXPIRATION` | enforced | serve events past their NIP-40 `expiration` again |
50
+ | `--expiration-reap-grace-seconds` | `TOON_EXPIRATION_REAP_GRACE_SECONDS` | `86400` | how long an expired event stays on disk |
51
+ | `--expiration-reap-interval-seconds` | `TOON_EXPIRATION_REAP_INTERVAL_SECONDS` | `3600` | how often the reaper sweeps; `0` never |
52
+ | `--blocked-event-ids` | `TOON_BLOCKED_EVENT_IDS` | — | comma-separated 64-hex event ids to refuse |
53
+ | `--ephemeral-rate-limit` | `TOON_EPHEMERAL_RATE_LIMIT` | `200` | free-lane requests per key per window |
54
+ | `--ephemeral-rate-window-ms` | `TOON_EPHEMERAL_RATE_WINDOW_MS` | `10000` | free-lane rate-limit window |
55
+ | `--ephemeral-max-body-bytes` | `TOON_EPHEMERAL_MAX_BODY_BYTES` | `8192` | free-lane request body cap |
56
+
57
+ Retention behaviour — NIP-40 expiry, NIP-09 deletion, the blocklist — is
58
+ documented in
59
+ [`docs/retention.md`](https://github.com/toon-protocol/relay/blob/main/docs/retention.md).
34
60
 
35
- ## Run (programmatic)
61
+ ## HTTP surface
36
62
 
37
- ```ts
38
- import { startRelay } from '@toon-protocol/relay';
63
+ | Method | Path | Description |
64
+ | ------ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65
+ | `POST` | `/write` | Store an event. Body `{ "event": <NostrEvent> }`. Verifies the signature (ephemeral kinds: id check only, by default). Echoes the connector's payment statement when there is one — see below. |
66
+ | `POST` | `/write-ephemeral` | The free ephemeral lane. Ephemeral kinds only (`400` otherwise), always fully verified, never stored, rate-limited and size-capped (`429` / `413`). |
67
+ | `GET` | `/health` | Liveness, identity (`pubkey`), `capabilities`, `version`. |
68
+ | `GET` | `/metrics` | JSON telemetry: event-loop delay, per-event verify timings and pool state, and the ephemeral lane's bounds. |
39
69
 
40
- const relay = await startRelay({ secretKey });
41
- // ... POST /write on 3100, read NIP-01 on 7100 ...
42
- await relay.stop();
43
- ```
70
+ ### The payment statement
44
71
 
45
- ## HTTP surface
72
+ A terminating connector states three headers on a delivery whose payment it
73
+ verified at its own client edge:
46
74
 
47
- | Method | Path | Description |
48
- |--------|------|-------------|
49
- | `POST` | `/write` | Store an event. Body `{ "event": <NostrEvent> }`. Trusts injected `X-TOON-Payer`/`-Amount`/`-Chain` headers (echoed, not validated); verifies only the event signature. |
50
- | `GET` | `/health` | Liveness, identity (`pubkey`), `capabilities`, and `version`. |
75
+ | Header | Value |
76
+ | --------------- | -------------------------------------------------------------- |
77
+ | `X-TOON-Payer` | the client channel key `evm:0x<64 hex>` or `solana:<base58>` |
78
+ | `X-TOON-Amount` | the route's price, decimal, in base units |
79
+ | `X-TOON-Chain` | `evm` or `solana` |
51
80
 
52
- ## WebSocket Relay Server
81
+ When all three are present and well-formed, `POST /write` returns them as a
82
+ `payment` object and includes them on its log line. The relay re-validates
83
+ none of it — it holds no chain state — but a malformed or partial statement is
84
+ discarded whole rather than half-recorded.
53
85
 
54
- NIP-01 compliant WebSocket server that stores and serves Nostr events in TOON format.
86
+ **Absence is not "unpaid".** The headers are stated only by the hop that took
87
+ the payment, so they are absent on a forwarded packet and on every free route.
88
+ Never treat their absence as a reason to refuse a write.
55
89
 
56
- ```ts
57
- import { NostrRelayServer, SqliteEventStore } from '@toon-protocol/relay';
90
+ ## The write port must stay private
58
91
 
59
- const eventStore = new SqliteEventStore('./events.db');
60
- const relay = new NostrRelayServer({ port: 7100 }, eventStore);
92
+ By default the relay **skips signature verification for ephemeral kinds**
93
+ (NIP-16, `20000 <= kind < 30000`) on `POST /write`, keeping only the SHA-256
94
+ id check. That is a deliberate, payment-gated bypass:
61
95
 
62
- await relay.start();
63
- relay.broadcastEvent(event); // push to matching subscriptions
64
- await relay.stop();
65
- ```
96
+ - **Why it is safe:** every request reaching `POST /write` has already passed
97
+ the connector's payment gate payment is the admission control. Clients
98
+ verify every signature themselves and never trust the relay's verdict, so
99
+ relay-side verification of paid ephemeral frames buys no trust that payment
100
+ has not already bought.
101
+ - **What is still checked:** the id check always runs, so the relay never
102
+ broadcasts bytes that disagree with the id clients index by.
103
+ - **When to turn it off:** if the write port is fronted by anything other than
104
+ a payment-gating connector, set `TOON_VERIFY_EPHEMERAL=true`.
66
105
 
67
- ## Event Storage
106
+ In Docker, never host-publish the write port (`expose:`, not `ports:` — a
107
+ docker publish bypasses ufw). Outside Docker, bind it with
108
+ `TOON_WRITE_HOST=127.0.0.1`. The relay logs a warning at startup if the write
109
+ listener binds a non-internal interface while the skip is active.
68
110
 
69
- ```ts
70
- import { InMemoryEventStore, SqliteEventStore } from '@toon-protocol/relay';
111
+ ## The free ephemeral lane
71
112
 
72
- const memStore = new InMemoryEventStore(); // ephemeral
73
- const sqlStore = new SqliteEventStore('./events.db'); // persistent
113
+ `POST /write-ephemeral` is a second write surface, distinct from the paid one,
114
+ because a connector cannot carry two prices on a single handler URL. It is
115
+ terminated by its own zero-priced route.
74
116
 
75
- memStore.store(event);
76
- const found = memStore.get(event.id);
77
- const results = memStore.query([{ kinds: [1], limit: 10 }]);
78
- ```
117
+ It accepts **only** ephemeral kinds, **never** stores, and **always** verifies
118
+ signatures in full — it has no payment gate to lean on, so verification is its
119
+ only defence against forged-signature spam. It is bounded by a per-key sliding
120
+ window and a body-size cap, both surfaced on `GET /metrics`.
121
+
122
+ ## Programmatic use
79
123
 
80
- ## TOON Codec
124
+ ```ts
125
+ import { startRelay } from '@toon-protocol/relay';
81
126
 
82
- Re-exported from [`@toon-protocol/core`](https://github.com/toon-protocol/core) for convenience.
127
+ const relay = await startRelay({ secretKey });
128
+ // POST /write on 3100, NIP-01 reads on 7100
129
+ await relay.stop();
130
+ ```
83
131
 
84
132
  ```ts
85
- import { encodeEventToToon, decodeEventFromToon } from '@toon-protocol/relay';
133
+ import { NostrRelayServer, SqliteEventStore } from '@toon-protocol/relay';
134
+
135
+ const eventStore = new SqliteEventStore('./events.db');
136
+ const relay = new NostrRelayServer({ port: 7100 }, eventStore);
137
+
138
+ await relay.start();
139
+ relay.broadcastEvent(event); // push to matching subscriptions
140
+ await relay.stop();
86
141
  ```
87
142
 
88
- ## Full API
89
-
90
- | Category | Exports |
91
- |----------|---------|
92
- | **Launcher** | `startRelay`, `RelayConfig`, `RelayInstance`, `RelaySubscription`, `ResolvedRelayConfig` |
93
- | **Relay** | `NostrRelayServer`, `ConnectionHandler`, `RelayServerConfig`, `DEFAULT_RELAY_CONFIG` |
94
- | **Storage** | `EventStore`, `InMemoryEventStore`, `SqliteEventStore`, `RelayError` |
95
- | **Write/Health** | `createWriteHandler`, `createHealthResponse` |
96
- | **Codec** | `encodeEventToToon`, `decodeEventFromToon`, `ToonEncodeError`, `ToonDecodeError` |
97
- | **Subscriber** | `RelaySubscriber`, `RelaySubscriberConfig` |
98
- | **Filter** | `matchFilter` |
99
- | **Constants** | `VERSION` |
143
+ | Category | Exports |
144
+ | --------------------- | ---------------------------------------------------------------------------------------- |
145
+ | **Launcher** | `startRelay`, `RelayConfig`, `RelayInstance`, `RelaySubscription`, `ResolvedRelayConfig` |
146
+ | **Relay** | `NostrRelayServer`, `ConnectionHandler`, `RelayServerConfig`, `DEFAULT_RELAY_CONFIG` |
147
+ | **Storage** | `EventStore`, `InMemoryEventStore`, `SqliteEventStore`, `RelayError` |
148
+ | **Write / health** | `createWriteHandler`, `createEphemeralWriteHandler`, `createHealthResponse` |
149
+ | **Payment statement** | `readPaymentAttribution`, `PaymentAttribution` |
150
+ | **Rate limiting** | `createRateLimiter` |
151
+ | **Subscriber** | `RelaySubscriber`, `RelaySubscriberConfig` |
152
+ | **Filter** | `matchFilter` |
153
+ | **Constants** | `VERSION` |
100
154
 
101
155
  ## License
102
156