@agent-cards/sdk 0.22.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 +46 -8
- package/PREFLIGHT.md +3 -1
- package/README.md +120 -15
- package/dist/adyen-merchant-hosted.generated.js +20 -8
- 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 +132 -11
- package/dist/card-endpoint.js +169 -17
- package/dist/cdp.js +336 -28
- package/dist/client.d.ts +61 -9
- package/dist/client.js +133 -16
- package/dist/creditguard.generated.d.ts +1 -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/ekashu.d.ts +35 -0
- package/dist/ekashu.js +55 -0
- package/dist/lifecycle.d.ts +7 -4
- package/dist/lifecycle.js +6 -3
- package/dist/merchant-card-endpoints.generated.d.ts +60 -1
- package/dist/merchant-card-endpoints.generated.js +3253 -95
- package/dist/pre-claim.d.ts +9 -0
- package/dist/pre-claim.js +179 -13
- package/dist/preflight-catalog.json +48 -0
- package/dist/preflight-schemas.json +4 -0
- package/dist/preflight.generated.js +11 -1
- package/dist/preparation.d.ts +7 -1
- package/dist/preparation.js +27 -2
- package/dist/stripe-checkout.generated.js +66 -1
- package/dist/verifone.generated.js +20 -8
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +2 -2
package/dist/card-endpoint.js
CHANGED
|
@@ -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
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
|
13
|
-
//
|
|
14
|
-
// the
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
99
|
-
*
|
|
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
|
-
|
|
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;
|