@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.
@@ -3,22 +3,64 @@ export interface CardEndpointProfile {
3
3
  readonly id: string;
4
4
  readonly merchant: string;
5
5
  readonly status: 'observe' | 'enabled';
6
- /** The exact https origin of the page the card is typed into. */
6
+ /** The exact https origin of the top-level page the card is typed into. */
7
7
  readonly checkout: string;
8
+ /**
9
+ * The exact https origin of the merchant's own card vault frame inside that page, when
10
+ * the card request is sent from it rather than from the page itself; else null.
11
+ */
12
+ readonly frame: string | null;
8
13
  /** The exact https origin the card request goes to. */
9
14
  readonly endpoint: string;
10
15
  readonly method: 'POST';
11
16
  readonly path: string;
12
17
  readonly query: Readonly<Record<string, string>>;
13
- readonly encoding: 'json' | 'form' | 'multipart';
18
+ /** json_api is a JSON object sent as application/vnd.api+json. */
19
+ readonly encoding: 'json' | 'json_api' | 'form' | 'multipart';
20
+ /** A fetch or XHR (null), or a form the page submits as a navigation of the page ('top') or of an iframe ('frame'). */
21
+ readonly navigation: 'top' | 'frame' | null;
14
22
  readonly session: {
15
23
  readonly cookies: boolean;
24
+ /** Null for every cookie of the endpoint's jar, or the only names that may travel ('X*' takes every name that starts with X). */
25
+ readonly cookie_names: readonly string[] | null;
16
26
  readonly headers: readonly string[];
17
27
  readonly required: readonly string[];
18
28
  };
19
29
  readonly response: {
20
30
  readonly headers: readonly string[];
21
31
  };
32
+ /** True when the card is typed into a frame the merchant's page embeds: the request pauses on that frame's document. */
33
+ readonly checkout_frame?: boolean;
34
+ /** Where a body names its payment method, and the values that mean a card: any other method continues untouched. */
35
+ readonly payment_method?: {
36
+ readonly at: unknown;
37
+ readonly card: readonly string[];
38
+ } | null;
39
+ /** Requests on the endpoint origin that send the typed card again outside the card request: aborted in every status. */
40
+ readonly resends?: readonly {
41
+ readonly method: string;
42
+ readonly path: string;
43
+ readonly query: Readonly<Record<string, string>>;
44
+ }[];
45
+ /**
46
+ * The merchant's own record of the order, read from these responses (card-endpoint-facts.ts), and the calls on the
47
+ * same checkout session that change nothing it holds (inert).
48
+ */
49
+ readonly facts?: {
50
+ readonly reader: string;
51
+ readonly responses: readonly {
52
+ readonly method: string;
53
+ readonly path: string;
54
+ readonly query: Readonly<Record<string, string>>;
55
+ readonly format: string;
56
+ }[];
57
+ readonly inert: readonly {
58
+ readonly method: string;
59
+ readonly path: string;
60
+ readonly query: Readonly<Record<string, string>>;
61
+ }[];
62
+ readonly bind: string;
63
+ } | null;
22
64
  }
23
65
  /** One cookie of the endpoint's jar, name and value only. */
24
66
  export interface CardEndpointCookie {
@@ -28,13 +70,16 @@ export interface CardEndpointCookie {
28
70
  /**
29
71
  * What the adapters capture from a paused card request and hand to the create: the
30
72
  * URL, the origin of the page it was paused on, the profile's session headers, the
31
- * endpoint's cookies for a cookie profile, a Referer on the checkout page, and the
32
- * body, which holds only the placeholder card the agent typed. Never a real card,
73
+ * endpoint's cookies for a cookie profile, a Referer on the origin the request is sent
74
+ * from (the profile's card vault frame when it names one, else the checkout page), and
75
+ * the body, which holds only the placeholder card the agent typed. Never a real card,
33
76
  * always cardholder data: it travels only in `merchant_card_endpoint.session`.
34
77
  */
35
78
  export interface CardEndpointSession {
36
79
  url: string;
37
80
  page_origin: string;
81
+ /** For a navigation profile: what the browser said the paused request was. */
82
+ navigation?: 'top' | 'frame';
38
83
  headers: Record<string, string>;
39
84
  cookies?: CardEndpointCookie[];
40
85
  referer?: string;
@@ -53,12 +98,17 @@ export declare const CARD_ENDPOINT_MODE: "card_endpoint";
53
98
  * - `preparation_unsupported`: a prepared checkout cannot pay a merchant's own card endpoint.
54
99
  * - `request_unsupported`: the paused request is not the card request Agentcard reviewed
55
100
  * (`detail` says what, naming fields, never values).
101
+ * - `facts_unavailable`: the merchant keeps its own record of the order (profile.facts),
102
+ * and this runtime read none for this checkout, or one it cannot place.
103
+ * - `facts_refused`: the merchant's own record says the order renews on a schedule, or
104
+ * that its checkout is in test mode. A total or currency that differs from the agent's
105
+ * is an AmountMismatchError instead.
56
106
  *
57
- * Every reason but `request_unsupported` answers the same way for every request on the
58
- * attachment, so the adapters stop asking; `request_unsupported` refuses only the
59
- * request it is about.
107
+ * `not_enabled`, `amount_required`, `approval_required` and `preparation_unsupported`
108
+ * answer the same way for every request on the attachment, so the adapters stop asking;
109
+ * the others refuse only the request they are about.
60
110
  */
61
- export type CardEndpointRefusal = 'not_enabled' | 'amount_required' | 'approval_required' | 'preparation_unsupported' | 'request_unsupported';
111
+ export type CardEndpointRefusal = 'not_enabled' | 'amount_required' | 'approval_required' | 'preparation_unsupported' | 'request_unsupported' | 'facts_unavailable' | 'facts_refused';
62
112
  export declare class CardEndpointRefusedError extends Error {
63
113
  readonly profile: string;
64
114
  readonly reason: CardEndpointRefusal;
@@ -71,8 +121,44 @@ export declare class CardEndpointRefusedError extends Error {
71
121
  export declare function cardEndpointFor(url: string, method: string): CardEndpointProfile | null;
72
122
  /** The reviewed profile with this id, or null. */
73
123
  export declare function cardEndpointById(id: string): CardEndpointProfile | null;
124
+ /**
125
+ * Whether a request to the profile's card endpoint is a card-free payment (the profile's
126
+ * payment_method names another method in the body): it is never Agentcard's and
127
+ * continues untouched, in every status. False for anything the rule cannot read, which
128
+ * is then held to the card request's rules.
129
+ */
130
+ export declare function cardEndpointPasses(profile: CardEndpointProfile, body: string | null, contentType: string | undefined): boolean;
131
+ /** The reviewed profile (any status) one of whose resend requests `url` and `method` are, or null: the adapters abort it. */
132
+ export declare function cardEndpointResendFor(url: string, method: string): CardEndpointProfile | null;
133
+ /** The origins of the profiles whose merchant keeps its own record of the order: the only responses card-endpoint-facts.ts records. */
134
+ export declare function cardEndpointFactsOrigins(): readonly string[];
135
+ /**
136
+ * The checkout session a request on the endpoint of a profile with facts belongs to: the
137
+ * endpoint origin and the profile's card path up to its bind placeholder, written with the
138
+ * request's own segments, the session payment-core binds the record to
139
+ * ('https://rekordcloud.onfastspring.com/popup-lexicon/session/<id>'). Null for any other
140
+ * request. card-endpoint-facts.ts counts what it keeps, and what a checkout lost, by it.
141
+ * When the paths of two profiles on one origin both fit, the shorter session is the one:
142
+ * every request on a card request's session then gets the card request's own.
143
+ */
144
+ export declare function cardEndpointFactsSession(url: string): string | null;
74
145
  /** Whether this build's copy of the profile takes payments (enabled, nothing pending, a reviewed HAR, not recurring). */
75
146
  export declare function cardEndpointLive(profile: CardEndpointProfile | string): boolean;
147
+ /**
148
+ * Whether a paused request to the profile's card request URL carries a card, so the
149
+ * adapters hold it (payment-core merchantCardEndpointCarriesCard): anything at the
150
+ * profile's card number field, or a Luhn-valid number of 12 to 19 digits anywhere in
151
+ * the body, however it is written. A body the adapter could not read (null), an empty
152
+ * one, and any answer the rule cannot give count as carrying one. A post to a
153
+ * navigation profile carries none only when it also reads under the profile's
154
+ * encoding, its card fields are empty and it holds no card number anywhere: no run of
155
+ * 12 to 19 digits, spaces, dots and dashes dropped, that passes the card check digit.
156
+ * Its page posts such a body for a cancel, another payment method or a bank payment,
157
+ * and it continues untouched.
158
+ */
159
+ export declare function cardEndpointCarriesCard(profile: CardEndpointProfile, body: string | null | undefined, contentType: string | undefined): boolean;
160
+ /** Whether a paused body is the card request the profile reviewed (its body rules, never its status). */
161
+ export declare function cardEndpointReviewed(profile: CardEndpointProfile, body: string | null | undefined, contentType: string | undefined): boolean;
76
162
  /** What an event may say about a card endpoint request: the profile and the endpoint origin, never the URL's path or query. */
77
163
  export declare function cardEndpointDetail(profile: CardEndpointProfile): {
78
164
  profile: string;
@@ -92,15 +178,21 @@ export interface CardEndpointCapture {
92
178
  headers: Record<string, string> | undefined;
93
179
  /** The paused body: the placeholder card the agent typed and the rest of the merchant's fields. */
94
180
  body: string;
95
- /** The origin of the top-level page the request was paused on, when it could be read. */
181
+ /**
182
+ * The origin of the page the request was paused on, when it could be read: the
183
+ * top-level page, or for a 'frame' profile the document of the iframe its form sits in.
184
+ */
96
185
  pageOrigin: string | undefined;
186
+ /** For a document request: a navigation of the page ('top') or of an iframe ('frame'). Absent for a fetch or XHR. */
187
+ navigation?: 'top' | 'frame';
97
188
  /** The endpoint's cookie jar for this URL (HttpOnly included). Read only for a cookie profile. */
98
189
  cookies: () => Promise<CardEndpointCookie[]>;
99
190
  }
100
191
  /**
101
192
  * The session the create carries for a paused card endpoint request: the profile's own
102
- * header names and nothing else, the Referer only when it is a page on the checkout
103
- * origin, the endpoint's cookies only for a cookie profile, and the body. Checked with
193
+ * header names and nothing else, the Referer only when it is a page on the origin the
194
+ * request is sent from (the profile's card vault frame when it names one, else the
195
+ * checkout page), the endpoint's cookies only for a cookie profile, and the body. Checked with
104
196
  * the same rules the API and the device run (payment-core
105
197
  * merchantCardEndpointSessionRefusal), so a request they would refuse never leaves this
106
198
  * process: a CardEndpointRefusedError (`request_unsupported`) instead, naming no value.
@@ -123,6 +215,17 @@ export declare const CARD_ENDPOINT_WITHHELD_ANSWER: Readonly<{
123
215
  headers: Readonly<Record<string, string>>;
124
216
  body: string;
125
217
  }>;
218
+ /**
219
+ * The fixed answer for a merchant answer the device withheld because it asks the page
220
+ * for a step the review refuses (withheld `refused_step_in_response`: FastSpring's EBANX
221
+ * step, which reads the typed card back and sends it again). The card request was sent,
222
+ * so the outcome is unknown until the order is checked.
223
+ */
224
+ export declare const CARD_ENDPOINT_REFUSED_STEP_ANSWER: Readonly<{
225
+ status: number;
226
+ headers: Readonly<Record<string, string>>;
227
+ body: string;
228
+ }>;
126
229
  /**
127
230
  * A Set-Cookie value from the merchant's answer, as an adapter writes it into the
128
231
  * agent's browser: host-only on the endpoint host, or on the parent domain its Domain
@@ -173,5 +276,23 @@ export declare function cardEndpointCookies(profile: CardEndpointProfile, reques
173
276
  cookies: CardEndpointSetCookie[];
174
277
  skipped: number;
175
278
  };
279
+ /**
280
+ * How a paused navigation is answered from the merchant's answer (payment-core
281
+ * merchantCardEndpointNavigationAnswer): the page, a 301, 302 or 303 to one https URL,
282
+ * or refused (a redirect that repeats the post, or a Location that is not one https URL).
283
+ */
284
+ export declare function cardEndpointNavigationAnswer(profile: CardEndpointProfile, answer: {
285
+ status: number;
286
+ headers: Record<string, string>;
287
+ }, requestUrl: string): {
288
+ kind: 'page';
289
+ } | {
290
+ kind: 'redirect';
291
+ status: number;
292
+ location: string;
293
+ } | {
294
+ kind: 'refused';
295
+ reason: 'redirect_repeats_post' | 'redirect_location_invalid';
296
+ };
176
297
  /** Why a session is not one the profile lets travel (payment-core merchantCardEndpointSessionRefusal), or null. Names no value. */
177
298
  export declare function cardEndpointSessionRefusal(profile: CardEndpointProfile, session: unknown): string | null;
@@ -2,18 +2,26 @@
2
2
  //
3
3
  // Some merchants post the typed card from their own checkout page straight to their
4
4
  // own server, with no processor in between (AT&T, Italo, Universal Orlando, Target
5
- // Optical, Opus Virtual Offices). Agentcard reviews each one and pins it as a profile
6
- // in the payment core (merchant-card-endpoints.generated.ts, the SDK's copy of the
7
- // same rules the API and the Vault run): the page the card is typed into, the exact
8
- // origin, path and query of the card request, which session headers and cookies it
9
- // needs, and which answer headers may travel back.
5
+ // Optical, Opus Virtual Offices, Delta, Hilton, IHG, American Airlines, EconomyBookings,
6
+ // The Verb Hotel, RunSignUp, Mindbody, LookFantastic), some after their page encrypts it
7
+ // under a key the merchant publishes (Hyatt, LifeMiles, Southwest, United): the paused
8
+ // body then holds the placeholder card only as the page encrypted it. Agentcard reviews
9
+ // each one and pins it as a profile in the payment core (merchant-card-endpoints.generated.ts,
10
+ // the SDK's copy of the same rules the API and the Vault run): the page the card is
11
+ // typed into, the exact origin, path and query of the card request, which session
12
+ // headers and cookies it needs, and which answer headers may travel back.
10
13
  //
11
14
  // The adapters arm every profile's endpoint. A POST that is exactly a profile's card
12
- // request pauses. While this build's copy of the profile does not take payments
13
- // (observe, a fact only a partner HAR can pin, no reviewed HAR, a recurring charge)
14
- // the request is aborted as an unsupported checkout and nothing is created. Otherwise
15
+ // request URL pauses when it carries a card (cardEndpointCarriesCard); one that carries
16
+ // none, such as a stay paid with points or a card a processor's own fields tokenized,
17
+ // is the merchant's own business and continues untouched. While this build's copy of
18
+ // the profile does not take payments (observe, a fact only a partner HAR can pin, no
19
+ // reviewed HAR, a recurring charge) the card request is aborted as an unsupported
20
+ // checkout and nothing is created. Otherwise
15
21
  // the adapter captures the request's session (the profile's own header list, the
16
- // endpoint's cookie jar for a cookie profile, a Referer on the checkout page) and
22
+ // endpoint's cookie jar for a cookie profile, a Referer on the page the request is sent
23
+ // from: the merchant's card vault frame when the profile names one, else the checkout
24
+ // page) and
17
25
  // hands it with the paused body, which holds only the placeholder card the agent
18
26
  // typed, to the create. The cardholder's device sends the real card request to the
19
27
  // merchant itself, over the checkout relay, and reports the merchant's answer; the
@@ -22,11 +30,28 @@
22
30
  // write replaces) and answers the paused request with it. The real card never reaches
23
31
  // this process or the agent's page.
24
32
  //
33
+ // Some merchants' pages submit the card form as a page load, not a fetch (a profile
34
+ // whose `navigation` is 'top', or 'frame' for a form inside an iframe): MyRec, Conduent
35
+ // eTIMS, the D-EDGE card frame. For those the adapters let a post that carries no card
36
+ // through untouched (a cancel, another payment method), refuse a card post that is not
37
+ // that navigation, answer the paid one with the merchant's page or a GET redirect, and
38
+ // answer every stopped one with a page that says what happened (card-endpoint-page.ts),
39
+ // never with an aborted load that invites another click on Pay.
40
+ //
41
+ // Some merchants need more (FastSpring's popup checkout for Lexicon). The card is typed
42
+ // into a frame the merchant's page embeds, so for a profile with checkout_frame the page
43
+ // a request pauses on is that frame's own document. The same URL also takes card-free
44
+ // payments (PayPal, wallets), which continue untouched (cardEndpointPasses). A request
45
+ // that sends the typed card again outside the card request is aborted in every status
46
+ // (cardEndpointResendFor). And the merchant's own record of the order, which its page
47
+ // reads from the endpoint origin, is recorded read-only (card-endpoint-facts.ts) and sent
48
+ // with the create, so the agent's amount is held to the merchant's total.
49
+ //
25
50
  // The endpoint URL (it carries cart, booking and instance ids), the paused body, the
26
51
  // session and the answer are cardholder data even with no real card in them: no
27
52
  // event, error or log line from this module names any of them. Events name the
28
53
  // profile id and the endpoint origin only.
29
- import { MERCHANT_CARD_ENDPOINT_MODE as SHARED_MODE, MERCHANT_CARD_ENDPOINT_PSP as SHARED_PSP, MERCHANT_CARD_ENDPOINT_RECOGNIZERS as SHARED_RECOGNIZERS, matchMerchantCardEndpoint as sharedMatch, merchantCardEndpointLive as sharedLive, merchantCardEndpointSessionRefusal as sharedSessionRefusal, } from './merchant-card-endpoints.generated.js';
54
+ import { MERCHANT_CARD_ENDPOINT_MODE as SHARED_MODE, MERCHANT_CARD_ENDPOINT_PSP as SHARED_PSP, MERCHANT_CARD_ENDPOINT_RECOGNIZERS as SHARED_RECOGNIZERS, matchMerchantCardEndpoint as sharedMatch, matchMerchantCardEndpointResend as sharedResend, merchantCardEndpointBodyRefusal as sharedBodyRefusal, merchantCardEndpointCarriesCard as sharedCarriesCard, merchantCardEndpointLive as sharedLive, merchantCardEndpointNavigationAnswer as sharedNavigationAnswer, merchantCardEndpointPasses as sharedPasses, merchantCardEndpointSessionRefusal as sharedSessionRefusal, } from './merchant-card-endpoints.generated.js';
30
55
  /** The psp and mode a card endpoint authorization carries; neither is a processor. */
31
56
  export const CARD_ENDPOINT_PSP = SHARED_PSP;
32
57
  export const CARD_ENDPOINT_MODE = SHARED_MODE;
@@ -43,7 +68,7 @@ export class CardEndpointRefusedError extends Error {
43
68
  this.name = 'CardEndpointRefusedError';
44
69
  }
45
70
  /** True when every later request on the attachment would be refused the same way. */
46
- get permanent() { return this.reason !== 'request_unsupported'; }
71
+ get permanent() { return !['request_unsupported', 'facts_unavailable', 'facts_refused'].includes(this.reason); }
47
72
  }
48
73
  /** The reviewed profile (any status) whose card request `url` and `method` are exactly, or null. */
49
74
  export function cardEndpointFor(url, method) {
@@ -58,10 +83,99 @@ export function cardEndpointFor(url, method) {
58
83
  export function cardEndpointById(id) {
59
84
  return PROFILES.find((profile) => profile.id === id) ?? null;
60
85
  }
86
+ /**
87
+ * Whether a request to the profile's card endpoint is a card-free payment (the profile's
88
+ * payment_method names another method in the body): it is never Agentcard's and
89
+ * continues untouched, in every status. False for anything the rule cannot read, which
90
+ * is then held to the card request's rules.
91
+ */
92
+ export function cardEndpointPasses(profile, body, contentType) {
93
+ if (typeof body !== 'string')
94
+ return false;
95
+ try {
96
+ return sharedPasses(profile.id, body, contentType) === true;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ /** The reviewed profile (any status) one of whose resend requests `url` and `method` are, or null: the adapters abort it. */
103
+ export function cardEndpointResendFor(url, method) {
104
+ try {
105
+ return sharedResend(url, method) ?? null;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ /** The origins of the profiles whose merchant keeps its own record of the order: the only responses card-endpoint-facts.ts records. */
112
+ export function cardEndpointFactsOrigins() {
113
+ return [...new Set(PROFILES.filter((profile) => profile.facts).map((profile) => profile.endpoint))];
114
+ }
115
+ /**
116
+ * The checkout session a request on the endpoint of a profile with facts belongs to: the
117
+ * endpoint origin and the profile's card path up to its bind placeholder, written with the
118
+ * request's own segments, the session payment-core binds the record to
119
+ * ('https://rekordcloud.onfastspring.com/popup-lexicon/session/<id>'). Null for any other
120
+ * request. card-endpoint-facts.ts counts what it keeps, and what a checkout lost, by it.
121
+ * When the paths of two profiles on one origin both fit, the shorter session is the one:
122
+ * every request on a card request's session then gets the card request's own.
123
+ */
124
+ export function cardEndpointFactsSession(url) {
125
+ let parsed;
126
+ try {
127
+ parsed = new URL(url);
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ const segments = parsed.pathname.split('/');
133
+ let shortest = null;
134
+ for (const profile of PROFILES) {
135
+ if (!profile.facts || profile.endpoint !== parsed.origin)
136
+ continue;
137
+ const template = profile.path.split('/');
138
+ const at = template.indexOf(`:${profile.facts.bind}`);
139
+ if (at < 1 || segments.length <= at || (shortest !== null && at >= shortest))
140
+ continue;
141
+ if (template.slice(0, at + 1).every((part, i) => (part.startsWith(':') ? segments[i] !== '' : part === segments[i])))
142
+ shortest = at;
143
+ }
144
+ return shortest === null ? null : `${parsed.origin}${segments.slice(0, shortest + 1).join('/')}`;
145
+ }
61
146
  /** Whether this build's copy of the profile takes payments (enabled, nothing pending, a reviewed HAR, not recurring). */
62
147
  export function cardEndpointLive(profile) {
63
148
  return sharedLive(typeof profile === 'string' ? profile : profile.id) === true;
64
149
  }
150
+ /**
151
+ * Whether a paused request to the profile's card request URL carries a card, so the
152
+ * adapters hold it (payment-core merchantCardEndpointCarriesCard): anything at the
153
+ * profile's card number field, or a Luhn-valid number of 12 to 19 digits anywhere in
154
+ * the body, however it is written. A body the adapter could not read (null), an empty
155
+ * one, and any answer the rule cannot give count as carrying one. A post to a
156
+ * navigation profile carries none only when it also reads under the profile's
157
+ * encoding, its card fields are empty and it holds no card number anywhere: no run of
158
+ * 12 to 19 digits, spaces, dots and dashes dropped, that passes the card check digit.
159
+ * Its page posts such a body for a cancel, another payment method or a bank payment,
160
+ * and it continues untouched.
161
+ */
162
+ export function cardEndpointCarriesCard(profile, body, contentType) {
163
+ try {
164
+ return sharedCarriesCard(profile.id, body, contentType) !== false;
165
+ }
166
+ catch {
167
+ return true;
168
+ }
169
+ }
170
+ /** Whether a paused body is the card request the profile reviewed (its body rules, never its status). */
171
+ export function cardEndpointReviewed(profile, body, contentType) {
172
+ try {
173
+ return typeof body === 'string' && sharedBodyRefusal(profile.id, body, contentType) === null;
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ }
65
179
  /** What an event may say about a card endpoint request: the profile and the endpoint origin, never the URL's path or query. */
66
180
  export function cardEndpointDetail(profile) {
67
181
  return { profile: profile.id, endpoint: profile.endpoint };
@@ -75,19 +189,26 @@ const globEscape = (text) => text.replace(/[\\*?]/g, (char) => `\\${char}`);
75
189
  * each paused request is held to, and anything it refuses continues untouched.
76
190
  */
77
191
  export function cardEndpointPatterns() {
78
- return [...new Set(PROFILES.map((profile) => `${globEscape(profile.endpoint)}${profile.path.split('/').map((segment) => (segment.startsWith(':') ? '*' : globEscape(segment))).join('/')}*`))];
192
+ const pattern = (origin, path) => `${globEscape(origin)}${path.split('/').map((segment) => (segment.startsWith(':') ? '*' : globEscape(segment))).join('/')}*`;
193
+ return [...new Set(PROFILES.flatMap((profile) => [pattern(profile.endpoint, profile.path),
194
+ // Each resend is paused only to be refused (cardEndpointResendFor).
195
+ ...(profile.resends ?? []).map((resend) => pattern(profile.endpoint, resend.path))]))];
79
196
  }
80
197
  // Header names a capture never keeps, whatever a profile lists: the device's transport
81
198
  // writes Cookie, Origin and Host itself, the connection's own headers are its own, and
82
199
  // the Referer travels in its own member. payment-core's session check refuses the same.
83
200
  const NEVER_KEPT = /^(?:cookie|cookie2|set-cookie|origin|referer|host|content-length|transfer-encoding|connection|keep-alive|upgrade|te|trailer|expect|proxy-.*|:.*)$/;
84
- /** The paused request's Referer when it is a page on the profile's checkout origin, written as a browser serializes it; else nothing. */
201
+ /**
202
+ * The paused request's Referer when it is a page on the origin the request is sent from
203
+ * (the profile's card vault frame when it names one, else its checkout origin), written
204
+ * as a browser serializes it; else nothing.
205
+ */
85
206
  function keptReferer(profile, value) {
86
207
  if (typeof value !== 'string' || value.includes('#'))
87
208
  return undefined;
88
209
  try {
89
210
  const url = new URL(value);
90
- return url.origin === profile.checkout && url.href === value && !url.username && !url.password ? value : undefined;
211
+ return url.origin === (profile.frame ?? profile.checkout) && url.href === value && !url.username && !url.password ? value : undefined;
91
212
  }
92
213
  catch {
93
214
  return undefined;
@@ -95,8 +216,9 @@ function keptReferer(profile, value) {
95
216
  }
96
217
  /**
97
218
  * The session the create carries for a paused card endpoint request: the profile's own
98
- * header names and nothing else, the Referer only when it is a page on the checkout
99
- * origin, the endpoint's cookies only for a cookie profile, and the body. Checked with
219
+ * header names and nothing else, the Referer only when it is a page on the origin the
220
+ * request is sent from (the profile's card vault frame when it names one, else the
221
+ * checkout page), the endpoint's cookies only for a cookie profile, and the body. Checked with
100
222
  * the same rules the API and the device run (payment-core
101
223
  * merchantCardEndpointSessionRefusal), so a request they would refuse never leaves this
102
224
  * process: a CardEndpointRefusedError (`request_unsupported`) instead, naming no value.
@@ -119,9 +241,13 @@ export async function captureCardEndpointSession(profile, capture) {
119
241
  headers[lower] = typeof value === 'string' ? value : String(value);
120
242
  }
121
243
  const session = { url: capture.url, page_origin: capture.pageOrigin ?? '', headers, body: capture.body };
244
+ if (capture.navigation !== undefined)
245
+ session.navigation = capture.navigation;
122
246
  if (profile.session.cookies) {
123
247
  const jar = await capture.cookies();
124
- session.cookies = (Array.isArray(jar) ? jar : []).map(({ name, value }) => ({ name, value }));
248
+ // Only the names the profile lists leave this process; every other cookie of the jar stays in the browser.
249
+ session.cookies = (Array.isArray(jar) ? jar : []).map(({ name, value }) => ({ name, value }))
250
+ .filter(({ name }) => cookieListed(profile, name));
125
251
  }
126
252
  if (referer !== undefined)
127
253
  session.referer = referer;
@@ -156,6 +282,17 @@ export const CARD_ENDPOINT_WITHHELD_ANSWER = Object.freeze({
156
282
  headers: Object.freeze({ 'content-type': 'application/json', 'cache-control': 'no-store' }),
157
283
  body: JSON.stringify({ error: { code: 'agentcard_answer_withheld', message: 'The merchant answered, and Agentcard withheld the answer because it could carry card data. Check the order with the merchant before trying again.' } }),
158
284
  });
285
+ /**
286
+ * The fixed answer for a merchant answer the device withheld because it asks the page
287
+ * for a step the review refuses (withheld `refused_step_in_response`: FastSpring's EBANX
288
+ * step, which reads the typed card back and sends it again). The card request was sent,
289
+ * so the outcome is unknown until the order is checked.
290
+ */
291
+ export const CARD_ENDPOINT_REFUSED_STEP_ANSWER = Object.freeze({
292
+ status: 502,
293
+ headers: Object.freeze({ 'content-type': 'application/json', 'cache-control': 'no-store' }),
294
+ body: JSON.stringify({ error: { code: 'agentcard_step_refused', message: 'The merchant asked for a payment step Agentcard does not take part in, so Agentcard withheld its answer. Check the order with the merchant before trying again.' } }),
295
+ });
159
296
  // RFC 6265 cookie-name (a token) and cookie-value, as payment-core checks a session cookie.
160
297
  const COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,256}$/;
161
298
  const COOKIE_VALUE = /^(?:[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*|"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*")$/;
@@ -280,6 +417,21 @@ export function cardEndpointCookies(profile, requestUrl, setCookies, nowMs = Dat
280
417
  }
281
418
  return { cookies, skipped };
282
419
  }
420
+ /** Whether a cookie name is one the profile lets travel (payment-core reads the same list). */
421
+ function cookieListed(profile, name) {
422
+ const names = profile.session.cookie_names;
423
+ if (names === null)
424
+ return true;
425
+ return typeof name === 'string' && names.some((entry) => (entry.endsWith('*') ? name.startsWith(entry.slice(0, -1)) && name.length > entry.length - 1 : name === entry));
426
+ }
427
+ /**
428
+ * How a paused navigation is answered from the merchant's answer (payment-core
429
+ * merchantCardEndpointNavigationAnswer): the page, a 301, 302 or 303 to one https URL,
430
+ * or refused (a redirect that repeats the post, or a Location that is not one https URL).
431
+ */
432
+ export function cardEndpointNavigationAnswer(profile, answer, requestUrl) {
433
+ return sharedNavigationAnswer(profile.id, answer, requestUrl);
434
+ }
283
435
  /** Why a session is not one the profile lets travel (payment-core merchantCardEndpointSessionRefusal), or null. Names no value. */
284
436
  export function cardEndpointSessionRefusal(profile, session) {
285
437
  return sharedSessionRefusal(profile.id, session) ?? null;