@agent-cards/sdk 0.21.0 → 0.23.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 (113) hide show
  1. package/CHANGELOG.md +87 -2
  2. package/PREFLIGHT.md +32 -0
  3. package/README.md +1177 -23
  4. package/dist/aci.d.ts +173 -0
  5. package/dist/aci.generated.d.ts +87 -0
  6. package/dist/aci.generated.js +306 -0
  7. package/dist/aci.js +371 -0
  8. package/dist/adyen-merchant-hosted.generated.d.ts +3 -3
  9. package/dist/adyen-merchant-hosted.generated.js +302 -56
  10. package/dist/agentcard.d.ts +9 -2
  11. package/dist/agentcard.js +4 -2
  12. package/dist/amadeus-checkout.generated.d.ts +57 -0
  13. package/dist/amadeus-checkout.generated.js +549 -0
  14. package/dist/amadeus.d.ts +41 -0
  15. package/dist/amadeus.js +81 -0
  16. package/dist/builtin-registry.generated.js +1 -1
  17. package/dist/card-endpoint-facts.d.ts +96 -0
  18. package/dist/card-endpoint-facts.js +275 -0
  19. package/dist/card-endpoint-page.d.ts +54 -0
  20. package/dist/card-endpoint-page.js +69 -0
  21. package/dist/card-endpoint.d.ts +298 -0
  22. package/dist/card-endpoint.js +438 -0
  23. package/dist/card-fields.generated.d.ts +5 -3
  24. package/dist/card-fields.generated.js +284 -7
  25. package/dist/cdp.d.ts +5 -1
  26. package/dist/cdp.js +1155 -141
  27. package/dist/client.d.ts +456 -20
  28. package/dist/client.js +1072 -104
  29. package/dist/creditguard.generated.d.ts +79 -0
  30. package/dist/creditguard.generated.js +662 -0
  31. package/dist/cse-body.js +11 -1
  32. package/dist/cybersource-flex.d.ts +30 -0
  33. package/dist/cybersource-flex.generated.d.ts +27 -1
  34. package/dist/cybersource-flex.generated.js +42 -2
  35. package/dist/cybersource-flex.js +46 -0
  36. package/dist/datatrans.d.ts +96 -0
  37. package/dist/datatrans.generated.d.ts +64 -0
  38. package/dist/datatrans.generated.js +426 -0
  39. package/dist/datatrans.js +178 -0
  40. package/dist/ekashu.d.ts +35 -0
  41. package/dist/ekashu.js +55 -0
  42. package/dist/elavon-converge.d.ts +37 -0
  43. package/dist/elavon-converge.generated.d.ts +49 -0
  44. package/dist/elavon-converge.generated.js +439 -0
  45. package/dist/elavon-converge.js +84 -0
  46. package/dist/eprotect.d.ts +152 -0
  47. package/dist/eprotect.generated.d.ts +77 -0
  48. package/dist/eprotect.generated.js +498 -0
  49. package/dist/eprotect.js +222 -0
  50. package/dist/hosted-form.d.ts +3 -2
  51. package/dist/hosted-form.js +3 -2
  52. package/dist/index.d.ts +17 -3
  53. package/dist/index.js +6 -1
  54. package/dist/juspay-gate.d.ts +82 -0
  55. package/dist/juspay-gate.js +126 -0
  56. package/dist/juspay.generated.d.ts +89 -0
  57. package/dist/juspay.generated.js +705 -0
  58. package/dist/lifecycle.d.ts +60 -6
  59. package/dist/lifecycle.js +122 -8
  60. package/dist/merchant-card-endpoints.generated.d.ts +122 -0
  61. package/dist/merchant-card-endpoints.generated.js +4261 -0
  62. package/dist/merchant-hosted.d.ts +4 -1
  63. package/dist/merchant-hosted.js +5 -2
  64. package/dist/merchant-session-probe.d.ts +48 -0
  65. package/dist/merchant-session-probe.js +53 -0
  66. package/dist/merchant-session.generated.d.ts +69 -0
  67. package/dist/merchant-session.generated.js +919 -0
  68. package/dist/payrails.d.ts +133 -0
  69. package/dist/payrails.generated.d.ts +73 -0
  70. package/dist/payrails.generated.js +596 -0
  71. package/dist/payrails.js +305 -0
  72. package/dist/pelecard.d.ts +32 -0
  73. package/dist/pelecard.generated.d.ts +32 -0
  74. package/dist/pelecard.generated.js +283 -0
  75. package/dist/pelecard.js +42 -0
  76. package/dist/pre-claim.d.ts +81 -2
  77. package/dist/pre-claim.js +1023 -5
  78. package/dist/preflight-catalog.json +2267 -288
  79. package/dist/preflight-schemas.json +183 -6
  80. package/dist/preflight.generated.d.ts +1 -1
  81. package/dist/preflight.generated.js +243 -4
  82. package/dist/preparation.d.ts +12 -3
  83. package/dist/preparation.js +63 -12
  84. package/dist/prepared-processor.d.ts +84 -5
  85. package/dist/prepared-processor.js +147 -6
  86. package/dist/primer.d.ts +53 -0
  87. package/dist/primer.generated.d.ts +38 -0
  88. package/dist/primer.generated.js +624 -0
  89. package/dist/primer.js +82 -0
  90. package/dist/registry.d.ts +100 -8
  91. package/dist/registry.js +85 -11
  92. package/dist/shiji-sts.d.ts +144 -0
  93. package/dist/shiji-sts.generated.d.ts +72 -0
  94. package/dist/shiji-sts.generated.js +913 -0
  95. package/dist/shiji-sts.js +222 -0
  96. package/dist/stripe-checkout.generated.d.ts +1 -1
  97. package/dist/stripe-checkout.generated.js +764 -9
  98. package/dist/stripe-setup.d.ts +16 -0
  99. package/dist/stripe-setup.js +61 -0
  100. package/dist/substitutions.generated.js +187 -29
  101. package/dist/verifone.d.ts +68 -0
  102. package/dist/verifone.generated.d.ts +51 -0
  103. package/dist/verifone.generated.js +1413 -0
  104. package/dist/verifone.js +119 -0
  105. package/dist/voice-types.d.ts +27 -0
  106. package/dist/voice.d.ts +6 -1
  107. package/dist/voice.js +6 -1
  108. package/dist/wspay.d.ts +41 -0
  109. package/dist/wspay.generated.d.ts +41 -0
  110. package/dist/wspay.generated.js +370 -0
  111. package/dist/wspay.js +65 -0
  112. package/examples/preflight/kernel-native/inventory.json +1 -1
  113. package/package.json +3 -3
@@ -0,0 +1,438 @@
1
+ // A reviewed merchant's own card endpoint: the SDK's half of the card endpoint mode.
2
+ //
3
+ // Some merchants post the typed card from their own checkout page straight to their
4
+ // own server, with no processor in between (AT&T, Italo, Universal Orlando, Target
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.
13
+ //
14
+ // The adapters arm every profile's endpoint. A POST that is exactly a profile's card
15
+ // request URL pauses when it carries a card (cardEndpointCarriesCard); one that carries
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
21
+ // the adapter captures the request's session (the profile's own header list, the
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
25
+ // hands it with the paused body, which holds only the placeholder card the agent
26
+ // typed, to the create. The cardholder's device sends the real card request to the
27
+ // merchant itself, over the checkout relay, and reports the merchant's answer; the
28
+ // adapter writes its Set-Cookie values into the agent's browser on the endpoint host
29
+ // (or on the parent domain of a cookie the browser already holds there, which the
30
+ // write replaces) and answers the paused request with it. The real card never reaches
31
+ // this process or the agent's page.
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
+ //
50
+ // The endpoint URL (it carries cart, booking and instance ids), the paused body, the
51
+ // session and the answer are cardholder data even with no real card in them: no
52
+ // event, error or log line from this module names any of them. Events name the
53
+ // profile id and the endpoint origin only.
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';
55
+ /** The psp and mode a card endpoint authorization carries; neither is a processor. */
56
+ export const CARD_ENDPOINT_PSP = SHARED_PSP;
57
+ export const CARD_ENDPOINT_MODE = SHARED_MODE;
58
+ const PROFILES = SHARED_RECOGNIZERS;
59
+ export class CardEndpointRefusedError extends Error {
60
+ profile;
61
+ reason;
62
+ detail;
63
+ constructor(profile, reason, detail = null) {
64
+ super(`refused a payment to the ${profile} card endpoint (${reason})${detail ? `: ${detail}` : ''}. Nothing was created and nothing was charged.`);
65
+ this.profile = profile;
66
+ this.reason = reason;
67
+ this.detail = detail;
68
+ this.name = 'CardEndpointRefusedError';
69
+ }
70
+ /** True when every later request on the attachment would be refused the same way. */
71
+ get permanent() { return !['request_unsupported', 'facts_unavailable', 'facts_refused'].includes(this.reason); }
72
+ }
73
+ /** The reviewed profile (any status) whose card request `url` and `method` are exactly, or null. */
74
+ export function cardEndpointFor(url, method) {
75
+ try {
76
+ return sharedMatch(url, method, undefined) ?? null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /** The reviewed profile with this id, or null. */
83
+ export function cardEndpointById(id) {
84
+ return PROFILES.find((profile) => profile.id === id) ?? null;
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
+ }
146
+ /** Whether this build's copy of the profile takes payments (enabled, nothing pending, a reviewed HAR, not recurring). */
147
+ export function cardEndpointLive(profile) {
148
+ return sharedLive(typeof profile === 'string' ? profile : profile.id) === true;
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
+ }
179
+ /** What an event may say about a card endpoint request: the profile and the endpoint origin, never the URL's path or query. */
180
+ export function cardEndpointDetail(profile) {
181
+ return { profile: profile.id, endpoint: profile.endpoint };
182
+ }
183
+ // A CDP URL pattern treats '*' and '?' as wildcards and '\' as their escape.
184
+ const globEscape = (text) => text.replace(/[\\*?]/g, (char) => `\\${char}`);
185
+ /**
186
+ * Fetch.enable patterns covering every profile's card request, in any status: its
187
+ * origin and path with each `:name` segment and the query as wildcards. A coarse
188
+ * pre-filter, like cardUrlPatterns for processors; cardEndpointFor is the exact check
189
+ * each paused request is held to, and anything it refuses continues untouched.
190
+ */
191
+ export function cardEndpointPatterns() {
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))]))];
196
+ }
197
+ // Header names a capture never keeps, whatever a profile lists: the device's transport
198
+ // writes Cookie, Origin and Host itself, the connection's own headers are its own, and
199
+ // the Referer travels in its own member. payment-core's session check refuses the same.
200
+ const NEVER_KEPT = /^(?:cookie|cookie2|set-cookie|origin|referer|host|content-length|transfer-encoding|connection|keep-alive|upgrade|te|trailer|expect|proxy-.*|:.*)$/;
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
+ */
206
+ function keptReferer(profile, value) {
207
+ if (typeof value !== 'string' || value.includes('#'))
208
+ return undefined;
209
+ try {
210
+ const url = new URL(value);
211
+ return url.origin === (profile.frame ?? profile.checkout) && url.href === value && !url.username && !url.password ? value : undefined;
212
+ }
213
+ catch {
214
+ return undefined;
215
+ }
216
+ }
217
+ /**
218
+ * The session the create carries for a paused card endpoint request: the profile's own
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
222
+ * the same rules the API and the device run (payment-core
223
+ * merchantCardEndpointSessionRefusal), so a request they would refuse never leaves this
224
+ * process: a CardEndpointRefusedError (`request_unsupported`) instead, naming no value.
225
+ */
226
+ export async function captureCardEndpointSession(profile, capture) {
227
+ const headers = {};
228
+ const seen = new Set();
229
+ let referer;
230
+ for (const [name, value] of Object.entries(capture.headers ?? {})) {
231
+ const lower = name.toLowerCase();
232
+ if (seen.has(lower))
233
+ throw new CardEndpointRefusedError(profile.id, 'request_unsupported', 'the paused request names one header twice');
234
+ seen.add(lower);
235
+ if (lower === 'referer') {
236
+ referer = keptReferer(profile, value);
237
+ continue;
238
+ }
239
+ if (NEVER_KEPT.test(lower) || !profile.session.headers.includes(lower))
240
+ continue;
241
+ headers[lower] = typeof value === 'string' ? value : String(value);
242
+ }
243
+ const session = { url: capture.url, page_origin: capture.pageOrigin ?? '', headers, body: capture.body };
244
+ if (capture.navigation !== undefined)
245
+ session.navigation = capture.navigation;
246
+ if (profile.session.cookies) {
247
+ const jar = await capture.cookies();
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));
251
+ }
252
+ if (referer !== undefined)
253
+ session.referer = referer;
254
+ const refusal = sharedSessionRefusal(profile.id, session);
255
+ if (refusal)
256
+ throw new CardEndpointRefusedError(profile.id, 'request_unsupported', refusal);
257
+ return session;
258
+ }
259
+ /**
260
+ * The headers the paused request is answered with: the profile's answer headers the
261
+ * merchant sent, and never Set-Cookie (cookies reach the browser through
262
+ * cardEndpointCookies, on the endpoint host only).
263
+ */
264
+ export function cardEndpointAnswerHeaders(profile, headers) {
265
+ const out = {};
266
+ for (const [name, value] of Object.entries(headers)) {
267
+ const lower = name.toLowerCase();
268
+ if (lower === 'set-cookie' || !profile.response.headers.includes(lower) || typeof value !== 'string' || !/^[\x20-\x7e\t]*$/.test(value))
269
+ continue;
270
+ out[lower] = value;
271
+ }
272
+ return out;
273
+ }
274
+ /**
275
+ * The fixed answer a paused request gets when the cardholder's device withheld the
276
+ * merchant's answer (it carried the card, or a compressed stream the device cannot
277
+ * read): an error the merchant's page stops on. The card request was sent; whether
278
+ * the merchant took the payment is unknown until the order is checked.
279
+ */
280
+ export const CARD_ENDPOINT_WITHHELD_ANSWER = Object.freeze({
281
+ status: 502,
282
+ headers: Object.freeze({ 'content-type': 'application/json', 'cache-control': 'no-store' }),
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.' } }),
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
+ });
296
+ // RFC 6265 cookie-name (a token) and cookie-value, as payment-core checks a session cookie.
297
+ const COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,256}$/;
298
+ const COOKIE_VALUE = /^(?:[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*|"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*")$/;
299
+ const COOKIE_PATH = /^\/[\x21-\x3a\x3c-\x7e]{0,1023}$/;
300
+ // Browsers keep a cookie for at most 400 days.
301
+ const MAX_COOKIE_AGE_S = 400 * 24 * 60 * 60;
302
+ /** RFC 6265 5.1.4: the directory of the request path, the path a cookie with no Path attribute gets. */
303
+ function defaultCookiePath(requestUrl) {
304
+ let path;
305
+ try {
306
+ path = new URL(requestUrl).pathname;
307
+ }
308
+ catch {
309
+ return '/';
310
+ }
311
+ const last = path.lastIndexOf('/');
312
+ return last <= 0 ? '/' : path.slice(0, last);
313
+ }
314
+ /**
315
+ * One Set-Cookie line as the cookie to write, or null when a browser would not store it
316
+ * or it cannot be written safely. `jar` is the endpoint's cookies (null when they could
317
+ * not be read). A Domain attribute the endpoint host does not belong to drops the cookie
318
+ * (RFC 6265 5.3), as a browser does. One the host belongs to puts the cookie on that
319
+ * domain when the jar already holds a domain cookie of this name there, so the rotation
320
+ * replaces it; otherwise the cookie stays host-only. With the jar unread, a cookie that
321
+ * names a Domain is not written, since its write could leave two cookies of one name.
322
+ */
323
+ function parseSetCookie(profile, requestUrl, line, nowS, jar) {
324
+ if (typeof line !== 'string' || line.length > 8192 || /[\x00-\x08\x0a-\x1f\x7f]/.test(line))
325
+ return null;
326
+ const [pair, ...attributes] = line.split(';');
327
+ const eq = pair.indexOf('=');
328
+ if (eq <= 0)
329
+ return null;
330
+ const name = pair.slice(0, eq).trim();
331
+ const value = pair.slice(eq + 1).trim();
332
+ if (!COOKIE_NAME.test(name) || !COOKIE_VALUE.test(value))
333
+ return null;
334
+ let path = defaultCookiePath(requestUrl);
335
+ let secure = false;
336
+ let httpOnly = false;
337
+ let sameSite;
338
+ let expires;
339
+ let maxAge;
340
+ let named;
341
+ for (const attribute of attributes) {
342
+ const at = attribute.indexOf('=');
343
+ const key = (at === -1 ? attribute : attribute.slice(0, at)).trim().toLowerCase();
344
+ const text = at === -1 ? '' : attribute.slice(at + 1).trim();
345
+ if (key === 'path') {
346
+ if (COOKIE_PATH.test(text))
347
+ path = text;
348
+ }
349
+ else if (key === 'secure')
350
+ secure = true;
351
+ else if (key === 'httponly')
352
+ httpOnly = true;
353
+ else if (key === 'samesite') {
354
+ const lower = text.toLowerCase();
355
+ sameSite = lower === 'strict' ? 'Strict' : lower === 'lax' ? 'Lax' : lower === 'none' ? 'None' : undefined;
356
+ }
357
+ else if (key === 'max-age') {
358
+ if (/^-?\d{1,12}$/.test(text))
359
+ maxAge = Number(text);
360
+ }
361
+ else if (key === 'expires') {
362
+ const at = Date.parse(text);
363
+ if (Number.isFinite(at))
364
+ expires = Math.floor(at / 1000);
365
+ }
366
+ // RFC 6265 5.2.3: the last non-empty Domain counts, without a leading dot, in lower case.
367
+ else if (key === 'domain') {
368
+ const domain = text.replace(/^\./, '').toLowerCase();
369
+ if (domain !== '')
370
+ named = domain;
371
+ }
372
+ }
373
+ if (maxAge !== undefined)
374
+ expires = maxAge <= 0 ? 1 : nowS + Math.min(maxAge, MAX_COOKIE_AGE_S);
375
+ else if (expires !== undefined)
376
+ expires = expires <= nowS ? 1 : Math.min(expires, nowS + MAX_COOKIE_AGE_S);
377
+ // The prefixes a browser enforces on the merchant's own line: __Secure- needs Secure,
378
+ // __Host- also needs Path=/.
379
+ if ((name.startsWith('__Secure-') || name.startsWith('__Host-')) && !secure)
380
+ return null;
381
+ if (name.startsWith('__Host-') && path !== '/')
382
+ return null;
383
+ const host = new URL(profile.endpoint).hostname;
384
+ let domain;
385
+ if (named !== undefined) {
386
+ // __Host- cookies never name a Domain; a Domain the endpoint host is not inside is refused by a browser.
387
+ if (name.startsWith('__Host-') || (host !== named && !host.endsWith(`.${named}`)))
388
+ return null;
389
+ if (jar === null)
390
+ return null;
391
+ if (jar.some((held) => held.name === name && String(held.domain).toLowerCase() === `.${named}`))
392
+ domain = named;
393
+ }
394
+ return {
395
+ name, value, origin: profile.endpoint, host, ...(domain !== undefined ? { domain } : {}), path, secure: true, httpOnly,
396
+ ...(sameSite ? { sameSite } : {}), ...(expires !== undefined ? { expires } : {}),
397
+ };
398
+ }
399
+ /**
400
+ * The merchant's Set-Cookie values as cookies to write (parseSetCookie), and how many
401
+ * could not be written as one (a malformed line, a prefix or a Domain the browser would
402
+ * refuse, a Domain with the jar unread). `jar` is the endpoint's cookies as the adapter
403
+ * read them, null when the read failed. Only a profile whose answer may carry cookies
404
+ * has any.
405
+ */
406
+ export function cardEndpointCookies(profile, requestUrl, setCookies, nowMs = Date.now(), jar = []) {
407
+ if (!profile.response.headers.includes('set-cookie'))
408
+ return { cookies: [], skipped: setCookies.length };
409
+ const cookies = [];
410
+ let skipped = 0;
411
+ for (const line of setCookies) {
412
+ const cookie = parseSetCookie(profile, requestUrl, line, Math.floor(nowMs / 1000), jar);
413
+ if (cookie)
414
+ cookies.push(cookie);
415
+ else
416
+ skipped += 1;
417
+ }
418
+ return { cookies, skipped };
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
+ }
435
+ /** Why a session is not one the profile lets travel (payment-core merchantCardEndpointSessionRefusal), or null. Names no value. */
436
+ export function cardEndpointSessionRefusal(profile, session) {
437
+ return sharedSessionRefusal(profile.id, session) ?? null;
438
+ }
@@ -1,3 +1,5 @@
1
- declare function carriesCardFields(recognizer: any, body: any): boolean;
2
- declare function requestWithoutCard(recognizer: any, url: any, body: any): "continue" | "refuse" | null;
3
- export { carriesCardFields, requestWithoutCard };
1
+ declare function carriesCardFields(recognizer: any, body: any): any;
2
+ declare function splitCardLeg(recognizer: any, body: any): "absorb" | "pass" | null;
3
+ declare function requestWithoutCard(recognizer: any, url: any, body: any): "refuse" | "continue" | null;
4
+ declare function cardRequestRefusal(recognizer: any, url: any, body: any): any;
5
+ export { cardRequestRefusal, carriesCardFields, requestWithoutCard, splitCardLeg };