@agent-cards/checkout 0.17.0 → 0.18.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 +8 -0
- package/README.md +15 -7
- package/dist/builtin-registry.generated.js +1 -1
- package/dist/card-fields.generated.d.ts +3 -0
- package/dist/card-fields.generated.js +46 -0
- package/dist/cdp.d.ts +2 -0
- package/dist/cdp.js +158 -3
- package/dist/client.d.ts +49 -0
- package/dist/client.js +77 -2
- package/dist/lifecycle.d.ts +23 -0
- package/dist/lifecycle.js +49 -2
- package/dist/preflight-capabilities.generated.d.ts +1 -1
- package/dist/preflight-capabilities.generated.js +1 -1
- package/dist/preflight-catalog.json +1 -1
- package/dist/preflight.generated.d.ts +1 -1
- package/dist/preparation.d.ts +6 -0
- package/dist/preparation.js +13 -3
- package/dist/registry.d.ts +15 -0
- package/dist/stripe-checkout.generated.js +45 -14
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Pay a Stripe Checkout Elements page with a vaulted card. A page built with `stripe.initCheckout` (Checkout Elements) sends the card inline in the Checkout Session confirm, `/v1/payment_pages/{cs_...}/confirm`, which the SDK never recognized, so the placeholder card reached Stripe and the purchase failed. The confirm now pauses for the cardholder's approval like a PaymentIntent confirm, and the phone sends the real card. The API refuses it unless the amount you pass equals the total the Session will charge (`expected_amount`). Hosted and embedded Checkout confirm the same endpoint with the payment method they created first; that confirm carries no card and continues untouched, as before, even after the approval. `syncRegistry()` asks for `features=checkout_sessions`: the API serves the Checkout Session endpoint only to an SDK that lets such a request through ahead of its holds.
|
|
6
|
+
|
|
7
|
+
- Let a page pay with the Stripe card token the cardholder just approved. A page that tokenizes first (`stripe.createPaymentMethod`, `createToken`, `createSource`, `createConfirmationToken`) and then confirms the payment in the browser with what came back (`confirmCardPayment` with `payment_method: 'pm_...'`, `confirmPayment` with the confirmation token) used to fail: the SDK held that card-free confirmation, because the token alone could pay any amount. The SDK now asks the API (`VaultClient.checkStripeContinuation`), which reads the payment from Stripe and allows it only for the approved amount and currency, on the same Stripe account, paid with exactly the approved token. Allowed, the page's own request continues untouched, the SDK emits `stripe_payment_continued`, and the controller reads `awaiting_merchant` with reason `stripe_payment_continued`. One confirmation continues per approval; later ones stay held, and a refused one is `blocked` with the API's code (at most three checks per approval). The matching API release is required; an older API refuses, which leaves the hold as it was.
|
|
8
|
+
|
|
9
|
+
- Pause Stripe confirmation tokens and card sources. A page that calls `stripe.createConfirmationToken` (collect the card, confirm on your own server) or the older `stripe.createSource` sends the card to `/v1/confirmation_tokens` or `/v1/sources`; the SDK never recognized either, so the placeholder card reached Stripe and the purchase failed. Both now pause for the cardholder's approval, and the page receives a real confirmation token or card source. A confirmation token for a saved method, or a bank source, carries no card and continues untouched. The matching API serves these endpoints only to an SDK that asks for `features=card_fields`, which this release does. They are not eligible for auto-approval yet.
|
|
10
|
+
|
|
11
|
+
- Refuse a Stripe confirmation that carries no card at once. A page that pays with a method it created itself confirms with `payment_method=pm_...`; the SDK used to pause that request and ask the cardholder to approve something their card could not complete. The SDK now reads the body after its holds: a Stripe request without `card[number]` or `payment_method_data[card][number]` is refused at once with a `blocked` event (`request_without_card`) and never reaches the API. A confirmation that follows an approved card token keeps its existing hold. `VaultClient.withoutCard(url, body)` exposes the judgement, and `syncRegistry()` asks for `features=card_fields` (an older API ignores it). The matching API refuses a Stripe request with no card as `400 card_fields_missing`.
|
|
12
|
+
|
|
5
13
|
- Approve a Paysafe Checkout 1.8.0 fresh-card checkout before Pay with `prepare({ psp: 'paysafe', environment: 'production' | 'sandbox' })`. Phone approval finishes before the hosted checkout starts its card request. One matching request consumes the approval; saved cards and other Paysafe APIs do not use preparation. Matching API, Vault and database releases are required. A token does not confirm merchant payment.
|
|
6
14
|
|
|
7
15
|
- Keep the cardholder's approval when the merchant page gives up on its own card request. A page whose script times out its tokenization while the person is still deciding (Braintree after 60 seconds, Square after about 10) no longer cancels the approval: the SDK reports `merchant_request_lost`, answers the page's next request for the same purchase from the same authorization, and reads `ready_to_submit` with reason `awaiting_merchant_retry` when the approval lands before the page asks again, so one more Pay click completes the purchase with no second prompt. The wait for that request is bounded by `merchantRetryWaitMs` (two minutes by default, never past the approval window); an approval the page never asks for again is retired through the API as `merchant_never_retried` and the controller reads `declined` with that reason. Applies to `token` and `cse` checkouts; hosted forms, prepared checkouts and native Stripe Checkout keep cancelling. `VaultClient.cancelAuthorization` takes the reason and `checkoutModeOf` reads a request's mode. The matching API and Vault releases are required for the new reason; an older API answers its retirement as unknown, which the SDK holds.
|
package/README.md
CHANGED
|
@@ -458,14 +458,22 @@ the original request's claim. Do not cancel that checkout in response to the dup
|
|
|
458
458
|
authorization before another attempt. Neither disposition proves a payment outcome.
|
|
459
459
|
|
|
460
460
|
With `requireMerchantResult`, subsequent card requests stay blocked after
|
|
461
|
-
handoff. Stripe `/v1/payment_methods
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
intent, changed amounts and retries. It reports `awaiting_merchant` with reason
|
|
461
|
+
handoff. Stripe tokenization handoffs (`/v1/payment_methods`, `/v1/tokens`,
|
|
462
|
+
`/v1/sources`, `/v1/confirmation_tokens`) always hold further recognized card
|
|
463
|
+
requests, even when that option is false. A tokenization approval has no
|
|
464
|
+
authoritative binding to a specific PaymentIntent, amount or currency, and the
|
|
465
|
+
page cannot supply one. The SDK reports `awaiting_merchant` with reason
|
|
467
466
|
`stripe_tokenization_unbound`, while ordinary browser traffic stays available.
|
|
468
|
-
|
|
467
|
+
|
|
468
|
+
One follow-up can continue: the page's own card-free PaymentIntent confirm that
|
|
469
|
+
pays with the approved token (`confirmCardPayment` with `payment_method: 'pm_...'`,
|
|
470
|
+
`confirmPayment` with the confirmation token). The SDK asks the API, which reads
|
|
471
|
+
the payment from Stripe and allows it only for the approved amount and
|
|
472
|
+
currency, on the same Stripe account, paid with exactly the approved token.
|
|
473
|
+
Allowed, the request continues untouched, the SDK emits `stripe_payment_continued`
|
|
474
|
+
and the reason becomes `stripe_payment_continued`. One confirm continues per
|
|
475
|
+
approval; every other follow-up, including an unrelated intent, a changed
|
|
476
|
+
amount and a retry, stays blocked.
|
|
469
477
|
Unrecognized merchant-server endpoints remain outside this guard unless listed
|
|
470
478
|
in `paymentEndpoints`; this is not a guarantee against a merchant charging a
|
|
471
479
|
saved token on its own server.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const BUILTIN_REGISTRY = [{ "psp": "spreedly", "match": /core\.spreedly\.com\/v1\/payment_methods\/restricted\.json/i, "hosts": ["^core\\.spreedly\\.com$"], "encoding": "json", "passthroughHeaders": [/^spreedly-environment-key$/i] }, { "psp": "shopify", "match": /(checkout\.pci\.shopifyinc\.com|deposit\.[a-z0-9-]+\.shopifycs\.com)\/sessions/i, "hosts": ["^checkout\\.pci\\.shopifyinc\\.com$", "^deposit\\.[a-z0-9-]+\\.shopifycs\\.com$"], "encoding": "json", "passthroughHeaders": [/^shopify-identification-signature$/i] }, { "psp": "stripe", "match": /api\.stripe\.com\/v1\/(payment_methods|tokens|setup_intents\/seti_\w+\/confirm|payment_intents\/pi_\w+\/confirm)/i, "hosts": ["^api\\.stripe\\.com$"], "encoding": "form", "passthroughHeaders": [/^authorization$/i, /^stripe-version$/i, /^x-stripe-client-user-agent$/i] }, { "psp": "braintree", "match": /payments(\.sandbox)?\.braintree-api\.com\/graphql/i, "hosts": ["^payments\\.braintree-api\\.com$", "^payments\\.sandbox\\.braintree-api\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^braintree-version$/i] }, { "psp": "checkout_com", "match": /(api|card-acquisition-gateway)(\.sandbox)?\.checkout\.com\/tokens/i, "hosts": ["^api\\.checkout\\.com$", "^api\\.sandbox\\.checkout\\.com$", "^card-acquisition-gateway\\.checkout\\.com$", "^card-acquisition-gateway\\.sandbox\\.checkout\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i] }, { "psp": "adyen", "match": /(checkoutshopper-(test|live(-[a-z]+)?)\.adyen\.com|([a-z0-9-]+\.)*adyenpayments\.com)\/checkoutshopper\/v1\/sessions\/[A-Za-z0-9_-]+\/payments/i, "hosts": ["^checkoutshopper-(test|live(-[a-z]+)?)\\.adyen\\.com$", "^([a-z0-9-]+\\.)*adyenpayments\\.com$"], "encoding": "json", "passthroughHeaders": [], "clientSideEncrypted": true, "mode": "cse" }, { "psp": "tranzila", "match": /(direct\.tranzila\.com\/process\/n\/|directng\.tranzila\.com\/process\/?)/i, "hosts": ["^direct\\.tranzila\\.com$", "^directng\\.tranzila\\.com$"], "encoding": "form", "passthroughHeaders": [], "mode": "hosted_form" }, { "psp": "square", "match": /pci-connect\.squareup(?:sandbox)?\.com\/v2\/card-nonce/i, "hosts": ["^pci-connect\\.squareup\\.com$", "^pci-connect\\.squareupsandbox\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "authorize_net", "match": /(api2|apitest)\.authorize\.net\/xml\/v1\/request\.api/i, "hosts": ["^api2\\.authorize\\.net$", "^apitest\\.authorize\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "worldpay", "match": /(try\.)?access\.worldpay\.com\/(sessions\/card|verifiedTokens\/sessions)/i, "hosts": ["^access\\.worldpay\\.com$", "^try\\.access\\.worldpay\\.com$"], "encoding": "json", "passthroughHeaders": [/^accept$/i] }, { "psp": "nuvei", "match": /(secure|ppp-test)\.safecharge\.com\/ppp\/api\/v1\/(?:cardTokenization|clientPayment|websdk\/initPaymentWithCardTokenization)\.do/i, "hosts": ["^secure\\.safecharge\\.com$", "^ppp-test\\.safecharge\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "airwallex", "match": /(checkout(\.sandbox)?\.airwallex\.com|pci-api(\.sandbox)?\.airwallex\.com)\/api\/v1\/pa\/(payment_intents\/[^/?#]+\/confirm|payment_consents\/[^/?#]+\/verify|payment_methods\/create)(\?|$)/i, "hosts": ["^checkout\\.airwallex\\.com$", "^checkout\\.sandbox\\.airwallex\\.com$", "^pci-api\\.airwallex\\.com$", "^pci-api\\.sandbox\\.airwallex\\.com$"], "encoding": "json", "passthroughHeaders": [/^client-secret$/i, /^authorization$/i, /^x-auth-token$/i, /^x-on-behalf-of$/i, /^x-api-version$/i] }, { "psp": "rapyd", "match": /(api|sandboxapi)\.rapyd\.net\/v1\/(?:hosted\/collect\/card\/[^/?#]+\/payment_method|checkout\/checkout_[A-Za-z0-9]+)(?:[?#]|$)/i, "hosts": ["^api\\.rapyd\\.net$", "^sandboxapi\\.rapyd\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "dlocal", "match": /ppmcc(-sandbox)?\.dlocal\.com\/cvault\/credit-card\/temporal/i, "hosts": ["^ppmcc\\.dlocal\\.com$", "^ppmcc-sandbox\\.dlocal\\.com$"], "encoding": "json", "passthroughHeaders": [/^x-fields-api-key$/i, /^x-uow$/i, /^x-dlocal-infrav2$/i] }, { "psp": "ebanx", "match": /(?:(?:customer-frontier|customer|api-local-latam|api|sandbox-local-latam|sandbox)\.ebanx\.com|(?:api-diamond|api|sandbox)\.ebanxpay\.com)\/ws\/token/i, "hosts": ["^customer\\.ebanx\\.com$", "^customer-frontier\\.ebanx\\.com$", "^api\\.ebanx\\.com$", "^sandbox\\.ebanx\\.com$", "^api-local-latam\\.ebanx\\.com$", "^sandbox-local-latam\\.ebanx\\.com$", "^api-diamond\\.ebanxpay\\.com$", "^api\\.ebanxpay\\.com$", "^sandbox\\.ebanxpay\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "mercado_pago", "match": /api\.mercadopago\.com\/v1\/card_tokens(?![\w/-])/i, "hosts": ["^api\\.mercadopago\\.com$"], "encoding": "json", "passthroughHeaders": [/^x-product-id$/i] }, { "psp": "payu", "match": /(secure\.payu\.com|(merch-prod|secure)\.snd\.payu\.com)\/api\/front\/tokens/i, "hosts": ["^secure\\.payu\\.com$", "^(merch-prod|secure)\\.snd\\.payu\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i] }, { "psp": "razorpay", "match": /api\.razorpay\.com\/v1\/(?:standard_checkout\/)?payments\/create\/(?:ajax|checkout|fees)/i, "hosts": ["^api\\.razorpay\\.com$"], "encoding": "form", "passthroughHeaders": [/^x-razorpay-sessionid$/i, /^x-customer-access-token$/i] }, { "psp": "mollie", "match": /api\.cc\.mollie\.com\/v1\/card-tokens/i, "hosts": ["^api\\.cc\\.mollie\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "paysafe", "match": /(?:api(\.test)?\.paysafe\.com\/(?:paymenthub\/v1\/singleusepaymenthandles|js\/api\/v1\/tokenize)|hosted(\.test)?\.paysafe\.com\/checkout\/api\/v1\/tokenize)(?![\w/-])/i, "hosts": ["^api\\.paysafe\\.com$", "^api\\.test\\.paysafe\\.com$", "^hosted\\.paysafe\\.com$", "^hosted\\.test\\.paysafe\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^x-paysafe-credentials$/i, /^correlationid$/i] }, { "psp": "recurly", "match": /api(\.eu)?\.recurly\.com\/js\/v1\/token(?![\w-])/i, "hosts": ["^api\\.recurly\\.com$", "^api\\.eu\\.recurly\\.com$"], "encoding": "form", "passthroughHeaders": [/^recurly-credential-checkout-hostname$/i] }, { "psp": "moneris", "match": /(www3|esqa|gateway|gatewayqa|gatewayt|gatewaydev)\.moneris\.com\/HPPtoken\/request\.php/i, "hosts": ["^www3\\.moneris\\.com$", "^esqa\\.moneris\\.com$", "^gateway\\.moneris\\.com$", "^gatewayqa\\.moneris\\.com$", "^gatewayt\\.moneris\\.com$", "^gatewaydev\\.moneris\\.com$"], "encoding": "form", "passthroughHeaders": [] }, { "psp": "bambora", "match": /(api\.na\.bambora\.com|api\.bam\.shift4api\.net)\/scripts\/tokenization\/tokens/i, "hosts": ["^api\\.na\\.bambora\\.com$", "^api\\.bam\\.shift4api\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "global_payments", "match": /(?:api\.heartlandportico\.com\/SecureSubmit\.v1\/api\/token|cert\.api2\.heartlandportico\.com\/Hps\.Exchange\.PosGateway\.Hpf\.v1\/api\/token|apis(?:\.sandbox)?(?:\.eu)?\.globalpay\.com\/ucp\/(?:merchants\/[^/]+\/)?payment-methods)/i, "hosts": ["^api\\.heartlandportico\\.com$", "^cert\\.api2\\.heartlandportico\\.com$", "^apis\\.globalpay\\.com$", "^apis\\.sandbox\\.globalpay\\.com$", "^apis\\.eu\\.globalpay\\.com$", "^apis\\.sandbox\\.eu\\.globalpay\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^x-gp-version$/i] }];
|
|
1
|
+
export const BUILTIN_REGISTRY = [{ "psp": "spreedly", "match": /core\.spreedly\.com\/v1\/payment_methods\/restricted\.json/i, "hosts": ["^core\\.spreedly\\.com$"], "encoding": "json", "passthroughHeaders": [/^spreedly-environment-key$/i] }, { "psp": "shopify", "match": /(checkout\.pci\.shopifyinc\.com|deposit\.[a-z0-9-]+\.shopifycs\.com)\/sessions/i, "hosts": ["^checkout\\.pci\\.shopifyinc\\.com$", "^deposit\\.[a-z0-9-]+\\.shopifycs\\.com$"], "encoding": "json", "passthroughHeaders": [/^shopify-identification-signature$/i] }, { "psp": "stripe", "match": /api\.stripe\.com\/v1\/(payment_methods|tokens|sources|confirmation_tokens|setup_intents\/seti_\w+\/confirm|payment_intents\/pi_\w+\/confirm|payment_pages\/cs_(?:test|live)_\w+\/confirm)/i, "hosts": ["^api\\.stripe\\.com$"], "encoding": "form", "passthroughHeaders": [/^authorization$/i, /^stripe-version$/i, /^x-stripe-client-user-agent$/i], "cardFields": ["card[number]", "payment_method_data[card][number]"], "passWithoutCard": "api\\.stripe\\.com/v1/(sources|confirmation_tokens|payment_pages/cs_(?:test|live)_\\w+/confirm)" }, { "psp": "braintree", "match": /payments(\.sandbox)?\.braintree-api\.com\/graphql/i, "hosts": ["^payments\\.braintree-api\\.com$", "^payments\\.sandbox\\.braintree-api\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^braintree-version$/i] }, { "psp": "checkout_com", "match": /(api|card-acquisition-gateway)(\.sandbox)?\.checkout\.com\/tokens/i, "hosts": ["^api\\.checkout\\.com$", "^api\\.sandbox\\.checkout\\.com$", "^card-acquisition-gateway\\.checkout\\.com$", "^card-acquisition-gateway\\.sandbox\\.checkout\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i] }, { "psp": "adyen", "match": /(checkoutshopper-(test|live(-[a-z]+)?)\.adyen\.com|([a-z0-9-]+\.)*adyenpayments\.com)\/checkoutshopper\/v1\/sessions\/[A-Za-z0-9_-]+\/payments/i, "hosts": ["^checkoutshopper-(test|live(-[a-z]+)?)\\.adyen\\.com$", "^([a-z0-9-]+\\.)*adyenpayments\\.com$"], "encoding": "json", "passthroughHeaders": [], "clientSideEncrypted": true, "mode": "cse" }, { "psp": "tranzila", "match": /(direct\.tranzila\.com\/process\/n\/|directng\.tranzila\.com\/process\/?)/i, "hosts": ["^direct\\.tranzila\\.com$", "^directng\\.tranzila\\.com$"], "encoding": "form", "passthroughHeaders": [], "mode": "hosted_form" }, { "psp": "square", "match": /pci-connect\.squareup(?:sandbox)?\.com\/v2\/card-nonce/i, "hosts": ["^pci-connect\\.squareup\\.com$", "^pci-connect\\.squareupsandbox\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "authorize_net", "match": /(api2|apitest)\.authorize\.net\/xml\/v1\/request\.api/i, "hosts": ["^api2\\.authorize\\.net$", "^apitest\\.authorize\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "worldpay", "match": /(try\.)?access\.worldpay\.com\/(sessions\/card|verifiedTokens\/sessions)/i, "hosts": ["^access\\.worldpay\\.com$", "^try\\.access\\.worldpay\\.com$"], "encoding": "json", "passthroughHeaders": [/^accept$/i] }, { "psp": "nuvei", "match": /(secure|ppp-test)\.safecharge\.com\/ppp\/api\/v1\/(?:cardTokenization|clientPayment|websdk\/initPaymentWithCardTokenization)\.do/i, "hosts": ["^secure\\.safecharge\\.com$", "^ppp-test\\.safecharge\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "airwallex", "match": /(checkout(\.sandbox)?\.airwallex\.com|pci-api(\.sandbox)?\.airwallex\.com)\/api\/v1\/pa\/(payment_intents\/[^/?#]+\/confirm|payment_consents\/[^/?#]+\/verify|payment_methods\/create)(\?|$)/i, "hosts": ["^checkout\\.airwallex\\.com$", "^checkout\\.sandbox\\.airwallex\\.com$", "^pci-api\\.airwallex\\.com$", "^pci-api\\.sandbox\\.airwallex\\.com$"], "encoding": "json", "passthroughHeaders": [/^client-secret$/i, /^authorization$/i, /^x-auth-token$/i, /^x-on-behalf-of$/i, /^x-api-version$/i] }, { "psp": "rapyd", "match": /(api|sandboxapi)\.rapyd\.net\/v1\/(?:hosted\/collect\/card\/[^/?#]+\/payment_method|checkout\/checkout_[A-Za-z0-9]+)(?:[?#]|$)/i, "hosts": ["^api\\.rapyd\\.net$", "^sandboxapi\\.rapyd\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "dlocal", "match": /ppmcc(-sandbox)?\.dlocal\.com\/cvault\/credit-card\/temporal/i, "hosts": ["^ppmcc\\.dlocal\\.com$", "^ppmcc-sandbox\\.dlocal\\.com$"], "encoding": "json", "passthroughHeaders": [/^x-fields-api-key$/i, /^x-uow$/i, /^x-dlocal-infrav2$/i] }, { "psp": "ebanx", "match": /(?:(?:customer-frontier|customer|api-local-latam|api|sandbox-local-latam|sandbox)\.ebanx\.com|(?:api-diamond|api|sandbox)\.ebanxpay\.com)\/ws\/token/i, "hosts": ["^customer\\.ebanx\\.com$", "^customer-frontier\\.ebanx\\.com$", "^api\\.ebanx\\.com$", "^sandbox\\.ebanx\\.com$", "^api-local-latam\\.ebanx\\.com$", "^sandbox-local-latam\\.ebanx\\.com$", "^api-diamond\\.ebanxpay\\.com$", "^api\\.ebanxpay\\.com$", "^sandbox\\.ebanxpay\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "mercado_pago", "match": /api\.mercadopago\.com\/v1\/card_tokens(?![\w/-])/i, "hosts": ["^api\\.mercadopago\\.com$"], "encoding": "json", "passthroughHeaders": [/^x-product-id$/i] }, { "psp": "payu", "match": /(secure\.payu\.com|(merch-prod|secure)\.snd\.payu\.com)\/api\/front\/tokens/i, "hosts": ["^secure\\.payu\\.com$", "^(merch-prod|secure)\\.snd\\.payu\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i] }, { "psp": "razorpay", "match": /api\.razorpay\.com\/v1\/(?:standard_checkout\/)?payments\/create\/(?:ajax|checkout|fees)/i, "hosts": ["^api\\.razorpay\\.com$"], "encoding": "form", "passthroughHeaders": [/^x-razorpay-sessionid$/i, /^x-customer-access-token$/i] }, { "psp": "mollie", "match": /api\.cc\.mollie\.com\/v1\/card-tokens/i, "hosts": ["^api\\.cc\\.mollie\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "paysafe", "match": /(?:api(\.test)?\.paysafe\.com\/(?:paymenthub\/v1\/singleusepaymenthandles|js\/api\/v1\/tokenize)|hosted(\.test)?\.paysafe\.com\/checkout\/api\/v1\/tokenize)(?![\w/-])/i, "hosts": ["^api\\.paysafe\\.com$", "^api\\.test\\.paysafe\\.com$", "^hosted\\.paysafe\\.com$", "^hosted\\.test\\.paysafe\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^x-paysafe-credentials$/i, /^correlationid$/i] }, { "psp": "recurly", "match": /api(\.eu)?\.recurly\.com\/js\/v1\/token(?![\w-])/i, "hosts": ["^api\\.recurly\\.com$", "^api\\.eu\\.recurly\\.com$"], "encoding": "form", "passthroughHeaders": [/^recurly-credential-checkout-hostname$/i] }, { "psp": "moneris", "match": /(www3|esqa|gateway|gatewayqa|gatewayt|gatewaydev)\.moneris\.com\/HPPtoken\/request\.php/i, "hosts": ["^www3\\.moneris\\.com$", "^esqa\\.moneris\\.com$", "^gateway\\.moneris\\.com$", "^gatewayqa\\.moneris\\.com$", "^gatewayt\\.moneris\\.com$", "^gatewaydev\\.moneris\\.com$"], "encoding": "form", "passthroughHeaders": [] }, { "psp": "bambora", "match": /(api\.na\.bambora\.com|api\.bam\.shift4api\.net)\/scripts\/tokenization\/tokens/i, "hosts": ["^api\\.na\\.bambora\\.com$", "^api\\.bam\\.shift4api\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "global_payments", "match": /(?:api\.heartlandportico\.com\/SecureSubmit\.v1\/api\/token|cert\.api2\.heartlandportico\.com\/Hps\.Exchange\.PosGateway\.Hpf\.v1\/api\/token|apis(?:\.sandbox)?(?:\.eu)?\.globalpay\.com\/ucp\/(?:merchants\/[^/]+\/)?payment-methods)/i, "hosts": ["^api\\.heartlandportico\\.com$", "^cert\\.api2\\.heartlandportico\\.com$", "^apis\\.globalpay\\.com$", "^apis\\.sandbox\\.globalpay\\.com$", "^apis\\.eu\\.globalpay\\.com$", "^apis\\.sandbox\\.eu\\.globalpay\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^x-gp-version$/i] }];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
+
// artifact-sha256: ac6967df3f29912d12c310418d06b6583529d5a4d5346c362ef0a7987fd4f2ab
|
|
4
|
+
// src/card-fields.js
|
|
5
|
+
function carriesCardFields(recognizer, body) {
|
|
6
|
+
const fields = recognizer && recognizer.cardFields;
|
|
7
|
+
if (!Array.isArray(fields) || fields.length === 0)
|
|
8
|
+
return true;
|
|
9
|
+
if (recognizer.encoding !== "form")
|
|
10
|
+
return true;
|
|
11
|
+
if (typeof body !== "string" || body === "")
|
|
12
|
+
return false;
|
|
13
|
+
for (const chunk of body.split("&")) {
|
|
14
|
+
const eq = chunk.indexOf("=");
|
|
15
|
+
if (eq <= 0 || eq === chunk.length - 1)
|
|
16
|
+
continue;
|
|
17
|
+
let key;
|
|
18
|
+
try {
|
|
19
|
+
key = decodeURIComponent(chunk.slice(0, eq).replace(/\+/g, " "));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (fields.includes(key))
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function requestWithoutCard(recognizer, url, body) {
|
|
30
|
+
if (carriesCardFields(recognizer, body))
|
|
31
|
+
return null;
|
|
32
|
+
const pass = recognizer && recognizer.passWithoutCard;
|
|
33
|
+
if (typeof pass === "string" && pass !== "") {
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = new URL(url);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return "refuse";
|
|
40
|
+
}
|
|
41
|
+
if (new RegExp(`^(?:${pass})$`, "i").test(`${parsed.hostname.toLowerCase()}${parsed.pathname}`))
|
|
42
|
+
return "continue";
|
|
43
|
+
}
|
|
44
|
+
return "refuse";
|
|
45
|
+
}
|
|
46
|
+
export { carriesCardFields, requestWithoutCard };
|
package/dist/cdp.d.ts
CHANGED
|
@@ -109,6 +109,8 @@ export interface AttachOptions extends LifecycleOptions, ExecutionMetadata {
|
|
|
109
109
|
vault: VaultClient;
|
|
110
110
|
user: string;
|
|
111
111
|
merchant: string;
|
|
112
|
+
/** Return the caller's `Date.now()` at the pay click; the SDK reads it when the card request pauses. */
|
|
113
|
+
payClickedAt?: () => number | null | undefined;
|
|
112
114
|
/**
|
|
113
115
|
* Your hint at the amount, an integer in the currency's smallest unit or a
|
|
114
116
|
* decimal string in normal units, with its ISO 4217 code. See
|
package/dist/cdp.js
CHANGED
|
@@ -298,6 +298,17 @@ export function withCorsHeaders(headers, cors) {
|
|
|
298
298
|
function headerEntries(headers) {
|
|
299
299
|
return Object.entries(headers).map(([name, value]) => ({ name, value: String(value) }));
|
|
300
300
|
}
|
|
301
|
+
function readPayToInterceptMs(readClickedAt, approvalStartedAt) {
|
|
302
|
+
try {
|
|
303
|
+
const clickedAt = readClickedAt?.();
|
|
304
|
+
return typeof clickedAt === 'number' && Number.isFinite(clickedAt) && clickedAt <= approvalStartedAt
|
|
305
|
+
? Math.round(approvalStartedAt - clickedAt)
|
|
306
|
+
: undefined;
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
301
312
|
/**
|
|
302
313
|
* The origin of the top-level document the payment form is on, read when a
|
|
303
314
|
* card request pauses: the fact that names the merchant for the company's
|
|
@@ -406,6 +417,54 @@ function failureSummary(error) {
|
|
|
406
417
|
return `${error.name}: ${error.code ?? `http_${error.status}`}`;
|
|
407
418
|
return error instanceof Error ? error.name : 'CheckoutError';
|
|
408
419
|
}
|
|
420
|
+
const STRIPE_PAYMENT_INTENT_CONFIRM = /^\/v1\/payment_intents\/pi_[A-Za-z0-9]+\/confirm$/;
|
|
421
|
+
/**
|
|
422
|
+
* A page that tokenized the card through an approval, then confirms the
|
|
423
|
+
* payment in the browser with the token it got back (confirmCardPayment with
|
|
424
|
+
* payment_method: 'pm_...', confirmPayment with a confirmation token). That
|
|
425
|
+
* confirmation carries no card, and the hold after a Stripe tokenization
|
|
426
|
+
* would refuse it. The API decides instead whether it is the payment the
|
|
427
|
+
* cardholder approved (the approved amount and currency, on the same Stripe
|
|
428
|
+
* account, paid with exactly the approved token).
|
|
429
|
+
*
|
|
430
|
+
* Synchronous on purpose: every paused request passes through here, and an
|
|
431
|
+
* await would reorder it against the attachment's own events. Returns the
|
|
432
|
+
* approval to check, with the lifecycle's one continuation claimed, or null
|
|
433
|
+
* when this is not such a confirmation and the ordinary rules apply.
|
|
434
|
+
*/
|
|
435
|
+
function claimStripeContinuation(opts, lifecycle, url, method, body) {
|
|
436
|
+
if (body == null || typeof opts.vault.checkStripeContinuation !== 'function' || !lifecycle.isBlocked()
|
|
437
|
+
|| method.toUpperCase() !== 'POST')
|
|
438
|
+
return null;
|
|
439
|
+
let parsed;
|
|
440
|
+
try {
|
|
441
|
+
parsed = new URL(url);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
if (parsed.origin !== 'https://api.stripe.com' || !STRIPE_PAYMENT_INTENT_CONFIRM.test(parsed.pathname)
|
|
447
|
+
|| opts.vault.withoutCard?.(url, body) !== 'refuse')
|
|
448
|
+
return null;
|
|
449
|
+
return lifecycle.claimStripeContinuation();
|
|
450
|
+
}
|
|
451
|
+
/** Ask the API about a claimed continuation and settle the claim. True: continue the page's request untouched. */
|
|
452
|
+
async function checkStripeContinuation(opts, lifecycle, authorizationId, request) {
|
|
453
|
+
let paymentIntentId = null;
|
|
454
|
+
try {
|
|
455
|
+
paymentIntentId = (await opts.vault.checkStripeContinuation(authorizationId, request)).paymentIntentId;
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
opts.onEvent?.({ type: 'blocked', detail: failureSummary(error) });
|
|
459
|
+
}
|
|
460
|
+
// The checkout may have been cancelled or reconciled while the API answered.
|
|
461
|
+
const proceed = lifecycle.stripeContinuationChecked(paymentIntentId !== null);
|
|
462
|
+
if (proceed)
|
|
463
|
+
opts.onEvent?.({ type: 'stripe_payment_continued', detail: { authorizationId, paymentIntentId } });
|
|
464
|
+
else if (paymentIntentId !== null)
|
|
465
|
+
opts.onEvent?.({ type: 'blocked', detail: 'stripe_continuation_superseded' });
|
|
466
|
+
return proceed;
|
|
467
|
+
}
|
|
409
468
|
/**
|
|
410
469
|
* How long an approval waits, after the cardholder gives it, for the page to
|
|
411
470
|
* issue the request it will answer.
|
|
@@ -1346,14 +1405,62 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1346
1405
|
return;
|
|
1347
1406
|
}
|
|
1348
1407
|
}
|
|
1408
|
+
// A request the catalog marks as routine without a card (a confirmation
|
|
1409
|
+
// token for a saved method, a bank source, a Checkout Session confirm with
|
|
1410
|
+
// the method hosted Checkout created) was never ours: it continues in
|
|
1411
|
+
// every state, like a request the catalog does not recognize. One without
|
|
1412
|
+
// a card anywhere else is judged after the holds below.
|
|
1413
|
+
const continuationBody = pausedBody(request);
|
|
1414
|
+
const withoutCard = stripeStep ? null : opts.vault.withoutCard?.(request.url, continuationBody) ?? null;
|
|
1415
|
+
if (withoutCard === 'continue') {
|
|
1416
|
+
if (preparation)
|
|
1417
|
+
preparationGate.retireUnboundClaim();
|
|
1418
|
+
await cdp.send('Fetch.continueRequest', { requestId }, sessionId).catch(() => { });
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
// Not gated on awaitingApproval: the page can send this confirmation while
|
|
1422
|
+
// the token's own delivery is still being acknowledged. The lifecycle
|
|
1423
|
+
// opens the claim only once a Stripe tokenization is being handed over.
|
|
1424
|
+
const continuationId = stripeStep || terminal ? null
|
|
1425
|
+
: claimStripeContinuation(opts, lifecycle, request.url, request.method, continuationBody);
|
|
1426
|
+
if (continuationId) {
|
|
1427
|
+
if (preparation)
|
|
1428
|
+
preparationGate.retireUnboundClaim();
|
|
1429
|
+
if (await checkStripeContinuation(opts, lifecycle, continuationId, { url: request.url, method: request.method, headers: request.headers, body: continuationBody }) && !setupStop.signal.aborted) {
|
|
1430
|
+
await cdp.send('Fetch.continueRequest', { requestId }, sessionId).catch(() => { });
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: 'Aborted' }, sessionId).catch(() => { });
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1349
1436
|
// Same stop condition as the Playwright adapter: once a failure proves
|
|
1350
1437
|
// retrying is pointless, fail the request without calling the API again.
|
|
1351
1438
|
if (terminal || lifecycle.isBlocked() || awaitingApproval || Date.now() < quietUntil) {
|
|
1439
|
+
let repeatedAuthorizationId = null;
|
|
1440
|
+
if (lastSubmitted) {
|
|
1441
|
+
const body = pausedBody(request);
|
|
1442
|
+
if (body !== null && isRepeatOfSubmitted(lastSubmitted, request.url, body, repeatQuietMs)) {
|
|
1443
|
+
repeatedAuthorizationId = lastSubmitted.authorizationId;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1352
1446
|
const why = terminal ?? (lifecycle.isBlocked() ? lifecycle.getState().status : awaitingApproval ? 'an approval is already outstanding' : 'awaiting approval cooldown');
|
|
1353
1447
|
opts.onEvent?.({ type: 'blocked', detail: why instanceof Error ? failureSummary(why) : String(why) });
|
|
1354
1448
|
if (preparation)
|
|
1355
1449
|
preparationGate.retireUnboundClaim();
|
|
1356
1450
|
await cdp.send('Fetch.failRequest', { requestId, errorReason: 'Aborted' }, sessionId).catch(() => { });
|
|
1451
|
+
if (repeatedAuthorizationId)
|
|
1452
|
+
void opts.vault.reportDuplicateGuard?.(repeatedAuthorizationId);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
// A recognized request that carries no card is not a card request: a
|
|
1456
|
+
// Stripe confirmation paying with a method the page created itself. It is
|
|
1457
|
+
// judged only after the holds above, so a request that would reuse an
|
|
1458
|
+
// approved token is still refused there (card-fields.js requestWithoutCard).
|
|
1459
|
+
if (withoutCard === 'refuse') {
|
|
1460
|
+
if (preparation)
|
|
1461
|
+
preparationGate.retireUnboundClaim();
|
|
1462
|
+
opts.onEvent?.({ type: 'blocked', detail: 'request_without_card' });
|
|
1463
|
+
await cdp.send('Fetch.failRequest', { requestId, errorReason: 'Aborted' }, sessionId).catch(() => { });
|
|
1357
1464
|
return;
|
|
1358
1465
|
}
|
|
1359
1466
|
// Reserve BEFORE the first await (the authorize call below yields), so two
|
|
@@ -1381,11 +1488,14 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1381
1488
|
if (isRepeatOfSubmitted(lastSubmitted, request.url, body, repeatQuietMs)) {
|
|
1382
1489
|
opts.onEvent?.({ type: 'blocked', detail: HOSTED_FORM_REPEAT_REASON });
|
|
1383
1490
|
await cdp.send('Fetch.failRequest', { requestId, errorReason: 'Aborted' }, sessionId).catch(() => { });
|
|
1491
|
+
if (lastSubmitted)
|
|
1492
|
+
void opts.vault.reportDuplicateGuard?.(lastSubmitted.authorizationId);
|
|
1384
1493
|
return;
|
|
1385
1494
|
}
|
|
1386
1495
|
opts.onEvent?.({ type: 'card_request_paused', detail: { url: redactUrl(request.url), ...(resourceType ? { resourceType } : {}) } });
|
|
1387
1496
|
lifecycle.begin();
|
|
1388
1497
|
const approvalStartedAt = Date.now();
|
|
1498
|
+
const payToInterceptMs = readPayToInterceptMs(opts.payClickedAt, approvalStartedAt);
|
|
1389
1499
|
if (typeof networkId !== 'string' || !networkId) {
|
|
1390
1500
|
attempt.stop();
|
|
1391
1501
|
attempt.assertLive();
|
|
@@ -1422,6 +1532,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1422
1532
|
merchantOrigin,
|
|
1423
1533
|
pageOrigin,
|
|
1424
1534
|
pageAmount,
|
|
1535
|
+
...(payToInterceptMs !== undefined ? { payToInterceptMs } : {}),
|
|
1425
1536
|
preparation,
|
|
1426
1537
|
timeoutMs: opts.timeoutMs,
|
|
1427
1538
|
signal: lifecycle.abort.signal,
|
|
@@ -1480,7 +1591,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
1480
1591
|
body: Buffer.from(page.body).toString('base64'),
|
|
1481
1592
|
}, deliverOn);
|
|
1482
1593
|
attempt.assertLive();
|
|
1483
|
-
lastSubmitted = { url: deliverRequest.url, body: deliverBody, at: Date.now() };
|
|
1594
|
+
lastSubmitted = { url: deliverRequest.url, body: deliverBody, at: Date.now(), authorizationId: replay.authorizationId };
|
|
1484
1595
|
// Named for what it is: a device-attested submission with no
|
|
1485
1596
|
// processor evidence, never an `authorized` event.
|
|
1486
1597
|
opts.onEvent?.({ type: 'submitted_on_device', detail: { authorizationId: replay.authorizationId, submittedAt: replay.submittedAt, outcome: replay.outcome } });
|
|
@@ -1833,16 +1944,55 @@ export async function attachToPlaywright(page, opts) {
|
|
|
1833
1944
|
return;
|
|
1834
1945
|
}
|
|
1835
1946
|
}
|
|
1947
|
+
// Same order as attachToCdp: a routine card-free request continues in every state.
|
|
1948
|
+
const continuationBody = request.postData();
|
|
1949
|
+
const withoutCard = stripeStep ? null : opts.vault.withoutCard?.(request.url(), continuationBody) ?? null;
|
|
1950
|
+
if (withoutCard === 'continue') {
|
|
1951
|
+
if (preparation)
|
|
1952
|
+
preparationGate.retireUnboundClaim();
|
|
1953
|
+
return route.fallback();
|
|
1954
|
+
}
|
|
1955
|
+
const continuationId = stripeStep || terminal ? null
|
|
1956
|
+
: claimStripeContinuation(opts, lifecycle, request.url(), request.method(), continuationBody);
|
|
1957
|
+
if (continuationId) {
|
|
1958
|
+
if (preparation)
|
|
1959
|
+
preparationGate.retireUnboundClaim();
|
|
1960
|
+
if (await checkStripeContinuation(opts, lifecycle, continuationId, { url: request.url(), method: request.method(), headers: request.headers(), body: continuationBody }) && !setupStop.signal.aborted) {
|
|
1961
|
+
return route.fallback();
|
|
1962
|
+
}
|
|
1963
|
+
return route.abort('aborted');
|
|
1964
|
+
}
|
|
1836
1965
|
// Fail closed and stay quiet: no card may reach the PSP, but neither may
|
|
1837
1966
|
// the page's retry loop turn into a stream of doomed API calls. Every
|
|
1838
1967
|
// abort in this adapter is 'aborted' (ERR_ABORTED), the same code the
|
|
1839
1968
|
// CDP adapter's Fetch.failRequest uses, so a refused navigation
|
|
1840
1969
|
// resolves identically whichever adapter is attached.
|
|
1841
1970
|
if (terminal || lifecycle.isBlocked() || awaitingApproval || Date.now() < quietUntil) {
|
|
1971
|
+
let repeatedAuthorizationId = null;
|
|
1972
|
+
if (lastSubmitted) {
|
|
1973
|
+
let body = null;
|
|
1974
|
+
try {
|
|
1975
|
+
body = request.postData() ?? '';
|
|
1976
|
+
}
|
|
1977
|
+
catch { /* The refusal still holds when the body cannot be read. */ }
|
|
1978
|
+
if (body !== null && isRepeatOfSubmitted(lastSubmitted, request.url(), body, repeatQuietMs)) {
|
|
1979
|
+
repeatedAuthorizationId = lastSubmitted.authorizationId;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1842
1982
|
const why = terminal ?? (lifecycle.isBlocked() ? lifecycle.getState().status : awaitingApproval ? 'an approval is already outstanding' : 'awaiting approval cooldown');
|
|
1843
1983
|
opts.onEvent?.({ type: 'blocked', detail: why instanceof Error ? failureSummary(why) : String(why) });
|
|
1844
1984
|
if (preparation)
|
|
1845
1985
|
preparationGate.retireUnboundClaim();
|
|
1986
|
+
const refused = await route.abort('aborted');
|
|
1987
|
+
if (repeatedAuthorizationId)
|
|
1988
|
+
void opts.vault.reportDuplicateGuard?.(repeatedAuthorizationId);
|
|
1989
|
+
return refused;
|
|
1990
|
+
}
|
|
1991
|
+
// Same judgement as attachToCdp, after the same holds.
|
|
1992
|
+
if (withoutCard === 'refuse') {
|
|
1993
|
+
if (preparation)
|
|
1994
|
+
preparationGate.retireUnboundClaim();
|
|
1995
|
+
opts.onEvent?.({ type: 'blocked', detail: 'request_without_card' });
|
|
1846
1996
|
return route.abort('aborted');
|
|
1847
1997
|
}
|
|
1848
1998
|
// Reserved before anything that could yield, matching attachToCdp.
|
|
@@ -1879,11 +2029,15 @@ export async function attachToPlaywright(page, opts) {
|
|
|
1879
2029
|
const body = request.postData() ?? '';
|
|
1880
2030
|
if (isRepeatOfSubmitted(lastSubmitted, request.url(), body, repeatQuietMs)) {
|
|
1881
2031
|
opts.onEvent?.({ type: 'blocked', detail: HOSTED_FORM_REPEAT_REASON });
|
|
1882
|
-
|
|
2032
|
+
const refused = await route.abort('aborted');
|
|
2033
|
+
if (lastSubmitted)
|
|
2034
|
+
void opts.vault.reportDuplicateGuard?.(lastSubmitted.authorizationId);
|
|
2035
|
+
return refused;
|
|
1883
2036
|
}
|
|
1884
2037
|
opts.onEvent?.({ type: 'card_request_paused', detail: { url: redactUrl(request.url()) } });
|
|
1885
2038
|
lifecycle.begin();
|
|
1886
2039
|
const approvalStartedAt = Date.now();
|
|
2040
|
+
const payToInterceptMs = readPayToInterceptMs(opts.payClickedAt, approvalStartedAt);
|
|
1887
2041
|
attempt.bind({ route, request, frames });
|
|
1888
2042
|
activeRequest = { request, frames, attempt,
|
|
1889
2043
|
identity: requestIdentity(opts, request.url(), request.method(), body, undefined, undefined) };
|
|
@@ -1914,6 +2068,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
1914
2068
|
merchantOrigin,
|
|
1915
2069
|
pageOrigin,
|
|
1916
2070
|
pageAmount,
|
|
2071
|
+
...(payToInterceptMs !== undefined ? { payToInterceptMs } : {}),
|
|
1917
2072
|
preparation,
|
|
1918
2073
|
timeoutMs: opts.timeoutMs,
|
|
1919
2074
|
signal: lifecycle.abort.signal,
|
|
@@ -1957,7 +2112,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
1957
2112
|
// Inert on a navigation (never CORS-checked); one path for every fulfill.
|
|
1958
2113
|
await deliverTo.fulfill({ status: synthetic.status, headers: withCorsHeaders(synthetic.headers, corsHeadersFor(deliverRequest.url(), deliverRequest.headers())), body: synthetic.body });
|
|
1959
2114
|
assertRequestLive();
|
|
1960
|
-
lastSubmitted = { url: deliverRequest.url(), body: deliverBody, at: Date.now() };
|
|
2115
|
+
lastSubmitted = { url: deliverRequest.url(), body: deliverBody, at: Date.now(), authorizationId: replay.authorizationId };
|
|
1961
2116
|
opts.onEvent?.({ type: 'submitted_on_device', detail: { authorizationId: replay.authorizationId, submittedAt: replay.submittedAt, outcome: replay.outcome } });
|
|
1962
2117
|
}
|
|
1963
2118
|
else if (replay.mode === 'cse') {
|
package/dist/client.d.ts
CHANGED
|
@@ -15,6 +15,16 @@ export interface PausedRequest {
|
|
|
15
15
|
* is never paused) and sent on every create.
|
|
16
16
|
*/
|
|
17
17
|
export declare const SUPPORTED_MODES: readonly CheckoutMode[];
|
|
18
|
+
/**
|
|
19
|
+
* Registry features this SDK honours, asked for on syncRegistry next to the
|
|
20
|
+
* modes. `card_fields`: it reads a recognizer's cardFields and claims only a
|
|
21
|
+
* request whose body carries the card, so the API may serve it recognizers
|
|
22
|
+
* whose endpoints also run without a card. `checkout_sessions`: it lets a
|
|
23
|
+
* request the recognizer marks as routine without a card (passWithoutCard)
|
|
24
|
+
* through ahead of its holds, so the API may serve Stripe's Checkout Session
|
|
25
|
+
* confirm, which hosted Checkout sends after an approval.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SUPPORTED_REGISTRY_FEATURES: readonly string[];
|
|
18
28
|
/**
|
|
19
29
|
* What the amount on an authorization IS: held to a Stripe PaymentIntent
|
|
20
30
|
* (read back at create and before the replay), the parked form's own sum
|
|
@@ -199,6 +209,8 @@ export interface AuthorizeInput extends ExecutionMetadata {
|
|
|
199
209
|
amount: number;
|
|
200
210
|
currency: string;
|
|
201
211
|
};
|
|
212
|
+
/** Milliseconds from the caller's pay click until the SDK caught the card request, measured on one clock. */
|
|
213
|
+
payToInterceptMs?: number;
|
|
202
214
|
/**
|
|
203
215
|
* WHICH stored card should pay — a vault card id from
|
|
204
216
|
* GET /api/v2/vault_cards. The approval page preselects it (the human can
|
|
@@ -459,6 +471,11 @@ export interface VaultClientOptions {
|
|
|
459
471
|
/** Override the PSP registry (tests, or pinning). Defaults to the hosted list. */
|
|
460
472
|
registry?: Recognizer[];
|
|
461
473
|
fetchImpl?: typeof fetch;
|
|
474
|
+
/** Receives contained reporting failures that do not change checkout behavior. */
|
|
475
|
+
onEvent?: (event: {
|
|
476
|
+
type: string;
|
|
477
|
+
detail?: unknown;
|
|
478
|
+
}) => void;
|
|
462
479
|
pollIntervalMs?: number;
|
|
463
480
|
/**
|
|
464
481
|
* Waits before retrying a create the API answered 502 `amount_unverifiable`
|
|
@@ -482,6 +499,16 @@ export declare class VaultClient {
|
|
|
482
499
|
syncRegistry(): Promise<void>;
|
|
483
500
|
/** True when this request is a card tokenization we can take over. */
|
|
484
501
|
isCardRequest(url: string, method?: string): boolean;
|
|
502
|
+
/**
|
|
503
|
+
* What happens to a card request (by URL) whose body carries no card: null
|
|
504
|
+
* when it carries one, so it is a card request as usual; 'continue' when the
|
|
505
|
+
* recognizer marks the endpoint as one where such requests are routine and
|
|
506
|
+
* never ours; 'refuse' otherwise, such as a Stripe confirmation paying with
|
|
507
|
+
* a method this checkout never approved. The adapters ask only after their
|
|
508
|
+
* holds, so a request that would reuse an approved token is refused there
|
|
509
|
+
* first. A body that could not be read is not judged here.
|
|
510
|
+
*/
|
|
511
|
+
withoutCard(url: string, body: string | null | undefined): 'continue' | 'refuse' | null;
|
|
485
512
|
/**
|
|
486
513
|
* How the card would reach the processor on this request (`token`, `cse`
|
|
487
514
|
* or `hosted_form`; absent on the entry means `token`), or null when the
|
|
@@ -506,6 +533,8 @@ export declare class VaultClient {
|
|
|
506
533
|
prepareCheckout(input: PrepareCheckoutInput): Promise<PreparedCheckout>;
|
|
507
534
|
/** Cancel only an unconsumed preparation; a bound request is reconciled separately. */
|
|
508
535
|
cancelPreparation(id: string): Promise<void>;
|
|
536
|
+
observePreparation(id: string, guidance: 'presented_not_filled' | 'not_presented', reason?: string): Promise<void>;
|
|
537
|
+
reportDuplicateGuard(authorizationId: string): Promise<void>;
|
|
509
538
|
/**
|
|
510
539
|
* Hand us a paused tokenization request. We ask the cardholder to approve,
|
|
511
540
|
* their device supplies the card and calls the merchant, and you get back the
|
|
@@ -539,6 +568,26 @@ export declare class VaultClient {
|
|
|
539
568
|
cancelled: true;
|
|
540
569
|
processor_request_started: boolean;
|
|
541
570
|
}>;
|
|
571
|
+
/**
|
|
572
|
+
* Ask whether the page may pay with the Stripe card token an approval
|
|
573
|
+
* produced: a PaymentIntent confirm that carries no card and pays with
|
|
574
|
+
* exactly the approved payment method, card token, confirmation token or
|
|
575
|
+
* source. The API reads the payment from Stripe and answers only for the
|
|
576
|
+
* approved amount and currency on the same Stripe account; any other answer
|
|
577
|
+
* rejects with a CheckoutApiError whose code says why (for example
|
|
578
|
+
* `continuation_not_bound`, `amount_mismatch`). Nothing is charged here:
|
|
579
|
+
* the adapters continue the page's own request once this resolves.
|
|
580
|
+
*/
|
|
581
|
+
checkStripeContinuation(authorizationId: string, request: {
|
|
582
|
+
url: string;
|
|
583
|
+
method: string;
|
|
584
|
+
headers: Record<string, string>;
|
|
585
|
+
body: string;
|
|
586
|
+
}): Promise<{
|
|
587
|
+
paymentIntentId: string;
|
|
588
|
+
amount: number;
|
|
589
|
+
currency: string;
|
|
590
|
+
}>;
|
|
542
591
|
/**
|
|
543
592
|
* POST the create, with two typed twists: a 502 `amount_unverifiable`
|
|
544
593
|
* (Stripe did not answer the read-back) is retried on a short backoff
|
package/dist/client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { BUILTIN_REGISTRY, cardUrlPatterns as deriveCardUrlPatterns, findRecogni
|
|
|
2
2
|
import { isMercadoTokenRequest, parseMercadoCheckoutContext, validateMercadoProcessorContext } from './mercado-checkout.generated.js';
|
|
3
3
|
import { matchesPreparedRequest, preparationMode, validPreparationEnvironment } from './prepared-processor.js';
|
|
4
4
|
import { hasOwnedShopMarker, parseOwnedShopOrder, parseOwnedShopReceipt } from './owned-shop.generated.js';
|
|
5
|
+
import { requestWithoutCard } from './card-fields.generated.js';
|
|
5
6
|
import { classifyStripeCheckoutRequest, encodeStripeCheckoutContext, hasStripeCheckoutMarker, parseStripeCheckoutContext, parseStripeCheckoutResponse, STRIPE_CHECKOUT_CONTEXT_HEADER, } from './stripe-checkout.generated.js';
|
|
6
7
|
/**
|
|
7
8
|
* The modes this SDK can finish. Asked for on syncRegistry (the API serves
|
|
@@ -9,6 +10,16 @@ import { classifyStripeCheckoutRequest, encodeStripeCheckoutContext, hasStripeCh
|
|
|
9
10
|
* is never paused) and sent on every create.
|
|
10
11
|
*/
|
|
11
12
|
export const SUPPORTED_MODES = ['token', 'cse', 'hosted_form'];
|
|
13
|
+
/**
|
|
14
|
+
* Registry features this SDK honours, asked for on syncRegistry next to the
|
|
15
|
+
* modes. `card_fields`: it reads a recognizer's cardFields and claims only a
|
|
16
|
+
* request whose body carries the card, so the API may serve it recognizers
|
|
17
|
+
* whose endpoints also run without a card. `checkout_sessions`: it lets a
|
|
18
|
+
* request the recognizer marks as routine without a card (passWithoutCard)
|
|
19
|
+
* through ahead of its holds, so the API may serve Stripe's Checkout Session
|
|
20
|
+
* confirm, which hosted Checkout sends after an approval.
|
|
21
|
+
*/
|
|
22
|
+
export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
|
|
12
23
|
/** An integer in the smallest unit, or a decimal string in normal units with a point; nothing else. */
|
|
13
24
|
export function validAmountInput(amount) {
|
|
14
25
|
if (typeof amount === 'number')
|
|
@@ -395,7 +406,7 @@ export class VaultClient {
|
|
|
395
406
|
// spread verbatim.
|
|
396
407
|
let raw;
|
|
397
408
|
try {
|
|
398
|
-
raw = await this.get(`/v2/checkout/recognizers?modes=${SUPPORTED_MODES.join(',')}`);
|
|
409
|
+
raw = await this.get(`/v2/checkout/recognizers?modes=${SUPPORTED_MODES.join(',')}&features=${SUPPORTED_REGISTRY_FEATURES.join(',')}`);
|
|
399
410
|
}
|
|
400
411
|
catch {
|
|
401
412
|
return;
|
|
@@ -412,6 +423,21 @@ export class VaultClient {
|
|
|
412
423
|
isCardRequest(url, method = 'POST') {
|
|
413
424
|
return method.toUpperCase() === 'POST' && findRecognizer(url, this.registry) !== null;
|
|
414
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* What happens to a card request (by URL) whose body carries no card: null
|
|
428
|
+
* when it carries one, so it is a card request as usual; 'continue' when the
|
|
429
|
+
* recognizer marks the endpoint as one where such requests are routine and
|
|
430
|
+
* never ours; 'refuse' otherwise, such as a Stripe confirmation paying with
|
|
431
|
+
* a method this checkout never approved. The adapters ask only after their
|
|
432
|
+
* holds, so a request that would reuse an approved token is refused there
|
|
433
|
+
* first. A body that could not be read is not judged here.
|
|
434
|
+
*/
|
|
435
|
+
withoutCard(url, body) {
|
|
436
|
+
if (body == null)
|
|
437
|
+
return null;
|
|
438
|
+
const rec = findRecognizer(url, this.registry);
|
|
439
|
+
return rec ? requestWithoutCard(rec, url, body) : null;
|
|
440
|
+
}
|
|
415
441
|
/**
|
|
416
442
|
* How the card would reach the processor on this request (`token`, `cse`
|
|
417
443
|
* or `hosted_form`; absent on the entry means `token`), or null when the
|
|
@@ -534,6 +560,26 @@ export class VaultClient {
|
|
|
534
560
|
if (state?.id !== id || !['cancelled', 'expired'].includes(state.status))
|
|
535
561
|
throw new CheckoutPreparationError(id, 'cancel_unconfirmed');
|
|
536
562
|
}
|
|
563
|
+
async observePreparation(id, guidance, reason) {
|
|
564
|
+
try {
|
|
565
|
+
await this.post(`/v2/checkout/preparations/${id}/observe`, {
|
|
566
|
+
form_guidance: guidance, ...(reason ? { reason } : {}),
|
|
567
|
+
}, AbortSignal.timeout(5_000));
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
this.opts.onEvent?.({ type: 'telemetry_skipped', detail: 'form_guidance' });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async reportDuplicateGuard(authorizationId) {
|
|
574
|
+
try {
|
|
575
|
+
await this.post(`/v2/checkout/authorizations/${authorizationId}/duplicate-guard`, {
|
|
576
|
+
guard: 'hosted_form_repeat',
|
|
577
|
+
}, AbortSignal.timeout(5_000));
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
this.opts.onEvent?.({ type: 'telemetry_skipped', detail: 'duplicate_guard' });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
537
583
|
/**
|
|
538
584
|
* Hand us a paused tokenization request. We ask the cardholder to approve,
|
|
539
585
|
* their device supplies the card and calls the merchant, and you get back the
|
|
@@ -647,6 +693,9 @@ export class VaultClient {
|
|
|
647
693
|
// snake_case on the wire; camelCase is this SDK's convention.
|
|
648
694
|
...(hasAmount ? { amount: input.amount, currency: input.currency } : {}),
|
|
649
695
|
...(input.pageAmount ? { page_amount: input.pageAmount.amount, page_currency: input.pageAmount.currency } : {}),
|
|
696
|
+
...(Number.isInteger(input.payToInterceptMs) && input.payToInterceptMs >= 0
|
|
697
|
+
? { pay_to_intercept_ms: input.payToInterceptMs }
|
|
698
|
+
: {}),
|
|
650
699
|
psp: rec.psp,
|
|
651
700
|
// The mode this request will be finished in. The API checks it against
|
|
652
701
|
// the recognizer and refuses a disagreement before a row exists.
|
|
@@ -657,7 +706,7 @@ export class VaultClient {
|
|
|
657
706
|
...(input.merchantOrigin && !preparation ? { merchant_origin: input.merchantOrigin } : {}),
|
|
658
707
|
// A prepared checkout already carries the page origin as merchant_origin.
|
|
659
708
|
...(preparation
|
|
660
|
-
? { preparation_id: preparation.id, checkout_key: preparation.checkoutKey, merchant_origin: preparation.merchantOrigin }
|
|
709
|
+
? { preparation_id: preparation.id, checkout_key: preparation.checkoutKey, merchant_origin: preparation.merchantOrigin, form_guidance: 'filled' }
|
|
661
710
|
: input.pageOrigin ? { checkout_origin: input.pageOrigin } : {}),
|
|
662
711
|
request: {
|
|
663
712
|
url: input.request.url,
|
|
@@ -996,6 +1045,32 @@ export class VaultClient {
|
|
|
996
1045
|
return { id: authorizationId, status: 'declined', reason: recorded,
|
|
997
1046
|
cancelled: true, processor_request_started: result.processor_request_started };
|
|
998
1047
|
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Ask whether the page may pay with the Stripe card token an approval
|
|
1050
|
+
* produced: a PaymentIntent confirm that carries no card and pays with
|
|
1051
|
+
* exactly the approved payment method, card token, confirmation token or
|
|
1052
|
+
* source. The API reads the payment from Stripe and answers only for the
|
|
1053
|
+
* approved amount and currency on the same Stripe account; any other answer
|
|
1054
|
+
* rejects with a CheckoutApiError whose code says why (for example
|
|
1055
|
+
* `continuation_not_bound`, `amount_mismatch`). Nothing is charged here:
|
|
1056
|
+
* the adapters continue the page's own request once this resolves.
|
|
1057
|
+
*/
|
|
1058
|
+
async checkStripeContinuation(authorizationId, request) {
|
|
1059
|
+
if (!/^cauth_[A-Za-z0-9_-]{1,128}$/.test(authorizationId))
|
|
1060
|
+
throw new Error('Invalid authorization ID.');
|
|
1061
|
+
const rec = findRecognizer(request.url, this.registry);
|
|
1062
|
+
if (rec?.psp !== 'stripe')
|
|
1063
|
+
throw new Error('Only a Stripe request can continue an approved Stripe token.');
|
|
1064
|
+
const result = await this.post(`/v2/checkout/authorizations/${authorizationId}/continuations`, {
|
|
1065
|
+
request: { url: request.url, method: request.method, headers: pickHeaders(request.headers, rec.passthroughHeaders), body: request.body },
|
|
1066
|
+
}, AbortSignal.timeout(15_000));
|
|
1067
|
+
const pi = result?.payment_intent;
|
|
1068
|
+
if (result?.object !== 'checkout_continuation' || result.continuation !== 'allowed' || result.authorization !== authorizationId
|
|
1069
|
+
|| !pi || typeof pi.id !== 'string' || !Number.isSafeInteger(pi.amount) || typeof pi.currency !== 'string') {
|
|
1070
|
+
throw new Error('The continuation answer was not recognized.');
|
|
1071
|
+
}
|
|
1072
|
+
return { paymentIntentId: pi.id, amount: pi.amount, currency: pi.currency };
|
|
1073
|
+
}
|
|
999
1074
|
/**
|
|
1000
1075
|
* POST the create, with two typed twists: a 502 `amount_unverifiable`
|
|
1001
1076
|
* (Stripe did not answer the read-back) is retried on a short backoff
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -61,6 +61,8 @@ export interface CheckoutController {
|
|
|
61
61
|
status: 'failed';
|
|
62
62
|
}): void;
|
|
63
63
|
}
|
|
64
|
+
/** How many times one approval's card-free Stripe confirmation may be checked before it is simply held. */
|
|
65
|
+
export declare const MAX_STRIPE_CONTINUATION_CHECKS = 3;
|
|
64
66
|
/** Shared by the raw CDP and Playwright transports. No browser ownership or payment execution lives here. */
|
|
65
67
|
export declare class CheckoutLifecycle implements CheckoutController {
|
|
66
68
|
private readonly options;
|
|
@@ -70,6 +72,9 @@ export declare class CheckoutLifecycle implements CheckoutController {
|
|
|
70
72
|
private cancelled;
|
|
71
73
|
private merchantAborted;
|
|
72
74
|
private unboundStripeToken;
|
|
75
|
+
private stripeTokenPrepared;
|
|
76
|
+
private stripeContinuation;
|
|
77
|
+
private stripeContinuationChecks;
|
|
73
78
|
private ownedShopOrderId;
|
|
74
79
|
private reconciliation;
|
|
75
80
|
private preparationHandler?;
|
|
@@ -121,6 +126,24 @@ export declare class CheckoutLifecycle implements CheckoutController {
|
|
|
121
126
|
merchantNeverRetried(authorizationId: string | null): void;
|
|
122
127
|
unsupported(): void;
|
|
123
128
|
prepareHandoff(replay: ReplayResponse, requestUrl: string): void;
|
|
129
|
+
/**
|
|
130
|
+
* The approval a page's card-free Stripe confirmation may continue, or null.
|
|
131
|
+
* Only once a Stripe tokenization is being handed to the page, and while
|
|
132
|
+
* the checkout still awaits it (the page can send its confirmation before
|
|
133
|
+
* the browser acknowledges the delivery) or the merchant; for one check at
|
|
134
|
+
* a time and at most MAX_STRIPE_CONTINUATION_CHECKS checks. Reserves the check synchronously,
|
|
135
|
+
* so two confirmations can never both continue: the caller settles it with
|
|
136
|
+
* stripeContinuationChecked(). The hold stays on throughout.
|
|
137
|
+
*/
|
|
138
|
+
claimStripeContinuation(): string | null;
|
|
139
|
+
/**
|
|
140
|
+
* Settle a claimed check. True only when the API allowed it and the checkout
|
|
141
|
+
* still awaits the merchant (nothing cancelled, aborted or reconciled it
|
|
142
|
+
* while the API answered): the page's confirmation continues, and no later
|
|
143
|
+
* one will.
|
|
144
|
+
*/
|
|
145
|
+
stripeContinuationChecked(allowed: boolean): boolean;
|
|
146
|
+
private stripeContinuationOpen;
|
|
124
147
|
handedOff(replay: ReplayResponse): void;
|
|
125
148
|
failed(error: unknown, handoffStarted?: boolean): void;
|
|
126
149
|
requestUserAction(reason: '3ds' | 'redirect' | 'other'): Promise<void>;
|
package/dist/lifecycle.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ApprovalDeclinedError, ApprovalTimeoutError, CheckoutCancelledError, CheckoutPreparationError, IntentNotConfirmableError, PaymentOutcomeUnknownError, ProcessorRefusedError } from './client.js';
|
|
2
2
|
import { parseOwnedShopReceipt, OWNED_SHOP_SKU, OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY } from './owned-shop.generated.js';
|
|
3
|
+
/** How many times one approval's card-free Stripe confirmation may be checked before it is simply held. */
|
|
4
|
+
export const MAX_STRIPE_CONTINUATION_CHECKS = 3;
|
|
3
5
|
/** Shared by the raw CDP and Playwright transports. No browser ownership or payment execution lives here. */
|
|
4
6
|
export class CheckoutLifecycle {
|
|
5
7
|
options;
|
|
@@ -9,6 +11,10 @@ export class CheckoutLifecycle {
|
|
|
9
11
|
cancelled = false;
|
|
10
12
|
merchantAborted = false;
|
|
11
13
|
unboundStripeToken = false;
|
|
14
|
+
// The page's own confirmation paying with that token: see claimStripeContinuation.
|
|
15
|
+
stripeTokenPrepared = false;
|
|
16
|
+
stripeContinuation = 'none';
|
|
17
|
+
stripeContinuationChecks = 0;
|
|
12
18
|
ownedShopOrderId = null;
|
|
13
19
|
reconciliation = null;
|
|
14
20
|
preparationHandler;
|
|
@@ -141,16 +147,53 @@ export class CheckoutLifecycle {
|
|
|
141
147
|
if (!replay.mode || replay.mode === 'token') {
|
|
142
148
|
try {
|
|
143
149
|
const url = new URL(requestUrl);
|
|
144
|
-
if (url.origin === 'https://api.stripe.com' && ['/v1/payment_methods', '/v1/tokens'].includes(url.pathname)) {
|
|
150
|
+
if (url.origin === 'https://api.stripe.com' && ['/v1/payment_methods', '/v1/tokens', '/v1/sources', '/v1/confirmation_tokens'].includes(url.pathname)) {
|
|
145
151
|
// Set before delivery: a lost browser acknowledgement must not reset
|
|
146
152
|
// an already-issued token into a retryable authorization.
|
|
147
153
|
this.unboundStripeToken = true;
|
|
154
|
+
this.stripeTokenPrepared = true;
|
|
148
155
|
this.held = true;
|
|
149
156
|
}
|
|
150
157
|
}
|
|
151
158
|
catch { /* unknown URL cannot establish a token binding */ }
|
|
152
159
|
}
|
|
153
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* The approval a page's card-free Stripe confirmation may continue, or null.
|
|
163
|
+
* Only once a Stripe tokenization is being handed to the page, and while
|
|
164
|
+
* the checkout still awaits it (the page can send its confirmation before
|
|
165
|
+
* the browser acknowledges the delivery) or the merchant; for one check at
|
|
166
|
+
* a time and at most MAX_STRIPE_CONTINUATION_CHECKS checks. Reserves the check synchronously,
|
|
167
|
+
* so two confirmations can never both continue: the caller settles it with
|
|
168
|
+
* stripeContinuationChecked(). The hold stays on throughout.
|
|
169
|
+
*/
|
|
170
|
+
claimStripeContinuation() {
|
|
171
|
+
if (!this.stripeContinuationOpen() || this.stripeContinuation !== 'none'
|
|
172
|
+
|| this.stripeContinuationChecks >= MAX_STRIPE_CONTINUATION_CHECKS)
|
|
173
|
+
return null;
|
|
174
|
+
this.stripeContinuation = 'checking';
|
|
175
|
+
this.stripeContinuationChecks++;
|
|
176
|
+
return this.state.authorizationId;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Settle a claimed check. True only when the API allowed it and the checkout
|
|
180
|
+
* still awaits the merchant (nothing cancelled, aborted or reconciled it
|
|
181
|
+
* while the API answered): the page's confirmation continues, and no later
|
|
182
|
+
* one will.
|
|
183
|
+
*/
|
|
184
|
+
stripeContinuationChecked(allowed) {
|
|
185
|
+
if (this.stripeContinuation !== 'checking')
|
|
186
|
+
return false;
|
|
187
|
+
const proceed = allowed && this.stripeContinuationOpen();
|
|
188
|
+
this.stripeContinuation = proceed ? 'continued' : 'none';
|
|
189
|
+
if (proceed)
|
|
190
|
+
this.set({ ...this.state, reason: 'stripe_payment_continued' });
|
|
191
|
+
return proceed;
|
|
192
|
+
}
|
|
193
|
+
stripeContinuationOpen() {
|
|
194
|
+
return this.stripeTokenPrepared && !this.cancelled && !this.merchantAborted && !!this.state.authorizationId
|
|
195
|
+
&& (this.state.status === 'awaiting_approval' || this.state.status === 'awaiting_merchant');
|
|
196
|
+
}
|
|
154
197
|
handedOff(replay) {
|
|
155
198
|
if (this.merchantAborted)
|
|
156
199
|
return;
|
|
@@ -161,9 +204,13 @@ export class CheckoutLifecycle {
|
|
|
161
204
|
}
|
|
162
205
|
const mismatch = (!replay.mode || replay.mode === 'token') && replay.amountVerified === false;
|
|
163
206
|
this.held = !!this.options.requireMerchantResult || replay.mode === 'hosted_form' || mismatch || this.unboundStripeToken;
|
|
207
|
+
if (mismatch)
|
|
208
|
+
this.stripeTokenPrepared = false;
|
|
164
209
|
this.set({ status: 'awaiting_merchant', authorizationId: replay.authorizationId, mode: replay.mode ?? 'token',
|
|
165
210
|
...(this.state.preparationId ? { preparationId: this.state.preparationId } : {}),
|
|
166
|
-
...(mismatch ? { reason: 'charged_amount_mismatch' }
|
|
211
|
+
...(mismatch ? { reason: 'charged_amount_mismatch' }
|
|
212
|
+
: this.stripeContinuation === 'continued' ? { reason: 'stripe_payment_continued' }
|
|
213
|
+
: this.unboundStripeToken ? { reason: 'stripe_tokenization_unbound' } : {}) });
|
|
167
214
|
}
|
|
168
215
|
failed(error, handoffStarted = false) {
|
|
169
216
|
if (this.cancelled)
|
|
@@ -124,7 +124,7 @@ export declare const CHECKOUT_PREFLIGHT_CAPABILITIES: {
|
|
|
124
124
|
readonly requires: "reviewed_final_payment_and_continuation_evidence";
|
|
125
125
|
}, {
|
|
126
126
|
readonly id: "stripe.tokenization";
|
|
127
|
-
readonly path_pattern: "^/v1/(?:payment_methods|tokens)$";
|
|
127
|
+
readonly path_pattern: "^/v1/(?:payment_methods|tokens|sources|confirmation_tokens)$";
|
|
128
128
|
readonly effect: "token_or_method_creation";
|
|
129
129
|
readonly amount_binding: "none";
|
|
130
130
|
readonly psp: "stripe";
|
|
@@ -188,7 +188,7 @@ export const CHECKOUT_PREFLIGHT_CAPABILITIES = {
|
|
|
188
188
|
},
|
|
189
189
|
{
|
|
190
190
|
"id": "stripe.tokenization",
|
|
191
|
-
"path_pattern": "^/v1/(?:payment_methods|tokens)$",
|
|
191
|
+
"path_pattern": "^/v1/(?:payment_methods|tokens|sources|confirmation_tokens)$",
|
|
192
192
|
"effect": "token_or_method_creation",
|
|
193
193
|
"amount_binding": "none",
|
|
194
194
|
"psp": "stripe",
|
|
@@ -2640,7 +2640,7 @@
|
|
|
2640
2640
|
},
|
|
2641
2641
|
{
|
|
2642
2642
|
"id": "stripe.tokenization",
|
|
2643
|
-
"path_pattern": "^/v1/(?:payment_methods|tokens)$",
|
|
2643
|
+
"path_pattern": "^/v1/(?:payment_methods|tokens|sources|confirmation_tokens)$",
|
|
2644
2644
|
"effect": "token_or_method_creation",
|
|
2645
2645
|
"amount_binding": "none",
|
|
2646
2646
|
"psp": "stripe",
|
|
@@ -271,7 +271,7 @@ export declare function getCheckoutPreflightCatalog(): {
|
|
|
271
271
|
readonly requires: "reviewed_final_payment_and_continuation_evidence";
|
|
272
272
|
}, {
|
|
273
273
|
readonly id: "stripe.tokenization";
|
|
274
|
-
readonly path_pattern: "^/v1/(?:payment_methods|tokens)$";
|
|
274
|
+
readonly path_pattern: "^/v1/(?:payment_methods|tokens|sources|confirmation_tokens)$";
|
|
275
275
|
readonly effect: "token_or_method_creation";
|
|
276
276
|
readonly amount_binding: "none";
|
|
277
277
|
readonly psp: "stripe";
|
package/dist/preparation.d.ts
CHANGED
|
@@ -22,4 +22,10 @@ export declare class PreparationGate {
|
|
|
22
22
|
retireUnboundClaim(): void;
|
|
23
23
|
/** A bound native request has its own cancellation/unknown-outcome machinery. */
|
|
24
24
|
invalidate(reason: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Retire the server handle first, then report the form outcome. Neither
|
|
27
|
+
* call waits on the other: a stalled report cannot delay the cancel, and a
|
|
28
|
+
* stalled cancel cannot lose the report.
|
|
29
|
+
*/
|
|
30
|
+
private cancelThenObserve;
|
|
25
31
|
}
|
package/dist/preparation.js
CHANGED
|
@@ -69,7 +69,7 @@ export class PreparationGate {
|
|
|
69
69
|
if (prepared.psp !== options.psp || prepared.environment !== options.environment)
|
|
70
70
|
throw new CheckoutPreparationError(prepared.id, 'checkout_changed');
|
|
71
71
|
if (signal.aborted || this.state !== 'preparing') {
|
|
72
|
-
void this.
|
|
72
|
+
void this.cancelThenObserve(prepared.id, 'cancelled');
|
|
73
73
|
throw new CheckoutPreparationError(prepared.id, 'cancelled');
|
|
74
74
|
}
|
|
75
75
|
await this.assertDocument();
|
|
@@ -135,7 +135,7 @@ export class PreparationGate {
|
|
|
135
135
|
retireUnboundClaim() {
|
|
136
136
|
if (this.state !== 'consumed' || !this.prepared || this.lifecycle.getState().authorizationId)
|
|
137
137
|
return;
|
|
138
|
-
void this.
|
|
138
|
+
void this.cancelThenObserve(this.prepared.id, 'request_not_bound');
|
|
139
139
|
// No bound ID means the adapter must not leave a spent handle appearing ready.
|
|
140
140
|
if (this.lifecycle.getState().status === 'ready_to_submit')
|
|
141
141
|
this.lifecycle.preparationFailed(new CheckoutPreparationError(this.prepared.id, 'request_not_bound'));
|
|
@@ -148,7 +148,17 @@ export class PreparationGate {
|
|
|
148
148
|
clearTimeout(this.expiryTimer);
|
|
149
149
|
this.stop.abort();
|
|
150
150
|
if (this.prepared)
|
|
151
|
-
void this.
|
|
151
|
+
void this.cancelThenObserve(this.prepared.id, reason);
|
|
152
152
|
this.lifecycle.preparationFailed(new CheckoutPreparationError(this.prepared?.id ?? null, reason));
|
|
153
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Retire the server handle first, then report the form outcome. Neither
|
|
156
|
+
* call waits on the other: a stalled report cannot delay the cancel, and a
|
|
157
|
+
* stalled cancel cannot lose the report.
|
|
158
|
+
*/
|
|
159
|
+
async cancelThenObserve(id, reason) {
|
|
160
|
+
const cancellation = this.opts.vault.cancelPreparation(id).catch(() => { });
|
|
161
|
+
const observation = this.opts.vault.observePreparation?.(id, this.observedRequest ? 'presented_not_filled' : 'not_presented', reason);
|
|
162
|
+
await Promise.all([cancellation, observation?.catch(() => { })]);
|
|
163
|
+
}
|
|
154
164
|
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -40,6 +40,21 @@ export interface Recognizer {
|
|
|
40
40
|
* the call (signatures, client tokens). Matched case-insensitively.
|
|
41
41
|
*/
|
|
42
42
|
passthroughHeaders: RegExp[];
|
|
43
|
+
/**
|
|
44
|
+
* Form keys that carry the card number. When present, a request on this
|
|
45
|
+
* recognizer's endpoints is a card request only if its body carries one of
|
|
46
|
+
* them: a Stripe confirmation that pays with a method the page already
|
|
47
|
+
* created (`payment_method=pm_...`) carries none, so there is nothing for
|
|
48
|
+
* the cardholder to approve. Served by the API and carried through
|
|
49
|
+
* syncRegistry() verbatim.
|
|
50
|
+
*/
|
|
51
|
+
cardFields?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* With cardFields: the endpoints (regex source over hostname + pathname,
|
|
54
|
+
* anchored) where a request without a card is routine and never ours, so
|
|
55
|
+
* it continues untouched; a request without a card anywhere else is refused.
|
|
56
|
+
*/
|
|
57
|
+
passWithoutCard?: string;
|
|
43
58
|
/**
|
|
44
59
|
* True when the card is encrypted inside the page before the request leaves.
|
|
45
60
|
* A digit swap is useless here; the vault must re-run the PSP's client-side
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
-
// artifact-sha256:
|
|
3
|
+
// artifact-sha256: 128bbf2359c38eda0d29a58c9b6f83123cf2d27d94365a5665e1569d605e9d2c
|
|
4
4
|
// src/definitions.js
|
|
5
5
|
function deepFreeze(value) {
|
|
6
6
|
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
@@ -28,21 +28,42 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
psp: "stripe",
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// (
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
31
|
+
// Every endpoint Stripe.js sends a card to, read from its own controller
|
|
32
|
+
// bundle: `tokens` (createToken), `payment_methods` (createPaymentMethod),
|
|
33
|
+
// `sources` (createSource, the older API), `confirmation_tokens`
|
|
34
|
+
// (createConfirmationToken: the page collects the card and confirms on its
|
|
35
|
+
// own server), and the intent confirmations that carry the card inline as
|
|
36
|
+
// `payment_method_data[card][number]` (confirmPayment, confirmSetup). The
|
|
37
|
+
// modern Payment Element mostly confirms inline or through a confirmation
|
|
38
|
+
// token, so matching the tokenizers alone never paused it. Observed live on
|
|
39
|
+
// Anthropic's own billing page (platform.claude.com/settings/billing),
|
|
40
|
+
// which posts to /v1/setup_intents/{seti_...}/confirm, the "save a card"
|
|
41
|
+
// flow; /v1/payment_intents/{pi_...}/confirm is the same shape for "pay
|
|
42
|
+
// now". Stripe Checkout Sessions confirm at
|
|
43
|
+
// /v1/payment_pages/{cs_...}/confirm: Checkout Elements (initCheckout)
|
|
44
|
+
// sends the card there inline, while hosted and embedded Checkout send only
|
|
45
|
+
// the payment method they created first. The id segment is matched
|
|
46
|
+
// narrowly (seti_/pi_/cs_ + word chars) rather than `.*` so the pattern
|
|
47
|
+
// cannot be widened by a crafted path. A client that does not check
|
|
48
|
+
// cardFields is served the pattern it knew before sources, confirmation
|
|
49
|
+
// tokens and Checkout Sessions joined (recognition.js recognizersForClient).
|
|
50
|
+
match: "api\\.stripe\\.com/v1/(payment_methods|tokens|sources|confirmation_tokens|setup_intents/seti_\\w+/confirm|payment_intents/pi_\\w+/confirm|payment_pages/cs_(?:test|live)_\\w+/confirm)",
|
|
42
51
|
hosts: ["^api\\.stripe\\.com$"],
|
|
43
52
|
encoding: "form",
|
|
44
53
|
// Elements' own surface markers; without these Stripe rejects the surface.
|
|
45
|
-
passthroughHeaders: ["^authorization$", "^stripe-version$", "^x-stripe-client-user-agent$"]
|
|
54
|
+
passthroughHeaders: ["^authorization$", "^stripe-version$", "^x-stripe-client-user-agent$"],
|
|
55
|
+
// The same confirmations also run WITHOUT a card: a page that created the
|
|
56
|
+
// method first confirms with `payment_method=pm_...`. Only a body that
|
|
57
|
+
// carries one of these fields is a card request (card-fields.js). Any
|
|
58
|
+
// other request on these endpoints has no card to approve, so it is
|
|
59
|
+
// refused (requestWithoutCard) instead of paused for the cardholder,
|
|
60
|
+
// unless passWithoutCard below names its endpoint.
|
|
61
|
+
cardFields: ["card[number]", "payment_method_data[card][number]"],
|
|
62
|
+
// A confirmation token that wraps a saved method, a source for a bank, or a
|
|
63
|
+
// Checkout Session confirm paying with the method hosted or embedded
|
|
64
|
+
// Checkout created first carries no card and was never ours: it continues
|
|
65
|
+
// untouched, as it did before these endpoints were recognized.
|
|
66
|
+
passWithoutCard: "api\\.stripe\\.com/v1/(sources|confirmation_tokens|payment_pages/cs_(?:test|live)_\\w+/confirm)"
|
|
46
67
|
},
|
|
47
68
|
{
|
|
48
69
|
psp: "braintree",
|
|
@@ -498,6 +519,16 @@ function trustedClientRecognizer(pspOrEntry) {
|
|
|
498
519
|
throw new Error("Refusing to pay: unknown payment processor.");
|
|
499
520
|
return definition;
|
|
500
521
|
}
|
|
522
|
+
// src/recognition.js
|
|
523
|
+
var PATTERN_WITHOUT_CARD_FIELDS = Object.freeze({
|
|
524
|
+
stripe: "api\\.stripe\\.com/v1/(payment_methods|tokens|setup_intents/seti_\\w+/confirm|payment_intents/pi_\\w+/confirm)"
|
|
525
|
+
});
|
|
526
|
+
var WITHOUT_CHECKOUT_SESSIONS = Object.freeze({
|
|
527
|
+
stripe: Object.freeze({
|
|
528
|
+
match: "api\\.stripe\\.com/v1/(payment_methods|tokens|sources|confirmation_tokens|setup_intents/seti_\\w+/confirm|payment_intents/pi_\\w+/confirm)",
|
|
529
|
+
passWithoutCard: "api\\.stripe\\.com/v1/(sources|confirmation_tokens)"
|
|
530
|
+
})
|
|
531
|
+
});
|
|
501
532
|
// src/request-policy.js
|
|
502
533
|
var HEADER_NEVER_FORWARD = /^(cookie|set-cookie|authorization2|proxy-authorization|host|content-length|origin|referer)$/i;
|
|
503
534
|
var ALLOWED_MEDIA_TYPES = {
|
|
@@ -591,7 +622,7 @@ function validateStripeCheckoutCard(card, environment = "test") {
|
|
|
591
622
|
reject();
|
|
592
623
|
}
|
|
593
624
|
else {
|
|
594
|
-
if (!/^\d{
|
|
625
|
+
if (!/^\d{12,19}(?![\s\S])/.test(card.number) || /^(\d)\1+(?![\s\S])/.test(card.number) || !/^\d{3,4}(?![\s\S])/.test(card.cvc) || SYNTHETIC_CARDS.includes(card.number))
|
|
595
626
|
reject();
|
|
596
627
|
let sum = 0, double = false;
|
|
597
628
|
for (let i = card.number.length - 1; i >= 0; i--) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"catalog_version": "2026-09-15.1",
|
|
4
|
-
"catalog_sha256": "
|
|
4
|
+
"catalog_sha256": "c605e3f370a2560e15d695e9ce80248f117076e4556f8dc7912ba1e54fb92315",
|
|
5
5
|
"baseline_version": "kernel-native-docs-2026-09-11.1",
|
|
6
6
|
"baseline_sha256": "d86722f49633d8223fccae54cd3d485df4375aee06aa156a273cc689b7c8507c",
|
|
7
7
|
"native_build": null,
|
package/package.json
CHANGED