@weaccelerateinc/agent-sdk 0.10.3 → 0.11.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.
package/index.js CHANGED
@@ -1,13 +1,13 @@
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 { validateCheckoutDiagnostics, validateCheckoutReconciliation } from './checkout-diagnostics.js';
5
5
  export { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
6
6
 
7
7
  const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
8
8
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
9
  const STATUSES = new Set(['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved',
10
- 'ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
10
+ 'ready_for_submit', 'final_click_armed', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
11
11
  const WAIT_STOPS = new Set(['ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown']);
12
12
  const HOSTED_SERVICE = 'https://accelerate-agent-sdk.vercel.app';
13
13
  const ERROR_CODES = new Set(['invalid_input', 'unauthorized', 'permission_denied', 'not_found', 'conflict',
@@ -15,12 +15,13 @@ const ERROR_CODES = new Set(['invalid_input', 'unauthorized', 'permission_denied
15
15
  'operation_in_progress', 'fill_continuation_required', 'review_unavailable', 'capability_disabled', 'checkout_unsupported', 'integration_unavailable', 'outcome_unknown',
16
16
  'authentication_required', 'checkout_closed', 'checkout_frozen', 'upstream_unavailable', 'wallet_required',
17
17
  'revision_conflict', 'address_changed', 'details_required', 'quote_changed', 'submit_unavailable',
18
- 'approval_unavailable', 'authentication_completed', 'verification_required', 'verification_failed',
18
+ 'approval_unavailable', 'authentication_completed', 'verification_required', 'verification_failed', 'verification_pending',
19
19
  'verification_consumed', 'handoff_consumed', 'wallet_refreshing', 'consent_required',
20
20
  'idempotency_conflict', 'operation_conflict', 'checkout_not_found', 'tenant_unauthorized',
21
21
  'checkout_busy', 'store_unavailable', 'store_integrity_error', 'selection_rejected', 'upstream_invalid',
22
22
  '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']);
23
+ 'route_not_found', 'method_not_allowed', 'json_required', 'invalid_json', 'request_too_large', 'checkout_event_unconfirmed',
24
+ 'reconciliation_unavailable', 'reconciliation_mismatch', 'reconciliation_conflict', 'customer_click_required', 'final_click_in_progress']);
24
25
  const MESSAGES = {
25
26
  invalid_input: 'Invalid Accelerate client request.',
26
27
  invalid_response: 'Accelerate returned an unrecognized private response.',
@@ -41,6 +42,8 @@ for (const [codes, message, nextAction] of [
41
42
  'Check the server API key, merchant ID and enabled Accelerate capabilities.', 'check_configuration'],
42
43
  [['invalid_input', 'invalid_json', 'json_required', 'request_too_large', 'method_not_allowed', 'route_not_found', 'upstream_request_invalid'],
43
44
  'Check the checkout request against the SDK contract.', 'check_request'],
45
+ [['verification_pending'],
46
+ '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
47
  [['operation_in_progress', 'operation_conflict', 'checkout_busy', 'rate_limited', 'wallet_refreshing', 'conflict'],
45
48
  'Read checkout status before continuing. Do not repeat an uncertain payment action.', 'check_status'],
46
49
  [['wallet_required', 'authentication_required', 'authentication_completed', 'selection_rejected'],
@@ -62,6 +65,16 @@ for (const [codes, message, nextAction] of [
62
65
  'Reconcile checkout status and the merchant order. Do not repeat the payment action.', 'reconcile_order'],
63
66
  [['checkout_event_unconfirmed'],
64
67
  'Private chat progress delivery could not be confirmed. Check this existing checkout; do not create a replacement.', 'check_status'],
68
+ [['final_click_in_progress'],
69
+ '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'],
70
+ [['customer_click_required'],
71
+ 'Accelerate never performs the final payment click. Use the customer-owned browser host: arm, click once in the customer browser, then complete.', 'check_request'],
72
+ [['reconciliation_unavailable'],
73
+ 'This checkout has no submit outcome to reconcile. Nothing was recorded. Read its status.', 'check_status'],
74
+ [['reconciliation_mismatch'],
75
+ 'The order does not match the approved quote. Nothing was recorded. Check the merchant order.', 'reconcile_order'],
76
+ [['reconciliation_conflict'],
77
+ 'A different result is already recorded for this checkout. It cannot be replaced.', 'check_status'],
65
78
  ]) for (const code of codes) GUIDANCE[code] = Object.freeze({ message, nextAction });
66
79
 
67
80
  /** Safe diagnostics only: no response body, URL, credentials, cause or provider stack. */
@@ -115,6 +128,16 @@ function preparationText(value, max = 2000, code = 'invalid_input') {
115
128
  !/\b(?:cvv|cvc|security\s*code|card\s*(?:number|expiry|expiration))\b.{0,30}\d/iu.test(value), code);
116
129
  return value;
117
130
  }
131
+ function reconciliationInput(input) {
132
+ keys(input, ['state', 'order_reference', 'amount_cents', 'currency', 'source'], ['state', 'source']);
133
+ check(['confirmed', 'declined', 'not_found'].includes(input.state) && typeof input.source === 'string' &&
134
+ /^[a-z][a-z0-9_.-]{0,39}$/u.test(input.source) && !/(?:\d[ -]?){13,19}/u.test(input.source));
135
+ if (input.order_reference !== undefined) string(input.order_reference, 200);
136
+ if (input.amount_cents !== undefined) integer(input.amount_cents, 1, 2147483647);
137
+ check(input.currency === undefined || input.currency === 'USD');
138
+ check(input.state !== 'confirmed' || [input.order_reference, input.amount_cents, input.currency].every(value => value !== undefined));
139
+ return { ...input };
140
+ }
118
141
  function browserInput(input) {
119
142
  keys(input, ['operation', 'arguments']);
120
143
  try { return { operation: input.operation, arguments: normalizeBrowserOperation(input.operation, input.arguments) }; }
@@ -144,7 +167,7 @@ function safeText(value, max = 1000, multiline = false) {
144
167
  }
145
168
  function statusResponse(value, expectedId, { create, origin, loopback }) {
146
169
  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'] : [])],
170
+ 'fill_after_verification', 'reconciliation', 'final_click_actor', 'submit_actor', 'retry_safe', 'payment_confirmed', ...(create ? ['wallet_url'] : [])],
148
171
  ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'], 'invalid_response');
149
172
  check(typeof value.id === 'string' && ID.test(value.id) && (!expectedId || value.id === expectedId) && STATUSES.has(value.status) &&
150
173
  value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
@@ -152,10 +175,19 @@ function statusResponse(value, expectedId, { create, origin, loopback }) {
152
175
  if (value.purpose !== undefined || value.submit_allowed !== undefined) check(
153
176
  ['purchase', 'sandbox_source_comparison', 'sandbox_fill_only'].includes(value.purpose) &&
154
177
  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');
178
+ 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
179
  if (value.fill_after_verification !== undefined) check(value.fill_after_verification === true && value.approval_policy === 'after_fill' &&
157
180
  ['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
158
181
  if (value.submit_expires_at !== undefined) timestamp(value.submit_expires_at);
182
+ // Only the customer's own backend/browser may ever perform the final click.
183
+ if (value.final_click_actor !== undefined) check(value.final_click_actor === 'customer' && value.purpose === 'purchase', 'invalid_response');
184
+ if (value.submit_actor !== undefined) check(value.submit_actor === 'customer' && value.final_click_actor === 'customer' &&
185
+ ['submit_attempted', 'outcome_unknown'].includes(value.status), 'invalid_response');
186
+ // An attested attempt names the customer; an unknown outcome cannot assert that a click happened.
187
+ if (value.status === 'submit_attempted' && value.purpose === 'purchase') check(value.submit_actor === 'customer', 'invalid_response');
188
+ if (value.reconciliation !== undefined) {
189
+ try { validateCheckoutReconciliation(value.reconciliation, value.status); } catch { fail('invalid_response'); }
190
+ }
159
191
  if (value.diagnostics !== undefined) {
160
192
  try { validateCheckoutDiagnostics(value.diagnostics, value); } catch { fail('invalid_response'); }
161
193
  }
@@ -246,6 +278,16 @@ function retirementInspectionResponse(value, id, input) {
246
278
  value.retirement.recording_disabled === true, 'invalid_response');
247
279
  return value;
248
280
  }
281
+ const ARM_ID = /^[\x21-\x7e]{20,16000}$/u;
282
+ function armId(value, code = 'invalid_input') { check(typeof value === 'string' && ARM_ID.test(value), code); return value; }
283
+ function finalClickArmResponse(value, input) {
284
+ keys(value, ['arm_id', 'armed_expires_at', 'target', 'retry_safe', 'payment_confirmed'], undefined, 'invalid_response');
285
+ armId(value.arm_id, 'invalid_response'); timestamp(value.armed_expires_at);
286
+ check(value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
287
+ keys(value.target, ['checkout_url', 'submit_selector'], undefined, 'invalid_response');
288
+ try { merchantUrl(value.target.checkout_url); string(value.target.submit_selector, 200); } catch { fail('invalid_response'); }
289
+ return value;
290
+ }
249
291
  function connectionResponse(value, merchantId) {
250
292
  keys(value, ['status', 'api_version', 'merchant_id', 'interaction_modes', 'currencies'], undefined, 'invalid_response');
251
293
  check(value.status === 'connected' && value.api_version === 'v1' && value.merchant_id === merchantId &&
@@ -340,8 +382,24 @@ export class AccelerateClient {
340
382
  return result;
341
383
  },
342
384
  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),
385
+ // The old executor-click endpoint is permanently closed: Accelerate never sends the final click.
386
+ submit: async () => fail('customer_click_required'),
387
+ /** Backend only. The returned target is never model/MCP output. */
388
+ armFinalClick: async (id, input, options) => this.#request('POST', checkoutId(id), '/final-click/arm', revisionInput(input), options),
389
+ completeFinalClick: async (id, input, options) => {
390
+ keys(input, ['revision', 'arm_id']); integer(input.revision, 1, 2147483647); armId(input.arm_id);
391
+ const result = await this.#request('POST', checkoutId(id), '/final-click/complete', { revision: input.revision, arm_id: input.arm_id }, options);
392
+ check(['submit_attempted', 'outcome_unknown'].includes(result.status) && result.purpose === 'purchase' &&
393
+ result.final_click_actor === 'customer' && (result.status === 'outcome_unknown' || result.submit_actor === 'customer'), 'invalid_response');
394
+ return result;
395
+ },
344
396
  cancel: async (id, options) => this.#request('POST', checkoutId(id), '/cancel', {}, options),
397
+ reconcile: async (id, input, options) => {
398
+ const body = reconciliationInput(input);
399
+ const result = await this.#request('POST', checkoutId(id), '/reconcile', body, options);
400
+ check(result.reconciliation?.state === body.state && result.reconciliation.source === body.source, 'invalid_response');
401
+ return result;
402
+ },
345
403
  });
346
404
  }
347
405
  async #waitFor(id, input, options = {}) {
@@ -373,7 +431,7 @@ export class AccelerateClient {
373
431
  } finally { clearTimeout(timer); }
374
432
  }
375
433
  #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'],
434
+ 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
435
  ['customer_id', 'workspace_id', 'phone_number', 'start_url', 'idempotency_key']);
378
436
  check(typeof input.customer_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.customer_id));
379
437
  check(typeof input.workspace_id === 'string' && /^[A-Za-z0-9_.:-]{1,500}$/u.test(input.workspace_id));
@@ -386,6 +444,10 @@ export class AccelerateClient {
386
444
  check(input.approval_policy !== 'after_fill' || input.interaction_mode === 'chat' && input.purpose !== 'sandbox_source_comparison');
387
445
  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
446
  check(input.purpose !== 'sandbox_fill_only' || input.interaction_mode === 'chat');
447
+ // Purchases are never clicked by Accelerate. They require the customer's own trusted browser and final-click actor.
448
+ check(input.final_click_actor === undefined || input.final_click_actor === 'customer' && (input.purpose ?? 'purchase') === 'purchase');
449
+ if ((input.purpose ?? 'purchase') === 'purchase') check(input.final_click_actor === 'customer' && input.interaction_mode === 'chat' &&
450
+ input.approval_policy === 'after_fill' && input.browser?.custody_mode === 'customer_backend_trusted');
389
451
  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
452
  if (input.browser !== undefined) {
391
453
  keys(input.browser, ['provider', 'browser_id', 'custody_mode', ...(input.browser?.provider === 'connector' ? ['connector_id'] : [])]);
@@ -401,7 +463,8 @@ export class AccelerateClient {
401
463
  check(input.purpose !== 'sandbox_fill_only' || result.purpose === 'sandbox_fill_only' && result.submit_allowed === false, 'invalid_response');
402
464
  check(input.approval_policy !== 'after_fill' || result.approval_policy === 'after_fill' && result.purchase_approved === false, 'invalid_response');
403
465
  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;
466
+ check(input.fill_after_verification !== true || result.fill_after_verification === true, 'invalid_response');
467
+ check(input.final_click_actor !== 'customer' || result.final_click_actor === 'customer' && result.purpose === 'purchase', 'invalid_response'); return result;
405
468
  });
406
469
  }
407
470
  async #request(method, id, suffix, body, options = {}, connection = false) {
@@ -409,7 +472,7 @@ export class AccelerateClient {
409
472
  check(options.signal === undefined || options.signal instanceof AbortSignal);
410
473
  const controller = new AbortController();
411
474
  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];
475
+ '/final-click/arm': 20000, '/final-click/complete': 125000, '/browser': 85000, '/cancel': 45000, '/reconcile': 45000, '': 45000 })[suffix];
413
476
  const timer = setTimeout(() => controller.abort(), milliseconds); timer.unref?.();
414
477
  const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
415
478
  const url = connection ? `${this.#origin}/v1/connection` :
@@ -429,7 +492,7 @@ export class AccelerateClient {
429
492
  if (!response.ok) throw new AccelerateError(ERROR_CODES.has(value?.error?.code) ? value.error.code : 'request_failed', response.status,
430
493
  response.headers.get('x-accelerate-support-reference'));
431
494
  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, {
495
+ 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
496
  create: id === null, origin: this.#origin, loopback: this.#loopback });
434
497
  } catch (error) {
435
498
  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'. */
package/managed-mcp.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AccelerateClient, AccelerateError } from './index.js';
2
2
  import { browserOperationJsonSchema, parseBrowserOperation, normalizeBrowserOperation } from './browser-operations.js';
3
- import { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics } from './checkout-diagnostics.js';
3
+ import { checkoutDiagnosticsJsonSchema, validateCheckoutDiagnostics, validateCheckoutReconciliation } from './checkout-diagnostics.js';
4
4
  import { createHash } from 'node:crypto';
5
5
 
6
6
  const ID = /^mcs_[A-Za-z0-9_-]{43}$/;
@@ -8,9 +8,12 @@ const OWNER = /^[A-Za-z0-9_.:-]{1,500}$/;
8
8
  const PHONE = /^\+[1-9]\d{6,14}$/;
9
9
  const CONTEXT_KEYS = ['customer_id', 'workspace_id', 'conversation_id', 'phone_number'];
10
10
  const STATUSES = ['wallet_required', 'selection_ready', 'details_ready', 'approval_required', 'approved',
11
- 'ready_for_submit', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown'];
11
+ 'ready_for_submit', 'final_click_armed', 'cancelled', 'expired', 'failed', 'submit_attempted', 'outcome_unknown'];
12
12
  const NEXT_ACTIONS = ['check_configuration', 'check_request', 'check_status', 'complete_wallet',
13
13
  'complete_verification', 'refresh_quote', 'refresh_revision', 'reconcile_order', 'stop'];
14
+ // Errors that prove the final-click arm was not created. Everything else after an arm request is an unknown outcome.
15
+ const PRE_ARM_REFUSALS = new Set(['approval_required', 'revision_conflict', 'checkout_expired', 'checkout_closed', 'customer_click_required',
16
+ 'permission_denied', 'submit_unavailable', 'capability_disabled']);
14
17
  const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) &&
15
18
  [Object.prototype, null].includes(Object.getPrototypeOf(value));
16
19
  const check = (value, code = 'invalid_input') => { if (!value) throw new AccelerateError(code); };
@@ -59,6 +62,7 @@ const checkoutSchema = schemaObject({
59
62
  expires_at: timestampSchema, purpose: { type: 'string', enum: ['purchase', 'sandbox_source_comparison', 'sandbox_fill_only'] },
60
63
  approval_policy: { const: 'after_fill' }, purchase_approved: { type: 'boolean' },
61
64
  fill_after_verification: { const: true },
65
+ final_click_actor: { const: 'customer' }, submit_actor: { const: 'customer' },
62
66
  submit_allowed: { type: 'boolean' }, retry_safe: { const: false }, payment_confirmed: { const: false },
63
67
  card: schemaObject({ last4: { anyOf: [{ type: 'string', pattern: '^\\d{4}$' }, { type: 'null' }] }, brand: nullableText(40) }),
64
68
  address: schemaObject({ label: nullableText(400), city: nullableText(128), state: nullableText(128), postal_code_mask: nullableText(128) }, ['label']),
@@ -107,7 +111,7 @@ export const managedCheckoutToolDefinitions = freeze([
107
111
  description: 'Cancel this exact owned checkout and request private browser cleanup. Cancellation does not reverse an attempted order or prove that no purchase occurred. Keep uncertain-payment fences until independently reconciled.',
108
112
  inputSchema: idInput, outputSchema: outputSchema('checkout'), destructive: true },
109
113
  { name: 'accelerate_checkout_submit', title: 'Explicitly submit approved purchase',
110
- description: 'Explicit final purchase decision for this exact owned revision while its returned ready claim remains valid. Calls the one-use private final-submit operation once. Never automatically invoke after fill or retry after uncertainty. The default review profile requires the customer’s private post-fill approval first. submit_attempted is not order or payment confirmation. Available only when the server explicitly enables purchase submission.',
114
+ description: 'Explicit final purchase decision for this exact owned revision while its returned ready claim remains valid. Accelerate never clicks: this arms one short-lived click, your own backend performs the single physical final click in the customer’s existing browser, then Accelerate observes the outcome once. Never automatically invoke after fill or retry after uncertainty. The default review profile requires the customer’s private post-fill approval first. submit_attempted is not order or payment confirmation. Available only when the server explicitly enables purchase submission.',
111
115
  inputSchema: revisionInput, outputSchema: outputSchema('checkout'), destructive: true, submitOnly: true },
112
116
  ].map(value => ({ ...value, annotations: { readOnlyHint: value.readOnly === true, idempotentHint: value.readOnly === true,
113
117
  destructiveHint: value.destructive === true, openWorldHint: true } })));
@@ -126,7 +130,7 @@ function safeText(value, max = 1000, multiline = false) {
126
130
  function timestamp(value) { check(typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/u.test(value) && Number.isFinite(Date.parse(value)), 'invalid_response'); return value; }
127
131
  function projectCheckout(value) {
128
132
  shape(value, ['id', 'status', 'revision', 'expires_at', 'retry_safe', 'payment_confirmed'],
129
- ['purpose', 'submit_allowed', 'approval_policy', 'purchase_approved', 'fill_after_verification', 'card', 'address', 'quote', 'submit_expires_at', 'diagnostics'], 'invalid_response');
133
+ ['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');
130
134
  check(typeof value.id === 'string' && ID.test(value.id) && STATUSES.includes(value.status) && Number.isSafeInteger(value.revision) &&
131
135
  value.revision >= 0 && value.revision <= 2147483647 && value.retry_safe === false && value.payment_confirmed === false, 'invalid_response');
132
136
  const result = { id: value.id, status: value.status, revision: value.revision, expires_at: timestamp(value.expires_at), retry_safe: false, payment_confirmed: false };
@@ -135,7 +139,7 @@ function projectCheckout(value) {
135
139
  result.purpose = value.purpose; result.submit_allowed = value.submit_allowed;
136
140
  }
137
141
  if (value.approval_policy !== undefined || value.purchase_approved !== undefined) {
138
- check(value.approval_policy === 'after_fill' && typeof value.purchase_approved === 'boolean' && (!value.purchase_approved || value.status === 'ready_for_submit' && value.purpose === 'purchase'), 'invalid_response');
142
+ 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');
139
143
  result.approval_policy = value.approval_policy; result.purchase_approved = value.purchase_approved;
140
144
  }
141
145
  if (value.fill_after_verification !== undefined) {
@@ -143,6 +147,17 @@ function projectCheckout(value) {
143
147
  ['purchase', 'sandbox_fill_only'].includes(value.purpose), 'invalid_response');
144
148
  result.fill_after_verification = true;
145
149
  }
150
+ // The customer's own backend/browser performs any final click; Accelerate never does.
151
+ if (value.final_click_actor !== undefined) {
152
+ check(value.final_click_actor === 'customer' && value.purpose === 'purchase', 'invalid_response');
153
+ result.final_click_actor = 'customer';
154
+ }
155
+ if (value.submit_actor !== undefined) {
156
+ check(value.submit_actor === 'customer' && value.final_click_actor === 'customer' && ['submit_attempted', 'outcome_unknown'].includes(value.status), 'invalid_response');
157
+ result.submit_actor = 'customer';
158
+ }
159
+ // An attested attempt names the customer; an unknown outcome cannot assert that a click happened.
160
+ if (value.status === 'submit_attempted' && value.purpose === 'purchase') check(value.submit_actor === 'customer', 'invalid_response');
146
161
  if (value.card !== undefined) {
147
162
  shape(value.card, ['last4', 'brand'], [], 'invalid_response');
148
163
  check(value.card.last4 === null || typeof value.card.last4 === 'string' && /^\d{4}$/u.test(value.card.last4), 'invalid_response');
@@ -164,6 +179,8 @@ function projectCheckout(value) {
164
179
  result.quote = { checkout_origin: value.quote.checkout_origin, amount_cents: value.quote.amount_cents, currency: 'USD' };
165
180
  }
166
181
  if (value.submit_expires_at !== undefined) result.submit_expires_at = timestamp(value.submit_expires_at);
182
+ // A backend record: checked here so a reconciled checkout stays readable, and never projected to the agent.
183
+ if (value.reconciliation !== undefined) { try { validateCheckoutReconciliation(value.reconciliation, value.status); } catch { check(false, 'invalid_response'); } }
167
184
  if (value.diagnostics !== undefined) result.diagnostics = JSON.parse(JSON.stringify(validateCheckoutDiagnostics(value.diagnostics, result)));
168
185
  return result;
169
186
  }
@@ -243,6 +260,9 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
243
260
  const { verificationTiming, approvalPolicy, fillAfterVerification = false } = flowProfile({ profile, verificationTiming: requestedTiming, approvalPolicy: requestedApproval });
244
261
  check(server && typeof server.registerTool === 'function' && client?.checkouts &&
245
262
  ['create', 'get', 'browser', 'applyDetails', 'requestApproval', 'fill', 'submit', 'cancel'].every(key => typeof client.checkouts[key] === 'function') &&
263
+ (!enableSubmit || ['armFinalClick', 'completeFinalClick'].every(key => typeof client.checkouts[key] === 'function')) &&
264
+ // Purchases are never clicked by Accelerate or by Accelerate's private Kernel browser: only the customer's own browser host can click.
265
+ (purpose !== 'purchase' || browserMode === 'customer_backend_trusted' && ['prepareFinalClick', 'performFinalClick'].every(key => typeof browserConnector?.[key] === 'function')) &&
246
266
  [resolveContext, withOwnedCheckout, withCheckoutCreation, dispatchCheckoutEvent].every(value => typeof value === 'function') &&
247
267
  ['purchase', 'sandbox_fill_only'].includes(purpose) && typeof enableSubmit === 'boolean' && (!enableSubmit || purpose === 'purchase') &&
248
268
  ['on_start', 'on_payment'].includes(verificationTiming) && ['before_fill', 'after_fill'].includes(approvalPolicy) &&
@@ -251,6 +271,7 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
251
271
  (browserConnector === undefined || browserMode === 'customer_backend_trusted' && browserConnector?.contract === 'accelerate_existing_browser_v1' &&
252
272
  ['prepareAttachment', 'commitAttachment', 'assertPaymentReady', 'confirmCleanup'].every(key => typeof browserConnector[key] === 'function')));
253
273
  const requirePolicy = checkout => {
274
+ check(purpose !== 'purchase' || checkout.final_click_actor === 'customer', 'invalid_response');
254
275
  check(approvalPolicy === 'after_fill' ? checkout.approval_policy === 'after_fill' && typeof checkout.purchase_approved === 'boolean'
255
276
  : checkout.approval_policy === undefined && checkout.purchase_approved === undefined, 'invalid_response');
256
277
  return checkout;
@@ -322,6 +343,7 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
322
343
  creationStarted = true;
323
344
  const created = await client.checkouts.create({ ...context, start_url: input.start_url,
324
345
  idempotency_key: scope.idempotency_key, interaction_mode: 'chat', purpose,
346
+ ...(purpose === 'purchase' ? { final_click_actor: 'customer' } : {}),
325
347
  ...(browserMode === 'customer_backend_trusted' ? { browser: { ...browser } } : {}),
326
348
  ...(attachment?.browser_resource ?? scope.browser_resource ? { browser_resource: attachment?.browser_resource ?? scope.browser_resource } : {}),
327
349
  ...(approvalPolicy === 'after_fill' ? { approval_policy: approvalPolicy } : {}),
@@ -350,7 +372,30 @@ export function registerManagedCheckoutTools(server, { client = AccelerateClient
350
372
  value = await client.checkouts.requestApproval(id, { ...approval, ...(fillAfterVerification ? { fill_after_verification: true } : {}) }, options);
351
373
  check(!fillAfterVerification || value.fill_after_verification === true, 'invalid_response');
352
374
  } else if (definition.name === 'accelerate_checkout_fill') value = await client.checkouts.fill(id, { revision: input.revision }, options);
353
- else if (definition.name === 'accelerate_checkout_submit') { check(enableSubmit && purpose === 'purchase', 'permission_denied'); value = await client.checkouts.submit(id, { revision: input.revision }, options); }
375
+ else if (definition.name === 'accelerate_checkout_submit') {
376
+ check(enableSubmit && purpose === 'purchase' && typeof browserConnector?.performFinalClick === 'function' && typeof browserConnector.prepareFinalClick === 'function', 'permission_denied');
377
+ // prepare -> arm -> customer-owned host click -> complete, exactly once. The arm token and target stay in this scope.
378
+ // Slow provider/extension readiness runs BEFORE the arm so it cannot consume the short click window.
379
+ const preparation = await browserConnector.prepareFinalClick(context, { checkout_id: id }, options); requireAdmission();
380
+ let armed;
381
+ try { armed = await client.checkouts.armFinalClick(id, { revision: input.revision }, options); }
382
+ catch (error) {
383
+ // Only an explicit refusal proves nothing was armed. Transport, schema, timeout and upstream failures may have burned the arm.
384
+ if (error instanceof AccelerateError && PRE_ARM_REFUSALS.has(error.code)) throw error;
385
+ throw new AccelerateError('outcome_unknown');
386
+ }
387
+ try {
388
+ await browserConnector.performFinalClick(context, { checkout_id: id, preparation,
389
+ checkout_url: armed.target.checkout_url, submit_selector: armed.target.submit_selector,
390
+ armed_expires_at: armed.armed_expires_at }, options);
391
+ } catch { /* Host failure is not retried; the service observation below is the only truth. */ }
392
+ try { value = await client.checkouts.completeFinalClick(id, { revision: input.revision, arm_id: armed.arm_id }, options); }
393
+ catch { throw new AccelerateError('outcome_unknown'); }
394
+ // After a click attempt any unusable observation is an uncertain outcome, never a retryable failure.
395
+ try { requirePolicy(projectCheckout(value)); } catch { throw new AccelerateError('outcome_unknown'); }
396
+ check(['submit_attempted', 'outcome_unknown'].includes(value?.status) && value.final_click_actor === 'customer' &&
397
+ (value.status === 'outcome_unknown' || value.submit_actor === 'customer'), 'outcome_unknown');
398
+ }
354
399
  else value = await client.checkouts.cancel(id, options);
355
400
  requireAdmission();
356
401
  const projected = kind === 'browser' ? projectBrowser(value) : requirePolicy(projectCheckout(value));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weaccelerateinc/agent-sdk",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "description": "Server-only payment SDK, existing-browser connectors and private host adapters for agent platforms",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -2,8 +2,10 @@
2
2
  ACCELERATE_STARTER_MODE=synthetic
3
3
  # Explicit new profile: private CVV continues the armed fill in the backend.
4
4
  ACCELERATE_CHECKOUT_PROFILE=managed_review_v2
5
- ACCELERATE_STARTER_PURPOSE=sandbox_fill_only
6
- ACCELERATE_ENABLE_PURCHASE_SUBMIT=false
5
+ ACCELERATE_STARTER_PURPOSE=purchase
6
+ ACCELERATE_ENABLE_PURCHASE_SUBMIT=true
7
+ # For a fill-only test, set purpose to sandbox_fill_only and submit to false.
8
+ # This file is a template; export values or load it with your server environment.
7
9
  # Live: operator-controlled absolute file exporting createPartnerDependencies().
8
10
  # ACCELERATE_CONTEXT_MODULE=/absolute/protected/partner-host.mjs
9
11
  # ACCELERATE_API_KEY=<tenant server key>
@@ -1,20 +1,20 @@
1
1
  # Existing-browser payment starter
2
2
 
3
- SDK source 0.10.3, explicit `managed_review_v2`, `customer_backend_trusted`. The matching service must support this profile; source changes do not establish an archive publication or live deployment. Your platform creates its browser and installs the approved extension. This starter attaches that same browser at payment; it never creates a replacement. The older `accelerate-agent init <dir>` starter still selects Accelerate-managed Kernel custody.
3
+ SDK 0.11.0, explicit `managed_review_v2`, `customer_backend_trusted`. Newly generated existing-browser apps default to `purpose:'purchase'` with the submit tool enabled. The matching service must support this profile; publishing the SDK does not deploy the service or qualify a merchant layout. Your platform creates its browser and installs the approved extension. This starter attaches that same browser at payment; it never creates a replacement. The older `accelerate-agent init <dir>` starter still selects Accelerate-managed Kernel custody and its fill-only default.
4
4
 
5
5
  ## Run without credentials
6
6
 
7
- Once Accelerate supplies the matching verified 0.10.3 archive, install it, then:
7
+ Once Accelerate supplies the matching verified 0.11.0 archive, install it, then:
8
8
 
9
9
  ```sh
10
10
  npx --no-install accelerate-agent init ./partner-demo --browser existing
11
11
  cd partner-demo
12
- npm install /secure/downloads/weaccelerateinc-agent-sdk-0.10.3.tgz
12
+ npm install /secure/downloads/weaccelerateinc-agent-sdk-0.11.0.tgz
13
13
  npm test
14
14
  npm run smoke
15
15
  ```
16
16
 
17
- The synthetic fixture uses the actual connector, durable-host interface, private ingress/outbox and MCP registration. It simulates verification/provider operations; no real browser, message, credential or order is created. Its in-memory persistence and fake extension metadata are test fixtures only.
17
+ The synthetic fixture uses the actual connector, durable-host interface, private ingress/outbox and MCP registration. It simulates verification/provider operations; no real browser, message, credential or order is created. The smoke exercises default purchase mode, confirms submission is rejected before private approval, then cancels without approving or submitting. Its in-memory persistence and fake extension metadata are test fixtures only.
18
18
 
19
19
  ## Connect your platform
20
20
 
@@ -41,8 +41,21 @@ At create, v2 declares `fill_after_verification:true` and requires the service t
41
41
 
42
42
  Before attachment, use ordinary customer details for required non-secret fields or ask for missing details. Leave card number/PAN, expiry and CVV empty; never obtain them through ordinary browser tools.
43
43
 
44
- PAN displays stars + last four, CVV stars, expiry visible. Filled-page inspection is blocked for the agent. Provider-owner backend access remains trusted. The published release baseline qualifies `https://demos.weaccelerate.com/normal/payment`. The new universal-card source path probes compatible public HTTPS card forms instead of keeping a merchant hostname allowlist. It requires private `universal_card_v1` admission bound to the exact existing document and immutable purpose before OTP/CVV; ambiguous/unsupported forms fail without a replacement browser. That capability requires coordinated service/executor deployment and does not promise every checkout works. A candidate artifact or generic interface alone does not qualify a browser.
44
+ PAN displays stars + last four, CVV stars, expiry visible. Filled-page inspection is blocked for the agent. Provider-owner backend access remains trusted. `universal_card_v1` is private runtime admission for the exact HTTPS checkout document and requested purpose, including both purchase and fill-only. It detects compatible payment fields and safe review support across merchants; unsupported or ambiguous forms fail before OTP/CVV without a replacement browser. It requires a matching service/executor deployment. The approved extension's original manifest records demo fill/review/cancel evidence; the [2 November sandbox report](https://github.com/weaccelerateinc/accelerate/blob/main/products/agent-sdk/qualification/REPORT.md) includes additional real merchant fill/review runs, with no real-store order submission or production qualification. The universal path reuses the approved 0.0.0.1 extension; it does not require candidate 0.0.0.2.
45
45
 
46
- The default is `sandbox_fill_only`, with no submit tool. After its informational image, call cancel and verify cleanup. Enable purchase only through explicit backend `purpose:'purchase'` and `enableSubmit:true` after qualification. Customer approval never automatically submits. Uncertain outcomes retain fences and require reconciliation; status/cleanup never authorize a replacement by themselves.
46
+ ## Purchase and fill-only configuration
47
+
48
+ The new starter defaults are:
49
+
50
+ ```dotenv
51
+ ACCELERATE_STARTER_PURPOSE=purchase
52
+ ACCELERATE_ENABLE_PURCHASE_SUBMIT=true
53
+ ```
54
+
55
+ Purchase mode exposes `accelerate_checkout_submit`. Accelerate never sends the final click: that tool arms one short click, your `createPartnerDependencies` browser connector host must implement `performFinalClick` to click once in the customer's existing browser, then Accelerate observes the outcome. Registration fails without that host. The synthetic fixture simulates the arm, host click and completion; its smoke still cancels without a purchase decision, and the starter tests run the approved path. Private CVV, a delivered masked review, a fresh customer decision and a separate explicit submit for the matching revision are still required. A ready form or customer approval never submits automatically. The default `ACCELERATE_STARTER_MODE=synthetic` makes no external requests. `live` selects real backend dependencies, not the production wallet environment; the approved key, Merchant ID and service configuration select that environment.
56
+
57
+ For a no-order test, set `ACCELERATE_STARTER_PURPOSE=sandbox_fill_only` and remove the submit override or set `ACCELERATE_ENABLE_PURCHASE_SUBMIT=false`. If you copied `.env.example`, change both lines. An explicit true submit setting with fill-only purpose is rejected. After its informational review, cancel and verify cleanup. Purchase purpose with explicit submit false is also supported.
58
+
59
+ The starter does not load `.env` automatically; export the settings or load them through your server's environment configuration. This change affects future `init --browser existing` output after 0.11.0 is released. Published 0.10.3 and already generated apps retain their old files; configure their existing switches explicitly to enable purchase. Reusable SDK registration defaults remain fill-only. Existing checkout purpose is immutable, so an existing fill-only checkout cannot become a purchase. Uncertain outcomes retain fences and require reconciliation; status/cleanup never authorize a replacement by themselves.
47
60
 
48
61
  Before live testing, confirm the session is executing the deployed v2 tool bundle. Updating a durable agent app does not necessarily replace an active session’s tools; settle its old work and use the framework’s supported idle deployment handoff or create a new authenticated session. Verify the quote response acknowledges `fill_after_verification:true`; a successful deployment alone is insufficient.
@@ -12,7 +12,7 @@
12
12
  "test": "node --test tests/*.test.mjs"
13
13
  },
14
14
  "dependencies": {
15
- "@weaccelerateinc/agent-sdk": "0.10.3",
15
+ "@weaccelerateinc/agent-sdk": "0.11.0",
16
16
  "@modelcontextprotocol/server": "2.3.1"
17
17
  },
18
18
  "devDependencies": {
@@ -9,8 +9,9 @@ export async function configureStarter(server, env = process.env) {
9
9
  const mode = env.ACCELERATE_STARTER_MODE ?? 'synthetic';
10
10
  const profile = env.ACCELERATE_CHECKOUT_PROFILE ?? 'managed_review_v2';
11
11
  if (!['managed_review_v1', 'managed_review_v2'].includes(profile)) throw Error('Unsupported partner checkout profile.');
12
- const purpose = env.ACCELERATE_STARTER_PURPOSE ?? 'sandbox_fill_only';
13
- const enableSubmit = env.ACCELERATE_ENABLE_PURCHASE_SUBMIT === 'true';
12
+ const purpose = env.ACCELERATE_STARTER_PURPOSE ?? 'purchase';
13
+ const enableSubmit = env.ACCELERATE_ENABLE_PURCHASE_SUBMIT === undefined
14
+ ? purpose === 'purchase' : env.ACCELERATE_ENABLE_PURCHASE_SUBMIT === 'true';
14
15
  if (!['sandbox_fill_only', 'purchase'].includes(purpose) ||
15
16
  env.ACCELERATE_ENABLE_PURCHASE_SUBMIT !== undefined && !['true', 'false'].includes(env.ACCELERATE_ENABLE_PURCHASE_SUBMIT) ||
16
17
  enableSubmit && purpose !== 'purchase') throw Error('Unsupported server purchase policy.');
@@ -12,9 +12,9 @@ const timer = setTimeout(() => { void transport.close(); }, 15000);
12
12
  try {
13
13
  await client.connect(transport);
14
14
  const tools = await client.listTools();
15
- assert.equal(tools.tools.length, 7);
15
+ assert.equal(tools.tools.length, 8);
16
16
  assert.equal(tools.tools.some(tool => tool.name === 'accelerate_checkout_request_payment'), true);
17
- assert.equal(tools.tools.some(tool => tool.name === 'accelerate_checkout_submit'), false);
17
+ assert.equal(tools.tools.some(tool => tool.name === 'accelerate_checkout_submit'), true);
18
18
  assert.equal(tools.tools.some(tool => tool.name === 'accelerate_checkout_fill'), false);
19
19
  const call = async (name, args) => {
20
20
  const result = await client.callTool({ name, arguments: args });
@@ -23,8 +23,9 @@ try {
23
23
  };
24
24
  const checkoutUrl = 'https://demos.weaccelerate.com/normal/payment';
25
25
  const checkout = (await call('accelerate_checkout_start', { start_url: checkoutUrl })).checkout;
26
- assert.equal(checkout.purpose, 'sandbox_fill_only'); assert.equal(checkout.submit_allowed, false);
26
+ assert.equal(checkout.purpose, 'purchase'); assert.equal(checkout.submit_allowed, true);
27
27
  assert.equal(checkout.approval_policy, 'after_fill'); assert.equal(checkout.purchase_approved, false);
28
+ assert.equal(checkout.final_click_actor, 'customer');
28
29
  assert.equal(checkout.status, 'wallet_required'); assert.equal(checkout.card, undefined);
29
30
  const id = checkout.id;
30
31
  assert.equal((await call('accelerate_checkout_status', { checkout_id: id })).checkout.status, 'wallet_required');
@@ -42,12 +43,16 @@ try {
42
43
  assert.equal(reviewed.purchase_approved, false);
43
44
  const remaining = Date.parse(reviewed.submit_expires_at) - Date.now();
44
45
  assert.ok(remaining > 290000 && remaining <= 300000);
46
+ const rejected = await client.callTool({ name: 'accelerate_checkout_submit', arguments: { checkout_id: id, revision: reviewed.revision } });
47
+ const rejection = rejected.structuredContent ?? JSON.parse(rejected.content.find(part => part.type === 'text').text);
48
+ assert.equal(rejection.ok, false); assert.equal(rejection.error.code, 'submit_unavailable');
45
49
  assert.equal((await call('accelerate_checkout_cancel', { checkout_id: id })).checkout.status, 'cancelled');
46
50
  complete = true;
47
51
  process.stdout.write(JSON.stringify({ ok: true, synthetic_only: true, mcp_protocol: true,
48
52
  profile: 'managed_review_v2', existing_browser_connector: true, browser_mode: 'customer_backend_trusted', verification_deferred: true, private_verification_before_fill: true,
49
- private_review_receipt_acknowledged: true, approval_window_ms: 300000, purchase_approval_requested: false,
50
- submit_tool_absent: true, manual_fill_tool_absent: true, fill_ready: true, cancelled: true, live_checkout_requests: 0 }) + '\n');
53
+ private_review_receipt_acknowledged: true, approval_window_ms: 300000, purchase_approval_requested: true,
54
+ purpose: 'purchase', submit_tool_present: true, unapproved_submit_blocked: true, purchase_decision_received: false,
55
+ submit_attempted: false, final_click_actor: 'customer', manual_fill_tool_absent: true, fill_ready: true, cancelled: true, live_checkout_requests: 0 }) + '\n');
51
56
  } catch {
52
57
  process.stderr.write(JSON.stringify({ ok: false, code: 'synthetic_smoke_failed' }) + '\n');
53
58
  } finally {