@agent-cards/checkout 0.7.0 → 0.8.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 @@
1
+ export const BUILTIN_REGISTRY = [{ "psp": "shopify", "match": /(checkout\.pci\.shopifyinc\.com|deposit\.[a-z0-9-]+\.shopifycs\.com)\/sessions/i, "hosts": ["^checkout\\.pci\\.shopifyinc\\.com$", "^deposit\\.[a-z0-9-]+\\.shopifycs\\.com$"], "encoding": "json", "passthroughHeaders": [/^shopify-identification-signature$/i] }, { "psp": "stripe", "match": /api\.stripe\.com\/v1\/(payment_methods|tokens|setup_intents\/seti_\w+\/confirm|payment_intents\/pi_\w+\/confirm)/i, "hosts": ["^api\\.stripe\\.com$"], "encoding": "form", "passthroughHeaders": [/^authorization$/i, /^stripe-version$/i, /^x-stripe-client-user-agent$/i] }, { "psp": "braintree", "match": /payments(\.sandbox)?\.braintree-api\.com\/graphql/i, "hosts": ["^payments\\.braintree-api\\.com$", "^payments\\.sandbox\\.braintree-api\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^braintree-version$/i] }, { "psp": "checkout_com", "match": /(api|card-acquisition-gateway)(\.sandbox)?\.checkout\.com\/tokens/i, "hosts": ["^api\\.checkout\\.com$", "^api\\.sandbox\\.checkout\\.com$", "^card-acquisition-gateway\\.checkout\\.com$", "^card-acquisition-gateway\\.sandbox\\.checkout\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i] }, { "psp": "adyen", "match": /(checkoutshopper-(test|live(-[a-z]+)?)\.adyen\.com|([a-z0-9-]+\.)*adyenpayments\.com)\/checkoutshopper\/v1\/sessions\/[A-Za-z0-9_-]+\/payments/i, "hosts": ["^checkoutshopper-(test|live(-[a-z]+)?)\\.adyen\\.com$", "^([a-z0-9-]+\\.)*adyenpayments\\.com$"], "encoding": "json", "passthroughHeaders": [], "clientSideEncrypted": true, "mode": "cse" }, { "psp": "tranzila", "match": /(direct\.tranzila\.com\/process\/n\/|directng\.tranzila\.com\/process\/?)/i, "hosts": ["^direct\\.tranzila\\.com$", "^directng\\.tranzila\\.com$"], "encoding": "form", "passthroughHeaders": [], "mode": "hosted_form" }, { "psp": "square", "match": /pci-connect\.squareup(?:sandbox)?\.com\/v2\/card-nonce/i, "hosts": ["^pci-connect\\.squareup\\.com$", "^pci-connect\\.squareupsandbox\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "authorize_net", "match": /(api2|apitest)\.authorize\.net\/xml\/v1\/request\.api/i, "hosts": ["^api2\\.authorize\\.net$", "^apitest\\.authorize\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "worldpay", "match": /(try\.)?access\.worldpay\.com\/(sessions\/card|verifiedTokens\/sessions)/i, "hosts": ["^access\\.worldpay\\.com$", "^try\\.access\\.worldpay\\.com$"], "encoding": "json", "passthroughHeaders": [/^accept$/i] }, { "psp": "nuvei", "match": /(secure|ppp-test)\.safecharge\.com\/ppp\/api\/v1\/(?:cardTokenization|clientPayment|websdk\/initPaymentWithCardTokenization)\.do/i, "hosts": ["^secure\\.safecharge\\.com$", "^ppp-test\\.safecharge\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "airwallex", "match": /(checkout(\.sandbox)?\.airwallex\.com|pci-api(\.sandbox)?\.airwallex\.com)\/api\/v1\/pa\/(payment_intents\/[^/?#]+\/confirm|payment_consents\/[^/?#]+\/verify|payment_methods\/create)(\?|$)/i, "hosts": ["^checkout\\.airwallex\\.com$", "^checkout\\.sandbox\\.airwallex\\.com$", "^pci-api\\.airwallex\\.com$", "^pci-api\\.sandbox\\.airwallex\\.com$"], "encoding": "json", "passthroughHeaders": [/^client-secret$/i, /^authorization$/i, /^x-auth-token$/i, /^x-on-behalf-of$/i, /^x-api-version$/i] }, { "psp": "rapyd", "match": /(api|sandboxapi)\.rapyd\.net\/v1\/(?:hosted\/collect\/card\/[^/?#]+\/payment_method|checkout\/checkout_[A-Za-z0-9]+)(?:[?#]|$)/i, "hosts": ["^api\\.rapyd\\.net$", "^sandboxapi\\.rapyd\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "dlocal", "match": /ppmcc(-sandbox)?\.dlocal\.com\/cvault\/credit-card\/temporal/i, "hosts": ["^ppmcc\\.dlocal\\.com$", "^ppmcc-sandbox\\.dlocal\\.com$"], "encoding": "json", "passthroughHeaders": [/^x-fields-api-key$/i, /^x-uow$/i, /^x-dlocal-infrav2$/i] }, { "psp": "ebanx", "match": /(?:(?:customer-frontier|customer|api-local-latam|api|sandbox-local-latam|sandbox)\.ebanx\.com|(?:api-diamond|api|sandbox)\.ebanxpay\.com)\/ws\/token/i, "hosts": ["^customer\\.ebanx\\.com$", "^customer-frontier\\.ebanx\\.com$", "^api\\.ebanx\\.com$", "^sandbox\\.ebanx\\.com$", "^api-local-latam\\.ebanx\\.com$", "^sandbox-local-latam\\.ebanx\\.com$", "^api-diamond\\.ebanxpay\\.com$", "^api\\.ebanxpay\\.com$", "^sandbox\\.ebanxpay\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "mercado_pago", "match": /api\.mercadopago\.com\/v1\/card_tokens(?![\w/-])/i, "hosts": ["^api\\.mercadopago\\.com$"], "encoding": "json", "passthroughHeaders": [/^x-product-id$/i] }, { "psp": "payu", "match": /(secure\.payu\.com|(merch-prod|secure)\.snd\.payu\.com)\/api\/front\/tokens/i, "hosts": ["^secure\\.payu\\.com$", "^(merch-prod|secure)\\.snd\\.payu\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "razorpay", "match": /api\.razorpay\.com\/v1\/(?:standard_checkout\/)?payments\/create\/(?:ajax|checkout|fees)/i, "hosts": ["^api\\.razorpay\\.com$"], "encoding": "form", "passthroughHeaders": [/^x-razorpay-sessionid$/i, /^x-customer-access-token$/i] }, { "psp": "mollie", "match": /api\.cc\.mollie\.com\/v1\/card-tokens/i, "hosts": ["^api\\.cc\\.mollie\\.com$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "paysafe", "match": /(?:api(\.test)?\.paysafe\.com\/(?:paymenthub\/v1\/singleusepaymenthandles|js\/api\/v1\/tokenize)|hosted(\.test)?\.paysafe\.com\/checkout\/api\/v1\/tokenize)(?![\w/-])/i, "hosts": ["^api\\.paysafe\\.com$", "^api\\.test\\.paysafe\\.com$", "^hosted\\.paysafe\\.com$", "^hosted\\.test\\.paysafe\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^x-paysafe-credentials$/i, /^correlationid$/i] }, { "psp": "recurly", "match": /api(\.eu)?\.recurly\.com\/js\/v1\/token(?![\w-])/i, "hosts": ["^api\\.recurly\\.com$", "^api\\.eu\\.recurly\\.com$"], "encoding": "form", "passthroughHeaders": [/^recurly-credential-checkout-hostname$/i] }, { "psp": "moneris", "match": /(www3|esqa|gateway|gatewayqa|gatewayt|gatewaydev)\.moneris\.com\/HPPtoken\/request\.php/i, "hosts": ["^www3\\.moneris\\.com$", "^esqa\\.moneris\\.com$", "^gateway\\.moneris\\.com$", "^gatewayqa\\.moneris\\.com$", "^gatewayt\\.moneris\\.com$", "^gatewaydev\\.moneris\\.com$"], "encoding": "form", "passthroughHeaders": [] }, { "psp": "bambora", "match": /(api\.na\.bambora\.com|api\.bam\.shift4api\.net)\/scripts\/tokenization\/tokens/i, "hosts": ["^api\\.na\\.bambora\\.com$", "^api\\.bam\\.shift4api\\.net$"], "encoding": "json", "passthroughHeaders": [] }, { "psp": "global_payments", "match": /(?:api\.heartlandportico\.com\/SecureSubmit\.v1\/api\/token|cert\.api2\.heartlandportico\.com\/Hps\.Exchange\.PosGateway\.Hpf\.v1\/api\/token|apis(?:\.sandbox)?(?:\.eu)?\.globalpay\.com\/ucp\/(?:merchants\/[^/]+\/)?payment-methods)/i, "hosts": ["^api\\.heartlandportico\\.com$", "^cert\\.api2\\.heartlandportico\\.com$", "^apis\\.globalpay\\.com$", "^apis\\.sandbox\\.globalpay\\.com$", "^apis\\.eu\\.globalpay\\.com$", "^apis\\.sandbox\\.eu\\.globalpay\\.com$"], "encoding": "json", "passthroughHeaders": [/^authorization$/i, /^x-gp-version$/i] }];
package/dist/cdp.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type VaultClient } from './client.js';
1
+ import { type VaultClient, type ExecutionMetadata } from './client.js';
2
2
  import { type CheckoutController, type LifecycleOptions, type PaymentEndpointGuard } from './lifecycle.js';
3
3
  /**
4
4
  * The CORS headers a fulfilled CROSS-ORIGIN request needs, or null when the
@@ -71,7 +71,9 @@ export interface CdpLike {
71
71
  send(method: string, params?: any, sessionId?: string): Promise<any>;
72
72
  on(handler: (method: string, params: any, sessionId?: string) => void): void;
73
73
  }
74
- export interface AttachOptions extends LifecycleOptions {
74
+ export interface AttachOptions extends LifecycleOptions, ExecutionMetadata {
75
+ /** Fallback when the browser cannot expose its top-level origin. Never an amount/payee authority. */
76
+ merchantOrigin?: string;
75
77
  vault: VaultClient;
76
78
  user: string;
77
79
  merchant: string;
package/dist/cdp.js CHANGED
@@ -282,9 +282,23 @@ function headerEntries(headers) {
282
282
  * undefined and the authorization goes out without one (a merchant, category
283
283
  * or place rule then refuses it as unknown, never the request itself).
284
284
  */
285
- async function pageOriginOf(readDocumentUrl) {
285
+ async function pageOriginOf(readDocumentUrl, signal) {
286
+ let timer;
287
+ let abort;
286
288
  try {
287
- const url = new URL(await readDocumentUrl());
289
+ const documentUrl = await Promise.race([
290
+ Promise.resolve().then(readDocumentUrl).catch(() => undefined),
291
+ new Promise((resolve) => {
292
+ timer = setTimeout(resolve, 1_000);
293
+ abort = () => resolve(undefined);
294
+ signal.addEventListener('abort', abort, { once: true });
295
+ if (signal.aborted)
296
+ abort();
297
+ }),
298
+ ]);
299
+ if (typeof documentUrl !== 'string')
300
+ return undefined;
301
+ const url = new URL(documentUrl);
288
302
  if (url.username || url.password)
289
303
  return undefined;
290
304
  if (url.protocol === 'https:' || (url.protocol === 'http:' && url.hostname === 'localhost'))
@@ -294,6 +308,12 @@ async function pageOriginOf(readDocumentUrl) {
294
308
  catch {
295
309
  return undefined;
296
310
  }
311
+ finally {
312
+ if (timer)
313
+ clearTimeout(timer);
314
+ if (abort)
315
+ signal.removeEventListener('abort', abort);
316
+ }
297
317
  }
298
318
  /**
299
319
  * The page total, the lowest amount authority, through the integrator's own
@@ -587,15 +607,21 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
587
607
  activeRequest = { attempt, networkId, sessionId, frameId };
588
608
  if (preparation)
589
609
  await preparationGate.assertDocument();
590
- const pageOrigin = await pageOriginOf(readDocumentUrl);
610
+ const pageOrigin = await pageOriginOf(readDocumentUrl, attempt.signal);
591
611
  const pageAmount = await pageAmountOf(opts);
592
612
  attempt.assertLive();
613
+ const merchantOrigin = preparation?.merchantOrigin ?? (opts.executionMode === 'user_approval'
614
+ ? opts.merchantOrigin : pageOrigin?.startsWith('https:') ? pageOrigin : opts.merchantOrigin);
615
+ attempt.assertLive();
593
616
  const replay = await opts.vault.authorize({
594
617
  user: opts.user,
595
618
  merchant: opts.merchant,
596
619
  amount: opts.amount,
597
620
  currency: opts.currency,
598
621
  cardId: preparation?.cardId ?? opts.cardId,
622
+ executionMode: opts.executionMode,
623
+ grantId: opts.grantId,
624
+ merchantOrigin,
599
625
  pageOrigin,
600
626
  pageAmount,
601
627
  preparation,
@@ -858,15 +884,21 @@ export async function attachToPlaywright(page, opts) {
858
884
  activeRequest = { request, frames, attempt };
859
885
  if (preparation)
860
886
  await preparationGate.assertDocument();
861
- const pageOrigin = await pageOriginOf(readDocumentUrl);
887
+ const pageOrigin = await pageOriginOf(readDocumentUrl, attempt.signal);
862
888
  const pageAmount = await pageAmountOf(opts);
863
889
  assertRequestLive();
890
+ const merchantOrigin = preparation?.merchantOrigin ?? (opts.executionMode === 'user_approval'
891
+ ? opts.merchantOrigin : pageOrigin?.startsWith('https:') ? pageOrigin : opts.merchantOrigin);
892
+ assertRequestLive();
864
893
  const replay = await opts.vault.authorize({
865
894
  user: opts.user,
866
895
  merchant: opts.merchant,
867
896
  amount: opts.amount,
868
897
  currency: opts.currency,
869
898
  cardId: preparation?.cardId ?? opts.cardId,
899
+ executionMode: opts.executionMode,
900
+ grantId: opts.grantId,
901
+ merchantOrigin,
870
902
  pageOrigin,
871
903
  pageAmount,
872
904
  preparation,
package/dist/client.d.ts CHANGED
@@ -29,11 +29,18 @@ export declare const SUPPORTED_MODES: readonly CheckoutMode[];
29
29
  export type AmountAuthority = 'processor' | 'agent' | 'page' | 'none';
30
30
  /** An integer in the smallest unit, or a decimal string in normal units with a point; nothing else. */
31
31
  export declare function validAmountInput(amount: unknown): amount is number | string;
32
+ /** How an existing authorization is being handled; omitted by older APIs. */
33
+ export interface ExecutionMetadata {
34
+ executionMode?: 'user_approval' | 'autopilot';
35
+ grantId?: string;
36
+ }
32
37
  /**
33
38
  * The token flow: the cardholder's device called the processor itself, and
34
39
  * this is the processor's answer to replay into the paused request.
35
40
  */
36
- export interface TokenReplay {
41
+ export interface TokenReplay extends ExecutionMetadata {
42
+ /** Present only after validating the explicit owned-shop purchase receipt. */
43
+ shopOrderId?: string;
37
44
  /** Absent on older API versions; always 'token' here. */
38
45
  mode?: 'token';
39
46
  /** The approved authorization (`cauth_…`). */
@@ -73,7 +80,7 @@ export interface TokenReplay {
73
80
  * The processor's answer then reaches the page as it normally would; the
74
81
  * merchant's order state is where the outcome shows up.
75
82
  */
76
- export interface CseReplay {
83
+ export interface CseReplay extends ExecutionMetadata {
77
84
  mode: 'cse';
78
85
  authorizationId: string;
79
86
  substitutions: Substitutions;
@@ -96,7 +103,7 @@ export interface CseReplay {
96
103
  * processor evidence on this mode and cannot obtain any. Treat it as "the
97
104
  * person paid, or tried to, on their own device" and confirm with the merchant.
98
105
  */
99
- export interface HostedFormReplay {
106
+ export interface HostedFormReplay extends ExecutionMetadata {
100
107
  mode: 'hosted_form';
101
108
  /** What this resolution is: a device-attested submission, not a processor answer. */
102
109
  kind: 'submitted_on_device';
@@ -152,7 +159,9 @@ export declare class CheckoutPreparationError extends Error {
152
159
  reason: string;
153
160
  constructor(preparationId: string | null, reason: string);
154
161
  }
155
- export interface AuthorizeInput {
162
+ export interface AuthorizeInput extends ExecutionMetadata {
163
+ /** Observed top-level HTTPS merchant origin; a routing hint, never payment authority. */
164
+ merchantOrigin?: string;
156
165
  /** Your identifier for the person whose card should pay. */
157
166
  user: string;
158
167
  /** Shown to the user on the approval screen. Judged by nothing: the merchant comes from the checkout page. */
package/dist/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { BUILTIN_REGISTRY, cardUrlPatterns as deriveCardUrlPatterns, findRecognizer, } from './registry.js';
2
2
  import { matchesPreparedRequest, validPreparationEnvironment } from './prepared-processor.js';
3
+ import { hasOwnedShopMarker, parseOwnedShopOrder, parseOwnedShopReceipt } from './owned-shop.generated.js';
3
4
  /**
4
5
  * The modes this SDK can finish. Asked for on syncRegistry (the API serves
5
6
  * only recognizers in these modes, so a request this build cannot complete
@@ -237,7 +238,7 @@ export class PresetRefusedError extends ApprovalDeclinedError {
237
238
  }
238
239
  /** A decline reason only the company presets stamp (see PresetRefusedError). */
239
240
  export const PRESET_REFUSAL_REASONS = new Set([
240
- 'spend_total_exceeded', 'spend_rate_exceeded', 'spend_rate_unknown',
241
+ 'spend_total_exceeded', 'spend_rate_exceeded', 'spend_rate_unknown', 'amount_unknown', 'rate_unavailable',
241
242
  'category_denied', 'category_unknown', 'merchant_denied', 'merchant_unknown',
242
243
  'geo_denied', 'geo_unknown', 'currency_denied', 'currency_unknown',
243
244
  'time_window_denied', 'surface_denied', 'surface_unknown',
@@ -512,6 +513,27 @@ export class VaultClient {
512
513
  * response to replay into the browser. Your process never sees a card.
513
514
  */
514
515
  async authorize(input) {
516
+ const ownedShop = hasOwnedShopMarker(input.request);
517
+ const shopOrder = ownedShop ? parseOwnedShopOrder(input.request) : null;
518
+ if (ownedShop) {
519
+ if (!shopOrder || input.preparation || input.executionMode === 'user_approval' ||
520
+ !input.merchantOrigin || (input.amount != null && input.amount !== shopOrder.amount &&
521
+ !(typeof input.amount === 'string' && Number(input.amount) * 100 === shopOrder.amount)) ||
522
+ (input.currency !== undefined && input.currency.toLowerCase() !== shopOrder.currency))
523
+ throw new Error('The shop order cannot use this checkout configuration.');
524
+ input = { ...input, amount: shopOrder.amount, currency: shopOrder.currency };
525
+ }
526
+ if (input.executionMode !== undefined && !['user_approval', 'autopilot'].includes(input.executionMode))
527
+ throw new Error('Unsupported checkout execution mode.');
528
+ if (input.grantId !== undefined && !/^apg_[A-Za-z0-9_-]{1,128}$/.test(input.grantId))
529
+ throw new Error('Invalid autopilot grant ID.');
530
+ if (input.preparation && (input.executionMode === 'autopilot' || input.grantId))
531
+ throw new Error('A device preparation cannot also select autopilot.');
532
+ if (input.merchantOrigin !== undefined && !input.preparation) {
533
+ const merchantUrl = new URL(input.merchantOrigin);
534
+ if (merchantUrl.protocol !== 'https:' || merchantUrl.origin !== input.merchantOrigin)
535
+ throw new Error('merchantOrigin must be an exact HTTPS origin.');
536
+ }
515
537
  const preparation = input.preparation;
516
538
  if (preparation) {
517
539
  if (!this.preparations.has(preparation) || this.usedPreparations.has(preparation))
@@ -582,6 +604,9 @@ export class VaultClient {
582
604
  // the recognizer and refuses a disagreement before a row exists.
583
605
  mode,
584
606
  ...(input.cardId ? { cardId: input.cardId } : {}),
607
+ ...(input.executionMode ? { execution_mode: input.executionMode } : {}),
608
+ ...(input.grantId ? { grant_id: input.grantId } : {}),
609
+ ...(input.merchantOrigin && !preparation ? { merchant_origin: input.merchantOrigin } : {}),
585
610
  // A prepared checkout already carries the page origin as merchant_origin.
586
611
  ...(preparation
587
612
  ? { preparation_id: preparation.id, checkout_key: preparation.checkoutKey, merchant_origin: preparation.merchantOrigin }
@@ -625,6 +650,20 @@ export class VaultClient {
625
650
  if (!created || typeof created.id !== 'string' || !created.id)
626
651
  throw new PaymentOutcomeUnknownError(preparation ? await this.retireUncertainPreparation(preparation, input.onAuthorizationCreated) : null, 'authorization_create_malformed');
627
652
  const authorizationId = created.id;
653
+ let execution = {};
654
+ let approvalDelivered = false;
655
+ const deliverApproval = (state) => {
656
+ if (ownedShop || preparation || approvalDelivered || execution.executionMode === 'autopilot')
657
+ return;
658
+ const url = state.approvalUrl ?? state.approval_url;
659
+ if (typeof url !== 'string' || !url)
660
+ return;
661
+ approvalDelivered = true;
662
+ try {
663
+ Promise.resolve(input.onApprovalUrl?.(url)).catch(() => { });
664
+ }
665
+ catch { /* observer only */ }
666
+ };
628
667
  let failed = false;
629
668
  const stopSignal = input.merchantSignal
630
669
  ? AbortSignal.any([input.merchantSignal, ...(input.signal ? [input.signal] : [])]) : input.signal;
@@ -635,12 +674,8 @@ export class VaultClient {
635
674
  catch { /* observer only */ }
636
675
  if (input.merchantSignal?.aborted)
637
676
  throw new PaymentOutcomeUnknownError(authorizationId, 'merchant_request_aborted');
638
- if (!preparation) {
639
- try {
640
- Promise.resolve(input.onApprovalUrl?.(created.approvalUrl)).catch(() => { });
641
- }
642
- catch { /* approval delivery must not lose an existing authorization */ }
643
- }
677
+ execution = executionMetadata(created, authorizationId);
678
+ deliverApproval(created);
644
679
  while (Date.now() < deadline) {
645
680
  if (stopSignal?.aborted)
646
681
  throw new PaymentOutcomeUnknownError(authorizationId, input.merchantSignal?.aborted ? 'merchant_request_aborted' : 'local_cancel');
@@ -661,6 +696,11 @@ export class VaultClient {
661
696
  if (!s || typeof s !== 'object' || Array.isArray(s) || typeof s.status !== 'string') {
662
697
  throw new PaymentOutcomeUnknownError(authorizationId, 'authorization_status_malformed');
663
698
  }
699
+ execution = executionMetadata(s, authorizationId, execution);
700
+ if (shopOrder && (s.status === 'submitted_on_device' ||
701
+ (s.status === 'approved' && (s.mode !== 'token' || execution.executionMode !== 'autopilot'))))
702
+ throw new PaymentOutcomeUnknownError(authorizationId, 'shop_receipt_unconfirmed');
703
+ deliverApproval(s);
664
704
  const amountAuthority = typeof s.amount_authority === 'string' && AMOUNT_AUTHORITIES.includes(s.amount_authority)
665
705
  ? { amountAuthority: s.amount_authority }
666
706
  : {};
@@ -676,7 +716,9 @@ export class VaultClient {
676
716
  if (typeof s.submitted_at !== 'string' || !s.submitted_at) {
677
717
  throw new PaymentOutcomeUnknownError(authorizationId, 'submission_timestamp_missing');
678
718
  }
679
- return { mode: 'hosted_form', kind: 'submitted_on_device', outcome: 'unverified', authorizationId, submittedAt: s.submitted_at, ...amountAuthority };
719
+ if (execution.executionMode === 'autopilot')
720
+ throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_submission_mode_invalid');
721
+ return { mode: 'hosted_form', kind: 'submitted_on_device', outcome: 'unverified', authorizationId, submittedAt: s.submitted_at, ...amountAuthority, ...execution };
680
722
  }
681
723
  if (s.status === 'approved') {
682
724
  const approvedMode = typeof s.mode === 'string' ? s.mode : 'token';
@@ -687,6 +729,8 @@ export class VaultClient {
687
729
  throw new PaymentOutcomeUnknownError(authorizationId, 'hosted_form_approval_malformed');
688
730
  }
689
731
  if (approvedMode === 'cse') {
732
+ if (execution.executionMode === 'autopilot')
733
+ throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_submission_mode_invalid');
690
734
  const sub = s.substitutions;
691
735
  const fieldsOk = sub && typeof sub === 'object' && sub.encoding === 'json' && typeof sub.at === 'string' && sub.at
692
736
  && sub.fields && typeof sub.fields === 'object' && !Array.isArray(sub.fields)
@@ -711,6 +755,7 @@ export class VaultClient {
711
755
  ...(removeRaw ? { remove: [...removeRaw] } : {}),
712
756
  },
713
757
  ...amountAuthority,
758
+ ...execution,
714
759
  };
715
760
  }
716
761
  if (approvedMode !== 'token')
@@ -720,15 +765,29 @@ export class VaultClient {
720
765
  || typeof response.body !== 'string' || !response.headers || typeof response.headers !== 'object' || Array.isArray(response.headers)) {
721
766
  throw new PaymentOutcomeUnknownError(authorizationId, 'approved_response_malformed');
722
767
  }
768
+ if (shopOrder) {
769
+ let receipt;
770
+ try {
771
+ receipt = parseOwnedShopReceipt(JSON.parse(response.body), shopOrder);
772
+ }
773
+ catch { /* No raw error or token replay. */ }
774
+ if (execution.executionMode !== 'autopilot' || response.status !== 200 || !receipt)
775
+ throw new PaymentOutcomeUnknownError(authorizationId, 'shop_receipt_unconfirmed');
776
+ // Only canonical receipt fields and our content type enter the page.
777
+ response.body = JSON.stringify(receipt);
778
+ response.headers = { 'content-type': 'application/json' };
779
+ }
723
780
  return {
724
781
  mode: 'token',
725
782
  authorizationId,
783
+ ...(shopOrder ? { shopOrderId: shopOrder.order_id } : {}),
726
784
  ...response,
727
785
  amountVerified: typeof s.amount_verified === 'boolean' ? s.amount_verified : null,
728
786
  chargedAmount: typeof s.charged_amount === 'number' ? s.charged_amount : null,
729
787
  chargedCurrency: typeof s.charged_currency === 'string' ? s.charged_currency : null,
730
788
  chargedKind: s.charged_kind === 'captured' || s.charged_kind === 'authorized' || s.charged_kind === 'none' ? s.charged_kind : null,
731
789
  ...amountAuthority,
790
+ ...execution,
732
791
  };
733
792
  }
734
793
  if (s.status === 'declined') {
@@ -916,6 +975,19 @@ export class VaultClient {
916
975
  return this.call(path, { signal });
917
976
  }
918
977
  }
978
+ function executionMetadata(state, authorizationId, previous = {}) {
979
+ if (state.execution_mode === undefined)
980
+ return previous;
981
+ if (state.execution_mode !== 'user_approval' && state.execution_mode !== 'autopilot')
982
+ throw new PaymentOutcomeUnknownError(authorizationId, 'execution_mode_unrecognized');
983
+ if (state.execution_mode === 'user_approval')
984
+ return { executionMode: 'user_approval' };
985
+ if (typeof state.grant_id !== 'string' || !/^apg_[A-Za-z0-9_-]{1,128}$/.test(state.grant_id))
986
+ throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_grant_unconfirmed');
987
+ if (previous.grantId && previous.grantId !== state.grant_id)
988
+ throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_grant_changed');
989
+ return { executionMode: 'autopilot', grantId: state.grant_id };
990
+ }
919
991
  /**
920
992
  * Forward only the headers the merchant needs to accept the replay. Everything
921
993
  * else (cookies, UA, tracing) is dropped so we transmit as little as possible.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { VaultClient, CardEncryptedError, UnsupportedModeError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, } from './client.js';
2
- export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, HostedFormReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, RazorpayProcessorError, } from './client.js';
2
+ export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, HostedFormReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, RazorpayProcessorError, ExecutionMetadata, } from './client.js';
3
3
  export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
4
4
  export { CheckoutAttachmentError } from './attachment.js';
5
5
  export type { CdpLike, AttachOptions, CorsOutcome } from './cdp.js';
@@ -59,6 +59,7 @@ export declare class CheckoutLifecycle implements CheckoutController {
59
59
  private cancelled;
60
60
  private merchantAborted;
61
61
  private unboundStripeToken;
62
+ private ownedShopOrderId;
62
63
  private reconciliation;
63
64
  private preparationHandler?;
64
65
  private preparationUsed;
package/dist/lifecycle.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ApprovalDeclinedError, ApprovalTimeoutError, CheckoutCancelledError, CheckoutPreparationError, IntentNotConfirmableError, PaymentOutcomeUnknownError, ProcessorRefusedError } from './client.js';
2
+ import { parseOwnedShopReceipt, OWNED_SHOP_SKU, OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY } from './owned-shop.generated.js';
2
3
  /** Shared by the raw CDP and Playwright transports. No browser ownership or payment execution lives here. */
3
4
  export class CheckoutLifecycle {
4
5
  options;
@@ -8,6 +9,7 @@ export class CheckoutLifecycle {
8
9
  cancelled = false;
9
10
  merchantAborted = false;
10
11
  unboundStripeToken = false;
12
+ ownedShopOrderId = null;
11
13
  reconciliation = null;
12
14
  preparationHandler;
13
15
  preparationUsed = false;
@@ -82,6 +84,20 @@ export class CheckoutLifecycle {
82
84
  this.set({ ...this.state, status: 'unsupported', reason: 'unrecognized_payment_endpoint' });
83
85
  }
84
86
  prepareHandoff(replay, requestUrl) {
87
+ if (replay.mode === 'token' && replay.executionMode === 'autopilot' && replay.shopOrderId) {
88
+ let receipt;
89
+ try {
90
+ receipt = parseOwnedShopReceipt(JSON.parse(replay.body), {
91
+ order_id: replay.shopOrderId, sku: OWNED_SHOP_SKU, amount: OWNED_SHOP_AMOUNT_CENTS, currency: OWNED_SHOP_CURRENCY,
92
+ });
93
+ }
94
+ catch { /* An unrecognized response remains subject to the token continuation guard. */ }
95
+ if (receipt) {
96
+ this.ownedShopOrderId = receipt.order_id;
97
+ this.held = true;
98
+ return;
99
+ }
100
+ }
85
101
  // A tokenization approval has no authoritative PaymentIntent or amount
86
102
  // binding. Neither the first observed confirm nor the returned opaque
87
103
  // token can supply one. Hold even in compatibility mode: this attachment
@@ -102,6 +118,11 @@ export class CheckoutLifecycle {
102
118
  handedOff(replay) {
103
119
  if (this.merchantAborted)
104
120
  return;
121
+ if (this.ownedShopOrderId) {
122
+ this.held = true;
123
+ this.set({ status: 'completed', authorizationId: replay.authorizationId, mode: 'token', orderId: this.ownedShopOrderId });
124
+ return;
125
+ }
105
126
  const mismatch = (!replay.mode || replay.mode === 'token') && replay.amountVerified === false;
106
127
  this.held = !!this.options.requireMerchantResult || replay.mode === 'hosted_form' || mismatch || this.unboundStripeToken;
107
128
  this.set({ status: 'awaiting_merchant', authorizationId: replay.authorizationId, mode: replay.mode ?? 'token',
@@ -178,7 +199,10 @@ export class CheckoutLifecycle {
178
199
  result = { status: 'unknown' };
179
200
  if (result.status === 'completed' && typeof result.orderId === 'string' && result.orderId.length > 0) {
180
201
  this.held = true;
181
- this.set({ ...this.state, status: 'completed', orderId: result.orderId });
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 });
182
206
  }
183
207
  else if (result.status === 'failed') {
184
208
  // Keep the guard armed until the application deliberately starts another attempt.
@@ -195,6 +219,8 @@ export class CheckoutLifecycle {
195
219
  return this.getState();
196
220
  }
197
221
  retryAfterMerchantFailure(result) {
222
+ if (this.ownedShopOrderId)
223
+ throw new Error('This shop payment already has a receipt; another payment is not permitted.');
198
224
  if (this.preparationUsed)
199
225
  throw new Error('Prepared checkouts are single use. Reconcile this attempt and create a new attachment.');
200
226
  if (this.active || this.reconciliation)
@@ -0,0 +1,24 @@
1
+ declare var OWNED_SHOP_OPERATION: string;
2
+ declare var OWNED_SHOP_SKU: string;
3
+ declare var OWNED_SHOP_AMOUNT_CENTS: number;
4
+ declare var OWNED_SHOP_CURRENCY: string;
5
+ declare var OWNED_SHOP_ORDER_FIELD: string;
6
+ declare function isOwnedShopPaymentIntentId(value: any): boolean;
7
+ declare function serializeOwnedShopOrder(value: any): string;
8
+ declare function hasOwnedShopMarker(request: any): any;
9
+ declare function parseOwnedShopOrder(request: any): {
10
+ order_id: any;
11
+ sku: string;
12
+ amount: number;
13
+ currency: string;
14
+ } | null;
15
+ declare function parseOwnedShopReceipt(value: any, expectedOrder: any): {
16
+ object: string;
17
+ order_id: any;
18
+ payment_intent_id: any;
19
+ status: string;
20
+ amount: number;
21
+ currency: string;
22
+ livemode: boolean;
23
+ } | null;
24
+ export { OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY, OWNED_SHOP_OPERATION, OWNED_SHOP_ORDER_FIELD, OWNED_SHOP_SKU, hasOwnedShopMarker, isOwnedShopPaymentIntentId, parseOwnedShopOrder, parseOwnedShopReceipt, serializeOwnedShopOrder };
@@ -0,0 +1,108 @@
1
+ // @ts-nocheck
2
+ // Generated from @agent-cards/payment-core. Do not edit.
3
+ // artifact-sha256: c8cfb844f3de3f156ee741a0167256d2e764ddfb7d243ce813e011761d3b711a
4
+ // src/owned-shop.js
5
+ var OWNED_SHOP_OPERATION = "stripe.owned_shop_token_to_payment_intent.v1";
6
+ var OWNED_SHOP_SKU = "autopilot-pilot-100";
7
+ var OWNED_SHOP_AMOUNT_CENTS = 100;
8
+ var OWNED_SHOP_CURRENCY = "usd";
9
+ var OWNED_SHOP_ORDER_FIELD = "agentcard_shop_order";
10
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}(?![\s\S])/;
11
+ var INTENT = /^pi_[A-Za-z0-9]{1,200}(?![\s\S])/;
12
+ var refuse = () => {
13
+ throw new Error("stripe_owned_shop_refused");
14
+ };
15
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
16
+ var keys = (value, allowed) => record(value) && Object.keys(value).length === allowed.length && Object.keys(value).every((key) => allowed.includes(key));
17
+ function isOwnedShopPaymentIntentId(value) {
18
+ return typeof value === "string" && INTENT.test(value);
19
+ }
20
+ function order(value) {
21
+ if (!keys(value, ["order_id", "sku", "amount", "currency"]) || typeof value.order_id !== "string" || !UUID.test(value.order_id) || value.sku !== OWNED_SHOP_SKU || value.amount !== OWNED_SHOP_AMOUNT_CENTS || value.currency !== OWNED_SHOP_CURRENCY)
22
+ refuse();
23
+ return {
24
+ order_id: value.order_id,
25
+ sku: OWNED_SHOP_SKU,
26
+ amount: OWNED_SHOP_AMOUNT_CENTS,
27
+ currency: OWNED_SHOP_CURRENCY
28
+ };
29
+ }
30
+ function serializeOwnedShopOrder(value) {
31
+ return JSON.stringify(order(value));
32
+ }
33
+ function hasOwnedShopMarker(request) {
34
+ if (typeof request?.body !== "string")
35
+ return false;
36
+ return request.body.split("&").some((part) => {
37
+ const raw = part.split("=", 1)[0];
38
+ const loose = raw.replace(/\+/g, " ").replace(/%([0-9a-f]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
39
+ return loose.toLowerCase().includes(OWNED_SHOP_ORDER_FIELD);
40
+ });
41
+ }
42
+ function form(body) {
43
+ if (typeof body !== "string" || !body || body.length > 16384 || /[^\x21-\x7e]/.test(body))
44
+ refuse();
45
+ const chunks = body.split("&");
46
+ if (chunks.length > 16)
47
+ refuse();
48
+ const entries = /* @__PURE__ */ new Map();
49
+ const seen = /* @__PURE__ */ new Set();
50
+ for (const chunk of chunks) {
51
+ const eq = chunk.indexOf("=");
52
+ if (eq < 1)
53
+ refuse();
54
+ let key, value;
55
+ try {
56
+ key = decodeURIComponent(chunk.slice(0, eq).replace(/\+/g, " "));
57
+ value = decodeURIComponent(chunk.slice(eq + 1).replace(/\+/g, " "));
58
+ }
59
+ catch {
60
+ refuse();
61
+ }
62
+ if (seen.has(key.toLowerCase()) || value.length > 2048 || /[\x00-\x1f\x7f]/.test(key + value))
63
+ refuse();
64
+ seen.add(key.toLowerCase());
65
+ entries.set(key, value);
66
+ }
67
+ return entries;
68
+ }
69
+ function parseOwnedShopOrder(request) {
70
+ if (!hasOwnedShopMarker(request))
71
+ return null;
72
+ const entries = form(request.body);
73
+ if (!entries.has(OWNED_SHOP_ORDER_FIELD) || [...entries.keys()].some((key) => key !== OWNED_SHOP_ORDER_FIELD && key.toLowerCase().includes(OWNED_SHOP_ORDER_FIELD)))
74
+ refuse();
75
+ const encoded = entries.get(OWNED_SHOP_ORDER_FIELD);
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(encoded);
79
+ }
80
+ catch {
81
+ refuse();
82
+ }
83
+ const result = order(parsed);
84
+ if (serializeOwnedShopOrder(result) !== encoded)
85
+ refuse();
86
+ return result;
87
+ }
88
+ function parseOwnedShopReceipt(value, expectedOrder) {
89
+ let boundOrder;
90
+ try {
91
+ boundOrder = order(expectedOrder);
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ if (!keys(value, ["object", "order_id", "payment_intent_id", "status", "amount", "currency", "livemode"]) || value.object !== "agentcard_shop_receipt" || value.order_id !== boundOrder.order_id || !isOwnedShopPaymentIntentId(value.payment_intent_id) || value.status !== "succeeded" || value.amount !== OWNED_SHOP_AMOUNT_CENTS || value.currency !== OWNED_SHOP_CURRENCY || value.livemode !== false)
97
+ return null;
98
+ return {
99
+ object: "agentcard_shop_receipt",
100
+ order_id: boundOrder.order_id,
101
+ payment_intent_id: value.payment_intent_id,
102
+ status: "succeeded",
103
+ amount: OWNED_SHOP_AMOUNT_CENTS,
104
+ currency: OWNED_SHOP_CURRENCY,
105
+ livemode: false
106
+ };
107
+ }
108
+ export { OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY, OWNED_SHOP_OPERATION, OWNED_SHOP_ORDER_FIELD, OWNED_SHOP_SKU, hasOwnedShopMarker, isOwnedShopPaymentIntentId, parseOwnedShopOrder, parseOwnedShopReceipt, serializeOwnedShopOrder };
@@ -50,25 +50,8 @@ export interface Recognizer {
50
50
  /** How the card reaches the processor; absent means 'token'. Carried verbatim by syncRegistry. */
51
51
  mode?: CheckoutMode;
52
52
  }
53
- /**
54
- * Every PSP the service recognizes, shipped with the package.
55
- *
56
- * This list is the FALLBACK that decides interception whenever
57
- * `syncRegistry()` is skipped or its fetch fails — and that failure is silent
58
- * by design ("never break checkout over a registry fetch"). A PSP missing here
59
- * is not an error: its tokenization request is simply never paused, the vault
60
- * is never consulted, and the shopper pays with their OWN card.
61
- *
62
- * It shipped with only 5 of the service's 22 recognizers, which meant 17
63
- * processors degraded to a silent wrong-card charge for any integrator whose
64
- * sync had not landed. Generated from the backend registry
65
- * (packages/backend/src/services/checkout-recognizers.ts) so the two cannot
66
- * drift by transcription; `sdk-registry-drift.test.ts` fails CI if they do.
67
- *
68
- * Syncing still matters — it is how a NEW processor arrives without an npm
69
- * release. This list is the floor, not the ceiling.
70
- */
71
- export declare const BUILTIN_REGISTRY: Recognizer[];
53
+ import { BUILTIN_REGISTRY } from './builtin-registry.generated.js';
54
+ export { BUILTIN_REGISTRY };
72
55
  export declare function findRecognizer(url: string, registry?: Recognizer[]): Recognizer | null;
73
56
  /**
74
57
  * Glob url patterns covering every host in `registry` — what to hand