@weaccelerateinc/agent-sdk 0.10.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +11 -7
  2. package/approved-extension-releases/0.0.0.1.json +4 -1
  3. package/approved-extension-releases/0.0.0.2.json +4 -1
  4. package/approved-extension.js +1 -1
  5. package/browser-connector.d.ts +22 -0
  6. package/browser-connector.js +48 -4
  7. package/browser-provenance.js +17 -0
  8. package/browser-use-host.d.ts +14 -0
  9. package/browser-use-host.js +77 -18
  10. package/checkout-diagnostics.d.ts +13 -2
  11. package/checkout-diagnostics.js +18 -4
  12. package/cli.js +4 -1
  13. package/host-adapter.d.ts +5 -0
  14. package/host-adapter.js +21 -5
  15. package/index.d.ts +63 -5
  16. package/index.js +84 -13
  17. package/managed-mcp.d.ts +9 -3
  18. package/managed-mcp.js +56 -7
  19. package/package.json +21 -2
  20. package/partner-starter/.env.example +4 -2
  21. package/partner-starter/README.md +19 -6
  22. package/partner-starter/package.json +1 -1
  23. package/partner-starter/runtime.mjs +3 -2
  24. package/partner-starter/smoke.mjs +10 -5
  25. package/partner-starter/synthetic.mjs +37 -7
  26. package/partner-starter/tests/starter.test.mjs +58 -8
  27. package/partner-starter/trusted-context.example.mjs +3 -0
  28. package/partner.js +4 -0
  29. package/postgres-host.d.ts +7 -3
  30. package/postgres-whatsapp-journal.d.ts +8 -0
  31. package/postgres-whatsapp-journal.js +95 -0
  32. package/private-chat.js +10 -3
  33. package/private-ingress.d.ts +4 -0
  34. package/private-ingress.js +22 -9
  35. package/starter/README.md +1 -7
  36. package/starter/package.json +1 -1
  37. package/starter/runtime.mjs +7 -2
  38. package/starter/synthetic.mjs +1 -1
  39. package/starter/tests/starter.test.mjs +7 -39
  40. package/whatsapp-journal.d.ts +11 -0
  41. package/whatsapp-journal.js +78 -0
  42. package/whatsapp-messaging.d.ts +64 -0
  43. package/whatsapp-messaging.js +182 -0
package/README.md CHANGED
@@ -1,9 +1,13 @@
1
- # @weaccelerateinc/agent-sdk 0.10.3
1
+ # @weaccelerateinc/agent-sdk 0.12.0
2
2
 
3
- Server-only ESM SDK with TypeScript declarations, Node 22+, zero runtime dependencies. New partners use their existing browser and explicitly select `managed_review_v2`, which continues an armed fill immediately after private CVV verification. Existing callers keep `managed_review_v1` unless they opt in. Public npm releases use `@weaccelerateinc/agent-sdk`; install an exact available version with `npm install @weaccelerateinc/agent-sdk@0.10.3`. Check `npm view @weaccelerateinc/agent-sdk@0.10.3 version` to confirm registry availability. The package remains `UNLICENSED`.
3
+ SDK 0.12.0 adds opt-in Browser Use run-created fill-only checkout, exact additional-extension policies, and private WhatsApp adapters with durable sent-receipt reconciliation. The platform must implement durable owner/session/viewer fences and qualify its provider. Existing connector defaults and the 0.11.0 customer-final-click purchase contract remain. The approved extension ZIP is unchanged; its compatibility metadata includes 0.12.0.
4
+
5
+ Version 0.11.0 is a breaking release: **Accelerate never sends the final payment click.** A purchase arms one short click, the customer's agent performs it once through its own browser connection (`BrowserConnectorHost.performFinalClick`) in the customer's existing browser, then Accelerate observes the outcome. Purchase requires `final_click_actor:'customer'` and that host; the managed Kernel browser is fill-only. `client.checkouts.submit` rejects locally with `customer_click_required`. See [customer-owned final click](../../docs/customer-final-click.md). Release 0.10.3 keeps its existing behavior. Merchant layouts remain limited to the qualified sandbox page; this SDK release does not by itself qualify any merchant layout, a service deployment or production payments.
6
+
7
+ Server-only ESM SDK with TypeScript declarations, Node 22+, zero runtime dependencies. New partners use their existing browser and explicitly select `managed_review_v2`, which continues an armed fill immediately after private CVV verification. Existing callers keep `managed_review_v1` unless they opt in. Public npm releases use `@weaccelerateinc/agent-sdk`; install an exact available version with `npm install @weaccelerateinc/agent-sdk@0.12.0`. Check `npm view @weaccelerateinc/agent-sdk@0.12.0 version` to confirm registry availability. The package remains `UNLICENSED`.
4
8
 
5
9
  ```sh
6
- npm install --ignore-scripts @weaccelerateinc/agent-sdk@0.10.3
10
+ npm install --ignore-scripts @weaccelerateinc/agent-sdk@0.12.0
7
11
  npx --no-install accelerate-agent init ./partner-demo --browser existing
8
12
  ```
9
13
 
@@ -25,15 +29,15 @@ The platform creates the browser and installs the approved extension using its e
25
29
  | `/managed-mcp` | Lower-level canonical tool registration and explicit legacy/managed browser compatibility. |
26
30
  | `/browser-operations` | Strict bounded operation schemas; attached-customer mode permits pre-fill reads only. |
27
31
 
28
- The matching [partner guide](https://github.com/weaccelerateinc/accelerate-agent-sdk/blob/main/docs/new-customer-integration.md) and packaged `partner-starter/` show composition. Connect actual backend authentication, document controls, durable queues and private webhook before live use; examples intentionally fail without them.
32
+ The matching [partner guide](https://github.com/weaccelerateinc/accelerate/blob/main/products/agent-sdk/docs/new-customer-integration.md) and packaged `partner-starter/` show composition. Connect actual backend authentication, document controls, durable queues and private webhook before live use; examples intentionally fail without them.
29
33
 
30
34
  Store `ACCELERATE_API_KEY` and `ACCELERATE_MERCHANT_ID` on the server. `ACCELERATE_BASE_URL` defaults to `https://accelerate-agent-sdk.vercel.app`. `accelerate-agent doctor` checks only that credential/API pair, never live downstream readiness. `new AccelerateClient({baseUrl,apiKey,merchantId,fetchImpl?})` supports custom transport; `allowLoopbackForTests:true` is local-test-only.
31
35
 
32
- For v2, create declares `fill_after_verification:true` and requires the service acknowledgement before OTP. That declaration alone cannot fill. The later verified quote repeats the flag to arm one exact revision; the successful private CVV reply fills immediately under the same backend lease and returns `review_delivery_required`. Private ingress delivers and acknowledges the masked image without asking the model to call fill. The v2 MCP registry omits the fill tool; an armed checkout rejects direct manual fill with `fill_continuation_required`. Read only its safe status. Final submit remains explicit. See [managed review v2](https://github.com/weaccelerateinc/accelerate-agent-sdk/blob/main/docs/managed-review-v2.md).
36
+ For v2, create declares `fill_after_verification:true` and requires the service acknowledgement before OTP. That declaration alone cannot fill. The later verified quote repeats the flag to arm one exact revision; the successful private CVV reply fills immediately under the same backend lease and returns `review_delivery_required`. Private ingress delivers and acknowledges the masked image without asking the model to call fill. The v2 MCP registry omits the fill tool; an armed checkout rejects direct manual fill with `fill_continuation_required`. Read only its safe status. Final submit remains explicit. See [managed review v2](https://github.com/weaccelerateinc/accelerate/blob/main/products/agent-sdk/docs/managed-review-v2.md).
33
37
 
34
38
  Required sequence: shop → payment → same-browser attachment → private OTP/card → address/final amount → private CVV → fill once → private masked image/amount → five-minute decision → explicit submit → cleanup. PAN stars+last four, CVV stars, expiry visible. Compatible layouts require the matching qualified service/executor; unsupported fields or safe review stop before private collection. Provider-owner access remains trusted.
35
39
 
36
- `registerPartnerCheckoutTools({profile:'managed_review_v2', ...})` selects automatic private continuation and defaults to `purpose:'sandbox_fill_only',enableSubmit:false`. Its returned private ingress must run before framework logging/parsing/history/AI. A fresh private receipt/approval enables purchase; it never triggers submission. Fill-only tests cancel after delivered informational review. Private replies/browser credentials are never tool inputs/results. Recover only safe deliveries; never replay fill/submit.
40
+ `registerPartnerCheckoutTools({profile:'managed_review_v2', ...})` selects automatic private continuation and retains its API defaults `purpose:'sandbox_fill_only',enableSubmit:false`. Pass `purpose:'purchase',enableSubmit:true` for purchase registration; it also requires a connector whose host implements `performFinalClick`. New `init --browser existing` projects from 0.11.0 select purchase mode by default; already generated apps keep their copied runtime. Its returned private ingress must run before framework logging/parsing/history/AI. A fresh private receipt/approval enables purchase; it never triggers submission. Fill-only tests cancel after delivered informational review. Private replies/browser credentials are never tool inputs/results. Recover only safe deliveries; never replay fill/submit.
37
41
 
38
42
  Compatibility: `accelerate-agent init <dir>` without `--browser existing` keeps the original managed Kernel starter. `registerManagedCheckoutTools` still defaults to `browserMode:'accelerate_managed'`. Existing before-fill integrations explicitly use `legacy_before_fill_v1`. Low-level hosted/raw APIs keep their compatibility defaults; a package upgrade does not change custody.
39
43
 
@@ -117,7 +121,7 @@ import {
117
121
 
118
122
  Use this entry point only in authenticated ingress **before** history, queues, analytics or model calls. `chat.start/get/reply/cancel` require the exact customer/workspace/conversation binding. `chat.reply` also requires a stable provider message ID and transient `text`; the mutable input text is cleared after the request, including failures.
119
123
 
120
- `renderPrivateChatState(state,{messageId})` returns a safe prompt or `undefined`. `privateChatProgress` returns a fixed safe preparation/verification/closure event. Both revalidate safe metadata and provide a hashed delivery idempotency key. They do not verify webhooks, authorize owners or replace durable ingress/delivery deduplication. Reuse your existing authenticated webhook, ownership lock and message sender. See the [private ingress example](https://github.com/weaccelerateinc/accelerate-agent-sdk/blob/main/examples/private-chat-ingress.js).
124
+ `renderPrivateChatState(state,{messageId})` returns a safe prompt or `undefined`. `privateChatProgress` returns a fixed safe preparation/verification/closure event. Both revalidate safe metadata and provide a hashed delivery idempotency key. They do not verify webhooks, authorize owners or replace durable ingress/delivery deduplication. Reuse your existing authenticated webhook, ownership lock and message sender. See the [private ingress example](https://github.com/weaccelerateinc/accelerate/blob/main/products/agent-sdk/examples/private-chat-ingress.js).
121
125
 
122
126
  ## Safe results and recovery
123
127
 
@@ -25,7 +25,10 @@
25
25
  "0.10.0",
26
26
  "0.10.1",
27
27
  "0.10.2",
28
- "0.10.3"
28
+ "0.10.3",
29
+ "0.10.4",
30
+ "0.11.0",
31
+ "0.12.0"
29
32
  ],
30
33
  "executor_contract": "accelerate_manual_checkout_v1"
31
34
  },
@@ -24,7 +24,10 @@
24
24
  "0.10.0",
25
25
  "0.10.1",
26
26
  "0.10.2",
27
- "0.10.3"
27
+ "0.10.3",
28
+ "0.10.4",
29
+ "0.11.0",
30
+ "0.12.0"
28
31
  ],
29
32
  "executor_contract": "accelerate_manual_checkout_v1"
30
33
  },
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
 
3
- const SDK_VERSION = '0.10.3';
3
+ const SDK_VERSION = '0.12.0';
4
4
  const EXECUTOR_CONTRACT = 'accelerate_manual_checkout_v1';
5
5
  const BRIDGE_PROTOCOL = 'accelerate_extension_bridge_v1';
6
6
  const LEGACY_SHA256 = '65574cbb27ab344687963f72a136f2653551b57458b284e486946b27699099c8';
@@ -46,8 +46,22 @@ export interface BrowserConnectorHost {
46
46
  }>, options: RequestOptions): Promise<{
47
47
  provider_browser_stopped: true; credentials_cleared: true; fence_retained: true;
48
48
  }>;
49
+ /** Optional, but REQUIRED for purchase. Perform exactly one trusted pointer click on the unique selector in the exact bound
50
+ * browser, document and URL under the existing owner fence. No retry, no general DOM reads, never lift the fence.
51
+ * Accelerate sends no physical input. A thrown error or dispatched:false is observation only; the caller then completes once. */
52
+ performFinalClick?(context: Readonly<ManagedCheckoutContext>, request: Readonly<FinalClickRequest>, options: RequestOptions): Promise<{ dispatched: boolean }>;
53
+ }
54
+ /** Private request to the customer-owned browser host for the ONE physical final click. Never model/MCP data. */
55
+ export interface FinalClickRequest extends FencedBrowserDocument {
56
+ checkout_id: CheckoutId;
57
+ /** Unique selector bound at approval, inside the exact fenced document. */
58
+ submit_selector: string;
59
+ armed_expires_at: string;
49
60
  }
50
61
  declare const attachmentBrand: unique symbol;
62
+ declare const finalClickPreparationBrand: unique symbol;
63
+ /** One-use private readiness proof from prepareFinalClick; valid for one performFinalClick on this connector instance. */
64
+ export interface PreparedFinalClick { readonly [finalClickPreparationBrand]: true }
51
65
  /** Private one-connector capability returned only to the authenticated backend. */
52
66
  export interface PreparedBrowserAttachment {
53
67
  readonly browser: Readonly<CustomerBrowserBinding>;
@@ -70,6 +84,14 @@ export interface BrowserConnector {
70
84
  confirmCleanup(context: Readonly<ManagedCheckoutContext>, request: Readonly<{
71
85
  checkout_id: CheckoutId;
72
86
  }>, options?: RequestOptions): Promise<void>;
87
+ /** Present only when the host implements performFinalClick. Call BEFORE arming: resolves the fenced owned document, checks
88
+ * provider/extension readiness and the drained fence, without lifting it, so none of that work consumes the short click window. */
89
+ prepareFinalClick?(context: Readonly<ManagedCheckoutContext>, request: Readonly<{ checkout_id: CheckoutId }>, options?: RequestOptions): Promise<PreparedFinalClick>;
90
+ /** Cheap checks only (one-use preparation, exact URL, arm window <= 15s, browser outlives the arm), then asks the customer's
91
+ * host for one physical click. The arm token never reaches the host. No retry. */
92
+ performFinalClick?(context: Readonly<ManagedCheckoutContext>, request: Readonly<{
93
+ checkout_id: CheckoutId; preparation: PreparedFinalClick; checkout_url: string; submit_selector: string; armed_expires_at: string;
94
+ }>, options?: RequestOptions): Promise<{ dispatched: boolean }>;
73
95
  }
74
96
  export interface ExistingBrowserConnectorOptions {
75
97
  /** Provisioned server connector ID; the Accelerate executor must enable this ID separately. */
@@ -1,4 +1,5 @@
1
1
  import { AccelerateError } from './index.js';
2
+ import { browserUseRunProvenance, sameBrowserUseProvenance } from './browser-provenance.js';
2
3
  import { normalizeBrowserOperation } from './browser-operations.js';
3
4
  import { validateApprovedExtensionRelease, verifyApprovedExtensionRuntime } from './approved-extension.js';
4
5
 
@@ -40,11 +41,14 @@ function options(value = {}) {
40
41
  return Object.freeze(value.signal ? { signal: value.signal } : {});
41
42
  }
42
43
  function browser(value, provider, connectorId) {
43
- shape(value, ['provider', 'browser_id', 'custody_mode'], provider === 'connector' ? ['connector_id'] : [], 'permission_denied');
44
+ shape(value, ['provider', 'browser_id', 'custody_mode'], provider === 'connector' ? ['connector_id'] : ['provenance'], 'permission_denied');
44
45
  check(value.provider === provider && value.custody_mode === 'customer_backend_trusted' &&
45
46
  typeof value.browser_id === 'string' && (provider === 'browser_use' ? UUID : OPAQUE).test(value.browser_id) &&
46
47
  (provider !== 'connector' || value.connector_id === connectorId), 'permission_denied');
47
- return { ...value };
48
+ let provenance;
49
+ try { if (value.provenance !== undefined) provenance = browserUseRunProvenance(value.provenance); }
50
+ catch { throw new AccelerateError('permission_denied'); }
51
+ return { ...value, ...(provenance ? { provenance } : {}) };
48
52
  }
49
53
  function record(value, context, config, { fenced = false, readiness = false, checkoutUrl } = {}) {
50
54
  shape(value, DOC, [...(fenced ? ['fence_id'] : []), ...(readiness ? ['recording_disabled', 'extension', 'automation'] : [])], 'permission_denied');
@@ -66,6 +70,7 @@ function record(value, context, config, { fenced = false, readiness = false, che
66
70
  function sameDocument(actual, expected) {
67
71
  check(actual.document_id === expected.document_id && actual.checkout_url === expected.checkout_url &&
68
72
  ['provider', 'browser_id', 'connector_id', 'custody_mode'].every(key => actual.browser[key] === expected.browser[key]) &&
73
+ sameBrowserUseProvenance(actual.browser.provenance, expected.browser.provenance) &&
69
74
  actual.expires_at === expected.expires_at, 'permission_denied');
70
75
  }
71
76
  function makeConnector(configuration, provider) {
@@ -91,14 +96,14 @@ function makeConnector(configuration, provider) {
91
96
  }
92
97
  };
93
98
  const checkpoint = opts => check(!opts.signal?.aborted, 'request_cancelled');
94
- const inspect = async (context, expected, opts, fenceId) => {
99
+ const inspect = async (context, expected, opts, fenceId, minimum = minimumRemainingMs) => {
95
100
  checkpoint(opts);
96
101
  const value = await hostCall('inspectBrowser', context, expected, opts);
97
102
  checkpoint(opts);
98
103
  const actual = record(value, context, config, { readiness: true });
99
104
  sameDocument(actual, expected);
100
105
  const current = now();
101
- check(Number.isFinite(current) && Date.parse(actual.expires_at) - current >= minimumRemainingMs &&
106
+ check(Number.isFinite(current) && Date.parse(actual.expires_at) - current >= minimum &&
102
107
  value.recording_disabled === true, 'integration_unavailable');
103
108
  check(object(value.extension) && value.extension.artifact_sha256 === release.artifact.sha256, 'integration_unavailable');
104
109
  try { verifyApprovedExtensionRuntime(value.extension, release, { executorContract: 'accelerate_manual_checkout_v1', provider }); }
@@ -115,6 +120,44 @@ function makeConnector(configuration, provider) {
115
120
  checkpoint(opts);
116
121
  return record(resolved, context, config, { fenced: true });
117
122
  };
123
+ const stamp = value => {
124
+ check(typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/u.test(value) && Number.isFinite(Date.parse(value)), 'invalid_input');
125
+ return Date.parse(value);
126
+ };
127
+ const preparations = new WeakMap();
128
+ const canClick = typeof host.performFinalClick === 'function';
129
+ // Slow work (ownership lookup, provider read, runtime inspection) happens here, BEFORE the short arm exists,
130
+ // so none of it consumes the click window. The fence is never lifted. One preparation serves one click.
131
+ const prepareClick = canClick ? async (contextValue, input, requestOptions) => {
132
+ const context = owner(contextValue), opts = options(requestOptions);
133
+ shape(input, ['checkout_id'], [], 'invalid_input');
134
+ const expected = await resolve(context, input.checkout_id, opts);
135
+ // Fenced, drained runtime check; the ten-minute attachment headroom does not apply after the review window.
136
+ await inspect(context, expected, opts, expected.fence_id, 1);
137
+ const preparation = Object.freeze({});
138
+ preparations.set(preparation, { context, checkout_id: input.checkout_id, record: expected, at: now(), used: false });
139
+ return preparation;
140
+ } : undefined;
141
+ const finalClick = canClick ? async (contextValue, input, requestOptions) => {
142
+ const context = owner(contextValue), opts = options(requestOptions);
143
+ shape(input, ['checkout_id', 'preparation', 'checkout_url', 'submit_selector', 'armed_expires_at'], [], 'invalid_input');
144
+ const saved = preparations.get(input.preparation);
145
+ check(saved && !saved.used && sameOwner(saved.context, context) && saved.checkout_id === input.checkout_id, 'permission_denied');
146
+ saved.used = true;
147
+ check(typeof input.submit_selector === 'string' && input.submit_selector.length > 0 && input.submit_selector.length <= 200 &&
148
+ !/[\u0000-\u001f\u007f]/u.test(input.submit_selector), 'invalid_input');
149
+ const armedUntil = stamp(input.armed_expires_at), started = now();
150
+ // The service arm lives at most 15 seconds (never accepted longer); refuse an expired or implausibly long window.
151
+ check(Number.isFinite(started) && armedUntil > started && armedUntil - started <= 15000 && started - saved.at <= 60000, 'submit_unavailable');
152
+ // Cheap checks only inside the arm window: exact URL, browser outlives the arm, then the customer's physical click.
153
+ check(url(input.checkout_url) === saved.record.checkout_url && Date.parse(saved.record.expires_at) > armedUntil, 'permission_denied');
154
+ checkpoint(opts);
155
+ const result = await hostCall('performFinalClick', context, freeze({ checkout_id: input.checkout_id,
156
+ submit_selector: input.submit_selector, armed_expires_at: input.armed_expires_at, ...saved.record }), opts);
157
+ shape(result, ['dispatched'], [], 'integration_unavailable');
158
+ check(typeof result.dispatched === 'boolean', 'integration_unavailable');
159
+ return Object.freeze({ dispatched: result.dispatched });
160
+ } : undefined;
118
161
  // Every method belongs to the server integration. A connector never creates a
119
162
  // replacement browser, releases its durable fence or retries a payment call.
120
163
  return Object.freeze({ contract: 'accelerate_existing_browser_v1', provider,
@@ -163,6 +206,7 @@ function makeConnector(configuration, provider) {
163
206
  if (input.checkout_url !== undefined) check(url(input.checkout_url) === expected.checkout_url, 'permission_denied');
164
207
  await inspect(context, expected, opts, expected.fence_id);
165
208
  },
209
+ ...(finalClick ? { prepareFinalClick: prepareClick, performFinalClick: finalClick } : {}),
166
210
  async confirmCleanup(contextValue, input, requestOptions) {
167
211
  const context = owner(contextValue), opts = options(requestOptions);
168
212
  shape(input, ['checkout_id'], [], 'invalid_input');
@@ -0,0 +1,17 @@
1
+ const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u;
2
+ const fields = ['type', 'run_id', 'session_id', 'provider_workspace_id', 'binding_sha256'];
3
+ /** Private backend binding. Provider workspace membership is not shopper ownership. */
4
+ export function browserUseRunProvenance(value) {
5
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
6
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)) || Object.keys(value).length !== fields.length ||
7
+ !fields.every(key => Object.hasOwn(value, key)) || value.type !== 'browser_use_run_v1' ||
8
+ !uuid.test(value.run_id) || !uuid.test(value.session_id) ||
9
+ !(value.provider_workspace_id === null || uuid.test(value.provider_workspace_id)) ||
10
+ typeof value.binding_sha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(value.binding_sha256)) throw Error('Invalid browser provenance.');
11
+ return Object.freeze(Object.fromEntries(fields.map(key => [key, value[key]])));
12
+ }
13
+ export function sameBrowserUseProvenance(a, b) {
14
+ if (a === undefined || b === undefined) return a === b;
15
+ try { return JSON.stringify(browserUseRunProvenance(a)) === JSON.stringify(browserUseRunProvenance(b)); }
16
+ catch { return false; }
17
+ }
@@ -10,6 +10,9 @@ export interface BrowserUseDocumentObservation {
10
10
  checkout_url: string;
11
11
  extension: Omit<ApprovedExtensionRuntime, 'artifact_sha256'>;
12
12
  automation: BrowserReadiness['automation'];
13
+ /** Required after a run-created browser is fenced. Derived from the durable session store,
14
+ * including queued/follow-up work, live-view issuance and human takeover routes. */
15
+ run_control?: { checkout_reserved: true; session_scheduling_fenced: true; live_view_blocked: true };
13
16
  }
14
17
  export interface BrowserUsePlatformHost {
15
18
  /** Resolve an EXISTING browser from authenticated durable ownership. SDK reads actual provider expiry. */
@@ -22,6 +25,9 @@ export interface BrowserUsePlatformHost {
22
25
  * if suspended. Do not create/navigate a page or accept model-authored code. */
23
26
  inspectDocument(context: Readonly<ManagedCheckoutContext>, browser: Readonly<OwnedBrowserDocument>,
24
27
  runtime: Readonly<{ runtime_id: string }>, options: RequestOptions): Promise<BrowserUseDocumentObservation>;
28
+ /** Optional; required for purchase. One trusted pointer click on the unique bound selector in the exact fenced document,
29
+ * through your existing server browser control. No retry, no general DOM reads, never lift the fence. */
30
+ performFinalClick?: BrowserConnectorHost['performFinalClick'];
25
31
  /** Independently verify extension/page cleanup and retain every payment uncertainty fence. */
26
32
  confirmLocalCleanup(context: Readonly<ManagedCheckoutContext>, browser: Readonly<FencedBrowserDocument>,
27
33
  request: Readonly<{ checkout_id: import('./index.js').CheckoutId }>, options: RequestOptions): Promise<{
@@ -35,8 +41,16 @@ export function createBrowserUseHost(options: {
35
41
  apiKey: string;
36
42
  merchantId: string;
37
43
  extensionId: string;
44
+ /** Primary runtime ID pinned in durable operator installation configuration.
45
+ * Required for run-created or multi-extension mode; stable across host restarts. */
46
+ extensionRuntimeId?: string;
38
47
  extensionRelease: ApprovedExtensionRelease;
39
48
  host: BrowserUsePlatformHost;
49
+ /** Requires a durable owned run reservation and a fence covering all session runs and queued work.
50
+ * The executor must separately enable this merchant. Default false. */
51
+ allowRunCreatedBrowsers?: boolean;
52
+ /** Complete additional extension set approved for this merchant/provider. This is not qualification evidence. */
53
+ additionalExtensions?: ReadonlyArray<Readonly<{ id: string; runtimeId: string; version: string }>>;
40
54
  fetchImpl?: typeof fetch;
41
55
  /** Test clock only. */
42
56
  now?: () => number;
@@ -1,4 +1,5 @@
1
1
  import { AccelerateError } from './index.js';
2
+ import { browserUseRunProvenance } from './browser-provenance.js';
2
3
  import { validateApprovedExtensionRelease, verifyApprovedExtensionRuntime } from './approved-extension.js';
3
4
  import { normalizeBrowserOperation } from './browser-operations.js';
4
5
 
@@ -44,12 +45,23 @@ const freeze = value => {
44
45
  };
45
46
 
46
47
  export function createBrowserUseHost(configuration = {}) {
47
- exact(configuration, ['apiKey', 'merchantId', 'extensionId', 'extensionRelease', 'host'], ['fetchImpl', 'now']);
48
- const { apiKey, merchantId, extensionId, host, fetchImpl = fetch, now = Date.now } = configuration;
48
+ exact(configuration, ['apiKey', 'merchantId', 'extensionId', 'extensionRelease', 'host'], ['fetchImpl', 'now', 'allowRunCreatedBrowsers', 'additionalExtensions', 'extensionRuntimeId']);
49
+ const { apiKey, merchantId, extensionId, host, fetchImpl = fetch, now = Date.now,
50
+ allowRunCreatedBrowsers = false, additionalExtensions = [], extensionRuntimeId } = configuration;
49
51
  check(typeof apiKey === 'string' && apiKey.length >= 16 && apiKey.length <= 500 && !/[\u0000-\u0020\u007f]/u.test(apiKey) &&
50
52
  UUID.test(merchantId) && UUID.test(extensionId) && typeof fetchImpl === 'function' && typeof now === 'function' && host &&
51
53
  ['resolveOwnedBrowser', 'drainAndFence', 'bindCheckout', 'resolveCheckoutBrowser', 'inspectDocument', 'confirmLocalCleanup']
52
54
  .every(key => typeof host[key] === 'function'), 'configuration_invalid');
55
+ check(typeof allowRunCreatedBrowsers === 'boolean' && Array.isArray(additionalExtensions) && additionalExtensions.length <= 2, 'configuration_invalid');
56
+ check(extensionRuntimeId === undefined || RUNTIME.test(extensionRuntimeId), 'configuration_invalid');
57
+ check(!(allowRunCreatedBrowsers || additionalExtensions.length) || RUNTIME.test(extensionRuntimeId ?? ''), 'configuration_invalid');
58
+ const extensionIds = new Set([extensionId]), runtimes = new Set();
59
+ const extras = additionalExtensions.map(value => {
60
+ exact(value, ['id', 'runtimeId', 'version']);
61
+ check(UUID.test(value.id) && RUNTIME.test(value.runtimeId) && typeof value.version === 'string' &&
62
+ /^\d+(?:\.\d+){0,3}$/u.test(value.version) && !extensionIds.has(value.id) && !runtimes.has(value.runtimeId), 'configuration_invalid');
63
+ extensionIds.add(value.id); runtimes.add(value.runtimeId); return Object.freeze({ ...value });
64
+ });
53
65
  let release;
54
66
  try { release = validateApprovedExtensionRelease(configuration.extensionRelease); }
55
67
  catch { throw new AccelerateError('configuration_invalid'); }
@@ -58,21 +70,25 @@ export function createBrowserUseHost(configuration = {}) {
58
70
  try { return await host[method](...args); }
59
71
  catch { throw new AccelerateError('integration_unavailable'); }
60
72
  };
61
- const owned = (context, document) => {
73
+ const owned = (context, document, cleanup = false) => {
62
74
  exact(document, ['owner', 'browser', 'document_id', 'checkout_url'], ['expires_at', 'fence_id']);
63
75
  exact(document.owner, ['customer_id', 'workspace_id', 'conversation_id', 'phone_number']);
64
76
  check(['customer_id', 'workspace_id', 'conversation_id', 'phone_number'].every(key => document.owner[key] === context[key]), 'permission_denied');
65
- exact(document.browser, ['provider', 'browser_id', 'custody_mode']);
77
+ exact(document.browser, ['provider', 'browser_id', 'custody_mode'], ['provenance']);
66
78
  check(document.browser.provider === 'browser_use' && document.browser.custody_mode === 'customer_backend_trusted' && UUID.test(document.browser.browser_id) &&
67
79
  typeof document.document_id === 'string' && /^[A-Za-z0-9_.:-]{1,200}$/u.test(document.document_id), 'permission_denied');
68
80
  try { normalizeBrowserOperation('navigate', { url: document.checkout_url }); }
69
81
  catch { throw new AccelerateError('permission_denied'); }
70
- return { owner: { ...document.owner }, browser: { ...document.browser }, document_id: document.document_id, checkout_url: document.checkout_url };
82
+ let provenance;
83
+ if (Object.hasOwn(document.browser, 'provenance')) {
84
+ check(allowRunCreatedBrowsers || cleanup && typeof document.fence_id === 'string' && document.fence_id.length > 0, 'permission_denied');
85
+ try { provenance = browserUseRunProvenance(document.browser.provenance); }
86
+ catch { throw new AccelerateError('permission_denied'); }
87
+ }
88
+ return { owner: { ...document.owner }, browser: { ...document.browser, ...(provenance ? { provenance } : {}) }, document_id: document.document_id, checkout_url: document.checkout_url };
71
89
  };
72
- const resource = async (context, document, requiredStatus, options = {}) => {
73
- const binding = owned(context, document).browser;
74
- const timeout = AbortSignal.timeout(10000), signal = AbortSignal.any([timeout, ...(options.signal ? [options.signal] : [])]);
75
- const endpoint = `${BASE}/browsers/${binding.browser_id}`;
90
+ const providerRead = async (path, signal) => {
91
+ const endpoint = `${BASE}${path}`;
76
92
  let response;
77
93
  try {
78
94
  check(!signal.aborted);
@@ -85,26 +101,63 @@ export function createBrowserUseHost(configuration = {}) {
85
101
  /^application\/json(?:\s*;|$)/iu.test(response.headers.get('content-type') ?? '') && response.body);
86
102
  const length = response.headers.get('content-length');
87
103
  check(length === null || /^\d+$/u.test(length) && Number(length) <= 262144);
88
- const value = await json(response, signal);
104
+ return await json(response, signal);
105
+ } finally { void response?.body?.cancel().catch(() => {}); }
106
+ };
107
+ const resource = async (context, document, requiredStatus, options = {}) => {
108
+ const binding = owned(context, document, requiredStatus === 'stopped').browser;
109
+ const timeout = AbortSignal.timeout(10000), signal = AbortSignal.any([timeout, ...(options.signal ? [options.signal] : [])]);
110
+ try {
111
+ const value = await providerRead(`/browsers/${binding.browser_id}`, signal);
89
112
  const metadata = value?.metadata;
113
+ let runEvidence;
114
+ if (binding.provenance) {
115
+ const provenance = binding.provenance;
116
+ const run = await providerRead(`/runs/${provenance.run_id}`, signal);
117
+ check(object(run) && run.id === provenance.run_id && run.sessionId === provenance.session_id &&
118
+ run.workspaceId === provenance.provider_workspace_id && value.agentSessionId === provenance.session_id);
119
+ // A breached scheduling fence must block payment, but must not prevent
120
+ // confirmation that this exact original owned browser has stopped.
121
+ if (requiredStatus !== 'stopped') {
122
+ const session = await providerRead(`/sessions/${provenance.session_id}`, signal);
123
+ const queue = await providerRead(`/sessions/${provenance.session_id}/queue`, signal);
124
+ check(['completed', 'cancelled'].includes(run.status) &&
125
+ object(session) && session.sessionId === provenance.session_id && session.latestRunId === provenance.run_id &&
126
+ session.workspaceId === provenance.provider_workspace_id && ['completed', 'cancelled'].includes(session.status) &&
127
+ object(queue) && Array.isArray(queue.queue) && queue.queue.length === 0 && value.agentSessionId === provenance.session_id);
128
+ runEvidence = { agentSessionId: value.agentSessionId,
129
+ agentRun: { id: run.id, sessionId: run.sessionId, workspaceId: run.workspaceId, status: run.status },
130
+ agentSession: { sessionId: session.sessionId, workspaceId: session.workspaceId, latestRunId: session.latestRunId, status: session.status },
131
+ agentQueue: { queue: [] } };
132
+ }
133
+ }
90
134
  check(object(value) && value.id === binding.browser_id && value.status === requiredStatus &&
91
135
  value.recordingAvailable === false && (value.recordingUrl == null || value.recordingUrl === '') &&
92
- object(metadata) && metadata.accelerate_merchant_id === merchantId &&
136
+ (binding.provenance || object(metadata) && metadata.accelerate_merchant_id === merchantId &&
93
137
  metadata.accelerate_owner_user_id === context.customer_id && metadata.accelerate_workspace_id === context.workspace_id &&
94
138
  metadata.accelerate_custody_mode === 'customer_backend_trusted' && metadata.accelerate_agent_extension_id === extensionId &&
95
- metadata.accelerate_extension_sha256 === release.artifact.sha256 && metadata.accelerate_extension_version === release.extension_version &&
139
+ metadata.accelerate_extension_sha256 === release.artifact.sha256 && metadata.accelerate_extension_version === release.extension_version) &&
96
140
  typeof value.timeoutAt === 'string' && Number.isFinite(Date.parse(value.timeoutAt)) &&
97
141
  (requiredStatus === 'stopped' || Date.parse(value.timeoutAt) > now()) &&
98
- Array.isArray(value.extensions) && value.extensions.length === 1 && value.extensions[0]?.id === extensionId &&
99
- value.extensions[0]?.version === release.extension_version && RUNTIME.test(value.extensions[0]?.runtimeId ?? ''));
142
+ Array.isArray(value.extensions) && value.extensions.length === extras.length + 1 &&
143
+ new Set(value.extensions.map(item => item?.id)).size === value.extensions.length &&
144
+ new Set(value.extensions.map(item => item?.runtimeId)).size === value.extensions.length);
145
+ const primary = value.extensions.find(item => item?.id === extensionId);
146
+ check(primary?.version === release.extension_version && RUNTIME.test(primary?.runtimeId ?? '') &&
147
+ extras.every(expected => value.extensions.some(item => ['id', 'version', 'runtimeId'].every(key => item?.[key] === expected[key]))));
148
+ check(extensionRuntimeId === undefined || primary.runtimeId === extensionRuntimeId);
100
149
  // Deliberately project away provider CDP/viewer URLs and unknown response fields.
101
- const read = freeze({ expires_at: value.timeoutAt, runtime_id: value.extensions[0].runtimeId });
150
+ const read = freeze({ expires_at: value.timeoutAt, runtime_id: primary.runtimeId });
102
151
  if (!options.withConnection) return read;
103
152
  check(typeof value.cdpUrl === 'string' && value.cdpUrl.length <= 2048 && typeof value.startedAt === 'string');
104
153
  return freeze({ ...read, connection: freeze({ id: value.id, status: value.status, cdpUrl: value.cdpUrl, startedAt: value.startedAt,
105
- timeoutAt: value.timeoutAt, recordingAvailable: false, metadata, extensions: value.extensions }) });
154
+ timeoutAt: value.timeoutAt, recordingAvailable: false,
155
+ metadata: binding.provenance ? {} : Object.fromEntries(['accelerate_merchant_id', 'accelerate_owner_user_id',
156
+ 'accelerate_workspace_id', 'accelerate_custody_mode', 'accelerate_agent_extension_id',
157
+ 'accelerate_extension_sha256', 'accelerate_extension_version'].map(key => [key, metadata[key]])),
158
+ extensions: value.extensions.map(item => ({ id: item.id, runtimeId: item.runtimeId, version: item.version })),
159
+ ...runEvidence }) });
106
160
  } catch {
107
- void response?.body?.cancel().catch(() => {});
108
161
  throw new AccelerateError(options.signal?.aborted ? 'request_cancelled' : 'integration_unavailable');
109
162
  }
110
163
  };
@@ -121,7 +174,11 @@ export function createBrowserUseHost(configuration = {}) {
121
174
  async inspectBrowser(context, document, options) {
122
175
  const observed = await resource(context, document, 'active', options);
123
176
  const current = await callHost('inspectDocument', context, document, freeze({ runtime_id: observed.runtime_id }), options);
124
- exact(current, ['document_id', 'checkout_url', 'extension', 'automation']);
177
+ exact(current, ['document_id', 'checkout_url', 'extension', 'automation'], ['run_control']);
178
+ if (document.browser.provenance && current.automation?.agent_access_fenced === true) {
179
+ exact(current.run_control, ['checkout_reserved', 'session_scheduling_fenced', 'live_view_blocked']);
180
+ check(Object.values(current.run_control).every(value => value === true));
181
+ }
125
182
  exact(current.extension, ['extension_version', 'success', 'initialized', 'wasmInitialized', 'autosaveDisabled',
126
183
  'boundedRetentionSupported', 'maximumRetentionMs'], ['bridge_protocol']);
127
184
  const extension = { ...current.extension, artifact_sha256: release.artifact.sha256 };
@@ -131,6 +188,8 @@ export function createBrowserUseHost(configuration = {}) {
131
188
  recording_disabled: true, extension,
132
189
  automation: current.automation };
133
190
  },
191
+ // Optional: only a platform host that can click may enable purchases. Readiness was read by prepareFinalClick before the arm.
192
+ ...(typeof host.performFinalClick === 'function' ? { performFinalClick: (...args) => callHost('performFinalClick', ...args) } : {}),
134
193
  drainAndFence: (...args) => callHost('drainAndFence', ...args),
135
194
  bindCheckout: (...args) => callHost('bindCheckout', ...args),
136
195
  resolveCheckoutBrowser: (...args) => callHost('resolveCheckoutBrowser', ...args),
@@ -1,9 +1,19 @@
1
1
  export type CheckoutDiagnosticStage = 'not_started' | 'identity_required' | 'otp_required' | 'selection_required' |
2
- 'preparing_checkout' | 'approval_required' | 'cvv_required' | 'approved' | 'ready_for_submit' | 'review_delivery_required' | 'review_delivered' | 'approved_for_submit' |
2
+ 'preparing_checkout' | 'approval_required' | 'cvv_required' | 'approved' | 'ready_for_submit' | 'review_delivery_required' | 'review_delivered' | 'approved_for_submit' | 'final_click_armed' |
3
3
  'cancelled' | 'expired' | 'failed' | 'submit_attempted' | 'outcome_unknown';
4
4
  export type CheckoutDiagnosticAction = 'get_status' | 'continue_private_chat' | 'prepare_checkout' | 'apply_details' |
5
5
  'request_approval' | 'fill' | 'submit' | 'cancel' | 'reconcile_order';
6
6
  export type CheckoutCleanupStatus = 'not_required' | 'not_started' | 'pending' | 'confirmed';
7
+ export type CheckoutReconciliationState = 'confirmed' | 'declined' | 'not_found';
8
+ /** The client backend's statement of the merchant's result. Not Accelerate's proof: payment_confirmed stays false. */
9
+ export interface CheckoutReconciliation {
10
+ state: CheckoutReconciliationState;
11
+ /** Short label for where the client read the result, such as merchant_order_api. */
12
+ source: string;
13
+ amount_cents: number | null;
14
+ currency: 'USD' | null;
15
+ reconciled_at: string;
16
+ }
7
17
  export interface CheckoutDiagnostics {
8
18
  version: 'v1';
9
19
  stage: CheckoutDiagnosticStage;
@@ -19,5 +29,6 @@ export const checkoutDiagnosticActions: readonly CheckoutDiagnosticAction[];
19
29
  export const checkoutCleanupStatuses: readonly CheckoutCleanupStatus[];
20
30
  export const checkoutDiagnosticsJsonSchema: Readonly<Record<string, unknown>>;
21
31
  export function validateCheckoutDiagnostics(value: unknown, checkout?: {
22
- status: string; purpose?: string; submit_allowed?: boolean; submit_expires_at?: string; approval_policy?: string; purchase_approved?: boolean;
32
+ status: string; purpose?: string; submit_allowed?: boolean; submit_expires_at?: string; approval_policy?: string; purchase_approved?: boolean; reconciliation?: unknown; final_click_actor?: string;
23
33
  }): CheckoutDiagnostics;
34
+ export function validateCheckoutReconciliation(value: unknown, status?: string): CheckoutReconciliation;
@@ -1,6 +1,6 @@
1
1
  // Safe lifecycle metadata only. No transport, credentials, provider text or automatic actions.
2
2
  export const checkoutDiagnosticStages = Object.freeze(['not_started', 'identity_required', 'otp_required', 'selection_required',
3
- 'preparing_checkout', 'approval_required', 'cvv_required', 'approved', 'ready_for_submit', 'review_delivery_required', 'review_delivered', 'approved_for_submit',
3
+ 'preparing_checkout', 'approval_required', 'cvv_required', 'approved', 'ready_for_submit', 'review_delivery_required', 'review_delivered', 'approved_for_submit', 'final_click_armed',
4
4
  'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
5
5
  export const checkoutDiagnosticActions = Object.freeze(['get_status', 'continue_private_chat', 'prepare_checkout', 'apply_details',
6
6
  'request_approval', 'fill', 'submit', 'cancel', 'reconcile_order']);
@@ -38,7 +38,7 @@ export function validateCheckoutDiagnostics(input, checkout) {
38
38
  typeof value.support_reference === 'string' && /^acs_[a-f0-9]{32}$/.test(value.support_reference) &&
39
39
  checkoutCleanupStatuses.includes(value.cleanup_status) && (value.submit_deadline === undefined || timestamp(value.submit_deadline)));
40
40
  if (checkout !== undefined) {
41
- const { status, purpose = 'purchase', submit_allowed, approval_policy, purchase_approved } = checkout;
41
+ const { status, purpose = 'purchase', submit_allowed, approval_policy, purchase_approved, reconciliation, final_click_actor } = checkout;
42
42
  check(terminal.has(status) ? value.stage === status : status === 'wallet_required'
43
43
  ? ['not_started', 'identity_required', 'otp_required', 'selection_required'].includes(value.stage)
44
44
  : ['selection_ready', 'details_ready'].includes(status) ? value.stage === 'preparing_checkout'
@@ -46,7 +46,7 @@ export function validateCheckoutDiagnostics(input, checkout) {
46
46
  check(status === 'ready_for_submit' ? timestamp(value.submit_deadline) && value.submit_deadline === checkout.submit_expires_at : value.submit_deadline === undefined);
47
47
  for (const action of value.next_actions) {
48
48
  if (action === 'submit')
49
- check(status === 'ready_for_submit' && purpose === 'purchase' && submit_allowed !== false && (approval_policy !== 'after_fill' || purchase_approved === true));
49
+ check(status === 'ready_for_submit' && purpose === 'purchase' && submit_allowed !== false && final_click_actor === 'customer' && (approval_policy !== 'after_fill' || purchase_approved === true));
50
50
  if (action === 'fill')
51
51
  check(status === 'approved' && purpose !== 'sandbox_source_comparison');
52
52
  if (['prepare_checkout', 'apply_details'].includes(action))
@@ -56,8 +56,11 @@ export function validateCheckoutDiagnostics(input, checkout) {
56
56
  if (action === 'continue_private_chat')
57
57
  check(['wallet_required', 'approval_required'].includes(status) || approval_policy === 'after_fill' && status === 'ready_for_submit' && !purchase_approved);
58
58
  if (action === 'reconcile_order')
59
- check(['submit_attempted', 'outcome_unknown'].includes(status));
59
+ check(['submit_attempted', 'outcome_unknown'].includes(status) && reconciliation === undefined);
60
60
  }
61
+ // While a customer click is armed only a safe read or cancel is advertised: no browser work, no re-arm.
62
+ if (status === 'final_click_armed')
63
+ check(value.next_actions.every(action => ['get_status', 'cancel'].includes(action)));
61
64
  if (!terminal.has(status))
62
65
  check(value.cleanup_status !== 'pending' && value.cleanup_status !== 'confirmed');
63
66
  if (terminal.has(status))
@@ -65,3 +68,14 @@ export function validateCheckoutDiagnostics(input, checkout) {
65
68
  }
66
69
  return value;
67
70
  }
71
+ /** Validate the client backend's stored statement of the merchant's result. Never proof of payment. */
72
+ export function validateCheckoutReconciliation(input, status) {
73
+ const value = input;
74
+ check(value !== null && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value)) &&
75
+ Object.keys(value).length === 5 && ['confirmed', 'declined', 'not_found'].includes(value.state) &&
76
+ typeof value.source === 'string' && /^[a-z][a-z0-9_.-]{0,39}$/.test(value.source) && !/(?:\d[ -]?){13,19}/.test(value.source) &&
77
+ (value.amount_cents === null || Number.isSafeInteger(value.amount_cents) && value.amount_cents > 0 && value.amount_cents <= 2147483647) &&
78
+ (value.currency === null || value.currency === 'USD') && (value.state !== 'confirmed' || value.amount_cents !== null && value.currency === 'USD') &&
79
+ timestamp(value.reconciled_at) && (status === undefined || ['submit_attempted', 'outcome_unknown'].includes(status)));
80
+ return value;
81
+ }
package/cli.js CHANGED
@@ -18,7 +18,10 @@ It does not qualify the downstream payment source or browser configuration.
18
18
 
19
19
  Init copies the packaged synthetic MCP starter without credentials, network
20
20
  requests or overwrites. --browser existing selects the new provider-independent
21
- existing-browser starter; omission preserves the managed Kernel starter.
21
+ existing-browser starter with purchase purpose and a submit tool; omission
22
+ preserves the managed Kernel starter, which is fill-only. Both start in
23
+ synthetic mode. Purchase requires private approval, an explicit submit and your
24
+ own browser host performing the one final click: Accelerate never clicks.
22
25
  Existing symlink paths and nonempty targets are rejected.
23
26
  `;
24
27
  const starterFiles = ['package.json', '.env.example', '.gitignore', 'README.md', 'server.mjs', 'runtime.mjs',
package/host-adapter.d.ts CHANGED
@@ -70,6 +70,11 @@ export interface DurableCheckoutHost<Extra = unknown> {
70
70
  * without uploading/sending the same review again. */
71
71
  withPrivateDelivery(context: ManagedCheckoutContext, idempotencyKey: string,
72
72
  deliver: () => Promise<{ message_id: string; sent_at: string }>): Promise<Readonly<{ message_id: string; sent_at: string }>>;
73
+ /** Recover only an existing pending delivery under its owner lock. lookup must
74
+ * be a trusted read-only journal function binding exact context, destination,
75
+ * and prompt idempotency key; it must never upload or send. */
76
+ reconcilePrivateDelivery(context: ManagedCheckoutContext, idempotencyKey: string,
77
+ lookup: (context: Readonly<ManagedCheckoutContext>, idempotencyKey: string) => Promise<{ message_id: string; sent_at: string }>): Promise<Readonly<{ message_id: string; sent_at: string }>>;
73
78
  /** Call ONLY after connector independently verifies stopped/cleared and retained fence. */
74
79
  confirmBrowserCleanup(context: ManagedCheckoutContext, checkoutId: CheckoutId): Promise<void>;
75
80
  withPrivateCheckout<T>(context: ManagedCheckoutContext, metadata: PrivateHostOperation,