@agent-cards/sdk 0.24.1 → 0.25.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/dist/cdp.js CHANGED
@@ -15,6 +15,7 @@ import { isPaddleVgsCardsUrl } from './paddle-vgs.generated.js';
15
15
  import { isToastPaymentMethodsUrl, readToastBody } from './toast.generated.js';
16
16
  import { PADDLE_VGS_PAY_PATTERNS, PaddleVgsPayGate, isPaddleCheckoutService, recognizesPaddleVgs } from './paddle-vgs-pay.js';
17
17
  import { DatatransRepeats } from './datatrans.js';
18
+ import { TokenRepeats } from './token-repeats.js';
18
19
  import { TokenexIframeRepeats, isArmedTokenexIframeUrl } from './tokenex-iframe.js';
19
20
  import { isArmedPaytrailUrl } from './paytrail.js';
20
21
  import { SHIJI_STS_COMPANION_WINDOW_MS, ShijiStsCompanions, ShijiStsError, isShijiStsHost, shijiStsTokenContinuation, } from './shiji-sts.js';
@@ -43,6 +44,7 @@ import { CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS, CYBERSOURCE_SECURE_ACCEPTANCE_R
43
44
  import { CheckoutAttachmentError, attachmentDeadline, attachmentFailure, withinAttachmentDeadline } from './attachment.js';
44
45
  import { cseBody } from './cse-body.js';
45
46
  import { STRIPE_SETUP_INTENT_CONFIRM, setupConfirmPayingWith } from './stripe-setup.js';
47
+ import { confirmPayingWith } from './stripe-continuation.js';
46
48
  import { replayTemplatedUrl, reviewedMerchantProfileUrlPatterns } from './merchant-hosted.js';
47
49
  import { verifoneTemplatedUrl } from './verifone.js';
48
50
  import { MerchantHandoffWatch } from './merchant-handoff.js';
@@ -1112,30 +1114,41 @@ function claimStripeContinuation(opts, lifecycle, url, method, body) {
1112
1114
  }
1113
1115
  if (parsed.origin !== 'https://api.stripe.com')
1114
1116
  return null;
1115
- if (STRIPE_PAYMENT_INTENT_CONFIRM.test(parsed.pathname)) {
1116
- if (typeof opts.vault.checkStripeContinuation !== 'function' || opts.vault.withoutCard?.(url, body) !== 'refuse')
1117
- return null;
1118
- const authorizationId = lifecycle.claimStripeContinuation();
1119
- return authorizationId ? { authorizationId, kind: 'payment', body, rewritten: false } : null;
1120
- }
1121
- if (!STRIPE_SETUP_INTENT_CONFIRM.test(parsed.pathname) || typeof opts.vault.checkStripeSetupContinuation !== 'function')
1117
+ const payment = STRIPE_PAYMENT_INTENT_CONFIRM.test(parsed.pathname);
1118
+ const setup = !payment && STRIPE_SETUP_INTENT_CONFIRM.test(parsed.pathname);
1119
+ if (!payment && !setup)
1120
+ return null;
1121
+ // Each kind needs the client method that judges it; an older client keeps the ordinary hold.
1122
+ if (payment && typeof opts.vault.checkStripeContinuation !== 'function')
1123
+ return null;
1124
+ if (setup && typeof opts.vault.checkStripeSetupContinuation !== 'function')
1122
1125
  return null;
1123
1126
  const withoutCard = opts.vault.withoutCard?.(url, body);
1124
1127
  let send = null;
1125
1128
  if (withoutCard === 'refuse')
1126
1129
  send = body;
1127
1130
  else if (withoutCard === null) {
1128
- // The card again, inline: only the approved payment method may replace it,
1129
- // and only when what is left carries no card at all.
1130
- const paymentMethod = lifecycle.approvedStripePaymentMethod();
1131
- send = paymentMethod ? setupConfirmPayingWith(body, paymentMethod) : null;
1131
+ // The card again, inline: only the object the cardholder's approval
1132
+ // returned may replace it, and only when what is left carries no card at
1133
+ // all. A payment pays with whichever object its approval produced (a
1134
+ // confirmation token, for a store that creates one for a fraud check and
1135
+ // then confirms with the Payment Element); a card save pays with a payment
1136
+ // method alone, which is all a SetupIntent confirm takes.
1137
+ if (payment) {
1138
+ const way = lifecycle.approvedStripeWay();
1139
+ send = way ? confirmPayingWith(body, way) : null;
1140
+ }
1141
+ else {
1142
+ const paymentMethod = lifecycle.approvedStripePaymentMethod();
1143
+ send = paymentMethod ? setupConfirmPayingWith(body, paymentMethod) : null;
1144
+ }
1132
1145
  if (send !== null && opts.vault.withoutCard?.(url, send) !== 'refuse')
1133
1146
  send = null;
1134
1147
  }
1135
1148
  if (send === null)
1136
1149
  return null;
1137
1150
  const authorizationId = lifecycle.claimStripeContinuation();
1138
- return authorizationId ? { authorizationId, kind: 'setup', body: send, rewritten: send !== body } : null;
1151
+ return authorizationId ? { authorizationId, kind: payment ? 'payment' : 'setup', body: send, rewritten: send !== body } : null;
1139
1152
  }
1140
1153
  /**
1141
1154
  * Ask the API about a claimed continuation, with the body it would continue
@@ -1158,7 +1171,7 @@ async function checkStripeContinuation(opts, lifecycle, claim, request) {
1158
1171
  if (proceed) {
1159
1172
  opts.onEvent?.(kind === 'setup'
1160
1173
  ? { type: 'stripe_setup_continued', detail: { authorizationId, setupIntentId: intentId, rewritten: claim.rewritten } }
1161
- : { type: 'stripe_payment_continued', detail: { authorizationId, paymentIntentId: intentId } });
1174
+ : { type: 'stripe_payment_continued', detail: { authorizationId, paymentIntentId: intentId, rewritten: claim.rewritten } });
1162
1175
  }
1163
1176
  else if (intentId !== null)
1164
1177
  opts.onEvent?.({ type: 'blocked', detail: 'stripe_continuation_superseded' });
@@ -2084,6 +2097,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2084
2097
  // The approved TokenEx iframe answers, per frameId (the card-number frame is an out-of-process iframe whose own
2085
2098
  // session pauses its post; its frameId names it): the repeat rule (tokenex-iframe.ts).
2086
2099
  const tokenexIframeRepeats = new TokenexIframeRepeats();
2100
+ // The last approved plain token answer, for the page's identical re-send (token-repeats.ts).
2101
+ const tokenRepeats = new TokenRepeats();
2087
2102
  // The card product each Worldline MyCheckout hosted checkout chose (worldline-mycheckout.ts).
2088
2103
  const worldlineProducts = new WorldlineMyCheckoutProducts();
2089
2104
  // The Shiji STS getCvvId companion of the getTokenId this attachment last continued (shiji-sts.ts).
@@ -2905,6 +2920,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2905
2920
  juspay: juspayCheckout,
2906
2921
  datatrans: datatransRepeats,
2907
2922
  tokenexIframe: tokenexIframeRepeats,
2923
+ tokenRepeats,
2908
2924
  ebanx: ebanxFollowUps,
2909
2925
  payrexx: payrexxFollowUps,
2910
2926
  nikeOrders,
@@ -3446,6 +3462,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
3446
3462
  // The frame that received it: the paused request's, or its retry's once rebound.
3447
3463
  datatransRepeats.remember(activeRequest?.frameId || undefined, deliverRequest.url, deliverBody, replay);
3448
3464
  tokenexIframeRepeats.remember(activeRequest?.frameId || undefined, deliverRequest.url, deliverBody, replay);
3465
+ tokenRepeats.remember(activeRequest?.frameId || undefined, deliverRequest.url, deliverBody, replay);
3449
3466
  attempt.assertLive();
3450
3467
  // An ACI card-number request answered: its checkout's held CVC and expiry requests get the
3451
3468
  // same answer, and its /payment submission may continue (see AciGate).
@@ -3982,6 +3999,8 @@ export async function attachToPlaywright(page, opts) {
3982
3999
  const datatransRepeats = new DatatransRepeats();
3983
4000
  // The approved TokenEx iframe answers, per Frame: the repeat rule (tokenex-iframe.ts).
3984
4001
  const tokenexIframeRepeats = new TokenexIframeRepeats();
4002
+ // The last approved plain token answer, for the page's identical re-send (token-repeats.ts).
4003
+ const tokenRepeats = new TokenRepeats();
3985
4004
  // The card product each Worldline MyCheckout hosted checkout chose (worldline-mycheckout.ts).
3986
4005
  const worldlineProducts = new WorldlineMyCheckoutProducts();
3987
4006
  const frameOf = (request) => { try {
@@ -4180,6 +4199,7 @@ export async function attachToPlaywright(page, opts) {
4180
4199
  juspay: juspayCheckout,
4181
4200
  datatrans: datatransRepeats,
4182
4201
  tokenexIframe: tokenexIframeRepeats,
4202
+ tokenRepeats,
4183
4203
  ebanx: ebanxFollowUps,
4184
4204
  payrexx: payrexxFollowUps,
4185
4205
  nikeOrders,
@@ -4586,6 +4606,7 @@ export async function attachToPlaywright(page, opts) {
4586
4606
  await deliverTo.fulfill({ status: replay.status, headers: withCorsHeaders(replay.headers, cors.headers), body: replay.body });
4587
4607
  datatransRepeats.remember(frameOf(deliverRequest), deliverRequest.url(), deliverBody, replay);
4588
4608
  tokenexIframeRepeats.remember(frameOf(deliverRequest), deliverRequest.url(), deliverBody, replay);
4609
+ tokenRepeats.remember(frameOf(deliverRequest), deliverRequest.url(), deliverBody, replay);
4589
4610
  assertRequestLive();
4590
4611
  // See the same step in attachToCdp.
4591
4612
  aci.answered(deliverRequest.url(), { status: replay.status, headers: replay.headers, body: replay.body }, replay.authorizationId);
package/dist/client.d.ts CHANGED
@@ -608,6 +608,15 @@ export declare const SUPPORTED_REGISTRY_FEATURES: readonly string[];
608
608
  * judge. Served on the same terms: only to a build that lists it, for a company Agentcard turned Frisbii
609
609
  * on for.
610
610
  *
611
+ * `bigcommerce_card_form`: the payment BigCommerce's own card form (the card fields on
612
+ * payments.bigcommerce.com/pay/hosted_forms/<id>/field, or the store page's own inputs) sends as JSON to
613
+ * https://payments.bigcommerce.com/api/public/v1/orders/payments under the order's payment token, the card
614
+ * in payment.credit_card. This build pauses only a body that carries a new card there (payment-core
615
+ * processors/bigcommerce.js); a card saved at the store, another processor's token and a wallet continue
616
+ * untouched, and a card the store would keep fails in the browser before anything is created. The body
617
+ * names the store's total, which the cardholder approves. Served on the same terms: only to a build that
618
+ * lists it, for a company Agentcard turned BigCommerce on for.
619
+ *
611
620
  * `aps_custom_merchant_page`: Amazon Payment Services' custom merchant page, whose own card form
612
621
  * posts top-level to /FortAPI/paymentPage on checkout.payfort.com (sbcheckout.payfort.com on APS's
613
622
  * sandbox) with service_command=TOKENIZATION. This build pauses that navigation; the cardholder's
package/dist/client.js CHANGED
@@ -602,6 +602,15 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
602
602
  * judge. Served on the same terms: only to a build that lists it, for a company Agentcard turned Frisbii
603
603
  * on for.
604
604
  *
605
+ * `bigcommerce_card_form`: the payment BigCommerce's own card form (the card fields on
606
+ * payments.bigcommerce.com/pay/hosted_forms/<id>/field, or the store page's own inputs) sends as JSON to
607
+ * https://payments.bigcommerce.com/api/public/v1/orders/payments under the order's payment token, the card
608
+ * in payment.credit_card. This build pauses only a body that carries a new card there (payment-core
609
+ * processors/bigcommerce.js); a card saved at the store, another processor's token and a wallet continue
610
+ * untouched, and a card the store would keep fails in the browser before anything is created. The body
611
+ * names the store's total, which the cardholder approves. Served on the same terms: only to a build that
612
+ * lists it, for a company Agentcard turned BigCommerce on for.
613
+ *
605
614
  * `aps_custom_merchant_page`: Amazon Payment Services' custom merchant page, whose own card form
606
615
  * posts top-level to /FortAPI/paymentPage on checkout.payfort.com (sbcheckout.payfort.com on APS's
607
616
  * sandbox) with service_command=TOKENIZATION. This build pauses that navigation; the cardholder's
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 169752c8d6a6c9f131c83502766f0026b2ffa45cf9f27c450735eef5ad2fa931
3
+ // artifact-sha256: c4280e1c95d016a93163f49d9d66178a9c4fdfae08e4570952c9c7f533a1c643
4
4
  // src/answer-card-leak.js
5
5
  var DECIMAL = /\p{Nd}/u;
6
6
  function digitValue(char) {
@@ -20,6 +20,8 @@ function enclosedValue(char) {
20
20
  var asciiDigits = (text2) => (typeof text2.normalize === "function" ? text2.normalize("NFKC") : text2).replace(ENCLOSED_CHAR, enclosedValue).replace(/\p{Nd}/gu, (char) => char >= "0" && char <= "9" ? char : digitValue(char));
21
21
  var digitCount = (text2) => (asciiDigits(String(text2)).match(/\p{N}/gu) ?? []).length;
22
22
  var NAME_DIGITS = 3;
23
+ var UTF8_SEQUENCE = "[\\x00-\\x7f]|[\\xc2-\\xdf][\\x80-\\xbf]|\\xe0[\\xa0-\\xbf][\\x80-\\xbf]|[\\xe1-\\xec\\xee\\xef][\\x80-\\xbf]{2}|\\xed[\\x80-\\x9f][\\x80-\\xbf]|\\xf0[\\x90-\\xbf][\\x80-\\xbf]{2}|[\\xf1-\\xf3][\\x80-\\xbf]{3}|\\xf4[\\x80-\\x8f][\\x80-\\xbf]{2}";
24
+ var UTF8_WHOLE = /* @__PURE__ */ new RegExp(`^(?:${UTF8_SEQUENCE})*$`);
23
25
  // src/datatrans.js
24
26
  var DATATRANS_HOSTS = Object.freeze(["pay.datatrans.com", "pay.sandbox.datatrans.com"]);
25
27
  var DATATRANS_TOKENIZE_PATH = "/upp/payment/SecureFields/paymentField";
package/dist/index.d.ts CHANGED
@@ -46,7 +46,7 @@ export type { ApiTransport } from './api-module.js';
46
46
  export { BuyModule, BuyError, isBuyDeclined, BUY_TURN_TIMEOUT_MS } from './buy.js';
47
47
  export type { BuyStatus, BuyCart, BuyCartLine, BuyDeliveryAddress, BuyPaymentSource, BuyOutcome, BuyPlacement, BuyCatalog, BuyUnmatched, BuyTurn, BuyDeclined, BuyOrder, BuyLastCheckout, BuyConversation, BuyAskInput, BuyConfirmInput, BuyConversationInput, BuyCheckoutHandoff, BuyOpenOffer, BuyBrowserCheckout, BuyApiAmount, } from './buy.js';
48
48
  export { ApprovalsModule, CANCEL_REASONS, SANDBOX_REQUEST } from './approvals.js';
49
- export type { CreateAuthorizationInput, Authorization, AuthorizationResult, AuthorizationFields, Cancelled, PayingCard, ChoosableCard } from './approvals.js';
49
+ export type { CreateAuthorizationInput, OrderLineItem, OrderShipTo, Authorization, AuthorizationResult, AuthorizationFields, Cancelled, PayingCard, ChoosableCard } from './approvals.js';
50
50
  export { verifyWebhook, signWebhook, parseWebhookEvent, parseSignatureHeader, isCheckoutAuthorizationEvent, isVaultEvent, CHECKOUT_AUTHORIZATION_EVENT_TYPES, VAULT_EVENT_TYPES } from './webhooks.js';
51
51
  export type { WebhookEvent, AgentcardEvent, CheckoutAuthorizationEvent, CheckoutAuthorizationEventType, CheckoutAuthorizationEventData, VaultEvent, VaultEventType, VaultEventData, OtherEvent, VerifyWebhookOptions } from './webhooks.js';
52
52
  export { AgentcardError } from './api-types.js';
@@ -6,6 +6,7 @@ import type { ShijiStsUnsupportedReason } from './shiji-sts.js';
6
6
  import type { PaddleVgsPayRefusal } from './paddle-vgs-pay.js';
7
7
  import type { PaypalCardFormReason } from './paypal-card-forms.js';
8
8
  import { type CardRequestHold, type HoldReleaseInput, type HoldReleaseOutcome } from './hold.js';
9
+ import { type StripeApprovedWay } from './stripe-continuation.js';
9
10
  /** The application's resolver confirmed the payment for this checkout authorization. */
10
11
  export interface MerchantPaymentConfirmation {
11
12
  kind: 'merchant_payment';
@@ -104,11 +105,12 @@ export declare class CheckoutLifecycle implements CheckoutController {
104
105
  private wspayChallenge;
105
106
  private heldBeforeHandoff;
106
107
  private stripeContinuedReason;
107
- private approvedPaymentMethod;
108
+ private approvedWay;
108
109
  private ownedShopOrderId;
109
110
  private orderRefusal;
110
111
  private readonly handoffs;
111
112
  private reconciliation;
113
+ private pendingHold;
112
114
  /**
113
115
  * EBANX sends this attachment claimed that are still out (pre-claim.ts ebanxFollowUp): the cardholder's device may be
114
116
  * sending a confirm that pays, outside `active`, so reconcile() and retryAfterMerchantFailure() wait for them too.
@@ -150,6 +152,15 @@ export declare class CheckoutLifecycle implements CheckoutController {
150
152
  isCancelled(): boolean;
151
153
  /** Whether the application gave a merchant resolver, so reconcile() reads the merchant's own record. */
152
154
  hasMerchantResolver(): boolean;
155
+ /**
156
+ * Whether the page's byte-identical re-send of the card request this approval answered may get the same answer
157
+ * (token-repeats.ts, pre-claim.ts tokenRepeatPreClaim): the checkout awaits the merchant on a plain token handoff
158
+ * that held nothing, and the only hold since is the merchant resolver reading the order as pending. A hold from
159
+ * anything else stands: an amount the processor charged that differs from the approval's, an unbound Stripe token,
160
+ * a hosted form or card endpoint, an integrator's requireMerchantResult, a follow-up send still out, an unsupported
161
+ * request, a cancel or an aborted request. The gate still asks the resolver before it answers.
162
+ */
163
+ repeatAnswerable(): boolean;
153
164
  /**
154
165
  * For a Datatrans re-submit once the approved answer reached the page (pre-claim.ts): whether the
155
166
  * application's merchant resolver still reports this approval's order pending. A read only: no
@@ -258,12 +269,19 @@ export declare class CheckoutLifecycle implements CheckoutController {
258
269
  * VaultClient.withoutCard, which then refuses a Nuvei payment without a card.
259
270
  */
260
271
  cardHandoffs(): readonly string[];
272
+ /**
273
+ * The object a Stripe confirmation may pay with in place of the card it sends
274
+ * again (cdp.ts claimStripeContinuation): the one this attachment's approved
275
+ * tokenization returned, with the field that names it, while its continuation
276
+ * is still open. Null for a tokenization no confirmation names by id
277
+ * (`/v1/tokens`), and once the checkout was cancelled, aborted or reconciled.
278
+ */
279
+ approvedStripeWay(): StripeApprovedWay | null;
261
280
  /**
262
281
  * The payment method a Stripe card save may pay with in place of the card
263
- * it sends again (cdp.ts claimStripeContinuation): the one this
264
- * attachment's approved `/v1/payment_methods` request returned, while its
265
- * continuation is still open. Null for any other tokenization, and once the
266
- * checkout was cancelled, aborted or reconciled.
282
+ * it sends again: the `pm_` an approved `/v1/payment_methods` request
283
+ * returned. Null for every other approved object, since a SetupIntent
284
+ * confirm pays with a payment method alone.
267
285
  */
268
286
  approvedStripePaymentMethod(): string | null;
269
287
  /**
package/dist/lifecycle.js CHANGED
@@ -3,6 +3,7 @@ import { CardEndpointRefusedError } from './card-endpoint.js';
3
3
  import { parseOwnedShopReceipt, OWNED_SHOP_SKU, OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY } from './owned-shop.generated.js';
4
4
  import { isWspayChallenge } from './wspay.js';
5
5
  import { HoldAnsweredError, HoldEndedError } from './hold.js';
6
+ import { stripeApprovedWay } from './stripe-continuation.js';
6
7
  /**
7
8
  * The statuses that end an attempt: its outcome is known (`completed`, `declined`, `failed`,
8
9
  * `timed_out`, `unsupported`, `cancelled`), or the SDK already reported it unknown with its own
@@ -25,19 +26,6 @@ export const MAX_STRIPE_CONTINUATION_CHECKS = 3;
25
26
  * the post becomes a new approval instead of hanging the checkout.
26
27
  */
27
28
  export const MERCHANT_ORDER_READ_MS = 5_000;
28
- const STRIPE_PAYMENT_METHOD_ID = /^pm_[A-Za-z0-9]{1,250}$/;
29
- /** The `pm_` id in a Stripe `/v1/payment_methods` answer, or null for any other body. */
30
- function stripePaymentMethodId(body) {
31
- if (typeof body !== 'string')
32
- return null;
33
- try {
34
- const id = JSON.parse(body)?.id;
35
- return typeof id === 'string' && STRIPE_PAYMENT_METHOD_ID.test(id) ? id : null;
36
- }
37
- catch {
38
- return null;
39
- }
40
- }
41
29
  /** Shared by the raw CDP and Playwright transports. No browser ownership or payment execution lives here. */
42
30
  export class CheckoutLifecycle {
43
31
  options;
@@ -62,14 +50,17 @@ export class CheckoutLifecycle {
62
50
  heldBeforeHandoff = false;
63
51
  // What the continuation that went through confirmed: a payment, or a card save.
64
52
  stripeContinuedReason = 'stripe_payment_continued';
65
- // The payment method a Stripe /v1/payment_methods approval returned: see approvedStripePaymentMethod.
66
- approvedPaymentMethod = null;
53
+ // The object a Stripe tokenization approval returned, and the field a confirmation
54
+ // names it by: see approvedStripeWay.
55
+ approvedWay = null;
67
56
  ownedShopOrderId = null;
68
57
  // The reason Nike's order was refused while this attempt was under way (merchantOrderRefused): its handoff keeps it failed.
69
58
  orderRefusal = null;
70
59
  // Every request this attachment handed the card to: see cardHandoffs().
71
60
  handoffs = [];
72
61
  reconciliation = null;
62
+ // The hold is only the merchant resolver's `pending` on a handoff nothing else held (resolve): see repeatAnswerable.
63
+ pendingHold = false;
73
64
  /**
74
65
  * EBANX sends this attachment claimed that are still out (pre-claim.ts ebanxFollowUp): the cardholder's device may be
75
66
  * sending a confirm that pays, outside `active`, so reconcile() and retryAfterMerchantFailure() wait for them too.
@@ -140,6 +131,18 @@ export class CheckoutLifecycle {
140
131
  isCancelled() { return this.cancelled; }
141
132
  /** Whether the application gave a merchant resolver, so reconcile() reads the merchant's own record. */
142
133
  hasMerchantResolver() { return typeof this.options.resolveMerchantResult === 'function'; }
134
+ /**
135
+ * Whether the page's byte-identical re-send of the card request this approval answered may get the same answer
136
+ * (token-repeats.ts, pre-claim.ts tokenRepeatPreClaim): the checkout awaits the merchant on a plain token handoff
137
+ * that held nothing, and the only hold since is the merchant resolver reading the order as pending. A hold from
138
+ * anything else stands: an amount the processor charged that differs from the approval's, an unbound Stripe token,
139
+ * a hosted form or card endpoint, an integrator's requireMerchantResult, a follow-up send still out, an unsupported
140
+ * request, a cancel or an aborted request. The gate still asks the resolver before it answers.
141
+ */
142
+ repeatAnswerable() {
143
+ return this.state.status === 'awaiting_merchant' && !!this.state.authorizationId && !this.active && !this.cancelled
144
+ && !this.merchantAborted && this.followUpsOut === 0 && (!this.held || this.pendingHold);
145
+ }
143
146
  /**
144
147
  * For a Datatrans re-submit once the approved answer reached the page (pre-claim.ts): whether the
145
148
  * application's merchant resolver still reports this approval's order pending. A read only: no
@@ -169,6 +172,10 @@ export class CheckoutLifecycle {
169
172
  }
170
173
  end() { this.active = false; }
171
174
  set(state) {
175
+ // Any state but awaiting the merchant (a challenge, a failure, a completion) ends the pending read's exception for
176
+ // good: a later pending read that brings awaiting_merchant back holds every card request again (repeatAnswerable).
177
+ if (state.status !== 'awaiting_merchant')
178
+ this.pendingHold = false;
172
179
  this.state = state;
173
180
  // Application telemetry must never interrupt a paused payment after it reaches the processor.
174
181
  try {
@@ -287,6 +294,7 @@ export class CheckoutLifecycle {
287
294
  */
288
295
  unsupported(reason = 'unrecognized_payment_endpoint') {
289
296
  this.held = true;
297
+ this.pendingHold = false;
290
298
  if (this.state.authorizationId)
291
299
  return; // do not hide an outstanding/completed payment behind a later unsupported request
292
300
  this.set({ ...this.state, status: 'unsupported', reason });
@@ -348,8 +356,7 @@ export class CheckoutLifecycle {
348
356
  this.unboundStripeToken = true;
349
357
  this.stripeTokenPrepared = true;
350
358
  this.held = true;
351
- if (url.pathname === '/v1/payment_methods')
352
- this.approvedPaymentMethod = stripePaymentMethodId(replay.body);
359
+ this.approvedWay = stripeApprovedWay(url.pathname, replay.body);
353
360
  }
354
361
  }
355
362
  catch { /* unknown URL cannot establish a token binding */ }
@@ -363,15 +370,25 @@ export class CheckoutLifecycle {
363
370
  * VaultClient.withoutCard, which then refuses a Nuvei payment without a card.
364
371
  */
365
372
  cardHandoffs() { return [...this.handoffs]; }
373
+ /**
374
+ * The object a Stripe confirmation may pay with in place of the card it sends
375
+ * again (cdp.ts claimStripeContinuation): the one this attachment's approved
376
+ * tokenization returned, with the field that names it, while its continuation
377
+ * is still open. Null for a tokenization no confirmation names by id
378
+ * (`/v1/tokens`), and once the checkout was cancelled, aborted or reconciled.
379
+ */
380
+ approvedStripeWay() {
381
+ return this.stripeContinuationOpen() ? this.approvedWay : null;
382
+ }
366
383
  /**
367
384
  * The payment method a Stripe card save may pay with in place of the card
368
- * it sends again (cdp.ts claimStripeContinuation): the one this
369
- * attachment's approved `/v1/payment_methods` request returned, while its
370
- * continuation is still open. Null for any other tokenization, and once the
371
- * checkout was cancelled, aborted or reconciled.
385
+ * it sends again: the `pm_` an approved `/v1/payment_methods` request
386
+ * returned. Null for every other approved object, since a SetupIntent
387
+ * confirm pays with a payment method alone.
372
388
  */
373
389
  approvedStripePaymentMethod() {
374
- return this.stripeContinuationOpen() ? this.approvedPaymentMethod : null;
390
+ const way = this.approvedStripeWay();
391
+ return way?.field === 'payment_method' ? way.id : null;
375
392
  }
376
393
  /**
377
394
  * The approval a page's card-free Stripe confirmation may continue, or null.
@@ -416,6 +433,7 @@ export class CheckoutLifecycle {
416
433
  if (this.merchantAborted)
417
434
  return;
418
435
  this.settled = true;
436
+ this.pendingHold = false;
419
437
  if (this.orderRefusal) {
420
438
  // Nike's order was refused while this approval was on its way: the answer lands, and the checkout stays failed.
421
439
  this.held = true;
@@ -591,6 +609,10 @@ export class CheckoutLifecycle {
591
609
  await this.requestUserAction(result.reason);
592
610
  }
593
611
  else {
612
+ // A pending order on a handoff nothing held still lets the page's identical re-send of the same card request have
613
+ // its answer again (repeatAnswerable); every other card request stays refused.
614
+ if (!this.held && result.status === 'pending' && this.state.status === 'awaiting_merchant')
615
+ this.pendingHold = true;
594
616
  this.held = true;
595
617
  this.set({ ...this.state, status: result.status === 'pending' ? 'awaiting_merchant' : 'outcome_unknown', reason: 'merchant_not_confirmed' });
596
618
  }
@@ -612,6 +634,7 @@ export class CheckoutLifecycle {
612
634
  if (this.unboundStripeToken)
613
635
  throw new Error('This attachment delivered an unbound Stripe token; reconcile the merchant order and use a separately validated checkout flow.');
614
636
  this.held = false;
637
+ this.pendingHold = false;
615
638
  this.merchantAborted = false;
616
639
  this.orderRefusal = null;
617
640
  this.set({ status: 'idle', authorizationId: null });
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 4acbca9604c5eee50f5f73dc3dea5ab0d40c9cba1214ccda0d2074271b3554a5
3
+ // artifact-sha256: e2228c7919341a7b3615b285c3aa77948c247c13145a1e394a1bb1c64acbca06
4
4
  // src/json-path.js
5
5
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
6
6
  function isPlainObject(v) {
@@ -318,17 +318,39 @@ function enclosedValue(char) {
318
318
  }
319
319
  var asciiDigits = (text) => (typeof text.normalize === "function" ? text.normalize("NFKC") : text).replace(ENCLOSED_CHAR, enclosedValue).replace(/\p{Nd}/gu, (char) => char >= "0" && char <= "9" ? char : digitValue(char));
320
320
  var unescaped = (text) => text.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
321
+ var UTF8_SEQUENCE = "[\\x00-\\x7f]|[\\xc2-\\xdf][\\x80-\\xbf]|\\xe0[\\xa0-\\xbf][\\x80-\\xbf]|[\\xe1-\\xec\\xee\\xef][\\x80-\\xbf]{2}|\\xed[\\x80-\\x9f][\\x80-\\xbf]|\\xf0[\\x90-\\xbf][\\x80-\\xbf]{2}|[\\xf1-\\xf3][\\x80-\\xbf]{3}|\\xf4[\\x80-\\x8f][\\x80-\\xbf]{2}";
322
+ var UTF8_WHOLE = /* @__PURE__ */ new RegExp(`^(?:${UTF8_SEQUENCE})*$`);
323
+ var UTF8_ONE = /* @__PURE__ */ new RegExp(UTF8_SEQUENCE, "g");
324
+ function utf8Of(bytes) {
325
+ if (!/[\x80-\xff]/.test(bytes) || !UTF8_WHOLE.test(bytes))
326
+ return null;
327
+ return bytes.replace(UTF8_ONE, (sequence) => {
328
+ if (sequence.length === 1)
329
+ return sequence;
330
+ const lead = sequence.charCodeAt(0);
331
+ let point = lead & (sequence.length === 2 ? 31 : sequence.length === 3 ? 15 : 7);
332
+ for (let i = 1; i < sequence.length; i += 1)
333
+ point = point << 6 | sequence.charCodeAt(i) & 63;
334
+ return String.fromCodePoint(point);
335
+ });
336
+ }
337
+ var utf8Runs = (text) => text.replace(/[\x80-\xff]+/g, (run) => utf8Of(run) ?? run);
321
338
  var decodedInPlace = (text) => text.replace(BASE64_PIECE, (piece) => {
322
339
  const bare = piece.replace(/=+$/, "");
323
340
  for (let skip = 0; skip < 4 && skip < bare.length - 1; skip += 1) {
324
341
  const bytes = base64Text(bare.slice(skip));
325
342
  if (/^[\x20-\x7e]+$/.test(bytes))
326
343
  return bare.slice(0, skip) + bytes;
344
+ const utf82 = utf8Of(bytes);
345
+ if (utf82 !== null && !/\p{C}/u.test(utf82))
346
+ return bare.slice(0, skip) + utf82;
327
347
  }
328
348
  return piece;
329
349
  });
330
350
  function collect(text, layers, streams, runs) {
331
351
  const base = /* @__PURE__ */ new Set([...readings(text), ...readings(unescaped(text))]);
352
+ for (const reading of [...base])
353
+ base.add(utf8Runs(reading));
332
354
  for (const reading of /* @__PURE__ */ new Set([...base, ...[...base].map(decodedInPlace)])) {
333
355
  const digits = asciiDigits(reading);
334
356
  const sequence = digits.match(/[0-9]+/g) ?? [];
@@ -341,8 +363,13 @@ function collect(text, layers, streams, runs) {
341
363
  if (layers === 0)
342
364
  continue;
343
365
  for (const run of new Set(reading.match(BASE64_RUN))) {
344
- for (let skip = 0; skip < 4; skip += 1)
345
- collect(base64Text(run.slice(skip)), layers - 1, streams, runs);
366
+ for (let skip = 0; skip < 4; skip += 1) {
367
+ const bytes = base64Text(run.slice(skip));
368
+ collect(bytes, layers - 1, streams, runs);
369
+ const utf82 = utf8Of(bytes);
370
+ if (utf82 !== null)
371
+ collect(utf82, layers - 1, streams, runs);
372
+ }
346
373
  }
347
374
  }
348
375
  }
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: d6d50b6577a932c3586fcade6b8cbd03c562bd3394e8addcb954d797ea9999b4
3
+ // artifact-sha256: d5e732dd0bce3debee02e3f31107e65059e0d3888b54a605357a5817bb8909ce
4
4
  // src/json-path.js
5
5
  function roundTrips(parsed, body) {
6
6
  try {
@@ -29,6 +29,8 @@ function enclosedValue(char) {
29
29
  var asciiDigits = (text) => (typeof text.normalize === "function" ? text.normalize("NFKC") : text).replace(ENCLOSED_CHAR, enclosedValue).replace(/\p{Nd}/gu, (char) => char >= "0" && char <= "9" ? char : digitValue(char));
30
30
  var digitCount = (text) => (asciiDigits(String(text)).match(/\p{N}/gu) ?? []).length;
31
31
  var NAME_DIGITS = 3;
32
+ var UTF8_SEQUENCE = "[\\x00-\\x7f]|[\\xc2-\\xdf][\\x80-\\xbf]|\\xe0[\\xa0-\\xbf][\\x80-\\xbf]|[\\xe1-\\xec\\xee\\xef][\\x80-\\xbf]{2}|\\xed[\\x80-\\x9f][\\x80-\\xbf]|\\xf0[\\x90-\\xbf][\\x80-\\xbf]{2}|[\\xf1-\\xf3][\\x80-\\xbf]{3}|\\xf4[\\x80-\\x8f][\\x80-\\xbf]{2}";
33
+ var UTF8_WHOLE = /* @__PURE__ */ new RegExp(`^(?:${UTF8_SEQUENCE})*$`);
32
34
  // src/sha256.js
33
35
  var K = /* @__PURE__ */ new Uint32Array([
34
36
  1116352408,
@@ -15,6 +15,7 @@ import { type CardEndpointProfile } from './card-endpoint.js';
15
15
  import { type NikeOrders } from './nike-order.js';
16
16
  import { type PayrailsSessionCapture, type PayrailsSidecar } from './payrails.js';
17
17
  import { type VerifoneProfile } from './verifone.js';
18
+ import type { TokenRepeats } from './token-repeats.js';
18
19
  import { type WorldlineMyCheckoutProducts } from './worldline-mycheckout.js';
19
20
  import { type PaddleVgsPayGate } from './paddle-vgs-pay.js';
20
21
  import { type EbanxFollowUps } from './ebanx.js';
@@ -210,6 +211,8 @@ export interface PreClaimContext {
210
211
  datatrans?: DatatransRepeats;
211
212
  /** The attachment's approved TokenEx iframe answers (the repeat rule, tokenex-iframe.ts). Absent: none are kept. */
212
213
  tokenexIframe?: TokenexIframeRepeats;
214
+ /** The attachment's last approved plain token answer, for the page's identical re-send (token-repeats.ts). Absent: none is kept. */
215
+ tokenRepeats?: TokenRepeats;
213
216
  /** The adapter's identity for the request's frame (CDP frameId, Playwright Frame); undefined when unknown. */
214
217
  frame?: unknown;
215
218
  /**
package/dist/pre-claim.js CHANGED
@@ -1002,6 +1002,50 @@ const tokenexIframePreClaim = (ctx) => {
1002
1002
  return ctx.lifecycle.getState().status === 'awaiting_merchant'
1003
1003
  ? repeatAfterHandoff(ctx, repeat, TOKENEX_IFRAME_PSP) : answerRepeat(ctx, repeat, TOKENEX_IFRAME_PSP);
1004
1004
  };
1005
+ // Any processor's plain token request, sent again by the page byte for byte after its approved answer reached it
1006
+ // (token-repeats.ts). Last among the gates, so a processor with its own repeat rule (Datatrans, the TokenEx iframe) or
1007
+ // its own hold (Stripe, a card endpoint) decides first. While the lifecycle reads the re-send as answerable
1008
+ // (repeatAnswerable: the same approval awaits the merchant, and nothing but a pending read holds it), the gate asks the
1009
+ // merchant resolver and answers with the stored token only while the order still reads pending, as repeatAfterHandoff
1010
+ // does. Every other card request, another amount or approval among them, defers to the terminal arbiter and the
1011
+ // adapters' hold, as before.
1012
+ const tokenRepeatPreClaim = (ctx) => {
1013
+ const { url, method } = ctx.request;
1014
+ if (!ctx.tokenRepeats || method.toUpperCase() !== 'POST' || !ctx.lifecycle.repeatAnswerable?.() || !ctx.lifecycle.hasMerchantResolver?.())
1015
+ return undefined;
1016
+ // A processor with its own repeat rule keeps its own limits: a re-send its rule declined is a new question.
1017
+ if (isDatatransHost(url) || isTokenexIframePath(url))
1018
+ return undefined;
1019
+ if (ctx.terminal() || ctx.interceptionAborted() || ctx.lifecycleAborted())
1020
+ return undefined;
1021
+ // Read only here, past every cheaper check: a body the adapter cannot read is no re-send of an answered one.
1022
+ let body;
1023
+ try {
1024
+ body = ctx.request.body;
1025
+ }
1026
+ catch {
1027
+ return undefined;
1028
+ }
1029
+ const repeats = ctx.tokenRepeats;
1030
+ const repeat = repeats.answerFor(ctx.frame, url, body, ctx.lifecycle.getState().authorizationId);
1031
+ if (!repeat)
1032
+ return undefined;
1033
+ // A re-send the page never got an answer for gives its place in the count back.
1034
+ return tokenRepeatWhilePending(ctx, repeat).then((decision) => {
1035
+ if (decision?.kind !== 'answer')
1036
+ repeats.released(repeat.authorizationId);
1037
+ return decision;
1038
+ }, (error) => { repeats.released(repeat.authorizationId); throw error; });
1039
+ };
1040
+ async function tokenRepeatWhilePending(ctx, repeat) {
1041
+ if (!await ctx.lifecycle.merchantOrderPending())
1042
+ return undefined;
1043
+ // The resolver answered asynchronously: the same approval must still be the one awaiting the merchant.
1044
+ if (!ctx.lifecycle.repeatAnswerable() || ctx.lifecycle.getState().authorizationId !== repeat.authorizationId
1045
+ || ctx.terminal() || ctx.interceptionAborted() || ctx.lifecycleAborted())
1046
+ return undefined;
1047
+ return answerRepeat(ctx, repeat, 'token');
1048
+ }
1005
1049
  async function tokenexIframeBlock(ctx, reason) {
1006
1050
  ctx.onEvent?.({ type: 'blocked', detail: reason });
1007
1051
  await ctx.effects.abort();
@@ -2623,7 +2667,7 @@ async function unsupportedContinue(ctx) {
2623
2667
  * pauses each one itself. */
2624
2668
  export const PSP_PRE_CLAIM_GATES = [cardEndpointResendPreClaim, cardEndpointPreClaim, nikeOrderPreClaim, mercadoPreClaim, juspayPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, verifonePreClaim, fiservPreClaim, gmoPreClaim, cybersourceFlexPreClaim, datatransPreClaim, tokenexIframePreClaim, pelecardPreClaim, aciPreClaim, creditguardPreClaim, primerPreClaim, payrailsPreClaim, convergePreClaim, eprotectPreClaim, sbpsPreClaim, wspayPreClaim, shijiStsPreClaim, ekashuPreClaim, redsysPreClaim, quickpayPreClaim, comgatePreClaim, przelewy24PreClaim, transbankPreClaim, eximbayPreClaim, barionPreClaim, payplugPreClaim, cardlinkVposPreClaim, eghlPreClaim, paypalHostedFieldsPreClaim,
2625
2669
  paypalCardFormsPreClaim, cybersourceSecureAcceptancePreClaim, worldlineMyCheckoutPreClaim, paddleVgsPayPreClaim, ebanxPreClaim, payrexxPreClaim, hipayPreClaim, paytrailPreClaim,
2626
- oceanpaymentPreClaim, aurusPreClaim];
2670
+ oceanpaymentPreClaim, aurusPreClaim, tokenRepeatPreClaim];
2627
2671
  /** Run each gate in order; the first to return a decision wins. If none does,
2628
2672
  * the terminal arbiter decides between continue, abort (unsupported) and pause.
2629
2673
  * Returns synchronously while every gate defers synchronously (an ordinary card
@@ -3522,6 +3522,31 @@
3522
3522
  "hostname_pattern": "^payment\\.pay1\\.checkout\\.worldline-solutions\\.com$",
3523
3523
  "pathname_pattern": "^\\/checkout$"
3524
3524
  },
3525
+ {
3526
+ "id": "bigcommerce-hosted-card-field",
3527
+ "psp": "bigcommerce",
3528
+ "kinds": [
3529
+ "document",
3530
+ "frame",
3531
+ "iframe"
3532
+ ],
3533
+ "origin": "https://payments.bigcommerce.com",
3534
+ "path_pattern": "/pay/hosted_forms/:form/field",
3535
+ "sources": [
3536
+ {
3537
+ "kind": "saved_observation",
3538
+ "reference": "drsturm-2026-11-06:bigcommerce-hosted-form-field-1.997.1",
3539
+ "reviewed_on": "2026-11-06"
3540
+ },
3541
+ {
3542
+ "kind": "primary_source",
3543
+ "reference": "https://checkout-sdk.bigcommerce.com/v1/loader-v1.997.1.js",
3544
+ "reviewed_on": "2026-11-06"
3545
+ }
3546
+ ],
3547
+ "hostname_pattern": "^payments\\.bigcommerce\\.com$",
3548
+ "pathname_pattern": "^\\/pay\\/hosted_forms\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\/field$"
3549
+ },
3525
3550
  {
3526
3551
  "id": "gmo-mp-token-script-test",
3527
3552
  "psp": "gmo_pg",
@@ -525,6 +525,7 @@
525
525
  "amadeus-data-collection-page",
526
526
  "worldline-mycheckout-page",
527
527
  "worldline-mycheckout-card-form",
528
+ "bigcommerce-hosted-card-field",
528
529
  "gmo-mp-token-script-test",
529
530
  "gmo-token-v1-script-test",
530
531
  "gmo-elements-card-frame-test",
@@ -1170,6 +1171,7 @@
1170
1171
  "amadeus-data-collection-page",
1171
1172
  "worldline-mycheckout-page",
1172
1173
  "worldline-mycheckout-card-form",
1174
+ "bigcommerce-hosted-card-field",
1173
1175
  "gmo-mp-token-script-test",
1174
1176
  "gmo-token-v1-script-test",
1175
1177
  "gmo-elements-card-frame-test",