codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.4
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 +42 -0
- package/README.md +43 -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/download-blocked-by-browser.json +22 -0
- package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
- package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
- package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
- package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
- package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
- 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 +185 -1
- package/contracts/v1/parity-suite.json +106 -8
- 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 +45882 -9361
- package/dist/codex-chatgpt-control-journal.mjs +5547 -0
- package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
- package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
- package/dist/codex-chatgpt-control.bundle.mjs +48392 -10260
- 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/chatgpt-url.d.ts +2 -0
- package/dist/src/browser/chatgpt-url.js +9 -0
- package/dist/src/browser/downloads.d.ts +17 -1
- package/dist/src/browser/downloads.js +150 -28
- package/dist/src/browser/page-state.js +6 -0
- package/dist/src/client.d.ts +43 -0
- package/dist/src/client.js +1279 -59
- package/dist/src/commands/artifacts.js +6 -4
- package/dist/src/commands/chat-popover.d.ts +37 -0
- package/dist/src/commands/chat-popover.js +537 -0
- package/dist/src/commands/configuration.d.ts +5 -0
- package/dist/src/commands/configuration.js +134 -21
- package/dist/src/commands/context.js +10 -3
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +27 -4
- package/dist/src/commands/experience.d.ts +5 -0
- package/dist/src/commands/experience.js +104 -21
- package/dist/src/commands/files.js +14 -33
- package/dist/src/commands/modes.js +85 -21
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +669 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +19 -6
- package/dist/src/commands/work.js +31 -0
- package/dist/src/dom/composer-text.d.ts +6 -0
- package/dist/src/dom/composer-text.js +85 -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 +167 -0
- package/dist/src/operations/browser-adapter.js +2103 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1256 -0
- package/dist/src/operations/browser-target.d.ts +105 -0
- package/dist/src/operations/browser-target.js +531 -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 +1232 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1145 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -0
- package/dist/src/operations/configuration-routing.d.ts +4 -0
- package/dist/src/operations/configuration-routing.js +14 -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 +25 -0
- package/dist/src/operations/index.js +25 -0
- package/dist/src/operations/journal-authority.d.ts +12 -0
- package/dist/src/operations/journal-authority.js +1 -0
- package/dist/src/operations/journal-rpc-client.d.ts +12 -0
- package/dist/src/operations/journal-rpc-client.js +225 -0
- package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
- package/dist/src/operations/journal-rpc-protocol.js +214 -0
- package/dist/src/operations/journal-rpc-server.d.ts +15 -0
- package/dist/src/operations/journal-rpc-server.js +272 -0
- package/dist/src/operations/journal.d.ts +143 -0
- package/dist/src/operations/journal.js +1957 -0
- package/dist/src/operations/production-attachments.d.ts +118 -0
- package/dist/src/operations/production-attachments.js +1035 -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 +80 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1544 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +856 -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 +126 -0
- package/dist/src/operations/runtime-adapter.js +802 -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 +286 -0
- package/dist/src/operations/service.js +2831 -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/transactional-chat-power.d.ts +17 -0
- package/dist/src/operations/transactional-chat-power.js +212 -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 +41 -0
- package/dist/src/runtime/coordinated-page.js +651 -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/blockers.js +8 -1
- package/dist/src/safety/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -1
- package/dist/src/scripts/capture-surface-profile.js +7 -2
- package/dist/src/scripts/journal-server.d.ts +2 -0
- package/dist/src/scripts/journal-server.js +37 -0
- package/dist/src/scripts/live-smoke/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
- package/dist/src/scripts/live-smoke/scenarios.js +147 -30
- package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
- package/dist/src/scripts/live-smoke/transactional.js +254 -0
- 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 +8 -3
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/2026-09-06-journal-service.md +140 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +176 -0
- package/references/python-parity.md +137 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
import { types as nodeTypes } from "node:util";
|
|
2
|
+
import { coordinatedEventRegistrationBarrier } from "../runtime/coordinated-page.js";
|
|
3
|
+
import { isPlainDataRecord } from "../runtime/value-boundaries.js";
|
|
4
|
+
export const PRODUCTION_ATTACHMENT_SCHEMA_VERSION = "chatgpt.browser_control.production_attachments.v1";
|
|
5
|
+
const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/u;
|
|
6
|
+
const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/u;
|
|
7
|
+
const CAPABILITY_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9._:-]{0,127}$/u;
|
|
8
|
+
const MAX_FILES = 256;
|
|
9
|
+
const DEFAULT_MAX_CANDIDATES = 128;
|
|
10
|
+
const MAX_CANDIDATES = 512;
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
12
|
+
const MAX_TIMEOUT_MS = 30_000;
|
|
13
|
+
const MAX_DEADLINE_AT = Date.UTC(2100, 0, 1);
|
|
14
|
+
const MAX_GRAPH_DEPTH = 12;
|
|
15
|
+
const MAX_GRAPH_NODES = 4_096;
|
|
16
|
+
/**
|
|
17
|
+
* Build one request-scoped, non-repeatable attachment capability.
|
|
18
|
+
*
|
|
19
|
+
* The factory validates and freezes the entire identity graph before any
|
|
20
|
+
* provider callback can run. Handoff has one state machine: revalidate -> arm
|
|
21
|
+
* one chooser waiter -> resolve one visible control -> click at most once ->
|
|
22
|
+
* setFiles at most once. If either browser mutation can have happened and its
|
|
23
|
+
* outcome is not exact, the result is quarantined as uncertain.
|
|
24
|
+
*/
|
|
25
|
+
export function createProductionAttachmentPrimitive(options) {
|
|
26
|
+
const normalized = normalizeOptions(options);
|
|
27
|
+
return buildAttachmentPrimitive(normalized, snapshotFiles(normalized.files, normalized.identityDigest));
|
|
28
|
+
}
|
|
29
|
+
/** Snapshot caller input before awaiting a remote signing authority. */
|
|
30
|
+
export async function createProductionAttachmentPrimitiveAsync(options) {
|
|
31
|
+
const normalized = normalizeOptions(options);
|
|
32
|
+
let digests;
|
|
33
|
+
try {
|
|
34
|
+
digests = await Promise.all(normalized.files.map(async (file, ordinal) => await normalized.identityDigest(ordinal, file.manifest)));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error("attachment identity digest failed");
|
|
38
|
+
}
|
|
39
|
+
const snapshot = snapshotFiles(normalized.files, ordinal => digests[ordinal]);
|
|
40
|
+
return buildAttachmentPrimitive(normalized, snapshot);
|
|
41
|
+
}
|
|
42
|
+
function buildAttachmentPrimitive(normalized, snapshot) {
|
|
43
|
+
let handoffConsumed = false;
|
|
44
|
+
const observeAttachments = async (request, page, target) => {
|
|
45
|
+
const normalizedRequest = normalizeAttachmentRequest(request, snapshot);
|
|
46
|
+
if (normalizedRequest === undefined || !isSafeProviderObject(page) || !isSafeDataGraph(target)) {
|
|
47
|
+
return { status: "unavailable" };
|
|
48
|
+
}
|
|
49
|
+
let read;
|
|
50
|
+
try {
|
|
51
|
+
const result = normalized.observeSurface(cloneAttachmentRequest(normalizedRequest), page, target);
|
|
52
|
+
read = await boundedCallback(result, normalized.timeoutMs);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return { status: "unavailable" };
|
|
56
|
+
}
|
|
57
|
+
return normalizeSurfaceObservation(normalizedRequest, read, normalized.evidenceDigest);
|
|
58
|
+
};
|
|
59
|
+
const handoffFiles = async (request, page, target) => {
|
|
60
|
+
const normalizedRequest = normalizeHandoffRequest(request, snapshot);
|
|
61
|
+
if (normalizedRequest === undefined || !isSafeProviderObject(page) || !isSafeDataGraph(target)) {
|
|
62
|
+
return { status: "not_satisfied", blockerCode: "attachment_manifest_mismatch" };
|
|
63
|
+
}
|
|
64
|
+
if (normalizedRequest.manifest.count === 0) {
|
|
65
|
+
return { status: "not_satisfied", blockerCode: "attachment_manifest_mismatch" };
|
|
66
|
+
}
|
|
67
|
+
if (handoffConsumed) {
|
|
68
|
+
return { status: "uncertain", quarantine: "caller" };
|
|
69
|
+
}
|
|
70
|
+
handoffConsumed = true;
|
|
71
|
+
// The request deadline is the coordinator's absolute deadline. The
|
|
72
|
+
// provider timeout remains a local upper bound, so a provider callback can
|
|
73
|
+
// never outlive either budget. Once this request has entered the one-shot
|
|
74
|
+
// handoff state, any caller cancellation is observation-only even when it
|
|
75
|
+
// arrives before the first browser mutation.
|
|
76
|
+
const requestSignal = normalizedRequest.signal;
|
|
77
|
+
const requestDeadlineAt = normalizedRequest.deadlineAt;
|
|
78
|
+
const deadlineAt = Math.min(requestDeadlineAt ?? MAX_DEADLINE_AT, Date.now() + normalized.timeoutMs);
|
|
79
|
+
const requestCancellation = handoffCancellation(requestSignal, requestDeadlineAt);
|
|
80
|
+
if (requestCancellation !== undefined)
|
|
81
|
+
return requestCancellation;
|
|
82
|
+
const revalidated = await revalidateSnapshot(snapshot, normalized.revalidateFile, deadlineAt);
|
|
83
|
+
const afterRevalidationCancellation = handoffCancellation(requestSignal, requestDeadlineAt);
|
|
84
|
+
if (afterRevalidationCancellation !== undefined)
|
|
85
|
+
return afterRevalidationCancellation;
|
|
86
|
+
if (revalidated === "changed") {
|
|
87
|
+
return { status: "not_satisfied", blockerCode: "input_file_changed" };
|
|
88
|
+
}
|
|
89
|
+
if (revalidated === "timeout") {
|
|
90
|
+
return { status: "not_satisfied", blockerCode: "operation_timeout" };
|
|
91
|
+
}
|
|
92
|
+
const chooserBudget = remainingBudget(deadlineAt);
|
|
93
|
+
if (chooserBudget <= 0)
|
|
94
|
+
return { status: "not_satisfied", blockerCode: "operation_timeout" };
|
|
95
|
+
const waiter = startChooserWait(page, chooserBudget, requestSignal);
|
|
96
|
+
// A coordinated page registers the provider listener through its tab
|
|
97
|
+
// actor. Prove that short registration transaction completed, then drain
|
|
98
|
+
// one host turn for any outcome that was already scheduled before this
|
|
99
|
+
// operation's first activation. Such an outcome cannot be causal to the
|
|
100
|
+
// not-yet-issued click and must block the handoff.
|
|
101
|
+
const beforeActivation = await settleChooserBeforeMutation(waiter, deadlineAt, requestSignal);
|
|
102
|
+
const afterActivationFenceCancellation = handoffCancellation(requestSignal, requestDeadlineAt);
|
|
103
|
+
if (afterActivationFenceCancellation !== undefined)
|
|
104
|
+
return afterActivationFenceCancellation;
|
|
105
|
+
if (beforeActivation !== undefined) {
|
|
106
|
+
// A chooser that settled before this invocation's activation may belong
|
|
107
|
+
// to a human or an earlier browser action. Never feed request files to
|
|
108
|
+
// an event whose causal activation this primitive cannot prove.
|
|
109
|
+
if (beforeActivation.kind === "success")
|
|
110
|
+
return { status: "not_satisfied", blockerCode: "ambiguous_file_handoff" };
|
|
111
|
+
return beforeActivation.kind === "timeout"
|
|
112
|
+
? { status: "not_satisfied", blockerCode: "operation_timeout" }
|
|
113
|
+
: { status: "not_satisfied", blockerCode: "ambiguous_file_handoff" };
|
|
114
|
+
}
|
|
115
|
+
if (normalized.prepareActivation !== undefined) {
|
|
116
|
+
const preparationBudget = remainingBudget(deadlineAt);
|
|
117
|
+
if (preparationBudget <= 0)
|
|
118
|
+
return { status: "not_satisfied", blockerCode: "operation_timeout" };
|
|
119
|
+
const beforePreparationCancellation = handoffCancellation(requestSignal, requestDeadlineAt);
|
|
120
|
+
if (beforePreparationCancellation !== undefined)
|
|
121
|
+
return beforePreparationCancellation;
|
|
122
|
+
let preparation;
|
|
123
|
+
try {
|
|
124
|
+
const rawPreparation = normalized.prepareActivation(cloneHandoffRequest(normalizedRequest), page, target, { timeoutMs: preparationBudget });
|
|
125
|
+
preparation = await awaitMutatingCallback(rawPreparation);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return { status: "uncertain", quarantine: "provider" };
|
|
129
|
+
}
|
|
130
|
+
const afterPreparationCancellation = handoffCancellation(requestSignal, requestDeadlineAt);
|
|
131
|
+
if (afterPreparationCancellation !== undefined)
|
|
132
|
+
return afterPreparationCancellation;
|
|
133
|
+
if (!isPlainDataRecord(preparation))
|
|
134
|
+
return { status: "uncertain", quarantine: "provider" };
|
|
135
|
+
const preparationStatus = readData(preparation, "status");
|
|
136
|
+
if (preparationStatus === "not_satisfied") {
|
|
137
|
+
const blockerCode = readData(preparation, "blockerCode");
|
|
138
|
+
return blockerCode === "selector_drift" || blockerCode === "ambiguous_file_handoff" || blockerCode === "operation_timeout"
|
|
139
|
+
? { status: "not_satisfied", blockerCode }
|
|
140
|
+
: { status: "uncertain", quarantine: "provider" };
|
|
141
|
+
}
|
|
142
|
+
if (preparationStatus === "uncertain") {
|
|
143
|
+
const quarantine = readData(preparation, "quarantine");
|
|
144
|
+
return quarantine === "provider" || quarantine === "caller"
|
|
145
|
+
? { status: "uncertain", quarantine }
|
|
146
|
+
: { status: "uncertain", quarantine: "provider" };
|
|
147
|
+
}
|
|
148
|
+
const providerEvidenceDigest = readData(preparation, "providerEvidenceDigest");
|
|
149
|
+
if (preparationStatus !== "prepared" || !isDigest(providerEvidenceDigest)) {
|
|
150
|
+
return { status: "uncertain", quarantine: "provider" };
|
|
151
|
+
}
|
|
152
|
+
// If the preparation mutation delivered a chooser, it is not ours to
|
|
153
|
+
// consume unless the final activation below also occurs after this
|
|
154
|
+
// handoff invocation. A rejection means preparation may have acted.
|
|
155
|
+
const afterPreparation = await settleChooserBeforeMutation(waiter, deadlineAt, requestSignal);
|
|
156
|
+
if (afterPreparation !== undefined) {
|
|
157
|
+
return { status: "uncertain", quarantine: "provider" };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let activation;
|
|
161
|
+
const beforeResolveCancellation = handoffCancellation(requestSignal, requestDeadlineAt);
|
|
162
|
+
if (beforeResolveCancellation !== undefined)
|
|
163
|
+
return beforeResolveCancellation;
|
|
164
|
+
try {
|
|
165
|
+
const result = normalized.resolveActivation(cloneHandoffRequest(normalizedRequest), page, target);
|
|
166
|
+
activation = await boundedCallback(result, remainingBudget(deadlineAt));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return { status: "not_satisfied", blockerCode: "selector_drift" };
|
|
170
|
+
}
|
|
171
|
+
const afterResolveCancellation = handoffCancellation(requestSignal, requestDeadlineAt);
|
|
172
|
+
if (afterResolveCancellation !== undefined)
|
|
173
|
+
return afterResolveCancellation;
|
|
174
|
+
const beforeFinalActivation = await settleChooserBeforeMutation(waiter, deadlineAt, requestSignal);
|
|
175
|
+
if (beforeFinalActivation !== undefined) {
|
|
176
|
+
return { status: "not_satisfied", blockerCode: "ambiguous_file_handoff" };
|
|
177
|
+
}
|
|
178
|
+
if (activation === undefined || !validateActivation(activation, normalized.maxCandidates)) {
|
|
179
|
+
return { status: "not_satisfied", blockerCode: "selector_drift" };
|
|
180
|
+
}
|
|
181
|
+
const locator = readData(activation, "locator");
|
|
182
|
+
const providerActivation = safeMethod(activation, "activate");
|
|
183
|
+
let activate;
|
|
184
|
+
if (locator !== undefined) {
|
|
185
|
+
const locatorCount = await readLocatorCount(locator, remainingBudget(deadlineAt));
|
|
186
|
+
if (locatorCount !== 1) {
|
|
187
|
+
return { status: "not_satisfied", blockerCode: "selector_drift" };
|
|
188
|
+
}
|
|
189
|
+
const visible = await readLocatorVisible(locator, remainingBudget(deadlineAt));
|
|
190
|
+
if (visible !== true) {
|
|
191
|
+
return { status: "not_satisfied", blockerCode: "selector_drift" };
|
|
192
|
+
}
|
|
193
|
+
const click = safeMethod(locator, "click");
|
|
194
|
+
if (click === undefined) {
|
|
195
|
+
return { status: "not_satisfied", blockerCode: "selector_drift" };
|
|
196
|
+
}
|
|
197
|
+
activate = timeoutMs => click.call(locator, { timeout: timeoutMs, timeoutMs });
|
|
198
|
+
}
|
|
199
|
+
else if (providerActivation !== undefined) {
|
|
200
|
+
activate = timeoutMs => providerActivation.call(activation, { timeoutMs });
|
|
201
|
+
}
|
|
202
|
+
if (activate === undefined)
|
|
203
|
+
return { status: "not_satisfied", blockerCode: "selector_drift" };
|
|
204
|
+
const clickBudget = remainingBudget(deadlineAt);
|
|
205
|
+
if (clickBudget <= 0)
|
|
206
|
+
return { status: "not_satisfied", blockerCode: "operation_timeout" };
|
|
207
|
+
const beforeClickCancellation = handoffCancellation(requestSignal, requestDeadlineAt);
|
|
208
|
+
if (beforeClickCancellation !== undefined)
|
|
209
|
+
return beforeClickCancellation;
|
|
210
|
+
try {
|
|
211
|
+
await awaitMutatingCallback(activate(clickBudget));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// The chooser reconciliation below is authoritative. A bridge rejection
|
|
215
|
+
// may have followed a delivered browser gesture.
|
|
216
|
+
}
|
|
217
|
+
const afterClickCancellation = handoffCancellation(requestSignal, requestDeadlineAt);
|
|
218
|
+
if (afterClickCancellation !== undefined)
|
|
219
|
+
return afterClickCancellation;
|
|
220
|
+
// The activation has crossed the browser mutation boundary and its click
|
|
221
|
+
// promise has settled. A chooser event is an observation, not a pending
|
|
222
|
+
// mutation: bound that wait so a provider that ignores its own event
|
|
223
|
+
// timeout cannot strand the operation forever. A late chooser is merely
|
|
224
|
+
// recorded by the handled promise; no continuation can call setFiles.
|
|
225
|
+
// Once click has crossed the browser boundary, keep the native chooser
|
|
226
|
+
// reconciliation alive long enough for a late event even when the
|
|
227
|
+
// provider-local pre-activation budget elapsed. A caller/coordinator abort
|
|
228
|
+
// still short-circuits this observation and forbids setFiles.
|
|
229
|
+
const chooserTimeout = requestDeadlineAt === undefined
|
|
230
|
+
? normalized.timeoutMs
|
|
231
|
+
: Math.max(0, Math.min(normalized.timeoutMs, requestDeadlineAt - Date.now()));
|
|
232
|
+
const chooser = await awaitChooser(waiter, chooserTimeout, requestSignal);
|
|
233
|
+
if (chooser.kind === "aborted")
|
|
234
|
+
return { status: "uncertain", quarantine: "caller" };
|
|
235
|
+
if (chooser.kind === "success") {
|
|
236
|
+
const beforeSetFilesCancellation = handoffCancellation(requestSignal, requestDeadlineAt);
|
|
237
|
+
if (beforeSetFilesCancellation !== undefined)
|
|
238
|
+
return beforeSetFilesCancellation;
|
|
239
|
+
const result = await setChooserFilesOnce(chooser.chooser, snapshot, normalizedRequest, normalized, requestDeadlineAt === undefined
|
|
240
|
+
? normalized.timeoutMs
|
|
241
|
+
: Math.max(0, Math.min(normalized.timeoutMs, requestDeadlineAt - Date.now())));
|
|
242
|
+
// A click that rejected after delivering the gesture can still produce
|
|
243
|
+
// an exact chooser/setFiles result. Evidence remains authoritative.
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
if (chooser.kind === "timeout") {
|
|
247
|
+
return handoffCancellation(requestSignal, requestDeadlineAt)
|
|
248
|
+
?? { status: "uncertain", quarantine: "provider" };
|
|
249
|
+
}
|
|
250
|
+
// A visible activation occurred, but the chooser rejected. It is no longer
|
|
251
|
+
// safe to claim that no provider-side mutation happened.
|
|
252
|
+
return handoffCancellation(requestSignal, requestDeadlineAt)
|
|
253
|
+
?? { status: "uncertain", quarantine: "provider" };
|
|
254
|
+
};
|
|
255
|
+
const handoffFilesForAdapter = async (request, files, page, target) => {
|
|
256
|
+
if (!sameIdentityList(files, snapshot.files)) {
|
|
257
|
+
return { status: "not_satisfied", blockerCode: "attachment_manifest_mismatch" };
|
|
258
|
+
}
|
|
259
|
+
return await handoffFiles(request, page, target);
|
|
260
|
+
};
|
|
261
|
+
return Object.freeze({
|
|
262
|
+
observeAttachments,
|
|
263
|
+
handoffFiles,
|
|
264
|
+
handoffFilesForAdapter
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/** Alias retained for integrations that name the layer after its provider. */
|
|
268
|
+
export const createOperationProductionAttachments = createProductionAttachmentPrimitive;
|
|
269
|
+
function normalizeOptions(options) {
|
|
270
|
+
if (!isPlainDataRecord(options))
|
|
271
|
+
throw new Error("invalid attachment options");
|
|
272
|
+
const evidenceDigestValue = readData(options, "evidenceDigest");
|
|
273
|
+
const files = readData(options, "files");
|
|
274
|
+
const identityDigestValue = readData(options, "identityDigest");
|
|
275
|
+
const revalidateValue = readData(options, "revalidateFile");
|
|
276
|
+
const observeSurfaceValue = readData(options, "observeSurface");
|
|
277
|
+
const resolveActivationValue = readData(options, "resolveActivation");
|
|
278
|
+
const prepareActivationValue = readData(options, "prepareActivation");
|
|
279
|
+
const timeoutValue = readData(options, "timeoutMs");
|
|
280
|
+
const maxCandidateValue = readData(options, "maxCandidates");
|
|
281
|
+
if (typeof evidenceDigestValue !== "function"
|
|
282
|
+
|| typeof identityDigestValue !== "function"
|
|
283
|
+
|| !Array.isArray(files)
|
|
284
|
+
|| typeof observeSurfaceValue !== "function"
|
|
285
|
+
|| typeof resolveActivationValue !== "function"
|
|
286
|
+
|| typeof revalidateValue !== "function"
|
|
287
|
+
|| (prepareActivationValue !== undefined && typeof prepareActivationValue !== "function")) {
|
|
288
|
+
throw new Error("invalid attachment options");
|
|
289
|
+
}
|
|
290
|
+
const timeoutMs = timeoutValue === undefined ? DEFAULT_TIMEOUT_MS : timeoutValue;
|
|
291
|
+
const maxCandidates = maxCandidateValue === undefined ? DEFAULT_MAX_CANDIDATES : maxCandidateValue;
|
|
292
|
+
if (!isPositiveSafeInteger(timeoutMs) || timeoutMs > MAX_TIMEOUT_MS) {
|
|
293
|
+
throw new Error("invalid attachment timeout");
|
|
294
|
+
}
|
|
295
|
+
if (!isPositiveSafeInteger(maxCandidates) || maxCandidates > MAX_CANDIDATES) {
|
|
296
|
+
throw new Error("invalid attachment candidate bound");
|
|
297
|
+
}
|
|
298
|
+
const normalizedFiles = cloneIdentityList(files);
|
|
299
|
+
return {
|
|
300
|
+
files: normalizedFiles,
|
|
301
|
+
identityDigest: identityDigestValue,
|
|
302
|
+
evidenceDigest: evidenceDigestValue,
|
|
303
|
+
revalidateFile: revalidateValue,
|
|
304
|
+
observeSurface: observeSurfaceValue,
|
|
305
|
+
resolveActivation: resolveActivationValue,
|
|
306
|
+
prepareActivation: prepareActivationValue,
|
|
307
|
+
timeoutMs,
|
|
308
|
+
maxCandidates
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function snapshotFiles(files, identityDigest) {
|
|
312
|
+
if (files.length > MAX_FILES)
|
|
313
|
+
throw new Error("attachment manifest exceeds its bounded item limit");
|
|
314
|
+
const identityDigests = [];
|
|
315
|
+
for (let ordinal = 0; ordinal < files.length; ordinal += 1) {
|
|
316
|
+
const identity = files[ordinal];
|
|
317
|
+
if (identity === undefined)
|
|
318
|
+
throw new Error("attachment identity is missing");
|
|
319
|
+
let digest;
|
|
320
|
+
try {
|
|
321
|
+
digest = identityDigest(ordinal, identity.manifest);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
throw new Error("attachment identity digest failed");
|
|
325
|
+
}
|
|
326
|
+
if (isNativePromise(digest))
|
|
327
|
+
void digest.catch(() => undefined);
|
|
328
|
+
if (!isDigest(digest) || identityDigests.includes(digest))
|
|
329
|
+
throw new Error("attachment identity digest is invalid");
|
|
330
|
+
identityDigests.push(digest);
|
|
331
|
+
}
|
|
332
|
+
return Object.freeze({
|
|
333
|
+
files: Object.freeze([...files]),
|
|
334
|
+
identityDigests: Object.freeze(identityDigests)
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function normalizeAttachmentRequest(request, snapshot) {
|
|
338
|
+
if (!isPlainDataRecord(request))
|
|
339
|
+
return undefined;
|
|
340
|
+
const operationId = readData(request, "operationId");
|
|
341
|
+
const requestDigest = readData(request, "requestDigest");
|
|
342
|
+
const surface = readData(request, "surface");
|
|
343
|
+
const targetBindingDigest = readData(request, "targetBindingDigest");
|
|
344
|
+
const manifest = normalizeManifest(readData(request, "manifest"), snapshot);
|
|
345
|
+
if (typeof operationId !== "string" || !ID_PATTERN.test(operationId)
|
|
346
|
+
|| !isDigest(requestDigest)
|
|
347
|
+
|| (surface !== "chat" && surface !== "work")
|
|
348
|
+
|| !isDigest(targetBindingDigest)
|
|
349
|
+
|| manifest === undefined)
|
|
350
|
+
return undefined;
|
|
351
|
+
return { operationId, requestDigest, surface, targetBindingDigest, manifest };
|
|
352
|
+
}
|
|
353
|
+
function normalizeHandoffRequest(request, snapshot) {
|
|
354
|
+
if (!isPlainDataRecord(request))
|
|
355
|
+
return undefined;
|
|
356
|
+
const operationId = readData(request, "operationId");
|
|
357
|
+
const requestDigest = readData(request, "requestDigest");
|
|
358
|
+
const surface = readData(request, "surface");
|
|
359
|
+
const actionId = readData(request, "actionId");
|
|
360
|
+
const targetBindingDigest = readData(request, "targetBindingDigest");
|
|
361
|
+
const manifest = normalizeManifest(readData(request, "manifest"), snapshot);
|
|
362
|
+
const signal = readData(request, "signal");
|
|
363
|
+
const deadlineAt = readData(request, "deadlineAt");
|
|
364
|
+
if (typeof operationId !== "string" || !ID_PATTERN.test(operationId)
|
|
365
|
+
|| !isDigest(requestDigest)
|
|
366
|
+
|| (surface !== "chat" && surface !== "work")
|
|
367
|
+
|| typeof actionId !== "string" || !ID_PATTERN.test(actionId)
|
|
368
|
+
|| !isDigest(targetBindingDigest)
|
|
369
|
+
|| manifest === undefined
|
|
370
|
+
|| signal !== undefined && !isAbortSignal(signal)
|
|
371
|
+
|| deadlineAt !== undefined && (!Number.isSafeInteger(deadlineAt) || deadlineAt < 0 || deadlineAt > MAX_DEADLINE_AT))
|
|
372
|
+
return undefined;
|
|
373
|
+
return {
|
|
374
|
+
operationId,
|
|
375
|
+
requestDigest,
|
|
376
|
+
surface,
|
|
377
|
+
actionId,
|
|
378
|
+
targetBindingDigest,
|
|
379
|
+
manifest,
|
|
380
|
+
...(signal === undefined ? {} : { signal }),
|
|
381
|
+
...(deadlineAt === undefined ? {} : { deadlineAt })
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function normalizeManifest(value, snapshot) {
|
|
385
|
+
if (!isPlainDataRecord(value))
|
|
386
|
+
return undefined;
|
|
387
|
+
const count = readData(value, "count");
|
|
388
|
+
const orderPolicy = readData(value, "orderPolicy");
|
|
389
|
+
const identities = readData(value, "identities");
|
|
390
|
+
if (!isNonnegativeSafeInteger(count) || count > MAX_FILES || orderPolicy !== "exact" || !Array.isArray(identities) || !hasSafeArrayDescriptors(identities) || identities.length !== count || count !== snapshot.files.length)
|
|
391
|
+
return undefined;
|
|
392
|
+
const result = [];
|
|
393
|
+
const seen = new Set();
|
|
394
|
+
for (let ordinal = 0; ordinal < count; ordinal += 1) {
|
|
395
|
+
const entry = identities[ordinal];
|
|
396
|
+
if (!isPlainDataRecord(entry))
|
|
397
|
+
return undefined;
|
|
398
|
+
const identityDigest = readData(entry, "identityDigest");
|
|
399
|
+
const entryOrdinal = readData(entry, "ordinal");
|
|
400
|
+
if (!isDigest(identityDigest) || entryOrdinal !== ordinal || identityDigest !== snapshot.identityDigests[ordinal] || seen.has(identityDigest))
|
|
401
|
+
return undefined;
|
|
402
|
+
seen.add(identityDigest);
|
|
403
|
+
result.push({ identityDigest, ordinal });
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
count,
|
|
407
|
+
orderPolicy: "exact",
|
|
408
|
+
identities: Object.freeze(result)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
async function normalizeSurfaceObservation(request, read, evidenceDigest) {
|
|
412
|
+
if (!isPlainDataRecord(read))
|
|
413
|
+
return { status: "unavailable" };
|
|
414
|
+
const status = readData(read, "status");
|
|
415
|
+
const source = readData(read, "source");
|
|
416
|
+
const providerEvidenceDigest = readData(read, "providerEvidenceDigest");
|
|
417
|
+
if (source !== "live_surface" || typeof status !== "string")
|
|
418
|
+
return { status: "unavailable" };
|
|
419
|
+
if (providerEvidenceDigest !== undefined && !isDigest(providerEvidenceDigest))
|
|
420
|
+
return { status: "unavailable" };
|
|
421
|
+
if (status === "mismatch" || status === "delayed" || status === "ambiguous" || status === "unavailable") {
|
|
422
|
+
const evidence = await safeEvidence(evidenceDigest, "attachment-surface", {
|
|
423
|
+
operationId: request.operationId,
|
|
424
|
+
requestDigest: request.requestDigest,
|
|
425
|
+
targetBindingDigest: request.targetBindingDigest,
|
|
426
|
+
status,
|
|
427
|
+
providerEvidenceDigest
|
|
428
|
+
});
|
|
429
|
+
return evidence === undefined ? { status } : { status, evidenceDigest: evidence };
|
|
430
|
+
}
|
|
431
|
+
const count = readData(read, "count");
|
|
432
|
+
const identityDigests = readData(read, "identityDigests");
|
|
433
|
+
if (!isNonnegativeSafeInteger(count) || count > MAX_FILES || !Array.isArray(identityDigests) || !hasSafeArrayDescriptors(identityDigests) || identityDigests.length !== count) {
|
|
434
|
+
return { status: "unavailable" };
|
|
435
|
+
}
|
|
436
|
+
const observed = [];
|
|
437
|
+
const seen = new Set();
|
|
438
|
+
for (let index = 0; index < identityDigests.length; index += 1) {
|
|
439
|
+
const identity = identityDigests[index];
|
|
440
|
+
if (typeof identity !== "string" || !isDigest(identity) || seen.has(identity))
|
|
441
|
+
return { status: "ambiguous" };
|
|
442
|
+
seen.add(identity);
|
|
443
|
+
observed.push(identity);
|
|
444
|
+
}
|
|
445
|
+
if (status === "absent") {
|
|
446
|
+
// `request.manifest` is the desired non-empty envelope. An exact empty
|
|
447
|
+
// live surface is the precondition that authorizes the one durable
|
|
448
|
+
// handoff; requiring the desired manifest itself to be empty would make
|
|
449
|
+
// every real upload unreachable.
|
|
450
|
+
if (count !== 0 || observed.length !== 0 || request.manifest.count === 0 || providerEvidenceDigest === undefined)
|
|
451
|
+
return { status: "mismatch" };
|
|
452
|
+
const evidence = await safeEvidence(evidenceDigest, "attachment-surface", {
|
|
453
|
+
operationId: request.operationId,
|
|
454
|
+
requestDigest: request.requestDigest,
|
|
455
|
+
targetBindingDigest: request.targetBindingDigest,
|
|
456
|
+
status,
|
|
457
|
+
count: 0,
|
|
458
|
+
identityDigests: [],
|
|
459
|
+
providerEvidenceDigest
|
|
460
|
+
});
|
|
461
|
+
return evidence === undefined
|
|
462
|
+
? { status: "unavailable" }
|
|
463
|
+
: { status: "absent", evidenceDigest: evidence, count: 0, orderPolicy: "exact", identityDigests: [] };
|
|
464
|
+
}
|
|
465
|
+
if (status !== "exact")
|
|
466
|
+
return { status: "unavailable" };
|
|
467
|
+
const exact = providerEvidenceDigest !== undefined
|
|
468
|
+
&& count === request.manifest.count
|
|
469
|
+
&& observed.every((identity, index) => identity === request.manifest.identities[index]?.identityDigest);
|
|
470
|
+
const evidence = await safeEvidence(evidenceDigest, "attachment-surface", {
|
|
471
|
+
operationId: request.operationId,
|
|
472
|
+
requestDigest: request.requestDigest,
|
|
473
|
+
targetBindingDigest: request.targetBindingDigest,
|
|
474
|
+
status,
|
|
475
|
+
count,
|
|
476
|
+
identityDigests: observed,
|
|
477
|
+
providerEvidenceDigest
|
|
478
|
+
});
|
|
479
|
+
if (!exact)
|
|
480
|
+
return evidence === undefined ? { status: "mismatch" } : { status: "mismatch", evidenceDigest: evidence };
|
|
481
|
+
return evidence === undefined
|
|
482
|
+
? { status: "unavailable" }
|
|
483
|
+
: { status: "exact", evidenceDigest: evidence, count, orderPolicy: "exact", identityDigests: observed };
|
|
484
|
+
}
|
|
485
|
+
async function revalidateSnapshot(snapshot, revalidateFile, deadlineAt) {
|
|
486
|
+
for (const identity of snapshot.files) {
|
|
487
|
+
const budget = remainingBudget(deadlineAt);
|
|
488
|
+
if (budget <= 0)
|
|
489
|
+
return "timeout";
|
|
490
|
+
try {
|
|
491
|
+
const result = revalidateFile(identity);
|
|
492
|
+
await boundedCallback(result, budget);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
if (remainingBudget(deadlineAt) <= 0)
|
|
496
|
+
return "timeout";
|
|
497
|
+
return "changed";
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return "ok";
|
|
501
|
+
}
|
|
502
|
+
async function setChooserFilesOnce(chooser, snapshot, request, options, timeoutMs) {
|
|
503
|
+
if (timeoutMs <= 0)
|
|
504
|
+
return { status: "uncertain", quarantine: "provider" };
|
|
505
|
+
const beforeMutationCancellation = handoffCancellation(request.signal, request.deadlineAt);
|
|
506
|
+
if (beforeMutationCancellation !== undefined)
|
|
507
|
+
return beforeMutationCancellation;
|
|
508
|
+
// Chrome exposes FileChooser as a provider proxy around an object with
|
|
509
|
+
// private fields. Descriptor-walking to the prototype and calling that raw
|
|
510
|
+
// method with the proxy as `this` loses the provider's required binding.
|
|
511
|
+
// Read this one trusted capability through the proxy and retain its exact
|
|
512
|
+
// receiver, as the normal `chooser.setFiles(...)` API does.
|
|
513
|
+
const setFiles = providerCallable(chooser, "setFiles");
|
|
514
|
+
if (setFiles === undefined)
|
|
515
|
+
return { status: "uncertain", quarantine: "provider" };
|
|
516
|
+
const paths = snapshot.files.map(identity => identity.sourcePath);
|
|
517
|
+
let rawResult;
|
|
518
|
+
try {
|
|
519
|
+
rawResult = setFiles(paths, { timeout: timeoutMs, timeoutMs });
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return { status: "uncertain", quarantine: "provider" };
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
await awaitMutatingCallback(rawResult);
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
return { status: "uncertain", quarantine: "provider" };
|
|
529
|
+
}
|
|
530
|
+
// A cancellation during setFiles cannot prove whether the provider accepted
|
|
531
|
+
// the local paths. Awaiting the native promise above prevents actor release,
|
|
532
|
+
// but the result is still explicitly uncertain and must be reconciled by a
|
|
533
|
+
// later observation rather than retried.
|
|
534
|
+
if (request.signal?.aborted || request.deadlineAt !== undefined && Date.now() >= request.deadlineAt) {
|
|
535
|
+
return { status: "uncertain", quarantine: "caller" };
|
|
536
|
+
}
|
|
537
|
+
const evidence = await safeEvidence(options.evidenceDigest, "attachment-handoff", {
|
|
538
|
+
operationId: request.operationId,
|
|
539
|
+
requestDigest: request.requestDigest,
|
|
540
|
+
actionId: request.actionId,
|
|
541
|
+
targetBindingDigest: request.targetBindingDigest,
|
|
542
|
+
count: request.manifest.count,
|
|
543
|
+
identityDigests: request.manifest.identities.map(entry => entry.identityDigest),
|
|
544
|
+
status: "satisfied"
|
|
545
|
+
});
|
|
546
|
+
return evidence === undefined
|
|
547
|
+
? { status: "uncertain", quarantine: "provider" }
|
|
548
|
+
: { status: "satisfied", evidenceDigest: evidence };
|
|
549
|
+
}
|
|
550
|
+
function startChooserWait(page, timeoutMs, signal) {
|
|
551
|
+
const wait = {
|
|
552
|
+
promise: Promise.resolve({ kind: "rejected" })
|
|
553
|
+
};
|
|
554
|
+
if (signal?.aborted) {
|
|
555
|
+
wait.outcome = { kind: "aborted" };
|
|
556
|
+
wait.promise = Promise.resolve(wait.outcome);
|
|
557
|
+
return wait;
|
|
558
|
+
}
|
|
559
|
+
const waitForEvent = safeMethod(page, "waitForEvent");
|
|
560
|
+
if (waitForEvent === undefined) {
|
|
561
|
+
wait.outcome = { kind: "rejected" };
|
|
562
|
+
wait.promise = Promise.resolve(wait.outcome);
|
|
563
|
+
return wait;
|
|
564
|
+
}
|
|
565
|
+
let raw;
|
|
566
|
+
try {
|
|
567
|
+
// Registration is synchronous and happens before any provider activation.
|
|
568
|
+
raw = waitForEvent.call(page, "filechooser", { timeout: timeoutMs, timeoutMs });
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
wait.outcome = { kind: "rejected" };
|
|
572
|
+
wait.promise = Promise.resolve(wait.outcome);
|
|
573
|
+
return wait;
|
|
574
|
+
}
|
|
575
|
+
if (!isNativePromise(raw)) {
|
|
576
|
+
wait.outcome = { kind: "rejected" };
|
|
577
|
+
wait.promise = Promise.resolve(wait.outcome);
|
|
578
|
+
return wait;
|
|
579
|
+
}
|
|
580
|
+
const registration = coordinatedEventRegistrationBarrier(raw);
|
|
581
|
+
if (registration !== undefined)
|
|
582
|
+
wait.registration = registration;
|
|
583
|
+
const handled = raw.then(value => {
|
|
584
|
+
const outcome = !isSafeProviderObject(value) || safeMethod(value, "setFiles") === undefined
|
|
585
|
+
? { kind: "rejected" }
|
|
586
|
+
: { kind: "success", chooser: value };
|
|
587
|
+
wait.outcome = outcome;
|
|
588
|
+
return outcome;
|
|
589
|
+
}, () => {
|
|
590
|
+
const outcome = { kind: "rejected" };
|
|
591
|
+
wait.outcome = outcome;
|
|
592
|
+
return outcome;
|
|
593
|
+
});
|
|
594
|
+
wait.promise = handled;
|
|
595
|
+
return wait;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Fence a coordinated waitForEvent registration before a browser mutation.
|
|
599
|
+
* The additional host turn catches an already-scheduled provider rejection or
|
|
600
|
+
* stale chooser; neither can have been caused by the click that has not run.
|
|
601
|
+
*/
|
|
602
|
+
async function settleChooserBeforeMutation(waiter, deadlineAt, signal) {
|
|
603
|
+
if (waiter.outcome !== undefined)
|
|
604
|
+
return waiter.outcome;
|
|
605
|
+
if (signal?.aborted)
|
|
606
|
+
return { kind: "aborted" };
|
|
607
|
+
if (waiter.registration !== undefined) {
|
|
608
|
+
const budget = remainingBudget(deadlineAt);
|
|
609
|
+
if (budget <= 0)
|
|
610
|
+
return { kind: "timeout" };
|
|
611
|
+
const registration = await awaitRegistration(waiter.registration, budget, signal);
|
|
612
|
+
if (registration === "aborted")
|
|
613
|
+
return { kind: "aborted" };
|
|
614
|
+
if (registration === "timeout")
|
|
615
|
+
return { kind: "timeout" };
|
|
616
|
+
if (waiter.outcome !== undefined)
|
|
617
|
+
return waiter.outcome;
|
|
618
|
+
if (remainingBudget(deadlineAt) <= 0)
|
|
619
|
+
return { kind: "timeout" };
|
|
620
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
// An ordinary PageLike invokes waitForEvent in this stack. Native promise
|
|
624
|
+
// adoption needs only microtasks; avoid imposing a timer turn on a caller
|
|
625
|
+
// whose deliberately tiny provider timeout has already armed the waiter.
|
|
626
|
+
await flushMicrotasks();
|
|
627
|
+
}
|
|
628
|
+
if (signal?.aborted)
|
|
629
|
+
return { kind: "aborted" };
|
|
630
|
+
if (waiter.outcome !== undefined)
|
|
631
|
+
return waiter.outcome;
|
|
632
|
+
return remainingBudget(deadlineAt) <= 0 ? { kind: "timeout" } : undefined;
|
|
633
|
+
}
|
|
634
|
+
async function awaitRegistration(value, timeoutMs, signal) {
|
|
635
|
+
return await new Promise(resolve => {
|
|
636
|
+
let settled = false;
|
|
637
|
+
const timer = setTimeout(() => finish("timeout"), timeoutMs);
|
|
638
|
+
const onAbort = () => finish("aborted");
|
|
639
|
+
const finish = (result) => {
|
|
640
|
+
if (settled)
|
|
641
|
+
return;
|
|
642
|
+
settled = true;
|
|
643
|
+
clearTimeout(timer);
|
|
644
|
+
signal?.removeEventListener("abort", onAbort);
|
|
645
|
+
resolve(result);
|
|
646
|
+
};
|
|
647
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
648
|
+
if (signal?.aborted) {
|
|
649
|
+
finish("aborted");
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
void value.then(() => finish("settled"), () => finish("timeout"));
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
async function awaitChooser(waiter, timeoutMs, signal) {
|
|
656
|
+
if (waiter.outcome !== undefined)
|
|
657
|
+
return waiter.outcome;
|
|
658
|
+
return await new Promise(resolve => {
|
|
659
|
+
let settled = false;
|
|
660
|
+
const onAbort = () => finish({ kind: "aborted" });
|
|
661
|
+
const finish = (outcome) => {
|
|
662
|
+
if (settled)
|
|
663
|
+
return;
|
|
664
|
+
settled = true;
|
|
665
|
+
clearTimeout(timer);
|
|
666
|
+
signal?.removeEventListener("abort", onAbort);
|
|
667
|
+
resolve(outcome);
|
|
668
|
+
};
|
|
669
|
+
const timer = setTimeout(() => finish({ kind: "timeout" }), timeoutMs);
|
|
670
|
+
if (signal !== undefined) {
|
|
671
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
672
|
+
if (signal.aborted)
|
|
673
|
+
onAbort();
|
|
674
|
+
}
|
|
675
|
+
void waiter.promise.then(finish, () => finish({ kind: "rejected" }));
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function validateActivation(value, maxCandidates) {
|
|
679
|
+
if (!isPlainDataRecord(value))
|
|
680
|
+
return false;
|
|
681
|
+
const locator = readData(value, "locator");
|
|
682
|
+
const activate = readData(value, "activate");
|
|
683
|
+
const candidateCount = readData(value, "candidateCount");
|
|
684
|
+
const capabilityKey = readData(value, "capabilityKey");
|
|
685
|
+
const locatorActivation = isSafeProviderObject(locator) && activate === undefined;
|
|
686
|
+
const callbackActivation = locator === undefined && typeof activate === "function";
|
|
687
|
+
return (locatorActivation || callbackActivation)
|
|
688
|
+
&& Number.isSafeInteger(candidateCount)
|
|
689
|
+
&& candidateCount === 1
|
|
690
|
+
&& candidateCount <= maxCandidates
|
|
691
|
+
&& typeof capabilityKey === "string"
|
|
692
|
+
&& CAPABILITY_KEY_PATTERN.test(capabilityKey);
|
|
693
|
+
}
|
|
694
|
+
async function readLocatorCount(locator, timeoutMs) {
|
|
695
|
+
const count = safeMethod(locator, "count");
|
|
696
|
+
if (count === undefined)
|
|
697
|
+
return undefined;
|
|
698
|
+
try {
|
|
699
|
+
const value = await boundedCallback(count.call(locator), timeoutMs);
|
|
700
|
+
return isNonnegativeSafeInteger(value) && value <= MAX_CANDIDATES ? value : undefined;
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async function readLocatorVisible(locator, timeoutMs) {
|
|
707
|
+
const visible = safeMethod(locator, "isVisible");
|
|
708
|
+
if (visible === undefined)
|
|
709
|
+
return undefined;
|
|
710
|
+
try {
|
|
711
|
+
const value = await boundedCallback(visible.call(locator), timeoutMs);
|
|
712
|
+
return typeof value === "boolean" ? value : undefined;
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
return undefined;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async function boundedCallback(value, timeoutMs) {
|
|
719
|
+
// The browser contracts use native promises. Reject thenable proxies rather
|
|
720
|
+
// than reading a hostile `then` accessor through Promise.resolve().
|
|
721
|
+
if (isObjectLike(value) && !isNativePromise(value))
|
|
722
|
+
throw new Error("provider promise is not native");
|
|
723
|
+
if (!isNativePromise(value))
|
|
724
|
+
return value;
|
|
725
|
+
let timer;
|
|
726
|
+
const promise = new Promise((resolve, reject) => {
|
|
727
|
+
timer = setTimeout(() => reject(new Error("provider callback timed out")), timeoutMs);
|
|
728
|
+
value.then(resolve, reject);
|
|
729
|
+
});
|
|
730
|
+
try {
|
|
731
|
+
return await promise;
|
|
732
|
+
}
|
|
733
|
+
finally {
|
|
734
|
+
if (timer !== undefined)
|
|
735
|
+
clearTimeout(timer);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Mutating provider calls are deliberately not raced with a local timer.
|
|
740
|
+
* Browser bridges can deliver the click/setFiles action and reject or settle
|
|
741
|
+
* late; returning early would release a coordinated actor while that mutation
|
|
742
|
+
* is still able to land. The provider receives the timeout option, and the
|
|
743
|
+
* outer coordinator owns the transaction deadline/quarantine.
|
|
744
|
+
*/
|
|
745
|
+
async function awaitMutatingCallback(value) {
|
|
746
|
+
if (isObjectLike(value) && !isNativePromise(value))
|
|
747
|
+
throw new Error("provider promise is not native");
|
|
748
|
+
if (isNativePromise(value))
|
|
749
|
+
return await value;
|
|
750
|
+
return value;
|
|
751
|
+
}
|
|
752
|
+
function isNativePromise(value) {
|
|
753
|
+
try {
|
|
754
|
+
return nodeTypes.isPromise(value);
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function sameIdentityList(left, right) {
|
|
761
|
+
let normalized;
|
|
762
|
+
try {
|
|
763
|
+
normalized = cloneIdentityList(left);
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
if (normalized.length !== right.length)
|
|
769
|
+
return false;
|
|
770
|
+
for (let index = 0; index < right.length; index += 1) {
|
|
771
|
+
const candidate = normalized[index];
|
|
772
|
+
const expected = right[index];
|
|
773
|
+
if (candidate === undefined || expected === undefined || !sameIdentity(candidate, expected))
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
function sameIdentity(left, right) {
|
|
779
|
+
return left.sourcePath === right.sourcePath
|
|
780
|
+
&& left.manifest.displayName === right.manifest.displayName
|
|
781
|
+
&& left.manifest.bytes === right.manifest.bytes
|
|
782
|
+
&& left.manifest.contentSha256 === right.manifest.contentSha256
|
|
783
|
+
&& left.proof.device === right.proof.device
|
|
784
|
+
&& left.proof.inode === right.proof.inode
|
|
785
|
+
&& left.proof.size === right.proof.size
|
|
786
|
+
&& left.proof.modifiedNs === right.proof.modifiedNs
|
|
787
|
+
&& left.proof.changedNs === right.proof.changedNs;
|
|
788
|
+
}
|
|
789
|
+
function cloneIdentityList(value) {
|
|
790
|
+
if (!Array.isArray(value) || value.length > MAX_FILES || !hasSafeArrayDescriptors(value))
|
|
791
|
+
throw new Error("invalid attachment identities");
|
|
792
|
+
const result = [];
|
|
793
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
794
|
+
const identity = value[index];
|
|
795
|
+
if (identity === undefined || !isPlainDataRecord(identity))
|
|
796
|
+
throw new Error("invalid attachment identity");
|
|
797
|
+
const sourcePath = readData(identity, "sourcePath");
|
|
798
|
+
const manifest = cloneManifestEntry(readData(identity, "manifest"));
|
|
799
|
+
const proof = cloneProof(readData(identity, "proof"));
|
|
800
|
+
if (typeof sourcePath !== "string" || sourcePath.length === 0 || sourcePath.length > 4096 || /[\u0000-\u001f\u007f]/u.test(sourcePath) || manifest === undefined || proof === undefined)
|
|
801
|
+
throw new Error("invalid attachment identity");
|
|
802
|
+
result.push(Object.freeze({ sourcePath, manifest, proof }));
|
|
803
|
+
}
|
|
804
|
+
return Object.freeze(result);
|
|
805
|
+
}
|
|
806
|
+
function cloneManifestEntry(value) {
|
|
807
|
+
if (!isPlainDataRecord(value))
|
|
808
|
+
return undefined;
|
|
809
|
+
const displayName = readData(value, "displayName");
|
|
810
|
+
const bytes = readData(value, "bytes");
|
|
811
|
+
const contentSha256 = readData(value, "contentSha256");
|
|
812
|
+
if (typeof displayName !== "string" || displayName.length === 0 || displayName.length > 512 || /[\\/\u0000-\u001f\u007f]/u.test(displayName))
|
|
813
|
+
return undefined;
|
|
814
|
+
if (!isNonnegativeSafeInteger(bytes) || typeof contentSha256 !== "string" || !/^[0-9a-f]{64}$/u.test(contentSha256))
|
|
815
|
+
return undefined;
|
|
816
|
+
return Object.freeze({ displayName, bytes, contentSha256 });
|
|
817
|
+
}
|
|
818
|
+
function cloneProof(value) {
|
|
819
|
+
if (!isPlainDataRecord(value))
|
|
820
|
+
return undefined;
|
|
821
|
+
const device = readData(value, "device");
|
|
822
|
+
const inode = readData(value, "inode");
|
|
823
|
+
const size = readData(value, "size");
|
|
824
|
+
const modifiedNs = readData(value, "modifiedNs");
|
|
825
|
+
const changedNs = readData(value, "changedNs");
|
|
826
|
+
if (typeof device !== "string" || !/^[0-9]+$/u.test(device)
|
|
827
|
+
|| typeof inode !== "string" || !/^[0-9]+$/u.test(inode)
|
|
828
|
+
|| typeof size !== "string" || !/^[0-9]+$/u.test(size)
|
|
829
|
+
|| typeof modifiedNs !== "string" || !/^[0-9]+$/u.test(modifiedNs)
|
|
830
|
+
|| typeof changedNs !== "string" || !/^[0-9]+$/u.test(changedNs))
|
|
831
|
+
return undefined;
|
|
832
|
+
return Object.freeze({ device, inode, size, modifiedNs, changedNs });
|
|
833
|
+
}
|
|
834
|
+
function cloneAttachmentRequest(request) {
|
|
835
|
+
return Object.freeze({
|
|
836
|
+
operationId: request.operationId,
|
|
837
|
+
requestDigest: request.requestDigest,
|
|
838
|
+
surface: request.surface,
|
|
839
|
+
targetBindingDigest: request.targetBindingDigest,
|
|
840
|
+
manifest: Object.freeze({
|
|
841
|
+
count: request.manifest.count,
|
|
842
|
+
orderPolicy: "exact",
|
|
843
|
+
identities: Object.freeze(request.manifest.identities.map(entry => Object.freeze({ ...entry })))
|
|
844
|
+
})
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
function cloneHandoffRequest(request) {
|
|
848
|
+
return Object.freeze({
|
|
849
|
+
operationId: request.operationId,
|
|
850
|
+
requestDigest: request.requestDigest,
|
|
851
|
+
surface: request.surface,
|
|
852
|
+
actionId: request.actionId,
|
|
853
|
+
targetBindingDigest: request.targetBindingDigest,
|
|
854
|
+
manifest: Object.freeze({
|
|
855
|
+
count: request.manifest.count,
|
|
856
|
+
orderPolicy: "exact",
|
|
857
|
+
identities: Object.freeze(request.manifest.identities.map(entry => Object.freeze({ ...entry })))
|
|
858
|
+
}),
|
|
859
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
860
|
+
...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
async function safeEvidence(evidenceDigest, domain, material) {
|
|
864
|
+
try {
|
|
865
|
+
const result = await evidenceDigest(domain, material);
|
|
866
|
+
return isDigest(result) ? result : undefined;
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
return undefined;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function isDigest(value) {
|
|
873
|
+
return typeof value === "string" && DIGEST_PATTERN.test(value);
|
|
874
|
+
}
|
|
875
|
+
function remainingBudget(deadlineAt) {
|
|
876
|
+
return Math.max(0, Math.min(MAX_TIMEOUT_MS, deadlineAt - Date.now()));
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* A handoff request is issued only after its durable intent is recorded. A
|
|
880
|
+
* request-local abort therefore cannot authorize a retry, even when it arrives
|
|
881
|
+
* before the provider reaches click/setFiles. Return an explicit quarantine
|
|
882
|
+
* result so callers know to observe the live surface instead.
|
|
883
|
+
*/
|
|
884
|
+
function handoffCancellation(signal, deadlineAt) {
|
|
885
|
+
if (signal?.aborted)
|
|
886
|
+
return { status: "uncertain", quarantine: "caller" };
|
|
887
|
+
if (deadlineAt !== undefined && Date.now() >= deadlineAt) {
|
|
888
|
+
return { status: "uncertain", quarantine: "caller" };
|
|
889
|
+
}
|
|
890
|
+
return undefined;
|
|
891
|
+
}
|
|
892
|
+
function isAbortSignal(value) {
|
|
893
|
+
if (typeof AbortSignal === "undefined" || !(value instanceof AbortSignal))
|
|
894
|
+
return false;
|
|
895
|
+
try {
|
|
896
|
+
return typeof value.aborted === "boolean"
|
|
897
|
+
&& typeof value.addEventListener === "function"
|
|
898
|
+
&& typeof value.removeEventListener === "function";
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async function flushMicrotasks() {
|
|
905
|
+
await Promise.resolve();
|
|
906
|
+
await Promise.resolve();
|
|
907
|
+
}
|
|
908
|
+
function readData(value, key) {
|
|
909
|
+
try {
|
|
910
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
911
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
912
|
+
return undefined;
|
|
913
|
+
return descriptor.value;
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
return undefined;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function safeMethod(value, key) {
|
|
920
|
+
let current = value;
|
|
921
|
+
for (let depth = 0; current !== null && depth < MAX_GRAPH_DEPTH; depth += 1) {
|
|
922
|
+
const descriptor = readDescriptor(current, key);
|
|
923
|
+
if (descriptor !== undefined) {
|
|
924
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined || typeof descriptor.value !== "function")
|
|
925
|
+
return undefined;
|
|
926
|
+
return descriptor.value;
|
|
927
|
+
}
|
|
928
|
+
current = readPrototype(current);
|
|
929
|
+
}
|
|
930
|
+
return undefined;
|
|
931
|
+
}
|
|
932
|
+
function providerCallable(value, key) {
|
|
933
|
+
try {
|
|
934
|
+
const candidate = Reflect.get(value, key, value);
|
|
935
|
+
if (typeof candidate !== "function")
|
|
936
|
+
return undefined;
|
|
937
|
+
return (...args) => Reflect.apply(candidate, value, args);
|
|
938
|
+
}
|
|
939
|
+
catch {
|
|
940
|
+
return undefined;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function readDescriptor(value, key) {
|
|
944
|
+
try {
|
|
945
|
+
return Object.getOwnPropertyDescriptor(value, key);
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
return undefined;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function readPrototype(value) {
|
|
952
|
+
try {
|
|
953
|
+
return Object.getPrototypeOf(value);
|
|
954
|
+
}
|
|
955
|
+
catch {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function isSafeProviderObject(value) {
|
|
960
|
+
if (!isObjectLike(value))
|
|
961
|
+
return false;
|
|
962
|
+
// A descriptor walk is intentionally shallow for browser objects; provider
|
|
963
|
+
// methods may live on prototypes and are validated at each invocation.
|
|
964
|
+
try {
|
|
965
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
966
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
967
|
+
if (typeof key !== "string")
|
|
968
|
+
return false;
|
|
969
|
+
const descriptor = descriptors[key];
|
|
970
|
+
if (descriptor === undefined || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function isSafeDataGraph(value, seen = new Set(), depth = 0) {
|
|
980
|
+
if (value === null || typeof value !== "object")
|
|
981
|
+
return value !== undefined && typeof value !== "function";
|
|
982
|
+
if (depth > MAX_GRAPH_DEPTH || seen.has(value))
|
|
983
|
+
return depth <= MAX_GRAPH_DEPTH;
|
|
984
|
+
seen.add(value);
|
|
985
|
+
if (seen.size > MAX_GRAPH_NODES)
|
|
986
|
+
return false;
|
|
987
|
+
let prototype;
|
|
988
|
+
let descriptors;
|
|
989
|
+
try {
|
|
990
|
+
prototype = Object.getPrototypeOf(value);
|
|
991
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
992
|
+
}
|
|
993
|
+
catch {
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
if (prototype !== Object.prototype && prototype !== null && !Array.isArray(value))
|
|
997
|
+
return false;
|
|
998
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
999
|
+
if (typeof key !== "string")
|
|
1000
|
+
return false;
|
|
1001
|
+
const descriptor = descriptors[key];
|
|
1002
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1003
|
+
return false;
|
|
1004
|
+
if (!isSafeDataGraph(descriptor.value, seen, depth + 1))
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
function isObjectLike(value) {
|
|
1010
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
1011
|
+
}
|
|
1012
|
+
function hasSafeArrayDescriptors(value) {
|
|
1013
|
+
try {
|
|
1014
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1015
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
1016
|
+
if (typeof key !== "string")
|
|
1017
|
+
return false;
|
|
1018
|
+
const descriptor = descriptors[key];
|
|
1019
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
const lengthDescriptor = readDescriptor(value, "length");
|
|
1023
|
+
const length = lengthDescriptor !== undefined && "value" in lengthDescriptor ? lengthDescriptor.value : undefined;
|
|
1024
|
+
return isNonnegativeSafeInteger(length) && length === value.length;
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function isNonnegativeSafeInteger(value) {
|
|
1031
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
1032
|
+
}
|
|
1033
|
+
function isPositiveSafeInteger(value) {
|
|
1034
|
+
return isNonnegativeSafeInteger(value) && value > 0;
|
|
1035
|
+
}
|