@dotobokuri/fleet-console 1.54.1 → 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 +1205 -704
  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 +11 -2
  62. package/dist/fleet-plugins/skills/routes.mjs +14 -10
  63. package/dist/fleet-plugins/terminal/routes.mjs +317 -283
  64. package/dist/fleet.mjs +3914 -3456
  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";
@@ -32312,7 +32907,7 @@ function specToMcpTool(spec) {
32312
32907
  }
32313
32908
  function installExecutorToolCallRouter(runtime, sessionToken, ctx) {
32314
32909
  runtime.server.setOnToolCallArrived(sessionToken, (toolName, args) => {
32315
- const toolCallId = crypto6.randomUUID();
32910
+ const toolCallId = crypto5.randomUUID();
32316
32911
  void runtime.registry.invoke(toolName, args, {
32317
32912
  cwd: ctx.cwd,
32318
32913
  sessionLabel: ctx.sessionLabel,
@@ -32708,7 +33303,7 @@ function createServedMcpEndpoint(deps = {}) {
32708
33303
  if (activeStopPromise) await activeStopPromise;
32709
33304
  if (activeServer && activeServerUrl) return activeServerUrl;
32710
33305
  if (activeStartPromise) return activeStartPromise;
32711
- activeOpaquePath = `/${crypto6.randomUUID()}`;
33306
+ activeOpaquePath = `/${crypto5.randomUUID()}`;
32712
33307
  activeStartPromise = new Promise((resolve3, reject) => {
32713
33308
  const srv = http.createServer(handleRequest);
32714
33309
  srv.timeout = MCP_SERVER_TIMEOUT_MS;
@@ -32965,7 +33560,7 @@ function createExecutorMcpSession(deps, toolTimeoutSeconds, request) {
32965
33560
  throw new Error("Executor session cwd is required");
32966
33561
  }
32967
33562
  assertNonEmptyExecutorTools(request.serverName, request.specs);
32968
- const token = crypto6.randomUUID();
33563
+ const token = crypto5.randomUUID();
32969
33564
  registerExecutorSessionTools(runtime.runtime, token, [...request.specs]);
32970
33565
  installExecutorToolCallRouter(runtime.runtime, token, {
32971
33566
  cwd,
@@ -33004,7 +33599,7 @@ function issueSessionToken(deps, sessionTokensByLabel, request) {
33004
33599
  const allTools = runtime.registry.getAllAgentTools();
33005
33600
  const tools = request.includeTool ? allTools.filter((tool) => request.includeTool?.(tool.id) ?? true) : allTools;
33006
33601
  assertNonEmptyExecutorTools(name, tools);
33007
- const token = crypto6.randomUUID();
33602
+ const token = crypto5.randomUUID();
33008
33603
  registerExecutorSessionTools(runtime, token, tools);
33009
33604
  installExecutorToolCallRouter(runtime, token, {
33010
33605
  cwd,
@@ -34349,7 +34944,7 @@ var CoworkStore = class {
34349
34944
  if (found && found.state !== "closed" && found.state !== "applied") return found;
34350
34945
  }
34351
34946
  const now = (/* @__PURE__ */ new Date()).toISOString();
34352
- const id = crypto6.randomUUID();
34947
+ const id = crypto5.randomUUID();
34353
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 };
34354
34949
  this.sessions.set(this.key(workspaceId, id), record3);
34355
34950
  this.writers.set(`${workspaceId}:${entryId}`, id);
@@ -34428,7 +35023,7 @@ var CoworkStore = class {
34428
35023
  }
34429
35024
  };
34430
35025
  function hash2(value) {
34431
- return crypto6.createHash("sha256").update(value).digest("hex");
35026
+ return crypto5.createHash("sha256").update(value).digest("hex");
34432
35027
  }
34433
35028
  var COWORK_SYSTEM_PROMPT = `# Identity
34434
35029
 
@@ -35128,7 +35723,7 @@ function record2(value) {
35128
35723
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
35129
35724
  }
35130
35725
  function workspaceHash(canonicalCwd) {
35131
- return crypto6.createHash("sha256").update(canonicalCwd).digest("hex").slice(0, 12);
35726
+ return crypto5.createHash("sha256").update(canonicalCwd).digest("hex").slice(0, 12);
35132
35727
  }
35133
35728
  async function canonicalizeTheaterPath(cwd) {
35134
35729
  return canonicalizeTheaterPathSync(cwd);
@@ -35267,7 +35862,7 @@ function isWindowsAmbiguousPath(value, platform) {
35267
35862
  var GRANT_ID_BYTES = 24;
35268
35863
  var DEFAULT_GRANT_TTL_MS2 = 6e4;
35269
35864
  function createFolderGrantStore(deps = {}) {
35270
- const randomId = deps.randomId ?? (() => crypto6.randomBytes(GRANT_ID_BYTES).toString("base64url"));
35865
+ const randomId = deps.randomId ?? (() => crypto5.randomBytes(GRANT_ID_BYTES).toString("base64url"));
35271
35866
  const statSync2 = deps.statSync ?? fs9__default.statSync;
35272
35867
  const ttlMs = deps.ttlMs ?? DEFAULT_GRANT_TTL_MS2;
35273
35868
  const now = deps.now ?? Date.now;
@@ -35758,439 +36353,6 @@ function readRawHeaderValues(rawHeaders, name) {
35758
36353
  function stripIpv6Brackets2(host) {
35759
36354
  return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
35760
36355
  }
35761
- var MAX_GROUP_NAME_LENGTH = 64;
35762
- var DELETION_GRACE_MS = 8e3;
35763
- var FIXED_SENSITIVE_OPERATION_FIELDS = /* @__PURE__ */ new Set([
35764
- "canonicalCwd",
35765
- "cwd",
35766
- "persona",
35767
- "prompt",
35768
- "providerSession",
35769
- "ticket",
35770
- "token",
35771
- "toolAllowlist",
35772
- "transcriptPath"
35773
- ]);
35774
- function createSanitizedOpDto(node, options = {}) {
35775
- const sensitiveFields = /* @__PURE__ */ new Set([...FIXED_SENSITIVE_OPERATION_FIELDS, ...options.sensitiveFields ?? []]);
35776
- const payload = sanitizeRecord(node.payload, sensitiveFields);
35777
- delete payload.resumeAvailable;
35778
- if (isResumableProviderSession(node.payload?.providerSession)) payload.resumeAvailable = true;
35779
- return {
35780
- ...node,
35781
- payload
35782
- };
35783
- }
35784
- function isResumableProviderSession(value) {
35785
- if (!isRecord4(value)) return false;
35786
- return value.provider === "claude" && typeof value.sessionId === "string" && value.sessionId.length > 0;
35787
- }
35788
- function sanitizeRecord(value, sensitiveFields) {
35789
- const output = {};
35790
- for (const [key, item] of Object.entries(value)) {
35791
- if (sensitiveFields.has(key)) continue;
35792
- output[key] = sanitizeValue(item, sensitiveFields);
35793
- }
35794
- return output;
35795
- }
35796
- function sanitizeValue(value, sensitiveFields) {
35797
- if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, sensitiveFields));
35798
- if (!isRecord4(value)) return value;
35799
- return sanitizeRecord(value, sensitiveFields);
35800
- }
35801
- function isRecord4(value) {
35802
- return typeof value === "object" && value !== null && !Array.isArray(value);
35803
- }
35804
- function createOperationStore(deps = {}) {
35805
- const now = deps.now ?? Date.now;
35806
- const nodes = /* @__PURE__ */ new Map();
35807
- const groups = /* @__PURE__ */ new Map();
35808
- function list() {
35809
- return Array.from(nodes.values()).sort(compareOperationNodes);
35810
- }
35811
- function listByTheater(theaterId) {
35812
- return list().filter((node) => node.theaterId === theaterId);
35813
- }
35814
- function get(id) {
35815
- return nodes.get(id) ?? null;
35816
- }
35817
- function create2(input) {
35818
- const id = input.id ?? crypto6.randomUUID();
35819
- if (nodes.has(id)) throw new Error("operation_exists");
35820
- const node = normalizeCreateInput(input, id, now());
35821
- nodes.set(node.id, node);
35822
- return node;
35823
- }
35824
- function upsert(input) {
35825
- const existing = input.id ? nodes.get(input.id) : null;
35826
- if (!existing) return create2(input);
35827
- const updated = normalizePatch(existing, {
35828
- title: input.title,
35829
- accent: input.accent ?? existing.accent,
35830
- geometry: input.geometry ?? existing.geometry,
35831
- payload: input.payload ?? existing.payload
35832
- }, now());
35833
- nodes.set(existing.id, updated);
35834
- return updated;
35835
- }
35836
- function patch(id, input) {
35837
- const existing = nodes.get(id);
35838
- if (!existing) return null;
35839
- const updated = normalizePatch(existing, input, now());
35840
- nodes.set(id, updated);
35841
- return updated;
35842
- }
35843
- function deleteNode(id) {
35844
- if (!nodes.has(id)) return false;
35845
- nodes.delete(id);
35846
- return true;
35847
- }
35848
- function deleteByTheater(theaterId) {
35849
- let deleted = 0;
35850
- for (const node of Array.from(nodes.values())) {
35851
- if (node.theaterId !== theaterId) continue;
35852
- nodes.delete(node.id);
35853
- deleted += 1;
35854
- }
35855
- deleteGroupsByTheater(theaterId);
35856
- return deleted;
35857
- }
35858
- function replace(nextNodes) {
35859
- nodes.clear();
35860
- const validNodes = sanitizeReplacementNodes(nextNodes);
35861
- for (const node of validNodes) nodes.set(node.id, node);
35862
- }
35863
- function createGroup(input) {
35864
- const id = input.id ?? crypto6.randomUUID();
35865
- if (groups.has(id)) throw new Error("group_exists");
35866
- const order = input.order ?? groups.size;
35867
- const group = {
35868
- id,
35869
- theaterId: input.theaterId,
35870
- name: input.name.trim().slice(0, MAX_GROUP_NAME_LENGTH) || "Group",
35871
- color: input.color,
35872
- order,
35873
- createdAt: now()
35874
- };
35875
- groups.set(id, group);
35876
- return group;
35877
- }
35878
- function updateGroup(id, input) {
35879
- const existing = groups.get(id);
35880
- if (!existing) return null;
35881
- const updated = {
35882
- ...existing,
35883
- ...input.name !== void 0 ? { name: input.name.trim().slice(0, MAX_GROUP_NAME_LENGTH) || existing.name } : {},
35884
- ...input.color !== void 0 ? { color: input.color } : {},
35885
- ...input.order !== void 0 ? { order: input.order } : {}
35886
- };
35887
- groups.set(id, updated);
35888
- return updated;
35889
- }
35890
- function deleteGroup(id) {
35891
- if (!groups.has(id)) return false;
35892
- groups.delete(id);
35893
- for (const [nodeId, node] of nodes.entries()) {
35894
- if (node.groupId === id) nodes.set(nodeId, { ...node, groupId: null });
35895
- }
35896
- return true;
35897
- }
35898
- function listGroups(theaterId) {
35899
- return Array.from(groups.values()).filter((g) => g.theaterId === theaterId).sort((a, b) => a.order - b.order || a.createdAt - b.createdAt);
35900
- }
35901
- function listAllGroups() {
35902
- return Array.from(groups.values()).sort((a, b) => a.order - b.order || a.createdAt - b.createdAt);
35903
- }
35904
- function deleteGroupsByTheater(theaterId) {
35905
- let deleted = 0;
35906
- for (const [id, group] of Array.from(groups.entries())) {
35907
- if (group.theaterId !== theaterId) continue;
35908
- groups.delete(id);
35909
- deleted += 1;
35910
- }
35911
- return deleted;
35912
- }
35913
- function replaceGroups(nextGroups) {
35914
- groups.clear();
35915
- for (const group of nextGroups) {
35916
- if (!groups.has(group.id)) groups.set(group.id, group);
35917
- }
35918
- }
35919
- return { list, listByTheater, get, create: create2, upsert, patch, delete: deleteNode, deleteByTheater, replace, createGroup, updateGroup, deleteGroup, listGroups, listAllGroups, deleteGroupsByTheater, replaceGroups };
35920
- }
35921
- function normalizeCreateInput(input, id, timestamp) {
35922
- return {
35923
- id,
35924
- theaterId: input.theaterId,
35925
- type: input.type,
35926
- pluginId: input.pluginId,
35927
- title: input.title.trim() || "Untitled Operation",
35928
- ...input.accent ? { accent: input.accent.trim() } : {},
35929
- ...input.groupId !== void 0 ? { groupId: input.groupId } : {},
35930
- payload: input.payload ?? {},
35931
- geometry: input.geometry ?? null,
35932
- ts: {
35933
- createdAt: input.createdAt ?? timestamp,
35934
- updatedAt: timestamp
35935
- }
35936
- };
35937
- }
35938
- function normalizePatch(existing, input, timestamp) {
35939
- const title = input.title?.trim();
35940
- return {
35941
- ...existing,
35942
- ...title !== void 0 ? { title: title.length > 0 ? title : existing.title } : {},
35943
- ...input.accent !== void 0 ? { accent: input.accent && input.accent.trim() ? input.accent.trim() : void 0 } : {},
35944
- ...input.groupId !== void 0 ? { groupId: input.groupId } : {},
35945
- ...input.payload !== void 0 ? { payload: input.payload } : {},
35946
- ...input.geometry !== void 0 ? { geometry: input.geometry } : {},
35947
- ts: { ...existing.ts, updatedAt: timestamp }
35948
- };
35949
- }
35950
- function compareOperationNodes(a, b) {
35951
- return a.ts.createdAt - b.ts.createdAt || a.id.localeCompare(b.id);
35952
- }
35953
- function sanitizeReplacementNodes(nextNodes) {
35954
- const seen = /* @__PURE__ */ new Set();
35955
- const result = [];
35956
- for (const node of nextNodes) {
35957
- if (!seen.has(node.id)) {
35958
- seen.add(node.id);
35959
- result.push(node);
35960
- }
35961
- }
35962
- return result;
35963
- }
35964
- function createOperationsRouter(deps) {
35965
- return async ({ req, res, pathname }) => {
35966
- if (pathname === "/api/v1/operations") {
35967
- await handleCollection(req, res, deps);
35968
- return true;
35969
- }
35970
- if (pathname === "/api/v1/operations/catalog") {
35971
- if (req.method !== "GET") {
35972
- deps.writeJson(res, 405, { error: "Method not allowed" });
35973
- return true;
35974
- }
35975
- deps.writeJson(res, 200, deps.resolveLaunchCatalog ? await deps.resolveLaunchCatalog() : { plugins: [] });
35976
- return true;
35977
- }
35978
- if (pathname === "/api/v1/operations/events") {
35979
- if (req.method !== "GET") {
35980
- deps.writeJson(res, 405, { error: "Method not allowed" });
35981
- return true;
35982
- }
35983
- deps.subscribeOperationSse?.(req, res);
35984
- return true;
35985
- }
35986
- if (pathname === "/api/v1/operations/groups") {
35987
- await handleGroupCollection(req, res, deps);
35988
- return true;
35989
- }
35990
- const groupItemMatch = pathname.match(/^\/api\/v1\/operations\/groups\/([^/]+)$/);
35991
- if (groupItemMatch) {
35992
- await handleGroupItem(req, res, decodeURIComponent(groupItemMatch[1] ?? ""), deps);
35993
- return true;
35994
- }
35995
- const itemMatch = pathname.match(/^\/api\/v1\/operations\/([^/]+)$/);
35996
- if (itemMatch) {
35997
- await handleItem(req, res, decodeURIComponent(itemMatch[1] ?? ""), deps);
35998
- return true;
35999
- }
36000
- return false;
36001
- };
36002
- }
36003
- async function handleCollection(req, res, deps) {
36004
- if (req.method === "GET") {
36005
- const theaterId = readRequestUrl(req).searchParams.get("theaterId");
36006
- const nodes = theaterId ? deps.store.listByTheater(theaterId) : deps.store.list();
36007
- deps.writeJson(res, 200, { operations: nodes.map((node) => sanitizeOperationNode(node, deps)) });
36008
- return;
36009
- }
36010
- if (req.method !== "POST") {
36011
- deps.writeJson(res, 405, { error: "Method not allowed" });
36012
- return;
36013
- }
36014
- if (!deps.isAuthorized(req)) {
36015
- deps.writeJson(res, 401, { error: "unauthorized" });
36016
- return;
36017
- }
36018
- const body2 = await deps.readJsonBody(req);
36019
- if (!body2 || typeof body2.theaterId !== "string" || typeof body2.type !== "string" || typeof body2.pluginId !== "string" || typeof body2.title !== "string") {
36020
- deps.writeJson(res, 400, { error: "invalid_operation" });
36021
- return;
36022
- }
36023
- if (body2.accent !== void 0 && typeof body2.accent !== "string") {
36024
- deps.writeJson(res, 400, { error: "invalid_operation_accent" });
36025
- return;
36026
- }
36027
- if (typeof body2.id === "string" && deps.isPendingDeletion?.(body2.id)) {
36028
- deps.writeJson(res, 409, { error: "pending_deletion" });
36029
- return;
36030
- }
36031
- try {
36032
- const node = deps.store.create({
36033
- id: typeof body2.id === "string" ? body2.id : void 0,
36034
- theaterId: body2.theaterId,
36035
- type: body2.type,
36036
- pluginId: body2.pluginId,
36037
- title: body2.title,
36038
- ...typeof body2.accent === "string" ? { accent: body2.accent } : {},
36039
- payload: isRecord4(body2.payload) ? body2.payload : {},
36040
- geometry: isRecord4(body2.geometry) ? readGeometry(body2.geometry) : null
36041
- });
36042
- deps.persist();
36043
- deps.writeJson(res, 201, { operation: sanitizeOperationNode(node, deps) });
36044
- } catch (error51) {
36045
- deps.writeJson(res, 400, { error: error51 instanceof Error ? error51.message : "invalid_operation" });
36046
- }
36047
- }
36048
- async function handleItem(req, res, id, deps) {
36049
- if (req.method === "GET") {
36050
- const node = deps.store.get(id);
36051
- deps.writeJson(res, node ? 200 : 404, node ? { operation: sanitizeOperationNode(node, deps) } : { error: "operation_not_found" });
36052
- return;
36053
- }
36054
- if (req.method !== "PATCH" && req.method !== "DELETE") {
36055
- deps.writeJson(res, 405, { error: "Method not allowed" });
36056
- return;
36057
- }
36058
- if (!deps.isAuthorized(req)) {
36059
- deps.writeJson(res, 401, { error: "unauthorized" });
36060
- return;
36061
- }
36062
- if (req.method === "DELETE") {
36063
- const deletion = deps.deleteOperation(id);
36064
- deps.writeJson(res, 200, { ok: true, deletion });
36065
- return;
36066
- }
36067
- const body2 = await deps.readJsonBody(req);
36068
- if (!body2) {
36069
- deps.writeJson(res, 400, { error: "invalid_operation_patch" });
36070
- return;
36071
- }
36072
- if (body2.accent !== void 0 && body2.accent !== null && typeof body2.accent !== "string") {
36073
- deps.writeJson(res, 400, { error: "invalid_operation_accent" });
36074
- return;
36075
- }
36076
- if (body2.groupId !== void 0 && body2.groupId !== null && typeof body2.groupId !== "string") {
36077
- deps.writeJson(res, 400, { error: "invalid_operation_groupId" });
36078
- return;
36079
- }
36080
- try {
36081
- const previousNode = typeof body2.title === "string" ? deps.store.get(id) : null;
36082
- const node = deps.store.patch(id, {
36083
- ...typeof body2.title === "string" ? { title: body2.title } : {},
36084
- ...typeof body2.accent === "string" || body2.accent === null ? { accent: body2.accent } : {},
36085
- ...typeof body2.groupId === "string" || body2.groupId === null ? { groupId: body2.groupId } : {},
36086
- ...isRecord4(body2.payload) ? { payload: body2.payload } : {},
36087
- ...isRecord4(body2.geometry) || body2.geometry === null ? { geometry: body2.geometry === null ? null : readGeometry(body2.geometry) } : {}
36088
- });
36089
- if (!node) {
36090
- deps.writeJson(res, 404, { error: "operation_not_found" });
36091
- return;
36092
- }
36093
- deps.persist();
36094
- if (previousNode && typeof body2.title === "string") {
36095
- deps.publishRenameEvent?.({
36096
- operationId: node.id,
36097
- pluginId: node.pluginId,
36098
- type: node.type,
36099
- title: body2.title.trim() === "" ? "" : node.title,
36100
- previousTitle: previousNode.title
36101
- });
36102
- }
36103
- deps.broadcastOperationChanged?.(deps.store.get(id) ?? node);
36104
- deps.writeJson(res, 200, { operation: sanitizeOperationNode(node, deps) });
36105
- } catch (error51) {
36106
- deps.writeJson(res, 400, { error: error51 instanceof Error ? error51.message : "invalid_operation_patch" });
36107
- }
36108
- }
36109
- function sanitizeOperationNode(node, deps) {
36110
- return createSanitizedOpDto(node, { sensitiveFields: deps.getPluginSensitiveFields?.(node.pluginId) ?? [] });
36111
- }
36112
- async function handleGroupCollection(req, res, deps) {
36113
- if (req.method === "GET") {
36114
- const theaterId = readRequestUrl(req).searchParams.get("theaterId");
36115
- const groups = theaterId ? deps.store.listGroups(theaterId) : deps.store.listAllGroups();
36116
- deps.writeJson(res, 200, { groups });
36117
- return;
36118
- }
36119
- if (req.method !== "POST") {
36120
- deps.writeJson(res, 405, { error: "Method not allowed" });
36121
- return;
36122
- }
36123
- if (!deps.isAuthorized(req)) {
36124
- deps.writeJson(res, 401, { error: "unauthorized" });
36125
- return;
36126
- }
36127
- const body2 = await deps.readJsonBody(req);
36128
- if (!body2 || typeof body2.theaterId !== "string" || typeof body2.name !== "string" || typeof body2.color !== "string") {
36129
- deps.writeJson(res, 400, { error: "invalid_group" });
36130
- return;
36131
- }
36132
- try {
36133
- const group = deps.store.createGroup({
36134
- theaterId: body2.theaterId,
36135
- name: body2.name,
36136
- color: body2.color,
36137
- ...typeof body2.order === "number" ? { order: body2.order } : {}
36138
- });
36139
- deps.persist();
36140
- deps.writeJson(res, 201, { group });
36141
- } catch (error51) {
36142
- deps.writeJson(res, 400, { error: error51 instanceof Error ? error51.message : "invalid_group" });
36143
- }
36144
- }
36145
- async function handleGroupItem(req, res, id, deps) {
36146
- if (req.method !== "PATCH" && req.method !== "DELETE") {
36147
- deps.writeJson(res, 405, { error: "Method not allowed" });
36148
- return;
36149
- }
36150
- if (!deps.isAuthorized(req)) {
36151
- deps.writeJson(res, 401, { error: "unauthorized" });
36152
- return;
36153
- }
36154
- if (req.method === "DELETE") {
36155
- const deleted = deps.store.deleteGroup(id);
36156
- if (deleted) deps.persist();
36157
- deps.writeJson(res, deleted ? 200 : 404, deleted ? { ok: true } : { error: "group_not_found" });
36158
- return;
36159
- }
36160
- const body2 = await deps.readJsonBody(req);
36161
- if (!body2) {
36162
- deps.writeJson(res, 400, { error: "invalid_group_patch" });
36163
- return;
36164
- }
36165
- const group = deps.store.updateGroup(id, {
36166
- ...typeof body2.name === "string" ? { name: body2.name } : {},
36167
- ...typeof body2.color === "string" ? { color: body2.color } : {},
36168
- ...typeof body2.order === "number" ? { order: body2.order } : {}
36169
- });
36170
- if (!group) {
36171
- deps.writeJson(res, 404, { error: "group_not_found" });
36172
- return;
36173
- }
36174
- deps.persist();
36175
- deps.writeJson(res, 200, { group });
36176
- }
36177
- function readGeometry(value) {
36178
- const x = readFiniteNumber(value.x);
36179
- const y = readFiniteNumber(value.y);
36180
- const width = readFiniteNumber(value.width);
36181
- const height = readFiniteNumber(value.height);
36182
- const zIndex = readFiniteNumber(value.zIndex);
36183
- if (x === null || y === null || width === null || height === null || zIndex === null) return null;
36184
- return { x, y, width, height, zIndex };
36185
- }
36186
- function readFiniteNumber(value) {
36187
- return typeof value === "number" && Number.isFinite(value) ? value : null;
36188
- }
36189
- function readRequestUrl(req) {
36190
- return new URL(req.url ?? "/", "http://127.0.0.1");
36191
- }
36192
-
36193
- // core/host/deferred-deletion.ts
36194
36356
  var DeferredDeletionError = class extends Error {
36195
36357
  status;
36196
36358
  constructor(status, message) {
@@ -36205,7 +36367,7 @@ var OPERATION_PURGED_EVENT_CHANNEL = "operation:purged";
36205
36367
  var PURGE_RETRY_MS = 1e3;
36206
36368
  function createDeferredDeletionCoordinator(deps) {
36207
36369
  const now = deps.now ?? Date.now;
36208
- const randomId = deps.randomId ?? crypto6.randomUUID;
36370
+ const randomId = deps.randomId ?? crypto5.randomUUID;
36209
36371
  const setTimer = deps.setTimer ?? setTimeout;
36210
36372
  const clearTimer = deps.clearTimer ?? clearTimeout;
36211
36373
  let tombstones = [];
@@ -36861,40 +37023,51 @@ function isSafeCaptureId(value) {
36861
37023
  return value.length > 0 && path18__default.basename(value) === value && !value.includes(path18__default.sep) && !value.includes(path18__default.posix.sep);
36862
37024
  }
36863
37025
  var ENDPOINT_FILE = "listener.json";
36864
- var STORE_VERSION2 = 1;
37026
+ var STORE_VERSION2 = 2;
36865
37027
  function createRemoteEndpointStore(consoleDir, deps = {}) {
36866
37028
  const storage = createRemoteStorage(consoleDir, deps);
36867
37029
  const readFile7 = deps.readFile ?? ((file2) => fs9__default.readFileSync(file2, "utf8"));
36868
- let port = read();
37030
+ let endpoint = read();
36869
37031
  function read() {
36870
37032
  try {
36871
37033
  const parsed = JSON.parse(readFile7(storage.path(ENDPOINT_FILE)));
36872
- if (parsed?.version !== STORE_VERSION2) return null;
36873
- 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 };
36874
37039
  } catch {
36875
37040
  return null;
36876
37041
  }
36877
37042
  }
36878
37043
  function write(next) {
36879
- if (next === port) return;
36880
- 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
+ };
36881
37050
  storage.write(ENDPOINT_FILE, `${JSON.stringify(body2, null, 2)}
36882
37051
  `);
36883
- port = next;
37052
+ endpoint = next;
36884
37053
  }
36885
37054
  return {
36886
- read: () => port,
37055
+ read: () => endpoint,
36887
37056
  remember: (next) => {
36888
- 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);
36889
37059
  },
36890
37060
  forget: () => write(null)
36891
37061
  };
36892
37062
  }
36893
- function isBindablePort(value) {
37063
+ function sameEndpoint(left, right) {
37064
+ return left?.listenPort === right?.listenPort && left?.advertisedPort === right?.advertisedPort;
37065
+ }
37066
+ function isBindablePort2(value) {
36894
37067
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= 65535;
36895
37068
  }
36896
37069
 
36897
- // core/host/access-link.ts
37070
+ // access-protocol/index.ts
36898
37071
  var ACCESS_LINK_SCHEME = "fleet:";
36899
37072
  var ACCESS_LINK_HOST = "join";
36900
37073
  var ACCESS_LINK_PREFIX = "fleet://join?code=";
@@ -36905,6 +37078,8 @@ var MAX_ACCESS_LINK_LENGTH = 4096;
36905
37078
  var ACCESS_TOKEN = /^[A-Za-z0-9_-]{16,512}$/u;
36906
37079
  var CERTIFICATE_FINGERPRINT = /^[0-9A-F]{64}$/u;
36907
37080
  var PAYLOAD_KEYS = ["v", "endpoint", "token", "fingerprint", "label"];
37081
+ var BASE64_URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
37082
+ var BASE64_URL_DECODE = createBase64UrlDecodeTable();
36908
37083
  function encodeAccessLink(payload) {
36909
37084
  const envelope = {
36910
37085
  v: ACCESS_LINK_VERSION,
@@ -36913,13 +37088,13 @@ function encodeAccessLink(payload) {
36913
37088
  fingerprint: normalizeFingerprint(payload.fingerprint),
36914
37089
  label: sanitizeAccessLabel(payload.label)
36915
37090
  };
36916
- return `${ACCESS_LINK_PREFIX}${toBase64Url(JSON.stringify(envelope))}`;
37091
+ return `${ACCESS_LINK_PREFIX}${toBase64Url(new TextEncoder().encode(JSON.stringify(envelope)))}`;
36917
37092
  }
36918
37093
  function parseAccessLink(input) {
36919
37094
  if (typeof input !== "string") throw invalidLink();
36920
37095
  const candidate = input.trim();
36921
37096
  if (candidate.length === 0 || candidate.length > MAX_ACCESS_LINK_LENGTH) throw invalidLink();
36922
- if (/[\u0000-\u001f\u007f\s]/u.test(candidate)) throw invalidLink();
37097
+ if (containsDisallowedLinkWhitespace(candidate)) throw invalidLink();
36923
37098
  let url2;
36924
37099
  try {
36925
37100
  url2 = new URL(candidate);
@@ -36927,7 +37102,7 @@ function parseAccessLink(input) {
36927
37102
  throw invalidLink();
36928
37103
  }
36929
37104
  if (url2.protocol !== ACCESS_LINK_SCHEME || url2.username || url2.password || url2.hash) throw invalidLink();
36930
- if (url2.hostname !== ACCESS_LINK_HOST || url2.port !== "") throw invalidLink();
37105
+ if (url2.hostname !== ACCESS_LINK_HOST || url2.port !== "" || url2.pathname !== "") throw invalidLink();
36931
37106
  const keys = [...url2.searchParams.keys()];
36932
37107
  if (keys.length !== 1 || keys[0] !== "code") throw invalidLink();
36933
37108
  return readPayload(decodePayload(url2.searchParams.get("code") ?? ""));
@@ -36936,7 +37111,7 @@ function decodePayload(code) {
36936
37111
  if (!/^[A-Za-z0-9_-]+$/u.test(code)) throw invalidLink();
36937
37112
  let text;
36938
37113
  try {
36939
- text = Buffer.from(code, "base64url").toString("utf8");
37114
+ text = new TextDecoder("utf-8", { fatal: true }).decode(fromBase64Url(code));
36940
37115
  } catch {
36941
37116
  throw invalidLink();
36942
37117
  }
@@ -36992,10 +37167,61 @@ function normalizeFingerprint(value) {
36992
37167
  return value.replace(/[^0-9a-fA-F]/gu, "").toUpperCase();
36993
37168
  }
36994
37169
  function sanitizeAccessLabel(value) {
36995
- 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;
37178
+ }
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;
36996
37217
  }
36997
- function toBase64Url(value) {
36998
- return Buffer.from(value, "utf8").toString("base64url");
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;
36999
37225
  }
37000
37226
  function invalidLink() {
37001
37227
  return new Error("pairing_target_invalid");
@@ -37032,7 +37258,7 @@ function createRemoteIdentityStore(consoleDir, deps = {}) {
37032
37258
  async function rotate(host) {
37033
37259
  const { certificatePem, privateKeyPem } = await generate(host);
37034
37260
  const createdAt = now();
37035
- const expiresAt = Date.parse(new crypto6.X509Certificate(certificatePem).validTo);
37261
+ const expiresAt = Date.parse(new crypto5.X509Certificate(certificatePem).validTo);
37036
37262
  storage.write(KEY_FILE, privateKeyPem);
37037
37263
  storage.write(CERTIFICATE_FILE, certificatePem);
37038
37264
  storage.write(META_FILE, `${JSON.stringify({ host, createdAt, expiresAt })}
@@ -37049,12 +37275,12 @@ function createRemoteIdentityStore(consoleDir, deps = {}) {
37049
37275
  return { ensure, read, rotate };
37050
37276
  }
37051
37277
  function fingerprintOf(certificatePem) {
37052
- return new crypto6.X509Certificate(certificatePem).fingerprint256;
37278
+ return new crypto5.X509Certificate(certificatePem).fingerprint256;
37053
37279
  }
37054
37280
  function fingerprintsMatch(left, right) {
37055
37281
  const a = Buffer.from(normalizeFingerprint(left), "hex");
37056
37282
  const b = Buffer.from(normalizeFingerprint(right), "hex");
37057
- 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);
37058
37284
  }
37059
37285
  async function generateSelfSignedCertificate(host) {
37060
37286
  const isIpAddress = /^\d{1,3}(?:\.\d{1,3}){3}$/u.test(host);
@@ -37081,7 +37307,7 @@ function createRemoteHostStore(consoleDir, deps = {}) {
37081
37307
  const storage = createRemoteStorage(consoleDir, deps);
37082
37308
  const readFile7 = deps.readFile ?? ((file2) => fs9__default.readFileSync(file2, "utf8"));
37083
37309
  const now = deps.now ?? Date.now;
37084
- const randomId = deps.randomId ?? (() => crypto6.randomUUID());
37310
+ const randomId = deps.randomId ?? (() => crypto5.randomUUID());
37085
37311
  const pending = /* @__PURE__ */ new Map();
37086
37312
  let hosts = read();
37087
37313
  function read() {
@@ -37156,6 +37382,80 @@ function isRecord6(value) {
37156
37382
  const entry = value;
37157
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");
37158
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
+ }
37159
37459
  var REMOTE_PROBE_TIMEOUT_MS = 6e3;
37160
37460
  function probeRemoteIdentity(hostname5, port, expected, timeoutMs = REMOTE_PROBE_TIMEOUT_MS) {
37161
37461
  return new Promise((resolve3) => {
@@ -37325,7 +37625,7 @@ var SHIM_NAMED_EXPORTS = {
37325
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"],
37326
37626
  "react/jsx-runtime": ["Fragment", "jsx", "jsxs"],
37327
37627
  "@fleet-console/sdk/plugin/browser": ["React", "createClientCapabilities", "defineOperationKind", "definePlugin", "useOperationStatus", "useOperations", "usePluginApi", "usePluginSettings", "usePluginStorage", "useStoreSnapshot"],
37328
- "@fleet-console/sdk/settings/browser": ["SettingsCard", "SettingsField", "SettingsRow", "SettingsSelect", "SettingsToggle", "defineSettingsSection"],
37628
+ "@fleet-console/sdk/settings/browser": ["ExperimentalBadge", "SettingsCard", "SettingsField", "SettingsRow", "SettingsSelect", "SettingsToggle", "defineSettingsSection"],
37329
37629
  "@fleet-console/sdk/operations/browser": ["ApiError", "OperationBody", "assertOperationNode", "fetchOperationCatalog", "hasForbiddenBrowserPayloadKey"],
37330
37630
  "@fleet-console/sdk/notifications/browser": ["defineNotificationKind"],
37331
37631
  "@fleet-console/sdk/react/browser": ["PluginErrorBoundary", "React", "Select", "useSelect"]
@@ -37601,6 +37901,7 @@ function createFleetPluginHost(deps) {
37601
37901
  const plugins = filterDiscoveredPlugins(discoverFleetPlugins(deps));
37602
37902
  const sensitiveFieldsByPluginId = new Map(plugins.map((plugin) => [plugin.manifest.id, plugin.manifest.sensitiveFields ?? []]));
37603
37903
  const generatedBundleDirs = /* @__PURE__ */ new Set();
37904
+ const apiCatalog = [];
37604
37905
  const isProcessAlive2 = deps.isProcessAlive ?? isPluginBundleOwnerAlive;
37605
37906
  const removeBundleDir = deps.removeBundleDir ?? removePluginBundleDir;
37606
37907
  const importModule = deps.importModule ?? ((entry) => importPluginModule(entry, deps.bundleCacheDir, generatedBundleDirs));
@@ -37633,21 +37934,109 @@ function createFleetPluginHost(deps) {
37633
37934
  }
37634
37935
  }
37635
37936
  }
37636
- return { plugins, sensitiveFieldsByPluginId, boot: boot2, cleanup };
37937
+ return { plugins, sensitiveFieldsByPluginId, apiCatalog, boot: boot2, cleanup };
37637
37938
  async function bootPluginRoutes(plugin) {
37939
+ const pendingRoutes = [];
37940
+ const pendingUpgrades = [];
37941
+ const pendingCatalog = [];
37638
37942
  const mod = await importModule(plugin.routesEntry);
37639
37943
  const register = resolveRegister(mod);
37640
37944
  if (!register) return;
37641
- await register({
37642
- pluginId: plugin.manifest.id,
37643
- manifest: plugin.manifest,
37644
- basePath: `/plugins/${plugin.manifest.id}`,
37645
- wsBasePath: `/plugins/${plugin.manifest.id}/ws`,
37646
- host: deps.host,
37647
- registerRouter: createScopedRouteRegistrar(deps.routes, `/plugins/${plugin.manifest.id}`, `/api/v1/plugins/${plugin.manifest.id}`),
37648
- 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
+ }
37649
38009
  });
38010
+ return trackedUnregister;
37650
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
+ };
37651
38040
  }
37652
38041
  function filterDiscoveredPlugins(plugins) {
37653
38042
  const accepted = [];
@@ -37790,19 +38179,73 @@ function resolveRegister(mod) {
37790
38179
  if (typeof mod.default === "function") return mod.default;
37791
38180
  return mod.default?.register ?? null;
37792
38181
  }
37793
- function createScopedRouteRegistrar(routes, basePath, apiBasePath) {
37794
- return (requestedPath, handler) => {
38182
+ function createScopedRouteRegistrar(routes, apiCatalog, pendingRoutes, pendingCatalog, basePath, apiBasePath) {
38183
+ function registerRouter(requestedPath, handler, catalog) {
37795
38184
  const prefix = resolveScopedPrefix(basePath, requestedPath, apiBasePath);
37796
- assertNoRouteOverlap(prefix, routes.list().map((route) => route.prefix));
37797
- routes.register(prefix, handler);
37798
- };
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;
37799
38195
  }
37800
- function createScopedUpgradeRegistrar(upgrades, basePath) {
37801
- return (requestedPath, handler) => {
38196
+ function createScopedUpgradeRegistrar(upgrades, apiCatalog, pendingUpgrades, pendingCatalog, basePath) {
38197
+ function registerWsHandler(requestedPath, handler, catalog) {
37802
38198
  const prefix = resolveScopedPrefix(basePath, requestedPath);
37803
- assertNoRouteOverlap(prefix, upgrades.list().map((upgrade) => upgrade.prefix));
37804
- upgrades.register(prefix, handler);
37805
- };
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}`;
37806
38249
  }
37807
38250
  function resolveScopedPrefix(basePath, requestedPath, apiBasePath) {
37808
38251
  if (requestedPath.split("/").some((segment) => segment === "." || segment === "..")) throw new Error("plugin_route_outside_scope");
@@ -38757,231 +39200,232 @@ var SERVER_API_CATALOG = [
38757
39200
  path: PAIRING_IDENTITY_PATH,
38758
39201
  summary: "Read the loopback runtime pairing identity.",
38759
39202
  category: "Observer",
38760
- gate: "loopback"
39203
+ gate: "loopback",
39204
+ transport: "http"
38761
39205
  },
38762
39206
  {
38763
39207
  method: "GET",
38764
39208
  path: "/api/v1/status",
38765
39209
  summary: "\uCF58\uC194 \uAD00\uCE21 \uC0C1\uD0DC\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
38766
39210
  category: "Observer",
38767
- gate: "loopback"
39211
+ gate: "loopback",
39212
+ transport: "http"
38768
39213
  },
38769
39214
  {
38770
39215
  method: "GET",
38771
39216
  path: "/api/v1/settings/api-catalog",
38772
39217
  summary: "\uBC31\uC5D4\uB4DC API \uCE74\uD0C8\uB85C\uADF8\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
38773
39218
  category: "Observer",
38774
- gate: "loopback"
39219
+ gate: "loopback",
39220
+ transport: "http"
38775
39221
  },
38776
39222
  {
38777
39223
  method: "GET",
38778
39224
  path: "/api/v1/updates/release-notes",
38779
39225
  summary: "Get the console release notes.",
38780
39226
  category: "Update",
38781
- gate: "loopback"
39227
+ gate: "loopback",
39228
+ transport: "http"
38782
39229
  },
38783
39230
  {
38784
39231
  method: "GET",
38785
39232
  path: "/api/v1/theaters",
38786
39233
  summary: "Theater \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
38787
39234
  category: "Observer",
38788
- gate: "loopback"
39235
+ gate: "loopback",
39236
+ transport: "http"
38789
39237
  },
38790
39238
  {
38791
39239
  method: "POST",
38792
39240
  path: "/api/v1/theaters",
38793
39241
  summary: "\uC0C8 Theater\uB97C \uB4F1\uB85D\uD569\uB2C8\uB2E4.",
38794
39242
  category: "Observer",
38795
- gate: "origin-write"
39243
+ gate: "origin-write",
39244
+ transport: "http"
38796
39245
  },
38797
39246
  {
38798
39247
  method: "PATCH",
38799
39248
  path: "/api/v1/theaters/:theaterId",
38800
39249
  summary: "Theater \uD45C\uC2DC \uC21C\uC11C\uB97C \uBCC0\uACBD\uD569\uB2C8\uB2E4.",
38801
39250
  category: "Observer",
38802
- gate: "origin-write"
39251
+ gate: "origin-write",
39252
+ transport: "http"
38803
39253
  },
38804
39254
  {
38805
39255
  method: "DELETE",
38806
39256
  path: "/api/v1/theaters/:theaterId",
38807
39257
  summary: "Theater\uC640 \uC18C\uC18D Operation\uC744 \uC81C\uAC70\uD569\uB2C8\uB2E4.",
38808
39258
  category: "Observer",
38809
- gate: "origin-write"
39259
+ gate: "origin-write",
39260
+ transport: "http"
38810
39261
  },
38811
39262
  {
38812
39263
  method: "POST",
38813
39264
  path: "/api/v1/deletions/:deletionId/restore",
38814
39265
  summary: "\uC720\uC608 \uC911\uC778 Operation \uB610\uB294 Theater \uC0AD\uC81C\uB97C \uBCF5\uAD6C\uD569\uB2C8\uB2E4.",
38815
39266
  category: "Observer",
38816
- gate: "origin-write"
39267
+ gate: "origin-write",
39268
+ transport: "http"
38817
39269
  },
38818
39270
  {
38819
39271
  method: "POST",
38820
39272
  path: "/api/v1/theaters/:theaterId/codex-workspace",
38821
39273
  summary: "Resolve the Codex workspace for a Theater.",
38822
39274
  category: "Observer",
38823
- gate: "origin-write"
38824
- },
38825
- {
38826
- method: "POST",
38827
- path: "/plugins/terminal/shell/ticket",
38828
- summary: "Shell WebSocket \uC811\uC18D \uD2F0\uCF13\uC744 \uBC1C\uAE09\uD569\uB2C8\uB2E4.",
38829
- category: "Terminal Plugin",
38830
- gate: "origin-write"
38831
- },
38832
- {
38833
- method: "GET",
38834
- path: "/plugins/terminal/settings",
38835
- summary: "Get Terminal plugin prompt settings.",
38836
- category: "Terminal Plugin",
38837
- gate: "loopback"
38838
- },
38839
- {
38840
- method: "PUT",
38841
- path: "/plugins/terminal/settings",
38842
- summary: "Save Terminal plugin prompt settings.",
38843
- category: "Terminal Plugin",
38844
- gate: "origin-write"
39275
+ gate: "origin-write",
39276
+ transport: "http"
38845
39277
  },
38846
39278
  {
38847
39279
  method: "POST",
38848
39280
  path: "/api/v1/theaters/folder-listings",
38849
39281
  summary: "Theater \uD3F4\uB354 \uC120\uD0DD \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
38850
39282
  category: "Observer",
38851
- gate: "origin-write"
39283
+ gate: "origin-write",
39284
+ transport: "http"
38852
39285
  },
38853
39286
  {
38854
39287
  method: "POST",
38855
39288
  path: "/api/v1/theaters/folder-grants",
38856
39289
  summary: "Theater \uD3F4\uB354 \uC811\uADFC grant\uB97C \uBC1C\uAE09\uD569\uB2C8\uB2E4.",
38857
39290
  category: "Observer",
38858
- gate: "origin-write"
39291
+ gate: "origin-write",
39292
+ transport: "http"
38859
39293
  },
38860
39294
  {
38861
39295
  method: "POST",
38862
39296
  path: "/api/v1/updates/apply",
38863
39297
  summary: "Request console update application.",
38864
39298
  category: "Update",
38865
- gate: "origin-strict"
39299
+ gate: "origin-strict",
39300
+ transport: "http"
38866
39301
  },
38867
39302
  {
38868
39303
  method: "POST",
38869
39304
  path: "/api/v1/access-grants",
38870
39305
  summary: "Issue a single-use grant that opens a console session.",
38871
39306
  category: "Access",
38872
- gate: "lock-token"
39307
+ gate: "lock-token",
39308
+ transport: "http"
38873
39309
  },
38874
39310
  {
38875
39311
  method: "GET",
38876
39312
  path: "/api/v1/access-links",
38877
39313
  summary: "Report the remote listener, its identity, its unused links, and the devices it has paired.",
38878
39314
  category: "Access",
38879
- gate: "loopback"
39315
+ gate: "loopback",
39316
+ transport: "http"
38880
39317
  },
38881
39318
  {
38882
39319
  method: "DELETE",
38883
39320
  path: "/api/v1/access-links/:linkId",
38884
39321
  summary: "Revoke one unused remote access link.",
38885
39322
  category: "Access",
38886
- gate: "origin-write"
39323
+ gate: "origin-write",
39324
+ transport: "http"
38887
39325
  },
38888
39326
  {
38889
39327
  method: "DELETE",
38890
39328
  path: "/api/v1/access-sessions/:sessionHandle",
38891
39329
  summary: "End one open remote session, leaving its pairing intact.",
38892
39330
  category: "Access",
38893
- gate: "origin-write"
39331
+ gate: "origin-write",
39332
+ transport: "http"
38894
39333
  },
38895
39334
  {
38896
39335
  method: "DELETE",
38897
39336
  path: "/api/v1/paired-devices/:deviceId",
38898
39337
  summary: "Unpair one device so it cannot rejoin without a new access link.",
38899
39338
  category: "Access",
38900
- gate: "origin-write"
39339
+ gate: "origin-write",
39340
+ transport: "http"
38901
39341
  },
38902
39342
  {
38903
39343
  method: "POST",
38904
39344
  path: "/api/v1/remote-identity/rotations",
38905
39345
  summary: "Issue a new remote certificate, invalidating every link, pin, and session for the old one.",
38906
39346
  category: "Access",
38907
- gate: "origin-write"
38908
- },
38909
- {
38910
- method: "GET",
38911
- path: "/join",
38912
- summary: "Explain that an access link opens in Fleet Console Desktop.",
38913
- category: "Access",
38914
- gate: "loopback"
39347
+ gate: "origin-write",
39348
+ transport: "http"
38915
39349
  },
38916
39350
  {
38917
39351
  method: "POST",
38918
39352
  path: "/api/v1/access-links",
38919
39353
  summary: "Create a remote access link for this console.",
38920
39354
  category: "Access",
38921
- gate: "lock-token"
39355
+ gate: "lock-token",
39356
+ transport: "http"
38922
39357
  },
38923
39358
  {
38924
39359
  method: "POST",
38925
39360
  path: "/api/v1/join",
38926
39361
  summary: "Exchange a single-use grant for a pairing, or resume an existing pairing.",
38927
39362
  category: "Access",
38928
- gate: "loopback"
39363
+ gate: "loopback",
39364
+ transport: "http"
38929
39365
  },
38930
39366
  {
38931
39367
  method: "GET",
38932
39368
  path: "/api/v1/remote-hosts",
38933
39369
  summary: "List the other consoles this one can jump to.",
38934
39370
  category: "Access",
38935
- gate: "loopback"
39371
+ gate: "loopback",
39372
+ transport: "http"
38936
39373
  },
38937
39374
  {
38938
39375
  method: "POST",
38939
39376
  path: "/api/v1/remote-hosts",
38940
39377
  summary: "Remember another console from its access link, after confirming its certificate.",
38941
39378
  category: "Access",
38942
- gate: "origin-write"
39379
+ gate: "origin-write",
39380
+ transport: "http"
38943
39381
  },
38944
39382
  {
38945
39383
  method: "PATCH",
38946
39384
  path: "/api/v1/remote-hosts/:hostId",
38947
39385
  summary: "Rename a remembered console.",
38948
39386
  category: "Access",
38949
- gate: "origin-write"
39387
+ gate: "origin-write",
39388
+ transport: "http"
38950
39389
  },
38951
39390
  {
38952
39391
  method: "DELETE",
38953
39392
  path: "/api/v1/remote-hosts/:hostId",
38954
39393
  summary: "Forget a remembered console and its certificate pin.",
38955
39394
  category: "Access",
38956
- gate: "origin-write"
39395
+ gate: "origin-write",
39396
+ transport: "http"
38957
39397
  },
38958
39398
  {
38959
39399
  method: "POST",
38960
39400
  path: "/api/v1/remote-hosts/:hostId/probes",
38961
39401
  summary: "Check whether a remembered console answers and still presents its pinned certificate.",
38962
39402
  category: "Access",
38963
- gate: "origin-write"
39403
+ gate: "origin-write",
39404
+ transport: "http"
38964
39405
  },
38965
39406
  {
38966
39407
  method: "GET",
38967
39408
  path: "/api/v1/local-consoles",
38968
39409
  summary: "List the consoles running on this machine that this one can point a window at.",
38969
39410
  category: "Access",
38970
- gate: "loopback"
39411
+ gate: "loopback",
39412
+ transport: "http"
38971
39413
  },
38972
39414
  {
38973
39415
  method: "POST",
38974
39416
  path: "/api/v1/desktop/handoff",
38975
39417
  summary: "Hand the attached Desktop what it needs to open one remembered console, consuming any pending grant.",
38976
39418
  category: "Desktop",
38977
- gate: "origin-write"
39419
+ gate: "origin-write",
39420
+ transport: "http"
38978
39421
  },
38979
39422
  {
38980
39423
  method: "GET",
38981
39424
  path: "/api/v1/health",
38982
39425
  summary: "Check console status with the lock token.",
38983
39426
  category: "Health",
38984
- gate: "lock-token"
39427
+ gate: "lock-token",
39428
+ transport: "http"
38985
39429
  }
38986
39430
  ];
38987
39431
  var REMOTE_HOSTS_PATH = "/api/v1/remote-hosts";
@@ -39035,6 +39479,7 @@ function createConsoleServer(deps = {}) {
39035
39479
  let remoteFingerprint = null;
39036
39480
  let remoteReconcile = Promise.resolve();
39037
39481
  let remoteLastError = null;
39482
+ const remoteJoinGuard = createRemoteJoinGuard();
39038
39483
  let boundPort = null;
39039
39484
  let controlPruneNotifyQueued = false;
39040
39485
  let lastPublishedControlHolder = null;
@@ -39191,6 +39636,7 @@ function createConsoleServer(deps = {}) {
39191
39636
  process.env.FLEET_CONSOLE_PACKAGE_ROOT = release.packageRoot;
39192
39637
  const pluginHost = createFleetPluginHost({
39193
39638
  ...resolveBuiltInPluginDiscoveryRoots(release.packageRoot),
39639
+ homeDir: deps.pluginHomeDir,
39194
39640
  bundleCacheDir: path18__default.join(durablePaths.dir, "plugin-cache"),
39195
39641
  routes: routeRegistry,
39196
39642
  upgrades: upgradeRegistry,
@@ -39242,10 +39688,11 @@ function createConsoleServer(deps = {}) {
39242
39688
  const globalSettingsRouter = createGlobalSettingsRouter({
39243
39689
  consoleSettingsStore,
39244
39690
  isAuthorized: isTerminalAuthorized,
39691
+ isRemoteAccessOwner: isLoopbackListener,
39245
39692
  readJsonBody,
39246
39693
  writeJson,
39247
39694
  onThemeChanged: broadcastDesktopThemeChanged,
39248
- onRemoteAccessChanged: () => reconcileRemoteAccess()
39695
+ onRemoteAccessChanged: (change) => reconcileRemoteAccess(change)
39249
39696
  });
39250
39697
  const desktopThemeRouter = createDesktopThemeRouter({
39251
39698
  getTheme: () => consoleSettingsStore.load().general?.theme ?? "instrument",
@@ -39370,14 +39817,13 @@ function createConsoleServer(deps = {}) {
39370
39817
  routeRegistry.register("/plugin-runtime", handlePluginRuntimeRoute);
39371
39818
  function isRequestHostAllowed(req) {
39372
39819
  const listener = listenerForRequest(req);
39373
- return listener !== null && validateHost(req, `${listener.host}:${listener.port}`);
39820
+ return listener !== null && validateHost(req, listenerAuthority(listener.host, listener.port, listener.secure), listener.secure);
39374
39821
  }
39375
39822
  function listenerForRequest(req) {
39376
39823
  return resolveListenerIdentity(listeners, req.socket);
39377
39824
  }
39378
39825
  function isRemoteRequestAdmitted(listener, req, pathname) {
39379
39826
  if (pathname === "/api/v1/join") return true;
39380
- if (pathname === JOIN_NOTICE_PATH && req.method === "GET") return true;
39381
39827
  const session = access.resolveSession(readSessionCookie(req.headers, listener.port), listener.audience);
39382
39828
  if (session === null) return false;
39383
39829
  return session.access !== "monitoring" || isReadOnlyRequest(req);
@@ -39405,10 +39851,6 @@ function createConsoleServer(deps = {}) {
39405
39851
  handlePairingIdentity(req, res);
39406
39852
  return;
39407
39853
  }
39408
- if (pathname === JOIN_NOTICE_PATH) {
39409
- handleJoinNotice(req, res);
39410
- return;
39411
- }
39412
39854
  if (tryServeStaticConsole2(req, res, pathname)) return;
39413
39855
  if (pathname === "/api/v1/health") {
39414
39856
  handleHealth2(req, res);
@@ -39586,10 +40028,15 @@ function createConsoleServer(deps = {}) {
39586
40028
  const remote = listeners.find((entry) => entry.audience === "remote");
39587
40029
  const sessions = access.listSessions("remote");
39588
40030
  writeJson(res, 200, {
39589
- listening: remote !== void 0 && remoteFingerprint !== null,
39590
- 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(),
39591
40039
  fingerprint: remoteFingerprint,
39592
- lastError: remoteLastError,
39593
40040
  // 발급 사실만 나간다 — 목록을 보는 것으로는 어떤 링크도 다시 쓸 수 없다.
39594
40041
  links: access.listGrants("remote"),
39595
40042
  /**
@@ -39681,12 +40128,12 @@ function createConsoleServer(deps = {}) {
39681
40128
  return true;
39682
40129
  }
39683
40130
  const configured = consoleSettingsStore.load().general?.remoteAccess;
39684
- const bindHost = listeners.find((entry) => entry.audience === "remote")?.host ?? (configured?.enabled === true ? configured.bindHost : null);
39685
- if (!bindHost) {
40131
+ const advertisedHost = listeners.find((entry) => entry.audience === "remote")?.host ?? (configured?.enabled === true ? effectiveRemoteAccessAdvertisedTuple(configured).host : null);
40132
+ if (!advertisedHost) {
39686
40133
  writeJson(res, 409, { error: "remote_access_disabled" });
39687
40134
  return true;
39688
40135
  }
39689
- await remoteIdentityStore.rotate(bindHost);
40136
+ await remoteIdentityStore.rotate(advertisedHost);
39690
40137
  access.revokeGrants("remote");
39691
40138
  pairedDeviceStore.revokeAll("remote");
39692
40139
  remoteEndpointStore.forget();
@@ -39698,21 +40145,6 @@ function createConsoleServer(deps = {}) {
39698
40145
  writeJson(res, 200, { fingerprint: remoteFingerprint });
39699
40146
  return true;
39700
40147
  }
39701
- function handleJoinNotice(req, res) {
39702
- if (req.method !== "GET") {
39703
- writeJson(res, 405, { error: "Method not allowed" });
39704
- return;
39705
- }
39706
- const body2 = Buffer.from(JOIN_NOTICE_DOCUMENT, "utf8");
39707
- res.writeHead(200, withSecurityHeaders2({
39708
- "Content-Type": "text/html; charset=utf-8",
39709
- "Content-Length": String(body2.byteLength),
39710
- "Cache-Control": "no-store",
39711
- "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
39712
- "Referrer-Policy": "no-referrer"
39713
- }));
39714
- res.end(body2);
39715
- }
39716
40148
  function handleAccessLinkIssue(req, res) {
39717
40149
  if (req.method !== "POST") {
39718
40150
  writeJson(res, 405, { error: "Method not allowed" });
@@ -39746,6 +40178,26 @@ function createConsoleServer(deps = {}) {
39746
40178
  writeJson(res, 401, { error: "unauthorized" });
39747
40179
  return true;
39748
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) {
39749
40201
  const body2 = await readJsonBody(req);
39750
40202
  const token = isPlainObject2(body2) && typeof body2.token === "string" ? body2.token : null;
39751
40203
  const device = isPlainObject2(body2) ? sanitizeDeviceName(body2.device) : null;
@@ -39757,10 +40209,10 @@ function createConsoleServer(deps = {}) {
39757
40209
  "Set-Cookie": expirePairingCookie({ secure: listener.secure, port: listener.port })
39758
40210
  }));
39759
40211
  res.end(JSON.stringify({ error: joined.error }));
39760
- return true;
40212
+ return false;
39761
40213
  }
39762
40214
  writeJson(res, joined.status, { error: joined.error });
39763
- return true;
40215
+ return false;
39764
40216
  }
39765
40217
  if (listener.audience === "remote" && joined.session.access === "full") broadcastControlChanged();
39766
40218
  const cookies = [formatSessionCookie(joined.session, { secure: listener.secure, port: listener.port })];
@@ -40101,7 +40553,7 @@ function createConsoleServer(deps = {}) {
40101
40553
  writeJson(res, 405, { error: "Method not allowed" });
40102
40554
  return;
40103
40555
  }
40104
- writeJson(res, 200, { version: version2, routes: buildApiCatalog() });
40556
+ writeJson(res, 200, { version: version2, routes: buildApiCatalog(pluginHost.apiCatalog) });
40105
40557
  }
40106
40558
  function handleEnvironmentDiagnostics(req, res) {
40107
40559
  if (channel !== "local") {
@@ -40518,12 +40970,19 @@ function createConsoleServer(deps = {}) {
40518
40970
  });
40519
40971
  }
40520
40972
  }
40521
- function reconcileRemoteAccess() {
40973
+ function reconcileRemoteAccess(change) {
40522
40974
  return queueRemoteReconcile(() => {
40523
- const configured = consoleSettingsStore.load().general?.remoteAccess;
40524
- const desiredHost = configured?.enabled === true ? configured.bindHost : void 0;
40525
- const active = listeners.find((entry) => entry.audience === "remote");
40526
- 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;
40527
40986
  });
40528
40987
  }
40529
40988
  function reconcileRemoteIdentity() {
@@ -40542,6 +41001,25 @@ function createConsoleServer(deps = {}) {
40542
41001
  remoteLastError = null;
40543
41002
  await startRemoteAccessGuarded(boundPort);
40544
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
+ }
40545
41023
  async function startRemoteAccessGuarded(actualPort) {
40546
41024
  try {
40547
41025
  await startRemoteAccessIfEnabled(actualPort);
@@ -40569,57 +41047,104 @@ function createConsoleServer(deps = {}) {
40569
41047
  remoteFingerprint = null;
40570
41048
  listeners = listeners.filter((entry) => entry.audience !== "remote");
40571
41049
  access.revokeSessions("remote");
40572
- access.revokeGrants("remote");
40573
41050
  for (const owner of desktopShellsByOwner.keys()) {
40574
41051
  if (owner !== "local") desktopShellsByOwner.delete(owner);
40575
41052
  }
40576
41053
  broadcastControlChanged();
40577
41054
  if (closing) await new Promise((resolve3) => closing.close(() => resolve3()));
40578
41055
  }
40579
- async function startRemoteAccessIfEnabled(actualPort) {
41056
+ async function startRemoteAccessIfEnabled(_actualPort) {
40580
41057
  const configured = consoleSettingsStore.load().general?.remoteAccess;
40581
- const bindHost = configured?.enabled === true ? configured.bindHost : void 0;
40582
- if (!bindHost) return;
40583
- const previousFingerprint = remoteIdentityStore.read()?.fingerprint ?? null;
40584
- const identity2 = await remoteIdentityStore.ensure(bindHost);
40585
- 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");
40586
41068
  pairedDeviceStore.revokeAll("remote");
40587
41069
  remoteEndpointStore.forget();
40588
41070
  }
40589
- const published = remoteEndpointStore.read();
40590
- const publishedPort = published ?? actualPort;
40591
- const started = await startRemoteListener({
40592
- identity: identity2,
40593
- bindHost,
40594
- port: publishedPort,
40595
- handler: handleRequest,
40596
- upgradeRegistry,
40597
- isHostAllowed: isRequestHostAllowed,
40598
- isAdmitted: (req) => {
40599
- const resolved = listenerForRequest(req);
40600
- return resolved === null || resolved.audience === "local" || isRemoteRequestAdmitted(resolved, req, getPathname(req));
40601
- }
40602
- }).catch((error51) => {
40603
- if (published !== null && isAddressInUse(error51)) throw remotePortUnavailable(error51);
40604
- throw error51;
40605
- });
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;
40606
41075
  const listener = {
40607
41076
  audience: "remote",
40608
- host: bindHost,
40609
- port: publishedPort,
40610
- origin: `https://${bindHost}:${publishedPort}`,
41077
+ host: effectiveAdvertised.host,
41078
+ port: effectiveAdvertised.port,
41079
+ origin: remoteOrigin(effectiveAdvertised.host, effectiveAdvertised.port),
40611
41080
  secure: true,
40612
- // 게이트는 소켓의 실제 주소로 리스너를 찾는다. 설정이 이름을 받으므로, 여기에 그 이름을
40613
- // 그대로 두면 어떤 요청도 자기 리스너를 찾지 못해 조인부터 전부 막힌다 — 리스너와 링크는
40614
- // 멀쩡해 보이는 채로.
40615
- bindAddress: started.address
41081
+ bindAddress: started.address,
41082
+ bindPort: started.port
40616
41083
  };
40617
41084
  listeners = [...listeners, listener];
40618
41085
  remoteFingerprint = identity2.fingerprint;
40619
- remoteServer = started.server;
40620
- remoteEndpointStore.remember(publishedPort);
41086
+ remoteEndpointStore.remember({ listenPort: started.port, advertisedPort: effectiveAdvertised.port });
40621
41087
  startControlExpirySweep();
40622
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
+ }
40623
41148
  function listenOnce(portToBind, statePatch) {
40624
41149
  return new Promise((resolve3, reject) => {
40625
41150
  const srv = createHttpServer(handleRequest, upgradeRegistry, isRequestHostAllowed);
@@ -40669,35 +41194,6 @@ function sanitizeDeviceName(value) {
40669
41194
  function consoleLabel() {
40670
41195
  return sanitizeAccessLabel(os4__default.hostname().replace(/\.local$/iu, "")) || "Fleet Console";
40671
41196
  }
40672
- var JOIN_NOTICE_PATH = "/join";
40673
- var JOIN_NOTICE_DOCUMENT = `<!doctype html>
40674
- <html lang="en">
40675
- <head>
40676
- <meta charset="utf-8">
40677
- <meta name="viewport" content="width=device-width, initial-scale=1">
40678
- <title>Fleet Console access link</title>
40679
- <style>
40680
- :root { color-scheme: dark; }
40681
- body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px;
40682
- background: #0b1116; color: #e6edf3;
40683
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
40684
- main { max-width: 30rem; }
40685
- h1 { margin: 0 0 12px; font-size: 20px; font-weight: 650; }
40686
- p { margin: 0 0 12px; color: #aebbc5; font-size: 14px; line-height: 1.55; }
40687
- strong { color: #e6edf3; font-weight: 600; }
40688
- </style>
40689
- </head>
40690
- <body>
40691
- <main>
40692
- <h1>This console is reached with an access link</h1>
40693
- <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>
40694
- <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>
40695
- <p>In the Fleet Console you are already using, open <strong>Settings &rarr; Remote access &rarr; Hosts</strong> and paste the link there.</p>
40696
- <p><strong>Remote access is experimental.</strong> How links and pairings behave can still change between releases.</p>
40697
- </main>
40698
- </body>
40699
- </html>
40700
- `;
40701
41197
  function decodeHandle(raw) {
40702
41198
  if (raw.includes("/")) return "";
40703
41199
  try {
@@ -40708,22 +41204,22 @@ function decodeHandle(raw) {
40708
41204
  }
40709
41205
  function remoteAccessErrorCode(error51) {
40710
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";
40711
41210
  if (code === REMOTE_PORT_UNAVAILABLE) return "remote_port_unavailable";
40712
41211
  if (code === "EADDRNOTAVAIL") return "bind_address_unavailable";
40713
- if (code === "EADDRINUSE") return "bind_address_in_use";
41212
+ if (code === "EADDRINUSE") return "custom_port_unavailable";
40714
41213
  if (code === "EACCES") return "bind_permission_denied";
40715
41214
  return "remote_listener_failed";
40716
41215
  }
41216
+ function codedRemoteError(code, cause) {
41217
+ return Object.assign(new Error(code), { code, cause });
41218
+ }
40717
41219
  function errorCodeOf(error51) {
40718
41220
  return typeof error51 === "object" && error51 !== null && "code" in error51 && typeof error51.code === "string" ? error51.code : "";
40719
41221
  }
40720
- function isAddressInUse(error51) {
40721
- return errorCodeOf(error51) === "EADDRINUSE";
40722
- }
40723
41222
  var REMOTE_PORT_UNAVAILABLE = "FLEET_REMOTE_PORT_UNAVAILABLE";
40724
- function remotePortUnavailable(cause) {
40725
- return Object.assign(new Error("remote_port_unavailable"), { code: REMOTE_PORT_UNAVAILABLE, cause });
40726
- }
40727
41223
  function resolveBuiltInPluginDiscoveryRoots(packageRoot) {
40728
41224
  const packageRootRepo = path18__default.resolve(packageRoot, "..", "..");
40729
41225
  const sourceRoot = path18__default.join(packageRootRepo, "runtime", "fleet-plugins");
@@ -40758,7 +41254,7 @@ async function startRemoteListener(input) {
40758
41254
  });
40759
41255
  });
40760
41256
  const address = srv.address();
40761
- 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 };
40762
41258
  }
40763
41259
  function createHttpServer(handler, upgradeRegistry, isHostAllowed2) {
40764
41260
  const srv = http.createServer(handler);
@@ -40888,13 +41384,18 @@ function runAsyncBooleanHandler(handler, res, fallback) {
40888
41384
  writeJson(res, 500, { error: "Internal server error" });
40889
41385
  });
40890
41386
  }
40891
- function validateHost(req, expectedHostPort) {
41387
+ function validateHost(req, expectedHostPort, secure) {
40892
41388
  if (req.url?.startsWith("http://") || req.url?.startsWith("https://")) return false;
40893
41389
  const hostHeaderCount = req.rawHeaders.filter((header, index) => index % 2 === 0 && header.toLowerCase() === "host").length;
40894
41390
  if (hostHeaderCount !== 1) return false;
40895
41391
  const hostHeader = req.headers.host;
40896
41392
  if (!hostHeader) return false;
40897
- 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;
40898
41399
  }
40899
41400
  function isAllowedTerminalOrigin(req, expectedOrigin) {
40900
41401
  const origin = req.headers.origin;