@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/start.js CHANGED
@@ -595,9 +595,27 @@ var RateLimiter = class {
595
595
  };
596
596
 
597
597
  // src/data-dir.ts
598
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
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, "token");
619
- writeFileSync(tokenPath, token + "\n", { mode: 384 });
620
- chmodSync(tokenPath, 384);
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 resolveAccessToken(opts) {
623
- if (opts.configured) return { token: opts.configured, generated: false };
624
- const tokenPath = join2(opts.dataDir, "token");
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
- const existing = readFileSync(tokenPath, "utf8").trim();
627
- if (existing) return { token: existing, generated: false };
628
- } catch {
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
- const token = (opts.generate ?? generateAccessToken)();
631
- persistAccessToken(opts.dataDir, token);
632
- return { token, generated: true };
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 || /[\p{Cc}\p{Zl}\p{Zp}]/u.test(normalized)) return void 0;
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 dirname3, join as join6 } from "path";
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 chmodSync3,
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 dirname2, join as join5, relative, resolve as resolve5 } from "path";
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 as chmodSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
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.1.0" ? "1.1.0".replace(/^v/, "") : packageVersion();
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 dirname3(fileURLToPath(import.meta.url));
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 unlinkSync2, readdirSync as readdirSync2 } from "fs";
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 chmodSync4, existsSync as existsSync5 } from "fs";
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 dirname4, join as join7, resolve as resolve6 } from "path";
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(dirname4(resolveNodePty()), "..");
3800
+ const packageRoot = resolve6(dirname5(resolveNodePty()), "..");
3555
3801
  const helper = join7(packageRoot, "prebuilds", `${platform2}-${arch}`, "spawn-helper");
3556
- if (existsSync5(helper)) chmodSync4(helper, 493);
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
- unlinkSync2(join9(dir, name));
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 chmodSync5, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
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
- unlinkSync3(path);
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
- chmodSync5(path, 384);
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 dirname5, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve7 } from "path";
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(dirname5(lexical)).catch(() => void 0);
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 = dirname5(commonDir);
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 dirname6, isAbsolute as isAbsolute5, join as join10, relative as relative4, resolve as resolve9 } from "path";
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(dirname6(output), { recursive: true, mode: 448 });
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(dirname6(target), { recursive: true, mode: 448 });
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 chmodSync6 } from "fs";
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:") chmodSync6(options.dbPath, 384);
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 presented = extractBearerToken(request.headers.authorization);
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 presented = extractBearerToken(request.headers.authorization);
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 chmodSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
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
- chmodSync7(path, 384);
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 constants2 } from "fs";
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 dirname7, isAbsolute as isAbsolute9, join as join14, sep as sep5 } from "path";
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, constants2.X_OK);
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 = dirname7(source);
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, constants2.COPYFILE_EXCL);
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 === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
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
- chmodSync as chmodSync9,
20168
- closeSync as closeSync3,
20169
- constants as constants4,
20170
- fsyncSync as fsyncSync2,
20171
- lstatSync as lstatSync3,
20172
- openSync as openSync3,
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 renameSync2,
20175
- unlinkSync as unlinkSync5,
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
- chmodSync as chmodSync8,
20184
- closeSync as closeSync2,
20185
- constants as constants3,
20554
+ closeSync as closeSync3,
20555
+ constants as constants5,
20186
20556
  existsSync as existsSync6,
20187
- fsyncSync,
20188
- linkSync,
20189
- lstatSync as lstatSync2,
20190
- openSync as openSync2,
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 unlinkSync4,
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 stat8;
20572
+ let before;
20200
20573
  try {
20201
- stat8 = lstatSync2(path);
20574
+ before = lstatSync3(path);
20202
20575
  } catch {
20203
20576
  throw new Error("relay identity could not be read");
20204
20577
  }
20205
- if (!stat8.isFile() || stat8.isSymbolicLink()) throw new Error("relay identity path must be a regular file");
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
- document = JSON.parse(readFileSync5(path, "utf8"));
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 = openSync2(temporary, constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY, 384);
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
- fsyncSync(descriptor);
20232
- closeSync2(descriptor);
20636
+ fsyncSync2(descriptor);
20637
+ closeSync3(descriptor);
20233
20638
  descriptor = void 0;
20234
- chmodSync8(temporary, 384);
20235
20639
  try {
20236
- linkSync(temporary, path);
20237
- return true;
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) closeSync2(descriptor);
20647
+ if (descriptor !== void 0) closeSync3(descriptor);
20244
20648
  try {
20245
- unlinkSync4(temporary);
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 || /[\p{Cc}\p{Zl}\p{Zp}]/u.test(normalized)) {
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 lstatSync3(path);
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 stat8 = existingConfigStat(path);
20304
- if (!stat8) return void 0;
20305
- if (!stat8.isFile() || stat8.isSymbolicLink()) throw new Error("relay host config path must be a regular file");
20306
- if (stat8.size > MAX_CONFIG_BYTES2) throw new Error("relay host config is too large");
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
- value = JSON.parse(readFileSync6(path, "utf8"));
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) return `relay returned ${response2.status}`;
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
- const body = (await response2.text()).slice(0, RESPONSE_LIMIT);
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
- return typeof parsed.error === "string" && parsed.error.length <= 200 ? parsed.error : `relay returned ${response2.status}`;
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,