@mcpjam/inspector 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{dead-clicks-autocapture-BOZKS-GG.js → dead-clicks-autocapture-DZQhxpyE.js} +2 -2
- package/dist/client/assets/{exception-autocapture-BbnuQ6YK.js → exception-autocapture-Bdeu9bOa.js} +2 -2
- package/dist/client/assets/{highlighted-body-OFNGDK62-BJ-PSGAP.js → highlighted-body-OFNGDK62-Vc5bDHAo.js} +2 -2
- package/dist/client/assets/index-CH97Co__.css +1 -0
- package/dist/client/assets/{index-CbyipXXu.js → index-DR-T1Ypl.js} +6 -6
- package/dist/client/assets/{jszip.min-Bh_hqLQz.js → jszip.min-CEdoiC4g.js} +2 -2
- package/dist/client/assets/{posthog-recorder-CestZegQ.js → posthog-recorder-C0GTRZkz.js} +2 -2
- package/dist/client/assets/{surveys-BjqljrVF.js → surveys-DU_wQSCF.js} +2 -2
- package/dist/client/assets/{web-vitals-Bqx_fCFg.js → web-vitals-CjCOmLo_.js} +2 -2
- package/dist/client/index.html +2 -2
- package/dist/server/{chunk-BLGJ4PIK.js → chunk-IIQQS3FK.js} +2 -2
- package/dist/server/chunk-RP4R2FQ5.js +673 -0
- package/dist/server/consent-lifetime-YBODZG23.js +9 -0
- package/dist/server/harness-install-cli.js +1 -1
- package/dist/server/index.js +10509 -7158
- package/package.json +9 -6
- package/dist/client/assets/index-Cd1YrfPX.css +0 -1
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
// server/utils/computers/browser-consent.ts
|
|
2
|
+
import { watch } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
// server/utils/device-consent.ts
|
|
7
|
+
import { lock } from "proper-lockfile";
|
|
8
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
9
|
+
import { dirname as dirname2 } from "path";
|
|
10
|
+
import { readFile, unlink } from "fs/promises";
|
|
11
|
+
|
|
12
|
+
// server/utils/local-capability.ts
|
|
13
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
14
|
+
import { mkdir, writeFile, rename, chmod } from "fs/promises";
|
|
15
|
+
import { dirname } from "path";
|
|
16
|
+
function mintCapabilityToken() {
|
|
17
|
+
return randomBytes(32).toString("base64url");
|
|
18
|
+
}
|
|
19
|
+
function capabilityFingerprint(token) {
|
|
20
|
+
return createHash("sha256").update(token, "utf8").digest("hex");
|
|
21
|
+
}
|
|
22
|
+
function capabilityMatches(token, hash) {
|
|
23
|
+
if (typeof token !== "string" || token.length < 16 || token.length > 256 || !/^[0-9a-f]{64}$/.test(hash))
|
|
24
|
+
return false;
|
|
25
|
+
return timingSafeEqual(
|
|
26
|
+
Buffer.from(capabilityFingerprint(token), "hex"),
|
|
27
|
+
Buffer.from(hash, "hex")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
function createCapabilityMutationLock() {
|
|
31
|
+
let chain = Promise.resolve();
|
|
32
|
+
return function withLock(op) {
|
|
33
|
+
const run = chain.then(op, op);
|
|
34
|
+
chain = run.then(
|
|
35
|
+
() => void 0,
|
|
36
|
+
() => void 0
|
|
37
|
+
);
|
|
38
|
+
return run;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function persistCapabilityState(file, state) {
|
|
42
|
+
await mkdir(dirname(file), { recursive: true, mode: 448 });
|
|
43
|
+
const tmp = `${file}.tmp`;
|
|
44
|
+
await writeFile(tmp, JSON.stringify(state), { mode: 384 });
|
|
45
|
+
await chmod(tmp, 384);
|
|
46
|
+
await rename(tmp, file);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// server/utils/device-consent.ts
|
|
50
|
+
function createDeviceConsent(consentFilePath) {
|
|
51
|
+
const serialize = createCapabilityMutationLock();
|
|
52
|
+
const withConsentMutationLock = (operation) => serialize(async () => {
|
|
53
|
+
const file = consentFilePath();
|
|
54
|
+
await mkdir2(dirname2(file), { recursive: true, mode: 448 });
|
|
55
|
+
const release = await lock(file, {
|
|
56
|
+
realpath: false,
|
|
57
|
+
retries: { retries: 40, minTimeout: 25, maxTimeout: 100 },
|
|
58
|
+
stale: 1e4
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
return await operation();
|
|
62
|
+
} finally {
|
|
63
|
+
await release();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
async function readPersistedConsent() {
|
|
67
|
+
try {
|
|
68
|
+
const raw = await readFile(consentFilePath(), "utf8");
|
|
69
|
+
const parsed = JSON.parse(raw);
|
|
70
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
71
|
+
const record = parsed;
|
|
72
|
+
if (typeof record.tokenHash !== "string" || !/^[0-9a-f]{64}$/.test(record.tokenHash)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
tokenHash: record.tokenHash,
|
|
77
|
+
grantedAt: typeof record.grantedAt === "string" ? record.grantedAt : "unknown"
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function grant() {
|
|
84
|
+
return withConsentMutationLock(async () => {
|
|
85
|
+
const token = mintCapabilityToken();
|
|
86
|
+
const grantedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
87
|
+
const body = { tokenHash: capabilityFingerprint(token), grantedAt };
|
|
88
|
+
await persistCapabilityState(consentFilePath(), body);
|
|
89
|
+
return { token, grantedAt };
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function verify(token) {
|
|
93
|
+
return await verifyAndFingerprint(token) !== null;
|
|
94
|
+
}
|
|
95
|
+
async function fingerprint() {
|
|
96
|
+
return (await readPersistedConsent())?.tokenHash ?? null;
|
|
97
|
+
}
|
|
98
|
+
async function verifyAndFingerprint(token) {
|
|
99
|
+
if (typeof token !== "string" || token.length < 16 || token.length > 256)
|
|
100
|
+
return null;
|
|
101
|
+
const persisted = await readPersistedConsent();
|
|
102
|
+
if (!persisted) return null;
|
|
103
|
+
return capabilityMatches(token, persisted.tokenHash) ? persisted.tokenHash : null;
|
|
104
|
+
}
|
|
105
|
+
function revoke(token) {
|
|
106
|
+
return withConsentMutationLock(async () => {
|
|
107
|
+
if (token != null && !await verify(token)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await unlink(consentFilePath()).catch(() => {
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
grant,
|
|
116
|
+
verify,
|
|
117
|
+
fingerprint,
|
|
118
|
+
verifyAndFingerprint,
|
|
119
|
+
revoke
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// server/utils/computers/browser-consent.ts
|
|
124
|
+
var BROWSER_CONSENT_HEADER = "x-mcpjam-browser-consent";
|
|
125
|
+
var consent = createDeviceConsent(
|
|
126
|
+
() => join(homedir(), ".mcpjam", "browser", "consent.json")
|
|
127
|
+
);
|
|
128
|
+
async function grantLocalBrowserConsent() {
|
|
129
|
+
const grant = await consent.grant();
|
|
130
|
+
const { invalidateBrowserConsentLifetimes: invalidateBrowserConsentLifetimes2 } = await import("./consent-lifetime-YBODZG23.js");
|
|
131
|
+
await invalidateBrowserConsentLifetimes2(await consent.fingerprint());
|
|
132
|
+
return grant;
|
|
133
|
+
}
|
|
134
|
+
var verifyLocalBrowserConsent = consent.verify;
|
|
135
|
+
var getBrowserConsentFingerprint = consent.fingerprint;
|
|
136
|
+
var verifyAndFingerprintBrowserConsent = consent.verifyAndFingerprint;
|
|
137
|
+
async function revokeLocalBrowserConsent(token) {
|
|
138
|
+
await consent.revoke(token);
|
|
139
|
+
const { invalidateBrowserConsentLifetimes: invalidateBrowserConsentLifetimes2 } = await import("./consent-lifetime-YBODZG23.js");
|
|
140
|
+
await invalidateBrowserConsentLifetimes2(await consent.fingerprint());
|
|
141
|
+
}
|
|
142
|
+
function watchBrowserConsentChanges(changed) {
|
|
143
|
+
try {
|
|
144
|
+
const watcher = watch(
|
|
145
|
+
join(homedir(), ".mcpjam", "browser"),
|
|
146
|
+
{ persistent: false },
|
|
147
|
+
changed
|
|
148
|
+
);
|
|
149
|
+
watcher.on("error", () => watcher.close());
|
|
150
|
+
return () => watcher.close();
|
|
151
|
+
} catch {
|
|
152
|
+
return () => {
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// server/services/browserd/local/security-policy.ts
|
|
158
|
+
import { networkInterfaces } from "os";
|
|
159
|
+
import { isIP } from "net";
|
|
160
|
+
import { lookup } from "dns/promises";
|
|
161
|
+
|
|
162
|
+
// shared/hosted-web-timeouts.ts
|
|
163
|
+
var WEB_CONNECT_TIMEOUT_MS = 1e4;
|
|
164
|
+
var WEB_CALL_TIMEOUT_MS = 3e4;
|
|
165
|
+
var WEB_STREAM_TIMEOUT_MS = 12e4;
|
|
166
|
+
|
|
167
|
+
// server/config.ts
|
|
168
|
+
var SERVER_PORT = process.env.SERVER_PORT ? parseInt(process.env.SERVER_PORT, 10) : 6274;
|
|
169
|
+
var SERVER_HOSTNAME = process.env.ENVIRONMENT === "dev" ? "localhost" : "127.0.0.1";
|
|
170
|
+
var LOCAL_SERVER_ADDR = `http://localhost:${SERVER_PORT}`;
|
|
171
|
+
var HOSTED_MODE = process.env.VITE_MCPJAM_HOSTED_MODE === "true";
|
|
172
|
+
var LOCAL_BROWSER_ENABLED = !HOSTED_MODE && process.env.MCPJAM_LOCAL_BROWSER_ENABLED !== "false";
|
|
173
|
+
var LOCAL_COMPUTER_ENABLED = !HOSTED_MODE && process.env.MCPJAM_LOCAL_COMPUTER_ENABLED !== "false";
|
|
174
|
+
var LOCAL_HARNESS_ENABLED = !HOSTED_MODE && process.env.MCPJAM_LOCAL_HARNESS_ENABLED === "true";
|
|
175
|
+
var SCHEDULED_EVALS_WRITE_ENABLED = process.env.MCPJAM_SCHEDULED_EVALS_WRITE_ENABLED === "true";
|
|
176
|
+
var WEBMCP_INSPECTOR_ENABLED = process.env.MCPJAM_WEBMCP_INSPECTOR_ENABLED !== "false";
|
|
177
|
+
function webmcpInspectorHostedEnabled(env = process.env) {
|
|
178
|
+
return HOSTED_MODE && env.MCPJAM_WEBMCP_INSPECTOR_HOSTED_ENABLED === "1";
|
|
179
|
+
}
|
|
180
|
+
function webmcpInspectorReachable(env = process.env) {
|
|
181
|
+
if (!WEBMCP_INSPECTOR_ENABLED) return false;
|
|
182
|
+
return !HOSTED_MODE || webmcpInspectorHostedEnabled(env);
|
|
183
|
+
}
|
|
184
|
+
function hostedBrowserEnabled(env = process.env) {
|
|
185
|
+
return env.HOSTED_BROWSER_TOOLS_ENABLED === "1";
|
|
186
|
+
}
|
|
187
|
+
function webmcpPageToolsMode(env = process.env) {
|
|
188
|
+
return env.MCPJAM_WEBMCP_PAGE_TOOLS === "verbs" ? "verbs" : "first_class";
|
|
189
|
+
}
|
|
190
|
+
var EVAL_WIDGET_MODEL_CONTEXT = process.env.MCPJAM_EVAL_WIDGET_MODEL_CONTEXT === "true";
|
|
191
|
+
var WEB_ALLOWED_ORIGINS = (process.env.WEB_ALLOWED_ORIGINS ?? "").split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
|
|
192
|
+
var CLIENT_PORT = process.env.CLIENT_PORT || "5173";
|
|
193
|
+
var DEFAULT_CORS_ORIGINS = [
|
|
194
|
+
`http://localhost:${CLIENT_PORT}`,
|
|
195
|
+
// Vite dev server
|
|
196
|
+
"http://localhost:8080",
|
|
197
|
+
// Electron renderer dev server
|
|
198
|
+
`http://localhost:${SERVER_PORT}`,
|
|
199
|
+
// Hono server
|
|
200
|
+
`http://127.0.0.1:${SERVER_PORT}`,
|
|
201
|
+
// Hono server production
|
|
202
|
+
"https://staging.mcpjam.com"
|
|
203
|
+
// Hosted deployment
|
|
204
|
+
];
|
|
205
|
+
var CORS_ORIGINS = HOSTED_MODE && WEB_ALLOWED_ORIGINS.length > 0 ? WEB_ALLOWED_ORIGINS : Array.from(/* @__PURE__ */ new Set([...DEFAULT_CORS_ORIGINS, ...WEB_ALLOWED_ORIGINS]));
|
|
206
|
+
var ELICITATION_FORM_TTL_MS = 5 * 6e4;
|
|
207
|
+
var ELICITATION_URL_CONSENT_TTL_MS = 2 * 6e4;
|
|
208
|
+
var ELICITATION_POLL_INTERVAL_MS = 1e3;
|
|
209
|
+
var ELICITATION_POLL_JITTER_MS = 250;
|
|
210
|
+
var ELICITATION_POLL_MAX_FAILURES = 5;
|
|
211
|
+
var ELICITATION_SERVICE_ROUTE_TIMEOUT_MS = 1e4;
|
|
212
|
+
var ELICITATION_TIMEOUT_EXTENSION_MS = ELICITATION_FORM_TTL_MS + 3e4;
|
|
213
|
+
var MRTR_CONTINUATION_TTL_MS = 10 * 6e4;
|
|
214
|
+
var MRTR_CONTINUATION_ROUTE_TIMEOUT_MS = 1e4;
|
|
215
|
+
var MRTR_CONTINUATION_LEASE_TTL_MS = 4 * MRTR_CONTINUATION_ROUTE_TIMEOUT_MS + WEB_CALL_TIMEOUT_MS + 6e4;
|
|
216
|
+
var MRTR_RESUME_STATE_MAX_BYTES = 128 * 1024;
|
|
217
|
+
var MRTR_DISPLAY_FIELD_MAX_BYTES = 16 * 1024;
|
|
218
|
+
var MRTR_RESPONSE_CONTENT_MAX_BYTES = 64 * 1024;
|
|
219
|
+
var MRTR_TEARDOWN_TIMEOUT_MS = 5e3;
|
|
220
|
+
var MCPJAM_HOSTED_ORIGIN = process.env.MCPJAM_HOSTED_ORIGIN?.replace(/\/+$/, "") || "https://app.mcpjam.com";
|
|
221
|
+
function parseAllowedHosts(raw) {
|
|
222
|
+
return raw ? raw.split(",").map((h) => h.trim().toLowerCase()).filter((h) => h.length > 0) : [];
|
|
223
|
+
}
|
|
224
|
+
var ALLOWED_HOSTS = parseAllowedHosts(
|
|
225
|
+
process.env.MCPJAM_ALLOWED_HOSTS
|
|
226
|
+
);
|
|
227
|
+
var CANIUSE_LANDING_HOSTS = new Set(
|
|
228
|
+
(process.env.CANIUSE_LANDING_HOSTS ?? "caniuse.dev,www.caniuse.dev").split(",").map((h) => h.trim().toLowerCase()).filter((h) => h.length > 0)
|
|
229
|
+
);
|
|
230
|
+
var SCORE_LANDING_HOSTS = new Set(
|
|
231
|
+
(process.env.SCORE_LANDING_HOSTS ?? "score.mcpjam.com,www.score.mcpjam.com").split(",").map((h) => h.trim().toLowerCase()).filter((h) => h.length > 0)
|
|
232
|
+
);
|
|
233
|
+
var BARE_HOSTNAME = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
|
|
234
|
+
function parseSandboxHosts(raw) {
|
|
235
|
+
return new Set(
|
|
236
|
+
raw.split(",").map((h) => h.trim().toLowerCase()).filter((h) => BARE_HOSTNAME.test(h))
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
var SANDBOX_HOSTS = parseSandboxHosts(
|
|
240
|
+
process.env.SANDBOX_HOSTS ?? "sandbox.mcpjam.com,sandbox-staging.mcpjam.com"
|
|
241
|
+
);
|
|
242
|
+
function resolveSandboxIsolation(sandboxHosts = SANDBOX_HOSTS, hostedOrigin = MCPJAM_HOSTED_ORIGIN) {
|
|
243
|
+
if (sandboxHosts.size === 0) {
|
|
244
|
+
return "unset";
|
|
245
|
+
}
|
|
246
|
+
let appHost;
|
|
247
|
+
try {
|
|
248
|
+
appHost = new URL(hostedOrigin).hostname.toLowerCase();
|
|
249
|
+
} catch {
|
|
250
|
+
return "unset";
|
|
251
|
+
}
|
|
252
|
+
return sandboxHosts.has(appHost) ? "same-origin" : "ok";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// server/utils/inspector-frontend-url.ts
|
|
256
|
+
function getInspectorFrontendUrl(options = {}) {
|
|
257
|
+
const explicitFrontendUrl = process.env.MCPJAM_INSPECTOR_FRONTEND_URL?.trim() || process.env.FRONTEND_URL?.trim();
|
|
258
|
+
if (explicitFrontendUrl) {
|
|
259
|
+
return explicitFrontendUrl;
|
|
260
|
+
}
|
|
261
|
+
const isElectron = options.isElectron ?? process.env.ELECTRON_APP === "true";
|
|
262
|
+
const isPackaged = options.isPackaged ?? process.env.IS_PACKAGED === "true";
|
|
263
|
+
const isProduction = options.isProduction ?? process.env.NODE_ENV === "production";
|
|
264
|
+
if (isProduction || isElectron && isPackaged) {
|
|
265
|
+
return process.env.BASE_URL?.trim() || `http://${SERVER_HOSTNAME}:${SERVER_PORT}`;
|
|
266
|
+
}
|
|
267
|
+
if (isElectron) {
|
|
268
|
+
return "http://localhost:8080";
|
|
269
|
+
}
|
|
270
|
+
return `http://localhost:${process.env.CLIENT_PORT || "5173"}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// server/services/browserd/local/security-policy.ts
|
|
274
|
+
var BrowserPolicyError = class extends Error {
|
|
275
|
+
constructor(message = "Browser access to this destination is not allowed.") {
|
|
276
|
+
super(`browser_policy_refused: ${message}`);
|
|
277
|
+
this.code = "browser_policy_refused";
|
|
278
|
+
this.name = "BrowserPolicyError";
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
var controllerPorts = /* @__PURE__ */ new Map();
|
|
282
|
+
var controllerOrigins = /* @__PURE__ */ new Map();
|
|
283
|
+
function registerBrowserController(url) {
|
|
284
|
+
const parsed = new URL(url);
|
|
285
|
+
const port = Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
|
|
286
|
+
controllerOrigins.set(
|
|
287
|
+
parsed.origin,
|
|
288
|
+
(controllerOrigins.get(parsed.origin) ?? 0) + 1
|
|
289
|
+
);
|
|
290
|
+
const local = isMachineAddress(parsed.hostname);
|
|
291
|
+
if (local) controllerPorts.set(port, (controllerPorts.get(port) ?? 0) + 1);
|
|
292
|
+
let removed = false;
|
|
293
|
+
return () => {
|
|
294
|
+
if (removed) return;
|
|
295
|
+
removed = true;
|
|
296
|
+
for (const [map, key] of [
|
|
297
|
+
[controllerOrigins, parsed.origin],
|
|
298
|
+
[controllerPorts, port]
|
|
299
|
+
]) {
|
|
300
|
+
if (map === controllerPorts && !local) continue;
|
|
301
|
+
const registry = map;
|
|
302
|
+
const count = registry.get(key) ?? 0;
|
|
303
|
+
if (count <= 1) registry.delete(key);
|
|
304
|
+
else registry.set(key, count - 1);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function host(raw) {
|
|
309
|
+
return raw.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
310
|
+
}
|
|
311
|
+
function isMachineAddress(raw) {
|
|
312
|
+
const value = host(raw);
|
|
313
|
+
if (value === "localhost" || value.endsWith(".localhost") || value === "::1" || value === "::" || value === "0.0.0.0" || /^127\./.test(value))
|
|
314
|
+
return true;
|
|
315
|
+
if (value.startsWith("::ffff:")) {
|
|
316
|
+
const tail = value.slice(7);
|
|
317
|
+
if (tail.includes(".")) return isMachineAddress(tail);
|
|
318
|
+
const parts = tail.split(":");
|
|
319
|
+
if (parts.length === 2 && parts.every((p) => /^[\da-f]{1,4}$/.test(p))) {
|
|
320
|
+
const first = parseInt(parts[0], 16), last = parseInt(parts[1], 16);
|
|
321
|
+
return isMachineAddress(
|
|
322
|
+
`${first >> 8}.${first & 255}.${last >> 8}.${last & 255}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return Object.values(networkInterfaces()).flat().some((a) => a && host(a.address) === value);
|
|
327
|
+
}
|
|
328
|
+
function createLocalBrowserSecurityPolicy(options = {}) {
|
|
329
|
+
const counts = {
|
|
330
|
+
navigationRefused: 0,
|
|
331
|
+
networkRefused: 0,
|
|
332
|
+
destinationRefused: 0
|
|
333
|
+
};
|
|
334
|
+
const increment = (key) => {
|
|
335
|
+
counts[key] = Math.min(1e6, counts[key] + 1);
|
|
336
|
+
};
|
|
337
|
+
const denyNetwork = () => {
|
|
338
|
+
increment("networkRefused");
|
|
339
|
+
return false;
|
|
340
|
+
};
|
|
341
|
+
const navigationError = (message) => {
|
|
342
|
+
increment("navigationRefused");
|
|
343
|
+
return new BrowserPolicyError(message);
|
|
344
|
+
};
|
|
345
|
+
const destinationError = () => {
|
|
346
|
+
increment("destinationRefused");
|
|
347
|
+
return new BrowserPolicyError();
|
|
348
|
+
};
|
|
349
|
+
let disposed = false;
|
|
350
|
+
const supplied = options.controllerUrls;
|
|
351
|
+
const registrations = [];
|
|
352
|
+
if (supplied)
|
|
353
|
+
for (const url of supplied)
|
|
354
|
+
registrations.push(registerBrowserController(url));
|
|
355
|
+
else {
|
|
356
|
+
registrations.push(
|
|
357
|
+
registerBrowserController(
|
|
358
|
+
`http://127.0.0.1:${process.env.SERVER_PORT || 6274}`
|
|
359
|
+
)
|
|
360
|
+
);
|
|
361
|
+
registrations.push(registerBrowserController(getInspectorFrontendUrl()));
|
|
362
|
+
}
|
|
363
|
+
const resolve = options.lookup ?? lookup;
|
|
364
|
+
const aliases = (process.env.MCPJAM_ALLOWED_HOSTS || "").split(",").map((v) => v.trim().toLowerCase()).filter(Boolean);
|
|
365
|
+
function deniedHost(name, port) {
|
|
366
|
+
if (!controllerPorts.has(port)) return false;
|
|
367
|
+
return isMachineAddress(name) || aliases.some(
|
|
368
|
+
(pattern) => new RegExp(
|
|
369
|
+
`^${pattern.split("*").map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`,
|
|
370
|
+
"i"
|
|
371
|
+
).test(name)
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const active = options.isActive ?? (() => true);
|
|
375
|
+
const assertActive = options.assertActive ?? (async () => {
|
|
376
|
+
});
|
|
377
|
+
const policy = {
|
|
378
|
+
discoveryBudget: { entries: /* @__PURE__ */ new Map() },
|
|
379
|
+
dispose: () => {
|
|
380
|
+
if (disposed) return;
|
|
381
|
+
disposed = true;
|
|
382
|
+
try {
|
|
383
|
+
options.onAudit?.({ ...counts });
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
registrations.forEach((remove) => remove());
|
|
387
|
+
options.dispose?.();
|
|
388
|
+
},
|
|
389
|
+
assertActive,
|
|
390
|
+
isActive: active,
|
|
391
|
+
onRevoked: options.onRevoked ?? (() => () => {
|
|
392
|
+
}),
|
|
393
|
+
assertNavigation(raw) {
|
|
394
|
+
let url;
|
|
395
|
+
try {
|
|
396
|
+
url = new URL(raw);
|
|
397
|
+
} catch {
|
|
398
|
+
throw navigationError(
|
|
399
|
+
"Browser navigation requires an HTTP or HTTPS URL."
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (!["http:", "https:"].includes(url.protocol) || !policy.allowsRequest(url.href))
|
|
403
|
+
throw navigationError(
|
|
404
|
+
"Browser navigation requires an allowed HTTP or HTTPS website."
|
|
405
|
+
);
|
|
406
|
+
},
|
|
407
|
+
allowsRequest(raw) {
|
|
408
|
+
if (!active()) return denyNetwork();
|
|
409
|
+
let url;
|
|
410
|
+
try {
|
|
411
|
+
url = new URL(raw);
|
|
412
|
+
} catch {
|
|
413
|
+
return denyNetwork();
|
|
414
|
+
}
|
|
415
|
+
if ([
|
|
416
|
+
"file:",
|
|
417
|
+
"javascript:",
|
|
418
|
+
"chrome:",
|
|
419
|
+
"chrome-extension:",
|
|
420
|
+
"devtools:"
|
|
421
|
+
].includes(url.protocol))
|
|
422
|
+
return denyNetwork();
|
|
423
|
+
if (!["http:", "https:", "ws:", "wss:"].includes(url.protocol))
|
|
424
|
+
return ["about:", "blob:", "data:"].includes(url.protocol) || denyNetwork();
|
|
425
|
+
const port = Number(
|
|
426
|
+
url.port || (["https:", "wss:"].includes(url.protocol) ? 443 : 80)
|
|
427
|
+
);
|
|
428
|
+
const origin = `${url.protocol === "ws:" ? "http:" : url.protocol === "wss:" ? "https:" : url.protocol}//${url.host}`;
|
|
429
|
+
return !controllerOrigins.has(origin) && !deniedHost(host(url.hostname), port) || denyNetwork();
|
|
430
|
+
},
|
|
431
|
+
async resolveDestination(name, port) {
|
|
432
|
+
await assertActive();
|
|
433
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535 || deniedHost(host(name), port))
|
|
434
|
+
throw destinationError();
|
|
435
|
+
const addresses = isIP(host(name)) ? [{ address: host(name), family: isIP(host(name)) }] : await resolve(host(name), { all: true, verbatim: true });
|
|
436
|
+
if (!addresses.length || addresses.some((a) => deniedHost(a.address, port)))
|
|
437
|
+
throw destinationError();
|
|
438
|
+
await assertActive();
|
|
439
|
+
return addresses;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
return policy;
|
|
443
|
+
}
|
|
444
|
+
function secureDriverContext(context, policy) {
|
|
445
|
+
const pages = /* @__PURE__ */ new WeakMap();
|
|
446
|
+
let closed = false;
|
|
447
|
+
let stop;
|
|
448
|
+
const close = async () => {
|
|
449
|
+
if (closed) return;
|
|
450
|
+
closed = true;
|
|
451
|
+
stop?.();
|
|
452
|
+
try {
|
|
453
|
+
await context.close();
|
|
454
|
+
} finally {
|
|
455
|
+
policy.dispose?.();
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
stop = policy.onRevoked(close);
|
|
459
|
+
function guard(page) {
|
|
460
|
+
const existing = pages.get(page);
|
|
461
|
+
if (existing) return existing;
|
|
462
|
+
const synchronous = /* @__PURE__ */ new Set([
|
|
463
|
+
"url",
|
|
464
|
+
"isClosed",
|
|
465
|
+
"consoleEntries",
|
|
466
|
+
"consoleCursor",
|
|
467
|
+
"dropConsoleSince",
|
|
468
|
+
"networkEntries",
|
|
469
|
+
"networkCursor",
|
|
470
|
+
"dropNetworkSince",
|
|
471
|
+
"pendingDialog"
|
|
472
|
+
]);
|
|
473
|
+
const wrapped = new Proxy(page, {
|
|
474
|
+
get(target, key) {
|
|
475
|
+
const value = Reflect.get(target, key);
|
|
476
|
+
if (typeof value !== "function") return value;
|
|
477
|
+
if (key === "close" || key === "isClosed") return value.bind(target);
|
|
478
|
+
if (synchronous.has(String(key)))
|
|
479
|
+
return (...args) => {
|
|
480
|
+
if (!policy.isActive())
|
|
481
|
+
throw new BrowserPolicyError("Browser permission was revoked.");
|
|
482
|
+
return value.apply(target, args);
|
|
483
|
+
};
|
|
484
|
+
return async (...args) => {
|
|
485
|
+
await policy.assertActive();
|
|
486
|
+
if (key === "goto") policy.assertNavigation(args[0]);
|
|
487
|
+
if (key !== "goto" && !policy.allowsRequest(target.url()))
|
|
488
|
+
throw new BrowserPolicyError();
|
|
489
|
+
const result = await value.apply(target, args);
|
|
490
|
+
await policy.assertActive();
|
|
491
|
+
if ((key === "cdp" || key === "webmcp") && result) {
|
|
492
|
+
return new Proxy(result, {
|
|
493
|
+
get(rpc, method) {
|
|
494
|
+
const member = Reflect.get(rpc, method);
|
|
495
|
+
if (typeof member !== "function") return member;
|
|
496
|
+
if (!["send", "invoke"].includes(String(method)))
|
|
497
|
+
return member.bind(rpc);
|
|
498
|
+
return async (...params) => {
|
|
499
|
+
await policy.assertActive();
|
|
500
|
+
if (!policy.allowsRequest(target.url()))
|
|
501
|
+
throw new BrowserPolicyError();
|
|
502
|
+
if (method === "send" && params[0] === "Page.navigate")
|
|
503
|
+
policy.assertNavigation(params[1].url);
|
|
504
|
+
const response = await member.apply(rpc, params);
|
|
505
|
+
await policy.assertActive();
|
|
506
|
+
return response;
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
return result;
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
pages.set(page, wrapped);
|
|
516
|
+
return wrapped;
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
async newPage() {
|
|
520
|
+
await policy.assertActive();
|
|
521
|
+
const page = await context.newPage();
|
|
522
|
+
await policy.assertActive();
|
|
523
|
+
return guard(page);
|
|
524
|
+
},
|
|
525
|
+
...context.onPageCreated ? {
|
|
526
|
+
onPageCreated: (listener) => context.onPageCreated(
|
|
527
|
+
(event) => listener({
|
|
528
|
+
...event,
|
|
529
|
+
page: guard(event.page),
|
|
530
|
+
opener: guard(event.opener)
|
|
531
|
+
})
|
|
532
|
+
)
|
|
533
|
+
} : {},
|
|
534
|
+
isConnected: () => !closed && policy.isActive() && context.isConnected(),
|
|
535
|
+
close
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// server/services/browserd/local/consent-lifetime.ts
|
|
540
|
+
async function createBrowserConsentLifetime(expected, read = getBrowserConsentFingerprint, admission) {
|
|
541
|
+
const fingerprint = expected ?? await read();
|
|
542
|
+
let valid = Boolean(fingerprint);
|
|
543
|
+
const callbacks = /* @__PURE__ */ new Set();
|
|
544
|
+
let disposal;
|
|
545
|
+
let checking;
|
|
546
|
+
let stopWatching;
|
|
547
|
+
let timer;
|
|
548
|
+
const revoke = () => {
|
|
549
|
+
if (disposal) return disposal;
|
|
550
|
+
valid = false;
|
|
551
|
+
clearInterval(timer);
|
|
552
|
+
stopWatching?.();
|
|
553
|
+
const current = [...callbacks];
|
|
554
|
+
callbacks.clear();
|
|
555
|
+
disposal = Promise.allSettled(
|
|
556
|
+
current.map((callback) => Promise.resolve().then(callback))
|
|
557
|
+
).then(() => {
|
|
558
|
+
});
|
|
559
|
+
return disposal;
|
|
560
|
+
};
|
|
561
|
+
const assertActive = async () => {
|
|
562
|
+
if (!valid || !fingerprint || await read().catch(() => null) !== fingerprint || admission && !await admission().catch(() => false)) {
|
|
563
|
+
void revoke();
|
|
564
|
+
throw new BrowserPolicyError(
|
|
565
|
+
"Browser permission was revoked or changed. Allow Browser again to start a new session."
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
await assertActive();
|
|
570
|
+
const check = () => {
|
|
571
|
+
if (checking) return;
|
|
572
|
+
checking = assertActive().catch(() => {
|
|
573
|
+
}).finally(() => {
|
|
574
|
+
checking = void 0;
|
|
575
|
+
});
|
|
576
|
+
};
|
|
577
|
+
timer = setInterval(check, 1e3);
|
|
578
|
+
if (read === getBrowserConsentFingerprint)
|
|
579
|
+
stopWatching = watchBrowserConsentChanges(check);
|
|
580
|
+
timer.unref?.();
|
|
581
|
+
const lifetime = {
|
|
582
|
+
fingerprint,
|
|
583
|
+
assertActive,
|
|
584
|
+
isActive: () => valid,
|
|
585
|
+
onRevoked(callback) {
|
|
586
|
+
if (!valid) {
|
|
587
|
+
void Promise.resolve().then(callback);
|
|
588
|
+
return () => {
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
callbacks.add(callback);
|
|
592
|
+
return () => callbacks.delete(callback);
|
|
593
|
+
},
|
|
594
|
+
revoke,
|
|
595
|
+
dispose() {
|
|
596
|
+
valid = false;
|
|
597
|
+
clearInterval(timer);
|
|
598
|
+
stopWatching?.();
|
|
599
|
+
callbacks.clear();
|
|
600
|
+
activeLifetimes.delete(lifetime);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
activeLifetimes.add(lifetime);
|
|
604
|
+
return lifetime;
|
|
605
|
+
}
|
|
606
|
+
var activeLifetimes = /* @__PURE__ */ new Set();
|
|
607
|
+
async function invalidateBrowserConsentLifetimes(current) {
|
|
608
|
+
await Promise.allSettled(
|
|
609
|
+
[...activeLifetimes].filter((lifetime) => lifetime.fingerprint !== current).map(async (lifetime) => {
|
|
610
|
+
await lifetime.revoke();
|
|
611
|
+
lifetime.dispose();
|
|
612
|
+
})
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export {
|
|
617
|
+
WEB_CONNECT_TIMEOUT_MS,
|
|
618
|
+
WEB_CALL_TIMEOUT_MS,
|
|
619
|
+
WEB_STREAM_TIMEOUT_MS,
|
|
620
|
+
SERVER_PORT,
|
|
621
|
+
LOCAL_SERVER_ADDR,
|
|
622
|
+
HOSTED_MODE,
|
|
623
|
+
LOCAL_BROWSER_ENABLED,
|
|
624
|
+
LOCAL_COMPUTER_ENABLED,
|
|
625
|
+
LOCAL_HARNESS_ENABLED,
|
|
626
|
+
SCHEDULED_EVALS_WRITE_ENABLED,
|
|
627
|
+
WEBMCP_INSPECTOR_ENABLED,
|
|
628
|
+
webmcpInspectorHostedEnabled,
|
|
629
|
+
webmcpInspectorReachable,
|
|
630
|
+
hostedBrowserEnabled,
|
|
631
|
+
webmcpPageToolsMode,
|
|
632
|
+
EVAL_WIDGET_MODEL_CONTEXT,
|
|
633
|
+
CORS_ORIGINS,
|
|
634
|
+
ELICITATION_FORM_TTL_MS,
|
|
635
|
+
ELICITATION_URL_CONSENT_TTL_MS,
|
|
636
|
+
ELICITATION_POLL_INTERVAL_MS,
|
|
637
|
+
ELICITATION_POLL_JITTER_MS,
|
|
638
|
+
ELICITATION_POLL_MAX_FAILURES,
|
|
639
|
+
ELICITATION_SERVICE_ROUTE_TIMEOUT_MS,
|
|
640
|
+
ELICITATION_TIMEOUT_EXTENSION_MS,
|
|
641
|
+
MRTR_CONTINUATION_TTL_MS,
|
|
642
|
+
MRTR_CONTINUATION_ROUTE_TIMEOUT_MS,
|
|
643
|
+
MRTR_CONTINUATION_LEASE_TTL_MS,
|
|
644
|
+
MRTR_RESUME_STATE_MAX_BYTES,
|
|
645
|
+
MRTR_DISPLAY_FIELD_MAX_BYTES,
|
|
646
|
+
MRTR_RESPONSE_CONTENT_MAX_BYTES,
|
|
647
|
+
MRTR_TEARDOWN_TIMEOUT_MS,
|
|
648
|
+
MCPJAM_HOSTED_ORIGIN,
|
|
649
|
+
parseAllowedHosts,
|
|
650
|
+
ALLOWED_HOSTS,
|
|
651
|
+
CANIUSE_LANDING_HOSTS,
|
|
652
|
+
SCORE_LANDING_HOSTS,
|
|
653
|
+
SANDBOX_HOSTS,
|
|
654
|
+
resolveSandboxIsolation,
|
|
655
|
+
getInspectorFrontendUrl,
|
|
656
|
+
registerBrowserController,
|
|
657
|
+
createLocalBrowserSecurityPolicy,
|
|
658
|
+
secureDriverContext,
|
|
659
|
+
mintCapabilityToken,
|
|
660
|
+
capabilityFingerprint,
|
|
661
|
+
capabilityMatches,
|
|
662
|
+
persistCapabilityState,
|
|
663
|
+
createDeviceConsent,
|
|
664
|
+
BROWSER_CONSENT_HEADER,
|
|
665
|
+
grantLocalBrowserConsent,
|
|
666
|
+
verifyLocalBrowserConsent,
|
|
667
|
+
getBrowserConsentFingerprint,
|
|
668
|
+
verifyAndFingerprintBrowserConsent,
|
|
669
|
+
revokeLocalBrowserConsent,
|
|
670
|
+
createBrowserConsentLifetime,
|
|
671
|
+
invalidateBrowserConsentLifetimes
|
|
672
|
+
};
|
|
673
|
+
//# sourceMappingURL=chunk-RP4R2FQ5.js.map
|