machine-bridge-mcp 1.1.5 → 1.2.1
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 +21 -0
- package/CODE_OF_CONDUCT.md +24 -0
- package/CONTRIBUTING.md +3 -1
- package/GOVERNANCE.md +50 -0
- package/README.md +26 -0
- package/SUPPORT.md +31 -0
- package/browser-extension/browser-operations.js +10 -4
- package/browser-extension/devtools-input.js +2 -2
- package/browser-extension/manifest.json +2 -2
- package/browser-extension/page-automation.js +1 -1
- package/docs/ARCHITECTURE.md +14 -17
- package/docs/AUDIT.md +28 -0
- package/docs/ENGINEERING.md +8 -2
- package/docs/PROJECT_STANDARDS.md +4 -2
- package/docs/TESTING.md +9 -3
- package/docs/UPGRADING.md +32 -0
- package/package.json +12 -4
- package/scripts/coverage-check.mjs +27 -10
- package/src/local/account-access.mjs +7 -5
- package/src/local/agent-context.mjs +7 -148
- package/src/local/agent-contract.mjs +206 -0
- package/src/local/browser-bridge.mjs +30 -281
- package/src/local/browser-extension-protocol.mjs +19 -4
- package/src/local/browser-operation-service.mjs +325 -0
- package/src/local/call-registry.mjs +44 -1
- package/src/local/capability-ranking.mjs +13 -0
- package/src/local/cli-local-admin.mjs +74 -38
- package/src/local/cli-options.mjs +18 -18
- package/src/local/cli-policy.mjs +1 -1
- package/src/local/cli-service.mjs +132 -0
- package/src/local/cli.mjs +27 -109
- package/src/local/daemon-process.mjs +1 -1
- package/src/local/full-access-test.mjs +1 -1
- package/src/local/job-runner.mjs +9 -3
- package/src/local/managed-job-plan.mjs +3 -3
- package/src/local/monotonic-deadline.mjs +7 -0
- package/src/local/numbers.mjs +8 -0
- package/src/local/policy.mjs +97 -16
- package/src/local/project-metadata.mjs +7 -1
- package/src/local/project-package.mjs +1 -1
- package/src/local/records.mjs +6 -0
- package/src/local/resource-operations.mjs +1 -1
- package/src/local/runtime-capabilities.mjs +66 -0
- package/src/local/runtime-diagnostics.mjs +91 -0
- package/src/local/runtime-reporting.mjs +113 -0
- package/src/local/runtime.mjs +54 -191
- package/src/local/service-convergence.mjs +1 -1
- package/src/local/service.mjs +1 -1
- package/src/local/state.mjs +1 -1
- package/src/local/windows-service.mjs +1 -1
- package/src/local/worker-deployment.mjs +15 -10
- package/src/local/worker-health.mjs +8 -4
- package/src/worker/access.ts +8 -7
- package/src/worker/account-admin.ts +2 -2
- package/src/worker/authority.ts +3 -3
- package/src/worker/http.ts +2 -2
- package/src/worker/index.ts +29 -337
- package/src/worker/oauth-controller.ts +343 -0
- package/src/worker/oauth-state.ts +1 -1
- package/src/worker/oauth-tokens.ts +2 -2
- package/src/worker/tool-catalog.ts +1 -1
- package/tsconfig.json +2 -1
- package/tsconfig.local.json +27 -0
|
@@ -12,23 +12,13 @@ import {
|
|
|
12
12
|
isAllowedExtensionOrigin, isAllowedLoopbackHost, loadOrCreatePairing,
|
|
13
13
|
pairingHtml, savePairing, securityHeaders, sendJson,
|
|
14
14
|
} from "./browser-pairing-store.mjs";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
normalizeInputMode, normalizeNavigationWait, normalizeTabCommand, optionalInteger, optionalString,
|
|
18
|
-
validateNavigationUrl,
|
|
19
|
-
} from "./browser-command.mjs";
|
|
15
|
+
import { clampInt } from "./browser-command.mjs";
|
|
16
|
+
import { BrowserOperationService } from "./browser-operation-service.mjs";
|
|
20
17
|
|
|
21
18
|
const MAX_PORT_ATTEMPTS = 10;
|
|
22
19
|
const MAX_PENDING = 32;
|
|
23
|
-
const MAX_SOURCE_BYTES = 4 * 1024 * 1024;
|
|
24
|
-
const MAX_FORM_FIELDS = 200;
|
|
25
|
-
const MAX_FIELD_VALUE_CHARS = 128 * 1024;
|
|
26
|
-
const MAX_FORM_VALUE_BYTES = 4 * 1024 * 1024;
|
|
27
|
-
const RESOURCE_NAME = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
28
20
|
const EXTENSION_HANDSHAKE_MS = 3_000;
|
|
29
21
|
|
|
30
|
-
|
|
31
|
-
|
|
32
22
|
export class BrowserBridgeManager {
|
|
33
23
|
constructor({ policy, authorizeTool = null, stateRoot = "", runProcess, readResourceText, readResourceBinary, throwIfCancelled = () => {} }) {
|
|
34
24
|
this.policy = policy || {};
|
|
@@ -57,248 +47,46 @@ export class BrowserBridgeManager {
|
|
|
57
47
|
this.port = 0;
|
|
58
48
|
this.token = "";
|
|
59
49
|
this.extensionPath = resolve(packageRoot, "browser-extension");
|
|
50
|
+
this.operationService = new BrowserOperationService({
|
|
51
|
+
authorizeTool: (tool) => this.authorizeTool(tool),
|
|
52
|
+
ensureStarted: (context) => this.ensureStarted(context),
|
|
53
|
+
request: (...args) => this.request(...args),
|
|
54
|
+
bridgeStatus: () => ({
|
|
55
|
+
port: this.port,
|
|
56
|
+
brokerRole: this.server ? "owner" : this.upstream?.readyState === 1 ? "client" : "unavailable",
|
|
57
|
+
extensionConnected: this.extensionConnected(),
|
|
58
|
+
extensionInfo: this.extensionStatusInfo(),
|
|
59
|
+
extensionReloadRequired: this.extensionReloadRequired(),
|
|
60
|
+
}),
|
|
61
|
+
extensionPath: this.extensionPath,
|
|
62
|
+
expectedExtensionVersion: EXPECTED_EXTENSION_VERSION,
|
|
63
|
+
runProcess: (...args) => this.runProcess(...args),
|
|
64
|
+
readResourceText: (name) => this.readResourceText(name),
|
|
65
|
+
readResourceBinary: (name) => this.readResourceBinary(name),
|
|
66
|
+
});
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
this.authorizeTool("browser_status");
|
|
64
|
-
await this.ensureStarted(context);
|
|
65
|
-
return {
|
|
66
|
-
available: true,
|
|
67
|
-
connected: this.extensionConnected(),
|
|
68
|
-
broker_role: this.server ? "owner" : this.upstream?.readyState === 1 ? "client" : "unavailable",
|
|
69
|
-
endpoint: `ws://127.0.0.1:${this.port}/extension`,
|
|
70
|
-
pairing_url: `http://127.0.0.1:${this.port}/pair`,
|
|
71
|
-
extension_path: this.extensionPath,
|
|
72
|
-
expected_extension_version: EXPECTED_EXTENSION_VERSION,
|
|
73
|
-
extension_protocol: this.extensionStatusInfo()?.protocol || null,
|
|
74
|
-
extension_version: this.extensionStatusInfo()?.version || "",
|
|
75
|
-
extension_capabilities: this.extensionStatusInfo()?.capabilities || [],
|
|
76
|
-
extension_reload_required: this.extensionReloadRequired(),
|
|
77
|
-
supported_browsers: ["Chrome", "Chromium", "Microsoft Edge", "Brave", "Vivaldi", "other Chromium browsers with Manifest V3"],
|
|
78
|
-
controls_existing_profile: true,
|
|
79
|
-
controls_extension_profile: true,
|
|
80
|
-
launches_browser_process: false,
|
|
81
|
-
launches_separate_automation_profile: false,
|
|
82
|
-
profile_identity_verifiable: false,
|
|
83
|
-
uses_existing_tabs_and_login_state: true,
|
|
84
|
-
source_access: true,
|
|
85
|
-
semantic_snapshot_refs: true,
|
|
86
|
-
actionability_waits: true,
|
|
87
|
-
trusted_input: true,
|
|
88
|
-
input_modes: ["auto", "trusted", "dom"],
|
|
89
|
-
complex_form_fill: true,
|
|
90
|
-
tab_management: true,
|
|
91
|
-
explicit_waits: true,
|
|
92
|
-
screenshots: true,
|
|
93
|
-
restricted_pages: ["browser-internal pages", "extension stores", "some PDF/plugin viewers", "pages blocked by enterprise policy"],
|
|
94
|
-
security: {
|
|
95
|
-
loopback_only: true,
|
|
96
|
-
bearer_pairing_token: true,
|
|
97
|
-
arbitrary_extension_code_from_mcp: false,
|
|
98
|
-
resource_values_returned_to_model: false,
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
}
|
|
69
|
+
status(context = {}) { return this.operationService.status(context); }
|
|
102
70
|
|
|
103
|
-
|
|
104
|
-
this.authorizeTool("pair_browser_extension");
|
|
105
|
-
const status = await this.status(context);
|
|
106
|
-
if (args.open !== false) {
|
|
107
|
-
const command = process.platform === "darwin"
|
|
108
|
-
? { cmd: "open", argv: [status.pairing_url] }
|
|
109
|
-
: process.platform === "win32"
|
|
110
|
-
? { cmd: "cmd.exe", argv: ["/d", "/s", "/c", "start", "", status.pairing_url] }
|
|
111
|
-
: { cmd: "xdg-open", argv: [status.pairing_url] };
|
|
112
|
-
await this.runProcess(command.cmd, command.argv, 30_000, false, 128 * 1024, context);
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
...status,
|
|
116
|
-
opened_pairing_page: args.open !== false,
|
|
117
|
-
setup_steps: [
|
|
118
|
-
"Open the browser extensions page and enable developer mode.",
|
|
119
|
-
"Load the unpacked extension from extension_path once.",
|
|
120
|
-
"Open pairing_url; the installed extension stores the loopback endpoint and token locally.",
|
|
121
|
-
"After upgrades, reload the unpacked extension and accept any newly requested browser permission.",
|
|
122
|
-
],
|
|
123
|
-
};
|
|
124
|
-
}
|
|
71
|
+
pair(args = {}, context = {}) { return this.operationService.pair(args, context); }
|
|
125
72
|
|
|
126
|
-
|
|
127
|
-
this.authorizeTool("browser_list_tabs");
|
|
128
|
-
const response = await this.request("list_tabs", {
|
|
129
|
-
currentWindow: args.current_window === true,
|
|
130
|
-
includePinned: args.include_pinned !== false,
|
|
131
|
-
}, clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
132
|
-
return response;
|
|
133
|
-
}
|
|
73
|
+
listTabs(args = {}, context = {}) { return this.operationService.listTabs(args, context); }
|
|
134
74
|
|
|
135
|
-
|
|
136
|
-
this.authorizeTool("browser_manage_tabs");
|
|
137
|
-
return this.request("manage_tabs", normalizeTabCommand(args), clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
138
|
-
}
|
|
75
|
+
manageTabs(args = {}, context = {}) { return this.operationService.manageTabs(args, context); }
|
|
139
76
|
|
|
140
|
-
|
|
141
|
-
this.authorizeTool("browser_wait");
|
|
142
|
-
const params = normalizeBrowserWait(args);
|
|
143
|
-
const conditionTimeoutSeconds = clampInt(args.timeout_seconds, 30, 1, 120);
|
|
144
|
-
return this.request("wait", params, conditionTimeoutSeconds + 5, context);
|
|
145
|
-
}
|
|
77
|
+
wait(args = {}, context = {}) { return this.operationService.wait(args, context); }
|
|
146
78
|
|
|
147
|
-
|
|
148
|
-
this.authorizeTool("browser_get_source");
|
|
149
|
-
const response = await this.request("get_source", {
|
|
150
|
-
tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
|
|
151
|
-
frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
|
|
152
|
-
allFrames: args.all_frames === true,
|
|
153
|
-
maxBytes: clampInt(args.max_bytes, 1024 * 1024, 1, MAX_SOURCE_BYTES),
|
|
154
|
-
}, clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
155
|
-
return response;
|
|
156
|
-
}
|
|
79
|
+
getSource(args = {}, context = {}) { return this.operationService.getSource(args, context); }
|
|
157
80
|
|
|
158
|
-
|
|
159
|
-
this.authorizeTool("browser_inspect_page");
|
|
160
|
-
return this.request("inspect_page", {
|
|
161
|
-
tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
|
|
162
|
-
frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
|
|
163
|
-
allFrames: args.all_frames !== false,
|
|
164
|
-
maxElements: clampInt(args.max_elements, 300, 1, 1000),
|
|
165
|
-
includeValues: args.include_values === true,
|
|
166
|
-
}, clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
167
|
-
}
|
|
81
|
+
inspectPage(args = {}, context = {}) { return this.operationService.inspectPage(args, context); }
|
|
168
82
|
|
|
169
|
-
|
|
170
|
-
this.authorizeTool("browser_action");
|
|
171
|
-
const action = normalizeBrowserAction(args.action);
|
|
172
|
-
const rawUrl = optionalString(args.url, "url", 32768);
|
|
173
|
-
const payload = {
|
|
174
|
-
tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
|
|
175
|
-
frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
|
|
176
|
-
action,
|
|
177
|
-
selector: normalizeBrowserSelector(args.selector, action),
|
|
178
|
-
url: action === "navigate" ? validateNavigationUrl(rawUrl) : "",
|
|
179
|
-
value: null,
|
|
180
|
-
key: optionalString(args.key, "key", 100),
|
|
181
|
-
waitFor: normalizeNavigationWait(args.wait_for),
|
|
182
|
-
inputMode: normalizeInputMode(args.input_mode),
|
|
183
|
-
elementTimeoutMs: clampInt(args.element_timeout_seconds, 10, 1, 60) * 1000,
|
|
184
|
-
};
|
|
185
|
-
if (action !== "navigate" && rawUrl) throw new Error("url is only valid for navigate");
|
|
186
|
-
if (action !== "press" && payload.key) throw new Error("key is only valid for press");
|
|
187
|
-
if (payload.inputMode === "trusted" && !["click", "double_click", "hover", "press", "type_text"].includes(action)) {
|
|
188
|
-
throw new Error("input_mode=trusted supports click, double_click, hover, press, and type_text only");
|
|
189
|
-
}
|
|
190
|
-
if (args.value !== undefined) payload.value = boundedValue(args.value, "value");
|
|
191
|
-
if (args.value_resource !== undefined) {
|
|
192
|
-
if (payload.value !== null) throw new Error("value and value_resource are mutually exclusive");
|
|
193
|
-
payload.value = boundedValue(await this.readResourceText(validateResource(args.value_resource)), "value_resource");
|
|
194
|
-
}
|
|
195
|
-
if (payload.value !== null && !["fill", "select", "press", "type_text"].includes(action)) throw new Error(`value is not valid for browser action '${action}'`);
|
|
196
|
-
const pageAction = !["navigate", "reload", "back", "forward"].includes(action);
|
|
197
|
-
const defaultTimeout = pageAction ? Math.min(120, Math.max(30, payload.elementTimeoutMs / 1000 + 5)) : 30;
|
|
198
|
-
const response = await this.request("action", payload, clampInt(args.timeout_seconds, defaultTimeout, 1, 120), context);
|
|
199
|
-
return {
|
|
200
|
-
...response,
|
|
201
|
-
value_source: args.value_resource !== undefined ? "local-resource" : payload.value === null ? "none" : "mcp-argument",
|
|
202
|
-
value_exposed: false,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
83
|
+
act(args = {}, context = {}) { return this.operationService.act(args, context); }
|
|
205
84
|
|
|
206
|
-
|
|
207
|
-
this.authorizeTool("browser_fill_form");
|
|
208
|
-
if (!Array.isArray(args.fields) || !args.fields.length) throw new Error("fields must be a non-empty array");
|
|
209
|
-
if (args.fields.length > MAX_FORM_FIELDS) throw new Error(`fields contains more than ${MAX_FORM_FIELDS} entries`);
|
|
210
|
-
const fields = [];
|
|
211
|
-
let totalValueBytes = 0;
|
|
212
|
-
for (let index = 0; index < args.fields.length; index += 1) {
|
|
213
|
-
const input = args.fields[index];
|
|
214
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error(`fields[${index}] must be an object`);
|
|
215
|
-
const allowed = new Set(["selector", "value", "value_resource", "action", "sensitive"]);
|
|
216
|
-
for (const key of Object.keys(input)) if (!allowed.has(key)) throw new Error(`unknown fields[${index}] property: ${key}`);
|
|
217
|
-
let value = input.value === undefined ? null : boundedValue(input.value, `fields[${index}].value`);
|
|
218
|
-
if (input.value_resource !== undefined) {
|
|
219
|
-
if (value !== null) throw new Error(`fields[${index}] value and value_resource are mutually exclusive`);
|
|
220
|
-
value = boundedValue(await this.readResourceText(validateResource(input.value_resource)), `fields[${index}].value_resource`);
|
|
221
|
-
}
|
|
222
|
-
if (value !== null) {
|
|
223
|
-
totalValueBytes += Buffer.byteLength(value);
|
|
224
|
-
if (totalValueBytes > MAX_FORM_VALUE_BYTES) throw new Error("form field values exceed 4 MiB total");
|
|
225
|
-
}
|
|
226
|
-
const action = input.action === undefined ? "fill" : normalizeFormAction(input.action);
|
|
227
|
-
if (value === null && !["check", "uncheck", "click"].includes(action)) throw new Error(`fields[${index}] requires value or value_resource`);
|
|
228
|
-
if (value !== null && !["fill", "select"].includes(action)) throw new Error(`fields[${index}] value is not valid for action '${action}'`);
|
|
229
|
-
fields.push({
|
|
230
|
-
selector: normalizeBrowserSelector(input.selector, action),
|
|
231
|
-
value,
|
|
232
|
-
action,
|
|
233
|
-
sensitive: input.sensitive === true || input.value_resource !== undefined,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
const elementTimeoutSeconds = clampInt(args.element_timeout_seconds, 10, 1, 60);
|
|
237
|
-
return this.request("fill_form", {
|
|
238
|
-
tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
|
|
239
|
-
frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
|
|
240
|
-
fields,
|
|
241
|
-
submit: args.submit === true,
|
|
242
|
-
submitSelector: args.submit_selector ? normalizeBrowserSelector(args.submit_selector, "click") : null,
|
|
243
|
-
waitFor: normalizeNavigationWait(args.wait_for),
|
|
244
|
-
elementTimeoutMs: elementTimeoutSeconds * 1000,
|
|
245
|
-
}, clampInt(args.timeout_seconds, Math.max(60, elementTimeoutSeconds + 5), 1, 180), context);
|
|
246
|
-
}
|
|
85
|
+
fillForm(args = {}, context = {}) { return this.operationService.fillForm(args, context); }
|
|
247
86
|
|
|
248
|
-
|
|
249
|
-
this.authorizeTool("browser_upload_files");
|
|
250
|
-
if (!Array.isArray(args.resources) || !args.resources.length || args.resources.length > 8) throw new Error("resources must contain 1 to 8 registered resource names");
|
|
251
|
-
const filenames = optionalStringArray(args.filenames, "filenames", 8, 255);
|
|
252
|
-
const mimeTypes = optionalStringArray(args.mime_types, "mime_types", 8, 200);
|
|
253
|
-
if (filenames.length > args.resources.length || mimeTypes.length > args.resources.length) throw new Error("filenames and mime_types cannot contain more entries than resources");
|
|
254
|
-
const files = [];
|
|
255
|
-
let total = 0;
|
|
256
|
-
for (const raw of args.resources) {
|
|
257
|
-
const name = validateResource(raw);
|
|
258
|
-
const resource = this.readResourceBinary(name);
|
|
259
|
-
total += resource.buffer.length;
|
|
260
|
-
if (total > 5 * 1024 * 1024) throw new Error("browser upload resources exceed 5 MiB total");
|
|
261
|
-
const suppliedFilename = filenames[files.length];
|
|
262
|
-
const derivedFilename = resource.path.split(/[\\/]/).pop() || name;
|
|
263
|
-
files.push({
|
|
264
|
-
filename: normalizeUploadFilename(suppliedFilename || derivedFilename, { derived: !suppliedFilename }),
|
|
265
|
-
mime: normalizeMimeType(mimeTypes[files.length] || "application/octet-stream"),
|
|
266
|
-
data: resource.buffer.toString("base64"),
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
const elementTimeoutSeconds = clampInt(args.element_timeout_seconds, 10, 1, 60);
|
|
270
|
-
const result = await this.request("upload_files", {
|
|
271
|
-
tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
|
|
272
|
-
frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
|
|
273
|
-
selector: normalizeBrowserSelector(args.selector, "fill"),
|
|
274
|
-
files,
|
|
275
|
-
elementTimeoutMs: elementTimeoutSeconds * 1000,
|
|
276
|
-
}, clampInt(args.timeout_seconds, Math.max(60, elementTimeoutSeconds + 5), 1, 180), context);
|
|
277
|
-
return { ...result, resource_names: args.resources.map(String), resource_contents_exposed: false };
|
|
278
|
-
}
|
|
87
|
+
uploadFiles(args = {}, context = {}) { return this.operationService.uploadFiles(args, context); }
|
|
279
88
|
|
|
280
|
-
|
|
281
|
-
this.authorizeTool("browser_screenshot");
|
|
282
|
-
const result = await this.request("screenshot", {
|
|
283
|
-
tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
|
|
284
|
-
format: args.format === "jpeg" ? "jpeg" : "png",
|
|
285
|
-
quality: clampInt(args.quality, 90, 1, 100),
|
|
286
|
-
}, clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
287
|
-
const data = String(result.data || "");
|
|
288
|
-
const match = /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/=]+)$/.exec(data);
|
|
289
|
-
if (!match) throw new Error("browser extension returned an invalid screenshot");
|
|
290
|
-
return {
|
|
291
|
-
$mcp: {
|
|
292
|
-
content: [{ type: "image", data: match[2], mimeType: match[1] }],
|
|
293
|
-
structuredContent: {
|
|
294
|
-
tab_id: result.tab_id,
|
|
295
|
-
url: result.url,
|
|
296
|
-
title: result.title,
|
|
297
|
-
mime_type: match[1],
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
};
|
|
301
|
-
}
|
|
89
|
+
screenshot(args = {}, context = {}) { return this.operationService.screenshot(args, context); }
|
|
302
90
|
|
|
303
91
|
async request(method, params, timeoutSeconds, context = {}) {
|
|
304
92
|
await this.ensureStarted(context);
|
|
@@ -783,42 +571,3 @@ export class BrowserBridgeManager {
|
|
|
783
571
|
}
|
|
784
572
|
|
|
785
573
|
}
|
|
786
|
-
|
|
787
|
-
function normalizeUploadFilename(value, { derived = false } = {}) {
|
|
788
|
-
let name = String(value || "");
|
|
789
|
-
if (derived) name = name.replace(/[\u0000-\u001f\u007f/\\]+/g, "_").trim();
|
|
790
|
-
if (!name || name === "." || name === ".." || name.length > 255 || /[\u0000-\u001f\u007f/\\]/.test(name)) {
|
|
791
|
-
if (derived) return "upload.bin";
|
|
792
|
-
throw new Error("filenames entries must be safe single-component filenames of at most 255 characters");
|
|
793
|
-
}
|
|
794
|
-
return name;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function normalizeMimeType(value) {
|
|
798
|
-
const mime = String(value || "").trim().toLowerCase();
|
|
799
|
-
if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mime) || mime.length > 200) {
|
|
800
|
-
throw new Error("mime_types entries must be valid media types");
|
|
801
|
-
}
|
|
802
|
-
return mime;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function optionalStringArray(value, label, maxItems, maxLength) {
|
|
806
|
-
if (value === undefined || value === null) return [];
|
|
807
|
-
if (!Array.isArray(value) || value.length > maxItems) throw new Error(`${label} must be an array with at most ${maxItems} entries`);
|
|
808
|
-
return value.map((item, index) => {
|
|
809
|
-
if (typeof item !== "string" || item.includes("\0") || !item.length || item.length > maxLength) throw new Error(`${label}[${index}] must be a non-empty string of at most ${maxLength} characters`);
|
|
810
|
-
return item;
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function boundedValue(value, label) {
|
|
815
|
-
const string = String(value);
|
|
816
|
-
if (string.includes("\0") || string.length > MAX_FIELD_VALUE_CHARS) throw new Error(`${label} exceeds the maximum length or contains a NUL byte`);
|
|
817
|
-
return string;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
function validateResource(value) {
|
|
821
|
-
const name = String(value || "").trim();
|
|
822
|
-
if (!RESOURCE_NAME.test(name)) throw new Error("value_resource is invalid");
|
|
823
|
-
return name;
|
|
824
|
-
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
import { readFileSync } from "node:fs";
|
|
2
4
|
import { resolve } from "node:path";
|
|
3
5
|
import { packageRoot } from "./state.mjs";
|
|
@@ -9,6 +11,10 @@ const REQUIRED_EXTENSION_CAPABILITIES = Object.freeze([
|
|
|
9
11
|
]);
|
|
10
12
|
export const MAX_BROWSER_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
11
13
|
|
|
14
|
+
/** @typedef {{protocol: number, version: string, capabilities: string[]}} ExtensionInfo */
|
|
15
|
+
/** @typedef {{readyState: number, close: (code?: number, reason?: string) => unknown, send: (value: string) => unknown}} ProtocolSocket */
|
|
16
|
+
|
|
17
|
+
/** @param {unknown} value @returns {ExtensionInfo | null} */
|
|
12
18
|
export function normalizeCompatibleExtensionInfo(value) {
|
|
13
19
|
const info = normalizeExtensionInfo(value);
|
|
14
20
|
if (!info || info.protocol !== BROWSER_EXTENSION_PROTOCOL || info.version !== EXPECTED_EXTENSION_VERSION) return null;
|
|
@@ -16,6 +22,7 @@ export function normalizeCompatibleExtensionInfo(value) {
|
|
|
16
22
|
return info;
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
/** @param {Record<string, unknown>} message @returns {ExtensionInfo} */
|
|
19
26
|
export function parseExtensionHello(message) {
|
|
20
27
|
if (message.role !== "extension" || message.protocol !== BROWSER_EXTENSION_PROTOCOL) {
|
|
21
28
|
throw new Error(`extension protocol mismatch; expected ${BROWSER_EXTENSION_PROTOCOL}; reload the extension`);
|
|
@@ -30,6 +37,10 @@ export function parseExtensionHello(message) {
|
|
|
30
37
|
return info;
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
/**
|
|
41
|
+
* @param {string | Buffer | Uint8Array} data
|
|
42
|
+
* @returns {{ok: true, message: Record<string, unknown>} | {ok: false, code: number, reason: string}}
|
|
43
|
+
*/
|
|
33
44
|
export function parseBrowserSocketMessage(data) {
|
|
34
45
|
if (Buffer.byteLength(data) > MAX_BROWSER_MESSAGE_BYTES) return { ok: false, code: 1009, reason: "message too large" };
|
|
35
46
|
let text;
|
|
@@ -44,10 +55,12 @@ export function parseBrowserSocketMessage(data) {
|
|
|
44
55
|
return { ok: true, message };
|
|
45
56
|
}
|
|
46
57
|
|
|
58
|
+
/** @param {ProtocolSocket} socket @param {number} code @param {string} reason */
|
|
47
59
|
export function closeProtocolSocket(socket, code, reason) {
|
|
48
60
|
try { socket.close(code, reason); } catch {}
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
/** @param {ProtocolSocket | null | undefined} socket @param {unknown} value */
|
|
51
64
|
export function safeSocketSend(socket, value) {
|
|
52
65
|
if (!socket || socket.readyState !== 1) return false;
|
|
53
66
|
try {
|
|
@@ -58,12 +71,14 @@ export function safeSocketSend(socket, value) {
|
|
|
58
71
|
}
|
|
59
72
|
}
|
|
60
73
|
|
|
74
|
+
/** @param {unknown} value @returns {ExtensionInfo | null} */
|
|
61
75
|
function normalizeExtensionInfo(value) {
|
|
62
76
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
77
|
+
const record = /** @type {Record<string, unknown>} */ (value);
|
|
78
|
+
const protocol = Number(record.protocol);
|
|
79
|
+
const version = typeof record.version === "string" && record.version.length <= 100 ? record.version : "";
|
|
80
|
+
const capabilities = Array.isArray(record.capabilities)
|
|
81
|
+
? [...new Set(record.capabilities.filter((entry) => typeof entry === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(entry)))].slice(0, 32)
|
|
67
82
|
: [];
|
|
68
83
|
if (!Number.isInteger(protocol) || protocol < 1 || !version) return null;
|
|
69
84
|
return { protocol, version, capabilities };
|