@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 +14 -0
- package/PREFLIGHT.md +12 -5
- package/README.md +58 -10
- package/dist/client.js +20 -1
- package/dist/index.d.ts +1 -1
- package/dist/lifecycle.d.ts +13 -2
- package/dist/lifecycle.js +29 -8
- package/dist/preflight-catalog.json +533 -8
- package/dist/preflight-playwright.js +143 -50
- package/dist/preflight-schemas.json +48 -7
- package/dist/preflight.generated.d.ts +11 -1
- package/dist/preflight.generated.js +82 -8
- package/dist/stripe-checkout.d.ts +6 -2
- package/dist/stripe-checkout.generated.d.ts +25 -1
- package/dist/stripe-checkout.generated.js +80 -31
- package/dist/stripe-checkout.js +16 -9
- package/examples/preflight/kernel-native/inventory.json +2 -2
- package/examples/preflight/kernel-profile.empty.json +1 -1
- package/examples/preflight/mollie-hosted.result.json +2 -2
- package/examples/preflight/stripe-script.direct.result.json +2 -2
- package/examples/preflight/stripe-script.result.json +2 -2
- package/package.json +2 -2
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-
|
|
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` |
|
|
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'
|
|
370
|
+
if (state.status === 'completed') await finishAgentTask(state);
|
|
371
371
|
```
|
|
372
372
|
|
|
373
|
-
`resolveMerchantResult` must
|
|
374
|
-
|
|
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
|
|
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`.
|
|
405
|
-
`request_validation_failed`
|
|
406
|
-
|
|
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
|
-
|
|
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';
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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() {
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
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.
|