@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 +118 -64
- package/dist/chunk-IZOSPMWV.js +1799 -0
- package/dist/chunk-IZOSPMWV.js.map +1 -0
- package/dist/chunk-SMT6G3XD.js +92 -0
- package/dist/chunk-SMT6G3XD.js.map +1 -0
- package/dist/cli.js +181 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1073 -57
- package/dist/index.js +53 -100
- 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 +9 -5
- package/dist/chunk-FXQSNOCG.js +0 -939
- package/dist/chunk-FXQSNOCG.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,89 +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
|
-
|
|
|
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
|
-
##
|
|
61
|
+
## HTTP surface
|
|
36
62
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
// ... POST /write on 3100, read NIP-01 on 7100 ...
|
|
42
|
-
await relay.stop();
|
|
43
|
-
```
|
|
70
|
+
### The payment statement
|
|
44
71
|
|
|
45
|
-
|
|
72
|
+
A terminating connector states three headers on a delivery whose payment it
|
|
73
|
+
verified at its own client edge:
|
|
46
74
|
|
|
47
|
-
|
|
|
48
|
-
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
import { NostrRelayServer, SqliteEventStore } from '@toon-protocol/relay';
|
|
90
|
+
## The write port must stay private
|
|
58
91
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
import { InMemoryEventStore, SqliteEventStore } from '@toon-protocol/relay';
|
|
111
|
+
## The free ephemeral lane
|
|
71
112
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
124
|
+
```ts
|
|
125
|
+
import { startRelay } from '@toon-protocol/relay';
|
|
81
126
|
|
|
82
|
-
|
|
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 {
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
|
91
|
-
|
|
92
|
-
| **
|
|
93
|
-
| **
|
|
94
|
-
| **
|
|
95
|
-
| **
|
|
96
|
-
| **
|
|
97
|
-
| **
|
|
98
|
-
| **
|
|
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
|
|