codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +27 -0
- package/contracts/v1/fixtures/backend-capabilities.json +40 -1
- package/contracts/v1/fixtures/backend-compatibility.json +21 -0
- package/contracts/v1/fixtures/backend-version.json +6 -1
- package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
- package/contracts/v1/fixtures/operation-action.json +13 -0
- package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
- package/contracts/v1/fixtures/operation-blocker.json +10 -0
- package/contracts/v1/fixtures/operation-collect-request.json +17 -0
- package/contracts/v1/fixtures/operation-collect-result.json +42 -0
- package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
- package/contracts/v1/fixtures/operation-control-request.json +18 -0
- package/contracts/v1/fixtures/operation-control-result.json +31 -0
- package/contracts/v1/fixtures/operation-event.json +18 -0
- package/contracts/v1/fixtures/operation-handle.json +10 -0
- package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
- package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
- package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
- package/contracts/v1/fixtures/operation-receipt.json +32 -0
- package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
- package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
- package/contracts/v1/fixtures/operation-request.json +33 -0
- package/contracts/v1/fixtures/operation-state.json +144 -0
- package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
- package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
- package/contracts/v1/fixtures/operation-submit-result.json +16 -0
- package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
- package/contracts/v1/manifest.json +155 -1
- package/contracts/v1/parity-suite.json +96 -1
- package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
- package/contracts/v1/schemas/backend-request.schema.json +8 -2
- package/contracts/v1/schemas/capabilities.schema.json +81 -1
- package/contracts/v1/schemas/manifest.schema.json +57 -0
- package/contracts/v1/schemas/operation-action.schema.json +155 -0
- package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
- package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
- package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
- package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
- package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
- package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
- package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
- package/contracts/v1/schemas/operation-event.schema.json +756 -0
- package/contracts/v1/schemas/operation-handle.schema.json +55 -0
- package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
- package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
- package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
- package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
- package/contracts/v1/schemas/operation-request.schema.json +158 -0
- package/contracts/v1/schemas/operation-state.schema.json +691 -0
- package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
- package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
- package/contracts/v1/surface-drift-policy.json +50 -0
- package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
- package/dist/codex-chatgpt-control-backend.mjs +43897 -9094
- package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
- package/dist/src/backend/client.d.ts +103 -2
- package/dist/src/backend/client.js +1436 -97
- package/dist/src/backend/compatibility.d.ts +12 -0
- package/dist/src/backend/compatibility.js +208 -0
- package/dist/src/backend/protocol.d.ts +84 -1
- package/dist/src/backend/protocol.js +31 -5
- package/dist/src/backend/runtime-identity.d.ts +10 -0
- package/dist/src/backend/runtime-identity.js +141 -0
- package/dist/src/backend/session.d.ts +10 -2
- package/dist/src/backend/session.js +478 -7
- package/dist/src/backend/stdio-server.d.ts +4 -1
- package/dist/src/backend/stdio-server.js +240 -28
- package/dist/src/browser/active-composer-file-input.d.ts +9 -0
- package/dist/src/browser/active-composer-file-input.js +33 -0
- package/dist/src/browser/attach.d.ts +4 -1
- package/dist/src/browser/attach.js +136 -28
- package/dist/src/browser/downloads.d.ts +2 -0
- package/dist/src/client.d.ts +38 -0
- package/dist/src/client.js +1222 -59
- package/dist/src/commands/configuration.js +13 -7
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +26 -3
- package/dist/src/commands/experience.js +35 -0
- package/dist/src/commands/files.js +4 -27
- package/dist/src/commands/modes.js +49 -17
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +627 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +7 -2
- package/dist/src/commands/work.js +31 -0
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +9 -0
- package/dist/src/operations/artifact-output.d.ts +69 -0
- package/dist/src/operations/artifact-output.js +1748 -0
- package/dist/src/operations/artifact-stream.d.ts +19 -0
- package/dist/src/operations/artifact-stream.js +35 -0
- package/dist/src/operations/artifact-transfer.d.ts +149 -0
- package/dist/src/operations/artifact-transfer.js +1312 -0
- package/dist/src/operations/browser-adapter.d.ts +161 -0
- package/dist/src/operations/browser-adapter.js +2023 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1208 -0
- package/dist/src/operations/browser-target.d.ts +103 -0
- package/dist/src/operations/browser-target.js +489 -0
- package/dist/src/operations/canonical.d.ts +4 -0
- package/dist/src/operations/canonical.js +412 -0
- package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
- package/dist/src/operations/chatgpt-runtime.js +1150 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1143 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -0
- package/dist/src/operations/control.d.ts +372 -0
- package/dist/src/operations/control.js +1498 -0
- package/dist/src/operations/file-identity.d.ts +33 -0
- package/dist/src/operations/file-identity.js +153 -0
- package/dist/src/operations/handle.d.ts +19 -0
- package/dist/src/operations/handle.js +727 -0
- package/dist/src/operations/index.d.ts +22 -0
- package/dist/src/operations/index.js +22 -0
- package/dist/src/operations/journal.d.ts +142 -0
- package/dist/src/operations/journal.js +1931 -0
- package/dist/src/operations/production-attachments.d.ts +116 -0
- package/dist/src/operations/production-attachments.js +1018 -0
- package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
- package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
- package/dist/src/operations/production-chatgpt-attachments.d.ts +71 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1363 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +1073 -0
- package/dist/src/operations/production-work-steer.d.ts +203 -0
- package/dist/src/operations/production-work-steer.js +1218 -0
- package/dist/src/operations/recovery.d.ts +80 -0
- package/dist/src/operations/recovery.js +174 -0
- package/dist/src/operations/runtime-adapter.d.ts +118 -0
- package/dist/src/operations/runtime-adapter.js +786 -0
- package/dist/src/operations/send-once.d.ts +224 -0
- package/dist/src/operations/send-once.js +1081 -0
- package/dist/src/operations/service.d.ts +281 -0
- package/dist/src/operations/service.js +2810 -0
- package/dist/src/operations/staging.d.ts +136 -0
- package/dist/src/operations/staging.js +630 -0
- package/dist/src/operations/state-machine.d.ts +29 -0
- package/dist/src/operations/state-machine.js +2047 -0
- package/dist/src/operations/submission.d.ts +423 -0
- package/dist/src/operations/submission.js +1676 -0
- package/dist/src/operations/turn-ownership.d.ts +202 -0
- package/dist/src/operations/turn-ownership.js +700 -0
- package/dist/src/operations/types.d.ts +454 -0
- package/dist/src/operations/types.js +20 -0
- package/dist/src/operations/wire-requests.d.ts +21 -0
- package/dist/src/operations/wire-requests.js +396 -0
- package/dist/src/operations/wire-results.d.ts +97 -0
- package/dist/src/operations/wire-results.js +818 -0
- package/dist/src/runner/responses.d.ts +3 -1
- package/dist/src/runner/responses.js +19 -1
- package/dist/src/runner/result.js +227 -55
- package/dist/src/runner/types.d.ts +12 -0
- package/dist/src/runtime/command-routing.d.ts +149 -0
- package/dist/src/runtime/command-routing.js +431 -0
- package/dist/src/runtime/coordinated-browser.d.ts +24 -0
- package/dist/src/runtime/coordinated-browser.js +315 -0
- package/dist/src/runtime/coordinated-page.d.ts +39 -0
- package/dist/src/runtime/coordinated-page.js +634 -0
- package/dist/src/runtime/operation-context.d.ts +142 -0
- package/dist/src/runtime/operation-context.js +410 -0
- package/dist/src/runtime/runtime-session.d.ts +95 -0
- package/dist/src/runtime/runtime-session.js +314 -0
- package/dist/src/runtime/tab-coordinator.d.ts +200 -0
- package/dist/src/runtime/tab-coordinator.js +1200 -0
- package/dist/src/runtime/value-boundaries.d.ts +18 -0
- package/dist/src/runtime/value-boundaries.js +49 -0
- package/dist/src/safety/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -1
- package/dist/src/scripts/live-smoke/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
- package/dist/src/scripts/live-smoke/scenarios.js +110 -18
- package/dist/src/scripts/live-smoke/types.d.ts +3 -0
- package/dist/src/scripts/release-canary-module.js +17 -4
- package/dist/src/types.d.ts +38 -1
- package/package.json +1 -1
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +73 -0
- package/references/python-parity.md +72 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -0,0 +1,1146 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { lstat, open } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { coordinatedEventRegistrationBarrier } from "../runtime/coordinated-page.js";
|
|
5
|
+
import { isByteArrayView } from "../runtime/value-boundaries.js";
|
|
6
|
+
import { MAX_PROVIDER_CHUNK_BYTES } from "./artifact-stream.js";
|
|
7
|
+
export const PRODUCTION_CHATGPT_ARTIFACTS_SCHEMA_VERSION = "chatgpt.browser_control.production_artifacts.v1";
|
|
8
|
+
const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/u;
|
|
9
|
+
const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/u;
|
|
10
|
+
const CONTENT_DIGEST_PATTERN = /^[0-9a-f]{64}$/u;
|
|
11
|
+
const MAX_TIMEOUT_MS = 30_000;
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
13
|
+
const DEFAULT_MAX_BYTES = 128 * 1024 * 1024;
|
|
14
|
+
const MAX_MAX_BYTES = 512 * 1024 * 1024;
|
|
15
|
+
const DEFAULT_MAX_ARTIFACTS = 32;
|
|
16
|
+
const MAX_MAX_ARTIFACTS = 256;
|
|
17
|
+
const MAX_GRAPH_DEPTH = 12;
|
|
18
|
+
const MAX_GRAPH_NODES = 4_096;
|
|
19
|
+
const MAX_STRING_LENGTH = 4_096;
|
|
20
|
+
const MAX_MIME_LENGTH = 128;
|
|
21
|
+
const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024;
|
|
22
|
+
const DOWNLOAD_STREAM_CHUNK_BYTES = 64 * 1024;
|
|
23
|
+
const MAX_PROVIDER_CHUNKS = 65_536;
|
|
24
|
+
/**
|
|
25
|
+
* Create a request-local ChatGPT artifact source adapter.
|
|
26
|
+
*
|
|
27
|
+
* The adapter performs two exact DOM reads around the one browser mutation:
|
|
28
|
+
* the first proves the request's HMAC identity, while the second proves that
|
|
29
|
+
* the same turn/kind/ordinal facts are still present immediately before the
|
|
30
|
+
* click. A download event is armed once, before the click, and a click that
|
|
31
|
+
* rejects after acting is reconciled against only that one waiter.
|
|
32
|
+
*
|
|
33
|
+
* `acquireDownload` is the short browser-actor phase. Callers should release
|
|
34
|
+
* the same-tab transaction as soon as it resolves, then call
|
|
35
|
+
* `materializeDownload`; `openSource` is retained for non-actor callers that
|
|
36
|
+
* explicitly want the two phases composed.
|
|
37
|
+
*/
|
|
38
|
+
export function createProductionChatGPTArtifacts(options) {
|
|
39
|
+
const normalized = normalizeOptions(options);
|
|
40
|
+
// A DownloadLike is a request-local capability. WeakMap state prevents a
|
|
41
|
+
// caller from materializing an arbitrary/preexisting download or consuming
|
|
42
|
+
// one causal event twice, while not retaining raw paths or labels.
|
|
43
|
+
const acquiredDownloads = new WeakMap();
|
|
44
|
+
const acquireDownload = async (request, pageOverride) => {
|
|
45
|
+
const normalizedRequest = normalizeRequest(request);
|
|
46
|
+
const page = pageOverride ?? normalized.page;
|
|
47
|
+
if (normalizedRequest === undefined || page === undefined || !isSafeProviderObject(page)
|
|
48
|
+
|| !isSafeDataGraph(page, new Set(), 0, true)) {
|
|
49
|
+
throw providerError();
|
|
50
|
+
}
|
|
51
|
+
if (normalized.signal !== undefined && normalized.signal.aborted)
|
|
52
|
+
throw providerError();
|
|
53
|
+
const probeArguments = Object.freeze({
|
|
54
|
+
assistantTurnId: normalizedRequest.assistantTurnId,
|
|
55
|
+
kind: normalizedRequest.kind,
|
|
56
|
+
ordinal: normalizedRequest.ordinal,
|
|
57
|
+
maxArtifacts: normalized.maxArtifacts
|
|
58
|
+
});
|
|
59
|
+
const firstFacts = await boundedRead(() => evaluatePage(page, probeExactArtifactInBrowser, Object.freeze({
|
|
60
|
+
...probeArguments,
|
|
61
|
+
mode: "read"
|
|
62
|
+
})), normalized.timeoutMs);
|
|
63
|
+
const exactFacts = normalizeFacts(firstFacts, normalizedRequest, normalized.maxArtifacts);
|
|
64
|
+
if (exactFacts === undefined || !matchesRequest(exactFacts, normalizedRequest)) {
|
|
65
|
+
throw providerError();
|
|
66
|
+
}
|
|
67
|
+
const expectedDigest = artifactEvidenceDigest(normalized, normalizedRequest, exactFacts);
|
|
68
|
+
if (expectedDigest === undefined || expectedDigest !== normalizedRequest.sourceIdentityDigest) {
|
|
69
|
+
throw providerError();
|
|
70
|
+
}
|
|
71
|
+
if (normalized.signal?.aborted === true)
|
|
72
|
+
throw providerError();
|
|
73
|
+
const waiter = startDownloadWait(page, normalized.timeoutMs);
|
|
74
|
+
const preMutation = await settleDownloadBeforeMutation(waiter, normalized.timeoutMs);
|
|
75
|
+
if (preMutation !== undefined) {
|
|
76
|
+
// A resolved/rejected event before this request's click is stale or
|
|
77
|
+
// ambiguous. It cannot be made causal by reusing the waiter.
|
|
78
|
+
throw providerError();
|
|
79
|
+
}
|
|
80
|
+
const clickArguments = Object.freeze({
|
|
81
|
+
...probeArguments,
|
|
82
|
+
mode: "click",
|
|
83
|
+
expected: exactFacts
|
|
84
|
+
});
|
|
85
|
+
let clickMayHaveActed = false;
|
|
86
|
+
try {
|
|
87
|
+
// Once this call is issued, a bridge rejection cannot prove that the
|
|
88
|
+
// DOM click did not land. The causal download waiter decides whether a
|
|
89
|
+
// useful effect actually followed it.
|
|
90
|
+
// This is the sole browser mutation in the source phase. It must remain
|
|
91
|
+
// inside the same-tab actor until the bridge promise settles: a local
|
|
92
|
+
// timeout race could return the actor while the serialized callback is
|
|
93
|
+
// still able to invoke node.click() later. Read-only probes above retain
|
|
94
|
+
// boundedRead; this mutation deliberately has no competing deadline.
|
|
95
|
+
const clicked = await evaluatePage(page, probeExactArtifactInBrowser, clickArguments);
|
|
96
|
+
clickMayHaveActed = clicked === true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// A browser bridge can reject after delivering the gesture. The one
|
|
100
|
+
// download waiter below remains authoritative and is never retried.
|
|
101
|
+
clickMayHaveActed = true;
|
|
102
|
+
}
|
|
103
|
+
const downloadOutcome = await awaitDownload(waiter, normalized.timeoutMs);
|
|
104
|
+
if (!clickMayHaveActed || downloadOutcome.kind !== "success") {
|
|
105
|
+
throw providerError();
|
|
106
|
+
}
|
|
107
|
+
if (normalized.signal !== undefined && normalized.signal.aborted)
|
|
108
|
+
throw providerError();
|
|
109
|
+
const download = downloadOutcome.download;
|
|
110
|
+
if (acquiredDownloads.has(download))
|
|
111
|
+
throw providerError();
|
|
112
|
+
acquiredDownloads.set(download, "ready");
|
|
113
|
+
return download;
|
|
114
|
+
};
|
|
115
|
+
const materializeDownload = async (download) => {
|
|
116
|
+
if (!isSafeProviderObject(download))
|
|
117
|
+
throw providerError();
|
|
118
|
+
const state = acquiredDownloads.get(download);
|
|
119
|
+
if (state !== "ready")
|
|
120
|
+
throw providerError();
|
|
121
|
+
// Consume before the first await. A failed local effect is not silently
|
|
122
|
+
// retried because saveAs/path may already have partially acted.
|
|
123
|
+
acquiredDownloads.set(download, "consumed");
|
|
124
|
+
return await materializeDownloadBytes(download, normalized);
|
|
125
|
+
};
|
|
126
|
+
const openSource = async (request, pageOverride) => {
|
|
127
|
+
const download = await acquireDownload(request, pageOverride);
|
|
128
|
+
return await materializeDownload(download);
|
|
129
|
+
};
|
|
130
|
+
return Object.freeze({ acquireDownload, materializeDownload, openSource });
|
|
131
|
+
}
|
|
132
|
+
/** Explicit aliases used by callers that name the provider after its role. */
|
|
133
|
+
export const createChatGPTArtifactSourceProvider = createProductionChatGPTArtifacts;
|
|
134
|
+
export const createProductionChatGPTArtifactSource = createProductionChatGPTArtifacts;
|
|
135
|
+
export const createProductionChatGPTArtifactSourceProvider = createProductionChatGPTArtifacts;
|
|
136
|
+
/**
|
|
137
|
+
* This callback is deliberately self-contained: browser providers serialize
|
|
138
|
+
* the function body and do not preserve module closures. Keep every selector,
|
|
139
|
+
* bound, and helper inside the callback. It returns only bounded identity
|
|
140
|
+
* facts; URLs, file names, and message text are never read or returned.
|
|
141
|
+
*/
|
|
142
|
+
function probeExactArtifactInBrowser(args) {
|
|
143
|
+
const turnRootSelector = "[data-testid^='conversation-turn'],[data-conversation-turn-id],[data-turn-id],[data-message-id]";
|
|
144
|
+
const artifactNodeSelector = "[data-artifact-id],[data-file-id],[data-attachment-id],[data-image-id],[data-testid*='artifact' i],[data-testid*='file' i],[data-testid*='image' i],a[download]";
|
|
145
|
+
const identityPattern = /^[A-Za-z0-9._:-]{1,512}$/u;
|
|
146
|
+
const contentDigestPattern = /^[0-9a-f]{64}$/u;
|
|
147
|
+
const maximumArtifacts = 256;
|
|
148
|
+
const maximumNodes = 4_096;
|
|
149
|
+
const maximumArtifactBytes = 128 * 1024 * 1024;
|
|
150
|
+
const maximumMimeLength = 128;
|
|
151
|
+
const boundedIdentity = (value) => typeof value === "string" && identityPattern.test(value);
|
|
152
|
+
const artifactKind = (value) => value === "file" || value === "image" || value === "other";
|
|
153
|
+
const nonnegativeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
154
|
+
const positiveInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
155
|
+
const classify = (node) => {
|
|
156
|
+
const testId = (node.getAttribute("data-testid") ?? "").toLowerCase();
|
|
157
|
+
if (node.tagName.toLowerCase() === "img" || testId.includes("image") || node.hasAttribute("data-image-id"))
|
|
158
|
+
return "image";
|
|
159
|
+
if (testId.includes("file") || node.hasAttribute("data-file-id") || node.hasAttribute("data-attachment-id") || node.tagName.toLowerCase() === "a")
|
|
160
|
+
return "file";
|
|
161
|
+
return "other";
|
|
162
|
+
};
|
|
163
|
+
const maximumRoots = 256;
|
|
164
|
+
const addIdentities = (info, node) => {
|
|
165
|
+
for (const name of ["data-message-id", "data-turn-id", "data-conversation-turn-id"]) {
|
|
166
|
+
const value = node.getAttribute(name);
|
|
167
|
+
if (value === null || value.length === 0)
|
|
168
|
+
continue;
|
|
169
|
+
if (!boundedIdentity(value))
|
|
170
|
+
return false;
|
|
171
|
+
info.identities.add(value);
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* One bounded SHOW_ALL-equivalent traversal. Every node, including text and
|
|
177
|
+
* comments, consumes the global visit budget; only elements are matched or
|
|
178
|
+
* indexed. Root attribution is carried by the traversal stack, avoiding a
|
|
179
|
+
* per-candidate subtree rescan for nested conversation turns.
|
|
180
|
+
*/
|
|
181
|
+
const walkDocument = (boundary, artifactLimit) => {
|
|
182
|
+
try {
|
|
183
|
+
const roots = [];
|
|
184
|
+
const rootByNode = new Map();
|
|
185
|
+
const artifacts = [];
|
|
186
|
+
const artifactCounts = new Map();
|
|
187
|
+
const artifactOverflow = new Set();
|
|
188
|
+
const frames = [{
|
|
189
|
+
node: boundary,
|
|
190
|
+
nextChild: boundary.firstChild
|
|
191
|
+
}];
|
|
192
|
+
let visited = 0;
|
|
193
|
+
while (frames.length > 0) {
|
|
194
|
+
const frame = frames[frames.length - 1];
|
|
195
|
+
if (frame === undefined)
|
|
196
|
+
return undefined;
|
|
197
|
+
const child = frame.nextChild;
|
|
198
|
+
if (child !== null) {
|
|
199
|
+
visited += 1;
|
|
200
|
+
// Fail before touching any property on a node beyond the cap. This
|
|
201
|
+
// makes the sentinel boundary deterministic and keeps traversal O(N).
|
|
202
|
+
if (visited > maximumNodes)
|
|
203
|
+
return undefined;
|
|
204
|
+
frame.nextChild = child.nextSibling;
|
|
205
|
+
let nearestTurnRoot = frame.nearestTurnRoot;
|
|
206
|
+
let nearestTurnRootStart = frame.nearestTurnRootStart;
|
|
207
|
+
if (child.nodeType === 1) {
|
|
208
|
+
const element = child;
|
|
209
|
+
let isTurnRoot;
|
|
210
|
+
try {
|
|
211
|
+
isTurnRoot = element.matches(turnRootSelector);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
if (isTurnRoot) {
|
|
217
|
+
nearestTurnRoot = element;
|
|
218
|
+
nearestTurnRootStart = visited;
|
|
219
|
+
}
|
|
220
|
+
let isArtifact = false;
|
|
221
|
+
try {
|
|
222
|
+
isArtifact = element.matches(artifactNodeSelector);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
if (isArtifact) {
|
|
228
|
+
// The caller's artifact bound is per exact turn, not global to
|
|
229
|
+
// the page. Keep only bounded candidates for each owner and
|
|
230
|
+
// remember overflow so a selected owner fails closed later;
|
|
231
|
+
// unrelated historical turns cannot starve this request.
|
|
232
|
+
if (nearestTurnRoot !== undefined) {
|
|
233
|
+
const count = artifactCounts.get(nearestTurnRoot) ?? 0;
|
|
234
|
+
if (count >= artifactLimit)
|
|
235
|
+
artifactOverflow.add(nearestTurnRoot);
|
|
236
|
+
else {
|
|
237
|
+
if (artifacts.length >= maximumNodes)
|
|
238
|
+
return undefined;
|
|
239
|
+
artifactCounts.set(nearestTurnRoot, count + 1);
|
|
240
|
+
artifacts.push({ node: element, at: visited, owner: nearestTurnRoot });
|
|
241
|
+
}
|
|
242
|
+
} // Unowned artifacts are deliberately not materialized.
|
|
243
|
+
}
|
|
244
|
+
const role = element.getAttribute("data-message-author-role");
|
|
245
|
+
if (role !== null) {
|
|
246
|
+
const owner = nearestTurnRoot ?? element;
|
|
247
|
+
const ownerStart = nearestTurnRootStart ?? visited;
|
|
248
|
+
let info = rootByNode.get(owner);
|
|
249
|
+
if (info === undefined) {
|
|
250
|
+
if (roots.length >= maximumRoots)
|
|
251
|
+
return undefined;
|
|
252
|
+
info = {
|
|
253
|
+
node: owner,
|
|
254
|
+
start: ownerStart,
|
|
255
|
+
roles: 0,
|
|
256
|
+
allAssistant: true,
|
|
257
|
+
identities: new Set()
|
|
258
|
+
};
|
|
259
|
+
if (!addIdentities(info, owner))
|
|
260
|
+
return undefined;
|
|
261
|
+
rootByNode.set(owner, info);
|
|
262
|
+
roots.push(info);
|
|
263
|
+
}
|
|
264
|
+
info.roles += 1;
|
|
265
|
+
if (role !== "assistant")
|
|
266
|
+
info.allAssistant = false;
|
|
267
|
+
if (owner !== element && !addIdentities(info, element))
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
frames.push({
|
|
272
|
+
node: child,
|
|
273
|
+
nextChild: child.firstChild,
|
|
274
|
+
...(nearestTurnRoot === undefined ? {} : { nearestTurnRoot }),
|
|
275
|
+
...(nearestTurnRootStart === undefined ? {} : { nearestTurnRootStart })
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
frames.pop();
|
|
280
|
+
if (frame.node.nodeType === 1) {
|
|
281
|
+
const info = rootByNode.get(frame.node);
|
|
282
|
+
if (info !== undefined)
|
|
283
|
+
info.end = visited;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { roots, artifacts, artifactOverflow };
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// A hostile or partially detached DOM must fail closed without exposing
|
|
291
|
+
// an exception through the serialized browser callback.
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const documentRoot = globalThis.document;
|
|
296
|
+
if (documentRoot === undefined || (args.mode !== "read" && args.mode !== "click"))
|
|
297
|
+
return undefined;
|
|
298
|
+
if (!boundedIdentity(args.assistantTurnId) || !artifactKind(args.kind)
|
|
299
|
+
|| !nonnegativeInteger(args.ordinal) || !positiveInteger(args.maxArtifacts)
|
|
300
|
+
|| args.maxArtifacts > maximumArtifacts)
|
|
301
|
+
return undefined;
|
|
302
|
+
const walked = walkDocument(documentRoot, args.maxArtifacts);
|
|
303
|
+
if (walked === undefined)
|
|
304
|
+
return undefined;
|
|
305
|
+
const matchingRoots = walked.roots.filter(info => {
|
|
306
|
+
const only = info.identities.values().next().value;
|
|
307
|
+
return info.roles > 0
|
|
308
|
+
&& info.allAssistant
|
|
309
|
+
&& info.end !== undefined
|
|
310
|
+
&& info.identities.size === 1
|
|
311
|
+
&& typeof only === "string"
|
|
312
|
+
&& only === args.assistantTurnId;
|
|
313
|
+
});
|
|
314
|
+
if (matchingRoots.length !== 1)
|
|
315
|
+
return undefined;
|
|
316
|
+
const selectedRoot = matchingRoots[0];
|
|
317
|
+
if (selectedRoot === undefined || selectedRoot.end === undefined
|
|
318
|
+
|| walked.artifactOverflow.has(selectedRoot.node))
|
|
319
|
+
return undefined;
|
|
320
|
+
const nodes = [];
|
|
321
|
+
for (const artifact of walked.artifacts) {
|
|
322
|
+
// Preorder ranges are retained for diagnostics, but cannot establish
|
|
323
|
+
// ownership: a nested turn is contained in the outer range. Only the
|
|
324
|
+
// exact nearest turn root may authorize an artifact for this request.
|
|
325
|
+
if (artifact.owner !== selectedRoot.node)
|
|
326
|
+
continue;
|
|
327
|
+
if (artifact.at <= selectedRoot.start || artifact.at > selectedRoot.end)
|
|
328
|
+
continue;
|
|
329
|
+
if (nodes.length >= args.maxArtifacts)
|
|
330
|
+
return undefined;
|
|
331
|
+
nodes.push(artifact.node);
|
|
332
|
+
}
|
|
333
|
+
const seen = new Set();
|
|
334
|
+
const facts = [];
|
|
335
|
+
for (let ordinal = 0; ordinal < nodes.length; ordinal += 1) {
|
|
336
|
+
const node = nodes[ordinal];
|
|
337
|
+
if (node === undefined)
|
|
338
|
+
return undefined;
|
|
339
|
+
const strongIdentities = new Set();
|
|
340
|
+
for (const name of ["data-artifact-id", "data-file-id", "data-attachment-id", "data-image-id"]) {
|
|
341
|
+
const value = node.getAttribute(name);
|
|
342
|
+
if (value === null || value.length === 0)
|
|
343
|
+
continue;
|
|
344
|
+
if (!boundedIdentity(value))
|
|
345
|
+
return undefined;
|
|
346
|
+
strongIdentities.add(value);
|
|
347
|
+
}
|
|
348
|
+
if (strongIdentities.size > 1)
|
|
349
|
+
return undefined;
|
|
350
|
+
const firstStrongIdentity = strongIdentities.values().next().value;
|
|
351
|
+
const identity = (typeof firstStrongIdentity === "string" ? firstStrongIdentity : undefined)
|
|
352
|
+
?? node.getAttribute("data-testid") ?? "";
|
|
353
|
+
if (!boundedIdentity(identity) || seen.has(identity))
|
|
354
|
+
return undefined;
|
|
355
|
+
seen.add(identity);
|
|
356
|
+
const kind = classify(node);
|
|
357
|
+
const contentDigest = node.getAttribute("data-content-sha256") ?? node.getAttribute("data-sha256") ?? undefined;
|
|
358
|
+
if (contentDigest !== undefined && !contentDigestPattern.test(contentDigest))
|
|
359
|
+
return undefined;
|
|
360
|
+
const bytesRaw = node.getAttribute("data-bytes") ?? node.getAttribute("data-size") ?? undefined;
|
|
361
|
+
const bytes = bytesRaw === undefined ? undefined : Number(bytesRaw);
|
|
362
|
+
if (bytes !== undefined && (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > maximumArtifactBytes))
|
|
363
|
+
return undefined;
|
|
364
|
+
const mimeType = node.getAttribute("data-mime-type") ?? node.getAttribute("type") ?? undefined;
|
|
365
|
+
if (mimeType !== undefined && (mimeType.length > maximumMimeLength || mimeType.includes("\u0000")))
|
|
366
|
+
return undefined;
|
|
367
|
+
facts.push({
|
|
368
|
+
kind,
|
|
369
|
+
ordinal,
|
|
370
|
+
identity,
|
|
371
|
+
...(contentDigest === undefined ? {} : { contentDigest }),
|
|
372
|
+
...(bytes === undefined ? {} : { bytes }),
|
|
373
|
+
...(mimeType === undefined ? {} : { mimeType })
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
const selectedFacts = facts[args.ordinal];
|
|
377
|
+
if (selectedFacts === undefined)
|
|
378
|
+
return undefined;
|
|
379
|
+
if (args.mode === "read")
|
|
380
|
+
return selectedFacts;
|
|
381
|
+
const expected = args.expected;
|
|
382
|
+
if (expected === undefined
|
|
383
|
+
|| expected.kind !== selectedFacts.kind
|
|
384
|
+
|| expected.ordinal !== selectedFacts.ordinal
|
|
385
|
+
|| expected.identity !== selectedFacts.identity
|
|
386
|
+
|| expected.contentDigest !== selectedFacts.contentDigest
|
|
387
|
+
|| expected.bytes !== selectedFacts.bytes
|
|
388
|
+
|| expected.mimeType !== selectedFacts.mimeType)
|
|
389
|
+
return false;
|
|
390
|
+
const node = nodes[args.ordinal];
|
|
391
|
+
if (node === undefined)
|
|
392
|
+
return false;
|
|
393
|
+
// The exact turn/kind/ordinal node is the sole capability used. Do not read
|
|
394
|
+
// a human label, URL, or path and never try a second candidate.
|
|
395
|
+
node.click();
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
function normalizeOptions(value) {
|
|
399
|
+
const record = ownDataRecord(value, [
|
|
400
|
+
"page", "evidenceDigest", "tempDirectory", "timeoutMs", "maxBytes", "maxArtifacts", "signal"
|
|
401
|
+
]);
|
|
402
|
+
const evidenceDigest = readData(record, "evidenceDigest");
|
|
403
|
+
const page = readData(record, "page");
|
|
404
|
+
const tempDirectory = readData(record, "tempDirectory");
|
|
405
|
+
const timeoutMs = readData(record, "timeoutMs");
|
|
406
|
+
const maxBytes = readData(record, "maxBytes");
|
|
407
|
+
const maxArtifacts = readData(record, "maxArtifacts");
|
|
408
|
+
const signal = readData(record, "signal");
|
|
409
|
+
if (typeof evidenceDigest !== "function")
|
|
410
|
+
throw providerError();
|
|
411
|
+
if (page !== undefined && (!isSafeProviderObject(page) || safeMethod(page, "evaluate") === undefined || safeMethod(page, "waitForEvent") === undefined))
|
|
412
|
+
throw providerError();
|
|
413
|
+
if (tempDirectory !== undefined && (typeof tempDirectory !== "string" || !isAbsolute(tempDirectory) || !isSafeString(tempDirectory, MAX_STRING_LENGTH)))
|
|
414
|
+
throw providerError();
|
|
415
|
+
if (timeoutMs !== undefined && (!isPositiveSafeInteger(timeoutMs) || timeoutMs > MAX_TIMEOUT_MS))
|
|
416
|
+
throw providerError();
|
|
417
|
+
if (maxBytes !== undefined && (!isPositiveSafeInteger(maxBytes) || maxBytes > MAX_MAX_BYTES))
|
|
418
|
+
throw providerError();
|
|
419
|
+
if (maxArtifacts !== undefined && (!isPositiveSafeInteger(maxArtifacts) || maxArtifacts > MAX_MAX_ARTIFACTS))
|
|
420
|
+
throw providerError();
|
|
421
|
+
if (signal !== undefined && !isGenuineAbortSignal(signal))
|
|
422
|
+
throw providerError();
|
|
423
|
+
if (page !== undefined && !isSafeDataGraph(page, new Set(), 0, true))
|
|
424
|
+
throw providerError();
|
|
425
|
+
return Object.freeze({
|
|
426
|
+
...(page === undefined ? {} : { page: page }),
|
|
427
|
+
evidenceDigest: evidenceDigest,
|
|
428
|
+
// Kept as a validated compatibility option. Transactional materialization
|
|
429
|
+
// deliberately does not create or recursively remove paths supplied by a
|
|
430
|
+
// caller; it uses a provider stream or a retained O_NOFOLLOW file handle.
|
|
431
|
+
...(tempDirectory === undefined ? {} : { tempDirectory }),
|
|
432
|
+
timeoutMs: timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
433
|
+
maxBytes: maxBytes ?? DEFAULT_MAX_BYTES,
|
|
434
|
+
maxArtifacts: maxArtifacts ?? DEFAULT_MAX_ARTIFACTS,
|
|
435
|
+
...(signal === undefined ? {} : { signal: signal })
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
function normalizeRequest(value) {
|
|
439
|
+
if (!isPlainDataRecord(value))
|
|
440
|
+
return undefined;
|
|
441
|
+
const record = value;
|
|
442
|
+
if (!hasExactKeys(record, [
|
|
443
|
+
"operationId", "requestDigest", "targetBindingDigest", "assistantTurnId", "sourceIdentityDigest",
|
|
444
|
+
"kind", "ordinal", "transferActionId", "destinationIdentityDigest"
|
|
445
|
+
]))
|
|
446
|
+
return undefined;
|
|
447
|
+
const operationId = readData(record, "operationId");
|
|
448
|
+
const requestDigest = readData(record, "requestDigest");
|
|
449
|
+
const targetBindingDigest = readData(record, "targetBindingDigest");
|
|
450
|
+
const assistantTurnId = readData(record, "assistantTurnId");
|
|
451
|
+
const sourceIdentityDigest = readData(record, "sourceIdentityDigest");
|
|
452
|
+
const kind = readData(record, "kind");
|
|
453
|
+
const ordinal = readData(record, "ordinal");
|
|
454
|
+
const transferActionId = readData(record, "transferActionId");
|
|
455
|
+
const destinationIdentityDigest = readData(record, "destinationIdentityDigest");
|
|
456
|
+
if (!isBoundedId(operationId) || !isDigest(requestDigest) || !isDigest(targetBindingDigest)
|
|
457
|
+
|| !isBoundedId(assistantTurnId) || !isDigest(sourceIdentityDigest) || !isArtifactKind(kind)
|
|
458
|
+
|| !isNonnegativeSafeInteger(ordinal) || ordinal > MAX_MAX_ARTIFACTS
|
|
459
|
+
|| !isBoundedId(transferActionId) || !isDigest(destinationIdentityDigest))
|
|
460
|
+
return undefined;
|
|
461
|
+
return Object.freeze({
|
|
462
|
+
operationId,
|
|
463
|
+
requestDigest,
|
|
464
|
+
targetBindingDigest,
|
|
465
|
+
assistantTurnId,
|
|
466
|
+
sourceIdentityDigest,
|
|
467
|
+
kind,
|
|
468
|
+
ordinal,
|
|
469
|
+
transferActionId,
|
|
470
|
+
destinationIdentityDigest
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function artifactEvidenceDigest(options, request, facts) {
|
|
474
|
+
const material = Object.freeze({
|
|
475
|
+
operationId: request.operationId,
|
|
476
|
+
turnId: request.assistantTurnId,
|
|
477
|
+
ordinal: facts.ordinal,
|
|
478
|
+
kind: facts.kind,
|
|
479
|
+
identity: facts.identity,
|
|
480
|
+
...(facts.contentDigest === undefined ? {} : { contentDigest: facts.contentDigest }),
|
|
481
|
+
...(facts.bytes === undefined ? {} : { bytes: facts.bytes }),
|
|
482
|
+
...(facts.mimeType === undefined ? {} : { mimeType: facts.mimeType })
|
|
483
|
+
});
|
|
484
|
+
try {
|
|
485
|
+
const value = options.evidenceDigest("browser-observation-artifact", material);
|
|
486
|
+
return isDigest(value) ? value : undefined;
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function matchesRequest(facts, request) {
|
|
493
|
+
return facts.kind === request.kind && facts.ordinal === request.ordinal;
|
|
494
|
+
}
|
|
495
|
+
function normalizeFacts(value, request, maxArtifacts) {
|
|
496
|
+
if (!isPlainDataRecord(value))
|
|
497
|
+
return undefined;
|
|
498
|
+
const record = value;
|
|
499
|
+
if (!hasExactKeys(record, ["kind", "ordinal", "identity", "contentDigest", "bytes", "mimeType"], true))
|
|
500
|
+
return undefined;
|
|
501
|
+
const kind = readData(record, "kind");
|
|
502
|
+
const ordinal = readData(record, "ordinal");
|
|
503
|
+
const identity = readData(record, "identity");
|
|
504
|
+
const contentDigest = readData(record, "contentDigest");
|
|
505
|
+
const bytes = readData(record, "bytes");
|
|
506
|
+
const mimeType = readData(record, "mimeType");
|
|
507
|
+
if (!isArtifactKind(kind) || !isNonnegativeSafeInteger(ordinal) || ordinal >= maxArtifacts
|
|
508
|
+
|| !isBoundedId(identity)
|
|
509
|
+
|| (contentDigest !== undefined && !isContentDigest(contentDigest))
|
|
510
|
+
|| (bytes !== undefined && (!isNonnegativeSafeInteger(bytes) || bytes > MAX_ARTIFACT_BYTES))
|
|
511
|
+
|| (mimeType !== undefined && !isBoundedText(mimeType, MAX_MIME_LENGTH)))
|
|
512
|
+
return undefined;
|
|
513
|
+
if (ordinal !== request.ordinal || kind !== request.kind)
|
|
514
|
+
return undefined;
|
|
515
|
+
return Object.freeze({
|
|
516
|
+
kind,
|
|
517
|
+
ordinal,
|
|
518
|
+
identity,
|
|
519
|
+
...(contentDigest === undefined ? {} : { contentDigest }),
|
|
520
|
+
...(bytes === undefined ? {} : { bytes }),
|
|
521
|
+
...(mimeType === undefined ? {} : { mimeType })
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
function startDownloadWait(page, timeoutMs) {
|
|
525
|
+
const waitForEvent = safeMethod(page, "waitForEvent");
|
|
526
|
+
if (waitForEvent === undefined) {
|
|
527
|
+
const outcome = { kind: "rejected" };
|
|
528
|
+
return { promise: Promise.resolve(outcome), outcome };
|
|
529
|
+
}
|
|
530
|
+
let raw;
|
|
531
|
+
try {
|
|
532
|
+
raw = waitForEvent.call(page, "download", { timeout: timeoutMs, timeoutMs });
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
const outcome = { kind: "rejected" };
|
|
536
|
+
return { promise: Promise.resolve(outcome), outcome };
|
|
537
|
+
}
|
|
538
|
+
if (!isPromiseLike(raw)) {
|
|
539
|
+
const outcome = { kind: "rejected" };
|
|
540
|
+
return { promise: Promise.resolve(outcome), outcome };
|
|
541
|
+
}
|
|
542
|
+
const registration = coordinatedEventRegistrationBarrier(raw);
|
|
543
|
+
const wait = {
|
|
544
|
+
...(registration === undefined ? {} : { registration }),
|
|
545
|
+
promise: Promise.resolve(raw).then(value => {
|
|
546
|
+
const outcome = isSafeProviderObject(value) && (safeMethod(value, "createReadStream") !== undefined || safeMethod(value, "saveAs") !== undefined || safeMethod(value, "path") !== undefined)
|
|
547
|
+
? { kind: "success", download: value }
|
|
548
|
+
: { kind: "rejected" };
|
|
549
|
+
wait.outcome = outcome;
|
|
550
|
+
return outcome;
|
|
551
|
+
}, () => {
|
|
552
|
+
const outcome = { kind: "rejected" };
|
|
553
|
+
wait.outcome = outcome;
|
|
554
|
+
return outcome;
|
|
555
|
+
})
|
|
556
|
+
};
|
|
557
|
+
return wait;
|
|
558
|
+
}
|
|
559
|
+
async function settleDownloadBeforeMutation(wait, timeoutMs) {
|
|
560
|
+
if (wait.outcome !== undefined)
|
|
561
|
+
return wait.outcome;
|
|
562
|
+
if (wait.registration !== undefined) {
|
|
563
|
+
try {
|
|
564
|
+
await boundedRead(() => wait.registration, timeoutMs);
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
return { kind: "rejected" };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
await flushMicrotasks();
|
|
571
|
+
return wait.outcome;
|
|
572
|
+
}
|
|
573
|
+
async function awaitDownload(wait, timeoutMs) {
|
|
574
|
+
if (wait.outcome !== undefined)
|
|
575
|
+
return wait.outcome;
|
|
576
|
+
return await new Promise(resolveOutcome => {
|
|
577
|
+
let settled = false;
|
|
578
|
+
const timer = setTimeout(() => {
|
|
579
|
+
if (settled)
|
|
580
|
+
return;
|
|
581
|
+
settled = true;
|
|
582
|
+
resolveOutcome({ kind: "rejected" });
|
|
583
|
+
}, timeoutMs);
|
|
584
|
+
void wait.promise.then(value => {
|
|
585
|
+
if (settled)
|
|
586
|
+
return;
|
|
587
|
+
settled = true;
|
|
588
|
+
clearTimeout(timer);
|
|
589
|
+
resolveOutcome(value);
|
|
590
|
+
}, () => {
|
|
591
|
+
if (settled)
|
|
592
|
+
return;
|
|
593
|
+
settled = true;
|
|
594
|
+
clearTimeout(timer);
|
|
595
|
+
resolveOutcome({ kind: "rejected" });
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
/** Local-only phase implementation; no PageLike or browser callback is used. */
|
|
600
|
+
async function materializeDownloadBytes(download, options) {
|
|
601
|
+
if (options.signal?.aborted === true)
|
|
602
|
+
throw providerError();
|
|
603
|
+
// Playwright exposes createReadStream() as a capability. It avoids passing
|
|
604
|
+
// a caller-controlled path to saveAs(), and therefore avoids both the
|
|
605
|
+
// temporary-directory replacement race and recursive cleanup of an
|
|
606
|
+
// unverified path. The provider call and every subsequent iterator call are
|
|
607
|
+
// independently bounded; a late provider settlement is observed by the
|
|
608
|
+
// promise handler in boundedProviderCall and is never retried.
|
|
609
|
+
const createReadStream = safeMethod(download, "createReadStream");
|
|
610
|
+
if (createReadStream !== undefined) {
|
|
611
|
+
try {
|
|
612
|
+
const raw = await boundedProviderCall(() => createReadStream.call(download), options.timeoutMs);
|
|
613
|
+
if (!isObjectLike(raw))
|
|
614
|
+
throw providerError();
|
|
615
|
+
return boundedProviderByteStream(raw, options.maxBytes, options.timeoutMs, options.signal);
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
throw providerError();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// A path() result is opened with O_NOFOLLOW and retained as a FileHandle.
|
|
622
|
+
// The pre-open lstat and post-open fstat must describe the same inode; once
|
|
623
|
+
// retained, later replacement of the pathname cannot redirect the bytes
|
|
624
|
+
// read by the returned stream. The browser-owned source is never deleted.
|
|
625
|
+
const pathMethod = safeMethod(download, "path");
|
|
626
|
+
if (pathMethod !== undefined) {
|
|
627
|
+
let opened;
|
|
628
|
+
try {
|
|
629
|
+
const candidate = await boundedProviderCall(() => pathMethod.call(download), options.timeoutMs);
|
|
630
|
+
if (typeof candidate !== "string" || !isAbsolute(candidate) || !isSafeString(candidate, MAX_STRING_LENGTH)) {
|
|
631
|
+
throw providerError();
|
|
632
|
+
}
|
|
633
|
+
opened = await openBoundedFile(candidate, options.maxBytes);
|
|
634
|
+
if (options.signal !== undefined && options.signal.aborted)
|
|
635
|
+
throw providerError();
|
|
636
|
+
const stream = boundedFileByteStream(opened.handle, opened.snapshot, options.signal);
|
|
637
|
+
opened = undefined;
|
|
638
|
+
return stream;
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
if (opened !== undefined) {
|
|
642
|
+
await opened.handle.close().catch(() => undefined);
|
|
643
|
+
}
|
|
644
|
+
throw providerError();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
// saveAs(path) cannot be made capability-safe in pure Node: the provider
|
|
648
|
+
// receives a pathname and can replace either the directory or target after
|
|
649
|
+
// validation. Refuse this legacy-only surface rather than creating an
|
|
650
|
+
// operation-owned path that could be redirected or recursively removed.
|
|
651
|
+
throw providerError();
|
|
652
|
+
}
|
|
653
|
+
async function openBoundedFile(path, maxBytes) {
|
|
654
|
+
let handle;
|
|
655
|
+
try {
|
|
656
|
+
const before = await lstat(path, { bigint: true });
|
|
657
|
+
if (before.isSymbolicLink() || !before.isFile() || before.size > BigInt(maxBytes))
|
|
658
|
+
throw providerError();
|
|
659
|
+
handle = await open(path, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
660
|
+
const opened = await handle.stat({ bigint: true });
|
|
661
|
+
if (!sameFileSnapshot(before, opened) || opened.size > BigInt(maxBytes))
|
|
662
|
+
throw providerError();
|
|
663
|
+
return Object.freeze({ handle, snapshot: opened });
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
if (handle !== undefined) {
|
|
667
|
+
try {
|
|
668
|
+
await handle.close();
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
// The stable redacted provider error remains authoritative.
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
throw providerError();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function boundedFileByteStream(handle, snapshot, signal) {
|
|
678
|
+
let closed = false;
|
|
679
|
+
let position = 0;
|
|
680
|
+
let queue = Promise.resolve();
|
|
681
|
+
const finalize = async (verifySnapshot) => {
|
|
682
|
+
if (closed)
|
|
683
|
+
return;
|
|
684
|
+
closed = true;
|
|
685
|
+
let failed = false;
|
|
686
|
+
if (verifySnapshot) {
|
|
687
|
+
try {
|
|
688
|
+
const after = await handle.stat({ bigint: true });
|
|
689
|
+
if (!sameFileSnapshot(snapshot, after) || BigInt(position) !== snapshot.size)
|
|
690
|
+
failed = true;
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
failed = true;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
await handle.close();
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
failed = true;
|
|
701
|
+
}
|
|
702
|
+
if (failed)
|
|
703
|
+
throw providerError();
|
|
704
|
+
};
|
|
705
|
+
const serialized = (callback) => {
|
|
706
|
+
const result = queue.then(callback, callback);
|
|
707
|
+
queue = result.then(() => undefined, () => undefined);
|
|
708
|
+
return result;
|
|
709
|
+
};
|
|
710
|
+
const iterator = Object.freeze({
|
|
711
|
+
next: () => serialized(async () => {
|
|
712
|
+
if (closed)
|
|
713
|
+
return { done: true, value: undefined };
|
|
714
|
+
if (signal?.aborted === true) {
|
|
715
|
+
try {
|
|
716
|
+
await finalize(false);
|
|
717
|
+
}
|
|
718
|
+
catch { /* return the same redacted error */ }
|
|
719
|
+
throw providerError();
|
|
720
|
+
}
|
|
721
|
+
const total = Number(snapshot.size);
|
|
722
|
+
if (!Number.isSafeInteger(total) || total < 0 || position > total) {
|
|
723
|
+
try {
|
|
724
|
+
await finalize(false);
|
|
725
|
+
}
|
|
726
|
+
catch { /* return the same redacted error */ }
|
|
727
|
+
throw providerError();
|
|
728
|
+
}
|
|
729
|
+
if (position === total) {
|
|
730
|
+
await finalize(true);
|
|
731
|
+
return { done: true, value: undefined };
|
|
732
|
+
}
|
|
733
|
+
const length = Math.min(DOWNLOAD_STREAM_CHUNK_BYTES, total - position);
|
|
734
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
735
|
+
let bytesRead;
|
|
736
|
+
try {
|
|
737
|
+
bytesRead = (await handle.read(buffer, 0, length, position)).bytesRead;
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
try {
|
|
741
|
+
await finalize(false);
|
|
742
|
+
}
|
|
743
|
+
catch { /* return the same redacted error */ }
|
|
744
|
+
throw providerError();
|
|
745
|
+
}
|
|
746
|
+
if (!Number.isSafeInteger(bytesRead) || bytesRead <= 0 || bytesRead > length) {
|
|
747
|
+
try {
|
|
748
|
+
await finalize(false);
|
|
749
|
+
}
|
|
750
|
+
catch { /* return the same redacted error */ }
|
|
751
|
+
throw providerError();
|
|
752
|
+
}
|
|
753
|
+
position += bytesRead;
|
|
754
|
+
// Never expose the reusable Buffer slab or file-backed mutable storage.
|
|
755
|
+
return { done: false, value: Uint8Array.from(buffer.subarray(0, bytesRead)) };
|
|
756
|
+
}),
|
|
757
|
+
return: () => serialized(async () => {
|
|
758
|
+
await finalize(false);
|
|
759
|
+
return { done: true, value: undefined };
|
|
760
|
+
})
|
|
761
|
+
});
|
|
762
|
+
return Object.freeze({
|
|
763
|
+
// One capability has one cursor. Repeated calls cannot replay a private
|
|
764
|
+
// download or allocate an independent reader over the same file handle.
|
|
765
|
+
[Symbol.asyncIterator]: () => iterator
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Defensive bounded adapter for a provider-owned Readable/AsyncIterable.
|
|
770
|
+
* There is no local pathname to clean up, so a timed-out provider is
|
|
771
|
+
* quarantined by abandoning exactly one iterator; all late settlements are
|
|
772
|
+
* observed and no second read is ever issued.
|
|
773
|
+
*/
|
|
774
|
+
function boundedProviderByteStream(source, maxBytes, timeoutMs, signal) {
|
|
775
|
+
const iterator = providerAsyncIterator(source);
|
|
776
|
+
let closed = false;
|
|
777
|
+
let bytes = 0;
|
|
778
|
+
let chunks = 0;
|
|
779
|
+
let queue = Promise.resolve();
|
|
780
|
+
const close = async (waitForSettlement = true) => {
|
|
781
|
+
if (closed)
|
|
782
|
+
return;
|
|
783
|
+
closed = true;
|
|
784
|
+
const returnMethod = safeMethod(iterator, "return");
|
|
785
|
+
if (returnMethod === undefined)
|
|
786
|
+
return;
|
|
787
|
+
if (!waitForSettlement) {
|
|
788
|
+
try {
|
|
789
|
+
const late = returnMethod.call(iterator);
|
|
790
|
+
if (isPromiseLike(late))
|
|
791
|
+
void Promise.resolve(late).then(() => undefined, () => undefined);
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
// The provider is already quarantined after the preceding failure.
|
|
795
|
+
}
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
try {
|
|
799
|
+
await boundedProviderCall(() => returnMethod.call(iterator), timeoutMs);
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
// A provider return that times out is already quarantined. Its late
|
|
803
|
+
// settlement is observed by boundedProviderCall; never retry close.
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
const serialized = (callback) => {
|
|
807
|
+
const result = queue.then(callback, callback);
|
|
808
|
+
queue = result.then(() => undefined, () => undefined);
|
|
809
|
+
return result;
|
|
810
|
+
};
|
|
811
|
+
const next = () => serialized(async () => {
|
|
812
|
+
if (closed)
|
|
813
|
+
return { done: true, value: undefined };
|
|
814
|
+
if (signal?.aborted === true) {
|
|
815
|
+
await close(false);
|
|
816
|
+
throw providerError();
|
|
817
|
+
}
|
|
818
|
+
let raw;
|
|
819
|
+
try {
|
|
820
|
+
raw = await boundedProviderCall(() => iterator.next(), timeoutMs);
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
await close(false);
|
|
824
|
+
throw providerError();
|
|
825
|
+
}
|
|
826
|
+
if (!isObjectLike(raw)) {
|
|
827
|
+
await close(false);
|
|
828
|
+
throw providerError();
|
|
829
|
+
}
|
|
830
|
+
const done = readData(raw, "done");
|
|
831
|
+
if (done === true) {
|
|
832
|
+
closed = true;
|
|
833
|
+
return { done: true, value: undefined };
|
|
834
|
+
}
|
|
835
|
+
if (done !== false) {
|
|
836
|
+
await close(false);
|
|
837
|
+
throw providerError();
|
|
838
|
+
}
|
|
839
|
+
const value = readData(raw, "value");
|
|
840
|
+
if (!isByteArrayView(value)
|
|
841
|
+
|| value.byteLength > MAX_PROVIDER_CHUNK_BYTES
|
|
842
|
+
|| value.byteLength > maxBytes - bytes
|
|
843
|
+
|| chunks >= MAX_PROVIDER_CHUNKS) {
|
|
844
|
+
await close(false);
|
|
845
|
+
throw providerError();
|
|
846
|
+
}
|
|
847
|
+
chunks += 1;
|
|
848
|
+
bytes += value.byteLength;
|
|
849
|
+
return { done: false, value: Uint8Array.from(value) };
|
|
850
|
+
});
|
|
851
|
+
const returned = () => serialized(async () => {
|
|
852
|
+
await close();
|
|
853
|
+
return { done: true, value: undefined };
|
|
854
|
+
});
|
|
855
|
+
return Object.freeze({
|
|
856
|
+
[Symbol.asyncIterator]: () => Object.freeze({ next, return: returned })
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
function providerAsyncIterator(source) {
|
|
860
|
+
let current = source;
|
|
861
|
+
for (let depth = 0; current !== null && depth < MAX_GRAPH_DEPTH; depth += 1) {
|
|
862
|
+
let descriptor;
|
|
863
|
+
try {
|
|
864
|
+
descriptor = Object.getOwnPropertyDescriptor(current, Symbol.asyncIterator);
|
|
865
|
+
if (descriptor !== undefined) {
|
|
866
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined || typeof descriptor.value !== "function") {
|
|
867
|
+
throw providerError();
|
|
868
|
+
}
|
|
869
|
+
const iterator = Reflect.apply(descriptor.value, source, []);
|
|
870
|
+
if (!isObjectLike(iterator) || safeMethod(iterator, "next") === undefined)
|
|
871
|
+
throw providerError();
|
|
872
|
+
return iterator;
|
|
873
|
+
}
|
|
874
|
+
current = Object.getPrototypeOf(current);
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
throw providerError();
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
throw providerError();
|
|
881
|
+
}
|
|
882
|
+
function sameFileSnapshot(left, right) {
|
|
883
|
+
return left.isFile() && right.isFile()
|
|
884
|
+
&& !left.isSymbolicLink() && !right.isSymbolicLink()
|
|
885
|
+
&& left.dev === right.dev
|
|
886
|
+
&& left.ino === right.ino
|
|
887
|
+
&& left.size === right.size
|
|
888
|
+
&& left.mtimeNs === right.mtimeNs
|
|
889
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
890
|
+
}
|
|
891
|
+
async function evaluatePage(page, fn, args) {
|
|
892
|
+
const evaluate = safeMethod(page, "evaluate");
|
|
893
|
+
if (evaluate === undefined)
|
|
894
|
+
throw providerError();
|
|
895
|
+
const result = evaluate.call(page, fn, args, { timeoutMs: MAX_TIMEOUT_MS });
|
|
896
|
+
return await Promise.resolve(result);
|
|
897
|
+
}
|
|
898
|
+
async function boundedRead(callback, timeoutMs) {
|
|
899
|
+
const value = callback();
|
|
900
|
+
if (!isPromiseLike(value))
|
|
901
|
+
return value;
|
|
902
|
+
return await new Promise((resolveValue, rejectValue) => {
|
|
903
|
+
let settled = false;
|
|
904
|
+
const timer = setTimeout(() => {
|
|
905
|
+
if (settled)
|
|
906
|
+
return;
|
|
907
|
+
settled = true;
|
|
908
|
+
rejectValue(providerError());
|
|
909
|
+
}, timeoutMs);
|
|
910
|
+
Promise.resolve(value).then(result => {
|
|
911
|
+
if (settled)
|
|
912
|
+
return;
|
|
913
|
+
settled = true;
|
|
914
|
+
clearTimeout(timer);
|
|
915
|
+
resolveValue(result);
|
|
916
|
+
}, () => {
|
|
917
|
+
if (settled)
|
|
918
|
+
return;
|
|
919
|
+
settled = true;
|
|
920
|
+
clearTimeout(timer);
|
|
921
|
+
rejectValue(providerError());
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Bound one provider promise while retaining a rejection/settlement observer
|
|
927
|
+
* for a call that outlives the local deadline. Promise.race alone would leave
|
|
928
|
+
* a late provider failure unhandled and could make a caller retry an effect
|
|
929
|
+
* that is still acting. The observer intentionally performs no second effect.
|
|
930
|
+
*/
|
|
931
|
+
async function boundedProviderCall(callback, timeoutMs) {
|
|
932
|
+
let value;
|
|
933
|
+
try {
|
|
934
|
+
value = callback();
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
throw providerError();
|
|
938
|
+
}
|
|
939
|
+
if (!isPromiseLike(value))
|
|
940
|
+
return value;
|
|
941
|
+
const promise = Promise.resolve(value);
|
|
942
|
+
let settled = false;
|
|
943
|
+
const observed = promise.then(result => {
|
|
944
|
+
settled = true;
|
|
945
|
+
return result;
|
|
946
|
+
}, () => {
|
|
947
|
+
settled = true;
|
|
948
|
+
return undefined;
|
|
949
|
+
});
|
|
950
|
+
return await new Promise((resolveValue, rejectValue) => {
|
|
951
|
+
const timer = setTimeout(() => {
|
|
952
|
+
if (settled)
|
|
953
|
+
return;
|
|
954
|
+
rejectValue(providerError());
|
|
955
|
+
}, timeoutMs);
|
|
956
|
+
void observed.then(result => {
|
|
957
|
+
if (settled !== true)
|
|
958
|
+
return;
|
|
959
|
+
clearTimeout(timer);
|
|
960
|
+
// A rejection is represented as undefined by the observer. Treating an
|
|
961
|
+
// undefined result as a provider failure is safe for all call sites.
|
|
962
|
+
if (result === undefined)
|
|
963
|
+
rejectValue(providerError());
|
|
964
|
+
else
|
|
965
|
+
resolveValue(result);
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
function ownDataRecord(value, allowed) {
|
|
970
|
+
if (!isObjectLike(value) || Array.isArray(value))
|
|
971
|
+
throw providerError();
|
|
972
|
+
let prototype;
|
|
973
|
+
let descriptors;
|
|
974
|
+
try {
|
|
975
|
+
prototype = Object.getPrototypeOf(value);
|
|
976
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
throw providerError();
|
|
980
|
+
}
|
|
981
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
982
|
+
throw providerError();
|
|
983
|
+
const allowedSet = new Set(allowed);
|
|
984
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
985
|
+
if (typeof key !== "string" || !allowedSet.has(key))
|
|
986
|
+
throw providerError();
|
|
987
|
+
const descriptor = descriptors[key];
|
|
988
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
989
|
+
throw providerError();
|
|
990
|
+
}
|
|
991
|
+
return value;
|
|
992
|
+
}
|
|
993
|
+
function isPlainDataRecord(value) {
|
|
994
|
+
if (!isObjectLike(value) || Array.isArray(value))
|
|
995
|
+
return false;
|
|
996
|
+
let prototype;
|
|
997
|
+
let descriptors;
|
|
998
|
+
try {
|
|
999
|
+
prototype = Object.getPrototypeOf(value);
|
|
1000
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
1006
|
+
return false;
|
|
1007
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
1008
|
+
if (typeof key !== "string")
|
|
1009
|
+
return false;
|
|
1010
|
+
const descriptor = descriptors[key];
|
|
1011
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
function hasExactKeys(record, keys, optional = false) {
|
|
1017
|
+
const allowed = new Set(keys);
|
|
1018
|
+
for (const key of Object.keys(record))
|
|
1019
|
+
if (!allowed.has(key))
|
|
1020
|
+
return false;
|
|
1021
|
+
if (optional)
|
|
1022
|
+
return true;
|
|
1023
|
+
return keys.every(key => Object.prototype.hasOwnProperty.call(record, key));
|
|
1024
|
+
}
|
|
1025
|
+
function readData(value, key) {
|
|
1026
|
+
try {
|
|
1027
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1028
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1029
|
+
return undefined;
|
|
1030
|
+
return descriptor.value;
|
|
1031
|
+
}
|
|
1032
|
+
catch {
|
|
1033
|
+
return undefined;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function safeMethod(value, key) {
|
|
1037
|
+
let current = value;
|
|
1038
|
+
for (let depth = 0; current !== null && depth < MAX_GRAPH_DEPTH; depth += 1) {
|
|
1039
|
+
let descriptor;
|
|
1040
|
+
try {
|
|
1041
|
+
descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
1042
|
+
if (descriptor !== undefined) {
|
|
1043
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined || typeof descriptor.value !== "function")
|
|
1044
|
+
return undefined;
|
|
1045
|
+
return descriptor.value;
|
|
1046
|
+
}
|
|
1047
|
+
current = Object.getPrototypeOf(current);
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
return undefined;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return undefined;
|
|
1054
|
+
}
|
|
1055
|
+
function isSafeProviderObject(value) {
|
|
1056
|
+
if (!isObjectLike(value))
|
|
1057
|
+
return false;
|
|
1058
|
+
try {
|
|
1059
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1060
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
1061
|
+
if (typeof key !== "string")
|
|
1062
|
+
return false;
|
|
1063
|
+
const descriptor = descriptors[key];
|
|
1064
|
+
if (descriptor === undefined || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
catch {
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function isSafeDataGraph(value, seen, depth, capability = false) {
|
|
1074
|
+
if (value === null || typeof value !== "object")
|
|
1075
|
+
return value !== undefined && typeof value !== "function";
|
|
1076
|
+
if (seen.has(value))
|
|
1077
|
+
return true;
|
|
1078
|
+
if (depth > MAX_GRAPH_DEPTH || seen.size >= MAX_GRAPH_NODES)
|
|
1079
|
+
return false;
|
|
1080
|
+
seen.add(value);
|
|
1081
|
+
let prototype;
|
|
1082
|
+
let descriptors;
|
|
1083
|
+
try {
|
|
1084
|
+
prototype = Object.getPrototypeOf(value);
|
|
1085
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
if (!capability && prototype !== Object.prototype && prototype !== null && !Array.isArray(value))
|
|
1091
|
+
return false;
|
|
1092
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
1093
|
+
if (typeof key !== "string")
|
|
1094
|
+
return false;
|
|
1095
|
+
const descriptor = descriptors[key];
|
|
1096
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1097
|
+
return false;
|
|
1098
|
+
if (typeof descriptor.value === "function")
|
|
1099
|
+
continue;
|
|
1100
|
+
if (!isSafeDataGraph(descriptor.value, seen, depth + 1, false))
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
function isGenuineAbortSignal(value) {
|
|
1106
|
+
return typeof AbortSignal !== "undefined" && value instanceof AbortSignal;
|
|
1107
|
+
}
|
|
1108
|
+
function isObjectLike(value) {
|
|
1109
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
1110
|
+
}
|
|
1111
|
+
function isPromiseLike(value) {
|
|
1112
|
+
return isObjectLike(value) && safeMethod(value, "then") !== undefined;
|
|
1113
|
+
}
|
|
1114
|
+
function isArtifactKind(value) {
|
|
1115
|
+
return value === "file" || value === "image" || value === "other";
|
|
1116
|
+
}
|
|
1117
|
+
function isDigest(value) {
|
|
1118
|
+
return typeof value === "string" && DIGEST_PATTERN.test(value);
|
|
1119
|
+
}
|
|
1120
|
+
function isContentDigest(value) {
|
|
1121
|
+
return typeof value === "string" && CONTENT_DIGEST_PATTERN.test(value);
|
|
1122
|
+
}
|
|
1123
|
+
function isBoundedId(value) {
|
|
1124
|
+
return typeof value === "string" && ID_PATTERN.test(value);
|
|
1125
|
+
}
|
|
1126
|
+
function isBoundedText(value, max) {
|
|
1127
|
+
return typeof value === "string" && value.length <= max && !value.includes("\u0000");
|
|
1128
|
+
}
|
|
1129
|
+
function isSafeString(value, max) {
|
|
1130
|
+
return value.length > 0 && value.length <= max && !value.includes("\u0000");
|
|
1131
|
+
}
|
|
1132
|
+
function isPositiveSafeInteger(value) {
|
|
1133
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
1134
|
+
}
|
|
1135
|
+
function isNonnegativeSafeInteger(value) {
|
|
1136
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
1137
|
+
}
|
|
1138
|
+
function isErrno(value, code) {
|
|
1139
|
+
return isObjectLike(value) && readData(value, "code") === code;
|
|
1140
|
+
}
|
|
1141
|
+
function flushMicrotasks() {
|
|
1142
|
+
return Promise.resolve().then(() => undefined);
|
|
1143
|
+
}
|
|
1144
|
+
function providerError() {
|
|
1145
|
+
return new Error("ChatGPT artifact source is unavailable.");
|
|
1146
|
+
}
|