@wenathlan/saddle 2.1.6 → 2.1.8
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/Dockerfile +170 -66
- package/LICENSE +0 -0
- package/README.md +73 -58
- package/alternatives.ts +0 -0
- package/biome.jsonc +134 -0
- package/boards.json +2 -2
- package/browser.ts +482 -0
- package/cli.ts +274 -0
- package/compute.ts +4 -4
- package/cores.json +2 -2
- package/distribution.ts +731 -0
- package/docker.config +37 -9
- package/docs/.gitkeep +0 -0
- package/docs/actionrecorder.md +0 -0
- package/docs/actionsincident.md +0 -0
- package/docs/alternatives.md +0 -0
- package/docs/architecture.md +0 -0
- package/docs/{architecture-1.8.19.md → architecture1819.md} +0 -0
- package/docs/{architecture-1.8.19.mmd → architecture1819.mmd} +0 -0
- package/docs/artifactavailability.md +2 -2
- package/docs/{artifactresearch-1.8.9.md → artifactresearch189.md} +0 -0
- package/docs/{batches-1816-1820.md → batches1816to1820.md} +0 -0
- package/docs/{brancharchive-2026-08-18.md → brancharchive20260818.md} +0 -0
- package/docs/branchaudit.md +0 -0
- package/docs/browser.md +18 -1
- package/docs/capabilityreport.md +0 -0
- package/docs/comparativeaudit.md +0 -0
- package/docs/consolidation.md +0 -0
- package/docs/{containerplatforms-1.8.17.md → containerplatforms1817.md} +0 -0
- package/docs/{containerplatforms-2.1.0.md → containerplatforms210.md} +1 -1
- package/docs/{documentmap-1.8.12.md → documentmap1812.md} +1 -1
- package/docs/{e2ugh-engine.md → e2ughengine.md} +0 -0
- package/docs/ecosystemplan.md +0 -0
- package/docs/enginearchitecture.md +0 -0
- package/docs/extension.md +13 -2
- package/docs/featureaudit.md +0 -0
- package/docs/{flatbuildresearch-1.8.11.md → flatbuildresearch1811.md} +0 -0
- package/docs/gapmatrix.md +0 -0
- package/docs/hardware.md +0 -0
- package/docs/libraryapi.md +1 -1
- package/docs/{licenseaudit-1.8.12.md → licenseaudit1812.md} +0 -0
- package/docs/logs/examplesession.json +54 -0
- package/docs/memoryengine.md +0 -0
- package/docs/{mobileconversionresearch-1.8.10.md → mobileconversionresearch1810.md} +0 -0
- package/docs/modes.md +0 -0
- package/docs/{nativeidentityresearch-1.8.11.md → nativeidentityresearch1811.md} +0 -0
- package/docs/{native-wrappers.md → nativewrappers.md} +0 -0
- package/docs/ninetymodalities.md +191 -0
- package/docs/optimization.md +0 -0
- package/docs/packageaudit185.md +0 -0
- package/docs/performance.md +0 -0
- package/docs/{planning.1.8.18.md → planning1818.md} +0 -0
- package/docs/{planning.1.8.19.md → planning1819.md} +0 -0
- package/docs/planningengine.md +186 -0
- package/docs/planningninety.md +391 -0
- package/docs/platformpipelineaudit.md +0 -0
- package/docs/platformpipelines.md +0 -0
- package/docs/productindex.md +0 -0
- package/docs/registryresearch.md +0 -0
- package/docs/release.md +0 -0
- package/docs/release17notes.md +0 -0
- package/docs/release181notes.md +0 -0
- package/docs/release182notes.md +0 -0
- package/docs/release184notes.md +0 -0
- package/docs/release185notes.md +0 -0
- package/docs/release18notes.md +0 -0
- package/docs/releaseassets.md +0 -0
- package/docs/{releasenotes-1.8.12.md → releasenotes1812.md} +0 -0
- package/docs/{releasenotes-1.8.13.md → releasenotes1813.md} +2 -2
- package/docs/{releasenotes-1.8.14.md → releasenotes1814.md} +1 -1
- package/docs/{releasenotes-1.8.15.md → releasenotes1815.md} +0 -0
- package/docs/{releasenotes-1.8.16.md → releasenotes1816.md} +0 -0
- package/docs/{releasenotes-1.8.17.md → releasenotes1817.md} +0 -0
- package/docs/{releasenotes-1.8.18.md → releasenotes1818.md} +0 -0
- package/docs/{releasenotes-1.8.19.md → releasenotes1819.md} +0 -0
- package/docs/{reorganization-1.8.8.md → reorganization188.md} +0 -0
- package/docs/{reorganization-1.8.9.md → reorganization189.md} +0 -0
- package/docs/{repo-analysis-1.8.21.json → repoanalysis1821.json} +0 -0
- package/docs/{repo-candidates-1.8.21.jsonl → repocandidates1821.jsonl} +0 -0
- package/docs/{repo-research-1.8.16.md → reporesearch1816.md} +0 -0
- package/docs/{repo-research-1.8.21.md → reporesearch1821.md} +0 -0
- package/docs/{repo-selected-1.8.21.json → reposelected1821.json} +0 -0
- package/docs/{repo-synthesis-1.8.16.md → reposynthesis1816.md} +1 -1
- package/docs/{repo-synthesis-1.8.21.md → reposynthesis1821.md} +0 -0
- package/docs/{research-1.8.18-isolation.md → research1818isolation.md} +0 -0
- package/docs/{research-1.8.19-virtual-browser.md → research1819virtualbrowser.md} +0 -0
- package/docs/roadmapp2p3.md +0 -0
- package/docs/s3compatible.md +0 -0
- package/docs/{scanningresearch-1.8.11.md → scanningresearch1811.md} +0 -0
- package/docs/security.md +0 -0
- package/docs/{securityaudit-1.8.7.md → securityaudit187.md} +0 -0
- package/docs/{securityreferences-1.8.11.md → securityreferences1811.md} +0 -0
- package/docs/{securityresearch-1.8.11.md → securityresearch1811.md} +0 -0
- package/docs/{signingresearch-1.8.12.md → signingresearch1812.md} +0 -0
- package/docs/signingwithoutcertificates.md +0 -0
- package/docs/signpathapplication.md +0 -0
- package/docs/sitemap.md +0 -0
- package/docs/{tauri-glib-risk-1.8.11.md → tauriglibrisk1811.md} +0 -0
- package/docs/todo.md +2 -2
- package/docs/{todo-1.8.15.md → todo1815.md} +1 -1
- package/docs/{todo-1.8.16.md → todo1816.md} +5 -5
- package/docs/{todo-1.8.18.md → todo1818.md} +0 -0
- package/docs/{todo-1.8.19.md → todo1819.md} +0 -0
- package/docs/{toolchainresearch-1.8.9.md → toolchainresearch189.md} +0 -0
- package/docs/toolchains.md +0 -0
- package/docs/usage.md +0 -0
- package/docs/viability.md +0 -0
- package/docs/virtualization.md +0 -0
- package/docs/{web-duplicatecleanup.md → webduplicatecleanup.md} +0 -0
- package/docs/{web-ideas.md → webideas.md} +0 -0
- package/docs/{workflowimprovements-2026-08-19.md → workflowimprovements20260819.md} +0 -0
- package/docs/workflowinputs.md +0 -0
- package/docs/workflowoperations.md +0 -0
- package/docs/workloadlaw.md +104 -0
- package/execution.ts +677 -0
- package/foundation.ts +314 -0
- package/gpumonitor.cpp +0 -0
- package/gpus.json +1 -1
- package/index.ts +38 -29
- package/isolation.ts +84 -0
- package/modalityregistry.ts +1493 -0
- package/{dist/modes.js → modes.ts} +79 -62
- package/mttg.config +1 -1
- package/orchestrator.ts +54 -17
- package/package.json +245 -146
- package/passage.config +0 -0
- package/performance.ts +0 -0
- package/processors.json +1 -1
- package/qemu.config +2 -2
- package/qemubridge.py +0 -0
- package/quantum.ts +0 -0
- package/render.ts +0 -0
- package/sandbox.ts +2035 -0
- package/scheduler.ts +1 -1
- package/security.ts +0 -0
- package/server.ts +52 -0
- package/tiers.ts +9 -3
- package/tsconfig.json +22 -13
- package/virtual.ts +1105 -0
- package/virtualcpu.ts +1 -0
- package/virtualgpu.ts +2 -1
- package/virtualhardware.c +0 -0
- package/virtualhardware.json +2 -2
- package/virtualization.ts +1 -1
- package/virtualizationcore.cpp +0 -0
- package/virtualmemory.ts +0 -0
- package/vm.config.json +2 -2
- package/workloadlaw.ts +293 -0
- package/biome.json +0 -151
- package/dist/acquisition.d.ts +0 -359
- package/dist/acquisition.d.ts.map +0 -1
- package/dist/acquisition.js +0 -629
- package/dist/acquisition.js.map +0 -1
- package/dist/alternatives.d.ts +0 -509
- package/dist/alternatives.d.ts.map +0 -1
- package/dist/alternatives.js +0 -951
- package/dist/alternatives.js.map +0 -1
- package/dist/automation.d.ts +0 -426
- package/dist/automation.d.ts.map +0 -1
- package/dist/automation.js +0 -561
- package/dist/automation.js.map +0 -1
- package/dist/browser.d.ts +0 -562
- package/dist/browser.d.ts.map +0 -1
- package/dist/browser.js +0 -1057
- package/dist/browser.js.map +0 -1
- package/dist/cli.d.ts +0 -17
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -169
- package/dist/cli.js.map +0 -1
- package/dist/communication.d.ts +0 -192
- package/dist/communication.d.ts.map +0 -1
- package/dist/communication.js +0 -418
- package/dist/communication.js.map +0 -1
- package/dist/compute.d.ts +0 -1161
- package/dist/compute.d.ts.map +0 -1
- package/dist/compute.js +0 -2273
- package/dist/compute.js.map +0 -1
- package/dist/distribution.d.ts +0 -529
- package/dist/distribution.d.ts.map +0 -1
- package/dist/distribution.js +0 -811
- package/dist/distribution.js.map +0 -1
- package/dist/docs/example-localjob.d.ts +0 -2
- package/dist/docs/example-localjob.d.ts.map +0 -1
- package/dist/docs/example-localjob.js +0 -15
- package/dist/docs/example-localjob.js.map +0 -1
- package/dist/docs/example-publicapi.d.ts +0 -2
- package/dist/docs/example-publicapi.d.ts.map +0 -1
- package/dist/docs/example-publicapi.js +0 -7
- package/dist/docs/example-publicapi.js.map +0 -1
- package/dist/execution.d.ts +0 -530
- package/dist/execution.d.ts.map +0 -1
- package/dist/execution.js +0 -761
- package/dist/execution.js.map +0 -1
- package/dist/format.d.ts +0 -19
- package/dist/format.d.ts.map +0 -1
- package/dist/format.js +0 -60
- package/dist/format.js.map +0 -1
- package/dist/foundation.d.ts +0 -176
- package/dist/foundation.d.ts.map +0 -1
- package/dist/foundation.js +0 -309
- package/dist/foundation.js.map +0 -1
- package/dist/index.d.ts +0 -735
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -1020
- package/dist/index.js.map +0 -1
- package/dist/integration.d.ts +0 -236
- package/dist/integration.d.ts.map +0 -1
- package/dist/integration.js +0 -236
- package/dist/integration.js.map +0 -1
- package/dist/intelligence.d.ts +0 -94
- package/dist/intelligence.d.ts.map +0 -1
- package/dist/intelligence.js +0 -145
- package/dist/intelligence.js.map +0 -1
- package/dist/isolation.d.ts +0 -129
- package/dist/isolation.d.ts.map +0 -1
- package/dist/isolation.js +0 -96
- package/dist/isolation.js.map +0 -1
- package/dist/media.d.ts +0 -344
- package/dist/media.d.ts.map +0 -1
- package/dist/media.js +0 -986
- package/dist/media.js.map +0 -1
- package/dist/modes.d.ts +0 -359
- package/dist/modes.d.ts.map +0 -1
- package/dist/modes.js.map +0 -1
- package/dist/operations.d.ts +0 -67
- package/dist/operations.d.ts.map +0 -1
- package/dist/operations.js +0 -94
- package/dist/operations.js.map +0 -1
- package/dist/orchestrator.d.ts +0 -1339
- package/dist/orchestrator.d.ts.map +0 -1
- package/dist/orchestrator.js +0 -4308
- package/dist/orchestrator.js.map +0 -1
- package/dist/packager/targetcli.js +0 -4
- package/dist/performance.d.ts +0 -416
- package/dist/performance.d.ts.map +0 -1
- package/dist/performance.js +0 -827
- package/dist/performance.js.map +0 -1
- package/dist/quantum.d.ts +0 -810
- package/dist/quantum.d.ts.map +0 -1
- package/dist/quantum.js +0 -2073
- package/dist/quantum.js.map +0 -1
- package/dist/render.d.ts +0 -381
- package/dist/render.d.ts.map +0 -1
- package/dist/render.js +0 -1073
- package/dist/render.js.map +0 -1
- package/dist/scheduler.d.ts +0 -596
- package/dist/scheduler.d.ts.map +0 -1
- package/dist/scheduler.js +0 -1259
- package/dist/scheduler.js.map +0 -1
- package/dist/security.d.ts +0 -501
- package/dist/security.d.ts.map +0 -1
- package/dist/security.js +0 -1170
- package/dist/security.js.map +0 -1
- package/dist/server.d.ts +0 -21
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -52
- package/dist/server.js.map +0 -1
- package/dist/tiers.d.ts +0 -1002
- package/dist/tiers.d.ts.map +0 -1
- package/dist/tiers.js +0 -2165
- package/dist/tiers.js.map +0 -1
- package/dist/virtual.d.ts +0 -641
- package/dist/virtual.d.ts.map +0 -1
- package/dist/virtual.js +0 -1285
- package/dist/virtual.js.map +0 -1
- package/dist/virtualcpu.d.ts +0 -316
- package/dist/virtualcpu.d.ts.map +0 -1
- package/dist/virtualcpu.js +0 -1180
- package/dist/virtualcpu.js.map +0 -1
- package/dist/virtualgpu.d.ts +0 -345
- package/dist/virtualgpu.d.ts.map +0 -1
- package/dist/virtualgpu.js +0 -1000
- package/dist/virtualgpu.js.map +0 -1
- package/dist/virtualization.d.ts +0 -607
- package/dist/virtualization.d.ts.map +0 -1
- package/dist/virtualization.js +0 -1187
- package/dist/virtualization.js.map +0 -1
- package/dist/virtualmemory.d.ts +0 -818
- package/dist/virtualmemory.d.ts.map +0 -1
- package/dist/virtualmemory.js +0 -1796
- package/dist/virtualmemory.js.map +0 -1
- package/dist/vitest.config.d.ts +0 -3
- package/dist/vitest.config.d.ts.map +0 -1
- package/dist/vitest.config.js +0 -26
- package/dist/vitest.config.js.map +0 -1
- package/dist/webscrape.d.ts +0 -720
- package/dist/webscrape.d.ts.map +0 -1
- package/dist/webscrape.js +0 -2697
- package/dist/webscrape.js.map +0 -1
- package/docs/CONVERSA.txt +0 -1523
- package/docs/architecture-1.8.19.png +0 -0
- package/docs/assets/architecture.svg +0 -45
- package/docs/assets/saddlemark.svg +0 -13
- package/docs/example-localjob.ts +0 -15
- package/docs/example-publicapi.ts +0 -7
- package/docs/saddle.archive.1.8.17.tar.gz.gpg +0 -0
- package/media.ts +0 -1337
- package/web/manifest.json +0 -30
- package/web/readme.md +0 -383
- package/web/tsconfig.json +0 -25
package/browser.ts
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser.ts — the transport-neutral virtual-browser contracts of the
|
|
3
|
+
* saddle engine.
|
|
4
|
+
*
|
|
5
|
+
* This file is the v2.0.0 grand-merge consolidation of the former nested
|
|
6
|
+
* folders of the repository (browser, extension) into the single
|
|
7
|
+
* root-level domain file of the saddle family standard: one optimized
|
|
8
|
+
* TypeScript file per correlated domain, ordinal sections, JSDoc on every
|
|
9
|
+
* block, intra-domain imports dissolved, cross-domain imports rewritten to
|
|
10
|
+
* ./<domain>.js. Every capability of every source file is preserved here;
|
|
11
|
+
* the pure re-export barrels (browser/index.ts, extension/index.ts) folded their surface into this file directly.
|
|
12
|
+
*
|
|
13
|
+
* WAVE 2 REGROUPING (the logic-regrouping PR): the Chrome Manifest V3
|
|
14
|
+
* extension surface — the former sections 10-17 (extension/permissions,
|
|
15
|
+
* extension/protocol, extension/content, extension/popup,
|
|
16
|
+
* extension/pagebridge, extension/worker, extension/serviceworker,
|
|
17
|
+
* extension/build) — migrated to devthink/browserextension.ts. The family
|
|
18
|
+
* rule that drove the move: the saddle is EXCLUSIVELY the virtualization
|
|
19
|
+
* engine (VM / sandbox / microVM / the ninety modalities / vGPU / vCPU)
|
|
20
|
+
* and never touches the browser, GPU, CPU or storage of the user — every
|
|
21
|
+
* feature that runs in the user's browser is not a VM modality and
|
|
22
|
+
* belongs to devthink, the owner of the family extension product. What
|
|
23
|
+
* stays here are the engine-side, transport-neutral contracts of the
|
|
24
|
+
* virtual browser (sections 1-9); every extension export keeps its name
|
|
25
|
+
* and signature at the new owner, so nothing is lost and no consumer of
|
|
26
|
+
* this file breaks.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { executiondecision, executionrequest } from "./isolation.ts";
|
|
30
|
+
|
|
31
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
32
|
+
/* Section 1: browser/fingerprint.ts — fingerprint profiles keep session settings coherent without modifying browser internals. */
|
|
33
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
34
|
+
/**
|
|
35
|
+
* fingerprint profiles keep session settings coherent without modifying browser internals.
|
|
36
|
+
*/
|
|
37
|
+
const profiles = Object.freeze({
|
|
38
|
+
desktopwindows: { os: "windows", platform: "Win32", browser: "chrome", locale: "en-US", timezone: "America/New_York", touch: false, devicepixelratio: 1 },
|
|
39
|
+
desktopmacos: { os: "macos", platform: "MacIntel", browser: "safari", locale: "en-US", timezone: "America/Los_Angeles", touch: false, devicepixelratio: 2 },
|
|
40
|
+
mobileandroid: { os: "android", platform: "Linux armv8l", browser: "chrome", locale: "en-US", timezone: "America/Chicago", touch: true, devicepixelratio: 2.75 }
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export function fingerprintprofile(name = "desktopwindows", overrides = {}) { return { ...(profiles[name] ?? profiles.desktopwindows), ...overrides, name }; }
|
|
44
|
+
export function fingerprintvalidate(profile) { return Boolean(profile?.os && profile?.platform && profile?.browser && profile?.locale && profile?.timezone && typeof profile.touch === "boolean"); }
|
|
45
|
+
export function fingerprintfor(sessionid, options = {}) { const names = options.profiles ?? Object.keys(profiles); const index = [...String(sessionid)].reduce((sum, value) => sum + value.charCodeAt(0), 0) % names.length; return fingerprintprofile(names[index], options.overrides); }
|
|
46
|
+
|
|
47
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
48
|
+
/* Section 2: browser/session.ts — browser sessions bind one fingerprint to one proxy and one event recorder. */
|
|
49
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
50
|
+
/**
|
|
51
|
+
* browser sessions bind one fingerprint to one proxy and one event recorder.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
export function browsersession(options = {}) {
|
|
55
|
+
const id = options.id ?? `browsersession${Date.now().toString(36)}`;
|
|
56
|
+
const fingerprint = options.fingerprint ?? fingerprintfor(id, options);
|
|
57
|
+
if (!fingerprintvalidate(fingerprint)) throw new TypeError("browser fingerprint is incoherent");
|
|
58
|
+
const events = [];
|
|
59
|
+
return {
|
|
60
|
+
id,
|
|
61
|
+
fingerprint,
|
|
62
|
+
proxy: options.proxy,
|
|
63
|
+
record(event) { if (!event || !Number.isFinite(event.t) || event.t < 0 || typeof event.type !== "string") throw new TypeError("browser event is invalid"); events.push({ ...event }); return event; },
|
|
64
|
+
events() { return events.map((event) => ({ ...event })); },
|
|
65
|
+
manifest() { return { id, fingerprint: { ...fingerprint }, proxy: options.proxy?.id ?? options.proxy, eventcount: events.length }; }
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
70
|
+
/* Section 3: browser/actions.ts — browser actions normalize adapter outcomes and preserve failure metadata for agents and replay. */
|
|
71
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
72
|
+
/**
|
|
73
|
+
* browser actions normalize adapter outcomes and preserve failure metadata for agents and replay.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
export const browseractions = Object.freeze(["navigate", "click", "type", "fill", "key", "scroll", "upload", "screenshot", "snapshot"]);
|
|
77
|
+
|
|
78
|
+
/** Creates a stable action result independent of the underlying browser vendor. */
|
|
79
|
+
export function actionresult(action, options = {}) {
|
|
80
|
+
if (!browseractions.includes(action)) throw new TypeError(`unsupported browser action: ${action}`);
|
|
81
|
+
return { version: 1, action, ok: true, startedat: Number(options.startedat ?? Date.now()), finishedat: Number(options.finishedat ?? Date.now()), tabid: options.tabid === undefined ? undefined : String(options.tabid), frameid: options.frameid === undefined ? undefined : String(options.frameid), snapshotid: options.snapshotid, value: options.value, metadata: options.metadata ?? {} };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Creates a stable action failure without leaking adapter internals or credentials. */
|
|
85
|
+
export function actionfailure(action, error, options = {}) {
|
|
86
|
+
if (!action || typeof action !== "string") throw new TypeError("browser action failure requires an action name");
|
|
87
|
+
return { version: 1, action, ok: false, code: String(options.code ?? error?.code ?? "BROWSER_ACTION_FAILED"), message: String(error?.message ?? error ?? "browser action failed"), retryable: Boolean(options.retryable ?? error?.retryable), tabid: options.tabid === undefined ? undefined : String(options.tabid), frameid: options.frameid === undefined ? undefined : String(options.frameid), snapshotid: options.snapshotid, metadata: options.metadata ?? {} };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Executes a bounded list of actions through an injected adapter and keeps per-action outcomes. */
|
|
91
|
+
export async function actionbatch(adapter, actions = [], options = {}) {
|
|
92
|
+
if (!adapter || typeof adapter !== "object") throw new TypeError("browser action batch requires an adapter");
|
|
93
|
+
if (!Array.isArray(actions) || actions.length > (options.maxactions ?? 100)) throw new TypeError("browser action batch is invalid or too large");
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const item of actions) {
|
|
96
|
+
const action = String(item?.action ?? "");
|
|
97
|
+
const method = adapter[action];
|
|
98
|
+
if (typeof method !== "function") { results.push(actionfailure(action, new Error(`browser adapter does not support ${action}`), { code: "UNSUPPORTED_ACTION" })); continue; }
|
|
99
|
+
const startedat = Date.now();
|
|
100
|
+
try { const value = await method(item.options ?? item.value); results.push(actionresult(action, { ...item, startedat, finishedat: Date.now(), value })); }
|
|
101
|
+
catch (error) { results.push(actionfailure(action, error, { ...item, tabid: item.tabid, frameid: item.frameid, snapshotid: item.snapshotid })); if (options.stoponerror) break; }
|
|
102
|
+
}
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
107
|
+
/* Section 4: browser/context.ts — browser context tracks tabs and frames without owning a browser implementation. */
|
|
108
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
109
|
+
/**
|
|
110
|
+
* browser context tracks tabs and frames without owning a browser implementation.
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
/** Creates a serializable browser context registry for an injected browser adapter. */
|
|
114
|
+
export function browsercontext(options = {}) {
|
|
115
|
+
const sessionid = String(options.sessionid ?? `context${Date.now().toString(36)}`);
|
|
116
|
+
const tabs = new Map();
|
|
117
|
+
let activeid = options.activeid === undefined ? undefined : String(options.activeid);
|
|
118
|
+
|
|
119
|
+
function opentab(input = {}) {
|
|
120
|
+
const id = String(input.id ?? `tab${tabs.size + 1}`);
|
|
121
|
+
const tab = { id, url: String(input.url ?? "about:blank"), title: String(input.title ?? ""), active: Boolean(input.active), frames: new Map() };
|
|
122
|
+
tabs.set(id, tab);
|
|
123
|
+
if (tab.active || activeid === undefined) { activeid = id; tab.active = true; }
|
|
124
|
+
return describetab(tab);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function closetab(id) {
|
|
128
|
+
const key = String(id);
|
|
129
|
+
if (!tabs.delete(key)) return false;
|
|
130
|
+
if (activeid === key) activeid = tabs.keys().next().value;
|
|
131
|
+
if (activeid && tabs.has(activeid)) tabs.get(activeid).active = true;
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function setactive(id) {
|
|
136
|
+
const key = String(id);
|
|
137
|
+
const tab = tabs.get(key);
|
|
138
|
+
if (!tab) throw new Error(`unknown browser tab: ${key}`);
|
|
139
|
+
for (const item of tabs.values()) item.active = false;
|
|
140
|
+
tab.active = true;
|
|
141
|
+
activeid = key;
|
|
142
|
+
return describetab(tab);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function openframe(tabid, input = {}) {
|
|
146
|
+
const tab = requiretab(tabid);
|
|
147
|
+
const id = String(input.id ?? `frame${tab.frames.size + 1}`);
|
|
148
|
+
tab.frames.set(id, { id, url: String(input.url ?? tab.url), parentid: input.parentid === undefined ? undefined : String(input.parentid), name: String(input.name ?? "") });
|
|
149
|
+
return { ...tab.frames.get(id) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function closeframe(tabid, frameid) { return Boolean(requiretab(tabid).frames.delete(String(frameid))); }
|
|
153
|
+
function activetab() { return activeid === undefined ? undefined : describetab(requiretab(activeid)); }
|
|
154
|
+
function describe() { return { sessionid, activeid, tabs: [...tabs.values()].map(describetab) }; }
|
|
155
|
+
|
|
156
|
+
return { sessionid, opentab, closetab, setactive, openframe, closeframe, activetab, describe };
|
|
157
|
+
|
|
158
|
+
function requiretab(id) { const tab = tabs.get(String(id)); if (!tab) throw new Error(`unknown browser tab: ${id}`); return tab; }
|
|
159
|
+
function describetab(tab) { return { id: tab.id, url: tab.url, title: tab.title, active: tab.active, frames: [...tab.frames.values()].map((frame) => ({ ...frame })) }; }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
163
|
+
/* Section 5: browser/snapshot.ts — browser snapshots turn page state into bounded serializable data with stable references. */
|
|
164
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
165
|
+
/**
|
|
166
|
+
* browser snapshots turn page state into bounded serializable data with stable references.
|
|
167
|
+
*/
|
|
168
|
+
|
|
169
|
+
/** Creates a validated page snapshot for browser, MCP and extension adapters. */
|
|
170
|
+
export function pagesnapshot(input = {}) {
|
|
171
|
+
if (!input || typeof input !== "object") throw new TypeError("page snapshot must be an object");
|
|
172
|
+
const snapshot = {
|
|
173
|
+
version: 1,
|
|
174
|
+
snapshotid: String(input.snapshotid ?? `snapshot${Date.now().toString(36)}`),
|
|
175
|
+
tabid: input.tabid === undefined ? undefined : String(input.tabid),
|
|
176
|
+
frameid: input.frameid === undefined ? undefined : String(input.frameid),
|
|
177
|
+
url: String(input.url ?? ""),
|
|
178
|
+
title: String(input.title ?? ""),
|
|
179
|
+
text: String(input.text ?? "").slice(0, input.maxtext ?? 100000),
|
|
180
|
+
elements: normalizeelements(input.elements)
|
|
181
|
+
};
|
|
182
|
+
if (!snapshot.snapshotid || !snapshot.url) throw new TypeError("page snapshot requires snapshotid and url");
|
|
183
|
+
return snapshot;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Creates an element reference bound to a snapshot and browser context. */
|
|
187
|
+
export function snapshotref(snapshot, input = {}) {
|
|
188
|
+
const current = pagesnapshot(snapshot);
|
|
189
|
+
const ref = String(input.ref ?? "");
|
|
190
|
+
if (!current.elements.some((element) => element.ref === ref)) throw new TypeError(`snapshot reference not found: ${ref}`);
|
|
191
|
+
return { version: 1, snapshotid: current.snapshotid, tabid: current.tabid, frameid: current.frameid, ref };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Throws a stable error when an action uses a reference from a previous page state. */
|
|
195
|
+
export function assertfreshsnapshot(snapshot, reference) {
|
|
196
|
+
const current = pagesnapshot(snapshot);
|
|
197
|
+
if (!reference || reference.snapshotid !== current.snapshotid || (reference.tabid !== undefined && reference.tabid !== current.tabid) || (reference.frameid !== undefined && reference.frameid !== current.frameid)) {
|
|
198
|
+
const error = new Error("browser snapshot is stale");
|
|
199
|
+
error.code = "STALE_SNAPSHOT";
|
|
200
|
+
error.retryable = true;
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Computes additions, removals and changed labels between two page snapshots (the page-side diff of the former browser/snapshot.ts; the extension-protocol diff of the same name lives in the migrated devthink/browserextension.ts). */
|
|
207
|
+
export function pagesnapshotdiff(previous, current) {
|
|
208
|
+
const before = pagesnapshot(previous);
|
|
209
|
+
const after = pagesnapshot(current);
|
|
210
|
+
const oldmap = new Map(before.elements.map((element) => [element.ref, element]));
|
|
211
|
+
const newmap = new Map(after.elements.map((element) => [element.ref, element]));
|
|
212
|
+
const added = after.elements.filter((element) => !oldmap.has(element.ref));
|
|
213
|
+
const removed = before.elements.filter((element) => !newmap.has(element.ref));
|
|
214
|
+
const changed = after.elements.filter((element) => oldmap.has(element.ref) && JSON.stringify(oldmap.get(element.ref)) !== JSON.stringify(element));
|
|
215
|
+
return { from: before.snapshotid, to: after.snapshotid, added, removed, changed };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Projects a page snapshot into a byte-bounded, allowlisted browser context. */
|
|
219
|
+
export function projectcontext(snapshot, options = {}) {
|
|
220
|
+
const current = pagesnapshot(snapshot);
|
|
221
|
+
const maxbytes = normalizebudget(options.maxbytes ?? 32768);
|
|
222
|
+
const allowed = normalizefields(options.fields ?? options.allowlist);
|
|
223
|
+
const context = { snapshotid: current.snapshotid };
|
|
224
|
+
const truncated = [];
|
|
225
|
+
for (const field of allowed) {
|
|
226
|
+
if (field === "snapshotid") continue;
|
|
227
|
+
const value = current[field];
|
|
228
|
+
if (value === undefined) continue;
|
|
229
|
+
if (field === "elements") {
|
|
230
|
+
const elements = [];
|
|
231
|
+
for (const element of value) {
|
|
232
|
+
if (fitscontext(context, field, [...elements, element], maxbytes)) elements.push(element);
|
|
233
|
+
else { truncated.push("elements"); break; }
|
|
234
|
+
}
|
|
235
|
+
if (fitscontext(context, field, elements, maxbytes)) context[field] = elements;
|
|
236
|
+
else truncated.push("elements");
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (fitscontext(context, field, value, maxbytes)) { context[field] = value; continue; }
|
|
240
|
+
if (typeof value === "string") context[field] = clipcontextstring(context, field, value, maxbytes);
|
|
241
|
+
else truncated.push(field);
|
|
242
|
+
if (typeof value === "string" && context[field].length < value.length) truncated.push(field);
|
|
243
|
+
}
|
|
244
|
+
const bytes = contextbytes(context);
|
|
245
|
+
if (bytes > maxbytes) throw new RangeError("browser context budget is smaller than stable snapshot identity");
|
|
246
|
+
return { version: 1, snapshotid: current.snapshotid, context, fields: Object.keys(context), bytes, maxbytes, truncated: [...new Set(truncated)] };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function normalizeelements(elements) {
|
|
250
|
+
if (!Array.isArray(elements)) return [];
|
|
251
|
+
return elements.slice(0, 500).map((element, index) => ({ ref: String(element?.ref ?? `e${index + 1}`), role: String(element?.role ?? "generic"), name: String(element?.name ?? "").trim().slice(0, 200), value: element?.value === undefined ? undefined : String(element.value).slice(0, 500), disabled: Boolean(element?.disabled) }));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const snapshotfields = Object.freeze(["snapshotid", "tabid", "frameid", "url", "title", "text", "elements"]);
|
|
255
|
+
|
|
256
|
+
function normalizefields(fields) {
|
|
257
|
+
if (fields === undefined) return [...snapshotfields];
|
|
258
|
+
if (!Array.isArray(fields) || fields.length === 0) throw new TypeError("browser context fields must be a non-empty array");
|
|
259
|
+
const normalized = [...new Set(fields.map((field) => String(field)))];
|
|
260
|
+
if (normalized.some((field) => !snapshotfields.includes(field))) throw new TypeError("browser context field is not allowlisted");
|
|
261
|
+
return normalized;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizebudget(value) { const normalized = Number(value); if (!Number.isSafeInteger(normalized) || normalized <= 0) throw new RangeError("browser context maxbytes must be a positive safe integer"); return normalized; }
|
|
265
|
+
|
|
266
|
+
function contextbytes(value) { return new TextEncoder().encode(JSON.stringify(value)).byteLength; }
|
|
267
|
+
|
|
268
|
+
function fitscontext(context, field, value, maxbytes) { return contextbytes({ ...context, [field]: value }) <= maxbytes; }
|
|
269
|
+
|
|
270
|
+
function clipcontextstring(context, field, value, maxbytes) {
|
|
271
|
+
let low = 0;
|
|
272
|
+
let high = value.length;
|
|
273
|
+
let result = "";
|
|
274
|
+
while (low <= high) {
|
|
275
|
+
const middle = Math.floor((low + high) / 2);
|
|
276
|
+
const candidate = value.slice(0, middle);
|
|
277
|
+
if (fitscontext(context, field, candidate, maxbytes)) { result = candidate; low = middle + 1; } else high = middle - 1;
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
283
|
+
/* Section 6: browser/recorder.ts — browser recorder captures action and snapshot boundaries for deterministic replay. */
|
|
284
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
285
|
+
/**
|
|
286
|
+
* browser recorder captures action and snapshot boundaries for deterministic replay.
|
|
287
|
+
* The optional event limit keeps long sessions bounded without owning persistence.
|
|
288
|
+
*/
|
|
289
|
+
|
|
290
|
+
/** Creates a recorder that links actions to snapshots and optionally bounds events. */
|
|
291
|
+
export function actionrecorder(options = {}) {
|
|
292
|
+
const startedat = Number(options.startedat ?? Date.now());
|
|
293
|
+
const maxevents = limit(options.maxevents);
|
|
294
|
+
const events = [];
|
|
295
|
+
let lastsnapshotid;
|
|
296
|
+
let dropped = 0;
|
|
297
|
+
|
|
298
|
+
function snapshot(value) {
|
|
299
|
+
lastsnapshotid = value?.snapshotid;
|
|
300
|
+
add({ type: "snapshot", t: Date.now() - startedat, snapshotid: lastsnapshotid, tabid: value?.tabid, frameid: value?.frameid, context: recordercontext(value) });
|
|
301
|
+
return value;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function action(input = {}) {
|
|
305
|
+
const context = recordercontext(input.context ?? input);
|
|
306
|
+
const event = { type: "action", t: Date.now() - startedat, action: String(input.action), snapshotid: input.snapshotid ?? lastsnapshotid, tabid: input.tabid, frameid: input.frameid, windowid: input.windowid, context, payload: clonevalue(input.payload ?? {}) };
|
|
307
|
+
add(event);
|
|
308
|
+
return clonevalue(event);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function list() { return events.map(clonevalue); }
|
|
312
|
+
function manifest() { return { version: 1, startedat, eventcount: events.length, dropped, lastsnapshotid, events: list() }; }
|
|
313
|
+
function exportjson() { return JSON.stringify(manifest()); }
|
|
314
|
+
function clear() { events.length = 0; dropped = 0; lastsnapshotid = undefined; }
|
|
315
|
+
|
|
316
|
+
return { snapshot, action, list, manifest, exportjson, clear };
|
|
317
|
+
|
|
318
|
+
function add(event) {
|
|
319
|
+
events.push(event);
|
|
320
|
+
while (events.length > maxevents) { events.shift(); dropped += 1; }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function limit(value) {
|
|
325
|
+
if (value === undefined) return Infinity;
|
|
326
|
+
const numeric = Number(value);
|
|
327
|
+
if (!Number.isInteger(numeric) || numeric < 0) throw new TypeError("maxevents must be a non-negative integer");
|
|
328
|
+
return numeric;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function clonevalue(value) {
|
|
332
|
+
if (Array.isArray(value)) return value.map(clonevalue);
|
|
333
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clonevalue(item)]));
|
|
334
|
+
return value;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function recordercontext(value = {}) {
|
|
338
|
+
const context = {};
|
|
339
|
+
for (const name of ["windowid", "tabid", "frameid"]) if (value[name] !== undefined) context[name] = String(value[name]);
|
|
340
|
+
return Object.keys(context).length ? context : undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
344
|
+
/* Section 7: browser/agent.ts — browser agent delegates browser actions to an injected runtime adapter. */
|
|
345
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
346
|
+
/**
|
|
347
|
+
* browser agent delegates browser actions to an injected runtime adapter.
|
|
348
|
+
*/
|
|
349
|
+
export function browseragent(adapter) {
|
|
350
|
+
const required = ["navigate", "click", "type", "screenshot", "html", "text", "title", "scrolltobottom", "executecommands"];
|
|
351
|
+
for (const name of required) if (typeof adapter?.[name] !== "function") throw new TypeError(`browser adapter requires ${name}`);
|
|
352
|
+
const optional = (name) => (...args) => { if (typeof adapter[name] !== "function") throw new Error(`browser adapter does not support ${name}`); return adapter[name](...args); };
|
|
353
|
+
return { navigate: (options) => adapter.navigate(options), click: (target) => adapter.click(target), type: (value) => adapter.type(value), screenshot: (options) => adapter.screenshot(options), html: () => adapter.html(), text: () => adapter.text(), title: () => adapter.title(), scrolltobottom: (options) => adapter.scrolltobottom(options), executecommands: (commands) => adapter.executecommands(commands), fill: optional("fill"), key: optional("key"), upload: optional("upload"), snapshot: optional("snapshot"), tabs: optional("tabs"), frames: optional("frames") };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
357
|
+
/* Section 8: browser/virtual.ts */
|
|
358
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
359
|
+
// @ts-nocheck
|
|
360
|
+
/**
|
|
361
|
+
* virtual browser contracts select remote browser capabilities without launching a browser or touching user state.
|
|
362
|
+
*/
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
const engines = new Set(["chromium", "gecko", "webkit", "custom"]);
|
|
366
|
+
const displays = new Set(["webrtc", "cdp", "webdriver", "remote-display", "custom"]);
|
|
367
|
+
const storages = new Set(["ephemeral", "caller-managed"]);
|
|
368
|
+
const receipts = new Set(["declared", "observed", "completed", "unknown"]);
|
|
369
|
+
const distributions = Object.freeze({
|
|
370
|
+
chromium: new Set(["chromium", "chrome", "edge", "brave", "vivaldi", "opera", "ungoogled-chromium"]),
|
|
371
|
+
gecko: new Set(["firefox", "tor-browser"]),
|
|
372
|
+
webkit: new Set(["safari"]),
|
|
373
|
+
custom: new Set(["saddle-browser", "custom"])
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
/** Creates a data-only request for a remote, adapter-owned browser session. */
|
|
377
|
+
export function virtualbrowserrequest(input = {}) {
|
|
378
|
+
const engine = valid(input.engine, engines, "browser engine");
|
|
379
|
+
const distribution = nonempty(input.distribution, "browser distribution");
|
|
380
|
+
if (!distributions[engine].has(distribution)) throw new TypeError("browser distribution does not match engine");
|
|
381
|
+
const display = valid(input.display ?? "webrtc", displays, "browser display transport");
|
|
382
|
+
const storage = valid(input.storage ?? "ephemeral", storages, "browser storage policy");
|
|
383
|
+
const execution = executionrequest({
|
|
384
|
+
id: input.id,
|
|
385
|
+
effect: "browser-session",
|
|
386
|
+
target: "remote",
|
|
387
|
+
source: input.source,
|
|
388
|
+
budget: input.budget
|
|
389
|
+
});
|
|
390
|
+
return Object.freeze({
|
|
391
|
+
version: 1,
|
|
392
|
+
state: "requested",
|
|
393
|
+
id: execution.id,
|
|
394
|
+
engine,
|
|
395
|
+
distribution,
|
|
396
|
+
display,
|
|
397
|
+
storage,
|
|
398
|
+
execution,
|
|
399
|
+
localaccess: Object.freeze({ browser: false, filesystem: false, storage: false, process: false }),
|
|
400
|
+
effects: Object.freeze([])
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Evaluates a virtual browser request without contacting a runtime or provider. */
|
|
405
|
+
export function virtualbrowserdecision(input, configuration = {}) {
|
|
406
|
+
const request = normalizerequest(input);
|
|
407
|
+
const base = executiondecision(request.execution, configuration);
|
|
408
|
+
const adapter = configuration.adapter ?? {};
|
|
409
|
+
const supportedengines = list(adapter.engines);
|
|
410
|
+
const supporteddistributions = list(adapter.distributions);
|
|
411
|
+
const reasons = [...base.reasons];
|
|
412
|
+
if (!supportedengines.includes(request.engine)) reasons.push("adapter-engine");
|
|
413
|
+
if (!supporteddistributions.includes(request.distribution)) reasons.push("adapter-distribution");
|
|
414
|
+
const allowed = reasons.length === 0;
|
|
415
|
+
return Object.freeze({
|
|
416
|
+
version: 1,
|
|
417
|
+
request,
|
|
418
|
+
state: allowed ? "caller-delegates" : "denied",
|
|
419
|
+
reasons: Object.freeze([...new Set(reasons)].sort()),
|
|
420
|
+
adapter: allowed ? Object.freeze({ owner: String(adapter.owner), engines: Object.freeze(supportedengines), distributions: Object.freeze(supporteddistributions) }) : null,
|
|
421
|
+
effects: Object.freeze([])
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Projects an adapter handoff without launching a browser or transferring user state. */
|
|
426
|
+
export function virtualbrowserhandoff(input = {}) {
|
|
427
|
+
const decision = virtualbrowserdecision(input.request, input.configuration);
|
|
428
|
+
if (decision.state !== "caller-delegates") return Object.freeze({ version: 1, state: "browser-disabled", code: "VIRTUAL_BROWSER_POLICY_DENIED", decision, effects: Object.freeze([]) });
|
|
429
|
+
return Object.freeze({ version: 1, state: "caller-delegates", code: "REMOTE_BROWSER_ADAPTER_REQUIRED", decision, effects: Object.freeze([]) });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Records declared or observed remote-browser capabilities without asserting isolation or trust. */
|
|
433
|
+
export function virtualbrowserreceipt(input = {}) {
|
|
434
|
+
const request = normalizerequest(input.request ?? input);
|
|
435
|
+
const state = valid(input.state ?? "declared", receipts, "browser receipt state");
|
|
436
|
+
const adapterid = nonempty(input.adapterid, "browser adapter id");
|
|
437
|
+
const image = nonempty(input.image, "browser image reference");
|
|
438
|
+
return Object.freeze({
|
|
439
|
+
version: 1,
|
|
440
|
+
state,
|
|
441
|
+
request,
|
|
442
|
+
adapterid,
|
|
443
|
+
image,
|
|
444
|
+
capabilities: Object.freeze(list(input.capabilities)),
|
|
445
|
+
effects: Object.freeze([])
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function list(value) { return [...new Set(Array.isArray(value) ? value.map((entry) => String(entry)) : [])].sort(); }
|
|
450
|
+
function normalizerequest(input) {
|
|
451
|
+
if (input?.execution?.effect === "browser-session") return virtualbrowserrequest({ id: input.id, source: input.execution.source, engine: input.engine, distribution: input.distribution, display: input.display, storage: input.storage, budget: input.execution.budget });
|
|
452
|
+
return virtualbrowserrequest(input);
|
|
453
|
+
}
|
|
454
|
+
function valid(value, collection, name) { const output = String(value ?? ""); if (!collection.has(output)) throw new TypeError(`${name} is invalid`); return output; }
|
|
455
|
+
function nonempty(value, name) { const output = String(value ?? ""); if (!output) throw new TypeError(`${name} is required`); return output; }
|
|
456
|
+
|
|
457
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
458
|
+
/* Section 9: browser/playwright.ts — browser playwright adapter keeps the optional Node browser provider outside the transport-neutral surface. */
|
|
459
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
460
|
+
/**
|
|
461
|
+
* browser playwright adapter keeps the optional Node browser provider outside the transport-neutral surface.
|
|
462
|
+
*/
|
|
463
|
+
|
|
464
|
+
/** Creates a caller-owned Playwright browser session when the optional peer is installed. */
|
|
465
|
+
export async function createplaywrightsession(options = {}) {
|
|
466
|
+
let playwright;
|
|
467
|
+
try {
|
|
468
|
+
playwright = await import("playwright");
|
|
469
|
+
} catch (error) {
|
|
470
|
+
const missing = new Error("optional peer dependency 'playwright' is required for the Playwright adapter", { cause: error });
|
|
471
|
+
missing.code = "OPTIONAL_DEPENDENCY_MISSING";
|
|
472
|
+
throw missing;
|
|
473
|
+
}
|
|
474
|
+
const browsername = options.browser ?? "chromium";
|
|
475
|
+
const browsertype = playwright[browsername];
|
|
476
|
+
if (!browsertype || typeof browsertype.launch !== "function") throw new TypeError(`unsupported Playwright browser: ${browsername}`);
|
|
477
|
+
const browser = await browsertype.launch({ headless: options.headless ?? true, ...(options.launch ?? {}) });
|
|
478
|
+
const context = await browser.newContext(options.context ?? {});
|
|
479
|
+
const page = await context.newPage();
|
|
480
|
+
return { browser, context, page, close: () => browser.close() };
|
|
481
|
+
}
|
|
482
|
+
|