machine-bridge-mcp 1.2.10 → 2.0.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/CHANGELOG.md +23 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +11 -6
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +23 -11
- package/docs/AUDIT.md +37 -3
- package/docs/CLIENTS.md +2 -2
- package/docs/ENGINEERING.md +2 -2
- package/docs/GETTING_STARTED.md +1 -1
- package/docs/LOCAL_AUTHORIZATION.md +111 -0
- package/docs/LOGGING.md +7 -5
- package/docs/MULTI_ACCOUNT.md +5 -5
- package/docs/OPERATIONS.md +33 -7
- package/docs/OVERVIEW.md +12 -8
- package/docs/PROJECT_STANDARDS.md +1 -1
- package/docs/RELEASING.md +4 -4
- package/docs/TESTING.md +9 -2
- package/docs/THREAT_MODEL.md +7 -6
- package/docs/TOOL_REFERENCE.md +4 -4
- package/docs/UPGRADING.md +10 -6
- package/package.json +8 -2
- package/scripts/check-plan.mjs +5 -0
- package/scripts/check-runner.mjs +75 -0
- package/scripts/coverage-check.mjs +1 -1
- package/scripts/github-backlog.mjs +116 -0
- package/scripts/github-push.mjs +4 -0
- package/scripts/local-release-acceptance.mjs +2 -2
- package/scripts/release-acceptance.mjs +36 -17
- package/scripts/run-checks.mjs +11 -21
- package/src/local/account-admin.mjs +28 -3
- package/src/local/bounded-output.mjs +20 -3
- package/src/local/cli-approval.mjs +117 -0
- package/src/local/cli-options.mjs +8 -2
- package/src/local/cli.mjs +13 -2
- package/src/local/device-identity.mjs +108 -0
- package/src/local/errors.mjs +5 -1
- package/src/local/execution-limits.mjs +6 -1
- package/src/local/operation-authorization.mjs +366 -0
- package/src/local/operation-risk.mjs +221 -0
- package/src/local/operation-state-lock.mjs +92 -0
- package/src/local/process-execution.mjs +94 -14
- package/src/local/process-output-stream.mjs +82 -0
- package/src/local/process-result-projection.mjs +25 -0
- package/src/local/process-sessions.mjs +37 -51
- package/src/local/relay-connection.mjs +12 -5
- package/src/local/runtime-relay.mjs +13 -5
- package/src/local/runtime.mjs +14 -7
- package/src/local/secure-file.mjs +24 -4
- package/src/local/state.mjs +9 -3
- package/src/local/tool-executor.mjs +4 -2
- package/src/local/tools.mjs +4 -9
- package/src/local/worker-deployment.mjs +4 -3
- package/src/local/worker-secret-file.mjs +2 -1
- package/src/shared/admin-auth.d.mts +10 -0
- package/src/shared/admin-auth.mjs +36 -0
- package/src/shared/daemon-auth.d.mts +20 -0
- package/src/shared/daemon-auth.mjs +55 -0
- package/src/shared/result-projection.d.mts +6 -0
- package/src/shared/result-projection.json +4 -0
- package/src/shared/result-projection.mjs +50 -0
- package/src/shared/server-metadata.json +1 -0
- package/src/shared/tool-catalog.json +4 -4
- package/src/worker/account-admin.ts +73 -4
- package/src/worker/daemon-auth.ts +205 -0
- package/src/worker/daemon-sockets.ts +15 -2
- package/src/worker/errors.ts +18 -4
- package/src/worker/index.ts +59 -10
- package/src/worker/mcp-jsonrpc.ts +4 -2
- package/src/worker/nonce-store.ts +79 -0
- package/src/worker/oauth-controller.ts +11 -6
- package/src/worker/oauth-refresh-families.ts +172 -0
- package/src/worker/oauth-state.ts +48 -3
- package/src/worker/oauth-tokens.ts +49 -40
|
@@ -7,6 +7,7 @@ import { terminateProcessTree, terminateProcessTreeWithEscalation } from "./proc
|
|
|
7
7
|
import { createToolAuthorizer } from "./policy.mjs";
|
|
8
8
|
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
9
9
|
import { clampInteger } from "./numbers.mjs";
|
|
10
|
+
import { ProcessOutputStream } from "./process-output-stream.mjs";
|
|
10
11
|
import {
|
|
11
12
|
MAX_PROCESS_SESSIONS, MAX_PROCESS_SESSION_OUTPUT_BYTES, MAX_PROCESS_SESSION_STDIN_BYTES, PROCESS_SESSION_RETENTION_MS,
|
|
12
13
|
} from "./execution-limits.mjs";
|
|
@@ -40,11 +41,37 @@ export class ProcessSessionManager {
|
|
|
40
41
|
for (const session of this.sessions.values()) notifySessionWaiters(session);
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
retainCompletedOutput({ command, cwd, stdout, stderr, exitCode, startedAt, closedAt = Date.now() }) {
|
|
45
|
+
this.prune();
|
|
46
|
+
this.evictExitedForCapacity();
|
|
47
|
+
if (this.sessions.size >= MAX_PROCESS_SESSIONS) return null;
|
|
48
|
+
const completedAt = Number.isFinite(Number(closedAt)) ? Number(closedAt) : Date.now();
|
|
49
|
+
const session = {
|
|
50
|
+
id: `proc_${randomBytes(24).toString("base64url")}`,
|
|
51
|
+
child: null,
|
|
52
|
+
argv0: basename(String(command || "process")),
|
|
53
|
+
cwd,
|
|
54
|
+
stdout,
|
|
55
|
+
stderr,
|
|
56
|
+
startedAt: Number.isFinite(Number(startedAt)) ? Number(startedAt) : completedAt,
|
|
57
|
+
lastActivity: completedAt,
|
|
58
|
+
closedAt: completedAt,
|
|
59
|
+
exitCode: Number.isInteger(exitCode) ? exitCode : null,
|
|
60
|
+
signal: null,
|
|
61
|
+
stdinClosed: true,
|
|
62
|
+
waiters: new Set(),
|
|
63
|
+
terminationTimer: null,
|
|
64
|
+
};
|
|
65
|
+
this.sessions.set(session.id, session);
|
|
66
|
+
return this.summary(session);
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
async start(args, context = {}) {
|
|
44
70
|
this.authorizeTool("start_process");
|
|
45
71
|
const argv = validateArgv(args.argv);
|
|
46
72
|
const cwd = await this.resolveCwd(args.cwd || ".");
|
|
47
73
|
this.prune();
|
|
74
|
+
this.evictExitedForCapacity();
|
|
48
75
|
if (this.sessions.size >= MAX_PROCESS_SESSIONS) throw new Error(`process session limit reached (${MAX_PROCESS_SESSIONS})`);
|
|
49
76
|
this.throwIfCancelled(context);
|
|
50
77
|
|
|
@@ -59,8 +86,8 @@ export class ProcessSessionManager {
|
|
|
59
86
|
child,
|
|
60
87
|
argv0: basename(argv[0]),
|
|
61
88
|
cwd,
|
|
62
|
-
stdout:
|
|
63
|
-
stderr:
|
|
89
|
+
stdout: new ProcessOutputStream(MAX_PROCESS_SESSION_OUTPUT_BYTES),
|
|
90
|
+
stderr: new ProcessOutputStream(MAX_PROCESS_SESSION_OUTPUT_BYTES),
|
|
64
91
|
startedAt: Date.now(),
|
|
65
92
|
lastActivity: Date.now(),
|
|
66
93
|
closedAt: null,
|
|
@@ -74,12 +101,12 @@ export class ProcessSessionManager {
|
|
|
74
101
|
this.trackChild(child, context.callId);
|
|
75
102
|
|
|
76
103
|
child.stdout.on("data", (chunk) => {
|
|
77
|
-
|
|
104
|
+
session.stdout.append(chunk);
|
|
78
105
|
session.lastActivity = Date.now();
|
|
79
106
|
notifySessionWaiters(session);
|
|
80
107
|
});
|
|
81
108
|
child.stderr.on("data", (chunk) => {
|
|
82
|
-
|
|
109
|
+
session.stderr.append(chunk);
|
|
83
110
|
session.lastActivity = Date.now();
|
|
84
111
|
notifySessionWaiters(session);
|
|
85
112
|
});
|
|
@@ -94,7 +121,7 @@ export class ProcessSessionManager {
|
|
|
94
121
|
});
|
|
95
122
|
|
|
96
123
|
child.on("error", (error) => {
|
|
97
|
-
|
|
124
|
+
session.stderr.append(Buffer.from(`${boundedErrorMessage(error)}\n`));
|
|
98
125
|
session.lastActivity = Date.now();
|
|
99
126
|
notifySessionWaiters(session);
|
|
100
127
|
});
|
|
@@ -132,8 +159,8 @@ export class ProcessSessionManager {
|
|
|
132
159
|
session.lastActivity = Date.now();
|
|
133
160
|
return {
|
|
134
161
|
...this.summary(session),
|
|
135
|
-
stdout:
|
|
136
|
-
stderr:
|
|
162
|
+
stdout: session.stdout.read(stdoutOffset, maxBytes),
|
|
163
|
+
stderr: session.stderr.read(stderrOffset, maxBytes),
|
|
137
164
|
};
|
|
138
165
|
}
|
|
139
166
|
|
|
@@ -207,6 +234,9 @@ export class ProcessSessionManager {
|
|
|
207
234
|
for (const [id, session] of this.sessions) {
|
|
208
235
|
if (session.closedAt !== null && session.lastActivity < cutoff) this.sessions.delete(id);
|
|
209
236
|
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
evictExitedForCapacity() {
|
|
210
240
|
if (this.sessions.size < MAX_PROCESS_SESSIONS) return;
|
|
211
241
|
const exited = [...this.sessions.values()]
|
|
212
242
|
.filter((session) => session.closedAt !== null)
|
|
@@ -240,50 +270,6 @@ function waitForSpawn(child) {
|
|
|
240
270
|
});
|
|
241
271
|
}
|
|
242
272
|
|
|
243
|
-
function createSessionStream() {
|
|
244
|
-
return { buffer: Buffer.alloc(0), baseOffset: 0, totalBytes: 0 };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function appendSessionStream(stream, chunk) {
|
|
248
|
-
const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? ""));
|
|
249
|
-
stream.totalBytes += input.length;
|
|
250
|
-
let combined = stream.buffer.length ? Buffer.concat([stream.buffer, input]) : Buffer.from(input);
|
|
251
|
-
if (combined.length > MAX_PROCESS_SESSION_OUTPUT_BYTES) {
|
|
252
|
-
const dropped = combined.length - MAX_PROCESS_SESSION_OUTPUT_BYTES;
|
|
253
|
-
combined = combined.subarray(dropped);
|
|
254
|
-
stream.baseOffset += dropped;
|
|
255
|
-
}
|
|
256
|
-
stream.buffer = combined;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function readSessionStream(stream, requestedOffset, maxBytes) {
|
|
260
|
-
const clampedOffset = Math.min(requestedOffset, stream.totalBytes);
|
|
261
|
-
const effectiveOffset = Math.max(clampedOffset, stream.baseOffset);
|
|
262
|
-
const start = effectiveOffset - stream.baseOffset;
|
|
263
|
-
const slice = stream.buffer.subarray(start, Math.min(stream.buffer.length, start + maxBytes));
|
|
264
|
-
let data;
|
|
265
|
-
let dataBase64;
|
|
266
|
-
let encoding = "utf8";
|
|
267
|
-
try {
|
|
268
|
-
data = new TextDecoder("utf-8", { fatal: true }).decode(slice);
|
|
269
|
-
} catch {
|
|
270
|
-
data = new TextDecoder("utf-8").decode(slice);
|
|
271
|
-
dataBase64 = slice.toString("base64");
|
|
272
|
-
encoding = "base64";
|
|
273
|
-
}
|
|
274
|
-
return {
|
|
275
|
-
data,
|
|
276
|
-
...(dataBase64 ? { data_base64: dataBase64 } : {}),
|
|
277
|
-
encoding,
|
|
278
|
-
requested_offset: requestedOffset,
|
|
279
|
-
start_offset: effectiveOffset,
|
|
280
|
-
next_offset: effectiveOffset + slice.length,
|
|
281
|
-
total_offset: stream.totalBytes,
|
|
282
|
-
truncated_before: requestedOffset < stream.baseOffset,
|
|
283
|
-
truncated_after: effectiveOffset + slice.length < stream.totalBytes,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
273
|
function sessionHasOutputAfter(session, stdoutOffset, stderrOffset) {
|
|
288
274
|
return session.stdout.totalBytes > stdoutOffset || session.stderr.totalBytes > stderrOffset;
|
|
289
275
|
}
|
|
@@ -23,10 +23,9 @@ const DEFAULT_SCHEDULER = Object.freeze({
|
|
|
23
23
|
export class RelayConnection {
|
|
24
24
|
constructor(options = {}) {
|
|
25
25
|
this.workerUrl = normalizeWorkerUrl(options.workerUrl);
|
|
26
|
-
if (typeof options.secret !== "string" || options.secret.length < 16) throw new Error("daemon secret is missing or too short");
|
|
27
|
-
this.secret = options.secret;
|
|
28
26
|
this.logger = options.logger || console;
|
|
29
27
|
this.helloMessage = typeof options.helloMessage === "function" ? options.helloMessage : () => ({ type: "hello" });
|
|
28
|
+
this.connectionHeaders = typeof options.connectionHeaders === "function" ? options.connectionHeaders : () => ({});
|
|
30
29
|
this.expectedServer = String(options.expectedServer || "");
|
|
31
30
|
this.expectedVersion = String(options.expectedVersion || "");
|
|
32
31
|
this.onMessage = typeof options.onMessage === "function" ? options.onMessage : () => {};
|
|
@@ -164,6 +163,13 @@ export class RelayConnection {
|
|
|
164
163
|
return false;
|
|
165
164
|
}
|
|
166
165
|
this.logger.debug?.("remote relay welcome received");
|
|
166
|
+
Promise.resolve(this.helloMessage(message)).then((hello) => {
|
|
167
|
+
if (this.socket !== socket || this.closed || this.authenticated) return;
|
|
168
|
+
if (!this.sendOnSocket(socket, hello)) this.failPermanently("relay_authentication_failed");
|
|
169
|
+
}).catch((error) => {
|
|
170
|
+
this.logger.debug?.("could not create daemon authentication proof", { error_class: classifyOperationalError(error) });
|
|
171
|
+
this.failPermanently("relay_authentication_failed");
|
|
172
|
+
});
|
|
167
173
|
return true;
|
|
168
174
|
}
|
|
169
175
|
|
|
@@ -273,9 +279,11 @@ export class RelayConnection {
|
|
|
273
279
|
let socket;
|
|
274
280
|
try {
|
|
275
281
|
const proxy = this.proxyAgentForUrl(wsUrl);
|
|
282
|
+
const headers = this.connectionHeaders();
|
|
283
|
+
if (!headers || typeof headers !== "object" || Array.isArray(headers)) throw new Error("relay connection headers are invalid");
|
|
276
284
|
this.networkRoute = proxy?.agent ? "proxy" : "direct";
|
|
277
285
|
socket = new this.WebSocketClass(wsUrl, {
|
|
278
|
-
headers
|
|
286
|
+
headers,
|
|
279
287
|
maxPayload: this.maxPayload,
|
|
280
288
|
...(proxy?.agent ? { agent: proxy.agent } : {}),
|
|
281
289
|
});
|
|
@@ -308,8 +316,7 @@ export class RelayConnection {
|
|
|
308
316
|
return;
|
|
309
317
|
}
|
|
310
318
|
this.lastInboundAt = this.now();
|
|
311
|
-
this.logger.debug?.("remote relay transport opened; awaiting
|
|
312
|
-
if (!this.sendOnSocket(socket, this.helloMessage())) return;
|
|
319
|
+
this.logger.debug?.("remote relay transport opened; awaiting device challenge");
|
|
313
320
|
this.clearTimer("handshakeTimer", "clearTimeout");
|
|
314
321
|
this.handshakeTimer = this.scheduler.setTimeout(() => {
|
|
315
322
|
if (this.socket !== socket || this.closed || this.ready) return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { RelayConnection } from "./relay-connection.mjs";
|
|
3
|
+
import { createDaemonAuthentication, createDaemonPreflightHeaders } from "./device-identity.mjs";
|
|
3
4
|
import { MCP_SUPPORTED_PROTOCOL_VERSIONS, SERVER_NAME } from "./tools.mjs";
|
|
4
5
|
import { normalizeAccountRole } from "./account-access.mjs";
|
|
5
6
|
import { clampInteger } from "./numbers.mjs";
|
|
@@ -7,21 +8,27 @@ import { isPlainRecord } from "./records.mjs";
|
|
|
7
8
|
|
|
8
9
|
export const MAX_RELAY_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
9
10
|
|
|
10
|
-
export function createRuntimeRelayConnection(runtime, { workerUrl,
|
|
11
|
+
export function createRuntimeRelayConnection(runtime, { workerUrl, deviceIdentity, expectedVersion, onFatal }) {
|
|
11
12
|
if (!workerUrl) return null;
|
|
12
13
|
return new RelayConnection({
|
|
13
14
|
workerUrl,
|
|
14
|
-
secret,
|
|
15
15
|
logger: runtime.logger,
|
|
16
16
|
maxPayload: MAX_RELAY_MESSAGE_BYTES,
|
|
17
17
|
expectedServer: SERVER_NAME,
|
|
18
18
|
expectedVersion: String(expectedVersion || ""),
|
|
19
|
-
|
|
19
|
+
connectionHeaders: () => createDaemonPreflightHeaders(
|
|
20
|
+
deviceIdentity,
|
|
21
|
+
workerUrl,
|
|
22
|
+
SERVER_NAME,
|
|
23
|
+
String(expectedVersion || ""),
|
|
24
|
+
),
|
|
25
|
+
helloMessage: async (welcome) => ({
|
|
20
26
|
type: "hello",
|
|
21
27
|
instance_id: runtime.relayInstanceId,
|
|
22
28
|
tools: runtime.tools(),
|
|
23
29
|
policy: runtime.policy,
|
|
24
30
|
protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
31
|
+
authentication: await createDaemonAuthentication(deviceIdentity, welcome, runtime.relayInstanceId),
|
|
25
32
|
}),
|
|
26
33
|
onMessage: (data, relayContext) => handleRelayData(runtime, data, relayContext),
|
|
27
34
|
onDisconnect: () => runtime.handleRelayDisconnect(),
|
|
@@ -82,8 +89,9 @@ function normalizeRelayAuthorization(value) {
|
|
|
82
89
|
if (!isPlainRecord(value)) return null;
|
|
83
90
|
const accountId = typeof value.account_id === "string" && /^acct_[A-Za-z0-9_-]{20,96}$/.test(value.account_id) ? value.account_id : "";
|
|
84
91
|
const accountVersion = Number(value.account_version);
|
|
92
|
+
const clientId = typeof value.client_id === "string" && /^mcp_client_[A-Za-z0-9_-]{43}$/.test(value.client_id) ? value.client_id : "";
|
|
85
93
|
let role;
|
|
86
94
|
try { role = normalizeAccountRole(value.role); } catch { return null; }
|
|
87
|
-
if (!accountId || !Number.isInteger(accountVersion) || accountVersion < 1) return null;
|
|
88
|
-
return Object.freeze({ account_id: accountId, account_version: accountVersion, role });
|
|
95
|
+
if (!accountId || !clientId || !Number.isInteger(accountVersion) || accountVersion < 1) return null;
|
|
96
|
+
return Object.freeze({ account_id: accountId, account_version: accountVersion, client_id: clientId, role });
|
|
89
97
|
}
|
package/src/local/runtime.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { AccountAccessGate } from "./account-access.mjs";
|
|
|
31
31
|
import { buildProjectOverview, buildRuntimeInfo } from "./runtime-reporting.mjs";
|
|
32
32
|
import { diagnoseRuntime as runRuntimeDiagnostics } from "./runtime-diagnostics.mjs";
|
|
33
33
|
import { bindRuntimeToolHandlers, runtimeToolHandlerNames as registeredRuntimeToolHandlerNames } from "./runtime-tool-handlers.mjs";
|
|
34
|
+
import { OperationAuthorizer } from "./operation-authorization.mjs";
|
|
34
35
|
import { createRuntimeRelayConnection, normalizeRelayResumeCalls, normalizeRelayToolCall } from "./runtime-relay.mjs";
|
|
35
36
|
import { RelayCallRecovery } from "./relay-call-recovery.mjs";
|
|
36
37
|
import { assertContainedPath, createRuntimeDir, redactRuntimeErrorMessage, stateRootFromProfileStatePath } from "./runtime-paths.mjs";
|
|
@@ -46,9 +47,8 @@ export function runtimeToolHandlerNames() {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export class LocalRuntime {
|
|
49
|
-
constructor({ workerUrl = "",
|
|
50
|
+
constructor({ workerUrl = "", deviceIdentity = null, expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", approvalRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "", agentHome = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "", recoverJobs = true, applicationAutomation = {} }) {
|
|
50
51
|
const remoteWorkerUrl = workerUrl ? String(workerUrl) : "";
|
|
51
|
-
const remoteSecret = secret || "";
|
|
52
52
|
this.workspaceInput = resolve(workspace || process.cwd());
|
|
53
53
|
this.workspace = realpathSync.native ? realpathSync.native(this.workspaceInput) : realpathSync(this.workspaceInput);
|
|
54
54
|
this.workspaceCanonicalPromise = null;
|
|
@@ -135,6 +135,7 @@ export class LocalRuntime {
|
|
|
135
135
|
resolveLocalCommand: (args, context) => this.agentContextManager.resolveLocalCommand(args, context),
|
|
136
136
|
displayPath: (value) => this.displayPath(value),
|
|
137
137
|
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
138
|
+
retainCompletedOutput: (value) => this.processSessionManager.retainCompletedOutput(value),
|
|
138
139
|
});
|
|
139
140
|
this.gitService = new GitService({
|
|
140
141
|
resolveExistingPath: (value) => this.resolveExistingPath(value),
|
|
@@ -154,6 +155,12 @@ export class LocalRuntime {
|
|
|
154
155
|
readResourceText,
|
|
155
156
|
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
156
157
|
});
|
|
158
|
+
this.operationAuthorizer = new OperationAuthorizer({
|
|
159
|
+
workspace: this.workspace,
|
|
160
|
+
root: approvalRoot,
|
|
161
|
+
resolveExistingPath: (value) => this.resolveExistingPath(value),
|
|
162
|
+
resolveWritePath: (value) => this.resolveWritePath(value),
|
|
163
|
+
});
|
|
157
164
|
this.browserBridgeManager = new BrowserBridgeManager({
|
|
158
165
|
policy: this.policy,
|
|
159
166
|
authorizeTool: (tool) => this.policyGate.assert(tool),
|
|
@@ -168,6 +175,7 @@ export class LocalRuntime {
|
|
|
168
175
|
handlers: bindRuntimeToolHandlers(this),
|
|
169
176
|
policyGate: this.policyGate,
|
|
170
177
|
accountAccessGate: this.accountAccessGate,
|
|
178
|
+
operationAuthorizer: this.operationAuthorizer,
|
|
171
179
|
callRegistry: this.callRegistry,
|
|
172
180
|
observability: this.observability,
|
|
173
181
|
logger: this.logger,
|
|
@@ -175,7 +183,7 @@ export class LocalRuntime {
|
|
|
175
183
|
slowMs: SLOW_TOOL_CALL_MS,
|
|
176
184
|
});
|
|
177
185
|
this.relay = createRuntimeRelayConnection(this, {
|
|
178
|
-
workerUrl: remoteWorkerUrl,
|
|
186
|
+
workerUrl: remoteWorkerUrl, deviceIdentity, expectedVersion: expectedRelayVersion, onFatal,
|
|
179
187
|
});
|
|
180
188
|
this.relayCallRecovery = new RelayCallRecovery({
|
|
181
189
|
logger: this.logger,
|
|
@@ -209,7 +217,7 @@ export class LocalRuntime {
|
|
|
209
217
|
}
|
|
210
218
|
|
|
211
219
|
async start() {
|
|
212
|
-
if (!this.relay) throw new Error("remote daemon start requires a Worker URL and
|
|
220
|
+
if (!this.relay) throw new Error("remote daemon start requires a Worker URL and device identity");
|
|
213
221
|
if (!this.lifecycle.beginStart()) return;
|
|
214
222
|
if (this.policy.profile === "full") {
|
|
215
223
|
void this.browserBridgeManager.ensureStarted().catch((error) => {
|
|
@@ -600,8 +608,8 @@ export class LocalRuntime {
|
|
|
600
608
|
|
|
601
609
|
async resolveWritePath(inputPath = ".") {
|
|
602
610
|
const candidate = this.resolvePath(inputPath);
|
|
603
|
-
if (this.policy.unrestrictedPaths) return candidate;
|
|
604
611
|
const candidateInfo = await lstat(candidate).catch(() => null);
|
|
612
|
+
if (candidateInfo?.isSymbolicLink()) throw new Error("refusing to overwrite a symbolic link");
|
|
605
613
|
let ancestor = candidate;
|
|
606
614
|
while (!(await lstat(ancestor).catch(() => null))) {
|
|
607
615
|
const parent = dirname(ancestor);
|
|
@@ -609,8 +617,7 @@ export class LocalRuntime {
|
|
|
609
617
|
ancestor = parent;
|
|
610
618
|
}
|
|
611
619
|
const [workspace, canonicalAncestor] = await Promise.all([this.canonicalWorkspace(), realpath(ancestor)]);
|
|
612
|
-
assertContainedPath(workspace, canonicalAncestor);
|
|
613
|
-
if (candidateInfo?.isSymbolicLink()) throw new Error("refusing to overwrite a symbolic link");
|
|
620
|
+
if (!this.policy.unrestrictedPaths) assertContainedPath(workspace, canonicalAncestor);
|
|
614
621
|
const suffix = relative(ancestor, candidate);
|
|
615
622
|
return suffix ? resolve(canonicalAncestor, suffix) : canonicalAncestor;
|
|
616
623
|
}
|
|
@@ -16,6 +16,11 @@ export function openRegularFileSync(file, flags, options = {}) {
|
|
|
16
16
|
try {
|
|
17
17
|
const info = fstatSync(fd);
|
|
18
18
|
if (!info.isFile()) throw new Error(`${label} is not a regular file`);
|
|
19
|
+
if (options.verifyPathIdentity === true) {
|
|
20
|
+
const pathInfo = lstatSync(file);
|
|
21
|
+
if (pathInfo.isSymbolicLink() || !pathInfo.isFile()) throw new Error(`${label} must be a regular file and not a symbolic link`);
|
|
22
|
+
if (!sameFileIdentity(info, pathInfo)) throw new Error(`${label} identity changed while opening`);
|
|
23
|
+
}
|
|
19
24
|
if (Number.isInteger(options.chmod)) setDescriptorMode(fd, options.chmod);
|
|
20
25
|
return { fd, info };
|
|
21
26
|
} catch (error) {
|
|
@@ -37,15 +42,19 @@ export function chmodRegularFileSync(file, mode, label = "path") {
|
|
|
37
42
|
return withRegularFileSync(file, fsConstants.O_RDONLY, { label, chmod: mode }, () => undefined);
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
export function readBoundedRegularFileSync(file, maxBytes, label = "path") {
|
|
41
|
-
return readBoundedRegularFileWithInfoSync(file, maxBytes, label).buffer;
|
|
45
|
+
export function readBoundedRegularFileSync(file, maxBytes, label = "path", options = {}) {
|
|
46
|
+
return readBoundedRegularFileWithInfoSync(file, maxBytes, label, options).buffer;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
export function readBoundedRegularFileWithInfoSync(file, maxBytes, label = "path") {
|
|
49
|
+
export function readBoundedRegularFileWithInfoSync(file, maxBytes, label = "path", options = {}) {
|
|
45
50
|
const limit = Number(maxBytes);
|
|
46
51
|
if (!Number.isSafeInteger(limit) || limit < 0) throw new Error("maximum file size must be a non-negative safe integer");
|
|
47
|
-
return withRegularFileSync(file, fsConstants.O_RDONLY, {
|
|
52
|
+
return withRegularFileSync(file, fsConstants.O_RDONLY, {
|
|
53
|
+
label,
|
|
54
|
+
verifyPathIdentity: options.verifyPathIdentity === true,
|
|
55
|
+
}, (fd, info) => {
|
|
48
56
|
if (info.size > limit) throw new Error(`file exceeds ${limit} bytes`);
|
|
57
|
+
options.afterOpen?.({ fd, info });
|
|
49
58
|
const buffer = Buffer.alloc(info.size);
|
|
50
59
|
let offset = 0;
|
|
51
60
|
while (offset < buffer.length) {
|
|
@@ -104,3 +113,14 @@ function setDescriptorMode(fd, mode) {
|
|
|
104
113
|
if (process.platform !== "win32") throw error;
|
|
105
114
|
}
|
|
106
115
|
}
|
|
116
|
+
|
|
117
|
+
function sameFileIdentity(left, right) {
|
|
118
|
+
const leftDevice = Number(left.dev);
|
|
119
|
+
const rightDevice = Number(right.dev);
|
|
120
|
+
const leftInode = Number(left.ino);
|
|
121
|
+
const rightInode = Number(right.ino);
|
|
122
|
+
if ([leftDevice, rightDevice, leftInode, rightInode].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
123
|
+
return leftDevice === rightDevice && leftInode === rightInode;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
package/src/local/state.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import serverMetadata from "../shared/server-metadata.json" with { type: "json"
|
|
|
7
7
|
import { replaceFileSync } from "./atomic-fs.mjs";
|
|
8
8
|
import { createExclusiveFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
9
9
|
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
10
|
+
import { createDeviceIdentity, validateDeviceIdentity } from "./device-identity.mjs";
|
|
10
11
|
import { currentProcessStartTimeMs, inspectProcessInstance } from "./process-identity.mjs";
|
|
11
12
|
import { chmodRegularFileSync, ensureOwnerOnlyDirectorySync, readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
12
13
|
|
|
@@ -591,9 +592,14 @@ function pruneBackups(filePath, keep) {
|
|
|
591
592
|
|
|
592
593
|
export function ensureWorkerSecrets(state, options = {}) {
|
|
593
594
|
state.worker ||= {};
|
|
595
|
+
const enrollingDeviceIdentity = !state.worker.deviceIdentity;
|
|
594
596
|
if (!state.worker.accountAdminSecret || options.rotateSecrets) state.worker.accountAdminSecret = randomToken("account_admin");
|
|
595
|
-
if (
|
|
596
|
-
|
|
597
|
+
if (enrollingDeviceIdentity || options.rotateSecrets) state.worker.deviceIdentity = createDeviceIdentity();
|
|
598
|
+
else validateDeviceIdentity(state.worker.deviceIdentity);
|
|
599
|
+
delete state.worker.daemonSecret;
|
|
600
|
+
if (!state.worker.oauthTokenVersion || enrollingDeviceIdentity || options.rotateSecrets) {
|
|
601
|
+
state.worker.oauthTokenVersion = randomToken("token_version");
|
|
602
|
+
}
|
|
597
603
|
|
|
598
604
|
const requestedName = options.workerName || "";
|
|
599
605
|
if (!state.worker.name) {
|
|
@@ -635,7 +641,7 @@ export function ownerOnlyFile(filePath) {
|
|
|
635
641
|
export function redactState(state) {
|
|
636
642
|
const clone = redactHomeInValue(JSON.parse(JSON.stringify(state)));
|
|
637
643
|
if (clone.worker?.accountAdminSecret) clone.worker.accountAdminSecret = "<redacted>";
|
|
638
|
-
if (clone.worker?.
|
|
644
|
+
if (clone.worker?.deviceIdentity?.privateJwk?.d) clone.worker.deviceIdentity.privateJwk.d = "<redacted>";
|
|
639
645
|
if (clone.worker?.oauthTokenVersion) clone.worker.oauthTokenVersion = "<redacted>";
|
|
640
646
|
if (clone.resources && typeof clone.resources === "object") {
|
|
641
647
|
for (const value of Object.values(clone.resources)) {
|
|
@@ -7,6 +7,7 @@ export class ToolExecutor {
|
|
|
7
7
|
this.policyGate = options.policyGate;
|
|
8
8
|
this.callRegistry = options.callRegistry;
|
|
9
9
|
this.accountAccessGate = options.accountAccessGate;
|
|
10
|
+
this.operationAuthorizer = options.operationAuthorizer;
|
|
10
11
|
this.observability = options.observability;
|
|
11
12
|
this.logger = options.logger || console;
|
|
12
13
|
this.safeMessage = typeof options.safeMessage === "function" ? options.safeMessage : (error) => String(error?.message || error || "operation failed");
|
|
@@ -14,7 +15,7 @@ export class ToolExecutor {
|
|
|
14
15
|
this.pipeline = composeMiddleware([
|
|
15
16
|
lifecycleMiddleware(this.callRegistry),
|
|
16
17
|
observabilityMiddleware(this.observability, this.logger, this.safeMessage, this.slowMs),
|
|
17
|
-
authorizeMiddleware(this.policyGate, this.accountAccessGate),
|
|
18
|
+
authorizeMiddleware(this.policyGate, this.accountAccessGate, this.operationAuthorizer),
|
|
18
19
|
], invokeHandler(this.handlers));
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -27,13 +28,14 @@ export function composeMiddleware(middleware, terminal) {
|
|
|
27
28
|
return middleware.reduceRight((next, current) => (operation) => current(operation, next), terminal);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function authorizeMiddleware(policyGate, accountAccessGate) {
|
|
31
|
+
function authorizeMiddleware(policyGate, accountAccessGate, operationAuthorizer) {
|
|
31
32
|
return async (operation, next) => {
|
|
32
33
|
policyGate.assert(operation.tool);
|
|
33
34
|
if (operation.context.origin === "relay") {
|
|
34
35
|
const role = operation.request.authorization?.role;
|
|
35
36
|
if (!role) throw new Error("relay tool call is missing an account role");
|
|
36
37
|
accountAccessGate.assert(role, operation.tool);
|
|
38
|
+
await operationAuthorizer?.authorize(operation);
|
|
37
39
|
}
|
|
38
40
|
return next(operation);
|
|
39
41
|
};
|
package/src/local/tools.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
|
|
2
|
+
import { projectMcpResult } from "../shared/result-projection.mjs";
|
|
2
3
|
export {
|
|
3
4
|
DEFAULT_POLICY_PROFILE,
|
|
4
5
|
DEFAULT_POLICY_REVISION,
|
|
@@ -28,10 +29,9 @@ export const MCP_INSTRUCTIONS = Object.freeze(serverMetadata.instructions.map((v
|
|
|
28
29
|
export function toolResult(value, isError = false) {
|
|
29
30
|
const special = specialMcpResult(value);
|
|
30
31
|
if (special) return { ...special, isError };
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
if (structuredContent) result.structuredContent = structuredContent;
|
|
32
|
+
const projection = projectMcpResult(value);
|
|
33
|
+
const result = { content: [{ type: "text", text: projection.text }], isError };
|
|
34
|
+
if (projection.structuredContent) result.structuredContent = projection.structuredContent;
|
|
35
35
|
return result;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -55,8 +55,3 @@ function specialMcpResult(value) {
|
|
|
55
55
|
}
|
|
56
56
|
return result;
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
function toStructuredContent(value) {
|
|
60
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
61
|
-
return value;
|
|
62
|
-
}
|
|
@@ -4,6 +4,7 @@ import path, { resolve } from "node:path";
|
|
|
4
4
|
import { runWrangler } from "./shell.mjs";
|
|
5
5
|
import { packageRoot, saveState } from "./state.mjs";
|
|
6
6
|
import { withWorkerSecretsFile } from "./worker-secret-file.mjs";
|
|
7
|
+
import { publicDeviceJwkJson } from "./device-identity.mjs";
|
|
7
8
|
import {
|
|
8
9
|
normalizeWorkerOrigin,
|
|
9
10
|
retryWorkerHealth,
|
|
@@ -81,11 +82,11 @@ export function workerDeploymentFingerprint(state, options = {}) {
|
|
|
81
82
|
const root = resolve(options.packageRoot || packageRoot);
|
|
82
83
|
const keyMaterial = [
|
|
83
84
|
String(state.worker.accountAdminSecret || ""),
|
|
84
|
-
|
|
85
|
+
publicDeviceJwkJson(state.worker.deviceIdentity),
|
|
85
86
|
String(state.worker.oauthTokenVersion || ""),
|
|
86
87
|
].join("\0");
|
|
87
88
|
const fingerprint = createHmac("sha256", keyMaterial);
|
|
88
|
-
fingerprint.update("mbm-worker-deploy-
|
|
89
|
+
fingerprint.update("mbm-worker-deploy-v4");
|
|
89
90
|
fingerprint.update(String(state.worker.name || ""));
|
|
90
91
|
for (const file of workerDeployHashFiles(root)) {
|
|
91
92
|
fingerprint.update(path.relative(root, file));
|
|
@@ -118,7 +119,7 @@ export function workerUrlMatchesName(workerUrl, workerName) {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
function hasCompleteWorkerState(worker = {}) {
|
|
121
|
-
return Boolean(worker.url && worker.mcpServerUrl && worker.accountAdminSecret && worker.
|
|
122
|
+
return Boolean(worker.url && worker.mcpServerUrl && worker.accountAdminSecret && worker.deviceIdentity && worker.oauthTokenVersion && worker.name);
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
function workerDeployHashFiles(root) {
|
|
@@ -3,6 +3,7 @@ import { lstatSync, readdirSync, unlinkSync } from "node:fs";
|
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { createExclusiveFileSync } from "./exclusive-file.mjs";
|
|
5
5
|
import { currentProcessStartTimeMs, inspectProcessInstance } from "./process-identity.mjs";
|
|
6
|
+
import { publicDeviceJwkJson } from "./device-identity.mjs";
|
|
6
7
|
import { chmodRegularFileSync, ensureOwnerOnlyDirectorySync } from "./secure-file.mjs";
|
|
7
8
|
|
|
8
9
|
const SECRET_FILE_PATTERN = /^worker-secrets-(\d+)-(\d+)(?:-p(\d+))?(?:-([a-f0-9]+))?\.json$/;
|
|
@@ -17,7 +18,7 @@ export async function withWorkerSecretsFile(state, callback, options = {}) {
|
|
|
17
18
|
const tempPath = resolve(dir, `worker-secrets-${process.pid}-${createdAt}-p${processStartedAt}-${random}.json`);
|
|
18
19
|
const payload = {
|
|
19
20
|
ACCOUNT_ADMIN_SECRET: state.worker.accountAdminSecret,
|
|
20
|
-
|
|
21
|
+
DAEMON_DEVICE_PUBLIC_KEY: publicDeviceJwkJson(state.worker.deviceIdentity),
|
|
21
22
|
OAUTH_TOKEN_VERSION: state.worker.oauthTokenVersion,
|
|
22
23
|
};
|
|
23
24
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const ADMIN_AUTH_SCHEME: "hmac-sha256-v1";
|
|
2
|
+
export const ADMIN_AUTH_TTL_SECONDS: number;
|
|
3
|
+
export function adminAuthTranscript(input: {
|
|
4
|
+
origin: unknown;
|
|
5
|
+
method: unknown;
|
|
6
|
+
pathname: unknown;
|
|
7
|
+
bodyHash: unknown;
|
|
8
|
+
issuedAt: unknown;
|
|
9
|
+
nonce: unknown;
|
|
10
|
+
}): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const ADMIN_AUTH_SCHEME = "hmac-sha256-v1";
|
|
2
|
+
export const ADMIN_AUTH_TTL_SECONDS = 5 * 60;
|
|
3
|
+
|
|
4
|
+
export function adminAuthTranscript(input = {}) {
|
|
5
|
+
const origin = requiredOrigin(input.origin);
|
|
6
|
+
const method = requiredToken(input.method, "method", /^[A-Z]{3,10}$/);
|
|
7
|
+
const pathname = requiredToken(input.pathname, "pathname", /^\/[A-Za-z0-9/_-]{1,255}$/);
|
|
8
|
+
const bodyHash = requiredToken(input.bodyHash, "body hash", /^[a-f0-9]{64}$/);
|
|
9
|
+
const issuedAt = requiredInteger(input.issuedAt, "issued at");
|
|
10
|
+
const nonce = requiredToken(input.nonce, "nonce", /^[A-Za-z0-9_-]{32,128}$/);
|
|
11
|
+
return [ADMIN_AUTH_SCHEME, origin, method, pathname, bodyHash, issuedAt, nonce].join("\0");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
function requiredOrigin(value) {
|
|
16
|
+
let url;
|
|
17
|
+
try { url = new URL(String(value || "")); } catch { throw new Error("admin authentication origin is invalid"); }
|
|
18
|
+
const secure = url.protocol === "https:";
|
|
19
|
+
const loopback = url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
20
|
+
if ((!secure && !loopback) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
21
|
+
throw new Error("admin authentication origin is invalid");
|
|
22
|
+
}
|
|
23
|
+
return url.origin;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requiredToken(value, label, pattern) {
|
|
27
|
+
const text = String(value || "");
|
|
28
|
+
if (!pattern.test(text)) throw new Error(`admin authentication ${label} is invalid`);
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function requiredInteger(value, label) {
|
|
33
|
+
const number = Number(value);
|
|
34
|
+
if (!Number.isSafeInteger(number) || number <= 0) throw new Error(`admin authentication ${label} is invalid`);
|
|
35
|
+
return String(number);
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const DAEMON_AUTH_SCHEME: "device-signature-v1";
|
|
2
|
+
export const DAEMON_PREFLIGHT_SCHEME: "device-preflight-v1";
|
|
3
|
+
export const DAEMON_AUTH_CHALLENGE_TTL_SECONDS: number;
|
|
4
|
+
export const DAEMON_PREFLIGHT_TTL_SECONDS: number;
|
|
5
|
+
export function daemonAuthTranscript(input: {
|
|
6
|
+
challenge: unknown;
|
|
7
|
+
workerOrigin: unknown;
|
|
8
|
+
server: unknown;
|
|
9
|
+
version: unknown;
|
|
10
|
+
instanceId: unknown;
|
|
11
|
+
issuedAt: unknown;
|
|
12
|
+
}): string;
|
|
13
|
+
|
|
14
|
+
export function daemonPreflightTranscript(input: {
|
|
15
|
+
workerOrigin: unknown;
|
|
16
|
+
server: unknown;
|
|
17
|
+
version: unknown;
|
|
18
|
+
nonce: unknown;
|
|
19
|
+
issuedAt: unknown;
|
|
20
|
+
}): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const DAEMON_AUTH_SCHEME = "device-signature-v1";
|
|
2
|
+
export const DAEMON_PREFLIGHT_SCHEME = "device-preflight-v1";
|
|
3
|
+
export const DAEMON_AUTH_CHALLENGE_TTL_SECONDS = 30;
|
|
4
|
+
export const DAEMON_PREFLIGHT_TTL_SECONDS = 5 * 60;
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function daemonPreflightTranscript(input = {}) {
|
|
8
|
+
const values = [
|
|
9
|
+
DAEMON_PREFLIGHT_SCHEME,
|
|
10
|
+
requiredOrigin(input.workerOrigin),
|
|
11
|
+
requiredText(input.server, "server", 1, 128),
|
|
12
|
+
requiredText(input.version, "version", 1, 64),
|
|
13
|
+
requiredText(input.nonce, "preflight nonce", 24, 128),
|
|
14
|
+
requiredInteger(input.issuedAt, "preflight issued at"),
|
|
15
|
+
];
|
|
16
|
+
return values.join("\0");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function daemonAuthTranscript(input = {}) {
|
|
20
|
+
const values = [
|
|
21
|
+
DAEMON_AUTH_SCHEME,
|
|
22
|
+
requiredText(input.challenge, "challenge", 16, 256),
|
|
23
|
+
requiredOrigin(input.workerOrigin),
|
|
24
|
+
requiredText(input.server, "server", 1, 128),
|
|
25
|
+
requiredText(input.version, "version", 1, 64),
|
|
26
|
+
requiredText(input.instanceId, "instance id", 16, 128),
|
|
27
|
+
requiredInteger(input.issuedAt, "issued at"),
|
|
28
|
+
];
|
|
29
|
+
return values.join("\0");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function requiredText(value, label, minimum, maximum) {
|
|
33
|
+
const text = String(value || "");
|
|
34
|
+
if (text.length < minimum || text.length > maximum || /[\0\r\n]/.test(text)) {
|
|
35
|
+
throw new Error(`daemon authentication ${label} is invalid`);
|
|
36
|
+
}
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function requiredOrigin(value) {
|
|
41
|
+
let url;
|
|
42
|
+
try { url = new URL(String(value || "")); } catch { throw new Error("daemon authentication Worker origin is invalid"); }
|
|
43
|
+
const secure = url.protocol === "https:";
|
|
44
|
+
const loopback = url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
45
|
+
if ((!secure && !loopback) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
46
|
+
throw new Error("daemon authentication Worker origin is invalid");
|
|
47
|
+
}
|
|
48
|
+
return url.origin;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function requiredInteger(value, label) {
|
|
52
|
+
const number = Number(value);
|
|
53
|
+
if (!Number.isSafeInteger(number) || number <= 0) throw new Error(`daemon authentication ${label} is invalid`);
|
|
54
|
+
return String(number);
|
|
55
|
+
}
|