@toon-protocol/relay 2.0.2 → 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 +119 -97
- package/dist/{chunk-QZQRHQEQ.js → chunk-IZOSPMWV.js} +619 -93
- package/dist/chunk-IZOSPMWV.js.map +1 -0
- package/dist/cli.js +125 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +666 -55
- package/dist/index.js +29 -98
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
- package/dist/chunk-QZQRHQEQ.js.map +0 -1
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
|
|
4
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,121 +16,141 @@ event and serves reads.
|
|
|
14
16
|
npm install @toon-protocol/relay
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
## Run
|
|
19
|
+
## Run
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
22
|
NOSTR_SECRET_KEY=<64-char-hex> npx @toon-protocol/relay
|
|
21
|
-
# reads:
|
|
22
|
-
# writes:
|
|
23
|
-
#
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`verifyEphemeral: true`). A free lane must NOT reuse this skip.
|
|
58
|
-
- **Exposure guard:** the write port must be reachable only via the
|
|
59
|
-
connector. In docker, never host-publish it (`expose:`, not `ports:` —
|
|
60
|
-
docker-published ports bypass ufw). Outside docker, bind it internally via
|
|
61
|
-
`TOON_WRITE_HOST=127.0.0.1`. At startup the relay logs a prominent warning
|
|
62
|
-
if the write listener binds a non-internal interface while the skip is
|
|
63
|
-
active (warning only — container topologies legitimately bind `0.0.0.0`
|
|
64
|
-
and stay private by not publishing the port).
|
|
65
|
-
|
|
66
|
-
## Run (programmatic)
|
|
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).
|
|
67
60
|
|
|
68
|
-
|
|
69
|
-
import { startRelay } from '@toon-protocol/relay';
|
|
61
|
+
## HTTP surface
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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. |
|
|
75
69
|
|
|
76
|
-
|
|
70
|
+
### The payment statement
|
|
77
71
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
| `POST` | `/write` | Store an event. Body `{ "event": <NostrEvent> }`. Trusts injected `X-TOON-Payer`/`-Amount`/`-Chain` headers (echoed, not validated); verifies only the event signature (ephemeral kinds: id check only by default, see above). |
|
|
81
|
-
| `GET` | `/health` | Liveness, identity (`pubkey`), `capabilities`, and `version`. |
|
|
82
|
-
| `GET` | `/metrics` | JSON telemetry: `eventLoopDelayMs` (mean/p50/p99/max — loop lag is ephemeral-frame tail latency) and `verify` (per-event verify wall time incl. pool queueing, active implementation, worker count). The trigger metrics for scaling decisions (relay#85). |
|
|
72
|
+
A terminating connector states three headers on a delivery whose payment it
|
|
73
|
+
verified at its own client edge:
|
|
83
74
|
|
|
84
|
-
|
|
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` |
|
|
85
80
|
|
|
86
|
-
|
|
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.
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
const relay = new NostrRelayServer({ port: 7100 }, eventStore);
|
|
90
|
+
## The write port must stay private
|
|
93
91
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
```
|
|
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:
|
|
98
95
|
|
|
99
|
-
|
|
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`.
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
const sqlStore = new SqliteEventStore('./events.db'); // persistent
|
|
111
|
+
## The free ephemeral lane
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
116
|
+
|
|
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`.
|
|
111
121
|
|
|
112
|
-
##
|
|
122
|
+
## Programmatic use
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { startRelay } from '@toon-protocol/relay';
|
|
113
126
|
|
|
114
|
-
|
|
127
|
+
const relay = await startRelay({ secretKey });
|
|
128
|
+
// POST /write on 3100, NIP-01 reads on 7100
|
|
129
|
+
await relay.stop();
|
|
130
|
+
```
|
|
115
131
|
|
|
116
132
|
```ts
|
|
117
|
-
import {
|
|
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();
|
|
118
141
|
```
|
|
119
142
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
|
123
|
-
|
|
124
|
-
| **
|
|
125
|
-
| **
|
|
126
|
-
| **
|
|
127
|
-
| **
|
|
128
|
-
| **
|
|
129
|
-
| **
|
|
130
|
-
| **
|
|
131
|
-
| **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` |
|
|
132
154
|
|
|
133
155
|
## License
|
|
134
156
|
|