@agent-cards/checkout 0.11.0 → 0.13.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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Identify hosted, regional, and versioned processor variants seen on live checkouts: Adyen secured fields on the regional live hosts and any release, the Square card element frame, dLocal Smart Fields releases, Mercado Pago secure fields and the guest Checkout Pro card form, and the Rapyd hosted checkout page. The Tranzila terminal-root page, the Authorize.Net hosted payment form, and Worldpay hosted payment pages identify their processor and return `unknown` with `processor_variant_unverified`. The catalog is `2026-09-15.1`; issue native profiles against it.
6
+
7
+ - Report a sole script-only candidate as `psp` only when the observation is complete. On a partial scan the script stays in `candidates` as `undetermined`, so a co-loaded SDK is never presented as the selected processor.
8
+
9
+ - Detect visible card controls in nested frames from field metadata only, never values. When those controls sit on a site no evidenced processor owns, the collector reports `card_entry_unrecognized`, the observation is incomplete, and the result stays `unknown`. Callers supplying their own observations can report the same reason.
10
+
11
+ - Match discovery rules on the origin and path only. A long query string on an unrelated wallet or widget frame no longer invalidates the observation. `invalid_url` covers unparsable locations, an origin and path over `4096` characters, or a raw location over `65536` characters.
12
+
13
+ - Compare committed frame URLs with committed frame URLs. A script-created frame that inherits the merchant URL through `document.open()` no longer reports `navigation_changed`; real navigations, added frames, and removed frames still do.
14
+
15
+ - Read frames one nesting level at a time with up to eight frames in flight, and release frame handles while the document read proceeds. Frame-heavy checkouts over a remote CDP connection fit the default 1,500 ms budget far more often. `collection.timeoutMs` remains available for slower connections.
16
+
17
+ - Complete a merchant-confirmed payment when the merchant supplies no order or receipt ID. Return `{ status: 'completed', confirmation: { kind: 'merchant_payment', authorizationId } }` from the merchant resolver using the current checkout authorization. The resolver must verify the original payment request, amount, currency, selected card and merchant success; HTTP 200 or a success page alone is insufficient. The completed state carries `confirmation` without inventing `orderId`, clears stale reasons and continues to block another payment. Existing completion results with a genuine `orderId` keep working.
18
+
5
19
  - Approve a Recurly checkout before its native card request starts. Call `prepare` with `psp: 'recurly'` and `environment: 'shared'`, then submit the merchant's form once. The same selected card and approved purchase bind a new-card POST on the US or EU endpoint. The matching API, database migration and Vault release are required. Token issuance and merchant payment completion remain separate results.
6
20
 
7
21
  - Keep the selected card's issuer in guest Mercado Pago Checkout Pro in Mexico. The Vault resolves the card's eight-digit prefix against the merchant's captured checkout configuration over browser TLS. The SDK returns the original card-token response and corrects one native card association. Missing configuration, ambiguous issuers, changed checkout context, and a different card brand or type stop continuation. The matching API, Vault and encrypted relay release is required. Other Mercado Pago checkout families and merchant payment completion need separate validation.
package/PREFLIGHT.md CHANGED
@@ -93,7 +93,7 @@ The [Mollie fixture](./examples/preflight/mollie-hosted.observations.json) ident
93
93
  | `signals[].visible` | `true`, `false`, or `null` when visibility is undetermined. Do not mark scripts active merely because they loaded. |
94
94
  | `observation.complete` | Whether the scan finished without missing observations. |
95
95
  | `observation.truncated` | Whether a collection limit discarded observations. |
96
- | `observation.reasons` | Documented collection reason codes for an incomplete observation. |
96
+ | `observation.reasons` | Documented collection reason codes for an incomplete observation. Report `card_entry_unrecognized` when your own collector sees visible card controls in a nested frame on a site no evidenced processor owns. |
97
97
 
98
98
  The normalized snapshot contains `schema_version: 1`, `catalog_version`, rule identifiers, frame relationships, and collection status. Raw URLs do not survive normalization. The assessment returns PSP candidates with reviewed evidence, support status and its scope, any identified flow, reason codes, and limitations. `scenario_defaulted` records whether the caller omitted the scenario and accepted `one_time`.
99
99
 
@@ -177,6 +177,8 @@ The catalog includes reviewed discovery rules and direct SDK implementation capa
177
177
 
178
178
  The bundled direct SDK profile declares processor support for `one_time` purchases. The catalog carries each processor's request limitations and known exclusions. A detection rule does not cover every product or regional variant sold under the processor's name. For example, Moneris Hosted Tokenization and Moneris Checkout use different payment paths; the implemented adapter covers Hosted Tokenization.
179
179
 
180
+ The catalog also carries reviewed hosted, regional, and versioned variants observed on live checkouts: Adyen secured fields on the regional live hosts and any release, the Square card element frame, dLocal Smart Fields releases, Mercado Pago secure fields and the guest Checkout Pro card form, and the Rapyd hosted checkout page. A variant the implemented adapter does not admit, such as the Tranzila terminal-root page, the Authorize.Net hosted payment form, or a Worldpay hosted payment page, identifies its processor and returns `unknown` with `processor_variant_unverified`. A new fingerprint never declares payment support by itself.
181
+
180
182
  | Observed checkout | Classification |
181
183
  | --- | --- |
182
184
  | A compatible Stripe script, `one_time`, direct SDK | `supported` at `processor_integration` scope; `checkout_flow_status` remains `unknown`. |
@@ -184,6 +186,8 @@ The bundled direct SDK profile declares processor support for `one_time` purchas
184
186
  | A detected processor with an empty Kernel native profile | `unknown`; the native adapter has not declared processor or flow support. |
185
187
  | Competing processors without enough evidence to select one | `unknown`, with `checkout_flow_status: "ambiguous"`. |
186
188
  | An incomplete observation | `unknown`; inspect again after the checkout settles. |
189
+ | A single processor script during an incomplete scan | `unknown`; `psp` stays `null` and the script remains in `candidates` as `undetermined` until a complete scan confirms it. |
190
+ | Visible card controls in a nested frame on a site no evidenced processor owns | `unknown` with `card_entry_unrecognized`; a co-loaded SDK script is listed as a candidate, not as the processor. |
187
191
 
188
192
  Processor support does not establish which payment method the merchant selected. Many SDKs also load wallets or fraud checks before a card form appears. Read `checkout_flow_status` and the returned limitations alongside `status`; never present `supported` alone as a promise that a merchant purchase will succeed.
189
193
 
@@ -208,7 +212,7 @@ Declare processor support and flow support separately. A flow entry cannot estab
208
212
 
209
213
  Take PSP identifiers, payment modes, flow identifiers, operation identifiers, and operation revisions from the packaged catalog. Verify each combination against the named adapter version before adding a `supported` entry. Use `unsupported` only for a known exclusion. A profile with a wrong schema, incompatible catalog, mismatched adapter version, or expired timestamp preserves `unknown`.
210
214
 
211
- The JSON contract uses `schema_version: 1`. The catalog bundled with this release uses `catalog_version: "2026-09-14.1"`. Keep the snapshot, classifier, and profile on the same catalog version. Re-normalize observations with the installed package after changing the catalog. Review the adapter declarations before issuing a compatible profile; changing the version string alone does not validate new behavior.
215
+ The JSON contract uses `schema_version: 1`. The catalog bundled with this release uses `catalog_version: "2026-09-15.1"`. Keep the snapshot, classifier, and profile on the same catalog version. Re-normalize observations with the installed package after changing the catalog. Review the adapter declarations before issuing a compatible profile; changing the version string alone does not validate new behavior.
212
216
 
213
217
  Kernel can integrate the JSON contract immediately and supply its validated native capabilities later. No deployed Agentcard endpoint or payment request is required for that work.
214
218
 
@@ -235,7 +239,7 @@ The current inventory leaves every native runtime status `unknown`. Kernel docum
235
239
  | `checkout_flow_status: "unknown"` | The observations do not identify the exact checkout flow. Processor support can still be known. |
236
240
  | `checkout_flow_status: "ambiguous"` | Competing evidence prevents a single flow assessment. Inspect after the payment method is selected. |
237
241
 
238
- A loaded PSP script establishes a possible processor, not necessarily the active card flow. The helper returns candidates rather than picking the first script. A missing fingerprint does not establish lack of support.
242
+ A loaded PSP script establishes a possible processor, not necessarily the active card flow. The helper returns candidates rather than picking the first script. A sole script candidate becomes `psp` only when the observation is complete. When a visible card-entry frame belongs to no evidenced processor's site, the collector reports `card_entry_unrecognized` and the script stays a candidate. A missing fingerprint does not establish lack of support.
239
243
 
240
244
  Every assessment is an advisory hint. The existing payment request checks still apply. A supported flow can require authentication or merchant configuration, and only the merchant's payment result establishes whether a purchase succeeded.
241
245
 
@@ -278,9 +282,10 @@ Collection reasons explain why an observation is partial:
278
282
  | `signal_limit` | Preserve `unknown` when the page exceeds the observation limit. |
279
283
  | `snapshot_limit` | Preserve `unknown` when the observations exceed the size budget. |
280
284
  | `frame_unavailable` | Inspect again after the frame is available. |
281
- | `navigation_changed` | Inspect the new checkout state after navigation finishes. |
285
+ | `navigation_changed` | A frame's committed URL changed, a frame was added or removed, or an http(s) document's URL changed during the scan. A script-created frame that inherits the merchant URL is not a navigation. Inspect the new checkout state after navigation finishes. |
286
+ | `card_entry_unrecognized` | Visible card controls sit in a nested frame on a site that no evidenced processor owns, or no processor evidence was found at all. Treat listed candidates as unconfirmed and inspect again after the payment method is selected. Identification needs a reviewed fingerprint for that frame. |
282
287
  | `invalid_observation` | Validate raw observations against the packaged schema. |
283
- | `invalid_url` | Correct the malformed or oversized asset location before normalization. |
288
+ | `invalid_url` | The location could not be parsed, its origin and path exceed `4096` characters, or the raw location exceeds `65536` characters. Matching reads the origin and path only, so a long query string on an unrelated frame never invalidates an observation. |
284
289
  | `collection_incomplete` | Inspect again after the underlying collection problem is resolved. |
285
290
 
286
291
  ## Keep observations current
@@ -293,6 +298,8 @@ Collect only the documented asset observations when using your own browser tools
293
298
 
294
299
  The default collection budget is `1500` milliseconds, with at most `64` frames, `2048` signals, and `262144` serialized snapshot bytes. Set lower bounds through `collection.timeoutMs`, `collection.maxFrames`, `collection.maxSignals`, or `collection.maxSnapshotBytes` on `inspectCheckout`. The maximum time budget is `10000` milliseconds; the remaining limits cannot exceed their defaults. The minimum snapshot budget is `512` bytes.
295
300
 
301
+ Frames are read one nesting level at a time with up to eight frames in flight, and each frame handle is released while its document read proceeds. A remote CDP connection therefore pays about three round trips per nesting level plus one per eight frames, rather than four round trips per frame. Over a remote connection, allow `3000` to `5000` milliseconds through `collection.timeoutMs`, inspect after the payment controls have loaded, and read `observation.reasons` before retrying. A longer budget cannot repair a scan that started before the checkout settled.
302
+
296
303
  ## Validate without a purchase
297
304
 
298
305
  The included fixture is synthetic and contains no card information. Fixture classifications validate the detector contract; they are not evidence of a real payment or of a merchant's coverage.
package/README.md CHANGED
@@ -367,17 +367,56 @@ const checkout = await attachToPlaywright(page, {
367
367
 
368
368
  // Your existing agent dispatches checkout. Later, once the paused request resumes:
369
369
  const state = await checkout.reconcile();
370
- if (state.status === 'completed' && state.orderId) await finishAgentTask(state.orderId);
370
+ if (state.status === 'completed') await finishAgentTask(state);
371
371
  ```
372
372
 
373
- `resolveMerchantResult` must read an authoritative merchant order/receipt tied
374
- to this attempt. It returns one of:
373
+ `resolveMerchantResult` must verify the merchant's result for the original
374
+ payment attempt. It returns one of:
375
375
 
376
- - `{ status: 'completed', orderId }`: merchant-confirmed success.
376
+ - `{ status: 'completed', orderId }`: merchant-confirmed success with a genuine order or receipt ID. Existing integrations keep this form.
377
+ - `{ status: 'completed', confirmation: { kind: 'merchant_payment', authorizationId } }`: merchant-confirmed payment when no order or receipt ID is available. The ID must match the current checkout authorization.
377
378
  - `{ status: 'failed' }`: merchant confirmed the attempt failed; no successful payment/order exists.
378
379
  - `{ status: 'pending' }` or `{ status: 'unknown' }`: keep waiting or reconcile; never click Pay again.
379
380
  - `{ status: 'requires_user_action', reason: '3ds' | 'redirect' | 'other' }`: deliver your own browser live view or supported challenge UI to the user.
380
381
 
382
+ For a merchant that confirms payment without returning an order ID, your
383
+ resolver can return the explicit payment confirmation:
384
+
385
+ ```ts
386
+ const checkout = await attachToPlaywright(page, {
387
+ vault, user, merchant, amount, currency,
388
+ requireMerchantResult: true,
389
+ resolveMerchantResult: async state => {
390
+ const payment = await readOriginalMerchantPayment(state);
391
+ if (!payment.confirmed || !state.authorizationId) return { status: 'unknown' };
392
+ return {
393
+ status: 'completed',
394
+ confirmation: {
395
+ kind: 'merchant_payment',
396
+ authorizationId: state.authorizationId,
397
+ },
398
+ };
399
+ },
400
+ });
401
+ ```
402
+
403
+ `readOriginalMerchantPayment` is your merchant-specific check. The check must
404
+ match the original payment request, amount, currency and selected card, and
405
+ verify authoritative merchant success for that attempt. HTTP 200 alone, a card
406
+ token, a success URL or text that anyone can open does not establish payment.
407
+ Copying `state.authorizationId` without checking the payment is insufficient.
408
+ The SDK checks the authorization binding; it does not independently authenticate
409
+ the merchant evidence supplied by your resolver.
410
+
411
+ Return one completion form at a time. The payment-confirmation form leaves
412
+ `state.orderId` absent and exposes `state.confirmation` with the exported
413
+ `MerchantPaymentConfirmation` type. Your consumer should finish on
414
+ `state.status === 'completed'` and treat `orderId` as optional. A missing or
415
+ mismatched authorization, or `{ status: 'completed' }` without either completion
416
+ form, leaves the outcome unknown. Confirmed completion clears stale failure or
417
+ authentication reasons and continues to block further payment submissions.
418
+ Never invent an order ID from a token or authorization ID.
419
+
381
420
  The SDK does not infer order success from `authorized` or a tokenization reply,
382
421
  and does not claim to detect or solve arbitrary 3DS challenges. Your merchant
383
422
  resolver (or `checkout.requestUserAction('3ds')` when your browser observes it)
@@ -388,22 +427,29 @@ isolated from the payment handoff.
388
427
  Native Stripe Checkout emits `checkout_blocked` before the existing `blocked`
389
428
  event when its local preparation rejects a request. Its
390
429
  `StripeCheckoutBlockedDetail` contains only fixed codes: `version: 1`,
391
- `processor: 'stripe'`, endpoint family, phase, stage, reason, gate state, and
392
- disposition. It contains no URLs, identifiers, request fields, or exception text.
430
+ `processor: 'stripe'`, endpoint family, phase, stage, reason, optional validation code, gate state, and
431
+ disposition. It contains no URLs, identifiers, request-derived field names or values, or exception text.
393
432
 
394
433
  | Field | Values |
395
434
  | --- | --- |
396
435
  | `endpoint_family` | `payment_methods`, `payment_page_confirm`, `other` |
397
436
  | `phase` | `tokenization`, `final`, `unknown` |
398
437
  | `stage` | `request_read`, `classification`, `claim`, `readiness`, `document`, `stub_response` |
438
+ | `validation_code` | Shared-core `StripeCheckoutValidationCode`; present only for `request_validation_failed` |
399
439
  | `gate_state` | `fresh`, `stubbed`, `submitted`, `stopped` |
400
440
  | `disposition` | `active_claim_preserved`, `checkout_stopped` |
401
441
 
402
442
  `reason` is the exported `StripeCheckoutBlockReason` union. It identifies SDK
403
443
  claim and readiness failures, such as `duplicate_confirmation`, `document_changed`,
404
- or `attachment_not_ready`. The shared core's rejection is
405
- `request_validation_failed` with phase `unknown`; this event does not infer which
406
- core predicate failed. `gate_state` records the state before the adapter handles
444
+ or `attachment_not_ready`. A shared-core rejection reports
445
+ `request_validation_failed` and a fixed `validation_code` identifying the failed
446
+ check, or `unclassified` when no recognized code is available. Initial request
447
+ classification uses phase `unknown`; a billing capture rejection uses
448
+ `tokenization`, and a billing attachment rejection uses `final`.
449
+ For example, `form_field_unknown` identifies an allowlist rejection without
450
+ revealing the field name or value. Identifying that field requires a separate
451
+ reviewed synthetic fixture; the code alone does not establish or fix a processor
452
+ schema mismatch. `gate_state` records the state before the adapter handles
407
453
  the failure, though document validation may already have stopped the gate.
408
454
 
409
455
  `active_claim_preserved` means a valid duplicate was refused without invalidating
@@ -579,10 +625,12 @@ When the cardholder has enabled an eligible spending rule in their vault, the sa
579
625
 
580
626
  Use `executionMode: 'user_approval'` to require the existing confirmation flow. Without a selected card or grant, the backend can use exactly one eligible rule; ambiguous card selection keeps confirmation. A `grantId` restricts selection to that rule. The initial executor supports only the configured controlled Stripe test flow, and production remains disabled.
581
627
 
582
- For controlled hosted Stripe Checkout, both attachment functions accept `stripeCheckout: { sessionId, publishableKey, environment? }`. The default is TEST, requiring `cs_test_` and `pk_test_`. LIVE requires explicit `environment: 'production'`, a `cs_live_` Session and its `pk_live_` key. Both modes require `executionMode: 'autopilot'`, an explicit `grantId`, and a positive numeric USD `amount` in cents. The top-level document must be that Session on `checkout.stripe.com`. Direct `authorize()` calls use `stripeCheckoutEnvironment: 'production'` for the same explicit LIVE opt-in; the attachment functions pass it automatically.
628
+ For controlled hosted Stripe Checkout, both attachment functions accept `stripeCheckout: { sessionId, publishableKey, environment? }`. The default is TEST, requiring `cs_test_` and `pk_test_`. LIVE requires explicit `environment: 'production'`, a `cs_live_` Session and its `pk_live_` key. Both modes require `executionMode: 'autopilot'`, an explicit `grantId`, and a positive numeric USD `amount` in cents. The top-level document must be that exact Session on `https://checkout.stripe.com`, at `/c/pay/{sessionId}`, `/pay/{sessionId}`, `/g/pay/{sessionId}`, or `/f/pay/{sessionId}`. The SDK validates the existing URL without rewriting or navigating it. Direct `authorize()` calls use `stripeCheckoutEnvironment: 'production'` for the same explicit LIVE opt-in; the attachment functions pass it automatically.
583
629
 
584
630
  The SDK answers dummy-card tokenization locally, then sends the browser's final native confirmation through the shared core and enclave. The local response is preparation only: it creates no authorization and makes no processor request. The final result includes `checkoutSessionId` only after the selected grant, restricted terminal response, and captured amount have been checked. Continue to confirm the merchant order through the checkout controller.
585
631
 
632
+ When Agentcard reports `autopilot_status: action_required` for the same authorization and grant, the SDK stops waiting and calls `onUserAction` with `reason: 'other'` and the authorization ID. The checkout controller records `requires_user_action` and holds further card requests. A direct `VaultClient.authorize()` call rejects with `PaymentOutcomeUnknownError` and reason `autopilot_action_required`. The existing authorization and reservation remain held; the SDK does not cancel or create another payment, return a processor payload, or infer a 3DS challenge. Your application's `resolveMerchantResult` can report the authoritative outcome of this same payment through `reconcile()`. Challenge execution and automatic continuation are not provided by this status report.
633
+
586
634
  The shared core carries a validated billing email from that tokenization request into the final confirmation before authorization. The email stays bound to the same Session and local PaymentMethod reference. Missing email stays missing; duplicate or conflicting values are refused. The SDK does not fill an email from the Agentcard account or invent one for the merchant.
587
635
 
588
636
  This integration requires matching backend and measured executor releases. TEST and LIVE have separate admission controls. LIVE also requires a newly authorized production grant on the selected real card, a fixed merchant account/profile with exactly one complete fixed-price USD line item and an independently reviewed operation qualification reference; the SDK option supplies none of these. LIVE deployment remains unqualified and disabled until that evidence exists. Exact observed totals do not establish atomic amount enforcement by Stripe. Subscriptions, saved-card flows and authentication continuations remain excluded. An unavailable preparation or uncertain result is declined or held for reconciliation; it cannot switch to a competing human-approval payment.
package/dist/client.js CHANGED
@@ -696,6 +696,21 @@ export class VaultClient {
696
696
  catch { /* observer only */ }
697
697
  };
698
698
  let failed = false;
699
+ let actionRequired = false;
700
+ const checkAutopilotAction = (state) => {
701
+ if (state.autopilot_status !== 'action_required')
702
+ return;
703
+ // The authenticated API reports only a verified completion category.
704
+ // Retain this authorization without returning its processor payload or
705
+ // inferring whether the required action is 3DS, a redirect, or another step.
706
+ if (state.id !== authorizationId || state.status !== 'awaiting_approval' || state.mode !== 'token'
707
+ || state.execution_mode !== 'autopilot' || execution.executionMode !== 'autopilot'
708
+ || state.grant_id !== execution.grantId || (input.grantId && state.grant_id !== input.grantId)) {
709
+ throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_action_unconfirmed');
710
+ }
711
+ actionRequired = true;
712
+ throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_action_required');
713
+ };
699
714
  const stopSignal = input.merchantSignal
700
715
  ? AbortSignal.any([input.merchantSignal, ...(input.signal ? [input.signal] : [])]) : input.signal;
701
716
  try {
@@ -706,6 +721,7 @@ export class VaultClient {
706
721
  if (input.merchantSignal?.aborted)
707
722
  throw new PaymentOutcomeUnknownError(authorizationId, 'merchant_request_aborted');
708
723
  execution = executionMetadata(created, authorizationId);
724
+ checkAutopilotAction(created);
709
725
  deliverApproval(created);
710
726
  while (Date.now() < deadline) {
711
727
  if (stopSignal?.aborted)
@@ -729,6 +745,7 @@ export class VaultClient {
729
745
  throw new PaymentOutcomeUnknownError(authorizationId, 'authorization_status_malformed');
730
746
  }
731
747
  execution = executionMetadata(s, authorizationId, execution);
748
+ checkAutopilotAction(s);
732
749
  if (nativeCheckout && (s.status === 'submitted_on_device' ||
733
750
  (s.status === 'approved' && (s.mode !== 'token' || execution.executionMode !== 'autopilot'
734
751
  || execution.grantId !== input.grantId))))
@@ -885,7 +902,9 @@ export class VaultClient {
885
902
  throw error;
886
903
  }
887
904
  finally {
888
- if (input.merchantSignal?.aborted || ((preparation || nativeCheckout) && failed)) {
905
+ // An attested action-required result retains its claim and reservation.
906
+ // Reporting that state must not request cancellation of the same attempt.
907
+ if (!actionRequired && (input.merchantSignal?.aborted || ((preparation || nativeCheckout) && failed))) {
889
908
  // Drain a create acknowledgement even after the merchant aborts so its
890
909
  // known ID can be retired. An unacknowledged create remains unknown.
891
910
  // A started/finalized replay or failed cleanup never becomes a claimed
package/dist/index.d.ts CHANGED
@@ -10,4 +10,4 @@ export { hostedFormSubmittedPage, HOSTED_FORM_SUBMITTED_OUTCOME } from './hosted
10
10
  export type { HostedFormSubmittedPageInput, SyntheticPage } from './hosted-form.js';
11
11
  export { BUILTIN_REGISTRY, cardUrlPatterns, findRecognizer } from './registry.js';
12
12
  export type { Recognizer, CheckoutMode } from './registry.js';
13
- export type { CheckoutController, CheckoutState, MerchantResult, UserAction, LifecycleOptions, PaymentEndpointGuard } from './lifecycle.js';
13
+ export type { CheckoutController, CheckoutState, MerchantResult, MerchantPaymentConfirmation, UserAction, LifecycleOptions, PaymentEndpointGuard } from './lifecycle.js';
@@ -1,9 +1,19 @@
1
1
  import { CheckoutPreparationError, type PrepareCheckoutOptions, type PreparedCheckout, type ReplayResponse } from './client.js';
2
2
  import type { CheckoutMode } from './registry.js';
3
- /** A processor approval is not an order. Only the merchant can confirm this result. */
3
+ /** The application's resolver confirmed the payment for this checkout authorization. */
4
+ export interface MerchantPaymentConfirmation {
5
+ kind: 'merchant_payment';
6
+ authorizationId: string;
7
+ }
8
+ /** Only authoritative merchant evidence can confirm completion; a processor token is insufficient. */
4
9
  export type MerchantResult = {
5
10
  status: 'completed';
6
11
  orderId: string;
12
+ confirmation?: never;
13
+ } | {
14
+ status: 'completed';
15
+ confirmation: MerchantPaymentConfirmation;
16
+ orderId?: never;
7
17
  } | {
8
18
  status: 'failed';
9
19
  } | {
@@ -18,6 +28,7 @@ export interface CheckoutState {
18
28
  preparationId?: string;
19
29
  mode?: CheckoutMode;
20
30
  orderId?: string;
31
+ confirmation?: MerchantPaymentConfirmation;
21
32
  /** Stable SDK category; never includes a request body, processor response, or approval link. */
22
33
  reason?: string;
23
34
  }
@@ -30,7 +41,7 @@ export interface UserAction {
30
41
  export interface LifecycleOptions {
31
42
  onStateChange?: (state: Readonly<CheckoutState>) => void;
32
43
  onUserAction?: (action: UserAction) => void | Promise<void>;
33
- /** Read authoritative merchant order state; do not click Pay or initiate a new charge here. */
44
+ /** Read authoritative merchant payment or order state; do not click Pay or initiate a new charge here. */
34
45
  resolveMerchantResult?: (state: Readonly<CheckoutState>) => Promise<MerchantResult>;
35
46
  /** Hold further card requests after handoff until the merchant result is reconciled. Default false for compatibility. */
36
47
  requireMerchantResult?: boolean;
package/dist/lifecycle.js CHANGED
@@ -17,7 +17,9 @@ export class CheckoutLifecycle {
17
17
  constructor(options) {
18
18
  this.options = options;
19
19
  }
20
- getState() { return { ...this.state }; }
20
+ getState() {
21
+ return { ...this.state, ...(this.state.confirmation ? { confirmation: { ...this.state.confirmation } } : {}) };
22
+ }
21
23
  setPreparationHandler(handler) { this.preparationHandler = handler; }
22
24
  prepare(options) {
23
25
  if (!this.preparationHandler)
@@ -137,7 +139,12 @@ export class CheckoutLifecycle {
137
139
  return;
138
140
  }
139
141
  const authorizationId = error instanceof PaymentOutcomeUnknownError || error instanceof ProcessorRefusedError ? error.authorizationId : this.state.authorizationId;
140
- if (handoffStarted || error instanceof PaymentOutcomeUnknownError || error instanceof IntentNotConfirmableError) {
142
+ if (!handoffStarted && error instanceof PaymentOutcomeUnknownError && error.reason === 'autopilot_action_required') {
143
+ this.held = true;
144
+ this.set({ ...this.state, authorizationId, status: 'requires_user_action', reason: 'other' });
145
+ this.notify({ reason: 'other', authorizationId });
146
+ }
147
+ else if (handoffStarted || error instanceof PaymentOutcomeUnknownError || error instanceof IntentNotConfirmableError) {
141
148
  this.held = true;
142
149
  this.set({ ...this.state, authorizationId, status: 'outcome_unknown', reason: error instanceof PaymentOutcomeUnknownError ? error.reason : error instanceof IntentNotConfirmableError ? 'intent_not_confirmable' : 'browser_handoff_failed' });
143
150
  }
@@ -195,14 +202,28 @@ export class CheckoutLifecycle {
195
202
  catch {
196
203
  result = { status: 'unknown' };
197
204
  }
198
- if (!result || typeof result !== 'object')
205
+ if (!result || typeof result !== 'object' || Array.isArray(result))
199
206
  result = { status: 'unknown' };
200
- if (result.status === 'completed' && typeof result.orderId === 'string' && result.orderId.length > 0) {
207
+ // The resolver is trusted application code: it must check the original
208
+ // request, amount, currency, selected card and authoritative merchant
209
+ // result. The SDK binds its explicit confirmation to this authorization;
210
+ // it does not turn an HTTP response or a success URL into payment proof.
211
+ const orderConfirmed = result.status === 'completed' && result.confirmation === undefined
212
+ && typeof result.orderId === 'string' && result.orderId.length > 0;
213
+ const paymentConfirmed = result.status === 'completed' && result.orderId === undefined
214
+ && result.confirmation && typeof result.confirmation === 'object' && !Array.isArray(result.confirmation)
215
+ && result.confirmation.kind === 'merchant_payment'
216
+ && typeof this.state.authorizationId === 'string' && this.state.authorizationId.length > 0
217
+ && result.confirmation.authorizationId === this.state.authorizationId;
218
+ if (result.status === 'completed' && (orderConfirmed || paymentConfirmed)) {
201
219
  this.held = true;
202
- // Merchant completion supersedes the earlier failure or user-action
203
- // reason; keep the authorization and preparation context for observers.
204
- const { reason: _previousReason, ...context } = this.state;
205
- this.set({ ...context, status: 'completed', orderId: result.orderId });
220
+ // Completion supersedes an earlier failure/challenge, while retaining
221
+ // preparation and authorization context. Never copy arbitrary evidence
222
+ // into public state or release the hold on additional payment requests.
223
+ const { reason: _previousReason, orderId: _previousOrder, confirmation: _previousConfirmation, ...context } = this.state;
224
+ this.set({ ...context, status: 'completed', ...(orderConfirmed
225
+ ? { orderId: result.orderId }
226
+ : { confirmation: { kind: 'merchant_payment', authorizationId: this.state.authorizationId } }) });
206
227
  }
207
228
  else if (result.status === 'failed') {
208
229
  // Keep the guard armed until the application deliberately starts another attempt.