@agent-cards/sdk 0.23.1 → 0.23.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,10 +2,26 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Stop PayPal's legacy Hosted Fields from sending the card your agent typed to PayPal's payment confirm. A checkout that loads PayPal's JS SDK with `components=hosted-fields`, such as Planet Golf UK's PayPal Checkout, takes the card in frames on `assets.braintreegateway.com` and sends it to `https://cors.api.paypal.com/v2/checkout/orders/{id}/confirm-payment-source` (`cors.api.sandbox.paypal.com` in PayPal's sandbox). No Agentcard payment takes that request over, and earlier releases let it through, so PayPal received the dummy card and declined it. Both adapters now fail that request in the browser for every company, whatever `syncRegistry()` served, and the controller reads `unsupported` with reason `unsupported_paypal_hosted_fields` and holds, like every `unsupported` reason: to pay another way on the same page, attach again. A confirm with no card number, such as a PayPal wallet payment, continues untouched. A confirm whose body the SDK cannot read is failed with a `blocked` event, `unreadable_paypal_request`, and the checkout is not held. A merchant that turns on PayPal's card installments also has the card number field send each typed number to PayPal's installment lookup, `/v1/credit/calculated-financing-options`; the SDK leaves that lookup alone, and it pays nothing. TypeScript: the controller's `unsupported()` reasons gain `unsupported_paypal_hosted_fields`; code that switches exhaustively over them needs the new case.
6
+
7
+ - Name PayPal's legacy Hosted Fields as PayPal in a preflight inspection, not as Braintree. Their card fields are braintree-web builds whose version carries `-payments-sdk` (`3.32.0-payments-sdk-dev` on Planet Golf UK), and inspection read the page as `braintree` with status `supported`. The frames now name `paypal`, and the result stays `unknown` with `processor_variant_unverified`. A braintree-web script from a `-payments-sdk` build is no longer Braintree evidence either, and a variant seen in several frames lists its limitation once. The catalog's `catalog_sha256` changes; `catalog_version` stays `2026-09-15.1`.
8
+
9
+ - Stop the card request AT&T's checkout sends today. AT&T's page posts the card to `/msapi/checkoutorch/v1/mfe/paymentInfo` with no query, and the adapters armed only the exporch form, `/msapi/checkoutexporch/v1/paymentInfo?cartId=`, which none of the checkouts Agentcard read live used, so the agent's dummy card reached AT&T. The adapters now pause the request AT&T sends and refuse it as `unsupported_checkout` while AT&T's profile is in `observe`. The exporch form is a profile of its own, `att_exporch_payment_info`, refused the same way, so a cart AT&T moves there is stopped too. AT&T's card-on-file step, which posts the typed card to `cardOnFile` on either base, is aborted in every status as `card_endpoint_refused` with reason `card_resend`, and so is the exporch form with no query, with its card-on-file step: AT&T's page sends both when its checkout names no cart id, and no payment can cover them. AT&T's profile reads the body its page builds: no `X-CSRF-TOKEN` on a guest checkout, `type` `creditcard` or `debitcard`, `statusCode` and `transactionId` as null, `cardSubType` one of `VISA`, `MASTERCARD`, `AMEX` or `DISCOVER`, a two-digit month and a four-digit year.
10
+
11
+ - Recognize Target Optical's card request as its checkout sends it today: a JSON `PUT` to `/wcs/resources/store/12001/cart/@self/addPayment` with the page's `nocache` query, the total in `piAmount` with five decimals (`"49.42000"`). The profile described the form post Target Optical's older checkout sent, which no request matches now, so the adapters let the dummy card reach Target Optical. The profile stays in observe: both adapters now pause that request and refuse it as `unsupported_checkout` before any authorization, let the saved-card body to the same URL through untouched, and abort the card save a signed-in shopper's page sends to `/wcs/resources/store/12001/mypayments` in every status. They also abort, in every status, a post to `/AjaxOrderChangeServicePIAddTO`, the form endpoint the older checkout used, which they refused before and which the storefront still declares in a script it serves, so moving the profile to the `PUT` leaves no path where the dummy card reaches Target Optical. `attachToPlaywright` routes a reviewed card endpoint's request by every method the profiles name; it asked only for `POST`, so a `PUT` card request or resend never reached its gates. A card endpoint create names the profile's own method in `request.method`; an API that reads every card endpoint request as a `POST` refuses a `PUT` create with `invalid_request`, so nothing is charged. TypeScript: `CardEndpointProfile.method` can be `PUT`.
12
+
5
13
  - Stop the card fields adyen-web 5 encrypts at a reviewed Adyen merchant profile. adyen-web 5 seals each field as an RSA-OAEP / A256CBC-HS512 JWE, where adyen-web 6 writes RSA-OAEP-256 / A256GCM, and the SDK read only adyen-web 6's form. So the card requests of Cathay Pacific's payment page and Dunelm's legacy checkout, and Cathay's charge request after its signing step, reached the merchant with the agent's dummy card. The adapters now read both forms as a card. Both profiles ship in `observe`, so the adapters abort each of these requests before it leaves the browser and report it as `merchant_profile_observed`. The Vault writes only adyen-web 6's form, so the API refuses a create for a card request in adyen-web 5's form with `cse_template_unsupported`, and nothing is charged.
6
14
 
7
15
  - Abort the requests in which IHG's pay page sends the typed card outside its reviewed card request, in every status, as `card_endpoint_refused` with reason `card_resend`. The page posts the same booking to IHG's enrollment service while its pre-ticked "Enroll for free" box stays ticked, posts it on other paths for more than one room, a travel agent or Business Rewards, and at a hotel that asks for 3-D Secure first posts the number to two 3-D Secure calls. The SDK let each of these reach IHG with the agent's dummy card. A booking with the box unticked reaches IHG's reviewed card request, which the adapters refuse as before while IHG is in `observe`.
8
16
 
17
+ - Read IHG's booking at a hotel that asks for no security code as IHG's reviewed card request. At the two US hotels a live check visited, the pay page shows no security code field and sends `cvvNumber: {token: ""}`. The adapters read that as a card in a body IHG's profile did not review and refused it with reason `unrecognized_payment_endpoint`, so the booking could never pause for the cardholder. They now refuse it with reason `merchant_card_endpoint_not_enabled`, naming IHG's profile, while IHG is in `observe`, and once Agentcard enables IHG it pauses and asks the cardholder like any other IHG booking. A security code the hotel asks for is still read as three or four digits, and any other value there is refused as before.
18
+
19
+ - Read a hotel booking at IHG, Hilton or The Verb Hotel as the hotel's reviewed card request only at a rate the hotel charges in full at booking: IHG's Book Early & Save (`ratePlanCode` `IDAPF`) in the hotel's own currency, a Hilton rate guaranteed `FP` with a deposit, and The Verb Hotel's Non-Refundable rate (`14DAYAP`). The adapters now refuse a rate that holds the card and charges it at the stay, an IHG group rate, an IHG booking that names `hotels[0].bookingCurrency` (the shopper's currency, which IHG's page sends only when the hotel prices in another one) and a Hilton booking with its pre-ticked "Join Hilton Honors for free" left on (`variables.enrollment`) with reason `unrecognized_payment_endpoint` and an event that names only the endpoint, where they refused each as `merchant_card_endpoint_not_enabled` naming the profile. All three hotels stay in `observe`, so the adapters refuse every booking there before any authorization, as before. IHG's and Hilton's profiles now carry the card network codes their pages write (VS, MC, AX and DS at IHG; VI, MC, AX and DS at Hilton): a body that names another code is unrecognized, and once a hotel is enabled the cardholder's device writes the code of the real card's network and refuses a card on any other. Hilton's profile reads `charge_timing: 'at_request'`, since Hilton charges its prepaid rate at booking. Italo's profile no longer waits on how its page sends `expire`, which the live checks of Sep 30 and Oct 6 read as left out.
20
+
21
+ - Stop Universal Orlando's card request at the checkout it runs today. Universal moved its store from `www.universalorlando.com/web-store` to `store.universalorlando.com`, whose guest checkout posts the card to `secure-api.universalparks.com/instances/{guest}/carts/{cart}/precheckout`. The SDK's profile still described the web-store request, so it let this one through and the agent's dummy card reached Universal. The adapters now refuse it before it leaves the browser with reason `merchant_card_endpoint_not_enabled`, naming Universal's profile, while Universal is in `observe`. They also abort the store's two other posts of the typed card, a signed-in customer's precheckout and the Cardinal 3-D Secure token call Universal keeps off today, in every status, as `card_endpoint_refused` with reason `card_resend`. The retired web-store checkout still loads from a deep link such as `www.universalorlando.com/web-store/en/us/checkout`, and the adapters abort its two posts of the typed card the same way: a guest's `/instances/{guest}/commerce-checkout`, the request they refused before this release, and a signed-in customer's `/guests/{guest}/commerce-checkout`. Once Agentcard enables Universal, the API takes one payment per cart and refuses another create for it with `409 merchant_checkout_in_use`.
22
+
23
+ - Read the card number on the D-EDGE card frame the way the frame groups each card network: 4-6-5 for an American Express number, 4-6-4 for a Diners Club number starting 36 or 300 to 305, and groups of four for any other. The SDK's copy of D-EDGE's rules read a grouped number only in groups of four, so an American Express or Diners Club placeholder as the frame groups it was not D-EDGE's reviewed card request, and the same number in groups of four, which the frame never posts, was. D-EDGE stays in `observe`, so its card post still gets the page that says Agentcard has not turned it on.
24
+
9
25
  ## 0.23.0
10
26
 
11
27
  ### Upgrading from 0.22.0
package/PREFLIGHT.md CHANGED
@@ -207,6 +207,8 @@ The bundled direct SDK profile declares processor support for `one_time` purchas
207
207
 
208
208
  The catalog also carries reviewed hosted, regional, and versioned variants observed on live checkouts: Adyen secured fields on the regional live hosts and any release, the Square card element frame, dLocal Smart Fields releases, Mercado Pago secure fields and the guest Checkout Pro card form, and the Rapyd hosted checkout page. A variant the implemented adapter does not admit, such as the Tranzila terminal-root page, the Authorize.Net hosted payment form, or a Worldpay hosted payment page, identifies its processor and returns `unknown` with `processor_variant_unverified`. A new fingerprint never declares payment support by itself.
209
209
 
210
+ PayPal's legacy Hosted Fields (`paypal`) are identified from the card fields PayPal's JS SDK renders with `components=hosted-fields`: braintree-web builds whose version carries `-payments-sdk`, such as `3.32.0-payments-sdk-dev`, served from `assets.braintreegateway.com` as `hosted-fields-input-frame.min.html` and `hosted-fields-payments-sdk-tokenization-frame.min.html`. PayPal's frames send the card to PayPal, not to Braintree, so a page that shows them names `paypal` and returns `unknown` with `processor_variant_unverified`, and no braintree-web file from a `-payments-sdk` build counts as Braintree evidence. The SDK fails the card request those frames send; see the PayPal Hosted Fields row in the [README's processor table](./README.md#supported-processors).
211
+
210
212
  Adyen's `supported` covers the Sessions payment request on Adyen's own hosts. Some Adyen merchants post the four encrypted fields to their own server instead, and their pages load the same secured fields, so an inspection identifies Adyen on both and cannot tell the two flows apart. The SDK pays a merchant's own endpoint only through a merchant profile Agentcard reviewed for it; the reviewed profiles (Dunelm and Cinemark) are in `observe`, so the SDK aborts their card requests and nothing is charged. The `actual_payment_request_validation` requirement is where that difference shows: check the paused request's host before treating an Adyen result as payable.
211
213
 
212
214
  Authorize.Net Direct Post Method and SIM ship dark. The SDK pauses their `transact.dll` card form only for an org Agentcard enabled for them (capability `authorize_net_dpm_form`), and the bundled catalog leaves them out, so the table above does not list them and inspection answers for their forms exactly as it did before: the SIM hosted form on `secure.authorize.net` identifies Authorize.Net and returns `unknown` with `processor_variant_unverified`.
package/README.md CHANGED
@@ -254,6 +254,7 @@ Coverage is specific to the processor request format, merchant setup, browser tr
254
254
  | Checkout.com | supported |
255
255
  | Mercado Pago | Card tokenization and prepared checkout are implemented. Guest Checkout Pro in Mexico also corrects the issuer for one native card association when the selected card has the same brand and type. One live MXN 40 Lotería Chida purchase with published SDK 0.10.0 completed automatically through Pay; the merchant confirmed paid status and PDF fulfillment. The historical SDK result remains unknown because the private receipt adapter rejected a relative download URL; a separate read with the corrected adapter confirms that same paid receipt. Independent processor capture/settlement and other country or integration paths remain unverified. |
256
256
  | VGS Collect (Very Good Security; Wolt) | not supported: VGS's proxy aliases only submissions from its own iframe, so a replay from the cardholder's device is refused by the merchant (verified on Wolt, 2026-09-03). Not recognized, so the agent's browser is not paused there |
257
+ | PayPal Hosted Fields (the legacy card fields of PayPal's JS SDK; Planet Golf UK) | not supported: no Agentcard payment takes over the card these fields send to `cors.api.paypal.com`. The SDK fails that request for every company, so PayPal's payment confirm never receives the dummy card, and the controller reads `unsupported` with reason `unsupported_paypal_hosted_fields` and holds the attachment until you attach again; a PayPal wallet payment continues untouched |
257
258
  | Adyen | supported (mode `cse`): the vault encrypts the card for Adyen on the cardholder's device and your browser sends it. Sessions flow: the paused request is `/checkoutshopper/v1/sessions/{id}/payments` on Adyen's own hosts. A merchant that posts the encrypted fields to its own server pauses only when Agentcard reviewed it as a merchant profile (Dunelm and Cinemark so far); both profiles are in `observe`, so their card requests are reported and aborted and nothing pays there yet (see "Adyen merchants that take the card on their own server"). Without a preparation, approval starts at Pay and must land before Adyen Web's own request timeout (observed at 60 seconds on Adyen Web 6.41 and 6.44; not enforced by the SDK); `prepare({ psp: 'adyen' })` moves the approval before Pay |
258
259
  | ACI Worldwide COPYandPAY (Halfords) | dark launch: served only after `syncRegistry()` and only for an org Agentcard enabled; prepared checkout only. The card-number request is replayed from the cardholder's device, the page's own CVC and expiry requests are answered locally, and `/payment` continues once. See [Pay an ACI COPYandPAY checkout](#pay-an-aci-copyandpay-checkout) |
259
260
  | Tranzila | supported (mode `hosted_form`): the cardholder finishes on Tranzila's own page; the paused form navigation resolves to a synthetic page, and you poll the merchant's order state |
@@ -424,21 +425,25 @@ mode; the difference matters if you drive `authorize()` yourself.
424
425
  page's origin, the exact origin, path and query of the card request, the
425
426
  session headers and cookies it needs, and the answer headers that may come
426
427
  back. The adapters arm every profile's endpoint (`cardEndpointPatterns()`
427
- lists the `Fetch.enable` patterns) and pause a POST to exactly a profile's
428
- card request URL when it carries a card: anything at the profile's card
429
- number field, or a Luhn-valid number of 12 to 19 digits anywhere in the
430
- body, however it is written. A body the adapter cannot read counts as
431
- carrying one. A GET, a CORS preflight, a look-alike URL, or a POST to the
432
- same URL with no card in it (an IHG stay paid with points, a LookFantastic
433
- order whose card a processor's own fields tokenized, a saved card, a room
434
- another SynXis hotel guarantees with a token) continues untouched. While this
428
+ lists the `Fetch.enable` patterns) and pause a request to exactly a
429
+ profile's card request URL, sent with the profile's own method (a POST, or
430
+ the PUT Target Optical's checkout sends), when it carries a card: anything
431
+ at the profile's card number field, or a Luhn-valid number of 12 to 19
432
+ digits anywhere in the body, however it is written. A body the adapter
433
+ cannot read counts as carrying one. A GET, a CORS preflight, another
434
+ method, a look-alike URL, or a request to the same URL with no card in it
435
+ (an IHG stay paid with points, a saved card, a room another SynXis hotel
436
+ guarantees with a token) continues untouched. While this
435
437
  build's copy of a profile does not take payments (every profile is in
436
438
  observe today) a card request is refused as `unsupported_checkout` before
437
439
  any authorization exists. When the body is the request the profile
438
440
  reviewed, the controller reads `unsupported` with reason
439
441
  `merchant_card_endpoint_not_enabled` and the event names the profile; a
440
442
  card in any other body reads reason `unrecognized_payment_endpoint`, and
441
- the event names only the endpoint origin. For a
443
+ the event names only the endpoint origin. LookFantastic's checkout takes
444
+ the card in BR-DGE's card fields and then sends the masked card number
445
+ BR-DGE returns in its order, so the adapters refuse that order with reason
446
+ `unrecognized_payment_endpoint`. For a
442
447
  profile that takes payments, the adapter captures the request's session:
443
448
  the profile's own header names from the paused request, the endpoint's
444
449
  cookie jar for a cookie profile (`Network.getCookies` or
@@ -12,7 +12,8 @@ export interface CardEndpointProfile {
12
12
  readonly frame: string | null;
13
13
  /** The exact https origin the card request goes to. */
14
14
  readonly endpoint: string;
15
- readonly method: 'POST';
15
+ /** A POST, or a PUT for a page that sends its card request as one (Target Optical's addPayment). */
16
+ readonly method: 'POST' | 'PUT';
16
17
  readonly path: string;
17
18
  readonly query: Readonly<Record<string, string>>;
18
19
  /** json_api is a JSON object sent as application/vnd.api+json. */
@@ -130,6 +131,13 @@ export declare function cardEndpointById(id: string): CardEndpointProfile | null
130
131
  export declare function cardEndpointPasses(profile: CardEndpointProfile, body: string | null, contentType: string | undefined): boolean;
131
132
  /** The reviewed profile (any status) one of whose resend requests `url` and `method` are, or null: the adapters abort it. */
132
133
  export declare function cardEndpointResendFor(url: string, method: string): CardEndpointProfile | null;
134
+ /**
135
+ * Whether a URL is any profile's card request or one of its resends, by any method the
136
+ * table names: the Playwright adapter's route filter, which sees the URL alone. The
137
+ * pre-claim gates then judge each routed request by its own method and body (a GET or a
138
+ * CORS preflight to the same URL defers), so routing a URL is never a decision to pause it.
139
+ */
140
+ export declare function cardEndpointRouted(url: string): boolean;
133
141
  /** The origins of the profiles whose merchant keeps its own record of the order: the only responses card-endpoint-facts.ts records. */
134
142
  export declare function cardEndpointFactsOrigins(): readonly string[];
135
143
  /**
@@ -11,8 +11,9 @@
11
11
  // typed into, the exact origin, path and query of the card request, which session
12
12
  // headers and cookies it needs, and which answer headers may travel back.
13
13
  //
14
- // The adapters arm every profile's endpoint. A POST that is exactly a profile's card
15
- // request URL pauses when it carries a card (cardEndpointCarriesCard); one that carries
14
+ // The adapters arm every profile's endpoint. A request that is exactly a profile's card
15
+ // request, its URL and its method (a POST, or Target Optical's PUT), pauses when it
16
+ // carries a card (cardEndpointCarriesCard); one that carries
16
17
  // none, such as a stay paid with points or a card a processor's own fields tokenized,
17
18
  // is the merchant's own business and continues untouched. While this build's copy of
18
19
  // the profile does not take payments (observe, a fact only a partner HAR can pin, no
@@ -108,6 +109,18 @@ export function cardEndpointResendFor(url, method) {
108
109
  return null;
109
110
  }
110
111
  }
112
+ // Every method a profile's card request or one of its resends is sent with (a POST, Target
113
+ // Optical's PUT addPayment and PUT card save), from this build's own table.
114
+ const ROUTED_METHODS = [...new Set(PROFILES.flatMap((profile) => [profile.method, ...(profile.resends ?? []).map((resend) => resend.method)]))];
115
+ /**
116
+ * Whether a URL is any profile's card request or one of its resends, by any method the
117
+ * table names: the Playwright adapter's route filter, which sees the URL alone. The
118
+ * pre-claim gates then judge each routed request by its own method and body (a GET or a
119
+ * CORS preflight to the same URL defers), so routing a URL is never a decision to pause it.
120
+ */
121
+ export function cardEndpointRouted(url) {
122
+ return ROUTED_METHODS.some((method) => cardEndpointFor(url, method) !== null || cardEndpointResendFor(url, method) !== null);
123
+ }
111
124
  /** The origins of the profiles whose merchant keeps its own record of the order: the only responses card-endpoint-facts.ts records. */
112
125
  export function cardEndpointFactsOrigins() {
113
126
  return [...new Set(PROFILES.filter((profile) => profile.facts).map((profile) => profile.endpoint))];
package/dist/cdp.js CHANGED
@@ -12,6 +12,7 @@ import { GMO_TOKEN_V1_PATTERNS, gmoRequestShop, isGmoTokenV1Request, recognizesG
12
12
  import { DatatransRepeats } from './datatrans.js';
13
13
  import { SHIJI_STS_COMPANION_WINDOW_MS, ShijiStsCompanions, ShijiStsError, isShijiStsHost, shijiStsTokenContinuation, } from './shiji-sts.js';
14
14
  import { isArmedEkashuUrl } from './ekashu.js';
15
+ import { PAYPAL_HOSTED_FIELDS_PATTERNS, isPaypalHostedFieldsConfirm } from './paypal-hosted-fields.js';
15
16
  import { CheckoutAttachmentError, attachmentDeadline, attachmentFailure, withinAttachmentDeadline } from './attachment.js';
16
17
  import { cseBody } from './cse-body.js';
17
18
  import { STRIPE_SETUP_INTENT_CONFIRM, setupConfirmPayingWith } from './stripe-setup.js';
@@ -23,7 +24,7 @@ import { hostedFormSubmittedPage } from './hosted-form.js';
23
24
  import { amadeusSendWindowMs, armSendWindow, failsAsNetworkError } from './amadeus.js';
24
25
  import { CheckoutLifecycle, paymentEndpointGuards } from './lifecycle.js';
25
26
  import { CaptureContexts, MicroformFrames, judgeMicroformFrameUrls, loadedCardMicroformFromUrls, refusedRequestFailure, } from './cybersource-flex.js';
26
- import { CARD_ENDPOINT_REFUSED_STEP_ANSWER, CARD_ENDPOINT_WITHHELD_ANSWER, CardEndpointRefusedError, captureCardEndpointSession, cardEndpointAnswerHeaders, cardEndpointCookies, cardEndpointDetail, cardEndpointFor, cardEndpointNavigationAnswer, cardEndpointPatterns, cardEndpointResendFor, } from './card-endpoint.js';
27
+ import { CARD_ENDPOINT_REFUSED_STEP_ANSWER, CARD_ENDPOINT_WITHHELD_ANSWER, CardEndpointRefusedError, captureCardEndpointSession, cardEndpointAnswerHeaders, cardEndpointCookies, cardEndpointDetail, cardEndpointNavigationAnswer, cardEndpointPatterns, cardEndpointRouted, } from './card-endpoint.js';
27
28
  import { CardEndpointFactsWatch } from './card-endpoint-facts.js';
28
29
  import { PAYRAILS_GUARD_PATTERNS, PAYRAILS_INIT_BODY_LIMIT, PayrailsSessionCapture, isPayrailsHost, payrailsInitCandidate, recognizesPayrails, } from './payrails.js';
29
30
  import { cardEndpointStoppedPage, refusedNavigationOutcome } from './card-endpoint-page.js';
@@ -621,13 +622,13 @@ export function withCorsHeaders(headers, cors) {
621
622
  return out;
622
623
  }
623
624
  /**
624
- * The request a card endpoint approval is asked for: `request` names the URL and
625
- * nothing else, and the profile travels with the captured session in
626
- * merchant_card_endpoint, the one member the API seals, with the merchant's own cut
627
- * record of the order for a profile that reads one.
625
+ * The request a card endpoint approval is asked for: `request` names the URL and the
626
+ * profile's own method and nothing else, and the profile travels with the captured
627
+ * session in merchant_card_endpoint, the one member the API seals, with the merchant's
628
+ * own cut record of the order for a profile that reads one.
628
629
  */
629
630
  function cardEndpointRequest(profile, session, facts = null) {
630
- return { url: session.url, method: 'POST', headers: {}, body: '', merchant_card_endpoint: { profile: profile.id, session, ...(facts ? { facts } : {}) } };
631
+ return { url: session.url, method: profile.method, headers: {}, body: '', merchant_card_endpoint: { profile: profile.id, session, ...(facts ? { facts } : {}) } };
631
632
  }
632
633
  /**
633
634
  * The merchant's own record of the order for a paused card request of a profile that
@@ -1909,10 +1910,13 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
1909
1910
  // merchant, and a request with no card in it continues untouched.
1910
1911
  // Every Payrails host is intercepted only for an org the API served Payrails
1911
1912
  // to, so the gate can abort a card-form payment (see payrails.ts).
1913
+ // PayPal's legacy Hosted Fields confirm is armed for every org, whatever the
1914
+ // registry serves: one that carries the typed card is paused only to be
1915
+ // refused, and one with no card continues (see paypal-hosted-fields.ts).
1912
1916
  const urlPatterns = [...new Set([...(derived.length > 0 ? derived : FALLBACK_CARD_PATTERNS), ...guards.patterns,
1913
1917
  ...(stripeCheckout.isEnabled() ? ['https://api.stripe.com/*'] : []), MERCADO_CHECKOUT_PATTERN, ...merchantPatterns,
1914
1918
  ...(recognizesGmo(opts.vault) ? GMO_TOKEN_V1_PATTERNS : []), ...juspayCheckout.urlPatterns(), ...cardEndpointPatterns(),
1915
- ...(payrails ? PAYRAILS_GUARD_PATTERNS : [])])];
1919
+ ...(payrails ? PAYRAILS_GUARD_PATTERNS : []), ...PAYPAL_HOSTED_FIELDS_PATTERNS])];
1916
1920
  const arm = async (sessionId, resume = false, worker = false) => {
1917
1921
  const key = sessionId ?? '__root__';
1918
1922
  if (armed.has(key))
@@ -3173,17 +3177,19 @@ export async function attachToPlaywright(page, opts) {
3173
3177
  // GMO token v1's JSONP GET, routed only for an org the API served GMO to (see attachToCdp).
3174
3178
  || (isGmoTokenV1Request(url.toString()) && recognizesGmo(opts.vault))
3175
3179
  || juspayCheckout.matches(url.toString())
3176
- // Every reviewed card endpoint, in any status (the pre-claim gate holds only a request that carries a card,
3177
- // and refuses one this build does not pay).
3178
- || cardEndpointFor(url.toString(), 'POST') !== null
3179
- // A request that sends the typed card to such a merchant again, refused in every status.
3180
- || cardEndpointResendFor(url.toString(), 'POST') !== null
3180
+ // Every reviewed card endpoint, in any status, and every request that sends the typed card to such a merchant
3181
+ // again, by any method the table names (Target Optical's addPayment and card save are PUTs). The pre-claim
3182
+ // gates hold only a card request that carries a card, refuse one this build does not pay, and refuse a resend
3183
+ // in every status.
3184
+ || cardEndpointRouted(url.toString())
3181
3185
  || aci.matches(url.toString())
3182
3186
  // Every Payrails host, routed only for an org the API served Payrails to (see attachToCdp).
3183
3187
  || (payrails !== null && isPayrailsHost(url.toString()))
3184
3188
  // Every request to an NMI host whose entry is served (ekashu.ts), as attachToCdp arms the
3185
3189
  // host: the card page's post and its 3-D Secure calls reach the NMI gate and fail.
3186
- || isArmedEkashuUrl(opts.vault, url.toString()), async (route) => {
3190
+ || isArmedEkashuUrl(opts.vault, url.toString())
3191
+ // PayPal's legacy Hosted Fields confirm, for every org, as attachToCdp arms it (paypal-hosted-fields.ts).
3192
+ || isPaypalHostedFieldsConfirm(url.toString()), async (route) => {
3187
3193
  const request = route.request();
3188
3194
  // When this request paused, and the page it came from: the merchant's total is read as of now.
3189
3195
  const pausedAt = Date.now();
package/dist/client.js CHANGED
@@ -1787,9 +1787,10 @@ export class VaultClient {
1787
1787
  // with that method, so a caller handing in another one would get it
1788
1788
  // silently rewritten and the cardholder's device would replay something the
1789
1789
  // caller never asked for. A direct caller deserves to be told, not surprised.
1790
- const method = (input.request.method ?? 'POST').toUpperCase();
1791
- // A card endpoint has no recognizer: its card request is a POST, as every recognizer's is but eProtect's.
1792
- const cardMethod = rec ? recognizerRequestMethod(rec) : 'POST';
1790
+ // A card endpoint has no recognizer: its card request is its profile's own method (a POST, or
1791
+ // Target Optical's PUT), which admitCardEndpoint already held the request to.
1792
+ const method = (input.request.method ?? cardEndpoint?.method ?? 'POST').toUpperCase();
1793
+ const cardMethod = rec ? recognizerRequestMethod(rec) : cardEndpoint ? cardEndpoint.method : 'POST';
1793
1794
  if (method !== cardMethod) {
1794
1795
  throw new Error(cardMethod === 'GET'
1795
1796
  ? `a ${rec?.psp} card registration is a GET; got ${method} for ${redactUrl(input.request.url)}.`
@@ -1854,7 +1855,7 @@ export class VaultClient {
1854
1855
  ...(input.cardId ? { cardId: input.cardId } : {}),
1855
1856
  execution_mode: 'user_approval',
1856
1857
  checkout_origin: cardEndpoint.session.page_origin,
1857
- request: { url: cardEndpoint.session.url, method: 'POST', headers: {}, body: '' },
1858
+ request: { url: cardEndpoint.session.url, method: cardEndpoint.method, headers: {}, body: '' },
1858
1859
  merchant_card_endpoint: { profile: cardEndpoint.profile, session: cardEndpoint.session, ...(cardEndpoint.facts ? { facts: cardEndpoint.facts } : {}) },
1859
1860
  } : {
1860
1861
  user: input.user,
@@ -2707,8 +2708,10 @@ export class VaultClient {
2707
2708
  /**
2708
2709
  * A card endpoint request, checked whole before any network call: a profile in this
2709
2710
  * build's table; no other checkout context, Autopilot or preparation (the cardholder
2710
- * approves every such payment); the agent's amount, the only authority here; a
2711
- * session the API and the device would take (payment-core
2711
+ * approves every such payment); the agent's amount, the only authority here; the
2712
+ * session's own URL with the profile's own method (a request that names none takes
2713
+ * the profile's, a POST or Target Optical's PUT); a session the API and the device
2714
+ * would take (payment-core
2712
2715
  * merchantCardEndpointSessionRefusal: the profile's URL, headers, cookies, Referer and
2713
2716
  * body, paused on its checkout page); for a merchant that keeps its own record of the
2714
2717
  * order, that record, held to the agent's amount and currency, and refused for an order
@@ -2731,7 +2734,7 @@ function admitCardEndpoint(input) {
2731
2734
  if (input.amount == null || typeof input.currency !== 'string' || !input.currency)
2732
2735
  throw new CardEndpointRefusedError(profile.id, 'amount_required');
2733
2736
  const session = named.session;
2734
- if (!isRecord(session) || (input.request.method ?? 'POST').toUpperCase() !== 'POST' || input.request.url !== session.url) {
2737
+ if (!isRecord(session) || (input.request.method ?? profile.method).toUpperCase() !== profile.method || input.request.url !== session.url) {
2735
2738
  throw new CardEndpointRefusedError(profile.id, 'request_unsupported', 'the request is not the session\'s card request');
2736
2739
  }
2737
2740
  const refusal = cardEndpointSessionRefusal(profile, session);
@@ -2740,7 +2743,7 @@ function admitCardEndpoint(input) {
2740
2743
  const facts = admitCardEndpointFacts(profile, session, named.facts, input);
2741
2744
  if (!cardEndpointLive(profile))
2742
2745
  throw new CardEndpointRefusedError(profile.id, 'not_enabled');
2743
- return { profile: profile.id, session, ...(facts ? { facts } : {}) };
2746
+ return { profile: profile.id, method: profile.method, session, ...(facts ? { facts } : {}) };
2744
2747
  }
2745
2748
  /**
2746
2749
  * The merchant's own record of the order, for a profile whose merchant keeps one
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: a2baaac410b5aaa629b3d5031136f88645c7b45c67c3bbc154cad72de216a04e
3
+ // artifact-sha256: c6a711b5dc964dbeb45863780001d99b79090f919b282fed9ae6fc06b4839681
4
4
  // src/cybersource-flex-rules.js
5
5
  var CYBERSOURCE_FLEX_RULES_SOURCE = Object.freeze({ version: "2.12.1", sha256: "8429ebae83e9745cd53ad8ff0d9ba69bee0087c1435ed0695a25444bed418327" });
6
6
  var CYBERSOURCE_FLEX_RULES = [
@@ -631,9 +631,11 @@ async function transientTokenFacts(status, body, verified, card, { crypto } = {}
631
631
  if (!/^[0-9]{12,19}$/.test(number) || typeof masked !== "string" || masked.length > 19 || !masked.endsWith(number.slice(-4))) {
632
632
  return unconfirmed("last_four");
633
633
  }
634
- const bin = tokenCard.number.bin;
635
- if (typeof bin !== "string" || !/^[0-9]{6,8}$/.test(bin) || !number.startsWith(bin))
634
+ const { bin, prefix } = tokenCard.number;
635
+ const startsThisCard = (value, form) => typeof value === "string" && form.test(value) && number.startsWith(value);
636
+ if (bin === void 0 && prefix === void 0 || bin !== void 0 && !startsThisCard(bin, /^[0-9]{6,8}$/) || prefix !== void 0 && !startsThisCard(prefix, number.length >= 16 ? /^(?:[0-9]{6}|[0-9]{8})$/ : /^[0-9]{6}$/)) {
636
637
  return unconfirmed("bin");
638
+ }
637
639
  const month = tokenCard.expirationMonth?.value;
638
640
  const year = tokenCard.expirationYear?.value;
639
641
  const cardMonth = String(card?.month ?? "").padStart(2, "0");
@@ -225,10 +225,12 @@ export declare class CheckoutLifecycle implements CheckoutController {
225
225
  * unsupported_primer_request), a Payrails request its gate refuses
226
226
  * (`unsupported_payrails_card_payment` and the card-save refusal codes), a
227
227
  * Shiji STS request its gate refuses (shiji_sts_keep_on_file and the other
228
- * shiji-sts.ts reasons), or a post to a processor's own card page that only
229
- * the cardholder's device can fill (processor_card_page: NMI's).
228
+ * shiji-sts.ts reasons), a post to a processor's own card page that only
229
+ * the cardholder's device can fill (processor_card_page: NMI's), or a card
230
+ * sent to PayPal's legacy Hosted Fields confirm, which no Vault mode takes
231
+ * over (unsupported_paypal_hosted_fields, paypal-hosted-fields.ts).
230
232
  */
231
- unsupported(reason?: 'unrecognized_payment_endpoint' | 'merchant_profile_observe' | 'merchant_request_unsupported' | GmoUnsupportedReason | 'microform_unsupported' | 'juspay_request_unsupported' | 'merchant_card_endpoint_not_enabled' | 'merchant_card_endpoint_resend_refused' | 'creditguard_three_d_secure' | 'unsupported_primer_sdk' | 'unsupported_primer_request' | PayrailsUnsupportedReason | ShijiStsUnsupportedReason | 'processor_card_page'): void;
233
+ unsupported(reason?: 'unrecognized_payment_endpoint' | 'merchant_profile_observe' | 'merchant_request_unsupported' | GmoUnsupportedReason | 'microform_unsupported' | 'juspay_request_unsupported' | 'merchant_card_endpoint_not_enabled' | 'merchant_card_endpoint_resend_refused' | 'creditguard_three_d_secure' | 'unsupported_primer_sdk' | 'unsupported_primer_request' | PayrailsUnsupportedReason | ShijiStsUnsupportedReason | 'processor_card_page' | 'unsupported_paypal_hosted_fields'): void;
232
234
  /**
233
235
  * A card request that only a prepared checkout pays (an Adyen merchant
234
236
  * profile's, or a Fiserv card capture) arrived with no preparation on this
package/dist/lifecycle.js CHANGED
@@ -201,8 +201,10 @@ export class CheckoutLifecycle {
201
201
  * unsupported_primer_request), a Payrails request its gate refuses
202
202
  * (`unsupported_payrails_card_payment` and the card-save refusal codes), a
203
203
  * Shiji STS request its gate refuses (shiji_sts_keep_on_file and the other
204
- * shiji-sts.ts reasons), or a post to a processor's own card page that only
205
- * the cardholder's device can fill (processor_card_page: NMI's).
204
+ * shiji-sts.ts reasons), a post to a processor's own card page that only
205
+ * the cardholder's device can fill (processor_card_page: NMI's), or a card
206
+ * sent to PayPal's legacy Hosted Fields confirm, which no Vault mode takes
207
+ * over (unsupported_paypal_hosted_fields, paypal-hosted-fields.ts).
206
208
  */
207
209
  unsupported(reason = 'unrecognized_payment_endpoint') {
208
210
  this.held = true;