@poe-platform/safe-bash 0.1.696 → 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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/safe-bash/playwright/index.d.ts +2 -1
  3. package/dist/safe-bash/playwright/index.d.ts.map +1 -1
  4. package/dist/safe-bash/playwright/index.js +2 -1
  5. package/dist/safe-bash/playwright/index.js.map +1 -1
  6. package/dist/safe-bash/playwright/native-storage-targets.d.ts.map +1 -1
  7. package/dist/safe-bash/playwright/native-storage-targets.js +19 -2
  8. package/dist/safe-bash/playwright/native-storage-targets.js.map +1 -1
  9. package/dist/safe-bash/playwright/profile.d.ts +38 -0
  10. package/dist/safe-bash/playwright/profile.d.ts.map +1 -0
  11. package/dist/safe-bash/playwright/profile.js +147 -0
  12. package/dist/safe-bash/playwright/profile.js.map +1 -0
  13. package/dist/safe-playwright-cloudflare/browser-artifact.js +46 -0
  14. package/dist/safe-playwright-cloudflare/browser-code-executor.d.ts +7 -0
  15. package/dist/safe-playwright-cloudflare/browser-code-executor.js +20 -0
  16. package/dist/safe-playwright-cloudflare/browser-codegen.generated.js +8701 -0
  17. package/dist/safe-playwright-cloudflare/browser-codegen.js +26 -0
  18. package/dist/safe-playwright-cloudflare/browser-page-cdp.js +21 -0
  19. package/dist/safe-playwright-cloudflare/browser-private-transport.js +338 -0
  20. package/dist/safe-playwright-cloudflare/browser-profile-runtime.js +33 -0
  21. package/dist/safe-playwright-cloudflare/browser-run-code-budget.js +130 -0
  22. package/dist/safe-playwright-cloudflare/browser-run-code-context-state.js +54 -0
  23. package/dist/safe-playwright-cloudflare/browser-run-code-contract.js +1 -0
  24. package/dist/safe-playwright-cloudflare/browser-run-code-guest.generated.js +2 -0
  25. package/dist/safe-playwright-cloudflare/browser-run-code-native.js +180 -0
  26. package/dist/safe-playwright-cloudflare/browser-run-code-relay.js +149 -0
  27. package/dist/safe-playwright-cloudflare/browser-run-code-state.js +102 -0
  28. package/dist/safe-playwright-cloudflare/browser-run-code.js +251 -0
  29. package/dist/safe-playwright-cloudflare/browser-snapshot-json-injected.js +80 -0
  30. package/dist/safe-playwright-cloudflare/browser-snapshot-json.js +89 -0
  31. package/dist/safe-playwright-cloudflare/browser-socket-closure.js +111 -0
  32. package/dist/safe-playwright-cloudflare/browser-storage-control.js +262 -0
  33. package/dist/safe-playwright-cloudflare/browser-trace.js +143 -0
  34. package/dist/safe-playwright-cloudflare/index.d.ts +2 -0
  35. package/dist/safe-playwright-cloudflare/index.js +1 -0
  36. package/dist/safe-playwright-cloudflare/shell-browser-resource.d.ts +17 -0
  37. package/dist/safe-playwright-cloudflare/shell-browser-resource.js +217 -0
  38. package/dist/safe-playwright-cloudflare/shell-playwright.d.ts +11 -0
  39. package/dist/safe-playwright-cloudflare/shell-playwright.js +110 -0
  40. package/package.json +10 -2
  41. package/third-party/safe-playwright-cloudflare/third-party/playwright/LICENSE +202 -0
  42. package/third-party/safe-playwright-cloudflare/third-party/playwright/NOTICE +12 -0
@@ -0,0 +1,80 @@
1
+ /** Runs in the native utility world. O(nodes + bytes), bounded before CDP transfer. */
2
+ export function serializeNativeSnapshot(injected, options) {
3
+ const MAX_SNAPSHOT_NODES = 20000;
4
+ const snapshot = injected._lastAriaSnapshotForQuery;
5
+ if (!snapshot)
6
+ throw new Error("Native browser snapshot is unavailable");
7
+ const encoder = new TextEncoder();
8
+ let bytes = 2;
9
+ let count = 0;
10
+ const account = (node) => {
11
+ // Reserve array/property separators while counting each shallow node once.
12
+ bytes += encoder.encode(JSON.stringify(node)).byteLength + 16;
13
+ if (bytes > options.maxBytes || ++count > MAX_SNAPSHOT_NODES)
14
+ throw new Error("Browser snapshot JSON limit exceeded");
15
+ };
16
+ const rectangle = (node) => {
17
+ const symbol = Object.getOwnPropertySymbols(node).find((key) => key.description === "element");
18
+ const element = symbol && node[symbol];
19
+ if (!element)
20
+ throw new Error("Native snapshot element is unavailable");
21
+ const { x, y, width, height } = element.getBoundingClientRect();
22
+ return {
23
+ x: Math.round(x),
24
+ y: Math.round(y),
25
+ width: Math.round(width),
26
+ height: Math.round(height),
27
+ };
28
+ };
29
+ const convertElement = (node) => {
30
+ const result = { role: node.role };
31
+ for (const key of [
32
+ "name",
33
+ "ref",
34
+ "checked",
35
+ "disabled",
36
+ "expanded",
37
+ "active",
38
+ "level",
39
+ "pressed",
40
+ "selected",
41
+ ]) {
42
+ if (node[key])
43
+ Object.assign(result, { [key]: node[key] });
44
+ }
45
+ if (node.ref && node.box.cursor === "pointer")
46
+ result.cursor = "pointer";
47
+ Object.assign(result, node.props);
48
+ if (options.boxes)
49
+ result.box = rectangle(node);
50
+ return result;
51
+ };
52
+ const convert = (node) => typeof node === "string"
53
+ ? { role: "text", text: node }
54
+ : convertElement(node);
55
+ const nodes = [];
56
+ if (snapshot.root.children.length > MAX_SNAPSHOT_NODES)
57
+ throw new Error("Browser snapshot JSON limit exceeded");
58
+ const pending = snapshot.root.children.map((node) => ({
59
+ node,
60
+ parent: nodes,
61
+ }));
62
+ const visit = (entry) => {
63
+ const result = convert(entry.node);
64
+ account(result);
65
+ entry.parent.push(result);
66
+ if (typeof entry.node === "string" || !entry.node.children.length)
67
+ return;
68
+ result.children = [];
69
+ if (pending.length + entry.node.children.length > MAX_SNAPSHOT_NODES)
70
+ throw new Error("Browser snapshot JSON limit exceeded");
71
+ for (const child of entry.node.children)
72
+ pending.push({ node: child, parent: result.children });
73
+ };
74
+ for (const entry of pending)
75
+ visit(entry);
76
+ const result = { nodes, iframeRefs: snapshot.iframeRefs };
77
+ if (encoder.encode(JSON.stringify(result)).byteLength > options.maxBytes)
78
+ throw new Error("Browser snapshot JSON limit exceeded");
79
+ return result;
80
+ }
@@ -0,0 +1,89 @@
1
+ import { serializeNativeSnapshot, } from "./browser-snapshot-json-injected.js";
2
+ const MAX_SNAPSHOT_FRAMES = 128;
3
+ // The pinned provider's native tree owns accessibility semantics and usable refs.
4
+ // Newer engine-only fields (invalid/ariaHidden) are not invented here.
5
+ // The controller retires the owning lease on rejection, including timeout,
6
+ // before accepting another command; native in-flight capture cannot be reused.
7
+ export const captureBrowserSnapshotJSON = async (page, options) => {
8
+ const native = page;
9
+ const signal = AbortSignal.any([
10
+ options.signal,
11
+ AbortSignal.timeout(options.timeoutMs),
12
+ ]);
13
+ signal.throwIfAborted();
14
+ let onAbort = () => { };
15
+ const aborted = new Promise((_, reject) => {
16
+ onAbort = () => reject(signal.reason);
17
+ signal.addEventListener("abort", onAbort, { once: true });
18
+ });
19
+ try {
20
+ return await Promise.race([
21
+ capture(native, { ...options, signal }),
22
+ aborted,
23
+ ]);
24
+ }
25
+ finally {
26
+ signal.removeEventListener("abort", onAbort);
27
+ }
28
+ };
29
+ async function capture(page, options) {
30
+ await page._snapshotForAI({ timeout: options.timeoutMs });
31
+ options.signal.throwIfAborted();
32
+ const root = page._connection.toImpl(page).mainFrame();
33
+ const encoder = new TextEncoder();
34
+ let remaining = options.maxBytes;
35
+ const forest = [];
36
+ const pending = [{ frame: root, target: forest }];
37
+ const visit = async (index) => {
38
+ options.signal.throwIfAborted();
39
+ const entry = pending[index];
40
+ if (!entry)
41
+ return;
42
+ if (index >= MAX_SNAPSHOT_FRAMES)
43
+ throw new Error("Browser snapshot frame limit exceeded");
44
+ const context = await entry.frame._utilityContext();
45
+ const injected = await context.injectedScript();
46
+ const result = await injected.evaluate(serializeNativeSnapshot, {
47
+ maxBytes: remaining,
48
+ boxes: options.boxes ?? false,
49
+ });
50
+ remaining -= encoder.encode(JSON.stringify(result.nodes)).byteLength;
51
+ entry.target.push(...result.nodes);
52
+ const byRef = indexNodes(result.nodes);
53
+ if (pending.length + result.iframeRefs.length > MAX_SNAPSHOT_FRAMES)
54
+ throw new Error("Browser snapshot frame limit exceeded");
55
+ const children = await Promise.all(result.iframeRefs.map(async (ref) => {
56
+ options.signal.throwIfAborted();
57
+ const target = byRef.get(ref);
58
+ if (!target)
59
+ return;
60
+ const resolved = await entry.frame.selectors.resolveFrameForSelector(`aria-ref=${ref} >> internal:control=enter-frame >> body`, { strict: true });
61
+ if (!resolved)
62
+ return;
63
+ target.children ??= [];
64
+ return { frame: resolved.frame, target: target.children };
65
+ }));
66
+ pending.push(...children.filter((child) => child !== undefined));
67
+ // Frame serialization shares one byte budget, so admit transfers in order.
68
+ await visit(index + 1);
69
+ };
70
+ await visit(0);
71
+ if (encoder.encode(JSON.stringify(forest)).byteLength > options.maxBytes)
72
+ throw new Error("Browser snapshot JSON limit exceeded");
73
+ return forest;
74
+ }
75
+ /** O(nodes), bounded by the renderer's byte and 20,000-node admission. */
76
+ function indexNodes(nodes) {
77
+ const byRef = new Map();
78
+ const pending = [...nodes];
79
+ for (let index = 0; index < pending.length; index++) {
80
+ const node = pending[index];
81
+ if (!node)
82
+ continue;
83
+ if (node.ref)
84
+ byRef.set(node.ref, node);
85
+ if (node.children)
86
+ pending.push(...node.children);
87
+ }
88
+ return byRef;
89
+ }
@@ -0,0 +1,111 @@
1
+ const closures = new WeakMap();
2
+ function nativeClosedEOF(socket, event) {
3
+ if (event instanceof ErrorEvent &&
4
+ event.isTrusted &&
5
+ socket.readyState === WebSocket.CLOSED &&
6
+ event.error instanceof Error &&
7
+ event.error.message === "Network connection lost.")
8
+ return event.error;
9
+ return undefined;
10
+ }
11
+ export function beginBrowserOwnerShutdown(sockets) {
12
+ for (const socket of sockets) {
13
+ const closure = closures.get(socket);
14
+ if (closure)
15
+ closure.ownerClosing = true;
16
+ }
17
+ }
18
+ export function finalizeBrowserOwnerTermination(sockets, deleted) {
19
+ for (const socket of sockets)
20
+ closures.get(socket)?.finalizeOwnerTermination(deleted);
21
+ }
22
+ export function registerBrowserSocketClose(socket, alias = socket) {
23
+ let closure = closures.get(socket);
24
+ if (!closure) {
25
+ const completion = Promise.withResolvers();
26
+ let settled = false;
27
+ let eof;
28
+ let eofDuringClose = false;
29
+ const cleanup = () => {
30
+ socket.removeEventListener("close", closed);
31
+ socket.removeEventListener("error", failed);
32
+ };
33
+ const finish = (error) => {
34
+ if (settled)
35
+ return;
36
+ settled = true;
37
+ if (error === undefined)
38
+ completion.resolve();
39
+ else
40
+ completion.reject(error);
41
+ };
42
+ const closed = (event) => {
43
+ cleanup();
44
+ finish(event.code === 1000 || event.code === 1005
45
+ ? undefined
46
+ : new Error(`Owned browser upstream closed: ${event.code} ${event.reason}`, { cause: eof }));
47
+ };
48
+ const failOperation = (error) => {
49
+ cleanup();
50
+ finish(error);
51
+ };
52
+ const failed = (event) => {
53
+ const nativeEOF = nativeClosedEOF(socket, event);
54
+ if (!nativeEOF) {
55
+ failOperation(new Error("Owned browser upstream socket failed before close confirmation"));
56
+ return;
57
+ }
58
+ eof = new Error("Owned browser upstream socket failed before close confirmation", { cause: nativeEOF });
59
+ eofDuringClose = closure.ownerClosing || closure.closing;
60
+ socket.removeEventListener("error", failed);
61
+ if (!eofDuringClose) {
62
+ failOperation(eof);
63
+ return;
64
+ }
65
+ if (closure.ownerTerminated !== undefined)
66
+ closure.finalizeOwnerTermination(closure.ownerTerminated);
67
+ };
68
+ closure = {
69
+ promise: completion.promise,
70
+ fail: failOperation,
71
+ ownerClosing: false,
72
+ closing: false,
73
+ finalizeOwnerTermination(deleted) {
74
+ closure.ownerTerminated = deleted;
75
+ if (!eof || !eofDuringClose || socket.readyState !== WebSocket.CLOSED)
76
+ return;
77
+ cleanup();
78
+ finish(deleted ? undefined : eof);
79
+ },
80
+ };
81
+ closures.set(socket, closure);
82
+ socket.addEventListener("close", closed);
83
+ socket.addEventListener("error", failed);
84
+ if (socket.readyState === WebSocket.CLOSED) {
85
+ cleanup();
86
+ finish();
87
+ }
88
+ void completion.promise.catch(() => { });
89
+ }
90
+ closures.set(alias, closure);
91
+ return closure.promise;
92
+ }
93
+ export function waitForBrowserSocketClose(socket) {
94
+ return (closures.get(socket)?.promise ??
95
+ Promise.reject(new Error("Untracked browser socket has no upstream close confirmation")));
96
+ }
97
+ export function closeBrowserSocket(socket) {
98
+ try {
99
+ const closure = closures.get(socket);
100
+ if (closure)
101
+ closure.closing = true;
102
+ if (socket.readyState === WebSocket.CLOSED)
103
+ return;
104
+ socket.accept();
105
+ socket.close();
106
+ }
107
+ catch (error) {
108
+ closures.get(socket)?.fail(error);
109
+ throw error;
110
+ }
111
+ }
@@ -0,0 +1,262 @@
1
+ function record(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ function identifier(value) {
5
+ return (typeof value === "string" &&
6
+ value.length > 0 &&
7
+ value.length <= 1024 &&
8
+ !value.includes("\0"));
9
+ }
10
+ function parseMessage(data, maxBytes) {
11
+ if (typeof data !== "string" ||
12
+ new TextEncoder().encode(data).length > maxBytes)
13
+ throw new Error("Owned storage control frame limit or type violation");
14
+ const value = JSON.parse(data);
15
+ if (!record(value))
16
+ throw new Error("Invalid owned storage control frame");
17
+ if (value["sessionId"] !== undefined && !identifier(value["sessionId"]))
18
+ throw new Error("Invalid owned storage control session");
19
+ if (value["id"] !== undefined)
20
+ return parseReply(value);
21
+ if (!identifier(value["method"]) ||
22
+ (value["params"] !== undefined && !record(value["params"])))
23
+ throw new Error("Invalid owned storage control event");
24
+ return {
25
+ method: value["method"],
26
+ params: value["params"],
27
+ sessionId: value["sessionId"],
28
+ };
29
+ }
30
+ function parseReply(value) {
31
+ if (typeof value["id"] !== "number" ||
32
+ !Number.isSafeInteger(value["id"]) ||
33
+ value["id"] <= 0)
34
+ throw new Error("Invalid owned storage control reply ID");
35
+ if (value["result"] !== undefined && !record(value["result"]))
36
+ throw new Error("Invalid owned storage control result");
37
+ const error = parseError(value["error"]);
38
+ if ((value["result"] === undefined) === (value["error"] === undefined))
39
+ throw new Error("Ambiguous owned storage control reply");
40
+ return {
41
+ method: "",
42
+ id: value["id"],
43
+ result: record(value["result"]) ? value["result"] : undefined,
44
+ error,
45
+ sessionId: identifier(value["sessionId"]) ? value["sessionId"] : undefined,
46
+ };
47
+ }
48
+ function parseError(value) {
49
+ if (value === undefined)
50
+ return;
51
+ if (!record(value) || typeof value["message"] !== "string")
52
+ throw new Error("Invalid owned storage control error");
53
+ return { message: value["message"] };
54
+ }
55
+ export function createBrowserStorageControl(options) {
56
+ const limits = {
57
+ maxMessageBytes: 16 * 1024 * 1024,
58
+ maxPendingCommands: 128,
59
+ maxPendingBytes: 4 * 1024 * 1024,
60
+ maxLateReplies: 128,
61
+ maxSubscriptions: 128,
62
+ commandTimeoutMs: 10000,
63
+ ...options.limits,
64
+ };
65
+ for (const [name, value] of Object.entries(limits)) {
66
+ if (!Number.isSafeInteger(value) ||
67
+ value <= 0 ||
68
+ (name.endsWith("TimeoutMs") && value > 2147483647))
69
+ throw new TypeError(`Invalid storage control limit: ${name}`);
70
+ }
71
+ const { socket } = options;
72
+ const pending = new Map();
73
+ const lateReplies = new Map();
74
+ const listeners = new Set();
75
+ const errors = [];
76
+ let pendingBytes = 0;
77
+ let sequence = 0;
78
+ let closed;
79
+ let closing;
80
+ function notifyDetached(reason) {
81
+ const event = {
82
+ method: "Inspector.detached",
83
+ params: { reason: reason.message },
84
+ };
85
+ for (const listener of [options.onEvent, ...listeners]) {
86
+ try {
87
+ listener?.(event);
88
+ }
89
+ catch (error) {
90
+ errors.push(error);
91
+ }
92
+ }
93
+ }
94
+ function shutdown(reason) {
95
+ if (closed)
96
+ return;
97
+ closed = reason;
98
+ socket.removeEventListener("message", receive);
99
+ socket.removeEventListener("close", disconnected);
100
+ socket.removeEventListener("error", failed);
101
+ notifyDetached(reason);
102
+ for (const command of pending.values()) {
103
+ command.deadline.removeEventListener("abort", command.expire);
104
+ command.reject(reason);
105
+ }
106
+ pending.clear();
107
+ lateReplies.clear();
108
+ pendingBytes = 0;
109
+ listeners.clear();
110
+ try {
111
+ socket.close();
112
+ }
113
+ catch (error) {
114
+ errors.push(error);
115
+ }
116
+ }
117
+ function fail(error) {
118
+ if (closed)
119
+ return;
120
+ const reason = error instanceof Error ? error : new Error(String(error));
121
+ errors.push(reason);
122
+ shutdown(reason);
123
+ }
124
+ function disconnected(event) {
125
+ fail(new Error(`Owned storage control disconnected: ${event.code} ${event.reason}`));
126
+ }
127
+ function failed() {
128
+ fail(new Error("Owned storage control socket failed"));
129
+ }
130
+ function receive(event) {
131
+ try {
132
+ const message = parseMessage(event.data, limits.maxMessageBytes);
133
+ if (message.id !== undefined) {
134
+ settle(message);
135
+ return;
136
+ }
137
+ options.onEvent?.(message);
138
+ for (const listener of [...listeners]) {
139
+ if (closed)
140
+ break;
141
+ listener(message);
142
+ }
143
+ }
144
+ catch (error) {
145
+ fail(error);
146
+ }
147
+ }
148
+ function settleLateReply(message) {
149
+ if (!lateReplies.has(message.id))
150
+ throw new Error("Unexpected owned storage control reply identity");
151
+ if (lateReplies.get(message.id) !== message.sessionId)
152
+ throw new Error("Unexpected owned storage control late reply session");
153
+ lateReplies.delete(message.id);
154
+ }
155
+ function settle(message) {
156
+ const command = pending.get(message.id);
157
+ if (!command) {
158
+ settleLateReply(message);
159
+ return;
160
+ }
161
+ if (command.sessionId !== message.sessionId)
162
+ throw new Error("Unexpected owned storage control reply identity");
163
+ pending.delete(message.id);
164
+ pendingBytes -= command.bytes;
165
+ command.deadline.removeEventListener("abort", command.expire);
166
+ if (message.error)
167
+ command.reject(new Error(message.error.message));
168
+ else
169
+ command.resolve(message.result);
170
+ }
171
+ socket.addEventListener("message", receive);
172
+ socket.addEventListener("close", disconnected);
173
+ socket.addEventListener("error", failed);
174
+ try {
175
+ socket.accept();
176
+ }
177
+ catch (error) {
178
+ fail(error);
179
+ }
180
+ function encodeCommand(method, params, sessionId) {
181
+ if (!identifier(method) ||
182
+ (sessionId !== undefined && !identifier(sessionId)))
183
+ throw new TypeError("Invalid owned storage control command identity");
184
+ if (params !== undefined && !record(params))
185
+ throw new TypeError("Invalid owned storage control command parameters");
186
+ if (sequence >= Number.MAX_SAFE_INTEGER)
187
+ throw new Error("Storage control ID capacity exceeded");
188
+ const id = ++sequence;
189
+ const data = JSON.stringify({ id, method, params, sessionId });
190
+ const bytes = new TextEncoder().encode(data).length;
191
+ return { id, data, bytes };
192
+ }
193
+ function admitCommand(bytes) {
194
+ if (bytes > limits.maxMessageBytes ||
195
+ pending.size >= limits.maxPendingCommands ||
196
+ bytes > limits.maxPendingBytes - pendingBytes)
197
+ throw new Error("Owned storage control pending or frame capacity exceeded");
198
+ }
199
+ function expireCommand(id, method) {
200
+ const command = pending.get(id);
201
+ if (!command)
202
+ return;
203
+ const reason = new Error(`Owned storage control command timed out: ${method}`);
204
+ if (method === "Target.createTarget" ||
205
+ lateReplies.size >= limits.maxLateReplies) {
206
+ fail(method === "Target.createTarget"
207
+ ? reason
208
+ : new Error("Owned storage control late reply capacity exceeded"));
209
+ return;
210
+ }
211
+ pending.delete(id);
212
+ pendingBytes -= command.bytes;
213
+ command.deadline.removeEventListener("abort", command.expire);
214
+ lateReplies.set(id, command.sessionId);
215
+ command.reject(reason);
216
+ }
217
+ return {
218
+ async send(method, params, sessionId) {
219
+ if (closed)
220
+ throw closed;
221
+ const { id, data, bytes } = encodeCommand(method, params, sessionId);
222
+ admitCommand(bytes);
223
+ const response = Promise.withResolvers();
224
+ const deadline = AbortSignal.timeout(limits.commandTimeoutMs);
225
+ const expire = () => expireCommand(id, method);
226
+ pending.set(id, {
227
+ ...response,
228
+ bytes,
229
+ sessionId,
230
+ deadline,
231
+ expire,
232
+ });
233
+ deadline.addEventListener("abort", expire, { once: true });
234
+ pendingBytes += bytes;
235
+ try {
236
+ socket.send(data);
237
+ }
238
+ catch (error) {
239
+ fail(error);
240
+ }
241
+ return await response.promise;
242
+ },
243
+ subscribe(listener) {
244
+ if (closed)
245
+ throw closed;
246
+ if (listeners.size >= limits.maxSubscriptions)
247
+ throw new Error("Owned storage control subscription capacity exceeded");
248
+ listeners.add(listener);
249
+ return () => {
250
+ listeners.delete(listener);
251
+ };
252
+ },
253
+ close() {
254
+ shutdown(new Error("Owned storage control closed"));
255
+ closing ??= Promise.resolve().then(() => {
256
+ if (errors.length)
257
+ throw new AggregateError([...errors], "Owned storage control cleanup failed");
258
+ });
259
+ return closing;
260
+ },
261
+ };
262
+ }
@@ -0,0 +1,143 @@
1
+ import { lstat, open } from "node:fs/promises";
2
+ import { PlaywrightResourceLimitError, } from "@poe-platform/safe-bash/playwright";
3
+ const MAX_TRACE_FILES = 1024;
4
+ const MAX_TRACE_PATH = 256;
5
+ const pathsByTracing = new WeakMap();
6
+ function safeName(name) {
7
+ return (name.length > 0 &&
8
+ name.length <= MAX_TRACE_PATH &&
9
+ name !== "." &&
10
+ name !== ".." &&
11
+ [...name].every((character) => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-@".includes(character)));
12
+ }
13
+ function nativeTracing(context) {
14
+ // @cloudflare/playwright 1.3.6 embeds its server inside the Worker. Only these
15
+ // pinned native internals own the trace paths; page data never supplies them.
16
+ const tracing = context.tracing;
17
+ if (!tracing?._connection?.toImpl)
18
+ throw new Error("Playwright provider does not support live trace capture");
19
+ const native = tracing._connection.toImpl(tracing);
20
+ if (!native?._fs?.syncAndGetError)
21
+ throw new Error("Native browser tracing unavailable");
22
+ return native;
23
+ }
24
+ function tracePaths(native) {
25
+ const state = native._state;
26
+ if (!state) {
27
+ const previous = pathsByTracing.get(native);
28
+ if (!previous)
29
+ throw new Error("Native browser trace has not started");
30
+ return previous;
31
+ }
32
+ const paths = {
33
+ tracesDir: state.tracesDir,
34
+ traceFile: state.traceFile,
35
+ networkFile: state.networkFile,
36
+ resourcesDir: state.resourcesDir,
37
+ traceSha1s: state.traceSha1s,
38
+ networkSha1s: state.networkSha1s,
39
+ };
40
+ const rootName = paths.tracesDir?.slice("/tmp/".length);
41
+ if (!paths.tracesDir?.startsWith("/tmp/playwright-artifacts-") ||
42
+ !safeName(rootName))
43
+ throw new Error("Invalid native browser trace directory");
44
+ for (const path of [paths.traceFile, paths.networkFile]) {
45
+ if (!path.startsWith(`${paths.tracesDir}/`) ||
46
+ !safeName(path.slice(paths.tracesDir.length + 1)))
47
+ throw new Error("Invalid native browser trace path");
48
+ }
49
+ if (paths.resourcesDir !== `${paths.tracesDir}/resources`)
50
+ throw new Error("Invalid native browser trace resources");
51
+ pathsByTracing.set(native, paths);
52
+ return paths;
53
+ }
54
+ async function readTraceFile(options) {
55
+ const { path, signal, remaining } = options;
56
+ signal.throwIfAborted();
57
+ const metadata = await lstat(path);
58
+ if (!metadata.isFile() || metadata.isSymbolicLink())
59
+ throw new Error("Native trace must be a regular file");
60
+ if (metadata.size > remaining)
61
+ throw new PlaywrightResourceLimitError("Browser trace byte limit exceeded");
62
+ const file = await open(path, "r");
63
+ try {
64
+ const current = await file.stat();
65
+ if (!current.isFile() ||
66
+ current.ino !== metadata.ino ||
67
+ current.dev !== metadata.dev)
68
+ throw new Error("Native trace file changed during capture");
69
+ const bytes = new Uint8Array(metadata.size);
70
+ let offset = 0;
71
+ while (offset < bytes.length) {
72
+ signal.throwIfAborted();
73
+ // biome-ignore lint/performance/noAwaitInLoops: Each short read advances the same bounded file cursor.
74
+ const { bytesRead } = await file.read(bytes, offset, bytes.length - offset, offset);
75
+ if (!bytesRead)
76
+ throw new Error("Native trace file truncated during capture");
77
+ offset += bytesRead;
78
+ }
79
+ return bytes;
80
+ }
81
+ finally {
82
+ await file.close();
83
+ }
84
+ }
85
+ function traceResources(paths) {
86
+ if (!(paths.traceSha1s instanceof Set) ||
87
+ !(paths.networkSha1s instanceof Set))
88
+ throw new Error("Native trace resource ownership unavailable");
89
+ if (paths.traceSha1s.size > MAX_TRACE_FILES ||
90
+ paths.networkSha1s.size > MAX_TRACE_FILES)
91
+ throw new PlaywrightResourceLimitError("Browser trace file count limit exceeded");
92
+ return [...paths.traceSha1s, ...paths.networkSha1s];
93
+ }
94
+ async function traceFiles(paths, signal) {
95
+ const root = await lstat(paths.tracesDir);
96
+ const resources = await lstat(paths.resourcesDir);
97
+ if (!root.isDirectory() || !resources.isDirectory())
98
+ throw new Error("Native trace paths must be directories");
99
+ const candidates = [paths.traceFile, paths.networkFile];
100
+ const seen = new Set();
101
+ // Keep references to the native recording's sets: stop replaces the state
102
+ // fields, while these sets still own its final resources, never sibling traces.
103
+ for (const name of traceResources(paths)) {
104
+ signal.throwIfAborted();
105
+ if (seen.has(name))
106
+ continue;
107
+ if (candidates.length >= MAX_TRACE_FILES)
108
+ throw new PlaywrightResourceLimitError("Browser trace file count limit exceeded");
109
+ if (!safeName(name))
110
+ throw new Error("Invalid native browser trace resource name");
111
+ seen.add(name);
112
+ candidates.push(`${paths.resourcesDir}/${name}`);
113
+ }
114
+ return candidates;
115
+ }
116
+ /** O(files + bytes), capped at 1024 files and the caller's aggregate byte limit.
117
+ * Native tracing can append after its flush; each read captures the admitted
118
+ * prefix, preserving future appends for the next command's flush. */
119
+ export const captureBrowserTrace = async (context, options) => {
120
+ options.signal.throwIfAborted();
121
+ if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1)
122
+ throw new Error("Invalid browser trace byte limit");
123
+ const native = nativeTracing(context);
124
+ const paths = tracePaths(native);
125
+ const failure = await native._fs.syncAndGetError();
126
+ if (failure)
127
+ throw failure;
128
+ const candidates = await traceFiles(paths, options.signal);
129
+ const files = [];
130
+ let remaining = options.maxBytes;
131
+ for (const path of candidates) {
132
+ // biome-ignore lint/performance/noAwaitInLoops: Admission decrements the shared aggregate byte budget before the next file.
133
+ const bytes = await readTraceFile({
134
+ path,
135
+ signal: options.signal,
136
+ remaining,
137
+ });
138
+ remaining -= bytes.byteLength;
139
+ files.push({ path: path.slice(paths.tracesDir.length + 1), bytes });
140
+ }
141
+ options.signal.throwIfAborted();
142
+ return { files };
143
+ };
@@ -0,0 +1,2 @@
1
+ export { createCloudflarePlaywrightAdapter } from './shell-playwright.js';
2
+ export type { BrowserCodeRuntime } from './browser-code-executor.js';
@@ -0,0 +1 @@
1
+ export { createCloudflarePlaywrightAdapter } from './shell-playwright.js';