@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/dist/cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ DEFAULT_EPHEMERAL_RATE_LIMIT,
4
+ parseBlockedEventIds,
3
5
  startRelay
4
- } from "./chunk-FXQSNOCG.js";
6
+ } from "./chunk-IZOSPMWV.js";
7
+ import "./chunk-SMT6G3XD.js";
5
8
 
6
9
  // src/launcher/cli.ts
7
10
  import { parseArgs } from "util";
@@ -16,8 +19,62 @@ Options:
16
19
  --relay-port <port> WebSocket read port (default: 7100)
17
20
  --bls-port <port> HTTP write/health port (default: 3100)
18
21
  --host <host> WebSocket bind host (default: 0.0.0.0)
22
+ --write-host <host> HTTP write/health bind host (default: 0.0.0.0).
23
+ The write port must only be reachable via the
24
+ payment-gating connector; bind it to a loopback/
25
+ internal address when not isolated by docker
26
+ networking (relay#85 exposure guard)
19
27
  --data-dir <path> Data directory for the SQLite store (default: ./data)
20
28
  --dev-mode Skip event-signature verification on POST /write
29
+ --verify-ephemeral Run FULL schnorr verification on ephemeral kinds
30
+ (20000 <= kind < 30000). Default OFF: the write
31
+ path is payment-gated and clients verify
32
+ signatures themselves, so ephemeral kinds skip
33
+ schnorr and keep only the SHA-256 id check
34
+ (relay#85). Set this when the write port is
35
+ fronted by anything other than a payment-gating
36
+ connector
37
+ --verify-workers <n> Worker threads for persistent-kind signature
38
+ verification (default: CPU count - 1, minimum 0).
39
+ 0 = verify inline on the event loop (automatic on
40
+ 1-core boxes; the escape hatch back to the
41
+ pre-pool behavior, relay#85)
42
+ --max-connections <n> Maximum concurrent WebSocket read connections
43
+ (default: 4096; each costs one file descriptor --
44
+ mind ulimit -n, relay#90)
45
+ --ephemeral-rate-limit <n>
46
+ Free ephemeral write lane (POST /write-ephemeral,
47
+ relay#129): max requests per key per window
48
+ (default: 200). This lane has no payment gate, so
49
+ this bound IS its admission control
50
+ --ephemeral-rate-window-ms <n>
51
+ Free ephemeral write lane: rate-limit window in
52
+ milliseconds (default: 10000)
53
+ --ephemeral-max-body-bytes <n>
54
+ Free ephemeral write lane: request body size cap
55
+ in bytes (default: 8192)
56
+ --log-writes Log one line per accepted POST /write (debug; off
57
+ by default -- per-event logging is write-path
58
+ tail jitter, relay#85)
59
+ --no-enforce-expiration Serve events past their NIP-40 expiration tag.
60
+ KILL SWITCH back to the pre-relay#137 behaviour;
61
+ enforcement is ON by default. Also disables the
62
+ reaper -- a relay still serving expired events
63
+ must not be silently deleting them
64
+ --expiration-reap-grace-seconds <n>
65
+ How long an expired event stays on disk before the
66
+ reaper deletes it (default: 86400). The window in
67
+ which flipping enforcement back off is a real
68
+ recovery rather than an apology. 0 = reap at once
69
+ --expiration-reap-interval-seconds <n>
70
+ Reaper sweep interval (default: 3600). 0 disables
71
+ reaping; serve-time filtering is unaffected
72
+ --blocked-event-ids <ids>
73
+ Comma-separated 64-hex event ids this relay
74
+ refuses to store or serve. The escape hatch for an
75
+ event whose author key is gone, so neither NIP-01
76
+ replacement nor NIP-09 deletion can reach it. Ids
77
+ only, never pubkeys; every id is logged at startup
21
78
  --help Show this help message
22
79
 
23
80
  Environment Variables:
@@ -27,8 +84,20 @@ Environment Variables:
27
84
  TOON_RELAY_PORT Same as --relay-port
28
85
  TOON_BLS_PORT Same as --bls-port
29
86
  TOON_HOST Same as --host
87
+ TOON_WRITE_HOST Same as --write-host
30
88
  TOON_DATA_DIR Same as --data-dir
31
89
  TOON_DEV_MODE Same as --dev-mode (set to "true")
90
+ TOON_VERIFY_EPHEMERAL Same as --verify-ephemeral (set to "true")
91
+ TOON_VERIFY_WORKERS Same as --verify-workers
92
+ TOON_MAX_CONNECTIONS Same as --max-connections
93
+ TOON_EPHEMERAL_RATE_LIMIT Same as --ephemeral-rate-limit
94
+ TOON_EPHEMERAL_RATE_WINDOW_MS Same as --ephemeral-rate-window-ms
95
+ TOON_EPHEMERAL_MAX_BODY_BYTES Same as --ephemeral-max-body-bytes
96
+ TOON_LOG_WRITES Same as --log-writes (set to "true")
97
+ TOON_ENFORCE_EXPIRATION Set to "false" for --no-enforce-expiration
98
+ TOON_EXPIRATION_REAP_GRACE_SECONDS Same as --expiration-reap-grace-seconds
99
+ TOON_EXPIRATION_REAP_INTERVAL_SECONDS Same as --expiration-reap-interval-seconds
100
+ TOON_BLOCKED_EVENT_IDS Same as --blocked-event-ids
32
101
 
33
102
  Security:
34
103
  Prefer TOON_MNEMONIC / TOON_SECRET_KEY / NOSTR_SECRET_KEY environment
@@ -37,6 +106,28 @@ Security:
37
106
  `.trim()
38
107
  );
39
108
  }
109
+ function parsePositiveIntOption(flag, raw) {
110
+ if (!raw) {
111
+ return void 0;
112
+ }
113
+ const parsed = parseInt(raw, 10);
114
+ if (Number.isNaN(parsed) || parsed <= 0) {
115
+ console.error(`Error: --${flag} must be a positive integer`);
116
+ process.exit(1);
117
+ }
118
+ return parsed;
119
+ }
120
+ function parseNonNegativeIntOption(flag, raw) {
121
+ if (raw === void 0 || raw === "") {
122
+ return void 0;
123
+ }
124
+ const parsed = parseInt(raw, 10);
125
+ if (Number.isNaN(parsed) || parsed < 0) {
126
+ console.error(`Error: --${flag} must be an integer >= 0`);
127
+ process.exit(1);
128
+ }
129
+ return parsed;
130
+ }
40
131
  function parseCli() {
41
132
  const { values } = parseArgs({
42
133
  options: {
@@ -45,8 +136,20 @@ function parseCli() {
45
136
  "relay-port": { type: "string" },
46
137
  "bls-port": { type: "string" },
47
138
  host: { type: "string" },
139
+ "write-host": { type: "string" },
48
140
  "data-dir": { type: "string" },
49
141
  "dev-mode": { type: "boolean" },
142
+ "verify-ephemeral": { type: "boolean" },
143
+ "verify-workers": { type: "string" },
144
+ "max-connections": { type: "string" },
145
+ "ephemeral-rate-limit": { type: "string" },
146
+ "ephemeral-rate-window-ms": { type: "string" },
147
+ "ephemeral-max-body-bytes": { type: "string" },
148
+ "log-writes": { type: "boolean" },
149
+ "no-enforce-expiration": { type: "boolean" },
150
+ "expiration-reap-grace-seconds": { type: "string" },
151
+ "expiration-reap-interval-seconds": { type: "string" },
152
+ "blocked-event-ids": { type: "string" },
50
153
  help: { type: "boolean" }
51
154
  },
52
155
  strict: true,
@@ -83,9 +186,7 @@ function parseCli() {
83
186
  process.exit(1);
84
187
  }
85
188
  if (mnemonic && secretKey) {
86
- console.error(
87
- "Error: provide either a mnemonic or a secret key, not both"
88
- );
189
+ console.error("Error: provide either a mnemonic or a secret key, not both");
89
190
  process.exit(1);
90
191
  }
91
192
  const relayPortStr = values["relay-port"] ?? process.env["TOON_RELAY_PORT"] ?? void 0;
@@ -101,16 +202,81 @@ function parseCli() {
101
202
  process.exit(1);
102
203
  }
103
204
  const host = values.host ?? process.env["TOON_HOST"] ?? void 0;
205
+ const writeHost = values["write-host"] ?? process.env["TOON_WRITE_HOST"] ?? void 0;
104
206
  const dataDir = values["data-dir"] ?? process.env["TOON_DATA_DIR"] ?? void 0;
105
207
  const devMode = values["dev-mode"] ?? (process.env["TOON_DEV_MODE"] === "true" ? true : void 0);
208
+ const verifyEphemeral = values["verify-ephemeral"] ?? (process.env["TOON_VERIFY_EPHEMERAL"] === "true" ? true : void 0);
209
+ const verifyWorkersStr = values["verify-workers"] ?? process.env["TOON_VERIFY_WORKERS"] ?? void 0;
210
+ const verifyWorkers = verifyWorkersStr ? parseInt(verifyWorkersStr, 10) : void 0;
211
+ if (verifyWorkers !== void 0 && (Number.isNaN(verifyWorkers) || verifyWorkers < 0 || verifyWorkers > 256)) {
212
+ console.error("Error: --verify-workers must be an integer >= 0");
213
+ process.exit(1);
214
+ }
215
+ const maxConnectionsStr = values["max-connections"] ?? process.env["TOON_MAX_CONNECTIONS"] ?? void 0;
216
+ const maxConnections = maxConnectionsStr ? parseInt(maxConnectionsStr, 10) : void 0;
217
+ if (maxConnections !== void 0 && (Number.isNaN(maxConnections) || maxConnections <= 0)) {
218
+ console.error("Error: --max-connections must be a positive integer");
219
+ process.exit(1);
220
+ }
221
+ const ephemeralRateLimitMax = parsePositiveIntOption(
222
+ "ephemeral-rate-limit",
223
+ values["ephemeral-rate-limit"] ?? process.env["TOON_EPHEMERAL_RATE_LIMIT"]
224
+ );
225
+ const ephemeralRateWindowMs = parsePositiveIntOption(
226
+ "ephemeral-rate-window-ms",
227
+ values["ephemeral-rate-window-ms"] ?? process.env["TOON_EPHEMERAL_RATE_WINDOW_MS"]
228
+ );
229
+ const ephemeralMaxBodyBytes = parsePositiveIntOption(
230
+ "ephemeral-max-body-bytes",
231
+ values["ephemeral-max-body-bytes"] ?? process.env["TOON_EPHEMERAL_MAX_BODY_BYTES"]
232
+ );
233
+ const ephemeralRateLimit = ephemeralRateLimitMax !== void 0 || ephemeralRateWindowMs !== void 0 ? {
234
+ maxRequests: ephemeralRateLimitMax ?? DEFAULT_EPHEMERAL_RATE_LIMIT.maxRequests,
235
+ windowMs: ephemeralRateWindowMs ?? DEFAULT_EPHEMERAL_RATE_LIMIT.windowMs
236
+ } : void 0;
237
+ const logWrites = values["log-writes"] ?? (process.env["TOON_LOG_WRITES"] === "true" ? true : void 0);
238
+ const enforceExpiration = values["no-enforce-expiration"] === true || process.env["TOON_ENFORCE_EXPIRATION"] === "false" ? false : void 0;
239
+ const expirationReapGraceSeconds = parseNonNegativeIntOption(
240
+ "expiration-reap-grace-seconds",
241
+ values["expiration-reap-grace-seconds"] ?? process.env["TOON_EXPIRATION_REAP_GRACE_SECONDS"]
242
+ );
243
+ const expirationReapIntervalSeconds = parseNonNegativeIntOption(
244
+ "expiration-reap-interval-seconds",
245
+ values["expiration-reap-interval-seconds"] ?? process.env["TOON_EXPIRATION_REAP_INTERVAL_SECONDS"]
246
+ );
247
+ const blocklist = parseBlockedEventIds(
248
+ values["blocked-event-ids"] ?? process.env["TOON_BLOCKED_EVENT_IDS"]
249
+ );
250
+ if (blocklist.invalid.length > 0) {
251
+ console.error(
252
+ `Error: --blocked-event-ids entries must be 64-character hex event ids; rejected: ${blocklist.invalid.join(", ")}`
253
+ );
254
+ process.exit(1);
255
+ }
256
+ const blockedEventIds = blocklist.ids.length > 0 ? blocklist.ids : void 0;
106
257
  const config = {
107
258
  ...mnemonic && { mnemonic },
108
259
  ...secretKey && { secretKey },
109
260
  ...relayPort !== void 0 && { relayPort },
110
261
  ...blsPort !== void 0 && { blsPort },
111
262
  ...host && { host },
263
+ ...writeHost && { writeHost },
112
264
  ...dataDir && { dataDir },
113
- ...devMode !== void 0 && { devMode }
265
+ ...devMode !== void 0 && { devMode },
266
+ ...verifyEphemeral !== void 0 && { verifyEphemeral },
267
+ ...verifyWorkers !== void 0 && { verifyWorkers },
268
+ ...maxConnections !== void 0 && { maxConnections },
269
+ ...ephemeralRateLimit !== void 0 && { ephemeralRateLimit },
270
+ ...ephemeralMaxBodyBytes !== void 0 && { ephemeralMaxBodyBytes },
271
+ ...logWrites !== void 0 && { logWrites },
272
+ ...enforceExpiration !== void 0 && { enforceExpiration },
273
+ ...expirationReapGraceSeconds !== void 0 && {
274
+ expirationReapGraceSeconds
275
+ },
276
+ ...expirationReapIntervalSeconds !== void 0 && {
277
+ expirationReapIntervalSeconds
278
+ },
279
+ ...blockedEventIds !== void 0 && { blockedEventIds }
114
280
  };
115
281
  return config;
116
282
  }
@@ -124,9 +290,16 @@ async function main() {
124
290
  console.log("TOON Relay Ready");
125
291
  console.log("=".repeat(50));
126
292
  console.log(` Pubkey: ${instance.pubkey}`);
127
- console.log(` Reads: ws://localhost:${instance.config.relayPort}`);
128
- console.log(` Writes: http://localhost:${instance.config.blsPort}/write`);
129
- console.log(` Health: http://localhost:${instance.config.blsPort}/health`);
293
+ console.log(` Reads: ws://localhost:${instance.config.relayPort}`);
294
+ console.log(
295
+ ` Writes: http://localhost:${instance.config.blsPort}/write`
296
+ );
297
+ console.log(
298
+ ` Ephemeral: http://localhost:${instance.config.blsPort}/write-ephemeral (free, relay#129)`
299
+ );
300
+ console.log(
301
+ ` Health: http://localhost:${instance.config.blsPort}/health`
302
+ );
130
303
  console.log("=".repeat(50) + "\n");
131
304
  const shutdown = async (signal) => {
132
305
  console.log(`
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/launcher/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * CLI entrypoint for @toon-protocol/relay.\n *\n * Thin wrapper around startRelay() that parses CLI flags and environment\n * variables, then delegates to relay.ts. The relay is a plain read/write app:\n * free NIP-01 WebSocket reads plus an HTTP `POST /write` surface. Payment is\n * enforced upstream by an external terminator, so there are no connector,\n * ILP, chain, or pricing options here.\n *\n * Usage:\n * relay --secret-key <hex>\n * NOSTR_SECRET_KEY=<hex> relay\n *\n * Environment variables override defaults; CLI flags override environment variables.\n */\n\nimport { parseArgs } from 'node:util';\nimport { startRelay } from './relay.js';\nimport type { RelayConfig, RelayInstance } from './relay.js';\n\n// ---------- CLI Parsing ----------\n\nfunction printHelp(): void {\n console.log(\n `\nUsage: relay [options]\n\nOptions:\n --mnemonic <words> BIP-39 mnemonic (12 or 24 words; NIP-06 derivation)\n --secret-key <hex> 32-byte secret key in hex\n --relay-port <port> WebSocket read port (default: 7100)\n --bls-port <port> HTTP write/health port (default: 3100)\n --host <host> WebSocket bind host (default: 0.0.0.0)\n --data-dir <path> Data directory for the SQLite store (default: ./data)\n --dev-mode Skip event-signature verification on POST /write\n --help Show this help message\n\nEnvironment Variables:\n TOON_MNEMONIC Same as --mnemonic\n TOON_SECRET_KEY Same as --secret-key\n NOSTR_SECRET_KEY Alias for TOON_SECRET_KEY (identity); TOON_SECRET_KEY wins\n TOON_RELAY_PORT Same as --relay-port\n TOON_BLS_PORT Same as --bls-port\n TOON_HOST Same as --host\n TOON_DATA_DIR Same as --data-dir\n TOON_DEV_MODE Same as --dev-mode (set to \"true\")\n\nSecurity:\n Prefer TOON_MNEMONIC / TOON_SECRET_KEY / NOSTR_SECRET_KEY environment\n variables over --mnemonic / --secret-key CLI flags. CLI arguments are visible\n to other users on the system via process listings (e.g. ps aux). See CWE-214.\n`.trim()\n );\n}\n\nfunction parseCli(): RelayConfig {\n const { values } = parseArgs({\n options: {\n mnemonic: { type: 'string' },\n 'secret-key': { type: 'string' },\n 'relay-port': { type: 'string' },\n 'bls-port': { type: 'string' },\n host: { type: 'string' },\n 'data-dir': { type: 'string' },\n 'dev-mode': { type: 'boolean' },\n help: { type: 'boolean' },\n },\n strict: true,\n allowPositionals: false,\n });\n\n if (values.help) {\n printHelp();\n process.exit(0);\n }\n\n // Warn about process-listing exposure (CWE-214) when secrets are passed via CLI flags\n if (values.mnemonic) {\n console.warn(\n 'Warning: --mnemonic is visible in process listings. ' +\n 'Prefer TOON_MNEMONIC environment variable for production use.'\n );\n }\n if (values['secret-key']) {\n console.warn(\n 'Warning: --secret-key is visible in process listings. ' +\n 'Prefer TOON_SECRET_KEY environment variable for production use.'\n );\n }\n\n const mnemonic = values.mnemonic ?? process.env['TOON_MNEMONIC'] ?? undefined;\n\n // Identity secret key. NOSTR_SECRET_KEY is accepted as an alias for\n // TOON_SECRET_KEY so the container honors the same identity env the connector\n // compose uses. TOON_SECRET_KEY wins when both are set.\n const secretKeyHex =\n values['secret-key'] ??\n process.env['TOON_SECRET_KEY'] ??\n process.env['NOSTR_SECRET_KEY'] ??\n undefined;\n\n let secretKey: Uint8Array | undefined;\n if (secretKeyHex) {\n if (secretKeyHex.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(secretKeyHex)) {\n console.error('Error: --secret-key must be a 64-character hex string');\n process.exit(1);\n }\n secretKey = Uint8Array.from(Buffer.from(secretKeyHex, 'hex'));\n }\n\n if (!mnemonic && !secretKey) {\n console.error(\n 'Error: one of --mnemonic (or TOON_MNEMONIC) or --secret-key ' +\n '(or TOON_SECRET_KEY / NOSTR_SECRET_KEY) is required'\n );\n process.exit(1);\n }\n if (mnemonic && secretKey) {\n console.error(\n 'Error: provide either a mnemonic or a secret key, not both'\n );\n process.exit(1);\n }\n\n const relayPortStr =\n values['relay-port'] ?? process.env['TOON_RELAY_PORT'] ?? undefined;\n const relayPort = relayPortStr ? parseInt(relayPortStr, 10) : undefined;\n if (\n relayPort !== undefined &&\n (Number.isNaN(relayPort) || relayPort <= 0 || relayPort > 65535)\n ) {\n console.error('Error: --relay-port must be an integer between 1 and 65535');\n process.exit(1);\n }\n\n const blsPortStr =\n values['bls-port'] ?? process.env['TOON_BLS_PORT'] ?? undefined;\n const blsPort = blsPortStr ? parseInt(blsPortStr, 10) : undefined;\n if (\n blsPort !== undefined &&\n (Number.isNaN(blsPort) || blsPort <= 0 || blsPort > 65535)\n ) {\n console.error('Error: --bls-port must be an integer between 1 and 65535');\n process.exit(1);\n }\n\n const host = values.host ?? process.env['TOON_HOST'] ?? undefined;\n\n const dataDir =\n values['data-dir'] ?? process.env['TOON_DATA_DIR'] ?? undefined;\n\n const devMode =\n values['dev-mode'] ??\n (process.env['TOON_DEV_MODE'] === 'true' ? true : undefined);\n\n const config: RelayConfig = {\n ...(mnemonic && { mnemonic }),\n ...(secretKey && { secretKey }),\n ...(relayPort !== undefined && { relayPort }),\n ...(blsPort !== undefined && { blsPort }),\n ...(host && { host }),\n ...(dataDir && { dataDir }),\n ...(devMode !== undefined && { devMode }),\n };\n\n return config;\n}\n\n// ---------- Main ----------\n\nasync function main(): Promise<void> {\n const config = parseCli();\n\n console.log('\\n' + '='.repeat(50));\n console.log('TOON Relay Starting');\n console.log('='.repeat(50) + '\\n');\n\n const instance: RelayInstance = await startRelay(config);\n\n console.log('\\n' + '='.repeat(50));\n console.log('TOON Relay Ready');\n console.log('='.repeat(50));\n console.log(` Pubkey: ${instance.pubkey}`);\n console.log(` Reads: ws://localhost:${instance.config.relayPort}`);\n console.log(` Writes: http://localhost:${instance.config.blsPort}/write`);\n console.log(` Health: http://localhost:${instance.config.blsPort}/health`);\n console.log('='.repeat(50) + '\\n');\n\n // Wire graceful shutdown\n const shutdown = async (signal: string): Promise<void> => {\n console.log(`\\n[Shutdown] Received ${signal}`);\n await instance.stop();\n console.log('[Shutdown] Complete');\n process.exit(0);\n };\n\n process.on('SIGINT', () => {\n shutdown('SIGINT').catch(console.error);\n });\n process.on('SIGTERM', () => {\n shutdown('SIGTERM').catch(console.error);\n });\n}\n\nmain().catch((error: unknown) => {\n console.error('[Fatal] Startup error:', error);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/launcher/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * CLI entrypoint for @toon-protocol/relay.\n *\n * Thin wrapper around startRelay() that parses CLI flags and environment\n * variables, then delegates to relay.ts. The relay is a plain read/write app:\n * free NIP-01 WebSocket reads plus an HTTP `POST /write` surface. Payment is\n * enforced upstream by an external terminator, so there are no connector,\n * ILP, chain, or pricing options here.\n *\n * Usage:\n * relay --secret-key <hex>\n * NOSTR_SECRET_KEY=<hex> relay\n *\n * Environment variables override defaults; CLI flags override environment variables.\n */\n\nimport { parseArgs } from 'node:util';\nimport { startRelay } from './relay.js';\nimport type { RelayConfig, RelayInstance } from './relay.js';\nimport { DEFAULT_EPHEMERAL_RATE_LIMIT } from './handlers/write-ephemeral-handler.js';\nimport { parseBlockedEventIds } from '../nips/blocklist.js';\n\n// ---------- CLI Parsing ----------\n\nfunction printHelp(): void {\n console.log(\n `\nUsage: relay [options]\n\nOptions:\n --mnemonic <words> BIP-39 mnemonic (12 or 24 words; NIP-06 derivation)\n --secret-key <hex> 32-byte secret key in hex\n --relay-port <port> WebSocket read port (default: 7100)\n --bls-port <port> HTTP write/health port (default: 3100)\n --host <host> WebSocket bind host (default: 0.0.0.0)\n --write-host <host> HTTP write/health bind host (default: 0.0.0.0).\n The write port must only be reachable via the\n payment-gating connector; bind it to a loopback/\n internal address when not isolated by docker\n networking (relay#85 exposure guard)\n --data-dir <path> Data directory for the SQLite store (default: ./data)\n --dev-mode Skip event-signature verification on POST /write\n --verify-ephemeral Run FULL schnorr verification on ephemeral kinds\n (20000 <= kind < 30000). Default OFF: the write\n path is payment-gated and clients verify\n signatures themselves, so ephemeral kinds skip\n schnorr and keep only the SHA-256 id check\n (relay#85). Set this when the write port is\n fronted by anything other than a payment-gating\n connector\n --verify-workers <n> Worker threads for persistent-kind signature\n verification (default: CPU count - 1, minimum 0).\n 0 = verify inline on the event loop (automatic on\n 1-core boxes; the escape hatch back to the\n pre-pool behavior, relay#85)\n --max-connections <n> Maximum concurrent WebSocket read connections\n (default: 4096; each costs one file descriptor --\n mind ulimit -n, relay#90)\n --ephemeral-rate-limit <n>\n Free ephemeral write lane (POST /write-ephemeral,\n relay#129): max requests per key per window\n (default: 200). This lane has no payment gate, so\n this bound IS its admission control\n --ephemeral-rate-window-ms <n>\n Free ephemeral write lane: rate-limit window in\n milliseconds (default: 10000)\n --ephemeral-max-body-bytes <n>\n Free ephemeral write lane: request body size cap\n in bytes (default: 8192)\n --log-writes Log one line per accepted POST /write (debug; off\n by default -- per-event logging is write-path\n tail jitter, relay#85)\n --no-enforce-expiration Serve events past their NIP-40 expiration tag.\n KILL SWITCH back to the pre-relay#137 behaviour;\n enforcement is ON by default. Also disables the\n reaper -- a relay still serving expired events\n must not be silently deleting them\n --expiration-reap-grace-seconds <n>\n How long an expired event stays on disk before the\n reaper deletes it (default: 86400). The window in\n which flipping enforcement back off is a real\n recovery rather than an apology. 0 = reap at once\n --expiration-reap-interval-seconds <n>\n Reaper sweep interval (default: 3600). 0 disables\n reaping; serve-time filtering is unaffected\n --blocked-event-ids <ids>\n Comma-separated 64-hex event ids this relay\n refuses to store or serve. The escape hatch for an\n event whose author key is gone, so neither NIP-01\n replacement nor NIP-09 deletion can reach it. Ids\n only, never pubkeys; every id is logged at startup\n --help Show this help message\n\nEnvironment Variables:\n TOON_MNEMONIC Same as --mnemonic\n TOON_SECRET_KEY Same as --secret-key\n NOSTR_SECRET_KEY Alias for TOON_SECRET_KEY (identity); TOON_SECRET_KEY wins\n TOON_RELAY_PORT Same as --relay-port\n TOON_BLS_PORT Same as --bls-port\n TOON_HOST Same as --host\n TOON_WRITE_HOST Same as --write-host\n TOON_DATA_DIR Same as --data-dir\n TOON_DEV_MODE Same as --dev-mode (set to \"true\")\n TOON_VERIFY_EPHEMERAL Same as --verify-ephemeral (set to \"true\")\n TOON_VERIFY_WORKERS Same as --verify-workers\n TOON_MAX_CONNECTIONS Same as --max-connections\n TOON_EPHEMERAL_RATE_LIMIT Same as --ephemeral-rate-limit\n TOON_EPHEMERAL_RATE_WINDOW_MS Same as --ephemeral-rate-window-ms\n TOON_EPHEMERAL_MAX_BODY_BYTES Same as --ephemeral-max-body-bytes\n TOON_LOG_WRITES Same as --log-writes (set to \"true\")\n TOON_ENFORCE_EXPIRATION Set to \"false\" for --no-enforce-expiration\n TOON_EXPIRATION_REAP_GRACE_SECONDS Same as --expiration-reap-grace-seconds\n TOON_EXPIRATION_REAP_INTERVAL_SECONDS Same as --expiration-reap-interval-seconds\n TOON_BLOCKED_EVENT_IDS Same as --blocked-event-ids\n\nSecurity:\n Prefer TOON_MNEMONIC / TOON_SECRET_KEY / NOSTR_SECRET_KEY environment\n variables over --mnemonic / --secret-key CLI flags. CLI arguments are visible\n to other users on the system via process listings (e.g. ps aux). See CWE-214.\n`.trim()\n );\n}\n\n/**\n * Parse an optional numeric option that must be a positive integer.\n *\n * @param flag - The flag name, used verbatim in the error message.\n * @param raw - The flag value, or its env-var fallback, or undefined.\n * @returns The parsed value, or undefined when neither source is set (so the\n * caller leaves the field off `RelayConfig` and `startRelay()` applies its\n * own default). Exits the process on a non-positive/non-numeric value.\n */\nfunction parsePositiveIntOption(\n flag: string,\n raw: string | undefined\n): number | undefined {\n if (!raw) {\n return undefined;\n }\n const parsed = parseInt(raw, 10);\n if (Number.isNaN(parsed) || parsed <= 0) {\n console.error(`Error: --${flag} must be a positive integer`);\n process.exit(1);\n }\n return parsed;\n}\n\n/**\n * Parse an optional numeric option that must be a non-negative integer.\n *\n * Separate from `parsePositiveIntOption` because 0 is MEANINGFUL for the\n * retention knobs: a 0 reap grace means \"delete the moment it expires\" and a\n * 0 reap interval means \"never sweep\", both deliberate operator choices.\n *\n * @param flag - The flag name, used verbatim in the error message.\n * @param raw - The flag value, or its env-var fallback, or undefined.\n * @returns The parsed value, or undefined when neither source is set.\n */\nfunction parseNonNegativeIntOption(\n flag: string,\n raw: string | undefined\n): number | undefined {\n if (raw === undefined || raw === '') {\n return undefined;\n }\n const parsed = parseInt(raw, 10);\n if (Number.isNaN(parsed) || parsed < 0) {\n console.error(`Error: --${flag} must be an integer >= 0`);\n process.exit(1);\n }\n return parsed;\n}\n\nfunction parseCli(): RelayConfig {\n const { values } = parseArgs({\n options: {\n mnemonic: { type: 'string' },\n 'secret-key': { type: 'string' },\n 'relay-port': { type: 'string' },\n 'bls-port': { type: 'string' },\n host: { type: 'string' },\n 'write-host': { type: 'string' },\n 'data-dir': { type: 'string' },\n 'dev-mode': { type: 'boolean' },\n 'verify-ephemeral': { type: 'boolean' },\n 'verify-workers': { type: 'string' },\n 'max-connections': { type: 'string' },\n 'ephemeral-rate-limit': { type: 'string' },\n 'ephemeral-rate-window-ms': { type: 'string' },\n 'ephemeral-max-body-bytes': { type: 'string' },\n 'log-writes': { type: 'boolean' },\n 'no-enforce-expiration': { type: 'boolean' },\n 'expiration-reap-grace-seconds': { type: 'string' },\n 'expiration-reap-interval-seconds': { type: 'string' },\n 'blocked-event-ids': { type: 'string' },\n help: { type: 'boolean' },\n },\n strict: true,\n allowPositionals: false,\n });\n\n if (values.help) {\n printHelp();\n process.exit(0);\n }\n\n // Warn about process-listing exposure (CWE-214) when secrets are passed via CLI flags\n if (values.mnemonic) {\n console.warn(\n 'Warning: --mnemonic is visible in process listings. ' +\n 'Prefer TOON_MNEMONIC environment variable for production use.'\n );\n }\n if (values['secret-key']) {\n console.warn(\n 'Warning: --secret-key is visible in process listings. ' +\n 'Prefer TOON_SECRET_KEY environment variable for production use.'\n );\n }\n\n const mnemonic = values.mnemonic ?? process.env['TOON_MNEMONIC'] ?? undefined;\n\n // Identity secret key. NOSTR_SECRET_KEY is accepted as an alias for\n // TOON_SECRET_KEY so the container honors the same identity env the connector\n // compose uses. TOON_SECRET_KEY wins when both are set.\n const secretKeyHex =\n values['secret-key'] ??\n process.env['TOON_SECRET_KEY'] ??\n process.env['NOSTR_SECRET_KEY'] ??\n undefined;\n\n let secretKey: Uint8Array | undefined;\n if (secretKeyHex) {\n if (secretKeyHex.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(secretKeyHex)) {\n console.error('Error: --secret-key must be a 64-character hex string');\n process.exit(1);\n }\n secretKey = Uint8Array.from(Buffer.from(secretKeyHex, 'hex'));\n }\n\n if (!mnemonic && !secretKey) {\n console.error(\n 'Error: one of --mnemonic (or TOON_MNEMONIC) or --secret-key ' +\n '(or TOON_SECRET_KEY / NOSTR_SECRET_KEY) is required'\n );\n process.exit(1);\n }\n if (mnemonic && secretKey) {\n console.error('Error: provide either a mnemonic or a secret key, not both');\n process.exit(1);\n }\n\n const relayPortStr =\n values['relay-port'] ?? process.env['TOON_RELAY_PORT'] ?? undefined;\n const relayPort = relayPortStr ? parseInt(relayPortStr, 10) : undefined;\n if (\n relayPort !== undefined &&\n (Number.isNaN(relayPort) || relayPort <= 0 || relayPort > 65535)\n ) {\n console.error('Error: --relay-port must be an integer between 1 and 65535');\n process.exit(1);\n }\n\n const blsPortStr =\n values['bls-port'] ?? process.env['TOON_BLS_PORT'] ?? undefined;\n const blsPort = blsPortStr ? parseInt(blsPortStr, 10) : undefined;\n if (\n blsPort !== undefined &&\n (Number.isNaN(blsPort) || blsPort <= 0 || blsPort > 65535)\n ) {\n console.error('Error: --bls-port must be an integer between 1 and 65535');\n process.exit(1);\n }\n\n const host = values.host ?? process.env['TOON_HOST'] ?? undefined;\n\n const writeHost =\n values['write-host'] ?? process.env['TOON_WRITE_HOST'] ?? undefined;\n\n const dataDir =\n values['data-dir'] ?? process.env['TOON_DATA_DIR'] ?? undefined;\n\n const devMode =\n values['dev-mode'] ??\n (process.env['TOON_DEV_MODE'] === 'true' ? true : undefined);\n\n // Paid-ephemeral verify skip (relay#85): full verification on ephemeral\n // kinds is opt-in (the skip is the default; see RelayConfig.verifyEphemeral\n // for the payment-gated invariant that makes that safe).\n const verifyEphemeral =\n values['verify-ephemeral'] ??\n (process.env['TOON_VERIFY_EPHEMERAL'] === 'true' ? true : undefined);\n\n // Verify-pool size (relay#85). 0 is a meaningful value (inline path), so\n // only an absent/blank setting falls through to the default.\n const verifyWorkersStr =\n values['verify-workers'] ?? process.env['TOON_VERIFY_WORKERS'] ?? undefined;\n const verifyWorkers = verifyWorkersStr\n ? parseInt(verifyWorkersStr, 10)\n : undefined;\n if (\n verifyWorkers !== undefined &&\n (Number.isNaN(verifyWorkers) || verifyWorkers < 0 || verifyWorkers > 256)\n ) {\n console.error('Error: --verify-workers must be an integer >= 0');\n process.exit(1);\n }\n\n // WS connection cap (relay#90). Each connection costs one fd; the server\n // logs an advisory warning at startup if the cap exceeds the soft limit.\n const maxConnectionsStr =\n values['max-connections'] ??\n process.env['TOON_MAX_CONNECTIONS'] ??\n undefined;\n const maxConnections = maxConnectionsStr\n ? parseInt(maxConnectionsStr, 10)\n : undefined;\n if (\n maxConnections !== undefined &&\n (Number.isNaN(maxConnections) || maxConnections <= 0)\n ) {\n console.error('Error: --max-connections must be a positive integer');\n process.exit(1);\n }\n\n // Free ephemeral write lane bounds (relay#129) -- POST /write-ephemeral has\n // no payment gate, so these ARE its admission control. All three share the\n // same shape: unset falls through to startRelay()'s own default, anything\n // that is not a positive integer is a startup error.\n const ephemeralRateLimitMax = parsePositiveIntOption(\n 'ephemeral-rate-limit',\n values['ephemeral-rate-limit'] ?? process.env['TOON_EPHEMERAL_RATE_LIMIT']\n );\n const ephemeralRateWindowMs = parsePositiveIntOption(\n 'ephemeral-rate-window-ms',\n values['ephemeral-rate-window-ms'] ??\n process.env['TOON_EPHEMERAL_RATE_WINDOW_MS']\n );\n const ephemeralMaxBodyBytes = parsePositiveIntOption(\n 'ephemeral-max-body-bytes',\n values['ephemeral-max-body-bytes'] ??\n process.env['TOON_EPHEMERAL_MAX_BODY_BYTES']\n );\n\n // maxRequests/windowMs travel together as one RelayConfig field -- only\n // build it once at least one half was actually provided, so an unset pair\n // still falls through to startRelay()'s own default.\n const ephemeralRateLimit =\n ephemeralRateLimitMax !== undefined || ephemeralRateWindowMs !== undefined\n ? {\n maxRequests:\n ephemeralRateLimitMax ?? DEFAULT_EPHEMERAL_RATE_LIMIT.maxRequests,\n windowMs:\n ephemeralRateWindowMs ?? DEFAULT_EPHEMERAL_RATE_LIMIT.windowMs,\n }\n : undefined;\n\n const logWrites =\n values['log-writes'] ??\n (process.env['TOON_LOG_WRITES'] === 'true' ? true : undefined);\n\n // --- Retention (relay#137) ---\n // Enforcement is ON by default, so the only thing worth expressing here is\n // turning it OFF. `TOON_ENFORCE_EXPIRATION` is compared against the exact\n // string 'false' so a typo ('False', 'no', '0') fails SAFE -- towards\n // enforcing -- rather than silently reopening the hole this closed.\n const enforceExpiration =\n values['no-enforce-expiration'] === true ||\n process.env['TOON_ENFORCE_EXPIRATION'] === 'false'\n ? false\n : undefined;\n\n const expirationReapGraceSeconds = parseNonNegativeIntOption(\n 'expiration-reap-grace-seconds',\n values['expiration-reap-grace-seconds'] ??\n process.env['TOON_EXPIRATION_REAP_GRACE_SECONDS']\n );\n const expirationReapIntervalSeconds = parseNonNegativeIntOption(\n 'expiration-reap-interval-seconds',\n values['expiration-reap-interval-seconds'] ??\n process.env['TOON_EXPIRATION_REAP_INTERVAL_SECONDS']\n );\n\n // Operator blocklist. A malformed id is a hard startup error, not a warning:\n // its failure mode is an operator who believes an event is blocked while the\n // relay keeps serving it.\n const blocklist = parseBlockedEventIds(\n values['blocked-event-ids'] ?? process.env['TOON_BLOCKED_EVENT_IDS']\n );\n if (blocklist.invalid.length > 0) {\n console.error(\n 'Error: --blocked-event-ids entries must be 64-character hex event ids; ' +\n `rejected: ${blocklist.invalid.join(', ')}`\n );\n process.exit(1);\n }\n const blockedEventIds = blocklist.ids.length > 0 ? blocklist.ids : undefined;\n\n const config: RelayConfig = {\n ...(mnemonic && { mnemonic }),\n ...(secretKey && { secretKey }),\n ...(relayPort !== undefined && { relayPort }),\n ...(blsPort !== undefined && { blsPort }),\n ...(host && { host }),\n ...(writeHost && { writeHost }),\n ...(dataDir && { dataDir }),\n ...(devMode !== undefined && { devMode }),\n ...(verifyEphemeral !== undefined && { verifyEphemeral }),\n ...(verifyWorkers !== undefined && { verifyWorkers }),\n ...(maxConnections !== undefined && { maxConnections }),\n ...(ephemeralRateLimit !== undefined && { ephemeralRateLimit }),\n ...(ephemeralMaxBodyBytes !== undefined && { ephemeralMaxBodyBytes }),\n ...(logWrites !== undefined && { logWrites }),\n ...(enforceExpiration !== undefined && { enforceExpiration }),\n ...(expirationReapGraceSeconds !== undefined && {\n expirationReapGraceSeconds,\n }),\n ...(expirationReapIntervalSeconds !== undefined && {\n expirationReapIntervalSeconds,\n }),\n ...(blockedEventIds !== undefined && { blockedEventIds }),\n };\n\n return config;\n}\n\n// ---------- Main ----------\n\nasync function main(): Promise<void> {\n const config = parseCli();\n\n console.log('\\n' + '='.repeat(50));\n console.log('TOON Relay Starting');\n console.log('='.repeat(50) + '\\n');\n\n const instance: RelayInstance = await startRelay(config);\n\n console.log('\\n' + '='.repeat(50));\n console.log('TOON Relay Ready');\n console.log('='.repeat(50));\n console.log(` Pubkey: ${instance.pubkey}`);\n console.log(` Reads: ws://localhost:${instance.config.relayPort}`);\n console.log(\n ` Writes: http://localhost:${instance.config.blsPort}/write`\n );\n console.log(\n ` Ephemeral: http://localhost:${instance.config.blsPort}/write-ephemeral (free, relay#129)`\n );\n console.log(\n ` Health: http://localhost:${instance.config.blsPort}/health`\n );\n console.log('='.repeat(50) + '\\n');\n\n // Wire graceful shutdown\n const shutdown = async (signal: string): Promise<void> => {\n console.log(`\\n[Shutdown] Received ${signal}`);\n await instance.stop();\n console.log('[Shutdown] Complete');\n process.exit(0);\n };\n\n process.on('SIGINT', () => {\n shutdown('SIGINT').catch(console.error);\n });\n process.on('SIGTERM', () => {\n shutdown('SIGTERM').catch(console.error);\n });\n}\n\nmain().catch((error: unknown) => {\n console.error('[Fatal] Startup error:', error);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}