@agent-cards/checkout 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,915 @@
1
+ // @ts-nocheck
2
+ // Generated from @agent-cards/payment-core. Do not edit.
3
+ // artifact-sha256: 2f7e340ba22bc33539f11e384a3e8362ae3fa510a9f8b540783c02bebcbc1ebf
4
+ // src/definitions.js
5
+ function deepFreeze(value) {
6
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
7
+ for (const child of Object.values(value))
8
+ deepFreeze(child);
9
+ Object.freeze(value);
10
+ }
11
+ return value;
12
+ }
13
+ var PROCESSOR_DEFINITIONS = deepFreeze([
14
+ {
15
+ psp: "shopify",
16
+ match: "(checkout\\.pci\\.shopifyinc\\.com|deposit\\.[a-z0-9-]+\\.shopifycs\\.com)/sessions",
17
+ hosts: ["^checkout\\.pci\\.shopifyinc\\.com$", "^deposit\\.[a-z0-9-]+\\.shopifycs\\.com$"],
18
+ encoding: "json",
19
+ passthroughHeaders: ["^shopify-identification-signature$"]
20
+ },
21
+ {
22
+ psp: "stripe",
23
+ // FOUR PAN-bearing surfaces, not two. `payment_methods` and `tokens` are the
24
+ // LEGACY tokenizers. Modern Payment Element does not call either: it puts the
25
+ // card inline in the intent confirmation as
26
+ // `payment_method_data[card][number]`, so a merchant on the current SDK was
27
+ // never paused at all. Observed live on Anthropic's own billing page
28
+ // (platform.claude.com/settings/billing), which posts to
29
+ // /v1/setup_intents/{seti_...}/confirm — that is the "save a card" flow;
30
+ // /v1/payment_intents/{pi_...}/confirm is the same shape for "pay now".
31
+ // The id segment is matched narrowly (seti_/pi_ + word chars) rather than
32
+ // `.*` so the pattern cannot be widened by a crafted path.
33
+ match: "api\\.stripe\\.com/v1/(payment_methods|tokens|setup_intents/seti_\\w+/confirm|payment_intents/pi_\\w+/confirm)",
34
+ hosts: ["^api\\.stripe\\.com$"],
35
+ encoding: "form",
36
+ // Elements' own surface markers; without these Stripe rejects the surface.
37
+ passthroughHeaders: ["^authorization$", "^stripe-version$", "^x-stripe-client-user-agent$"]
38
+ },
39
+ {
40
+ psp: "braintree",
41
+ match: "payments(\\.sandbox)?\\.braintree-api\\.com/graphql",
42
+ hosts: ["^payments\\.braintree-api\\.com$", "^payments\\.sandbox\\.braintree-api\\.com$"],
43
+ encoding: "json",
44
+ passthroughHeaders: ["^authorization$", "^braintree-version$"]
45
+ },
46
+ {
47
+ psp: "checkout_com",
48
+ // Flow 1.226.0's card controller selects api or card-acquisition-gateway
49
+ // for the same card-token body and Bearer public-key Authorization.
50
+ // Native source: checkout-web-components.checkout.com/pages/1.226.0/index.html.
51
+ // Endpoint recognition does not establish consumer-wallet, CVV-only,
52
+ // reusable-token, 3DS or merchant payment completion compatibility.
53
+ match: "(api|card-acquisition-gateway)(\\.sandbox)?\\.checkout\\.com/tokens",
54
+ hosts: [
55
+ "^api\\.checkout\\.com$",
56
+ "^api\\.sandbox\\.checkout\\.com$",
57
+ "^card-acquisition-gateway\\.checkout\\.com$",
58
+ "^card-acquisition-gateway\\.sandbox\\.checkout\\.com$"
59
+ ],
60
+ encoding: "json",
61
+ passthroughHeaders: ["^authorization$"]
62
+ },
63
+ // NOT here, on purpose: VGS Collect (Very Good Security; Wolt's add-a-card
64
+ // call through `<tenant>.<live|sandbox>.verygoodproxy.com`). Served for a
65
+ // few hours on 2026-09-03 and withdrawn the same night: the proxy aliases
66
+ // only submissions that originate in VGS's own iframe (its Collect session
67
+ // handshake), so a byte-identical replay from the cardholder's device, 4 s
68
+ // after capture, with the iframe's own headers and origin, is forwarded
69
+ // un-aliased and the merchant answers 400 "Failed to process card". A
70
+ // recognizer that pauses a call the vault cannot finish only blocks the
71
+ // agent's browser from adding a card at all, so the entry is gone from
72
+ // every registry; the page keeps its on-file wording machinery for the
73
+ // merchant that does allow it.
74
+ {
75
+ psp: "adyen",
76
+ encryptedCardFields: { number: "encryptedCardNumber", month: "encryptedExpiryMonth", year: "encryptedExpiryYear", cvc: "encryptedSecurityCode" },
77
+ // The Sessions-flow payments call only (adyen-web's makePayment): the
78
+ // card rides in paymentMethod as four JWE blobs the vault page produces
79
+ // under the merchant's Adyen public key (services/checkout-cse-adyen.ts),
80
+ // and the runtime substitutes them into the paused body. binLookup,
81
+ // analytics and paymentDetails share these hosts, carry no card, and must
82
+ // never raise an approval; the hosts test pins them refused. The host
83
+ // alternation covers the regional live hosts (live-us, live-au, live-apse,
84
+ // live-in, live-nea): `checkoutshopper-[a-z]+` never matched those, so
85
+ // merchants on them were silently never paused.
86
+ match: "(checkoutshopper-(test|live(-[a-z]+)?)\\.adyen\\.com|([a-z0-9-]+\\.)*adyenpayments\\.com)/checkoutshopper/v1/sessions/[A-Za-z0-9_-]+/payments",
87
+ hosts: ["^checkoutshopper-(test|live(-[a-z]+)?)\\.adyen\\.com$", "^([a-z0-9-]+\\.)*adyenpayments\\.com$"],
88
+ encoding: "json",
89
+ passthroughHeaders: [],
90
+ clientSideEncrypted: true,
91
+ // kept: older SDKs read this and refuse locally
92
+ mode: "cse",
93
+ cse: {
94
+ at: "paymentMethod",
95
+ fields: ["encryptedCardNumber", "encryptedExpiryMonth", "encryptedExpiryYear", "encryptedSecurityCode"],
96
+ // adyen-web stamps `paymentMethod.brand` from the digits typed into its
97
+ // Card component, i.e. the agent's dummy. Proven on Adyen TEST
98
+ // (2026-09-02): a Mastercard dummy swapped for the vault's Visa
99
+ // ciphertext was refused 422/109 "Invalid variant"; without `brand`
100
+ // Adyen reads it off the decrypted PAN and authorises.
101
+ remove: ["brand"]
102
+ }
103
+ },
104
+ {
105
+ psp: "tranzila",
106
+ // Tranzila's hosted form (direct.tranzila.com/<terminal>/iframenew.php,
107
+ // embedded by the merchant page) submits the card as a top-level form
108
+ // POST to /process/n/ (directng: /process), application/x-www-form-
109
+ // urlencoded, with the card in ccno/expmonth/expyear/mycvv and the
110
+ // Israeli ID number in myid. The paused request is a Document navigation,
111
+ // not an XHR: the runtime cannot resume it with the real card, so the
112
+ // cardholder's device rebuilds and submits the SAME form itself, top-level
113
+ // from the vault origin (mode hosted_form), and the runtime resolves the
114
+ // paused navigation with a synthetic page once the device says the form
115
+ // left. The iframe page itself and Tranzila's /done/ result page share
116
+ // these hosts, carry no card, and are refused by `match`. Served only to
117
+ // SDKs that ask for hosted_form on sync: an older SDK would arm these
118
+ // hosts, pause the navigation, get a 400 and abort the frame.
119
+ match: "(direct\\.tranzila\\.com/process/n/|directng\\.tranzila\\.com/process/?)",
120
+ hosts: ["^direct\\.tranzila\\.com$", "^directng\\.tranzila\\.com$"],
121
+ encoding: "form",
122
+ passthroughHeaders: [],
123
+ mode: "hosted_form",
124
+ hostedForm: {
125
+ required: ["sum", "supplier"],
126
+ card: { number: "ccno", month: "expmonth", year: "expyear", cvc: "mycvv", nationalId: "myid" },
127
+ amount: { sum: "sum", currency: "currency" },
128
+ // supplier (the terminal) + benid (the iframe session) + the amount:
129
+ // one submission, whatever nonce the form regenerated on a re-click.
130
+ dedupe: ["supplier", "benid", "sum", "currency"],
131
+ // One plain sale, reporting to the merchant the person sees: tranmode
132
+ // A (or absent) only (V verifies, K charges a token, C refunds, D
133
+ // forces), no TranzilaTK in either direction (`1` asks for a reusable
134
+ // token back, a token charges one), and every *_url_address on the
135
+ // merchant's host (Tranzila redirects the device there and POSTs the
136
+ // result there with the card's last four, expiry and ID number).
137
+ sale: { key: "tranmode", values: ["A"] },
138
+ token: "TranzilaTK",
139
+ callbackUrls: ["success_url_address", "fail_url_address", "notify_url_address"]
140
+ }
141
+ },
142
+ // ── Verified tokenizers (plaintext PAN in the body) ───────────────────────
143
+ // Each entry below was checked against the PSP's own shipped browser SDK or a
144
+ // live probe, then run through resolveRecognizerForUrl with positive, sibling
145
+ // and spoof URLs. Two rules the vectors keep proving, worth restating before
146
+ // anyone edits one of these:
147
+ // 1. Every dot in `hosts` is escaped. `^api.mercadopago.com$` admits the
148
+ // REGISTRABLE domain apiamercadopago.com; the escape is the allowlist.
149
+ // 2. `hosts` is pinned to literal labels, never widened to a wildcard label
150
+ // (`[a-z0-9-]+\.psp\.com`). Several of these vendors run wildcard DNS,
151
+ // so "the hostname resolves" is not evidence that it receives cards.
152
+ {
153
+ psp: "square",
154
+ // PAN@ card_data.number. POST /v2/card-nonce (URL always carries ?_=&version=).
155
+ match: "pci-connect\\.squareup(?:sandbox)?\\.com/v2/card-nonce",
156
+ hosts: ["^pci-connect\\.squareup\\.com$", "^pci-connect\\.squareupsandbox\\.com$"],
157
+ encoding: "json",
158
+ passthroughHeaders: []
159
+ // auth = client_id in body; no auth header
160
+ },
161
+ {
162
+ psp: "authorize_net",
163
+ // Accept.js. PAN@ securePaymentContainerRequest.data.token.cardNumber.
164
+ // Auth is body-level (merchantAuthentication: apiLoginID + clientKey) — nothing to replay.
165
+ // NOTE: prod is api2, not api. `api.authorize.net` is the server-side AIM/API host and is
166
+ // deliberately NOT allowlisted — a browser never posts a PAN there. Legacy SIM/DPM
167
+ // (secure2.authorize.net/gateway/transact.dll) is excluded for the same reason plus: it is
168
+ // retired, and it is a top-level form POST rather than an XHR.
169
+ // Coverage caveat: AcceptUI (the hosted lightbox) renders its fields inside an
170
+ // accept.authorize.net iframe, so that POST is cross-origin to the merchant page and an
171
+ // in-page hook cannot observe it. This entry covers the Accept.js custom-fields path, which
172
+ // does run in the merchant page. /xml/v1/request.api is Authorize.Net's single monolithic
173
+ // endpoint (request type is chosen by the body root key), so the path pin admits sibling
174
+ // request types on the same URL — the body, not the URL, is what distinguishes them.
175
+ match: "(api2|apitest)\\.authorize\\.net/xml/v1/request\\.api",
176
+ hosts: ["^api2\\.authorize\\.net$", "^apitest\\.authorize\\.net$"],
177
+ encoding: "json",
178
+ // JSON body; Content-Type is sometimes text/plain (preflight dodge), replayed as-is
179
+ passthroughHeaders: []
180
+ },
181
+ {
182
+ psp: "worldpay",
183
+ // Access Worldpay. PAN@ root key `cardNumber`. Accept is load-bearing:
184
+ // the two paths need DIFFERENT vendor media types; `application/json` → HTTP 406.
185
+ match: "(try\\.)?access\\.worldpay\\.com/(sessions/card|verifiedTokens/sessions)",
186
+ hosts: ["^access\\.worldpay\\.com$", "^try\\.access\\.worldpay\\.com$"],
187
+ encoding: "json",
188
+ passthroughHeaders: ["^accept$"]
189
+ },
190
+ {
191
+ psp: "nuvei",
192
+ // Safecharge Fields. PAN@ paymentOption.card.cardNumber.
193
+ // THREE PAN-bearing endpoints, not one. Nuvei picks between them from
194
+ // `sourceApplication`, and the covered one was the WRONG half of that
195
+ // branch: WEB_SDK (merchant holds the raw PAN in their own page) posts
196
+ // cardTokenization.do, while WEB_SDK_FIELDS (hosted fields, the
197
+ // PCI-conscious integration) posts websdk/initPaymentWithCardTokenization.do
198
+ // and clientPayment.do. Confirmed against sfc_inner_card.min.js, whose
199
+ // initPayment() opens initPaymentWithCardTokenization.do unconditionally
200
+ // with no branch back to the covered path.
201
+ match: "(secure|ppp-test)\\.safecharge\\.com/ppp/api/v1/(?:cardTokenization|clientPayment|websdk/initPaymentWithCardTokenization)\\.do",
202
+ hosts: ["^secure\\.safecharge\\.com$", "^ppp-test\\.safecharge\\.com$"],
203
+ encoding: "json",
204
+ passthroughHeaders: []
205
+ },
206
+ // bluesnap: DELIBERATELY NOT SHIPPED — it cannot be intercepted at all. Its tokenizer is a PUT, and isCardRequest
207
+ // gates on POST — but accepting PUT is not enough on its own. hpf.js fires the
208
+ // same token PUT SEVERAL TIMES per checkout (blur and submit each send a delta
209
+ // body), so intercepting it would raise one approval prompt per fire and hand the
210
+ // device partial card data. An entry that cannot actually be intercepted does not
211
+ // belong in an allowlist that advertises interception: it reads as support we do
212
+ // not have, and on the CDP path it arms Fetch.enable, gets refused by the POST
213
+ // gate, and is then continued straight to the PSP while the integrator believes
214
+ // the card was protected. Ship it when the multi-fire semantics are handled.
215
+ {
216
+ psp: "airwallex",
217
+ // PAN@ payment_method.card.number (payment_intents/confirm,
218
+ // payment_consents/verify) / card.number (payment_methods/create).
219
+ // ⚠ reference/bin/lookup is EXCLUDED. It egresses the PAN in an `x-pan` request
220
+ // HEADER, and it is a POST — so matching it would not merely "pause" it, it would
221
+ // intercept it and raise a real approval prompt for a bin lookup whose body holds
222
+ // no card. Handling header-borne PANs needs its own design; until then this
223
+ // header-aware handling, not the body cardFieldPath path. `x-pan` is deliberately NOT a
224
+ // passthrough header, so today the replay carries no PAN at all (inert, never a leak).
225
+ // `card/tokenize` was dropped from the candidate: an OPTIONS preflight on BOTH pci-api
226
+ // hosts returns the same 404 as a bogus control path, while real POST-only routes
227
+ // (payment_methods/create, payment_consents/create) answer 200 + Allow-Methods: POST.
228
+ // The trailing (\?|$) is a no-op server-side (pathname has no query) — it is the
229
+ // right-edge boundary for the SDK's UNANCHORED client-side test of the full URL.
230
+ match: "(checkout(\\.sandbox)?\\.airwallex\\.com|pci-api(\\.sandbox)?\\.airwallex\\.com)/api/v1/pa/(payment_intents/[^/?#]+/confirm|payment_consents/[^/?#]+/verify|payment_methods/create)(\\?|$)",
231
+ hosts: [
232
+ "^checkout\\.airwallex\\.com$",
233
+ "^checkout\\.sandbox\\.airwallex\\.com$",
234
+ "^pci-api\\.airwallex\\.com$",
235
+ "^pci-api\\.sandbox\\.airwallex\\.com$"
236
+ ],
237
+ encoding: "json",
238
+ passthroughHeaders: ["^client-secret$", "^authorization$", "^x-auth-token$", "^x-on-behalf-of$", "^x-api-version$"]
239
+ },
240
+ {
241
+ psp: "rapyd",
242
+ // PAN@ required_fields.number (hosted collect page) OR payment_method.fields.number
243
+ // (checkout page — it renders on checkout.rapyd.net/?token=checkout_… and posts cross-origin to
244
+ // api.rapyd.net). The checkout-id segment is pinned to the documented `checkout_` prefix: a bare
245
+ // [^/?#]+ also admitted /v1/checkout/collect and the encoded-slash smuggle
246
+ // /v1/checkout/checkout_x%2Fadmin. ⚠ /v1/checkout/{id} is ALSO the documented GET Retrieve
247
+ // Checkout Page (signature-authed, server-to-server, no card) — the POST gate is what splits them.
248
+ match: "(api|sandboxapi)\\.rapyd\\.net/v1/(?:hosted/collect/card/[^/?#]+/payment_method|checkout/checkout_[A-Za-z0-9]+)(?:[?#]|$)",
249
+ hosts: ["^api\\.rapyd\\.net$", "^sandboxapi\\.rapyd\\.net$"],
250
+ encoding: "json",
251
+ passthroughHeaders: []
252
+ // auth rides the opaque hosted-page / checkout token in the URL
253
+ },
254
+ {
255
+ psp: "dlocal",
256
+ // Smart Fields cvault. PAN@ pan. Do NOT match /cvault/cvv/store (CVV only).
257
+ // Hosts are two literal labels (no wildcard/`[a-z]+` label), so there is no
258
+ // multi-tenant surface: `ppmcc` is dlocal-operated, not merchant-registrable.
259
+ match: "ppmcc(-sandbox)?\\.dlocal\\.com/cvault/credit-card/temporal",
260
+ hosts: ["^ppmcc\\.dlocal\\.com$", "^ppmcc-sandbox\\.dlocal\\.com$"],
261
+ encoding: "json",
262
+ passthroughHeaders: ["^x-fields-api-key$", "^x-uow$", "^x-dlocal-infrav2$"]
263
+ },
264
+ {
265
+ psp: "ebanx",
266
+ compose: { card_due_date: "MM/YYYY" },
267
+ // PAN@ card.card_number. POST /ws/token. Body content-type is text/plain
268
+ // (CORS-preflight dodge) — replay it verbatim; encoding:json describes the body shape.
269
+ // The two domains SHARE one `/ws/token` tail on purpose: a bare top-level `|` (the shape
270
+ // this entry arrived in) degrades to `(^A)|(B$)` under any caller that anchors without
271
+ // re-grouping, admitting both `api.ebanx.com/ws/tokenizer_admin` and
272
+ // `evil.com/api-diamond.ebanxpay.com/ws/token`. Keep the alternation wrapped.
273
+ //
274
+ // `api.ebanxpay.com` / `sandbox.ebanxpay.com` are the hosts a STOCK integration
275
+ // actually uses: every published EBANX JS SDK (1.6.0 through 1.11.0, verified
276
+ // against the live bundles) hardcodes
277
+ // path: () => config.isLive() ? 'https://api.ebanxpay.com/' : 'https://sandbox.ebanxpay.com/'
278
+ // Without them we recognized only the `.ebanx.com` and `api-diamond` variants, so
279
+ // an ordinary EBANX checkout was invisible to us and the cardholder was never asked.
280
+ match: "(?:(?:customer-frontier|customer|api-local-latam|api|sandbox-local-latam|sandbox)\\.ebanx\\.com|(?:api-diamond|api|sandbox)\\.ebanxpay\\.com)/ws/token",
281
+ hosts: [
282
+ "^customer\\.ebanx\\.com$",
283
+ "^customer-frontier\\.ebanx\\.com$",
284
+ "^api\\.ebanx\\.com$",
285
+ "^sandbox\\.ebanx\\.com$",
286
+ "^api-local-latam\\.ebanx\\.com$",
287
+ "^sandbox-local-latam\\.ebanx\\.com$",
288
+ "^api-diamond\\.ebanxpay\\.com$",
289
+ "^api\\.ebanxpay\\.com$",
290
+ "^sandbox\\.ebanxpay\\.com$"
291
+ ],
292
+ encoding: "json",
293
+ passthroughHeaders: []
294
+ },
295
+ {
296
+ psp: "mercado_pago",
297
+ // PAN@ card_number. POST api.mercadopago.com/v1/card_tokens — the ONLY
298
+ // PAN egress in sdk.mercadopago.com/js/v2 (verified against the live bundle). No sandbox host:
299
+ // test mode is the TEST-* public_key. Auth + attribution ride the QUERY (the SDK's
300
+ // defaultQueries appends public_key, locale, js_version, referer), so replay the URL verbatim
301
+ // and never give `match` a trailing `$` — the SDK tests it unanchored against the raw URL.
302
+ // Same endpoint also serves saved-card {card_id,security_code} (no PAN) → check body before swap.
303
+ // (?![\w/-]) pins the exact endpoint: the `/card_tokens/{id}` sibling is a PUT carrying only
304
+ // {security_code,expiration_*} (no PAN), and the guard stops the SDK pausing it or /{id}/clone.
305
+ match: "api\\.mercadopago\\.com/v1/card_tokens(?![\\w/-])",
306
+ hosts: ["^api\\.mercadopago\\.com$"],
307
+ encoding: "json",
308
+ passthroughHeaders: ["^x-product-id$"]
309
+ // SDK sets X-Product-Id on the tokenize POST
310
+ },
311
+ {
312
+ psp: "payu",
313
+ // GPO Secure Forms. PAN@ card.number.
314
+ match: "(secure\\.payu\\.com|(merch-prod|secure)\\.snd\\.payu\\.com)/api/front/tokens",
315
+ hosts: ["^secure\\.payu\\.com$", "^(merch-prod|secure)\\.snd\\.payu\\.com$"],
316
+ encoding: "json",
317
+ passthroughHeaders: []
318
+ },
319
+ {
320
+ psp: "razorpay",
321
+ // PAN@ card[number] (form). Body carries risk/fingerprint fields — replay
322
+ // body verbatim, swap only card[*]. Auth = ?key_id= (or ?x_entity_id=) in the URL.
323
+ // THREE sinks, all POST, all carrying the SAME payload object. checkout-frame.js builds
324
+ // them as ib(r,"payments/create/ajax") and ib(r,"payments/create/"+(fees?"fees":"checkout")):
325
+ // /ajax XHR, standard flow
326
+ // /checkout form-POST redirect flow
327
+ // /fees form-POST, customer-fee-bearer merchants
328
+ // `fees` is NOT optional: it is the same card body behind a boolean (vS(this.r, t.fees)),
329
+ // so dropping it silently un-intercepts every fee-bearer merchant.
330
+ // The optional `standard_checkout/` segment appears when a session_token is present
331
+ // (builder: api + "v1/" + "standard_checkout" + "/" + path).
332
+ // NOT matched, deliberately: the pg_router shape (api + "pg_router/" + "v1/" +
333
+ // "standard_checkout" + path — note the MISSING slash before the path). Every ib() call
334
+ // site passes only 2 args, so that flag is always false and the shape is unreachable for
335
+ // card paths. If Razorpay ever enables it we go inert (fail-closed), never leaky.
336
+ // NOT allowlisted, deliberately: api-<region>.razorpay.com. checkout.js only builds that
337
+ // when the merchant page sets Razorpay.config.api="" — page-controlled input must never
338
+ // widen a card allowlist.
339
+ match: "api\\.razorpay\\.com/v1/(?:standard_checkout/)?payments/create/(?:ajax|checkout|fees)",
340
+ hosts: ["^api\\.razorpay\\.com$"],
341
+ encoding: "form",
342
+ passthroughHeaders: ["^x-razorpay-sessionid$", "^x-customer-access-token$"]
343
+ },
344
+ {
345
+ psp: "mollie",
346
+ // Components v1+v2. PAN@ cardNumber. No sandbox host (body `testmode` boolean).
347
+ match: "api\\.cc\\.mollie\\.com/v1/card-tokens",
348
+ hosts: ["^api\\.cc\\.mollie\\.com$"],
349
+ encoding: "json",
350
+ passthroughHeaders: [],
351
+ compose: { cardExpiryDate: "MM/YY" }
352
+ },
353
+ {
354
+ psp: "paysafe",
355
+ // Paysafe.js. PAN@ card.cardNum, expiry@ card.cardExpiry.{month,year}.
356
+ // `paymenthub/v1/singleusepaymenthandles` was pinned from the docs and
357
+ // has NEVER been observed in a capture; every run of the live SDK posts to
358
+ // `/js/api/v1/tokenize` instead, from hosted.paysafe.com's own iframe. Both are kept:
359
+ // the SUPH path may be reachable on integrations we have not exercised.
360
+ // Auth travels as `x-paysafe-credentials` on the observed path (NOT `Authorization`,
361
+ // which the docs describe for SUPH), so both are declared — replaying the wrong one
362
+ // gets the tokenization rejected.
363
+ // Token boundary is `(?![\w/-])`, not `(?!/)`: the server anchors `match` with ^(?:…)$
364
+ // so either works there, but the SDK compiles `match` with `i` ALONE (unanchored), and
365
+ // `(?!/)` lets `…/singleusepaymenthandlesEVIL` satisfy the browser-side pause test.
366
+ // NOTE: legacy Paysafe.js v1 (/customervault/v1/singleusetokens, same two hosts) is
367
+ // deliberately NOT covered here — it needs its own verification pass before adding.
368
+ // Checkout 1.8 uses a separate hosted origin, with the same nested card shape.
369
+ // Verified from https://hosted.test.paysafe.com/checkout/1.8.0/main.bundle.js
370
+ // and its byte-identical production sibling (SHA-256 e9e111ff03073cc936a47674e8e7c5db4a6d1050e1b65bf5e20723116fd5080c).
371
+ // Its native LIVE/TEST environment map selects exactly hosted[.test].paysafe.com.
372
+ // Keep host/path pairs separate: details/log are not card requests;
373
+ // card/tokenize uses a saved paymentToken, and 3DS-specific paths are unverified.
374
+ match: "(?:api(\\.test)?\\.paysafe\\.com/(?:paymenthub/v1/singleusepaymenthandles|js/api/v1/tokenize)|hosted(\\.test)?\\.paysafe\\.com/checkout/api/v1/tokenize)(?![\\w/-])",
375
+ hosts: ["^api\\.paysafe\\.com$", "^api\\.test\\.paysafe\\.com$", "^hosted\\.paysafe\\.com$", "^hosted\\.test\\.paysafe\\.com$"],
376
+ encoding: "json",
377
+ passthroughHeaders: ["^authorization$", "^x-paysafe-credentials$", "^correlationid$"]
378
+ },
379
+ {
380
+ psp: "recurly",
381
+ // PAN@ number (form-encoded POST). recurly.js hardcodes exactly TWO API bases:
382
+ // "https://api.recurly.com/js/v1" and "https://api.eu.recurly.com/js/v1". EU is auto-selected
383
+ // when the public key starts with `fra-`. No sandbox host: sandbox sites reuse api.recurly.com
384
+ // with a sandbox public key.
385
+ // WARNING: never widen these to [a-z0-9-]+\.recurly\.com. *.recurly.com is WILDCARD DNS (every
386
+ // merchant subdomain resolves to the same LB), so a broad host pattern is a card-exfil hole.
387
+ // (?![\w-]) excludes the live /js/v1/tokens sibling (iban/bacs/becs bank accounts + 3DS action
388
+ // results, no PAN); /js/v1/payment_methods/token is APM/wallet ({currency,amount}) and is
389
+ // excluded by the anchored path too.
390
+ // WARNING: the opt-in `cors:false` legacy path tokenizes over a JSONP GET, which the POST gate
391
+ // in isCardRequest cannot intercept. Default is `cors:true` (XHR POST), so this entry is live.
392
+ match: "api(\\.eu)?\\.recurly\\.com/js/v1/token(?![\\w-])",
393
+ hosts: ["^api\\.recurly\\.com$", "^api\\.eu\\.recurly\\.com$"],
394
+ encoding: "form",
395
+ passthroughHeaders: ["^recurly-credential-checkout-hostname$"]
396
+ // only in configure({hostname}) mode
397
+ },
398
+ {
399
+ psp: "moneris",
400
+ compose: { input_exp: "MMYY" },
401
+ // Hosted Tokenization. VERIFIED live against hpp_common.js on all six
402
+ // hosts: the iframe does `request.open('POST', 'request.php')` (XHR, relative to
403
+ // /HPPtoken/index.php) with Content-Type application/x-www-form-urlencoded and
404
+ // PAN@ input_data. It sets no auth header (the HT profile is the ?id= on the iframe
405
+ // URL), so passthroughHeaders is correctly empty. hpp_common.js references exactly
406
+ // one URL and one method, so there is no second tokenization path to cover.
407
+ // NOTE: do NOT add Moneris Checkout (chkt/) as a separate entry. It shares these
408
+ // hosts and resolveRecognizerForUrl .find()s on host first, so one would permanently
409
+ // shadow the other; chkt/ instead falls out at the path check (refused, not leaked).
410
+ match: "(www3|esqa|gateway|gatewayqa|gatewayt|gatewaydev)\\.moneris\\.com/HPPtoken/request\\.php",
411
+ hosts: [
412
+ "^www3\\.moneris\\.com$",
413
+ "^esqa\\.moneris\\.com$",
414
+ "^gateway\\.moneris\\.com$",
415
+ "^gatewayqa\\.moneris\\.com$",
416
+ "^gatewayt\\.moneris\\.com$",
417
+ "^gatewaydev\\.moneris\\.com$"
418
+ ],
419
+ encoding: "form",
420
+ passthroughHeaders: []
421
+ },
422
+ {
423
+ psp: "bambora",
424
+ // Bambora NA / Worldline Custom Checkout. PAN@ number. POST. Content-Type is
425
+ // text/plain (preflight dodge) — replay verbatim; `encoding: 'json'` describes the body shape.
426
+ // No sandbox host: test mode rides the same host, decided by merchant id + API key.
427
+ // api.bam.shift4api.net is the post-migration host (Dec-2026 cutover); both hosts are pinned
428
+ // exact — never widen to the docs' *.bam.shift4api.net wildcard. api.na.bambora.com CNAMEs to
429
+ // the legacy beanstream.com origin; do NOT add www.beanstream.com, the browser SDK never calls it.
430
+ match: "(api\\.na\\.bambora\\.com|api\\.bam\\.shift4api\\.net)/scripts/tokenization/tokens",
431
+ hosts: ["^api\\.na\\.bambora\\.com$", "^api\\.bam\\.shift4api\\.net$"],
432
+ encoding: "json",
433
+ passthroughHeaders: []
434
+ },
435
+ {
436
+ psp: "global_payments",
437
+ // GlobalPayments.js, two gateways behind one PSP id. PAN@ card.number on
438
+ // both (verified in the shipped js.globalpay.com/v1/globalpayments.js, actionTokenize$3/$4).
439
+ // Heartland/Portico: prod api.heartlandportico.com/SecureSubmit.v1/api/token, sandbox
440
+ // cert.api2.heartlandportico.com/Hps.Exchange.PosGateway.Hpf.v1/api/token. The host/path
441
+ // pairing is ASYMMETRIC (prod is not api2, sandbox is not SecureSubmit.v1). That is the
442
+ // vendor's own domains$4/urls$4 table, not a typo, so do not "correct" it. Auth is ?api_key=
443
+ // in the URL, so there is nothing to replay in headers. api2.heartlandportico.com is
444
+ // deliberately absent: it is the server-side PHP SDK's endpoint, never a browser call.
445
+ // GP-API/UCP: apis[.sandbox][.eu].globalpay.com/ucp/[merchants/{id}/]payment-methods, auth via
446
+ // Authorization Bearer + X-GP-Version. All four hosts verified live and serving cross-origin
447
+ // POST. accounts/{id}/ is deliberately NOT accepted: the tokenizer only ever prefixes
448
+ // merchants/{id}/, so allowing it would widen the path with no endpoint behind it.
449
+ // Both gateways POST application/json, so isCardRequest's POST gate lets them through.
450
+ 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)",
451
+ hosts: [
452
+ "^api\\.heartlandportico\\.com$",
453
+ "^cert\\.api2\\.heartlandportico\\.com$",
454
+ "^apis\\.globalpay\\.com$",
455
+ "^apis\\.sandbox\\.globalpay\\.com$",
456
+ "^apis\\.eu\\.globalpay\\.com$",
457
+ "^apis\\.sandbox\\.eu\\.globalpay\\.com$"
458
+ ],
459
+ encoding: "json",
460
+ passthroughHeaders: ["^authorization$", "^x-gp-version$"]
461
+ }
462
+ ]);
463
+ var PSP_RECOGNIZERS = deepFreeze(PROCESSOR_DEFINITIONS.map(({ compose, encryptedCardFields, ...definition }) => definition));
464
+ var PSP_CLIENT_TABLE = deepFreeze(PROCESSOR_DEFINITIONS.map((definition) => ({
465
+ psp: definition.psp,
466
+ match: new RegExp(definition.match, "i"),
467
+ hosts: definition.hosts.map((source) => new RegExp(source, "i")),
468
+ encoding: definition.encoding,
469
+ headers: definition.passthroughHeaders.map((source) => new RegExp(source, "i")),
470
+ ...definition.mode ? { mode: definition.mode } : {},
471
+ ...definition.clientSideEncrypted ? { clientSideEncrypted: true } : {},
472
+ ...definition.compose ? { compose: definition.compose } : {},
473
+ ...definition.encryptedCardFields ? { cse: { fields: definition.encryptedCardFields } } : {},
474
+ ...definition.hostedForm ? { form: definition.hostedForm.card } : {}
475
+ })));
476
+ var BUILTIN_REGISTRY = deepFreeze(PSP_RECOGNIZERS.map(({ cse, hostedForm, ...definition }) => ({
477
+ ...definition,
478
+ match: new RegExp(definition.match, "i"),
479
+ passthroughHeaders: definition.passthroughHeaders.map((source) => new RegExp(source, "i"))
480
+ })));
481
+ var CHECKOUT_MODES = deepFreeze(["token", "cse", "hosted_form"]);
482
+ function recognizerByPsp(psp) {
483
+ return typeof psp === "string" ? PSP_CLIENT_TABLE.find((entry) => entry.psp === psp) ?? null : null;
484
+ }
485
+ function trustedClientRecognizer(pspOrEntry) {
486
+ const psp = typeof pspOrEntry === "string" ? pspOrEntry : pspOrEntry?.psp;
487
+ const definition = recognizerByPsp(psp);
488
+ if (!definition)
489
+ throw new Error("Refusing to pay: unknown payment processor.");
490
+ return definition;
491
+ }
492
+ // src/request-policy.js
493
+ var HEADER_NEVER_FORWARD = /^(cookie|set-cookie|authorization2|proxy-authorization|host|content-length|origin|referer)$/i;
494
+ var ALLOWED_MEDIA_TYPES = {
495
+ json: ["application/json", "text/plain"],
496
+ form: ["application/x-www-form-urlencoded"]
497
+ };
498
+ function contentTypeFor(pspOrEntry, headers) {
499
+ const rec = trustedClientRecognizer(pspOrEntry);
500
+ const encoding = rec && rec.encoding === "form" ? "form" : "json";
501
+ const allowed = ALLOWED_MEDIA_TYPES[encoding];
502
+ const fallback = allowed[0];
503
+ const entry = Object.entries(headers || {}).find(([k]) => /^content-type$/i.test(k));
504
+ const raw = entry && entry[1];
505
+ if (typeof raw !== "string")
506
+ return fallback;
507
+ const base = raw.split(";")[0].trim().toLowerCase();
508
+ const worldpaySessionType = rec.psp === "worldpay" && encoding === "json" && base === "application/vnd.worldpay.sessions-v1.hal+json";
509
+ if (!allowed.includes(base) && !worldpaySessionType)
510
+ return fallback;
511
+ const charset = raw.match(/;\s*charset=([\w-]+)/i);
512
+ return charset ? `${base}; charset=${charset[1]}` : base;
513
+ }
514
+ function replayHeaders(pspOrEntry, captured = {}) {
515
+ const rec = trustedClientRecognizer(pspOrEntry);
516
+ const headers = { "content-type": contentTypeFor(rec, captured) };
517
+ for (const [name, value] of Object.entries(captured)) {
518
+ if (rec.headers.some((pattern) => pattern.test(name)) && !HEADER_NEVER_FORWARD.test(name))
519
+ headers[name] = value;
520
+ }
521
+ return headers;
522
+ }
523
+ var replayPolicy = Object.freeze({ contentTypeFor, HEADER_NEVER_FORWARD });
524
+ // src/stripe-checkout.js
525
+ var STRIPE_CHECKOUT_OPERATION = "stripe.device_checkout_confirm.v1";
526
+ var STRIPE_CHECKOUT_CONTEXT_HEADER = "x-agentcard-stripe-checkout-context";
527
+ var SESSION = /^cs_(?:test|live)_[A-Za-z0-9]{1,200}(?![\s\S])/;
528
+ var KEY = /^pk_(?:test|live)_[A-Za-z0-9_]{1,256}(?![\s\S])/;
529
+ var SYNTHETIC = /^pm_agentcard_checkout_[A-Za-z0-9_-]{16,80}(?![\s\S])/;
530
+ var reject = () => {
531
+ throw new Error("stripe_checkout_rejected");
532
+ };
533
+ var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
534
+ var opaque = (value) => typeof value === "string" && value.length <= 8192 && !/[\x00-\x1f\x7f]/.test(value);
535
+ var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
536
+ function validateStripeCheckoutIdentity(value) {
537
+ if (!object(value) || !["test", "production"].includes(value.environment ?? "test") || value.environment === null || !["sessionId", "publishableKey", "livemode"].some((key) => Object.hasOwn(value, key)))
538
+ reject();
539
+ const environment = value.environment ?? "test", livemode = environment === "production";
540
+ const mode = livemode ? "live" : "test";
541
+ if (Object.hasOwn(value, "sessionId") && (typeof value.sessionId !== "string" || !SESSION.test(value.sessionId) || !value.sessionId.startsWith(`cs_${mode}_`)) || Object.hasOwn(value, "publishableKey") && (typeof value.publishableKey !== "string" || !KEY.test(value.publishableKey) || !value.publishableKey.startsWith(`pk_${mode}_`)) || Object.hasOwn(value, "livemode") && value.livemode !== livemode)
542
+ reject();
543
+ return Object.freeze({ environment, livemode });
544
+ }
545
+ var TEST_CARDS = ["4242424242424242", "4000000000000002", "4000002500003155"];
546
+ var SYNTHETIC_CARDS = [...TEST_CARDS, "4111111111111111"];
547
+ function validateStripeCheckoutCard(card, environment = "test") {
548
+ if (!["test", "production"].includes(environment) || !object(card) || typeof card.number !== "string" || typeof card.cvc !== "string" || !/^(?:0?[1-9]|1[0-2])(?![\s\S])/.test(String(card.month)) || !/^(?:\d{2}|\d{4})(?![\s\S])/.test(String(card.year)) || card.name !== void 0 && !opaque(card.name))
549
+ reject();
550
+ if (environment === "test") {
551
+ if (!TEST_CARDS.includes(card.number) || card.cvc !== "123")
552
+ reject();
553
+ }
554
+ else {
555
+ if (!/^\d{13,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))
556
+ reject();
557
+ let sum = 0, double = false;
558
+ for (let i = card.number.length - 1; i >= 0; i--) {
559
+ let digit = Number(card.number[i]);
560
+ if (double) {
561
+ digit *= 2;
562
+ if (digit > 9)
563
+ digit -= 9;
564
+ }
565
+ sum += digit;
566
+ double = !double;
567
+ }
568
+ if (sum % 10 !== 0)
569
+ reject();
570
+ }
571
+ return true;
572
+ }
573
+ function isStripeCheckoutHostedMode(value) {
574
+ return value === "hosted" || value === "hosted_page";
575
+ }
576
+ function encodeAscii(value) {
577
+ let result = "", bits = 0, count = 0;
578
+ for (const char of value) {
579
+ bits = bits << 8 | char.charCodeAt(0);
580
+ count += 8;
581
+ while (count >= 6) {
582
+ count -= 6;
583
+ result += alphabet[bits >>> count & 63];
584
+ }
585
+ }
586
+ return result + (count ? alphabet[bits << 6 - count & 63] : "");
587
+ }
588
+ function context(value) {
589
+ if (!object(value) || Object.keys(value).length !== 3 || value.version !== 1 || !["version", "session_id", "synthetic_payment_method_id"].every((key) => Object.hasOwn(value, key)) || !SESSION.test(value.session_id) || !SYNTHETIC.test(value.synthetic_payment_method_id))
590
+ reject();
591
+ return { version: 1, session_id: value.session_id, synthetic_payment_method_id: value.synthetic_payment_method_id };
592
+ }
593
+ function encodeStripeCheckoutContext(value) {
594
+ return encodeAscii(JSON.stringify(context(value)));
595
+ }
596
+ function hasStripeCheckoutMarker(request) {
597
+ return object(request?.headers) && Object.keys(request.headers).some((key) => key.toLowerCase() === STRIPE_CHECKOUT_CONTEXT_HEADER);
598
+ }
599
+ function parseStripeCheckoutContext(request) {
600
+ const entries = Object.entries(request?.headers ?? {}).filter(([key]) => key.toLowerCase() === STRIPE_CHECKOUT_CONTEXT_HEADER);
601
+ if (!entries.length)
602
+ return null;
603
+ if (entries.length !== 1 || typeof entries[0][1] !== "string" || !/^[A-Za-z0-9_-]{1,1024}$/.test(entries[0][1]))
604
+ reject();
605
+ const encoded = entries[0][1];
606
+ let text = "", bits = 0, count = 0;
607
+ for (const char of encoded) {
608
+ bits = bits << 6 | alphabet.indexOf(char);
609
+ count += 6;
610
+ if (count >= 8) {
611
+ count -= 8;
612
+ text += String.fromCharCode(bits >>> count & 255);
613
+ }
614
+ }
615
+ let value;
616
+ try {
617
+ value = context(JSON.parse(text));
618
+ }
619
+ catch {
620
+ reject();
621
+ }
622
+ if (encodeStripeCheckoutContext(value) !== encoded)
623
+ reject();
624
+ return value;
625
+ }
626
+ function endpoint(raw) {
627
+ let url;
628
+ try {
629
+ url = new URL(raw);
630
+ }
631
+ catch {
632
+ return null;
633
+ }
634
+ if (url.origin !== "https://api.stripe.com" || url.port || url.username || url.password || url.search || url.hash)
635
+ return null;
636
+ return url;
637
+ }
638
+ function isStripeCheckoutEndpoint(raw) {
639
+ const url = endpoint(raw);
640
+ const match = url && /^\/v1\/payment_pages\/(cs_(?:test|live)_[A-Za-z0-9]+)\/confirm$/.exec(url.pathname);
641
+ return !!match && SESSION.test(match[1]);
642
+ }
643
+ var finalFields = /* @__PURE__ */ new Set([
644
+ "payment_method",
645
+ "key",
646
+ "expected_amount",
647
+ "expected_payment_method_type",
648
+ "payment_method_data[billing_details][email]",
649
+ "init_checksum",
650
+ "js_checksum",
651
+ "version",
652
+ "rv_timestamp",
653
+ "eid",
654
+ "link_brand",
655
+ "guid",
656
+ "muid",
657
+ "sid",
658
+ "passive_captcha_token",
659
+ "passive_captcha_ekey",
660
+ ...[
661
+ "checkout_config_id",
662
+ "checkout_session_id",
663
+ "client_session_id",
664
+ "merchant_integration_source",
665
+ "merchant_integration_version",
666
+ "payment_method_selection_flow"
667
+ ].map((key) => `client_attribution_metadata[${key}]`),
668
+ ...["shipping_rate_amount", "subtotal", "total_discount_amount", "total_exclusive_tax", "total_inclusive_tax"].map((key) => `last_displayed_line_item_group_details[${key}]`)
669
+ ]);
670
+ var addressFields = ["city", "country", "line1", "line2", "postal_code", "state"];
671
+ var tokenFields = /* @__PURE__ */ new Set([
672
+ "type",
673
+ "key",
674
+ "card[number]",
675
+ "card[cvc]",
676
+ "card[exp_month]",
677
+ "card[exp_year]",
678
+ "billing_details[name]",
679
+ "billing_details[email]",
680
+ "billing_details[phone]",
681
+ "allow_redisplay",
682
+ ...addressFields.map((key) => `billing_details[address][${key}]`),
683
+ "guid",
684
+ "muid",
685
+ "sid",
686
+ "payment_user_agent",
687
+ "referrer",
688
+ "time_on_page",
689
+ ...[
690
+ "checkout_config_id",
691
+ "checkout_session_id",
692
+ "client_session_id",
693
+ "merchant_integration_source",
694
+ "merchant_integration_version",
695
+ "payment_method_selection_flow"
696
+ ].map((key) => `client_attribution_metadata[${key}]`)
697
+ ]);
698
+ function form(request, fields) {
699
+ if (!object(request) || request.method !== "POST" || typeof request.body !== "string" || request.body.length > 2e4 || !object(request.headers) || Object.keys(request.headers).length > 40 || Object.values(request.headers).some((value) => !opaque(value)))
700
+ reject();
701
+ const pairs = [];
702
+ const seen = /* @__PURE__ */ new Set();
703
+ for (const raw of request.body.split("&")) {
704
+ const index = raw.indexOf("=");
705
+ if (index < 1)
706
+ reject();
707
+ let key, value;
708
+ try {
709
+ key = decodeURIComponent(raw.slice(0, index).replace(/\+/g, " "));
710
+ value = decodeURIComponent(raw.slice(index + 1).replace(/\+/g, " "));
711
+ }
712
+ catch {
713
+ reject();
714
+ }
715
+ if (!fields.has(key) || seen.has(key) || !opaque(value))
716
+ reject();
717
+ seen.add(key);
718
+ pairs.push({ key, value, raw });
719
+ }
720
+ return { pairs, values: new Map(pairs.map(({ key, value }) => [key, value])) };
721
+ }
722
+ function keyFor(request, values, expected) {
723
+ const authorization = Object.entries(request.headers).filter(([key2]) => key2.toLowerCase() === "authorization");
724
+ if (authorization.length > 1)
725
+ reject();
726
+ let headerKey;
727
+ if (authorization.length) {
728
+ const match = /^Bearer (pk_(?:test|live)_[A-Za-z0-9_]{1,256})$/.exec(authorization[0][1]);
729
+ if (!match)
730
+ reject();
731
+ headerKey = match[1];
732
+ }
733
+ const key = values.get("key") || headerKey;
734
+ if (!KEY.test(key) || headerKey && headerKey !== key || expected !== void 0 && expected !== key)
735
+ reject();
736
+ return key;
737
+ }
738
+ var billingEmail = (value) => value === null || typeof value === "string" && value.length > 0 && value.length <= 320 && opaque(value);
739
+ function captureStripeCheckoutBilling(request, options) {
740
+ const phase = classifyStripeCheckoutRequest(request, options);
741
+ if (phase?.phase !== "tokenization" || !SYNTHETIC.test(options?.syntheticPaymentMethodId))
742
+ reject();
743
+ const email = form(request, tokenFields).values.get("billing_details[email]") ?? null;
744
+ if (!billingEmail(email))
745
+ reject();
746
+ return Object.freeze({
747
+ session_id: phase.session_id,
748
+ synthetic_payment_method_id: options.syntheticPaymentMethodId,
749
+ email
750
+ });
751
+ }
752
+ function attachStripeCheckoutBilling(request, captured, options) {
753
+ const phase = classifyStripeCheckoutRequest(request, options);
754
+ if (phase?.phase !== "final" || !object(captured) || Object.keys(captured).length !== 3 || !["session_id", "synthetic_payment_method_id", "email"].every((key) => Object.hasOwn(captured, key)) || captured.session_id !== phase.session_id || captured.synthetic_payment_method_id !== phase.synthetic_payment_method_id || !billingEmail(captured.email))
755
+ reject();
756
+ const field = "payment_method_data[billing_details][email]";
757
+ const existing = form(request, finalFields).values.get(field);
758
+ if (existing !== void 0 && existing !== captured.email)
759
+ reject();
760
+ if (captured.email === null || existing !== void 0)
761
+ return { ...request };
762
+ const body = `${request.body}&${encodeURIComponent(field)}=${encodeURIComponent(captured.email)}`;
763
+ const result = { ...request, body };
764
+ classifyStripeCheckoutRequest(result, options);
765
+ return result;
766
+ }
767
+ function classifyStripeCheckoutRequest(request, options = {}) {
768
+ const url = endpoint(request?.url);
769
+ if (!url) {
770
+ if (hasStripeCheckoutMarker(request))
771
+ reject();
772
+ return null;
773
+ }
774
+ const final = isStripeCheckoutEndpoint(request.url);
775
+ const token = url.pathname === "/v1/payment_methods";
776
+ if (!final && !token) {
777
+ if (hasStripeCheckoutMarker(request))
778
+ reject();
779
+ return null;
780
+ }
781
+ if (token && options.sessionId === void 0 && !hasStripeCheckoutMarker(request))
782
+ return null;
783
+ const marker = parseStripeCheckoutContext(request);
784
+ const sessionId = final ? url.pathname.split("/")[3] : options.sessionId;
785
+ if (!SESSION.test(sessionId) || options.sessionId !== void 0 && options.sessionId !== sessionId || marker && marker.session_id !== sessionId)
786
+ reject();
787
+ const { values } = form(request, final ? finalFields : tokenFields);
788
+ const key = keyFor(request, values, options.publishableKey);
789
+ validateStripeCheckoutIdentity({ environment: options.environment, sessionId, publishableKey: key });
790
+ const attributionSession = values.get("client_attribution_metadata[checkout_session_id]");
791
+ if (attributionSession !== void 0 && attributionSession !== sessionId)
792
+ reject();
793
+ if (token) {
794
+ const number = values.get("card[number]");
795
+ if (values.get("type") !== "card" || typeof number !== "string" || number.length > 64 || /[^0-9 -]/.test(number) || !["4111111111111111", "4242424242424242"].includes(number.replace(/[ -]/g, "")) || !/^\d{3}$/.test(values.get("card[cvc]") ?? "") || !/^(?:0?[1-9]|1[0-2])$/.test(values.get("card[exp_month]") ?? "") || !/^(?:\d{2}|\d{4})$/.test(values.get("card[exp_year]") ?? "") || values.has("allow_redisplay") && values.get("allow_redisplay") !== "unspecified")
796
+ reject();
797
+ return { phase: "tokenization", session_id: sessionId, publishable_key: key };
798
+ }
799
+ const amount = values.get("expected_amount");
800
+ const syntheticId = values.get("payment_method");
801
+ const email = values.get("payment_method_data[billing_details][email]");
802
+ if (email !== void 0 && !billingEmail(email))
803
+ reject();
804
+ if (!/^[1-9]\d{0,11}$/.test(amount ?? "") || !Number.isSafeInteger(Number(amount)) || options.amountCents !== void 0 && options.amountCents !== Number(amount) || values.get("expected_payment_method_type") !== "card" || !SYNTHETIC.test(syntheticId) || ["init_checksum", "js_checksum", "version", "rv_timestamp"].some((key2) => !values.get(key2)) || options.syntheticPaymentMethodId !== void 0 && options.syntheticPaymentMethodId !== syntheticId || marker && marker.synthetic_payment_method_id !== syntheticId)
805
+ reject();
806
+ return {
807
+ phase: "final",
808
+ session_id: sessionId,
809
+ publishable_key: key,
810
+ synthetic_payment_method_id: syntheticId,
811
+ amount_cents: Number(amount),
812
+ currency: "usd"
813
+ };
814
+ }
815
+ function createStripeCheckoutTokenizationStub(request, options) {
816
+ const phase = classifyStripeCheckoutRequest(request, options);
817
+ if (phase?.phase !== "tokenization" || !SYNTHETIC.test(options?.syntheticPaymentMethodId))
818
+ reject();
819
+ const { values } = form(request, tokenFields);
820
+ const address = Object.fromEntries(addressFields.filter((key) => values.has(`billing_details[address][${key}]`)).map((key) => [key, values.get(`billing_details[address][${key}]`)]));
821
+ const billing = Object.fromEntries(["name", "email", "phone"].filter((key) => values.has(`billing_details[${key}]`)).map((key) => [key, values.get(`billing_details[${key}]`)]));
822
+ const year = values.get("card[exp_year]");
823
+ return { status: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({
824
+ id: options.syntheticPaymentMethodId,
825
+ object: "payment_method",
826
+ type: "card",
827
+ livemode: validateStripeCheckoutIdentity({ environment: options.environment, sessionId: phase.session_id }).livemode,
828
+ billing_details: { ...billing, address },
829
+ card: {
830
+ brand: "visa",
831
+ last4: "4242",
832
+ exp_month: Number(values.get("card[exp_month]")),
833
+ exp_year: Number(year.length === 2 ? `20${year}` : year),
834
+ funding: "credit",
835
+ country: "US"
836
+ }
837
+ }) };
838
+ }
839
+ var terminalFields = /* @__PURE__ */ new Set([
840
+ "id",
841
+ "object",
842
+ "state",
843
+ "status",
844
+ "payment_status",
845
+ "currency",
846
+ "livemode",
847
+ "session_id",
848
+ "mode",
849
+ "ui_mode",
850
+ "success_url",
851
+ "return_url"
852
+ ]);
853
+ function parseStripeCheckoutResponse(raw, options) {
854
+ let value;
855
+ try {
856
+ value = typeof raw === "string" ? JSON.parse(raw) : raw;
857
+ validateStripeCheckoutIdentity({ environment: options?.environment, sessionId: options?.sessionId, livemode: value?.livemode });
858
+ }
859
+ catch {
860
+ return null;
861
+ }
862
+ if (!object(value) || !SESSION.test(options?.sessionId) || value.session_id !== options.sessionId || Object.keys(value).some((key) => !terminalFields.has(key)) || !["id", "object", "state", "status", "payment_status", "currency", "livemode", "session_id", "mode", "ui_mode"].every((key) => Object.hasOwn(value, key)) || typeof value.id !== "string" || !/^[A-Za-z0-9_]{1,256}(?![\s\S])/.test(value.id) || value.object !== "checkout.session" || value.state !== "succeeded" || value.status !== "complete" || value.payment_status !== "paid" || value.currency !== "usd" || value.mode !== "payment" || !isStripeCheckoutHostedMode(value.ui_mode))
863
+ return null;
864
+ for (const key of ["success_url", "return_url"]) {
865
+ if (!Object.hasOwn(value, key) || value[key] === null)
866
+ continue;
867
+ if (typeof value[key] !== "string" || value[key].length > 2048)
868
+ return null;
869
+ let url;
870
+ try {
871
+ url = new URL(value[key]);
872
+ }
873
+ catch {
874
+ return null;
875
+ }
876
+ let parameters;
877
+ try {
878
+ parameters = decodeURIComponent(url.search + url.hash);
879
+ }
880
+ catch {
881
+ return null;
882
+ }
883
+ if (url.protocol !== "https:" || url.username || url.password || options.returnOrigin !== void 0 && url.origin !== options.returnOrigin || /(?:client_secret|payment_method|secret_)/i.test(parameters))
884
+ return null;
885
+ }
886
+ return { ...value };
887
+ }
888
+ function prepareStripeCheckoutReplay(request, card, options = {}) {
889
+ const marker = parseStripeCheckoutContext(request);
890
+ if (!marker)
891
+ reject();
892
+ const phase = classifyStripeCheckoutRequest(request, {
893
+ ...options,
894
+ sessionId: marker.session_id,
895
+ syntheticPaymentMethodId: marker.synthetic_payment_method_id
896
+ });
897
+ if (phase?.phase !== "final" || options.amountCents !== void 0 && phase.amount_cents !== options.amountCents || options.sessionId !== void 0 && options.sessionId !== marker.session_id || !validateStripeCheckoutCard(card, options.environment))
898
+ reject();
899
+ const replacements = {
900
+ "payment_method_data[type]": "card",
901
+ "payment_method_data[card][number]": card.number,
902
+ "payment_method_data[card][cvc]": card.cvc,
903
+ "payment_method_data[card][exp_month]": String(card.month),
904
+ "payment_method_data[card][exp_year]": String(card.year),
905
+ ...card.name ? { "payment_method_data[billing_details][name]": card.name } : {}
906
+ };
907
+ const inline = Object.entries(replacements).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
908
+ const body = form(request, finalFields).pairs.map((pair) => pair.key === "payment_method" ? inline : pair.raw).join("&");
909
+ return {
910
+ psp: "stripe",
911
+ request: { url: request.url, method: "POST", headers: replayHeaders("stripe", request.headers), body },
912
+ where: Object.keys(replacements)
913
+ };
914
+ }
915
+ export { STRIPE_CHECKOUT_CONTEXT_HEADER, STRIPE_CHECKOUT_OPERATION, attachStripeCheckoutBilling, captureStripeCheckoutBilling, classifyStripeCheckoutRequest, createStripeCheckoutTokenizationStub, encodeStripeCheckoutContext, hasStripeCheckoutMarker, isStripeCheckoutEndpoint, isStripeCheckoutHostedMode, parseStripeCheckoutContext, parseStripeCheckoutResponse, prepareStripeCheckoutReplay, validateStripeCheckoutCard, validateStripeCheckoutIdentity };