@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.
- package/CHANGELOG.md +87 -2
- package/PREFLIGHT.md +32 -0
- package/README.md +1177 -23
- package/dist/aci.d.ts +173 -0
- package/dist/aci.generated.d.ts +87 -0
- package/dist/aci.generated.js +306 -0
- package/dist/aci.js +371 -0
- package/dist/adyen-merchant-hosted.generated.d.ts +3 -3
- package/dist/adyen-merchant-hosted.generated.js +302 -56
- package/dist/agentcard.d.ts +9 -2
- package/dist/agentcard.js +4 -2
- package/dist/amadeus-checkout.generated.d.ts +57 -0
- package/dist/amadeus-checkout.generated.js +549 -0
- package/dist/amadeus.d.ts +41 -0
- package/dist/amadeus.js +81 -0
- package/dist/builtin-registry.generated.js +1 -1
- package/dist/card-endpoint-facts.d.ts +96 -0
- package/dist/card-endpoint-facts.js +275 -0
- package/dist/card-endpoint-page.d.ts +54 -0
- package/dist/card-endpoint-page.js +69 -0
- package/dist/card-endpoint.d.ts +298 -0
- package/dist/card-endpoint.js +438 -0
- package/dist/card-fields.generated.d.ts +5 -3
- package/dist/card-fields.generated.js +284 -7
- package/dist/cdp.d.ts +5 -1
- package/dist/cdp.js +1155 -141
- package/dist/client.d.ts +456 -20
- package/dist/client.js +1072 -104
- package/dist/creditguard.generated.d.ts +79 -0
- package/dist/creditguard.generated.js +662 -0
- package/dist/cse-body.js +11 -1
- package/dist/cybersource-flex.d.ts +30 -0
- package/dist/cybersource-flex.generated.d.ts +27 -1
- package/dist/cybersource-flex.generated.js +42 -2
- package/dist/cybersource-flex.js +46 -0
- package/dist/datatrans.d.ts +96 -0
- package/dist/datatrans.generated.d.ts +64 -0
- package/dist/datatrans.generated.js +426 -0
- package/dist/datatrans.js +178 -0
- package/dist/ekashu.d.ts +35 -0
- package/dist/ekashu.js +55 -0
- package/dist/elavon-converge.d.ts +37 -0
- package/dist/elavon-converge.generated.d.ts +49 -0
- package/dist/elavon-converge.generated.js +439 -0
- package/dist/elavon-converge.js +84 -0
- package/dist/eprotect.d.ts +152 -0
- package/dist/eprotect.generated.d.ts +77 -0
- package/dist/eprotect.generated.js +498 -0
- package/dist/eprotect.js +222 -0
- package/dist/hosted-form.d.ts +3 -2
- package/dist/hosted-form.js +3 -2
- package/dist/index.d.ts +17 -3
- package/dist/index.js +6 -1
- package/dist/juspay-gate.d.ts +82 -0
- package/dist/juspay-gate.js +126 -0
- package/dist/juspay.generated.d.ts +89 -0
- package/dist/juspay.generated.js +705 -0
- package/dist/lifecycle.d.ts +60 -6
- package/dist/lifecycle.js +122 -8
- package/dist/merchant-card-endpoints.generated.d.ts +122 -0
- package/dist/merchant-card-endpoints.generated.js +4261 -0
- package/dist/merchant-hosted.d.ts +4 -1
- package/dist/merchant-hosted.js +5 -2
- package/dist/merchant-session-probe.d.ts +48 -0
- package/dist/merchant-session-probe.js +53 -0
- package/dist/merchant-session.generated.d.ts +69 -0
- package/dist/merchant-session.generated.js +919 -0
- package/dist/payrails.d.ts +133 -0
- package/dist/payrails.generated.d.ts +73 -0
- package/dist/payrails.generated.js +596 -0
- package/dist/payrails.js +305 -0
- package/dist/pelecard.d.ts +32 -0
- package/dist/pelecard.generated.d.ts +32 -0
- package/dist/pelecard.generated.js +283 -0
- package/dist/pelecard.js +42 -0
- package/dist/pre-claim.d.ts +81 -2
- package/dist/pre-claim.js +1023 -5
- package/dist/preflight-catalog.json +2267 -288
- package/dist/preflight-schemas.json +183 -6
- package/dist/preflight.generated.d.ts +1 -1
- package/dist/preflight.generated.js +243 -4
- package/dist/preparation.d.ts +12 -3
- package/dist/preparation.js +63 -12
- package/dist/prepared-processor.d.ts +84 -5
- package/dist/prepared-processor.js +147 -6
- package/dist/primer.d.ts +53 -0
- package/dist/primer.generated.d.ts +38 -0
- package/dist/primer.generated.js +624 -0
- package/dist/primer.js +82 -0
- package/dist/registry.d.ts +100 -8
- package/dist/registry.js +85 -11
- package/dist/shiji-sts.d.ts +144 -0
- package/dist/shiji-sts.generated.d.ts +72 -0
- package/dist/shiji-sts.generated.js +913 -0
- package/dist/shiji-sts.js +222 -0
- package/dist/stripe-checkout.generated.d.ts +1 -1
- package/dist/stripe-checkout.generated.js +764 -9
- package/dist/stripe-setup.d.ts +16 -0
- package/dist/stripe-setup.js +61 -0
- package/dist/substitutions.generated.js +187 -29
- package/dist/verifone.d.ts +68 -0
- package/dist/verifone.generated.d.ts +51 -0
- package/dist/verifone.generated.js +1413 -0
- package/dist/verifone.js +119 -0
- package/dist/voice-types.d.ts +27 -0
- package/dist/voice.d.ts +6 -1
- package/dist/voice.js +6 -1
- package/dist/wspay.d.ts +41 -0
- package/dist/wspay.generated.d.ts +41 -0
- package/dist/wspay.generated.js +370 -0
- package/dist/wspay.js +65 -0
- package/examples/preflight/kernel-native/inventory.json +1 -1
- 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):
|
|
2
|
-
declare function
|
|
3
|
-
|
|
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 };
|