@dotobokuri/fleet-console 1.54.0 → 1.55.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.
Files changed (71) hide show
  1. package/AGENTS.md +1 -1
  2. package/dist/access-protocol.d.ts +58 -0
  3. package/dist/access-protocol.mjs +165 -0
  4. package/dist/cli.mjs +1207 -716
  5. package/dist/client/assets/{_baseUniq-B2XggV1b.js → _baseUniq-Cp0TuK00.js} +1 -1
  6. package/dist/client/assets/{arc-CPqvR_Wl.js → arc-Cwtz1wxV.js} +1 -1
  7. package/dist/client/assets/{architectureDiagram-Q4EWVU46-BczXSpod.js → architectureDiagram-Q4EWVU46-f2trvyEV.js} +1 -1
  8. package/dist/client/assets/{blockDiagram-DXYQGD6D-BPtW_zZR.js → blockDiagram-DXYQGD6D-BhEa9Mh6.js} +1 -1
  9. package/dist/client/assets/{c4Diagram-AHTNJAMY-Cry2oyLL.js → c4Diagram-AHTNJAMY-CuqbJbZK.js} +1 -1
  10. package/dist/client/assets/channel-D9kPbZ6w.js +1 -0
  11. package/dist/client/assets/{chunk-4BX2VUAB-DKkKt4m0.js → chunk-4BX2VUAB-Vx63wo1i.js} +1 -1
  12. package/dist/client/assets/{chunk-4TB4RGXK-DKGkDbqm.js → chunk-4TB4RGXK-DHRtIlLJ.js} +1 -1
  13. package/dist/client/assets/{chunk-55IACEB6-DawsVr8O.js → chunk-55IACEB6-ChTm2kCK.js} +1 -1
  14. package/dist/client/assets/{chunk-EDXVE4YY-tS8vRy63.js → chunk-EDXVE4YY-C_BQtaY5.js} +1 -1
  15. package/dist/client/assets/{chunk-FMBD7UC4-B0jQQkjI.js → chunk-FMBD7UC4-BPV9Mw6e.js} +1 -1
  16. package/dist/client/assets/{chunk-OYMX7WX6-FZQSRkEo.js → chunk-OYMX7WX6-ya-VyPHj.js} +1 -1
  17. package/dist/client/assets/{chunk-QZHKN3VN-Bq1WBmr8.js → chunk-QZHKN3VN-CLiSGdJ6.js} +1 -1
  18. package/dist/client/assets/{chunk-YZCP3GAM-CNWIYKHc.js → chunk-YZCP3GAM-DasGOM9R.js} +1 -1
  19. package/dist/client/assets/classDiagram-6PBFFD2Q-B1zyip5T.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-HSJHXN6E-B1zyip5T.js +1 -0
  21. package/dist/client/assets/clone-x5mGq45q.js +1 -0
  22. package/dist/client/assets/{cose-bilkent-S5V4N54A-CmOaWS4P.js → cose-bilkent-S5V4N54A-C11PcGWz.js} +1 -1
  23. package/dist/client/assets/{dagre-KV5264BT-D2gJaf1o.js → dagre-KV5264BT-BGiNFCUB.js} +1 -1
  24. package/dist/client/assets/{diagram-5BDNPKRD-DH-M3VUk.js → diagram-5BDNPKRD-CSxK9ZtI.js} +1 -1
  25. package/dist/client/assets/{diagram-G4DWMVQ6-169IB8uN.js → diagram-G4DWMVQ6-COZtgx_6.js} +1 -1
  26. package/dist/client/assets/{diagram-MMDJMWI5-D1OVxCyQ.js → diagram-MMDJMWI5-4-mD0i99.js} +1 -1
  27. package/dist/client/assets/{diagram-TYMM5635-CkiS1glz.js → diagram-TYMM5635-BNX8i0L7.js} +1 -1
  28. package/dist/client/assets/{erDiagram-SMLLAGMA-CZUp7ncJ.js → erDiagram-SMLLAGMA-1a9K7CGs.js} +1 -1
  29. package/dist/client/assets/{flowDiagram-DWJPFMVM-9SIhR3aC.js → flowDiagram-DWJPFMVM-B7BZTT0P.js} +1 -1
  30. package/dist/client/assets/{ganttDiagram-T4ZO3ILL-CA4smcv3.js → ganttDiagram-T4ZO3ILL-FeCgSTyp.js} +1 -1
  31. package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-DF8CSmoM.js → gitGraphDiagram-UUTBAWPF-ItC4KjyQ.js} +1 -1
  32. package/dist/client/assets/{graph-Bc_ceTI8.js → graph-C0wWu6sL.js} +1 -1
  33. package/dist/client/assets/{index-NNL49IzO.css → index-B0961B8M.css} +1 -1
  34. package/dist/client/assets/index-DEW90Ngb.js +479 -0
  35. package/dist/client/assets/{infoDiagram-42DDH7IO-BC5KSucM.js → infoDiagram-42DDH7IO-5ub69t3k.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-JJIJclQD.js → ishikawaDiagram-UXIWVN3A-BEvDRmdo.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-VCZTEJTY-ERzBaHVf.js → journeyDiagram-VCZTEJTY-DUxQ-ZUX.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-6JOO6SKY-KvnzNd3g.js → kanban-definition-6JOO6SKY-BiPkJbpE.js} +1 -1
  39. package/dist/client/assets/{layout-DJbQbTix.js → layout-Bhtd-qjm.js} +1 -1
  40. package/dist/client/assets/{linear-BzBkH5Aq.js → linear-BT8b8xym.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-CAqwSbV2.js → mermaid.core-BlTpil8e.js} +4 -4
  42. package/dist/client/assets/{min-giSfwdiX.js → min-DrnMPIwV.js} +1 -1
  43. package/dist/client/assets/{mindmap-definition-QFDTVHPH-BH0wnMRm.js → mindmap-definition-QFDTVHPH-uQH7Nv45.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-DEJITSTG-WOHMlPDe.js → pieDiagram-DEJITSTG-MOetI488.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-34T5L4WZ-DcIpvIOj.js → quadrantDiagram-34T5L4WZ-C-jeoH4D.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-MS252O5E-C99uWiwG.js → requirementDiagram-MS252O5E-C7cKIVp5.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-XADWPNL6-czWBcwBu.js → sankeyDiagram-XADWPNL6-Dcv2A0AY.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-FGHM5R23-CjtCPppG.js → sequenceDiagram-FGHM5R23-CXO-xPvA.js} +1 -1
  49. package/dist/client/assets/{stateDiagram-FHFEXIEX-D0UKF8ze.js → stateDiagram-FHFEXIEX-GKivP2tt.js} +1 -1
  50. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-BfnEUJRo.js +1 -0
  51. package/dist/client/assets/{timeline-definition-GMOUNBTQ-3isWQZNU.js → timeline-definition-GMOUNBTQ-DLJSUfwG.js} +1 -1
  52. package/dist/client/assets/{vennDiagram-DHZGUBPP-C_LF2g8l.js → vennDiagram-DHZGUBPP-DFihaprP.js} +1 -1
  53. package/dist/client/assets/{wardley-RL74JXVD-Bw0Y4M6p.js → wardley-RL74JXVD-BBV41M5Z.js} +1 -1
  54. package/dist/client/assets/{wardleyDiagram-NUSXRM2D-B6e3mxQI.js → wardleyDiagram-NUSXRM2D-B5J5AiY-.js} +1 -1
  55. package/dist/client/assets/{xychartDiagram-5P7HB3ND-DjlZXnLg.js → xychartDiagram-5P7HB3ND-D1NFEt18.js} +1 -1
  56. package/dist/client/index.html +2 -2
  57. package/dist/fleet-plugins/file-explorer/routes.mjs +13 -9
  58. package/dist/fleet-plugins/ledger/routes.mjs +6 -2
  59. package/dist/fleet-plugins/quota/routes.mjs +22 -18
  60. package/dist/fleet-plugins/repository/routes.mjs +17 -13
  61. package/dist/fleet-plugins/scuttlebutt/routes.mjs +13 -14
  62. package/dist/fleet-plugins/skills/routes.mjs +14 -10
  63. package/dist/fleet-plugins/terminal/routes.mjs +354 -314
  64. package/dist/fleet.mjs +3950 -3486
  65. package/package.json +5 -1
  66. package/dist/client/assets/channel-D5C5uwWd.js +0 -1
  67. package/dist/client/assets/classDiagram-6PBFFD2Q-Cv4Bxw_r.js +0 -1
  68. package/dist/client/assets/classDiagram-v2-HSJHXN6E-Cv4Bxw_r.js +0 -1
  69. package/dist/client/assets/clone-C7dthYDe.js +0 -1
  70. package/dist/client/assets/index-kTAFpDLu.js +0 -479
  71. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-BdRUP2Pp.js +0 -1
package/dist/cli.mjs CHANGED
@@ -8,7 +8,7 @@ import { execFile, spawn, execFileSync } from 'node:child_process';
8
8
  import process4 from 'node:process';
9
9
  import * as os4 from 'node:os';
10
10
  import os4__default, { tmpdir } from 'node:os';
11
- import crypto6, { createHash, randomUUID } from 'node:crypto';
11
+ import crypto5, { createHash, randomUUID } from 'node:crypto';
12
12
  import { builtinModules, createRequire } from 'node:module';
13
13
  import http from 'node:http';
14
14
  import https from 'node:https';
@@ -224,7 +224,7 @@ var LOCK_FILE_MODE = 384;
224
224
  function createConsoleLock(deps = {}) {
225
225
  const fsImpl = deps.fs ?? fs9__default;
226
226
  const now = deps.now ?? Date.now;
227
- const randomToken = deps.randomToken ?? (() => crypto6.randomBytes(32).toString("base64url"));
227
+ const randomToken = deps.randomToken ?? (() => crypto5.randomBytes(32).toString("base64url"));
228
228
  const hostname5 = deps.hostname ?? (() => "127.0.0.1");
229
229
  function ensureLockDir(dir) {
230
230
  fsImpl.mkdirSync(dir, { recursive: true, mode: LOCK_DIR_MODE });
@@ -2619,7 +2619,7 @@ async function writeWikiEntryAtTarget(entry, target, paths) {
2619
2619
  throw new Error("wiki patch body id must match target filename");
2620
2620
  }
2621
2621
  await mkdir(path18__default.dirname(absoluteTarget), { recursive: true });
2622
- await writeMarkdownAtomic(absoluteTarget, serializeWikiEntry(entry), paths);
2622
+ await writeAtomic(absoluteTarget, serializeWikiEntry(entry), paths);
2623
2623
  return path18__default.join("wiki", relativeTarget).replaceAll(path18__default.sep, "/");
2624
2624
  }
2625
2625
  async function writeRawSourceEntry(entry, paths) {
@@ -2639,7 +2639,7 @@ async function writeRawSourceEntry(entry, paths) {
2639
2639
  },
2640
2640
  entry.content
2641
2641
  );
2642
- await writeMarkdownAtomic(path18__default.join(paths.root, relativePath2), content, paths);
2642
+ await writeAtomic(path18__default.join(paths.root, relativePath2), content, paths);
2643
2643
  return relativePath2;
2644
2644
  }
2645
2645
  async function readRawSourceEntry(rawSourceRef, paths) {
@@ -2687,7 +2687,7 @@ async function rebuildIndex(paths) {
2687
2687
  entryPathMap[item.entry.id] = item.path;
2688
2688
  }
2689
2689
  await writeJsonAtomic(paths.indexFile, nextIndex, paths);
2690
- await writeMarkdownAtomic(getIndexMarkdownFile(paths), renderIndexMarkdown(entries.map((item) => item.entry), entryPathMap), paths);
2690
+ await writeAtomic(getIndexMarkdownFile(paths), renderIndexMarkdown(entries.map((item) => item.entry), entryPathMap), paths);
2691
2691
  await appendLog(paths, "index rebuilt", { entry_count: entries.length });
2692
2692
  return nextIndex;
2693
2693
  }
@@ -2767,7 +2767,7 @@ async function readPatchFile(filePath) {
2767
2767
  return readFile(filePath, "utf8");
2768
2768
  }
2769
2769
  async function writePatchFile(filePath, content, paths) {
2770
- await writeMarkdownAtomic(filePath, content, paths);
2770
+ await writeAtomic(filePath, content, paths);
2771
2771
  }
2772
2772
  async function readJsonFile2(filePath) {
2773
2773
  const raw = await readFile(filePath, "utf8");
@@ -2831,7 +2831,7 @@ function stripLeadingFrontmatter(body2) {
2831
2831
  return stripped ? next : body2;
2832
2832
  }
2833
2833
  function computeContentHash(content) {
2834
- return crypto6.createHash("sha256").update(content, "utf8").digest("hex").slice(0, 8);
2834
+ return crypto5.createHash("sha256").update(content, "utf8").digest("hex").slice(0, 8);
2835
2835
  }
2836
2836
  function assertWithinRawDir(absolutePath, paths) {
2837
2837
  const relative = path18__default.relative(paths.rawDir, absolutePath);
@@ -3034,9 +3034,6 @@ function isFrontmatterBlock(rawFrontmatter) {
3034
3034
  }
3035
3035
  return keys.has("id") && keys.has("title");
3036
3036
  }
3037
- async function writeMarkdownAtomic(filePath, content, paths) {
3038
- await writeAtomic(filePath, content, paths);
3039
- }
3040
3037
  async function writeJsonAtomic(filePath, value, paths) {
3041
3038
  await writeAtomic(filePath, JSON.stringify(value, null, 2), paths);
3042
3039
  }
@@ -3044,7 +3041,7 @@ async function writeAtomic(filePath, content, paths) {
3044
3041
  await ensureMemoryRoot(paths);
3045
3042
  const tempPath = path18__default.join(
3046
3043
  path18__default.dirname(filePath),
3047
- `.tmp-${process.pid}-${Date.now()}-${crypto6.randomUUID()}-${os4__default.hostname()}`
3044
+ `.tmp-${process.pid}-${Date.now()}-${crypto5.randomUUID()}-${os4__default.hostname()}`
3048
3045
  );
3049
3046
  await writeFile(tempPath, content, "utf8");
3050
3047
  await rename(tempPath, filePath);
@@ -9011,7 +9008,7 @@ function parseOptionalIsoDate(value) {
9011
9008
  return Number.isFinite(timestamp) ? timestamp : null;
9012
9009
  }
9013
9010
  function hashBodyForComparison(body2) {
9014
- return crypto6.createHash("sha256").update(body2.replace(/\s+/g, " ").trim(), "utf8").digest("hex").slice(0, 8);
9011
+ return crypto5.createHash("sha256").update(body2.replace(/\s+/g, " ").trim(), "utf8").digest("hex").slice(0, 8);
9015
9012
  }
9016
9013
  function hasConflictingStatuses(entries) {
9017
9014
  const statuses = new Set(entries.map((entry) => entry.status).filter((status) => status !== "unknown"));
@@ -12050,7 +12047,8 @@ var DESKTOP_FULLSCREEN_API_CATALOG = [{
12050
12047
  path: DESKTOP_FULLSCREEN_PATH,
12051
12048
  summary: "Update the ephemeral Desktop native fullscreen snapshot.",
12052
12049
  category: "Desktop",
12053
- gate: "origin-strict"
12050
+ gate: "origin-strict",
12051
+ transport: "http"
12054
12052
  }];
12055
12053
  function createDesktopFullscreenRouter(deps) {
12056
12054
  return async ({ req, res, pathname }) => {
@@ -12095,14 +12093,16 @@ var DESKTOP_SHELL_API_CATALOG = [
12095
12093
  path: DESKTOP_SHELL_PATH,
12096
12094
  summary: "Read where the Desktop that published it can send this window home; every other viewer reads nothing.",
12097
12095
  category: "Desktop",
12098
- gate: "loopback"
12096
+ gate: "loopback",
12097
+ transport: "http"
12099
12098
  },
12100
12099
  {
12101
12100
  method: "PUT",
12102
12101
  path: DESKTOP_SHELL_PATH,
12103
12102
  summary: "Publish the console the attached Desktop launched, so this window can go back to it.",
12104
12103
  category: "Desktop",
12105
- gate: "origin-strict"
12104
+ gate: "origin-strict",
12105
+ transport: "http"
12106
12106
  }
12107
12107
  ];
12108
12108
  function createDesktopShellRouter(deps) {
@@ -12148,14 +12148,16 @@ var DESKTOP_THEME_API_CATALOG = [
12148
12148
  path: DESKTOP_THEME_PATH,
12149
12149
  summary: "Get the Console-owned Desktop title bar theme.",
12150
12150
  category: "Desktop",
12151
- gate: "origin-strict"
12151
+ gate: "origin-strict",
12152
+ transport: "http"
12152
12153
  },
12153
12154
  {
12154
12155
  method: "GET",
12155
12156
  path: DESKTOP_THEME_EVENTS_PATH,
12156
12157
  summary: "Stream server-confirmed Desktop title bar theme changes.",
12157
12158
  category: "Desktop",
12158
- gate: "origin-strict"
12159
+ gate: "origin-strict",
12160
+ transport: "sse"
12159
12161
  }
12160
12162
  ];
12161
12163
  function createDesktopThemeRouter(deps) {
@@ -12178,13 +12180,36 @@ function createDesktopThemeRouter(deps) {
12178
12180
  return true;
12179
12181
  };
12180
12182
  }
12183
+ var REMOTE_AUTO_PORT_MIN = 49152;
12184
+ var REMOTE_AUTO_PORT_MAX = 65535;
12185
+ var REMOTE_AUTO_PORT_ATTEMPTS = 12;
12186
+ function effectiveRemoteAccessAdvertisedTuple(settings2) {
12187
+ return settings2.publicEndpointEnabled ? { host: settings2.advertisedHost, port: settings2.advertisedPort.value } : { host: settings2.listenAddress, port: settings2.listenPort.value };
12188
+ }
12181
12189
  var REMOTE_BIND_HOST = /^(?:\d{1,3}(?:\.\d{1,3}){3}|[A-Za-z0-9](?:[A-Za-z0-9._-]{0,252}[A-Za-z0-9])?)$/u;
12182
12190
  function sanitizeRemoteAccessSettings(value) {
12183
- if (!isRecord3(value)) return void 0;
12184
- const enabled = typeof value.enabled === "boolean" ? value.enabled : void 0;
12185
- const bindHost = isValidRemoteBindHost(value.bindHost) ? canonicalizeRemoteBindHost(value.bindHost) : void 0;
12186
- if (enabled === void 0 && bindHost === void 0) return void 0;
12187
- return { ...enabled !== void 0 ? { enabled } : {}, ...bindHost !== void 0 ? { bindHost } : {} };
12191
+ if (!isRecord3(value) || "bindHost" in value) return void 0;
12192
+ if (typeof value.enabled !== "boolean") return void 0;
12193
+ const listenAddress = value.listenAddress === "" ? "" : isValidRemoteBindHost(value.listenAddress) ? canonicalizeRemoteBindHost(value.listenAddress) : null;
12194
+ const advertisedHost = value.advertisedHost === "" ? "" : isValidRemoteAdvertisedHost(value.advertisedHost) ? canonicalizeRemoteBindHost(value.advertisedHost) : null;
12195
+ if (listenAddress === null || advertisedHost === null) return void 0;
12196
+ const listenPort = sanitizeRemotePortSetting(value.listenPort);
12197
+ const advertisedPort = sanitizeRemotePortSetting(value.advertisedPort);
12198
+ if (!listenPort || !advertisedPort) return void 0;
12199
+ const acknowledgment = sanitizeAcknowledgment(value.acknowledgment);
12200
+ const explicitPublicEndpointEnabled = typeof value.publicEndpointEnabled === "boolean" ? value.publicEndpointEnabled : null;
12201
+ if (explicitPublicEndpointEnabled === true && value.acknowledgment !== null && !acknowledgment) return void 0;
12202
+ const publicEndpointEnabled = explicitPublicEndpointEnabled ?? acknowledgmentMatches({ listenAddress, advertisedHost, listenPort, advertisedPort }, acknowledgment);
12203
+ if (value.enabled && (listenAddress === "" || publicEndpointEnabled && (advertisedHost === "" || !acknowledgmentMatches({ listenAddress, advertisedHost, listenPort, advertisedPort }, acknowledgment)))) return void 0;
12204
+ return {
12205
+ enabled: value.enabled,
12206
+ publicEndpointEnabled,
12207
+ listenAddress,
12208
+ advertisedHost,
12209
+ listenPort,
12210
+ advertisedPort,
12211
+ acknowledgment: publicEndpointEnabled ? acknowledgment : null
12212
+ };
12188
12213
  }
12189
12214
  var UNUSABLE_REMOTE_BIND_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "0.0.0.0"]);
12190
12215
  function canonicalizeRemoteBindHost(value) {
@@ -12207,7 +12232,9 @@ var MAX_FEATURE_TOUR_KEY_LENGTH = 64;
12207
12232
  function createConsoleSettingsStore(deps = {}) {
12208
12233
  const paths = deps.paths ?? createConsoleDataPaths();
12209
12234
  const createStore = deps.createStore ?? createDurableJsonStore;
12210
- return createStore({
12235
+ const randomInt = deps.randomInt ?? crypto5.randomInt;
12236
+ const readFile7 = deps.readFile ?? ((file2) => fs9__default.readFileSync(file2, "utf8"));
12237
+ const base = createStore({
12211
12238
  filePath: paths.settingsFile,
12212
12239
  lockDir: path18__default.join(paths.dir, SETTINGS_LOCK_DIR_NAME),
12213
12240
  lockOwnerFileName: SETTINGS_LOCK_OWNER_FILE_NAME,
@@ -12216,6 +12243,31 @@ function createConsoleSettingsStore(deps = {}) {
12216
12243
  sensitivity: "sensitive",
12217
12244
  tempCleanupPrefix: SETTINGS_TEMP_PREFIX
12218
12245
  });
12246
+ let initialized = false;
12247
+ function initialize() {
12248
+ if (initialized) return base.load();
12249
+ initialized = true;
12250
+ const current = base.load();
12251
+ const raw = readRawSettings(paths.settingsFile, readFile7);
12252
+ const legacy = readLegacyRemoteAccess(raw, paths.dir, readFile7, randomInt);
12253
+ const remoteAccess = current.general?.remoteAccess ?? legacy ?? createDefaultRemoteAccess(randomInt);
12254
+ const next = { ...current, general: { ...current.general, remoteAccess } };
12255
+ if (!current.general?.remoteAccess || legacy || currentRemoteAccessNeedsMigration(raw)) base.save(next);
12256
+ return next;
12257
+ }
12258
+ return {
12259
+ path: base.path,
12260
+ load: initialize,
12261
+ save(data) {
12262
+ initialized = true;
12263
+ base.save(data);
12264
+ },
12265
+ update(mutate) {
12266
+ const next = mutate(initialize());
12267
+ base.save(next);
12268
+ return base.load();
12269
+ }
12270
+ };
12219
12271
  }
12220
12272
  function sanitizeConsoleSettingsData(value) {
12221
12273
  if (!isRecord3(value)) return emptyConsoleSettingsData();
@@ -12287,6 +12339,56 @@ function readConsolePluginSettings(value) {
12287
12339
  }
12288
12340
  return result;
12289
12341
  }
12342
+ function isValidRemoteAdvertisedHost(value) {
12343
+ return isValidRemoteBindHost(value);
12344
+ }
12345
+ function sanitizeRemotePortSetting(value) {
12346
+ if (!isRecord3(value) || value.mode !== "auto" && value.mode !== "custom" || !isBindablePort(value.value)) return null;
12347
+ if (value.mode === "auto" && (value.value < REMOTE_AUTO_PORT_MIN || value.value > REMOTE_AUTO_PORT_MAX)) return null;
12348
+ return { mode: value.mode, value: value.value };
12349
+ }
12350
+ function sanitizeAcknowledgment(value) {
12351
+ if (value === null) return null;
12352
+ if (!isRecord3(value) || value.version !== 1 || !isValidRemoteBindHost(value.listenAddress) || !isValidRemoteAdvertisedHost(value.advertisedHost) || !isBindablePort(value.listenPort) || !isBindablePort(value.advertisedPort)) return null;
12353
+ return { version: 1, listenAddress: canonicalizeRemoteBindHost(value.listenAddress), listenPort: value.listenPort, advertisedHost: canonicalizeRemoteBindHost(value.advertisedHost), advertisedPort: value.advertisedPort };
12354
+ }
12355
+ function acknowledgmentMatches(settings2, acknowledgment) {
12356
+ return acknowledgment !== null && acknowledgment.version === 1 && acknowledgment.listenAddress === settings2.listenAddress && acknowledgment.listenPort === settings2.listenPort.value && acknowledgment.advertisedHost === settings2.advertisedHost && acknowledgment.advertisedPort === settings2.advertisedPort.value;
12357
+ }
12358
+ function createDefaultRemoteAccess(randomInt) {
12359
+ return { enabled: false, publicEndpointEnabled: false, listenAddress: "", advertisedHost: "", listenPort: { mode: "auto", value: randomAutoPort(randomInt) }, advertisedPort: { mode: "auto", value: randomAutoPort(randomInt) }, acknowledgment: null };
12360
+ }
12361
+ function randomAutoPort(randomInt) {
12362
+ return randomInt(REMOTE_AUTO_PORT_MIN, REMOTE_AUTO_PORT_MAX + 1);
12363
+ }
12364
+ function isBindablePort(value) {
12365
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= 65535;
12366
+ }
12367
+ function readRawSettings(file2, readFile7) {
12368
+ try {
12369
+ return JSON.parse(readFile7(file2));
12370
+ } catch {
12371
+ return null;
12372
+ }
12373
+ }
12374
+ function currentRemoteAccessNeedsMigration(raw) {
12375
+ return isRecord3(raw) && raw.version === 1 && isRecord3(raw.general) && isRecord3(raw.general.remoteAccess) && !("bindHost" in raw.general.remoteAccess) && typeof raw.general.remoteAccess.publicEndpointEnabled !== "boolean";
12376
+ }
12377
+ function readLegacyRemoteAccess(raw, consoleDir, readFile7, randomInt) {
12378
+ if (!isRecord3(raw) || raw.version !== 1 || !isRecord3(raw.general) || !isRecord3(raw.general.remoteAccess)) return null;
12379
+ const legacy = raw.general.remoteAccess;
12380
+ if (!isValidRemoteBindHost(legacy.bindHost)) return null;
12381
+ const host = canonicalizeRemoteBindHost(legacy.bindHost);
12382
+ let port = null;
12383
+ try {
12384
+ const endpoint = JSON.parse(readFile7(path18__default.join(consoleDir, "remote", "listener.json")));
12385
+ if (isRecord3(endpoint) && endpoint.version === 1 && isBindablePort(endpoint.port)) port = endpoint.port;
12386
+ } catch {
12387
+ }
12388
+ const listenPort = port === null ? { mode: "auto", value: randomAutoPort(randomInt) } : { mode: "custom", value: port };
12389
+ const advertisedPort = port === null ? { mode: "auto", value: randomAutoPort(randomInt) } : { mode: "custom", value: port };
12390
+ return { enabled: legacy.enabled === true, publicEndpointEnabled: false, listenAddress: host, advertisedHost: host, listenPort, advertisedPort, acknowledgment: null };
12391
+ }
12290
12392
  function isRecord3(value) {
12291
12393
  return typeof value === "object" && value !== null && !Array.isArray(value);
12292
12394
  }
@@ -12310,14 +12412,16 @@ var GLOBAL_SETTINGS_API_CATALOG = [
12310
12412
  path: "/api/v1/settings/global",
12311
12413
  summary: "Get the global console settings status.",
12312
12414
  category: "Settings",
12313
- gate: "loopback"
12415
+ gate: "loopback",
12416
+ transport: "http"
12314
12417
  },
12315
12418
  {
12316
12419
  method: "PUT",
12317
12420
  path: "/api/v1/settings/global",
12318
12421
  summary: "Save the global console settings.",
12319
12422
  category: "Settings",
12320
- gate: "origin-write"
12423
+ gate: "origin-write",
12424
+ transport: "http"
12321
12425
  }
12322
12426
  ];
12323
12427
  function createGlobalSettingsRouter(deps) {
@@ -12325,7 +12429,7 @@ function createGlobalSettingsRouter(deps) {
12325
12429
  const { req, res, pathname } = context;
12326
12430
  if (pathname === "/api/v1/settings/global") {
12327
12431
  if (req.method === "GET") {
12328
- deps.writeJson(res, 200, buildGlobalSettingsState(deps.consoleSettingsStore));
12432
+ deps.writeJson(res, 200, withRemoteAccessVisibility(buildGlobalSettingsState(deps.consoleSettingsStore), req, deps));
12329
12433
  return true;
12330
12434
  }
12331
12435
  if (req.method === "PUT") {
@@ -12367,6 +12471,10 @@ async function mutateGlobalSettings(req, res, deps) {
12367
12471
  deps.writeJson(res, 400, { error: "invalid_language" });
12368
12472
  return;
12369
12473
  }
12474
+ if (body2.remoteAccess !== void 0 && deps.isRemoteAccessOwner !== void 0 && !deps.isRemoteAccessOwner(req)) {
12475
+ deps.writeJson(res, 401, { error: "unauthorized" });
12476
+ return;
12477
+ }
12370
12478
  if (body2.remoteAccess !== void 0 && !isValidRemoteAccessInput(body2.remoteAccess)) {
12371
12479
  deps.writeJson(res, 400, { error: "invalid_remote_access" });
12372
12480
  return;
@@ -12384,7 +12492,10 @@ async function mutateGlobalSettings(req, res, deps) {
12384
12492
  return;
12385
12493
  }
12386
12494
  const theme = body2.theme === "instrument" || body2.theme === "maritime" || body2.theme === "carbon" || body2.theme === "whites" ? body2.theme : void 0;
12387
- const updated = deps.consoleSettingsStore.update((current) => ({
12495
+ const beforeUpdate = deps.consoleSettingsStore.load();
12496
+ const previousRemoteAccess = beforeUpdate.general?.remoteAccess ?? createDefaultRemoteAccess(crypto5.randomInt);
12497
+ const nextRemoteAccess = body2.remoteAccess !== void 0 ? normalizeRemoteAccessInput(body2.remoteAccess) : previousRemoteAccess;
12498
+ deps.consoleSettingsStore.update((current) => ({
12388
12499
  ...current,
12389
12500
  version: 1,
12390
12501
  general: {
@@ -12392,7 +12503,7 @@ async function mutateGlobalSettings(req, res, deps) {
12392
12503
  ...body2.consolePortMode === "dynamic" || body2.consolePortMode === "static" ? { consolePortMode: body2.consolePortMode } : {},
12393
12504
  ...isValidGlobalStaticPortInput(body2.consoleStaticPort) ? { consoleStaticPort: body2.consoleStaticPort } : {},
12394
12505
  ...body2.language === "auto" || body2.language === "en" || body2.language === "ko" ? { language: body2.language } : {},
12395
- ...body2.remoteAccess !== void 0 ? { remoteAccess: normalizeRemoteAccessInput(body2.remoteAccess) } : {},
12506
+ ...body2.remoteAccess !== void 0 ? { remoteAccess: nextRemoteAccess } : {},
12396
12507
  ...body2.seenFeatureTours !== void 0 ? { seenFeatureTours: sanitizeSeenFeatureTours(body2.seenFeatureTours) ?? [] } : {},
12397
12508
  ...theme !== void 0 ? { theme } : {},
12398
12509
  ...isUiFontSettings(body2.uiFont) ? { uiFont: body2.uiFont } : {}
@@ -12400,20 +12511,41 @@ async function mutateGlobalSettings(req, res, deps) {
12400
12511
  plugins: current.plugins
12401
12512
  }));
12402
12513
  if (theme !== void 0) deps.onThemeChanged?.(theme);
12403
- if (body2.remoteAccess !== void 0) await deps.onRemoteAccessChanged?.();
12404
- const response = { state: toGlobalSettingsState(updated) };
12514
+ if (body2.remoteAccess !== void 0) await deps.onRemoteAccessChanged?.({ previous: previousRemoteAccess, next: nextRemoteAccess });
12515
+ const response = { state: withRemoteAccessVisibility(buildGlobalSettingsState(deps.consoleSettingsStore), req, deps) };
12405
12516
  deps.writeJson(res, 200, response);
12406
12517
  }
12407
12518
  function isValidRemoteAccessInput(value) {
12408
- if (!isRecord3(value)) return false;
12409
- if (value.enabled !== void 0 && typeof value.enabled !== "boolean") return false;
12410
- if (value.bindHost !== void 0 && value.bindHost !== null && !isValidRemoteBindHost(value.bindHost)) return false;
12411
- return value.enabled !== true || isValidRemoteBindHost(value.bindHost);
12519
+ if (!isRecord3(value) || "bindHost" in value || typeof value.enabled !== "boolean" || typeof value.publicEndpointEnabled !== "boolean") return false;
12520
+ const validListenAddress = value.listenAddress === "" || isValidRemoteBindHost(value.listenAddress);
12521
+ const validAdvertisedHost = value.advertisedHost === "" || isValidRemoteAdvertisedHost(value.advertisedHost);
12522
+ if (!validListenAddress || !validAdvertisedHost) return false;
12523
+ const listenPort = sanitizeRemotePortSetting(value.listenPort);
12524
+ const advertisedPort = sanitizeRemotePortSetting(value.advertisedPort);
12525
+ if (!listenPort || !advertisedPort) return false;
12526
+ const acknowledgment = sanitizeAcknowledgment(value.acknowledgment);
12527
+ if (value.acknowledgment !== null && !acknowledgment) return false;
12528
+ if (value.publicEndpointEnabled !== true) return value.acknowledgment === null && (value.enabled !== true || value.listenAddress !== "");
12529
+ if (value.enabled !== true) return true;
12530
+ return value.listenAddress !== "" && value.advertisedHost !== "" && acknowledgmentMatches({ listenAddress: value.listenAddress, advertisedHost: value.advertisedHost, listenPort, advertisedPort }, acknowledgment);
12412
12531
  }
12413
12532
  function normalizeRemoteAccessInput(value) {
12414
- const record3 = isRecord3(value) ? value : {};
12415
- const enabled = typeof record3.enabled === "boolean" ? record3.enabled : false;
12416
- return { enabled, ...isValidRemoteBindHost(record3.bindHost) ? { bindHost: record3.bindHost } : {} };
12533
+ const record3 = value;
12534
+ const publicEndpointEnabled = record3.publicEndpointEnabled === true;
12535
+ return {
12536
+ enabled: record3.enabled === true,
12537
+ publicEndpointEnabled,
12538
+ listenAddress: record3.listenAddress === "" ? "" : canonicalizeRemoteBindHost(record3.listenAddress),
12539
+ advertisedHost: record3.advertisedHost === "" ? "" : canonicalizeRemoteBindHost(record3.advertisedHost),
12540
+ listenPort: sanitizeRemotePortSetting(record3.listenPort),
12541
+ advertisedPort: sanitizeRemotePortSetting(record3.advertisedPort),
12542
+ acknowledgment: publicEndpointEnabled ? sanitizeAcknowledgment(record3.acknowledgment) : null
12543
+ };
12544
+ }
12545
+ function withRemoteAccessVisibility(state, req, deps) {
12546
+ if (deps.isRemoteAccessOwner === void 0 || deps.isRemoteAccessOwner(req)) return state;
12547
+ const { remoteAccess: _hidden, ...rest } = state;
12548
+ return rest;
12417
12549
  }
12418
12550
  function toGlobalSettingsState(data) {
12419
12551
  const general = data.general ?? {};
@@ -12421,7 +12553,7 @@ function toGlobalSettingsState(data) {
12421
12553
  consolePortMode: general.consolePortMode ?? "dynamic",
12422
12554
  consoleStaticPort: general.consoleStaticPort ?? null,
12423
12555
  language: general.language ?? "auto",
12424
- remoteAccess: { enabled: general.remoteAccess?.enabled ?? false, bindHost: general.remoteAccess?.bindHost ?? null },
12556
+ remoteAccess: general.remoteAccess ?? createDefaultRemoteAccess(crypto5.randomInt),
12425
12557
  seenFeatureTours: general.seenFeatureTours ?? [],
12426
12558
  theme: general.theme ?? "instrument",
12427
12559
  uiFont: general.uiFont ?? DEFAULT_UI_FONT_SETTINGS
@@ -12448,14 +12580,16 @@ var PLUGIN_SETTINGS_API_CATALOG = [
12448
12580
  path: "/api/v1/settings/plugins/:pluginId",
12449
12581
  summary: "Get the settings for a specific plugin.",
12450
12582
  category: "Settings",
12451
- gate: "loopback"
12583
+ gate: "loopback",
12584
+ transport: "http"
12452
12585
  },
12453
12586
  {
12454
12587
  method: "PUT",
12455
12588
  path: "/api/v1/settings/plugins/:pluginId",
12456
12589
  summary: "Save the settings for a specific plugin.",
12457
12590
  category: "Settings",
12458
- gate: "origin-write"
12591
+ gate: "origin-write",
12592
+ transport: "http"
12459
12593
  }
12460
12594
  ];
12461
12595
  function createPluginSettingsRouter(deps) {
@@ -12515,6 +12649,450 @@ function isPluginSettingsJsonRequest(req) {
12515
12649
  const contentType = req.headers["content-type"];
12516
12650
  return typeof contentType === "string" && contentType.toLowerCase().split(";")[0]?.trim() === "application/json";
12517
12651
  }
12652
+ var MAX_GROUP_NAME_LENGTH = 64;
12653
+ var DELETION_GRACE_MS = 8e3;
12654
+ var FIXED_SENSITIVE_OPERATION_FIELDS = /* @__PURE__ */ new Set([
12655
+ "canonicalCwd",
12656
+ "cwd",
12657
+ "persona",
12658
+ "prompt",
12659
+ "providerSession",
12660
+ "ticket",
12661
+ "token",
12662
+ "toolAllowlist",
12663
+ "transcriptPath"
12664
+ ]);
12665
+ function createSanitizedOpDto(node, options = {}) {
12666
+ const sensitiveFields = /* @__PURE__ */ new Set([...FIXED_SENSITIVE_OPERATION_FIELDS, ...options.sensitiveFields ?? []]);
12667
+ const payload = sanitizeRecord(node.payload, sensitiveFields);
12668
+ delete payload.resumeAvailable;
12669
+ if (isResumableProviderSession(node.payload?.providerSession)) payload.resumeAvailable = true;
12670
+ return {
12671
+ ...node,
12672
+ payload
12673
+ };
12674
+ }
12675
+ function isResumableProviderSession(value) {
12676
+ if (!isRecord4(value)) return false;
12677
+ return value.provider === "claude" && typeof value.sessionId === "string" && value.sessionId.length > 0;
12678
+ }
12679
+ function sanitizeRecord(value, sensitiveFields) {
12680
+ const output = {};
12681
+ for (const [key, item] of Object.entries(value)) {
12682
+ if (sensitiveFields.has(key)) continue;
12683
+ output[key] = sanitizeValue(item, sensitiveFields);
12684
+ }
12685
+ return output;
12686
+ }
12687
+ function sanitizeValue(value, sensitiveFields) {
12688
+ if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, sensitiveFields));
12689
+ if (!isRecord4(value)) return value;
12690
+ return sanitizeRecord(value, sensitiveFields);
12691
+ }
12692
+ function isRecord4(value) {
12693
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12694
+ }
12695
+ function createOperationStore(deps = {}) {
12696
+ const now = deps.now ?? Date.now;
12697
+ const nodes = /* @__PURE__ */ new Map();
12698
+ const groups = /* @__PURE__ */ new Map();
12699
+ function list() {
12700
+ return Array.from(nodes.values()).sort(compareOperationNodes);
12701
+ }
12702
+ function listByTheater(theaterId) {
12703
+ return list().filter((node) => node.theaterId === theaterId);
12704
+ }
12705
+ function get(id) {
12706
+ return nodes.get(id) ?? null;
12707
+ }
12708
+ function create2(input) {
12709
+ const id = input.id ?? crypto5.randomUUID();
12710
+ if (nodes.has(id)) throw new Error("operation_exists");
12711
+ const node = normalizeCreateInput(input, id, now());
12712
+ nodes.set(node.id, node);
12713
+ return node;
12714
+ }
12715
+ function upsert(input) {
12716
+ const existing = input.id ? nodes.get(input.id) : null;
12717
+ if (!existing) return create2(input);
12718
+ const updated = normalizePatch(existing, {
12719
+ title: input.title,
12720
+ accent: input.accent ?? existing.accent,
12721
+ geometry: input.geometry ?? existing.geometry,
12722
+ payload: input.payload ?? existing.payload
12723
+ }, now());
12724
+ nodes.set(existing.id, updated);
12725
+ return updated;
12726
+ }
12727
+ function patch(id, input) {
12728
+ const existing = nodes.get(id);
12729
+ if (!existing) return null;
12730
+ const updated = normalizePatch(existing, input, now());
12731
+ nodes.set(id, updated);
12732
+ return updated;
12733
+ }
12734
+ function deleteNode(id) {
12735
+ if (!nodes.has(id)) return false;
12736
+ nodes.delete(id);
12737
+ return true;
12738
+ }
12739
+ function deleteByTheater(theaterId) {
12740
+ let deleted = 0;
12741
+ for (const node of Array.from(nodes.values())) {
12742
+ if (node.theaterId !== theaterId) continue;
12743
+ nodes.delete(node.id);
12744
+ deleted += 1;
12745
+ }
12746
+ deleteGroupsByTheater(theaterId);
12747
+ return deleted;
12748
+ }
12749
+ function replace(nextNodes) {
12750
+ nodes.clear();
12751
+ const validNodes = sanitizeReplacementNodes(nextNodes);
12752
+ for (const node of validNodes) nodes.set(node.id, node);
12753
+ }
12754
+ function createGroup(input) {
12755
+ const id = input.id ?? crypto5.randomUUID();
12756
+ if (groups.has(id)) throw new Error("group_exists");
12757
+ const order = input.order ?? groups.size;
12758
+ const group = {
12759
+ id,
12760
+ theaterId: input.theaterId,
12761
+ name: input.name.trim().slice(0, MAX_GROUP_NAME_LENGTH) || "Group",
12762
+ color: input.color,
12763
+ order,
12764
+ createdAt: now()
12765
+ };
12766
+ groups.set(id, group);
12767
+ return group;
12768
+ }
12769
+ function updateGroup(id, input) {
12770
+ const existing = groups.get(id);
12771
+ if (!existing) return null;
12772
+ const updated = {
12773
+ ...existing,
12774
+ ...input.name !== void 0 ? { name: input.name.trim().slice(0, MAX_GROUP_NAME_LENGTH) || existing.name } : {},
12775
+ ...input.color !== void 0 ? { color: input.color } : {},
12776
+ ...input.order !== void 0 ? { order: input.order } : {}
12777
+ };
12778
+ groups.set(id, updated);
12779
+ return updated;
12780
+ }
12781
+ function deleteGroup(id) {
12782
+ if (!groups.has(id)) return false;
12783
+ groups.delete(id);
12784
+ for (const [nodeId, node] of nodes.entries()) {
12785
+ if (node.groupId === id) nodes.set(nodeId, { ...node, groupId: null });
12786
+ }
12787
+ return true;
12788
+ }
12789
+ function listGroups(theaterId) {
12790
+ return Array.from(groups.values()).filter((g) => g.theaterId === theaterId).sort((a, b) => a.order - b.order || a.createdAt - b.createdAt);
12791
+ }
12792
+ function listAllGroups() {
12793
+ return Array.from(groups.values()).sort((a, b) => a.order - b.order || a.createdAt - b.createdAt);
12794
+ }
12795
+ function deleteGroupsByTheater(theaterId) {
12796
+ let deleted = 0;
12797
+ for (const [id, group] of Array.from(groups.entries())) {
12798
+ if (group.theaterId !== theaterId) continue;
12799
+ groups.delete(id);
12800
+ deleted += 1;
12801
+ }
12802
+ return deleted;
12803
+ }
12804
+ function replaceGroups(nextGroups) {
12805
+ groups.clear();
12806
+ for (const group of nextGroups) {
12807
+ if (!groups.has(group.id)) groups.set(group.id, group);
12808
+ }
12809
+ }
12810
+ return { list, listByTheater, get, create: create2, upsert, patch, delete: deleteNode, deleteByTheater, replace, createGroup, updateGroup, deleteGroup, listGroups, listAllGroups, deleteGroupsByTheater, replaceGroups };
12811
+ }
12812
+ function normalizeCreateInput(input, id, timestamp) {
12813
+ return {
12814
+ id,
12815
+ theaterId: input.theaterId,
12816
+ type: input.type,
12817
+ pluginId: input.pluginId,
12818
+ title: input.title.trim() || "Untitled Operation",
12819
+ ...input.accent ? { accent: input.accent.trim() } : {},
12820
+ ...input.groupId !== void 0 ? { groupId: input.groupId } : {},
12821
+ payload: input.payload ?? {},
12822
+ geometry: input.geometry ?? null,
12823
+ ts: {
12824
+ createdAt: input.createdAt ?? timestamp,
12825
+ updatedAt: timestamp
12826
+ }
12827
+ };
12828
+ }
12829
+ function normalizePatch(existing, input, timestamp) {
12830
+ const title = input.title?.trim();
12831
+ return {
12832
+ ...existing,
12833
+ ...title !== void 0 ? { title: title.length > 0 ? title : existing.title } : {},
12834
+ ...input.accent !== void 0 ? { accent: input.accent && input.accent.trim() ? input.accent.trim() : void 0 } : {},
12835
+ ...input.groupId !== void 0 ? { groupId: input.groupId } : {},
12836
+ ...input.payload !== void 0 ? { payload: input.payload } : {},
12837
+ ...input.geometry !== void 0 ? { geometry: input.geometry } : {},
12838
+ ts: { ...existing.ts, updatedAt: timestamp }
12839
+ };
12840
+ }
12841
+ function compareOperationNodes(a, b) {
12842
+ return a.ts.createdAt - b.ts.createdAt || a.id.localeCompare(b.id);
12843
+ }
12844
+ function sanitizeReplacementNodes(nextNodes) {
12845
+ const seen = /* @__PURE__ */ new Set();
12846
+ const result = [];
12847
+ for (const node of nextNodes) {
12848
+ if (!seen.has(node.id)) {
12849
+ seen.add(node.id);
12850
+ result.push(node);
12851
+ }
12852
+ }
12853
+ return result;
12854
+ }
12855
+ var OPERATIONS_API_CATALOG = [
12856
+ { method: "GET", path: "/api/v1/operations", summary: "List Operations.", category: "Operations", gate: "loopback", transport: "http" },
12857
+ { method: "POST", path: "/api/v1/operations", summary: "Create an Operation.", category: "Operations", gate: "origin-write", transport: "http" },
12858
+ { method: "GET", path: "/api/v1/operations/:operationId", summary: "Get an Operation.", category: "Operations", gate: "loopback", transport: "http" },
12859
+ { method: "PATCH", path: "/api/v1/operations/:operationId", summary: "Update an Operation.", category: "Operations", gate: "origin-write", transport: "http" },
12860
+ { method: "DELETE", path: "/api/v1/operations/:operationId", summary: "Delete an Operation.", category: "Operations", gate: "origin-write", transport: "http" },
12861
+ { method: "GET", path: "/api/v1/operations/catalog", summary: "List available Operation launch kinds.", category: "Operations", gate: "loopback", transport: "http" },
12862
+ { method: "GET", path: "/api/v1/operations/events", summary: "Stream Operation changes.", category: "Operations", gate: "loopback", transport: "sse" },
12863
+ { method: "GET", path: "/api/v1/operations/groups", summary: "List Operation groups.", category: "Operations", gate: "loopback", transport: "http" },
12864
+ { method: "POST", path: "/api/v1/operations/groups", summary: "Create an Operation group.", category: "Operations", gate: "origin-write", transport: "http" },
12865
+ { method: "PATCH", path: "/api/v1/operations/groups/:groupId", summary: "Update an Operation group.", category: "Operations", gate: "origin-write", transport: "http" },
12866
+ { method: "DELETE", path: "/api/v1/operations/groups/:groupId", summary: "Delete an Operation group.", category: "Operations", gate: "origin-write", transport: "http" }
12867
+ ];
12868
+ function createOperationsRouter(deps) {
12869
+ return async ({ req, res, pathname }) => {
12870
+ if (pathname === "/api/v1/operations") {
12871
+ await handleCollection(req, res, deps);
12872
+ return true;
12873
+ }
12874
+ if (pathname === "/api/v1/operations/catalog") {
12875
+ if (req.method !== "GET") {
12876
+ deps.writeJson(res, 405, { error: "Method not allowed" });
12877
+ return true;
12878
+ }
12879
+ deps.writeJson(res, 200, deps.resolveLaunchCatalog ? await deps.resolveLaunchCatalog() : { plugins: [] });
12880
+ return true;
12881
+ }
12882
+ if (pathname === "/api/v1/operations/events") {
12883
+ if (req.method !== "GET") {
12884
+ deps.writeJson(res, 405, { error: "Method not allowed" });
12885
+ return true;
12886
+ }
12887
+ deps.subscribeOperationSse?.(req, res);
12888
+ return true;
12889
+ }
12890
+ if (pathname === "/api/v1/operations/groups") {
12891
+ await handleGroupCollection(req, res, deps);
12892
+ return true;
12893
+ }
12894
+ const groupItemMatch = pathname.match(/^\/api\/v1\/operations\/groups\/([^/]+)$/);
12895
+ if (groupItemMatch) {
12896
+ await handleGroupItem(req, res, decodeURIComponent(groupItemMatch[1] ?? ""), deps);
12897
+ return true;
12898
+ }
12899
+ const itemMatch = pathname.match(/^\/api\/v1\/operations\/([^/]+)$/);
12900
+ if (itemMatch) {
12901
+ await handleItem(req, res, decodeURIComponent(itemMatch[1] ?? ""), deps);
12902
+ return true;
12903
+ }
12904
+ return false;
12905
+ };
12906
+ }
12907
+ async function handleCollection(req, res, deps) {
12908
+ if (req.method === "GET") {
12909
+ const theaterId = readRequestUrl(req).searchParams.get("theaterId");
12910
+ const nodes = theaterId ? deps.store.listByTheater(theaterId) : deps.store.list();
12911
+ deps.writeJson(res, 200, { operations: nodes.map((node) => sanitizeOperationNode(node, deps)) });
12912
+ return;
12913
+ }
12914
+ if (req.method !== "POST") {
12915
+ deps.writeJson(res, 405, { error: "Method not allowed" });
12916
+ return;
12917
+ }
12918
+ if (!deps.isAuthorized(req)) {
12919
+ deps.writeJson(res, 401, { error: "unauthorized" });
12920
+ return;
12921
+ }
12922
+ const body2 = await deps.readJsonBody(req);
12923
+ if (!body2 || typeof body2.theaterId !== "string" || typeof body2.type !== "string" || typeof body2.pluginId !== "string" || typeof body2.title !== "string") {
12924
+ deps.writeJson(res, 400, { error: "invalid_operation" });
12925
+ return;
12926
+ }
12927
+ if (body2.accent !== void 0 && typeof body2.accent !== "string") {
12928
+ deps.writeJson(res, 400, { error: "invalid_operation_accent" });
12929
+ return;
12930
+ }
12931
+ if (typeof body2.id === "string" && deps.isPendingDeletion?.(body2.id)) {
12932
+ deps.writeJson(res, 409, { error: "pending_deletion" });
12933
+ return;
12934
+ }
12935
+ try {
12936
+ const node = deps.store.create({
12937
+ id: typeof body2.id === "string" ? body2.id : void 0,
12938
+ theaterId: body2.theaterId,
12939
+ type: body2.type,
12940
+ pluginId: body2.pluginId,
12941
+ title: body2.title,
12942
+ ...typeof body2.accent === "string" ? { accent: body2.accent } : {},
12943
+ payload: isRecord4(body2.payload) ? body2.payload : {},
12944
+ geometry: isRecord4(body2.geometry) ? readGeometry(body2.geometry) : null
12945
+ });
12946
+ deps.persist();
12947
+ deps.writeJson(res, 201, { operation: sanitizeOperationNode(node, deps) });
12948
+ } catch (error51) {
12949
+ deps.writeJson(res, 400, { error: error51 instanceof Error ? error51.message : "invalid_operation" });
12950
+ }
12951
+ }
12952
+ async function handleItem(req, res, id, deps) {
12953
+ if (req.method === "GET") {
12954
+ const node = deps.store.get(id);
12955
+ deps.writeJson(res, node ? 200 : 404, node ? { operation: sanitizeOperationNode(node, deps) } : { error: "operation_not_found" });
12956
+ return;
12957
+ }
12958
+ if (req.method !== "PATCH" && req.method !== "DELETE") {
12959
+ deps.writeJson(res, 405, { error: "Method not allowed" });
12960
+ return;
12961
+ }
12962
+ if (!deps.isAuthorized(req)) {
12963
+ deps.writeJson(res, 401, { error: "unauthorized" });
12964
+ return;
12965
+ }
12966
+ if (req.method === "DELETE") {
12967
+ const deletion = deps.deleteOperation(id);
12968
+ deps.writeJson(res, 200, { ok: true, deletion });
12969
+ return;
12970
+ }
12971
+ const body2 = await deps.readJsonBody(req);
12972
+ if (!body2) {
12973
+ deps.writeJson(res, 400, { error: "invalid_operation_patch" });
12974
+ return;
12975
+ }
12976
+ if (body2.accent !== void 0 && body2.accent !== null && typeof body2.accent !== "string") {
12977
+ deps.writeJson(res, 400, { error: "invalid_operation_accent" });
12978
+ return;
12979
+ }
12980
+ if (body2.groupId !== void 0 && body2.groupId !== null && typeof body2.groupId !== "string") {
12981
+ deps.writeJson(res, 400, { error: "invalid_operation_groupId" });
12982
+ return;
12983
+ }
12984
+ try {
12985
+ const previousNode = typeof body2.title === "string" ? deps.store.get(id) : null;
12986
+ const node = deps.store.patch(id, {
12987
+ ...typeof body2.title === "string" ? { title: body2.title } : {},
12988
+ ...typeof body2.accent === "string" || body2.accent === null ? { accent: body2.accent } : {},
12989
+ ...typeof body2.groupId === "string" || body2.groupId === null ? { groupId: body2.groupId } : {},
12990
+ ...isRecord4(body2.payload) ? { payload: body2.payload } : {},
12991
+ ...isRecord4(body2.geometry) || body2.geometry === null ? { geometry: body2.geometry === null ? null : readGeometry(body2.geometry) } : {}
12992
+ });
12993
+ if (!node) {
12994
+ deps.writeJson(res, 404, { error: "operation_not_found" });
12995
+ return;
12996
+ }
12997
+ deps.persist();
12998
+ if (previousNode && typeof body2.title === "string") {
12999
+ deps.publishRenameEvent?.({
13000
+ operationId: node.id,
13001
+ pluginId: node.pluginId,
13002
+ type: node.type,
13003
+ title: body2.title.trim() === "" ? "" : node.title,
13004
+ previousTitle: previousNode.title
13005
+ });
13006
+ }
13007
+ deps.broadcastOperationChanged?.(deps.store.get(id) ?? node);
13008
+ deps.writeJson(res, 200, { operation: sanitizeOperationNode(node, deps) });
13009
+ } catch (error51) {
13010
+ deps.writeJson(res, 400, { error: error51 instanceof Error ? error51.message : "invalid_operation_patch" });
13011
+ }
13012
+ }
13013
+ function sanitizeOperationNode(node, deps) {
13014
+ return createSanitizedOpDto(node, { sensitiveFields: deps.getPluginSensitiveFields?.(node.pluginId) ?? [] });
13015
+ }
13016
+ async function handleGroupCollection(req, res, deps) {
13017
+ if (req.method === "GET") {
13018
+ const theaterId = readRequestUrl(req).searchParams.get("theaterId");
13019
+ const groups = theaterId ? deps.store.listGroups(theaterId) : deps.store.listAllGroups();
13020
+ deps.writeJson(res, 200, { groups });
13021
+ return;
13022
+ }
13023
+ if (req.method !== "POST") {
13024
+ deps.writeJson(res, 405, { error: "Method not allowed" });
13025
+ return;
13026
+ }
13027
+ if (!deps.isAuthorized(req)) {
13028
+ deps.writeJson(res, 401, { error: "unauthorized" });
13029
+ return;
13030
+ }
13031
+ const body2 = await deps.readJsonBody(req);
13032
+ if (!body2 || typeof body2.theaterId !== "string" || typeof body2.name !== "string" || typeof body2.color !== "string") {
13033
+ deps.writeJson(res, 400, { error: "invalid_group" });
13034
+ return;
13035
+ }
13036
+ try {
13037
+ const group = deps.store.createGroup({
13038
+ theaterId: body2.theaterId,
13039
+ name: body2.name,
13040
+ color: body2.color,
13041
+ ...typeof body2.order === "number" ? { order: body2.order } : {}
13042
+ });
13043
+ deps.persist();
13044
+ deps.writeJson(res, 201, { group });
13045
+ } catch (error51) {
13046
+ deps.writeJson(res, 400, { error: error51 instanceof Error ? error51.message : "invalid_group" });
13047
+ }
13048
+ }
13049
+ async function handleGroupItem(req, res, id, deps) {
13050
+ if (req.method !== "PATCH" && req.method !== "DELETE") {
13051
+ deps.writeJson(res, 405, { error: "Method not allowed" });
13052
+ return;
13053
+ }
13054
+ if (!deps.isAuthorized(req)) {
13055
+ deps.writeJson(res, 401, { error: "unauthorized" });
13056
+ return;
13057
+ }
13058
+ if (req.method === "DELETE") {
13059
+ const deleted = deps.store.deleteGroup(id);
13060
+ if (deleted) deps.persist();
13061
+ deps.writeJson(res, deleted ? 200 : 404, deleted ? { ok: true } : { error: "group_not_found" });
13062
+ return;
13063
+ }
13064
+ const body2 = await deps.readJsonBody(req);
13065
+ if (!body2) {
13066
+ deps.writeJson(res, 400, { error: "invalid_group_patch" });
13067
+ return;
13068
+ }
13069
+ const group = deps.store.updateGroup(id, {
13070
+ ...typeof body2.name === "string" ? { name: body2.name } : {},
13071
+ ...typeof body2.color === "string" ? { color: body2.color } : {},
13072
+ ...typeof body2.order === "number" ? { order: body2.order } : {}
13073
+ });
13074
+ if (!group) {
13075
+ deps.writeJson(res, 404, { error: "group_not_found" });
13076
+ return;
13077
+ }
13078
+ deps.persist();
13079
+ deps.writeJson(res, 200, { group });
13080
+ }
13081
+ function readGeometry(value) {
13082
+ const x = readFiniteNumber(value.x);
13083
+ const y = readFiniteNumber(value.y);
13084
+ const width = readFiniteNumber(value.width);
13085
+ const height = readFiniteNumber(value.height);
13086
+ const zIndex = readFiniteNumber(value.zIndex);
13087
+ if (x === null || y === null || width === null || height === null || zIndex === null) return null;
13088
+ return { x, y, width, height, zIndex };
13089
+ }
13090
+ function readFiniteNumber(value) {
13091
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
13092
+ }
13093
+ function readRequestUrl(req) {
13094
+ return new URL(req.url ?? "/", "http://127.0.0.1");
13095
+ }
12518
13096
  var MAX_FAMILY_LENGTH = 128;
12519
13097
  var SUCCESS_TTL_MS = 5 * 60 * 1e3;
12520
13098
  var FAILURE_TTL_MS = 30 * 1e3;
@@ -12624,7 +13202,8 @@ var SYSTEM_FONTS_API_CATALOG = [
12624
13202
  path: SYSTEM_FONTS_PATH,
12625
13203
  summary: "List sanitized system font families for built-in settings.",
12626
13204
  category: "Settings",
12627
- gate: "loopback"
13205
+ gate: "loopback",
13206
+ transport: "http"
12628
13207
  }
12629
13208
  ];
12630
13209
  function createSystemFontsRouter(deps) {
@@ -12644,17 +13223,26 @@ function createSystemFontsRouter(deps) {
12644
13223
  }
12645
13224
 
12646
13225
  // core/host/api-catalog.ts
12647
- var compareApiCatalogEntries = (left, right) => left.category.localeCompare(right.category) || left.path.localeCompare(right.path) || left.method.localeCompare(right.method);
12648
- function buildApiCatalog() {
12649
- return [
13226
+ var compareApiCatalogEntries = (left, right) => left.category.localeCompare(right.category) || left.path.localeCompare(right.path) || left.method.localeCompare(right.method) || left.transport.localeCompare(right.transport);
13227
+ function buildApiCatalog(extraEntries = []) {
13228
+ const entries = [
12650
13229
  ...SERVER_API_CATALOG,
12651
13230
  ...DESKTOP_FULLSCREEN_API_CATALOG,
12652
13231
  ...DESKTOP_SHELL_API_CATALOG,
12653
13232
  ...DESKTOP_THEME_API_CATALOG,
12654
13233
  ...GLOBAL_SETTINGS_API_CATALOG,
12655
13234
  ...PLUGIN_SETTINGS_API_CATALOG,
12656
- ...SYSTEM_FONTS_API_CATALOG
12657
- ].slice().sort(compareApiCatalogEntries);
13235
+ ...SYSTEM_FONTS_API_CATALOG,
13236
+ ...OPERATIONS_API_CATALOG,
13237
+ ...extraEntries
13238
+ ];
13239
+ const identities = /* @__PURE__ */ new Set();
13240
+ for (const entry of entries) {
13241
+ const identity2 = `${entry.method}|${entry.path}|${entry.transport}`;
13242
+ if (identities.has(identity2)) throw new Error(`duplicate_api_catalog_entry:${entry.method}:${entry.path}:${entry.transport}`);
13243
+ identities.add(identity2);
13244
+ }
13245
+ return entries.slice().sort(compareApiCatalogEntries);
12658
13246
  }
12659
13247
 
12660
13248
  // core/host/access-control-contract.ts
@@ -12673,8 +13261,8 @@ function createAccessRegistry(deps = {}) {
12673
13261
  const sessionTtlMs = deps.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS;
12674
13262
  const sessionIdleTtlMs = deps.sessionIdleTtlMs ?? DEFAULT_SESSION_IDLE_TTL_MS;
12675
13263
  const now = deps.now ?? Date.now;
12676
- const randomToken = deps.randomToken ?? (() => crypto6.randomBytes(32).toString("base64url"));
12677
- const randomHandle = deps.randomHandle ?? (() => crypto6.randomBytes(8).toString("hex"));
13264
+ const randomToken = deps.randomToken ?? (() => crypto5.randomBytes(32).toString("base64url"));
13265
+ const randomHandle = deps.randomHandle ?? (() => crypto5.randomBytes(8).toString("hex"));
12678
13266
  const grants = /* @__PURE__ */ new Map();
12679
13267
  const sessions = /* @__PURE__ */ new Map();
12680
13268
  function issueGrant(audience, access = "full") {
@@ -12789,8 +13377,15 @@ function createAccessRegistry(deps = {}) {
12789
13377
  }
12790
13378
  return { grantTtlMs, issueGrant, redeemGrant, consumeGrant, openSession, peekGrant, resolveSession, listGrants, revokeGrant, revokeGrants, listSessions, hasSession, revokeSession, revokeSessionByHandle, revokeSessionsByPairing, revokeSessions, revokeAllSessions, prune };
12791
13379
  }
13380
+ function listenerAuthority(host, port, secure) {
13381
+ const formatted = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
13382
+ return port === (secure ? 443 : 80) ? formatted : `${formatted}:${port}`;
13383
+ }
13384
+ function listenerOrigin(host, port, secure) {
13385
+ return `${secure ? "https" : "http"}://${listenerAuthority(host, port, secure)}`;
13386
+ }
12792
13387
  function createLoopbackListenerIdentity(port) {
12793
- return { audience: "local", host: "127.0.0.1", port, origin: `http://127.0.0.1:${port}`, secure: false, bindAddress: "127.0.0.1" };
13388
+ return { audience: "local", host: "127.0.0.1", port, origin: listenerOrigin("127.0.0.1", port, false), secure: false, bindAddress: "127.0.0.1", bindPort: port };
12794
13389
  }
12795
13390
  function normalizeBindAddress(address) {
12796
13391
  if (!address) return "";
@@ -12799,7 +13394,7 @@ function normalizeBindAddress(address) {
12799
13394
  }
12800
13395
  function resolveListenerIdentity(listeners, socket) {
12801
13396
  const address = normalizeBindAddress(socket.localAddress);
12802
- return listeners.find((listener) => listener.bindAddress === address && (socket.localPort === void 0 || listener.port === socket.localPort)) ?? null;
13397
+ return listeners.find((listener) => listener.bindAddress === address && (socket.localPort === void 0 || (listener.bindPort ?? listener.port) === socket.localPort)) ?? null;
12803
13398
  }
12804
13399
  function sessionCookieName(port) {
12805
13400
  return `${SESSION_COOKIE_NAME}_${port}`;
@@ -12870,14 +13465,14 @@ var PAIRED_DEVICE_LIMIT = 64;
12870
13465
  var MAX_DEVICES = PAIRED_DEVICE_LIMIT;
12871
13466
  var LAST_SEEN_FLUSH_MS = 6e4;
12872
13467
  function hashPairingSecret(secret) {
12873
- return crypto6.createHash("sha256").update(secret, "utf8").digest("hex");
13468
+ return crypto5.createHash("sha256").update(secret, "utf8").digest("hex");
12874
13469
  }
12875
13470
  function createPairedDeviceStore(consoleDir, deps = {}) {
12876
13471
  const storage = createRemoteStorage(consoleDir, deps);
12877
13472
  const readFile7 = deps.readFile ?? ((file2) => fs9__default.readFileSync(file2, "utf8"));
12878
13473
  const now = deps.now ?? Date.now;
12879
- const randomId = deps.randomId ?? (() => crypto6.randomBytes(8).toString("hex"));
12880
- const randomSecret = deps.randomSecret ?? (() => crypto6.randomBytes(32).toString("base64url"));
13474
+ const randomId = deps.randomId ?? (() => crypto5.randomBytes(8).toString("hex"));
13475
+ const randomSecret = deps.randomSecret ?? (() => crypto5.randomBytes(32).toString("base64url"));
12881
13476
  let devices = read();
12882
13477
  let flushedAt = now();
12883
13478
  function read() {
@@ -12970,7 +13565,7 @@ function listRemoteInterfaces(interfaces = os4__default.networkInterfaces()) {
12970
13565
  if (entry.family !== "IPv4" || entry.internal || seen.has(entry.address)) continue;
12971
13566
  seen.add(entry.address);
12972
13567
  const kind = isCarrierGradeNat(entry.address) ? "tailscale" : "local";
12973
- candidates.push({ kind, label: kind === "tailscale" ? "Tailscale" : labelFor(name), address: entry.address });
13568
+ candidates.push({ kind, label: kind === "tailscale" ? `Tailscale (${sanitizeName(name)})` : labelFor(name), address: entry.address });
12974
13569
  }
12975
13570
  }
12976
13571
  return candidates.sort((left, right) => left.kind === right.kind ? left.address.localeCompare(right.address) : left.kind === "tailscale" ? -1 : 1);
@@ -12980,7 +13575,7 @@ function isCarrierGradeNat(address) {
12980
13575
  return octets.length === 4 && octets[0] === 100 && octets[1] !== void 0 && octets[1] >= 64 && octets[1] <= 127;
12981
13576
  }
12982
13577
  function labelFor(interfaceName) {
12983
- return /^(?:en|eth|wl|wlan)/u.test(interfaceName) ? "Local network" : `Local network (${sanitizeName(interfaceName)})`;
13578
+ return `Local network (${sanitizeName(interfaceName)})`;
12984
13579
  }
12985
13580
  function sanitizeName(value) {
12986
13581
  return value.replace(/[^A-Za-z0-9._-]/gu, "").slice(0, 24) || "interface";
@@ -30470,10 +31065,6 @@ new Map(
30470
31065
  REASONING_EFFORTS.map((effort, index) => [effort, index])
30471
31066
  );
30472
31067
  var CLAUDE_COMPAT_CONTEXT_WINDOW = 1e6;
30473
- var CLAUDE_DEFAULT_CONTEXT_WINDOW = 2e5;
30474
- function canProjectClaudeContextWindow(contextWindow) {
30475
- return typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > CLAUDE_DEFAULT_CONTEXT_WINDOW;
30476
- }
30477
31068
  function isClaudeOneMillionContextWindow(contextWindow) {
30478
31069
  return typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow >= CLAUDE_COMPAT_CONTEXT_WINDOW;
30479
31070
  }
@@ -31010,10 +31601,6 @@ var models_default = {
31010
31601
  };
31011
31602
  var GATEWAY_PROVIDERS = ["codex", "cursor", "kimi", "opencode"];
31012
31603
  var GATEWAY_MODEL_WIRES = ["anthropic", "responses", "chat-completions"];
31013
- function isAnthropicPassthroughModel(model) {
31014
- if (model.provider === "kimi") return true;
31015
- return model.provider === "opencode" && (model.wire ?? "anthropic") === "anthropic";
31016
- }
31017
31604
  var GATEWAY_REASONING_EFFORTS = [
31018
31605
  "low",
31019
31606
  "medium",
@@ -31149,10 +31736,7 @@ function toGatewayModelAlias(modelId) {
31149
31736
  }
31150
31737
  function toClaudeGatewayModelId(model) {
31151
31738
  const alias = toGatewayModelAlias(model.id);
31152
- if (canProjectClaudeContextWindow(model.contextWindow) && (!isAnthropicPassthroughModel(model) || isClaudeOneMillionContextWindow(model.contextWindow))) {
31153
- return `${alias}${CLAUDE_ONE_MILLION_MARKER}`;
31154
- }
31155
- return alias;
31739
+ return isClaudeOneMillionContextWindow(model.contextWindow) ? `${alias}${CLAUDE_ONE_MILLION_MARKER}` : alias;
31156
31740
  }
31157
31741
  function toClaudeGatewayModelDisplayName(model) {
31158
31742
  return isClaudeOneMillionContextWindow(model.contextWindow) ? `${model.displayName}${CLAUDE_ONE_MILLION_DISPLAY_SUFFIX}` : model.displayName;
@@ -32323,7 +32907,7 @@ function specToMcpTool(spec) {
32323
32907
  }
32324
32908
  function installExecutorToolCallRouter(runtime, sessionToken, ctx) {
32325
32909
  runtime.server.setOnToolCallArrived(sessionToken, (toolName, args) => {
32326
- const toolCallId = crypto6.randomUUID();
32910
+ const toolCallId = crypto5.randomUUID();
32327
32911
  void runtime.registry.invoke(toolName, args, {
32328
32912
  cwd: ctx.cwd,
32329
32913
  sessionLabel: ctx.sessionLabel,
@@ -32719,7 +33303,7 @@ function createServedMcpEndpoint(deps = {}) {
32719
33303
  if (activeStopPromise) await activeStopPromise;
32720
33304
  if (activeServer && activeServerUrl) return activeServerUrl;
32721
33305
  if (activeStartPromise) return activeStartPromise;
32722
- activeOpaquePath = `/${crypto6.randomUUID()}`;
33306
+ activeOpaquePath = `/${crypto5.randomUUID()}`;
32723
33307
  activeStartPromise = new Promise((resolve3, reject) => {
32724
33308
  const srv = http.createServer(handleRequest);
32725
33309
  srv.timeout = MCP_SERVER_TIMEOUT_MS;
@@ -32976,7 +33560,7 @@ function createExecutorMcpSession(deps, toolTimeoutSeconds, request) {
32976
33560
  throw new Error("Executor session cwd is required");
32977
33561
  }
32978
33562
  assertNonEmptyExecutorTools(request.serverName, request.specs);
32979
- const token = crypto6.randomUUID();
33563
+ const token = crypto5.randomUUID();
32980
33564
  registerExecutorSessionTools(runtime.runtime, token, [...request.specs]);
32981
33565
  installExecutorToolCallRouter(runtime.runtime, token, {
32982
33566
  cwd,
@@ -33015,7 +33599,7 @@ function issueSessionToken(deps, sessionTokensByLabel, request) {
33015
33599
  const allTools = runtime.registry.getAllAgentTools();
33016
33600
  const tools = request.includeTool ? allTools.filter((tool) => request.includeTool?.(tool.id) ?? true) : allTools;
33017
33601
  assertNonEmptyExecutorTools(name, tools);
33018
- const token = crypto6.randomUUID();
33602
+ const token = crypto5.randomUUID();
33019
33603
  registerExecutorSessionTools(runtime, token, tools);
33020
33604
  installExecutorToolCallRouter(runtime, token, {
33021
33605
  cwd,
@@ -34360,7 +34944,7 @@ var CoworkStore = class {
34360
34944
  if (found && found.state !== "closed" && found.state !== "applied") return found;
34361
34945
  }
34362
34946
  const now = (/* @__PURE__ */ new Date()).toISOString();
34363
- const id = crypto6.randomUUID();
34947
+ const id = crypto5.randomUUID();
34364
34948
  const record3 = { id, workspaceId, entryId, state: "idle", revision: 0, draft: body2, baseDraft: body2, baseHash, baseVersion, selection: null, annotations: [], ...identity2, targetPath, createdAt: now, updatedAt: now };
34365
34949
  this.sessions.set(this.key(workspaceId, id), record3);
34366
34950
  this.writers.set(`${workspaceId}:${entryId}`, id);
@@ -34439,7 +35023,7 @@ var CoworkStore = class {
34439
35023
  }
34440
35024
  };
34441
35025
  function hash2(value) {
34442
- return crypto6.createHash("sha256").update(value).digest("hex");
35026
+ return crypto5.createHash("sha256").update(value).digest("hex");
34443
35027
  }
34444
35028
  var COWORK_SYSTEM_PROMPT = `# Identity
34445
35029
 
@@ -34749,6 +35333,7 @@ function claudeGatewayLaunchEnv(inherited, options) {
34749
35333
  env.CLAUDE_SECURESTORAGE_CONFIG_DIR = "";
34750
35334
  env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY = "1";
34751
35335
  env.ENABLE_TOOL_SEARCH = "true";
35336
+ env.CLAUDE_CODE_AUTO_COMPACT_WINDOW ??= "1000000";
34752
35337
  delete env.ANTHROPIC_AUTH_TOKEN;
34753
35338
  delete env.ANTHROPIC_API_KEY;
34754
35339
  delete env.ANTHROPIC_MODEL;
@@ -35138,7 +35723,7 @@ function record2(value) {
35138
35723
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
35139
35724
  }
35140
35725
  function workspaceHash(canonicalCwd) {
35141
- return crypto6.createHash("sha256").update(canonicalCwd).digest("hex").slice(0, 12);
35726
+ return crypto5.createHash("sha256").update(canonicalCwd).digest("hex").slice(0, 12);
35142
35727
  }
35143
35728
  async function canonicalizeTheaterPath(cwd) {
35144
35729
  return canonicalizeTheaterPathSync(cwd);
@@ -35277,7 +35862,7 @@ function isWindowsAmbiguousPath(value, platform) {
35277
35862
  var GRANT_ID_BYTES = 24;
35278
35863
  var DEFAULT_GRANT_TTL_MS2 = 6e4;
35279
35864
  function createFolderGrantStore(deps = {}) {
35280
- const randomId = deps.randomId ?? (() => crypto6.randomBytes(GRANT_ID_BYTES).toString("base64url"));
35865
+ const randomId = deps.randomId ?? (() => crypto5.randomBytes(GRANT_ID_BYTES).toString("base64url"));
35281
35866
  const statSync2 = deps.statSync ?? fs9__default.statSync;
35282
35867
  const ttlMs = deps.ttlMs ?? DEFAULT_GRANT_TTL_MS2;
35283
35868
  const now = deps.now ?? Date.now;
@@ -35768,439 +36353,6 @@ function readRawHeaderValues(rawHeaders, name) {
35768
36353
  function stripIpv6Brackets2(host) {
35769
36354
  return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
35770
36355
  }
35771
- var MAX_GROUP_NAME_LENGTH = 64;
35772
- var DELETION_GRACE_MS = 8e3;
35773
- var FIXED_SENSITIVE_OPERATION_FIELDS = /* @__PURE__ */ new Set([
35774
- "canonicalCwd",
35775
- "cwd",
35776
- "persona",
35777
- "prompt",
35778
- "providerSession",
35779
- "ticket",
35780
- "token",
35781
- "toolAllowlist",
35782
- "transcriptPath"
35783
- ]);
35784
- function createSanitizedOpDto(node, options = {}) {
35785
- const sensitiveFields = /* @__PURE__ */ new Set([...FIXED_SENSITIVE_OPERATION_FIELDS, ...options.sensitiveFields ?? []]);
35786
- const payload = sanitizeRecord(node.payload, sensitiveFields);
35787
- delete payload.resumeAvailable;
35788
- if (isResumableProviderSession(node.payload?.providerSession)) payload.resumeAvailable = true;
35789
- return {
35790
- ...node,
35791
- payload
35792
- };
35793
- }
35794
- function isResumableProviderSession(value) {
35795
- if (!isRecord4(value)) return false;
35796
- return value.provider === "claude" && typeof value.sessionId === "string" && value.sessionId.length > 0;
35797
- }
35798
- function sanitizeRecord(value, sensitiveFields) {
35799
- const output = {};
35800
- for (const [key, item] of Object.entries(value)) {
35801
- if (sensitiveFields.has(key)) continue;
35802
- output[key] = sanitizeValue(item, sensitiveFields);
35803
- }
35804
- return output;
35805
- }
35806
- function sanitizeValue(value, sensitiveFields) {
35807
- if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, sensitiveFields));
35808
- if (!isRecord4(value)) return value;
35809
- return sanitizeRecord(value, sensitiveFields);
35810
- }
35811
- function isRecord4(value) {
35812
- return typeof value === "object" && value !== null && !Array.isArray(value);
35813
- }
35814
- function createOperationStore(deps = {}) {
35815
- const now = deps.now ?? Date.now;
35816
- const nodes = /* @__PURE__ */ new Map();
35817
- const groups = /* @__PURE__ */ new Map();
35818
- function list() {
35819
- return Array.from(nodes.values()).sort(compareOperationNodes);
35820
- }
35821
- function listByTheater(theaterId) {
35822
- return list().filter((node) => node.theaterId === theaterId);
35823
- }
35824
- function get(id) {
35825
- return nodes.get(id) ?? null;
35826
- }
35827
- function create2(input) {
35828
- const id = input.id ?? crypto6.randomUUID();
35829
- if (nodes.has(id)) throw new Error("operation_exists");
35830
- const node = normalizeCreateInput(input, id, now());
35831
- nodes.set(node.id, node);
35832
- return node;
35833
- }
35834
- function upsert(input) {
35835
- const existing = input.id ? nodes.get(input.id) : null;
35836
- if (!existing) return create2(input);
35837
- const updated = normalizePatch(existing, {
35838
- title: input.title,
35839
- accent: input.accent ?? existing.accent,
35840
- geometry: input.geometry ?? existing.geometry,
35841
- payload: input.payload ?? existing.payload
35842
- }, now());
35843
- nodes.set(existing.id, updated);
35844
- return updated;
35845
- }
35846
- function patch(id, input) {
35847
- const existing = nodes.get(id);
35848
- if (!existing) return null;
35849
- const updated = normalizePatch(existing, input, now());
35850
- nodes.set(id, updated);
35851
- return updated;
35852
- }
35853
- function deleteNode(id) {
35854
- if (!nodes.has(id)) return false;
35855
- nodes.delete(id);
35856
- return true;
35857
- }
35858
- function deleteByTheater(theaterId) {
35859
- let deleted = 0;
35860
- for (const node of Array.from(nodes.values())) {
35861
- if (node.theaterId !== theaterId) continue;
35862
- nodes.delete(node.id);
35863
- deleted += 1;
35864
- }
35865
- deleteGroupsByTheater(theaterId);
35866
- return deleted;
35867
- }
35868
- function replace(nextNodes) {
35869
- nodes.clear();
35870
- const validNodes = sanitizeReplacementNodes(nextNodes);
35871
- for (const node of validNodes) nodes.set(node.id, node);
35872
- }
35873
- function createGroup(input) {
35874
- const id = input.id ?? crypto6.randomUUID();
35875
- if (groups.has(id)) throw new Error("group_exists");
35876
- const order = input.order ?? groups.size;
35877
- const group = {
35878
- id,
35879
- theaterId: input.theaterId,
35880
- name: input.name.trim().slice(0, MAX_GROUP_NAME_LENGTH) || "Group",
35881
- color: input.color,
35882
- order,
35883
- createdAt: now()
35884
- };
35885
- groups.set(id, group);
35886
- return group;
35887
- }
35888
- function updateGroup(id, input) {
35889
- const existing = groups.get(id);
35890
- if (!existing) return null;
35891
- const updated = {
35892
- ...existing,
35893
- ...input.name !== void 0 ? { name: input.name.trim().slice(0, MAX_GROUP_NAME_LENGTH) || existing.name } : {},
35894
- ...input.color !== void 0 ? { color: input.color } : {},
35895
- ...input.order !== void 0 ? { order: input.order } : {}
35896
- };
35897
- groups.set(id, updated);
35898
- return updated;
35899
- }
35900
- function deleteGroup(id) {
35901
- if (!groups.has(id)) return false;
35902
- groups.delete(id);
35903
- for (const [nodeId, node] of nodes.entries()) {
35904
- if (node.groupId === id) nodes.set(nodeId, { ...node, groupId: null });
35905
- }
35906
- return true;
35907
- }
35908
- function listGroups(theaterId) {
35909
- return Array.from(groups.values()).filter((g) => g.theaterId === theaterId).sort((a, b) => a.order - b.order || a.createdAt - b.createdAt);
35910
- }
35911
- function listAllGroups() {
35912
- return Array.from(groups.values()).sort((a, b) => a.order - b.order || a.createdAt - b.createdAt);
35913
- }
35914
- function deleteGroupsByTheater(theaterId) {
35915
- let deleted = 0;
35916
- for (const [id, group] of Array.from(groups.entries())) {
35917
- if (group.theaterId !== theaterId) continue;
35918
- groups.delete(id);
35919
- deleted += 1;
35920
- }
35921
- return deleted;
35922
- }
35923
- function replaceGroups(nextGroups) {
35924
- groups.clear();
35925
- for (const group of nextGroups) {
35926
- if (!groups.has(group.id)) groups.set(group.id, group);
35927
- }
35928
- }
35929
- return { list, listByTheater, get, create: create2, upsert, patch, delete: deleteNode, deleteByTheater, replace, createGroup, updateGroup, deleteGroup, listGroups, listAllGroups, deleteGroupsByTheater, replaceGroups };
35930
- }
35931
- function normalizeCreateInput(input, id, timestamp) {
35932
- return {
35933
- id,
35934
- theaterId: input.theaterId,
35935
- type: input.type,
35936
- pluginId: input.pluginId,
35937
- title: input.title.trim() || "Untitled Operation",
35938
- ...input.accent ? { accent: input.accent.trim() } : {},
35939
- ...input.groupId !== void 0 ? { groupId: input.groupId } : {},
35940
- payload: input.payload ?? {},
35941
- geometry: input.geometry ?? null,
35942
- ts: {
35943
- createdAt: input.createdAt ?? timestamp,
35944
- updatedAt: timestamp
35945
- }
35946
- };
35947
- }
35948
- function normalizePatch(existing, input, timestamp) {
35949
- const title = input.title?.trim();
35950
- return {
35951
- ...existing,
35952
- ...title !== void 0 ? { title: title.length > 0 ? title : existing.title } : {},
35953
- ...input.accent !== void 0 ? { accent: input.accent && input.accent.trim() ? input.accent.trim() : void 0 } : {},
35954
- ...input.groupId !== void 0 ? { groupId: input.groupId } : {},
35955
- ...input.payload !== void 0 ? { payload: input.payload } : {},
35956
- ...input.geometry !== void 0 ? { geometry: input.geometry } : {},
35957
- ts: { ...existing.ts, updatedAt: timestamp }
35958
- };
35959
- }
35960
- function compareOperationNodes(a, b) {
35961
- return a.ts.createdAt - b.ts.createdAt || a.id.localeCompare(b.id);
35962
- }
35963
- function sanitizeReplacementNodes(nextNodes) {
35964
- const seen = /* @__PURE__ */ new Set();
35965
- const result = [];
35966
- for (const node of nextNodes) {
35967
- if (!seen.has(node.id)) {
35968
- seen.add(node.id);
35969
- result.push(node);
35970
- }
35971
- }
35972
- return result;
35973
- }
35974
- function createOperationsRouter(deps) {
35975
- return async ({ req, res, pathname }) => {
35976
- if (pathname === "/api/v1/operations") {
35977
- await handleCollection(req, res, deps);
35978
- return true;
35979
- }
35980
- if (pathname === "/api/v1/operations/catalog") {
35981
- if (req.method !== "GET") {
35982
- deps.writeJson(res, 405, { error: "Method not allowed" });
35983
- return true;
35984
- }
35985
- deps.writeJson(res, 200, deps.resolveLaunchCatalog ? await deps.resolveLaunchCatalog() : { plugins: [] });
35986
- return true;
35987
- }
35988
- if (pathname === "/api/v1/operations/events") {
35989
- if (req.method !== "GET") {
35990
- deps.writeJson(res, 405, { error: "Method not allowed" });
35991
- return true;
35992
- }
35993
- deps.subscribeOperationSse?.(req, res);
35994
- return true;
35995
- }
35996
- if (pathname === "/api/v1/operations/groups") {
35997
- await handleGroupCollection(req, res, deps);
35998
- return true;
35999
- }
36000
- const groupItemMatch = pathname.match(/^\/api\/v1\/operations\/groups\/([^/]+)$/);
36001
- if (groupItemMatch) {
36002
- await handleGroupItem(req, res, decodeURIComponent(groupItemMatch[1] ?? ""), deps);
36003
- return true;
36004
- }
36005
- const itemMatch = pathname.match(/^\/api\/v1\/operations\/([^/]+)$/);
36006
- if (itemMatch) {
36007
- await handleItem(req, res, decodeURIComponent(itemMatch[1] ?? ""), deps);
36008
- return true;
36009
- }
36010
- return false;
36011
- };
36012
- }
36013
- async function handleCollection(req, res, deps) {
36014
- if (req.method === "GET") {
36015
- const theaterId = readRequestUrl(req).searchParams.get("theaterId");
36016
- const nodes = theaterId ? deps.store.listByTheater(theaterId) : deps.store.list();
36017
- deps.writeJson(res, 200, { operations: nodes.map((node) => sanitizeOperationNode(node, deps)) });
36018
- return;
36019
- }
36020
- if (req.method !== "POST") {
36021
- deps.writeJson(res, 405, { error: "Method not allowed" });
36022
- return;
36023
- }
36024
- if (!deps.isAuthorized(req)) {
36025
- deps.writeJson(res, 401, { error: "unauthorized" });
36026
- return;
36027
- }
36028
- const body2 = await deps.readJsonBody(req);
36029
- if (!body2 || typeof body2.theaterId !== "string" || typeof body2.type !== "string" || typeof body2.pluginId !== "string" || typeof body2.title !== "string") {
36030
- deps.writeJson(res, 400, { error: "invalid_operation" });
36031
- return;
36032
- }
36033
- if (body2.accent !== void 0 && typeof body2.accent !== "string") {
36034
- deps.writeJson(res, 400, { error: "invalid_operation_accent" });
36035
- return;
36036
- }
36037
- if (typeof body2.id === "string" && deps.isPendingDeletion?.(body2.id)) {
36038
- deps.writeJson(res, 409, { error: "pending_deletion" });
36039
- return;
36040
- }
36041
- try {
36042
- const node = deps.store.create({
36043
- id: typeof body2.id === "string" ? body2.id : void 0,
36044
- theaterId: body2.theaterId,
36045
- type: body2.type,
36046
- pluginId: body2.pluginId,
36047
- title: body2.title,
36048
- ...typeof body2.accent === "string" ? { accent: body2.accent } : {},
36049
- payload: isRecord4(body2.payload) ? body2.payload : {},
36050
- geometry: isRecord4(body2.geometry) ? readGeometry(body2.geometry) : null
36051
- });
36052
- deps.persist();
36053
- deps.writeJson(res, 201, { operation: sanitizeOperationNode(node, deps) });
36054
- } catch (error51) {
36055
- deps.writeJson(res, 400, { error: error51 instanceof Error ? error51.message : "invalid_operation" });
36056
- }
36057
- }
36058
- async function handleItem(req, res, id, deps) {
36059
- if (req.method === "GET") {
36060
- const node = deps.store.get(id);
36061
- deps.writeJson(res, node ? 200 : 404, node ? { operation: sanitizeOperationNode(node, deps) } : { error: "operation_not_found" });
36062
- return;
36063
- }
36064
- if (req.method !== "PATCH" && req.method !== "DELETE") {
36065
- deps.writeJson(res, 405, { error: "Method not allowed" });
36066
- return;
36067
- }
36068
- if (!deps.isAuthorized(req)) {
36069
- deps.writeJson(res, 401, { error: "unauthorized" });
36070
- return;
36071
- }
36072
- if (req.method === "DELETE") {
36073
- const deletion = deps.deleteOperation(id);
36074
- deps.writeJson(res, 200, { ok: true, deletion });
36075
- return;
36076
- }
36077
- const body2 = await deps.readJsonBody(req);
36078
- if (!body2) {
36079
- deps.writeJson(res, 400, { error: "invalid_operation_patch" });
36080
- return;
36081
- }
36082
- if (body2.accent !== void 0 && body2.accent !== null && typeof body2.accent !== "string") {
36083
- deps.writeJson(res, 400, { error: "invalid_operation_accent" });
36084
- return;
36085
- }
36086
- if (body2.groupId !== void 0 && body2.groupId !== null && typeof body2.groupId !== "string") {
36087
- deps.writeJson(res, 400, { error: "invalid_operation_groupId" });
36088
- return;
36089
- }
36090
- try {
36091
- const previousNode = typeof body2.title === "string" ? deps.store.get(id) : null;
36092
- const node = deps.store.patch(id, {
36093
- ...typeof body2.title === "string" ? { title: body2.title } : {},
36094
- ...typeof body2.accent === "string" || body2.accent === null ? { accent: body2.accent } : {},
36095
- ...typeof body2.groupId === "string" || body2.groupId === null ? { groupId: body2.groupId } : {},
36096
- ...isRecord4(body2.payload) ? { payload: body2.payload } : {},
36097
- ...isRecord4(body2.geometry) || body2.geometry === null ? { geometry: body2.geometry === null ? null : readGeometry(body2.geometry) } : {}
36098
- });
36099
- if (!node) {
36100
- deps.writeJson(res, 404, { error: "operation_not_found" });
36101
- return;
36102
- }
36103
- deps.persist();
36104
- if (previousNode && typeof body2.title === "string") {
36105
- deps.publishRenameEvent?.({
36106
- operationId: node.id,
36107
- pluginId: node.pluginId,
36108
- type: node.type,
36109
- title: body2.title.trim() === "" ? "" : node.title,
36110
- previousTitle: previousNode.title
36111
- });
36112
- }
36113
- deps.broadcastOperationChanged?.(deps.store.get(id) ?? node);
36114
- deps.writeJson(res, 200, { operation: sanitizeOperationNode(node, deps) });
36115
- } catch (error51) {
36116
- deps.writeJson(res, 400, { error: error51 instanceof Error ? error51.message : "invalid_operation_patch" });
36117
- }
36118
- }
36119
- function sanitizeOperationNode(node, deps) {
36120
- return createSanitizedOpDto(node, { sensitiveFields: deps.getPluginSensitiveFields?.(node.pluginId) ?? [] });
36121
- }
36122
- async function handleGroupCollection(req, res, deps) {
36123
- if (req.method === "GET") {
36124
- const theaterId = readRequestUrl(req).searchParams.get("theaterId");
36125
- const groups = theaterId ? deps.store.listGroups(theaterId) : deps.store.listAllGroups();
36126
- deps.writeJson(res, 200, { groups });
36127
- return;
36128
- }
36129
- if (req.method !== "POST") {
36130
- deps.writeJson(res, 405, { error: "Method not allowed" });
36131
- return;
36132
- }
36133
- if (!deps.isAuthorized(req)) {
36134
- deps.writeJson(res, 401, { error: "unauthorized" });
36135
- return;
36136
- }
36137
- const body2 = await deps.readJsonBody(req);
36138
- if (!body2 || typeof body2.theaterId !== "string" || typeof body2.name !== "string" || typeof body2.color !== "string") {
36139
- deps.writeJson(res, 400, { error: "invalid_group" });
36140
- return;
36141
- }
36142
- try {
36143
- const group = deps.store.createGroup({
36144
- theaterId: body2.theaterId,
36145
- name: body2.name,
36146
- color: body2.color,
36147
- ...typeof body2.order === "number" ? { order: body2.order } : {}
36148
- });
36149
- deps.persist();
36150
- deps.writeJson(res, 201, { group });
36151
- } catch (error51) {
36152
- deps.writeJson(res, 400, { error: error51 instanceof Error ? error51.message : "invalid_group" });
36153
- }
36154
- }
36155
- async function handleGroupItem(req, res, id, deps) {
36156
- if (req.method !== "PATCH" && req.method !== "DELETE") {
36157
- deps.writeJson(res, 405, { error: "Method not allowed" });
36158
- return;
36159
- }
36160
- if (!deps.isAuthorized(req)) {
36161
- deps.writeJson(res, 401, { error: "unauthorized" });
36162
- return;
36163
- }
36164
- if (req.method === "DELETE") {
36165
- const deleted = deps.store.deleteGroup(id);
36166
- if (deleted) deps.persist();
36167
- deps.writeJson(res, deleted ? 200 : 404, deleted ? { ok: true } : { error: "group_not_found" });
36168
- return;
36169
- }
36170
- const body2 = await deps.readJsonBody(req);
36171
- if (!body2) {
36172
- deps.writeJson(res, 400, { error: "invalid_group_patch" });
36173
- return;
36174
- }
36175
- const group = deps.store.updateGroup(id, {
36176
- ...typeof body2.name === "string" ? { name: body2.name } : {},
36177
- ...typeof body2.color === "string" ? { color: body2.color } : {},
36178
- ...typeof body2.order === "number" ? { order: body2.order } : {}
36179
- });
36180
- if (!group) {
36181
- deps.writeJson(res, 404, { error: "group_not_found" });
36182
- return;
36183
- }
36184
- deps.persist();
36185
- deps.writeJson(res, 200, { group });
36186
- }
36187
- function readGeometry(value) {
36188
- const x = readFiniteNumber(value.x);
36189
- const y = readFiniteNumber(value.y);
36190
- const width = readFiniteNumber(value.width);
36191
- const height = readFiniteNumber(value.height);
36192
- const zIndex = readFiniteNumber(value.zIndex);
36193
- if (x === null || y === null || width === null || height === null || zIndex === null) return null;
36194
- return { x, y, width, height, zIndex };
36195
- }
36196
- function readFiniteNumber(value) {
36197
- return typeof value === "number" && Number.isFinite(value) ? value : null;
36198
- }
36199
- function readRequestUrl(req) {
36200
- return new URL(req.url ?? "/", "http://127.0.0.1");
36201
- }
36202
-
36203
- // core/host/deferred-deletion.ts
36204
36356
  var DeferredDeletionError = class extends Error {
36205
36357
  status;
36206
36358
  constructor(status, message) {
@@ -36215,7 +36367,7 @@ var OPERATION_PURGED_EVENT_CHANNEL = "operation:purged";
36215
36367
  var PURGE_RETRY_MS = 1e3;
36216
36368
  function createDeferredDeletionCoordinator(deps) {
36217
36369
  const now = deps.now ?? Date.now;
36218
- const randomId = deps.randomId ?? crypto6.randomUUID;
36370
+ const randomId = deps.randomId ?? crypto5.randomUUID;
36219
36371
  const setTimer = deps.setTimer ?? setTimeout;
36220
36372
  const clearTimer = deps.clearTimer ?? clearTimeout;
36221
36373
  let tombstones = [];
@@ -36871,40 +37023,51 @@ function isSafeCaptureId(value) {
36871
37023
  return value.length > 0 && path18__default.basename(value) === value && !value.includes(path18__default.sep) && !value.includes(path18__default.posix.sep);
36872
37024
  }
36873
37025
  var ENDPOINT_FILE = "listener.json";
36874
- var STORE_VERSION2 = 1;
37026
+ var STORE_VERSION2 = 2;
36875
37027
  function createRemoteEndpointStore(consoleDir, deps = {}) {
36876
37028
  const storage = createRemoteStorage(consoleDir, deps);
36877
37029
  const readFile7 = deps.readFile ?? ((file2) => fs9__default.readFileSync(file2, "utf8"));
36878
- let port = read();
37030
+ let endpoint = read();
36879
37031
  function read() {
36880
37032
  try {
36881
37033
  const parsed = JSON.parse(readFile7(storage.path(ENDPOINT_FILE)));
36882
- if (parsed?.version !== STORE_VERSION2) return null;
36883
- return isBindablePort(parsed.port) ? parsed.port : null;
37034
+ if (parsed?.version === 1 && isBindablePort2(parsed.port)) {
37035
+ return { listenPort: parsed.port, advertisedPort: parsed.port };
37036
+ }
37037
+ if (parsed?.version !== STORE_VERSION2 || !isBindablePort2(parsed.listenPort) || !isBindablePort2(parsed.advertisedPort)) return null;
37038
+ return { listenPort: parsed.listenPort, advertisedPort: parsed.advertisedPort };
36884
37039
  } catch {
36885
37040
  return null;
36886
37041
  }
36887
37042
  }
36888
37043
  function write(next) {
36889
- if (next === port) return;
36890
- const body2 = { version: STORE_VERSION2, port: next };
37044
+ if (sameEndpoint(next, endpoint)) return;
37045
+ const body2 = {
37046
+ version: STORE_VERSION2,
37047
+ listenPort: next?.listenPort ?? null,
37048
+ advertisedPort: next?.advertisedPort ?? null
37049
+ };
36891
37050
  storage.write(ENDPOINT_FILE, `${JSON.stringify(body2, null, 2)}
36892
37051
  `);
36893
- port = next;
37052
+ endpoint = next;
36894
37053
  }
36895
37054
  return {
36896
- read: () => port,
37055
+ read: () => endpoint,
36897
37056
  remember: (next) => {
36898
- if (isBindablePort(next)) write(next);
37057
+ const normalized = typeof next === "number" ? { listenPort: next, advertisedPort: next } : next;
37058
+ if (isBindablePort2(normalized.listenPort) && isBindablePort2(normalized.advertisedPort)) write(normalized);
36899
37059
  },
36900
37060
  forget: () => write(null)
36901
37061
  };
36902
37062
  }
36903
- function isBindablePort(value) {
37063
+ function sameEndpoint(left, right) {
37064
+ return left?.listenPort === right?.listenPort && left?.advertisedPort === right?.advertisedPort;
37065
+ }
37066
+ function isBindablePort2(value) {
36904
37067
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= 65535;
36905
37068
  }
36906
37069
 
36907
- // core/host/access-link.ts
37070
+ // access-protocol/index.ts
36908
37071
  var ACCESS_LINK_SCHEME = "fleet:";
36909
37072
  var ACCESS_LINK_HOST = "join";
36910
37073
  var ACCESS_LINK_PREFIX = "fleet://join?code=";
@@ -36915,6 +37078,8 @@ var MAX_ACCESS_LINK_LENGTH = 4096;
36915
37078
  var ACCESS_TOKEN = /^[A-Za-z0-9_-]{16,512}$/u;
36916
37079
  var CERTIFICATE_FINGERPRINT = /^[0-9A-F]{64}$/u;
36917
37080
  var PAYLOAD_KEYS = ["v", "endpoint", "token", "fingerprint", "label"];
37081
+ var BASE64_URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
37082
+ var BASE64_URL_DECODE = createBase64UrlDecodeTable();
36918
37083
  function encodeAccessLink(payload) {
36919
37084
  const envelope = {
36920
37085
  v: ACCESS_LINK_VERSION,
@@ -36923,13 +37088,13 @@ function encodeAccessLink(payload) {
36923
37088
  fingerprint: normalizeFingerprint(payload.fingerprint),
36924
37089
  label: sanitizeAccessLabel(payload.label)
36925
37090
  };
36926
- return `${ACCESS_LINK_PREFIX}${toBase64Url(JSON.stringify(envelope))}`;
37091
+ return `${ACCESS_LINK_PREFIX}${toBase64Url(new TextEncoder().encode(JSON.stringify(envelope)))}`;
36927
37092
  }
36928
37093
  function parseAccessLink(input) {
36929
37094
  if (typeof input !== "string") throw invalidLink();
36930
37095
  const candidate = input.trim();
36931
37096
  if (candidate.length === 0 || candidate.length > MAX_ACCESS_LINK_LENGTH) throw invalidLink();
36932
- if (/[\u0000-\u001f\u007f\s]/u.test(candidate)) throw invalidLink();
37097
+ if (containsDisallowedLinkWhitespace(candidate)) throw invalidLink();
36933
37098
  let url2;
36934
37099
  try {
36935
37100
  url2 = new URL(candidate);
@@ -36937,7 +37102,7 @@ function parseAccessLink(input) {
36937
37102
  throw invalidLink();
36938
37103
  }
36939
37104
  if (url2.protocol !== ACCESS_LINK_SCHEME || url2.username || url2.password || url2.hash) throw invalidLink();
36940
- if (url2.hostname !== ACCESS_LINK_HOST || url2.port !== "") throw invalidLink();
37105
+ if (url2.hostname !== ACCESS_LINK_HOST || url2.port !== "" || url2.pathname !== "") throw invalidLink();
36941
37106
  const keys = [...url2.searchParams.keys()];
36942
37107
  if (keys.length !== 1 || keys[0] !== "code") throw invalidLink();
36943
37108
  return readPayload(decodePayload(url2.searchParams.get("code") ?? ""));
@@ -36946,7 +37111,7 @@ function decodePayload(code) {
36946
37111
  if (!/^[A-Za-z0-9_-]+$/u.test(code)) throw invalidLink();
36947
37112
  let text;
36948
37113
  try {
36949
- text = Buffer.from(code, "base64url").toString("utf8");
37114
+ text = new TextDecoder("utf-8", { fatal: true }).decode(fromBase64Url(code));
36950
37115
  } catch {
36951
37116
  throw invalidLink();
36952
37117
  }
@@ -37002,10 +37167,61 @@ function normalizeFingerprint(value) {
37002
37167
  return value.replace(/[^0-9a-fA-F]/gu, "").toUpperCase();
37003
37168
  }
37004
37169
  function sanitizeAccessLabel(value) {
37005
- return value.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/gu, "").replace(/\s+/gu, " ").trim().slice(0, MAX_ACCESS_LABEL_LENGTH);
37170
+ return Array.from(value).filter((character) => !isDisallowedAccessLabelCharacter(character.codePointAt(0))).join("").replace(/\s+/gu, " ").trim().slice(0, MAX_ACCESS_LABEL_LENGTH);
37171
+ }
37172
+ function containsDisallowedLinkWhitespace(value) {
37173
+ for (const character of value) {
37174
+ const codePoint = character.codePointAt(0);
37175
+ if (codePoint <= 31 || codePoint === 127 || isWhitespaceCharacter(character)) return true;
37176
+ }
37177
+ return false;
37006
37178
  }
37007
- function toBase64Url(value) {
37008
- return Buffer.from(value, "utf8").toString("base64url");
37179
+ function isWhitespaceCharacter(character) {
37180
+ return character.trim().length === 0;
37181
+ }
37182
+ function isDisallowedAccessLabelCharacter(codePoint) {
37183
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint >= 8203 && codePoint <= 8207 || codePoint === 8232 || codePoint === 8233 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297;
37184
+ }
37185
+ function toBase64Url(bytes) {
37186
+ let encoded = "";
37187
+ for (let index = 0; index < bytes.length; index += 3) {
37188
+ const first = bytes[index];
37189
+ const second = bytes[index + 1];
37190
+ const third = bytes[index + 2];
37191
+ encoded += BASE64_URL_ALPHABET[first >> 2];
37192
+ encoded += BASE64_URL_ALPHABET[(first & 3) << 4 | (second ?? 0) >> 4];
37193
+ if (second !== void 0) encoded += BASE64_URL_ALPHABET[(second & 15) << 2 | (third ?? 0) >> 6];
37194
+ if (third !== void 0) encoded += BASE64_URL_ALPHABET[third & 63];
37195
+ }
37196
+ return encoded;
37197
+ }
37198
+ function fromBase64Url(value) {
37199
+ if (value.length % 4 === 1) throw invalidLink();
37200
+ const bytes = new Uint8Array(Math.floor(value.length * 6 / 8));
37201
+ let buffer = 0;
37202
+ let bits = 0;
37203
+ let offset = 0;
37204
+ for (const character of value) {
37205
+ const decoded = BASE64_URL_DECODE[character.charCodeAt(0)] ?? -1;
37206
+ if (decoded < 0) throw invalidLink();
37207
+ buffer = buffer << 6 | decoded;
37208
+ bits += 6;
37209
+ if (bits >= 8) {
37210
+ bits -= 8;
37211
+ bytes[offset] = buffer >> bits & 255;
37212
+ offset += 1;
37213
+ }
37214
+ }
37215
+ if (bits > 0 && (buffer & (1 << bits) - 1) !== 0) throw invalidLink();
37216
+ return bytes;
37217
+ }
37218
+ function createBase64UrlDecodeTable() {
37219
+ const table = new Int16Array(128);
37220
+ table.fill(-1);
37221
+ for (let index = 0; index < BASE64_URL_ALPHABET.length; index += 1) {
37222
+ table[BASE64_URL_ALPHABET.charCodeAt(index)] = index;
37223
+ }
37224
+ return table;
37009
37225
  }
37010
37226
  function invalidLink() {
37011
37227
  return new Error("pairing_target_invalid");
@@ -37042,7 +37258,7 @@ function createRemoteIdentityStore(consoleDir, deps = {}) {
37042
37258
  async function rotate(host) {
37043
37259
  const { certificatePem, privateKeyPem } = await generate(host);
37044
37260
  const createdAt = now();
37045
- const expiresAt = Date.parse(new crypto6.X509Certificate(certificatePem).validTo);
37261
+ const expiresAt = Date.parse(new crypto5.X509Certificate(certificatePem).validTo);
37046
37262
  storage.write(KEY_FILE, privateKeyPem);
37047
37263
  storage.write(CERTIFICATE_FILE, certificatePem);
37048
37264
  storage.write(META_FILE, `${JSON.stringify({ host, createdAt, expiresAt })}
@@ -37059,12 +37275,12 @@ function createRemoteIdentityStore(consoleDir, deps = {}) {
37059
37275
  return { ensure, read, rotate };
37060
37276
  }
37061
37277
  function fingerprintOf(certificatePem) {
37062
- return new crypto6.X509Certificate(certificatePem).fingerprint256;
37278
+ return new crypto5.X509Certificate(certificatePem).fingerprint256;
37063
37279
  }
37064
37280
  function fingerprintsMatch(left, right) {
37065
37281
  const a = Buffer.from(normalizeFingerprint(left), "hex");
37066
37282
  const b = Buffer.from(normalizeFingerprint(right), "hex");
37067
- return a.length > 0 && a.length === b.length && crypto6.timingSafeEqual(a, b);
37283
+ return a.length > 0 && a.length === b.length && crypto5.timingSafeEqual(a, b);
37068
37284
  }
37069
37285
  async function generateSelfSignedCertificate(host) {
37070
37286
  const isIpAddress = /^\d{1,3}(?:\.\d{1,3}){3}$/u.test(host);
@@ -37091,7 +37307,7 @@ function createRemoteHostStore(consoleDir, deps = {}) {
37091
37307
  const storage = createRemoteStorage(consoleDir, deps);
37092
37308
  const readFile7 = deps.readFile ?? ((file2) => fs9__default.readFileSync(file2, "utf8"));
37093
37309
  const now = deps.now ?? Date.now;
37094
- const randomId = deps.randomId ?? (() => crypto6.randomUUID());
37310
+ const randomId = deps.randomId ?? (() => crypto5.randomUUID());
37095
37311
  const pending = /* @__PURE__ */ new Map();
37096
37312
  let hosts = read();
37097
37313
  function read() {
@@ -37166,6 +37382,80 @@ function isRecord6(value) {
37166
37382
  const entry = value;
37167
37383
  return typeof entry.id === "string" && entry.id.length > 0 && typeof entry.label === "string" && typeof entry.origin === "string" && entry.origin.startsWith("https://") && typeof entry.hostname === "string" && entry.hostname.length > 0 && typeof entry.port === "number" && Number.isSafeInteger(entry.port) && typeof entry.fingerprint === "string" && /^[0-9A-F]{64}$/u.test(entry.fingerprint) && typeof entry.addedAt === "number" && (entry.lastOpenedAt === null || typeof entry.lastOpenedAt === "number");
37168
37384
  }
37385
+
37386
+ // core/host/remote-join-guard.ts
37387
+ var DEFAULT_FAILURE_LIMIT = 10;
37388
+ var DEFAULT_WINDOW_MS = 6e4;
37389
+ var DEFAULT_SOURCE_SLOTS = 1024;
37390
+ var DEFAULT_CONCURRENCY = 8;
37391
+ function createRemoteJoinGuard(deps = {}) {
37392
+ const now = deps.now ?? (() => Date.now());
37393
+ const failureLimit = deps.failureLimit ?? DEFAULT_FAILURE_LIMIT;
37394
+ const windowMs = deps.windowMs ?? DEFAULT_WINDOW_MS;
37395
+ const sourceSlots = deps.sourceSlots ?? DEFAULT_SOURCE_SLOTS;
37396
+ const concurrency = deps.concurrency ?? DEFAULT_CONCURRENCY;
37397
+ const buckets = /* @__PURE__ */ new Map();
37398
+ let inFlight = 0;
37399
+ let rejected = 0;
37400
+ let lastRejectedAt = null;
37401
+ function liveBucket(source) {
37402
+ const bucket = buckets.get(source);
37403
+ if (bucket === void 0) return null;
37404
+ if (bucket.resetAt > now()) return bucket;
37405
+ buckets.delete(source);
37406
+ return null;
37407
+ }
37408
+ function openBucket(source) {
37409
+ const live = liveBucket(source);
37410
+ if (live !== null) return live;
37411
+ while (buckets.size >= sourceSlots) {
37412
+ const oldest = buckets.keys().next();
37413
+ if (oldest.done === true) break;
37414
+ buckets.delete(oldest.value);
37415
+ }
37416
+ const fresh = { failures: 0, resetAt: now() + windowMs };
37417
+ buckets.set(source, fresh);
37418
+ return fresh;
37419
+ }
37420
+ function countRejection() {
37421
+ rejected += 1;
37422
+ lastRejectedAt = now();
37423
+ }
37424
+ return {
37425
+ begin(source) {
37426
+ const bucket = liveBucket(source);
37427
+ if (bucket !== null && bucket.failures >= failureLimit) {
37428
+ countRejection();
37429
+ return "throttled";
37430
+ }
37431
+ if (inFlight >= concurrency) {
37432
+ countRejection();
37433
+ return "busy";
37434
+ }
37435
+ inFlight += 1;
37436
+ return "ok";
37437
+ },
37438
+ settle(source, outcome) {
37439
+ inFlight = Math.max(0, inFlight - 1);
37440
+ if (outcome === "paired") {
37441
+ buckets.delete(source);
37442
+ return;
37443
+ }
37444
+ openBucket(source).failures += 1;
37445
+ },
37446
+ retryAfterSeconds(source) {
37447
+ const bucket = liveBucket(source);
37448
+ if (bucket === null) return Math.ceil(windowMs / 1e3);
37449
+ return Math.max(1, Math.ceil((bucket.resetAt - now()) / 1e3));
37450
+ },
37451
+ stats: () => ({ count: rejected, lastAt: lastRejectedAt })
37452
+ };
37453
+ }
37454
+ function normalizeRemoteJoinSource(address) {
37455
+ if (address === void 0 || address === "") return "unknown";
37456
+ const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/iu.exec(address);
37457
+ return mapped === null ? address : mapped[1];
37458
+ }
37169
37459
  var REMOTE_PROBE_TIMEOUT_MS = 6e3;
37170
37460
  function probeRemoteIdentity(hostname5, port, expected, timeoutMs = REMOTE_PROBE_TIMEOUT_MS) {
37171
37461
  return new Promise((resolve3) => {
@@ -37335,7 +37625,7 @@ var SHIM_NAMED_EXPORTS = {
37335
37625
  "react": ["Activity", "Children", "Component", "Fragment", "Profiler", "PureComponent", "StrictMode", "Suspense", "__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE", "__COMPILER_RUNTIME", "act", "cache", "cacheSignal", "captureOwnerStack", "cloneElement", "createContext", "createElement", "createRef", "forwardRef", "isValidElement", "lazy", "memo", "startTransition", "unstable_useCacheRefresh", "use", "useActionState", "useCallback", "useContext", "useDebugValue", "useDeferredValue", "useEffect", "useEffectEvent", "useId", "useImperativeHandle", "useInsertionEffect", "useLayoutEffect", "useMemo", "useOptimistic", "useReducer", "useRef", "useState", "useSyncExternalStore", "useTransition", "version"],
37336
37626
  "react/jsx-runtime": ["Fragment", "jsx", "jsxs"],
37337
37627
  "@fleet-console/sdk/plugin/browser": ["React", "createClientCapabilities", "defineOperationKind", "definePlugin", "useOperationStatus", "useOperations", "usePluginApi", "usePluginSettings", "usePluginStorage", "useStoreSnapshot"],
37338
- "@fleet-console/sdk/settings/browser": ["SettingsCard", "SettingsField", "SettingsRow", "SettingsSelect", "SettingsToggle", "defineSettingsSection"],
37628
+ "@fleet-console/sdk/settings/browser": ["ExperimentalBadge", "SettingsCard", "SettingsField", "SettingsRow", "SettingsSelect", "SettingsToggle", "defineSettingsSection"],
37339
37629
  "@fleet-console/sdk/operations/browser": ["ApiError", "OperationBody", "assertOperationNode", "fetchOperationCatalog", "hasForbiddenBrowserPayloadKey"],
37340
37630
  "@fleet-console/sdk/notifications/browser": ["defineNotificationKind"],
37341
37631
  "@fleet-console/sdk/react/browser": ["PluginErrorBoundary", "React", "Select", "useSelect"]
@@ -37611,6 +37901,7 @@ function createFleetPluginHost(deps) {
37611
37901
  const plugins = filterDiscoveredPlugins(discoverFleetPlugins(deps));
37612
37902
  const sensitiveFieldsByPluginId = new Map(plugins.map((plugin) => [plugin.manifest.id, plugin.manifest.sensitiveFields ?? []]));
37613
37903
  const generatedBundleDirs = /* @__PURE__ */ new Set();
37904
+ const apiCatalog = [];
37614
37905
  const isProcessAlive2 = deps.isProcessAlive ?? isPluginBundleOwnerAlive;
37615
37906
  const removeBundleDir = deps.removeBundleDir ?? removePluginBundleDir;
37616
37907
  const importModule = deps.importModule ?? ((entry) => importPluginModule(entry, deps.bundleCacheDir, generatedBundleDirs));
@@ -37643,21 +37934,109 @@ function createFleetPluginHost(deps) {
37643
37934
  }
37644
37935
  }
37645
37936
  }
37646
- return { plugins, sensitiveFieldsByPluginId, boot: boot2, cleanup };
37937
+ return { plugins, sensitiveFieldsByPluginId, apiCatalog, boot: boot2, cleanup };
37647
37938
  async function bootPluginRoutes(plugin) {
37939
+ const pendingRoutes = [];
37940
+ const pendingUpgrades = [];
37941
+ const pendingCatalog = [];
37648
37942
  const mod = await importModule(plugin.routesEntry);
37649
37943
  const register = resolveRegister(mod);
37650
37944
  if (!register) return;
37651
- await register({
37652
- pluginId: plugin.manifest.id,
37653
- manifest: plugin.manifest,
37654
- basePath: `/plugins/${plugin.manifest.id}`,
37655
- wsBasePath: `/plugins/${plugin.manifest.id}/ws`,
37656
- host: deps.host,
37657
- registerRouter: createScopedRouteRegistrar(deps.routes, `/plugins/${plugin.manifest.id}`, `/api/v1/plugins/${plugin.manifest.id}`),
37658
- registerWsHandler: createScopedUpgradeRegistrar(deps.upgrades, `/plugins/${plugin.manifest.id}/ws`)
37945
+ const registrationTransaction = createPluginRegistrationTransaction(deps.host);
37946
+ try {
37947
+ await register({
37948
+ pluginId: plugin.manifest.id,
37949
+ manifest: plugin.manifest,
37950
+ basePath: `/plugins/${plugin.manifest.id}`,
37951
+ wsBasePath: `/plugins/${plugin.manifest.id}/ws`,
37952
+ host: registrationTransaction.host,
37953
+ registerRouter: createScopedRouteRegistrar(
37954
+ deps.routes,
37955
+ apiCatalog,
37956
+ pendingRoutes,
37957
+ pendingCatalog,
37958
+ `/plugins/${plugin.manifest.id}`,
37959
+ `/api/v1/plugins/${plugin.manifest.id}`
37960
+ ),
37961
+ registerWsHandler: createScopedUpgradeRegistrar(
37962
+ deps.upgrades,
37963
+ apiCatalog,
37964
+ pendingUpgrades,
37965
+ pendingCatalog,
37966
+ `/plugins/${plugin.manifest.id}/ws`
37967
+ )
37968
+ });
37969
+ } catch (error51) {
37970
+ await registrationTransaction.rollback();
37971
+ throw error51;
37972
+ }
37973
+ for (const route of pendingRoutes) deps.routes.register(route.prefix, route.handler);
37974
+ for (const upgrade of pendingUpgrades) deps.upgrades.register(upgrade.prefix, upgrade.handler);
37975
+ apiCatalog.push(...pendingCatalog);
37976
+ }
37977
+ }
37978
+ function createPluginRegistrationTransaction(host) {
37979
+ const rollbackActions = [];
37980
+ function track(disposer) {
37981
+ let active = true;
37982
+ const trackedDisposer = () => {
37983
+ if (!active) return;
37984
+ active = false;
37985
+ disposer();
37986
+ };
37987
+ rollbackActions.push(trackedDisposer);
37988
+ return trackedDisposer;
37989
+ }
37990
+ function trackCleanup(cleanup) {
37991
+ const unregister = host.lifecycle.registerCleanup(cleanup);
37992
+ let active = true;
37993
+ const trackedUnregister = () => {
37994
+ if (!active) return;
37995
+ active = false;
37996
+ unregister();
37997
+ };
37998
+ rollbackActions.push(async () => {
37999
+ if (!active) return;
38000
+ active = false;
38001
+ try {
38002
+ await cleanup();
38003
+ } catch {
38004
+ }
38005
+ try {
38006
+ unregister();
38007
+ } catch {
38008
+ }
37659
38009
  });
38010
+ return trackedUnregister;
37660
38011
  }
38012
+ return {
38013
+ host: {
38014
+ ...host,
38015
+ operations: {
38016
+ ...host.operations,
38017
+ registerOperationType: (type) => track(host.operations.registerOperationType(type)),
38018
+ registerPayloadSanitizer: (pluginId, fields) => track(host.operations.registerPayloadSanitizer(pluginId, fields)),
38019
+ registerLaunchCatalog: (pluginId, provider) => track(host.operations.registerLaunchCatalog(pluginId, provider))
38020
+ },
38021
+ events: {
38022
+ ...host.events,
38023
+ subscribe: (channel, listener) => track(host.events.subscribe(channel, listener)),
38024
+ registerSseChannel: (channel) => track(host.events.registerSseChannel(channel))
38025
+ },
38026
+ lifecycle: {
38027
+ ...host.lifecycle,
38028
+ registerCleanup: trackCleanup
38029
+ }
38030
+ },
38031
+ rollback: async () => {
38032
+ for (let index = rollbackActions.length - 1; index >= 0; index -= 1) {
38033
+ try {
38034
+ await rollbackActions[index]();
38035
+ } catch {
38036
+ }
38037
+ }
38038
+ }
38039
+ };
37661
38040
  }
37662
38041
  function filterDiscoveredPlugins(plugins) {
37663
38042
  const accepted = [];
@@ -37800,19 +38179,73 @@ function resolveRegister(mod) {
37800
38179
  if (typeof mod.default === "function") return mod.default;
37801
38180
  return mod.default?.register ?? null;
37802
38181
  }
37803
- function createScopedRouteRegistrar(routes, basePath, apiBasePath) {
37804
- return (requestedPath, handler) => {
38182
+ function createScopedRouteRegistrar(routes, apiCatalog, pendingRoutes, pendingCatalog, basePath, apiBasePath) {
38183
+ function registerRouter(requestedPath, handler, catalog) {
37805
38184
  const prefix = resolveScopedPrefix(basePath, requestedPath, apiBasePath);
37806
- assertNoRouteOverlap(prefix, routes.list().map((route) => route.prefix));
37807
- routes.register(prefix, handler);
37808
- };
38185
+ const entries = catalog ? resolveCatalogEntries(prefix, catalog) : [];
38186
+ assertNoCatalogDuplicates([...apiCatalog, ...pendingCatalog], entries);
38187
+ assertNoRouteOverlap(prefix, [
38188
+ ...routes.list().map((route) => route.prefix),
38189
+ ...pendingRoutes.map((route) => route.prefix)
38190
+ ]);
38191
+ pendingRoutes.push({ prefix, handler });
38192
+ pendingCatalog.push(...entries);
38193
+ }
38194
+ return registerRouter;
37809
38195
  }
37810
- function createScopedUpgradeRegistrar(upgrades, basePath) {
37811
- return (requestedPath, handler) => {
38196
+ function createScopedUpgradeRegistrar(upgrades, apiCatalog, pendingUpgrades, pendingCatalog, basePath) {
38197
+ function registerWsHandler(requestedPath, handler, catalog) {
37812
38198
  const prefix = resolveScopedPrefix(basePath, requestedPath);
37813
- assertNoRouteOverlap(prefix, upgrades.list().map((upgrade) => upgrade.prefix));
37814
- upgrades.register(prefix, handler);
37815
- };
38199
+ const entries = catalog ? resolveCatalogEntries(prefix, catalog) : [];
38200
+ assertNoCatalogDuplicates([...apiCatalog, ...pendingCatalog], entries);
38201
+ assertNoRouteOverlap(prefix, [
38202
+ ...upgrades.list().map((upgrade) => upgrade.prefix),
38203
+ ...pendingUpgrades.map((upgrade) => upgrade.prefix)
38204
+ ]);
38205
+ pendingUpgrades.push({ prefix, handler });
38206
+ pendingCatalog.push(...entries);
38207
+ }
38208
+ return registerWsHandler;
38209
+ }
38210
+ var API_CATALOG_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "*"]);
38211
+ var API_CATALOG_GATES = /* @__PURE__ */ new Set(["loopback", "origin-write", "origin-strict", "lock-token", "anthropic-credential", "one-use-ticket"]);
38212
+ var API_CATALOG_TRANSPORTS = /* @__PURE__ */ new Set(["http", "sse", "websocket", "proxy"]);
38213
+ function resolveCatalogEntries(prefix, catalog) {
38214
+ return (Array.isArray(catalog) ? catalog : [catalog]).map((entry) => {
38215
+ assertValidCatalogEntry(entry);
38216
+ return {
38217
+ ...entry,
38218
+ path: resolveCatalogPath(prefix, entry.path)
38219
+ };
38220
+ });
38221
+ }
38222
+ function assertValidCatalogEntry(entry) {
38223
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error("plugin_catalog_entry_invalid");
38224
+ const candidate = entry;
38225
+ if (!API_CATALOG_METHODS.has(candidate.method) || typeof candidate.summary !== "string" || candidate.summary.trim().length === 0 || typeof candidate.category !== "string" || candidate.category.trim().length === 0 || !API_CATALOG_GATES.has(candidate.gate) || !API_CATALOG_TRANSPORTS.has(candidate.transport) || !isValidCatalogPath(candidate.path)) {
38226
+ throw new Error("plugin_catalog_entry_invalid");
38227
+ }
38228
+ }
38229
+ function isValidCatalogPath(value) {
38230
+ if (typeof value !== "string") return false;
38231
+ if (value === "") return true;
38232
+ if (!value.startsWith("/") || value.startsWith("//") || value.includes("//") || /[?#\\]/u.test(value)) return false;
38233
+ return !value.split("/").some((segment) => segment === "." || segment === "..");
38234
+ }
38235
+ function resolveCatalogPath(prefix, suffix) {
38236
+ if (suffix === "") return prefix;
38237
+ return normalizePrefix(`${prefix}${suffix}`);
38238
+ }
38239
+ function assertNoCatalogDuplicates(existing, additions) {
38240
+ const identities = new Set(existing.map(catalogIdentity));
38241
+ for (const entry of additions) {
38242
+ const identity2 = catalogIdentity(entry);
38243
+ if (identities.has(identity2)) throw new Error(`duplicate_api_catalog_entry:${entry.method}:${entry.path}:${entry.transport}`);
38244
+ identities.add(identity2);
38245
+ }
38246
+ }
38247
+ function catalogIdentity(entry) {
38248
+ return `${entry.method}|${entry.path}|${entry.transport}`;
37816
38249
  }
37817
38250
  function resolveScopedPrefix(basePath, requestedPath, apiBasePath) {
37818
38251
  if (requestedPath.split("/").some((segment) => segment === "." || segment === "..")) throw new Error("plugin_route_outside_scope");
@@ -38767,231 +39200,232 @@ var SERVER_API_CATALOG = [
38767
39200
  path: PAIRING_IDENTITY_PATH,
38768
39201
  summary: "Read the loopback runtime pairing identity.",
38769
39202
  category: "Observer",
38770
- gate: "loopback"
39203
+ gate: "loopback",
39204
+ transport: "http"
38771
39205
  },
38772
39206
  {
38773
39207
  method: "GET",
38774
39208
  path: "/api/v1/status",
38775
39209
  summary: "\uCF58\uC194 \uAD00\uCE21 \uC0C1\uD0DC\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
38776
39210
  category: "Observer",
38777
- gate: "loopback"
39211
+ gate: "loopback",
39212
+ transport: "http"
38778
39213
  },
38779
39214
  {
38780
39215
  method: "GET",
38781
39216
  path: "/api/v1/settings/api-catalog",
38782
39217
  summary: "\uBC31\uC5D4\uB4DC API \uCE74\uD0C8\uB85C\uADF8\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
38783
39218
  category: "Observer",
38784
- gate: "loopback"
39219
+ gate: "loopback",
39220
+ transport: "http"
38785
39221
  },
38786
39222
  {
38787
39223
  method: "GET",
38788
39224
  path: "/api/v1/updates/release-notes",
38789
39225
  summary: "Get the console release notes.",
38790
39226
  category: "Update",
38791
- gate: "loopback"
39227
+ gate: "loopback",
39228
+ transport: "http"
38792
39229
  },
38793
39230
  {
38794
39231
  method: "GET",
38795
39232
  path: "/api/v1/theaters",
38796
39233
  summary: "Theater \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
38797
39234
  category: "Observer",
38798
- gate: "loopback"
39235
+ gate: "loopback",
39236
+ transport: "http"
38799
39237
  },
38800
39238
  {
38801
39239
  method: "POST",
38802
39240
  path: "/api/v1/theaters",
38803
39241
  summary: "\uC0C8 Theater\uB97C \uB4F1\uB85D\uD569\uB2C8\uB2E4.",
38804
39242
  category: "Observer",
38805
- gate: "origin-write"
39243
+ gate: "origin-write",
39244
+ transport: "http"
38806
39245
  },
38807
39246
  {
38808
39247
  method: "PATCH",
38809
39248
  path: "/api/v1/theaters/:theaterId",
38810
39249
  summary: "Theater \uD45C\uC2DC \uC21C\uC11C\uB97C \uBCC0\uACBD\uD569\uB2C8\uB2E4.",
38811
39250
  category: "Observer",
38812
- gate: "origin-write"
39251
+ gate: "origin-write",
39252
+ transport: "http"
38813
39253
  },
38814
39254
  {
38815
39255
  method: "DELETE",
38816
39256
  path: "/api/v1/theaters/:theaterId",
38817
39257
  summary: "Theater\uC640 \uC18C\uC18D Operation\uC744 \uC81C\uAC70\uD569\uB2C8\uB2E4.",
38818
39258
  category: "Observer",
38819
- gate: "origin-write"
39259
+ gate: "origin-write",
39260
+ transport: "http"
38820
39261
  },
38821
39262
  {
38822
39263
  method: "POST",
38823
39264
  path: "/api/v1/deletions/:deletionId/restore",
38824
39265
  summary: "\uC720\uC608 \uC911\uC778 Operation \uB610\uB294 Theater \uC0AD\uC81C\uB97C \uBCF5\uAD6C\uD569\uB2C8\uB2E4.",
38825
39266
  category: "Observer",
38826
- gate: "origin-write"
39267
+ gate: "origin-write",
39268
+ transport: "http"
38827
39269
  },
38828
39270
  {
38829
39271
  method: "POST",
38830
39272
  path: "/api/v1/theaters/:theaterId/codex-workspace",
38831
39273
  summary: "Resolve the Codex workspace for a Theater.",
38832
39274
  category: "Observer",
38833
- gate: "origin-write"
38834
- },
38835
- {
38836
- method: "POST",
38837
- path: "/plugins/terminal/shell/ticket",
38838
- summary: "Shell WebSocket \uC811\uC18D \uD2F0\uCF13\uC744 \uBC1C\uAE09\uD569\uB2C8\uB2E4.",
38839
- category: "Terminal Plugin",
38840
- gate: "origin-write"
38841
- },
38842
- {
38843
- method: "GET",
38844
- path: "/plugins/terminal/settings",
38845
- summary: "Get Terminal plugin prompt settings.",
38846
- category: "Terminal Plugin",
38847
- gate: "loopback"
38848
- },
38849
- {
38850
- method: "PUT",
38851
- path: "/plugins/terminal/settings",
38852
- summary: "Save Terminal plugin prompt settings.",
38853
- category: "Terminal Plugin",
38854
- gate: "origin-write"
39275
+ gate: "origin-write",
39276
+ transport: "http"
38855
39277
  },
38856
39278
  {
38857
39279
  method: "POST",
38858
39280
  path: "/api/v1/theaters/folder-listings",
38859
39281
  summary: "Theater \uD3F4\uB354 \uC120\uD0DD \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
38860
39282
  category: "Observer",
38861
- gate: "origin-write"
39283
+ gate: "origin-write",
39284
+ transport: "http"
38862
39285
  },
38863
39286
  {
38864
39287
  method: "POST",
38865
39288
  path: "/api/v1/theaters/folder-grants",
38866
39289
  summary: "Theater \uD3F4\uB354 \uC811\uADFC grant\uB97C \uBC1C\uAE09\uD569\uB2C8\uB2E4.",
38867
39290
  category: "Observer",
38868
- gate: "origin-write"
39291
+ gate: "origin-write",
39292
+ transport: "http"
38869
39293
  },
38870
39294
  {
38871
39295
  method: "POST",
38872
39296
  path: "/api/v1/updates/apply",
38873
39297
  summary: "Request console update application.",
38874
39298
  category: "Update",
38875
- gate: "origin-strict"
39299
+ gate: "origin-strict",
39300
+ transport: "http"
38876
39301
  },
38877
39302
  {
38878
39303
  method: "POST",
38879
39304
  path: "/api/v1/access-grants",
38880
39305
  summary: "Issue a single-use grant that opens a console session.",
38881
39306
  category: "Access",
38882
- gate: "lock-token"
39307
+ gate: "lock-token",
39308
+ transport: "http"
38883
39309
  },
38884
39310
  {
38885
39311
  method: "GET",
38886
39312
  path: "/api/v1/access-links",
38887
39313
  summary: "Report the remote listener, its identity, its unused links, and the devices it has paired.",
38888
39314
  category: "Access",
38889
- gate: "loopback"
39315
+ gate: "loopback",
39316
+ transport: "http"
38890
39317
  },
38891
39318
  {
38892
39319
  method: "DELETE",
38893
39320
  path: "/api/v1/access-links/:linkId",
38894
39321
  summary: "Revoke one unused remote access link.",
38895
39322
  category: "Access",
38896
- gate: "origin-write"
39323
+ gate: "origin-write",
39324
+ transport: "http"
38897
39325
  },
38898
39326
  {
38899
39327
  method: "DELETE",
38900
39328
  path: "/api/v1/access-sessions/:sessionHandle",
38901
39329
  summary: "End one open remote session, leaving its pairing intact.",
38902
39330
  category: "Access",
38903
- gate: "origin-write"
39331
+ gate: "origin-write",
39332
+ transport: "http"
38904
39333
  },
38905
39334
  {
38906
39335
  method: "DELETE",
38907
39336
  path: "/api/v1/paired-devices/:deviceId",
38908
39337
  summary: "Unpair one device so it cannot rejoin without a new access link.",
38909
39338
  category: "Access",
38910
- gate: "origin-write"
39339
+ gate: "origin-write",
39340
+ transport: "http"
38911
39341
  },
38912
39342
  {
38913
39343
  method: "POST",
38914
39344
  path: "/api/v1/remote-identity/rotations",
38915
39345
  summary: "Issue a new remote certificate, invalidating every link, pin, and session for the old one.",
38916
39346
  category: "Access",
38917
- gate: "origin-write"
38918
- },
38919
- {
38920
- method: "GET",
38921
- path: "/join",
38922
- summary: "Explain that an access link opens in Fleet Console Desktop.",
38923
- category: "Access",
38924
- gate: "loopback"
39347
+ gate: "origin-write",
39348
+ transport: "http"
38925
39349
  },
38926
39350
  {
38927
39351
  method: "POST",
38928
39352
  path: "/api/v1/access-links",
38929
39353
  summary: "Create a remote access link for this console.",
38930
39354
  category: "Access",
38931
- gate: "lock-token"
39355
+ gate: "lock-token",
39356
+ transport: "http"
38932
39357
  },
38933
39358
  {
38934
39359
  method: "POST",
38935
39360
  path: "/api/v1/join",
38936
39361
  summary: "Exchange a single-use grant for a pairing, or resume an existing pairing.",
38937
39362
  category: "Access",
38938
- gate: "loopback"
39363
+ gate: "loopback",
39364
+ transport: "http"
38939
39365
  },
38940
39366
  {
38941
39367
  method: "GET",
38942
39368
  path: "/api/v1/remote-hosts",
38943
39369
  summary: "List the other consoles this one can jump to.",
38944
39370
  category: "Access",
38945
- gate: "loopback"
39371
+ gate: "loopback",
39372
+ transport: "http"
38946
39373
  },
38947
39374
  {
38948
39375
  method: "POST",
38949
39376
  path: "/api/v1/remote-hosts",
38950
39377
  summary: "Remember another console from its access link, after confirming its certificate.",
38951
39378
  category: "Access",
38952
- gate: "origin-write"
39379
+ gate: "origin-write",
39380
+ transport: "http"
38953
39381
  },
38954
39382
  {
38955
39383
  method: "PATCH",
38956
39384
  path: "/api/v1/remote-hosts/:hostId",
38957
39385
  summary: "Rename a remembered console.",
38958
39386
  category: "Access",
38959
- gate: "origin-write"
39387
+ gate: "origin-write",
39388
+ transport: "http"
38960
39389
  },
38961
39390
  {
38962
39391
  method: "DELETE",
38963
39392
  path: "/api/v1/remote-hosts/:hostId",
38964
39393
  summary: "Forget a remembered console and its certificate pin.",
38965
39394
  category: "Access",
38966
- gate: "origin-write"
39395
+ gate: "origin-write",
39396
+ transport: "http"
38967
39397
  },
38968
39398
  {
38969
39399
  method: "POST",
38970
39400
  path: "/api/v1/remote-hosts/:hostId/probes",
38971
39401
  summary: "Check whether a remembered console answers and still presents its pinned certificate.",
38972
39402
  category: "Access",
38973
- gate: "origin-write"
39403
+ gate: "origin-write",
39404
+ transport: "http"
38974
39405
  },
38975
39406
  {
38976
39407
  method: "GET",
38977
39408
  path: "/api/v1/local-consoles",
38978
39409
  summary: "List the consoles running on this machine that this one can point a window at.",
38979
39410
  category: "Access",
38980
- gate: "loopback"
39411
+ gate: "loopback",
39412
+ transport: "http"
38981
39413
  },
38982
39414
  {
38983
39415
  method: "POST",
38984
39416
  path: "/api/v1/desktop/handoff",
38985
39417
  summary: "Hand the attached Desktop what it needs to open one remembered console, consuming any pending grant.",
38986
39418
  category: "Desktop",
38987
- gate: "origin-write"
39419
+ gate: "origin-write",
39420
+ transport: "http"
38988
39421
  },
38989
39422
  {
38990
39423
  method: "GET",
38991
39424
  path: "/api/v1/health",
38992
39425
  summary: "Check console status with the lock token.",
38993
39426
  category: "Health",
38994
- gate: "lock-token"
39427
+ gate: "lock-token",
39428
+ transport: "http"
38995
39429
  }
38996
39430
  ];
38997
39431
  var REMOTE_HOSTS_PATH = "/api/v1/remote-hosts";
@@ -39045,6 +39479,7 @@ function createConsoleServer(deps = {}) {
39045
39479
  let remoteFingerprint = null;
39046
39480
  let remoteReconcile = Promise.resolve();
39047
39481
  let remoteLastError = null;
39482
+ const remoteJoinGuard = createRemoteJoinGuard();
39048
39483
  let boundPort = null;
39049
39484
  let controlPruneNotifyQueued = false;
39050
39485
  let lastPublishedControlHolder = null;
@@ -39201,6 +39636,7 @@ function createConsoleServer(deps = {}) {
39201
39636
  process.env.FLEET_CONSOLE_PACKAGE_ROOT = release.packageRoot;
39202
39637
  const pluginHost = createFleetPluginHost({
39203
39638
  ...resolveBuiltInPluginDiscoveryRoots(release.packageRoot),
39639
+ homeDir: deps.pluginHomeDir,
39204
39640
  bundleCacheDir: path18__default.join(durablePaths.dir, "plugin-cache"),
39205
39641
  routes: routeRegistry,
39206
39642
  upgrades: upgradeRegistry,
@@ -39252,10 +39688,11 @@ function createConsoleServer(deps = {}) {
39252
39688
  const globalSettingsRouter = createGlobalSettingsRouter({
39253
39689
  consoleSettingsStore,
39254
39690
  isAuthorized: isTerminalAuthorized,
39691
+ isRemoteAccessOwner: isLoopbackListener,
39255
39692
  readJsonBody,
39256
39693
  writeJson,
39257
39694
  onThemeChanged: broadcastDesktopThemeChanged,
39258
- onRemoteAccessChanged: () => reconcileRemoteAccess()
39695
+ onRemoteAccessChanged: (change) => reconcileRemoteAccess(change)
39259
39696
  });
39260
39697
  const desktopThemeRouter = createDesktopThemeRouter({
39261
39698
  getTheme: () => consoleSettingsStore.load().general?.theme ?? "instrument",
@@ -39380,14 +39817,13 @@ function createConsoleServer(deps = {}) {
39380
39817
  routeRegistry.register("/plugin-runtime", handlePluginRuntimeRoute);
39381
39818
  function isRequestHostAllowed(req) {
39382
39819
  const listener = listenerForRequest(req);
39383
- return listener !== null && validateHost(req, `${listener.host}:${listener.port}`);
39820
+ return listener !== null && validateHost(req, listenerAuthority(listener.host, listener.port, listener.secure), listener.secure);
39384
39821
  }
39385
39822
  function listenerForRequest(req) {
39386
39823
  return resolveListenerIdentity(listeners, req.socket);
39387
39824
  }
39388
39825
  function isRemoteRequestAdmitted(listener, req, pathname) {
39389
39826
  if (pathname === "/api/v1/join") return true;
39390
- if (pathname === JOIN_NOTICE_PATH && req.method === "GET") return true;
39391
39827
  const session = access.resolveSession(readSessionCookie(req.headers, listener.port), listener.audience);
39392
39828
  if (session === null) return false;
39393
39829
  return session.access !== "monitoring" || isReadOnlyRequest(req);
@@ -39415,10 +39851,6 @@ function createConsoleServer(deps = {}) {
39415
39851
  handlePairingIdentity(req, res);
39416
39852
  return;
39417
39853
  }
39418
- if (pathname === JOIN_NOTICE_PATH) {
39419
- handleJoinNotice(req, res);
39420
- return;
39421
- }
39422
39854
  if (tryServeStaticConsole2(req, res, pathname)) return;
39423
39855
  if (pathname === "/api/v1/health") {
39424
39856
  handleHealth2(req, res);
@@ -39596,10 +40028,15 @@ function createConsoleServer(deps = {}) {
39596
40028
  const remote = listeners.find((entry) => entry.audience === "remote");
39597
40029
  const sessions = access.listSessions("remote");
39598
40030
  writeJson(res, 200, {
39599
- listening: remote !== void 0 && remoteFingerprint !== null,
39600
- origin: remote?.origin ?? null,
40031
+ listener: {
40032
+ listening: remote !== void 0 && remoteFingerprint !== null,
40033
+ origin: remote?.origin ?? null,
40034
+ lastError: remoteLastError
40035
+ },
40036
+ publicReachability: "unverified",
40037
+ // 거절이 일어나고 있다는 사실 자체가 "지금 열어둘 만한가"의 판단 재료다.
40038
+ rejectedJoins: remoteJoinGuard.stats(),
39601
40039
  fingerprint: remoteFingerprint,
39602
- lastError: remoteLastError,
39603
40040
  // 발급 사실만 나간다 — 목록을 보는 것으로는 어떤 링크도 다시 쓸 수 없다.
39604
40041
  links: access.listGrants("remote"),
39605
40042
  /**
@@ -39691,12 +40128,12 @@ function createConsoleServer(deps = {}) {
39691
40128
  return true;
39692
40129
  }
39693
40130
  const configured = consoleSettingsStore.load().general?.remoteAccess;
39694
- const bindHost = listeners.find((entry) => entry.audience === "remote")?.host ?? (configured?.enabled === true ? configured.bindHost : null);
39695
- if (!bindHost) {
40131
+ const advertisedHost = listeners.find((entry) => entry.audience === "remote")?.host ?? (configured?.enabled === true ? effectiveRemoteAccessAdvertisedTuple(configured).host : null);
40132
+ if (!advertisedHost) {
39696
40133
  writeJson(res, 409, { error: "remote_access_disabled" });
39697
40134
  return true;
39698
40135
  }
39699
- await remoteIdentityStore.rotate(bindHost);
40136
+ await remoteIdentityStore.rotate(advertisedHost);
39700
40137
  access.revokeGrants("remote");
39701
40138
  pairedDeviceStore.revokeAll("remote");
39702
40139
  remoteEndpointStore.forget();
@@ -39708,21 +40145,6 @@ function createConsoleServer(deps = {}) {
39708
40145
  writeJson(res, 200, { fingerprint: remoteFingerprint });
39709
40146
  return true;
39710
40147
  }
39711
- function handleJoinNotice(req, res) {
39712
- if (req.method !== "GET") {
39713
- writeJson(res, 405, { error: "Method not allowed" });
39714
- return;
39715
- }
39716
- const body2 = Buffer.from(JOIN_NOTICE_DOCUMENT, "utf8");
39717
- res.writeHead(200, withSecurityHeaders2({
39718
- "Content-Type": "text/html; charset=utf-8",
39719
- "Content-Length": String(body2.byteLength),
39720
- "Cache-Control": "no-store",
39721
- "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
39722
- "Referrer-Policy": "no-referrer"
39723
- }));
39724
- res.end(body2);
39725
- }
39726
40148
  function handleAccessLinkIssue(req, res) {
39727
40149
  if (req.method !== "POST") {
39728
40150
  writeJson(res, 405, { error: "Method not allowed" });
@@ -39756,6 +40178,26 @@ function createConsoleServer(deps = {}) {
39756
40178
  writeJson(res, 401, { error: "unauthorized" });
39757
40179
  return true;
39758
40180
  }
40181
+ if (listener.audience !== "remote") return performAccessJoin(req, res, listener);
40182
+ const source = normalizeRemoteJoinSource(req.socket.remoteAddress);
40183
+ const verdict = remoteJoinGuard.begin(source);
40184
+ if (verdict !== "ok") {
40185
+ res.writeHead(verdict === "throttled" ? 429 : 503, withSecurityHeaders2({
40186
+ "Content-Type": "application/json",
40187
+ "Retry-After": String(remoteJoinGuard.retryAfterSeconds(source))
40188
+ }));
40189
+ res.end(JSON.stringify({ error: verdict === "throttled" ? "too_many_attempts" : "busy" }));
40190
+ return true;
40191
+ }
40192
+ let paired = false;
40193
+ try {
40194
+ paired = await performAccessJoin(req, res, listener);
40195
+ } finally {
40196
+ remoteJoinGuard.settle(source, paired ? "paired" : "rejected");
40197
+ }
40198
+ return true;
40199
+ }
40200
+ async function performAccessJoin(req, res, listener) {
39759
40201
  const body2 = await readJsonBody(req);
39760
40202
  const token = isPlainObject2(body2) && typeof body2.token === "string" ? body2.token : null;
39761
40203
  const device = isPlainObject2(body2) ? sanitizeDeviceName(body2.device) : null;
@@ -39767,10 +40209,10 @@ function createConsoleServer(deps = {}) {
39767
40209
  "Set-Cookie": expirePairingCookie({ secure: listener.secure, port: listener.port })
39768
40210
  }));
39769
40211
  res.end(JSON.stringify({ error: joined.error }));
39770
- return true;
40212
+ return false;
39771
40213
  }
39772
40214
  writeJson(res, joined.status, { error: joined.error });
39773
- return true;
40215
+ return false;
39774
40216
  }
39775
40217
  if (listener.audience === "remote" && joined.session.access === "full") broadcastControlChanged();
39776
40218
  const cookies = [formatSessionCookie(joined.session, { secure: listener.secure, port: listener.port })];
@@ -40111,7 +40553,7 @@ function createConsoleServer(deps = {}) {
40111
40553
  writeJson(res, 405, { error: "Method not allowed" });
40112
40554
  return;
40113
40555
  }
40114
- writeJson(res, 200, { version: version2, routes: buildApiCatalog() });
40556
+ writeJson(res, 200, { version: version2, routes: buildApiCatalog(pluginHost.apiCatalog) });
40115
40557
  }
40116
40558
  function handleEnvironmentDiagnostics(req, res) {
40117
40559
  if (channel !== "local") {
@@ -40528,12 +40970,19 @@ function createConsoleServer(deps = {}) {
40528
40970
  });
40529
40971
  }
40530
40972
  }
40531
- function reconcileRemoteAccess() {
40973
+ function reconcileRemoteAccess(change) {
40532
40974
  return queueRemoteReconcile(() => {
40533
- const configured = consoleSettingsStore.load().general?.remoteAccess;
40534
- const desiredHost = configured?.enabled === true ? configured.bindHost : void 0;
40535
- const active = listeners.find((entry) => entry.audience === "remote");
40536
- return (active?.host ?? void 0) === desiredHost ? null : restartRemoteAccess;
40975
+ const { previous, next } = change;
40976
+ const previousAdvertised = effectiveRemoteAccessAdvertisedTuple(previous);
40977
+ const nextAdvertised = effectiveRemoteAccessAdvertisedTuple(next);
40978
+ const publicChanged = previousAdvertised.host !== nextAdvertised.host || previousAdvertised.port !== nextAdvertised.port;
40979
+ const localChanged = previous.listenAddress !== next.listenAddress || previous.listenPort.value !== next.listenPort.value;
40980
+ const explicitDisable = previous.enabled && !next.enabled && !publicChanged && !localChanged;
40981
+ if (publicChanged) return () => reconcilePublicEndpointChange(next);
40982
+ if (explicitDisable) return stopRemoteAccessForDisable;
40983
+ if (localChanged) return () => reconcileLocalEndpointChange(next);
40984
+ if (!previous.enabled && next.enabled) return restartRemoteAccess;
40985
+ return null;
40537
40986
  });
40538
40987
  }
40539
40988
  function reconcileRemoteIdentity() {
@@ -40552,6 +41001,25 @@ function createConsoleServer(deps = {}) {
40552
41001
  remoteLastError = null;
40553
41002
  await startRemoteAccessGuarded(boundPort);
40554
41003
  }
41004
+ async function stopRemoteAccessForDisable() {
41005
+ await stopRemoteAccess();
41006
+ access.revokeGrants("remote");
41007
+ }
41008
+ async function reconcileLocalEndpointChange(next) {
41009
+ await stopRemoteAccess();
41010
+ remoteLastError = null;
41011
+ if (next.enabled) await startRemoteAccessGuarded(boundPort);
41012
+ }
41013
+ async function reconcilePublicEndpointChange(next) {
41014
+ await stopRemoteAccess();
41015
+ access.revokeGrants("remote");
41016
+ pairedDeviceStore.revokeAll("remote");
41017
+ remoteEndpointStore.forget();
41018
+ if (next.enabled) {
41019
+ remoteLastError = null;
41020
+ await startRemoteAccessGuarded(boundPort);
41021
+ }
41022
+ }
40555
41023
  async function startRemoteAccessGuarded(actualPort) {
40556
41024
  try {
40557
41025
  await startRemoteAccessIfEnabled(actualPort);
@@ -40579,57 +41047,104 @@ function createConsoleServer(deps = {}) {
40579
41047
  remoteFingerprint = null;
40580
41048
  listeners = listeners.filter((entry) => entry.audience !== "remote");
40581
41049
  access.revokeSessions("remote");
40582
- access.revokeGrants("remote");
40583
41050
  for (const owner of desktopShellsByOwner.keys()) {
40584
41051
  if (owner !== "local") desktopShellsByOwner.delete(owner);
40585
41052
  }
40586
41053
  broadcastControlChanged();
40587
41054
  if (closing) await new Promise((resolve3) => closing.close(() => resolve3()));
40588
41055
  }
40589
- async function startRemoteAccessIfEnabled(actualPort) {
41056
+ async function startRemoteAccessIfEnabled(_actualPort) {
40590
41057
  const configured = consoleSettingsStore.load().general?.remoteAccess;
40591
- const bindHost = configured?.enabled === true ? configured.bindHost : void 0;
40592
- if (!bindHost) return;
40593
- const previousFingerprint = remoteIdentityStore.read()?.fingerprint ?? null;
40594
- const identity2 = await remoteIdentityStore.ensure(bindHost);
40595
- if (previousFingerprint === null || !fingerprintsMatch(previousFingerprint, identity2.fingerprint)) {
41058
+ if (configured?.enabled !== true || configured.listenAddress === "") return;
41059
+ if (configured.publicEndpointEnabled && !acknowledgmentMatches(configured, configured.acknowledgment)) return;
41060
+ const advertised = effectiveRemoteAccessAdvertisedTuple(configured);
41061
+ const previousIdentity = remoteIdentityStore.read();
41062
+ const identity2 = await remoteIdentityStore.ensure(advertised.host);
41063
+ const storedEndpoint = remoteEndpointStore.read();
41064
+ const publicIdentityChanged = previousIdentity === null || !fingerprintsMatch(previousIdentity.fingerprint, identity2.fingerprint) || storedEndpoint !== null && storedEndpoint.advertisedPort !== advertised.port;
41065
+ if (publicIdentityChanged) {
41066
+ access.revokeGrants("remote");
41067
+ access.revokeSessions("remote");
40596
41068
  pairedDeviceStore.revokeAll("remote");
40597
41069
  remoteEndpointStore.forget();
40598
41070
  }
40599
- const published = remoteEndpointStore.read();
40600
- const publishedPort = published ?? actualPort;
40601
- const started = await startRemoteListener({
40602
- identity: identity2,
40603
- bindHost,
40604
- port: publishedPort,
40605
- handler: handleRequest,
40606
- upgradeRegistry,
40607
- isHostAllowed: isRequestHostAllowed,
40608
- isAdmitted: (req) => {
40609
- const resolved = listenerForRequest(req);
40610
- return resolved === null || resolved.audience === "local" || isRemoteRequestAdmitted(resolved, req, getPathname(req));
40611
- }
40612
- }).catch((error51) => {
40613
- if (published !== null && isAddressInUse(error51)) throw remotePortUnavailable(error51);
40614
- throw error51;
40615
- });
41071
+ const started = await startConfiguredRemoteListener(configured, identity2, storedEndpoint !== null && !publicIdentityChanged);
41072
+ const effectiveConfigured = started.port === configured.listenPort.value ? configured : { ...configured, listenPort: { ...configured.listenPort, value: started.port } };
41073
+ const effectiveAdvertised = effectiveRemoteAccessAdvertisedTuple(effectiveConfigured);
41074
+ remoteServer = started.server;
40616
41075
  const listener = {
40617
41076
  audience: "remote",
40618
- host: bindHost,
40619
- port: publishedPort,
40620
- origin: `https://${bindHost}:${publishedPort}`,
41077
+ host: effectiveAdvertised.host,
41078
+ port: effectiveAdvertised.port,
41079
+ origin: remoteOrigin(effectiveAdvertised.host, effectiveAdvertised.port),
40621
41080
  secure: true,
40622
- // 게이트는 소켓의 실제 주소로 리스너를 찾는다. 설정이 이름을 받으므로, 여기에 그 이름을
40623
- // 그대로 두면 어떤 요청도 자기 리스너를 찾지 못해 조인부터 전부 막힌다 — 리스너와 링크는
40624
- // 멀쩡해 보이는 채로.
40625
- bindAddress: started.address
41081
+ bindAddress: started.address,
41082
+ bindPort: started.port
40626
41083
  };
40627
41084
  listeners = [...listeners, listener];
40628
41085
  remoteFingerprint = identity2.fingerprint;
40629
- remoteServer = started.server;
40630
- remoteEndpointStore.remember(publishedPort);
41086
+ remoteEndpointStore.remember({ listenPort: started.port, advertisedPort: effectiveAdvertised.port });
40631
41087
  startControlExpirySweep();
40632
41088
  }
41089
+ async function startConfiguredRemoteListener(configured, identity2, published) {
41090
+ if (configured.listenPort.mode === "custom") {
41091
+ try {
41092
+ return await startRemoteListener({ identity: identity2, bindHost: configured.listenAddress, port: configured.listenPort.value, handler: handleRequest, upgradeRegistry, isHostAllowed: isRequestHostAllowed, isAdmitted: remoteAdmission });
41093
+ } catch (error51) {
41094
+ if (errorCodeOf(error51) === "EADDRINUSE") throw codedRemoteError("FLEET_CUSTOM_PORT_UNAVAILABLE", error51);
41095
+ throw error51;
41096
+ }
41097
+ }
41098
+ const attempted = /* @__PURE__ */ new Set();
41099
+ while (attempted.size < REMOTE_AUTO_PORT_ATTEMPTS) {
41100
+ const candidate = attempted.size === 0 ? configured.listenPort.value : nextRemoteAutoPort(attempted);
41101
+ attempted.add(candidate);
41102
+ try {
41103
+ const started = await startRemoteListener({ identity: identity2, bindHost: configured.listenAddress, port: candidate, handler: handleRequest, upgradeRegistry, isHostAllowed: isRequestHostAllowed, isAdmitted: remoteAdmission });
41104
+ if (candidate === configured.listenPort.value) return started;
41105
+ if (published) {
41106
+ await closeHttpServer(started.server);
41107
+ throw codedRemoteError("FLEET_REMOTE_PORT_UNAVAILABLE");
41108
+ }
41109
+ const updated = { ...configured, listenPort: { mode: "auto", value: candidate }, acknowledgment: null };
41110
+ if (configured.publicEndpointEnabled) {
41111
+ await closeHttpServer(started.server);
41112
+ consoleSettingsStore.update((current) => ({ ...current, general: { ...current.general, remoteAccess: { ...updated, enabled: false } } }));
41113
+ throw codedRemoteError("FLEET_ACKNOWLEDGMENT_REQUIRED");
41114
+ }
41115
+ try {
41116
+ consoleSettingsStore.update((current) => ({ ...current, general: { ...current.general, remoteAccess: updated } }));
41117
+ } catch (error51) {
41118
+ await closeHttpServer(started.server);
41119
+ throw error51;
41120
+ }
41121
+ return started;
41122
+ } catch (error51) {
41123
+ const code = errorCodeOf(error51);
41124
+ if (code === "FLEET_ACKNOWLEDGMENT_REQUIRED" || code === REMOTE_PORT_UNAVAILABLE) throw error51;
41125
+ if (code !== "EADDRINUSE" && code !== "EADDRNOTAVAIL") throw error51;
41126
+ }
41127
+ }
41128
+ throw codedRemoteError("FLEET_AUTO_PORT_EXHAUSTED");
41129
+ }
41130
+ function remoteOrigin(hostname5, port2) {
41131
+ return listenerOrigin(hostname5, port2, true);
41132
+ }
41133
+ function remoteAdmission(req) {
41134
+ const resolved = listenerForRequest(req);
41135
+ return resolved === null || resolved.audience === "local" || isRemoteRequestAdmitted(resolved, req, getPathname(req));
41136
+ }
41137
+ function nextRemoteAutoPort(attempted) {
41138
+ const randomInt = deps.remoteRandomInt ?? crypto5.randomInt;
41139
+ for (let draw = 0; draw < REMOTE_AUTO_PORT_ATTEMPTS; draw += 1) {
41140
+ const candidate = randomInt(REMOTE_AUTO_PORT_MIN, REMOTE_AUTO_PORT_MAX + 1);
41141
+ if (!attempted.has(candidate)) return candidate;
41142
+ }
41143
+ for (let candidate = REMOTE_AUTO_PORT_MIN; candidate <= REMOTE_AUTO_PORT_MAX; candidate += 1) {
41144
+ if (!attempted.has(candidate)) return candidate;
41145
+ }
41146
+ throw codedRemoteError("FLEET_AUTO_PORT_EXHAUSTED");
41147
+ }
40633
41148
  function listenOnce(portToBind, statePatch) {
40634
41149
  return new Promise((resolve3, reject) => {
40635
41150
  const srv = createHttpServer(handleRequest, upgradeRegistry, isRequestHostAllowed);
@@ -40679,35 +41194,6 @@ function sanitizeDeviceName(value) {
40679
41194
  function consoleLabel() {
40680
41195
  return sanitizeAccessLabel(os4__default.hostname().replace(/\.local$/iu, "")) || "Fleet Console";
40681
41196
  }
40682
- var JOIN_NOTICE_PATH = "/join";
40683
- var JOIN_NOTICE_DOCUMENT = `<!doctype html>
40684
- <html lang="en">
40685
- <head>
40686
- <meta charset="utf-8">
40687
- <meta name="viewport" content="width=device-width, initial-scale=1">
40688
- <title>Fleet Console access link</title>
40689
- <style>
40690
- :root { color-scheme: dark; }
40691
- body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px;
40692
- background: #0b1116; color: #e6edf3;
40693
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
40694
- main { max-width: 30rem; }
40695
- h1 { margin: 0 0 12px; font-size: 20px; font-weight: 650; }
40696
- p { margin: 0 0 12px; color: #aebbc5; font-size: 14px; line-height: 1.55; }
40697
- strong { color: #e6edf3; font-weight: 600; }
40698
- </style>
40699
- </head>
40700
- <body>
40701
- <main>
40702
- <h1>This console is reached with an access link</h1>
40703
- <p>You have found the remote listener of a Fleet Console. It does not open in a browser: a browser cannot check this console&#39;s certificate fingerprint, so <strong>remote access is not available to browsers</strong>.</p>
40704
- <p>Ask for an access link instead. It looks like <strong>fleet://join?code=&hellip;</strong> and carries the address, a one-time credential, and the fingerprint together.</p>
40705
- <p>In the Fleet Console you are already using, open <strong>Settings &rarr; Remote access &rarr; Hosts</strong> and paste the link there.</p>
40706
- <p><strong>Remote access is experimental.</strong> How links and pairings behave can still change between releases.</p>
40707
- </main>
40708
- </body>
40709
- </html>
40710
- `;
40711
41197
  function decodeHandle(raw) {
40712
41198
  if (raw.includes("/")) return "";
40713
41199
  try {
@@ -40718,22 +41204,22 @@ function decodeHandle(raw) {
40718
41204
  }
40719
41205
  function remoteAccessErrorCode(error51) {
40720
41206
  const code = errorCodeOf(error51);
41207
+ if (code === "FLEET_AUTO_PORT_EXHAUSTED") return "auto_port_exhausted";
41208
+ if (code === "FLEET_ACKNOWLEDGMENT_REQUIRED") return "acknowledgment_required";
41209
+ if (code === "FLEET_CUSTOM_PORT_UNAVAILABLE") return "custom_port_unavailable";
40721
41210
  if (code === REMOTE_PORT_UNAVAILABLE) return "remote_port_unavailable";
40722
41211
  if (code === "EADDRNOTAVAIL") return "bind_address_unavailable";
40723
- if (code === "EADDRINUSE") return "bind_address_in_use";
41212
+ if (code === "EADDRINUSE") return "custom_port_unavailable";
40724
41213
  if (code === "EACCES") return "bind_permission_denied";
40725
41214
  return "remote_listener_failed";
40726
41215
  }
41216
+ function codedRemoteError(code, cause) {
41217
+ return Object.assign(new Error(code), { code, cause });
41218
+ }
40727
41219
  function errorCodeOf(error51) {
40728
41220
  return typeof error51 === "object" && error51 !== null && "code" in error51 && typeof error51.code === "string" ? error51.code : "";
40729
41221
  }
40730
- function isAddressInUse(error51) {
40731
- return errorCodeOf(error51) === "EADDRINUSE";
40732
- }
40733
41222
  var REMOTE_PORT_UNAVAILABLE = "FLEET_REMOTE_PORT_UNAVAILABLE";
40734
- function remotePortUnavailable(cause) {
40735
- return Object.assign(new Error("remote_port_unavailable"), { code: REMOTE_PORT_UNAVAILABLE, cause });
40736
- }
40737
41223
  function resolveBuiltInPluginDiscoveryRoots(packageRoot) {
40738
41224
  const packageRootRepo = path18__default.resolve(packageRoot, "..", "..");
40739
41225
  const sourceRoot = path18__default.join(packageRootRepo, "runtime", "fleet-plugins");
@@ -40768,7 +41254,7 @@ async function startRemoteListener(input) {
40768
41254
  });
40769
41255
  });
40770
41256
  const address = srv.address();
40771
- return { server: srv, address: typeof address === "object" && address ? address.address : input.bindHost };
41257
+ return { server: srv, address: typeof address === "object" && address ? address.address : input.bindHost, port: typeof address === "object" && address ? address.port : input.port };
40772
41258
  }
40773
41259
  function createHttpServer(handler, upgradeRegistry, isHostAllowed2) {
40774
41260
  const srv = http.createServer(handler);
@@ -40898,13 +41384,18 @@ function runAsyncBooleanHandler(handler, res, fallback) {
40898
41384
  writeJson(res, 500, { error: "Internal server error" });
40899
41385
  });
40900
41386
  }
40901
- function validateHost(req, expectedHostPort) {
41387
+ function validateHost(req, expectedHostPort, secure) {
40902
41388
  if (req.url?.startsWith("http://") || req.url?.startsWith("https://")) return false;
40903
41389
  const hostHeaderCount = req.rawHeaders.filter((header, index) => index % 2 === 0 && header.toLowerCase() === "host").length;
40904
41390
  if (hostHeaderCount !== 1) return false;
40905
41391
  const hostHeader = req.headers.host;
40906
41392
  if (!hostHeader) return false;
40907
- return hostHeader === expectedHostPort;
41393
+ return stripDefaultPort(hostHeader, secure) === stripDefaultPort(expectedHostPort, secure);
41394
+ }
41395
+ function stripDefaultPort(authority, secure) {
41396
+ if (secure === void 0) return authority;
41397
+ const suffix = secure ? ":443" : ":80";
41398
+ return authority.endsWith(suffix) ? authority.slice(0, -suffix.length) : authority;
40908
41399
  }
40909
41400
  function isAllowedTerminalOrigin(req, expectedOrigin) {
40910
41401
  const origin = req.headers.origin;