@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/host-adapter.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { AccelerateError } from './index.js';
4
- import { validateCheckoutDiagnostics } from './checkout-diagnostics.js';
4
+ import { validateCheckoutDiagnostics, validateCheckoutReconciliation } from './checkout-diagnostics.js';
5
5
  import { renderPrivateChatState, privateChatProgress } from './private-chat.js';
6
6
 
7
7
  const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
@@ -9,9 +9,9 @@ const OWNER = /^[A-Za-z0-9_.:-]{1,500}$/;
9
9
  const MESSAGE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$/;
10
10
  const RECOVER = new Set(['accelerate_checkout_status', 'accelerate_checkout_cancel', 'private_state', 'private_cancel']);
11
11
  const TERMINAL = new Set(['cancelled', 'expired', 'failed']);
12
- const STATUS_ORDER = new Map(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit'].map((status, index) => [status, index]));
12
+ const STATUS_ORDER = new Map(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit', 'final_click_armed'].map((status, index) => [status, index]));
13
13
  const STAGES = new Set(['not_started', 'identity_required', 'otp_required', 'selection_required', 'preparing_checkout', 'approval_required', 'cvv_required', 'verified', 'review_delivery_required', 'review_delivered', 'approved_for_submit', 'closed']);
14
- const STATUSES = new Set(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
14
+ const STATUSES = new Set(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved', 'ready_for_submit', 'final_click_armed', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
15
15
  const object = value => value && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
16
16
  const check = (condition, code = 'invalid_input') => { if (!condition) throw new AccelerateError(code); };
17
17
  const clone = value => structuredClone(value);
@@ -50,7 +50,7 @@ function safeText(value, max = 10000) {
50
50
  return value;
51
51
  }
52
52
  function safeCheckout(value) {
53
- exact(value, ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], ['purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'fill_after_verification', 'card', 'address', 'quote', 'submit_expires_at', 'diagnostics'], 'invalid_response');
53
+ exact(value, ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], ['purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'fill_after_verification', 'card', 'address', 'quote', 'submit_expires_at', 'diagnostics', 'reconciliation', 'final_click_actor', 'submit_actor'], 'invalid_response');
54
54
  check(ID.test(value.id) && STATUSES.has(value.status) && Number.isSafeInteger(value.revision) && value.revision >= 0 && value.retry_safe === false && value.payment_confirmed === false && validDate(value.expires_at), 'invalid_response');
55
55
  if (value.card !== undefined) {
56
56
  exact(value.card, ['brand', 'last4'], [], 'invalid_response');
@@ -71,8 +71,12 @@ function safeCheckout(value) {
71
71
  if (value.submit_expires_at !== undefined) check(validDate(value.submit_expires_at), 'invalid_response');
72
72
  if (value.submit_allowed !== undefined) check(typeof value.submit_allowed === 'boolean', 'invalid_response');
73
73
  if (value.fill_after_verification !== undefined) check(value.fill_after_verification === true && value.approval_policy === 'after_fill' && ['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
74
+ if (value.final_click_actor !== undefined) check(value.final_click_actor === 'customer' && value.purpose === 'purchase', 'invalid_response');
75
+ if (value.submit_actor !== undefined) check(value.submit_actor === 'customer' && value.final_click_actor === 'customer' && ['submit_attempted', 'outcome_unknown'].includes(value.status), 'invalid_response');
76
+ if (value.status === 'submit_attempted' && value.purpose === 'purchase') check(value.submit_actor === 'customer', 'invalid_response');
74
77
  if (value.purchase_approved !== undefined) check(typeof value.purchase_approved === 'boolean', 'invalid_response');
75
78
  if (value.approval_policy !== undefined) check(value.approval_policy === 'after_fill' && typeof value.purchase_approved === 'boolean', 'invalid_response');
79
+ if (value.reconciliation !== undefined) { try { validateCheckoutReconciliation(value.reconciliation, value.status); } catch { check(false, 'invalid_response'); } }
76
80
  // The SDK owns detailed diagnostics validation. This projection keeps no URLs,
77
81
  // provider capabilities or data that could accidentally become an effect input.
78
82
  const result = clone(value);
@@ -212,7 +216,7 @@ export function createDurableCheckoutHost({ persistence, resolveContext } = {})
212
216
  }
213
217
  if (metadata.operation === 'accelerate_checkout_submit') {
214
218
  const checkout = active.record.attempt.checkout;
215
- check(checkout?.purpose === 'purchase' && checkout.submit_allowed === true && checkout.status === 'ready_for_submit' && checkout.purchase_approved === true && active.record.attempt.private_stage === 'approved_for_submit' && validDate(checkout.submit_expires_at) && Date.parse(checkout.submit_expires_at) > Date.now() && metadata.revision === checkout.revision, 'submit_unavailable');
219
+ check(checkout?.purpose === 'purchase' && checkout.submit_allowed === true && checkout.final_click_actor === 'customer' && checkout.status === 'ready_for_submit' && checkout.purchase_approved === true && active.record.attempt.private_stage === 'approved_for_submit' && validDate(checkout.submit_expires_at) && Date.parse(checkout.submit_expires_at) > Date.now() && metadata.revision === checkout.revision, 'submit_unavailable');
216
220
  }
217
221
  const oneUse = ['accelerate_checkout_fill', 'accelerate_checkout_submit'].includes(metadata.operation);
218
222
  if (oneUse) {
@@ -296,6 +300,18 @@ export function createDurableCheckoutHost({ persistence, resolveContext } = {})
296
300
  check(MESSAGE.test(receipt.message_id) && validDate(receipt.sent_at) && Date.parse(receipt.sent_at) <= Date.now(), 'invalid_response');
297
301
  entry.receipt = clone(receipt); await commit(active); return Object.freeze(clone(receipt));
298
302
  }),
303
+ reconcilePrivateDelivery: (context, idempotencyKey, lookup) => locked(context, async (active, resolved) => {
304
+ check(/^accelerate-chat:[a-f0-9]{64}$/.test(idempotencyKey) && typeof lookup === 'function');
305
+ const entry = active.record.deliveries?.find(item => item.idempotency_key === idempotencyKey);
306
+ check(entry && active.record.attempt?.checkout_id, 'permission_denied');
307
+ if (entry.receipt) return Object.freeze(clone(entry.receipt));
308
+ // Only a trusted read-only journal lookup may be passed here. It must bind
309
+ // the exact owner, destination and prompt key; it must never send.
310
+ const receipt = await lookup(Object.freeze({ ...resolved }), idempotencyKey);
311
+ exact(receipt, ['message_id', 'sent_at'], [], 'invalid_response');
312
+ check(MESSAGE.test(receipt.message_id) && validDate(receipt.sent_at) && Date.parse(receipt.sent_at) <= Date.now(), 'invalid_response');
313
+ entry.receipt = clone(receipt); await commit(active); return Object.freeze(clone(receipt));
314
+ }),
299
315
  confirmBrowserCleanup: (context, checkoutId) => locked(context, async active => {
300
316
  check(active.record.attempt?.checkout_id === checkoutId && active.record.attempt.checkout?.diagnostics?.cleanup_status === 'confirmed', 'operation_conflict');
301
317
  active.record.attempt.cleanup_confirmed = true; await commit(active);
package/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import type { BrowserOperation, BrowserResult } from './browser-operations.js';
2
- import type { CheckoutDiagnostics } from './checkout-diagnostics.js';
2
+ import type { CheckoutDiagnostics, CheckoutReconciliation, CheckoutReconciliationState } from './checkout-diagnostics.js';
3
3
  export { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
4
- export type { CheckoutDiagnostics, CheckoutDiagnosticStage, CheckoutDiagnosticAction, CheckoutCleanupStatus } from './checkout-diagnostics.js';
4
+ export type { CheckoutDiagnostics, CheckoutDiagnosticStage, CheckoutDiagnosticAction, CheckoutCleanupStatus, CheckoutReconciliation, CheckoutReconciliationState } from './checkout-diagnostics.js';
5
5
  export type { BrowserOperation, BrowserResult, BrowserStep, BrowserCondition } from './browser-operations.js';
6
6
 
7
7
  export type CheckoutId = `mcs_${string}`;
8
8
  export type CheckoutPurpose = 'purchase' | 'sandbox_source_comparison' | 'sandbox_fill_only';
9
9
  export type CheckoutStatus = 'wallet_required' | 'selection_ready' | 'details_ready' | 'approval_required' |
10
- 'approved' | 'ready_for_submit' | 'cancelled' | 'expired' | 'failed' | 'submit_attempted' | 'outcome_unknown';
10
+ 'approved' | 'ready_for_submit' | 'final_click_armed' | 'cancelled' | 'expired' | 'failed' | 'submit_attempted' | 'outcome_unknown';
11
11
  export interface RequestOptions { signal?: AbortSignal }
12
12
  export interface WaitForCheckout {
13
13
  /** Return on these states, or immediately on ready_for_submit or any terminal state. GET requests only. */
@@ -47,16 +47,40 @@ export interface Checkout {
47
47
  /** Explicit v2 policy acknowledged at creation; a later explicit quote arm lets private CVV continue
48
48
  * one fill under the backend lock. Does not authorize final submission. */
49
49
  fill_after_verification?: true;
50
+ /** Purchases only: the customer's own trusted backend/browser performs the physical final click. Accelerate never does. */
51
+ final_click_actor?: 'customer';
52
+ /** Present on submit_attempted (required for a purchase) and optionally on outcome_unknown, which cannot assert that a click happened. Never payment confirmation. */
53
+ submit_actor?: 'customer';
50
54
  card?: { last4: string | null; brand: string | null };
51
55
  address?: { label: string | null; city?: string | null; state?: string | null; postal_code_mask?: string | null };
52
56
  quote?: { checkout_origin: string; amount_cents: number; currency: 'USD' };
53
57
  submit_expires_at?: string;
58
+ /** Present once the client backend recorded the merchant's result. Absent on older services.
59
+ * This is the client's statement, not Accelerate's proof, so payment_confirmed stays false. */
60
+ reconciliation?: CheckoutReconciliation;
54
61
  retry_safe: false;
55
62
  payment_confirmed: false;
56
63
  /** Negotiated safe metadata; absent when connecting to an older service. */
57
64
  diagnostics?: CheckoutDiagnostics;
58
65
  }
59
66
  export interface CreatedCheckout extends Checkout { wallet_url?: string }
67
+ export interface ReconciledCheckout extends Checkout { reconciliation: CheckoutReconciliation }
68
+ /** Server only, after your backend checked the merchant's order system. Never model input.
69
+ * order_reference is stored only as a hash and is never returned. */
70
+ export type CheckoutReconciliationRequest = {
71
+ state: 'confirmed';
72
+ order_reference: string;
73
+ /** Must equal the approved quote exactly. */
74
+ amount_cents: number;
75
+ currency: 'USD';
76
+ source: string;
77
+ } | {
78
+ state: Exclude<CheckoutReconciliationState, 'confirmed'>;
79
+ order_reference?: string;
80
+ amount_cents?: number;
81
+ currency?: 'USD';
82
+ source: string;
83
+ };
60
84
  /** Server-owned existing browser, resolved from the authenticated application's
61
85
  * durable browser record. Never accept this binding from model arguments.
62
86
  * The customer backend retains provider access; this is not private-from-creation custody. */
@@ -64,6 +88,16 @@ export interface BrowserUseBrowserBinding {
64
88
  provider: 'browser_use';
65
89
  browser_id: string;
66
90
  custody_mode: 'customer_backend_trusted';
91
+ /** Opt-in provider run binding from a durable authenticated owner mapping. Never model input. */
92
+ provenance?: BrowserUseRunProvenance;
93
+ }
94
+ export interface BrowserUseRunProvenance {
95
+ type: 'browser_use_run_v1';
96
+ run_id: string;
97
+ session_id: string;
98
+ provider_workspace_id: string | null;
99
+ /** SHA-256 of the host's immutable owner/run reservation, not raw shopper data. */
100
+ binding_sha256: string;
67
101
  }
68
102
  /** Registered server-side executor connector; IDs are resolved from owned backend records. */
69
103
  export interface RegisteredBrowserBinding {
@@ -125,6 +159,9 @@ export interface CreateCheckout {
125
159
  /** Explicit chat/after_fill attachment. Omit to retain Accelerate-created
126
160
  * private Kernel custody. Attachment never navigates or replaces the document. */
127
161
  browser?: CustomerBrowserBinding;
162
+ /** Required for purchase (the default purpose): the customer's backend, not Accelerate, performs the final click.
163
+ * Purchase also requires chat, after_fill and a customer_backend_trusted browser. */
164
+ final_click_actor?: 'customer';
128
165
  /** Your own provider read of `browser`, so Accelerate never needs your provider API key.
129
166
  * Send it on every create. It is held in memory for this checkout and never stored. */
130
167
  browser_resource?: ClientBrowserResource;
@@ -137,8 +174,12 @@ export interface ClientBrowserResource {
137
174
  startedAt: string;
138
175
  timeoutAt: string;
139
176
  recordingAvailable: false;
177
+ agentSessionId?: string;
178
+ agentRun?: { id: string; sessionId: string; workspaceId: string | null; status: 'completed' | 'cancelled' };
179
+ agentSession?: { sessionId: string; workspaceId: string | null; latestRunId: string; status: 'completed' | 'cancelled' };
180
+ agentQueue?: { queue: never[] };
140
181
  metadata: Record<string, string>;
141
- extensions: [{ id: string; version: string; runtimeId: string }];
182
+ extensions: [{ id: string; version: string; runtimeId: string }, ...{ id: string; version: string; runtimeId: string }[]];
142
183
  }
143
184
  /** Hypothesis to inspect against the actual checkout; never a caller assertion of currency evidence. */
144
185
  export interface QuoteVerificationRequest {
@@ -165,6 +206,16 @@ export interface ApprovalRequest {
165
206
  * Successful private verification fills this exact revision without AI. */
166
207
  fill_after_verification?: true;
167
208
  }
209
+ /** BACKEND ONLY. The target and arm_id must never appear in MCP arguments, results, history or outbox. */
210
+ export interface ArmedFinalClick {
211
+ /** Opaque bounded string (20..16000 printable ASCII). Never display or log. */
212
+ arm_id: string;
213
+ /** Short window (service limit 15 seconds) in which the customer host may click once. */
214
+ armed_expires_at: string;
215
+ target: { checkout_url: string; submit_selector: string };
216
+ retry_safe: false;
217
+ payment_confirmed: false;
218
+ }
168
219
  export interface AccelerateClientOptions {
169
220
  baseUrl: string;
170
221
  apiKey: string;
@@ -203,7 +254,14 @@ export class AccelerateClient {
203
254
  verifyQuote(id: CheckoutId, input: QuoteVerificationRequest, options?: RequestOptions): Promise<VerifiedCheckoutQuote>;
204
255
  requestApproval(id: CheckoutId, input: ApprovalRequest, options?: RequestOptions): Promise<Checkout>;
205
256
  fill(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<Checkout>;
206
- submit(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<Checkout>;
257
+ /** Removed behavior: Accelerate never clicks. Always rejects with customer_click_required and sends no request. */
258
+ submit(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<never>;
259
+ /** Backend only: prepare one short-lived customer-owned final click. Accelerate sends no physical input. */
260
+ armFinalClick(id: CheckoutId, input: { revision: number }, options?: RequestOptions): Promise<ArmedFinalClick>;
261
+ /** Backend only: observe the customer's click once. Dispatch claims are not sent; never retry. */
262
+ completeFinalClick(id: CheckoutId, input: { revision: number; arm_id: string }, options?: RequestOptions): Promise<Checkout>;
207
263
  cancel(id: CheckoutId, options?: RequestOptions): Promise<Checkout>;
264
+ /** Records one merchant result after submit. Repeating the same input returns the same result; different input is refused. */
265
+ reconcile(id: CheckoutId, input: CheckoutReconciliationRequest, options?: RequestOptions): Promise<ReconciledCheckout>;
208
266
  };
209
267
  }
package/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { env as processEnv, versions } from 'node:process';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { normalizeBrowserOperation } from './browser-operations.js';
4
- import { validateCheckoutDiagnostics } from './checkout-diagnostics.js';
4
+ import { browserUseRunProvenance } from './browser-provenance.js';
5
+ import { validateCheckoutDiagnostics, validateCheckoutReconciliation } from './checkout-diagnostics.js';
5
6
  export { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
6
7
 
7
8
  const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
8
9
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
10
  const STATUSES = new Set(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved',
10
- 'ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
11
+ 'ready_for_submit', 'final_click_armed', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
11
12
  const WAIT_STOPS = new Set(['ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
12
13
  const HOSTED_SERVICE = 'https://accelerate-agent-sdk.vercel.app';
13
14
  const ERROR_CODES = new Set(['invalid_input', 'unauthorized', 'permission_denied', 'not_found', 'conflict',
@@ -15,12 +16,13 @@ const ERROR_CODES = new Set(['invalid_input', 'unauthorized', 'permission_denied
15
16
  'operation_in_progress', 'fill_continuation_required', 'review_unavailable', 'capability_disabled', 'checkout_unsupported', 'integration_unavailable', 'outcome_unknown',
16
17
  'authentication_required', 'checkout_closed', 'checkout_frozen', 'upstream_unavailable', 'wallet_required',
17
18
  'revision_conflict', 'address_changed', 'details_required', 'quote_changed', 'submit_unavailable',
18
- 'approval_unavailable', 'authentication_completed', 'verification_required', 'verification_failed',
19
+ 'approval_unavailable', 'authentication_completed', 'verification_required', 'verification_failed', 'verification_pending',
19
20
  'verification_consumed', 'handoff_consumed', 'wallet_refreshing', 'consent_required',
20
21
  'idempotency_conflict', 'operation_conflict', 'checkout_not_found', 'tenant_unauthorized',
21
22
  'checkout_busy', 'store_unavailable', 'store_integrity_error', 'selection_rejected', 'upstream_invalid',
22
23
  'upstream_request_invalid', 'upstream_response_invalid', 'configuration_error', 'configuration_invalid',
23
- 'route_not_found', 'method_not_allowed', 'json_required', 'invalid_json', 'request_too_large', 'checkout_event_unconfirmed']);
24
+ 'route_not_found', 'method_not_allowed', 'json_required', 'invalid_json', 'request_too_large', 'checkout_event_unconfirmed',
25
+ 'reconciliation_unavailable', 'reconciliation_mismatch', 'reconciliation_conflict', 'customer_click_required', 'final_click_in_progress']);
24
26
  const MESSAGES = {
25
27
  invalid_input: 'Invalid Accelerate client request.',
26
28
  invalid_response: 'Accelerate returned an unrecognized private response.',
@@ -41,6 +43,8 @@ for (const [codes, message, nextAction] of [
41
43
  'Check the server API key, merchant ID and enabled Accelerate capabilities.', 'check_configuration'],
42
44
  [['invalid_input', 'invalid_json', 'json_required', 'request_too_large', 'method_not_allowed', 'route_not_found', 'upstream_request_invalid'],
43
45
  'Check the checkout request against the SDK contract.', 'check_request'],
46
+ [['verification_pending'],
47
+ 'A private verification for this shopper, card, total and site is still open. Wait for it to expire, up to five minutes, then start a new checkout.', 'stop'],
44
48
  [['operation_in_progress', 'operation_conflict', 'checkout_busy', 'rate_limited', 'wallet_refreshing', 'conflict'],
45
49
  'Read checkout status before continuing. Do not repeat an uncertain payment action.', 'check_status'],
46
50
  [['wallet_required', 'authentication_required', 'authentication_completed', 'selection_rejected'],
@@ -62,6 +66,16 @@ for (const [codes, message, nextAction] of [
62
66
  'Reconcile checkout status and the merchant order. Do not repeat the payment action.', 'reconcile_order'],
63
67
  [['checkout_event_unconfirmed'],
64
68
  'Private chat progress delivery could not be confirmed. Check this existing checkout; do not create a replacement.', 'check_status'],
69
+ [['final_click_in_progress'],
70
+ 'A customer-owned final click is armed and not yet observed. Read the checkout status; do not cancel, re-arm or click again.', 'check_status'],
71
+ [['customer_click_required'],
72
+ 'Accelerate never performs the final payment click. Use the customer-owned browser host: arm, click once in the customer browser, then complete.', 'check_request'],
73
+ [['reconciliation_unavailable'],
74
+ 'This checkout has no submit outcome to reconcile. Nothing was recorded. Read its status.', 'check_status'],
75
+ [['reconciliation_mismatch'],
76
+ 'The order does not match the approved quote. Nothing was recorded. Check the merchant order.', 'reconcile_order'],
77
+ [['reconciliation_conflict'],
78
+ 'A different result is already recorded for this checkout. It cannot be replaced.', 'check_status'],
65
79
  ]) for (const code of codes) GUIDANCE[code] = Object.freeze({ message, nextAction });
66
80
 
67
81
  /** Safe diagnostics only: no response body, URL, credentials, cause or provider stack. */
@@ -115,6 +129,16 @@ function preparationText(value, max = 2000, code = 'invalid_input') {
115
129
  !/\b(?:cvv|cvc|security\s*code|card\s*(?:number|expiry|expiration))\b.{0,30}\d/iu.test(value), code);
116
130
  return value;
117
131
  }
132
+ function reconciliationInput(input) {
133
+ keys(input, ['state', 'order_reference', 'amount_cents', 'currency', 'source'], ['state', 'source']);
134
+ check(['confirmed', 'declined', 'not_found'].includes(input.state) && typeof input.source === 'string' &&
135
+ /^[a-z][a-z0-9_.-]{0,39}$/u.test(input.source) && !/(?:\d[ -]?){13,19}/u.test(input.source));
136
+ if (input.order_reference !== undefined) string(input.order_reference, 200);
137
+ if (input.amount_cents !== undefined) integer(input.amount_cents, 1, 2147483647);
138
+ check(input.currency === undefined || input.currency === 'USD');
139
+ check(input.state !== 'confirmed' || [input.order_reference, input.amount_cents, input.currency].every(value => value !== undefined));
140
+ return { ...input };
141
+ }
118
142
  function browserInput(input) {
119
143
  keys(input, ['operation', 'arguments']);
120
144
  try { return { operation: input.operation, arguments: normalizeBrowserOperation(input.operation, input.arguments) }; }
@@ -144,7 +168,7 @@ function safeText(value, max = 1000, multiline = false) {
144
168
  }
145
169
  function statusResponse(value, expectedId, { create, origin, loopback }) {
146
170
  keys(value, ['id', 'status', 'revision', 'expires_at', 'card', 'address', 'quote', 'submit_expires_at', 'purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'diagnostics',
147
- 'fill_after_verification', 'retry_safe', 'payment_confirmed', ...(create ? ['wallet_url'] : [])],
171
+ 'fill_after_verification', 'reconciliation', 'final_click_actor', 'submit_actor', 'retry_safe', 'payment_confirmed', ...(create ? ['wallet_url'] : [])],
148
172
  ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], 'invalid_response');
149
173
  check(typeof value.id === 'string' && ID.test(value.id) && (!expectedId || value.id === expectedId) && STATUSES.has(value.status) &&
150
174
  value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
@@ -152,10 +176,19 @@ function statusResponse(value, expectedId, { create, origin, loopback }) {
152
176
  if (value.purpose !== undefined || value.submit_allowed !== undefined) check(
153
177
  ['purchase', 'sandbox_source_comparison', 'sandbox_fill_only'].includes(value.purpose) &&
154
178
  value.submit_allowed === (value.purpose === 'purchase'), 'invalid_response');
155
- if (value.approval_policy !== undefined || value.purchase_approved !== undefined) check(value.approval_policy === 'after_fill' && typeof value.purchase_approved === 'boolean' && (!value.purchase_approved || value.status === 'ready_for_submit' && value.purpose === 'purchase'), 'invalid_response');
179
+ if (value.approval_policy !== undefined || value.purchase_approved !== undefined) check(value.approval_policy === 'after_fill' && typeof value.purchase_approved === 'boolean' && (!value.purchase_approved || ['ready_for_submit', 'final_click_armed'].includes(value.status) && value.purpose === 'purchase'), 'invalid_response');
156
180
  if (value.fill_after_verification !== undefined) check(value.fill_after_verification === true && value.approval_policy === 'after_fill' &&
157
181
  ['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
158
182
  if (value.submit_expires_at !== undefined) timestamp(value.submit_expires_at);
183
+ // Only the customer's own backend/browser may ever perform the final click.
184
+ if (value.final_click_actor !== undefined) check(value.final_click_actor === 'customer' && value.purpose === 'purchase', 'invalid_response');
185
+ if (value.submit_actor !== undefined) check(value.submit_actor === 'customer' && value.final_click_actor === 'customer' &&
186
+ ['submit_attempted', 'outcome_unknown'].includes(value.status), 'invalid_response');
187
+ // An attested attempt names the customer; an unknown outcome cannot assert that a click happened.
188
+ if (value.status === 'submit_attempted' && value.purpose === 'purchase') check(value.submit_actor === 'customer', 'invalid_response');
189
+ if (value.reconciliation !== undefined) {
190
+ try { validateCheckoutReconciliation(value.reconciliation, value.status); } catch { fail('invalid_response'); }
191
+ }
159
192
  if (value.diagnostics !== undefined) {
160
193
  try { validateCheckoutDiagnostics(value.diagnostics, value); } catch { fail('invalid_response'); }
161
194
  }
@@ -210,7 +243,10 @@ function creationInspectionInput(input) {
210
243
  for (const name of ['customer_id', 'workspace_id', 'conversation_id'])
211
244
  check(typeof input[name] === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input[name]));
212
245
  string(input.idempotency_key, 200);
213
- keys(input.browser, ['provider', 'browser_id', 'custody_mode']);
246
+ keys(input.browser, ['provider', 'browser_id', 'custody_mode', 'provenance'], ['provider', 'browser_id', 'custody_mode']);
247
+ if (Object.hasOwn(input.browser, 'provenance')) {
248
+ try { browserUseRunProvenance(input.browser.provenance); } catch { check(false); }
249
+ }
214
250
  check(input.browser.provider === 'browser_use' && typeof input.browser.browser_id === 'string' &&
215
251
  UUID.test(input.browser.browser_id) && input.browser.browser_id === input.browser.browser_id.toLowerCase() &&
216
252
  input.browser.custody_mode === 'customer_backend_trusted');
@@ -246,6 +282,16 @@ function retirementInspectionResponse(value, id, input) {
246
282
  value.retirement.recording_disabled === true, 'invalid_response');
247
283
  return value;
248
284
  }
285
+ const ARM_ID = /^[\x21-\x7e]{20,16000}$/u;
286
+ function armId(value, code = 'invalid_input') { check(typeof value === 'string' && ARM_ID.test(value), code); return value; }
287
+ function finalClickArmResponse(value, input) {
288
+ keys(value, ['arm_id', 'armed_expires_at', 'target', 'retry_safe', 'payment_confirmed'], undefined, 'invalid_response');
289
+ armId(value.arm_id, 'invalid_response'); timestamp(value.armed_expires_at);
290
+ check(value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
291
+ keys(value.target, ['checkout_url', 'submit_selector'], undefined, 'invalid_response');
292
+ try { merchantUrl(value.target.checkout_url); string(value.target.submit_selector, 200); } catch { fail('invalid_response'); }
293
+ return value;
294
+ }
249
295
  function connectionResponse(value, merchantId) {
250
296
  keys(value, ['status', 'api_version', 'merchant_id', 'interaction_modes', 'currencies'], undefined, 'invalid_response');
251
297
  check(value.status === 'connected' && value.api_version === 'v1' && value.merchant_id === merchantId &&
@@ -340,8 +386,24 @@ export class AccelerateClient {
340
386
  return result;
341
387
  },
342
388
  fill: async (id, input, options) => this.#request('POST', checkoutId(id), '/fill', revisionInput(input), options),
343
- submit: async (id, input, options) => this.#request('POST', checkoutId(id), '/submit', revisionInput(input), options),
389
+ // The old executor-click endpoint is permanently closed: Accelerate never sends the final click.
390
+ submit: async () => fail('customer_click_required'),
391
+ /** Backend only. The returned target is never model/MCP output. */
392
+ armFinalClick: async (id, input, options) => this.#request('POST', checkoutId(id), '/final-click/arm', revisionInput(input), options),
393
+ completeFinalClick: async (id, input, options) => {
394
+ keys(input, ['revision', 'arm_id']); integer(input.revision, 1, 2147483647); armId(input.arm_id);
395
+ const result = await this.#request('POST', checkoutId(id), '/final-click/complete', { revision: input.revision, arm_id: input.arm_id }, options);
396
+ check(['submit_attempted', 'outcome_unknown'].includes(result.status) && result.purpose === 'purchase' &&
397
+ result.final_click_actor === 'customer' && (result.status === 'outcome_unknown' || result.submit_actor === 'customer'), 'invalid_response');
398
+ return result;
399
+ },
344
400
  cancel: async (id, options) => this.#request('POST', checkoutId(id), '/cancel', {}, options),
401
+ reconcile: async (id, input, options) => {
402
+ const body = reconciliationInput(input);
403
+ const result = await this.#request('POST', checkoutId(id), '/reconcile', body, options);
404
+ check(result.reconciliation?.state === body.state && result.reconciliation.source === body.source, 'invalid_response');
405
+ return result;
406
+ },
345
407
  });
346
408
  }
347
409
  async #waitFor(id, input, options = {}) {
@@ -373,7 +435,7 @@ export class AccelerateClient {
373
435
  } finally { clearTimeout(timer); }
374
436
  }
375
437
  #create(input, options) {
376
- keys(input, ['customer_id', 'workspace_id', 'phone_number', 'start_url', 'idempotency_key', 'interaction_mode', 'conversation_id', 'purpose', 'approval_policy', 'browser', 'browser_resource', 'fill_after_verification'],
438
+ keys(input, ['customer_id', 'workspace_id', 'phone_number', 'start_url', 'idempotency_key', 'interaction_mode', 'conversation_id', 'purpose', 'approval_policy', 'browser', 'browser_resource', 'fill_after_verification', 'final_click_actor'],
377
439
  ['customer_id', 'workspace_id', 'phone_number', 'start_url', 'idempotency_key']);
378
440
  check(typeof input.customer_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.customer_id));
379
441
  check(typeof input.workspace_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.workspace_id));
@@ -386,9 +448,17 @@ export class AccelerateClient {
386
448
  check(input.approval_policy !== 'after_fill' || input.interaction_mode === 'chat' && input.purpose !== 'sandbox_source_comparison');
387
449
  check(input.fill_after_verification === undefined || input.fill_after_verification === true && input.interaction_mode === 'chat' && input.approval_policy === 'after_fill' && ['purchase', 'sandbox_fill_only'].includes(input.purpose ?? 'purchase'));
388
450
  check(input.purpose !== 'sandbox_fill_only' || input.interaction_mode === 'chat');
451
+ // Purchases are never clicked by Accelerate. They require the customer's own trusted browser and final-click actor.
452
+ check(input.final_click_actor === undefined || input.final_click_actor === 'customer' && (input.purpose ?? 'purchase') === 'purchase');
453
+ if ((input.purpose ?? 'purchase') === 'purchase') check(input.final_click_actor === 'customer' && input.interaction_mode === 'chat' &&
454
+ input.approval_policy === 'after_fill' && input.browser?.custody_mode === 'customer_backend_trusted');
389
455
  check(input.interaction_mode === 'chat' ? typeof input.conversation_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.conversation_id) : input.conversation_id === undefined);
390
456
  if (input.browser !== undefined) {
391
- keys(input.browser, ['provider', 'browser_id', 'custody_mode', ...(input.browser?.provider === 'connector' ? ['connector_id'] : [])]);
457
+ keys(input.browser, ['provider', 'browser_id', 'custody_mode', ...(input.browser?.provider === 'connector' ? ['connector_id'] : ['provenance'])],
458
+ ['provider', 'browser_id', 'custody_mode', ...(input.browser?.provider === 'connector' ? ['connector_id'] : [])]);
459
+ if (Object.hasOwn(input.browser, 'provenance')) {
460
+ try { browserUseRunProvenance(input.browser.provenance); } catch { check(false); }
461
+ }
392
462
  const supportedBinding = input.browser.provider === 'browser_use'
393
463
  ? typeof input.browser.browser_id === 'string' && UUID.test(input.browser.browser_id) && input.browser.browser_id === input.browser.browser_id.toLowerCase()
394
464
  : input.browser.provider === 'connector' && typeof input.browser.connector_id === 'string' && /^[a-z][a-z0-9_-]{0,63}$/u.test(input.browser.connector_id) &&
@@ -401,7 +471,8 @@ export class AccelerateClient {
401
471
  check(input.purpose !== 'sandbox_fill_only' || result.purpose === 'sandbox_fill_only' && result.submit_allowed === false, 'invalid_response');
402
472
  check(input.approval_policy !== 'after_fill' || result.approval_policy === 'after_fill' && result.purchase_approved === false, 'invalid_response');
403
473
  check(input.interaction_mode !== 'chat' || result.wallet_url === undefined, 'invalid_response');
404
- check(input.fill_after_verification !== true || result.fill_after_verification === true, 'invalid_response'); return result;
474
+ check(input.fill_after_verification !== true || result.fill_after_verification === true, 'invalid_response');
475
+ check(input.final_click_actor !== 'customer' || result.final_click_actor === 'customer' && result.purpose === 'purchase', 'invalid_response'); return result;
405
476
  });
406
477
  }
407
478
  async #request(method, id, suffix, body, options = {}, connection = false) {
@@ -409,7 +480,7 @@ export class AccelerateClient {
409
480
  check(options.signal === undefined || options.signal instanceof AbortSignal);
410
481
  const controller = new AbortController();
411
482
  const milliseconds = connection ? 45000 : ['/inspect-creation', '/inspect-retirement'].includes(suffix) ? 60000 : id === null ? 245000 : ({ '/fill': 235000, '/details': 125000, '/verify-quote': 125000, '/approval': 125000,
412
- '/submit': 125000, '/browser': 85000, '/cancel': 45000, '': 45000 })[suffix];
483
+ '/final-click/arm': 20000, '/final-click/complete': 125000, '/browser': 85000, '/cancel': 45000, '/reconcile': 45000, '': 45000 })[suffix];
413
484
  const timer = setTimeout(() => controller.abort(), milliseconds); timer.unref?.();
414
485
  const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
415
486
  const url = connection ? `${this.#origin}/v1/connection` :
@@ -429,7 +500,7 @@ export class AccelerateClient {
429
500
  if (!response.ok) throw new AccelerateError(ERROR_CODES.has(value?.error?.code) ? value.error.code : 'request_failed', response.status,
430
501
  response.headers.get('x-accelerate-support-reference'));
431
502
  return connection ? connectionResponse(value, this.#merchantId) : suffix === '/browser' ? browserResponse(value) :
432
- suffix === '/verify-quote' ? verifiedQuoteResponse(value, id, body) : suffix === '/inspect-creation' ? creationInspectionResponse(value, body) : suffix === '/inspect-retirement' ? retirementInspectionResponse(value, id, body) : statusResponse(value, id, {
503
+ suffix === '/verify-quote' ? verifiedQuoteResponse(value, id, body) : suffix === '/final-click/arm' ? finalClickArmResponse(value, body) : suffix === '/inspect-creation' ? creationInspectionResponse(value, body) : suffix === '/inspect-retirement' ? retirementInspectionResponse(value, id, body) : statusResponse(value, id, {
433
504
  create: id === null, origin: this.#origin, loopback: this.#loopback });
434
505
  } catch (error) {
435
506
  Promise.resolve(response?.body?.cancel()).catch(() => {});
package/managed-mcp.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AccelerateClient, AccelerateNextAction, Checkout, CheckoutId, CustomerBrowserBinding } from './index.js';
1
+ import type { AccelerateClient, AccelerateNextAction, Checkout, CheckoutId, ClientBrowserResource, CustomerBrowserBinding } from './index.js';
2
2
  import type { BrowserResult } from './browser-operations.js';
3
3
  import type { BrowserConnector } from './browser-connector.js';
4
4
 
@@ -17,6 +17,8 @@ export interface ManagedCheckoutCreation {
17
17
  /** Optional existing browser from the host's owned workflow, never MCP input.
18
18
  * Only permitted with browserMode:'customer_backend_trusted'. */
19
19
  browser?: CustomerBrowserBinding;
20
+ /** Your own provider read of `browser`, so Accelerate never needs your provider API key. */
21
+ browser_resource?: ClientBrowserResource;
20
22
  /** Resolve only after durable storage. Retain an uncertainty fence if acknowledgement is lost. */
21
23
  persistCreated(binding: Readonly<ManagedCheckoutBinding>): Promise<void>;
22
24
  }
@@ -108,9 +110,13 @@ export interface ManagedCheckoutToolsOptions<Extra = unknown> {
108
110
  * checkout ID/revision; the existing owner record and uncertainty fence remain.
109
111
  */
110
112
  dispatchCheckoutEvent(context: Readonly<ManagedCheckoutContext>, event: Readonly<ManagedCheckoutEvent>): Promise<void>;
111
- /** Immutable for newly created checkouts. Default sandbox_fill_only. */
113
+ /** Immutable for newly created checkouts. Default sandbox_fill_only. Purchase requires browserMode
114
+ * customer_backend_trusted and a browserConnector whose host implements performFinalClick: Accelerate
115
+ * never sends the final click, and Accelerate-managed private browsers cannot purchase. */
112
116
  purpose?: 'sandbox_fill_only' | 'purchase';
113
- /** Default false. Registers final-submit tool only when true and purpose is purchase. */
117
+ /** Default false. Registers the final-submit tool only when true and purpose is purchase. That tool
118
+ * arms one short click, calls browserConnector.performFinalClick once in the customer's browser, then
119
+ * completes once to observe. Its result is a safe Checkout; the arm token and target never leave the backend. */
114
120
  enableSubmit?: boolean;
115
121
  /** Default managed_review_v1: verification at payment and approval after fill.
116
122
  * Legacy before-fill behavior requires this explicit profile or approvalPolicy:'before_fill'. */