@agent-cards/checkout 0.10.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.
@@ -0,0 +1,87 @@
1
+ // Generated from @agent-cards/payment-core. Do not edit.
2
+ // source-sha256: 6a1932841621aca1a7371165e0b54a161c4dd5bb077acc190a0c5399229fa350
3
+ /** Normal Recurly.js 4 new-card POST, before it consumes an approval.
4
+ * Native fields and indexed form serialization: recurly/recurly-js commit
5
+ * 7cd9a2b131bf12d7cebfbc7eea1ba86aa54c60e7, lib/recurly/{token,request,fraud}.js
6
+ * and risk/{risk,three-d-secure/three-d-secure}.js. Worldpay and Cybersource
7
+ * risk sessions depend on the original card; proactive Braintree also produces
8
+ * another token type. All nonempty risk results are refused until collection
9
+ * can use the approved card. This function never rewrites the body.
10
+ */
11
+ const SCALARS = new Set(['number', 'month', 'year', 'cvv', 'type', 'key', 'version', 'deviceId', 'sessionId', 'instanceId',
12
+ 'first_name', 'last_name', 'address1', 'address2', 'company', 'country', 'city', 'state', 'postal_code', 'phone',
13
+ 'vat_number', 'tax_identifier', 'tax_identifier_type', 'fraud_session_id', 'card_network_preference']);
14
+ const BROWSER = new Set(['color_depth', 'java_enabled', 'language', 'referrer_url', 'screen_height', 'screen_width', 'time_zone_offset', 'user_agent']);
15
+ const FRAUD_PROCESSORS = new Set(['kount', 'litle_threat_metrix', 'braintree', 'fraudnet']);
16
+ const contiguous = (values) => [...values.keys()].sort((a, b) => Number(a) - Number(b)).every((value, index) => value === String(index));
17
+ export function isPreparedRecurlyRequest(body) {
18
+ if (typeof body !== 'string' || !body || body.length > 65536)
19
+ return false;
20
+ const chunks = body.split('&');
21
+ if (chunks.length > 200)
22
+ return false;
23
+ const fields = new Map();
24
+ const fraud = new Map();
25
+ try {
26
+ for (const chunk of chunks) {
27
+ const separator = chunk.indexOf('=');
28
+ if (separator < 1)
29
+ return false;
30
+ const name = decodeURIComponent(chunk.slice(0, separator).replace(/\+/g, ' '));
31
+ const value = decodeURIComponent(chunk.slice(separator + 1).replace(/\+/g, ' '));
32
+ if (fields.has(name) || value.length > 4096 || /[\u0000-\u001f\u007f]/.test(value))
33
+ return false;
34
+ fields.set(name, value);
35
+ if (SCALARS.has(name))
36
+ continue;
37
+ const browser = /^browser\[([a-z_]+)\]$/.exec(name);
38
+ if (browser && BROWSER.has(browser[1]))
39
+ continue;
40
+ const structured = /^fraud\[([0-9]|1[0-9])\]\[(.+)\]$/.exec(name);
41
+ if (!structured)
42
+ return false;
43
+ const [, index, leaf] = structured;
44
+ if (!['processor', 'session_id'].includes(leaf) && !/^udf\]\[([0-9]|1[0-9])\]\[(label|value)$/.test(leaf))
45
+ return false;
46
+ if (!fraud.has(index))
47
+ fraud.set(index, new Map());
48
+ fraud.get(index).set(leaf, value);
49
+ }
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ const number = fields.get('number') ?? '', month = fields.get('month') ?? '', year = fields.get('year') ?? '';
55
+ if (!/^[0-9]{12,19}$/.test(number) || !/^[0-9]{1,2}$/.test(month) || Number(month) < 1 || Number(month) > 12
56
+ || !/^(?:[0-9]{2}|20[0-9]{2})$/.test(year))
57
+ return false;
58
+ if (fields.has('cvv') && fields.get('cvv') !== '' && !/^[0-9]{3,4}$/.test(fields.get('cvv')))
59
+ return false;
60
+ if (fields.has('type') && fields.get('type') !== 'credit_card')
61
+ return false;
62
+ // A co-badged network choice belongs to the entered card's supported brands.
63
+ if (fields.get('card_network_preference'))
64
+ return false;
65
+ if (fields.has('key') && !fields.get('key')?.trim())
66
+ return false;
67
+ if (!contiguous(fraud))
68
+ return false;
69
+ for (const item of fraud.values()) {
70
+ if (!FRAUD_PROCESSORS.has(item.get('processor') ?? '') || !item.get('session_id'))
71
+ return false;
72
+ const udf = new Map();
73
+ for (const key of item.keys()) {
74
+ const field = /^udf\]\[([0-9]|1[0-9])\]\[(label|value)$/.exec(key);
75
+ if (!field)
76
+ continue;
77
+ if (item.get('processor') !== 'kount')
78
+ return false;
79
+ if (!udf.has(field[1]))
80
+ udf.set(field[1], new Set());
81
+ udf.get(field[1]).add(field[2]);
82
+ }
83
+ if (!contiguous(udf) || [...udf.values()].some(keys => keys.size !== 2))
84
+ return false;
85
+ }
86
+ return true;
87
+ }
@@ -1,6 +1,8 @@
1
+ import { STRIPE_CHECKOUT_VALIDATION_CODES } from './stripe-checkout.generated.js';
1
2
  import type { PausedRequest } from './client.js';
2
- /** Explicit development enrollment; this option cannot enable production execution. */
3
+ /** Test by default. Production requires explicit opt-in and an independently admitted live grant. */
3
4
  export interface StripeCheckoutOptions {
5
+ environment?: 'test' | 'production';
4
6
  sessionId: string;
5
7
  publishableKey: string;
6
8
  }
@@ -18,11 +20,12 @@ type Step = {
18
20
  };
19
21
  type Phase = Step['phase'] | 'unknown';
20
22
  type GateState = 'fresh' | 'stubbed' | 'submitted' | 'stopped';
23
+ type ValidationCode = (typeof STRIPE_CHECKOUT_VALIDATION_CODES)[keyof typeof STRIPE_CHECKOUT_VALIDATION_CODES];
21
24
  export type StripeCheckoutBlockStage = 'request_read' | 'classification' | 'claim' | 'readiness' | 'document' | 'stub_response';
22
25
  type ReadinessReason = 'attachment_not_ready' | 'cancelled' | 'checkout_inactive' | 'approval_pending' | 'request_inactive';
23
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';
24
27
  /** Native pre-authorization refusal metadata. Never contains request or exception text.
25
- * Classification failures intentionally do not guess which shared-core predicate rejected.
28
+ * A classification code comes only from the shared core's fixed vocabulary.
26
29
  * gate_state is observed before the adapter retires a failed claim. A preserved duplicate
27
30
  * belongs to a competing request: consumers must not cancel the active checkout for it.
28
31
  */
@@ -33,6 +36,7 @@ export interface StripeCheckoutBlockedDetail {
33
36
  phase: Phase;
34
37
  stage: StripeCheckoutBlockStage;
35
38
  reason: StripeCheckoutBlockReason;
39
+ validation_code?: ValidationCode;
36
40
  gate_state: GateState;
37
41
  disposition: 'active_claim_preserved' | 'checkout_stopped';
38
42
  }
@@ -40,7 +44,8 @@ declare class StripeCheckoutRejectionError extends Error {
40
44
  readonly stage: StripeCheckoutBlockStage;
41
45
  readonly reason: StripeCheckoutBlockReason;
42
46
  readonly phase?: Phase | undefined;
43
- 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);
44
49
  }
45
50
  export declare function stripeCheckoutReadinessError(reason: ReadinessReason): Error;
46
51
  /** A valid competing request must not retire the request that already claimed this step. */
@@ -53,6 +58,7 @@ export declare class StripeCheckoutClaimConflictError extends StripeCheckoutReje
53
58
  */
54
59
  export declare class StripeCheckoutGate {
55
60
  private state;
61
+ private billing?;
56
62
  private readonly binding?;
57
63
  constructor(options: {
58
64
  stripeCheckout?: StripeCheckoutOptions;
@@ -1,5 +1,34 @@
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";
27
+ declare function validateStripeCheckoutIdentity(value: any): Readonly<{
28
+ environment: any;
29
+ livemode: boolean;
30
+ }>;
31
+ declare function validateStripeCheckoutCard(card: any, environment?: string): boolean;
3
32
  declare function isStripeCheckoutHostedMode(value: any): boolean;
4
33
  declare function encodeStripeCheckoutContext(value: any): string;
5
34
  declare function hasStripeCheckoutMarker(request: any): boolean;
@@ -9,6 +38,12 @@ declare function parseStripeCheckoutContext(request: any): {
9
38
  synthetic_payment_method_id: any;
10
39
  } | null | undefined;
11
40
  declare function isStripeCheckoutEndpoint(raw: any): boolean;
41
+ declare function captureStripeCheckoutBilling(request: any, options: any): Readonly<{
42
+ session_id: any;
43
+ synthetic_payment_method_id: any;
44
+ email: string | null;
45
+ }>;
46
+ declare function attachStripeCheckoutBilling(request: any, captured: any, options: any): any;
12
47
  declare function classifyStripeCheckoutRequest(request: any, options?: {}): {
13
48
  phase: string;
14
49
  session_id: any;
@@ -44,4 +79,4 @@ declare function prepareStripeCheckoutReplay(request: any, card: any, options?:
44
79
  };
45
80
  where: string[];
46
81
  };
47
- export { STRIPE_CHECKOUT_CONTEXT_HEADER, STRIPE_CHECKOUT_OPERATION, classifyStripeCheckoutRequest, createStripeCheckoutTokenizationStub, encodeStripeCheckoutContext, hasStripeCheckoutMarker, isStripeCheckoutEndpoint, isStripeCheckoutHostedMode, parseStripeCheckoutContext, parseStripeCheckoutResponse, prepareStripeCheckoutReplay };
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: 41b6fde86ad6193d1d7cb505c7dda522c2d664a9939ee0a4a0b3028cfa8bfa06
3
+ // artifact-sha256: edd2d9de25c51bf0f4ebdbe729366e6d94b9a045b054a0910571e0565bd3cae0
4
4
  // src/definitions.js
5
5
  function deepFreeze(value) {
6
6
  if (value && typeof value === "object" && !Object.isFrozen(value)) {
@@ -524,15 +524,82 @@ var replayPolicy = Object.freeze({ contentTypeFor, HEADER_NEVER_FORWARD });
524
524
  // src/stripe-checkout.js
525
525
  var STRIPE_CHECKOUT_OPERATION = "stripe.device_checkout_confirm.v1";
526
526
  var STRIPE_CHECKOUT_CONTEXT_HEADER = "x-agentcard-stripe-checkout-context";
527
- var SESSION = /^cs_test_[A-Za-z0-9]{1,200}(?![\s\S])/;
528
- var KEY = /^pk_test_[A-Za-z0-9_]{1,256}(?![\s\S])/;
527
+ var SESSION = /^cs_(?:test|live)_[A-Za-z0-9]{1,200}(?![\s\S])/;
528
+ var KEY = /^pk_(?:test|live)_[A-Za-z0-9_]{1,256}(?![\s\S])/;
529
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-_";
566
+ function validateStripeCheckoutIdentity(value) {
567
+ if (!object(value) || !["test", "production"].includes(value.environment ?? "test") || value.environment === null || !["sessionId", "publishableKey", "livemode"].some((key) => Object.hasOwn(value, key)))
568
+ reject("mode_binding");
569
+ const environment = value.environment ?? "test", livemode = environment === "production";
570
+ const mode = livemode ? "live" : "test";
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)
572
+ reject("mode_binding");
573
+ return Object.freeze({ environment, livemode });
574
+ }
575
+ var TEST_CARDS = ["4242424242424242", "4000000000000002", "4000002500003155"];
576
+ var SYNTHETIC_CARDS = [...TEST_CARDS, "4111111111111111"];
577
+ function validateStripeCheckoutCard(card, environment = "test") {
578
+ if (!["test", "production"].includes(environment) || !object(card) || typeof card.number !== "string" || typeof card.cvc !== "string" || !/^(?:0?[1-9]|1[0-2])(?![\s\S])/.test(String(card.month)) || !/^(?:\d{2}|\d{4})(?![\s\S])/.test(String(card.year)) || card.name !== void 0 && !opaque(card.name))
579
+ reject();
580
+ if (environment === "test") {
581
+ if (!TEST_CARDS.includes(card.number) || card.cvc !== "123")
582
+ reject();
583
+ }
584
+ else {
585
+ if (!/^\d{13,19}(?![\s\S])/.test(card.number) || /^(\d)\1+(?![\s\S])/.test(card.number) || !/^\d{3,4}(?![\s\S])/.test(card.cvc) || SYNTHETIC_CARDS.includes(card.number))
586
+ reject();
587
+ let sum = 0, double = false;
588
+ for (let i = card.number.length - 1; i >= 0; i--) {
589
+ let digit = Number(card.number[i]);
590
+ if (double) {
591
+ digit *= 2;
592
+ if (digit > 9)
593
+ digit -= 9;
594
+ }
595
+ sum += digit;
596
+ double = !double;
597
+ }
598
+ if (sum % 10 !== 0)
599
+ reject();
600
+ }
601
+ return true;
602
+ }
536
603
  function isStripeCheckoutHostedMode(value) {
537
604
  return value === "hosted" || value === "hosted_page";
538
605
  }
@@ -550,7 +617,7 @@ function encodeAscii(value) {
550
617
  }
551
618
  function context(value) {
552
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))
553
- reject();
620
+ reject("context_invalid");
554
621
  return { version: 1, session_id: value.session_id, synthetic_payment_method_id: value.synthetic_payment_method_id };
555
622
  }
556
623
  function encodeStripeCheckoutContext(value) {
@@ -564,7 +631,7 @@ function parseStripeCheckoutContext(request) {
564
631
  if (!entries.length)
565
632
  return null;
566
633
  if (entries.length !== 1 || typeof entries[0][1] !== "string" || !/^[A-Za-z0-9_-]{1,1024}$/.test(entries[0][1]))
567
- reject();
634
+ reject("context_invalid");
568
635
  const encoded = entries[0][1];
569
636
  let text = "", bits = 0, count = 0;
570
637
  for (const char of encoded) {
@@ -580,10 +647,10 @@ function parseStripeCheckoutContext(request) {
580
647
  value = context(JSON.parse(text));
581
648
  }
582
649
  catch {
583
- reject();
650
+ reject("context_invalid");
584
651
  }
585
652
  if (encodeStripeCheckoutContext(value) !== encoded)
586
- reject();
653
+ reject("context_invalid");
587
654
  return value;
588
655
  }
589
656
  function endpoint(raw) {
@@ -600,7 +667,7 @@ function endpoint(raw) {
600
667
  }
601
668
  function isStripeCheckoutEndpoint(raw) {
602
669
  const url = endpoint(raw);
603
- const match = url && /^\/v1\/payment_pages\/(cs_test_[A-Za-z0-9]+)\/confirm$/.exec(url.pathname);
670
+ const match = url && /^\/v1\/payment_pages\/(cs_(?:test|live)_[A-Za-z0-9]+)\/confirm$/.exec(url.pathname);
604
671
  return !!match && SESSION.test(match[1]);
605
672
  }
606
673
  var finalFields = /* @__PURE__ */ new Set([
@@ -608,6 +675,7 @@ var finalFields = /* @__PURE__ */ new Set([
608
675
  "key",
609
676
  "expected_amount",
610
677
  "expected_payment_method_type",
678
+ "payment_method_data[billing_details][email]",
611
679
  "init_checksum",
612
680
  "js_checksum",
613
681
  "version",
@@ -619,6 +687,9 @@ var finalFields = /* @__PURE__ */ new Set([
619
687
  "sid",
620
688
  "passive_captcha_token",
621
689
  "passive_captcha_ekey",
690
+ "pxcts",
691
+ "pxvid",
692
+ "px3",
622
693
  ...[
623
694
  "checkout_config_id",
624
695
  "checkout_session_id",
@@ -658,24 +729,32 @@ var tokenFields = /* @__PURE__ */ new Set([
658
729
  ].map((key) => `client_attribution_metadata[${key}]`)
659
730
  ]);
660
731
  function form(request, fields) {
661
- 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)))
662
- 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");
663
738
  const pairs = [];
664
739
  const seen = /* @__PURE__ */ new Set();
665
740
  for (const raw of request.body.split("&")) {
666
741
  const index = raw.indexOf("=");
667
742
  if (index < 1)
668
- reject();
743
+ reject("form_encoding");
669
744
  let key, value;
670
745
  try {
671
746
  key = decodeURIComponent(raw.slice(0, index).replace(/\+/g, " "));
672
747
  value = decodeURIComponent(raw.slice(index + 1).replace(/\+/g, " "));
673
748
  }
674
749
  catch {
675
- reject();
750
+ reject("form_encoding");
676
751
  }
677
- if (!fields.has(key) || seen.has(key) || !opaque(value))
678
- 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");
679
758
  seen.add(key);
680
759
  pairs.push({ key, value, raw });
681
760
  }
@@ -684,31 +763,60 @@ function form(request, fields) {
684
763
  function keyFor(request, values, expected) {
685
764
  const authorization = Object.entries(request.headers).filter(([key2]) => key2.toLowerCase() === "authorization");
686
765
  if (authorization.length > 1)
687
- reject();
766
+ reject("publishable_key");
688
767
  let headerKey;
689
768
  if (authorization.length) {
690
- const match = /^Bearer (pk_test_[A-Za-z0-9_]{1,256})$/.exec(authorization[0][1]);
769
+ const match = /^Bearer (pk_(?:test|live)_[A-Za-z0-9_]{1,256})$/.exec(authorization[0][1]);
691
770
  if (!match)
692
- reject();
771
+ reject("publishable_key");
693
772
  headerKey = match[1];
694
773
  }
695
774
  const key = values.get("key") || headerKey;
696
775
  if (!KEY.test(key) || headerKey && headerKey !== key || expected !== void 0 && expected !== key)
697
- reject();
776
+ reject("publishable_key");
698
777
  return key;
699
778
  }
779
+ var billingEmail = (value) => value === null || typeof value === "string" && value.length > 0 && value.length <= 320 && opaque(value);
780
+ function captureStripeCheckoutBilling(request, options) {
781
+ const phase = classifyStripeCheckoutRequest(request, options);
782
+ if (phase?.phase !== "tokenization" || !SYNTHETIC.test(options?.syntheticPaymentMethodId))
783
+ reject("billing_binding");
784
+ const email = form(request, tokenFields).values.get("billing_details[email]") ?? null;
785
+ if (!billingEmail(email))
786
+ reject("billing_email");
787
+ return Object.freeze({
788
+ session_id: phase.session_id,
789
+ synthetic_payment_method_id: options.syntheticPaymentMethodId,
790
+ email
791
+ });
792
+ }
793
+ function attachStripeCheckoutBilling(request, captured, options) {
794
+ const phase = classifyStripeCheckoutRequest(request, options);
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))
796
+ reject("billing_binding");
797
+ const field = "payment_method_data[billing_details][email]";
798
+ const existing = form(request, finalFields).values.get(field);
799
+ if (existing !== void 0 && existing !== captured.email)
800
+ reject("billing_conflict");
801
+ if (captured.email === null || existing !== void 0)
802
+ return { ...request };
803
+ const body = `${request.body}&${encodeURIComponent(field)}=${encodeURIComponent(captured.email)}`;
804
+ const result = { ...request, body };
805
+ classifyStripeCheckoutRequest(result, options);
806
+ return result;
807
+ }
700
808
  function classifyStripeCheckoutRequest(request, options = {}) {
701
809
  const url = endpoint(request?.url);
702
810
  if (!url) {
703
811
  if (hasStripeCheckoutMarker(request))
704
- reject();
812
+ reject("context_invalid");
705
813
  return null;
706
814
  }
707
815
  const final = isStripeCheckoutEndpoint(request.url);
708
816
  const token = url.pathname === "/v1/payment_methods";
709
817
  if (!final && !token) {
710
818
  if (hasStripeCheckoutMarker(request))
711
- reject();
819
+ reject("context_invalid");
712
820
  return null;
713
821
  }
714
822
  if (token && options.sessionId === void 0 && !hasStripeCheckoutMarker(request))
@@ -716,22 +824,34 @@ function classifyStripeCheckoutRequest(request, options = {}) {
716
824
  const marker = parseStripeCheckoutContext(request);
717
825
  const sessionId = final ? url.pathname.split("/")[3] : options.sessionId;
718
826
  if (!SESSION.test(sessionId) || options.sessionId !== void 0 && options.sessionId !== sessionId || marker && marker.session_id !== sessionId)
719
- reject();
827
+ reject("session_binding");
720
828
  const { values } = form(request, final ? finalFields : tokenFields);
721
829
  const key = keyFor(request, values, options.publishableKey);
830
+ validateStripeCheckoutIdentity({ environment: options.environment, sessionId, publishableKey: key });
722
831
  const attributionSession = values.get("client_attribution_metadata[checkout_session_id]");
723
832
  if (attributionSession !== void 0 && attributionSession !== sessionId)
724
- reject();
833
+ reject("attribution_session");
725
834
  if (token) {
726
835
  const number = values.get("card[number]");
727
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")
728
- reject();
837
+ reject("tokenization_fields");
729
838
  return { phase: "tokenization", session_id: sessionId, publishable_key: key };
730
839
  }
731
840
  const amount = values.get("expected_amount");
732
841
  const syntheticId = values.get("payment_method");
733
- 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)
734
- reject();
842
+ const email = values.get("payment_method_data[billing_details][email]");
843
+ if (email !== void 0 && !billingEmail(email))
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");
735
855
  return {
736
856
  phase: "final",
737
857
  session_id: sessionId,
@@ -753,7 +873,7 @@ function createStripeCheckoutTokenizationStub(request, options) {
753
873
  id: options.syntheticPaymentMethodId,
754
874
  object: "payment_method",
755
875
  type: "card",
756
- livemode: false,
876
+ livemode: validateStripeCheckoutIdentity({ environment: options.environment, sessionId: phase.session_id }).livemode,
757
877
  billing_details: { ...billing, address },
758
878
  card: {
759
879
  brand: "visa",
@@ -783,11 +903,12 @@ function parseStripeCheckoutResponse(raw, options) {
783
903
  let value;
784
904
  try {
785
905
  value = typeof raw === "string" ? JSON.parse(raw) : raw;
906
+ validateStripeCheckoutIdentity({ environment: options?.environment, sessionId: options?.sessionId, livemode: value?.livemode });
786
907
  }
787
908
  catch {
788
909
  return null;
789
910
  }
790
- if (!object(value) || !SESSION.test(options?.sessionId) || value.session_id !== options.sessionId || Object.keys(value).some((key) => !terminalFields.has(key)) || !["id", "object", "state", "status", "payment_status", "currency", "livemode", "session_id", "mode", "ui_mode"].every((key) => Object.hasOwn(value, key)) || typeof value.id !== "string" || !/^[A-Za-z0-9_]{1,256}(?![\s\S])/.test(value.id) || value.object !== "checkout.session" || value.state !== "succeeded" || value.status !== "complete" || value.payment_status !== "paid" || value.currency !== "usd" || value.livemode !== false || value.mode !== "payment" || !isStripeCheckoutHostedMode(value.ui_mode))
911
+ if (!object(value) || !SESSION.test(options?.sessionId) || value.session_id !== options.sessionId || Object.keys(value).some((key) => !terminalFields.has(key)) || !["id", "object", "state", "status", "payment_status", "currency", "livemode", "session_id", "mode", "ui_mode"].every((key) => Object.hasOwn(value, key)) || typeof value.id !== "string" || !/^[A-Za-z0-9_]{1,256}(?![\s\S])/.test(value.id) || value.object !== "checkout.session" || value.state !== "succeeded" || value.status !== "complete" || value.payment_status !== "paid" || value.currency !== "usd" || value.mode !== "payment" || !isStripeCheckoutHostedMode(value.ui_mode))
791
912
  return null;
792
913
  for (const key of ["success_url", "return_url"]) {
793
914
  if (!Object.hasOwn(value, key) || value[key] === null)
@@ -822,7 +943,7 @@ function prepareStripeCheckoutReplay(request, card, options = {}) {
822
943
  sessionId: marker.session_id,
823
944
  syntheticPaymentMethodId: marker.synthetic_payment_method_id
824
945
  });
825
- if (phase?.phase !== "final" || options.amountCents !== void 0 && phase.amount_cents !== options.amountCents || options.sessionId !== void 0 && options.sessionId !== marker.session_id || !card || !["4242424242424242", "4000000000000002", "4000002500003155"].includes(card.number) || card.cvc !== "123" || !/^(?:0?[1-9]|1[0-2])$/.test(String(card.month)) || !/^(?:\d{2}|\d{4})$/.test(String(card.year)) || card.name !== void 0 && !opaque(card.name))
946
+ if (phase?.phase !== "final" || options.amountCents !== void 0 && phase.amount_cents !== options.amountCents || options.sessionId !== void 0 && options.sessionId !== marker.session_id || !validateStripeCheckoutCard(card, options.environment))
826
947
  reject();
827
948
  const replacements = {
828
949
  "payment_method_data[type]": "card",
@@ -840,4 +961,4 @@ function prepareStripeCheckoutReplay(request, card, options = {}) {
840
961
  where: Object.keys(replacements)
841
962
  };
842
963
  }
843
- export { STRIPE_CHECKOUT_CONTEXT_HEADER, STRIPE_CHECKOUT_OPERATION, classifyStripeCheckoutRequest, createStripeCheckoutTokenizationStub, encodeStripeCheckoutContext, hasStripeCheckoutMarker, isStripeCheckoutEndpoint, isStripeCheckoutHostedMode, parseStripeCheckoutContext, parseStripeCheckoutResponse, prepareStripeCheckoutReplay };
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, } 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) {
@@ -26,22 +28,23 @@ export class StripeCheckoutClaimConflictError extends StripeCheckoutRejectionErr
26
28
  */
27
29
  export class StripeCheckoutGate {
28
30
  state = 'fresh';
31
+ billing;
29
32
  binding;
30
33
  constructor(options) {
31
34
  if (!options.stripeCheckout)
32
35
  return;
33
- const { sessionId, publishableKey } = options.stripeCheckout;
36
+ const { sessionId, publishableKey, environment } = options.stripeCheckout;
37
+ validateStripeCheckoutIdentity({ environment, sessionId, publishableKey });
34
38
  if (options.executionMode !== 'autopilot' || !/^apg_[A-Za-z0-9_-]{1,128}(?![\s\S])/.test(options.grantId ?? '')
35
- || !Number.isSafeInteger(options.amount) || Number(options.amount) <= 0 || options.currency !== 'usd'
36
- || !/^cs_test_[A-Za-z0-9]{1,200}(?![\s\S])/.test(sessionId) || !/^pk_test_[A-Za-z0-9_]{1,256}(?![\s\S])/.test(publishableKey))
37
- throw new Error('Native Stripe Checkout requires a TEST session, grant and numeric USD amount.');
38
- this.binding = Object.freeze({ sessionId, publishableKey, amountCents: options.amount,
39
+ || !Number.isSafeInteger(options.amount) || Number(options.amount) <= 0 || options.currency !== 'usd')
40
+ throw new Error('Native Stripe Checkout requires a mode-bound session, grant and numeric USD amount.');
41
+ this.binding = Object.freeze({ environment, sessionId, publishableKey, amountCents: options.amount,
39
42
  syntheticPaymentMethodId: `pm_agentcard_checkout_${crypto.randomUUID().replaceAll('-', '')}` });
40
43
  }
41
44
  matches(url) { return !!this.binding && isStripeCheckoutEndpoint(url); }
42
45
  isEnabled() { return !!this.binding; }
43
46
  isPrepared() { return this.state !== 'fresh'; }
44
- invalidate() { this.state = 'stopped'; }
47
+ invalidate() { this.state = 'stopped'; this.billing = undefined; }
45
48
  /** Observability only: this endpoint label never participates in a payment decision. */
46
49
  describeRejection(error, rawUrl, stage, phase = 'unknown') {
47
50
  let family = 'other';
@@ -58,8 +61,10 @@ export class StripeCheckoutGate {
58
61
  readiness: 'readiness_unavailable', document: 'document_unavailable', stub_response: 'stub_response_failed',
59
62
  };
60
63
  const rejection = error instanceof StripeCheckoutRejectionError ? error : undefined;
64
+ const reason = rejection?.reason ?? fallback[stage];
61
65
  return { version: 1, processor: 'stripe', endpoint_family: family,
62
- 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 } : {}),
63
68
  gate_state: this.state, disposition: error instanceof StripeCheckoutClaimConflictError ? 'active_claim_preserved' : 'checkout_stopped' };
64
69
  }
65
70
  assertClaim(step) {
@@ -72,7 +77,7 @@ export class StripeCheckoutGate {
72
77
  return;
73
78
  const url = new URL(raw);
74
79
  if (url.origin !== 'https://checkout.stripe.com' || url.username || url.password
75
- || ![`/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)) {
76
81
  this.invalidate();
77
82
  throw new StripeCheckoutRejectionError('Native Stripe Checkout document changed.', 'document', 'document_changed');
78
83
  }
@@ -86,8 +91,8 @@ export class StripeCheckoutGate {
86
91
  try {
87
92
  phase = classifyStripeCheckoutRequest(request, this.binding);
88
93
  }
89
- catch {
90
- 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));
91
96
  }
92
97
  if (!phase)
93
98
  return null;
@@ -102,9 +107,13 @@ export class StripeCheckoutGate {
102
107
  this.state = 'stopped';
103
108
  let response;
104
109
  try {
110
+ this.billing = captureStripeCheckoutBilling(request, this.binding);
105
111
  response = createStripeCheckoutTokenizationStub(request, this.binding);
106
112
  }
107
- 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);
108
117
  throw new StripeCheckoutRejectionError('stripe_checkout_rejected', 'stub_response', 'stub_response_failed', 'tokenization');
109
118
  }
110
119
  this.state = 'stubbed';
@@ -114,8 +123,17 @@ export class StripeCheckoutGate {
114
123
  throw new StripeCheckoutClaimConflictError('final');
115
124
  if (phase.phase !== 'final' || this.state !== 'stubbed')
116
125
  throw new StripeCheckoutRejectionError('Native Stripe Checkout has no matching prepared request.', 'claim', 'preparation_missing', diagnosticPhase);
126
+ let final;
127
+ try {
128
+ final = attachStripeCheckoutBilling(request, this.billing, this.binding);
129
+ }
130
+ catch (error) {
131
+ this.invalidate();
132
+ throw new StripeCheckoutRejectionError('stripe_checkout_rejected', 'classification', 'request_validation_failed', 'final', stripeCheckoutValidationCode(error));
133
+ }
117
134
  this.state = 'submitted';
118
- return { phase: 'final', request: { ...request, headers: { ...request.headers,
135
+ this.billing = undefined;
136
+ return { phase: 'final', request: { ...final, headers: { ...final.headers,
119
137
  [STRIPE_CHECKOUT_CONTEXT_HEADER]: encodeStripeCheckoutContext({ version: 1,
120
138
  session_id: this.binding.sessionId, synthetic_payment_method_id: this.binding.syntheticPaymentMethodId }) } } };
121
139
  }