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,1145 @@
|
|
|
1
|
+
import { fingerprintOperationFile, revalidateOperationFile } from "./file-identity.js";
|
|
2
|
+
import { assertDurableCapturePolicyShape } from "./state-machine.js";
|
|
3
|
+
/**
|
|
4
|
+
* The public operations facade is deliberately a thin composition layer.
|
|
5
|
+
*
|
|
6
|
+
* `OperationService` owns request identity, journal state, and non-repeatable
|
|
7
|
+
* action idempotency. This class owns only the local-file boundary: it
|
|
8
|
+
* snapshots the caller request, hashes local files in request order, and
|
|
9
|
+
* supplies a browser adapter whose one file-handoff callback revalidates the
|
|
10
|
+
* same file identities immediately before handing them to the provider.
|
|
11
|
+
*
|
|
12
|
+
* The browser adapter factory is optional. A direct adapter is useful for
|
|
13
|
+
* callers that already created a request-scoped closure. The factory exists
|
|
14
|
+
* for the normal SDK path, where prompt/configuration/path values should stay
|
|
15
|
+
* in an ephemeral adapter closure rather than crossing the service boundary.
|
|
16
|
+
*/
|
|
17
|
+
const SAFE_INPUT_PATH_PREFIX = "operation-input-";
|
|
18
|
+
const SAFE_OUTPUT_PATH = "operation-output";
|
|
19
|
+
/** Keep ephemeral raw-path closures bounded even when callers never collect. */
|
|
20
|
+
const DEFAULT_MAX_CACHED_ADAPTERS = 8;
|
|
21
|
+
const MAX_CACHED_ADAPTERS = 256;
|
|
22
|
+
export class OperationClientError extends Error {
|
|
23
|
+
code;
|
|
24
|
+
constructor(code, message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.name = "OperationClientError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Additive TypeScript operations API. The supplied adapter is also used for
|
|
32
|
+
* browser-free completion paths because the service requires a uniform port;
|
|
33
|
+
* `inspect` remains entirely browser-free. A request-scoped factory may
|
|
34
|
+
* replace the base adapter for submit/run and is cached in memory only for
|
|
35
|
+
* subsequent collect/control calls in the same process.
|
|
36
|
+
*/
|
|
37
|
+
export class OperationClient {
|
|
38
|
+
service;
|
|
39
|
+
adapter;
|
|
40
|
+
fingerprint;
|
|
41
|
+
revalidate;
|
|
42
|
+
adapterFactory;
|
|
43
|
+
handleAdapterFactory;
|
|
44
|
+
controlAdapterFactory;
|
|
45
|
+
maxCachedAdapters;
|
|
46
|
+
requestAdapters = new Map();
|
|
47
|
+
constructor(service, adapter, options = {}) {
|
|
48
|
+
this.service = service;
|
|
49
|
+
this.adapter = adapter;
|
|
50
|
+
this.fingerprint = options.fingerprint ?? fingerprintOperationFile;
|
|
51
|
+
this.revalidate = options.revalidate ?? revalidateOperationFile;
|
|
52
|
+
this.adapterFactory = options.adapterFactory;
|
|
53
|
+
this.handleAdapterFactory = options.handleAdapterFactory;
|
|
54
|
+
this.controlAdapterFactory = options.controlAdapterFactory;
|
|
55
|
+
this.maxCachedAdapters = validateMaxCachedAdapters(options.maxCachedAdapters);
|
|
56
|
+
}
|
|
57
|
+
/** Fingerprint inputs, then execute the service's one-submit protocol. */
|
|
58
|
+
async submit(request, options = {}) {
|
|
59
|
+
const prepared = await this.prepareSubmit(request, options.signal);
|
|
60
|
+
const adapter = await this.adapterForSubmit(prepared);
|
|
61
|
+
const result = await this.service.submit(prepared.serviceRequest, prepared.manifest, adapter, forwardSubmitOptions(options, prepared.signal));
|
|
62
|
+
if (isTerminalSubmitResult(result)) {
|
|
63
|
+
this.forgetAdapter(result.handle);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.rememberAdapter(result.handle, adapter);
|
|
67
|
+
}
|
|
68
|
+
return freshResult(result);
|
|
69
|
+
}
|
|
70
|
+
/** Collect only the exact operation-owned turn; never composes or submits. */
|
|
71
|
+
async collect(handle, options = {}) {
|
|
72
|
+
const snapshot = cloneFrozen(handle, "invalid_operation_handle");
|
|
73
|
+
const adapter = await this.adapterForHandle(snapshot, options.signal);
|
|
74
|
+
const result = await this.service.collect(snapshot, adapter, forwardCollectorOptions(options));
|
|
75
|
+
if (result.kind === "completed")
|
|
76
|
+
this.forgetAdapter(snapshot);
|
|
77
|
+
return freshResult(result);
|
|
78
|
+
}
|
|
79
|
+
/** Inspect durable state without touching the browser. */
|
|
80
|
+
async inspect(handle) {
|
|
81
|
+
const snapshot = cloneFrozen(handle, "invalid_operation_handle");
|
|
82
|
+
return freshResult(await this.service.inspect(snapshot));
|
|
83
|
+
}
|
|
84
|
+
/** Apply one operation-bound Stop or Work steer. */
|
|
85
|
+
async control(request, options = {}) {
|
|
86
|
+
const snapshot = cloneFrozen(request, "invalid_operation_control_request");
|
|
87
|
+
// Authenticate exactly once before selecting an adapter. A control
|
|
88
|
+
// factory receives the same durable target/state snapshot used below;
|
|
89
|
+
// invoking it before inspection would permit a stale target binding and
|
|
90
|
+
// invoking adapterForHandle first would perform a second inspect and/or
|
|
91
|
+
// accidentally reuse a submit adapter that cannot carry steer prompt
|
|
92
|
+
// state.
|
|
93
|
+
const reconstruction = await this.reconstructionForHandle(snapshot.parent);
|
|
94
|
+
const adapter = this.controlAdapterFactory === undefined
|
|
95
|
+
? await this.adapterForAuthenticatedHandle(snapshot.parent, options.signal, reconstruction)
|
|
96
|
+
: await this.adapterForControl(snapshot, options.signal, reconstruction);
|
|
97
|
+
return freshResult(await this.service.control(snapshot, adapter, forwardControlOptions(options)));
|
|
98
|
+
}
|
|
99
|
+
/** SDK-only composition of one submit followed by one collect. */
|
|
100
|
+
async run(request, options = {}) {
|
|
101
|
+
const prepared = await this.prepareSubmit(request, options.signal);
|
|
102
|
+
const adapter = await this.adapterForSubmit(prepared);
|
|
103
|
+
const result = await this.service.run(prepared.serviceRequest, prepared.manifest, adapter, forwardRunOptions(options, prepared.signal));
|
|
104
|
+
if (isTerminalRunResult(result)) {
|
|
105
|
+
this.forgetAdapter(result.submit.handle);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this.rememberAdapter(result.submit.handle, adapter);
|
|
109
|
+
}
|
|
110
|
+
return freshResult(result);
|
|
111
|
+
}
|
|
112
|
+
async prepareSubmit(request, requestedSignal) {
|
|
113
|
+
const snapshot = cloneFrozen(request, "invalid_operation_request");
|
|
114
|
+
const signal = requestedSignal ?? new AbortController().signal;
|
|
115
|
+
assertAbortSignal(signal);
|
|
116
|
+
const requestedFiles = readRequestedFiles(snapshot);
|
|
117
|
+
const identities = [];
|
|
118
|
+
for (const file of requestedFiles) {
|
|
119
|
+
try {
|
|
120
|
+
const identity = await this.fingerprint(file.path, file.displayName, { signal });
|
|
121
|
+
identities.push(freezeIdentity(identity));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw fileBoundaryError(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const manifest = Object.freeze(identities.map(identity => identity.manifest));
|
|
128
|
+
const serviceRequest = sanitizeServiceRequest(snapshot, identities);
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
request: snapshot,
|
|
131
|
+
serviceRequest,
|
|
132
|
+
identities: Object.freeze(identities),
|
|
133
|
+
manifest,
|
|
134
|
+
signal
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async adapterForSubmit(prepared) {
|
|
138
|
+
// A non-terminal submit result deliberately retains its request-scoped
|
|
139
|
+
// adapter so an identical same-operation retry can reconcile the durable
|
|
140
|
+
// Send boundary observation-only. The client cannot recompute the
|
|
141
|
+
// journal's keyed request digest, so select by the already validated
|
|
142
|
+
// operation ID. OperationService authenticates the immutable request
|
|
143
|
+
// digest before it invokes any adapter method; a changed same-ID request
|
|
144
|
+
// therefore still fails browser-free with operation_request_mismatch.
|
|
145
|
+
const cached = this.cachedAdapterForOperation(prepared.request.operationId);
|
|
146
|
+
if (cached !== undefined)
|
|
147
|
+
return cached;
|
|
148
|
+
let adapter = this.adapter;
|
|
149
|
+
if (this.adapterFactory !== undefined) {
|
|
150
|
+
try {
|
|
151
|
+
adapter = await this.adapterFactory(Object.freeze({
|
|
152
|
+
request: prepared.request,
|
|
153
|
+
files: prepared.identities,
|
|
154
|
+
signal: prepared.signal
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Let OperationService create the durable request record before the
|
|
159
|
+
// read-only target probe reports this blocker. Throwing here would
|
|
160
|
+
// lose the operation handle and make a same-ID recovery opaque.
|
|
161
|
+
adapter = unavailableAdapter("adapter_unavailable");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
return this.guardAdapter(adapter, prepared.identities, prepared.signal);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
throw new OperationClientError("adapter_unavailable", "The operation browser adapter is incomplete.");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
guardAdapter(adapter, identities, signal) {
|
|
172
|
+
if (adapter === null || typeof adapter !== "object") {
|
|
173
|
+
throw new OperationClientError("adapter_unavailable", "The operation browser adapter is incomplete.");
|
|
174
|
+
}
|
|
175
|
+
// Factory output is untrusted at this boundary. Read only own data
|
|
176
|
+
// descriptors so an accessor (or a proxy get trap) cannot run while the
|
|
177
|
+
// facade is merely validating the adapter. Capture every method before
|
|
178
|
+
// building wrappers so a later mutation/accessor replacement cannot alter
|
|
179
|
+
// the validated surface.
|
|
180
|
+
const resolveTarget = requiredMethod(adapter, "resolveTarget");
|
|
181
|
+
const submissionInput = requiredAdapterObject(adapter, "submission");
|
|
182
|
+
const observeStaging = requiredMethod(submissionInput, "observeStaging");
|
|
183
|
+
const executeFileHandoffOnce = requiredMethod(submissionInput, "executeFileHandoffOnce");
|
|
184
|
+
const observeAttachments = requiredMethod(submissionInput, "observeAttachments");
|
|
185
|
+
const prepareSend = requiredMethod(submissionInput, "prepareSend");
|
|
186
|
+
const executePreparedSend = requiredMethod(submissionInput, "executePreparedSend");
|
|
187
|
+
const verifyPreparedSend = requiredMethod(submissionInput, "verifyPreparedSend");
|
|
188
|
+
const recoverSend = requiredMethod(submissionInput, "recoverSend");
|
|
189
|
+
const recoverAuthenticatedSend = optionalMethod(submissionInput, "recoverAuthenticatedSend");
|
|
190
|
+
const executeFinalTabTransaction = requiredMethod(submissionInput, "executeFinalTabTransaction");
|
|
191
|
+
const collectorInput = requiredAdapterObject(adapter, "collector");
|
|
192
|
+
const readContext = requiredMethod(collectorInput, "readContext");
|
|
193
|
+
const observe = requiredMethod(collectorInput, "observe");
|
|
194
|
+
const sleep = requiredMethod(collectorInput, "sleep");
|
|
195
|
+
const submission = Object.freeze({
|
|
196
|
+
observeStaging: (request) => observeStaging(request),
|
|
197
|
+
executeFileHandoffOnce: async (request) => {
|
|
198
|
+
for (const identity of identities) {
|
|
199
|
+
try {
|
|
200
|
+
await this.revalidate(identity, { signal });
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
throw fileBoundaryError(error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return executeFileHandoffOnce(request);
|
|
207
|
+
},
|
|
208
|
+
observeAttachments: (request) => observeAttachments(request),
|
|
209
|
+
prepareSend: (request) => prepareSend(request),
|
|
210
|
+
executePreparedSend: (request) => executePreparedSend(request),
|
|
211
|
+
verifyPreparedSend: (request) => verifyPreparedSend(request),
|
|
212
|
+
recoverSend: (request) => recoverSend(request),
|
|
213
|
+
...(recoverAuthenticatedSend === undefined ? {} : { recoverAuthenticatedSend }),
|
|
214
|
+
executeFinalTabTransaction: (request) => executeFinalTabTransaction(request)
|
|
215
|
+
});
|
|
216
|
+
const collector = Object.freeze({
|
|
217
|
+
readContext: (request) => readContext(request),
|
|
218
|
+
observe: (request) => observe(request),
|
|
219
|
+
sleep: (milliseconds, sleepSignal) => sleep(milliseconds, sleepSignal)
|
|
220
|
+
});
|
|
221
|
+
let control;
|
|
222
|
+
const controlInput = optionalDataProperty(adapter, "control");
|
|
223
|
+
if (controlInput !== undefined) {
|
|
224
|
+
const controlObject = adapterObject(controlInput);
|
|
225
|
+
const observeTurn = requiredMethod(controlObject, "observeTurn");
|
|
226
|
+
const executeOnce = requiredMethod(controlObject, "executeOnce");
|
|
227
|
+
const observePostcondition = requiredMethod(controlObject, "observePostcondition");
|
|
228
|
+
const postconditionRetryInput = optionalDataProperty(controlObject, "postconditionRetry");
|
|
229
|
+
const postconditionRetry = postconditionRetryInput === undefined
|
|
230
|
+
? undefined
|
|
231
|
+
: (() => {
|
|
232
|
+
const policy = adapterObject(postconditionRetryInput);
|
|
233
|
+
const maxAttempts = optionalDataProperty(policy, "maxAttempts");
|
|
234
|
+
const intervalMs = optionalDataProperty(policy, "intervalMs");
|
|
235
|
+
if (!Number.isSafeInteger(maxAttempts) || !Number.isSafeInteger(intervalMs)) {
|
|
236
|
+
throw new OperationClientError("adapter_unavailable", "The operation control adapter has an invalid postcondition retry policy.");
|
|
237
|
+
}
|
|
238
|
+
return Object.freeze({
|
|
239
|
+
maxAttempts: maxAttempts,
|
|
240
|
+
intervalMs: intervalMs
|
|
241
|
+
});
|
|
242
|
+
})();
|
|
243
|
+
// Work-steer is an optional four-phase capability. Existing Stop-only
|
|
244
|
+
// adapters remain valid, but a partially supplied phase surface is not
|
|
245
|
+
// safe: allowing it through would defer an adapter contract failure
|
|
246
|
+
// until after a durable control intent exists.
|
|
247
|
+
const prepareSteer = optionalMethod(controlObject, "prepareSteer");
|
|
248
|
+
const executeSteerPrepared = optionalMethod(controlObject, "executeSteerPrepared");
|
|
249
|
+
const verifySteer = optionalMethod(controlObject, "verifySteer");
|
|
250
|
+
const recoverSteer = optionalMethod(controlObject, "recoverSteer");
|
|
251
|
+
const steerPhaseMethods = [prepareSteer, executeSteerPrepared, verifySteer, recoverSteer];
|
|
252
|
+
const steerPhaseCount = steerPhaseMethods.filter(method => method !== undefined).length;
|
|
253
|
+
if (steerPhaseCount !== 0 && steerPhaseCount !== steerPhaseMethods.length) {
|
|
254
|
+
throw new OperationClientError("adapter_unavailable", "The operation control adapter has incomplete Work-steer phases.");
|
|
255
|
+
}
|
|
256
|
+
const guardedControl = {
|
|
257
|
+
observeTurn: (request) => observeTurn(request),
|
|
258
|
+
executeOnce: (request) => executeOnce(request),
|
|
259
|
+
observePostcondition: (request) => observePostcondition(request)
|
|
260
|
+
};
|
|
261
|
+
if (postconditionRetry !== undefined)
|
|
262
|
+
guardedControl.postconditionRetry = postconditionRetry;
|
|
263
|
+
if (prepareSteer !== undefined && executeSteerPrepared !== undefined && verifySteer !== undefined && recoverSteer !== undefined) {
|
|
264
|
+
guardedControl.prepareSteer = (request) => prepareSteer(request);
|
|
265
|
+
guardedControl.executeSteerPrepared = (request) => executeSteerPrepared(request);
|
|
266
|
+
guardedControl.verifySteer = (request) => verifySteer(request);
|
|
267
|
+
guardedControl.recoverSteer = (request) => recoverSteer(request);
|
|
268
|
+
}
|
|
269
|
+
control = Object.freeze(guardedControl);
|
|
270
|
+
}
|
|
271
|
+
let artifacts;
|
|
272
|
+
const artifactsInput = optionalDataProperty(adapter, "artifacts");
|
|
273
|
+
if (artifactsInput !== undefined) {
|
|
274
|
+
const artifactsObject = adapterObject(artifactsInput);
|
|
275
|
+
const transfer = requiredMethod(artifactsObject, "transfer");
|
|
276
|
+
artifacts = Object.freeze({
|
|
277
|
+
transfer: (request) => transfer(request)
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
let staging;
|
|
281
|
+
const stagingInput = optionalDataProperty(adapter, "staging");
|
|
282
|
+
if (stagingInput !== undefined) {
|
|
283
|
+
const stagingObject = adapterObject(stagingInput);
|
|
284
|
+
const readCurrent = requiredMethod(stagingObject, "readCurrent");
|
|
285
|
+
const mutateOnce = requiredMethod(stagingObject, "mutateOnce");
|
|
286
|
+
const observeStagingState = requiredMethod(stagingObject, "observe");
|
|
287
|
+
staging = Object.freeze({
|
|
288
|
+
readCurrent: (request) => readCurrent(request),
|
|
289
|
+
mutateOnce: (request) => mutateOnce(request),
|
|
290
|
+
observe: (request) => observeStagingState(request)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const guarded = {
|
|
294
|
+
resolveTarget: (request) => resolveTarget(request),
|
|
295
|
+
submission,
|
|
296
|
+
collector,
|
|
297
|
+
...(artifacts === undefined ? {} : { artifacts }),
|
|
298
|
+
...(staging === undefined ? {} : { staging }),
|
|
299
|
+
...(control === undefined ? {} : { control })
|
|
300
|
+
};
|
|
301
|
+
return Object.freeze(guarded);
|
|
302
|
+
}
|
|
303
|
+
async adapterForHandle(handle, requestedSignal) {
|
|
304
|
+
const reconstruction = await this.reconstructionForHandle(handle);
|
|
305
|
+
return await this.adapterForAuthenticatedHandle(handle, requestedSignal, reconstruction);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Authenticate a handle and project only the immutable target context
|
|
309
|
+
* needed by a request-local adapter. The inspect result is intentionally
|
|
310
|
+
* consumed once and passed to adapter selection; callers that need a
|
|
311
|
+
* prompt-bearing control closure must not repeat this read.
|
|
312
|
+
*/
|
|
313
|
+
async reconstructionForHandle(handle) {
|
|
314
|
+
// Always authenticate the locator before consulting the in-process cache.
|
|
315
|
+
// Otherwise a caller could mutate revision/phase on a still-cacheable
|
|
316
|
+
// identity and reuse a target-bound adapter without a fresh journal read.
|
|
317
|
+
const inspected = await this.service.inspect(handle);
|
|
318
|
+
try {
|
|
319
|
+
return reconstructionContext(inspected, handle);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
// A pre-target record cannot supply a target-bound reconstruction
|
|
323
|
+
// context. Do not invoke a factory with a type-unsound partial context;
|
|
324
|
+
// OperationService remains authoritative and rejects the missing target
|
|
325
|
+
// before browser use.
|
|
326
|
+
if (error instanceof OperationClientError && error.code === "target_binding_missing") {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async adapterForAuthenticatedHandle(handle, requestedSignal, reconstruction) {
|
|
333
|
+
if (reconstruction === undefined)
|
|
334
|
+
return this.adapter;
|
|
335
|
+
if (reconstruction.target.targetLifecycle === "new_pending") {
|
|
336
|
+
throw new OperationClientError("new_target_not_established", "A pending new target cannot be reconstructed for collection or control.");
|
|
337
|
+
}
|
|
338
|
+
if (reconstruction.state.phase === "completed") {
|
|
339
|
+
// OperationService returns completed receipts without touching its
|
|
340
|
+
// adapter. Keep that browser-free guarantee after a process restart too.
|
|
341
|
+
return this.adapter;
|
|
342
|
+
}
|
|
343
|
+
const factoryContext = reconstruction.context;
|
|
344
|
+
const key = adapterKey(handle.operationId, handle.requestDigest);
|
|
345
|
+
const cached = this.requestAdapters.get(key);
|
|
346
|
+
if (cached !== undefined) {
|
|
347
|
+
// Map insertion order is the LRU order. A read promotes this entry.
|
|
348
|
+
this.requestAdapters.delete(key);
|
|
349
|
+
this.requestAdapters.set(key, cached);
|
|
350
|
+
return cached;
|
|
351
|
+
}
|
|
352
|
+
if (this.handleAdapterFactory === undefined)
|
|
353
|
+
return this.adapter;
|
|
354
|
+
let recreated;
|
|
355
|
+
try {
|
|
356
|
+
recreated = await this.handleAdapterFactory(factoryContext);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
throw new OperationClientError("adapter_unavailable", "The operation browser adapter could not be recreated from the operation handle.");
|
|
360
|
+
}
|
|
361
|
+
const signal = requestedSignal ?? new AbortController().signal;
|
|
362
|
+
assertAbortSignal(signal);
|
|
363
|
+
let guarded;
|
|
364
|
+
try {
|
|
365
|
+
guarded = this.guardAdapter(recreated, [], signal);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
throw new OperationClientError("adapter_unavailable", "The operation browser adapter could not be recreated from the operation handle.");
|
|
369
|
+
}
|
|
370
|
+
this.rememberAdapter(factoryContext.handle, guarded);
|
|
371
|
+
return guarded;
|
|
372
|
+
}
|
|
373
|
+
async adapterForControl(request, requestedSignal, reconstruction) {
|
|
374
|
+
// Completed records and pre-target records remain browser-free/legacy
|
|
375
|
+
// compatible. There is no safe target-bound context with which to invoke
|
|
376
|
+
// a control factory in either case, and OperationService owns the final
|
|
377
|
+
// blocker/receipt decision.
|
|
378
|
+
if (reconstruction === undefined || reconstruction.state.phase === "completed") {
|
|
379
|
+
return await this.adapterForAuthenticatedHandle(request.parent, requestedSignal, reconstruction);
|
|
380
|
+
}
|
|
381
|
+
if (reconstruction.target.targetLifecycle === "new_pending") {
|
|
382
|
+
throw new OperationClientError("new_target_not_established", "A pending new target cannot be reconstructed for collection or control.");
|
|
383
|
+
}
|
|
384
|
+
const signal = requestedSignal ?? new AbortController().signal;
|
|
385
|
+
assertAbortSignal(signal);
|
|
386
|
+
let created;
|
|
387
|
+
try {
|
|
388
|
+
created = await this.controlAdapterFactory(makeControlFactoryContext(request, reconstruction));
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// The control factory is the only component that can hold a raw steer
|
|
392
|
+
// prompt and a provider/browser closure. If it fails, do not fall back
|
|
393
|
+
// to a cached submit adapter: that would either lose the prompt or
|
|
394
|
+
// mutate an unintended target. Keep this message static and redacted.
|
|
395
|
+
throw new OperationClientError("adapter_unavailable", "The operation control browser adapter is unavailable.");
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
// Control adapters are deliberately not remembered in requestAdapters.
|
|
399
|
+
// Their closure may contain a steer prompt and must die with this call.
|
|
400
|
+
return this.guardAdapter(created, [], signal);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
throw new OperationClientError("adapter_unavailable", "The operation control browser adapter is incomplete.");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
rememberAdapter(handle, adapter) {
|
|
407
|
+
const key = adapterKey(handle.operationId, handle.requestDigest);
|
|
408
|
+
this.requestAdapters.delete(key);
|
|
409
|
+
this.requestAdapters.set(key, adapter);
|
|
410
|
+
while (this.requestAdapters.size > this.maxCachedAdapters) {
|
|
411
|
+
const oldest = this.requestAdapters.keys().next().value;
|
|
412
|
+
if (oldest === undefined)
|
|
413
|
+
break;
|
|
414
|
+
this.requestAdapters.delete(oldest);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
cachedAdapterForOperation(operationId) {
|
|
418
|
+
const prefix = `${operationId}\0`;
|
|
419
|
+
let matchedKey;
|
|
420
|
+
let matchedAdapter;
|
|
421
|
+
for (const [key, adapter] of this.requestAdapters) {
|
|
422
|
+
if (key.startsWith(prefix)) {
|
|
423
|
+
matchedKey = key;
|
|
424
|
+
matchedAdapter = adapter;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (matchedKey === undefined || matchedAdapter === undefined)
|
|
428
|
+
return undefined;
|
|
429
|
+
// Preserve the cache's LRU invariant when a submit retry hits it.
|
|
430
|
+
this.requestAdapters.delete(matchedKey);
|
|
431
|
+
this.requestAdapters.set(matchedKey, matchedAdapter);
|
|
432
|
+
return matchedAdapter;
|
|
433
|
+
}
|
|
434
|
+
forgetAdapter(handle) {
|
|
435
|
+
this.requestAdapters.delete(adapterKey(handle.operationId, handle.requestDigest));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/** Alias kept for callers that prefer the plural namespace terminology. */
|
|
439
|
+
export const OperationsClient = OperationClient;
|
|
440
|
+
export function createOperationClient(service, adapter, options = {}) {
|
|
441
|
+
return new OperationClient(service, adapter, options);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Project one authenticated inspect result into the restart factory surface.
|
|
445
|
+
* Only own data descriptors are read. This is deliberately verbose: using a
|
|
446
|
+
* spread, structuredClone, or direct property access here would execute a
|
|
447
|
+
* hostile getter before the adapter factory has even been called.
|
|
448
|
+
*/
|
|
449
|
+
function reconstructionContext(inspected, requestedHandle) {
|
|
450
|
+
const inspectedRecord = requiredObject(inspected, "inspect result");
|
|
451
|
+
const freshHandle = normalizeHandle(requiredData(inspectedRecord, "handle"), requestedHandle);
|
|
452
|
+
const rawState = requiredData(inspectedRecord, "state");
|
|
453
|
+
const stateRecord = requiredObject(rawState, "durable operation state");
|
|
454
|
+
const state = normalizeDurableState(stateRecord, freshHandle);
|
|
455
|
+
const target = state.target;
|
|
456
|
+
const context = makeFactoryContext(freshHandle, state, target);
|
|
457
|
+
return Object.freeze({ context, state, target });
|
|
458
|
+
}
|
|
459
|
+
function normalizeHandle(value, requested) {
|
|
460
|
+
const record = requiredObject(value, "operation handle");
|
|
461
|
+
const schemaVersion = requiredString(record, "schemaVersion");
|
|
462
|
+
const operationId = requiredString(record, "operationId");
|
|
463
|
+
const requestDigest = requiredString(record, "requestDigest");
|
|
464
|
+
const surface = requiredString(record, "surface");
|
|
465
|
+
const revision = requiredSafeInteger(record, "revision");
|
|
466
|
+
const phase = requiredString(record, "phase");
|
|
467
|
+
const mutationBoundary = requiredString(record, "mutationBoundary");
|
|
468
|
+
const targetBindingDigest = optionalString(record, "targetBindingDigest");
|
|
469
|
+
if (schemaVersion !== requested.schemaVersion
|
|
470
|
+
|| operationId !== requested.operationId
|
|
471
|
+
|| requestDigest !== requested.requestDigest
|
|
472
|
+
|| surface !== requested.surface
|
|
473
|
+
|| revision < requested.revision
|
|
474
|
+
|| !isOperationSurface(surface)
|
|
475
|
+
|| !isOperationPhase(phase)
|
|
476
|
+
|| !isMutationBoundary(mutationBoundary)
|
|
477
|
+
|| (targetBindingDigest !== undefined && !isDigest(targetBindingDigest))
|
|
478
|
+
|| (requested.targetBindingDigest !== undefined
|
|
479
|
+
&& targetBindingDigest !== requested.targetBindingDigest)
|
|
480
|
+
|| (revision === requested.revision
|
|
481
|
+
&& (phase !== requested.phase
|
|
482
|
+
|| mutationBoundary !== requested.mutationBoundary
|
|
483
|
+
|| targetBindingDigest !== requested.targetBindingDigest))) {
|
|
484
|
+
throw new OperationClientError("invalid_operation_handle", "The authenticated operation handle is inconsistent.");
|
|
485
|
+
}
|
|
486
|
+
return Object.freeze({
|
|
487
|
+
schemaVersion: schemaVersion,
|
|
488
|
+
operationId,
|
|
489
|
+
requestDigest,
|
|
490
|
+
surface: surface,
|
|
491
|
+
revision,
|
|
492
|
+
phase: phase,
|
|
493
|
+
mutationBoundary: mutationBoundary,
|
|
494
|
+
...(targetBindingDigest === undefined ? {} : { targetBindingDigest })
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
function normalizeDurableState(value, handle) {
|
|
498
|
+
const schemaVersion = requiredString(value, "schemaVersion");
|
|
499
|
+
const operationId = requiredString(value, "operationId");
|
|
500
|
+
const requestDigest = requiredString(value, "requestDigest");
|
|
501
|
+
const surface = requiredString(value, "surface");
|
|
502
|
+
const phase = requiredString(value, "phase");
|
|
503
|
+
const mutationBoundary = requiredString(value, "mutationBoundary");
|
|
504
|
+
const revision = requiredSafeInteger(value, "revision");
|
|
505
|
+
const capturePolicyValue = optionalDataProperty(value, "capturePolicy");
|
|
506
|
+
if (capturePolicyValue !== undefined)
|
|
507
|
+
assertDurableCapturePolicyShape(capturePolicyValue);
|
|
508
|
+
const targetValue = optionalDataProperty(value, "target");
|
|
509
|
+
if (targetValue === undefined) {
|
|
510
|
+
throw new OperationClientError("target_binding_missing", "The durable operation has no target binding.");
|
|
511
|
+
}
|
|
512
|
+
const target = normalizeTarget(targetValue);
|
|
513
|
+
if (schemaVersion !== "chatgpt.browser_control.operation.v1"
|
|
514
|
+
|| operationId !== handle.operationId
|
|
515
|
+
|| requestDigest !== handle.requestDigest
|
|
516
|
+
|| surface !== handle.surface
|
|
517
|
+
|| revision !== handle.revision
|
|
518
|
+
|| phase !== handle.phase
|
|
519
|
+
|| mutationBoundary !== handle.mutationBoundary
|
|
520
|
+
|| handle.targetBindingDigest === undefined
|
|
521
|
+
|| (target.targetEstablishment !== undefined
|
|
522
|
+
&& target.targetEstablishment.targetBindingDigest !== handle.targetBindingDigest)
|
|
523
|
+
|| !isOperationSurface(surface)
|
|
524
|
+
|| !isOperationPhase(phase)
|
|
525
|
+
|| !isMutationBoundary(mutationBoundary)) {
|
|
526
|
+
throw new OperationClientError("invalid_operation_state", "The authenticated operation state is inconsistent.");
|
|
527
|
+
}
|
|
528
|
+
return Object.freeze({
|
|
529
|
+
schemaVersion: schemaVersion,
|
|
530
|
+
operationId,
|
|
531
|
+
requestDigest,
|
|
532
|
+
surface: surface,
|
|
533
|
+
phase: phase,
|
|
534
|
+
mutationBoundary: mutationBoundary,
|
|
535
|
+
revision,
|
|
536
|
+
target,
|
|
537
|
+
...(capturePolicyValue === undefined ? {} : { capturePolicy: capturePolicyValue })
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function normalizeTarget(value) {
|
|
541
|
+
const record = requiredObject(value, "durable target binding");
|
|
542
|
+
const providerId = requiredBoundedString(record, "providerId");
|
|
543
|
+
const browserId = requiredBoundedString(record, "browserId");
|
|
544
|
+
const tabId = requiredBoundedString(record, "tabId");
|
|
545
|
+
const coordinationScope = requiredString(record, "coordinationScope");
|
|
546
|
+
const evidenceProfile = normalizeEvidenceProfile(requiredData(record, "evidenceProfile"));
|
|
547
|
+
const targetLifecycle = optionalString(record, "targetLifecycle") ?? "fixed";
|
|
548
|
+
const tabClaimEvidenceDigest = optionalDigest(record, "tabClaimEvidenceDigest");
|
|
549
|
+
const canonicalThreadUrl = optionalBoundedString(record, "canonicalThreadUrl");
|
|
550
|
+
const conversationId = optionalBoundedString(record, "conversationId");
|
|
551
|
+
const userTurnBaselineDigest = optionalDigest(record, "userTurnBaselineDigest");
|
|
552
|
+
const assistantTurnBaselineDigest = optionalDigest(record, "assistantTurnBaselineDigest");
|
|
553
|
+
const configurationReceiptDigest = optionalDigest(record, "configurationReceiptDigest");
|
|
554
|
+
const newTargetAnchorDigest = optionalDigest(record, "newTargetAnchorDigest");
|
|
555
|
+
const blankTaskEvidenceDigest = optionalDigest(record, "blankTaskEvidenceDigest");
|
|
556
|
+
const targetEstablishmentValue = optionalDataProperty(record, "targetEstablishment");
|
|
557
|
+
const targetEstablishment = targetEstablishmentValue === undefined
|
|
558
|
+
? undefined
|
|
559
|
+
: normalizeTargetEstablishment(targetEstablishmentValue);
|
|
560
|
+
if ((coordinationScope !== "process" && coordinationScope !== "provider")
|
|
561
|
+
|| (targetLifecycle !== "fixed" && targetLifecycle !== "new_pending" && targetLifecycle !== "new_established")
|
|
562
|
+
|| (coordinationScope === "provider"
|
|
563
|
+
&& (tabClaimEvidenceDigest === undefined || evidenceProfile.authoritativeTabClaim !== "required"))) {
|
|
564
|
+
throw new OperationClientError("invalid_operation_state", "The authenticated target binding is invalid.");
|
|
565
|
+
}
|
|
566
|
+
validateTargetLifecycle(targetLifecycle, evidenceProfile, canonicalThreadUrl, conversationId, newTargetAnchorDigest, blankTaskEvidenceDigest, targetEstablishment);
|
|
567
|
+
return Object.freeze({
|
|
568
|
+
providerId,
|
|
569
|
+
browserId,
|
|
570
|
+
tabId,
|
|
571
|
+
coordinationScope,
|
|
572
|
+
...(tabClaimEvidenceDigest === undefined ? {} : { tabClaimEvidenceDigest }),
|
|
573
|
+
...(canonicalThreadUrl === undefined ? {} : { canonicalThreadUrl }),
|
|
574
|
+
...(conversationId === undefined ? {} : { conversationId }),
|
|
575
|
+
...(userTurnBaselineDigest === undefined ? {} : { userTurnBaselineDigest }),
|
|
576
|
+
...(assistantTurnBaselineDigest === undefined ? {} : { assistantTurnBaselineDigest }),
|
|
577
|
+
...(configurationReceiptDigest === undefined ? {} : { configurationReceiptDigest }),
|
|
578
|
+
evidenceProfile,
|
|
579
|
+
...(targetLifecycle === "fixed" ? {} : { targetLifecycle }),
|
|
580
|
+
...(newTargetAnchorDigest === undefined ? {} : { newTargetAnchorDigest }),
|
|
581
|
+
...(blankTaskEvidenceDigest === undefined ? {} : { blankTaskEvidenceDigest }),
|
|
582
|
+
...(targetEstablishment === undefined ? {} : { targetEstablishment })
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
function validateTargetLifecycle(lifecycle, evidenceProfile, canonicalThreadUrl, conversationId, newTargetAnchorDigest, blankTaskEvidenceDigest, targetEstablishment) {
|
|
586
|
+
if (lifecycle === "fixed") {
|
|
587
|
+
if (newTargetAnchorDigest !== undefined || blankTaskEvidenceDigest !== undefined || targetEstablishment !== undefined) {
|
|
588
|
+
throw new OperationClientError("invalid_operation_state", "A fixed target contains new-target establishment fields.");
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (newTargetAnchorDigest === undefined || blankTaskEvidenceDigest === undefined) {
|
|
593
|
+
throw new OperationClientError("invalid_operation_state", "A new target is missing its immutable anchor evidence.");
|
|
594
|
+
}
|
|
595
|
+
if (lifecycle === "new_pending") {
|
|
596
|
+
if (canonicalThreadUrl !== undefined || conversationId !== undefined || targetEstablishment !== undefined) {
|
|
597
|
+
throw new OperationClientError("invalid_operation_state", "A pending new target contains provider identity too early.");
|
|
598
|
+
}
|
|
599
|
+
if (evidenceProfile.stableConversationId !== "unavailable" || evidenceProfile.stableUserTurnId !== "unavailable") {
|
|
600
|
+
throw new OperationClientError("invalid_operation_state", "A pending new target advertises provider identity too early.");
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (canonicalThreadUrl === undefined
|
|
605
|
+
|| conversationId === undefined
|
|
606
|
+
|| targetEstablishment === undefined
|
|
607
|
+
|| evidenceProfile.stableConversationId !== "required"
|
|
608
|
+
|| evidenceProfile.stableUserTurnId !== "required") {
|
|
609
|
+
throw new OperationClientError("invalid_operation_state", "An established new target is missing provider identity evidence.");
|
|
610
|
+
}
|
|
611
|
+
if (targetEstablishment.conversationId !== conversationId
|
|
612
|
+
|| targetEstablishment.canonicalThreadUrl !== canonicalThreadUrl
|
|
613
|
+
|| targetEstablishment.anchorDigest !== newTargetAnchorDigest) {
|
|
614
|
+
throw new OperationClientError("invalid_operation_state", "New-target establishment identity does not match its durable binding.");
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function normalizeEvidenceProfile(value) {
|
|
618
|
+
const record = requiredObject(value, "target evidence profile");
|
|
619
|
+
const providerIdentity = requiredString(record, "providerIdentity");
|
|
620
|
+
const stableTabId = requiredString(record, "stableTabId");
|
|
621
|
+
const stableConversationId = requiredString(record, "stableConversationId");
|
|
622
|
+
const stableUserTurnId = requiredString(record, "stableUserTurnId");
|
|
623
|
+
const authoritativeTabClaim = requiredString(record, "authoritativeTabClaim");
|
|
624
|
+
const replacementTabRecovery = requiredBoolean(record, "replacementTabRecovery");
|
|
625
|
+
if (!isAvailability(providerIdentity)
|
|
626
|
+
|| !isAvailability(stableTabId)
|
|
627
|
+
|| !isAvailability(stableConversationId)
|
|
628
|
+
|| !isAvailability(stableUserTurnId)
|
|
629
|
+
|| !isAvailability(authoritativeTabClaim)) {
|
|
630
|
+
throw new OperationClientError("invalid_operation_state", "The authenticated target evidence profile is invalid.");
|
|
631
|
+
}
|
|
632
|
+
return Object.freeze({
|
|
633
|
+
providerIdentity: providerIdentity,
|
|
634
|
+
stableTabId: stableTabId,
|
|
635
|
+
stableConversationId: stableConversationId,
|
|
636
|
+
stableUserTurnId: stableUserTurnId,
|
|
637
|
+
authoritativeTabClaim: authoritativeTabClaim,
|
|
638
|
+
replacementTabRecovery
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function normalizeTargetEstablishment(value) {
|
|
642
|
+
const record = requiredObject(value, "target establishment");
|
|
643
|
+
const targetBindingDigest = requiredDigest(record, "targetBindingDigest");
|
|
644
|
+
const anchorDigest = requiredDigest(record, "anchorDigest");
|
|
645
|
+
const causalSendActionId = requiredBoundedString(record, "causalSendActionId");
|
|
646
|
+
const conversationId = requiredBoundedString(record, "conversationId");
|
|
647
|
+
const canonicalThreadUrl = requiredBoundedString(record, "canonicalThreadUrl");
|
|
648
|
+
const userTurnId = requiredBoundedString(record, "userTurnId");
|
|
649
|
+
const userTurnEvidenceDigest = requiredDigest(record, "userTurnEvidenceDigest");
|
|
650
|
+
const evidenceDigest = requiredDigest(record, "evidenceDigest");
|
|
651
|
+
const observedAt = requiredBoundedString(record, "observedAt");
|
|
652
|
+
return Object.freeze({
|
|
653
|
+
targetBindingDigest,
|
|
654
|
+
anchorDigest,
|
|
655
|
+
causalSendActionId,
|
|
656
|
+
conversationId,
|
|
657
|
+
canonicalThreadUrl,
|
|
658
|
+
userTurnId,
|
|
659
|
+
userTurnEvidenceDigest,
|
|
660
|
+
evidenceDigest,
|
|
661
|
+
observedAt
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
function makeFactoryContext(handle, state, target) {
|
|
665
|
+
const context = { ...handle };
|
|
666
|
+
Object.defineProperties(context, {
|
|
667
|
+
handle: { value: handle, enumerable: false, writable: false, configurable: false },
|
|
668
|
+
state: { value: state, enumerable: false, writable: false, configurable: false },
|
|
669
|
+
target: { value: target, enumerable: false, writable: false, configurable: false }
|
|
670
|
+
});
|
|
671
|
+
return Object.freeze(context);
|
|
672
|
+
}
|
|
673
|
+
function makeControlFactoryContext(request, reconstruction) {
|
|
674
|
+
const context = { request };
|
|
675
|
+
Object.defineProperties(context, {
|
|
676
|
+
handle: {
|
|
677
|
+
value: reconstruction.context.handle,
|
|
678
|
+
enumerable: false,
|
|
679
|
+
writable: false,
|
|
680
|
+
configurable: false
|
|
681
|
+
},
|
|
682
|
+
state: {
|
|
683
|
+
value: reconstruction.context.state,
|
|
684
|
+
enumerable: false,
|
|
685
|
+
writable: false,
|
|
686
|
+
configurable: false
|
|
687
|
+
},
|
|
688
|
+
target: {
|
|
689
|
+
value: reconstruction.context.target,
|
|
690
|
+
enumerable: false,
|
|
691
|
+
writable: false,
|
|
692
|
+
configurable: false
|
|
693
|
+
},
|
|
694
|
+
durable: {
|
|
695
|
+
value: reconstruction.context,
|
|
696
|
+
enumerable: false,
|
|
697
|
+
writable: false,
|
|
698
|
+
configurable: false
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
return Object.freeze(context);
|
|
702
|
+
}
|
|
703
|
+
function unavailableAdapter(code) {
|
|
704
|
+
const unavailable = async () => {
|
|
705
|
+
throw Object.freeze({ code });
|
|
706
|
+
};
|
|
707
|
+
return Object.freeze({
|
|
708
|
+
resolveTarget: unavailable,
|
|
709
|
+
submission: Object.freeze({
|
|
710
|
+
observeStaging: unavailable,
|
|
711
|
+
executeFileHandoffOnce: unavailable,
|
|
712
|
+
observeAttachments: unavailable,
|
|
713
|
+
prepareSend: unavailable,
|
|
714
|
+
executePreparedSend: unavailable,
|
|
715
|
+
verifyPreparedSend: unavailable,
|
|
716
|
+
recoverSend: unavailable,
|
|
717
|
+
executeFinalTabTransaction: unavailable
|
|
718
|
+
}),
|
|
719
|
+
collector: Object.freeze({
|
|
720
|
+
readContext: unavailable,
|
|
721
|
+
observe: unavailable,
|
|
722
|
+
sleep: unavailable
|
|
723
|
+
})
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function readRequestedFiles(request) {
|
|
727
|
+
if (request.files === undefined)
|
|
728
|
+
return [];
|
|
729
|
+
if (!Array.isArray(request.files)) {
|
|
730
|
+
throw new OperationClientError("invalid_operation_request", "Operation request files must be an array.");
|
|
731
|
+
}
|
|
732
|
+
return request.files;
|
|
733
|
+
}
|
|
734
|
+
function sanitizeServiceRequest(request, identities) {
|
|
735
|
+
const copy = cloneFrozen(request, "invalid_operation_request", false);
|
|
736
|
+
if (request.files !== undefined) {
|
|
737
|
+
copy.files = identities.map((identity, index) => ({
|
|
738
|
+
path: `${SAFE_INPUT_PATH_PREFIX}${index + 1}`,
|
|
739
|
+
displayName: identity.manifest.displayName
|
|
740
|
+
}));
|
|
741
|
+
}
|
|
742
|
+
if (request.capture !== undefined && request.capture.outputDirectory !== undefined) {
|
|
743
|
+
copy.capture = {
|
|
744
|
+
...request.capture,
|
|
745
|
+
outputDirectory: SAFE_OUTPUT_PATH
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return freezeDeep(copy);
|
|
749
|
+
}
|
|
750
|
+
function freezeIdentity(identity) {
|
|
751
|
+
const snapshot = cloneDataGraph(identity, "invalid_file_identity", false);
|
|
752
|
+
if (snapshot === null || typeof snapshot !== "object" || snapshot.manifest === undefined || snapshot.proof === undefined) {
|
|
753
|
+
throw new OperationClientError("invalid_file_identity", "The operation file identity is invalid.");
|
|
754
|
+
}
|
|
755
|
+
return freezeDeep({
|
|
756
|
+
sourcePath: snapshot.sourcePath,
|
|
757
|
+
manifest: { ...snapshot.manifest },
|
|
758
|
+
proof: { ...snapshot.proof }
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
function forwardSubmitOptions(options, signal) {
|
|
762
|
+
return Object.freeze({
|
|
763
|
+
signal,
|
|
764
|
+
...(options.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt })
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
function forwardCollectorOptions(options) {
|
|
768
|
+
const forwarded = {
|
|
769
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
770
|
+
...(options.wait === undefined ? {} : { wait: options.wait }),
|
|
771
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
772
|
+
...(options.maxAttempts === undefined ? {} : { maxAttempts: options.maxAttempts }),
|
|
773
|
+
...(options.pollIntervalMs === undefined ? {} : { pollIntervalMs: options.pollIntervalMs }),
|
|
774
|
+
...(options.responseContent === undefined ? {} : { responseContent: options.responseContent }),
|
|
775
|
+
...(options.responseFormat === undefined ? {} : { responseFormat: options.responseFormat }),
|
|
776
|
+
...(options.now === undefined ? {} : { now: options.now })
|
|
777
|
+
};
|
|
778
|
+
return Object.freeze(forwarded);
|
|
779
|
+
}
|
|
780
|
+
function forwardControlOptions(options) {
|
|
781
|
+
return Object.freeze({
|
|
782
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
783
|
+
...(options.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt }),
|
|
784
|
+
...(options.now === undefined ? {} : { now: options.now })
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
function forwardRunOptions(options, signal) {
|
|
788
|
+
const forwarded = {
|
|
789
|
+
signal,
|
|
790
|
+
...(options.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt }),
|
|
791
|
+
...(options.wait === undefined ? {} : { wait: options.wait }),
|
|
792
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
793
|
+
...(options.maxAttempts === undefined ? {} : { maxAttempts: options.maxAttempts }),
|
|
794
|
+
...(options.pollIntervalMs === undefined ? {} : { pollIntervalMs: options.pollIntervalMs }),
|
|
795
|
+
...(options.responseContent === undefined ? {} : { responseContent: options.responseContent }),
|
|
796
|
+
...(options.responseFormat === undefined ? {} : { responseFormat: options.responseFormat }),
|
|
797
|
+
...(options.now === undefined ? {} : { now: options.now })
|
|
798
|
+
};
|
|
799
|
+
return Object.freeze(forwarded);
|
|
800
|
+
}
|
|
801
|
+
function adapterKey(operationId, requestDigest) {
|
|
802
|
+
return `${operationId}\0${requestDigest}`;
|
|
803
|
+
}
|
|
804
|
+
function validateMaxCachedAdapters(value) {
|
|
805
|
+
const result = value ?? DEFAULT_MAX_CACHED_ADAPTERS;
|
|
806
|
+
if (!Number.isSafeInteger(result) || result < 1 || result > MAX_CACHED_ADAPTERS) {
|
|
807
|
+
throw new OperationClientError("invalid_adapter_cache_size", `maxCachedAdapters must be a positive integer no greater than ${MAX_CACHED_ADAPTERS}.`);
|
|
808
|
+
}
|
|
809
|
+
return result;
|
|
810
|
+
}
|
|
811
|
+
function isTerminalSubmitResult(result) {
|
|
812
|
+
return result.submission.kind === "completed_receipt";
|
|
813
|
+
}
|
|
814
|
+
function isTerminalRunResult(result) {
|
|
815
|
+
return result.submit.submission.kind === "completed_receipt"
|
|
816
|
+
|| result.collect?.kind === "completed";
|
|
817
|
+
}
|
|
818
|
+
function assertAbortSignal(value) {
|
|
819
|
+
if (value === null || typeof value !== "object" || typeof AbortSignal !== "function") {
|
|
820
|
+
throw new OperationClientError("invalid_signal", "Operation signal must be an AbortSignal.");
|
|
821
|
+
}
|
|
822
|
+
const abortedGetter = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted")?.get;
|
|
823
|
+
if (abortedGetter === undefined) {
|
|
824
|
+
throw new OperationClientError("invalid_signal", "Operation signal must be an AbortSignal.");
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
if (typeof Reflect.apply(abortedGetter, value, []) !== "boolean")
|
|
828
|
+
throw new Error("invalid");
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
throw new OperationClientError("invalid_signal", "Operation signal must be an AbortSignal.");
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function fileBoundaryError(error) {
|
|
835
|
+
let candidate = "operation_file_identity_failed";
|
|
836
|
+
if (error !== null && typeof error === "object") {
|
|
837
|
+
try {
|
|
838
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, "code");
|
|
839
|
+
if (descriptor !== undefined && "value" in descriptor && typeof descriptor.value === "string") {
|
|
840
|
+
candidate = descriptor.value;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
// Treat proxies and accessors as untrusted provider input.
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const code = /^[a-z][a-z0-9_]{0,63}$/.test(candidate)
|
|
848
|
+
? candidate
|
|
849
|
+
: "operation_file_identity_failed";
|
|
850
|
+
return new OperationClientError(code, "The operation input file could not be established or revalidated safely.");
|
|
851
|
+
}
|
|
852
|
+
function cloneFrozen(value, code, freeze = true) {
|
|
853
|
+
if (value === null || typeof value !== "object") {
|
|
854
|
+
throw new OperationClientError(code, "The operation input is invalid.");
|
|
855
|
+
}
|
|
856
|
+
const clone = cloneDataGraph(value, code, false);
|
|
857
|
+
return freeze ? freezeDeep(clone) : clone;
|
|
858
|
+
}
|
|
859
|
+
function freezeDeep(value, seen = new WeakSet()) {
|
|
860
|
+
if (value === null || typeof value !== "object")
|
|
861
|
+
return value;
|
|
862
|
+
const object = value;
|
|
863
|
+
if (seen.has(object))
|
|
864
|
+
return value;
|
|
865
|
+
seen.add(object);
|
|
866
|
+
let descriptors;
|
|
867
|
+
try {
|
|
868
|
+
descriptors = Object.getOwnPropertyDescriptors(object);
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
throw new OperationClientError("invalid_operation_input", "The operation input could not be frozen safely.");
|
|
872
|
+
}
|
|
873
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
874
|
+
if ("value" in descriptor)
|
|
875
|
+
freezeDeep(descriptor.value, seen);
|
|
876
|
+
}
|
|
877
|
+
return Object.freeze(value);
|
|
878
|
+
}
|
|
879
|
+
function freshResult(value) {
|
|
880
|
+
return cloneDataGraph(value, "result_clone_failed", false);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Bounded descriptor-only clone used at all untrusted facade boundaries.
|
|
884
|
+
* `structuredClone` is unsuitable here because it invokes enumerable
|
|
885
|
+
* getters. Unknown future fields are preserved for ordinary results, but any
|
|
886
|
+
* accessor, function, symbol, exotic object, cycle, or oversized graph fails
|
|
887
|
+
* closed with a static client error.
|
|
888
|
+
*/
|
|
889
|
+
function cloneDataGraph(value, code, _freeze, depth = 0, seen = new WeakSet(), budget = { nodes: 0 }) {
|
|
890
|
+
if (value === null || typeof value !== "object") {
|
|
891
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
892
|
+
throw new OperationClientError(code, "The operation data could not be copied safely.");
|
|
893
|
+
}
|
|
894
|
+
return value;
|
|
895
|
+
}
|
|
896
|
+
if (depth > MAX_SAFE_DATA_DEPTH || budget.nodes >= MAX_SAFE_DATA_NODES) {
|
|
897
|
+
throw new OperationClientError(code, "The operation data exceeds its safety bound.");
|
|
898
|
+
}
|
|
899
|
+
const object = value;
|
|
900
|
+
if (seen.has(object)) {
|
|
901
|
+
throw new OperationClientError(code, "The operation data contains a cycle.");
|
|
902
|
+
}
|
|
903
|
+
seen.add(object);
|
|
904
|
+
budget.nodes += 1;
|
|
905
|
+
let prototype;
|
|
906
|
+
let descriptors;
|
|
907
|
+
try {
|
|
908
|
+
prototype = Object.getPrototypeOf(object);
|
|
909
|
+
descriptors = Object.getOwnPropertyDescriptors(object);
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
throw new OperationClientError(code, "The operation data could not be copied safely.");
|
|
913
|
+
}
|
|
914
|
+
if (!Array.isArray(object) && !isPlainDataPrototype(prototype)) {
|
|
915
|
+
throw new OperationClientError(code, "The operation data contains an unsupported object.");
|
|
916
|
+
}
|
|
917
|
+
if (Reflect.ownKeys(descriptors).some(key => typeof key !== "string")) {
|
|
918
|
+
throw new OperationClientError(code, "The operation data contains an unsupported symbol property.");
|
|
919
|
+
}
|
|
920
|
+
if (Array.isArray(object)) {
|
|
921
|
+
const lengthDescriptor = descriptors.length;
|
|
922
|
+
if (lengthDescriptor === undefined || !("value" in lengthDescriptor) || typeof lengthDescriptor.value !== "number") {
|
|
923
|
+
throw new OperationClientError(code, "The operation data contains an invalid array.");
|
|
924
|
+
}
|
|
925
|
+
const length = lengthDescriptor.value;
|
|
926
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > MAX_SAFE_DATA_NODES) {
|
|
927
|
+
throw new OperationClientError(code, "The operation data contains an oversized array.");
|
|
928
|
+
}
|
|
929
|
+
const clone = [];
|
|
930
|
+
for (let index = 0; index < length; index += 1) {
|
|
931
|
+
const descriptor = descriptors[String(index)];
|
|
932
|
+
if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
933
|
+
throw new OperationClientError(code, "The operation data contains an unsafe array entry.");
|
|
934
|
+
}
|
|
935
|
+
clone.push(cloneDataGraph(descriptor.value, code, false, depth + 1, seen, budget));
|
|
936
|
+
}
|
|
937
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
938
|
+
if (key === "length" || /^\d+$/u.test(key))
|
|
939
|
+
continue;
|
|
940
|
+
if (!descriptor.enumerable)
|
|
941
|
+
continue;
|
|
942
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
943
|
+
throw new OperationClientError(code, "The operation data contains an unsafe property.");
|
|
944
|
+
}
|
|
945
|
+
Object.defineProperty(clone, key, {
|
|
946
|
+
value: cloneDataGraph(descriptor.value, code, false, depth + 1, seen, budget),
|
|
947
|
+
enumerable: true,
|
|
948
|
+
writable: true,
|
|
949
|
+
configurable: true
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
seen.delete(object);
|
|
953
|
+
return clone;
|
|
954
|
+
}
|
|
955
|
+
const clone = {};
|
|
956
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
957
|
+
if (!descriptor.enumerable)
|
|
958
|
+
continue;
|
|
959
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
960
|
+
throw new OperationClientError(code, "The operation data contains an unsafe property.");
|
|
961
|
+
}
|
|
962
|
+
Object.defineProperty(clone, key, {
|
|
963
|
+
value: cloneDataGraph(descriptor.value, code, false, depth + 1, seen, budget),
|
|
964
|
+
enumerable: true,
|
|
965
|
+
writable: true,
|
|
966
|
+
configurable: true
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
seen.delete(object);
|
|
970
|
+
return clone;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Recognize ordinary records from another JavaScript realm without trusting
|
|
974
|
+
* constructors, `Symbol.toStringTag`, or inherited accessors. A realm's
|
|
975
|
+
* intrinsic `Object.prototype` is itself a direct child of `null`; custom
|
|
976
|
+
* class instances have at least one additional prototype layer. The clone
|
|
977
|
+
* remains descriptor-only and always discards the source prototype.
|
|
978
|
+
*/
|
|
979
|
+
function isPlainDataPrototype(prototype) {
|
|
980
|
+
if (prototype === null || prototype === Object.prototype)
|
|
981
|
+
return true;
|
|
982
|
+
try {
|
|
983
|
+
return Object.getPrototypeOf(prototype) === null;
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function isDigest(value) {
|
|
990
|
+
return DIGEST_PATTERN.test(value);
|
|
991
|
+
}
|
|
992
|
+
function isOperationSurface(value) {
|
|
993
|
+
return value === "chat" || value === "work";
|
|
994
|
+
}
|
|
995
|
+
function isOperationPhase(value) {
|
|
996
|
+
return value === "prepared"
|
|
997
|
+
|| value === "handoff_pending"
|
|
998
|
+
|| value === "ready"
|
|
999
|
+
|| value === "send_pending"
|
|
1000
|
+
|| value === "submitted"
|
|
1001
|
+
|| value === "generating"
|
|
1002
|
+
|| value === "capturing"
|
|
1003
|
+
|| value === "completed"
|
|
1004
|
+
|| value === "uncertain";
|
|
1005
|
+
}
|
|
1006
|
+
function isMutationBoundary(value) {
|
|
1007
|
+
return value === "none"
|
|
1008
|
+
|| value === "handoff_may_have_occurred"
|
|
1009
|
+
|| value === "send_may_have_occurred"
|
|
1010
|
+
|| value === "control_may_have_occurred";
|
|
1011
|
+
}
|
|
1012
|
+
function isAvailability(value) {
|
|
1013
|
+
return value === "required" || value === "unavailable";
|
|
1014
|
+
}
|
|
1015
|
+
const MAX_SAFE_DATA_DEPTH = 24;
|
|
1016
|
+
const MAX_SAFE_DATA_NODES = 4096;
|
|
1017
|
+
const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/u;
|
|
1018
|
+
function requiredObject(value, label) {
|
|
1019
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1020
|
+
throw new OperationClientError("invalid_operation_state", `The ${label} is invalid.`);
|
|
1021
|
+
}
|
|
1022
|
+
return value;
|
|
1023
|
+
}
|
|
1024
|
+
function requiredData(record, key) {
|
|
1025
|
+
const value = optionalDataProperty(record, key);
|
|
1026
|
+
if (value === undefined) {
|
|
1027
|
+
throw new OperationClientError("invalid_operation_state", "The authenticated operation data is incomplete.");
|
|
1028
|
+
}
|
|
1029
|
+
return value;
|
|
1030
|
+
}
|
|
1031
|
+
/** Read only an own data property; accessor descriptors are never invoked. */
|
|
1032
|
+
function optionalDataProperty(record, key) {
|
|
1033
|
+
let descriptor;
|
|
1034
|
+
try {
|
|
1035
|
+
descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
throw new OperationClientError("invalid_operation_state", "The operation data could not be read safely.");
|
|
1039
|
+
}
|
|
1040
|
+
if (descriptor === undefined)
|
|
1041
|
+
return undefined;
|
|
1042
|
+
if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
1043
|
+
throw new OperationClientError("invalid_operation_state", "The operation data contains an unsafe property.");
|
|
1044
|
+
}
|
|
1045
|
+
return descriptor.value;
|
|
1046
|
+
}
|
|
1047
|
+
function requiredString(record, key) {
|
|
1048
|
+
const value = requiredData(record, key);
|
|
1049
|
+
if (typeof value !== "string") {
|
|
1050
|
+
throw new OperationClientError("invalid_operation_state", "The operation data contains an invalid value.");
|
|
1051
|
+
}
|
|
1052
|
+
return value;
|
|
1053
|
+
}
|
|
1054
|
+
function requiredBoundedString(record, key) {
|
|
1055
|
+
const value = requiredString(record, key);
|
|
1056
|
+
if (value.length === 0 || value.length > 4096) {
|
|
1057
|
+
throw new OperationClientError("invalid_operation_state", "The operation data contains an unbounded value.");
|
|
1058
|
+
}
|
|
1059
|
+
return value;
|
|
1060
|
+
}
|
|
1061
|
+
function optionalString(record, key) {
|
|
1062
|
+
const value = optionalDataProperty(record, key);
|
|
1063
|
+
if (value === undefined)
|
|
1064
|
+
return undefined;
|
|
1065
|
+
if (typeof value !== "string") {
|
|
1066
|
+
throw new OperationClientError("invalid_operation_state", "The operation data contains an invalid value.");
|
|
1067
|
+
}
|
|
1068
|
+
return value;
|
|
1069
|
+
}
|
|
1070
|
+
function optionalBoundedString(record, key) {
|
|
1071
|
+
const value = optionalString(record, key);
|
|
1072
|
+
if (value === undefined)
|
|
1073
|
+
return undefined;
|
|
1074
|
+
if (value.length === 0 || value.length > 4096) {
|
|
1075
|
+
throw new OperationClientError("invalid_operation_state", "The operation data contains an unbounded value.");
|
|
1076
|
+
}
|
|
1077
|
+
return value;
|
|
1078
|
+
}
|
|
1079
|
+
function requiredSafeInteger(record, key) {
|
|
1080
|
+
const value = requiredData(record, key);
|
|
1081
|
+
if (!Number.isSafeInteger(value)) {
|
|
1082
|
+
throw new OperationClientError("invalid_operation_state", "The operation data contains an invalid revision.");
|
|
1083
|
+
}
|
|
1084
|
+
return value;
|
|
1085
|
+
}
|
|
1086
|
+
function requiredBoolean(record, key) {
|
|
1087
|
+
const value = requiredData(record, key);
|
|
1088
|
+
if (typeof value !== "boolean") {
|
|
1089
|
+
throw new OperationClientError("invalid_operation_state", "The operation data contains an invalid flag.");
|
|
1090
|
+
}
|
|
1091
|
+
return value;
|
|
1092
|
+
}
|
|
1093
|
+
function requiredDigest(record, key) {
|
|
1094
|
+
const value = requiredString(record, key);
|
|
1095
|
+
if (!isDigest(value)) {
|
|
1096
|
+
throw new OperationClientError("invalid_operation_state", "The operation data contains an invalid digest.");
|
|
1097
|
+
}
|
|
1098
|
+
return value;
|
|
1099
|
+
}
|
|
1100
|
+
function optionalDigest(record, key) {
|
|
1101
|
+
const value = optionalString(record, key);
|
|
1102
|
+
if (value === undefined)
|
|
1103
|
+
return undefined;
|
|
1104
|
+
if (!isDigest(value)) {
|
|
1105
|
+
throw new OperationClientError("invalid_operation_state", "The operation data contains an invalid digest.");
|
|
1106
|
+
}
|
|
1107
|
+
return value;
|
|
1108
|
+
}
|
|
1109
|
+
function requiredMethod(record, key) {
|
|
1110
|
+
const value = optionalDataProperty(record, key);
|
|
1111
|
+
if (typeof value !== "function") {
|
|
1112
|
+
throw new OperationClientError("adapter_unavailable", "The operation browser adapter is incomplete.");
|
|
1113
|
+
}
|
|
1114
|
+
// Bind the captured function to the exact validated owner. The returned
|
|
1115
|
+
// callable no longer consults a mutable property or depends on the caller's
|
|
1116
|
+
// `this`, while the descriptor-only read above keeps accessors/proxies out
|
|
1117
|
+
// of the validation path.
|
|
1118
|
+
return Reflect.apply(Function.prototype.bind, value, [record]);
|
|
1119
|
+
}
|
|
1120
|
+
/** Read and bind an optional method using the same accessor-free boundary. */
|
|
1121
|
+
function optionalMethod(record, key) {
|
|
1122
|
+
const value = optionalDataProperty(record, key);
|
|
1123
|
+
if (value === undefined)
|
|
1124
|
+
return undefined;
|
|
1125
|
+
if (typeof value !== "function") {
|
|
1126
|
+
throw new OperationClientError("adapter_unavailable", "The operation browser adapter contains an invalid optional method.");
|
|
1127
|
+
}
|
|
1128
|
+
return Reflect.apply(Function.prototype.bind, value, [record]);
|
|
1129
|
+
}
|
|
1130
|
+
function requiredAdapterObject(record, key) {
|
|
1131
|
+
if (record === null || typeof record !== "object") {
|
|
1132
|
+
throw new OperationClientError("adapter_unavailable", "The operation browser adapter is incomplete.");
|
|
1133
|
+
}
|
|
1134
|
+
const value = optionalDataProperty(record, key);
|
|
1135
|
+
if (value === undefined) {
|
|
1136
|
+
throw new OperationClientError("adapter_unavailable", "The operation browser adapter is incomplete.");
|
|
1137
|
+
}
|
|
1138
|
+
return adapterObject(value);
|
|
1139
|
+
}
|
|
1140
|
+
function adapterObject(value) {
|
|
1141
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1142
|
+
throw new OperationClientError("adapter_unavailable", "The operation browser adapter is incomplete.");
|
|
1143
|
+
}
|
|
1144
|
+
return value;
|
|
1145
|
+
}
|