@agent-cards/sdk 0.22.0 → 0.23.1

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/client.d.ts CHANGED
@@ -9,8 +9,9 @@ import { VoiceModule, type VoiceOptions } from './voice.js';
9
9
  import { BuyModule } from './buy.js';
10
10
  import { RequestRegistry } from './requests.js';
11
11
  import type { ApiTransport } from './api-module.js';
12
- import { type MicroformSidecar } from './cybersource-flex.js';
12
+ import { type MicroformField, type MicroformSidecar } from './cybersource-flex.js';
13
13
  import { type CardEndpointSession } from './card-endpoint.js';
14
+ import type { CardEndpointFactsRecord } from './card-endpoint-facts.js';
14
15
  import { type PayrailsSidecar } from './payrails.js';
15
16
  export interface PausedRequest {
16
17
  url: string;
@@ -29,12 +30,15 @@ export interface PausedRequest {
29
30
  microform?: MicroformSidecar;
30
31
  /**
31
32
  * A reviewed merchant's own card endpoint (see card-endpoint.ts): the profile and the
32
- * paused request's session, which the adapters capture. Cardholder data: it travels
33
- * only in the create's `merchant_card_endpoint` member, never in `request`.
33
+ * paused request's session, which the adapters capture, and, for a merchant that keeps
34
+ * its own record of the order, that record as the adapters read it from the page, cut
35
+ * to the values payment-core's reader reads (card-endpoint-facts.ts). Cardholder data:
36
+ * it travels only in the create's `merchant_card_endpoint` member, never in `request`.
34
37
  */
35
38
  merchant_card_endpoint?: {
36
39
  profile: string;
37
40
  session: CardEndpointSession;
41
+ facts?: CardEndpointFactsRecord;
38
42
  };
39
43
  /**
40
44
  * A Payrails card save (mode device_encrypt) only: the session key the page's
@@ -169,6 +173,9 @@ export declare const SUPPORTED_REGISTRY_FEATURES: readonly string[];
169
173
  * verifone.js blob sent to the merchant's server), which this build pauses and pays
170
174
  * only through a prepared checkout (verifone.ts); the API serves the Verifone
171
175
  * profiles only to a build that lists it, for a company Agentcard turned Verifone on for.
176
+ * `ekashu_hosted_payment_page`: NMI's hosted payment page, whose card-free entry this
177
+ * build pauses for the cardholder's device and whose card page this build never lets
178
+ * the agent's browser post to (pre-claim.ts, ekashu.ts); served on the same terms.
172
179
  */
173
180
  export declare const SUPPORTED_CAPABILITIES: readonly string[];
174
181
  /**
@@ -364,11 +371,12 @@ export interface HostedFormReplay extends ExecutionMetadata {
364
371
  * `setCookies` written into the agent's browser on the endpoint host, or on the parent
365
372
  * domain of a cookie the browser already holds there, which the write replaces.
366
373
  *
367
- * `withheld` is set when the device found the card in the answer, or a compressed
368
- * stream it cannot read: nothing of the answer travels, the adapters answer the paused
369
- * request with a fixed error, and the outcome is unknown until the merchant order is
370
- * checked. The card request was sent either way. Amounts are not reconciled on this
371
- * mode: the agent's amount is the only authority.
374
+ * `withheld` is set when the device found the card in the answer, a compressed
375
+ * stream it cannot read, or a step the review refuses (FastSpring's EBANX step): nothing
376
+ * of the answer travels, the adapters answer the paused request with a fixed error, and
377
+ * the outcome is unknown until the merchant order is checked. The card request was sent
378
+ * either way. Amounts are not reconciled on this mode: the agent's amount is the only
379
+ * authority, held before the create to the merchant's own total where its page keeps one.
372
380
  */
373
381
  export interface CardEndpointReplay extends ExecutionMetadata {
374
382
  mode: 'card_endpoint';
@@ -380,7 +388,7 @@ export interface CardEndpointReplay extends ExecutionMetadata {
380
388
  headers: Record<string, string>;
381
389
  setCookies: string[];
382
390
  body: string;
383
- withheld: 'card_in_response' | 'unreadable_in_response' | null;
391
+ withheld: 'card_in_response' | 'unreadable_in_response' | 'refused_step_in_response' | null;
384
392
  amountAuthority?: AmountAuthority;
385
393
  }
386
394
  /**
@@ -448,8 +456,25 @@ export interface PrepareCheckoutOptions {
448
456
  * processor.
449
457
  */
450
458
  paypageId?: string;
459
+ /**
460
+ * CyberSource Flex only: the cardholder's device tokenizes the card when it approves, before
461
+ * Pay, under the capture context of the card fields the page already loaded, so the page's
462
+ * createToken is answered at once. For a page that gives createToken only seconds (Alaska
463
+ * Airlines' flight checkout gives it three). Call prepare() once the card fields show; the
464
+ * attachment reads their capture context and fields and refuses with `microform_not_loaded`,
465
+ * `microform_ambiguous` or `microform_unsupported` when it cannot.
466
+ */
467
+ tokenizeBeforePay?: boolean;
451
468
  signal?: AbortSignal;
452
469
  }
470
+ /** A CyberSource Flex preparation that tokenizes before Pay: the loaded card field's capture context and fields. */
471
+ export interface PreparedCybersourceFlex {
472
+ captureContext: string;
473
+ microform: {
474
+ version: string;
475
+ fields: MicroformField[];
476
+ };
477
+ }
453
478
  export interface PrepareCheckoutInput extends PrepareCheckoutOptions {
454
479
  user: string;
455
480
  merchant: string;
@@ -462,6 +487,12 @@ export interface PrepareCheckoutInput extends PrepareCheckoutOptions {
462
487
  timeoutMs?: number;
463
488
  onPreparationCreated?: (id: string) => void;
464
489
  onApprovalUrl?: (url: string) => void;
490
+ /**
491
+ * CyberSource Flex, tokenized before Pay: the capture context and fields of the card Microform
492
+ * the page loaded (the attachments read them for prepare({ tokenizeBeforePay: true })). Verified
493
+ * here and by the API before any preparation exists.
494
+ */
495
+ cybersourceFlex?: PreparedCybersourceFlex;
465
496
  }
466
497
  /** Real user consent and an unlocked device; no processor request or payment yet. */
467
498
  export interface PreparedCheckout {
@@ -494,6 +525,18 @@ export interface PreparedCheckout {
494
525
  readonly paymentPageUrl?: string;
495
526
  /** Worldpay eProtect only: the paypageId the API confirmed, the one merchant whose registration may spend this approval. */
496
527
  readonly paypageId?: string;
528
+ /**
529
+ * CyberSource Flex, tokenized before Pay: the capture context and fields the cardholder's device
530
+ * tokenized the card under. Only a field request loaded under this context, with these fields,
531
+ * binds this preparation.
532
+ */
533
+ readonly cybersourceFlex?: Readonly<{
534
+ captureContext: string;
535
+ microform: Readonly<{
536
+ version: string;
537
+ fields: readonly MicroformField[];
538
+ }>;
539
+ }>;
497
540
  }
498
541
  export declare class CheckoutPreparationError extends Error {
499
542
  preparationId: string | null;
@@ -703,6 +746,15 @@ export declare const RUNTIME_CANCEL_REASONS: readonly RuntimeCancelReason[];
703
746
  export declare class PaymentOutcomeUnknownError extends Error {
704
747
  authorizationId: string | null;
705
748
  reason: string;
749
+ /**
750
+ * False when the API confirmed it retired this authorization before any processor
751
+ * request started: authorize() cancels an authorization it could not finish (a card
752
+ * endpoint, a prepared or a native checkout, or a request the page abandoned), and the
753
+ * cancel came back with `processor_request_started: false`, so nothing reached the
754
+ * processor or the merchant. Null when that is not known. The controller still reads
755
+ * `outcome_unknown`; a card form navigation's page reads this to say nothing was sent.
756
+ */
757
+ processorRequestStarted: false | null;
706
758
  constructor(authorizationId: string | null, reason: string);
707
759
  }
708
760
  export declare class ApprovalDeclinedError extends Error {
package/dist/client.js CHANGED
@@ -21,10 +21,11 @@ import { Languages } from './languages.js';
21
21
  import { RequestRegistry } from './requests.js';
22
22
  import { AgentcardError } from './voice-types.js';
23
23
  import { GMO_CAPABILITY, GMO_PSP, gmoTokenAnswer } from './gmo.js';
24
- import { isCybersourceFlexTokenShape } from './cybersource-flex.generated.js';
25
- import { CYBERSOURCE_FLEX_CAPABILITY, CYBERSOURCE_FLEX_PSP, validCybersourceFlexSidecar } from './cybersource-flex.js';
24
+ import { admitCybersourceFlexPreparation, isCybersourceFlexTokenShape, sameCybersourceFlexPreparation } from './cybersource-flex.generated.js';
25
+ import { CYBERSOURCE_FLEX_CAPABILITY, CYBERSOURCE_FLEX_PSP, validCybersourceFlexSidecar, } from './cybersource-flex.js';
26
26
  import { JUSPAY_CAPABILITY, JUSPAY_PSP, juspayPageAnswer } from './juspay-gate.js';
27
27
  import { CARD_ENDPOINT_MODE, CARD_ENDPOINT_PSP, CardEndpointRefusedError, cardEndpointById, cardEndpointLive, cardEndpointSessionRefusal, } from './card-endpoint.js';
28
+ import { merchantCardEndpointFactsRefusal as sharedFactsRefusal, readMerchantCardEndpointFacts as sharedReadFacts, } from './merchant-card-endpoints.generated.js';
28
29
  import { DATATRANS_CAPABILITY } from './datatrans.js';
29
30
  import { PELECARD_CAPABILITY, PELECARD_PSP } from './pelecard.js';
30
31
  import { PRIMER_CAPABILITY, PRIMER_PSP, primerTokenAnswer } from './primer.js';
@@ -154,8 +155,11 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
154
155
  * verifone.js blob sent to the merchant's server), which this build pauses and pays
155
156
  * only through a prepared checkout (verifone.ts); the API serves the Verifone
156
157
  * profiles only to a build that lists it, for a company Agentcard turned Verifone on for.
158
+ * `ekashu_hosted_payment_page`: NMI's hosted payment page, whose card-free entry this
159
+ * build pauses for the cardholder's device and whose card page this build never lets
160
+ * the agent's browser post to (pre-claim.ts, ekashu.ts); served on the same terms.
157
161
  */
158
- export const SUPPORTED_CAPABILITIES = ['fiserv_card_capture', GMO_CAPABILITY, CYBERSOURCE_FLEX_CAPABILITY, JUSPAY_CAPABILITY, DATATRANS_CAPABILITY, 'authorize_net_dpm_form', PELECARD_CAPABILITY, ACI_CAPABILITY, 'creditguard_pps_process', PRIMER_CAPABILITY, PAYRAILS_CAPABILITY, CONVERGE_CAPABILITY, EPROTECT_CAPABILITY, WSPAY_CAPABILITY, SHIJI_STS_CAPABILITY, AMADEUS_CHECKOUT_CAPABILITY, 'worldline_mycheckout_form', 'verifone_card_capture'];
162
+ export const SUPPORTED_CAPABILITIES = ['fiserv_card_capture', GMO_CAPABILITY, CYBERSOURCE_FLEX_CAPABILITY, JUSPAY_CAPABILITY, DATATRANS_CAPABILITY, 'authorize_net_dpm_form', PELECARD_CAPABILITY, ACI_CAPABILITY, 'creditguard_pps_process', PRIMER_CAPABILITY, PAYRAILS_CAPABILITY, CONVERGE_CAPABILITY, EPROTECT_CAPABILITY, WSPAY_CAPABILITY, SHIJI_STS_CAPABILITY, AMADEUS_CHECKOUT_CAPABILITY, 'worldline_mycheckout_form', 'verifone_card_capture', 'ekashu_hosted_payment_page'];
159
163
  /**
160
164
  * Processors whose paused request names no amount at all, so the pair the
161
165
  * agent passes is the amount the cardholder approves: authorize() refuses one
@@ -436,6 +440,15 @@ export const RUNTIME_CANCEL_REASONS = ['merchant_request_aborted', 'merchant_nev
436
440
  export class PaymentOutcomeUnknownError extends Error {
437
441
  authorizationId;
438
442
  reason;
443
+ /**
444
+ * False when the API confirmed it retired this authorization before any processor
445
+ * request started: authorize() cancels an authorization it could not finish (a card
446
+ * endpoint, a prepared or a native checkout, or a request the page abandoned), and the
447
+ * cancel came back with `processor_request_started: false`, so nothing reached the
448
+ * processor or the merchant. Null when that is not known. The controller still reads
449
+ * `outcome_unknown`; a card form navigation's page reads this to say nothing was sent.
450
+ */
451
+ processorRequestStarted = null;
439
452
  constructor(authorizationId, reason) {
440
453
  super(`payment outcome unknown${authorizationId ? ` for ${authorizationId}` : ''}: ${reason}; check the merchant order before retrying`);
441
454
  this.authorizationId = authorizationId;
@@ -919,6 +932,26 @@ const FISERV_PREPARATION_REFUSALS = [
919
932
  * holds that pin). Only for an eProtect checkout.
920
933
  */
921
934
  const EPROTECT_PREPARATION_REFUSALS = ['merchant_profile_mismatch'];
935
+ /**
936
+ * Refusals a CyberSource Flex preparation that tokenizes before Pay answers (4xx) before any
937
+ * preparation exists, surfaced as the CheckoutPreparationError reason: the processor is off for
938
+ * this company (`processor_unavailable`), the environment is not the client's mode, or the capture
939
+ * context or fields read from the page cannot be verified or rebuilt. Only for such a preparation;
940
+ * a Flex preparation without a capture context keeps `preparation_unconfirmed`, as before.
941
+ */
942
+ const CYBERSOURCE_FLEX_PREPARATION_REFUSALS = [
943
+ 'processor_unavailable', 'sandbox_host_on_live_row', 'live_host_on_sandbox_row', 'capture_context_invalid', 'microform_unsupported',
944
+ 'cybersource_environment_mismatch',
945
+ ];
946
+ /** Why payment-core refused a Flex preparation's capture context or fields, as a CheckoutPreparationError reason. */
947
+ function cybersourceFlexPreparationRefusal(error) {
948
+ const reason = error?.reason;
949
+ if (reason === 'microform_version' || reason === 'fields')
950
+ return 'microform_unsupported';
951
+ if (reason === 'environment_mismatch')
952
+ return 'cybersource_environment_mismatch';
953
+ return 'capture_context_invalid';
954
+ }
922
955
  /** Default backoff for a 502 amount_unverifiable at create: two retries, then give up. */
923
956
  const UNVERIFIABLE_RETRY_DELAYS_MS = [500, 1500];
924
957
  const PREPARATION_READ_RETRY_DELAYS_MS = [250, 500];
@@ -1370,6 +1403,25 @@ export class VaultClient {
1370
1403
  throw fail('paypage_id_invalid');
1371
1404
  if (eprotect && !input.cardId)
1372
1405
  throw fail('card_required');
1406
+ // CyberSource Flex, tokenized before Pay: the loaded card field's capture context and fields,
1407
+ // verified here against the keys payment-core pins before anything is created (the API and the
1408
+ // cardholder's device verify them again).
1409
+ if (input.tokenizeBeforePay !== undefined && typeof input.tokenizeBeforePay !== 'boolean')
1410
+ throw fail('tokenize_before_pay_invalid');
1411
+ const flexContext = input.cybersourceFlex;
1412
+ if (input.tokenizeBeforePay === true && !flexContext)
1413
+ throw fail('microform_not_loaded');
1414
+ if (flexContext) {
1415
+ if (input.psp !== CYBERSOURCE_FLEX_PSP)
1416
+ throw fail('unsupported_processor');
1417
+ try {
1418
+ await admitCybersourceFlexPreparation({ capture_context: flexContext.captureContext,
1419
+ microform: { version: flexContext.microform.version, fields: [...flexContext.microform.fields] }, environment: input.environment }, { crypto: globalThis.crypto });
1420
+ }
1421
+ catch (error) {
1422
+ throw fail(cybersourceFlexPreparationRefusal(error));
1423
+ }
1424
+ }
1373
1425
  if (!validAmountInput(input.amount) || typeof input.currency !== 'string' || !/^[a-z]{3}$/i.test(input.currency)) {
1374
1426
  throw new CheckoutPreparationError(null, 'amount_required', undefined, input.amount != null && !validAmountInput(input.amount) ? amountRefusal(input.amount, input.currency) : undefined);
1375
1427
  }
@@ -1417,6 +1469,8 @@ export class VaultClient {
1417
1469
  ...(declaration ? { sandbox_declaration: { endpoint: declaration.endpoint, client_key: declaration.clientKey, environment: 'test' } } : {}),
1418
1470
  ...(input.psp === 'creditguard' ? { payment_page_url: input.paymentPageUrl } : {}),
1419
1471
  ...(eprotect ? { paypage_id: input.paypageId } : {}),
1472
+ ...(flexContext ? { capture_context: flexContext.captureContext,
1473
+ microform: { version: flexContext.microform.version, fields: [...flexContext.microform.fields] } } : {}),
1420
1474
  }, AbortSignal.timeout(30_000), signal);
1421
1475
  if (!created || typeof created.id !== 'string' || !/^cprep_[A-Za-z0-9_-]{1,128}$/.test(created.id))
1422
1476
  throw fail('create_unconfirmed');
@@ -1456,7 +1510,9 @@ export class VaultClient {
1456
1510
  || state.psp !== input.psp || state.mode !== preparationMode(input.psp) || state.environment !== input.environment
1457
1511
  || state.checkout_key !== input.checkoutKey
1458
1512
  // eProtect: the merchant and the card the cardholder approved are the ones asked for.
1459
- || (eprotect && (state.paypage_id !== input.paypageId || state.card_id !== input.cardId)))
1513
+ || (eprotect && (state.paypage_id !== input.paypageId || state.card_id !== input.cardId))
1514
+ // The API says a preparation tokenizes before Pay exactly when this one carried a capture context.
1515
+ || (state.tokenize_before_pay === true) !== !!flexContext)
1460
1516
  throw fail('ready_unconfirmed', id);
1461
1517
  const prepared = Object.freeze({
1462
1518
  id: preparationId, status: 'ready', psp: input.psp, environment: input.environment, mode: preparationMode(input.psp), expiresAt: state.ready_expires_at,
@@ -1470,6 +1526,8 @@ export class VaultClient {
1470
1526
  ...(declaration ? { sandboxDeclaration: declaration } : {}),
1471
1527
  ...(input.psp === 'creditguard' ? { paymentPageUrl: input.paymentPageUrl } : {}),
1472
1528
  ...(eprotect ? { paypageId: input.paypageId } : {}),
1529
+ ...(flexContext ? { cybersourceFlex: Object.freeze({ captureContext: flexContext.captureContext,
1530
+ microform: Object.freeze({ version: flexContext.microform.version, fields: Object.freeze([...flexContext.microform.fields]) }) }) } : {}),
1473
1531
  });
1474
1532
  this.preparations.add(prepared);
1475
1533
  ready = true;
@@ -1494,7 +1552,8 @@ export class VaultClient {
1494
1552
  if (!id && !signal.aborted && error instanceof CheckoutApiError && error.code
1495
1553
  && ((error.status >= 400 && error.status < 500 && (PREPARATION_REFUSALS.includes(error.code)
1496
1554
  || (input.psp === 'fiserv' && FISERV_PREPARATION_REFUSALS.includes(error.code))
1497
- || (input.psp === EPROTECT_PSP && EPROTECT_PREPARATION_REFUSALS.includes(error.code))))
1555
+ || (input.psp === EPROTECT_PSP && EPROTECT_PREPARATION_REFUSALS.includes(error.code))
1556
+ || (!!flexContext && CYBERSOURCE_FLEX_PREPARATION_REFUSALS.includes(error.code))))
1498
1557
  || (input.psp === 'fiserv' && error.status === 502 && error.code === 'cse_key_unavailable')))
1499
1558
  throw fail(error.code);
1500
1559
  throw fail(signal.aborted ? 'cancelled' : 'preparation_unconfirmed', id);
@@ -1717,6 +1776,11 @@ export class VaultClient {
1717
1776
  if (mode === 'device_encrypt' && (!sidecar || typeof sidecar.encryption_public_key !== 'string' || !sidecar.encryption_public_key)) {
1718
1777
  throw new Error('A Payrails card save needs request.payrails.encryption_public_key, the session key from the checkout\'s Payrails init payload (the adapters capture it). Nothing was created.');
1719
1778
  }
1779
+ // A preparation that tokenized before Pay pays only a field request loaded under the capture
1780
+ // context and fields its cardholder approved: the device holds CyberSource's token for that context.
1781
+ if (preparation?.cybersourceFlex && !(flex && sameCybersourceFlexPreparation({ capture_context: preparation.cybersourceFlex.captureContext, microform: preparation.cybersourceFlex.microform }, { capture_context: input.request.capture_context, microform: input.request.microform }))) {
1782
+ throw new CheckoutPreparationError(preparation.id, 'checkout_changed');
1783
+ }
1720
1784
  // Say no rather than coerce. Each recognizer's card request has exactly one
1721
1785
  // method (recognizerRequestMethod): a POST body, or for Worldpay eProtect the
1722
1786
  // GET whose query carries the card. The service stores the replay template
@@ -1791,7 +1855,7 @@ export class VaultClient {
1791
1855
  execution_mode: 'user_approval',
1792
1856
  checkout_origin: cardEndpoint.session.page_origin,
1793
1857
  request: { url: cardEndpoint.session.url, method: 'POST', headers: {}, body: '' },
1794
- merchant_card_endpoint: { profile: cardEndpoint.profile, session: cardEndpoint.session },
1858
+ merchant_card_endpoint: { profile: cardEndpoint.profile, session: cardEndpoint.session, ...(cardEndpoint.facts ? { facts: cardEndpoint.facts } : {}) },
1795
1859
  } : {
1796
1860
  user: input.user,
1797
1861
  merchant: input.merchant,
@@ -1882,6 +1946,13 @@ export class VaultClient {
1882
1946
  // registration is read every 500 ms instead of waiting out 2 seconds.
1883
1947
  const preparedFastPollUntil = preparation?.psp === 'mercado_pago' ? Date.now() + 10_000
1884
1948
  : preparation?.psp === EPROTECT_PSP ? Date.now() + 15_000 : 0;
1949
+ // A preparation that tokenized before Pay: the cardholder's device already holds CyberSource's
1950
+ // token and approves as soon as this bind reaches it, while the page may give createToken only
1951
+ // seconds (Alaska Airlines' flight checkout gives it three). Poll every 100 ms for 10 seconds,
1952
+ // then at the caller's interval; at the default 2 seconds the first read alone would miss it.
1953
+ // The API's bind names 100 ms as such a row's poll interval, so slow_down admits these reads
1954
+ // (as an eProtect row names the 500 ms read above).
1955
+ const preparedFlexPollUntil = preparation?.cybersourceFlex ? Date.now() + 10_000 : 0;
1885
1956
  let execution = {};
1886
1957
  let approvalDelivered = false;
1887
1958
  const deliverApproval = (state) => {
@@ -1897,6 +1968,8 @@ export class VaultClient {
1897
1968
  catch { /* observer only */ }
1898
1969
  };
1899
1970
  let failed = false;
1971
+ // What this attempt throws, once it fails: a confirmed retirement below is written onto it.
1972
+ let failure = null;
1900
1973
  let actionRequired = false;
1901
1974
  const checkAutopilotAction = (state) => {
1902
1975
  if (state.autopilot_status !== 'action_required')
@@ -1927,7 +2000,8 @@ export class VaultClient {
1927
2000
  while (Date.now() < deadline) {
1928
2001
  if (stopSignal?.aborted)
1929
2002
  throw new PaymentOutcomeUnknownError(authorizationId, input.merchantSignal?.aborted ? 'merchant_request_aborted' : 'local_cancel');
1930
- const pollInterval = Date.now() < preparedFastPollUntil ? Math.min(this.pollIntervalMs, 500) : this.pollIntervalMs;
2003
+ const pollInterval = Date.now() < preparedFastPollUntil ? Math.min(this.pollIntervalMs, 500)
2004
+ : Date.now() < preparedFlexPollUntil ? Math.min(this.pollIntervalMs, 100) : this.pollIntervalMs;
1931
2005
  await interruptibleSleep(Math.min(pollInterval, Math.max(0, deadline - Date.now())), stopSignal);
1932
2006
  if (stopSignal?.aborted)
1933
2007
  throw new PaymentOutcomeUnknownError(authorizationId, input.merchantSignal?.aborted ? 'merchant_request_aborted' : 'local_cancel');
@@ -2257,9 +2331,8 @@ export class VaultClient {
2257
2331
  }
2258
2332
  catch (error) {
2259
2333
  failed = true;
2260
- if (input.merchantSignal?.aborted)
2261
- throw new PaymentOutcomeUnknownError(authorizationId, 'merchant_request_aborted');
2262
- throw error;
2334
+ failure = input.merchantSignal?.aborted ? new PaymentOutcomeUnknownError(authorizationId, 'merchant_request_aborted') : error;
2335
+ throw failure;
2263
2336
  }
2264
2337
  finally {
2265
2338
  // An attested action-required result retains its claim and reservation.
@@ -2271,7 +2344,12 @@ export class VaultClient {
2271
2344
  // known ID can be retired. An unacknowledged create remains unknown.
2272
2345
  // A started/finalized replay or failed cleanup never becomes a claimed
2273
2346
  // cancellation, and this best-effort cleanup never retries a payment.
2274
- await this.cancelAuthorization(authorizationId).catch(() => { });
2347
+ const retired = await this.cancelAuthorization(authorizationId).catch(() => null);
2348
+ // A retirement whose acknowledgement says no processor request started: the thrown error
2349
+ // says so. The API echoes the reason the row carries, so a row retired earlier as
2350
+ // merchant_never_retried can answer that a processor request did start; then nothing changes.
2351
+ if (retired?.processor_request_started === false && failure instanceof PaymentOutcomeUnknownError)
2352
+ failure.processorRequestStarted = false;
2275
2353
  }
2276
2354
  }
2277
2355
  }
@@ -2632,9 +2710,11 @@ export class VaultClient {
2632
2710
  * approves every such payment); the agent's amount, the only authority here; a
2633
2711
  * session the API and the device would take (payment-core
2634
2712
  * merchantCardEndpointSessionRefusal: the profile's URL, headers, cookies, Referer and
2635
- * body, paused on its checkout page); and the profile's status last, so that refusal
2636
- * means every other check passed. Nothing it throws names the URL, the body or a
2637
- * session value.
2713
+ * body, paused on its checkout page); for a merchant that keeps its own record of the
2714
+ * order, that record, held to the agent's amount and currency, and refused for an order
2715
+ * that renews or a checkout in test mode (admitCardEndpointFacts); and the profile's
2716
+ * status last, so that refusal means every other check passed. Nothing it throws names
2717
+ * the URL, the body or a session value.
2638
2718
  */
2639
2719
  function admitCardEndpoint(input) {
2640
2720
  const named = input.request.merchant_card_endpoint;
@@ -2657,9 +2737,45 @@ function admitCardEndpoint(input) {
2657
2737
  const refusal = cardEndpointSessionRefusal(profile, session);
2658
2738
  if (refusal)
2659
2739
  throw new CardEndpointRefusedError(profile.id, 'request_unsupported', refusal);
2740
+ const facts = admitCardEndpointFacts(profile, session, named.facts, input);
2660
2741
  if (!cardEndpointLive(profile))
2661
2742
  throw new CardEndpointRefusedError(profile.id, 'not_enabled');
2662
- return { profile: profile.id, session };
2743
+ return { profile: profile.id, session, ...(facts ? { facts } : {}) };
2744
+ }
2745
+ /**
2746
+ * The merchant's own record of the order, for a profile whose merchant keeps one
2747
+ * (profile.facts), read with the rules the API reads it with (payment-core
2748
+ * readMerchantCardEndpointFacts and merchantCardEndpointFactsRefusal): required, bound
2749
+ * to this checkout's session, in the reviewed layout; a total or currency that differs
2750
+ * from the agent's is an AmountMismatchError against the merchant's total, and a
2751
+ * renewing order or a checkout in test mode is refused. Card endpoint payments are live
2752
+ * only, so the record must say the checkout is live. Undefined for any other profile,
2753
+ * which sends none.
2754
+ */
2755
+ function admitCardEndpointFacts(profile, session, sent, input) {
2756
+ if (!profile.facts) {
2757
+ if (sent !== undefined)
2758
+ throw new CardEndpointRefusedError(profile.id, 'request_unsupported', 'the request carries an order record this merchant has none of');
2759
+ return undefined;
2760
+ }
2761
+ if (sent === undefined)
2762
+ throw new CardEndpointRefusedError(profile.id, 'facts_unavailable', `${profile.merchant}'s own record of this order was not read`);
2763
+ const read = sharedReadFacts(profile.id, session.url, sent);
2764
+ if (!read.ok)
2765
+ throw new CardEndpointRefusedError(profile.id, 'facts_unavailable', read.refusal);
2766
+ const currency = String(input.currency).toLowerCase();
2767
+ const refusal = sharedFactsRefusal(profile.id, read.facts, { amountCents: typeof input.amount === 'number' ? input.amount : Number.NaN, currency, live: true });
2768
+ if (refusal?.code === 'amount' || refusal?.code === 'currency') {
2769
+ // An integer amount is compared here; a decimal string is left to the API, which
2770
+ // reads it in the currency's units and holds it to the same total.
2771
+ if (typeof input.amount === 'number') {
2772
+ throw new AmountMismatchError(null, input.amount, read.facts.amount.amountCents, currency, read.facts.amount.currency, 'create', 'merchant_total');
2773
+ }
2774
+ }
2775
+ else if (refusal) {
2776
+ throw new CardEndpointRefusedError(profile.id, 'facts_refused', refusal.reason);
2777
+ }
2778
+ return sent;
2663
2779
  }
2664
2780
  /**
2665
2781
  * The merchant's answer to the device's card request, as the API serves it once
@@ -2682,8 +2798,9 @@ function cardEndpointReplay(s, authorizationId, profile, metadata) {
2682
2798
  throw unknown('merchant_answer_malformed');
2683
2799
  const base = { mode: CARD_ENDPOINT_MODE, authorizationId, profile, status: answer.status, ...metadata };
2684
2800
  if (answer.withheld !== undefined) {
2685
- if (answer.withheld !== 'card_in_response' && answer.withheld !== 'unreadable_in_response')
2801
+ if (answer.withheld !== 'card_in_response' && answer.withheld !== 'unreadable_in_response' && answer.withheld !== 'refused_step_in_response') {
2686
2802
  throw unknown('merchant_answer_malformed');
2803
+ }
2687
2804
  return { ...base, headers: {}, setCookies: [], body: '', withheld: answer.withheld };
2688
2805
  }
2689
2806
  const headers = answer.headers;
@@ -54,7 +54,7 @@ declare function parseCreditguardTransaction(body: any, txId: any): {
54
54
  personalIdAlwaysEnabled: boolean;
55
55
  inputs: any;
56
56
  } | null;
57
- declare function creditguardTransactionRefusal(txn: any, expectation: any): "otp" | "keep_on_file" | "dcc" | "installments" | "personal_id" | "jwe_unsupported" | "amount_mismatch" | "currency_mismatch" | "transaction_unreadable" | "three_d_secure" | "transaction_type" | "form_inputs" | "card_field_hidden" | "no_retries_left" | "environment_mismatch" | "jwe_public_key_malformed" | null;
57
+ declare function creditguardTransactionRefusal(txn: any, expectation: any): "otp" | "keep_on_file" | "dcc" | "installments" | "personal_id" | "jwe_unsupported" | "amount_mismatch" | "environment_mismatch" | "currency_mismatch" | "transaction_unreadable" | "three_d_secure" | "transaction_type" | "form_inputs" | "card_field_hidden" | "no_retries_left" | "jwe_public_key_malformed" | null;
58
58
  declare function creditguardSendWindowRefusal(timing: any): "timing_unknown" | "page_expired" | "recaptcha_expired" | null;
59
59
  declare var CREDITGUARD_REFUSAL_CODES: readonly string[];
60
60
  declare function creditguardResponseOutcome(status: any, body: any, options: any): {
@@ -158,6 +158,8 @@ export declare class MicroformFrames {
158
158
  /** A frame's committed document URL, fragment included. A URL that is not a field iframe's forgets the frame. */
159
159
  observe(frameId: unknown, url: unknown): void;
160
160
  forget(frameId: unknown): void;
161
+ /** The card Microform whose field documents this attachment recorded, for prepare({ tokenizeBeforePay: true }). */
162
+ loadedCard(): LoadedCardMicroform;
161
163
  /**
162
164
  * Judge a POST the frame `frameId` sent. When that frame is not on record, `reread` reads the
163
165
  * pausing session's frame tree once (recording what it finds) and resolves to the frame's
@@ -173,6 +175,34 @@ export declare class MicroformFrames {
173
175
  * request's frame could not be read at all.
174
176
  */
175
177
  export declare function judgeMicroformFrameUrls(request: FlexRequest, issuingUrl: string | undefined, frameUrls: Iterable<string | undefined>, contexts: CaptureContexts): CybersourceFlexDecision | Promise<CybersourceFlexDecision>;
178
+ /**
179
+ * What prepare({ tokenizeBeforePay: true }) reads from the page before Pay: the one card Microform
180
+ * whose fields loaded, its capture context and its fields, or why there is none to read.
181
+ */
182
+ export type LoadedCardMicroform = {
183
+ ok: true;
184
+ captureContext: string;
185
+ environment: string;
186
+ microform: {
187
+ version: string;
188
+ fields: MicroformField[];
189
+ };
190
+ } | {
191
+ ok: false;
192
+ reason: 'microform_not_loaded' | 'microform_ambiguous' | 'microform_unsupported';
193
+ };
194
+ /**
195
+ * The card Microform a page loaded, for prepare({ tokenizeBeforePay: true }): every committed card
196
+ * field-iframe document, grouped by capture context, microform id, version and host. One group of a
197
+ * reviewed version, with a card number field and at most a security code field, is the checkout the
198
+ * cardholder's device tokenizes before Pay. No group is `microform_not_loaded` (prepare once the card
199
+ * fields show), two or more are `microform_ambiguous`, and any other group is `microform_unsupported`.
200
+ * A check (eCheck) Microform carries no card and is not counted. Reads the frames only; the capture
201
+ * context is verified by prepareCheckout, the API and the device.
202
+ */
203
+ export declare function loadedCardMicroform(frames: Iterable<MicroformFrame>): LoadedCardMicroform;
204
+ /** The Playwright adapter's read: every frame URL on the page (Playwright keeps the fragment in frame.url()). */
205
+ export declare function loadedCardMicroformFromUrls(urls: Iterable<string | undefined>): LoadedCardMicroform;
176
206
  /**
177
207
  * Whether a create's sidecar has the shape the API admits (routes/api/v2/checkout.ts
178
208
  * createSchema): a capture context of 1 to 16384 characters, and a Microform version with a
@@ -89,4 +89,30 @@ declare function transientTokenFacts(status: any, body: any, verified: any, card
89
89
  }>;
90
90
  declare function cybersourceFlexPreparationUrl(environment: any): any;
91
91
  declare function isPreparedCybersourceFlexRequest(url: any, method: any, body: any, environment: any, headers: any): boolean;
92
- export { CYBERSOURCE_FLEX_CAPTURE_CONTEXT_MAX_LENGTH, CYBERSOURCE_FLEX_CONTENT_TYPE, CYBERSOURCE_FLEX_PSP, CYBERSOURCE_FLEX_REFUSAL_REASONS, CYBERSOURCE_FLEX_REVIEWED_IFRAME_VERSIONS, CYBERSOURCE_FLEX_RULES_SOURCE, CYBERSOURCE_FLEX_SIGNING_KEYS, CYBERSOURCE_FLEX_TEST_PANS, CYBERSOURCE_FLEX_TOKENS_URLS, CybersourceFlexError, assertCybersourceFlexTemplate, buildCardPlaintext, cybersourceFlexEnvironment, flexHostEnvironment as cybersourceFlexHostEnvironment, cybersourceFlexPreparationUrl, cybersourceFlexRefusalCode, cybersourceFlexTemplateKid, detectCardTypes, isCybersourceFlexMediaType, isCybersourceFlexRequest, isCybersourceFlexTemplateBody, isCybersourceFlexTokenShape, isPreparedCybersourceFlexRequest, isVerifiedCaptureContext, parseMicroformFragment, sealForContext, transientTokenFacts, transientTokenSigned, verifyCaptureContext };
92
+ declare var CYBERSOURCE_FLEX_PRESEAL_TRIGGER: string;
93
+ declare function admitCybersourceFlexPreparation(prepared: any, { crypto, now }?: {}): Promise<Readonly<{
94
+ context: Readonly<{
95
+ raw: any;
96
+ signingKid: any;
97
+ environment: any;
98
+ origin: string;
99
+ path: string;
100
+ url: string;
101
+ jwk: Readonly<{
102
+ kty: "RSA";
103
+ n: any;
104
+ e: any;
105
+ kid: any;
106
+ }>;
107
+ modulusBytes: number;
108
+ index: any;
109
+ contextType: any;
110
+ allowedCardNetworks: readonly any[] | null;
111
+ targetOrigins: readonly any[];
112
+ iat: any;
113
+ exp: any;
114
+ }>;
115
+ fields: readonly string[];
116
+ }>>;
117
+ declare function sameCybersourceFlexPreparation(prepared: any, request: any): boolean;
118
+ export { CYBERSOURCE_FLEX_CAPTURE_CONTEXT_MAX_LENGTH, CYBERSOURCE_FLEX_CONTENT_TYPE, CYBERSOURCE_FLEX_PRESEAL_TRIGGER, CYBERSOURCE_FLEX_PSP, CYBERSOURCE_FLEX_REFUSAL_REASONS, CYBERSOURCE_FLEX_REVIEWED_IFRAME_VERSIONS, CYBERSOURCE_FLEX_RULES_SOURCE, CYBERSOURCE_FLEX_SIGNING_KEYS, CYBERSOURCE_FLEX_TEST_PANS, CYBERSOURCE_FLEX_TOKENS_URLS, CybersourceFlexError, admitCybersourceFlexPreparation, assertCybersourceFlexTemplate, buildCardPlaintext, cybersourceFlexEnvironment, flexHostEnvironment as cybersourceFlexHostEnvironment, cybersourceFlexPreparationUrl, cybersourceFlexRefusalCode, cybersourceFlexTemplateKid, detectCardTypes, isCybersourceFlexMediaType, isCybersourceFlexRequest, isCybersourceFlexTemplateBody, isCybersourceFlexTokenShape, isPreparedCybersourceFlexRequest, isVerifiedCaptureContext, parseMicroformFragment, sameCybersourceFlexPreparation, sealForContext, transientTokenFacts, transientTokenSigned, verifyCaptureContext };
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: a2260c2d905a5cd4ef148ab8a79e90fb2f9f6790d9efba06e95ed35f8bea2755
3
+ // artifact-sha256: a2baaac410b5aaa629b3d5031136f88645c7b45c67c3bbc154cad72de216a04e
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 = [
@@ -686,4 +686,44 @@ function iframeHeaders(headers) {
686
686
  function isPreparedCybersourceFlexRequest(url, method, body, environment, headers) {
687
687
  return (environment === "production" || environment === "sandbox") && method === "POST" && cybersourceFlexEnvironment(url) === environment && iframeHeaders(headers) && isCybersourceFlexTemplateBody(body);
688
688
  }
689
- export { CYBERSOURCE_FLEX_CAPTURE_CONTEXT_MAX_LENGTH, CYBERSOURCE_FLEX_CONTENT_TYPE, CYBERSOURCE_FLEX_PSP, CYBERSOURCE_FLEX_REFUSAL_REASONS, CYBERSOURCE_FLEX_REVIEWED_IFRAME_VERSIONS, CYBERSOURCE_FLEX_RULES_SOURCE, CYBERSOURCE_FLEX_SIGNING_KEYS, CYBERSOURCE_FLEX_TEST_PANS, CYBERSOURCE_FLEX_TOKENS_URLS, CybersourceFlexError, assertCybersourceFlexTemplate, buildCardPlaintext, cybersourceFlexEnvironment, flexHostEnvironment as cybersourceFlexHostEnvironment, cybersourceFlexPreparationUrl, cybersourceFlexRefusalCode, cybersourceFlexTemplateKid, detectCardTypes, isCybersourceFlexMediaType, isCybersourceFlexRequest, isCybersourceFlexTemplateBody, isCybersourceFlexTokenShape, isPreparedCybersourceFlexRequest, isVerifiedCaptureContext, parseMicroformFragment, sealForContext, transientTokenFacts, transientTokenSigned, verifyCaptureContext };
689
+ var CYBERSOURCE_FLEX_PRESEAL_TRIGGER = "number";
690
+ var isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
691
+ function preparedLayout(microform) {
692
+ if (!isRecord(microform) || typeof microform.version !== "string" || !CYBERSOURCE_FLEX_REVIEWED_IFRAME_VERSIONS.includes(microform.version)) {
693
+ refuse("microform_version", "this payment's card field is not a Microform version this device can rebuild.");
694
+ }
695
+ if (Object.keys(microform).some((key) => key !== "version" && key !== "fields")) {
696
+ refuse("fields", "the Microform fields this payment loaded are not a card number field and an optional security code field.");
697
+ }
698
+ fieldSet(microform.fields, CYBERSOURCE_FLEX_PRESEAL_TRIGGER);
699
+ return Object.freeze(FIELD_TYPES.filter((field) => microform.fields.includes(field)));
700
+ }
701
+ async function admitCybersourceFlexPreparation(prepared, { crypto, now } = {}) {
702
+ if (!isRecord(prepared))
703
+ refuse("request", "malformed checkout preparation.");
704
+ const fields = preparedLayout(prepared.microform);
705
+ const { environment } = prepared;
706
+ if (environment !== "production" && environment !== "sandbox") {
707
+ refuse("environment_mismatch", "the preparation names no CyberSource environment.");
708
+ }
709
+ const context = await verifyCaptureContext(prepared.capture_context, { crypto, now });
710
+ if (context.environment !== environment || context.url !== CYBERSOURCE_FLEX_TOKENS_URLS[environment]) {
711
+ refuse("environment_mismatch", "the CyberSource test and live environments are mixed in this payment.");
712
+ }
713
+ return Object.freeze({ context, fields });
714
+ }
715
+ function sameCybersourceFlexPreparation(prepared, request) {
716
+ if (!isRecord(prepared) || !isRecord(request) || !isRecord(prepared.microform) || !isRecord(request.microform))
717
+ return false;
718
+ const context = prepared.capture_context;
719
+ if (typeof context !== "string" || context.length < 1 || context.length > CAPTURE_CONTEXT_MAX_LENGTH || request.capture_context !== context)
720
+ return false;
721
+ const one = prepared.microform;
722
+ const two = request.microform;
723
+ if (typeof one.version !== "string" || one.version !== two.version || !Array.isArray(one.fields) || !Array.isArray(two.fields))
724
+ return false;
725
+ const mine = new Set(one.fields);
726
+ const theirs = new Set(two.fields);
727
+ return mine.size === one.fields.length && theirs.size === two.fields.length && mine.size === theirs.size && [...mine].every((field) => theirs.has(field));
728
+ }
729
+ export { CYBERSOURCE_FLEX_CAPTURE_CONTEXT_MAX_LENGTH, CYBERSOURCE_FLEX_CONTENT_TYPE, CYBERSOURCE_FLEX_PRESEAL_TRIGGER, CYBERSOURCE_FLEX_PSP, CYBERSOURCE_FLEX_REFUSAL_REASONS, CYBERSOURCE_FLEX_REVIEWED_IFRAME_VERSIONS, CYBERSOURCE_FLEX_RULES_SOURCE, CYBERSOURCE_FLEX_SIGNING_KEYS, CYBERSOURCE_FLEX_TEST_PANS, CYBERSOURCE_FLEX_TOKENS_URLS, CybersourceFlexError, admitCybersourceFlexPreparation, assertCybersourceFlexTemplate, buildCardPlaintext, cybersourceFlexEnvironment, flexHostEnvironment as cybersourceFlexHostEnvironment, cybersourceFlexPreparationUrl, cybersourceFlexRefusalCode, cybersourceFlexTemplateKid, detectCardTypes, isCybersourceFlexMediaType, isCybersourceFlexRequest, isCybersourceFlexTemplateBody, isCybersourceFlexTokenShape, isPreparedCybersourceFlexRequest, isVerifiedCaptureContext, parseMicroformFragment, sameCybersourceFlexPreparation, sealForContext, transientTokenFacts, transientTokenSigned, verifyCaptureContext };
@@ -222,6 +222,10 @@ export class MicroformFrames {
222
222
  if (typeof frameId === 'string')
223
223
  this.frames.delete(frameId);
224
224
  }
225
+ /** The card Microform whose field documents this attachment recorded, for prepare({ tokenizeBeforePay: true }). */
226
+ loadedCard() {
227
+ return loadedCardMicroform(this.frames.values());
228
+ }
225
229
  /**
226
230
  * Judge a POST the frame `frameId` sent. When that frame is not on record, `reread` reads the
227
231
  * pausing session's frame tree once (recording what it finds) and resolves to the frame's
@@ -251,6 +255,48 @@ export function judgeMicroformFrameUrls(request, issuingUrl, frameUrls, contexts
251
255
  }
252
256
  return judgeMicroformRequest(request, issuingUrl === undefined ? undefined : frameOf(issuingUrl), frames, contexts);
253
257
  }
258
+ /**
259
+ * The card Microform a page loaded, for prepare({ tokenizeBeforePay: true }): every committed card
260
+ * field-iframe document, grouped by capture context, microform id, version and host. One group of a
261
+ * reviewed version, with a card number field and at most a security code field, is the checkout the
262
+ * cardholder's device tokenizes before Pay. No group is `microform_not_loaded` (prepare once the card
263
+ * fields show), two or more are `microform_ambiguous`, and any other group is `microform_unsupported`.
264
+ * A check (eCheck) Microform carries no card and is not counted. Reads the frames only; the capture
265
+ * context is verified by prepareCheckout, the API and the device.
266
+ */
267
+ export function loadedCardMicroform(frames) {
268
+ const groups = new Map();
269
+ for (const frame of frames) {
270
+ if (frame.microformType !== 'card')
271
+ continue;
272
+ const key = JSON.stringify([frame.captureContext, frame.microformId, frame.version, frame.environment]);
273
+ const group = groups.get(key);
274
+ if (group)
275
+ group.types.add(frame.fieldType);
276
+ else
277
+ groups.set(key, { frame, types: new Set([frame.fieldType]) });
278
+ }
279
+ if (groups.size === 0)
280
+ return { ok: false, reason: 'microform_not_loaded' };
281
+ if (groups.size > 1)
282
+ return { ok: false, reason: 'microform_ambiguous' };
283
+ const [{ frame, types }] = [...groups.values()];
284
+ if (!frame.reviewed || !types.has('number') || [...types].some((type) => !MICROFORM_FIELDS.includes(type))) {
285
+ return { ok: false, reason: 'microform_unsupported' };
286
+ }
287
+ return { ok: true, captureContext: frame.captureContext, environment: frame.environment,
288
+ microform: { version: frame.version, fields: MICROFORM_FIELDS.filter((field) => types.has(field)) } };
289
+ }
290
+ /** The Playwright adapter's read: every frame URL on the page (Playwright keeps the fragment in frame.url()). */
291
+ export function loadedCardMicroformFromUrls(urls) {
292
+ const frames = [];
293
+ for (const url of urls) {
294
+ const frame = frameOf(url);
295
+ if (frame)
296
+ frames.push(frame);
297
+ }
298
+ return loadedCardMicroform(frames);
299
+ }
254
300
  /**
255
301
  * Whether a create's sidecar has the shape the API admits (routes/api/v2/checkout.ts
256
302
  * createSchema): a capture context of 1 to 16384 characters, and a Microform version with a