@poe-platform/safe-bash 0.1.695 → 0.1.697

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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/dist/safe-bash/playwright/adapter.d.ts +4 -2
  3. package/dist/safe-bash/playwright/adapter.d.ts.map +1 -1
  4. package/dist/safe-bash/playwright/adapter.js.map +1 -1
  5. package/dist/safe-bash/playwright/controller.d.ts.map +1 -1
  6. package/dist/safe-bash/playwright/controller.js +8 -2
  7. package/dist/safe-bash/playwright/controller.js.map +1 -1
  8. package/dist/safe-bash/playwright/index.d.ts +2 -1
  9. package/dist/safe-bash/playwright/index.d.ts.map +1 -1
  10. package/dist/safe-bash/playwright/index.js +2 -1
  11. package/dist/safe-bash/playwright/index.js.map +1 -1
  12. package/dist/safe-bash/playwright/native-storage-targets.d.ts.map +1 -1
  13. package/dist/safe-bash/playwright/native-storage-targets.js +19 -2
  14. package/dist/safe-bash/playwright/native-storage-targets.js.map +1 -1
  15. package/dist/safe-bash/playwright/profile.d.ts +38 -0
  16. package/dist/safe-bash/playwright/profile.d.ts.map +1 -0
  17. package/dist/safe-bash/playwright/profile.js +147 -0
  18. package/dist/safe-bash/playwright/profile.js.map +1 -0
  19. package/dist/safe-playwright-cloudflare/browser-artifact.js +46 -0
  20. package/dist/safe-playwright-cloudflare/browser-code-executor.d.ts +7 -0
  21. package/dist/safe-playwright-cloudflare/browser-code-executor.js +20 -0
  22. package/dist/safe-playwright-cloudflare/browser-codegen.generated.js +8701 -0
  23. package/dist/safe-playwright-cloudflare/browser-codegen.js +26 -0
  24. package/dist/safe-playwright-cloudflare/browser-page-cdp.js +21 -0
  25. package/dist/safe-playwright-cloudflare/browser-private-transport.js +338 -0
  26. package/dist/safe-playwright-cloudflare/browser-profile-runtime.js +33 -0
  27. package/dist/safe-playwright-cloudflare/browser-run-code-budget.js +130 -0
  28. package/dist/safe-playwright-cloudflare/browser-run-code-context-state.js +54 -0
  29. package/dist/safe-playwright-cloudflare/browser-run-code-contract.js +1 -0
  30. package/dist/safe-playwright-cloudflare/browser-run-code-guest.generated.js +2 -0
  31. package/dist/safe-playwright-cloudflare/browser-run-code-native.js +180 -0
  32. package/dist/safe-playwright-cloudflare/browser-run-code-relay.js +149 -0
  33. package/dist/safe-playwright-cloudflare/browser-run-code-state.js +102 -0
  34. package/dist/safe-playwright-cloudflare/browser-run-code.js +251 -0
  35. package/dist/safe-playwright-cloudflare/browser-snapshot-json-injected.js +80 -0
  36. package/dist/safe-playwright-cloudflare/browser-snapshot-json.js +89 -0
  37. package/dist/safe-playwright-cloudflare/browser-socket-closure.js +111 -0
  38. package/dist/safe-playwright-cloudflare/browser-storage-control.js +262 -0
  39. package/dist/safe-playwright-cloudflare/browser-trace.js +143 -0
  40. package/dist/safe-playwright-cloudflare/index.d.ts +2 -0
  41. package/dist/safe-playwright-cloudflare/index.js +1 -0
  42. package/dist/safe-playwright-cloudflare/shell-browser-resource.d.ts +17 -0
  43. package/dist/safe-playwright-cloudflare/shell-browser-resource.js +217 -0
  44. package/dist/safe-playwright-cloudflare/shell-playwright.d.ts +11 -0
  45. package/dist/safe-playwright-cloudflare/shell-playwright.js +110 -0
  46. package/package.json +10 -2
  47. package/third-party/safe-playwright-cloudflare/third-party/playwright/LICENSE +202 -0
  48. package/third-party/safe-playwright-cloudflare/third-party/playwright/NOTICE +12 -0
@@ -0,0 +1,180 @@
1
+ import { PlaywrightResourceLimitError } from "@poe-platform/safe-bash/playwright";
2
+ import { browserPageCDP } from "./browser-page-cdp.js";
3
+ import { MAX_PAGE_STATE_BYTES, MAX_RUN_CODE_INIT_SCRIPTS, } from "./browser-run-code-state.js";
4
+ // Pinned @cloudflare/playwright 1.3.6 server/page.js InitScript wraps each source.
5
+ // Transferred sources already have the guest wrapper; host registration adds one.
6
+ const NATIVE_INIT_SCRIPT_WRAPPER_BYTES = new TextEncoder().encode("(() => {\n \n })();").byteLength;
7
+ function connection(browser) {
8
+ return browser._connection;
9
+ }
10
+ /** CDP has no context-disposed event for another connection. Call only after
11
+ * Target.getBrowserContexts confirms this owned context no longer exists. */
12
+ export async function forgetClosedRunCodeContext(browser, context, contextId) {
13
+ if (!browser.contexts().includes(context))
14
+ return;
15
+ const nativeBrowser = connection(browser).toImpl(browser);
16
+ const nativeContext = connection(browser).toImpl(context);
17
+ if (nativeBrowser._contexts.get(contextId) !== nativeContext)
18
+ throw new Error("Native Playwright context identity changed");
19
+ const closed = context.waitForEvent("close", { timeout: 0 });
20
+ nativeBrowser._contexts.delete(contextId);
21
+ nativeContext.initScripts = [];
22
+ nativeContext._browserClosed();
23
+ await closed;
24
+ }
25
+ /** Native defaults live on the client, independently of browser context options. */
26
+ function timeoutSettings(value) {
27
+ return value._timeoutSettings;
28
+ }
29
+ export function captureRunCodeTimeouts(value) {
30
+ const settings = timeoutSettings(value);
31
+ return {
32
+ action: settings.defaultTimeout() ?? null,
33
+ navigation: settings.defaultNavigationTimeout() ?? null,
34
+ };
35
+ }
36
+ export function restoreRunCodeTimeouts(value, timeouts) {
37
+ // The pinned native setters accept undefined to restore inheritance.
38
+ const settings = timeoutSettings(value);
39
+ settings.setDefaultTimeout(timeouts.action ?? undefined);
40
+ settings.setDefaultNavigationTimeout(timeouts.navigation ?? undefined);
41
+ }
42
+ /** Pinned @cloudflare/playwright 1.3.6 reconnects incognito pages as default. */
43
+ export function adoptRunCodeContext(browser, metadata) {
44
+ const native = connection(browser).toImpl(browser);
45
+ if (!native._defaultContext || !(native._contexts instanceof Map))
46
+ throw new Error("Unsupported native Playwright context implementation");
47
+ const context = native._defaultContext;
48
+ context._browserContextId = metadata.contextId;
49
+ context._options = metadata.contextOptions;
50
+ native._contexts.set(metadata.contextId, context);
51
+ const client = browser.contexts()[0];
52
+ if (!client)
53
+ throw new Error("Native Playwright context missing");
54
+ client._options = metadata.contextOptions;
55
+ restoreRunCodeTimeouts(client, metadata.state.contextTimeouts);
56
+ }
57
+ export function captureRunCodePageState(browser, page) {
58
+ const native = connection(browser).toImpl(page);
59
+ return {
60
+ targetId: native.delegate._targetId,
61
+ viewport: page.viewportSize(),
62
+ size: native.emulatedSize() ?? null,
63
+ media: native._emulatedMedia,
64
+ timeouts: captureRunCodeTimeouts(page),
65
+ // Existing registrations belong to the retained connection. Reinstalling
66
+ // them in the guest would execute each script twice on navigation.
67
+ initScripts: [],
68
+ };
69
+ }
70
+ /** Only scripts newly registered on the guest connection need transferring. */
71
+ export function captureRunCodeState(browser, context) {
72
+ if (!browser.contexts().includes(context))
73
+ return {
74
+ // Closed contexts have no native mapping and will not be restored.
75
+ context: { headers: [], offline: false, geolocation: null },
76
+ pages: [],
77
+ contextInitScripts: [],
78
+ contextTimeouts: captureRunCodeTimeouts(context),
79
+ };
80
+ const native = connection(browser).toImpl(context);
81
+ return {
82
+ context: captureRunCodeContextState(browser, context),
83
+ pages: context.pages().map((page) => ({
84
+ ...captureRunCodePageState(browser, page),
85
+ initScripts: connection(browser)
86
+ .toImpl(page)
87
+ .initScripts.map((script) => script.source),
88
+ })),
89
+ contextTimeouts: captureRunCodeTimeouts(context),
90
+ contextInitScripts: native.initScripts
91
+ .filter((script) => script !== native.bindingsInitScript)
92
+ .map((script) => script.source),
93
+ };
94
+ }
95
+ export function captureRunCodeContextState(browser, context) {
96
+ const options = connection(browser).toImpl(context)._options;
97
+ return {
98
+ headers: options.extraHTTPHeaders?.map(({ name, value }) => ({ name, value })) ??
99
+ [],
100
+ offline: options.offline ?? false,
101
+ geolocation: options.geolocation ?? null,
102
+ };
103
+ }
104
+ /** Replace configured registrations with their captured dynamic equivalents. */
105
+ export async function replaceRunCodeContextInitScripts(browser, context, sources) {
106
+ const native = connection(browser).toImpl(context);
107
+ await native.removeInitScripts(native.initScripts.filter(script => script !== native.bindingsInitScript));
108
+ for (const source of sources)
109
+ await context.addInitScript(source);
110
+ }
111
+ /** Admit the entire batch against live native registrations before transferring it.
112
+ * Closed pages/contexts release capacity naturally; no lifetime counter is kept. */
113
+ export function validateRunCodeInitScripts(browser, context, state) {
114
+ let count = 0;
115
+ let bytes = 0;
116
+ const encoder = new TextEncoder();
117
+ const include = (source, wrapperBytes = 0) => {
118
+ count++;
119
+ bytes += encoder.encode(source).byteLength + wrapperBytes;
120
+ if (count > MAX_RUN_CODE_INIT_SCRIPTS || bytes > MAX_PAGE_STATE_BYTES)
121
+ throw new PlaywrightResourceLimitError("Run-code retained init script limit exceeded");
122
+ };
123
+ const native = connection(browser).toImpl(context);
124
+ for (const script of native.initScripts)
125
+ if (script !== native.bindingsInitScript)
126
+ include(script.source);
127
+ for (const source of state.contextInitScripts)
128
+ include(source, NATIVE_INIT_SCRIPT_WRAPPER_BYTES);
129
+ for (const page of context.pages())
130
+ includePageInitScripts(browser, page, state.pages, include);
131
+ }
132
+ function includePageInitScripts(browser, page, pages, include) {
133
+ if (page.isClosed())
134
+ return;
135
+ const retained = connection(browser).toImpl(page);
136
+ for (const script of retained.initScripts)
137
+ include(script.source);
138
+ const incoming = pages.find((value) => value.targetId === retained.delegate._targetId);
139
+ if (!incoming)
140
+ throw new Error("Run-code page state is unavailable");
141
+ for (const source of incoming.initScripts)
142
+ include(source, NATIVE_INIT_SCRIPT_WRAPPER_BYTES);
143
+ }
144
+ /** Reconnecting CDP clients reset emulation on detach. Reapply the native state,
145
+ * including user changes, and invalidate Chromium's now-stale metrics cache. */
146
+ export async function restoreRunCodePageState(browser, page, state) {
147
+ const native = connection(browser).toImpl(page);
148
+ restoreRunCodeTimeouts(page, state.timeouts);
149
+ page._viewportSize = state.viewport;
150
+ native._setEmulatedSize(state.size);
151
+ native._emulatedMedia = state.media;
152
+ native.delegate._mainFrameSession._metricsOverride = undefined;
153
+ const cdp = await browserPageCDP(page);
154
+ await Promise.all([
155
+ ...state.initScripts.map((source) => page.addInitScript(source)),
156
+ native.delegate.updateEmulatedViewportSize(true),
157
+ native.delegate.updateEmulateMedia(),
158
+ native.delegate.updateUserAgent(),
159
+ ]);
160
+ const metrics = native.delegate._mainFrameSession._metricsOverride;
161
+ if (metrics) {
162
+ // Another CDP client's detach clears renderer emulation, but Chromium
163
+ // retains this session's last metrics and ignores an identical override.
164
+ // Clear that protocol cache before restoring repeated guest executions.
165
+ await cdp.send("Emulation.clearDeviceMetricsOverride");
166
+ await cdp.send("Emulation.setDeviceMetricsOverride", metrics);
167
+ }
168
+ }
169
+ export function runCodeContextOptions(browser, page) {
170
+ const options = connection(browser).toImpl(page.context())._options;
171
+ const { storageState: ignoredStorageState, ...result } = options;
172
+ return result;
173
+ }
174
+ /** Pinned native target identity avoids page CDP detach resetting touch. */
175
+ export function runCodePageTarget(browser, page) {
176
+ const targetId = connection(browser).toImpl(page).delegate._targetId;
177
+ if (typeof targetId !== "string" || !targetId)
178
+ throw new Error("Unsupported native Playwright page implementation");
179
+ return targetId;
180
+ }
@@ -0,0 +1,149 @@
1
+ import { RpcTarget } from "cloudflare:workers";
2
+ import { waitForBrowserSocketClose } from "./browser-private-transport.js";
3
+ import { createRunCodeFrameBudget, } from "./browser-run-code-budget.js";
4
+ import { BROWSER_RUN_CODE_URL, } from "./browser-run-code-contract.js";
5
+ import { closeBrowserSocket, registerBrowserSocketClose, } from "./browser-socket-closure.js";
6
+ export function createRunCodeRelay(options) {
7
+ const charge = createRunCodeFrameBudget();
8
+ let revoked = false;
9
+ let opening = false;
10
+ let socket;
11
+ let receiver;
12
+ let incoming = Promise.resolve();
13
+ const admitted = Promise.withResolvers();
14
+ let closing;
15
+ const check = () => {
16
+ options.signal.throwIfAborted();
17
+ if (revoked)
18
+ throw new Error("Run-code browser access revoked");
19
+ };
20
+ async function closeSocket() {
21
+ if (!socket)
22
+ return;
23
+ const failures = [];
24
+ try {
25
+ closeBrowserSocket(socket);
26
+ }
27
+ catch (error) {
28
+ failures.push(error);
29
+ }
30
+ try {
31
+ await waitForBrowserSocketClose(socket);
32
+ }
33
+ catch (error) {
34
+ failures.push(error);
35
+ }
36
+ if (failures.length)
37
+ throw new AggregateError(failures, "Run-code socket cleanup failed");
38
+ }
39
+ async function closeReceiver() {
40
+ if (!receiver)
41
+ return;
42
+ const retained = receiver;
43
+ receiver = undefined;
44
+ const failures = [];
45
+ try {
46
+ await retained.close();
47
+ }
48
+ catch (error) {
49
+ failures.push(error);
50
+ }
51
+ try {
52
+ retained[Symbol.dispose]();
53
+ }
54
+ catch (error) {
55
+ failures.push(error);
56
+ }
57
+ if (failures.length)
58
+ throw new AggregateError(failures, "Run-code receiver cleanup failed");
59
+ }
60
+ function assertCleanup(outcomes) {
61
+ const failures = outcomes
62
+ .filter((outcome) => outcome.status === "rejected")
63
+ .map((outcome) => outcome.reason);
64
+ if (failures.length)
65
+ throw new AggregateError(failures, "Run-code transport cleanup failed");
66
+ }
67
+ const close = () => {
68
+ if (closing)
69
+ return closing;
70
+ revoked = true;
71
+ closing = (async () => {
72
+ if (opening)
73
+ await admitted.promise;
74
+ assertCleanup(await Promise.allSettled([closeSocket(), closeReceiver()]));
75
+ })();
76
+ void closing.catch(() => { });
77
+ return closing;
78
+ };
79
+ const fail = (error) => {
80
+ if (revoked)
81
+ return;
82
+ void close().catch(options.fail);
83
+ options.fail(error);
84
+ };
85
+ class Session extends RpcTarget {
86
+ send(message) {
87
+ try {
88
+ check();
89
+ options.creations.command(charge(message));
90
+ socket.send(message);
91
+ }
92
+ catch (error) {
93
+ fail(error);
94
+ throw error;
95
+ }
96
+ }
97
+ close() {
98
+ return close();
99
+ }
100
+ }
101
+ class Relay extends RpcTarget {
102
+ async open(url, callback) {
103
+ check();
104
+ if (opening || url !== BROWSER_RUN_CODE_URL)
105
+ throw new Error("Run-code may connect only to its owned browser");
106
+ opening = true;
107
+ try {
108
+ receiver = callback.dup();
109
+ socket = await options.connectSocket(options.signal);
110
+ registerBrowserSocketClose(socket);
111
+ check();
112
+ socket.accept();
113
+ socket.addEventListener("message", (event) => {
114
+ try {
115
+ check();
116
+ options.creations.reply(charge(event.data));
117
+ const message = event.data;
118
+ incoming = incoming.then(async () => {
119
+ if (revoked)
120
+ return;
121
+ check();
122
+ await receiver.frame(message);
123
+ });
124
+ void incoming.catch(fail);
125
+ }
126
+ catch (error) {
127
+ fail(error);
128
+ }
129
+ });
130
+ socket.addEventListener("close", () => {
131
+ if (!revoked)
132
+ fail(new Error("Run-code CDP transport closed"));
133
+ });
134
+ socket.addEventListener("error", () => {
135
+ fail(new Error("Run-code CDP transport failed"));
136
+ });
137
+ return new Session();
138
+ }
139
+ catch (error) {
140
+ fail(error);
141
+ throw error;
142
+ }
143
+ finally {
144
+ admitted.resolve();
145
+ }
146
+ }
147
+ }
148
+ return { capability: new Relay(), close, opened: () => opening };
149
+ }
@@ -0,0 +1,102 @@
1
+ import { isRunCodeContextState, } from "./browser-run-code-context-state.js";
2
+ export const MAX_PAGE_STATE_BYTES = 64 * 1024;
3
+ export const MAX_RUN_CODE_INIT_SCRIPTS = 256;
4
+ const mediaValues = {
5
+ media: ["screen", "print", "no-override"],
6
+ colorScheme: ["dark", "light", "no-preference", "no-override"],
7
+ reducedMotion: ["reduce", "no-preference", "no-override"],
8
+ forcedColors: ["active", "none", "no-override"],
9
+ contrast: ["more", "no-preference", "no-override"],
10
+ };
11
+ // type-erasure-boundary -- JSON completion data comes from the untrusted Worker guest.
12
+ function allowedObject(value, keys) {
13
+ return (typeof value === "object" &&
14
+ value !== null &&
15
+ !Array.isArray(value) &&
16
+ Object.keys(value).every((key) => keys.includes(key)));
17
+ }
18
+ function dimensions(value) {
19
+ if (!allowedObject(value, ["width", "height"]))
20
+ return false;
21
+ return [value.width, value.height].every((dimension) => typeof dimension === "number" &&
22
+ Number.isSafeInteger(dimension) &&
23
+ dimension >= 1 &&
24
+ dimension <= 32768);
25
+ }
26
+ function media(value) {
27
+ if (!allowedObject(value, Object.keys(mediaValues)))
28
+ return false;
29
+ return Object.entries(value).every(([key, entry]) => typeof entry === "string" &&
30
+ mediaValues[key].includes(entry));
31
+ }
32
+ function emulatedSize(value) {
33
+ return (value === null ||
34
+ (allowedObject(value, ["viewport", "screen"]) &&
35
+ dimensions(value.viewport) &&
36
+ dimensions(value.screen)));
37
+ }
38
+ function scripts(value) {
39
+ return (Array.isArray(value) &&
40
+ value.length <= MAX_RUN_CODE_INIT_SCRIPTS &&
41
+ value.every((source) => typeof source === "string"));
42
+ }
43
+ function timeouts(value) {
44
+ if (!allowedObject(value, ["action", "navigation"]))
45
+ return false;
46
+ return [value.action, value.navigation].every((timeout) => timeout === null ||
47
+ (typeof timeout === "number" && Number.isFinite(timeout) && timeout >= 0));
48
+ }
49
+ function pageState(value) {
50
+ if (!allowedObject(value, [
51
+ "targetId",
52
+ "viewport",
53
+ "size",
54
+ "media",
55
+ "initScripts",
56
+ "timeouts",
57
+ ]))
58
+ return false;
59
+ const id = value.targetId;
60
+ return (typeof id === "string" &&
61
+ id.length > 0 &&
62
+ id.length <= 256 &&
63
+ (value.viewport === null || dimensions(value.viewport)) &&
64
+ emulatedSize(value.size) &&
65
+ media(value.media) &&
66
+ timeouts(value.timeouts) &&
67
+ scripts(value.initScripts));
68
+ }
69
+ /** Fixed schema and byte cap bound the guest's separate native state response. */
70
+ export function parseRunCodeState(json) {
71
+ if (typeof json !== "string" ||
72
+ new TextEncoder().encode(json).byteLength > MAX_PAGE_STATE_BYTES)
73
+ throw new Error("Run-code page state byte limit exceeded");
74
+ const value = JSON.parse(json);
75
+ if (!allowedObject(value, [
76
+ "pages",
77
+ "contextInitScripts",
78
+ "contextTimeouts",
79
+ "context",
80
+ ]) ||
81
+ !isRunCodeContextState(value.context) ||
82
+ !Array.isArray(value.pages) ||
83
+ value.pages.length > 64 ||
84
+ !value.pages.every(pageState) ||
85
+ !scripts(value.contextInitScripts) ||
86
+ !timeouts(value.contextTimeouts) ||
87
+ value.contextInitScripts.length +
88
+ value.pages.reduce((count, page) => count + page.initScripts.length, 0) >
89
+ MAX_RUN_CODE_INIT_SCRIPTS)
90
+ throw new Error("Invalid run-code page state");
91
+ return {
92
+ context: value.context,
93
+ pages: value.pages,
94
+ contextInitScripts: value.contextInitScripts,
95
+ contextTimeouts: value.contextTimeouts,
96
+ };
97
+ }
98
+ export function serializeRunCodeState(state) {
99
+ const json = JSON.stringify(state);
100
+ parseRunCodeState(json);
101
+ return json;
102
+ }
@@ -0,0 +1,251 @@
1
+ import { PlaywrightResourceLimitError } from "@poe-platform/safe-bash/playwright";
2
+ import { browserPageCDP } from "./browser-page-cdp.js";
3
+ import { createRunCodeCreationBudget, MAX_RUN_CODE_CONTEXTS, MAX_RUN_CODE_TARGETS, } from "./browser-run-code-budget.js";
4
+ import { restoreRunCodeContextState } from "./browser-run-code-context-state.js";
5
+ import { captureRunCodeContextState, captureRunCodePageState, captureRunCodeTimeouts, forgetClosedRunCodeContext, restoreRunCodePageState, restoreRunCodeTimeouts, runCodeContextOptions, validateRunCodeInitScripts, } from "./browser-run-code-native.js";
6
+ import { createRunCodeRelay } from "./browser-run-code-relay.js";
7
+ import { parseRunCodeState } from "./browser-run-code-state.js";
8
+ const activeOwners = new Set();
9
+ const MAX_SOURCE_BYTES = 1024 * 1024;
10
+ const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
11
+ class RunCodeUserError extends Error {
12
+ }
13
+ function validate(input) {
14
+ input.signal.throwIfAborted();
15
+ if (new TextEncoder().encode(input.source).byteLength > MAX_SOURCE_BYTES)
16
+ throw new PlaywrightResourceLimitError("Run-code source limit exceeded");
17
+ for (const [value, maximum] of [
18
+ [input.timeoutMs, 30_000],
19
+ [input.maxOutputBytes, MAX_OUTPUT_BYTES],
20
+ [input.maxPages, 64],
21
+ ]) {
22
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
23
+ throw new PlaywrightResourceLimitError("Invalid run-code limits");
24
+ }
25
+ }
26
+ async function identify(options, input) {
27
+ const cdp = await browserPageCDP(input.page);
28
+ const { targetInfo } = await cdp.send("Target.getTargetInfo");
29
+ if (!targetInfo.browserContextId)
30
+ throw new Error("Run-code requires an owned private context");
31
+ return {
32
+ targetId: targetInfo.targetId,
33
+ contextId: targetInfo.browserContextId,
34
+ contextOptions: runCodeContextOptions(options.browser, input.page),
35
+ state: {
36
+ context: captureRunCodeContextState(options.browser, input.page.context()),
37
+ pages: input.page
38
+ .context()
39
+ .pages()
40
+ .map((page) => captureRunCodePageState(options.browser, page)),
41
+ contextInitScripts: [],
42
+ contextTimeouts: captureRunCodeTimeouts(input.page.context()),
43
+ },
44
+ maxOutputBytes: input.maxOutputBytes,
45
+ };
46
+ }
47
+ /** One execution per trusted owner. Browser identity never comes from source. */
48
+ export function createBrowserRunCode(options) {
49
+ if (!options.ownerId || options.ownerId.length > 256)
50
+ throw new Error("Invalid browser owner");
51
+ return async (input) => {
52
+ validate(input);
53
+ if (activeOwners.has(options.ownerId))
54
+ throw new PlaywrightResourceLimitError("Run-code owner execution limit exceeded");
55
+ activeOwners.add(options.ownerId);
56
+ try {
57
+ return await execute(options, input);
58
+ }
59
+ finally {
60
+ activeOwners.delete(options.ownerId);
61
+ }
62
+ };
63
+ }
64
+ async function execute(options, input) {
65
+ const abort = new AbortController();
66
+ const failed = Promise.withResolvers();
67
+ // Observe rejection immediately while setup itself is still awaiting RPCs.
68
+ void failed.promise.catch(() => { });
69
+ let relay;
70
+ let retired;
71
+ const fail = (error) => {
72
+ if (abort.signal.aborted)
73
+ return;
74
+ abort.abort(error);
75
+ relay?.close();
76
+ retired = Promise.resolve().then(options.retire);
77
+ void retired.catch(() => { });
78
+ failed.reject(error);
79
+ };
80
+ const onAbort = () => fail(input.signal.reason);
81
+ input.signal.addEventListener("abort", onAbort, { once: true });
82
+ const deadline = AbortSignal.timeout(input.timeoutMs);
83
+ const onDeadline = () => fail(new PlaywrightResourceLimitError("Run-code deadline exceeded"));
84
+ deadline.addEventListener("abort", onDeadline, { once: true });
85
+ try {
86
+ input.signal.throwIfAborted();
87
+ return await Promise.race([
88
+ prepareAndRun(options, input, abort.signal, (value) => {
89
+ relay = value;
90
+ }, fail),
91
+ failed.promise,
92
+ ]);
93
+ }
94
+ catch (error) {
95
+ if (!(error instanceof RunCodeUserError))
96
+ fail(error);
97
+ throw error;
98
+ }
99
+ finally {
100
+ deadline.removeEventListener("abort", onDeadline);
101
+ input.signal.removeEventListener("abort", onAbort);
102
+ void relay?.close();
103
+ await retired;
104
+ }
105
+ }
106
+ async function prepareAndRun(options, input, signal, setRelay, fail) {
107
+ const metadata = await identify(options, input);
108
+ signal.throwIfAborted();
109
+ const observer = await options.browser.newBrowserCDPSession();
110
+ try {
111
+ const { targetInfos } = await observer.send("Target.getTargets");
112
+ const { browserContextIds } = await observer.send("Target.getBrowserContexts");
113
+ const foreign = targetInfos.filter((target) => target.type === "page" &&
114
+ target.browserContextId !== metadata.contextId);
115
+ if (foreign.some((target) => target.url !== "about:blank" ||
116
+ browserContextIds.includes(target.browserContextId ?? "")))
117
+ throw new Error("Run-code cannot reconnect another existing browser context");
118
+ await Promise.all(foreign.map((target) => observer.send("Target.closeTarget", { targetId: target.targetId })));
119
+ const targets = new Map(targetInfos
120
+ .filter((target) => target.browserContextId === metadata.contextId)
121
+ .map((target) => [target.targetId, target.type]));
122
+ if (targets.size > MAX_RUN_CODE_TARGETS ||
123
+ browserContextIds.length > MAX_RUN_CODE_CONTEXTS)
124
+ throw new PlaywrightResourceLimitError("Run-code browser resource limit exceeded");
125
+ const creations = createRunCodeCreationBudget({
126
+ maxPages: input.maxPages,
127
+ pages: [...targets]
128
+ .filter(([, type]) => type === "page")
129
+ .map(([id]) => id),
130
+ contexts: browserContextIds,
131
+ });
132
+ observer.on("Target.targetCreated", ({ targetInfo }) => {
133
+ try {
134
+ targets.set(targetInfo.targetId, targetInfo.type);
135
+ if (targets.size > MAX_RUN_CODE_TARGETS)
136
+ throw new PlaywrightResourceLimitError("Run-code target limit exceeded");
137
+ if (targetInfo.type === "page")
138
+ creations.pageCreated(targetInfo.targetId);
139
+ }
140
+ catch (error) {
141
+ fail(error);
142
+ }
143
+ });
144
+ observer.on("Target.targetDestroyed", ({ targetId }) => {
145
+ targets.delete(targetId);
146
+ creations.pageDestroyed(targetId);
147
+ });
148
+ await observer.send("Target.setDiscoverTargets", { discover: true });
149
+ signal.throwIfAborted();
150
+ const relay = createRunCodeRelay({
151
+ signal,
152
+ connectSocket: options.connectSocket,
153
+ creations,
154
+ fail,
155
+ });
156
+ setRelay(relay);
157
+ try {
158
+ return await runGuest(options, input, metadata, relay, signal);
159
+ }
160
+ catch (error) {
161
+ // Loader compilation can fail before any guest obtains browser access.
162
+ if (!relay.opened())
163
+ throw new RunCodeUserError(String(error));
164
+ throw error;
165
+ }
166
+ finally {
167
+ const closing = relay.close();
168
+ if (!signal.aborted) {
169
+ await closing;
170
+ signal.throwIfAborted();
171
+ await restoreState(options, input, metadata, observer, signal);
172
+ }
173
+ }
174
+ }
175
+ finally {
176
+ await observer.detach().catch(() => { });
177
+ }
178
+ }
179
+ async function runGuest(options, input, metadata, relay, signal) {
180
+ signal.throwIfAborted();
181
+ const worker = options.loader.load({
182
+ compatibilityDate: "2026-07-08",
183
+ compatibilityFlags: ["nodejs_compat"],
184
+ mainModule: "guest.js",
185
+ modules: {
186
+ "guest.js": options.guestSource,
187
+ "browser-user-code.js": `export default (${input.source}\n);`,
188
+ },
189
+ limits: { cpuMs: 1000, subRequests: 4096 },
190
+ });
191
+ const entry = worker.getEntrypoint();
192
+ const result = entry.run(relay.capability, metadata);
193
+ const dispose = () => {
194
+ result[Symbol.dispose]?.();
195
+ entry[Symbol.dispose]?.();
196
+ };
197
+ signal.addEventListener("abort", dispose, { once: true });
198
+ try {
199
+ const response = await result;
200
+ signal.throwIfAborted();
201
+ const state = parseRunCodeState(response.stateJson);
202
+ if (state.pages.length > input.maxPages)
203
+ throw new PlaywrightResourceLimitError("Run-code page state limit exceeded");
204
+ metadata.state = state;
205
+ if (!response.ok)
206
+ throw new RunCodeUserError(response.message.slice(0, 4096));
207
+ const json = response.json;
208
+ if (typeof json !== "string" ||
209
+ new TextEncoder().encode(json).byteLength > input.maxOutputBytes)
210
+ throw new PlaywrightResourceLimitError("Run-code output limit exceeded");
211
+ return JSON.parse(json);
212
+ }
213
+ finally {
214
+ signal.removeEventListener("abort", dispose);
215
+ dispose();
216
+ }
217
+ }
218
+ /** Restore after the provider confirms the guest connection has closed. */
219
+ async function restoreState(options, input, metadata, observer, signal) {
220
+ const { browserContextIds } = await observer.send("Target.getBrowserContexts");
221
+ signal.throwIfAborted();
222
+ if (!browserContextIds.includes(metadata.contextId)) {
223
+ await forgetClosedRunCodeContext(options.browser, input.page.context(), metadata.contextId);
224
+ return;
225
+ }
226
+ const state = metadata.state;
227
+ if (!options.browser.contexts().includes(input.page.context()))
228
+ return;
229
+ validateRunCodeInitScripts(options.browser, input.page.context(), state);
230
+ await restoreRunCodeContextState(input.page.context(), state.context);
231
+ restoreRunCodeTimeouts(input.page.context(), state.contextTimeouts);
232
+ await Promise.all(state.contextInitScripts.map((source) => input.page.context().addInitScript(source)));
233
+ await Promise.all(input.page
234
+ .context()
235
+ .pages()
236
+ .map(async (page) => {
237
+ if (page.isClosed())
238
+ return;
239
+ const target = captureRunCodePageState(options.browser, page).targetId;
240
+ const pageState = state.pages.find((value) => value.targetId === target);
241
+ if (!pageState)
242
+ throw new Error("Run-code page state is unavailable");
243
+ await restoreRunCodePageState(options.browser, page, pageState);
244
+ const hasTouch = runCodeContextOptions(options.browser, page).hasTouch === true;
245
+ const cdp = await browserPageCDP(page);
246
+ await cdp.send("Emulation.setTouchEmulationEnabled", {
247
+ enabled: hasTouch,
248
+ maxTouchPoints: hasTouch ? 1 : undefined,
249
+ });
250
+ }));
251
+ }