@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.
- package/AGENTS.md +1 -1
- package/dist/access-protocol.d.ts +58 -0
- package/dist/access-protocol.mjs +165 -0
- package/dist/cli.mjs +1205 -704
- package/dist/client/assets/{_baseUniq-B2XggV1b.js → _baseUniq-Cp0TuK00.js} +1 -1
- package/dist/client/assets/{arc-CPqvR_Wl.js → arc-Cwtz1wxV.js} +1 -1
- package/dist/client/assets/{architectureDiagram-Q4EWVU46-BczXSpod.js → architectureDiagram-Q4EWVU46-f2trvyEV.js} +1 -1
- package/dist/client/assets/{blockDiagram-DXYQGD6D-BPtW_zZR.js → blockDiagram-DXYQGD6D-BhEa9Mh6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AHTNJAMY-Cry2oyLL.js → c4Diagram-AHTNJAMY-CuqbJbZK.js} +1 -1
- package/dist/client/assets/channel-D9kPbZ6w.js +1 -0
- package/dist/client/assets/{chunk-4BX2VUAB-DKkKt4m0.js → chunk-4BX2VUAB-Vx63wo1i.js} +1 -1
- package/dist/client/assets/{chunk-4TB4RGXK-DKGkDbqm.js → chunk-4TB4RGXK-DHRtIlLJ.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-DawsVr8O.js → chunk-55IACEB6-ChTm2kCK.js} +1 -1
- package/dist/client/assets/{chunk-EDXVE4YY-tS8vRy63.js → chunk-EDXVE4YY-C_BQtaY5.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-B0jQQkjI.js → chunk-FMBD7UC4-BPV9Mw6e.js} +1 -1
- package/dist/client/assets/{chunk-OYMX7WX6-FZQSRkEo.js → chunk-OYMX7WX6-ya-VyPHj.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-Bq1WBmr8.js → chunk-QZHKN3VN-CLiSGdJ6.js} +1 -1
- package/dist/client/assets/{chunk-YZCP3GAM-CNWIYKHc.js → chunk-YZCP3GAM-DasGOM9R.js} +1 -1
- package/dist/client/assets/classDiagram-6PBFFD2Q-B1zyip5T.js +1 -0
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-B1zyip5T.js +1 -0
- package/dist/client/assets/clone-x5mGq45q.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-CmOaWS4P.js → cose-bilkent-S5V4N54A-C11PcGWz.js} +1 -1
- package/dist/client/assets/{dagre-KV5264BT-D2gJaf1o.js → dagre-KV5264BT-BGiNFCUB.js} +1 -1
- package/dist/client/assets/{diagram-5BDNPKRD-DH-M3VUk.js → diagram-5BDNPKRD-CSxK9ZtI.js} +1 -1
- package/dist/client/assets/{diagram-G4DWMVQ6-169IB8uN.js → diagram-G4DWMVQ6-COZtgx_6.js} +1 -1
- package/dist/client/assets/{diagram-MMDJMWI5-D1OVxCyQ.js → diagram-MMDJMWI5-4-mD0i99.js} +1 -1
- package/dist/client/assets/{diagram-TYMM5635-CkiS1glz.js → diagram-TYMM5635-BNX8i0L7.js} +1 -1
- package/dist/client/assets/{erDiagram-SMLLAGMA-CZUp7ncJ.js → erDiagram-SMLLAGMA-1a9K7CGs.js} +1 -1
- package/dist/client/assets/{flowDiagram-DWJPFMVM-9SIhR3aC.js → flowDiagram-DWJPFMVM-B7BZTT0P.js} +1 -1
- package/dist/client/assets/{ganttDiagram-T4ZO3ILL-CA4smcv3.js → ganttDiagram-T4ZO3ILL-FeCgSTyp.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-DF8CSmoM.js → gitGraphDiagram-UUTBAWPF-ItC4KjyQ.js} +1 -1
- package/dist/client/assets/{graph-Bc_ceTI8.js → graph-C0wWu6sL.js} +1 -1
- package/dist/client/assets/{index-NNL49IzO.css → index-B0961B8M.css} +1 -1
- package/dist/client/assets/index-DEW90Ngb.js +479 -0
- package/dist/client/assets/{infoDiagram-42DDH7IO-BC5KSucM.js → infoDiagram-42DDH7IO-5ub69t3k.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-JJIJclQD.js → ishikawaDiagram-UXIWVN3A-BEvDRmdo.js} +1 -1
- package/dist/client/assets/{journeyDiagram-VCZTEJTY-ERzBaHVf.js → journeyDiagram-VCZTEJTY-DUxQ-ZUX.js} +1 -1
- package/dist/client/assets/{kanban-definition-6JOO6SKY-KvnzNd3g.js → kanban-definition-6JOO6SKY-BiPkJbpE.js} +1 -1
- package/dist/client/assets/{layout-DJbQbTix.js → layout-Bhtd-qjm.js} +1 -1
- package/dist/client/assets/{linear-BzBkH5Aq.js → linear-BT8b8xym.js} +1 -1
- package/dist/client/assets/{mermaid.core-CAqwSbV2.js → mermaid.core-BlTpil8e.js} +4 -4
- package/dist/client/assets/{min-giSfwdiX.js → min-DrnMPIwV.js} +1 -1
- package/dist/client/assets/{mindmap-definition-QFDTVHPH-BH0wnMRm.js → mindmap-definition-QFDTVHPH-uQH7Nv45.js} +1 -1
- package/dist/client/assets/{pieDiagram-DEJITSTG-WOHMlPDe.js → pieDiagram-DEJITSTG-MOetI488.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-34T5L4WZ-DcIpvIOj.js → quadrantDiagram-34T5L4WZ-C-jeoH4D.js} +1 -1
- package/dist/client/assets/{requirementDiagram-MS252O5E-C99uWiwG.js → requirementDiagram-MS252O5E-C7cKIVp5.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-XADWPNL6-czWBcwBu.js → sankeyDiagram-XADWPNL6-Dcv2A0AY.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-FGHM5R23-CjtCPppG.js → sequenceDiagram-FGHM5R23-CXO-xPvA.js} +1 -1
- package/dist/client/assets/{stateDiagram-FHFEXIEX-D0UKF8ze.js → stateDiagram-FHFEXIEX-GKivP2tt.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-BfnEUJRo.js +1 -0
- package/dist/client/assets/{timeline-definition-GMOUNBTQ-3isWQZNU.js → timeline-definition-GMOUNBTQ-DLJSUfwG.js} +1 -1
- package/dist/client/assets/{vennDiagram-DHZGUBPP-C_LF2g8l.js → vennDiagram-DHZGUBPP-DFihaprP.js} +1 -1
- package/dist/client/assets/{wardley-RL74JXVD-Bw0Y4M6p.js → wardley-RL74JXVD-BBV41M5Z.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-NUSXRM2D-B6e3mxQI.js → wardleyDiagram-NUSXRM2D-B5J5AiY-.js} +1 -1
- package/dist/client/assets/{xychartDiagram-5P7HB3ND-DjlZXnLg.js → xychartDiagram-5P7HB3ND-D1NFEt18.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/fleet-plugins/file-explorer/routes.mjs +13 -9
- package/dist/fleet-plugins/ledger/routes.mjs +6 -2
- package/dist/fleet-plugins/quota/routes.mjs +22 -18
- package/dist/fleet-plugins/repository/routes.mjs +17 -13
- package/dist/fleet-plugins/scuttlebutt/routes.mjs +11 -2
- package/dist/fleet-plugins/skills/routes.mjs +14 -10
- package/dist/fleet-plugins/terminal/routes.mjs +317 -283
- package/dist/fleet.mjs +3914 -3456
- package/package.json +5 -1
- package/dist/client/assets/channel-D5C5uwWd.js +0 -1
- package/dist/client/assets/classDiagram-6PBFFD2Q-Cv4Bxw_r.js +0 -1
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-Cv4Bxw_r.js +0 -1
- package/dist/client/assets/clone-C7dthYDe.js +0 -1
- package/dist/client/assets/index-kTAFpDLu.js +0 -479
- 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
|
|
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 ?? (() =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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()}-${
|
|
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
|
|
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
|
-
|
|
12185
|
-
const
|
|
12186
|
-
|
|
12187
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
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 =
|
|
12415
|
-
const
|
|
12416
|
-
return {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? (() =>
|
|
12677
|
-
const randomHandle = deps.randomHandle ?? (() =>
|
|
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:
|
|
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
|
|
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 ?? (() =>
|
|
12880
|
-
const randomSecret = deps.randomSecret ?? (() =>
|
|
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" ?
|
|
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
|
|
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 =
|
|
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 = `/${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 ?? (() =>
|
|
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 ??
|
|
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 =
|
|
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
|
|
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
|
|
36873
|
-
|
|
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
|
|
36880
|
-
const body2 = {
|
|
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
|
-
|
|
37052
|
+
endpoint = next;
|
|
36884
37053
|
}
|
|
36885
37054
|
return {
|
|
36886
|
-
read: () =>
|
|
37055
|
+
read: () => endpoint,
|
|
36887
37056
|
remember: (next) => {
|
|
36888
|
-
|
|
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
|
|
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
|
-
//
|
|
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 (
|
|
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 =
|
|
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.
|
|
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
|
|
36998
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
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 ?? (() =>
|
|
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
|
-
|
|
37642
|
-
|
|
37643
|
-
|
|
37644
|
-
|
|
37645
|
-
|
|
37646
|
-
|
|
37647
|
-
|
|
37648
|
-
|
|
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
|
-
|
|
38182
|
+
function createScopedRouteRegistrar(routes, apiCatalog, pendingRoutes, pendingCatalog, basePath, apiBasePath) {
|
|
38183
|
+
function registerRouter(requestedPath, handler, catalog) {
|
|
37795
38184
|
const prefix = resolveScopedPrefix(basePath, requestedPath, apiBasePath);
|
|
37796
|
-
|
|
37797
|
-
|
|
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
|
-
|
|
38196
|
+
function createScopedUpgradeRegistrar(upgrades, apiCatalog, pendingUpgrades, pendingCatalog, basePath) {
|
|
38197
|
+
function registerWsHandler(requestedPath, handler, catalog) {
|
|
37802
38198
|
const prefix = resolveScopedPrefix(basePath, requestedPath);
|
|
37803
|
-
|
|
37804
|
-
|
|
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,
|
|
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
|
-
|
|
39590
|
-
|
|
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
|
|
39685
|
-
if (!
|
|
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(
|
|
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
|
|
40212
|
+
return false;
|
|
39761
40213
|
}
|
|
39762
40214
|
writeJson(res, joined.status, { error: joined.error });
|
|
39763
|
-
return
|
|
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
|
|
40524
|
-
const
|
|
40525
|
-
const
|
|
40526
|
-
|
|
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(
|
|
41056
|
+
async function startRemoteAccessIfEnabled(_actualPort) {
|
|
40580
41057
|
const configured = consoleSettingsStore.load().general?.remoteAccess;
|
|
40581
|
-
|
|
40582
|
-
if (!
|
|
40583
|
-
const
|
|
40584
|
-
const
|
|
40585
|
-
|
|
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
|
|
40590
|
-
const
|
|
40591
|
-
const
|
|
40592
|
-
|
|
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:
|
|
40609
|
-
port:
|
|
40610
|
-
origin:
|
|
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
|
-
|
|
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'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=…</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 → Remote access → 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 "
|
|
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;
|