@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.
- package/README.md +1 -1
- package/dist/safe-bash/playwright/adapter.d.ts +4 -2
- package/dist/safe-bash/playwright/adapter.d.ts.map +1 -1
- package/dist/safe-bash/playwright/adapter.js.map +1 -1
- package/dist/safe-bash/playwright/controller.d.ts.map +1 -1
- package/dist/safe-bash/playwright/controller.js +8 -2
- package/dist/safe-bash/playwright/controller.js.map +1 -1
- package/dist/safe-bash/playwright/index.d.ts +2 -1
- package/dist/safe-bash/playwright/index.d.ts.map +1 -1
- package/dist/safe-bash/playwright/index.js +2 -1
- package/dist/safe-bash/playwright/index.js.map +1 -1
- package/dist/safe-bash/playwright/native-storage-targets.d.ts.map +1 -1
- package/dist/safe-bash/playwright/native-storage-targets.js +19 -2
- package/dist/safe-bash/playwright/native-storage-targets.js.map +1 -1
- package/dist/safe-bash/playwright/profile.d.ts +38 -0
- package/dist/safe-bash/playwright/profile.d.ts.map +1 -0
- package/dist/safe-bash/playwright/profile.js +147 -0
- package/dist/safe-bash/playwright/profile.js.map +1 -0
- package/dist/safe-playwright-cloudflare/browser-artifact.js +46 -0
- package/dist/safe-playwright-cloudflare/browser-code-executor.d.ts +7 -0
- package/dist/safe-playwright-cloudflare/browser-code-executor.js +20 -0
- package/dist/safe-playwright-cloudflare/browser-codegen.generated.js +8701 -0
- package/dist/safe-playwright-cloudflare/browser-codegen.js +26 -0
- package/dist/safe-playwright-cloudflare/browser-page-cdp.js +21 -0
- package/dist/safe-playwright-cloudflare/browser-private-transport.js +338 -0
- package/dist/safe-playwright-cloudflare/browser-profile-runtime.js +33 -0
- package/dist/safe-playwright-cloudflare/browser-run-code-budget.js +130 -0
- package/dist/safe-playwright-cloudflare/browser-run-code-context-state.js +54 -0
- package/dist/safe-playwright-cloudflare/browser-run-code-contract.js +1 -0
- package/dist/safe-playwright-cloudflare/browser-run-code-guest.generated.js +2 -0
- package/dist/safe-playwright-cloudflare/browser-run-code-native.js +180 -0
- package/dist/safe-playwright-cloudflare/browser-run-code-relay.js +149 -0
- package/dist/safe-playwright-cloudflare/browser-run-code-state.js +102 -0
- package/dist/safe-playwright-cloudflare/browser-run-code.js +251 -0
- package/dist/safe-playwright-cloudflare/browser-snapshot-json-injected.js +80 -0
- package/dist/safe-playwright-cloudflare/browser-snapshot-json.js +89 -0
- package/dist/safe-playwright-cloudflare/browser-socket-closure.js +111 -0
- package/dist/safe-playwright-cloudflare/browser-storage-control.js +262 -0
- package/dist/safe-playwright-cloudflare/browser-trace.js +143 -0
- package/dist/safe-playwright-cloudflare/index.d.ts +2 -0
- package/dist/safe-playwright-cloudflare/index.js +1 -0
- package/dist/safe-playwright-cloudflare/shell-browser-resource.d.ts +17 -0
- package/dist/safe-playwright-cloudflare/shell-browser-resource.js +217 -0
- package/dist/safe-playwright-cloudflare/shell-playwright.d.ts +11 -0
- package/dist/safe-playwright-cloudflare/shell-playwright.js +110 -0
- package/package.json +10 -2
- package/third-party/safe-playwright-cloudflare/third-party/playwright/LICENSE +202 -0
- package/third-party/safe-playwright-cloudflare/third-party/playwright/NOTICE +12 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { languageSet } from "./browser-codegen.generated.js";
|
|
2
|
+
const MAX_ACTION_BYTES = 1024 * 1024;
|
|
3
|
+
const MAX_CODE_BYTES = 8 * 1024 * 1024;
|
|
4
|
+
/** Uses the qualified native generators; source strings stay in the CLI renderer. */
|
|
5
|
+
export const generateBrowserActionCode = ({ language, action, }) => {
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
if (encoder.encode(JSON.stringify(action)).byteLength > MAX_ACTION_BYTES)
|
|
8
|
+
throw new Error("Playwright code generation input limit exceeded");
|
|
9
|
+
const id = language === "typescript" ? "playwright-test" : language;
|
|
10
|
+
const generator = [...languageSet()].find((candidate) => candidate.id === id);
|
|
11
|
+
if (!generator)
|
|
12
|
+
throw new Error("Unsupported native code generation language");
|
|
13
|
+
const code = generator.generateAction({
|
|
14
|
+
frame: { pageGuid: "page", pageAlias: "page", framePath: [] },
|
|
15
|
+
startTime: 0,
|
|
16
|
+
action: { ...action, signals: [] },
|
|
17
|
+
});
|
|
18
|
+
if (encoder.encode(code).byteLength > MAX_CODE_BYTES)
|
|
19
|
+
throw new Error("Playwright code generation output limit exceeded");
|
|
20
|
+
const lines = code.split("\n");
|
|
21
|
+
const indents = lines
|
|
22
|
+
.filter((line) => line.trim())
|
|
23
|
+
.map((line) => line.length - line.trimStart().length);
|
|
24
|
+
const indent = indents.length ? Math.min(...indents) : 0;
|
|
25
|
+
return lines.map((line) => line.substring(indent)).join("\n");
|
|
26
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const sessions = new WeakMap();
|
|
2
|
+
/** Cloudflare resets touch emulation when any page CDP session detaches. Keep
|
|
3
|
+
* one identity session per live page; page/context destruction releases it.
|
|
4
|
+
* The owning browser's page limit bounds these sessions. Native frame identity
|
|
5
|
+
* also unifies the page wrappers used for navigation and run-code. */
|
|
6
|
+
export function browserPageCDP(page) {
|
|
7
|
+
const frame = page.mainFrame();
|
|
8
|
+
const existing = sessions.get(frame);
|
|
9
|
+
if (existing)
|
|
10
|
+
return existing;
|
|
11
|
+
const pending = page.context().newCDPSession(page);
|
|
12
|
+
sessions.set(frame, pending);
|
|
13
|
+
const forget = () => {
|
|
14
|
+
if (sessions.get(frame) === pending)
|
|
15
|
+
sessions.delete(frame);
|
|
16
|
+
page.off("close", forget);
|
|
17
|
+
};
|
|
18
|
+
page.on("close", forget);
|
|
19
|
+
void pending.catch(forget);
|
|
20
|
+
return pending;
|
|
21
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { createPlaywrightPrivateTargetTransport, } from "@poe-platform/safe-bash/playwright";
|
|
2
|
+
import { closeBrowserSocket, registerBrowserSocketClose, } from "./browser-socket-closure.js";
|
|
3
|
+
export { waitForBrowserSocketClose } from "./browser-socket-closure.js";
|
|
4
|
+
function targetIdentity(value) {
|
|
5
|
+
return (typeof value === "string" &&
|
|
6
|
+
value.length > 0 &&
|
|
7
|
+
value.length <= 1024 &&
|
|
8
|
+
!value.includes("\0"));
|
|
9
|
+
}
|
|
10
|
+
function protocolObject(data, maxBytes) {
|
|
11
|
+
if (typeof data !== "string" ||
|
|
12
|
+
new TextEncoder().encode(data).length > maxBytes)
|
|
13
|
+
throw new Error("Private browser frame limit or type violation");
|
|
14
|
+
const value = JSON.parse(data);
|
|
15
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
16
|
+
throw new Error("Invalid private browser protocol frame");
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export function createBrowserPrivateTransport(options = {}) {
|
|
20
|
+
const { maxClients = 8, ...guardOptions } = options;
|
|
21
|
+
const maxTargets = options.maxPrivateTargets ?? 256;
|
|
22
|
+
const maxBytes = options.maxMessageBytes ?? 16 * 1024 * 1024;
|
|
23
|
+
const creationTimeoutMs = options.creationTimeoutMs ?? 2000;
|
|
24
|
+
for (const [name, value] of Object.entries({
|
|
25
|
+
...guardOptions,
|
|
26
|
+
maxClients,
|
|
27
|
+
maxTargets,
|
|
28
|
+
maxBytes,
|
|
29
|
+
creationTimeoutMs,
|
|
30
|
+
})) {
|
|
31
|
+
if (!Number.isSafeInteger(value) ||
|
|
32
|
+
value <= 0 ||
|
|
33
|
+
(name.endsWith("TimeoutMs") && value > 2147483647))
|
|
34
|
+
throw new TypeError(`Invalid private browser limit: ${name}`);
|
|
35
|
+
}
|
|
36
|
+
const clients = new Set();
|
|
37
|
+
const rawClosures = new Set();
|
|
38
|
+
const active = new Set();
|
|
39
|
+
const retired = new Set();
|
|
40
|
+
const errors = [];
|
|
41
|
+
let creation;
|
|
42
|
+
let closed;
|
|
43
|
+
let closing;
|
|
44
|
+
function check() {
|
|
45
|
+
if (closed)
|
|
46
|
+
throw closed;
|
|
47
|
+
}
|
|
48
|
+
function closeClients(reason) {
|
|
49
|
+
if (closed)
|
|
50
|
+
return;
|
|
51
|
+
closed = reason;
|
|
52
|
+
if (creation) {
|
|
53
|
+
creation.deadline.removeEventListener("abort", creation.expire);
|
|
54
|
+
creation.active = false;
|
|
55
|
+
creation.failed = true;
|
|
56
|
+
creation = undefined;
|
|
57
|
+
}
|
|
58
|
+
for (const client of [...clients])
|
|
59
|
+
client.stop();
|
|
60
|
+
active.clear();
|
|
61
|
+
retired.clear();
|
|
62
|
+
}
|
|
63
|
+
function fail(error) {
|
|
64
|
+
if (closed)
|
|
65
|
+
return;
|
|
66
|
+
const reason = error instanceof Error ? error : new Error(String(error));
|
|
67
|
+
errors.push(reason);
|
|
68
|
+
closeClients(reason);
|
|
69
|
+
}
|
|
70
|
+
function seed(client) {
|
|
71
|
+
for (const targetId of retired) {
|
|
72
|
+
client.guard.beginCreation().commit(targetId);
|
|
73
|
+
client.upstream.onmessage?.({
|
|
74
|
+
method: "Target.targetDestroyed",
|
|
75
|
+
params: { targetId },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
for (const targetId of active)
|
|
79
|
+
client.guard.beginCreation().commit(targetId);
|
|
80
|
+
if (creation)
|
|
81
|
+
creation.tokens.set(client, client.guard.beginCreation());
|
|
82
|
+
}
|
|
83
|
+
function closeRejected(socket, error) {
|
|
84
|
+
try {
|
|
85
|
+
registerBrowserSocketClose(socket);
|
|
86
|
+
closeBrowserSocket(socket);
|
|
87
|
+
}
|
|
88
|
+
catch (cleanup) {
|
|
89
|
+
throw new AggregateError([error, cleanup], "Private client admission and cleanup failed");
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
function wrap(socket, signal) {
|
|
94
|
+
try {
|
|
95
|
+
check();
|
|
96
|
+
signal.throwIfAborted();
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return closeRejected(socket, error);
|
|
100
|
+
}
|
|
101
|
+
if (rawClosures.size >= maxClients)
|
|
102
|
+
return closeRejected(socket, new Error("Private browser client capacity exceeded"));
|
|
103
|
+
const pair = new WebSocketPair();
|
|
104
|
+
const rawClosed = registerBrowserSocketClose(socket, pair[0]);
|
|
105
|
+
rawClosures.add(rawClosed);
|
|
106
|
+
void rawClosed.then(() => rawClosures.delete(rawClosed), (error) => {
|
|
107
|
+
rawClosures.delete(rawClosed);
|
|
108
|
+
errors.push(error);
|
|
109
|
+
});
|
|
110
|
+
const bridge = pair[1];
|
|
111
|
+
let stopped = false;
|
|
112
|
+
const upstream = {
|
|
113
|
+
open() {
|
|
114
|
+
socket.accept();
|
|
115
|
+
},
|
|
116
|
+
send(message) {
|
|
117
|
+
socket.send(JSON.stringify(message));
|
|
118
|
+
},
|
|
119
|
+
close() {
|
|
120
|
+
client.stop();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const guard = createPlaywrightPrivateTargetTransport(upstream, guardOptions);
|
|
124
|
+
const cleanup = (operation) => {
|
|
125
|
+
try {
|
|
126
|
+
operation();
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
errors.push(error);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const client = {
|
|
133
|
+
guard,
|
|
134
|
+
upstream,
|
|
135
|
+
stop(error) {
|
|
136
|
+
if (stopped)
|
|
137
|
+
return;
|
|
138
|
+
stopped = true;
|
|
139
|
+
if (error !== undefined)
|
|
140
|
+
errors.push(error);
|
|
141
|
+
clients.delete(client);
|
|
142
|
+
creation?.tokens.delete(client);
|
|
143
|
+
signal.removeEventListener("abort", aborted);
|
|
144
|
+
socket.removeEventListener("message", incoming);
|
|
145
|
+
socket.removeEventListener("close", disconnected);
|
|
146
|
+
socket.removeEventListener("error", socketFailed);
|
|
147
|
+
bridge.removeEventListener("message", outgoing);
|
|
148
|
+
bridge.removeEventListener("close", clientClosed);
|
|
149
|
+
bridge.removeEventListener("error", socketFailed);
|
|
150
|
+
cleanup(() => guard.transport.close());
|
|
151
|
+
cleanup(() => closeBrowserSocket(socket));
|
|
152
|
+
cleanup(() => bridge.close());
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
function incoming(event) {
|
|
156
|
+
try {
|
|
157
|
+
upstream.onmessage?.(protocolObject(event.data, maxBytes));
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
client.stop(error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function outgoing(event) {
|
|
164
|
+
try {
|
|
165
|
+
guard.transport.send(protocolObject(event.data, maxBytes));
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
client.stop(error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function aborted() {
|
|
172
|
+
client.stop();
|
|
173
|
+
}
|
|
174
|
+
function disconnected(event) {
|
|
175
|
+
client.stop(event.code === 1000
|
|
176
|
+
? undefined
|
|
177
|
+
: new Error(`Private browser disconnected: ${event.code} ${event.reason}`));
|
|
178
|
+
}
|
|
179
|
+
function socketFailed() {
|
|
180
|
+
client.stop(new Error("Private browser socket failed"));
|
|
181
|
+
}
|
|
182
|
+
function clientClosed() {
|
|
183
|
+
client.stop();
|
|
184
|
+
}
|
|
185
|
+
guard.transport.onmessage = (message) => {
|
|
186
|
+
try {
|
|
187
|
+
bridge.send(JSON.stringify(message));
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
client.stop(error);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
guard.transport.onclose = (reason) => client.stop(new Error(reason ?? "Private browser transport failed"));
|
|
194
|
+
try {
|
|
195
|
+
bridge.accept();
|
|
196
|
+
clients.add(client);
|
|
197
|
+
seed(client);
|
|
198
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
199
|
+
socket.addEventListener("message", incoming);
|
|
200
|
+
socket.addEventListener("close", disconnected);
|
|
201
|
+
socket.addEventListener("error", socketFailed);
|
|
202
|
+
bridge.addEventListener("message", outgoing);
|
|
203
|
+
bridge.addEventListener("close", clientClosed);
|
|
204
|
+
bridge.addEventListener("error", socketFailed);
|
|
205
|
+
signal.throwIfAborted();
|
|
206
|
+
guard.transport.open?.();
|
|
207
|
+
if (stopped)
|
|
208
|
+
throw new Error("Private browser closed during admission");
|
|
209
|
+
return pair[0];
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
client.stop();
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function admitIdentity(entry, targetId) {
|
|
217
|
+
if (!targetIdentity(targetId) ||
|
|
218
|
+
active.has(targetId) ||
|
|
219
|
+
retired.has(targetId) ||
|
|
220
|
+
entry.destroyed.has(targetId)) {
|
|
221
|
+
const error = new Error("Invalid, reused or destroyed private target identity");
|
|
222
|
+
fail(error);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
active.add(targetId);
|
|
226
|
+
}
|
|
227
|
+
function settleTokens(entry, targetId) {
|
|
228
|
+
for (const token of entry.tokens.values()) {
|
|
229
|
+
if (targetId === undefined)
|
|
230
|
+
token.rollback();
|
|
231
|
+
else
|
|
232
|
+
token.commit(targetId);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function finish(entry, targetId) {
|
|
236
|
+
check();
|
|
237
|
+
if (!entry.active || creation !== entry)
|
|
238
|
+
throw new Error("Private creation is no longer active");
|
|
239
|
+
if (targetId !== undefined)
|
|
240
|
+
admitIdentity(entry, targetId);
|
|
241
|
+
entry.deadline.removeEventListener("abort", entry.expire);
|
|
242
|
+
entry.active = false;
|
|
243
|
+
creation = undefined;
|
|
244
|
+
try {
|
|
245
|
+
settleTokens(entry, targetId);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
entry.failed = true;
|
|
249
|
+
fail(error);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function beginCreation() {
|
|
254
|
+
check();
|
|
255
|
+
if (creation)
|
|
256
|
+
throw new Error("Private target creation already pending");
|
|
257
|
+
if (active.size >= maxTargets)
|
|
258
|
+
throw new Error("Private target active capacity exceeded");
|
|
259
|
+
const entry = {
|
|
260
|
+
active: true,
|
|
261
|
+
tokens: new Map(),
|
|
262
|
+
destroyed: new Set(),
|
|
263
|
+
deadline: AbortSignal.timeout(creationTimeoutMs),
|
|
264
|
+
expire: () => fail(new Error("Private target creation identity timed out")),
|
|
265
|
+
};
|
|
266
|
+
creation = entry;
|
|
267
|
+
entry.deadline.addEventListener("abort", entry.expire, { once: true });
|
|
268
|
+
try {
|
|
269
|
+
for (const client of clients)
|
|
270
|
+
entry.tokens.set(client, client.guard.beginCreation());
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
fail(error);
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
commit(targetId) {
|
|
278
|
+
finish(entry, targetId);
|
|
279
|
+
},
|
|
280
|
+
rollback() {
|
|
281
|
+
finish(entry);
|
|
282
|
+
},
|
|
283
|
+
fail(error) {
|
|
284
|
+
if (entry.failed)
|
|
285
|
+
return;
|
|
286
|
+
if (!entry.active || creation !== entry)
|
|
287
|
+
throw new Error("Private creation is no longer active");
|
|
288
|
+
fail(error);
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function rememberDestroyed(targetId) {
|
|
293
|
+
if (!creation)
|
|
294
|
+
return;
|
|
295
|
+
if (creation.destroyed.size >= maxTargets) {
|
|
296
|
+
const error = new Error("Private creation retirement capacity exceeded");
|
|
297
|
+
fail(error);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
creation.destroyed.add(targetId);
|
|
301
|
+
}
|
|
302
|
+
function observe(event) {
|
|
303
|
+
check();
|
|
304
|
+
if (event.method !== "Target.targetDestroyed")
|
|
305
|
+
return;
|
|
306
|
+
const targetId = event.params?.["targetId"];
|
|
307
|
+
if (!targetIdentity(targetId)) {
|
|
308
|
+
const error = new Error("Invalid native private retirement identity");
|
|
309
|
+
fail(error);
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
if (!active.delete(targetId)) {
|
|
313
|
+
rememberDestroyed(targetId);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
retired.add(targetId);
|
|
317
|
+
if (retired.size > maxTargets)
|
|
318
|
+
retired.delete(retired.values().next().value);
|
|
319
|
+
for (const client of clients)
|
|
320
|
+
client.upstream.onmessage?.({
|
|
321
|
+
method: event.method,
|
|
322
|
+
params: { targetId },
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
wrap,
|
|
327
|
+
beginCreation,
|
|
328
|
+
observe,
|
|
329
|
+
close() {
|
|
330
|
+
closeClients(new Error("Private browser coordinator closed"));
|
|
331
|
+
closing ??= Promise.allSettled([...rawClosures]).then(() => {
|
|
332
|
+
if (errors.length)
|
|
333
|
+
throw new AggregateError([...errors], "Private browser transport cleanup failed");
|
|
334
|
+
});
|
|
335
|
+
return closing;
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { captureRunCodeState, replaceRunCodeContextInitScripts, restoreRunCodePageState, restoreRunCodeTimeouts } from './browser-run-code-native.js';
|
|
2
|
+
import { restoreRunCodeContextState } from './browser-run-code-context-state.js';
|
|
3
|
+
import { parseRunCodeState, serializeRunCodeState } from './browser-run-code-state.js';
|
|
4
|
+
const provider = '@cloudflare/playwright@1.3.6';
|
|
5
|
+
/** Reuse reconnect state transfer, with ordered new pages instead of old target identities. */
|
|
6
|
+
export function browserProfileRuntime(browser, context) {
|
|
7
|
+
return {
|
|
8
|
+
async capture(signal) {
|
|
9
|
+
signal.throwIfAborted();
|
|
10
|
+
const state = captureRunCodeState(browser, context);
|
|
11
|
+
serializeRunCodeState(state);
|
|
12
|
+
return { provider, state };
|
|
13
|
+
},
|
|
14
|
+
async restore(value, signal) {
|
|
15
|
+
signal.throwIfAborted();
|
|
16
|
+
if (!value || typeof value !== 'object' || !('provider' in value) || value.provider !== provider || !('state' in value))
|
|
17
|
+
throw new Error('Unsupported Cloudflare browser profile settings');
|
|
18
|
+
const state = parseRunCodeState(JSON.stringify(value.state));
|
|
19
|
+
const pages = context.pages();
|
|
20
|
+
if (state.pages.length !== pages.length && !(state.pages.length === 0 && pages.length === 1))
|
|
21
|
+
throw new Error('Cloudflare browser profile tab settings mismatch');
|
|
22
|
+
await restoreRunCodeContextState(context, state.context);
|
|
23
|
+
restoreRunCodeTimeouts(context, state.contextTimeouts);
|
|
24
|
+
signal.throwIfAborted();
|
|
25
|
+
await replaceRunCodeContextInitScripts(browser, context, state.contextInitScripts);
|
|
26
|
+
for (let index = 0; index < state.pages.length; index++) {
|
|
27
|
+
signal.throwIfAborted();
|
|
28
|
+
await restoreRunCodePageState(browser, pages[index], state.pages[index]);
|
|
29
|
+
}
|
|
30
|
+
signal.throwIfAborted();
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { PlaywrightResourceLimitError } from "@poe-platform/safe-bash/playwright";
|
|
2
|
+
export const MAX_RUN_CODE_FRAME_BYTES = 32 * 1024 * 1024;
|
|
3
|
+
export const MAX_RUN_CODE_BYTES = 64 * 1024 * 1024;
|
|
4
|
+
export const MAX_RUN_CODE_FRAMES = 4096;
|
|
5
|
+
export const MAX_RUN_CODE_CONTEXTS = 16;
|
|
6
|
+
export const MAX_RUN_CODE_TARGETS = 64;
|
|
7
|
+
function codePointByteLength(character) {
|
|
8
|
+
const codePoint = character.codePointAt(0);
|
|
9
|
+
if (codePoint <= 0x7f)
|
|
10
|
+
return 1;
|
|
11
|
+
if (codePoint <= 0x7ff)
|
|
12
|
+
return 2;
|
|
13
|
+
return codePoint <= 0xffff ? 3 : 4;
|
|
14
|
+
}
|
|
15
|
+
/** O(n) in code points, O(1) auxiliary memory; never materializes encoded frames. */
|
|
16
|
+
export function frameByteLength(message) {
|
|
17
|
+
let bytes = 0;
|
|
18
|
+
for (const character of message) {
|
|
19
|
+
bytes += codePointByteLength(character);
|
|
20
|
+
if (bytes > MAX_RUN_CODE_FRAME_BYTES)
|
|
21
|
+
throw new PlaywrightResourceLimitError("Run-code CDP frame limit exceeded");
|
|
22
|
+
}
|
|
23
|
+
return bytes;
|
|
24
|
+
}
|
|
25
|
+
/** O(n) in frame bytes; retained accounting is bounded by the frame limit. */
|
|
26
|
+
export function createRunCodeFrameBudget() {
|
|
27
|
+
let frames = 0;
|
|
28
|
+
let bytes = 0;
|
|
29
|
+
return (message) => {
|
|
30
|
+
if (typeof message !== "string" ||
|
|
31
|
+
message.length > MAX_RUN_CODE_FRAME_BYTES)
|
|
32
|
+
throw new PlaywrightResourceLimitError("Run-code CDP frame limit exceeded");
|
|
33
|
+
const size = frameByteLength(message);
|
|
34
|
+
frames++;
|
|
35
|
+
bytes += size;
|
|
36
|
+
if (size > MAX_RUN_CODE_FRAME_BYTES ||
|
|
37
|
+
frames > MAX_RUN_CODE_FRAMES ||
|
|
38
|
+
bytes > MAX_RUN_CODE_BYTES)
|
|
39
|
+
throw new PlaywrightResourceLimitError("Run-code CDP transport limit exceeded");
|
|
40
|
+
const parsed = JSON.parse(message);
|
|
41
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
42
|
+
throw new Error("Invalid run-code CDP message");
|
|
43
|
+
return parsed;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Reservations count before forwarding, including commands awaiting replies. */
|
|
47
|
+
export function createRunCodeCreationBudget(options) {
|
|
48
|
+
const pages = new Set(options.pages);
|
|
49
|
+
const contexts = new Set(options.contexts);
|
|
50
|
+
const pending = new Map();
|
|
51
|
+
const disposals = new Map();
|
|
52
|
+
const groups = {
|
|
53
|
+
page: { ids: pages, limit: options.maxPages, field: "targetId" },
|
|
54
|
+
context: {
|
|
55
|
+
ids: contexts,
|
|
56
|
+
limit: MAX_RUN_CODE_CONTEXTS,
|
|
57
|
+
field: "browserContextId",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const count = (kind) => [...pending.values()].filter((value) => value === kind).length;
|
|
61
|
+
const reserveDisposal = (message) => {
|
|
62
|
+
if (!Number.isSafeInteger(message.id) ||
|
|
63
|
+
pending.has(message.id) ||
|
|
64
|
+
disposals.has(message.id))
|
|
65
|
+
throw new Error("Invalid run-code creation command ID");
|
|
66
|
+
const id = message.params?.["browserContextId"];
|
|
67
|
+
if (typeof id === "string")
|
|
68
|
+
disposals.set(message.id, id);
|
|
69
|
+
};
|
|
70
|
+
const confirmDisposal = (message) => {
|
|
71
|
+
const disposed = disposals.get(message.id);
|
|
72
|
+
if (disposed === undefined)
|
|
73
|
+
return false;
|
|
74
|
+
disposals.delete(message.id);
|
|
75
|
+
if (!message.error)
|
|
76
|
+
contexts.delete(disposed);
|
|
77
|
+
return true;
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
command(message) {
|
|
81
|
+
if (message.method === "Target.disposeBrowserContext") {
|
|
82
|
+
reserveDisposal(message);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const kind = creationKind(message.method);
|
|
86
|
+
if (!kind)
|
|
87
|
+
return;
|
|
88
|
+
if (!Number.isSafeInteger(message.id) ||
|
|
89
|
+
pending.has(message.id) ||
|
|
90
|
+
disposals.has(message.id))
|
|
91
|
+
throw new Error("Invalid run-code creation command ID");
|
|
92
|
+
const group = groups[kind];
|
|
93
|
+
if (group.ids.size + count(kind) >= group.limit)
|
|
94
|
+
throw new PlaywrightResourceLimitError(`Run-code ${kind} limit exceeded`);
|
|
95
|
+
pending.set(message.id, kind);
|
|
96
|
+
},
|
|
97
|
+
reply(message) {
|
|
98
|
+
if (message.id === undefined)
|
|
99
|
+
return;
|
|
100
|
+
if (confirmDisposal(message))
|
|
101
|
+
return;
|
|
102
|
+
const kind = pending.get(message.id);
|
|
103
|
+
if (!kind)
|
|
104
|
+
return;
|
|
105
|
+
pending.delete(message.id);
|
|
106
|
+
if (message.error)
|
|
107
|
+
return;
|
|
108
|
+
const group = groups[kind];
|
|
109
|
+
const id = message.result?.[group.field];
|
|
110
|
+
if (typeof id !== "string")
|
|
111
|
+
throw new Error("Invalid CDP creation response");
|
|
112
|
+
group.ids.add(id);
|
|
113
|
+
},
|
|
114
|
+
pageCreated(id) {
|
|
115
|
+
pages.add(id);
|
|
116
|
+
if (pages.size > options.maxPages)
|
|
117
|
+
throw new PlaywrightResourceLimitError("Run-code page limit exceeded");
|
|
118
|
+
},
|
|
119
|
+
pageDestroyed(id) {
|
|
120
|
+
pages.delete(id);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function creationKind(method) {
|
|
125
|
+
if (method === "Target.createTarget")
|
|
126
|
+
return "page";
|
|
127
|
+
if (method === "Target.createBrowserContext")
|
|
128
|
+
return "context";
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function header(value) {
|
|
2
|
+
if (!value ||
|
|
3
|
+
typeof value !== "object" ||
|
|
4
|
+
!("name" in value) ||
|
|
5
|
+
!("value" in value))
|
|
6
|
+
return false;
|
|
7
|
+
if (typeof value.name !== "string" || typeof value.value !== "string")
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
new Headers([[value.name, value.value]]);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function geolocation(value) {
|
|
18
|
+
if (value === null)
|
|
19
|
+
return true;
|
|
20
|
+
if (!value ||
|
|
21
|
+
typeof value !== "object" ||
|
|
22
|
+
!("latitude" in value) ||
|
|
23
|
+
!("longitude" in value))
|
|
24
|
+
return false;
|
|
25
|
+
return (typeof value.latitude === "number" &&
|
|
26
|
+
Number.isFinite(value.latitude) &&
|
|
27
|
+
Math.abs(value.latitude) <= 90 &&
|
|
28
|
+
typeof value.longitude === "number" &&
|
|
29
|
+
Number.isFinite(value.longitude) &&
|
|
30
|
+
Math.abs(value.longitude) <= 180 &&
|
|
31
|
+
(!("accuracy" in value) ||
|
|
32
|
+
(typeof value.accuracy === "number" &&
|
|
33
|
+
Number.isFinite(value.accuracy) &&
|
|
34
|
+
value.accuracy >= 0)));
|
|
35
|
+
}
|
|
36
|
+
/** The completion envelope already bounds aggregate bytes; preserve native API ranges. */
|
|
37
|
+
export function isRunCodeContextState(value) {
|
|
38
|
+
return (!!value &&
|
|
39
|
+
typeof value === "object" &&
|
|
40
|
+
"headers" in value &&
|
|
41
|
+
Array.isArray(value.headers) &&
|
|
42
|
+
value.headers.every(header) &&
|
|
43
|
+
"offline" in value &&
|
|
44
|
+
typeof value.offline === "boolean" &&
|
|
45
|
+
"geolocation" in value &&
|
|
46
|
+
geolocation(value.geolocation));
|
|
47
|
+
}
|
|
48
|
+
export async function restoreRunCodeContextState(context, state) {
|
|
49
|
+
await Promise.all([
|
|
50
|
+
context.setExtraHTTPHeaders(Object.fromEntries(state.headers.map(({ name, value }) => [name, value]))),
|
|
51
|
+
context.setOffline(state.offline),
|
|
52
|
+
context.setGeolocation(state.geolocation),
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const BROWSER_RUN_CODE_URL = "http://fake.host/v1/devtools/browser/owned?persistent=true";
|