@agent-cards/checkout 0.11.0 → 0.12.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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Complete a merchant-confirmed payment when the merchant supplies no order or receipt ID. Return `{ status: 'completed', confirmation: { kind: 'merchant_payment', authorizationId } }` from the merchant resolver using the current checkout authorization. The resolver must verify the original payment request, amount, currency, selected card and merchant success; HTTP 200 or a success page alone is insufficient. The completed state carries `confirmation` without inventing `orderId`, clears stale reasons and continues to block another payment. Existing completion results with a genuine `orderId` keep working.
6
+
5
7
  - Approve a Recurly checkout before its native card request starts. Call `prepare` with `psp: 'recurly'` and `environment: 'shared'`, then submit the merchant's form once. The same selected card and approved purchase bind a new-card POST on the US or EU endpoint. The matching API, database migration and Vault release are required. Token issuance and merchant payment completion remain separate results.
6
8
 
7
9
  - Keep the selected card's issuer in guest Mercado Pago Checkout Pro in Mexico. The Vault resolves the card's eight-digit prefix against the merchant's captured checkout configuration over browser TLS. The SDK returns the original card-token response and corrects one native card association. Missing configuration, ambiguous issuers, changed checkout context, and a different card brand or type stop continuation. The matching API, Vault and encrypted relay release is required. Other Mercado Pago checkout families and merchant payment completion need separate validation.
package/README.md CHANGED
@@ -367,17 +367,56 @@ const checkout = await attachToPlaywright(page, {
367
367
 
368
368
  // Your existing agent dispatches checkout. Later, once the paused request resumes:
369
369
  const state = await checkout.reconcile();
370
- if (state.status === 'completed' && state.orderId) await finishAgentTask(state.orderId);
370
+ if (state.status === 'completed') await finishAgentTask(state);
371
371
  ```
372
372
 
373
- `resolveMerchantResult` must read an authoritative merchant order/receipt tied
374
- to this attempt. It returns one of:
373
+ `resolveMerchantResult` must verify the merchant's result for the original
374
+ payment attempt. It returns one of:
375
375
 
376
- - `{ status: 'completed', orderId }`: merchant-confirmed success.
376
+ - `{ status: 'completed', orderId }`: merchant-confirmed success with a genuine order or receipt ID. Existing integrations keep this form.
377
+ - `{ status: 'completed', confirmation: { kind: 'merchant_payment', authorizationId } }`: merchant-confirmed payment when no order or receipt ID is available. The ID must match the current checkout authorization.
377
378
  - `{ status: 'failed' }`: merchant confirmed the attempt failed; no successful payment/order exists.
378
379
  - `{ status: 'pending' }` or `{ status: 'unknown' }`: keep waiting or reconcile; never click Pay again.
379
380
  - `{ status: 'requires_user_action', reason: '3ds' | 'redirect' | 'other' }`: deliver your own browser live view or supported challenge UI to the user.
380
381
 
382
+ For a merchant that confirms payment without returning an order ID, your
383
+ resolver can return the explicit payment confirmation:
384
+
385
+ ```ts
386
+ const checkout = await attachToPlaywright(page, {
387
+ vault, user, merchant, amount, currency,
388
+ requireMerchantResult: true,
389
+ resolveMerchantResult: async state => {
390
+ const payment = await readOriginalMerchantPayment(state);
391
+ if (!payment.confirmed || !state.authorizationId) return { status: 'unknown' };
392
+ return {
393
+ status: 'completed',
394
+ confirmation: {
395
+ kind: 'merchant_payment',
396
+ authorizationId: state.authorizationId,
397
+ },
398
+ };
399
+ },
400
+ });
401
+ ```
402
+
403
+ `readOriginalMerchantPayment` is your merchant-specific check. The check must
404
+ match the original payment request, amount, currency and selected card, and
405
+ verify authoritative merchant success for that attempt. HTTP 200 alone, a card
406
+ token, a success URL or text that anyone can open does not establish payment.
407
+ Copying `state.authorizationId` without checking the payment is insufficient.
408
+ The SDK checks the authorization binding; it does not independently authenticate
409
+ the merchant evidence supplied by your resolver.
410
+
411
+ Return one completion form at a time. The payment-confirmation form leaves
412
+ `state.orderId` absent and exposes `state.confirmation` with the exported
413
+ `MerchantPaymentConfirmation` type. Your consumer should finish on
414
+ `state.status === 'completed'` and treat `orderId` as optional. A missing or
415
+ mismatched authorization, or `{ status: 'completed' }` without either completion
416
+ form, leaves the outcome unknown. Confirmed completion clears stale failure or
417
+ authentication reasons and continues to block further payment submissions.
418
+ Never invent an order ID from a token or authorization ID.
419
+
381
420
  The SDK does not infer order success from `authorized` or a tokenization reply,
382
421
  and does not claim to detect or solve arbitrary 3DS challenges. Your merchant
383
422
  resolver (or `checkout.requestUserAction('3ds')` when your browser observes it)
@@ -388,22 +427,29 @@ isolated from the payment handoff.
388
427
  Native Stripe Checkout emits `checkout_blocked` before the existing `blocked`
389
428
  event when its local preparation rejects a request. Its
390
429
  `StripeCheckoutBlockedDetail` contains only fixed codes: `version: 1`,
391
- `processor: 'stripe'`, endpoint family, phase, stage, reason, gate state, and
392
- disposition. It contains no URLs, identifiers, request fields, or exception text.
430
+ `processor: 'stripe'`, endpoint family, phase, stage, reason, optional validation code, gate state, and
431
+ disposition. It contains no URLs, identifiers, request-derived field names or values, or exception text.
393
432
 
394
433
  | Field | Values |
395
434
  | --- | --- |
396
435
  | `endpoint_family` | `payment_methods`, `payment_page_confirm`, `other` |
397
436
  | `phase` | `tokenization`, `final`, `unknown` |
398
437
  | `stage` | `request_read`, `classification`, `claim`, `readiness`, `document`, `stub_response` |
438
+ | `validation_code` | Shared-core `StripeCheckoutValidationCode`; present only for `request_validation_failed` |
399
439
  | `gate_state` | `fresh`, `stubbed`, `submitted`, `stopped` |
400
440
  | `disposition` | `active_claim_preserved`, `checkout_stopped` |
401
441
 
402
442
  `reason` is the exported `StripeCheckoutBlockReason` union. It identifies SDK
403
443
  claim and readiness failures, such as `duplicate_confirmation`, `document_changed`,
404
- or `attachment_not_ready`. The shared core's rejection is
405
- `request_validation_failed` with phase `unknown`; this event does not infer which
406
- core predicate failed. `gate_state` records the state before the adapter handles
444
+ or `attachment_not_ready`. A shared-core rejection reports
445
+ `request_validation_failed` and a fixed `validation_code` identifying the failed
446
+ check, or `unclassified` when no recognized code is available. Initial request
447
+ classification uses phase `unknown`; a billing capture rejection uses
448
+ `tokenization`, and a billing attachment rejection uses `final`.
449
+ For example, `form_field_unknown` identifies an allowlist rejection without
450
+ revealing the field name or value. Identifying that field requires a separate
451
+ reviewed synthetic fixture; the code alone does not establish or fix a processor
452
+ schema mismatch. `gate_state` records the state before the adapter handles
407
453
  the failure, though document validation may already have stopped the gate.
408
454
 
409
455
  `active_claim_preserved` means a valid duplicate was refused without invalidating
@@ -579,7 +625,7 @@ When the cardholder has enabled an eligible spending rule in their vault, the sa
579
625
 
580
626
  Use `executionMode: 'user_approval'` to require the existing confirmation flow. Without a selected card or grant, the backend can use exactly one eligible rule; ambiguous card selection keeps confirmation. A `grantId` restricts selection to that rule. The initial executor supports only the configured controlled Stripe test flow, and production remains disabled.
581
627
 
582
- For controlled hosted Stripe Checkout, both attachment functions accept `stripeCheckout: { sessionId, publishableKey, environment? }`. The default is TEST, requiring `cs_test_` and `pk_test_`. LIVE requires explicit `environment: 'production'`, a `cs_live_` Session and its `pk_live_` key. Both modes require `executionMode: 'autopilot'`, an explicit `grantId`, and a positive numeric USD `amount` in cents. The top-level document must be that Session on `checkout.stripe.com`. Direct `authorize()` calls use `stripeCheckoutEnvironment: 'production'` for the same explicit LIVE opt-in; the attachment functions pass it automatically.
628
+ For controlled hosted Stripe Checkout, both attachment functions accept `stripeCheckout: { sessionId, publishableKey, environment? }`. The default is TEST, requiring `cs_test_` and `pk_test_`. LIVE requires explicit `environment: 'production'`, a `cs_live_` Session and its `pk_live_` key. Both modes require `executionMode: 'autopilot'`, an explicit `grantId`, and a positive numeric USD `amount` in cents. The top-level document must be that exact Session on `https://checkout.stripe.com`, at `/c/pay/{sessionId}`, `/pay/{sessionId}`, `/g/pay/{sessionId}`, or `/f/pay/{sessionId}`. The SDK validates the existing URL without rewriting or navigating it. Direct `authorize()` calls use `stripeCheckoutEnvironment: 'production'` for the same explicit LIVE opt-in; the attachment functions pass it automatically.
583
629
 
584
630
  The SDK answers dummy-card tokenization locally, then sends the browser's final native confirmation through the shared core and enclave. The local response is preparation only: it creates no authorization and makes no processor request. The final result includes `checkoutSessionId` only after the selected grant, restricted terminal response, and captured amount have been checked. Continue to confirm the merchant order through the checkout controller.
585
631
 
package/dist/index.d.ts CHANGED
@@ -10,4 +10,4 @@ export { hostedFormSubmittedPage, HOSTED_FORM_SUBMITTED_OUTCOME } from './hosted
10
10
  export type { HostedFormSubmittedPageInput, SyntheticPage } from './hosted-form.js';
11
11
  export { BUILTIN_REGISTRY, cardUrlPatterns, findRecognizer } from './registry.js';
12
12
  export type { Recognizer, CheckoutMode } from './registry.js';
13
- export type { CheckoutController, CheckoutState, MerchantResult, UserAction, LifecycleOptions, PaymentEndpointGuard } from './lifecycle.js';
13
+ export type { CheckoutController, CheckoutState, MerchantResult, MerchantPaymentConfirmation, UserAction, LifecycleOptions, PaymentEndpointGuard } from './lifecycle.js';
@@ -1,9 +1,19 @@
1
1
  import { CheckoutPreparationError, type PrepareCheckoutOptions, type PreparedCheckout, type ReplayResponse } from './client.js';
2
2
  import type { CheckoutMode } from './registry.js';
3
- /** A processor approval is not an order. Only the merchant can confirm this result. */
3
+ /** The application's resolver confirmed the payment for this checkout authorization. */
4
+ export interface MerchantPaymentConfirmation {
5
+ kind: 'merchant_payment';
6
+ authorizationId: string;
7
+ }
8
+ /** Only authoritative merchant evidence can confirm completion; a processor token is insufficient. */
4
9
  export type MerchantResult = {
5
10
  status: 'completed';
6
11
  orderId: string;
12
+ confirmation?: never;
13
+ } | {
14
+ status: 'completed';
15
+ confirmation: MerchantPaymentConfirmation;
16
+ orderId?: never;
7
17
  } | {
8
18
  status: 'failed';
9
19
  } | {
@@ -18,6 +28,7 @@ export interface CheckoutState {
18
28
  preparationId?: string;
19
29
  mode?: CheckoutMode;
20
30
  orderId?: string;
31
+ confirmation?: MerchantPaymentConfirmation;
21
32
  /** Stable SDK category; never includes a request body, processor response, or approval link. */
22
33
  reason?: string;
23
34
  }
@@ -30,7 +41,7 @@ export interface UserAction {
30
41
  export interface LifecycleOptions {
31
42
  onStateChange?: (state: Readonly<CheckoutState>) => void;
32
43
  onUserAction?: (action: UserAction) => void | Promise<void>;
33
- /** Read authoritative merchant order state; do not click Pay or initiate a new charge here. */
44
+ /** Read authoritative merchant payment or order state; do not click Pay or initiate a new charge here. */
34
45
  resolveMerchantResult?: (state: Readonly<CheckoutState>) => Promise<MerchantResult>;
35
46
  /** Hold further card requests after handoff until the merchant result is reconciled. Default false for compatibility. */
36
47
  requireMerchantResult?: boolean;
package/dist/lifecycle.js CHANGED
@@ -17,7 +17,9 @@ export class CheckoutLifecycle {
17
17
  constructor(options) {
18
18
  this.options = options;
19
19
  }
20
- getState() { return { ...this.state }; }
20
+ getState() {
21
+ return { ...this.state, ...(this.state.confirmation ? { confirmation: { ...this.state.confirmation } } : {}) };
22
+ }
21
23
  setPreparationHandler(handler) { this.preparationHandler = handler; }
22
24
  prepare(options) {
23
25
  if (!this.preparationHandler)
@@ -195,14 +197,28 @@ export class CheckoutLifecycle {
195
197
  catch {
196
198
  result = { status: 'unknown' };
197
199
  }
198
- if (!result || typeof result !== 'object')
200
+ if (!result || typeof result !== 'object' || Array.isArray(result))
199
201
  result = { status: 'unknown' };
200
- if (result.status === 'completed' && typeof result.orderId === 'string' && result.orderId.length > 0) {
202
+ // The resolver is trusted application code: it must check the original
203
+ // request, amount, currency, selected card and authoritative merchant
204
+ // result. The SDK binds its explicit confirmation to this authorization;
205
+ // it does not turn an HTTP response or a success URL into payment proof.
206
+ const orderConfirmed = result.status === 'completed' && result.confirmation === undefined
207
+ && typeof result.orderId === 'string' && result.orderId.length > 0;
208
+ const paymentConfirmed = result.status === 'completed' && result.orderId === undefined
209
+ && result.confirmation && typeof result.confirmation === 'object' && !Array.isArray(result.confirmation)
210
+ && result.confirmation.kind === 'merchant_payment'
211
+ && typeof this.state.authorizationId === 'string' && this.state.authorizationId.length > 0
212
+ && result.confirmation.authorizationId === this.state.authorizationId;
213
+ if (result.status === 'completed' && (orderConfirmed || paymentConfirmed)) {
201
214
  this.held = true;
202
- // Merchant completion supersedes the earlier failure or user-action
203
- // reason; keep the authorization and preparation context for observers.
204
- const { reason: _previousReason, ...context } = this.state;
205
- this.set({ ...context, status: 'completed', orderId: result.orderId });
215
+ // Completion supersedes an earlier failure/challenge, while retaining
216
+ // preparation and authorization context. Never copy arbitrary evidence
217
+ // into public state or release the hold on additional payment requests.
218
+ const { reason: _previousReason, orderId: _previousOrder, confirmation: _previousConfirmation, ...context } = this.state;
219
+ this.set({ ...context, status: 'completed', ...(orderConfirmed
220
+ ? { orderId: result.orderId }
221
+ : { confirmation: { kind: 'merchant_payment', authorizationId: this.state.authorizationId } }) });
206
222
  }
207
223
  else if (result.status === 'failed') {
208
224
  // Keep the guard armed until the application deliberately starts another attempt.
@@ -1,3 +1,4 @@
1
+ import { STRIPE_CHECKOUT_VALIDATION_CODES } from './stripe-checkout.generated.js';
1
2
  import type { PausedRequest } from './client.js';
2
3
  /** Test by default. Production requires explicit opt-in and an independently admitted live grant. */
3
4
  export interface StripeCheckoutOptions {
@@ -19,11 +20,12 @@ type Step = {
19
20
  };
20
21
  type Phase = Step['phase'] | 'unknown';
21
22
  type GateState = 'fresh' | 'stubbed' | 'submitted' | 'stopped';
23
+ type ValidationCode = (typeof STRIPE_CHECKOUT_VALIDATION_CODES)[keyof typeof STRIPE_CHECKOUT_VALIDATION_CODES];
22
24
  export type StripeCheckoutBlockStage = 'request_read' | 'classification' | 'claim' | 'readiness' | 'document' | 'stub_response';
23
25
  type ReadinessReason = 'attachment_not_ready' | 'cancelled' | 'checkout_inactive' | 'approval_pending' | 'request_inactive';
24
26
  export type StripeCheckoutBlockReason = ReadinessReason | 'request_unavailable' | 'request_validation_failed' | 'context_already_present' | 'duplicate_tokenization' | 'duplicate_confirmation' | 'preparation_unavailable' | 'preparation_missing' | 'claim_invalidated' | 'document_changed' | 'document_unavailable' | 'readiness_unavailable' | 'stub_response_failed';
25
27
  /** Native pre-authorization refusal metadata. Never contains request or exception text.
26
- * Classification failures intentionally do not guess which shared-core predicate rejected.
28
+ * A classification code comes only from the shared core's fixed vocabulary.
27
29
  * gate_state is observed before the adapter retires a failed claim. A preserved duplicate
28
30
  * belongs to a competing request: consumers must not cancel the active checkout for it.
29
31
  */
@@ -34,6 +36,7 @@ export interface StripeCheckoutBlockedDetail {
34
36
  phase: Phase;
35
37
  stage: StripeCheckoutBlockStage;
36
38
  reason: StripeCheckoutBlockReason;
39
+ validation_code?: ValidationCode;
37
40
  gate_state: GateState;
38
41
  disposition: 'active_claim_preserved' | 'checkout_stopped';
39
42
  }
@@ -41,7 +44,8 @@ declare class StripeCheckoutRejectionError extends Error {
41
44
  readonly stage: StripeCheckoutBlockStage;
42
45
  readonly reason: StripeCheckoutBlockReason;
43
46
  readonly phase?: Phase | undefined;
44
- constructor(message: string, stage: StripeCheckoutBlockStage, reason: StripeCheckoutBlockReason, phase?: Phase | undefined);
47
+ readonly validationCode?: ValidationCode | undefined;
48
+ constructor(message: string, stage: StripeCheckoutBlockStage, reason: StripeCheckoutBlockReason, phase?: Phase | undefined, validationCode?: ValidationCode | undefined);
45
49
  }
46
50
  export declare function stripeCheckoutReadinessError(reason: ReadinessReason): Error;
47
51
  /** A valid competing request must not retire the request that already claimed this step. */
@@ -1,5 +1,29 @@
1
1
  declare var STRIPE_CHECKOUT_OPERATION: string;
2
2
  declare var STRIPE_CHECKOUT_CONTEXT_HEADER: string;
3
+ declare var STRIPE_CHECKOUT_VALIDATION_CODES: Readonly<{
4
+ unclassified: "unclassified";
5
+ request_shape: "request_shape";
6
+ request_body_bounds: "request_body_bounds";
7
+ request_headers: "request_headers";
8
+ form_encoding: "form_encoding";
9
+ form_field_unknown: "form_field_unknown";
10
+ form_field_duplicate: "form_field_duplicate";
11
+ form_value_bounds: "form_value_bounds";
12
+ context_invalid: "context_invalid";
13
+ session_binding: "session_binding";
14
+ publishable_key: "publishable_key";
15
+ mode_binding: "mode_binding";
16
+ attribution_session: "attribution_session";
17
+ tokenization_fields: "tokenization_fields";
18
+ billing_email: "billing_email";
19
+ billing_binding: "billing_binding";
20
+ billing_conflict: "billing_conflict";
21
+ expected_amount: "expected_amount";
22
+ payment_method_type: "payment_method_type";
23
+ synthetic_payment_method: "synthetic_payment_method";
24
+ required_final_fields: "required_final_fields";
25
+ }>;
26
+ declare function stripeCheckoutValidationCode(error: any): "unclassified" | "request_shape" | "request_body_bounds" | "request_headers" | "form_encoding" | "form_field_unknown" | "form_field_duplicate" | "form_value_bounds" | "context_invalid" | "session_binding" | "publishable_key" | "mode_binding" | "attribution_session" | "tokenization_fields" | "billing_email" | "billing_binding" | "billing_conflict" | "expected_amount" | "payment_method_type" | "synthetic_payment_method" | "required_final_fields";
3
27
  declare function validateStripeCheckoutIdentity(value: any): Readonly<{
4
28
  environment: any;
5
29
  livemode: boolean;
@@ -55,4 +79,4 @@ declare function prepareStripeCheckoutReplay(request: any, card: any, options?:
55
79
  };
56
80
  where: string[];
57
81
  };
58
- export { STRIPE_CHECKOUT_CONTEXT_HEADER, STRIPE_CHECKOUT_OPERATION, attachStripeCheckoutBilling, captureStripeCheckoutBilling, classifyStripeCheckoutRequest, createStripeCheckoutTokenizationStub, encodeStripeCheckoutContext, hasStripeCheckoutMarker, isStripeCheckoutEndpoint, isStripeCheckoutHostedMode, parseStripeCheckoutContext, parseStripeCheckoutResponse, prepareStripeCheckoutReplay, validateStripeCheckoutCard, validateStripeCheckoutIdentity };
82
+ export { STRIPE_CHECKOUT_CONTEXT_HEADER, STRIPE_CHECKOUT_OPERATION, STRIPE_CHECKOUT_VALIDATION_CODES, attachStripeCheckoutBilling, captureStripeCheckoutBilling, classifyStripeCheckoutRequest, createStripeCheckoutTokenizationStub, encodeStripeCheckoutContext, hasStripeCheckoutMarker, isStripeCheckoutEndpoint, isStripeCheckoutHostedMode, parseStripeCheckoutContext, parseStripeCheckoutResponse, prepareStripeCheckoutReplay, stripeCheckoutValidationCode, validateStripeCheckoutCard, validateStripeCheckoutIdentity };
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 2f7e340ba22bc33539f11e384a3e8362ae3fa510a9f8b540783c02bebcbc1ebf
3
+ // artifact-sha256: edd2d9de25c51bf0f4ebdbe729366e6d94b9a045b054a0910571e0565bd3cae0
4
4
  // src/definitions.js
5
5
  function deepFreeze(value) {
6
6
  if (value && typeof value === "object" && !Object.isFrozen(value)) {
@@ -527,19 +527,49 @@ var STRIPE_CHECKOUT_CONTEXT_HEADER = "x-agentcard-stripe-checkout-context";
527
527
  var SESSION = /^cs_(?:test|live)_[A-Za-z0-9]{1,200}(?![\s\S])/;
528
528
  var KEY = /^pk_(?:test|live)_[A-Za-z0-9_]{1,256}(?![\s\S])/;
529
529
  var SYNTHETIC = /^pm_agentcard_checkout_[A-Za-z0-9_-]{16,80}(?![\s\S])/;
530
- var reject = () => {
531
- throw new Error("stripe_checkout_rejected");
530
+ var STRIPE_CHECKOUT_VALIDATION_CODES = Object.freeze({
531
+ unclassified: "unclassified",
532
+ request_shape: "request_shape",
533
+ request_body_bounds: "request_body_bounds",
534
+ request_headers: "request_headers",
535
+ form_encoding: "form_encoding",
536
+ form_field_unknown: "form_field_unknown",
537
+ form_field_duplicate: "form_field_duplicate",
538
+ form_value_bounds: "form_value_bounds",
539
+ context_invalid: "context_invalid",
540
+ session_binding: "session_binding",
541
+ publishable_key: "publishable_key",
542
+ mode_binding: "mode_binding",
543
+ attribution_session: "attribution_session",
544
+ tokenization_fields: "tokenization_fields",
545
+ billing_email: "billing_email",
546
+ billing_binding: "billing_binding",
547
+ billing_conflict: "billing_conflict",
548
+ expected_amount: "expected_amount",
549
+ payment_method_type: "payment_method_type",
550
+ synthetic_payment_method: "synthetic_payment_method",
551
+ required_final_fields: "required_final_fields"
552
+ });
553
+ var validationCodes = /* @__PURE__ */ new WeakMap();
554
+ var reject = (code = "unclassified") => {
555
+ const error = new Error("stripe_checkout_rejected");
556
+ validationCodes.set(error, code);
557
+ throw error;
532
558
  };
559
+ function stripeCheckoutValidationCode(error) {
560
+ const code = validationCodes.get(error);
561
+ return Object.values(STRIPE_CHECKOUT_VALIDATION_CODES).find((value) => value === code) ?? STRIPE_CHECKOUT_VALIDATION_CODES.unclassified;
562
+ }
533
563
  var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
534
564
  var opaque = (value) => typeof value === "string" && value.length <= 8192 && !/[\x00-\x1f\x7f]/.test(value);
535
565
  var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
536
566
  function validateStripeCheckoutIdentity(value) {
537
567
  if (!object(value) || !["test", "production"].includes(value.environment ?? "test") || value.environment === null || !["sessionId", "publishableKey", "livemode"].some((key) => Object.hasOwn(value, key)))
538
- reject();
568
+ reject("mode_binding");
539
569
  const environment = value.environment ?? "test", livemode = environment === "production";
540
570
  const mode = livemode ? "live" : "test";
541
571
  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();
572
+ reject("mode_binding");
543
573
  return Object.freeze({ environment, livemode });
544
574
  }
545
575
  var TEST_CARDS = ["4242424242424242", "4000000000000002", "4000002500003155"];
@@ -587,7 +617,7 @@ function encodeAscii(value) {
587
617
  }
588
618
  function context(value) {
589
619
  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();
620
+ reject("context_invalid");
591
621
  return { version: 1, session_id: value.session_id, synthetic_payment_method_id: value.synthetic_payment_method_id };
592
622
  }
593
623
  function encodeStripeCheckoutContext(value) {
@@ -601,7 +631,7 @@ function parseStripeCheckoutContext(request) {
601
631
  if (!entries.length)
602
632
  return null;
603
633
  if (entries.length !== 1 || typeof entries[0][1] !== "string" || !/^[A-Za-z0-9_-]{1,1024}$/.test(entries[0][1]))
604
- reject();
634
+ reject("context_invalid");
605
635
  const encoded = entries[0][1];
606
636
  let text = "", bits = 0, count = 0;
607
637
  for (const char of encoded) {
@@ -617,10 +647,10 @@ function parseStripeCheckoutContext(request) {
617
647
  value = context(JSON.parse(text));
618
648
  }
619
649
  catch {
620
- reject();
650
+ reject("context_invalid");
621
651
  }
622
652
  if (encodeStripeCheckoutContext(value) !== encoded)
623
- reject();
653
+ reject("context_invalid");
624
654
  return value;
625
655
  }
626
656
  function endpoint(raw) {
@@ -657,6 +687,9 @@ var finalFields = /* @__PURE__ */ new Set([
657
687
  "sid",
658
688
  "passive_captcha_token",
659
689
  "passive_captcha_ekey",
690
+ "pxcts",
691
+ "pxvid",
692
+ "px3",
660
693
  ...[
661
694
  "checkout_config_id",
662
695
  "checkout_session_id",
@@ -696,24 +729,32 @@ var tokenFields = /* @__PURE__ */ new Set([
696
729
  ].map((key) => `client_attribution_metadata[${key}]`)
697
730
  ]);
698
731
  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();
732
+ if (!object(request) || request.method !== "POST" || typeof request.body !== "string")
733
+ reject("request_shape");
734
+ if (request.body.length > 2e4)
735
+ reject("request_body_bounds");
736
+ if (!object(request.headers) || Object.keys(request.headers).length > 40 || Object.values(request.headers).some((value) => !opaque(value)))
737
+ reject("request_headers");
701
738
  const pairs = [];
702
739
  const seen = /* @__PURE__ */ new Set();
703
740
  for (const raw of request.body.split("&")) {
704
741
  const index = raw.indexOf("=");
705
742
  if (index < 1)
706
- reject();
743
+ reject("form_encoding");
707
744
  let key, value;
708
745
  try {
709
746
  key = decodeURIComponent(raw.slice(0, index).replace(/\+/g, " "));
710
747
  value = decodeURIComponent(raw.slice(index + 1).replace(/\+/g, " "));
711
748
  }
712
749
  catch {
713
- reject();
750
+ reject("form_encoding");
714
751
  }
715
- if (!fields.has(key) || seen.has(key) || !opaque(value))
716
- reject();
752
+ if (!fields.has(key))
753
+ reject("form_field_unknown");
754
+ if (seen.has(key))
755
+ reject("form_field_duplicate");
756
+ if (!opaque(value))
757
+ reject("form_value_bounds");
717
758
  seen.add(key);
718
759
  pairs.push({ key, value, raw });
719
760
  }
@@ -722,27 +763,27 @@ function form(request, fields) {
722
763
  function keyFor(request, values, expected) {
723
764
  const authorization = Object.entries(request.headers).filter(([key2]) => key2.toLowerCase() === "authorization");
724
765
  if (authorization.length > 1)
725
- reject();
766
+ reject("publishable_key");
726
767
  let headerKey;
727
768
  if (authorization.length) {
728
769
  const match = /^Bearer (pk_(?:test|live)_[A-Za-z0-9_]{1,256})$/.exec(authorization[0][1]);
729
770
  if (!match)
730
- reject();
771
+ reject("publishable_key");
731
772
  headerKey = match[1];
732
773
  }
733
774
  const key = values.get("key") || headerKey;
734
775
  if (!KEY.test(key) || headerKey && headerKey !== key || expected !== void 0 && expected !== key)
735
- reject();
776
+ reject("publishable_key");
736
777
  return key;
737
778
  }
738
779
  var billingEmail = (value) => value === null || typeof value === "string" && value.length > 0 && value.length <= 320 && opaque(value);
739
780
  function captureStripeCheckoutBilling(request, options) {
740
781
  const phase = classifyStripeCheckoutRequest(request, options);
741
782
  if (phase?.phase !== "tokenization" || !SYNTHETIC.test(options?.syntheticPaymentMethodId))
742
- reject();
783
+ reject("billing_binding");
743
784
  const email = form(request, tokenFields).values.get("billing_details[email]") ?? null;
744
785
  if (!billingEmail(email))
745
- reject();
786
+ reject("billing_email");
746
787
  return Object.freeze({
747
788
  session_id: phase.session_id,
748
789
  synthetic_payment_method_id: options.syntheticPaymentMethodId,
@@ -752,11 +793,11 @@ function captureStripeCheckoutBilling(request, options) {
752
793
  function attachStripeCheckoutBilling(request, captured, options) {
753
794
  const phase = classifyStripeCheckoutRequest(request, options);
754
795
  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();
796
+ reject("billing_binding");
756
797
  const field = "payment_method_data[billing_details][email]";
757
798
  const existing = form(request, finalFields).values.get(field);
758
799
  if (existing !== void 0 && existing !== captured.email)
759
- reject();
800
+ reject("billing_conflict");
760
801
  if (captured.email === null || existing !== void 0)
761
802
  return { ...request };
762
803
  const body = `${request.body}&${encodeURIComponent(field)}=${encodeURIComponent(captured.email)}`;
@@ -768,14 +809,14 @@ function classifyStripeCheckoutRequest(request, options = {}) {
768
809
  const url = endpoint(request?.url);
769
810
  if (!url) {
770
811
  if (hasStripeCheckoutMarker(request))
771
- reject();
812
+ reject("context_invalid");
772
813
  return null;
773
814
  }
774
815
  const final = isStripeCheckoutEndpoint(request.url);
775
816
  const token = url.pathname === "/v1/payment_methods";
776
817
  if (!final && !token) {
777
818
  if (hasStripeCheckoutMarker(request))
778
- reject();
819
+ reject("context_invalid");
779
820
  return null;
780
821
  }
781
822
  if (token && options.sessionId === void 0 && !hasStripeCheckoutMarker(request))
@@ -783,26 +824,34 @@ function classifyStripeCheckoutRequest(request, options = {}) {
783
824
  const marker = parseStripeCheckoutContext(request);
784
825
  const sessionId = final ? url.pathname.split("/")[3] : options.sessionId;
785
826
  if (!SESSION.test(sessionId) || options.sessionId !== void 0 && options.sessionId !== sessionId || marker && marker.session_id !== sessionId)
786
- reject();
827
+ reject("session_binding");
787
828
  const { values } = form(request, final ? finalFields : tokenFields);
788
829
  const key = keyFor(request, values, options.publishableKey);
789
830
  validateStripeCheckoutIdentity({ environment: options.environment, sessionId, publishableKey: key });
790
831
  const attributionSession = values.get("client_attribution_metadata[checkout_session_id]");
791
832
  if (attributionSession !== void 0 && attributionSession !== sessionId)
792
- reject();
833
+ reject("attribution_session");
793
834
  if (token) {
794
835
  const number = values.get("card[number]");
795
836
  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();
837
+ reject("tokenization_fields");
797
838
  return { phase: "tokenization", session_id: sessionId, publishable_key: key };
798
839
  }
799
840
  const amount = values.get("expected_amount");
800
841
  const syntheticId = values.get("payment_method");
801
842
  const email = values.get("payment_method_data[billing_details][email]");
802
843
  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();
844
+ reject("billing_email");
845
+ if (!/^[1-9]\d{0,11}$/.test(amount ?? "") || !Number.isSafeInteger(Number(amount)) || options.amountCents !== void 0 && options.amountCents !== Number(amount))
846
+ reject("expected_amount");
847
+ if (values.get("expected_payment_method_type") !== "card")
848
+ reject("payment_method_type");
849
+ if (!SYNTHETIC.test(syntheticId))
850
+ reject("synthetic_payment_method");
851
+ if (["init_checksum", "js_checksum", "version", "rv_timestamp"].some((key2) => !values.get(key2)))
852
+ reject("required_final_fields");
853
+ if (options.syntheticPaymentMethodId !== void 0 && options.syntheticPaymentMethodId !== syntheticId || marker && marker.synthetic_payment_method_id !== syntheticId)
854
+ reject("synthetic_payment_method");
806
855
  return {
807
856
  phase: "final",
808
857
  session_id: sessionId,
@@ -912,4 +961,4 @@ function prepareStripeCheckoutReplay(request, card, options = {}) {
912
961
  where: Object.keys(replacements)
913
962
  };
914
963
  }
915
- export { STRIPE_CHECKOUT_CONTEXT_HEADER, STRIPE_CHECKOUT_OPERATION, attachStripeCheckoutBilling, captureStripeCheckoutBilling, classifyStripeCheckoutRequest, createStripeCheckoutTokenizationStub, encodeStripeCheckoutContext, hasStripeCheckoutMarker, isStripeCheckoutEndpoint, isStripeCheckoutHostedMode, parseStripeCheckoutContext, parseStripeCheckoutResponse, prepareStripeCheckoutReplay, validateStripeCheckoutCard, validateStripeCheckoutIdentity };
964
+ export { STRIPE_CHECKOUT_CONTEXT_HEADER, STRIPE_CHECKOUT_OPERATION, STRIPE_CHECKOUT_VALIDATION_CODES, attachStripeCheckoutBilling, captureStripeCheckoutBilling, classifyStripeCheckoutRequest, createStripeCheckoutTokenizationStub, encodeStripeCheckoutContext, hasStripeCheckoutMarker, isStripeCheckoutEndpoint, isStripeCheckoutHostedMode, parseStripeCheckoutContext, parseStripeCheckoutResponse, prepareStripeCheckoutReplay, stripeCheckoutValidationCode, validateStripeCheckoutCard, validateStripeCheckoutIdentity };
@@ -1,13 +1,15 @@
1
- import { classifyStripeCheckoutRequest, createStripeCheckoutTokenizationStub, encodeStripeCheckoutContext, isStripeCheckoutEndpoint, STRIPE_CHECKOUT_CONTEXT_HEADER, captureStripeCheckoutBilling, attachStripeCheckoutBilling, validateStripeCheckoutIdentity, } from './stripe-checkout.generated.js';
1
+ import { classifyStripeCheckoutRequest, createStripeCheckoutTokenizationStub, encodeStripeCheckoutContext, isStripeCheckoutEndpoint, STRIPE_CHECKOUT_CONTEXT_HEADER, captureStripeCheckoutBilling, attachStripeCheckoutBilling, validateStripeCheckoutIdentity, stripeCheckoutValidationCode, STRIPE_CHECKOUT_VALIDATION_CODES, } from './stripe-checkout.generated.js';
2
2
  class StripeCheckoutRejectionError extends Error {
3
3
  stage;
4
4
  reason;
5
5
  phase;
6
- constructor(message, stage, reason, phase) {
6
+ validationCode;
7
+ constructor(message, stage, reason, phase, validationCode) {
7
8
  super(message);
8
9
  this.stage = stage;
9
10
  this.reason = reason;
10
11
  this.phase = phase;
12
+ this.validationCode = validationCode;
11
13
  }
12
14
  }
13
15
  export function stripeCheckoutReadinessError(reason) {
@@ -59,8 +61,10 @@ export class StripeCheckoutGate {
59
61
  readiness: 'readiness_unavailable', document: 'document_unavailable', stub_response: 'stub_response_failed',
60
62
  };
61
63
  const rejection = error instanceof StripeCheckoutRejectionError ? error : undefined;
64
+ const reason = rejection?.reason ?? fallback[stage];
62
65
  return { version: 1, processor: 'stripe', endpoint_family: family,
63
- phase: rejection?.phase ?? phase, stage: rejection?.stage ?? stage, reason: rejection?.reason ?? fallback[stage],
66
+ phase: rejection?.phase ?? phase, stage: rejection?.stage ?? stage, reason,
67
+ ...(reason === 'request_validation_failed' ? { validation_code: rejection?.validationCode ?? STRIPE_CHECKOUT_VALIDATION_CODES.unclassified } : {}),
64
68
  gate_state: this.state, disposition: error instanceof StripeCheckoutClaimConflictError ? 'active_claim_preserved' : 'checkout_stopped' };
65
69
  }
66
70
  assertClaim(step) {
@@ -73,7 +77,7 @@ export class StripeCheckoutGate {
73
77
  return;
74
78
  const url = new URL(raw);
75
79
  if (url.origin !== 'https://checkout.stripe.com' || url.username || url.password
76
- || ![`/c/pay/${this.binding.sessionId}`, `/pay/${this.binding.sessionId}`].includes(url.pathname)) {
80
+ || ![`/c/pay/${this.binding.sessionId}`, `/pay/${this.binding.sessionId}`, `/g/pay/${this.binding.sessionId}`, `/f/pay/${this.binding.sessionId}`].includes(url.pathname)) {
77
81
  this.invalidate();
78
82
  throw new StripeCheckoutRejectionError('Native Stripe Checkout document changed.', 'document', 'document_changed');
79
83
  }
@@ -87,8 +91,8 @@ export class StripeCheckoutGate {
87
91
  try {
88
92
  phase = classifyStripeCheckoutRequest(request, this.binding);
89
93
  }
90
- catch {
91
- throw new StripeCheckoutRejectionError('stripe_checkout_rejected', 'classification', 'request_validation_failed', 'unknown');
94
+ catch (error) {
95
+ throw new StripeCheckoutRejectionError('stripe_checkout_rejected', 'classification', 'request_validation_failed', 'unknown', stripeCheckoutValidationCode(error));
92
96
  }
93
97
  if (!phase)
94
98
  return null;
@@ -106,7 +110,10 @@ export class StripeCheckoutGate {
106
110
  this.billing = captureStripeCheckoutBilling(request, this.binding);
107
111
  response = createStripeCheckoutTokenizationStub(request, this.binding);
108
112
  }
109
- catch {
113
+ catch (error) {
114
+ const code = stripeCheckoutValidationCode(error);
115
+ if (code !== STRIPE_CHECKOUT_VALIDATION_CODES.unclassified)
116
+ throw new StripeCheckoutRejectionError('stripe_checkout_rejected', 'classification', 'request_validation_failed', 'tokenization', code);
110
117
  throw new StripeCheckoutRejectionError('stripe_checkout_rejected', 'stub_response', 'stub_response_failed', 'tokenization');
111
118
  }
112
119
  this.state = 'stubbed';
@@ -120,9 +127,9 @@ export class StripeCheckoutGate {
120
127
  try {
121
128
  final = attachStripeCheckoutBilling(request, this.billing, this.binding);
122
129
  }
123
- catch {
130
+ catch (error) {
124
131
  this.invalidate();
125
- throw new StripeCheckoutRejectionError('stripe_checkout_rejected', 'classification', 'request_validation_failed', 'final');
132
+ throw new StripeCheckoutRejectionError('stripe_checkout_rejected', 'classification', 'request_validation_failed', 'final', stripeCheckoutValidationCode(error));
126
133
  }
127
134
  this.state = 'submitted';
128
135
  this.billing = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-cards/checkout",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Let browser agents pay with the user's own card, without your infrastructure ever touching card data.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,7 +27,7 @@
27
27
  "_comment_build": "The public SDK keeps zero runtime dependencies. Its build vendors deterministic metadata and substitution artifacts from the internal payment core; TypeScript remains pinned.",
28
28
  "build": "node ../payment-core/scripts/build.mjs && node scripts/generate-payment-core.mjs && npx -y -p typescript@5.9.3 tsc && node scripts/generate-preflight-contract.mjs",
29
29
  "prepublishOnly": "pnpm build",
30
- "test": "node test.mjs && node --test lifecycle.test.mjs merchant-abort.test.mjs preparation.test.mjs braintree.test.mjs autopilot.test.mjs stripe-checkout.test.mjs payment-core.test.mjs prepared-processor.test.mjs minimum-delay.test.mjs paysafe.test.mjs attachment.test.mjs mercado-checkout.test.mjs mercado-polling.test.mjs && node --test preflight-package.test.mjs preflight-collector.test.mjs && node --test kernel-native-qualification.test.mjs",
30
+ "test": "node test.mjs && node --test lifecycle.test.mjs merchant-abort.test.mjs preparation.test.mjs braintree.test.mjs autopilot.test.mjs stripe-checkout.test.mjs payment-core.test.mjs prepared-processor.test.mjs minimum-delay.test.mjs paysafe.test.mjs attachment.test.mjs mercado-checkout.test.mjs mercado-polling.test.mjs && node --test preflight-package.test.mjs preflight-collector.test.mjs && node --test kernel-native-qualification.test.mjs && node --test ../vault/scripts/recurly-validation/watch-duty-sdk-result.test.mjs",
31
31
  "test:browser": "node browser.test.mjs && node stripe-browser.test.mjs && node preparation-browser.test.mjs && node owned-shop-browser.test.mjs",
32
32
  "check:payment-core": "node scripts/generate-payment-core.mjs --check",
33
33
  "test:preflight": "node --test preflight-package.test.mjs preflight-collector.test.mjs kernel-native-qualification.test.mjs",