@agent-cards/checkout 0.2.1 → 0.3.1

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 ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## 0.3.1
4
+
5
+ - Detect a merchant request abort or owning frame/page closure while approval is pending. The attachment holds an unknown outcome and blocks automatic retries; an expired request is never reported as an authorized handoff.
6
+ - Retire pending authorizations through the org-scoped cancellation endpoint when the merchant request disappears. Cancellation can win only before the processor-send boundary; started or completed replay remains unknown to this operation. A late creation response is drained so its authorization ID can be cancelled without exposing another approval link.
7
+ - Square token mode uses the vault's verified browser TLS relay. The relay keeps TLS termination and card plaintext on the cardholder device and Square. SDK 0.2.1 can complete a prompt approval but does not contain the native-timeout lifecycle fixes; upgrade to 0.3.1 for Square checkout.
8
+ - Square's native tokenization request still expires after about 10 seconds, including approval and token handoff. Delayed approval cannot complete that checkout; a later SCA challenge has its own lifetime after handoff. This release does not extend the deadline or retry a payment after timeout. Reconcile the merchant outcome and explicitly begin another checkout when required.
9
+
10
+ ## 0.3.0
11
+
12
+ ### Browser checkout lifecycle
13
+
14
+ - `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.
15
+ - `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.
16
+ - 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.
17
+ - Exact `paymentEndpoints` guards abort unsupported payment endpoints identified by the integrator. Unknown endpoints outside those guards remain untouched.
18
+ - 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.
19
+
20
+ ### Migration from 0.2.x
21
+
22
+ - 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.
23
+ - `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.
24
+ - 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.
25
+ - 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.
26
+ - 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.
27
+ - Playwright attachment rejects contexts with active service workers. Create checkout contexts with `serviceWorkers: 'block'`; checking an existing context cannot prevent later worker registration.
28
+ - Observer callback failures no longer interrupt payment handoff. Event failure summaries omit processor/API response bodies and request query strings.
29
+
30
+ ### Verification
31
+
32
+ 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 [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:
@@ -76,8 +79,11 @@ Stripe's minor units, so the approval screen, the notifications and every
76
79
  read show one amount. Tokenization requests carry no amount, so there the
77
80
  pair is shown and reported (`amountAuthority: 'display_only'`), not enforced.
78
81
 
79
- Then let your agent click "Pay" like it always does. `attachToCdp` holds the
80
- request open until the cardholder approves, so the checkout simply continues.
82
+ Then let your agent click "Pay" like it always does. `attachToCdp` pauses the
83
+ request for approval and resumes it only while the merchant request remains
84
+ live. Merchant timeouts still apply: Square's observed tokenization deadline
85
+ is about 10 seconds for approval and token handoff, so delayed approval cannot
86
+ complete that checkout.
81
87
 
82
88
  Playwright:
83
89
 
@@ -118,12 +124,12 @@ before you put that in writing.
118
124
 
119
125
  ## Supported processors
120
126
 
121
- Merchants inherit their processor, so one entry covers every store on it.
127
+ 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
128
 
123
129
  | Processor | Status |
124
130
  |---|---|
125
131
  | Shopify | supported, verified end to end |
126
- | Stripe | supported, verified end to end |
132
+ | 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
133
  | Braintree / PayPal | supported, verified end to end |
128
134
  | Checkout.com | supported |
129
135
  | 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 |
@@ -213,8 +219,9 @@ three; the difference matters if you drive `authorize()` yourself.
213
219
  `syncRegistry()` asks the API for `SUPPORTED_MODES` only
214
220
  (`token,cse,hosted_form`), so a processor whose flow this build cannot finish
215
221
  is never paused; the API serves `hosted_form` entries only to callers that ask.
216
- An approval in some other mode throws `UnsupportedModeError`, which is
217
- terminal for that page (upgrade the SDK). The `authorized` event's detail names
222
+ A registry mode this SDK cannot finish throws `UnsupportedModeError` before
223
+ creation. An approval returned in an unexpected mode has an unknown outcome
224
+ and holds the attachment for reconciliation. The `authorized` event's detail names
218
225
  the `mode`, the `authorizationId` and, for `cse`, the `fields` that were
219
226
  substituted; it never carries ciphertext. The `submitted_on_device` event's
220
227
  detail names the `authorizationId`, `submittedAt` and `outcome:
@@ -224,8 +231,7 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
224
231
 
225
232
  ## Errors worth handling
226
233
 
227
- - `ApprovalTimeoutError` — the user never approved. Default window is 15 minutes;
228
- we have completed checkouts after a 5.5 minute approval delay.
234
+ - `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.
229
235
  - `ApprovalDeclinedError` — the user said no.
230
236
  - `AmountMismatchError`: the processor's amount did not match the amount the
231
237
  user was (or would have been) asked to approve. Nothing was charged. An
@@ -276,8 +282,9 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
276
282
  - `CardEncryptedError`: this processor encrypts the card in-page and its
277
283
  registry entry does not (yet) say the vault can produce that ciphertext;
278
284
  route the purchase to an Agentcard-issued card instead.
279
- - `UnsupportedModeError`: the approval came back in a mode this build of the
280
- SDK cannot finish. Terminal for the page; upgrade.
285
+ - `UnsupportedModeError`: the registry requests a mode this SDK cannot finish
286
+ before an authorization exists. Upgrade. An unexpected approved mode instead
287
+ raises `PaymentOutcomeUnknownError` and requires reconciliation.
281
288
  - `SubstitutionError`: a `cse` approval could not be written into the paused
282
289
  body (the four encrypted fields were not there). The request is failed and
283
290
  the next one is judged afresh.
@@ -292,3 +299,169 @@ unrelated transitive versions). Build it with the workspace TypeScript:
292
299
  ```bash
293
300
  cd packages/checkout && pnpm build && pnpm test
294
301
  ```
302
+
303
+
304
+ ## Browser integration and merchant outcomes
305
+
306
+ `attachToPlaywright` works with an existing Chromium page reached through
307
+ `chromium.connectOverCDP`. Browserbase supplies `session.connectUrl`; Kernel
308
+ supplies `browser.cdp_ws_url`; a custom browser must expose a compatible CDP
309
+ endpoint. This is Agentcard's **direct SDK** path. Kernel's native Vault alias
310
+ integration is a separate provider adapter with its own coverage and lifecycle;
311
+ do not install both interceptors on the same checkout without validating how
312
+ those routes interact.
313
+
314
+ The local browser suite validates Chromium and nested cross-origin frames over
315
+ both Playwright routing and a raw, session-aware CDP connection. It does not
316
+ establish live Browserbase, Kernel, 3DS or merchant coverage. A raw page-scoped
317
+ Playwright `CDPSession` is not the `CdpLike` interface. Raw CDP must preserve the
318
+ `sessionId` on every command/event and allow recursive target attachment.
319
+ Initial arming errors reject `attachToCdp`; a child that cannot be armed remains
320
+ paused and reports `browser_interception_unavailable` for operator recovery.
321
+
322
+ Use a checkout context created with `serviceWorkers: 'block'`. Playwright cannot
323
+ route requests intercepted by a service worker. The SDK rejects already active
324
+ service workers, but that check cannot prevent a site from registering one
325
+ later in an existing context configured to allow them. Attach before entering
326
+ card fields; keep the existing checkout tab. Separate popup tabs need their own
327
+ attachment. A page route does not cover a popup's first navigation; a popup
328
+ which submits payment on that navigation requires a separately validated
329
+ context/browser-level integration. Existing `page.route` handlers must call
330
+ `route.fallback()` when they do not handle a request; later routes have priority.
331
+
332
+ Both adapters now return a controller; existing code that ignores the return
333
+ value continues to work. Choose `requireMerchantResult: true` for a pilot:
334
+
335
+ ```ts
336
+ const checkout = await attachToPlaywright(page, {
337
+ vault, user, merchant, amountCents, currency,
338
+ requireMerchantResult: true,
339
+ onStateChange: state => recordState(state),
340
+ onUserAction: action => deliverPrivatelyToUser(action),
341
+ resolveMerchantResult: async state => readMerchantOrder(state),
342
+ paymentEndpoints: [
343
+ { origin: 'https://payments.example.com', pathname: '/submit', methods: ['POST'] },
344
+ ],
345
+ });
346
+
347
+ // Your existing agent dispatches checkout. Later, once the paused request resumes:
348
+ const state = await checkout.reconcile();
349
+ if (state.status === 'completed' && state.orderId) await finishAgentTask(state.orderId);
350
+ ```
351
+
352
+ `resolveMerchantResult` must read an authoritative merchant order/receipt tied
353
+ to this attempt. It returns one of:
354
+
355
+ - `{ status: 'completed', orderId }`: merchant-confirmed success.
356
+ - `{ status: 'failed' }`: merchant confirmed the attempt failed; no successful payment/order exists.
357
+ - `{ status: 'pending' }` or `{ status: 'unknown' }`: keep waiting or reconcile; never click Pay again.
358
+ - `{ status: 'requires_user_action', reason: '3ds' | 'redirect' | 'other' }`: deliver your own browser live view or supported challenge UI to the user.
359
+
360
+ The SDK does not infer order success from `authorized` or a tokenization reply,
361
+ and does not claim to detect or solve arbitrary 3DS challenges. Your merchant
362
+ resolver (or `checkout.requestUserAction('3ds')` when your browser observes it)
363
+ drives that hook. Deliver `onUserAction` approval URLs privately: they are
364
+ capabilities and never belong in general telemetry. Observer exceptions are
365
+ isolated from the payment handoff.
366
+
367
+ With `requireMerchantResult`, subsequent card requests stay blocked after
368
+ handoff. Stripe `/v1/payment_methods` and `/v1/tokens` handoffs always hold further
369
+ recognized card requests, even when that option is false. A tokenization approval has no
370
+ authoritative binding to a specific PaymentIntent, amount or currency. The first
371
+ observed confirm cannot supply that binding. The SDK therefore blocks every
372
+ follow-up confirm on that attachment, including the same token, an unrelated
373
+ intent, changed amounts and retries. It reports `awaiting_merchant` with reason
374
+ `stripe_tokenization_unbound`, while ordinary browser traffic stays available.
375
+ There is no automatic token-to-intent continuation or merchant-continuation hook.
376
+ Unrecognized merchant-server endpoints remain outside this guard unless listed
377
+ in `paymentEndpoints`; this is not a guarantee against a merchant charging a
378
+ saved token on its own server.
379
+ A direct card-bearing PaymentIntent confirm remains supported with the backend's
380
+ existing amount verification when `amountCents` and `currency` are supplied.
381
+
382
+ Hosted-form submissions also always stay blocked because their payment outcome
383
+ is unverified. `reconcile()` calls the resolver once, coalescing concurrent calls.
384
+ After an explicit merchant-confirmed failure, the application may call
385
+ `checkout.retryAfterMerchantFailure({ status: 'failed' })` to permit another
386
+ attempt where the attachment permits recovery. This is an assertion from your
387
+ merchant integration, not a timeout or a best guess. Completed orders, cancelled
388
+ attachments and attachments that issued an unbound Stripe token cannot reset
389
+ this way, including when the token's browser delivery acknowledgement was lost.
390
+ Reconcile the merchant outcome and use a separately validated checkout flow;
391
+ do not reuse that token in a new attachment as a workaround. Configuration and
392
+ unsupported-mode failures require fixing the integration. Bank flows requiring
393
+ another confirmation and other stored-token chains remain unverified.
394
+
395
+ Square saved-card checkout requires SDK 0.3.1 for merchant-request lifetime handling. Its observed native tokenization request expires after about 10 seconds, including approval-page loading, unlocking, approval, relay and token handoff. An approval that exceeds this window cannot finish that checkout. A subsequent SCA challenge has its own lifetime after the token handoff. This SDK does not pause Square's timers or automatically retry an expired checkout. If the merchant request aborts or its frame closes, the attachment blocks further card requests and tries to retire a pre-replay approval. A started replay or unconfirmed cancellation remains unknown. General delayed human approval is not supported by this Square flow.
396
+
397
+ Lost authorization polling, local approval timeouts, or interrupted browser
398
+ handoffs produce `outcome_unknown` and block automatic retry. The thrown
399
+ `PaymentOutcomeUnknownError` carries `authorizationId` when creation was
400
+ acknowledged. `checkout.cancel()` stops the local attachment and polling; it
401
+ does not revoke a pending approval link or undo a processor payment. Cancellation
402
+ after an attempt starts is therefore unknown until reconciled. A cancelled
403
+ attachment cannot restart.
404
+
405
+ ### Unsupported endpoints
406
+
407
+ `paymentEndpoints` is an explicit list supplied by the integrator after observing
408
+ the site's payment requests. Each guard uses a canonical origin, exact path and
409
+ mutation methods; it never examines or logs card bodies. If a guarded endpoint
410
+ is not recognized, the SDK aborts it and reports `unsupported_checkout` /
411
+ `unsupported` without creating an approval. Preflights and ordinary page traffic
412
+ continue. There is no wildcard or intercept-all fallback, and no automatic
413
+ conversion to an issued card. Unknown endpoints absent from these guards remain
414
+ untouched; the SDK cannot identify every payment request from its URL.
415
+
416
+ ### Runnable integration and local verification
417
+
418
+ `examples/existing-browser.mjs` runs against an existing provider session, using
419
+ an application-owned driver module for the agent's actions, user communication
420
+ and merchant-result resolver. Set `CHECKOUT_DRIVER` to that module's absolute
421
+ path and `CHECKOUT_CDP_URL` to the provider connection URL; optionally select the
422
+ existing tab with `CHECKOUT_PAGE_INDEX`. The module must export
423
+ `prepareCheckout(page)`, `submitCheckout(page)`, `resolveMerchantResult({page,
424
+ state})`, `onUserAction(action, {page})`, and `finishAfterPayment({page, orderId})`.
425
+ `prepareCheckout` returns the checkout options above. `submitCheckout` dispatches
426
+ the existing agent's approved purchase and returns without waiting for approval.
427
+ Install `playwright-core` in the example's host project. The SDK itself keeps no
428
+ runtime dependencies. The example is integration scaffolding, not a universal
429
+ merchant driver and not evidence of a live provider checkout.
430
+
431
+ ```sh
432
+ pnpm build
433
+ pnpm test
434
+ # Uses installed playwright-core, falling back to the monorepo backend dependency.
435
+ # Set CHECKOUT_CHROME_PATH if Chromium is not installed in Playwright's cache.
436
+ pnpm test:browser
437
+ ```
438
+
439
+ The browser fixtures never contact a payment service. The general suite uses
440
+ `psp.invalid`; the Stripe continuation suite forces `api.stripe.com` through an
441
+ allowlisted loopback proxy and a temporary self-signed TLS stub (requires the
442
+ `openssl` CLI). All other proxy destinations are rejected. Both suites use an
443
+ in-process Agentcard API fixture and loopback merchant pages. It proves nested-frame pause/resume, agent control during approval,
444
+ post-payment tasks in the same page, decline/expiry/cancel, unknown-outcome retry
445
+ blocking, explicit unsupported endpoint behavior, and blocking an immediate real-browser
446
+ Stripe token-to-intent fetch chain, including unrelated first intents, changed
447
+ amounts/currencies and delayed CDP acknowledgement. It does not test card
448
+ cryptography, real bank authorization or a cloud-provider deployment.
449
+
450
+ Provider/API references checked for this integration:
451
+ [Playwright CDP](https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp),
452
+ [Playwright routing limitations](https://playwright.dev/docs/api/class-page#page-route),
453
+ [Browserbase Playwright quickstart](https://docs.browserbase.com/welcome/quickstarts/playwright),
454
+ [Kernel native Agentcard integration](https://www.kernel.sh/docs/integrations/payments/agentcard).
455
+
456
+
457
+ The authenticated `GET /v2/checkout/coverage` endpoint describes direct-SDK
458
+ processor modes, limitations and verification levels. Use
459
+ `POST /v2/checkout/coverage/assess` with up to 1,000 uniquely identified cases:
460
+ `{ cases: [{ id, request_url, method: "POST", scenario: "one_time", weight: 1,
461
+ requires_3ds: false }] }`. Scenarios also include `save_card`,
462
+ `subscription_initial` and `subscription_renewal`. The endpoint assesses request
463
+ recognition, not purchases: `recognized`, `unsupported` and `unverified` are
464
+ coverage classifications, `recognized_traffic_share` is traffic-weighted, and
465
+ `purchase_success_rate` stays null without observed merchant outcomes. Do not
466
+ substitute the assessor for a browser/merchant validation run or use native
467
+ Kernel adapter coverage as evidence for this SDK's coverage.
package/dist/cdp.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type VaultClient } from './client.js';
2
+ import { type CheckoutController, type LifecycleOptions, type PaymentEndpointGuard } from './lifecycle.js';
2
3
  /**
3
4
  * The CORS headers a fulfilled CROSS-ORIGIN request needs, or null when the
4
5
  * request is same-origin (or carries no Origin, so no CORS check applies).
@@ -63,14 +64,14 @@ export declare function corsDecision(url: string, requestHeaders: Record<string,
63
64
  */
64
65
  export declare function withCorsHeaders(headers: Record<string, string>, cors: Record<string, string> | null): Record<string, string>;
65
66
  /**
66
- * Minimal shape of a CDP connection. Works with a raw websocket client, a
67
- * Puppeteer CDPSession, or Playwright's CDPSession.
67
+ * Browser-level, session-aware CDP connection. A page-scoped Playwright or
68
+ * Puppeteer CDPSession is NOT this interface; use attachToPlaywright for those.
68
69
  */
69
70
  export interface CdpLike {
70
71
  send(method: string, params?: any, sessionId?: string): Promise<any>;
71
72
  on(handler: (method: string, params: any, sessionId?: string) => void): void;
72
73
  }
73
- export interface AttachOptions {
74
+ export interface AttachOptions extends LifecycleOptions {
74
75
  vault: VaultClient;
75
76
  user: string;
76
77
  merchant: string;
@@ -84,6 +85,10 @@ export interface AttachOptions {
84
85
  */
85
86
  amountCents?: number;
86
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[];
87
92
  onApprovalUrl?: (url: string) => void;
88
93
  onEvent?: (e: {
89
94
  type: string;
@@ -111,7 +116,7 @@ export interface AttachOptions {
111
116
  * match. Patterns are resolved once, at attach, so every nested target ends up
112
117
  * armed identically.
113
118
  */
114
- export declare function attachToCdp(cdp: CdpLike, pageSessionId: string, opts: AttachOptions): Promise<void>;
119
+ export declare function attachToCdp(cdp: CdpLike, pageSessionId: string, opts: AttachOptions): Promise<CheckoutController>;
115
120
  /**
116
121
  * Playwright convenience wrapper — the path for cloud browsers that hand you a
117
122
  * CDP websocket (Kernel's `cdp_ws_url`, Browserbase, etc.):
@@ -123,4 +128,4 @@ export declare function attachToCdp(cdp: CdpLike, pageSessionId: string, opts: A
123
128
  * Uses Playwright's own request routing, which already spans subframes — see
124
129
  * the note in the body for why a hand-rolled CDPSession does not work here.
125
130
  */
126
- export declare function attachToPlaywright(page: any, opts: AttachOptions): Promise<void>;
131
+ export declare function attachToPlaywright(page: any, opts: AttachOptions): Promise<CheckoutController>;