@roamcode.ai/server 1.4.5 → 2.0.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.
@@ -1,2742 +0,0 @@
1
- // src/relay-start.ts
2
- import {
3
- closeSync as closeSync2,
4
- constants as constants2,
5
- fstatSync as fstatSync2,
6
- lstatSync as lstatSync2,
7
- openSync as openSync2,
8
- readFileSync as readFileSync2,
9
- readdirSync,
10
- realpathSync
11
- } from "fs";
12
- import { join as join2, resolve } from "path";
13
- import { fileURLToPath, pathToFileURL } from "url";
14
-
15
- // src/relay-broker.ts
16
- import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual3 } from "crypto";
17
- import Fastify from "fastify";
18
- import websocket from "@fastify/websocket";
19
-
20
- // src/relay-account-store.ts
21
- import { createHash, randomBytes, timingSafeEqual } from "crypto";
22
- import { createRequire } from "module";
23
- var require2 = createRequire(import.meta.url);
24
- var RelayAccountRevisionConflictError = class extends Error {
25
- constructor(current) {
26
- super("relay account revision conflict");
27
- this.current = current;
28
- this.name = "RelayAccountRevisionConflictError";
29
- }
30
- current;
31
- };
32
- var PLAN_DEFAULTS = {
33
- free: { maxRoutes: 3, maxDevicesPerRoute: 16 },
34
- team: { maxRoutes: 25, maxDevicesPerRoute: 64 },
35
- enterprise: { maxRoutes: 500, maxDevicesPerRoute: 500 }
36
- };
37
- var UNSAFE_TERMINAL_TEXT = /[\p{Cc}\p{Zl}\p{Zp}\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
38
- function safeId(value) {
39
- if (typeof value !== "string" || !/^rra_[A-Za-z0-9_-]{16,128}$/.test(value)) {
40
- throw new Error("invalid relay account id");
41
- }
42
- return value;
43
- }
44
- function safeLabel(value) {
45
- if (typeof value !== "string") throw new Error("relay account label is required");
46
- const label = value.trim().replace(/\s+/g, " ");
47
- if (!label || label.length > 120 || UNSAFE_TERMINAL_TEXT.test(label)) {
48
- throw new Error("invalid relay account label");
49
- }
50
- return label;
51
- }
52
- function safeCredential(value) {
53
- if (typeof value !== "string" || !/^rrk_[A-Za-z0-9_-]{43}$/.test(value)) {
54
- throw new Error("invalid relay account credential");
55
- }
56
- return value;
57
- }
58
- function digest(label, credential) {
59
- return createHash("sha256").update(label).update("\0").update(safeCredential(credential)).digest("base64url");
60
- }
61
- function relayAccountCredentialHash(credential) {
62
- return `sha256:${digest("roamcode-relay-account-credential-v1", credential)}`;
63
- }
64
- function relayAccountCredentialLookup(credential) {
65
- return `lookup:${digest("roamcode-relay-account-lookup-v1", credential)}`;
66
- }
67
- function generateRelayAccountCredential() {
68
- return `rrk_${randomBytes(32).toString("base64url")}`;
69
- }
70
- function safeHash(value) {
71
- if (typeof value !== "string" || !/^sha256:[A-Za-z0-9_-]{43}$/.test(value)) {
72
- throw new Error("invalid relay account credential hash");
73
- }
74
- return value;
75
- }
76
- function safeLookup(value) {
77
- if (typeof value !== "string" || !/^lookup:[A-Za-z0-9_-]{43}$/.test(value)) {
78
- throw new Error("invalid relay account credential lookup");
79
- }
80
- return value;
81
- }
82
- function credentialMaterial(input) {
83
- if (typeof input === "string") {
84
- return {
85
- credentialHash: relayAccountCredentialHash(input),
86
- credentialLookup: relayAccountCredentialLookup(input)
87
- };
88
- }
89
- if ("credential" in input && input.credential !== void 0) {
90
- if (input.credentialHash !== void 0 || input.credentialLookup !== void 0) {
91
- throw new Error("relay account credential must be raw or pre-hashed, not both");
92
- }
93
- return {
94
- credentialHash: relayAccountCredentialHash(input.credential),
95
- credentialLookup: relayAccountCredentialLookup(input.credential)
96
- };
97
- }
98
- return {
99
- credentialHash: safeHash(input.credentialHash),
100
- credentialLookup: safeLookup(input.credentialLookup)
101
- };
102
- }
103
- function hashMatches(expected, credential) {
104
- try {
105
- const left = Buffer.from(safeHash(expected));
106
- const right = Buffer.from(relayAccountCredentialHash(credential));
107
- return left.length === right.length && timingSafeEqual(left, right);
108
- } catch {
109
- return false;
110
- }
111
- }
112
- function storedCredentialMatches(expectedHash, expectedLookup, credential) {
113
- try {
114
- const actual = credentialMaterial(credential);
115
- const expectedHashBytes = Buffer.from(safeHash(expectedHash));
116
- const actualHashBytes = Buffer.from(actual.credentialHash);
117
- const expectedLookupBytes = Buffer.from(safeLookup(expectedLookup));
118
- const actualLookupBytes = Buffer.from(actual.credentialLookup);
119
- return expectedHashBytes.length === actualHashBytes.length && expectedLookupBytes.length === actualLookupBytes.length && timingSafeEqual(expectedHashBytes, actualHashBytes) && timingSafeEqual(expectedLookupBytes, actualLookupBytes);
120
- } catch {
121
- return false;
122
- }
123
- }
124
- function safePlan(value) {
125
- if (value !== "free" && value !== "team" && value !== "enterprise") throw new Error("invalid relay account plan");
126
- return value;
127
- }
128
- function boundedLimit(value, fallback, maximum, field) {
129
- const candidate = value === void 0 ? fallback : value;
130
- if (!Number.isSafeInteger(candidate) || candidate < 1 || candidate > maximum) {
131
- throw new Error(`invalid relay account ${field}`);
132
- }
133
- return candidate;
134
- }
135
- function safeStatus(value) {
136
- if (value !== "active" && value !== "suspended" && value !== "deleted") {
137
- throw new Error("invalid relay account status");
138
- }
139
- return value;
140
- }
141
- function clone(record) {
142
- return {
143
- id: record.id,
144
- label: record.label,
145
- status: record.status,
146
- plan: record.plan,
147
- maxRoutes: record.maxRoutes,
148
- maxDevicesPerRoute: record.maxDevicesPerRoute,
149
- revision: record.revision,
150
- createdAt: record.createdAt,
151
- updatedAt: record.updatedAt
152
- };
153
- }
154
- function createMemoryStore(options) {
155
- const accounts = /* @__PURE__ */ new Map();
156
- const lookup = /* @__PURE__ */ new Map();
157
- const generateAccountId = options.generateAccountId ?? (() => `rra_${randomBytes(18).toString("base64url")}`);
158
- const verifyCredential = (credential) => {
159
- let credentialLookup;
160
- try {
161
- credentialLookup = relayAccountCredentialLookup(credential);
162
- } catch {
163
- return;
164
- }
165
- const id = lookup.get(credentialLookup);
166
- const record = id ? accounts.get(id) : void 0;
167
- return record && record.status !== "deleted" && hashMatches(record.credentialHash, credential) ? clone(record) : void 0;
168
- };
169
- return {
170
- mode: "memory",
171
- createAccount(input, now = Date.now()) {
172
- const id = safeId(input.id ?? generateAccountId());
173
- if (accounts.has(id)) throw new Error("relay account already exists");
174
- const plan = safePlan(input.plan ?? "free");
175
- const { credentialHash, credentialLookup } = credentialMaterial(input);
176
- if (lookup.has(credentialLookup)) throw new Error("relay account credential already exists");
177
- const defaults = PLAN_DEFAULTS[plan];
178
- const record = {
179
- id,
180
- label: safeLabel(input.label),
181
- status: "active",
182
- plan,
183
- maxRoutes: boundedLimit(input.maxRoutes, defaults.maxRoutes, 1e4, "route limit"),
184
- maxDevicesPerRoute: boundedLimit(
185
- input.maxDevicesPerRoute,
186
- defaults.maxDevicesPerRoute,
187
- 1e5,
188
- "device limit"
189
- ),
190
- revision: 1,
191
- createdAt: now,
192
- updatedAt: now,
193
- credentialHash,
194
- credentialLookup
195
- };
196
- accounts.set(id, record);
197
- lookup.set(credentialLookup, id);
198
- return clone(record);
199
- },
200
- getAccount(id) {
201
- const record = accounts.get(safeId(id));
202
- return record ? clone(record) : void 0;
203
- },
204
- listAccounts({ includeDeleted = false } = {}) {
205
- return [...accounts.values()].filter((record) => includeDeleted || record.status !== "deleted").sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id)).map(clone);
206
- },
207
- verifyCredential,
208
- authenticate(credential) {
209
- const record = verifyCredential(credential);
210
- return record?.status === "active" ? record : void 0;
211
- },
212
- credentialMatches(id, credential) {
213
- try {
214
- const record = accounts.get(safeId(id));
215
- return !!record && storedCredentialMatches(record.credentialHash, record.credentialLookup, credential);
216
- } catch {
217
- return false;
218
- }
219
- },
220
- updateAccount(id, input, expectedRevision, now = Date.now()) {
221
- const current = accounts.get(safeId(id));
222
- if (!current) return void 0;
223
- if (current.revision !== expectedRevision) throw new RelayAccountRevisionConflictError(clone(current));
224
- if (current.status === "deleted") throw new Error("deleted relay account is immutable");
225
- const plan = input.plan === void 0 ? current.plan : safePlan(input.plan);
226
- const status = input.status === void 0 ? current.status : safeStatus(input.status);
227
- const defaults = PLAN_DEFAULTS[plan];
228
- const next = {
229
- ...current,
230
- ...input.label === void 0 ? {} : { label: safeLabel(input.label) },
231
- status,
232
- plan,
233
- maxRoutes: boundedLimit(
234
- input.maxRoutes,
235
- input.plan === void 0 ? current.maxRoutes : defaults.maxRoutes,
236
- 1e4,
237
- "route limit"
238
- ),
239
- maxDevicesPerRoute: boundedLimit(
240
- input.maxDevicesPerRoute,
241
- input.plan === void 0 ? current.maxDevicesPerRoute : defaults.maxDevicesPerRoute,
242
- 1e5,
243
- "device limit"
244
- ),
245
- revision: current.revision + 1,
246
- updatedAt: now
247
- };
248
- accounts.set(current.id, next);
249
- return clone(next);
250
- },
251
- rotateCredential(id, credential, expectedRevision, now = Date.now()) {
252
- const current = accounts.get(safeId(id));
253
- if (!current) return void 0;
254
- if (current.revision !== expectedRevision) throw new RelayAccountRevisionConflictError(clone(current));
255
- if (current.status === "deleted") throw new Error("deleted relay account is immutable");
256
- const { credentialHash, credentialLookup } = credentialMaterial(credential);
257
- const owner = lookup.get(credentialLookup);
258
- if (owner && owner !== current.id) throw new Error("relay account credential already exists");
259
- lookup.delete(current.credentialLookup);
260
- lookup.set(credentialLookup, current.id);
261
- const next = {
262
- ...current,
263
- credentialHash,
264
- credentialLookup,
265
- revision: current.revision + 1,
266
- updatedAt: now
267
- };
268
- accounts.set(current.id, next);
269
- return clone(next);
270
- },
271
- close() {
272
- accounts.clear();
273
- lookup.clear();
274
- }
275
- };
276
- }
277
- function normalizeSqliteAccountConstraint(error) {
278
- const code = error?.code;
279
- const message = error instanceof Error ? error.message : "";
280
- if (code === "SQLITE_CONSTRAINT_UNIQUE" && message.includes("relay_accounts.credential_lookup")) {
281
- throw new Error("relay account credential already exists");
282
- }
283
- if (code === "SQLITE_CONSTRAINT_UNIQUE" && message.includes("relay_accounts.id")) {
284
- throw new Error("relay account already exists");
285
- }
286
- throw error;
287
- }
288
- function fromRow(row) {
289
- return {
290
- id: row.id,
291
- label: row.label,
292
- status: row.status,
293
- plan: row.plan,
294
- maxRoutes: row.max_routes,
295
- maxDevicesPerRoute: row.max_devices_per_route,
296
- revision: row.revision,
297
- createdAt: row.created_at,
298
- updatedAt: row.updated_at
299
- };
300
- }
301
- function openRelayAccountStore(options) {
302
- let Database;
303
- try {
304
- if (options.loadDatabase) Database = options.loadDatabase();
305
- else {
306
- const mod = require2("better-sqlite3");
307
- Database = mod.default ?? mod;
308
- }
309
- } catch {
310
- return createMemoryStore(options);
311
- }
312
- const db = new Database(options.dbPath);
313
- db.pragma("journal_mode = WAL");
314
- db.pragma("busy_timeout = 5000");
315
- db.exec(`
316
- CREATE TABLE IF NOT EXISTS relay_accounts (
317
- id TEXT PRIMARY KEY,
318
- label TEXT NOT NULL,
319
- status TEXT NOT NULL CHECK(status IN ('active', 'suspended', 'deleted')),
320
- plan TEXT NOT NULL CHECK(plan IN ('free', 'team', 'enterprise')),
321
- max_routes INTEGER NOT NULL,
322
- max_devices_per_route INTEGER NOT NULL,
323
- revision INTEGER NOT NULL,
324
- credential_hash TEXT NOT NULL,
325
- credential_lookup TEXT NOT NULL UNIQUE,
326
- created_at INTEGER NOT NULL,
327
- updated_at INTEGER NOT NULL
328
- );
329
- CREATE INDEX IF NOT EXISTS relay_accounts_status_idx ON relay_accounts(status);
330
- `);
331
- const generateAccountId = options.generateAccountId ?? (() => `rra_${randomBytes(18).toString("base64url")}`);
332
- const rowById = (id) => db.prepare("SELECT * FROM relay_accounts WHERE id = ?").get(id);
333
- const verifyCredential = (credential) => {
334
- let credentialLookup;
335
- try {
336
- credentialLookup = relayAccountCredentialLookup(credential);
337
- } catch {
338
- return;
339
- }
340
- const row = db.prepare("SELECT * FROM relay_accounts WHERE credential_lookup = ? AND status != 'deleted'").get(credentialLookup);
341
- return row && hashMatches(row.credential_hash, credential) ? fromRow(row) : void 0;
342
- };
343
- const createAccount = db.transaction((input, now) => {
344
- const id = safeId(input.id ?? generateAccountId());
345
- if (rowById(id)) throw new Error("relay account already exists");
346
- const plan = safePlan(input.plan ?? "free");
347
- const { credentialHash, credentialLookup } = credentialMaterial(input);
348
- const defaults = PLAN_DEFAULTS[plan];
349
- const record = {
350
- id,
351
- label: safeLabel(input.label),
352
- status: "active",
353
- plan,
354
- maxRoutes: boundedLimit(input.maxRoutes, defaults.maxRoutes, 1e4, "route limit"),
355
- maxDevicesPerRoute: boundedLimit(input.maxDevicesPerRoute, defaults.maxDevicesPerRoute, 1e5, "device limit"),
356
- revision: 1,
357
- createdAt: now,
358
- updatedAt: now
359
- };
360
- try {
361
- db.prepare(
362
- `INSERT INTO relay_accounts
363
- (id, label, status, plan, max_routes, max_devices_per_route, revision, credential_hash,
364
- credential_lookup, created_at, updated_at)
365
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
366
- ).run(
367
- record.id,
368
- record.label,
369
- record.status,
370
- record.plan,
371
- record.maxRoutes,
372
- record.maxDevicesPerRoute,
373
- record.revision,
374
- credentialHash,
375
- credentialLookup,
376
- record.createdAt,
377
- record.updatedAt
378
- );
379
- } catch (error) {
380
- normalizeSqliteAccountConstraint(error);
381
- }
382
- return record;
383
- });
384
- return {
385
- mode: "sqlite",
386
- createAccount: (input, now = Date.now()) => clone(createAccount(input, now)),
387
- getAccount(id) {
388
- const row = rowById(safeId(id));
389
- return row ? fromRow(row) : void 0;
390
- },
391
- listAccounts({ includeDeleted = false } = {}) {
392
- const rows = db.prepare(
393
- `SELECT * FROM relay_accounts ${includeDeleted ? "" : "WHERE status != 'deleted'"}
394
- ORDER BY created_at, id`
395
- ).all();
396
- return rows.map(fromRow);
397
- },
398
- verifyCredential,
399
- authenticate(credential) {
400
- const record = verifyCredential(credential);
401
- return record?.status === "active" ? record : void 0;
402
- },
403
- credentialMatches(id, credential) {
404
- try {
405
- const row = rowById(safeId(id));
406
- return !!row && storedCredentialMatches(row.credential_hash, row.credential_lookup, credential);
407
- } catch {
408
- return false;
409
- }
410
- },
411
- updateAccount(id, input, expectedRevision, now = Date.now()) {
412
- const safeAccountId2 = safeId(id);
413
- const currentRow = rowById(safeAccountId2);
414
- if (!currentRow) return void 0;
415
- const current = fromRow(currentRow);
416
- if (current.revision !== expectedRevision) throw new RelayAccountRevisionConflictError(current);
417
- if (current.status === "deleted") throw new Error("deleted relay account is immutable");
418
- const plan = input.plan === void 0 ? current.plan : safePlan(input.plan);
419
- const defaults = PLAN_DEFAULTS[plan];
420
- const next = {
421
- ...current,
422
- ...input.label === void 0 ? {} : { label: safeLabel(input.label) },
423
- status: input.status === void 0 ? current.status : safeStatus(input.status),
424
- plan,
425
- maxRoutes: boundedLimit(
426
- input.maxRoutes,
427
- input.plan === void 0 ? current.maxRoutes : defaults.maxRoutes,
428
- 1e4,
429
- "route limit"
430
- ),
431
- maxDevicesPerRoute: boundedLimit(
432
- input.maxDevicesPerRoute,
433
- input.plan === void 0 ? current.maxDevicesPerRoute : defaults.maxDevicesPerRoute,
434
- 1e5,
435
- "device limit"
436
- ),
437
- revision: current.revision + 1,
438
- updatedAt: now
439
- };
440
- const result = db.prepare(
441
- `UPDATE relay_accounts SET label = ?, status = ?, plan = ?, max_routes = ?,
442
- max_devices_per_route = ?, revision = ?, updated_at = ? WHERE id = ? AND revision = ?`
443
- ).run(
444
- next.label,
445
- next.status,
446
- next.plan,
447
- next.maxRoutes,
448
- next.maxDevicesPerRoute,
449
- next.revision,
450
- next.updatedAt,
451
- next.id,
452
- expectedRevision
453
- );
454
- if (result.changes !== 1) {
455
- const latest = rowById(safeAccountId2);
456
- if (latest) throw new RelayAccountRevisionConflictError(fromRow(latest));
457
- return void 0;
458
- }
459
- return next;
460
- },
461
- rotateCredential(id, credential, expectedRevision, now = Date.now()) {
462
- const safeAccountId2 = safeId(id);
463
- const currentRow = rowById(safeAccountId2);
464
- if (!currentRow) return void 0;
465
- const current = fromRow(currentRow);
466
- if (current.revision !== expectedRevision) throw new RelayAccountRevisionConflictError(current);
467
- if (current.status === "deleted") throw new Error("deleted relay account is immutable");
468
- const { credentialHash, credentialLookup } = credentialMaterial(credential);
469
- let result;
470
- try {
471
- result = db.prepare(
472
- `UPDATE relay_accounts SET credential_hash = ?, credential_lookup = ?, revision = revision + 1,
473
- updated_at = ? WHERE id = ? AND revision = ?`
474
- ).run(credentialHash, credentialLookup, now, safeAccountId2, expectedRevision);
475
- } catch (error) {
476
- normalizeSqliteAccountConstraint(error);
477
- }
478
- if (result.changes !== 1) {
479
- const latest = rowById(safeAccountId2);
480
- if (latest) throw new RelayAccountRevisionConflictError(fromRow(latest));
481
- return void 0;
482
- }
483
- return fromRow(rowById(safeAccountId2));
484
- },
485
- close: () => db.close()
486
- };
487
- }
488
-
489
- // src/relay-store.ts
490
- import { createHash as createHash2, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
491
- import { createRequire as createRequire2 } from "module";
492
- var require3 = createRequire2(import.meta.url);
493
- var UNSAFE_DISPLAY_TEXT = /[\p{Cc}\p{Zl}\p{Zp}\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
494
- function safeId2(value, field) {
495
- if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,256}$/.test(value)) throw new Error(`invalid relay ${field}`);
496
- return value;
497
- }
498
- function safeOwnerAccountId(value) {
499
- if (typeof value !== "string" || !/^rra_[A-Za-z0-9_-]{16,128}$/.test(value)) {
500
- throw new Error("invalid relay route owner");
501
- }
502
- return value;
503
- }
504
- function safeLabel2(value) {
505
- if (typeof value !== "string") throw new Error("relay route label is required");
506
- const normalized = value.trim().replace(/\s+/g, " ");
507
- if (!normalized || normalized.length > 80 || UNSAFE_DISPLAY_TEXT.test(normalized)) {
508
- throw new Error("invalid relay route label");
509
- }
510
- return normalized;
511
- }
512
- function safeCredential2(value) {
513
- if (typeof value !== "string" || value.length < 32 || value.length > 256 || !/^[A-Za-z0-9_-]+$/.test(value)) {
514
- throw new Error("invalid relay credential");
515
- }
516
- return value;
517
- }
518
- function relayCredentialHash(credential) {
519
- return `sha256:${createHash2("sha256").update("roamcode-relay-credential-v1\0").update(safeCredential2(credential)).digest("base64url")}`;
520
- }
521
- function generateRelayCredential(prefix = "rrd") {
522
- return `${prefix}_${randomBytes2(32).toString("base64url")}`;
523
- }
524
- function safeCredentialHash(value) {
525
- if (typeof value !== "string" || !/^sha256:[A-Za-z0-9_-]{43}$/.test(value)) {
526
- throw new Error("invalid relay credential hash");
527
- }
528
- return value;
529
- }
530
- function safeExpiry(value) {
531
- if (value === void 0) return void 0;
532
- if (!Number.isSafeInteger(value) || value < 0) throw new Error("invalid relay device expiry");
533
- return value;
534
- }
535
- function hashMatches2(expected, credential) {
536
- try {
537
- const actual = relayCredentialHash(credential);
538
- const left = Buffer.from(safeCredentialHash(expected));
539
- const right = Buffer.from(actual);
540
- return left.length === right.length && timingSafeEqual2(left, right);
541
- } catch {
542
- return false;
543
- }
544
- }
545
- function cloneRoute(route) {
546
- return { ...route };
547
- }
548
- function cloneDevice(device) {
549
- return {
550
- routeId: device.routeId,
551
- deviceId: device.deviceId,
552
- credentialHash: device.credentialHash,
553
- createdAt: device.createdAt,
554
- updatedAt: device.updatedAt,
555
- ...device.expiresAt === void 0 ? {} : { expiresAt: device.expiresAt }
556
- };
557
- }
558
- function credentialOverlap(current, credentialHash, expiresAt, now) {
559
- if (!current || expiresAt !== void 0) return {};
560
- if (current.expiresAt !== void 0) {
561
- if (current.credentialHash === credentialHash) {
562
- throw new Error("relay bootstrap promotion must rotate the device credential");
563
- }
564
- return {
565
- previousCredentialHash: current.credentialHash,
566
- previousCredentialExpiresAt: current.expiresAt
567
- };
568
- }
569
- if (current.previousCredentialHash !== void 0 && current.previousCredentialExpiresAt !== void 0 && current.previousCredentialExpiresAt >= now) {
570
- if (current.previousCredentialHash === credentialHash) {
571
- throw new Error("relay bootstrap credential cannot become durable");
572
- }
573
- return {
574
- previousCredentialHash: current.previousCredentialHash,
575
- previousCredentialExpiresAt: current.previousCredentialExpiresAt
576
- };
577
- }
578
- return {};
579
- }
580
- function createMemoryStore2(options) {
581
- const routes = /* @__PURE__ */ new Map();
582
- const devices = /* @__PURE__ */ new Map();
583
- const generateRouteId = options.generateRouteId ?? (() => `rrt_${randomBytes2(16).toString("base64url")}`);
584
- const deviceKey = (routeId, deviceId) => `${routeId}\0${deviceId}`;
585
- const pruneExpiredDevices = (now) => {
586
- for (const [key, device] of devices) {
587
- if (device.expiresAt !== void 0 && device.expiresAt < now) devices.delete(key);
588
- else if (device.previousCredentialExpiresAt !== void 0 && device.previousCredentialExpiresAt < now) {
589
- delete device.previousCredentialHash;
590
- delete device.previousCredentialExpiresAt;
591
- }
592
- }
593
- };
594
- const liveDevice = (routeId, deviceId, now) => {
595
- const key = deviceKey(routeId, deviceId);
596
- const device = devices.get(key);
597
- if (device?.expiresAt !== void 0 && device.expiresAt < now) {
598
- devices.delete(key);
599
- return;
600
- }
601
- return device;
602
- };
603
- const listRoutes = (now, ownerAccountId) => {
604
- pruneExpiredDevices(now);
605
- return [...routes.values()].filter((route) => ownerAccountId === void 0 || route.ownerAccountId === ownerAccountId).sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id)).map((route) => ({
606
- id: route.id,
607
- label: route.label,
608
- deviceCount: [...devices.values()].filter(
609
- (device) => device.routeId === route.id && (device.expiresAt === void 0 || device.expiresAt >= now)
610
- ).length,
611
- createdAt: route.createdAt,
612
- updatedAt: route.updatedAt
613
- }));
614
- };
615
- return {
616
- mode: "memory",
617
- createRoute(input, now = Date.now()) {
618
- const id = safeId2(input.id ?? generateRouteId(), "route id");
619
- if (routes.has(id)) throw new Error("relay route already exists");
620
- const route = {
621
- id,
622
- label: safeLabel2(input.label),
623
- hostCredentialHash: safeCredentialHash(input.hostCredentialHash),
624
- ...input.ownerAccountId === void 0 ? {} : { ownerAccountId: safeOwnerAccountId(input.ownerAccountId) },
625
- createdAt: now,
626
- updatedAt: now
627
- };
628
- routes.set(id, route);
629
- return cloneRoute(route);
630
- },
631
- getRoute(id) {
632
- const route = routes.get(safeId2(id, "route id"));
633
- return route ? cloneRoute(route) : void 0;
634
- },
635
- listRoutes: (now = Date.now()) => listRoutes(now),
636
- listRoutesByOwner(ownerAccountId, now = Date.now()) {
637
- return listRoutes(now, safeOwnerAccountId(ownerAccountId));
638
- },
639
- countDevices(routeId, now = Date.now()) {
640
- pruneExpiredDevices(now);
641
- const safeRouteId = safeId2(routeId, "route id");
642
- return [...devices.values()].filter(
643
- (device) => device.routeId === safeRouteId && (device.expiresAt === void 0 || device.expiresAt >= now)
644
- ).length;
645
- },
646
- rotateHostCredential(routeId, credentialHash, now = Date.now()) {
647
- const route = routes.get(safeId2(routeId, "route id"));
648
- if (!route) return false;
649
- route.hostCredentialHash = safeCredentialHash(credentialHash);
650
- route.updatedAt = now;
651
- return true;
652
- },
653
- deleteRoute(id) {
654
- const safeRouteId = safeId2(id, "route id");
655
- if (!routes.delete(safeRouteId)) return false;
656
- for (const [key, device] of devices) if (device.routeId === safeRouteId) devices.delete(key);
657
- return true;
658
- },
659
- authenticateHost(routeId, credential) {
660
- const route = routes.get(safeId2(routeId, "route id"));
661
- return !!route && hashMatches2(route.hostCredentialHash, credential);
662
- },
663
- putDevice(input, now = Date.now()) {
664
- pruneExpiredDevices(now);
665
- const routeId = safeId2(input.routeId, "route id");
666
- if (!routes.has(routeId)) throw new Error("relay route not found");
667
- const deviceId = safeId2(input.deviceId, "device id");
668
- const key = deviceKey(routeId, deviceId);
669
- const current = devices.get(key);
670
- const credentialHash = safeCredentialHash(input.credentialHash);
671
- const expiresAt = input.expiresAt === void 0 ? void 0 : safeExpiry(input.expiresAt);
672
- const device = {
673
- routeId,
674
- deviceId,
675
- credentialHash,
676
- createdAt: current?.createdAt ?? now,
677
- updatedAt: now,
678
- ...expiresAt === void 0 ? {} : { expiresAt },
679
- ...credentialOverlap(current, credentialHash, expiresAt, now)
680
- };
681
- devices.set(key, device);
682
- const route = routes.get(routeId);
683
- route.updatedAt = now;
684
- return cloneDevice(device);
685
- },
686
- getDevice(routeId, deviceId, now = Date.now()) {
687
- const device = liveDevice(safeId2(routeId, "route id"), safeId2(deviceId, "device id"), now);
688
- return device ? cloneDevice(device) : void 0;
689
- },
690
- authenticateDevice(routeId, deviceId, credential, now = Date.now()) {
691
- const device = liveDevice(safeId2(routeId, "route id"), safeId2(deviceId, "device id"), now);
692
- return !!device && (hashMatches2(device.credentialHash, credential) || device.previousCredentialHash !== void 0 && device.previousCredentialExpiresAt !== void 0 && device.previousCredentialExpiresAt >= now && hashMatches2(device.previousCredentialHash, credential));
693
- },
694
- revokeDevice(routeId, deviceId) {
695
- const safeRouteId = safeId2(routeId, "route id");
696
- const removed = devices.delete(deviceKey(safeRouteId, safeId2(deviceId, "device id")));
697
- if (removed) routes.get(safeRouteId).updatedAt = Date.now();
698
- return removed;
699
- },
700
- close() {
701
- routes.clear();
702
- devices.clear();
703
- }
704
- };
705
- }
706
- function routeFromRow(row) {
707
- return {
708
- id: row.id,
709
- label: row.label,
710
- hostCredentialHash: row.host_credential_hash,
711
- ...row.owner_account_id === null ? {} : { ownerAccountId: row.owner_account_id },
712
- createdAt: row.created_at,
713
- updatedAt: row.updated_at
714
- };
715
- }
716
- function deviceFromRow(row) {
717
- return {
718
- routeId: row.route_id,
719
- deviceId: row.device_id,
720
- credentialHash: row.credential_hash,
721
- createdAt: row.created_at,
722
- updatedAt: row.updated_at,
723
- ...row.expires_at === null || row.expires_at === void 0 ? {} : { expiresAt: row.expires_at }
724
- };
725
- }
726
- function openRelayRouteStore(options) {
727
- let Database;
728
- try {
729
- if (options.loadDatabase) Database = options.loadDatabase();
730
- else {
731
- const mod = require3("better-sqlite3");
732
- Database = mod.default ?? mod;
733
- }
734
- } catch {
735
- return createMemoryStore2(options);
736
- }
737
- const db = new Database(options.dbPath);
738
- db.pragma("journal_mode = WAL");
739
- db.pragma("busy_timeout = 5000");
740
- db.pragma("foreign_keys = ON");
741
- db.exec(`
742
- CREATE TABLE IF NOT EXISTS relay_routes (
743
- id TEXT PRIMARY KEY,
744
- label TEXT NOT NULL,
745
- host_credential_hash TEXT NOT NULL,
746
- owner_account_id TEXT,
747
- created_at INTEGER NOT NULL,
748
- updated_at INTEGER NOT NULL
749
- );
750
- CREATE TABLE IF NOT EXISTS relay_route_devices (
751
- route_id TEXT NOT NULL REFERENCES relay_routes(id) ON DELETE CASCADE,
752
- device_id TEXT NOT NULL,
753
- credential_hash TEXT NOT NULL,
754
- previous_credential_hash TEXT,
755
- previous_credential_expires_at INTEGER,
756
- created_at INTEGER NOT NULL,
757
- updated_at INTEGER NOT NULL,
758
- expires_at INTEGER,
759
- PRIMARY KEY(route_id, device_id)
760
- );
761
- `);
762
- const relayDeviceColumns = db.prepare("PRAGMA table_info(relay_route_devices)").all();
763
- if (!relayDeviceColumns.some((column) => column.name === "expires_at")) {
764
- db.exec("ALTER TABLE relay_route_devices ADD COLUMN expires_at INTEGER");
765
- }
766
- if (!relayDeviceColumns.some((column) => column.name === "previous_credential_hash")) {
767
- db.exec("ALTER TABLE relay_route_devices ADD COLUMN previous_credential_hash TEXT");
768
- }
769
- if (!relayDeviceColumns.some((column) => column.name === "previous_credential_expires_at")) {
770
- db.exec("ALTER TABLE relay_route_devices ADD COLUMN previous_credential_expires_at INTEGER");
771
- }
772
- const relayRouteColumns = db.prepare("PRAGMA table_info(relay_routes)").all();
773
- if (!relayRouteColumns.some((column) => column.name === "owner_account_id")) {
774
- db.exec("ALTER TABLE relay_routes ADD COLUMN owner_account_id TEXT");
775
- }
776
- db.exec("CREATE INDEX IF NOT EXISTS relay_routes_owner_idx ON relay_routes(owner_account_id)");
777
- const generateRouteId = options.generateRouteId ?? (() => `rrt_${randomBytes2(16).toString("base64url")}`);
778
- const getRoute = (id) => db.prepare("SELECT * FROM relay_routes WHERE id = ?").get(id);
779
- const getDevice = (routeId, deviceId) => db.prepare("SELECT * FROM relay_route_devices WHERE route_id = ? AND device_id = ?").get(routeId, deviceId);
780
- const pruneExpiredDevices = (now) => {
781
- db.prepare("DELETE FROM relay_route_devices WHERE expires_at IS NOT NULL AND expires_at < ?").run(now);
782
- db.prepare(
783
- `UPDATE relay_route_devices SET previous_credential_hash = NULL, previous_credential_expires_at = NULL
784
- WHERE previous_credential_expires_at IS NOT NULL AND previous_credential_expires_at < ?`
785
- ).run(now);
786
- };
787
- const liveDevice = (routeId, deviceId, now) => {
788
- const row = getDevice(routeId, deviceId);
789
- if (row?.expires_at !== null && row?.expires_at !== void 0 && row.expires_at < now) {
790
- db.prepare("DELETE FROM relay_route_devices WHERE route_id = ? AND device_id = ?").run(routeId, deviceId);
791
- return;
792
- }
793
- return row;
794
- };
795
- const createRoute = db.transaction(
796
- (input, now) => {
797
- const id = safeId2(input.id ?? generateRouteId(), "route id");
798
- if (getRoute(id)) throw new Error("relay route already exists");
799
- const route = {
800
- id,
801
- label: safeLabel2(input.label),
802
- hostCredentialHash: safeCredentialHash(input.hostCredentialHash),
803
- ...input.ownerAccountId === void 0 ? {} : { ownerAccountId: safeOwnerAccountId(input.ownerAccountId) },
804
- createdAt: now,
805
- updatedAt: now
806
- };
807
- db.prepare(
808
- `INSERT INTO relay_routes
809
- (id, label, host_credential_hash, owner_account_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`
810
- ).run(
811
- route.id,
812
- route.label,
813
- route.hostCredentialHash,
814
- route.ownerAccountId ?? null,
815
- route.createdAt,
816
- route.updatedAt
817
- );
818
- return route;
819
- }
820
- );
821
- const putDevice = db.transaction(
822
- (input, now) => {
823
- const routeId = safeId2(input.routeId, "route id");
824
- if (!getRoute(routeId)) throw new Error("relay route not found");
825
- pruneExpiredDevices(now);
826
- const deviceId = safeId2(input.deviceId, "device id");
827
- const current = getDevice(routeId, deviceId);
828
- const credentialHash = safeCredentialHash(input.credentialHash);
829
- const expiresAt = input.expiresAt === void 0 ? void 0 : safeExpiry(input.expiresAt);
830
- const overlap = credentialOverlap(
831
- current ? {
832
- ...deviceFromRow(current),
833
- ...current.previous_credential_hash === null || current.previous_credential_hash === void 0 ? {} : { previousCredentialHash: current.previous_credential_hash },
834
- ...current.previous_credential_expires_at === null || current.previous_credential_expires_at === void 0 ? {} : { previousCredentialExpiresAt: current.previous_credential_expires_at }
835
- } : void 0,
836
- credentialHash,
837
- expiresAt,
838
- now
839
- );
840
- const device = {
841
- routeId,
842
- deviceId,
843
- credentialHash,
844
- createdAt: current?.created_at ?? now,
845
- updatedAt: now,
846
- ...expiresAt === void 0 ? {} : { expiresAt }
847
- };
848
- db.prepare(
849
- `INSERT INTO relay_route_devices
850
- (route_id, device_id, credential_hash, previous_credential_hash, previous_credential_expires_at,
851
- created_at, updated_at, expires_at)
852
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
853
- ON CONFLICT(route_id, device_id) DO UPDATE SET credential_hash = excluded.credential_hash,
854
- previous_credential_hash = excluded.previous_credential_hash,
855
- previous_credential_expires_at = excluded.previous_credential_expires_at,
856
- updated_at = excluded.updated_at, expires_at = excluded.expires_at`
857
- ).run(
858
- device.routeId,
859
- device.deviceId,
860
- device.credentialHash,
861
- overlap.previousCredentialHash ?? null,
862
- overlap.previousCredentialExpiresAt ?? null,
863
- device.createdAt,
864
- device.updatedAt,
865
- device.expiresAt ?? null
866
- );
867
- db.prepare("UPDATE relay_routes SET updated_at = ? WHERE id = ?").run(now, routeId);
868
- return device;
869
- }
870
- );
871
- const listRoutes = (now, ownerAccountId) => {
872
- pruneExpiredDevices(now);
873
- return db.prepare(
874
- `SELECT r.id, r.label, r.created_at, r.updated_at, COUNT(d.device_id) AS device_count
875
- FROM relay_routes r LEFT JOIN relay_route_devices d ON d.route_id = r.id
876
- AND (d.expires_at IS NULL OR d.expires_at >= ?)
877
- ${ownerAccountId === void 0 ? "" : "WHERE r.owner_account_id = ?"}
878
- GROUP BY r.id ORDER BY r.created_at, r.id`
879
- ).all(now, ...ownerAccountId === void 0 ? [] : [safeOwnerAccountId(ownerAccountId)]).map((row) => ({
880
- id: row.id,
881
- label: row.label,
882
- deviceCount: row.device_count,
883
- createdAt: row.created_at,
884
- updatedAt: row.updated_at
885
- }));
886
- };
887
- return {
888
- mode: "sqlite",
889
- createRoute: (input, now = Date.now()) => cloneRoute(createRoute(input, now)),
890
- getRoute(id) {
891
- const row = getRoute(safeId2(id, "route id"));
892
- return row ? routeFromRow(row) : void 0;
893
- },
894
- listRoutes: (now = Date.now()) => listRoutes(now),
895
- listRoutesByOwner: (ownerAccountId, now = Date.now()) => listRoutes(now, ownerAccountId),
896
- countDevices(routeId, now = Date.now()) {
897
- pruneExpiredDevices(now);
898
- const row = db.prepare(
899
- `SELECT COUNT(*) AS count FROM relay_route_devices
900
- WHERE route_id = ? AND (expires_at IS NULL OR expires_at >= ?)`
901
- ).get(safeId2(routeId, "route id"), now);
902
- return row.count;
903
- },
904
- rotateHostCredential(routeId, credentialHash, now = Date.now()) {
905
- return db.prepare("UPDATE relay_routes SET host_credential_hash = ?, updated_at = ? WHERE id = ?").run(safeCredentialHash(credentialHash), now, safeId2(routeId, "route id")).changes > 0;
906
- },
907
- deleteRoute(id) {
908
- return db.prepare("DELETE FROM relay_routes WHERE id = ?").run(safeId2(id, "route id")).changes > 0;
909
- },
910
- authenticateHost(routeId, credential) {
911
- const row = getRoute(safeId2(routeId, "route id"));
912
- return !!row && hashMatches2(row.host_credential_hash, credential);
913
- },
914
- putDevice: (input, now = Date.now()) => cloneDevice(putDevice(input, now)),
915
- getDevice(routeId, deviceId, now = Date.now()) {
916
- const row = liveDevice(safeId2(routeId, "route id"), safeId2(deviceId, "device id"), now);
917
- return row ? deviceFromRow(row) : void 0;
918
- },
919
- authenticateDevice(routeId, deviceId, credential, now = Date.now()) {
920
- const row = liveDevice(safeId2(routeId, "route id"), safeId2(deviceId, "device id"), now);
921
- return !!row && (hashMatches2(row.credential_hash, credential) || row.previous_credential_hash !== null && row.previous_credential_hash !== void 0 && row.previous_credential_expires_at !== null && row.previous_credential_expires_at !== void 0 && row.previous_credential_expires_at >= now && hashMatches2(row.previous_credential_hash, credential));
922
- },
923
- revokeDevice(routeId, deviceId) {
924
- const safeRouteId = safeId2(routeId, "route id");
925
- const result = db.prepare("DELETE FROM relay_route_devices WHERE route_id = ? AND device_id = ?").run(safeRouteId, safeId2(deviceId, "device id"));
926
- if (result.changes > 0)
927
- db.prepare("UPDATE relay_routes SET updated_at = ? WHERE id = ?").run(Date.now(), safeRouteId);
928
- return result.changes > 0;
929
- },
930
- close: () => db.close()
931
- };
932
- }
933
-
934
- // src/relay-broker.ts
935
- var BLIND_RELAY_PROTOCOL_VERSION = 1;
936
- var BLIND_RELAY_DEFAULT_MAX_FRAME_BYTES = 15e5;
937
- var BLIND_RELAY_DEFAULT_MAX_QUEUE_BYTES = 4e6;
938
- var BLIND_RELAY_DEFAULT_MAX_TOTAL_CONNECTIONS = 1024;
939
- var BLIND_RELAY_DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 64;
940
- var BLIND_RELAY_DEFAULT_MAX_BYTES_PER_MINUTE = 64 * 1024 * 1024;
941
- var BLIND_RELAY_DEFAULT_MAX_MESSAGES_PER_MINUTE = 12e3;
942
- var UNSAFE_DISPLAY_TEXT2 = /[\p{Cc}\p{Zl}\p{Zp}\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
943
- function safeToken(value, field) {
944
- if (typeof value !== "string" || value.length < 32 || value.length > 256 || !/^[A-Za-z0-9_-]+$/.test(value)) {
945
- throw new Error(`invalid relay ${field}`);
946
- }
947
- return value;
948
- }
949
- function safeCredentialHash2(value) {
950
- if (typeof value !== "string" || !/^sha256:[A-Za-z0-9_-]{43}$/.test(value)) {
951
- throw new Error("invalid relay credential hash");
952
- }
953
- return value;
954
- }
955
- function safeId3(value, field) {
956
- if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,256}$/.test(value)) throw new Error(`invalid relay ${field}`);
957
- return value;
958
- }
959
- function safeLabel3(value) {
960
- if (typeof value !== "string") throw new Error("relay route label is required");
961
- const label = value.trim().replace(/\s+/g, " ");
962
- if (!label || label.length > 80 || UNSAFE_DISPLAY_TEXT2.test(label)) throw new Error("invalid relay route label");
963
- return label;
964
- }
965
- function safeExpiry2(value, now) {
966
- if (value === void 0 || value === null) return void 0;
967
- if (!Number.isSafeInteger(value) || value <= now || value > now + 10 * 6e4) {
968
- throw new Error("invalid relay device expiry");
969
- }
970
- return value;
971
- }
972
- function safeRevision(value) {
973
- if (!Number.isSafeInteger(value) || value < 1) throw new Error("invalid relay account revision");
974
- return value;
975
- }
976
- function safeAccountId(value) {
977
- if (typeof value !== "string" || !/^rra_[A-Za-z0-9_-]{16,128}$/.test(value)) {
978
- throw new Error("invalid relay account id");
979
- }
980
- return value;
981
- }
982
- function safeAccountLabel(value) {
983
- if (typeof value !== "string") throw new Error("relay account label is required");
984
- const label = value.trim().replace(/\s+/g, " ");
985
- if (!label || label.length > 120 || UNSAFE_DISPLAY_TEXT2.test(label)) throw new Error("invalid relay account label");
986
- return label;
987
- }
988
- function safeAccountPlan(value) {
989
- if (value !== "free" && value !== "team" && value !== "enterprise") {
990
- throw new Error("invalid relay account plan");
991
- }
992
- return value;
993
- }
994
- function safeAccountLimit(value, maximum) {
995
- if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
996
- throw new Error("invalid relay account limit");
997
- }
998
- return value;
999
- }
1000
- function strictInternalBody(value, allowedFields) {
1001
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid internal request body");
1002
- const body = value;
1003
- if (Object.keys(body).some((field) => !allowedFields.includes(field))) {
1004
- throw new Error("unknown internal request field");
1005
- }
1006
- return body;
1007
- }
1008
- function safeCredentialLookup(value) {
1009
- if (typeof value !== "string" || !/^lookup:[A-Za-z0-9_-]{43}$/.test(value)) {
1010
- throw new Error("invalid relay credential lookup");
1011
- }
1012
- return value;
1013
- }
1014
- function safeAccountCredentialMaterial(body) {
1015
- if (!body || body.credential !== void 0 || body.accountCredential !== void 0) {
1016
- throw new Error("client-hashed account credential material is required");
1017
- }
1018
- return {
1019
- credentialHash: safeCredentialHash2(body.credentialHash),
1020
- credentialLookup: safeCredentialLookup(body.credentialLookup)
1021
- };
1022
- }
1023
- function safeRouteCredentialHash(body, field = "credentialHash") {
1024
- if (!body || body.credential !== void 0 || body.hostCredential !== void 0) {
1025
- throw new Error("client-hashed route credential material is required");
1026
- }
1027
- return safeCredentialHash2(body[field]);
1028
- }
1029
- function bearer(request) {
1030
- const value = request.headers.authorization;
1031
- if (!value || Array.isArray(value)) return;
1032
- const match = /^Bearer ([A-Za-z0-9_-]{32,256})$/.exec(value);
1033
- return match?.[1];
1034
- }
1035
- function tokenMatches(left, right) {
1036
- if (!right) return false;
1037
- const a = Buffer.from(left);
1038
- const b = Buffer.from(right);
1039
- return a.length === b.length && timingSafeEqual3(a, b);
1040
- }
1041
- function publicRoute(route) {
1042
- return {
1043
- id: route.id,
1044
- label: route.label,
1045
- deviceCount: route.deviceCount,
1046
- createdAt: route.createdAt,
1047
- updatedAt: route.updatedAt
1048
- };
1049
- }
1050
- function normalizeOrigin(value) {
1051
- try {
1052
- const url = new URL(value);
1053
- const loopback = url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
1054
- if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.pathname !== "/" || url.search || url.hash)
1055
- return;
1056
- return url.origin;
1057
- } catch {
1058
- return;
1059
- }
1060
- }
1061
- function parseJson(raw, limit) {
1062
- const buffer = Buffer.isBuffer(raw) ? raw : raw instanceof ArrayBuffer ? Buffer.from(raw) : Buffer.concat(raw);
1063
- if (buffer.byteLength > limit) throw new Error("relay frame too large");
1064
- return JSON.parse(buffer.toString("utf8"));
1065
- }
1066
- function parseAuthHello(value) {
1067
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid relay hello");
1068
- const hello = value;
1069
- if (hello.v !== BLIND_RELAY_PROTOCOL_VERSION || hello.role !== "host" && hello.role !== "device") {
1070
- throw new Error("invalid relay hello");
1071
- }
1072
- const routeId = safeId3(hello.routeId, "route id");
1073
- const credential = safeToken(hello.credential, "credential");
1074
- return hello.role === "host" ? { v: 1, role: "host", routeId, credential } : { v: 1, role: "device", routeId, deviceId: safeId3(hello.deviceId, "device id"), credential };
1075
- }
1076
- function parsePayload(value, requireChannel, maxBytes) {
1077
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid relay frame");
1078
- const frame = value;
1079
- if (frame.t !== "frame" || typeof frame.payload !== "string" || !/^[A-Za-z0-9_-]+$/.test(frame.payload)) {
1080
- throw new Error("invalid relay frame");
1081
- }
1082
- const bytes = Math.floor(frame.payload.length * 3 / 4);
1083
- if (bytes < 1 || bytes > maxBytes) throw new Error("relay frame too large");
1084
- const canonical = Buffer.from(frame.payload, "base64url").toString("base64url");
1085
- if (canonical !== frame.payload) throw new Error("invalid relay frame");
1086
- return {
1087
- ...requireChannel ? { channelId: safeId3(frame.channelId, "channel id") } : {},
1088
- payload: frame.payload,
1089
- bytes
1090
- };
1091
- }
1092
- function safeSend(socket, value, maxQueueBytes) {
1093
- if (socket.readyState !== socket.OPEN) return false;
1094
- const encoded = JSON.stringify(value);
1095
- if (socket.bufferedAmount + Buffer.byteLength(encoded) > maxQueueBytes) return false;
1096
- try {
1097
- socket.send(encoded);
1098
- return true;
1099
- } catch {
1100
- return false;
1101
- }
1102
- }
1103
- function createBlindRelayServer(options) {
1104
- const rootTokens = [safeToken(options.rootToken, "root token")];
1105
- for (const token of options.previousRootTokens ?? []) rootTokens.push(safeToken(token, "previous root token"));
1106
- if (rootTokens.length > 4 || new Set(rootTokens).size !== rootTokens.length) {
1107
- throw new Error("invalid relay root token rotation set");
1108
- }
1109
- const store = options.store ?? openRelayRouteStore({ dbPath: ":memory:" });
1110
- const ownsStore = options.store === void 0;
1111
- const accountStore = options.accountStore;
1112
- const now = options.now ?? Date.now;
1113
- const handshakeTimeoutMs = options.handshakeTimeoutMs ?? 5e3;
1114
- const idleTimeoutMs = options.idleTimeoutMs ?? 2 * 6e4;
1115
- const maxFrameBytes = options.maxFrameBytes ?? BLIND_RELAY_DEFAULT_MAX_FRAME_BYTES;
1116
- const maxQueueBytes = options.maxQueueBytes ?? BLIND_RELAY_DEFAULT_MAX_QUEUE_BYTES;
1117
- const maxTotalConnections = options.maxTotalConnections ?? BLIND_RELAY_DEFAULT_MAX_TOTAL_CONNECTIONS;
1118
- const maxConnectionsPerRoute = options.maxConnectionsPerRoute ?? BLIND_RELAY_DEFAULT_MAX_CONNECTIONS_PER_ROUTE;
1119
- const maxBytesPerMinute = options.maxBytesPerMinute ?? BLIND_RELAY_DEFAULT_MAX_BYTES_PER_MINUTE;
1120
- const maxMessagesPerMinute = options.maxMessagesPerMinute ?? BLIND_RELAY_DEFAULT_MAX_MESSAGES_PER_MINUTE;
1121
- for (const [value, minimum, maximum, label] of [
1122
- [handshakeTimeoutMs, 1e3, 3e4, "handshake timeout"],
1123
- [idleTimeoutMs, 1e4, 60 * 6e4, "idle timeout"],
1124
- [maxFrameBytes, 1024, 16 * 1024 * 1024, "frame limit"],
1125
- [maxQueueBytes, 1024, 64 * 1024 * 1024, "queue limit"],
1126
- [maxTotalConnections, 1, 1e5, "total connection limit"],
1127
- [maxConnectionsPerRoute, 1, 1e4, "connection limit"],
1128
- [maxBytesPerMinute, 1024, 1024 * 1024 * 1024, "byte rate"],
1129
- [maxMessagesPerMinute, 10, 1e6, "message rate"]
1130
- ]) {
1131
- if (!Number.isSafeInteger(value) || value < minimum || value > maximum) throw new Error(`invalid relay ${label}`);
1132
- }
1133
- const maxEnvelopeBytes = Math.max(8 * 1024, Math.ceil(maxFrameBytes * 4 / 3) + 8 * 1024);
1134
- const configuredOrigins = options.allowedOrigins ?? [];
1135
- const normalizedOrigins = configuredOrigins.map(normalizeOrigin);
1136
- if (normalizedOrigins.some((origin) => origin === void 0)) {
1137
- throw new Error("invalid relay allowed origin");
1138
- }
1139
- const allowedOrigins = new Set(normalizedOrigins);
1140
- const generateChannelId = options.generateChannelId ?? (() => `rrc_${randomBytes3(16).toString("base64url")}`);
1141
- const hosts = /* @__PURE__ */ new Map();
1142
- const devicesByChannel = /* @__PURE__ */ new Map();
1143
- const sockets = /* @__PURE__ */ new Set();
1144
- const hostRates = /* @__PURE__ */ new Map();
1145
- const deviceRates = /* @__PURE__ */ new Map();
1146
- const maxRateIdentities = Math.min(1e5, Math.max(256, maxTotalConnections * 4));
1147
- const deviceRateKey = (routeId, deviceId) => `${routeId}\0${deviceId}`;
1148
- const clearRouteRates = (routeId) => {
1149
- hostRates.delete(routeId);
1150
- for (const key of deviceRates.keys()) if (key.startsWith(`${routeId}\0`)) deviceRates.delete(key);
1151
- };
1152
- const rateWindowFor = (windows, key) => {
1153
- const current = now();
1154
- if (windows.size >= maxRateIdentities) {
1155
- for (const [candidate, window] of windows) {
1156
- if (current - window.lastSeenAt >= 12e4) windows.delete(candidate);
1157
- }
1158
- }
1159
- const existing = windows.get(key);
1160
- if (existing) {
1161
- existing.lastSeenAt = current;
1162
- return existing;
1163
- }
1164
- if (windows.size >= maxRateIdentities) return;
1165
- const created = { startedAt: current, lastSeenAt: current, bytes: 0, messages: 0 };
1166
- windows.set(key, created);
1167
- return created;
1168
- };
1169
- const consumeRate = (window, bytes) => {
1170
- const current = now();
1171
- if (current - window.startedAt >= 6e4) {
1172
- window.startedAt = current;
1173
- window.bytes = 0;
1174
- window.messages = 0;
1175
- }
1176
- window.lastSeenAt = current;
1177
- window.bytes += bytes;
1178
- window.messages += 1;
1179
- return window.bytes <= maxBytesPerMinute && window.messages <= maxMessagesPerMinute;
1180
- };
1181
- const metrics = {
1182
- activeConnections: 0,
1183
- activeHosts: 0,
1184
- activeDevices: 0,
1185
- acceptedConnections: 0,
1186
- rejectedConnections: 0,
1187
- forwardedFrames: 0,
1188
- forwardedBytes: 0,
1189
- droppedFrames: 0
1190
- };
1191
- const app = Fastify({ logger: false, trustProxy: false, bodyLimit: 32 * 1024 });
1192
- const authenticatedAccounts = /* @__PURE__ */ new WeakMap();
1193
- const routeAccountIsActive = (routeId) => {
1194
- const ownerAccountId = store.getRoute(routeId)?.ownerAccountId;
1195
- if (!ownerAccountId) return true;
1196
- return accountStore?.getAccount(ownerAccountId)?.status === "active";
1197
- };
1198
- const requireRoot = async (request, reply) => {
1199
- const presented = bearer(request);
1200
- if (rootTokens.some((token) => tokenMatches(token, presented))) return;
1201
- reply.header("www-authenticate", "Bearer").code(401).send({ code: "RELAY_UNAUTHORIZED", error: "unauthorized" });
1202
- };
1203
- const requireHost = async (request, reply) => {
1204
- try {
1205
- if (routeAccountIsActive(request.params.routeId) && store.authenticateHost(request.params.routeId, bearer(request) ?? ""))
1206
- return;
1207
- } catch {
1208
- }
1209
- reply.header("www-authenticate", "Bearer").code(401).send({ code: "RELAY_UNAUTHORIZED", error: "unauthorized" });
1210
- };
1211
- const requireAccount = async (request, reply) => {
1212
- const account = accountStore?.authenticate(bearer(request) ?? "");
1213
- if (account) {
1214
- authenticatedAccounts.set(request, account);
1215
- return;
1216
- }
1217
- reply.header("www-authenticate", "Bearer").code(401).send({ code: "RELAY_UNAUTHORIZED", error: "unauthorized" });
1218
- };
1219
- const requireRecoverableAccount = async (request, reply) => {
1220
- const account = accountStore?.verifyCredential(bearer(request) ?? "");
1221
- if (account) {
1222
- authenticatedAccounts.set(request, account);
1223
- return;
1224
- }
1225
- reply.header("www-authenticate", "Bearer").code(401).send({ code: "RELAY_UNAUTHORIZED", error: "unauthorized" });
1226
- };
1227
- app.addHook("onSend", async (_request, reply, payload) => {
1228
- reply.header("cache-control", "no-store");
1229
- reply.header("content-security-policy", "default-src 'none'");
1230
- reply.header("x-content-type-options", "nosniff");
1231
- return payload;
1232
- });
1233
- app.get("/health", async () => ({ status: "ok", protocolVersion: BLIND_RELAY_PROTOCOL_VERSION }));
1234
- app.get("/ready", async (_request, reply) => {
1235
- try {
1236
- store.listRoutes(now());
1237
- accountStore?.listAccounts();
1238
- return { status: "ready", protocolVersion: BLIND_RELAY_PROTOCOL_VERSION };
1239
- } catch {
1240
- reply.code(503);
1241
- return { status: "unavailable", protocolVersion: BLIND_RELAY_PROTOCOL_VERSION };
1242
- }
1243
- });
1244
- app.get("/v1/metrics", { preHandler: requireRoot }, async () => ({
1245
- protocolVersion: BLIND_RELAY_PROTOCOL_VERSION,
1246
- metrics: {
1247
- ...metrics,
1248
- activeConnections: sockets.size,
1249
- activeHosts: hosts.size,
1250
- activeDevices: devicesByChannel.size
1251
- }
1252
- }));
1253
- app.get("/v1/routes", { preHandler: requireRoot }, async () => ({ routes: store.listRoutes().map(publicRoute) }));
1254
- app.post(
1255
- "/v1/routes",
1256
- { preHandler: requireRoot },
1257
- async (request, reply) => {
1258
- try {
1259
- const hostCredential = generateRelayCredential("rrh");
1260
- const route = store.createRoute({
1261
- ...request.body?.id === void 0 ? {} : { id: safeId3(request.body.id, "route id") },
1262
- label: safeLabel3(request.body?.label),
1263
- hostCredentialHash: relayCredentialHash(hostCredential)
1264
- });
1265
- reply.code(201).send({
1266
- route: {
1267
- id: route.id,
1268
- label: route.label,
1269
- deviceCount: 0,
1270
- createdAt: route.createdAt,
1271
- updatedAt: route.updatedAt
1272
- },
1273
- hostCredential
1274
- });
1275
- } catch (error) {
1276
- const conflict = error.message === "relay route already exists";
1277
- reply.code(conflict ? 409 : 400).send({
1278
- code: conflict ? "RELAY_ROUTE_EXISTS" : "INVALID_RELAY_ROUTE",
1279
- error: conflict ? "relay route already exists" : "invalid relay route"
1280
- });
1281
- }
1282
- }
1283
- );
1284
- app.delete(
1285
- "/v1/routes/:routeId",
1286
- { preHandler: requireRoot },
1287
- async (request, reply) => {
1288
- let removed = false;
1289
- try {
1290
- removed = store.deleteRoute(request.params.routeId);
1291
- } catch {
1292
- }
1293
- if (!removed) {
1294
- reply.code(404).send({ code: "RELAY_ROUTE_NOT_FOUND", error: "relay route not found" });
1295
- return;
1296
- }
1297
- clearRouteRates(request.params.routeId);
1298
- const host = hosts.get(request.params.routeId);
1299
- host?.socket.close(4403, "route deleted");
1300
- for (const device of host?.devices.values() ?? []) device.socket.close(4403, "route deleted");
1301
- reply.code(204).send();
1302
- }
1303
- );
1304
- app.get(
1305
- "/v1/routes/:routeId/status",
1306
- { preHandler: requireHost },
1307
- async (request) => ({
1308
- routeId: request.params.routeId,
1309
- hostOnline: hosts.has(request.params.routeId),
1310
- activeDevices: hosts.get(request.params.routeId)?.devices.size ?? 0
1311
- })
1312
- );
1313
- app.put("/v1/routes/:routeId/devices/:deviceId", { preHandler: requireHost }, async (request, reply) => {
1314
- try {
1315
- const route = store.getRoute(request.params.routeId);
1316
- if (route?.ownerAccountId && !store.getDevice(route.id, request.params.deviceId, now())) {
1317
- const account = accountStore?.getAccount(route.ownerAccountId);
1318
- if (!account || store.countDevices(route.id, now()) >= account.maxDevicesPerRoute) {
1319
- reply.code(429).send({ code: "RELAY_DEVICE_LIMIT", error: "relay device limit reached" });
1320
- return;
1321
- }
1322
- }
1323
- const device = store.putDevice({
1324
- routeId: request.params.routeId,
1325
- deviceId: request.params.deviceId,
1326
- credentialHash: typeof request.body?.credentialHash === "string" ? request.body.credentialHash : "invalid",
1327
- ...request.body?.expiresAt === void 0 ? {} : { expiresAt: safeExpiry2(request.body.expiresAt, now()) }
1328
- });
1329
- reply.code(200).send({
1330
- device: {
1331
- routeId: device.routeId,
1332
- deviceId: device.deviceId,
1333
- createdAt: device.createdAt,
1334
- updatedAt: device.updatedAt
1335
- }
1336
- });
1337
- } catch {
1338
- reply.code(400).send({ code: "INVALID_RELAY_DEVICE", error: "invalid relay device route" });
1339
- }
1340
- });
1341
- app.post("/v1/routes/:routeId/devices/:deviceId/promote", { preHandler: requireHost }, async (request, reply) => {
1342
- try {
1343
- const body = strictInternalBody(request.body, ["expectedCredentialHash", "credentialHash"]);
1344
- const routeId = safeId3(request.params.routeId, "route id");
1345
- const deviceId = safeId3(request.params.deviceId, "device id");
1346
- const expectedCredentialHash = safeCredentialHash2(body.expectedCredentialHash);
1347
- const credentialHash = safeCredentialHash2(body.credentialHash);
1348
- if (tokenMatches(expectedCredentialHash, credentialHash)) throw new Error("relay credentials must differ");
1349
- const current = store.getDevice(routeId, deviceId, now());
1350
- if (!current) {
1351
- reply.code(404).send({ code: "RELAY_DEVICE_NOT_FOUND", error: "relay device not found" });
1352
- return;
1353
- }
1354
- if (current.expiresAt === void 0 && tokenMatches(current.credentialHash, credentialHash)) {
1355
- reply.code(200).send({
1356
- device: {
1357
- routeId: current.routeId,
1358
- deviceId: current.deviceId,
1359
- createdAt: current.createdAt,
1360
- updatedAt: current.updatedAt,
1361
- expiresAt: null
1362
- }
1363
- });
1364
- return;
1365
- }
1366
- if (current.expiresAt === void 0 || !tokenMatches(current.credentialHash, expectedCredentialHash)) {
1367
- reply.code(409).send({
1368
- code: "RELAY_DEVICE_CREDENTIAL_CONFLICT",
1369
- error: "relay device credential conflict"
1370
- });
1371
- return;
1372
- }
1373
- const device = store.putDevice({ routeId, deviceId, credentialHash }, now());
1374
- reply.code(200).send({
1375
- device: {
1376
- routeId: device.routeId,
1377
- deviceId: device.deviceId,
1378
- createdAt: device.createdAt,
1379
- updatedAt: device.updatedAt,
1380
- expiresAt: null
1381
- }
1382
- });
1383
- } catch {
1384
- reply.code(400).send({ code: "INVALID_RELAY_DEVICE", error: "invalid relay device promotion" });
1385
- }
1386
- });
1387
- app.delete(
1388
- "/v1/routes/:routeId/devices/:deviceId",
1389
- { preHandler: requireHost },
1390
- async (request, reply) => {
1391
- let routeId;
1392
- let deviceId;
1393
- let expectedCredentialHash;
1394
- try {
1395
- routeId = safeId3(request.params.routeId, "route id");
1396
- deviceId = safeId3(request.params.deviceId, "device id");
1397
- if (request.body !== void 0) {
1398
- const body = strictInternalBody(request.body, ["expectedCredentialHash"]);
1399
- expectedCredentialHash = safeCredentialHash2(body.expectedCredentialHash);
1400
- }
1401
- } catch {
1402
- reply.code(400).send({ code: "INVALID_RELAY_DEVICE", error: "invalid relay device" });
1403
- return;
1404
- }
1405
- const current = store.getDevice(routeId, deviceId, now());
1406
- if (!current) {
1407
- if (expectedCredentialHash) {
1408
- reply.code(204).send();
1409
- return;
1410
- }
1411
- reply.code(404).send({ code: "RELAY_DEVICE_NOT_FOUND", error: "relay device not found" });
1412
- return;
1413
- }
1414
- if (expectedCredentialHash && !tokenMatches(current.credentialHash, expectedCredentialHash)) {
1415
- reply.code(409).send({
1416
- code: "RELAY_DEVICE_CREDENTIAL_CONFLICT",
1417
- error: "relay device credential conflict"
1418
- });
1419
- return;
1420
- }
1421
- store.revokeDevice(routeId, deviceId);
1422
- deviceRates.delete(deviceRateKey(routeId, deviceId));
1423
- const live = hosts.get(routeId)?.devices.get(deviceId);
1424
- live?.socket.close(4403, "device revoked");
1425
- reply.code(204).send();
1426
- }
1427
- );
1428
- const closeDevice = (device, code = 1e3, reason = "device disconnected") => {
1429
- if (device.closed) return;
1430
- device.closed = true;
1431
- if (device.idle) clearTimeout(device.idle);
1432
- devicesByChannel.delete(device.channelId);
1433
- const host = hosts.get(device.routeId);
1434
- if (host?.devices.get(device.deviceId) === device) host.devices.delete(device.deviceId);
1435
- metrics.activeDevices = Math.max(0, metrics.activeDevices - 1);
1436
- if (host && !safeSend(host.socket, { t: "peer-close", channelId: device.channelId, code }, maxQueueBytes)) {
1437
- metrics.droppedFrames += 1;
1438
- closeHost(host, 4408, "relay backpressure");
1439
- }
1440
- if (device.socket.readyState === device.socket.OPEN) device.socket.close(code, reason);
1441
- };
1442
- const touchDevice = (device) => {
1443
- if (device.idle) clearTimeout(device.idle);
1444
- device.idle = setTimeout(() => closeDevice(device, 4408, "idle timeout"), idleTimeoutMs);
1445
- device.idle.unref?.();
1446
- };
1447
- const closeHost = (host, code = 1e3, reason = "host disconnected") => {
1448
- if (host.closed) return;
1449
- host.closed = true;
1450
- if (host.idle) clearTimeout(host.idle);
1451
- if (hosts.get(host.routeId) === host) hosts.delete(host.routeId);
1452
- for (const device of [...host.devices.values()]) closeDevice(device, 4412, "host unavailable");
1453
- metrics.activeHosts = Math.max(0, metrics.activeHosts - 1);
1454
- if (host.socket.readyState === host.socket.OPEN) host.socket.close(code, reason);
1455
- };
1456
- const touchHost = (host) => {
1457
- if (host.idle) clearTimeout(host.idle);
1458
- host.idle = setTimeout(() => closeHost(host, 4408, "idle timeout"), idleTimeoutMs);
1459
- host.idle.unref?.();
1460
- };
1461
- if (accountStore) {
1462
- const accountEnvelope = (account) => ({
1463
- account,
1464
- usage: { routes: store.listRoutesByOwner(account.id, now()).length, maxRoutes: account.maxRoutes }
1465
- });
1466
- const closeAccountRoutes = (accountId, reason) => {
1467
- for (const host of [...hosts.values()]) if (host.ownerAccountId === accountId) closeHost(host, 4403, reason);
1468
- };
1469
- const purgeAccountRoutes = (accountId) => {
1470
- closeAccountRoutes(accountId, "account deleted");
1471
- for (const route of store.listRoutesByOwner(accountId, now())) {
1472
- store.deleteRoute(route.id);
1473
- clearRouteRates(route.id);
1474
- }
1475
- };
1476
- for (const listedRoute of store.listRoutes(now())) {
1477
- const route = store.getRoute(listedRoute.id);
1478
- if (!route?.ownerAccountId) continue;
1479
- const owner = accountStore.getAccount(route.ownerAccountId);
1480
- if (!owner || owner.status === "deleted") {
1481
- store.deleteRoute(route.id);
1482
- clearRouteRates(route.id);
1483
- }
1484
- }
1485
- const ownedRoute = (accountId, routeId) => {
1486
- const route = store.getRoute(routeId);
1487
- return route?.ownerAccountId === accountId ? route : void 0;
1488
- };
1489
- const internalRouteEnvelope = (accountId, routeId) => {
1490
- const route = ownedRoute(accountId, routeId);
1491
- if (!route) return;
1492
- return {
1493
- accountId,
1494
- route: publicRoute({ ...route, deviceCount: store.countDevices(route.id, now()) }),
1495
- status: {
1496
- hostOnline: hosts.has(route.id),
1497
- activeDevices: hosts.get(route.id)?.devices.size ?? 0
1498
- },
1499
- connection: { path: "/v1/connect", protocolVersion: BLIND_RELAY_PROTOCOL_VERSION }
1500
- };
1501
- };
1502
- const internalDeviceEnvelope = (accountId, device) => ({
1503
- accountId,
1504
- device: {
1505
- routeId: device.routeId,
1506
- deviceId: device.deviceId,
1507
- createdAt: device.createdAt,
1508
- updatedAt: device.updatedAt,
1509
- expiresAt: device.expiresAt ?? null
1510
- }
1511
- });
1512
- const closeAndDeleteRoute = (routeId, reason) => {
1513
- const route = store.getRoute(routeId);
1514
- if (!route) return false;
1515
- const host = hosts.get(routeId);
1516
- if (host) closeHost(host, 4403, reason);
1517
- const deleted = store.deleteRoute(routeId);
1518
- if (deleted) clearRouteRates(routeId);
1519
- return deleted;
1520
- };
1521
- const accountRevisionConflict = (reply, account) => reply.code(409).send({
1522
- code: "RELAY_ACCOUNT_REVISION_CONFLICT",
1523
- error: "relay account revision conflict",
1524
- current: accountEnvelope(account)
1525
- });
1526
- app.put(
1527
- "/internal/v1/accounts/:accountId",
1528
- { preHandler: requireRoot },
1529
- async (request, reply) => {
1530
- let accountId;
1531
- let input;
1532
- try {
1533
- const body = strictInternalBody(request.body, [
1534
- "label",
1535
- "plan",
1536
- "maxRoutes",
1537
- "maxDevicesPerRoute",
1538
- "credentialHash",
1539
- "credentialLookup"
1540
- ]);
1541
- accountId = safeAccountId(request.params.accountId);
1542
- input = {
1543
- id: accountId,
1544
- label: safeAccountLabel(body.label),
1545
- plan: safeAccountPlan(body.plan),
1546
- maxRoutes: safeAccountLimit(body.maxRoutes, 1e4),
1547
- maxDevicesPerRoute: safeAccountLimit(body.maxDevicesPerRoute, 1e5),
1548
- ...safeAccountCredentialMaterial(body)
1549
- };
1550
- } catch {
1551
- reply.code(400).send({ code: "INVALID_RELAY_ACCOUNT", error: "invalid relay account" });
1552
- return;
1553
- }
1554
- const existing = accountStore.getAccount(accountId);
1555
- if (existing) {
1556
- const matches = existing.status !== "deleted" && existing.label === input.label && existing.plan === input.plan && existing.maxRoutes === input.maxRoutes && existing.maxDevicesPerRoute === input.maxDevicesPerRoute && accountStore.credentialMatches(accountId, input);
1557
- if (!matches) {
1558
- reply.code(409).send({ code: "RELAY_ACCOUNT_EXISTS", error: "relay account already exists" });
1559
- return;
1560
- }
1561
- reply.code(200).send(accountEnvelope(existing));
1562
- return;
1563
- }
1564
- try {
1565
- const account = accountStore.createAccount(input);
1566
- reply.code(201).send(accountEnvelope(account));
1567
- } catch (error) {
1568
- if (error.message === "relay account already exists" || error.message === "relay account credential already exists") {
1569
- reply.code(409).send({ code: "RELAY_ACCOUNT_EXISTS", error: "relay account already exists" });
1570
- return;
1571
- }
1572
- throw error;
1573
- }
1574
- }
1575
- );
1576
- app.get(
1577
- "/internal/v1/accounts/:accountId/status",
1578
- { preHandler: requireRoot },
1579
- async (request, reply) => {
1580
- let account;
1581
- try {
1582
- account = accountStore.getAccount(safeAccountId(request.params.accountId));
1583
- } catch {
1584
- }
1585
- if (!account || account.status === "deleted") {
1586
- reply.code(404).send({ code: "RELAY_ACCOUNT_NOT_FOUND", error: "relay account not found" });
1587
- return;
1588
- }
1589
- reply.code(200).send(accountEnvelope(account));
1590
- }
1591
- );
1592
- app.put("/internal/v1/accounts/:accountId/metadata", { preHandler: requireRoot }, async (request, reply) => {
1593
- let accountId;
1594
- let expectedRevision;
1595
- let update;
1596
- try {
1597
- const body = strictInternalBody(request.body, [
1598
- "expectedRevision",
1599
- "label",
1600
- "plan",
1601
- "maxRoutes",
1602
- "maxDevicesPerRoute"
1603
- ]);
1604
- accountId = safeAccountId(request.params.accountId);
1605
- expectedRevision = safeRevision(body.expectedRevision);
1606
- update = {
1607
- label: safeAccountLabel(body.label),
1608
- plan: safeAccountPlan(body.plan),
1609
- maxRoutes: safeAccountLimit(body.maxRoutes, 1e4),
1610
- maxDevicesPerRoute: safeAccountLimit(body.maxDevicesPerRoute, 1e5)
1611
- };
1612
- } catch {
1613
- reply.code(400).send({ code: "INVALID_RELAY_ACCOUNT", error: "invalid relay account" });
1614
- return;
1615
- }
1616
- const current = accountStore.getAccount(accountId);
1617
- if (!current || current.status === "deleted") {
1618
- reply.code(404).send({ code: "RELAY_ACCOUNT_NOT_FOUND", error: "relay account not found" });
1619
- return;
1620
- }
1621
- const matches = current.label === update.label && current.plan === update.plan && current.maxRoutes === update.maxRoutes && current.maxDevicesPerRoute === update.maxDevicesPerRoute;
1622
- if (matches && (current.revision === expectedRevision || current.revision === expectedRevision + 1)) {
1623
- reply.code(200).send(accountEnvelope(current));
1624
- return;
1625
- }
1626
- if (current.revision !== expectedRevision) {
1627
- accountRevisionConflict(reply, current);
1628
- return;
1629
- }
1630
- try {
1631
- const account = accountStore.updateAccount(accountId, update, expectedRevision);
1632
- if (!account) {
1633
- reply.code(404).send({ code: "RELAY_ACCOUNT_NOT_FOUND", error: "relay account not found" });
1634
- return;
1635
- }
1636
- reply.code(200).send(accountEnvelope(account));
1637
- } catch (error) {
1638
- if (error instanceof RelayAccountRevisionConflictError) {
1639
- accountRevisionConflict(reply, error.current);
1640
- return;
1641
- }
1642
- throw error;
1643
- }
1644
- });
1645
- app.put("/internal/v1/accounts/:accountId/credential", { preHandler: requireRoot }, async (request, reply) => {
1646
- let accountId;
1647
- let expectedRevision;
1648
- let credential;
1649
- try {
1650
- const body = strictInternalBody(request.body, ["expectedRevision", "credentialHash", "credentialLookup"]);
1651
- accountId = safeAccountId(request.params.accountId);
1652
- expectedRevision = safeRevision(body.expectedRevision);
1653
- credential = safeAccountCredentialMaterial(body);
1654
- } catch {
1655
- reply.code(400).send({ code: "INVALID_RELAY_ACCOUNT", error: "invalid relay account" });
1656
- return;
1657
- }
1658
- const current = accountStore.getAccount(accountId);
1659
- if (!current || current.status === "deleted") {
1660
- reply.code(404).send({ code: "RELAY_ACCOUNT_NOT_FOUND", error: "relay account not found" });
1661
- return;
1662
- }
1663
- if (accountStore.credentialMatches(accountId, credential)) {
1664
- if (current.revision === expectedRevision || current.revision === expectedRevision + 1) {
1665
- reply.code(200).send(accountEnvelope(current));
1666
- return;
1667
- }
1668
- accountRevisionConflict(reply, current);
1669
- return;
1670
- }
1671
- if (current.revision !== expectedRevision) {
1672
- accountRevisionConflict(reply, current);
1673
- return;
1674
- }
1675
- try {
1676
- const account = accountStore.rotateCredential(accountId, credential, expectedRevision);
1677
- if (!account) {
1678
- reply.code(404).send({ code: "RELAY_ACCOUNT_NOT_FOUND", error: "relay account not found" });
1679
- return;
1680
- }
1681
- reply.code(200).send(accountEnvelope(account));
1682
- } catch (error) {
1683
- if (error instanceof RelayAccountRevisionConflictError) {
1684
- accountRevisionConflict(reply, error.current);
1685
- return;
1686
- }
1687
- if (error.message === "relay account credential already exists") {
1688
- reply.code(409).send({ code: "RELAY_ACCOUNT_CREDENTIAL_CONFLICT", error: "relay credential conflict" });
1689
- return;
1690
- }
1691
- throw error;
1692
- }
1693
- });
1694
- app.delete("/internal/v1/accounts/:accountId", { preHandler: requireRoot }, async (request, reply) => {
1695
- let accountId;
1696
- let expectedRevision;
1697
- try {
1698
- const body = strictInternalBody(request.body, ["expectedRevision"]);
1699
- accountId = safeAccountId(request.params.accountId);
1700
- expectedRevision = safeRevision(body.expectedRevision);
1701
- } catch {
1702
- reply.code(400).send({ code: "INVALID_RELAY_ACCOUNT", error: "invalid relay account" });
1703
- return;
1704
- }
1705
- const current = accountStore.getAccount(accountId);
1706
- if (!current || current.status === "deleted") {
1707
- reply.code(204).send();
1708
- return;
1709
- }
1710
- if (current.revision !== expectedRevision) {
1711
- accountRevisionConflict(reply, current);
1712
- return;
1713
- }
1714
- try {
1715
- const account = accountStore.updateAccount(accountId, { status: "deleted" }, expectedRevision);
1716
- if (account) purgeAccountRoutes(account.id);
1717
- reply.code(204).send();
1718
- } catch (error) {
1719
- if (error instanceof RelayAccountRevisionConflictError) {
1720
- accountRevisionConflict(reply, error.current);
1721
- return;
1722
- }
1723
- throw error;
1724
- }
1725
- });
1726
- app.put("/internal/v1/accounts/:accountId/routes/:routeId", { preHandler: requireRoot }, async (request, reply) => {
1727
- let accountId;
1728
- let routeId;
1729
- let label;
1730
- let credentialHash;
1731
- try {
1732
- const body = strictInternalBody(request.body, ["label", "credentialHash"]);
1733
- accountId = safeAccountId(request.params.accountId);
1734
- routeId = safeId3(request.params.routeId, "route id");
1735
- label = safeLabel3(body.label);
1736
- credentialHash = safeRouteCredentialHash(body);
1737
- } catch {
1738
- reply.code(400).send({ code: "INVALID_RELAY_ROUTE", error: "invalid relay route" });
1739
- return;
1740
- }
1741
- const account = accountStore.getAccount(accountId);
1742
- if (!account || account.status === "deleted") {
1743
- reply.code(404).send({ code: "RELAY_ACCOUNT_NOT_FOUND", error: "relay account not found" });
1744
- return;
1745
- }
1746
- if (account.status !== "active") {
1747
- reply.code(409).send({ code: "RELAY_ACCOUNT_UNAVAILABLE", error: "relay account unavailable" });
1748
- return;
1749
- }
1750
- const existing = store.getRoute(routeId);
1751
- if (existing) {
1752
- if (existing.ownerAccountId !== accountId || existing.label !== label || !tokenMatches(existing.hostCredentialHash, credentialHash)) {
1753
- reply.code(409).send({ code: "RELAY_ROUTE_EXISTS", error: "relay route already exists" });
1754
- return;
1755
- }
1756
- reply.code(200).send(internalRouteEnvelope(accountId, routeId));
1757
- return;
1758
- }
1759
- if (store.listRoutesByOwner(accountId, now()).length >= account.maxRoutes) {
1760
- reply.code(429).send({ code: "RELAY_ROUTE_LIMIT", error: "relay route limit reached" });
1761
- return;
1762
- }
1763
- try {
1764
- store.createRoute({ id: routeId, label, hostCredentialHash: credentialHash, ownerAccountId: accountId });
1765
- reply.code(201).send(internalRouteEnvelope(accountId, routeId));
1766
- } catch (error) {
1767
- if (error.message === "relay route already exists") {
1768
- reply.code(409).send({ code: "RELAY_ROUTE_EXISTS", error: "relay route already exists" });
1769
- return;
1770
- }
1771
- throw error;
1772
- }
1773
- });
1774
- app.get(
1775
- "/internal/v1/accounts/:accountId/routes/:routeId/status",
1776
- { preHandler: requireRoot },
1777
- async (request, reply) => {
1778
- let route;
1779
- try {
1780
- route = internalRouteEnvelope(
1781
- safeAccountId(request.params.accountId),
1782
- safeId3(request.params.routeId, "route id")
1783
- );
1784
- } catch {
1785
- }
1786
- if (!route) {
1787
- reply.code(404).send({ code: "RELAY_ROUTE_NOT_FOUND", error: "relay route not found" });
1788
- return;
1789
- }
1790
- reply.code(200).send(route);
1791
- }
1792
- );
1793
- app.put(
1794
- "/internal/v1/accounts/:accountId/routes/:routeId/devices/:deviceId",
1795
- { preHandler: requireRoot },
1796
- async (request, reply) => {
1797
- let accountId;
1798
- let routeId;
1799
- let deviceId;
1800
- let credentialHash;
1801
- let expiresAt;
1802
- try {
1803
- const body = strictInternalBody(request.body, ["credentialHash", "expiresAt"]);
1804
- accountId = safeAccountId(request.params.accountId);
1805
- routeId = safeId3(request.params.routeId, "route id");
1806
- deviceId = safeId3(request.params.deviceId, "device id");
1807
- credentialHash = safeCredentialHash2(body.credentialHash);
1808
- const parsedExpiry = safeExpiry2(body.expiresAt, now());
1809
- if (parsedExpiry === void 0) throw new Error("temporary relay device expiry is required");
1810
- expiresAt = parsedExpiry;
1811
- } catch {
1812
- reply.code(400).send({ code: "INVALID_RELAY_DEVICE", error: "invalid relay device" });
1813
- return;
1814
- }
1815
- const account = accountStore.getAccount(accountId);
1816
- if (!account || account.status === "deleted") {
1817
- reply.code(404).send({ code: "RELAY_ACCOUNT_NOT_FOUND", error: "relay account not found" });
1818
- return;
1819
- }
1820
- if (account.status !== "active") {
1821
- reply.code(409).send({ code: "RELAY_ACCOUNT_UNAVAILABLE", error: "relay account unavailable" });
1822
- return;
1823
- }
1824
- if (!ownedRoute(accountId, routeId)) {
1825
- reply.code(404).send({ code: "RELAY_ROUTE_NOT_FOUND", error: "relay route not found" });
1826
- return;
1827
- }
1828
- const current = store.getDevice(routeId, deviceId, now());
1829
- if (current) {
1830
- if (tokenMatches(current.credentialHash, credentialHash) && current.expiresAt === expiresAt) {
1831
- reply.code(200).send(internalDeviceEnvelope(accountId, current));
1832
- return;
1833
- }
1834
- reply.code(409).send({
1835
- code: "RELAY_DEVICE_CREDENTIAL_CONFLICT",
1836
- error: "relay device credential conflict",
1837
- current: internalDeviceEnvelope(accountId, current)
1838
- });
1839
- return;
1840
- }
1841
- if (store.countDevices(routeId, now()) >= account.maxDevicesPerRoute) {
1842
- reply.code(429).send({ code: "RELAY_DEVICE_LIMIT", error: "relay device limit reached" });
1843
- return;
1844
- }
1845
- const device = store.putDevice({ routeId, deviceId, credentialHash, expiresAt }, now());
1846
- reply.code(201).send(internalDeviceEnvelope(accountId, device));
1847
- }
1848
- );
1849
- app.post(
1850
- "/internal/v1/accounts/:accountId/routes/:routeId/devices/:deviceId/promote",
1851
- { preHandler: requireRoot },
1852
- async (request, reply) => {
1853
- let accountId;
1854
- let routeId;
1855
- let deviceId;
1856
- let expectedCredentialHash;
1857
- let credentialHash;
1858
- try {
1859
- const body = strictInternalBody(request.body, ["expectedCredentialHash", "credentialHash"]);
1860
- accountId = safeAccountId(request.params.accountId);
1861
- routeId = safeId3(request.params.routeId, "route id");
1862
- deviceId = safeId3(request.params.deviceId, "device id");
1863
- expectedCredentialHash = safeCredentialHash2(body.expectedCredentialHash);
1864
- credentialHash = safeCredentialHash2(body.credentialHash);
1865
- if (tokenMatches(expectedCredentialHash, credentialHash)) throw new Error("relay credentials must differ");
1866
- } catch {
1867
- reply.code(400).send({ code: "INVALID_RELAY_DEVICE", error: "invalid relay device promotion" });
1868
- return;
1869
- }
1870
- const account = accountStore.getAccount(accountId);
1871
- if (!account || account.status === "deleted") {
1872
- reply.code(404).send({ code: "RELAY_ACCOUNT_NOT_FOUND", error: "relay account not found" });
1873
- return;
1874
- }
1875
- if (account.status !== "active") {
1876
- reply.code(409).send({ code: "RELAY_ACCOUNT_UNAVAILABLE", error: "relay account unavailable" });
1877
- return;
1878
- }
1879
- if (!ownedRoute(accountId, routeId)) {
1880
- reply.code(404).send({ code: "RELAY_ROUTE_NOT_FOUND", error: "relay route not found" });
1881
- return;
1882
- }
1883
- const current = store.getDevice(routeId, deviceId, now());
1884
- if (!current) {
1885
- reply.code(404).send({ code: "RELAY_DEVICE_NOT_FOUND", error: "relay device not found" });
1886
- return;
1887
- }
1888
- if (current.expiresAt === void 0 && tokenMatches(current.credentialHash, credentialHash)) {
1889
- reply.code(200).send(internalDeviceEnvelope(accountId, current));
1890
- return;
1891
- }
1892
- if (current.expiresAt === void 0 || !tokenMatches(current.credentialHash, expectedCredentialHash)) {
1893
- reply.code(409).send({
1894
- code: "RELAY_DEVICE_CREDENTIAL_CONFLICT",
1895
- error: "relay device credential conflict",
1896
- current: internalDeviceEnvelope(accountId, current)
1897
- });
1898
- return;
1899
- }
1900
- const device = store.putDevice({ routeId, deviceId, credentialHash }, now());
1901
- reply.code(200).send(internalDeviceEnvelope(accountId, device));
1902
- }
1903
- );
1904
- app.delete(
1905
- "/internal/v1/accounts/:accountId/routes/:routeId/devices/:deviceId",
1906
- { preHandler: requireRoot },
1907
- async (request, reply) => {
1908
- let accountId;
1909
- let routeId;
1910
- let deviceId;
1911
- let expectedCredentialHash;
1912
- try {
1913
- const body = strictInternalBody(request.body, ["expectedCredentialHash"]);
1914
- accountId = safeAccountId(request.params.accountId);
1915
- routeId = safeId3(request.params.routeId, "route id");
1916
- deviceId = safeId3(request.params.deviceId, "device id");
1917
- expectedCredentialHash = safeCredentialHash2(body.expectedCredentialHash);
1918
- } catch {
1919
- reply.code(400).send({ code: "INVALID_RELAY_DEVICE", error: "invalid relay device" });
1920
- return;
1921
- }
1922
- const account = accountStore.getAccount(accountId);
1923
- if (!account || account.status === "deleted") {
1924
- reply.code(404).send({ code: "RELAY_ACCOUNT_NOT_FOUND", error: "relay account not found" });
1925
- return;
1926
- }
1927
- if (!ownedRoute(accountId, routeId)) {
1928
- reply.code(204).send();
1929
- return;
1930
- }
1931
- const current = store.getDevice(routeId, deviceId, now());
1932
- if (!current) {
1933
- reply.code(204).send();
1934
- return;
1935
- }
1936
- if (!tokenMatches(current.credentialHash, expectedCredentialHash)) {
1937
- reply.code(409).send({
1938
- code: "RELAY_DEVICE_CREDENTIAL_CONFLICT",
1939
- error: "relay device credential conflict",
1940
- current: internalDeviceEnvelope(accountId, current)
1941
- });
1942
- return;
1943
- }
1944
- store.revokeDevice(routeId, deviceId);
1945
- deviceRates.delete(deviceRateKey(routeId, deviceId));
1946
- const live = hosts.get(routeId)?.devices.get(deviceId);
1947
- live?.socket.close(4403, "device revoked");
1948
- reply.code(204).send();
1949
- }
1950
- );
1951
- app.put(
1952
- "/internal/v1/accounts/:accountId/routes/:routeId/credential",
1953
- { preHandler: requireRoot },
1954
- async (request, reply) => {
1955
- let accountId;
1956
- let routeId;
1957
- let expectedCredentialHash;
1958
- let credentialHash;
1959
- try {
1960
- const body = strictInternalBody(request.body, ["expectedCredentialHash", "credentialHash"]);
1961
- accountId = safeAccountId(request.params.accountId);
1962
- routeId = safeId3(request.params.routeId, "route id");
1963
- expectedCredentialHash = safeRouteCredentialHash(body, "expectedCredentialHash");
1964
- credentialHash = safeRouteCredentialHash(body);
1965
- } catch {
1966
- reply.code(400).send({ code: "INVALID_RELAY_CREDENTIAL", error: "invalid relay credential hash" });
1967
- return;
1968
- }
1969
- const route = ownedRoute(accountId, routeId);
1970
- if (!route) {
1971
- reply.code(404).send({ code: "RELAY_ROUTE_NOT_FOUND", error: "relay route not found" });
1972
- return;
1973
- }
1974
- if (tokenMatches(route.hostCredentialHash, credentialHash)) {
1975
- reply.code(200).send(internalRouteEnvelope(accountId, routeId));
1976
- return;
1977
- }
1978
- if (!tokenMatches(route.hostCredentialHash, expectedCredentialHash)) {
1979
- reply.code(409).send({
1980
- code: "RELAY_ROUTE_CREDENTIAL_CONFLICT",
1981
- error: "relay route credential conflict",
1982
- current: internalRouteEnvelope(accountId, routeId)
1983
- });
1984
- return;
1985
- }
1986
- if (!store.rotateHostCredential(routeId, credentialHash, now())) {
1987
- reply.code(404).send({ code: "RELAY_ROUTE_NOT_FOUND", error: "relay route not found" });
1988
- return;
1989
- }
1990
- const host = hosts.get(routeId);
1991
- if (host) closeHost(host, 4409, "host credential rotated");
1992
- reply.code(200).send(internalRouteEnvelope(accountId, routeId));
1993
- }
1994
- );
1995
- app.delete("/internal/v1/accounts/:accountId/routes/:routeId", { preHandler: requireRoot }, async (request, reply) => {
1996
- let accountId;
1997
- let routeId;
1998
- let expectedCredentialHash;
1999
- try {
2000
- const body = strictInternalBody(request.body, ["expectedCredentialHash"]);
2001
- accountId = safeAccountId(request.params.accountId);
2002
- routeId = safeId3(request.params.routeId, "route id");
2003
- expectedCredentialHash = safeRouteCredentialHash(body, "expectedCredentialHash");
2004
- } catch {
2005
- reply.code(400).send({ code: "INVALID_RELAY_ROUTE", error: "invalid relay route" });
2006
- return;
2007
- }
2008
- const route = ownedRoute(accountId, routeId);
2009
- if (!route) {
2010
- reply.code(204).send();
2011
- return;
2012
- }
2013
- if (!tokenMatches(route.hostCredentialHash, expectedCredentialHash)) {
2014
- reply.code(409).send({
2015
- code: "RELAY_ROUTE_CREDENTIAL_CONFLICT",
2016
- error: "relay route credential conflict",
2017
- current: internalRouteEnvelope(accountId, routeId)
2018
- });
2019
- return;
2020
- }
2021
- closeAndDeleteRoute(routeId, "route deleted");
2022
- reply.code(204).send();
2023
- });
2024
- app.get("/v1/accounts", { preHandler: requireRoot }, async () => ({
2025
- accounts: accountStore.listAccounts().map((account) => accountEnvelope(account))
2026
- }));
2027
- const createAccountHandler = (clientHashedOnly) => async (request, reply) => {
2028
- try {
2029
- const hasCredentialHash = request.body?.credentialHash !== void 0;
2030
- const hasCredentialLookup = request.body?.credentialLookup !== void 0;
2031
- if (clientHashedOnly && (!hasCredentialHash || !hasCredentialLookup)) {
2032
- throw new Error("client-hashed account credential material is required");
2033
- }
2034
- const suppliedCredentialMaterial = hasCredentialHash || hasCredentialLookup;
2035
- const accountCredential = suppliedCredentialMaterial ? void 0 : generateRelayAccountCredential();
2036
- const credential = suppliedCredentialMaterial ? {
2037
- credentialHash: request.body?.credentialHash,
2038
- credentialLookup: request.body?.credentialLookup
2039
- } : accountCredential;
2040
- const account = accountStore.createAccount({
2041
- label: request.body?.label,
2042
- ...request.body?.plan === void 0 ? {} : { plan: request.body.plan },
2043
- ...request.body?.maxRoutes === void 0 ? {} : { maxRoutes: request.body.maxRoutes },
2044
- ...request.body?.maxDevicesPerRoute === void 0 ? {} : { maxDevicesPerRoute: request.body.maxDevicesPerRoute },
2045
- ...typeof credential === "string" ? { credential } : {
2046
- credentialHash: credential.credentialHash,
2047
- credentialLookup: credential.credentialLookup
2048
- }
2049
- });
2050
- reply.code(201).send({
2051
- ...accountEnvelope(account),
2052
- ...accountCredential === void 0 ? {} : { accountCredential }
2053
- });
2054
- } catch {
2055
- reply.code(400).send({ code: "INVALID_RELAY_ACCOUNT", error: "invalid relay account" });
2056
- }
2057
- };
2058
- app.post(
2059
- "/v1/accounts",
2060
- { preHandler: requireRoot },
2061
- createAccountHandler(false)
2062
- );
2063
- app.post(
2064
- "/v1/accounts/client-hashed",
2065
- { preHandler: requireRoot },
2066
- createAccountHandler(true)
2067
- );
2068
- app.patch(
2069
- "/v1/accounts/:accountId",
2070
- { preHandler: requireRoot },
2071
- async (request, reply) => {
2072
- try {
2073
- const account = accountStore.updateAccount(
2074
- request.params.accountId,
2075
- request.body,
2076
- safeRevision(request.body?.expectedRevision)
2077
- );
2078
- if (!account) {
2079
- reply.code(404).send({ code: "RELAY_ACCOUNT_NOT_FOUND", error: "relay account not found" });
2080
- return;
2081
- }
2082
- if (account.status === "deleted") purgeAccountRoutes(account.id);
2083
- else if (account.status !== "active") closeAccountRoutes(account.id, "account unavailable");
2084
- reply.code(200).send(accountEnvelope(account));
2085
- } catch (error) {
2086
- if (error instanceof RelayAccountRevisionConflictError) {
2087
- reply.code(409).send({
2088
- code: "RELAY_ACCOUNT_REVISION_CONFLICT",
2089
- error: "relay account revision conflict",
2090
- current: accountEnvelope(error.current)
2091
- });
2092
- return;
2093
- }
2094
- reply.code(400).send({ code: "INVALID_RELAY_ACCOUNT", error: "invalid relay account" });
2095
- }
2096
- }
2097
- );
2098
- const rotateAccountHandler = (clientHashedOnly) => async (request, reply) => {
2099
- try {
2100
- const hasCredentialHash = request.body?.credentialHash !== void 0;
2101
- const hasCredentialLookup = request.body?.credentialLookup !== void 0;
2102
- if (clientHashedOnly && (!hasCredentialHash || !hasCredentialLookup)) {
2103
- throw new Error("client-hashed account credential material is required");
2104
- }
2105
- const suppliedCredentialMaterial = hasCredentialHash || hasCredentialLookup;
2106
- const accountCredential = suppliedCredentialMaterial ? void 0 : generateRelayAccountCredential();
2107
- const credential = suppliedCredentialMaterial ? {
2108
- credentialHash: request.body?.credentialHash,
2109
- credentialLookup: request.body?.credentialLookup
2110
- } : accountCredential;
2111
- const account = accountStore.rotateCredential(
2112
- request.params.accountId,
2113
- credential,
2114
- safeRevision(request.body?.expectedRevision)
2115
- );
2116
- if (!account) {
2117
- reply.code(404).send({ code: "RELAY_ACCOUNT_NOT_FOUND", error: "relay account not found" });
2118
- return;
2119
- }
2120
- reply.code(200).send({
2121
- ...accountEnvelope(account),
2122
- ...accountCredential === void 0 ? {} : { accountCredential }
2123
- });
2124
- } catch (error) {
2125
- if (error instanceof RelayAccountRevisionConflictError) {
2126
- reply.code(409).send({
2127
- code: "RELAY_ACCOUNT_REVISION_CONFLICT",
2128
- error: "relay account revision conflict",
2129
- current: accountEnvelope(error.current)
2130
- });
2131
- return;
2132
- }
2133
- reply.code(400).send({ code: "INVALID_RELAY_ACCOUNT", error: "invalid relay account" });
2134
- }
2135
- };
2136
- app.post(
2137
- "/v1/accounts/:accountId/credential",
2138
- { preHandler: requireRoot },
2139
- rotateAccountHandler(false)
2140
- );
2141
- app.post(
2142
- "/v1/accounts/:accountId/credential/client-hashed",
2143
- { preHandler: requireRoot },
2144
- rotateAccountHandler(true)
2145
- );
2146
- app.get(
2147
- "/v1/account/recovery",
2148
- { preHandler: requireRecoverableAccount },
2149
- async (request) => accountEnvelope(authenticatedAccounts.get(request))
2150
- );
2151
- app.get(
2152
- "/v1/account",
2153
- { preHandler: requireAccount },
2154
- async (request) => accountEnvelope(authenticatedAccounts.get(request))
2155
- );
2156
- app.get("/v1/account/routes", { preHandler: requireAccount }, async (request) => ({
2157
- routes: store.listRoutesByOwner(authenticatedAccounts.get(request).id, now()).map((route) => ({
2158
- ...publicRoute(route),
2159
- hostOnline: hosts.has(route.id)
2160
- }))
2161
- }));
2162
- app.post(
2163
- "/v1/account/routes",
2164
- { preHandler: requireAccount },
2165
- async (request, reply) => {
2166
- const account = authenticatedAccounts.get(request);
2167
- let id;
2168
- let label;
2169
- let credentialHash;
2170
- try {
2171
- id = safeId3(request.body?.id, "route id");
2172
- label = safeLabel3(request.body?.label);
2173
- credentialHash = safeCredentialHash2(request.body?.credentialHash);
2174
- } catch {
2175
- reply.code(400).send({ code: "INVALID_RELAY_ROUTE", error: "invalid relay route" });
2176
- return;
2177
- }
2178
- const existing = store.getRoute(id);
2179
- if (existing) {
2180
- if (existing.ownerAccountId !== account.id || existing.label !== label || !tokenMatches(existing.hostCredentialHash, credentialHash)) {
2181
- reply.code(409).send({ code: "RELAY_ROUTE_EXISTS", error: "relay route already exists" });
2182
- return;
2183
- }
2184
- reply.code(200).send({
2185
- route: {
2186
- ...publicRoute({ ...existing, deviceCount: store.countDevices(existing.id, now()) }),
2187
- hostOnline: hosts.has(id)
2188
- },
2189
- connection: { path: "/v1/connect", protocolVersion: BLIND_RELAY_PROTOCOL_VERSION }
2190
- });
2191
- return;
2192
- }
2193
- if (store.listRoutesByOwner(account.id, now()).length >= account.maxRoutes) {
2194
- reply.code(429).send({ code: "RELAY_ROUTE_LIMIT", error: "relay route limit reached" });
2195
- return;
2196
- }
2197
- try {
2198
- const route = store.createRoute({
2199
- id,
2200
- label,
2201
- hostCredentialHash: credentialHash,
2202
- ownerAccountId: account.id
2203
- });
2204
- reply.code(201).send({
2205
- route: { ...publicRoute({ ...route, deviceCount: 0 }), hostOnline: false },
2206
- connection: { path: "/v1/connect", protocolVersion: BLIND_RELAY_PROTOCOL_VERSION }
2207
- });
2208
- } catch {
2209
- reply.code(400).send({ code: "INVALID_RELAY_ROUTE", error: "invalid relay route" });
2210
- }
2211
- }
2212
- );
2213
- app.delete(
2214
- "/v1/account/routes/:routeId",
2215
- { preHandler: requireAccount },
2216
- async (request, reply) => {
2217
- const account = authenticatedAccounts.get(request);
2218
- let removed = false;
2219
- try {
2220
- removed = !!ownedRoute(account.id, request.params.routeId) && store.deleteRoute(request.params.routeId);
2221
- } catch {
2222
- }
2223
- if (!removed) {
2224
- reply.code(404).send({ code: "RELAY_ROUTE_NOT_FOUND", error: "relay route not found" });
2225
- return;
2226
- }
2227
- clearRouteRates(request.params.routeId);
2228
- const host = hosts.get(request.params.routeId);
2229
- if (host) closeHost(host, 4403, "route deleted");
2230
- reply.code(204).send();
2231
- }
2232
- );
2233
- app.post(
2234
- "/v1/account/routes/:routeId/credential",
2235
- { preHandler: requireAccount },
2236
- async (request, reply) => {
2237
- const account = authenticatedAccounts.get(request);
2238
- try {
2239
- if (!ownedRoute(account.id, request.params.routeId)) {
2240
- reply.code(404).send({ code: "RELAY_ROUTE_NOT_FOUND", error: "relay route not found" });
2241
- return;
2242
- }
2243
- } catch {
2244
- reply.code(404).send({ code: "RELAY_ROUTE_NOT_FOUND", error: "relay route not found" });
2245
- return;
2246
- }
2247
- let credentialHash;
2248
- try {
2249
- credentialHash = safeCredentialHash2(request.body?.credentialHash);
2250
- } catch {
2251
- reply.code(400).send({ code: "INVALID_RELAY_CREDENTIAL", error: "invalid relay credential hash" });
2252
- return;
2253
- }
2254
- if (!store.rotateHostCredential(request.params.routeId, credentialHash, now())) {
2255
- reply.code(404).send({ code: "RELAY_ROUTE_NOT_FOUND", error: "relay route not found" });
2256
- return;
2257
- }
2258
- const host = hosts.get(request.params.routeId);
2259
- if (host) closeHost(host, 4409, "host credential rotated");
2260
- reply.code(204).send();
2261
- }
2262
- );
2263
- }
2264
- app.register(websocket, { options: { maxPayload: maxEnvelopeBytes, perMessageDeflate: false } });
2265
- app.register(async (scope) => {
2266
- scope.get("/v1/connect", { websocket: true }, (socket, request) => {
2267
- if (sockets.size >= maxTotalConnections) {
2268
- metrics.rejectedConnections += 1;
2269
- socket.close(4429, "relay connection limit");
2270
- return;
2271
- }
2272
- sockets.add(socket);
2273
- let authenticated = false;
2274
- let liveHost;
2275
- let liveDevice;
2276
- const handshakeTimer = setTimeout(() => socket.close(4401, "authentication timeout"), handshakeTimeoutMs);
2277
- handshakeTimer.unref?.();
2278
- const reject = (code, reason) => {
2279
- metrics.rejectedConnections += 1;
2280
- socket.close(code, reason);
2281
- };
2282
- socket.on("message", (raw, isBinary) => {
2283
- if (isBinary) {
2284
- metrics.droppedFrames += 1;
2285
- reject(4400, "text envelope required");
2286
- return;
2287
- }
2288
- if (!authenticated) {
2289
- try {
2290
- const hello = parseAuthHello(parseJson(raw, 8 * 1024));
2291
- const presentedOrigin = request.headers.origin;
2292
- const origin = typeof presentedOrigin === "string" ? normalizeOrigin(presentedOrigin) : void 0;
2293
- if (hello.role === "device" && allowedOrigins.size > 0 && presentedOrigin !== void 0 && (!origin || !allowedOrigins.has(origin))) {
2294
- reject(4403, "origin denied");
2295
- return;
2296
- }
2297
- if (hello.role === "host") {
2298
- const route = store.getRoute(hello.routeId);
2299
- if (!route || route.ownerAccountId !== void 0 && accountStore?.getAccount(route.ownerAccountId)?.status !== "active" || !store.authenticateHost(hello.routeId, hello.credential)) {
2300
- reject(4401, "authentication failed");
2301
- return;
2302
- }
2303
- const rate = rateWindowFor(hostRates, hello.routeId);
2304
- if (!rate || !consumeRate(rate, Buffer.byteLength(raw.toString()))) {
2305
- reject(4429, "relay identity rate limit");
2306
- return;
2307
- }
2308
- const previous = hosts.get(hello.routeId);
2309
- if (previous) closeHost(previous, 4409, "superseded host");
2310
- liveHost = {
2311
- socket,
2312
- routeId: hello.routeId,
2313
- ...route.ownerAccountId === void 0 ? {} : { ownerAccountId: route.ownerAccountId },
2314
- devices: /* @__PURE__ */ new Map(),
2315
- rate,
2316
- closed: false
2317
- };
2318
- hosts.set(hello.routeId, liveHost);
2319
- metrics.activeHosts += 1;
2320
- touchHost(liveHost);
2321
- if (!safeSend(socket, { t: "ready", role: "host", protocolVersion: 1 }, maxQueueBytes)) {
2322
- metrics.rejectedConnections += 1;
2323
- closeHost(liveHost, 4408, "relay backpressure");
2324
- return;
2325
- }
2326
- } else {
2327
- if (!routeAccountIsActive(hello.routeId) || !store.authenticateDevice(hello.routeId, hello.deviceId, hello.credential, now())) {
2328
- reject(4401, "authentication failed");
2329
- return;
2330
- }
2331
- const host = hosts.get(hello.routeId);
2332
- if (!host || host.socket.readyState !== host.socket.OPEN) {
2333
- reject(4412, "host unavailable");
2334
- return;
2335
- }
2336
- if (host.devices.size >= maxConnectionsPerRoute && !host.devices.has(hello.deviceId)) {
2337
- reject(4429, "route connection limit");
2338
- return;
2339
- }
2340
- const rate = rateWindowFor(deviceRates, deviceRateKey(hello.routeId, hello.deviceId));
2341
- if (!rate || !consumeRate(rate, Buffer.byteLength(raw.toString()))) {
2342
- reject(4429, "relay identity rate limit");
2343
- return;
2344
- }
2345
- const previous = host.devices.get(hello.deviceId);
2346
- if (previous) closeDevice(previous, 4409, "superseded device");
2347
- const channelId = safeId3(generateChannelId(), "channel id");
2348
- liveDevice = {
2349
- socket,
2350
- routeId: hello.routeId,
2351
- deviceId: hello.deviceId,
2352
- channelId,
2353
- rate,
2354
- closed: false
2355
- };
2356
- host.devices.set(hello.deviceId, liveDevice);
2357
- devicesByChannel.set(channelId, liveDevice);
2358
- metrics.activeDevices += 1;
2359
- touchDevice(liveDevice);
2360
- if (!safeSend(socket, { t: "ready", role: "device", protocolVersion: 1, channelId }, maxQueueBytes)) {
2361
- metrics.rejectedConnections += 1;
2362
- closeDevice(liveDevice, 4408, "relay backpressure");
2363
- return;
2364
- }
2365
- if (!safeSend(host.socket, { t: "peer-open", channelId, deviceId: hello.deviceId }, maxQueueBytes)) {
2366
- metrics.rejectedConnections += 1;
2367
- closeDevice(liveDevice, 4408, "host backpressure");
2368
- return;
2369
- }
2370
- }
2371
- authenticated = true;
2372
- metrics.acceptedConnections += 1;
2373
- clearTimeout(handshakeTimer);
2374
- } catch {
2375
- reject(4400, "invalid authentication frame");
2376
- }
2377
- return;
2378
- }
2379
- try {
2380
- const value = parseJson(raw, maxEnvelopeBytes);
2381
- if (liveDevice) {
2382
- touchDevice(liveDevice);
2383
- if (value?.t === "ping") {
2384
- if (!consumeRate(liveDevice.rate, 1)) {
2385
- closeDevice(liveDevice, 4429, "rate limit");
2386
- return;
2387
- }
2388
- if (!safeSend(socket, { t: "pong", at: now() }, maxQueueBytes)) {
2389
- metrics.droppedFrames += 1;
2390
- closeDevice(liveDevice, 4408, "relay backpressure");
2391
- }
2392
- return;
2393
- }
2394
- const frame = parsePayload(value, false, maxFrameBytes);
2395
- if (!consumeRate(liveDevice.rate, frame.bytes)) {
2396
- closeDevice(liveDevice, 4429, "rate limit");
2397
- return;
2398
- }
2399
- const host = hosts.get(liveDevice.routeId);
2400
- if (!host || !safeSend(
2401
- host.socket,
2402
- { t: "frame", channelId: liveDevice.channelId, payload: frame.payload },
2403
- maxQueueBytes
2404
- )) {
2405
- metrics.droppedFrames += 1;
2406
- closeDevice(liveDevice, 4408, "host unavailable or slow");
2407
- return;
2408
- }
2409
- metrics.forwardedFrames += 1;
2410
- metrics.forwardedBytes += frame.bytes;
2411
- return;
2412
- }
2413
- if (liveHost) {
2414
- touchHost(liveHost);
2415
- if (value?.t === "ping") {
2416
- if (!consumeRate(liveHost.rate, 1)) {
2417
- closeHost(liveHost, 4429, "rate limit");
2418
- return;
2419
- }
2420
- if (!safeSend(socket, { t: "pong", at: now() }, maxQueueBytes)) {
2421
- metrics.droppedFrames += 1;
2422
- closeHost(liveHost, 4408, "relay backpressure");
2423
- }
2424
- return;
2425
- }
2426
- if (value?.t === "close-peer") {
2427
- const channelId = safeId3(value.channelId, "channel id");
2428
- if (!consumeRate(liveHost.rate, 1)) {
2429
- closeHost(liveHost, 4429, "rate limit");
2430
- return;
2431
- }
2432
- const device2 = devicesByChannel.get(channelId);
2433
- if (device2?.routeId === liveHost.routeId) closeDevice(device2, 4400, "host closed channel");
2434
- return;
2435
- }
2436
- const frame = parsePayload(value, true, maxFrameBytes);
2437
- if (!consumeRate(liveHost.rate, frame.bytes)) {
2438
- closeHost(liveHost, 4429, "rate limit");
2439
- return;
2440
- }
2441
- const device = devicesByChannel.get(frame.channelId);
2442
- if (!device || device.routeId !== liveHost.routeId) {
2443
- metrics.droppedFrames += 1;
2444
- return;
2445
- }
2446
- if (!safeSend(device.socket, { t: "frame", payload: frame.payload }, maxQueueBytes)) {
2447
- metrics.droppedFrames += 1;
2448
- closeDevice(device, 4408, "device backpressure");
2449
- return;
2450
- }
2451
- metrics.forwardedFrames += 1;
2452
- metrics.forwardedBytes += frame.bytes;
2453
- }
2454
- } catch {
2455
- metrics.droppedFrames += 1;
2456
- if (liveDevice) closeDevice(liveDevice, 4400, "invalid relay frame");
2457
- else if (liveHost) closeHost(liveHost, 4400, "invalid relay frame");
2458
- }
2459
- });
2460
- socket.once("close", () => {
2461
- sockets.delete(socket);
2462
- clearTimeout(handshakeTimer);
2463
- if (liveDevice) closeDevice(liveDevice);
2464
- if (liveHost) closeHost(liveHost);
2465
- });
2466
- socket.once("error", () => {
2467
- });
2468
- });
2469
- });
2470
- app.addHook("onClose", async () => {
2471
- for (const host of [...hosts.values()]) closeHost(host, 1001, "relay shutting down");
2472
- for (const socket of [...sockets]) socket.close(1001, "relay shutting down");
2473
- hostRates.clear();
2474
- deviceRates.clear();
2475
- if (ownsStore) store.close();
2476
- });
2477
- return {
2478
- app,
2479
- store,
2480
- ...accountStore ? { accountStore } : {},
2481
- metrics: () => ({
2482
- ...metrics,
2483
- activeConnections: sockets.size,
2484
- activeHosts: hosts.size,
2485
- activeDevices: devicesByChannel.size
2486
- })
2487
- };
2488
- }
2489
-
2490
- // src/data-dir.ts
2491
- import {
2492
- closeSync,
2493
- constants,
2494
- existsSync,
2495
- fchmodSync,
2496
- fstatSync,
2497
- fsyncSync,
2498
- linkSync,
2499
- lstatSync,
2500
- mkdirSync,
2501
- openSync,
2502
- readFileSync,
2503
- renameSync,
2504
- unlinkSync,
2505
- writeFileSync
2506
- } from "fs";
2507
- import { randomBytes as randomBytes4 } from "crypto";
2508
- import { dirname, join } from "path";
2509
- var MAX_ACCESS_TOKEN_BYTES = 4 * 1024;
2510
- var MAX_ACCESS_TOKEN_FILE_BYTES = MAX_ACCESS_TOKEN_BYTES + 2;
2511
- function resolveDataDir(env, exists = existsSync) {
2512
- if (env.ROAMCODE_DATA_DIR) return env.ROAMCODE_DATA_DIR;
2513
- if (env.REMOTE_CODER_DATA_DIR) return env.REMOTE_CODER_DATA_DIR;
2514
- const pick = (next, legacy) => !exists(next) && exists(legacy) ? legacy : next;
2515
- if (env.XDG_CONFIG_HOME)
2516
- return pick(join(env.XDG_CONFIG_HOME, "roamcode"), join(env.XDG_CONFIG_HOME, "remote-coder"));
2517
- if (env.HOME) return pick(join(env.HOME, ".config", "roamcode"), join(env.HOME, ".config", "remote-coder"));
2518
- return pick(join(process.cwd(), ".roamcode"), join(process.cwd(), ".remote-coder"));
2519
- }
2520
- function ensureDataDir(dir) {
2521
- mkdirSync(dir, { recursive: true, mode: 448 });
2522
- }
2523
-
2524
- // src/relay-start.ts
2525
- function boundedInteger(value, fallback, minimum, maximum, field) {
2526
- const parsed = value === void 0 ? fallback : Number(value);
2527
- if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) throw new Error(`invalid ${field}`);
2528
- return parsed;
2529
- }
2530
- function relayDataDir(env) {
2531
- const configured = env.ROAMCODE_RELAY_DATA_DIR?.trim();
2532
- return configured ? resolve(configured) : join2(resolveDataDir(env), "relay");
2533
- }
2534
- function relayOrigins(env) {
2535
- return (env.ROAMCODE_RELAY_ALLOWED_ORIGINS ?? "").split(",").map((value) => value.trim()).filter(Boolean);
2536
- }
2537
- function privateSecretFile(path, field) {
2538
- let descriptor;
2539
- try {
2540
- const before = lstatSync2(path);
2541
- if (!before.isFile() || before.isSymbolicLink() || before.size > 4096 || (before.mode & 63) !== 0 || typeof process.getuid === "function" && before.uid !== process.getuid()) {
2542
- throw new Error("unsafe secret file");
2543
- }
2544
- descriptor = openSync2(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
2545
- const opened = fstatSync2(descriptor);
2546
- if (!opened.isFile() || opened.size > 4096 || (opened.mode & 63) !== 0 || typeof process.getuid === "function" && opened.uid !== process.getuid() || opened.dev !== before.dev || opened.ino !== before.ino) {
2547
- throw new Error("unsafe secret file");
2548
- }
2549
- return readFileSync2(descriptor, "utf8").trim();
2550
- } catch {
2551
- throw new Error(`${field} could not be read securely`);
2552
- } finally {
2553
- if (descriptor !== void 0) closeSync2(descriptor);
2554
- }
2555
- }
2556
- function previousRootTokens(env) {
2557
- const inline = (env.ROAMCODE_RELAY_PREVIOUS_ROOT_TOKENS ?? "").split(",").map((value) => value.trim()).filter(Boolean);
2558
- const directory = env.ROAMCODE_RELAY_PREVIOUS_ROOT_TOKEN_DIR?.trim();
2559
- if (inline.length > 0 && directory) {
2560
- throw new Error(
2561
- "ROAMCODE_RELAY_PREVIOUS_ROOT_TOKENS and ROAMCODE_RELAY_PREVIOUS_ROOT_TOKEN_DIR are mutually exclusive"
2562
- );
2563
- }
2564
- if (!directory) return inline;
2565
- let before;
2566
- let entries;
2567
- try {
2568
- before = lstatSync2(directory);
2569
- if (!before.isDirectory() || before.isSymbolicLink() || (before.mode & 63) !== 0 || typeof process.getuid === "function" && before.uid !== process.getuid()) {
2570
- throw new Error("unsafe secret directory");
2571
- }
2572
- entries = readdirSync(directory, { withFileTypes: true });
2573
- const after = lstatSync2(directory);
2574
- if (after.dev !== before.dev || after.ino !== before.ino || (after.mode & 63) !== 0 || typeof process.getuid === "function" && after.uid !== process.getuid() || entries.some((entry) => !entry.isFile())) {
2575
- throw new Error("unsafe secret directory");
2576
- }
2577
- } catch {
2578
- throw new Error("ROAMCODE_RELAY_PREVIOUS_ROOT_TOKEN_DIR could not be read securely");
2579
- }
2580
- if (entries.length > 3) throw new Error("ROAMCODE_RELAY_PREVIOUS_ROOT_TOKEN_DIR may contain at most three files");
2581
- return entries.map((entry) => entry.name).sort().map((name) => privateSecretFile(join2(directory, name), "previous relay root capability"));
2582
- }
2583
- function explicitBoolean(value, field) {
2584
- if (value === void 0 || value === "" || value === "0" || value === "false") return false;
2585
- if (value === "1" || value === "true") return true;
2586
- throw new Error(`invalid ${field}`);
2587
- }
2588
- function secretValue(env, directKey, fileKey) {
2589
- const direct = env[directKey]?.trim();
2590
- const file = env[fileKey]?.trim();
2591
- if (direct && file) throw new Error(`${directKey} and ${fileKey} are mutually exclusive`);
2592
- if (direct) return direct;
2593
- if (!file) return void 0;
2594
- return privateSecretFile(file, fileKey);
2595
- }
2596
- async function startBlindRelay(env = process.env) {
2597
- const rootToken = secretValue(env, "ROAMCODE_RELAY_ROOT_TOKEN", "ROAMCODE_RELAY_ROOT_TOKEN_FILE");
2598
- if (!rootToken) throw new Error("ROAMCODE_RELAY_ROOT_TOKEN or ROAMCODE_RELAY_ROOT_TOKEN_FILE is required");
2599
- const previousRoots = previousRootTokens(env);
2600
- const allowedOrigins = relayOrigins(env);
2601
- const allowAnyOrigin = explicitBoolean(env.ROAMCODE_RELAY_ALLOW_ANY_ORIGIN, "relay allow-any-origin flag");
2602
- if (env.NODE_ENV === "production" && allowedOrigins.length === 0 && !allowAnyOrigin) {
2603
- throw new Error(
2604
- "ROAMCODE_RELAY_ALLOWED_ORIGINS is required in production; explicitly set ROAMCODE_RELAY_ALLOW_ANY_ORIGIN=1 only for a reviewed deployment"
2605
- );
2606
- }
2607
- const dataDir = relayDataDir(env);
2608
- ensureDataDir(dataDir);
2609
- const store = openRelayRouteStore({ dbPath: join2(dataDir, "routes.db") });
2610
- if (store.mode !== "sqlite") {
2611
- store.close();
2612
- throw new Error("relay requires durable SQLite; rebuild better-sqlite3 before starting");
2613
- }
2614
- const accountsEnabled = explicitBoolean(env.ROAMCODE_RELAY_ACCOUNTS_ENABLED, "relay accounts-enabled flag");
2615
- let accountStore;
2616
- if (accountsEnabled) {
2617
- accountStore = openRelayAccountStore({ dbPath: join2(dataDir, "accounts.db") });
2618
- if (accountStore.mode !== "sqlite") {
2619
- accountStore.close();
2620
- store.close();
2621
- throw new Error("relay accounts require durable SQLite; rebuild better-sqlite3 before starting");
2622
- }
2623
- }
2624
- let relay;
2625
- try {
2626
- relay = createBlindRelayServer({
2627
- rootToken,
2628
- previousRootTokens: previousRoots,
2629
- store,
2630
- ...accountStore ? { accountStore } : {},
2631
- allowedOrigins,
2632
- handshakeTimeoutMs: boundedInteger(
2633
- env.ROAMCODE_RELAY_HANDSHAKE_TIMEOUT_MS,
2634
- 5e3,
2635
- 1e3,
2636
- 3e4,
2637
- "relay handshake timeout"
2638
- ),
2639
- idleTimeoutMs: boundedInteger(
2640
- env.ROAMCODE_RELAY_IDLE_TIMEOUT_MS,
2641
- 12e4,
2642
- 1e4,
2643
- 36e5,
2644
- "relay idle timeout"
2645
- ),
2646
- maxFrameBytes: boundedInteger(
2647
- env.ROAMCODE_RELAY_MAX_FRAME_BYTES,
2648
- 15e5,
2649
- 1024,
2650
- 16 * 1024 * 1024,
2651
- "relay frame limit"
2652
- ),
2653
- maxQueueBytes: boundedInteger(
2654
- env.ROAMCODE_RELAY_MAX_QUEUE_BYTES,
2655
- 4e6,
2656
- 1024,
2657
- 64 * 1024 * 1024,
2658
- "relay queue limit"
2659
- ),
2660
- maxTotalConnections: boundedInteger(
2661
- env.ROAMCODE_RELAY_MAX_TOTAL_CONNECTIONS,
2662
- 1024,
2663
- 1,
2664
- 1e5,
2665
- "relay total connection limit"
2666
- ),
2667
- maxConnectionsPerRoute: boundedInteger(
2668
- env.ROAMCODE_RELAY_MAX_CONNECTIONS_PER_ROUTE,
2669
- 64,
2670
- 1,
2671
- 1e4,
2672
- "relay route connection limit"
2673
- ),
2674
- maxBytesPerMinute: boundedInteger(
2675
- env.ROAMCODE_RELAY_MAX_BYTES_PER_MINUTE,
2676
- 64 * 1024 * 1024,
2677
- 1024,
2678
- 1024 * 1024 * 1024,
2679
- "relay byte rate"
2680
- ),
2681
- maxMessagesPerMinute: boundedInteger(
2682
- env.ROAMCODE_RELAY_MAX_MESSAGES_PER_MINUTE,
2683
- 12e3,
2684
- 10,
2685
- 1e6,
2686
- "relay message rate"
2687
- )
2688
- });
2689
- } catch (error) {
2690
- accountStore?.close();
2691
- store.close();
2692
- throw error;
2693
- }
2694
- relay.app.addHook("onClose", async () => {
2695
- accountStore?.close();
2696
- store.close();
2697
- });
2698
- const host = env.ROAMCODE_RELAY_BIND?.trim() || "127.0.0.1";
2699
- const port = boundedInteger(env.ROAMCODE_RELAY_PORT, 4281, 0, 65535, "relay port");
2700
- try {
2701
- const url = await relay.app.listen({ host, port });
2702
- return { ...relay, url };
2703
- } catch (error) {
2704
- accountStore?.close();
2705
- store.close();
2706
- throw error;
2707
- }
2708
- }
2709
- function isRelayDirectExecution(moduleUrl, argv1, embeddedContainerBuild2 = false) {
2710
- if (embeddedContainerBuild2) return false;
2711
- if (!argv1) return false;
2712
- try {
2713
- return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(argv1);
2714
- } catch {
2715
- return moduleUrl === pathToFileURL(argv1).href;
2716
- }
2717
- }
2718
- var embeddedContainerBuild = true;
2719
- if (isRelayDirectExecution(import.meta.url, process.argv[1], embeddedContainerBuild)) {
2720
- void startBlindRelay().then((relay) => {
2721
- process.stdout.write(`RoamCode blind relay is listening at ${relay.url}
2722
- `);
2723
- const shutdown = () => relay.app.close().finally(() => process.exit(0));
2724
- process.once("SIGINT", shutdown);
2725
- process.once("SIGTERM", shutdown);
2726
- }).catch((error) => {
2727
- process.stderr.write(`roamcode relay failed to start: ${error.message}
2728
- `);
2729
- process.exitCode = 1;
2730
- });
2731
- }
2732
-
2733
- // src/relay-container.ts
2734
- void startBlindRelay().then((relay) => {
2735
- const shutdown = () => relay.app.close().finally(() => process.exit(0));
2736
- process.once("SIGINT", shutdown);
2737
- process.once("SIGTERM", shutdown);
2738
- }).catch((error) => {
2739
- process.stderr.write(`roamcode relay failed to start: ${error.message}
2740
- `);
2741
- process.exitCode = 1;
2742
- });