@roamcode.ai/server 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/container/relay.js +521 -140
- package/dist/index.d.ts +55 -8
- package/dist/index.js +1130 -278
- package/dist/relay-start.d.ts +15 -3
- package/dist/relay-start.js +521 -140
- package/dist/start.js +569 -109
- package/package.json +2 -2
package/dist/start.js
CHANGED
|
@@ -595,9 +595,27 @@ var RateLimiter = class {
|
|
|
595
595
|
};
|
|
596
596
|
|
|
597
597
|
// src/data-dir.ts
|
|
598
|
-
import {
|
|
598
|
+
import {
|
|
599
|
+
closeSync,
|
|
600
|
+
constants as constants2,
|
|
601
|
+
existsSync,
|
|
602
|
+
fchmodSync,
|
|
603
|
+
fstatSync,
|
|
604
|
+
fsyncSync,
|
|
605
|
+
linkSync,
|
|
606
|
+
lstatSync,
|
|
607
|
+
mkdirSync,
|
|
608
|
+
openSync,
|
|
609
|
+
readFileSync,
|
|
610
|
+
renameSync,
|
|
611
|
+
unlinkSync,
|
|
612
|
+
writeFileSync
|
|
613
|
+
} from "fs";
|
|
599
614
|
import { randomBytes } from "crypto";
|
|
600
|
-
import { join as join2 } from "path";
|
|
615
|
+
import { dirname, join as join2 } from "path";
|
|
616
|
+
var ACCESS_TOKEN_FILE = "token";
|
|
617
|
+
var MAX_ACCESS_TOKEN_BYTES = 4 * 1024;
|
|
618
|
+
var MAX_ACCESS_TOKEN_FILE_BYTES = MAX_ACCESS_TOKEN_BYTES + 2;
|
|
601
619
|
function resolveDataDir(env, exists = existsSync) {
|
|
602
620
|
if (env.ROAMCODE_DATA_DIR) return env.ROAMCODE_DATA_DIR;
|
|
603
621
|
if (env.REMOTE_CODER_DATA_DIR) return env.REMOTE_CODER_DATA_DIR;
|
|
@@ -613,29 +631,186 @@ function ensureDataDir(dir) {
|
|
|
613
631
|
function generateAccessToken() {
|
|
614
632
|
return randomBytes(32).toString("base64url");
|
|
615
633
|
}
|
|
634
|
+
function fsyncDirectory(path) {
|
|
635
|
+
let descriptor;
|
|
636
|
+
try {
|
|
637
|
+
descriptor = openSync(path, constants2.O_RDONLY);
|
|
638
|
+
fsyncSync(descriptor);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
const code = error.code;
|
|
641
|
+
if (code !== "EINVAL" && code !== "ENOTSUP" && !(process.platform === "win32" && code === "EPERM")) {
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
} finally {
|
|
645
|
+
if (descriptor !== void 0) closeSync(descriptor);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function existingTokenStat(path) {
|
|
649
|
+
try {
|
|
650
|
+
return lstatSync(path);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
if (error.code === "ENOENT") return void 0;
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function validateAccessToken(token) {
|
|
657
|
+
if (!token || Buffer.byteLength(token, "utf8") > MAX_ACCESS_TOKEN_BYTES || /[\u0000-\u0020\u007f]/u.test(token)) {
|
|
658
|
+
throw new Error("access token must be non-empty printable text without whitespace");
|
|
659
|
+
}
|
|
660
|
+
return token;
|
|
661
|
+
}
|
|
662
|
+
function decodePersistedAccessToken(content) {
|
|
663
|
+
let token = content;
|
|
664
|
+
if (token.endsWith("\n")) token = token.slice(0, -1);
|
|
665
|
+
if (token.endsWith("\r")) token = token.slice(0, -1);
|
|
666
|
+
return validateAccessToken(token);
|
|
667
|
+
}
|
|
668
|
+
function readPersistedAccessToken(path) {
|
|
669
|
+
const before = existingTokenStat(path);
|
|
670
|
+
if (!before) return void 0;
|
|
671
|
+
if (!before.isFile() || before.isSymbolicLink()) throw new Error("access token path must be a regular file");
|
|
672
|
+
if (before.size > MAX_ACCESS_TOKEN_FILE_BYTES) throw new Error("access token file is too large");
|
|
673
|
+
if (typeof process.getuid === "function" && before.uid !== process.getuid()) {
|
|
674
|
+
throw new Error("access token file must be owned by the current user");
|
|
675
|
+
}
|
|
676
|
+
let descriptor;
|
|
677
|
+
try {
|
|
678
|
+
descriptor = openSync(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
679
|
+
const opened = fstatSync(descriptor);
|
|
680
|
+
if (!opened.isFile() || opened.size > MAX_ACCESS_TOKEN_FILE_BYTES || opened.dev !== before.dev || opened.ino !== before.ino || typeof process.getuid === "function" && opened.uid !== process.getuid()) {
|
|
681
|
+
throw new Error("access token changed while it was being opened");
|
|
682
|
+
}
|
|
683
|
+
fchmodSync(descriptor, 384);
|
|
684
|
+
return decodePersistedAccessToken(readFileSync(descriptor, "utf8"));
|
|
685
|
+
} catch (error) {
|
|
686
|
+
if (error instanceof Error && error.message.startsWith("access token ")) throw error;
|
|
687
|
+
throw new Error("access token file could not be read safely");
|
|
688
|
+
} finally {
|
|
689
|
+
if (descriptor !== void 0) closeSync(descriptor);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function stageAccessToken(path, token) {
|
|
693
|
+
const temporary = `${path}.${randomBytes(12).toString("hex")}.tmp`;
|
|
694
|
+
let descriptor;
|
|
695
|
+
let failed = false;
|
|
696
|
+
let failure;
|
|
697
|
+
try {
|
|
698
|
+
descriptor = openSync(
|
|
699
|
+
temporary,
|
|
700
|
+
constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY | (constants2.O_NOFOLLOW ?? 0),
|
|
701
|
+
384
|
|
702
|
+
);
|
|
703
|
+
fchmodSync(descriptor, 384);
|
|
704
|
+
writeFileSync(descriptor, `${validateAccessToken(token)}
|
|
705
|
+
`, "utf8");
|
|
706
|
+
fsyncSync(descriptor);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
failed = true;
|
|
709
|
+
failure = error;
|
|
710
|
+
}
|
|
711
|
+
if (descriptor !== void 0) {
|
|
712
|
+
try {
|
|
713
|
+
closeSync(descriptor);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (!failed) {
|
|
716
|
+
failed = true;
|
|
717
|
+
failure = error;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
descriptor = void 0;
|
|
721
|
+
}
|
|
722
|
+
if (failed) {
|
|
723
|
+
try {
|
|
724
|
+
unlinkSync(temporary);
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
throw failure;
|
|
728
|
+
}
|
|
729
|
+
return temporary;
|
|
730
|
+
}
|
|
616
731
|
function persistAccessToken(dataDir, token) {
|
|
617
732
|
ensureDataDir(dataDir);
|
|
618
|
-
const tokenPath = join2(dataDir,
|
|
619
|
-
|
|
620
|
-
|
|
733
|
+
const tokenPath = join2(dataDir, ACCESS_TOKEN_FILE);
|
|
734
|
+
const existing = existingTokenStat(tokenPath);
|
|
735
|
+
if (existing) {
|
|
736
|
+
if (!existing.isFile() || existing.isSymbolicLink()) throw new Error("access token path must be a regular file");
|
|
737
|
+
if (typeof process.getuid === "function" && existing.uid !== process.getuid()) {
|
|
738
|
+
throw new Error("access token file must be owned by the current user");
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const temporary = stageAccessToken(tokenPath, token);
|
|
742
|
+
try {
|
|
743
|
+
renameSync(temporary, tokenPath);
|
|
744
|
+
try {
|
|
745
|
+
fsyncDirectory(dirname(tokenPath));
|
|
746
|
+
} catch (error) {
|
|
747
|
+
let visible;
|
|
748
|
+
try {
|
|
749
|
+
visible = readPersistedAccessToken(tokenPath);
|
|
750
|
+
} catch {
|
|
751
|
+
}
|
|
752
|
+
if (visible !== token) throw error;
|
|
753
|
+
}
|
|
754
|
+
} finally {
|
|
755
|
+
try {
|
|
756
|
+
unlinkSync(temporary);
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
}
|
|
621
760
|
}
|
|
622
|
-
function
|
|
623
|
-
|
|
624
|
-
const tokenPath = join2(
|
|
761
|
+
function installAccessTokenIfMissing(dataDir, token) {
|
|
762
|
+
ensureDataDir(dataDir);
|
|
763
|
+
const tokenPath = join2(dataDir, ACCESS_TOKEN_FILE);
|
|
764
|
+
const temporary = stageAccessToken(tokenPath, token);
|
|
765
|
+
let installed = false;
|
|
625
766
|
try {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
767
|
+
try {
|
|
768
|
+
linkSync(temporary, tokenPath);
|
|
769
|
+
installed = true;
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (error.code !== "EEXIST") throw error;
|
|
772
|
+
}
|
|
773
|
+
} finally {
|
|
774
|
+
try {
|
|
775
|
+
unlinkSync(temporary);
|
|
776
|
+
} catch {
|
|
777
|
+
}
|
|
629
778
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
779
|
+
if (installed) fsyncDirectory(dirname(tokenPath));
|
|
780
|
+
return installed;
|
|
781
|
+
}
|
|
782
|
+
function resolveAccessToken(opts) {
|
|
783
|
+
if (opts.configured) return { token: opts.configured, generated: false };
|
|
784
|
+
const tokenPath = join2(opts.dataDir, ACCESS_TOKEN_FILE);
|
|
785
|
+
const existing = readPersistedAccessToken(tokenPath);
|
|
786
|
+
if (existing) return { token: existing, generated: false };
|
|
787
|
+
const token = validateAccessToken((opts.generate ?? generateAccessToken)());
|
|
788
|
+
if (installAccessTokenIfMissing(opts.dataDir, token)) return { token, generated: true };
|
|
789
|
+
const winner = readPersistedAccessToken(tokenPath);
|
|
790
|
+
if (!winner) throw new Error("access token initialization raced with removal; retry startup");
|
|
791
|
+
return { token: winner, generated: false };
|
|
633
792
|
}
|
|
634
793
|
|
|
635
794
|
// src/static-routes.ts
|
|
636
795
|
import fastifyStatic from "@fastify/static";
|
|
637
796
|
import { existsSync as existsSync2 } from "fs";
|
|
638
797
|
import { resolve as resolve2, sep as sep2 } from "path";
|
|
798
|
+
var PWA_BOOT_WATCHDOG_SHA256 = "sha256-tcgQYptaPeNGqJtts8Ft/5H4tf+s+jfSWQSguIhTp8k=";
|
|
799
|
+
var PWA_CONTENT_SECURITY_POLICY = [
|
|
800
|
+
"default-src 'self'",
|
|
801
|
+
`script-src 'self' '${PWA_BOOT_WATCHDOG_SHA256}'`,
|
|
802
|
+
"style-src 'self' 'unsafe-inline'",
|
|
803
|
+
"img-src 'self' data: blob:",
|
|
804
|
+
"font-src 'self' data:",
|
|
805
|
+
"connect-src 'self' https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*",
|
|
806
|
+
"worker-src 'self' blob:",
|
|
807
|
+
"manifest-src 'self'",
|
|
808
|
+
"frame-src 'self' https: blob:",
|
|
809
|
+
"object-src 'none'",
|
|
810
|
+
"base-uri 'none'",
|
|
811
|
+
"form-action 'none'",
|
|
812
|
+
"frame-ancestors 'none'"
|
|
813
|
+
].join("; ");
|
|
639
814
|
var API_PATH_DENYLIST = [
|
|
640
815
|
/^\/sessions/,
|
|
641
816
|
/^\/resumable/,
|
|
@@ -690,6 +865,9 @@ function registerStatic(app, opts) {
|
|
|
690
865
|
app.register(fastifyStatic, { root: opts.webDir, wildcard: false });
|
|
691
866
|
app.addHook("onSend", (request, reply, payload, done) => {
|
|
692
867
|
const contentType = String(reply.getHeader("content-type") ?? "");
|
|
868
|
+
if (isPublicForRequest(request.url)) {
|
|
869
|
+
reply.header("content-security-policy", PWA_CONTENT_SECURITY_POLICY).header("permissions-policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()").header("referrer-policy", "no-referrer").header("x-content-type-options", "nosniff").header("x-frame-options", "DENY").header("x-permitted-cross-domain-policies", "none");
|
|
870
|
+
}
|
|
693
871
|
if (pathForGate(request.url) === "/sw.js" || contentType.includes("text/html")) {
|
|
694
872
|
reply.header("cache-control", "no-store, no-cache, must-revalidate");
|
|
695
873
|
}
|
|
@@ -1239,6 +1417,7 @@ function establishRelayChannel(options) {
|
|
|
1239
1417
|
var require2 = createRequire(import.meta.url);
|
|
1240
1418
|
var PAIRING_TTL_MS = 5 * 60 * 1e3;
|
|
1241
1419
|
var LAST_SEEN_WRITE_INTERVAL_MS = 60 * 1e3;
|
|
1420
|
+
var UNSAFE_DISPLAY_TEXT = /[\p{Cc}\p{Zl}\p{Zp}\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
|
|
1242
1421
|
var DevicePairingError = class extends Error {
|
|
1243
1422
|
constructor(code, message) {
|
|
1244
1423
|
super(message);
|
|
@@ -1294,7 +1473,7 @@ function randomCredential(prefix) {
|
|
|
1294
1473
|
function normalizeDeviceName(value) {
|
|
1295
1474
|
if (typeof value !== "string") return void 0;
|
|
1296
1475
|
const normalized = value.trim().replace(/\s+/g, " ");
|
|
1297
|
-
if (!normalized || normalized.length > 80 ||
|
|
1476
|
+
if (!normalized || normalized.length > 80 || UNSAFE_DISPLAY_TEXT.test(normalized)) return void 0;
|
|
1298
1477
|
return normalized;
|
|
1299
1478
|
}
|
|
1300
1479
|
function inMemoryStore(opts) {
|
|
@@ -1325,6 +1504,7 @@ function inMemoryStore(opts) {
|
|
|
1325
1504
|
}
|
|
1326
1505
|
throw new Error("could not allocate a unique pairing credential");
|
|
1327
1506
|
},
|
|
1507
|
+
cancelPairing: (secret) => pairings.delete(digest(secret)),
|
|
1328
1508
|
claimPairing(secret, rawName, now = Date.now(), relayIdentityPublicKey) {
|
|
1329
1509
|
const name = normalizeDeviceName(rawName);
|
|
1330
1510
|
if (!name) return void 0;
|
|
@@ -1374,13 +1554,46 @@ function inMemoryStore(opts) {
|
|
|
1374
1554
|
prune(now);
|
|
1375
1555
|
return [...pairings.values()].some((pairing) => pairing.deviceId === deviceId && pairing.expiresAt >= now);
|
|
1376
1556
|
},
|
|
1557
|
+
beginRelayPairingCancellation(deviceId, now = Date.now()) {
|
|
1558
|
+
prune(now);
|
|
1559
|
+
const pairing = [...pairings.values()].find((candidate) => candidate.deviceId === deviceId);
|
|
1560
|
+
if (!pairing || pairing.expiresAt < now) return { status: "missing" };
|
|
1561
|
+
if (pairing.cancellationId) return { status: "busy" };
|
|
1562
|
+
const reservation = { deviceId, reservationId: randomUUID2() };
|
|
1563
|
+
pairing.cancellationId = reservation.reservationId;
|
|
1564
|
+
return { status: "reserved", reservation };
|
|
1565
|
+
},
|
|
1566
|
+
releaseRelayPairingCancellation(reservation) {
|
|
1567
|
+
const pairing = [...pairings.values()].find((candidate) => candidate.deviceId === reservation.deviceId);
|
|
1568
|
+
if (pairing?.cancellationId !== reservation.reservationId) return false;
|
|
1569
|
+
delete pairing.cancellationId;
|
|
1570
|
+
return true;
|
|
1571
|
+
},
|
|
1572
|
+
finishRelayPairingCancellation(reservation) {
|
|
1573
|
+
for (const [secretHash, pairing] of pairings) {
|
|
1574
|
+
if (pairing.deviceId === reservation.deviceId && pairing.cancellationId === reservation.reservationId) {
|
|
1575
|
+
pairings.delete(secretHash);
|
|
1576
|
+
return true;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return false;
|
|
1580
|
+
},
|
|
1581
|
+
cancelRelayPairing(deviceId) {
|
|
1582
|
+
for (const [secretHash, pairing] of pairings) {
|
|
1583
|
+
if (pairing.deviceId === deviceId) {
|
|
1584
|
+
pairings.delete(secretHash);
|
|
1585
|
+
return true;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return false;
|
|
1589
|
+
},
|
|
1377
1590
|
claimRelayPairing(secret, token, rawName, relayIdentityPublicKey, now = Date.now()) {
|
|
1378
1591
|
const name = normalizeDeviceName(rawName);
|
|
1379
1592
|
if (!name || !/^rcd_[A-Za-z0-9_-]{43}$/.test(token)) return void 0;
|
|
1380
1593
|
prune(now);
|
|
1381
1594
|
const secretHash = digest(secret);
|
|
1382
1595
|
const pairing = pairings.get(secretHash);
|
|
1383
|
-
if (!pairing?.deviceId || !pairing.tokenHash || pairing.expiresAt < now || pairing.tokenHash !== digest(token))
|
|
1596
|
+
if (!pairing?.deviceId || !pairing.tokenHash || pairing.cancellationId || pairing.expiresAt < now || pairing.tokenHash !== digest(token))
|
|
1384
1597
|
return void 0;
|
|
1385
1598
|
const relayIdentity = relayIdentityForClaim(pairing.scopes, relayIdentityPublicKey);
|
|
1386
1599
|
pairings.delete(secretHash);
|
|
@@ -1464,7 +1677,8 @@ function openDeviceStore(opts) {
|
|
|
1464
1677
|
expires_at INTEGER NOT NULL,
|
|
1465
1678
|
scopes_json TEXT NOT NULL DEFAULT '["direct"]',
|
|
1466
1679
|
device_id TEXT,
|
|
1467
|
-
token_hash TEXT
|
|
1680
|
+
token_hash TEXT,
|
|
1681
|
+
cancellation_id TEXT
|
|
1468
1682
|
);
|
|
1469
1683
|
CREATE INDEX IF NOT EXISTS devices_last_seen_idx ON devices(last_seen_at DESC);
|
|
1470
1684
|
CREATE INDEX IF NOT EXISTS pairing_expiry_idx ON pairing_sessions(expires_at);
|
|
@@ -1489,6 +1703,9 @@ function openDeviceStore(opts) {
|
|
|
1489
1703
|
if (!pairingColumns.some((column) => column.name === "token_hash")) {
|
|
1490
1704
|
db.exec("ALTER TABLE pairing_sessions ADD COLUMN token_hash TEXT");
|
|
1491
1705
|
}
|
|
1706
|
+
if (!pairingColumns.some((column) => column.name === "cancellation_id")) {
|
|
1707
|
+
db.exec("ALTER TABLE pairing_sessions ADD COLUMN cancellation_id TEXT");
|
|
1708
|
+
}
|
|
1492
1709
|
db.exec(`
|
|
1493
1710
|
CREATE UNIQUE INDEX IF NOT EXISTS pairing_device_id_idx ON pairing_sessions(device_id) WHERE device_id IS NOT NULL;
|
|
1494
1711
|
CREATE UNIQUE INDEX IF NOT EXISTS pairing_token_hash_idx ON pairing_sessions(token_hash) WHERE token_hash IS NOT NULL;
|
|
@@ -1503,12 +1720,26 @@ function openDeviceStore(opts) {
|
|
|
1503
1720
|
"INSERT INTO pairing_sessions (secret_hash, created_at, expires_at, scopes_json, device_id, token_hash) VALUES (?, ?, ?, ?, ?, ?)"
|
|
1504
1721
|
);
|
|
1505
1722
|
const prunePairings = db.prepare("DELETE FROM pairing_sessions WHERE expires_at < ?");
|
|
1723
|
+
const cancelPairing = db.prepare("DELETE FROM pairing_sessions WHERE secret_hash = ?");
|
|
1506
1724
|
const findPairing = db.prepare(
|
|
1507
|
-
"SELECT secret_hash, expires_at, scopes_json, device_id, token_hash FROM pairing_sessions WHERE secret_hash = ?"
|
|
1725
|
+
"SELECT secret_hash, expires_at, scopes_json, device_id, token_hash, cancellation_id FROM pairing_sessions WHERE secret_hash = ?"
|
|
1508
1726
|
);
|
|
1509
1727
|
const findPendingRelayPairing = db.prepare(
|
|
1510
1728
|
"SELECT 1 FROM pairing_sessions WHERE device_id = ? AND expires_at >= ? LIMIT 1"
|
|
1511
1729
|
);
|
|
1730
|
+
const cancelRelayPairing = db.prepare("DELETE FROM pairing_sessions WHERE device_id = ?");
|
|
1731
|
+
const findRelayPairingByDevice = db.prepare(
|
|
1732
|
+
"SELECT secret_hash, expires_at, scopes_json, device_id, token_hash, cancellation_id FROM pairing_sessions WHERE device_id = ?"
|
|
1733
|
+
);
|
|
1734
|
+
const reserveRelayPairingCancellation = db.prepare(
|
|
1735
|
+
"UPDATE pairing_sessions SET cancellation_id = ? WHERE device_id = ? AND cancellation_id IS NULL"
|
|
1736
|
+
);
|
|
1737
|
+
const releaseRelayPairingCancellation = db.prepare(
|
|
1738
|
+
"UPDATE pairing_sessions SET cancellation_id = NULL WHERE device_id = ? AND cancellation_id = ?"
|
|
1739
|
+
);
|
|
1740
|
+
const finishRelayPairingCancellation = db.prepare(
|
|
1741
|
+
"DELETE FROM pairing_sessions WHERE device_id = ? AND cancellation_id = ?"
|
|
1742
|
+
);
|
|
1512
1743
|
const deletePairing = db.prepare("DELETE FROM pairing_sessions WHERE secret_hash = ?");
|
|
1513
1744
|
const insertDevice = db.prepare(
|
|
1514
1745
|
"INSERT INTO devices (id, name, token_hash, created_at, last_seen_at, scopes_json, relay_public_key, relay_fingerprint) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
@@ -1555,7 +1786,7 @@ function openDeviceStore(opts) {
|
|
|
1555
1786
|
(secretHash, token, name, now, relayIdentityPublicKey) => {
|
|
1556
1787
|
prunePairings.run(now);
|
|
1557
1788
|
const pairing = findPairing.get(secretHash);
|
|
1558
|
-
if (!pairing?.device_id || !pairing.token_hash || pairing.expires_at < now || pairing.token_hash !== digest(token))
|
|
1789
|
+
if (!pairing?.device_id || !pairing.token_hash || pairing.cancellation_id || pairing.expires_at < now || pairing.token_hash !== digest(token))
|
|
1559
1790
|
return void 0;
|
|
1560
1791
|
const scopes = scopesFromJson(pairing.scopes_json);
|
|
1561
1792
|
const relayIdentity = relayIdentityForClaim(scopes, relayIdentityPublicKey);
|
|
@@ -1602,6 +1833,7 @@ function openDeviceStore(opts) {
|
|
|
1602
1833
|
}
|
|
1603
1834
|
throw new Error("could not allocate a unique pairing credential");
|
|
1604
1835
|
},
|
|
1836
|
+
cancelPairing: (secret) => cancelPairing.run(digest(secret)).changes > 0,
|
|
1605
1837
|
claimPairing(secret, rawName, now = Date.now(), relayIdentityPublicKey) {
|
|
1606
1838
|
const name = normalizeDeviceName(rawName);
|
|
1607
1839
|
if (!name) return void 0;
|
|
@@ -1635,6 +1867,20 @@ function openDeviceStore(opts) {
|
|
|
1635
1867
|
prunePairings.run(now);
|
|
1636
1868
|
return findPendingRelayPairing.get(deviceId, now) !== void 0;
|
|
1637
1869
|
},
|
|
1870
|
+
beginRelayPairingCancellation(deviceId, now = Date.now()) {
|
|
1871
|
+
prunePairings.run(now);
|
|
1872
|
+
const pairing = findRelayPairingByDevice.get(deviceId);
|
|
1873
|
+
if (!pairing || pairing.expires_at < now) return { status: "missing" };
|
|
1874
|
+
if (pairing.cancellation_id) return { status: "busy" };
|
|
1875
|
+
const reservation = { deviceId, reservationId: randomUUID2() };
|
|
1876
|
+
if (reserveRelayPairingCancellation.run(reservation.reservationId, deviceId).changes !== 1) {
|
|
1877
|
+
return findRelayPairingByDevice.get(deviceId) ? { status: "busy" } : { status: "missing" };
|
|
1878
|
+
}
|
|
1879
|
+
return { status: "reserved", reservation };
|
|
1880
|
+
},
|
|
1881
|
+
releaseRelayPairingCancellation: (reservation) => releaseRelayPairingCancellation.run(reservation.deviceId, reservation.reservationId).changes === 1,
|
|
1882
|
+
finishRelayPairingCancellation: (reservation) => finishRelayPairingCancellation.run(reservation.deviceId, reservation.reservationId).changes === 1,
|
|
1883
|
+
cancelRelayPairing: (deviceId) => cancelRelayPairing.run(deviceId).changes > 0,
|
|
1638
1884
|
claimRelayPairing(secret, token, rawName, relayIdentityPublicKey, now = Date.now()) {
|
|
1639
1885
|
const name = normalizeDeviceName(rawName);
|
|
1640
1886
|
if (!name || !/^rcd_[A-Za-z0-9_-]{43}$/.test(token)) return void 0;
|
|
@@ -1683,7 +1929,7 @@ function generateRelayCredential(prefix = "rrd") {
|
|
|
1683
1929
|
|
|
1684
1930
|
// src/relay-pairing.ts
|
|
1685
1931
|
function isLoopback(hostname) {
|
|
1686
|
-
return hostname === "localhost" || hostname === "::1" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
1932
|
+
return hostname === "localhost" || hostname === "::1" || hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
1687
1933
|
}
|
|
1688
1934
|
function normalizeRelayAppUrl(value) {
|
|
1689
1935
|
const url = new URL(value.trim());
|
|
@@ -2931,35 +3177,35 @@ import {
|
|
|
2931
3177
|
renameSync as nodeRenameSync,
|
|
2932
3178
|
writeFileSync as nodeWriteFileSync
|
|
2933
3179
|
} from "fs";
|
|
2934
|
-
import { dirname as
|
|
3180
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
2935
3181
|
import { fileURLToPath } from "url";
|
|
2936
3182
|
|
|
2937
3183
|
// src/managed-runtime.ts
|
|
2938
3184
|
import { spawn } from "child_process";
|
|
2939
3185
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2940
3186
|
import {
|
|
2941
|
-
chmodSync as
|
|
3187
|
+
chmodSync as chmodSync2,
|
|
2942
3188
|
existsSync as existsSync4,
|
|
2943
|
-
lstatSync,
|
|
3189
|
+
lstatSync as lstatSync2,
|
|
2944
3190
|
mkdirSync as mkdirSync3,
|
|
2945
|
-
openSync,
|
|
2946
|
-
closeSync,
|
|
3191
|
+
openSync as openSync2,
|
|
3192
|
+
closeSync as closeSync2,
|
|
2947
3193
|
readdirSync,
|
|
2948
3194
|
readFileSync as readFileSync3,
|
|
2949
3195
|
realpathSync,
|
|
2950
|
-
renameSync,
|
|
3196
|
+
renameSync as renameSync2,
|
|
2951
3197
|
rmSync,
|
|
2952
3198
|
symlinkSync,
|
|
2953
|
-
unlinkSync,
|
|
3199
|
+
unlinkSync as unlinkSync2,
|
|
2954
3200
|
writeFileSync as writeFileSync3
|
|
2955
3201
|
} from "fs";
|
|
2956
3202
|
import { homedir as homedir2, tmpdir } from "os";
|
|
2957
|
-
import { basename as basename3, dirname as
|
|
3203
|
+
import { basename as basename3, dirname as dirname3, join as join5, relative, resolve as resolve5 } from "path";
|
|
2958
3204
|
|
|
2959
3205
|
// src/service-install.ts
|
|
2960
|
-
import { chmodSync
|
|
3206
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2961
3207
|
import { homedir, platform } from "os";
|
|
2962
|
-
import { dirname, join as join4 } from "path";
|
|
3208
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
2963
3209
|
import { spawnSync } from "child_process";
|
|
2964
3210
|
|
|
2965
3211
|
// src/managed-runtime.ts
|
|
@@ -3034,7 +3280,7 @@ function readPreviousVersion(root) {
|
|
|
3034
3280
|
}
|
|
3035
3281
|
|
|
3036
3282
|
// src/updater.ts
|
|
3037
|
-
var RUNNING_VERSION = "1.
|
|
3283
|
+
var RUNNING_VERSION = "1.2.0" ? "1.2.0".replace(/^v/, "") : packageVersion();
|
|
3038
3284
|
var RELEASES_API = "https://api.github.com/repos/burakgon/roamcode/releases?per_page=100";
|
|
3039
3285
|
var RELEASE_MANIFEST_ASSET = "roamcode-release.json";
|
|
3040
3286
|
var CHECK_CACHE_MS = 15 * 6e4;
|
|
@@ -3149,7 +3395,7 @@ function manifestIntegrities(value, version) {
|
|
|
3149
3395
|
return integrities;
|
|
3150
3396
|
}
|
|
3151
3397
|
function moduleDir() {
|
|
3152
|
-
return
|
|
3398
|
+
return dirname4(fileURLToPath(import.meta.url));
|
|
3153
3399
|
}
|
|
3154
3400
|
var Updater = class {
|
|
3155
3401
|
fs;
|
|
@@ -3535,7 +3781,7 @@ function createClaudeVersionProbe(deps) {
|
|
|
3535
3781
|
}
|
|
3536
3782
|
|
|
3537
3783
|
// src/terminal-manager.ts
|
|
3538
|
-
import { unlinkSync as
|
|
3784
|
+
import { unlinkSync as unlinkSync3, readdirSync as readdirSync2 } from "fs";
|
|
3539
3785
|
import { join as join9 } from "path";
|
|
3540
3786
|
|
|
3541
3787
|
// src/terminal-process.ts
|
|
@@ -3544,17 +3790,21 @@ import { EventEmitter } from "events";
|
|
|
3544
3790
|
import { createRequire as createRequire6 } from "module";
|
|
3545
3791
|
|
|
3546
3792
|
// src/node-pty-runtime.ts
|
|
3547
|
-
import { chmodSync as
|
|
3793
|
+
import { accessSync, chmodSync as chmodSync3, constants as constants3, existsSync as existsSync5 } from "fs";
|
|
3548
3794
|
import { createRequire as createRequire5 } from "module";
|
|
3549
|
-
import { dirname as
|
|
3795
|
+
import { dirname as dirname5, join as join7, resolve as resolve6 } from "path";
|
|
3550
3796
|
var require6 = createRequire5(import.meta.url);
|
|
3551
3797
|
function ensureNodePtySpawnHelperExecutable(resolveNodePty = () => require6.resolve("node-pty"), platform2 = process.platform, arch = process.arch) {
|
|
3552
|
-
if (platform2 !== "darwin") return;
|
|
3798
|
+
if (platform2 !== "darwin") return true;
|
|
3553
3799
|
try {
|
|
3554
|
-
const packageRoot = resolve6(
|
|
3800
|
+
const packageRoot = resolve6(dirname5(resolveNodePty()), "..");
|
|
3555
3801
|
const helper = join7(packageRoot, "prebuilds", `${platform2}-${arch}`, "spawn-helper");
|
|
3556
|
-
if (existsSync5(helper))
|
|
3802
|
+
if (!existsSync5(helper)) return true;
|
|
3803
|
+
chmodSync3(helper, 493);
|
|
3804
|
+
accessSync(helper, constants3.X_OK);
|
|
3805
|
+
return true;
|
|
3557
3806
|
} catch {
|
|
3807
|
+
return false;
|
|
3558
3808
|
}
|
|
3559
3809
|
}
|
|
3560
3810
|
|
|
@@ -3731,7 +3981,7 @@ var TerminalProcess = class extends EventEmitter {
|
|
|
3731
3981
|
}
|
|
3732
3982
|
};
|
|
3733
3983
|
var defaultPtySpawn = (file, args, opts) => {
|
|
3734
|
-
ensureNodePtySpawnHelperExecutable();
|
|
3984
|
+
if (!ensureNodePtySpawnHelperExecutable()) throw new Error("node-pty spawn helper is not executable");
|
|
3735
3985
|
const pty = require7("node-pty");
|
|
3736
3986
|
return pty.spawn(file, args, opts);
|
|
3737
3987
|
};
|
|
@@ -5779,7 +6029,7 @@ var TerminalManager = class {
|
|
|
5779
6029
|
const sessionId = m?.[1] ?? (name.startsWith(CODEX_MCP_TOKEN_PREFIX) ? name.slice(CODEX_MCP_TOKEN_PREFIX.length) : void 0);
|
|
5780
6030
|
if (!sessionId || liveIds.has(sessionId)) continue;
|
|
5781
6031
|
try {
|
|
5782
|
-
|
|
6032
|
+
unlinkSync3(join9(dir, name));
|
|
5783
6033
|
removed += 1;
|
|
5784
6034
|
} catch {
|
|
5785
6035
|
}
|
|
@@ -6440,7 +6690,9 @@ function tmuxOnPath() {
|
|
|
6440
6690
|
}
|
|
6441
6691
|
function ptyLoads() {
|
|
6442
6692
|
try {
|
|
6443
|
-
require9.resolve("node-pty");
|
|
6693
|
+
const entry = require9.resolve("node-pty");
|
|
6694
|
+
if (!ensureNodePtySpawnHelperExecutable(() => entry)) return false;
|
|
6695
|
+
require9("node-pty");
|
|
6444
6696
|
return true;
|
|
6445
6697
|
} catch {
|
|
6446
6698
|
return false;
|
|
@@ -6472,11 +6724,11 @@ function listTmuxSessions(run = defaultRun) {
|
|
|
6472
6724
|
}
|
|
6473
6725
|
|
|
6474
6726
|
// src/providers/provider-artifacts.ts
|
|
6475
|
-
import { chmodSync as
|
|
6727
|
+
import { chmodSync as chmodSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
6476
6728
|
function cleanupProviderArtifacts(paths) {
|
|
6477
6729
|
for (const path of paths) {
|
|
6478
6730
|
try {
|
|
6479
|
-
|
|
6731
|
+
unlinkSync4(path);
|
|
6480
6732
|
} catch {
|
|
6481
6733
|
}
|
|
6482
6734
|
}
|
|
@@ -6491,7 +6743,7 @@ function writeProviderArtifact0600(path, content, context, ownedPaths) {
|
|
|
6491
6743
|
ownedPaths.push(path);
|
|
6492
6744
|
try {
|
|
6493
6745
|
writeFileSync4(path, content, { mode: 384 });
|
|
6494
|
-
|
|
6746
|
+
chmodSync4(path, 384);
|
|
6495
6747
|
return true;
|
|
6496
6748
|
} catch {
|
|
6497
6749
|
cleanupProviderArtifacts([path]);
|
|
@@ -7515,6 +7767,30 @@ function buildOpenApiDocument(options) {
|
|
|
7515
7767
|
}
|
|
7516
7768
|
}
|
|
7517
7769
|
},
|
|
7770
|
+
"/api/v1/relay/pairing/cancel": {
|
|
7771
|
+
post: {
|
|
7772
|
+
operationId: "cancelRelayPairing",
|
|
7773
|
+
description: "Revokes an unused broker bootstrap before deleting its local one-use pairing state. A raced completed enrollment is never silently revoked.",
|
|
7774
|
+
parameters: [idempotency],
|
|
7775
|
+
requestBody: {
|
|
7776
|
+
required: true,
|
|
7777
|
+
content: json({
|
|
7778
|
+
type: "object",
|
|
7779
|
+
required: ["deviceId"],
|
|
7780
|
+
additionalProperties: false,
|
|
7781
|
+
properties: { deviceId: { type: "string", minLength: 1, maxLength: 128 } }
|
|
7782
|
+
})
|
|
7783
|
+
},
|
|
7784
|
+
responses: { "204": response("Cancelled remote pairing"), ...errorResponses }
|
|
7785
|
+
}
|
|
7786
|
+
},
|
|
7787
|
+
"/api/v1/relay/status": {
|
|
7788
|
+
get: {
|
|
7789
|
+
operationId: "getRelayStatus",
|
|
7790
|
+
description: "Returns privacy-bounded host connector health without routing identifiers or capabilities.",
|
|
7791
|
+
responses: { "200": response("Relay connector health", ref("RelayStatus")), ...errorResponses }
|
|
7792
|
+
}
|
|
7793
|
+
},
|
|
7518
7794
|
"/api/v1/team": {
|
|
7519
7795
|
get: {
|
|
7520
7796
|
operationId: "getTeam",
|
|
@@ -8070,6 +8346,18 @@ function buildOpenApiDocument(options) {
|
|
|
8070
8346
|
hostIdentityFingerprint: { type: "string", pattern: "^sha256:[A-Za-z0-9_-]{43}$" }
|
|
8071
8347
|
}
|
|
8072
8348
|
},
|
|
8349
|
+
RelayStatus: {
|
|
8350
|
+
type: "object",
|
|
8351
|
+
required: ["configured", "pairingAvailable", "status", "activeDevices", "reconnects"],
|
|
8352
|
+
additionalProperties: false,
|
|
8353
|
+
properties: {
|
|
8354
|
+
configured: { type: "boolean" },
|
|
8355
|
+
pairingAvailable: { type: "boolean" },
|
|
8356
|
+
status: { enum: ["not-configured", "idle", "connecting", "online", "reconnecting", "stopped"] },
|
|
8357
|
+
activeDevices: { type: "integer", minimum: 0 },
|
|
8358
|
+
reconnects: { type: "integer", minimum: 0 }
|
|
8359
|
+
}
|
|
8360
|
+
},
|
|
8073
8361
|
WorkspaceCreate: {
|
|
8074
8362
|
type: "object",
|
|
8075
8363
|
required: ["cwd"],
|
|
@@ -8772,7 +9060,7 @@ function buildOpenApiDocument(options) {
|
|
|
8772
9060
|
// src/worktree-service.ts
|
|
8773
9061
|
import { spawn as spawn4 } from "child_process";
|
|
8774
9062
|
import { realpath as realpath2, stat as stat2 } from "fs/promises";
|
|
8775
|
-
import { basename as basename4, dirname as
|
|
9063
|
+
import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve7 } from "path";
|
|
8776
9064
|
var MAX_GIT_OUTPUT_BYTES = 256 * 1024;
|
|
8777
9065
|
var GIT_TIMEOUT_MS = 6e4;
|
|
8778
9066
|
var WorktreeError = class extends Error {
|
|
@@ -8813,7 +9101,7 @@ function createWorktreeService(options) {
|
|
|
8813
9101
|
};
|
|
8814
9102
|
const confinedTarget = async (value) => {
|
|
8815
9103
|
const lexical = resolve7(value);
|
|
8816
|
-
const parent = await realpath2(
|
|
9104
|
+
const parent = await realpath2(dirname6(lexical)).catch(() => void 0);
|
|
8817
9105
|
if (!parent || !inside(await root(), parent)) {
|
|
8818
9106
|
throw new WorktreeError("WORKTREE_OUTSIDE_ROOT", "worktree parent is outside FS_ROOT", 403);
|
|
8819
9107
|
}
|
|
@@ -8888,7 +9176,7 @@ function createWorktreeService(options) {
|
|
|
8888
9176
|
const gitRaw = (await runGit(candidate, ["rev-parse", "--git-dir"])).stdout.trim();
|
|
8889
9177
|
const commonDir = await realpath2(resolve7(candidate, commonRaw));
|
|
8890
9178
|
const gitDir = await realpath2(resolve7(candidate, gitRaw));
|
|
8891
|
-
const repositoryPath =
|
|
9179
|
+
const repositoryPath = dirname6(commonDir);
|
|
8892
9180
|
if (!inside(await root(), repositoryPath)) {
|
|
8893
9181
|
throw new WorktreeError("WORKTREE_OUTSIDE_ROOT", "repository metadata is outside FS_ROOT", 403);
|
|
8894
9182
|
}
|
|
@@ -9184,7 +9472,7 @@ function createInstalledAdapterProvider(options) {
|
|
|
9184
9472
|
import { createHash as createHash5, createPublicKey as createPublicKey2, verify as verifySignature } from "crypto";
|
|
9185
9473
|
import { createRequire as createRequire9 } from "module";
|
|
9186
9474
|
import { access, mkdir as mkdir2, open as open2, opendir, readFile as readFile2, realpath as realpath4, rename as rename2, rm, stat as stat4 } from "fs/promises";
|
|
9187
|
-
import { dirname as
|
|
9475
|
+
import { dirname as dirname7, isAbsolute as isAbsolute5, join as join10, relative as relative4, resolve as resolve9 } from "path";
|
|
9188
9476
|
import { z as z5 } from "zod";
|
|
9189
9477
|
var require10 = createRequire9(import.meta.url);
|
|
9190
9478
|
var MAX_PACKAGE_FILES = 512;
|
|
@@ -9456,7 +9744,7 @@ async function materialize(snapshot, target) {
|
|
|
9456
9744
|
try {
|
|
9457
9745
|
for (const file of snapshot.files) {
|
|
9458
9746
|
const output = join10(temporary, ...file.path.split("/"));
|
|
9459
|
-
await mkdir2(
|
|
9747
|
+
await mkdir2(dirname7(output), { recursive: true, mode: 448 });
|
|
9460
9748
|
const handle = await open2(output, "wx", file.mode);
|
|
9461
9749
|
try {
|
|
9462
9750
|
await handle.writeFile(file.bytes);
|
|
@@ -9464,7 +9752,7 @@ async function materialize(snapshot, target) {
|
|
|
9464
9752
|
await handle.close();
|
|
9465
9753
|
}
|
|
9466
9754
|
}
|
|
9467
|
-
await mkdir2(
|
|
9755
|
+
await mkdir2(dirname7(target), { recursive: true, mode: 448 });
|
|
9468
9756
|
await rename2(temporary, target).catch(async (error) => {
|
|
9469
9757
|
if (error.code !== "EEXIST" && error.code !== "ENOTEMPTY")
|
|
9470
9758
|
throw error;
|
|
@@ -10926,7 +11214,7 @@ function openPolicyStore(options) {
|
|
|
10926
11214
|
|
|
10927
11215
|
// src/peer-store.ts
|
|
10928
11216
|
import { randomBytes as randomBytes9 } from "crypto";
|
|
10929
|
-
import { chmodSync as
|
|
11217
|
+
import { chmodSync as chmodSync5 } from "fs";
|
|
10930
11218
|
import { createRequire as createRequire12 } from "module";
|
|
10931
11219
|
var require13 = createRequire12(import.meta.url);
|
|
10932
11220
|
var PeerRevisionConflictError = class extends Error {
|
|
@@ -10953,7 +11241,7 @@ function safeLabel(value) {
|
|
|
10953
11241
|
return label;
|
|
10954
11242
|
}
|
|
10955
11243
|
function isLoopback2(hostname) {
|
|
10956
|
-
return hostname === "localhost" || hostname === "::1" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
11244
|
+
return hostname === "localhost" || hostname === "::1" || hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
10957
11245
|
}
|
|
10958
11246
|
function normalizePeerBaseUrl(value) {
|
|
10959
11247
|
if (typeof value !== "string") throw new Error("peer URL is required");
|
|
@@ -11151,7 +11439,7 @@ function openPeerStore(options) {
|
|
|
11151
11439
|
return createMemoryStore5(options);
|
|
11152
11440
|
}
|
|
11153
11441
|
const db = new Database(options.dbPath);
|
|
11154
|
-
if (options.dbPath !== ":memory:")
|
|
11442
|
+
if (options.dbPath !== ":memory:") chmodSync5(options.dbPath, 384);
|
|
11155
11443
|
db.pragma("journal_mode = WAL");
|
|
11156
11444
|
db.pragma("busy_timeout = 5000");
|
|
11157
11445
|
db.exec(`
|
|
@@ -12382,6 +12670,10 @@ function createServer(config, deps = {}) {
|
|
|
12382
12670
|
if (device) return { actorType: "device", actorId: device.id, label: device.name };
|
|
12383
12671
|
return hostPrincipal();
|
|
12384
12672
|
};
|
|
12673
|
+
const currentDeviceIdForRequest = (request) => {
|
|
12674
|
+
const principal = authenticatedPrincipals.get(request);
|
|
12675
|
+
return principal?.actorType === "device" || principal?.actorType === "relay" ? principal.actorId : void 0;
|
|
12676
|
+
};
|
|
12385
12677
|
const teamResourceForSession = (sessionId) => ({
|
|
12386
12678
|
hostId: commandStore.getHost().id,
|
|
12387
12679
|
...commandStore.placementForSession(sessionId)?.workspaceId ? { workspaceId: commandStore.placementForSession(sessionId).workspaceId } : {}
|
|
@@ -13313,6 +13605,25 @@ function createServer(config, deps = {}) {
|
|
|
13313
13605
|
reply.code(500).send({ error: "could not start device pairing" });
|
|
13314
13606
|
}
|
|
13315
13607
|
});
|
|
13608
|
+
app.post("/pairing/cancel", { bodyLimit: 8 * 1024 }, async (request, reply) => {
|
|
13609
|
+
const secret = request.body?.secret;
|
|
13610
|
+
if (typeof secret !== "string" || !/^rcp_[A-Za-z0-9_-]{43}$/.test(secret)) {
|
|
13611
|
+
reply.code(400).send({ code: "INVALID_PAIRING", error: "valid pairing capability is required" });
|
|
13612
|
+
return;
|
|
13613
|
+
}
|
|
13614
|
+
let cancelled = false;
|
|
13615
|
+
try {
|
|
13616
|
+
cancelled = deviceStore.cancelPairing(secret);
|
|
13617
|
+
} catch {
|
|
13618
|
+
reply.code(500).send({ code: "PAIRING_CANCEL_FAILED", error: "could not cancel pairing" });
|
|
13619
|
+
return;
|
|
13620
|
+
}
|
|
13621
|
+
if (!cancelled) {
|
|
13622
|
+
reply.code(404).send({ code: "PAIRING_NOT_FOUND", error: "pairing is expired, cancelled, or already used" });
|
|
13623
|
+
return;
|
|
13624
|
+
}
|
|
13625
|
+
reply.header("cache-control", "no-store").code(204).send();
|
|
13626
|
+
});
|
|
13316
13627
|
app.post("/api/v1/relay/pairing", async (_request, reply) => {
|
|
13317
13628
|
const relay = deps.relayPairing;
|
|
13318
13629
|
if (!relay) {
|
|
@@ -13322,9 +13633,14 @@ function createServer(config, deps = {}) {
|
|
|
13322
13633
|
});
|
|
13323
13634
|
return;
|
|
13324
13635
|
}
|
|
13636
|
+
let pendingDeviceId;
|
|
13325
13637
|
try {
|
|
13326
13638
|
const pairing = deviceStore.issueRelayPairing();
|
|
13639
|
+
pendingDeviceId = pairing.deviceId;
|
|
13327
13640
|
const deviceCredential = (relay.generateDeviceCredential ?? (() => generateRelayCredential("rrd")))();
|
|
13641
|
+
if (!/^rrd_[A-Za-z0-9_-]{43}$/.test(deviceCredential)) {
|
|
13642
|
+
throw new Error("invalid generated relay device credential");
|
|
13643
|
+
}
|
|
13328
13644
|
await relay.provisioner.putDevice(pairing.deviceId, relayCredentialHash(deviceCredential), pairing.expiresAt);
|
|
13329
13645
|
const payload = {
|
|
13330
13646
|
v: 1,
|
|
@@ -13341,9 +13657,65 @@ function createServer(config, deps = {}) {
|
|
|
13341
13657
|
};
|
|
13342
13658
|
reply.header("cache-control", "no-store").code(201).send({ pairing: payload, url: buildRelayPairingUrl(relay.appUrl, payload) });
|
|
13343
13659
|
} catch {
|
|
13660
|
+
if (pendingDeviceId) {
|
|
13661
|
+
try {
|
|
13662
|
+
deviceStore.cancelRelayPairing(pendingDeviceId);
|
|
13663
|
+
} catch {
|
|
13664
|
+
}
|
|
13665
|
+
await relay.provisioner.revokeDevice(pendingDeviceId).catch(() => {
|
|
13666
|
+
});
|
|
13667
|
+
}
|
|
13344
13668
|
reply.code(502).send({ code: "RELAY_PAIRING_FAILED", error: "could not prepare relay pairing" });
|
|
13345
13669
|
}
|
|
13346
13670
|
});
|
|
13671
|
+
app.post(
|
|
13672
|
+
"/api/v1/relay/pairing/cancel",
|
|
13673
|
+
{ bodyLimit: 8 * 1024 },
|
|
13674
|
+
async (request, reply) => {
|
|
13675
|
+
const relay = deps.relayPairing;
|
|
13676
|
+
if (!relay) {
|
|
13677
|
+
reply.code(409).send({ code: "RELAY_PAIRING_UNAVAILABLE", error: "relay pairing is not configured" });
|
|
13678
|
+
return;
|
|
13679
|
+
}
|
|
13680
|
+
const deviceId = request.body?.deviceId;
|
|
13681
|
+
if (typeof deviceId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(deviceId)) {
|
|
13682
|
+
reply.code(400).send({ code: "INVALID_RELAY_DEVICE", error: "valid relay device id is required" });
|
|
13683
|
+
return;
|
|
13684
|
+
}
|
|
13685
|
+
try {
|
|
13686
|
+
const cancellation = deviceStore.beginRelayPairingCancellation(deviceId);
|
|
13687
|
+
if (cancellation.status === "busy") {
|
|
13688
|
+
reply.code(409).send({ code: "RELAY_PAIRING_CANCEL_IN_PROGRESS", error: "relay pairing cancellation is in progress" });
|
|
13689
|
+
return;
|
|
13690
|
+
}
|
|
13691
|
+
if (cancellation.status === "missing") {
|
|
13692
|
+
reply.code(404).send({ code: "RELAY_PAIRING_NOT_FOUND", error: "relay pairing is expired or already used" });
|
|
13693
|
+
return;
|
|
13694
|
+
}
|
|
13695
|
+
try {
|
|
13696
|
+
await relay.provisioner.revokeDevice(deviceId);
|
|
13697
|
+
} catch {
|
|
13698
|
+
deviceStore.releaseRelayPairingCancellation(cancellation.reservation);
|
|
13699
|
+
throw new Error("broker revocation failed");
|
|
13700
|
+
}
|
|
13701
|
+
deviceStore.finishRelayPairingCancellation(cancellation.reservation);
|
|
13702
|
+
reply.header("cache-control", "no-store").code(204).send();
|
|
13703
|
+
} catch {
|
|
13704
|
+
reply.code(502).send({ code: "RELAY_PAIRING_CANCEL_FAILED", error: "could not cancel relay pairing" });
|
|
13705
|
+
}
|
|
13706
|
+
}
|
|
13707
|
+
);
|
|
13708
|
+
app.get("/api/v1/relay/status", async (_request, reply) => {
|
|
13709
|
+
const configured = deps.relayEnabled === true;
|
|
13710
|
+
const metrics = configured ? deps.relayStatus?.() : void 0;
|
|
13711
|
+
return reply.header("cache-control", "no-store").send({
|
|
13712
|
+
configured,
|
|
13713
|
+
pairingAvailable: configured && deps.relayPairing !== void 0,
|
|
13714
|
+
status: configured ? metrics?.status ?? "connecting" : "not-configured",
|
|
13715
|
+
activeDevices: metrics?.activeChannels ?? 0,
|
|
13716
|
+
reconnects: metrics?.reconnects ?? 0
|
|
13717
|
+
});
|
|
13718
|
+
});
|
|
13347
13719
|
app.post(
|
|
13348
13720
|
"/pairing/claim",
|
|
13349
13721
|
{ bodyLimit: 8 * 1024 },
|
|
@@ -13378,8 +13750,7 @@ function createServer(config, deps = {}) {
|
|
|
13378
13750
|
}
|
|
13379
13751
|
);
|
|
13380
13752
|
app.get("/devices", async (request, reply) => {
|
|
13381
|
-
const
|
|
13382
|
-
const currentDeviceId = presented ? deviceStore.authenticate(presented)?.id : void 0;
|
|
13753
|
+
const currentDeviceId = currentDeviceIdForRequest(request);
|
|
13383
13754
|
reply.header("cache-control", "no-store").send({
|
|
13384
13755
|
devices: deviceStore.list(),
|
|
13385
13756
|
...currentDeviceId ? { currentDeviceId } : {}
|
|
@@ -14751,8 +15122,7 @@ data: ${JSON.stringify(data)}
|
|
|
14751
15122
|
}
|
|
14752
15123
|
});
|
|
14753
15124
|
app.get("/api/v1/devices", async (request, reply) => {
|
|
14754
|
-
const
|
|
14755
|
-
const currentDeviceId = presented ? deviceStore.authenticate(presented)?.id : void 0;
|
|
15125
|
+
const currentDeviceId = currentDeviceIdForRequest(request);
|
|
14756
15126
|
reply.header("cache-control", "no-store").send({
|
|
14757
15127
|
devices: deviceStore.list(),
|
|
14758
15128
|
...currentDeviceId ? { currentDeviceId } : {}
|
|
@@ -16914,7 +17284,7 @@ function assertConfigAllowsStart(cfg) {
|
|
|
16914
17284
|
}
|
|
16915
17285
|
|
|
16916
17286
|
// src/vapid.ts
|
|
16917
|
-
import { chmodSync as
|
|
17287
|
+
import { chmodSync as chmodSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
16918
17288
|
import { join as join12 } from "path";
|
|
16919
17289
|
import webpush from "web-push";
|
|
16920
17290
|
function defaultGenerate() {
|
|
@@ -16933,7 +17303,7 @@ function resolveVapidKeys(opts) {
|
|
|
16933
17303
|
const keys = (opts.generate ?? defaultGenerate)();
|
|
16934
17304
|
ensureDataDir(opts.dataDir);
|
|
16935
17305
|
writeFileSync5(path, JSON.stringify(keys), { mode: 384 });
|
|
16936
|
-
|
|
17306
|
+
chmodSync6(path, 384);
|
|
16937
17307
|
return keys;
|
|
16938
17308
|
}
|
|
16939
17309
|
|
|
@@ -17193,7 +17563,7 @@ function createUsageRunner(opts) {
|
|
|
17193
17563
|
let child;
|
|
17194
17564
|
try {
|
|
17195
17565
|
const ptySpawn = opts.ptySpawn ?? ((file, args, options) => {
|
|
17196
|
-
ensureNodePtySpawnHelperExecutable();
|
|
17566
|
+
if (!ensureNodePtySpawnHelperExecutable()) throw new Error("node-pty spawn helper is not executable");
|
|
17197
17567
|
const pty = require15("node-pty");
|
|
17198
17568
|
return pty.spawn(file, args, options);
|
|
17199
17569
|
});
|
|
@@ -19154,10 +19524,10 @@ var CodexLatestService = class {
|
|
|
19154
19524
|
|
|
19155
19525
|
// src/providers/codex-executable.ts
|
|
19156
19526
|
import { execFile } from "child_process";
|
|
19157
|
-
import { constants as
|
|
19527
|
+
import { constants as constants4 } from "fs";
|
|
19158
19528
|
import { access as access2, chmod, copyFile, link, realpath as realpath7, rename as rename3, rm as rm2, rmdir as rmdir2, stat as stat7 } from "fs/promises";
|
|
19159
19529
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
19160
|
-
import { basename as basename5, delimiter, dirname as
|
|
19530
|
+
import { basename as basename5, delimiter, dirname as dirname8, isAbsolute as isAbsolute9, join as join14, sep as sep5 } from "path";
|
|
19161
19531
|
var CODEX_EXECUTABLE_PROBE_TIMEOUT_MS = 5e3;
|
|
19162
19532
|
var OPENAI_CODE_SIGNING_TEAM_ID = "2DC432GLL2";
|
|
19163
19533
|
var MAX_CODEX_BYTES = 1024 * 1024 * 1024;
|
|
@@ -19209,7 +19579,7 @@ async function defaultResolveExecutable(command, env) {
|
|
|
19209
19579
|
const candidates = isAbsolute9(command) || command.includes(sep5) ? [command] : (env.PATH ?? process.env.PATH ?? "").split(delimiter).filter(Boolean).map((entry) => join14(entry, command));
|
|
19210
19580
|
for (const candidate of candidates) {
|
|
19211
19581
|
try {
|
|
19212
|
-
await access2(candidate,
|
|
19582
|
+
await access2(candidate, constants4.X_OK);
|
|
19213
19583
|
return await realpath7(candidate);
|
|
19214
19584
|
} catch {
|
|
19215
19585
|
}
|
|
@@ -19267,7 +19637,7 @@ async function removeLegacyManagedCopy(dataDir) {
|
|
|
19267
19637
|
async function repairHomebrewExecutable(source, sourceStat, env, deps) {
|
|
19268
19638
|
if (!await deps.verifyOfficialSignature(source)) return false;
|
|
19269
19639
|
const nonce = randomUUID10();
|
|
19270
|
-
const directory =
|
|
19640
|
+
const directory = dirname8(source);
|
|
19271
19641
|
const name = basename5(source);
|
|
19272
19642
|
const temporary = join14(directory, `.${name}.roamcode-repair-${nonce}`);
|
|
19273
19643
|
const backup = join14(directory, `.${name}.roamcode-backup-${nonce}`);
|
|
@@ -19276,7 +19646,7 @@ async function repairHomebrewExecutable(source, sourceStat, env, deps) {
|
|
|
19276
19646
|
let committed = false;
|
|
19277
19647
|
let preserveBackup = false;
|
|
19278
19648
|
try {
|
|
19279
|
-
await copyFile(source, temporary,
|
|
19649
|
+
await copyFile(source, temporary, constants4.COPYFILE_EXCL);
|
|
19280
19650
|
await chmod(temporary, sourceStat.mode & 511);
|
|
19281
19651
|
if (!await deps.clearExtendedAttributes(temporary)) return false;
|
|
19282
19652
|
if (!await deps.verifyOfficialSignature(temporary)) return false;
|
|
@@ -19402,7 +19772,7 @@ function safeId6(value, field) {
|
|
|
19402
19772
|
return value;
|
|
19403
19773
|
}
|
|
19404
19774
|
function isLoopback3(hostname) {
|
|
19405
|
-
return hostname === "localhost" || hostname === "
|
|
19775
|
+
return hostname === "localhost" || hostname === "[::1]" || hostname === "::1" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
19406
19776
|
}
|
|
19407
19777
|
function relayConnectUrl(raw) {
|
|
19408
19778
|
let url;
|
|
@@ -20164,50 +20534,66 @@ function createRelayHostConnector(options) {
|
|
|
20164
20534
|
|
|
20165
20535
|
// src/relay-host-config.ts
|
|
20166
20536
|
import {
|
|
20167
|
-
|
|
20168
|
-
|
|
20169
|
-
|
|
20170
|
-
|
|
20171
|
-
|
|
20172
|
-
|
|
20537
|
+
closeSync as closeSync4,
|
|
20538
|
+
constants as constants6,
|
|
20539
|
+
fchmodSync as fchmodSync3,
|
|
20540
|
+
fstatSync as fstatSync3,
|
|
20541
|
+
fsyncSync as fsyncSync3,
|
|
20542
|
+
lstatSync as lstatSync4,
|
|
20543
|
+
openSync as openSync4,
|
|
20173
20544
|
readFileSync as readFileSync6,
|
|
20174
|
-
renameSync as
|
|
20175
|
-
unlinkSync as
|
|
20545
|
+
renameSync as renameSync3,
|
|
20546
|
+
unlinkSync as unlinkSync6,
|
|
20176
20547
|
writeFileSync as writeFileSync7
|
|
20177
20548
|
} from "fs";
|
|
20178
20549
|
import { randomBytes as randomBytes12 } from "crypto";
|
|
20179
|
-
import { join as join16 } from "path";
|
|
20550
|
+
import { dirname as dirname10, join as join16 } from "path";
|
|
20180
20551
|
|
|
20181
20552
|
// src/relay-identity-store.ts
|
|
20182
20553
|
import {
|
|
20183
|
-
|
|
20184
|
-
|
|
20185
|
-
constants as constants3,
|
|
20554
|
+
closeSync as closeSync3,
|
|
20555
|
+
constants as constants5,
|
|
20186
20556
|
existsSync as existsSync6,
|
|
20187
|
-
|
|
20188
|
-
|
|
20189
|
-
|
|
20190
|
-
|
|
20557
|
+
fchmodSync as fchmodSync2,
|
|
20558
|
+
fstatSync as fstatSync2,
|
|
20559
|
+
fsyncSync as fsyncSync2,
|
|
20560
|
+
linkSync as linkSync2,
|
|
20561
|
+
lstatSync as lstatSync3,
|
|
20562
|
+
openSync as openSync3,
|
|
20191
20563
|
readFileSync as readFileSync5,
|
|
20192
|
-
unlinkSync as
|
|
20564
|
+
unlinkSync as unlinkSync5,
|
|
20193
20565
|
writeFileSync as writeFileSync6
|
|
20194
20566
|
} from "fs";
|
|
20195
20567
|
import { randomBytes as randomBytes11 } from "crypto";
|
|
20196
|
-
import { join as join15 } from "path";
|
|
20568
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
20197
20569
|
var RELAY_IDENTITY_FILE = "relay-identity.json";
|
|
20570
|
+
var MAX_IDENTITY_BYTES = 32 * 1024;
|
|
20198
20571
|
function readIdentity(path) {
|
|
20199
|
-
let
|
|
20572
|
+
let before;
|
|
20200
20573
|
try {
|
|
20201
|
-
|
|
20574
|
+
before = lstatSync3(path);
|
|
20202
20575
|
} catch {
|
|
20203
20576
|
throw new Error("relay identity could not be read");
|
|
20204
20577
|
}
|
|
20205
|
-
if (!
|
|
20578
|
+
if (!before.isFile() || before.isSymbolicLink()) throw new Error("relay identity path must be a regular file");
|
|
20579
|
+
if (before.size > MAX_IDENTITY_BYTES) throw new Error("relay identity file is too large");
|
|
20580
|
+
if (typeof process.getuid === "function" && before.uid !== process.getuid()) {
|
|
20581
|
+
throw new Error("relay identity must be owned by the current user");
|
|
20582
|
+
}
|
|
20583
|
+
let descriptor;
|
|
20206
20584
|
let document;
|
|
20207
20585
|
try {
|
|
20208
|
-
|
|
20586
|
+
descriptor = openSync3(path, constants5.O_RDONLY | (constants5.O_NOFOLLOW ?? 0));
|
|
20587
|
+
const opened = fstatSync2(descriptor);
|
|
20588
|
+
if (!opened.isFile() || opened.size > MAX_IDENTITY_BYTES || opened.dev !== before.dev || opened.ino !== before.ino || typeof process.getuid === "function" && opened.uid !== process.getuid()) {
|
|
20589
|
+
throw new Error("relay identity changed while it was being opened");
|
|
20590
|
+
}
|
|
20591
|
+
fchmodSync2(descriptor, 384);
|
|
20592
|
+
document = JSON.parse(readFileSync5(descriptor, "utf8"));
|
|
20209
20593
|
} catch {
|
|
20210
20594
|
throw new Error("relay identity file is corrupt; restore it from backup instead of replacing it");
|
|
20595
|
+
} finally {
|
|
20596
|
+
if (descriptor !== void 0) closeSync3(descriptor);
|
|
20211
20597
|
}
|
|
20212
20598
|
if (document.version !== 1 || !Number.isSafeInteger(document.createdAt) || document.createdAt < 0) {
|
|
20213
20599
|
throw new Error("relay identity file has an unsupported format");
|
|
@@ -20218,34 +20604,54 @@ function readIdentity(path) {
|
|
|
20218
20604
|
} catch {
|
|
20219
20605
|
throw new Error("relay identity file contains an invalid or mismatched keypair");
|
|
20220
20606
|
}
|
|
20221
|
-
chmodSync8(path, 384);
|
|
20222
20607
|
return { identity: identity2, createdAt: document.createdAt, path, generated: false };
|
|
20223
20608
|
}
|
|
20609
|
+
function fsyncDirectory2(path) {
|
|
20610
|
+
let descriptor;
|
|
20611
|
+
try {
|
|
20612
|
+
descriptor = openSync3(path, constants5.O_RDONLY);
|
|
20613
|
+
fsyncSync2(descriptor);
|
|
20614
|
+
} catch (error) {
|
|
20615
|
+
const code = error.code;
|
|
20616
|
+
if (code !== "EINVAL" && code !== "ENOTSUP" && !(process.platform === "win32" && code === "EPERM")) {
|
|
20617
|
+
throw error;
|
|
20618
|
+
}
|
|
20619
|
+
} finally {
|
|
20620
|
+
if (descriptor !== void 0) closeSync3(descriptor);
|
|
20621
|
+
}
|
|
20622
|
+
}
|
|
20224
20623
|
function writeDurably(path, document) {
|
|
20225
20624
|
const temporary = `${path}.${randomBytes11(12).toString("hex")}.tmp`;
|
|
20226
20625
|
let descriptor;
|
|
20626
|
+
let installed = false;
|
|
20227
20627
|
try {
|
|
20228
|
-
descriptor =
|
|
20628
|
+
descriptor = openSync3(
|
|
20629
|
+
temporary,
|
|
20630
|
+
constants5.O_CREAT | constants5.O_EXCL | constants5.O_WRONLY | (constants5.O_NOFOLLOW ?? 0),
|
|
20631
|
+
384
|
|
20632
|
+
);
|
|
20633
|
+
fchmodSync2(descriptor, 384);
|
|
20229
20634
|
writeFileSync6(descriptor, `${JSON.stringify(document)}
|
|
20230
20635
|
`, "utf8");
|
|
20231
|
-
|
|
20232
|
-
|
|
20636
|
+
fsyncSync2(descriptor);
|
|
20637
|
+
closeSync3(descriptor);
|
|
20233
20638
|
descriptor = void 0;
|
|
20234
|
-
chmodSync8(temporary, 384);
|
|
20235
20639
|
try {
|
|
20236
|
-
|
|
20237
|
-
|
|
20640
|
+
linkSync2(temporary, path);
|
|
20641
|
+
installed = true;
|
|
20238
20642
|
} catch (error) {
|
|
20239
20643
|
if (error.code === "EEXIST") return false;
|
|
20240
20644
|
throw error;
|
|
20241
20645
|
}
|
|
20242
20646
|
} finally {
|
|
20243
|
-
if (descriptor !== void 0)
|
|
20647
|
+
if (descriptor !== void 0) closeSync3(descriptor);
|
|
20244
20648
|
try {
|
|
20245
|
-
|
|
20649
|
+
unlinkSync5(temporary);
|
|
20246
20650
|
} catch {
|
|
20247
20651
|
}
|
|
20248
20652
|
}
|
|
20653
|
+
if (installed) fsyncDirectory2(dirname9(path));
|
|
20654
|
+
return installed;
|
|
20249
20655
|
}
|
|
20250
20656
|
function loadOrCreateRelayIdentity(options) {
|
|
20251
20657
|
ensureDataDir(options.dataDir);
|
|
@@ -20262,10 +20668,11 @@ function loadOrCreateRelayIdentity(options) {
|
|
|
20262
20668
|
// src/relay-host-config.ts
|
|
20263
20669
|
var RELAY_HOST_CONFIG_FILE = "relay-host.json";
|
|
20264
20670
|
var MAX_CONFIG_BYTES2 = 16 * 1024;
|
|
20671
|
+
var UNSAFE_DISPLAY_TEXT2 = /[\p{Cc}\p{Zl}\p{Zp}\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
|
|
20265
20672
|
function safeHostLabel(value) {
|
|
20266
20673
|
if (typeof value !== "string") throw new Error("relay host label is required");
|
|
20267
20674
|
const normalized = value.trim().replace(/\s+/g, " ");
|
|
20268
|
-
if (!normalized || normalized.length > 80 ||
|
|
20675
|
+
if (!normalized || normalized.length > 80 || UNSAFE_DISPLAY_TEXT2.test(normalized)) {
|
|
20269
20676
|
throw new Error("relay host label must be 1-80 printable characters");
|
|
20270
20677
|
}
|
|
20271
20678
|
return normalized;
|
|
@@ -20292,7 +20699,7 @@ function relayHostConfigPath(dataDir) {
|
|
|
20292
20699
|
}
|
|
20293
20700
|
function existingConfigStat(path) {
|
|
20294
20701
|
try {
|
|
20295
|
-
return
|
|
20702
|
+
return lstatSync4(path);
|
|
20296
20703
|
} catch (error) {
|
|
20297
20704
|
if (error.code === "ENOENT") return void 0;
|
|
20298
20705
|
throw error;
|
|
@@ -20300,15 +20707,27 @@ function existingConfigStat(path) {
|
|
|
20300
20707
|
}
|
|
20301
20708
|
function readRelayHostConfig(dataDir) {
|
|
20302
20709
|
const path = relayHostConfigPath(dataDir);
|
|
20303
|
-
const
|
|
20304
|
-
if (!
|
|
20305
|
-
if (!
|
|
20306
|
-
if (
|
|
20710
|
+
const before = existingConfigStat(path);
|
|
20711
|
+
if (!before) return void 0;
|
|
20712
|
+
if (!before.isFile() || before.isSymbolicLink()) throw new Error("relay host config path must be a regular file");
|
|
20713
|
+
if (before.size > MAX_CONFIG_BYTES2) throw new Error("relay host config is too large");
|
|
20714
|
+
if (typeof process.getuid === "function" && before.uid !== process.getuid()) {
|
|
20715
|
+
throw new Error("relay host config must be owned by the current user");
|
|
20716
|
+
}
|
|
20717
|
+
let descriptor;
|
|
20307
20718
|
let value;
|
|
20308
20719
|
try {
|
|
20309
|
-
|
|
20720
|
+
descriptor = openSync4(path, constants6.O_RDONLY | (constants6.O_NOFOLLOW ?? 0));
|
|
20721
|
+
const opened = fstatSync3(descriptor);
|
|
20722
|
+
if (!opened.isFile() || opened.size > MAX_CONFIG_BYTES2 || opened.dev !== before.dev || opened.ino !== before.ino || typeof process.getuid === "function" && opened.uid !== process.getuid()) {
|
|
20723
|
+
throw new Error("relay host config changed while it was being opened");
|
|
20724
|
+
}
|
|
20725
|
+
fchmodSync3(descriptor, 384);
|
|
20726
|
+
value = JSON.parse(readFileSync6(descriptor, "utf8"));
|
|
20310
20727
|
} catch {
|
|
20311
20728
|
throw new Error("relay host config is corrupt");
|
|
20729
|
+
} finally {
|
|
20730
|
+
if (descriptor !== void 0) closeSync4(descriptor);
|
|
20312
20731
|
}
|
|
20313
20732
|
if (value.version !== 1) throw new Error("relay host config has an unsupported version");
|
|
20314
20733
|
const validated = validateCore({
|
|
@@ -20318,7 +20737,6 @@ function readRelayHostConfig(dataDir) {
|
|
|
20318
20737
|
appUrl: value.appUrl,
|
|
20319
20738
|
hostLabel: value.hostLabel
|
|
20320
20739
|
});
|
|
20321
|
-
chmodSync9(path, 384);
|
|
20322
20740
|
return { version: 1, ...validated };
|
|
20323
20741
|
}
|
|
20324
20742
|
function resolveRelayHostConfig(env, dataDir) {
|
|
@@ -20516,6 +20934,7 @@ function createLoopbackRelayHttpOpener(options) {
|
|
|
20516
20934
|
|
|
20517
20935
|
// src/relay-provision.ts
|
|
20518
20936
|
var RESPONSE_LIMIT = 16 * 1024;
|
|
20937
|
+
var UNSAFE_TERMINAL_TEXT = /[\p{Cc}\p{Zl}\p{Zp}\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
|
|
20519
20938
|
function safeId7(value, field) {
|
|
20520
20939
|
if (!/^[A-Za-z0-9._:-]{1,256}$/.test(value)) throw new Error(`invalid relay ${field}`);
|
|
20521
20940
|
return value;
|
|
@@ -20530,13 +20949,44 @@ function relayHttpOrigin(raw) {
|
|
|
20530
20949
|
}
|
|
20531
20950
|
async function boundedError(response2) {
|
|
20532
20951
|
const declared = Number(response2.headers.get("content-length"));
|
|
20533
|
-
if (Number.isFinite(declared) && declared > RESPONSE_LIMIT)
|
|
20952
|
+
if (Number.isFinite(declared) && declared > RESPONSE_LIMIT) {
|
|
20953
|
+
await discardBody(response2);
|
|
20954
|
+
return `relay returned ${response2.status}`;
|
|
20955
|
+
}
|
|
20956
|
+
if (!response2.body) return `relay returned ${response2.status}`;
|
|
20957
|
+
const chunks = [];
|
|
20958
|
+
let total = 0;
|
|
20959
|
+
const reader = response2.body.getReader();
|
|
20534
20960
|
try {
|
|
20535
|
-
|
|
20961
|
+
while (true) {
|
|
20962
|
+
const next = await reader.read();
|
|
20963
|
+
if (next.done) break;
|
|
20964
|
+
if (!next.value) continue;
|
|
20965
|
+
total += next.value.byteLength;
|
|
20966
|
+
if (total > RESPONSE_LIMIT) {
|
|
20967
|
+
try {
|
|
20968
|
+
await reader.cancel();
|
|
20969
|
+
} catch {
|
|
20970
|
+
}
|
|
20971
|
+
return `relay returned ${response2.status}`;
|
|
20972
|
+
}
|
|
20973
|
+
chunks.push(Buffer.from(next.value));
|
|
20974
|
+
}
|
|
20975
|
+
const body = Buffer.concat(chunks, total).toString("utf8");
|
|
20536
20976
|
const parsed = JSON.parse(body);
|
|
20537
|
-
|
|
20977
|
+
if (typeof parsed.error !== "string") return `relay returned ${response2.status}`;
|
|
20978
|
+
const message = parsed.error.trim().replace(/\s+/g, " ");
|
|
20979
|
+
return message && message.length <= 200 && !UNSAFE_TERMINAL_TEXT.test(message) && !/\brr[a-z]_[A-Za-z0-9_-]{20,}\b|\bsha256:[A-Za-z0-9_-]{43}\b|\bBearer\s+/i.test(message) ? message : `relay returned ${response2.status}`;
|
|
20538
20980
|
} catch {
|
|
20539
20981
|
return `relay returned ${response2.status}`;
|
|
20982
|
+
} finally {
|
|
20983
|
+
reader.releaseLock();
|
|
20984
|
+
}
|
|
20985
|
+
}
|
|
20986
|
+
async function discardBody(response2) {
|
|
20987
|
+
try {
|
|
20988
|
+
await response2.body?.cancel();
|
|
20989
|
+
} catch {
|
|
20540
20990
|
}
|
|
20541
20991
|
}
|
|
20542
20992
|
function createRelayDeviceProvisioner(options) {
|
|
@@ -20562,19 +21012,24 @@ function createRelayDeviceProvisioner(options) {
|
|
|
20562
21012
|
"content-type": "application/json"
|
|
20563
21013
|
},
|
|
20564
21014
|
body: JSON.stringify({ credentialHash, ...expiresAt === void 0 ? {} : { expiresAt } }),
|
|
21015
|
+
redirect: "error",
|
|
20565
21016
|
...requestSignal ? { signal: requestSignal } : {}
|
|
20566
21017
|
});
|
|
20567
21018
|
if (!response2.ok) throw new Error(`could not provision relay device: ${await boundedError(response2)}`);
|
|
21019
|
+
await discardBody(response2);
|
|
20568
21020
|
},
|
|
20569
21021
|
async revokeDevice(deviceId) {
|
|
20570
21022
|
const requestSignal = signal();
|
|
20571
21023
|
const response2 = await request(endpoint(deviceId), {
|
|
20572
21024
|
method: "DELETE",
|
|
20573
21025
|
headers: { authorization: `Bearer ${options.hostCredential}` },
|
|
21026
|
+
redirect: "error",
|
|
20574
21027
|
...requestSignal ? { signal: requestSignal } : {}
|
|
20575
21028
|
});
|
|
20576
|
-
if (!response2.ok && response2.status !== 404)
|
|
21029
|
+
if (!response2.ok && response2.status !== 404) {
|
|
20577
21030
|
throw new Error(`could not revoke relay device: ${await boundedError(response2)}`);
|
|
21031
|
+
}
|
|
21032
|
+
await discardBody(response2);
|
|
20578
21033
|
}
|
|
20579
21034
|
};
|
|
20580
21035
|
}
|
|
@@ -20937,6 +21392,11 @@ async function startServer(env = process.env) {
|
|
|
20937
21392
|
codexThreadResolver: (cwd) => new CodexThreadResolver({ inventory: createCodexThreadInventory(codexRpc, { cwd }) }),
|
|
20938
21393
|
disposeProviders: () => codexClient.stop(),
|
|
20939
21394
|
relayEnabled: relayConfig !== void 0,
|
|
21395
|
+
relayStatus: () => relayHost?.metrics() ?? {
|
|
21396
|
+
status: relayConfig ? "connecting" : "stopped",
|
|
21397
|
+
activeChannels: 0,
|
|
21398
|
+
reconnects: 0
|
|
21399
|
+
},
|
|
20940
21400
|
relayPairing: relayConfig?.appUrl && relayProvisioner ? {
|
|
20941
21401
|
appUrl: relayConfig.appUrl,
|
|
20942
21402
|
label: relayConfig.hostLabel,
|