@agent-cards/checkout 0.18.0 → 0.19.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/PREFLIGHT.md +4 -0
  3. package/README.md +91 -7
  4. package/dist/adyen-merchant-hosted.generated.d.ts +277 -0
  5. package/dist/adyen-merchant-hosted.generated.js +1902 -0
  6. package/dist/builtin-registry.generated.js +1 -1
  7. package/dist/cdp.d.ts +4 -1
  8. package/dist/cdp.js +416 -217
  9. package/dist/client.d.ts +236 -5
  10. package/dist/client.js +514 -11
  11. package/dist/cse-body.d.ts +25 -0
  12. package/dist/cse-body.js +41 -0
  13. package/dist/fiserv.d.ts +65 -0
  14. package/dist/fiserv.generated.d.ts +73 -0
  15. package/dist/fiserv.generated.js +830 -0
  16. package/dist/fiserv.js +104 -0
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.js +5 -1
  19. package/dist/lifecycle.d.ts +15 -1
  20. package/dist/lifecycle.js +28 -3
  21. package/dist/merchant-handoff.d.ts +54 -0
  22. package/dist/merchant-handoff.js +100 -0
  23. package/dist/merchant-hosted.d.ts +140 -0
  24. package/dist/merchant-hosted.js +170 -0
  25. package/dist/merchant-total-watch.d.ts +115 -0
  26. package/dist/merchant-total-watch.js +268 -0
  27. package/dist/merchant-total.d.ts +257 -0
  28. package/dist/merchant-total.js +383 -0
  29. package/dist/pre-claim.d.ts +123 -0
  30. package/dist/pre-claim.js +386 -0
  31. package/dist/preflight-catalog.json +132 -0
  32. package/dist/preflight-schemas.json +14 -2
  33. package/dist/preflight.generated.js +15 -1
  34. package/dist/preparation.d.ts +7 -0
  35. package/dist/preparation.js +33 -6
  36. package/dist/prepared-processor.d.ts +36 -3
  37. package/dist/prepared-processor.js +53 -3
  38. package/dist/registry.d.ts +45 -0
  39. package/dist/registry.js +14 -0
  40. package/dist/stripe-checkout.generated.js +96 -7
  41. package/dist/substitutions.generated.d.ts +2 -1
  42. package/dist/substitutions.generated.js +758 -6
  43. package/examples/preflight/kernel-native/inventory.json +1 -1
  44. package/package.json +3 -3
package/dist/fiserv.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Fiserv Commerce Hub card capture, as this SDK reads it.
3
+ *
4
+ * Every rule lives in the internal payment core (payment-core src/fiserv.js), vendored
5
+ * here as fiserv.generated.ts so the published package installs nothing else; the
6
+ * backend and the Vault load the same rules. That artifact is untyped JavaScript, so
7
+ * this module gives the adapters, the client and callers typed names for the parts the
8
+ * SDK uses, the way substitute.ts types the shared substitution. It adds no rule.
9
+ *
10
+ * The request is the Secure Data Capture frame's POST to Commerce Hub's card-capture
11
+ * endpoint. The frame encrypts the card the agent typed and posts the envelope as
12
+ * source.encryptionData; the cardholder's device builds the same envelope around the
13
+ * approved card under a key Agentcard pinned for the merchant, and the paused capture
14
+ * continues from the agent's browser with the envelope's four members swapped.
15
+ */
16
+ import * as generated from './fiserv.generated.js';
17
+ const core = generated;
18
+ /** The card-capture endpoint per environment, exactly as the Secure Data Capture frame writes it. */
19
+ export const FISERV_CARD_CAPTURE_ENDPOINTS = core.FISERV_CARD_CAPTURE_ENDPOINTS;
20
+ const CAPTURE_HOSTS = new Set(Object.values(core.FISERV_CARD_CAPTURE_ENDPOINTS).map((endpoint) => new URL(endpoint).hostname));
21
+ /**
22
+ * Whether `url` is on one of the two card-capture hosts (connect.fiservapis.com and
23
+ * connect-cert.fiservapis.com), whatever its path or query. The registry's Fiserv
24
+ * recognizer lists exactly these hosts, so a card request on one of them is Fiserv's.
25
+ */
26
+ export function isFiservCaptureHost(url) {
27
+ try {
28
+ return CAPTURE_HOSTS.has(new URL(url).hostname);
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ /** The pinned key with this id, in either environment, or null. */
35
+ export function fiservKeyPinById(id) {
36
+ return typeof id === 'string' ? core.FISERV_KEY_PINS.find((pin) => pin.id === id) ?? null : null;
37
+ }
38
+ /**
39
+ * The id of the pin a paused capture's card may be sealed under (its envelope's key,
40
+ * pinned for its endpoint's environment and for the merchant its body names), or null.
41
+ */
42
+ export function fiservCapturePinId(url, body) {
43
+ return core.fiservCapturePin({ url, body })?.id ?? null;
44
+ }
45
+ /** The card-capture endpoint a Fiserv preparation names for its environment. */
46
+ export function fiservPreparationUrl(environment) {
47
+ return core.fiservPreparationUrl(environment);
48
+ }
49
+ /**
50
+ * Whether the SDK pauses this capture body for the cardholder, or refuses it when no
51
+ * preparation is ready: every body except the captures that carry no Vault card.
52
+ * Fastlane (PaymentToken), ACH (PaymentCheck), gift and EBT captures share the endpoint
53
+ * and continue untouched, each only in its exact source shape. A body not proven
54
+ * card-free (one the adapter could not read, one over 64 KiB, one that is not JSON, a
55
+ * PaymentCard with a plaintext card, a non-card type or category that also carries a card
56
+ * member) is judged as a card capture, so the agent's placeholder never continues to
57
+ * Fiserv on it.
58
+ */
59
+ export function isFiservPausableBody(body) {
60
+ return core.isFiservPausableBody(body);
61
+ }
62
+ /**
63
+ * Whether this build pays the request only through a prepared checkout because it is
64
+ * Fiserv's: a recognizer named fiserv, or a request on one of Fiserv's capture hosts.
65
+ * The served entry says so too (preparationRequired); this build carries Fiserv's own
66
+ * rules, so it requires the preparation whatever the served registry says, and a
67
+ * registry that dropped the flag never opens an ordinary Fiserv authorization.
68
+ */
69
+ export function requiresFiservPreparation(psp, url) {
70
+ return psp === 'fiserv' || isFiservCaptureHost(url);
71
+ }
72
+ /**
73
+ * Whether a paused request is the capture a Fiserv preparation for `environment`
74
+ * consents to: that environment's endpoint exactly, the capture rules on the whole
75
+ * request, headers included, and an envelope under a key pinned for that environment
76
+ * and for the merchant the body names.
77
+ */
78
+ export function isPreparedFiservRequest(url, method, body, environment, headers) {
79
+ return core.isPreparedFiservRequest(url, method, body, environment, headers);
80
+ }
81
+ /**
82
+ * Whether an approval's substitution is exactly Fiserv's envelope: `encoding` json,
83
+ * `at` source.encryptionData, the four members keyId, encryptedKey, encryptionBlock and
84
+ * aesIv in their exact shape under a pinned key, and nothing removed.
85
+ */
86
+ export function isFiservSubstitution(sub) {
87
+ if (!sub || typeof sub !== 'object' || Array.isArray(sub))
88
+ return false;
89
+ const value = sub;
90
+ return Object.keys(value).every((name) => ['encoding', 'at', 'fields', 'remove'].includes(name))
91
+ && value.encoding === 'json' && value.at === core.FISERV_SUBSTITUTION.at
92
+ && (value.remove === undefined || (Array.isArray(value.remove) && value.remove.length === 0))
93
+ && core.fiservSubstitutionFieldsRefusal(value.fields) === null;
94
+ }
95
+ /**
96
+ * The paused Fiserv capture with the approval's envelope written over the four members
97
+ * its source.encryptionData already carries, and every other byte the frame's own. It
98
+ * throws, writing nothing, for a body that is not the capture Agentcard covers, for any
99
+ * substitution but Fiserv's envelope, and for an envelope under another key than the one
100
+ * the paused capture names. Run this on a Fiserv approval, never substituteEncryptedFields.
101
+ */
102
+ export function substituteFiservEnvelope(body, sub) {
103
+ return core.substituteFiservEnvelope(body, sub);
104
+ }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,16 @@
1
- export { VaultClient, CardEncryptedError, UnsupportedModeError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, } from './client.js';
2
- export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, HostedFormReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, RazorpayProcessorError, RuntimeCancelReason, ExecutionMetadata, } from './client.js';
1
+ export { VaultClient, CardEncryptedError, UnsupportedModeError, PreparationRequiredError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, AdyenTestPlatformRefusedError, MerchantTotalError, } from './client.js';
2
+ export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, MerchantHostedReplay, HostedFormReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, AdyenTestPlatformRefusal, MerchantTotalRefusal, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, RazorpayProcessorError, RuntimeCancelReason, ExecutionMetadata, } from './client.js';
3
3
  export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
4
4
  export { CheckoutAttachmentError } from './attachment.js';
5
5
  export type { CdpLike, AttachOptions, CorsOutcome } from './cdp.js';
6
6
  export type { StripeCheckoutOptions, StripeCheckoutBlockedDetail, StripeCheckoutBlockStage, StripeCheckoutBlockReason } from './stripe-checkout.js';
7
7
  export { substituteEncryptedFields, SubstitutionError } from './substitute.js';
8
8
  export type { Substitutions } from './substitute.js';
9
+ export { classifyDeclaredMerchantRequest, classifyMerchantRequest, substituteMerchantHostedBody } from './merchant-hosted.js';
10
+ export type { MerchantProfile, MerchantProfileStatus, MerchantRequestVerdict, MerchantHostedSubstitutions, MerchantHostedFact, JsonPathStep, SandboxMerchantDeclaration } from './merchant-hosted.js';
11
+ export { MerchantExchangeLog, merchantAmountExchanges, merchantAmountFromResponses, merchantChargeReport, merchantProfilePricedBySource, merchantTotalAtRelease, MERCHANT_EXCHANGES_MAX, } from './merchant-total.js';
12
+ export type { MerchantAmountCard, MerchantExchange, MerchantRequestSeen, MerchantTotalApproved, MerchantTotalCapture, MerchantTotalScope } from './merchant-total.js';
13
+ export { substituteFiservEnvelope } from './fiserv.js';
9
14
  export { hostedFormSubmittedPage, HOSTED_FORM_SUBMITTED_OUTCOME } from './hosted-form.js';
10
15
  export type { HostedFormSubmittedPageInput, SyntheticPage } from './hosted-form.js';
11
16
  export { BUILTIN_REGISTRY, cardUrlPatterns, findRecognizer } from './registry.js';
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
- export { VaultClient, CardEncryptedError, UnsupportedModeError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, } from './client.js';
1
+ export { VaultClient, CardEncryptedError, UnsupportedModeError, PreparationRequiredError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, AdyenTestPlatformRefusedError, MerchantTotalError, } from './client.js';
2
2
  export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
3
3
  export { CheckoutAttachmentError } from './attachment.js';
4
4
  export { substituteEncryptedFields, SubstitutionError } from './substitute.js';
5
+ export { classifyDeclaredMerchantRequest, classifyMerchantRequest, substituteMerchantHostedBody } from './merchant-hosted.js';
6
+ // The merchant's own total for a raw CDP runtime that intercepts on its own (merchant-total.ts).
7
+ export { MerchantExchangeLog, merchantAmountExchanges, merchantAmountFromResponses, merchantChargeReport, merchantProfilePricedBySource, merchantTotalAtRelease, MERCHANT_EXCHANGES_MAX, } from './merchant-total.js';
8
+ export { substituteFiservEnvelope } from './fiserv.js';
5
9
  export { hostedFormSubmittedPage, HOSTED_FORM_SUBMITTED_OUTCOME } from './hosted-form.js';
6
10
  export { BUILTIN_REGISTRY, cardUrlPatterns, findRecognizer } from './registry.js';
@@ -124,7 +124,21 @@ export declare class CheckoutLifecycle implements CheckoutController {
124
124
  * is reported through failed() instead, which holds.
125
125
  */
126
126
  merchantNeverRetried(authorizationId: string | null): void;
127
- unsupported(): void;
127
+ /**
128
+ * A card request this SDK cannot pay was aborted. `reason`: an unrecognized
129
+ * declared payment endpoint (the default), a merchant profile Agentcard has not
130
+ * turned on (merchant_profile_observe), or a card request the profile cannot
131
+ * finish (merchant_request_unsupported).
132
+ */
133
+ unsupported(reason?: 'unrecognized_payment_endpoint' | 'merchant_profile_observe' | 'merchant_request_unsupported'): void;
134
+ /**
135
+ * A card request that only a prepared checkout pays (an Adyen merchant
136
+ * profile's, or a Fiserv card capture) arrived with no preparation on this
137
+ * attachment and was aborted before any pause, prompt or authorization.
138
+ * Nothing is outstanding, so nothing is held; prepare() stays refused here, as
139
+ * after any first card request.
140
+ */
141
+ preparationRequired(): void;
128
142
  prepareHandoff(replay: ReplayResponse, requestUrl: string): void;
129
143
  /**
130
144
  * The approval a page's card-free Stripe confirmation may continue, or null.
package/dist/lifecycle.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ApprovalDeclinedError, ApprovalTimeoutError, CheckoutCancelledError, CheckoutPreparationError, IntentNotConfirmableError, PaymentOutcomeUnknownError, ProcessorRefusedError } from './client.js';
1
+ import { AdyenTestPlatformRefusedError, ApprovalDeclinedError, ApprovalTimeoutError, CheckoutCancelledError, CheckoutPreparationError, IntentNotConfirmableError, PaymentOutcomeUnknownError, ProcessorRefusedError } from './client.js';
2
2
  import { parseOwnedShopReceipt, OWNED_SHOP_SKU, OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY } from './owned-shop.generated.js';
3
3
  /** How many times one approval's card-free Stripe confirmation may be checked before it is simply held. */
4
4
  export const MAX_STRIPE_CONTINUATION_CHECKS = 3;
@@ -119,11 +119,29 @@ export class CheckoutLifecycle {
119
119
  return;
120
120
  this.set({ ...this.state, authorizationId, status: 'declined', reason: 'merchant_never_retried' });
121
121
  }
122
- unsupported() {
122
+ /**
123
+ * A card request this SDK cannot pay was aborted. `reason`: an unrecognized
124
+ * declared payment endpoint (the default), a merchant profile Agentcard has not
125
+ * turned on (merchant_profile_observe), or a card request the profile cannot
126
+ * finish (merchant_request_unsupported).
127
+ */
128
+ unsupported(reason = 'unrecognized_payment_endpoint') {
123
129
  this.held = true;
124
130
  if (this.state.authorizationId)
125
131
  return; // do not hide an outstanding/completed payment behind a later unsupported request
126
- this.set({ ...this.state, status: 'unsupported', reason: 'unrecognized_payment_endpoint' });
132
+ this.set({ ...this.state, status: 'unsupported', reason });
133
+ }
134
+ /**
135
+ * A card request that only a prepared checkout pays (an Adyen merchant
136
+ * profile's, or a Fiserv card capture) arrived with no preparation on this
137
+ * attachment and was aborted before any pause, prompt or authorization.
138
+ * Nothing is outstanding, so nothing is held; prepare() stays refused here, as
139
+ * after any first card request.
140
+ */
141
+ preparationRequired() {
142
+ if (this.cancelled || this.state.authorizationId)
143
+ return;
144
+ this.set({ ...this.state, status: 'failed', reason: 'preparation_required' });
127
145
  }
128
146
  prepareHandoff(replay, requestUrl) {
129
147
  if (replay.mode === 'token' && replay.executionMode === 'autopilot' && replay.shopOrderId) {
@@ -245,6 +263,13 @@ export class CheckoutLifecycle {
245
263
  this.held = true;
246
264
  this.set({ ...this.state, authorizationId, status: 'declined', reason: 'processor_refused' });
247
265
  }
266
+ else if (error instanceof AdyenTestPlatformRefusedError) {
267
+ // Agentcard's own rule, not a person's answer: refused at create before
268
+ // anyone was asked, the checkout failed as it did before the error was
269
+ // typed; declined before the card was sent, the authorization is declined.
270
+ // Either way the reason names the rule.
271
+ this.set({ ...this.state, status: error.stage === 'create' ? 'failed' : 'declined', reason: error.code });
272
+ }
248
273
  else if (error instanceof ApprovalDeclinedError) {
249
274
  this.set({ ...this.state, status: 'declined', reason: 'approval_declined' });
250
275
  }
@@ -0,0 +1,54 @@
1
+ import type { CheckoutLifecycle } from './lifecycle.js';
2
+ /** onEvent is ordinary integrator telemetry; the shape matches AttachOptions.onEvent. */
3
+ type HandoffEvent = {
4
+ type: string;
5
+ detail?: unknown;
6
+ };
7
+ /**
8
+ * A merchant-hosted card request after hand-off, shared by the CDP and
9
+ * Playwright adapters so both read it the same way.
10
+ *
11
+ * The browser continued the merchant's own request with the Vault's ciphertext in
12
+ * it; the merchant's server now charges the card through Adyen and answers its own
13
+ * page. Nothing Agentcard holds says whether it did. Two things after hand-off are
14
+ * therefore an unknown outcome, never a failure to retry:
15
+ * - the continued request answers 5xx or fails in the network: the merchant may
16
+ * have charged the card, and its page may never learn that it did;
17
+ * - the page sends the card request again before an answer arrived (Dick's
18
+ * retries on a network error or a 500, ATG on anything not OK): its retry
19
+ * carries the agent's dummy card, and the approval is spent.
20
+ * Either reports `payment_outcome_unknown` once and holds the checkout until the
21
+ * application reconciles the merchant order. Every card request to the same
22
+ * profile after hand-off is aborted, answered or not: a prepared approval is
23
+ * single use.
24
+ *
25
+ * Which of the two it reports follows the order the page saw them in. A page that
26
+ * retries because the merchant answered 5xx read that answer first, but an
27
+ * adapter can read the retry first (Chrome pauses a request in the browser
28
+ * process, ahead of the renderer's report of the answer), so retried() judges
29
+ * a retry once the adapter has read what the page reported before sending it.
30
+ */
31
+ export declare class MerchantHandoffWatch {
32
+ private readonly lifecycle;
33
+ private readonly onEvent?;
34
+ private handoff;
35
+ private answered;
36
+ private reported;
37
+ constructor(lifecycle: CheckoutLifecycle, onEvent?: ((event: HandoffEvent) => void) | undefined);
38
+ /** Record a hand-off before its continue is sent, so an answer that races the command is still read. */
39
+ begin(profile: string, authorizationId: string, url: string): void;
40
+ /** The continued request's response arrived with this status. */
41
+ responded(status: number): void;
42
+ /** The continued request failed before any response arrived. */
43
+ failed(): void;
44
+ /**
45
+ * The page sent a card request to a profile. True when it is the profile a
46
+ * hand-off already went to: the caller aborts it at once. A retry sent before
47
+ * any answer reached the page also reports the unknown outcome. When the
48
+ * adapter must first read more of what the page reported before it sent this
49
+ * retry, `inPageOrder` keeps `judge`, calls it once it has, and returns true.
50
+ */
51
+ retried(profile: string, inPageOrder?: (judge: () => void) => boolean): boolean;
52
+ private unknown;
53
+ }
54
+ export {};
@@ -0,0 +1,100 @@
1
+ import { PaymentOutcomeUnknownError } from './client.js';
2
+ /**
3
+ * A merchant-hosted card request after hand-off, shared by the CDP and
4
+ * Playwright adapters so both read it the same way.
5
+ *
6
+ * The browser continued the merchant's own request with the Vault's ciphertext in
7
+ * it; the merchant's server now charges the card through Adyen and answers its own
8
+ * page. Nothing Agentcard holds says whether it did. Two things after hand-off are
9
+ * therefore an unknown outcome, never a failure to retry:
10
+ * - the continued request answers 5xx or fails in the network: the merchant may
11
+ * have charged the card, and its page may never learn that it did;
12
+ * - the page sends the card request again before an answer arrived (Dick's
13
+ * retries on a network error or a 500, ATG on anything not OK): its retry
14
+ * carries the agent's dummy card, and the approval is spent.
15
+ * Either reports `payment_outcome_unknown` once and holds the checkout until the
16
+ * application reconciles the merchant order. Every card request to the same
17
+ * profile after hand-off is aborted, answered or not: a prepared approval is
18
+ * single use.
19
+ *
20
+ * Which of the two it reports follows the order the page saw them in. A page that
21
+ * retries because the merchant answered 5xx read that answer first, but an
22
+ * adapter can read the retry first (Chrome pauses a request in the browser
23
+ * process, ahead of the renderer's report of the answer), so retried() judges
24
+ * a retry once the adapter has read what the page reported before sending it.
25
+ */
26
+ export class MerchantHandoffWatch {
27
+ lifecycle;
28
+ onEvent;
29
+ handoff = null;
30
+ answered = false;
31
+ reported = false;
32
+ constructor(lifecycle, onEvent) {
33
+ this.lifecycle = lifecycle;
34
+ this.onEvent = onEvent;
35
+ }
36
+ /** Record a hand-off before its continue is sent, so an answer that races the command is still read. */
37
+ begin(profile, authorizationId, url) {
38
+ this.handoff = { profile, authorizationId, url };
39
+ this.answered = false;
40
+ this.reported = false;
41
+ }
42
+ /** The continued request's response arrived with this status. */
43
+ responded(status) {
44
+ if (!this.handoff || this.answered)
45
+ return;
46
+ this.answered = true;
47
+ if (Number.isInteger(status) && status >= 500)
48
+ this.unknown('merchant_server_error', { status });
49
+ }
50
+ /** The continued request failed before any response arrived. */
51
+ failed() {
52
+ if (!this.handoff || this.answered)
53
+ return;
54
+ this.answered = true;
55
+ this.unknown('merchant_request_failed');
56
+ }
57
+ /**
58
+ * The page sent a card request to a profile. True when it is the profile a
59
+ * hand-off already went to: the caller aborts it at once. A retry sent before
60
+ * any answer reached the page also reports the unknown outcome. When the
61
+ * adapter must first read more of what the page reported before it sent this
62
+ * retry, `inPageOrder` keeps `judge`, calls it once it has, and returns true.
63
+ */
64
+ retried(profile, inPageOrder) {
65
+ const handoff = this.handoff;
66
+ if (!handoff || handoff.profile !== profile)
67
+ return false;
68
+ let judged = false;
69
+ const judge = () => {
70
+ if (judged)
71
+ return;
72
+ judged = true;
73
+ if (this.handoff === handoff && !this.answered)
74
+ this.unknown('merchant_retried_after_handoff');
75
+ this.onEvent?.({ type: 'blocked', detail: 'merchant_retried_after_handoff' });
76
+ };
77
+ let waiting = false;
78
+ // An order the adapter cannot establish is judged on what it has read.
79
+ try {
80
+ waiting = !this.answered && inPageOrder?.(judge) === true;
81
+ }
82
+ catch {
83
+ waiting = false;
84
+ }
85
+ if (!waiting)
86
+ judge();
87
+ return true;
88
+ }
89
+ unknown(reason, extra = {}) {
90
+ if (!this.handoff || this.reported)
91
+ return;
92
+ // A result the application already confirmed with the merchant stands.
93
+ if (this.lifecycle.getState().status === 'completed')
94
+ return;
95
+ this.reported = true;
96
+ const { authorizationId, profile, url } = this.handoff;
97
+ this.lifecycle.failed(new PaymentOutcomeUnknownError(authorizationId, reason), true);
98
+ this.onEvent?.({ type: 'payment_outcome_unknown', detail: { authorizationId, reason, profile, url, ...extra } });
99
+ }
100
+ }
@@ -0,0 +1,140 @@
1
+ /** 'observe': paused, reported and aborted, never paid. 'enabled': a prepared checkout may pay it. */
2
+ export type MerchantProfileStatus = 'observe' | 'enabled';
3
+ /**
4
+ * A test-mode checkout against your own Adyen TEST account: your own server's
5
+ * card endpoint (an exact HTTPS URL), taking a card request shaped like a reviewed
6
+ * profile's, encrypted under your own Adyen TEST client key (test_...). The API
7
+ * accepts one only from a test-mode client of an org Agentcard turned declarations
8
+ * on for, reads the key's public key from Adyen's TEST platform itself, and the
9
+ * Vault encrypts only Adyen's documented test cards under it.
10
+ */
11
+ export interface SandboxMerchantDeclaration {
12
+ /** The reviewed profile whose card request your endpoint takes (its body rules, holder and fields). */
13
+ profile: string;
14
+ /** Your endpoint: HTTPS, written exactly as the browser sends it, no port, credentials or fragment. */
15
+ endpoint: string;
16
+ /** Your Adyen TEST client key (test_ followed by 32 letters and digits). */
17
+ clientKey: string;
18
+ }
19
+ /** A reviewed merchant profile this client arms: its id, the merchant it names, and its effective status. */
20
+ export interface MerchantProfile {
21
+ readonly id: string;
22
+ readonly merchant: string;
23
+ readonly status: MerchantProfileStatus;
24
+ /** Present when this client armed the profile's shape at its own declared test endpoint (declareSandboxMerchant). */
25
+ readonly sandboxDeclaration?: Readonly<Pick<SandboxMerchantDeclaration, 'endpoint' | 'clientKey'>>;
26
+ }
27
+ /** A profile's verdict on a paused request; see classifyMerchantRequest. */
28
+ export type MerchantRequestVerdict = {
29
+ verdict: 'pause';
30
+ } | {
31
+ verdict: 'abort';
32
+ reason: string;
33
+ } | {
34
+ verdict: 'pass';
35
+ };
36
+ /** One step of a JSON path: an object key, or an index into an array. */
37
+ export type JsonPathStep = string | number;
38
+ /** A card fact the API asks to write beside the ciphertext (reserved: no profile declares one yet). */
39
+ export interface MerchantHostedFact {
40
+ path: JsonPathStep[];
41
+ value: string;
42
+ }
43
+ /**
44
+ * Where a merchant-hosted approval's ciphertext goes: the profile's holder path
45
+ * (`[]` is the body root), the merchant's names for Adyen's four fields, and the
46
+ * keys at the holder to drop with the swap (adyen-web's `brand`, derived from the
47
+ * agent's dummy digits). payment-core's substituteMerchantHosted refuses anything
48
+ * but exactly the profile's own values.
49
+ */
50
+ export interface MerchantHostedSubstitutions {
51
+ encoding: 'json';
52
+ kind: 'merchant_hosted';
53
+ profile: string;
54
+ path: JsonPathStep[];
55
+ fields: Record<string, string>;
56
+ remove: string[];
57
+ /** Card facts to write after the swap. No profile declares any yet, and the substitution refuses them. */
58
+ set?: MerchantHostedFact[];
59
+ }
60
+ /**
61
+ * The profile's verdict on a paused request: pause (its reviewed card request),
62
+ * abort (encrypted card data it cannot finish, with the reason) or pass (no
63
+ * encrypted card data, or not its endpoint or a sibling of it). Reads only;
64
+ * throws for a profile id this build did not review.
65
+ */
66
+ export declare const classifyMerchantRequest: (profileId: string, url: string, method: string, body: string | null | undefined) => MerchantRequestVerdict;
67
+ /** The endpoint origin, the path TEMPLATE and the profile's own query: what an event may name. */
68
+ export declare const merchantProfileTemplatedUrl: (profileId: string, url: string) => string;
69
+ /** SHA-256 of a JSON body's sorted key paths, no value in it; null for a body that is not JSON. */
70
+ export declare const merchantBodyKeyPathSha256: (body: string) => string | null;
71
+ /** 'sandbox' for a profile on Adyen's TEST platform, 'production' for every live environment. */
72
+ export declare const merchantProfileEnvironment: (profileId: string) => 'production' | 'sandbox';
73
+ /** classifyMerchantRequest for a sandbox declaration: the profile's body rules at the declared endpoint instead of the profile's. */
74
+ export declare const classifyDeclaredMerchantRequest: (profileId: string, endpoint: string, url: string, method: string, body: string | null | undefined) => MerchantRequestVerdict;
75
+ /**
76
+ * The profiles this client arms: every profile this build reviewed, once each, in
77
+ * this build's order. A served entry is `{ merchant_profile: <the profile's wire
78
+ * projection> }`, and the first one that names a profile with exactly this build's
79
+ * rules (endpoint, body rules, holder, fields, key: everything but `status`) and a
80
+ * status this build knows decides it: 'enabled' only when both that entry and this
81
+ * build say so. Every other profile stays in 'observe': one the API did not serve (no
82
+ * sync yet, a failed sync, an API that predates profiles) and one the API reviewed
83
+ * again after this build was cut, which this build cannot pay. In 'observe' its card
84
+ * request is still paused and aborted, so the agent's dummy card never reaches a
85
+ * reviewed merchant, whatever the API answered.
86
+ */
87
+ export declare function armServedProfiles(entries?: readonly unknown[]): MerchantProfile[];
88
+ /**
89
+ * Arms a sandbox declaration: the reviewed profile's card request at your own
90
+ * endpoint, under your own Adyen TEST key. Throws a TypeError for a profile this
91
+ * build did not review, an endpoint the API would refuse, or a key that is not a
92
+ * test_ key. The endpoint rule is the API's own (payment-core sandboxDeclarationRefusal):
93
+ * an HTTPS URL in the exact form the browser writes it (no port, credentials or
94
+ * fragment), on a public domain name outside Adyen's domains, Agentcard's and every
95
+ * reviewed merchant's domain (declaredEndpointRefusal), and on no host this build's
96
+ * processor registry names. The armed entry names its host as the merchant, and is
97
+ * 'enabled' whatever the reviewed profile's status: it never pays the reviewed
98
+ * merchant, the API refuses it from a live client and from an org it has not turned
99
+ * declarations on for, and the Vault encrypts only Adyen's documented test cards
100
+ * under a test_ key.
101
+ */
102
+ export declare function declareSandboxMerchant(input: SandboxMerchantDeclaration): MerchantProfile;
103
+ /** The armed profile whose endpoint, or a sibling of it (same origin and path shape), a URL is; null for any other URL. */
104
+ export declare function merchantProfileFor(profiles: readonly MerchantProfile[], url: string): MerchantProfile | null;
105
+ /** The Fetch.enable globs that pause every armed profile's endpoint and its siblings, a declared endpoint included. */
106
+ export declare function merchantProfileUrlPatterns(profiles: readonly MerchantProfile[]): string[];
107
+ /** classifyMerchantRequest for an armed profile: at its declared endpoint when it has one. */
108
+ export declare function classifyProfileRequest(profile: MerchantProfile, url: string, method: string, body: string | null | undefined): MerchantRequestVerdict;
109
+ /** merchantProfileTemplatedUrl for an armed profile: a declared endpoint's origin and path, never its query. */
110
+ export declare function profileTemplatedUrl(profile: MerchantProfile, url: string): string;
111
+ /**
112
+ * The Fetch.enable globs for every profile this build reviewed. Every one of them is
113
+ * armed from the start (armServedProfiles), so attachToCdp arms all of them at attach
114
+ * and a syncRegistry() before or after it changes only a profile's status: its card
115
+ * request is paused and judged under CDP exactly as attachToPlaywright's per-request
116
+ * route judges it.
117
+ */
118
+ export declare function reviewedMerchantProfileUrlPatterns(): string[];
119
+ /**
120
+ * The continuation of a merchant-hosted approval: the live paused body with the
121
+ * Vault's ciphertext in place of the dummy fields. The API never sees the body the
122
+ * browser continues, so the live body must first hash to the one the API checked
123
+ * when the authorization was created (`bodySha256`); a page that rewrote its body
124
+ * while the cardholder decided is refused rather than paid. payment-core's
125
+ * substituteMerchantHosted then reads the profile's body rules again on the live
126
+ * body, writes only the profile's four fields, and refuses a profile that is not
127
+ * enabled, unless this client armed the endpoint from its own sandbox declaration
128
+ * (the replay carries `sandboxDeclaration`), where every body rule still applies.
129
+ * Throws SubstitutionError; nothing leaves the browser when it does.
130
+ */
131
+ export declare function substituteMerchantHostedBody(body: string, replay: {
132
+ substitutions: MerchantHostedSubstitutions;
133
+ bodySha256: string;
134
+ sandboxDeclaration?: MerchantProfile['sandboxDeclaration'];
135
+ }): string;
136
+ /** The URL an event names for a merchant-hosted replay's request: its declared endpoint's origin and path, or the profile's template. */
137
+ export declare function replayTemplatedUrl(replay: {
138
+ profile: string;
139
+ sandboxDeclaration?: MerchantProfile['sandboxDeclaration'];
140
+ }, url: string): string;