@agent-cards/checkout 0.2.0 → 0.3.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 +25 -0
- package/README.md +191 -9
- package/dist/cdp.d.ts +73 -5
- package/dist/cdp.js +238 -25
- package/dist/client.d.ts +16 -4
- package/dist/client.js +132 -36
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/lifecycle.d.ts +91 -0
- package/dist/lifecycle.js +193 -0
- package/examples/existing-browser.mjs +63 -0
- package/package.json +6 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Browser checkout lifecycle
|
|
6
|
+
|
|
7
|
+
- `attachToPlaywright` and `attachToCdp` now return a `CheckoutController` with state, cancellation, merchant reconciliation, user-action hooks and explicit retry after a merchant-confirmed failure. Existing callers may continue to ignore the return value.
|
|
8
|
+
- `requireMerchantResult: true` holds further card requests after handoff until the application's merchant integration confirms the outcome. Approval or tokenization alone does not establish a successful order.
|
|
9
|
+
- Stripe `/v1/payment_methods` and `/v1/tokens` handoffs hold further recognized card requests, even without `requireMerchantResult`. Tokenization does not bind a specific PaymentIntent, amount or currency, so automatic token-to-intent continuation is unsupported. Direct card-bearing PaymentIntent confirms retain their existing backend amount verification. Unknown merchant-server endpoints require explicit `paymentEndpoints` guards; server-side charges remain outside this browser guard.
|
|
10
|
+
- Exact `paymentEndpoints` guards abort unsupported payment endpoints identified by the integrator. Unknown endpoints outside those guards remain untouched.
|
|
11
|
+
- An existing-browser example covers the direct SDK connection used with Browserbase, Kernel or a compatible custom Chromium/CDP session. Cloud-provider sessions and real merchant/3DS flows still require separate validation; Kernel's native Vault integration is a separate path.
|
|
12
|
+
|
|
13
|
+
### Migration from 0.2.x
|
|
14
|
+
|
|
15
|
+
- Handle `PaymentOutcomeUnknownError` separately from decline or safe expiry. Lost create/poll responses, malformed post-create responses and local deadlines can leave an approval or payment outstanding. Reconcile the merchant order before starting another attempt; do not retry because a local timer elapsed.
|
|
16
|
+
- `timeoutMs` now bounds authentication, authorization creation and polling together. `cancel()` stops this attachment locally; it does not revoke an approval link or cancel a processor payment. A cancelled attachment cannot restart.
|
|
17
|
+
- An attachment that issued an unbound Stripe token cannot reset with `retryAfterMerchantFailure`, including when browser delivery is uncertain. Reconcile the merchant outcome and use a separately validated flow; do not reuse the token in a new attachment.
|
|
18
|
+
- Hosted-form submissions remain blocked until an explicit merchant-confirmed failure permits retry. The short duplicate-request cooldown is not proof that retry is safe.
|
|
19
|
+
- Initial raw-CDP interception setup errors now reject attachment. A child target that cannot be armed stays paused for operator recovery. Supply a browser-level, session-aware CDP connection for raw CDP; a page-scoped Playwright `CDPSession` is insufficient.
|
|
20
|
+
- Playwright attachment rejects contexts with active service workers. Create checkout contexts with `serviceWorkers: 'block'`; checking an existing context cannot prevent later worker registration.
|
|
21
|
+
- Observer callback failures no longer interrupt payment handoff. Event failure summaries omit processor/API response bodies and request query strings.
|
|
22
|
+
|
|
23
|
+
### Verification
|
|
24
|
+
|
|
25
|
+
Deterministic transport and lifecycle tests cover failure recovery and retry guards. Local Chromium fixtures cover nested cross-origin frames, same-page post-payment work, and blocking immediate Stripe payment-method-to-intent fetch chains for both adapters, including an unrelated first intent and changed amount/currency. All payment endpoints in those fixtures are local stubs; they do not establish production provider or processor coverage.
|
package/README.md
CHANGED
|
@@ -30,6 +30,9 @@ your agent ──drives──> merchant checkout
|
|
|
30
30
|
npm i @agent-cards/checkout
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
Upgrading from 0.2.x? Read the [0.3.0 migration notes](./CHANGELOG.md), especially
|
|
34
|
+
the unknown-outcome, cancellation and browser-context requirements.
|
|
35
|
+
|
|
33
36
|
## Use it
|
|
34
37
|
|
|
35
38
|
Two lines against a CDP session you already have:
|
|
@@ -118,14 +121,15 @@ before you put that in writing.
|
|
|
118
121
|
|
|
119
122
|
## Supported processors
|
|
120
123
|
|
|
121
|
-
|
|
124
|
+
Coverage is specific to the processor request format, merchant setup, browser transport and follow-up flow. A recognized endpoint is not proof that every store using that processor completes checkout.
|
|
122
125
|
|
|
123
126
|
| Processor | Status |
|
|
124
127
|
|---|---|
|
|
125
128
|
| Shopify | supported, verified end to end |
|
|
126
|
-
| Stripe |
|
|
129
|
+
| Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms with `amountCents` + `currency` use backend amount verification. Browser token-to-intent continuation is unsupported and held. Validate the exact merchant flow before pilot use |
|
|
127
130
|
| Braintree / PayPal | supported, verified end to end |
|
|
128
131
|
| Checkout.com | supported |
|
|
132
|
+
| VGS Collect (Very Good Security; Wolt) | not supported: VGS's proxy aliases only submissions from its own iframe, so a replay from the cardholder's device is refused by the merchant (verified on Wolt, 2026-09-03). Not recognized, so the agent's browser is not paused there |
|
|
129
133
|
| Adyen | supported (mode `cse`): the vault encrypts the card for Adyen on the cardholder's device and your browser sends it |
|
|
130
134
|
| Tranzila | supported (mode `hosted_form`): the cardholder finishes on Tranzila's own page; the paused form navigation resolves to a synthetic page, and you poll the merchant's order state |
|
|
131
135
|
|
|
@@ -146,7 +150,20 @@ three; the difference matters if you drive `authorize()` yourself.
|
|
|
146
150
|
|
|
147
151
|
- **`token`** (every processor but Adyen). The cardholder's device calls the
|
|
148
152
|
processor and reports its answer; `authorize()` resolves with `status`,
|
|
149
|
-
`headers` and `body` to fulfill the paused request with.
|
|
153
|
+
`headers` and `body` to fulfill the paused request with. The browser checks
|
|
154
|
+
a fulfilled answer exactly as it checks a real one, so when the page called
|
|
155
|
+
the processor cross-origin (Stripe always does: Checkout on
|
|
156
|
+
`checkout.stripe.com` and Elements in the `js.stripe.com` frame both fetch
|
|
157
|
+
`api.stripe.com`) the answer must carry `access-control-allow-origin` for
|
|
158
|
+
the request's own `Origin`, or the page's fetch rejects and the checkout
|
|
159
|
+
reports a connection error even though the cardholder approved. The
|
|
160
|
+
adapters add those headers (`corsHeadersFor` + `withCorsHeaders`, exported
|
|
161
|
+
for a runtime that fulfills by hand, and `corsDecision` when you also want
|
|
162
|
+
the reason) and report the decision on the `authorized` event as `cors`:
|
|
163
|
+
`echoed`, `same_origin`, or `none` (no usable Origin on the request, so
|
|
164
|
+
the page could not read the answer). Only what a browser serializes is
|
|
165
|
+
echoed: one canonical http(s) origin, or the opaque `null`. Shopify's
|
|
166
|
+
card iframe posts to its own origin, so it never needed them.
|
|
150
167
|
- **`cse`** (Adyen). Adyen's own page SDK encrypts the card before the request
|
|
151
168
|
leaves the browser, so the paused body carries ciphertext. The cardholder's
|
|
152
169
|
device produces the same ciphertext under the merchant's Adyen public key
|
|
@@ -199,8 +216,9 @@ three; the difference matters if you drive `authorize()` yourself.
|
|
|
199
216
|
`syncRegistry()` asks the API for `SUPPORTED_MODES` only
|
|
200
217
|
(`token,cse,hosted_form`), so a processor whose flow this build cannot finish
|
|
201
218
|
is never paused; the API serves `hosted_form` entries only to callers that ask.
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
A registry mode this SDK cannot finish throws `UnsupportedModeError` before
|
|
220
|
+
creation. An approval returned in an unexpected mode has an unknown outcome
|
|
221
|
+
and holds the attachment for reconciliation. The `authorized` event's detail names
|
|
204
222
|
the `mode`, the `authorizationId` and, for `cse`, the `fields` that were
|
|
205
223
|
substituted; it never carries ciphertext. The `submitted_on_device` event's
|
|
206
224
|
detail names the `authorizationId`, `submittedAt` and `outcome:
|
|
@@ -210,8 +228,7 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
|
|
|
210
228
|
|
|
211
229
|
## Errors worth handling
|
|
212
230
|
|
|
213
|
-
- `ApprovalTimeoutError` — the
|
|
214
|
-
we have completed checkouts after a 5.5 minute approval delay.
|
|
231
|
+
- `ApprovalTimeoutError` — the server confirms the authorization expired without a replay attempt. A local deadline is different: `PaymentOutcomeUnknownError` means the approval link may still be valid, so reconcile the merchant order before another attempt.
|
|
215
232
|
- `ApprovalDeclinedError` — the user said no.
|
|
216
233
|
- `AmountMismatchError`: the processor's amount did not match the amount the
|
|
217
234
|
user was (or would have been) asked to approve. Nothing was charged. An
|
|
@@ -262,8 +279,9 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
|
|
|
262
279
|
- `CardEncryptedError`: this processor encrypts the card in-page and its
|
|
263
280
|
registry entry does not (yet) say the vault can produce that ciphertext;
|
|
264
281
|
route the purchase to an Agentcard-issued card instead.
|
|
265
|
-
- `UnsupportedModeError`: the
|
|
266
|
-
|
|
282
|
+
- `UnsupportedModeError`: the registry requests a mode this SDK cannot finish
|
|
283
|
+
before an authorization exists. Upgrade. An unexpected approved mode instead
|
|
284
|
+
raises `PaymentOutcomeUnknownError` and requires reconciliation.
|
|
267
285
|
- `SubstitutionError`: a `cse` approval could not be written into the paused
|
|
268
286
|
body (the four encrypted fields were not there). The request is failed and
|
|
269
287
|
the next one is judged afresh.
|
|
@@ -278,3 +296,167 @@ unrelated transitive versions). Build it with the workspace TypeScript:
|
|
|
278
296
|
```bash
|
|
279
297
|
cd packages/checkout && pnpm build && pnpm test
|
|
280
298
|
```
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
## Browser integration and merchant outcomes
|
|
302
|
+
|
|
303
|
+
`attachToPlaywright` works with an existing Chromium page reached through
|
|
304
|
+
`chromium.connectOverCDP`. Browserbase supplies `session.connectUrl`; Kernel
|
|
305
|
+
supplies `browser.cdp_ws_url`; a custom browser must expose a compatible CDP
|
|
306
|
+
endpoint. This is Agentcard's **direct SDK** path. Kernel's native Vault alias
|
|
307
|
+
integration is a separate provider adapter with its own coverage and lifecycle;
|
|
308
|
+
do not install both interceptors on the same checkout without validating how
|
|
309
|
+
those routes interact.
|
|
310
|
+
|
|
311
|
+
The local browser suite validates Chromium and nested cross-origin frames over
|
|
312
|
+
both Playwright routing and a raw, session-aware CDP connection. It does not
|
|
313
|
+
establish live Browserbase, Kernel, 3DS or merchant coverage. A raw page-scoped
|
|
314
|
+
Playwright `CDPSession` is not the `CdpLike` interface. Raw CDP must preserve the
|
|
315
|
+
`sessionId` on every command/event and allow recursive target attachment.
|
|
316
|
+
Initial arming errors reject `attachToCdp`; a child that cannot be armed remains
|
|
317
|
+
paused and reports `browser_interception_unavailable` for operator recovery.
|
|
318
|
+
|
|
319
|
+
Use a checkout context created with `serviceWorkers: 'block'`. Playwright cannot
|
|
320
|
+
route requests intercepted by a service worker. The SDK rejects already active
|
|
321
|
+
service workers, but that check cannot prevent a site from registering one
|
|
322
|
+
later in an existing context configured to allow them. Attach before entering
|
|
323
|
+
card fields; keep the existing checkout tab. Separate popup tabs need their own
|
|
324
|
+
attachment. A page route does not cover a popup's first navigation; a popup
|
|
325
|
+
which submits payment on that navigation requires a separately validated
|
|
326
|
+
context/browser-level integration. Existing `page.route` handlers must call
|
|
327
|
+
`route.fallback()` when they do not handle a request; later routes have priority.
|
|
328
|
+
|
|
329
|
+
Both adapters now return a controller; existing code that ignores the return
|
|
330
|
+
value continues to work. Choose `requireMerchantResult: true` for a pilot:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const checkout = await attachToPlaywright(page, {
|
|
334
|
+
vault, user, merchant, amountCents, currency,
|
|
335
|
+
requireMerchantResult: true,
|
|
336
|
+
onStateChange: state => recordState(state),
|
|
337
|
+
onUserAction: action => deliverPrivatelyToUser(action),
|
|
338
|
+
resolveMerchantResult: async state => readMerchantOrder(state),
|
|
339
|
+
paymentEndpoints: [
|
|
340
|
+
{ origin: 'https://payments.example.com', pathname: '/submit', methods: ['POST'] },
|
|
341
|
+
],
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Your existing agent dispatches checkout. Later, once the paused request resumes:
|
|
345
|
+
const state = await checkout.reconcile();
|
|
346
|
+
if (state.status === 'completed' && state.orderId) await finishAgentTask(state.orderId);
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`resolveMerchantResult` must read an authoritative merchant order/receipt tied
|
|
350
|
+
to this attempt. It returns one of:
|
|
351
|
+
|
|
352
|
+
- `{ status: 'completed', orderId }`: merchant-confirmed success.
|
|
353
|
+
- `{ status: 'failed' }`: merchant confirmed the attempt failed; no successful payment/order exists.
|
|
354
|
+
- `{ status: 'pending' }` or `{ status: 'unknown' }`: keep waiting or reconcile; never click Pay again.
|
|
355
|
+
- `{ status: 'requires_user_action', reason: '3ds' | 'redirect' | 'other' }`: deliver your own browser live view or supported challenge UI to the user.
|
|
356
|
+
|
|
357
|
+
The SDK does not infer order success from `authorized` or a tokenization reply,
|
|
358
|
+
and does not claim to detect or solve arbitrary 3DS challenges. Your merchant
|
|
359
|
+
resolver (or `checkout.requestUserAction('3ds')` when your browser observes it)
|
|
360
|
+
drives that hook. Deliver `onUserAction` approval URLs privately: they are
|
|
361
|
+
capabilities and never belong in general telemetry. Observer exceptions are
|
|
362
|
+
isolated from the payment handoff.
|
|
363
|
+
|
|
364
|
+
With `requireMerchantResult`, subsequent card requests stay blocked after
|
|
365
|
+
handoff. Stripe `/v1/payment_methods` and `/v1/tokens` handoffs always hold further
|
|
366
|
+
recognized card requests, even when that option is false. A tokenization approval has no
|
|
367
|
+
authoritative binding to a specific PaymentIntent, amount or currency. The first
|
|
368
|
+
observed confirm cannot supply that binding. The SDK therefore blocks every
|
|
369
|
+
follow-up confirm on that attachment, including the same token, an unrelated
|
|
370
|
+
intent, changed amounts and retries. It reports `awaiting_merchant` with reason
|
|
371
|
+
`stripe_tokenization_unbound`, while ordinary browser traffic stays available.
|
|
372
|
+
There is no automatic token-to-intent continuation or merchant-continuation hook.
|
|
373
|
+
Unrecognized merchant-server endpoints remain outside this guard unless listed
|
|
374
|
+
in `paymentEndpoints`; this is not a guarantee against a merchant charging a
|
|
375
|
+
saved token on its own server.
|
|
376
|
+
A direct card-bearing PaymentIntent confirm remains supported with the backend's
|
|
377
|
+
existing amount verification when `amountCents` and `currency` are supplied.
|
|
378
|
+
|
|
379
|
+
Hosted-form submissions also always stay blocked because their payment outcome
|
|
380
|
+
is unverified. `reconcile()` calls the resolver once, coalescing concurrent calls.
|
|
381
|
+
After an explicit merchant-confirmed failure, the application may call
|
|
382
|
+
`checkout.retryAfterMerchantFailure({ status: 'failed' })` to permit another
|
|
383
|
+
attempt where the attachment permits recovery. This is an assertion from your
|
|
384
|
+
merchant integration, not a timeout or a best guess. Completed orders, cancelled
|
|
385
|
+
attachments and attachments that issued an unbound Stripe token cannot reset
|
|
386
|
+
this way, including when the token's browser delivery acknowledgement was lost.
|
|
387
|
+
Reconcile the merchant outcome and use a separately validated checkout flow;
|
|
388
|
+
do not reuse that token in a new attachment as a workaround. Configuration and
|
|
389
|
+
unsupported-mode failures require fixing the integration. Bank flows requiring
|
|
390
|
+
another confirmation and other stored-token chains remain unverified.
|
|
391
|
+
|
|
392
|
+
Lost authorization polling, local approval timeouts, or interrupted browser
|
|
393
|
+
handoffs produce `outcome_unknown` and block automatic retry. The thrown
|
|
394
|
+
`PaymentOutcomeUnknownError` carries `authorizationId` when creation was
|
|
395
|
+
acknowledged. `checkout.cancel()` stops the local attachment and polling; it
|
|
396
|
+
does not revoke a pending approval link or undo a processor payment. Cancellation
|
|
397
|
+
after an attempt starts is therefore unknown until reconciled. A cancelled
|
|
398
|
+
attachment cannot restart.
|
|
399
|
+
|
|
400
|
+
### Unsupported endpoints
|
|
401
|
+
|
|
402
|
+
`paymentEndpoints` is an explicit list supplied by the integrator after observing
|
|
403
|
+
the site's payment requests. Each guard uses a canonical origin, exact path and
|
|
404
|
+
mutation methods; it never examines or logs card bodies. If a guarded endpoint
|
|
405
|
+
is not recognized, the SDK aborts it and reports `unsupported_checkout` /
|
|
406
|
+
`unsupported` without creating an approval. Preflights and ordinary page traffic
|
|
407
|
+
continue. There is no wildcard or intercept-all fallback, and no automatic
|
|
408
|
+
conversion to an issued card. Unknown endpoints absent from these guards remain
|
|
409
|
+
untouched; the SDK cannot identify every payment request from its URL.
|
|
410
|
+
|
|
411
|
+
### Runnable integration and local verification
|
|
412
|
+
|
|
413
|
+
`examples/existing-browser.mjs` runs against an existing provider session, using
|
|
414
|
+
an application-owned driver module for the agent's actions, user communication
|
|
415
|
+
and merchant-result resolver. Set `CHECKOUT_DRIVER` to that module's absolute
|
|
416
|
+
path and `CHECKOUT_CDP_URL` to the provider connection URL; optionally select the
|
|
417
|
+
existing tab with `CHECKOUT_PAGE_INDEX`. The module must export
|
|
418
|
+
`prepareCheckout(page)`, `submitCheckout(page)`, `resolveMerchantResult({page,
|
|
419
|
+
state})`, `onUserAction(action, {page})`, and `finishAfterPayment({page, orderId})`.
|
|
420
|
+
`prepareCheckout` returns the checkout options above. `submitCheckout` dispatches
|
|
421
|
+
the existing agent's approved purchase and returns without waiting for approval.
|
|
422
|
+
Install `playwright-core` in the example's host project. The SDK itself keeps no
|
|
423
|
+
runtime dependencies. The example is integration scaffolding, not a universal
|
|
424
|
+
merchant driver and not evidence of a live provider checkout.
|
|
425
|
+
|
|
426
|
+
```sh
|
|
427
|
+
pnpm build
|
|
428
|
+
pnpm test
|
|
429
|
+
# Uses installed playwright-core, falling back to the monorepo backend dependency.
|
|
430
|
+
# Set CHECKOUT_CHROME_PATH if Chromium is not installed in Playwright's cache.
|
|
431
|
+
pnpm test:browser
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
The browser fixtures never contact a payment service. The general suite uses
|
|
435
|
+
`psp.invalid`; the Stripe continuation suite forces `api.stripe.com` through an
|
|
436
|
+
allowlisted loopback proxy and a temporary self-signed TLS stub (requires the
|
|
437
|
+
`openssl` CLI). All other proxy destinations are rejected. Both suites use an
|
|
438
|
+
in-process Agentcard API fixture and loopback merchant pages. It proves nested-frame pause/resume, agent control during approval,
|
|
439
|
+
post-payment tasks in the same page, decline/expiry/cancel, unknown-outcome retry
|
|
440
|
+
blocking, explicit unsupported endpoint behavior, and blocking an immediate real-browser
|
|
441
|
+
Stripe token-to-intent fetch chain, including unrelated first intents, changed
|
|
442
|
+
amounts/currencies and delayed CDP acknowledgement. It does not test card
|
|
443
|
+
cryptography, real bank authorization or a cloud-provider deployment.
|
|
444
|
+
|
|
445
|
+
Provider/API references checked for this integration:
|
|
446
|
+
[Playwright CDP](https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp),
|
|
447
|
+
[Playwright routing limitations](https://playwright.dev/docs/api/class-page#page-route),
|
|
448
|
+
[Browserbase Playwright quickstart](https://docs.browserbase.com/welcome/quickstarts/playwright),
|
|
449
|
+
[Kernel native Agentcard integration](https://www.kernel.sh/docs/integrations/payments/agentcard).
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
The authenticated `GET /v2/checkout/coverage` endpoint describes direct-SDK
|
|
453
|
+
processor modes, limitations and verification levels. Use
|
|
454
|
+
`POST /v2/checkout/coverage/assess` with up to 1,000 uniquely identified cases:
|
|
455
|
+
`{ cases: [{ id, request_url, method: "POST", scenario: "one_time", weight: 1,
|
|
456
|
+
requires_3ds: false }] }`. Scenarios also include `save_card`,
|
|
457
|
+
`subscription_initial` and `subscription_renewal`. The endpoint assesses request
|
|
458
|
+
recognition, not purchases: `recognized`, `unsupported` and `unverified` are
|
|
459
|
+
coverage classifications, `recognized_traffic_share` is traffic-weighted, and
|
|
460
|
+
`purchase_success_rate` stays null without observed merchant outcomes. Do not
|
|
461
|
+
substitute the assessor for a browser/merchant validation run or use native
|
|
462
|
+
Kernel adapter coverage as evidence for this SDK's coverage.
|
package/dist/cdp.d.ts
CHANGED
|
@@ -1,13 +1,77 @@
|
|
|
1
1
|
import { type VaultClient } from './client.js';
|
|
2
|
+
import { type CheckoutController, type LifecycleOptions, type PaymentEndpointGuard } from './lifecycle.js';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
4
|
+
* The CORS headers a fulfilled CROSS-ORIGIN request needs, or null when the
|
|
5
|
+
* request is same-origin (or carries no Origin, so no CORS check applies).
|
|
6
|
+
*
|
|
7
|
+
* The browser checks a fulfilled response exactly as it checks a real one. A
|
|
8
|
+
* page that fetches a processor on another origin therefore needs
|
|
9
|
+
* `access-control-allow-origin` on the synthetic answer, or its fetch rejects
|
|
10
|
+
* with "Failed to fetch" and the page never sees the processor's reply, even
|
|
11
|
+
* though the cardholder approved and the processor answered the vault.
|
|
12
|
+
* Shopify never hit this on its current host: the checkout.pci.shopifyinc.com
|
|
13
|
+
* card iframe posts to its own origin (its older deposit.<region>.shopifycs.com
|
|
14
|
+
* host is called from the checkout.shopifycs.com frame, cross-origin, and gets
|
|
15
|
+
* the answer like everyone else). Stripe hits it on every surface (Checkout on
|
|
16
|
+
* checkout.stripe.com or a merchant domain, and Elements in the js.stripe.com
|
|
17
|
+
* frame, all call api.stripe.com), and so does every other processor whose
|
|
18
|
+
* card frame calls a separate API host. Observed live on
|
|
19
|
+
* 2026-09-03: the vault replayed a Stripe PaymentMethod into a raw-CDP
|
|
20
|
+
* runtime, the browser refused the answer for want of this header, and
|
|
21
|
+
* Stripe Checkout showed "We are experiencing connection issues".
|
|
22
|
+
*
|
|
23
|
+
* The exact Origin is echoed rather than `*`: a credentialed request refuses
|
|
24
|
+
* `*`, the echo satisfies both. The processor's own value can never reach
|
|
25
|
+
* this adapter (a browser does not expose that header to the page that
|
|
26
|
+
* replayed the call), so whatever the replay carries under these names is
|
|
27
|
+
* replaced by the one value that is right for THIS request. Playwright adds
|
|
28
|
+
* the same headers inside route.fulfill when a cross-origin fulfill carries
|
|
29
|
+
* none (microsoft/playwright#12929), which is why attachToPlaywright never
|
|
30
|
+
* needed this; it writes them itself anyway, replacing a stale value, so both
|
|
31
|
+
* adapters answer the vault's replays identically.
|
|
32
|
+
*
|
|
33
|
+
* This widens nothing. A tokenization endpoint is built for anonymous
|
|
34
|
+
* browsers and answers every origin (`access-control-allow-origin: *` on
|
|
35
|
+
* Stripe's and Shopify's own replies), so the page that made the request
|
|
36
|
+
* could always read the processor's answer to it; the replay is made exactly
|
|
37
|
+
* as visible, to exactly that page. Whether a card goes anywhere at all is
|
|
38
|
+
* decided by the cardholder on the approval screen, never by this header.
|
|
39
|
+
*
|
|
40
|
+
* Only what a browser serializes is ever echoed: one canonical http(s)
|
|
41
|
+
* origin (`new URL(origin).origin === origin`), or the opaque `null` a
|
|
42
|
+
* sandboxed or data: document sends, which Chrome matches against
|
|
43
|
+
* `access-control-allow-origin: null` and which Playwright echoes too. That
|
|
44
|
+
* refuses userinfo, a path, an explicit default port, several origins in one
|
|
45
|
+
* value, or a control character that would break the fulfill after the
|
|
46
|
+
* cardholder already approved. Anything refused simply gets no CORS answer,
|
|
47
|
+
* which is what every fulfill got before this existed.
|
|
48
|
+
*/
|
|
49
|
+
export declare function corsHeadersFor(url: string, requestHeaders: Record<string, string> | undefined): Record<string, string> | null;
|
|
50
|
+
/** How a fulfill was answered, reported on the `authorized` event so a silent CORS failure is diagnosable from events alone. */
|
|
51
|
+
export type CorsOutcome = 'echoed' | 'same_origin' | 'none';
|
|
52
|
+
/** The CORS answer and its reason: `none` when no usable Origin was sent (or the url is not http(s)), `same_origin` when no check applies. */
|
|
53
|
+
export declare function corsDecision(url: string, requestHeaders: Record<string, string> | undefined): {
|
|
54
|
+
headers: Record<string, string> | null;
|
|
55
|
+
outcome: CorsOutcome;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* `headers` with the CORS answer for this request written in; the object
|
|
59
|
+
* itself when none is needed. Any header the answer names is replaced
|
|
60
|
+
* whatever its case, so a name is never sent twice. The answer varies by
|
|
61
|
+
* Origin, and a `vary` the replay already carries is extended rather than
|
|
62
|
+
* replaced (or left alone when it already covers Origin or is `*`); a `vary`
|
|
63
|
+
* the answer itself names is taken as given.
|
|
64
|
+
*/
|
|
65
|
+
export declare function withCorsHeaders(headers: Record<string, string>, cors: Record<string, string> | null): Record<string, string>;
|
|
66
|
+
/**
|
|
67
|
+
* Browser-level, session-aware CDP connection. A page-scoped Playwright or
|
|
68
|
+
* Puppeteer CDPSession is NOT this interface; use attachToPlaywright for those.
|
|
5
69
|
*/
|
|
6
70
|
export interface CdpLike {
|
|
7
71
|
send(method: string, params?: any, sessionId?: string): Promise<any>;
|
|
8
72
|
on(handler: (method: string, params: any, sessionId?: string) => void): void;
|
|
9
73
|
}
|
|
10
|
-
export interface AttachOptions {
|
|
74
|
+
export interface AttachOptions extends LifecycleOptions {
|
|
11
75
|
vault: VaultClient;
|
|
12
76
|
user: string;
|
|
13
77
|
merchant: string;
|
|
@@ -21,6 +85,10 @@ export interface AttachOptions {
|
|
|
21
85
|
*/
|
|
22
86
|
amountCents?: number;
|
|
23
87
|
currency?: string;
|
|
88
|
+
cardId?: string;
|
|
89
|
+
timeoutMs?: number;
|
|
90
|
+
/** Explicit payment endpoints to block if the registry cannot handle their method/format. Unlisted traffic is untouched. */
|
|
91
|
+
paymentEndpoints?: readonly PaymentEndpointGuard[];
|
|
24
92
|
onApprovalUrl?: (url: string) => void;
|
|
25
93
|
onEvent?: (e: {
|
|
26
94
|
type: string;
|
|
@@ -48,7 +116,7 @@ export interface AttachOptions {
|
|
|
48
116
|
* match. Patterns are resolved once, at attach, so every nested target ends up
|
|
49
117
|
* armed identically.
|
|
50
118
|
*/
|
|
51
|
-
export declare function attachToCdp(cdp: CdpLike, pageSessionId: string, opts: AttachOptions): Promise<
|
|
119
|
+
export declare function attachToCdp(cdp: CdpLike, pageSessionId: string, opts: AttachOptions): Promise<CheckoutController>;
|
|
52
120
|
/**
|
|
53
121
|
* Playwright convenience wrapper — the path for cloud browsers that hand you a
|
|
54
122
|
* CDP websocket (Kernel's `cdp_ws_url`, Browserbase, etc.):
|
|
@@ -60,4 +128,4 @@ export declare function attachToCdp(cdp: CdpLike, pageSessionId: string, opts: A
|
|
|
60
128
|
* Uses Playwright's own request routing, which already spans subframes — see
|
|
61
129
|
* the note in the body for why a hand-rolled CDPSession does not work here.
|
|
62
130
|
*/
|
|
63
|
-
export declare function attachToPlaywright(page: any, opts: AttachOptions): Promise<
|
|
131
|
+
export declare function attachToPlaywright(page: any, opts: AttachOptions): Promise<CheckoutController>;
|