@agent-cards/sdk 0.23.6 → 0.23.7
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 +7 -1
- package/PREFLIGHT.md +1 -1
- package/README.md +146 -4
- package/dist/amadeus.d.ts +16 -4
- package/dist/amadeus.js +51 -16
- package/dist/cdp.js +183 -26
- package/dist/client.d.ts +22 -0
- package/dist/client.js +165 -13
- package/dist/creditguard.generated.d.ts +1 -1
- package/dist/cybersource-secure-acceptance.d.ts +142 -34
- package/dist/cybersource-secure-acceptance.generated.d.ts +66 -0
- package/dist/cybersource-secure-acceptance.generated.js +955 -0
- package/dist/cybersource-secure-acceptance.js +244 -3
- package/dist/index.d.ts +1 -0
- package/dist/juspay.generated.d.ts +1 -1
- package/dist/pre-claim.d.ts +3 -1
- package/dist/pre-claim.js +85 -6
- package/dist/preparation.js +15 -2
- package/dist/prepared-processor.d.ts +19 -5
- package/dist/prepared-processor.js +70 -3
- package/dist/registry.js +14 -1
- package/dist/stripe-checkout.generated.js +35 -1
- package/package.json +1 -1
|
@@ -28,10 +28,14 @@
|
|
|
28
28
|
* and holds, as every unsupported refusal does;
|
|
29
29
|
* - no card_number, or an empty one, as in a payment with a stored payment_token: continue untouched;
|
|
30
30
|
* - a body the adapter could not read: stop that request alone, since nothing proves it carries no card.
|
|
31
|
-
* A request the synced registry recognizes as a card request belongs to
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* A request the synced registry recognizes as a card request belongs to the Secure Acceptance lane
|
|
32
|
+
* instead (below): the API serves that recognizer only to a build that declares
|
|
33
|
+
* `cybersource_sa_token_create` and only for a company Agentcard turned the lane on for, and it covers
|
|
34
|
+
* exactly the token create, POST https://<host>/silent/embedded/token/create on the production or the
|
|
35
|
+
* test host. Every other company, and every other /silent/ path, keeps the stop above. Hosted Checkout,
|
|
36
|
+
* the same hosts without /silent/, takes the card on CyberSource's own page, and neither adapter arms it.
|
|
34
37
|
*/
|
|
38
|
+
import { SECURE_ACCEPTANCE_MERCHANTS, SECURE_ACCEPTANCE_PAGE_CONTENT_TYPE, isSecureAcceptanceTokenRequest, secureAcceptanceMerchantEntry, secureAcceptanceReplyOutcome, } from './cybersource-secure-acceptance.generated.js';
|
|
35
39
|
export const CYBERSOURCE_SECURE_ACCEPTANCE_REASON = 'unsupported_cybersource_secure_acceptance';
|
|
36
40
|
/** Where the stopped page says the card was going. */
|
|
37
41
|
export const CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT = 'CyberSource';
|
|
@@ -212,3 +216,240 @@ function formFields(body) {
|
|
|
212
216
|
fields.push(pair);
|
|
213
217
|
return fields;
|
|
214
218
|
}
|
|
219
|
+
// ---- The Secure Acceptance lane (dark launch) ----
|
|
220
|
+
//
|
|
221
|
+
// For a company Agentcard turned it on for, the API serves one recognizer (payment-core definitions.js,
|
|
222
|
+
// capability cybersource_sa_token_create) for one request: POST /silent/embedded/token/create on
|
|
223
|
+
// secureacceptance.cybersource.com (production) or testsecureacceptance.cybersource.com (sandbox), a
|
|
224
|
+
// create_payment_token at a signed amount of 0, with the merchant's signed fields and the four unsigned
|
|
225
|
+
// card fields. The rules that judge that request and CyberSource's reply live in payment-core and reach
|
|
226
|
+
// this SDK as cybersource-secure-acceptance.generated.ts; the API and the cardholder's device run the same
|
|
227
|
+
// ones. The lane pays the request only through a prepared checkout, approved before the agent clicks
|
|
228
|
+
// Save, and only as the merchant's hidden frame sends it:
|
|
229
|
+
// - the pre-claim gate holds a card-bearing post to that URL to a navigation of a frame (pre-claim.ts);
|
|
230
|
+
// a top navigation, a fetch or an XHR is stopped as an unrecognized post is;
|
|
231
|
+
// - the claim binds the preparation only to the request payment-core admits for the preparation's
|
|
232
|
+
// environment, profile id and page origin (prepared-processor.ts);
|
|
233
|
+
// - the cardholder's device sends the real card to CyberSource through the checkout relay, and the API
|
|
234
|
+
// hands back CyberSource's own reply page once neither it nor the device finds a card in it;
|
|
235
|
+
// - the adapters answer the paused frame navigation with that page, status 200 and its one content type
|
|
236
|
+
// (secureAcceptanceFrameAnswer), so Chrome commits it as CyberSource's own document and its auto-post
|
|
237
|
+
// reaches the merchant's receipt page in the agent's own session, as it would with no Agentcard.
|
|
238
|
+
// The clocks fit Arrow's page, which asks its frame 30 times, a second apart, from Save: reads every
|
|
239
|
+
// 500 ms for 30 s after the create; a pause with nothing being handed over 20 s after it is retired (the
|
|
240
|
+
// send window, amadeus.ts armSendWindow); a reply not read 45 s after the pause is an unknown outcome;
|
|
241
|
+
// and once the page is delivered, a merchant whose reviewed entry names its next request is watched for
|
|
242
|
+
// it for 10 s.
|
|
243
|
+
/** The processor id, as the registry and the API name it. */
|
|
244
|
+
export const SECURE_ACCEPTANCE_PSP = 'cybersource_secure_acceptance';
|
|
245
|
+
/** The dark-launch capability the API serves the Secure Acceptance recognizer behind (payment-core definitions.js). */
|
|
246
|
+
export const SECURE_ACCEPTANCE_CAPABILITY = 'cybersource_sa_token_create';
|
|
247
|
+
/** How long after the pause a paused token request may still be sent; past it, nothing being handed over, it is retired. */
|
|
248
|
+
export const SECURE_ACCEPTANCE_SEND_WINDOW_MS = 20_000;
|
|
249
|
+
/** How long after the pause CyberSource's reply may still be read; past it the outcome is unknown. */
|
|
250
|
+
export const SECURE_ACCEPTANCE_REPLY_WINDOW_MS = 45_000;
|
|
251
|
+
/** How long after the create the authorization is read every 500 ms instead of at the caller's interval. */
|
|
252
|
+
export const SECURE_ACCEPTANCE_FAST_POLL_MS = 30_000;
|
|
253
|
+
/** How long after the page is delivered the merchant's follow-up request is watched for. */
|
|
254
|
+
export const SECURE_ACCEPTANCE_FOLLOW_UP_MS = 10_000;
|
|
255
|
+
/** The one content type the paused frame's answer carries, as CyberSource serves its page. */
|
|
256
|
+
export const SECURE_ACCEPTANCE_ANSWER_CONTENT_TYPE = SECURE_ACCEPTANCE_PAGE_CONTENT_TYPE;
|
|
257
|
+
/** A CyberSource Secure Acceptance profile id: a UUID shape, 36 characters, hex in either case. */
|
|
258
|
+
const PROFILE_ID = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/;
|
|
259
|
+
/** This build's reviewed entries, or the ones a test passes. */
|
|
260
|
+
function reviewedEntries(entries) {
|
|
261
|
+
return entries ?? SECURE_ACCEPTANCE_MERCHANTS;
|
|
262
|
+
}
|
|
263
|
+
/** The reviewed entry that lists this profile id for this origin, through payment-core's own lookup, or null. */
|
|
264
|
+
function entryFor(profileId, merchantOrigin, entries) {
|
|
265
|
+
try {
|
|
266
|
+
return secureAcceptanceMerchantEntry(profileId, merchantOrigin, entries) ?? null;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The profile id a Secure Acceptance checkout prepares, or null when it can name none. A profile id the
|
|
274
|
+
* caller passes must have CyberSource's UUID shape, and is the one the bound request must carry. On the
|
|
275
|
+
* production host the caller may leave it out when exactly one entry this build reviewed lists the
|
|
276
|
+
* checkout page's origin, under exactly one profile id: that one is used, so the agent names nothing it
|
|
277
|
+
* cannot see. The test host has no entries (it takes only CyberSource's test cards), so a sandbox
|
|
278
|
+
* checkout always names its own. `entries` is for tests.
|
|
279
|
+
*/
|
|
280
|
+
export function secureAcceptanceCheckoutProfile(input, entries) {
|
|
281
|
+
if (input.profileId !== undefined)
|
|
282
|
+
return typeof input.profileId === 'string' && PROFILE_ID.test(input.profileId) ? input.profileId : null;
|
|
283
|
+
if (input.environment !== 'production')
|
|
284
|
+
return null;
|
|
285
|
+
const listing = reviewedEntries(entries).filter((entry) => entry.environment === 'production' && entry.origins.includes(input.merchantOrigin));
|
|
286
|
+
if (listing.length !== 1 || listing[0].profileIds.length !== 1)
|
|
287
|
+
return null;
|
|
288
|
+
const [profileId] = listing[0].profileIds;
|
|
289
|
+
return typeof profileId === 'string' && PROFILE_ID.test(profileId) ? profileId : null;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Why a Secure Acceptance checkout may not be prepared under `merchant`, or null when it may:
|
|
293
|
+
* `secure_acceptance_profile_id_invalid` when it names no profile id this build can stand behind
|
|
294
|
+
* (secureAcceptanceCheckoutProfile), and, on the production host, `merchant_profile_mismatch` when the
|
|
295
|
+
* entry this build reviewed for that profile id and origin names another merchant, the one the approval
|
|
296
|
+
* shows. A profile id this build has not reviewed is the API's to judge (unsupported_checkout), as an
|
|
297
|
+
* eProtect paypageId is. `entries` is for tests.
|
|
298
|
+
*/
|
|
299
|
+
export function secureAcceptanceCheckoutRefusal(input, entries) {
|
|
300
|
+
const profileId = secureAcceptanceCheckoutProfile(input, entries);
|
|
301
|
+
if (profileId === null)
|
|
302
|
+
return 'secure_acceptance_profile_id_invalid';
|
|
303
|
+
if (input.environment !== 'production')
|
|
304
|
+
return null;
|
|
305
|
+
const entry = entryFor(profileId, input.merchantOrigin, entries);
|
|
306
|
+
return entry && entry.merchant !== input.merchant ? 'merchant_profile_mismatch' : null;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Whether a paused request is the one shape the lane pays: a POST to the exact token-create URL on one of
|
|
310
|
+
* the two hosts (payment-core isSecureAcceptanceTokenRequest), sent as a navigation of a frame, as a
|
|
311
|
+
* merchant's form targeted at its hidden iframe sends it. A top navigation, a fetch and an XHR are not.
|
|
312
|
+
*/
|
|
313
|
+
export function isSecureAcceptanceFramePost(request) {
|
|
314
|
+
if (request.navigation !== 'frame')
|
|
315
|
+
return false;
|
|
316
|
+
try {
|
|
317
|
+
return isSecureAcceptanceTokenRequest(request.url, String(request.method).toUpperCase()) === true;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The answer a paused token request gets, or null when the approved reply is not one to hand the frame:
|
|
325
|
+
* CyberSource's own page, byte for byte as the API stored it, with status 200 and the one content type
|
|
326
|
+
* CyberSource serves it with, and no other header. The page must pass payment-core's reply rule for the
|
|
327
|
+
* paused request (secureAcceptanceReplyOutcome: one hidden-input form posting to the receipt page the
|
|
328
|
+
* request signed, the request's own echoes, a masked number, a token, no card), run here with no card and
|
|
329
|
+
* no last four: the device and the API checked those.
|
|
330
|
+
*/
|
|
331
|
+
export function secureAcceptanceFrameAnswer(request, response) {
|
|
332
|
+
if (!request || typeof request.url !== 'string' || typeof request.body !== 'string' || !response || typeof response.body !== 'string')
|
|
333
|
+
return null;
|
|
334
|
+
let verdict;
|
|
335
|
+
try {
|
|
336
|
+
// No card and no last four: the device and the API checked the page against the card.
|
|
337
|
+
verdict = secureAcceptanceReplyOutcome({ status: response.status, headers: { ...(response.headers ?? {}) }, bodyText: response.body }, { url: request.url, body: request.body }, undefined, undefined);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
if (verdict?.outcome !== 'token' || verdict.page !== response.body)
|
|
343
|
+
return null;
|
|
344
|
+
return { status: 200, headers: { 'content-type': SECURE_ACCEPTANCE_ANSWER_CONTENT_TYPE }, body: response.body };
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* The request a reviewed merchant's page sends once its receipt page answered (Arrow: the payment-method
|
|
348
|
+
* set), for a production preparation whose profile id and origin an entry lists with one; null otherwise,
|
|
349
|
+
* the test host included. `entries` is for tests.
|
|
350
|
+
*/
|
|
351
|
+
export function secureAcceptanceFollowUp(preparation, entries) {
|
|
352
|
+
if (preparation.environment !== 'production' || typeof preparation.secureAcceptanceProfileId !== 'string')
|
|
353
|
+
return null;
|
|
354
|
+
const followUp = entryFor(preparation.secureAcceptanceProfileId, preparation.merchantOrigin, entries)?.followUp;
|
|
355
|
+
if (!followUp || typeof followUp.method !== 'string' || typeof followUp.url !== 'string')
|
|
356
|
+
return null;
|
|
357
|
+
try {
|
|
358
|
+
return { method: followUp.method.toUpperCase(), url: new URL(followUp.url).href };
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const INPUT_TAG = /<input\b((?:[^>"']|"[^"]*"|'[^']*')*)>/gi;
|
|
365
|
+
const ATTRIBUTE = /([^\s=/>"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
366
|
+
const NAMED_ENTITIES = { amp: '&', quot: '"', apos: "'", lt: '<', gt: '>' };
|
|
367
|
+
function decodeEntities(value) {
|
|
368
|
+
return value.replace(/&(#x[0-9a-f]{1,6}|#[0-9]{1,7}|[a-z]+);/gi, (whole, entity) => {
|
|
369
|
+
if (entity[0] === '#') {
|
|
370
|
+
const code = entity[1] === 'x' || entity[1] === 'X' ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10);
|
|
371
|
+
return Number.isInteger(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : whole;
|
|
372
|
+
}
|
|
373
|
+
return NAMED_ENTITIES[entity.toLowerCase()] ?? whole;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* The last four digits of the masked number CyberSource's reply page echoes (req_card_number, masked as
|
|
378
|
+
* x's and the last four), or null when the page carries no single such field. The page passed the reply
|
|
379
|
+
* rule before it was delivered, so this only reads it back for the follow-up event; it never carries more
|
|
380
|
+
* than those four digits.
|
|
381
|
+
*/
|
|
382
|
+
export function secureAcceptanceReplyLast4(page) {
|
|
383
|
+
if (typeof page !== 'string')
|
|
384
|
+
return null;
|
|
385
|
+
const values = [];
|
|
386
|
+
for (const tag of page.matchAll(INPUT_TAG)) {
|
|
387
|
+
const attributes = new Map();
|
|
388
|
+
for (const attribute of tag[1].matchAll(ATTRIBUTE)) {
|
|
389
|
+
const name = attribute[1].toLowerCase();
|
|
390
|
+
if (!attributes.has(name))
|
|
391
|
+
attributes.set(name, decodeEntities(attribute[2] ?? attribute[3] ?? attribute[4] ?? ''));
|
|
392
|
+
}
|
|
393
|
+
if (attributes.get('name') === 'req_card_number')
|
|
394
|
+
values.push(attributes.get('value') ?? '');
|
|
395
|
+
}
|
|
396
|
+
return values.length === 1 ? /^[xX]{9,15}([0-9]{4})$/.exec(values[0])?.[1] ?? null : null;
|
|
397
|
+
}
|
|
398
|
+
/** A URL's origin and path, the part a follow-up request is matched on (its query is the page's own). */
|
|
399
|
+
function originAndPath(url) {
|
|
400
|
+
try {
|
|
401
|
+
const parsed = new URL(url);
|
|
402
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* One attachment's watch for the merchant's follow-up request after a delivered reply. Read-only: it
|
|
410
|
+
* pauses, changes and delays nothing. The adapters report every request the page sends (CDP
|
|
411
|
+
* Network.requestWillBeSent, Playwright page.on('request')) through saw(); when none matches the
|
|
412
|
+
* merchant's follow-up within SECURE_ACCEPTANCE_FOLLOW_UP_MS, the event `secure_acceptance_reply_unused`
|
|
413
|
+
* names the authorization and the masked number's last four, so the agent can find the card the merchant
|
|
414
|
+
* may have kept (in its saved cards, ending in those four) before it ever asks for a new checkout.
|
|
415
|
+
*/
|
|
416
|
+
export class SecureAcceptanceFollowUpWatch {
|
|
417
|
+
onEvent;
|
|
418
|
+
ms;
|
|
419
|
+
watching = null;
|
|
420
|
+
constructor(onEvent, ms = SECURE_ACCEPTANCE_FOLLOW_UP_MS) {
|
|
421
|
+
this.onEvent = onEvent;
|
|
422
|
+
this.ms = ms;
|
|
423
|
+
}
|
|
424
|
+
/** Start watching for `followUp` after the reply for `authorizationId` was delivered; a watch already running ends unreported. */
|
|
425
|
+
begin(followUp, detail) {
|
|
426
|
+
this.end();
|
|
427
|
+
const target = originAndPath(followUp.url);
|
|
428
|
+
if (target === null)
|
|
429
|
+
return;
|
|
430
|
+
const timer = setTimeout(() => {
|
|
431
|
+
if (this.watching?.timer !== timer)
|
|
432
|
+
return;
|
|
433
|
+
this.watching = null;
|
|
434
|
+
this.onEvent?.({ type: 'secure_acceptance_reply_unused', detail: { authorizationId: detail.authorizationId, last4: detail.last4 } });
|
|
435
|
+
}, this.ms);
|
|
436
|
+
timer.unref?.();
|
|
437
|
+
this.watching = { target, method: String(followUp.method).toUpperCase(), timer };
|
|
438
|
+
}
|
|
439
|
+
/** A request the page sent: the follow-up, when it is the one being watched for, ends the watch. */
|
|
440
|
+
saw(method, url) {
|
|
441
|
+
if (!this.watching || typeof method !== 'string' || typeof url !== 'string')
|
|
442
|
+
return;
|
|
443
|
+
if (method.toUpperCase() === this.watching.method && originAndPath(url) === this.watching.target)
|
|
444
|
+
this.end();
|
|
445
|
+
}
|
|
446
|
+
/** True while a watch runs. */
|
|
447
|
+
get active() { return this.watching !== null; }
|
|
448
|
+
/** End the watch with no event. */
|
|
449
|
+
end() {
|
|
450
|
+
if (!this.watching)
|
|
451
|
+
return;
|
|
452
|
+
clearTimeout(this.watching.timer);
|
|
453
|
+
this.watching = null;
|
|
454
|
+
}
|
|
455
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { CONVERGE_REFUSAL_REASONS } from './elavon-converge.js';
|
|
|
9
9
|
export type { ConvergeRefusalReason } from './elavon-converge.js';
|
|
10
10
|
export type { WspayRefusalReason } from './wspay.js';
|
|
11
11
|
export type { ShijiStsUnsupportedReason } from './shiji-sts.js';
|
|
12
|
+
export type { SecureAcceptanceCheckoutRefusal, SecureAcceptanceUnknownReason } from './cybersource-secure-acceptance.js';
|
|
12
13
|
export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, MerchantHostedReplay, VerifoneReplay, HostedFormReplay, CardEndpointReplay, DeviceEncryptReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, AgentcardOptions, AdyenTestPlatformRefusal, MerchantTotalRefusal, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, RazorpayProcessorError, RuntimeCancelReason, ExecutionMetadata, PlaceholderCard, } from './client.js';
|
|
13
14
|
export type { MicroformField, MicroformSidecar } from './cybersource-flex.js';
|
|
14
15
|
export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
|
|
@@ -20,7 +20,7 @@ declare function juspayTokenizeAnswer(token: any): {
|
|
|
20
20
|
};
|
|
21
21
|
body: string;
|
|
22
22
|
};
|
|
23
|
-
declare function juspayTxnsTemplateRefusal(url: any, headers: any, body: any): "card_token" | "format" | "merchant_id" | "card_security_code" | "merchant_mismatch" | "juspay_endpoint" | "
|
|
23
|
+
declare function juspayTxnsTemplateRefusal(url: any, headers: any, body: any): "content_type" | "card_token" | "format" | "merchant_id" | "card_security_code" | "merchant_mismatch" | "juspay_endpoint" | "unreadable_body" | "unsupported_field" | "missing_field" | "order_id" | "routing_id" | "payment_method_type" | "redirect_after_payment" | "save_to_locker" | "tokenize" | "card_nickname" | null;
|
|
24
24
|
declare function assertJuspayTxnsTemplate(url: any, headers: any, body: any): Readonly<{
|
|
25
25
|
environment: "production" | "sandbox" | undefined;
|
|
26
26
|
host: any;
|
package/dist/pre-claim.d.ts
CHANGED
|
@@ -35,7 +35,8 @@ export type PreClaimDecision = {
|
|
|
35
35
|
} | {
|
|
36
36
|
kind: 'answer';
|
|
37
37
|
}
|
|
38
|
-
/** `payrails`: the session key a bound Payrails card save carries onto the create.
|
|
38
|
+
/** `payrails`: the session key a bound Payrails card save carries onto the create. `secureAcceptance`: a CyberSource
|
|
39
|
+
* Secure Acceptance token create the lane held, a navigation of a frame, answered with a page whatever its outcome. */
|
|
39
40
|
| {
|
|
40
41
|
kind: 'pause';
|
|
41
42
|
stripeStep: StripeStep;
|
|
@@ -43,6 +44,7 @@ export type PreClaimDecision = {
|
|
|
43
44
|
cardEndpoint?: CardEndpointProfile;
|
|
44
45
|
payrails?: PayrailsSidecar;
|
|
45
46
|
navigation?: 'top' | 'frame';
|
|
47
|
+
secureAcceptance?: true;
|
|
46
48
|
};
|
|
47
49
|
/** The adapter mechanics a gate performs. Each is awaited where the inline code
|
|
48
50
|
* awaited it; none swallow their own transport failure (a gate adds a .catch
|
package/dist/pre-claim.js
CHANGED
|
@@ -76,7 +76,7 @@ import { PAYRAILS_HOLDING_REASONS, PAYRAILS_SESSION_MESSAGE, isPayrailsHost, isP
|
|
|
76
76
|
import { classifyVerifoneProfileRequest, verifoneTemplatedUrl } from './verifone.js';
|
|
77
77
|
import { ekashuEntryUrl, isEkashuEntry, isEkashuHost } from './ekashu.js';
|
|
78
78
|
import { PAYPAL_HOSTED_FIELDS_REASON, isPaypalHostedFieldsConfirm, paypalHostedFieldsBody } from './paypal-hosted-fields.js';
|
|
79
|
-
import { CYBERSOURCE_SECURE_ACCEPTANCE_REASON, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, cybersourceSecureAcceptanceRequest, isCybersourceSecureAcceptanceUrl, } from './cybersource-secure-acceptance.js';
|
|
79
|
+
import { CYBERSOURCE_SECURE_ACCEPTANCE_REASON, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, cybersourceSecureAcceptanceRequest, isCybersourceSecureAcceptanceUrl, isSecureAcceptanceFramePost, } from './cybersource-secure-acceptance.js';
|
|
80
80
|
/**
|
|
81
81
|
* The URL an event may name for a request. For an armed merchant profile's
|
|
82
82
|
* endpoint or a sibling of it: the endpoint origin, the path TEMPLATE and the
|
|
@@ -1550,20 +1550,99 @@ async function paypalHostedFieldsUnreadable(ctx) {
|
|
|
1550
1550
|
// proves it carries no card;
|
|
1551
1551
|
// - any other request (a payment with a stored payment_token and no card, a GET, a CORS preflight)
|
|
1552
1552
|
// defers, and the arbiter continues it untouched.
|
|
1553
|
-
// A request the synced registry recognizes as a card request
|
|
1554
|
-
//
|
|
1555
|
-
//
|
|
1553
|
+
// A request the synced registry recognizes as a card request is the Secure Acceptance lane's
|
|
1554
|
+
// (cybersource-secure-acceptance.ts): the API serves that recognizer only to a build that declares
|
|
1555
|
+
// cybersource_sa_token_create, for a company Agentcard turned the lane on for, and only for the exact token
|
|
1556
|
+
// create on the production or the test host. Before it pauses, the gate holds such a request to the one
|
|
1557
|
+
// shape the lane pays:
|
|
1558
|
+
// - a card post that a merchant's form submits into a frame (a navigation of a frame, as Arrow's hidden
|
|
1559
|
+
// secureAcceptanceiFrame is) pauses for the claim flow, carrying the mark that answers it with a page
|
|
1560
|
+
// whatever its outcome. With no preparation on this attachment it is not paused: it gets the stopped
|
|
1561
|
+
// page, `stopped_before_send`, with a `blocked` event, `preparation_required`, and nothing reaches the
|
|
1562
|
+
// API, since only a prepared checkout pays it;
|
|
1563
|
+
// - a card post as the page's own navigation, from a fetch or from an XHR, or one whose card field the
|
|
1564
|
+
// recognizer reads under no name of its own (another spelling of card_number, which would otherwise
|
|
1565
|
+
// continue as card-free), is stopped as the post to an unrecognized /silent/ path is;
|
|
1566
|
+
// - a body the adapter could not read is stopped alone, as above;
|
|
1567
|
+
// - a body with no card number defers, and continues untouched (the recognizer's passWithoutCard).
|
|
1568
|
+
// A path outside /silent/ is not this gate's. Synchronous until it must read the frame or stop a request,
|
|
1569
|
+
// like every gate. Exported for the gate registry's tests.
|
|
1556
1570
|
export const cybersourceSecureAcceptancePreClaim = (ctx) => {
|
|
1557
1571
|
const { url, method } = ctx.request;
|
|
1558
|
-
if (!isCybersourceSecureAcceptanceUrl(url)
|
|
1572
|
+
if (!isCybersourceSecureAcceptanceUrl(url))
|
|
1559
1573
|
return undefined;
|
|
1574
|
+
const recognized = ctx.vault.isCardRequest(url, method);
|
|
1560
1575
|
// A reader that throws (Playwright's postData() can) reads as a body the adapter could not read.
|
|
1561
1576
|
const { body, contentType } = cardEndpointPausedBody(ctx);
|
|
1562
1577
|
const carried = cybersourceSecureAcceptanceRequest(url, method, body, contentType);
|
|
1563
1578
|
if (carried === 'no_card')
|
|
1564
1579
|
return undefined;
|
|
1565
|
-
|
|
1580
|
+
if (carried === 'unreadable')
|
|
1581
|
+
return cybersourceSecureAcceptanceUnreadable(ctx);
|
|
1582
|
+
if (!recognized)
|
|
1583
|
+
return cybersourceSecureAcceptanceUnsupported(ctx);
|
|
1584
|
+
// The recognizer's own reading must find the card this gate found, or the adapter would continue the
|
|
1585
|
+
// post as card-free (passWithoutCard) with the agent's card in it.
|
|
1586
|
+
if ((ctx.vault.withoutCard?.(url, body) ?? null) !== null)
|
|
1587
|
+
return cybersourceSecureAcceptanceUnsupported(ctx);
|
|
1588
|
+
return secureAcceptanceHold(ctx);
|
|
1566
1589
|
};
|
|
1590
|
+
/**
|
|
1591
|
+
* The lane's hold on a recognized card post: a navigation of a frame pauses, marked, unless no
|
|
1592
|
+
* preparation exists on an idle attachment; anything else is stopped as unsupported.
|
|
1593
|
+
*/
|
|
1594
|
+
async function secureAcceptanceHold(ctx) {
|
|
1595
|
+
let navigation = null;
|
|
1596
|
+
try {
|
|
1597
|
+
navigation = ctx.request.navigation === true ? await boundedNavigation(ctx) : null;
|
|
1598
|
+
}
|
|
1599
|
+
catch {
|
|
1600
|
+
navigation = null;
|
|
1601
|
+
}
|
|
1602
|
+
if (!isSecureAcceptanceFramePost({ url: ctx.request.url, method: ctx.request.method, navigation }))
|
|
1603
|
+
return cybersourceSecureAcceptanceUnsupported(ctx);
|
|
1604
|
+
const idle = ctx.attachmentReady() && !ctx.stripeAttachmentBusy() && !ctx.interceptionAborted() && !ctx.terminal()
|
|
1605
|
+
&& !ctx.lifecycle.isBlocked() && !ctx.awaitingApproval();
|
|
1606
|
+
if (idle && !ctx.preparationGate.isEngaged())
|
|
1607
|
+
return secureAcceptancePreparationRequired(ctx);
|
|
1608
|
+
return { kind: 'pause', stripeStep: ctx.stripeStep, navigation: 'frame', secureAcceptance: true };
|
|
1609
|
+
}
|
|
1610
|
+
/** How long the lane waits for the adapter to say which frame a paused navigation is for. */
|
|
1611
|
+
const SECURE_ACCEPTANCE_NAVIGATION_READ_MS = 2_000;
|
|
1612
|
+
/**
|
|
1613
|
+
* Which frame a paused navigation is for, as the adapter reads it, or null when it cannot say within
|
|
1614
|
+
* SECURE_ACCEPTANCE_NAVIGATION_READ_MS. A read that does not answer (Chromium does not answer a raw CDP
|
|
1615
|
+
* frame tree read on a session whose own top-level navigation is paused) stops the post as not the
|
|
1616
|
+
* reviewed shape, rather than leaving it paused.
|
|
1617
|
+
*/
|
|
1618
|
+
async function boundedNavigation(ctx) {
|
|
1619
|
+
if (typeof ctx.navigation !== 'function')
|
|
1620
|
+
return null;
|
|
1621
|
+
let timer;
|
|
1622
|
+
const late = new Promise((resolve) => { timer = setTimeout(() => resolve(null), SECURE_ACCEPTANCE_NAVIGATION_READ_MS); });
|
|
1623
|
+
try {
|
|
1624
|
+
return (await Promise.race([ctx.navigation().catch(() => null), late])) ?? null;
|
|
1625
|
+
}
|
|
1626
|
+
finally {
|
|
1627
|
+
clearTimeout(timer);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* A held frame post with no preparation on this attachment: refused before any pause, prompt or
|
|
1632
|
+
* authorization, as a Fiserv capture is, and answered with the stopped page naming CyberSource. A later
|
|
1633
|
+
* prepare() is refused too, as after any first card request.
|
|
1634
|
+
*/
|
|
1635
|
+
async function secureAcceptancePreparationRequired(ctx) {
|
|
1636
|
+
ctx.preparationGate.observeUnprepared();
|
|
1637
|
+
ctx.lifecycle.preparationRequired();
|
|
1638
|
+
ctx.onEvent?.({ type: 'blocked', detail: 'preparation_required' });
|
|
1639
|
+
try {
|
|
1640
|
+
return await stopNavigationBeforeSend(ctx, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, 'not_approved');
|
|
1641
|
+
}
|
|
1642
|
+
catch { /* the page could not be answered: fail the request below */ }
|
|
1643
|
+
await ctx.effects.abort().catch(() => { });
|
|
1644
|
+
return { kind: 'abort' };
|
|
1645
|
+
}
|
|
1567
1646
|
async function cybersourceSecureAcceptanceUnsupported(ctx) {
|
|
1568
1647
|
const { url, method } = ctx.request;
|
|
1569
1648
|
ctx.preparationGate.invalidate('unsupported_checkout');
|
package/dist/preparation.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CheckoutPreparationError } from './client.js';
|
|
2
2
|
import { amountRefusal, validAmountInput } from './client.js';
|
|
3
3
|
import { matchesPreparation, validPreparationEnvironment, preparationEndpoint, preparationBindsByShape, preparationRequestMethod, preparedRequestRefusal } from './prepared-processor.js';
|
|
4
|
+
import { SECURE_ACCEPTANCE_PSP, secureAcceptanceCheckoutProfile } from './cybersource-secure-acceptance.js';
|
|
4
5
|
/** A local, one-use rendezvous. It never starts or retries a merchant request. */
|
|
5
6
|
export class PreparationGate {
|
|
6
7
|
opts;
|
|
@@ -106,6 +107,13 @@ export class PreparationGate {
|
|
|
106
107
|
const page = new URL(this.documentUrl);
|
|
107
108
|
if (!(page.protocol === 'https:' || (page.protocol === 'http:' && page.hostname === 'localhost')) || page.username || page.password)
|
|
108
109
|
throw new CheckoutPreparationError(null, 'merchant_origin_invalid');
|
|
110
|
+
// A CyberSource Secure Acceptance checkout names the profile id its token create must carry: the one
|
|
111
|
+
// passed, or on the production host the one this build's reviewed entry for the page's origin pins
|
|
112
|
+
// (secureAcceptanceCheckoutProfile). prepareCheckout refuses a checkout that can name none, and the
|
|
113
|
+
// handle it returns must carry exactly this one.
|
|
114
|
+
const secureAcceptanceProfileId = options.psp === SECURE_ACCEPTANCE_PSP
|
|
115
|
+
? secureAcceptanceCheckoutProfile({ environment: options.environment, profileId: options.secureAcceptanceProfileId, merchantOrigin: page.origin }) ?? undefined
|
|
116
|
+
: undefined;
|
|
109
117
|
const prepared = await this.opts.vault.prepareCheckout({
|
|
110
118
|
...options, user: this.opts.user, merchant: this.opts.merchant,
|
|
111
119
|
amount: this.opts.amount, currency: this.opts.currency, cardId: this.opts.cardId,
|
|
@@ -125,8 +133,10 @@ export class PreparationGate {
|
|
|
125
133
|
this.prepared = prepared;
|
|
126
134
|
// A Fiserv preparation also names the key pin the cardholder approves (merchantProfile).
|
|
127
135
|
// One that tokenizes before Pay names the capture context its cardholder approved under.
|
|
136
|
+
// A Secure Acceptance preparation names the profile id its cardholder approved for.
|
|
128
137
|
if (prepared.psp !== options.psp || prepared.environment !== options.environment
|
|
129
138
|
|| prepared.merchantProfile !== options.merchantProfile
|
|
139
|
+
|| prepared.secureAcceptanceProfileId !== secureAcceptanceProfileId
|
|
130
140
|
|| (prepared.cybersourceFlex?.captureContext ?? null) !== (cybersourceFlex?.captureContext ?? null))
|
|
131
141
|
throw new CheckoutPreparationError(prepared.id, 'checkout_changed');
|
|
132
142
|
if (signal.aborted || this.state !== 'preparing') {
|
|
@@ -169,11 +179,14 @@ export class PreparationGate {
|
|
|
169
179
|
// A Fiserv preparation claims only a capture under the key pin it names.
|
|
170
180
|
// A CreditGuard preparation claims only a submit to the page it framed.
|
|
171
181
|
// A Worldpay eProtect preparation claims only its registration GET naming the paypageId it carries.
|
|
182
|
+
// A CyberSource Secure Acceptance preparation claims only its token create naming the profile id it
|
|
183
|
+
// carries, sent from the page it approved (the request's own Origin header names it).
|
|
172
184
|
if (Date.parse(prepared.expiresAt) <= Date.now() || !matchesPreparation(prepared, requestUrl, requestMethod, requestBody, requestHeaders)) {
|
|
173
185
|
// An Amadeus request payment-core refuses names its own reason (preparedRequestRefusal), so
|
|
174
|
-
// Air France's split expiry reads amadeus_merchant_held_expiry rather than a changed checkout
|
|
186
|
+
// Air France's split expiry reads amadeus_merchant_held_expiry rather than a changed checkout,
|
|
187
|
+
// and a Secure Acceptance frame post with no Origin reads secure_acceptance_origin_header.
|
|
175
188
|
const reason = Date.parse(prepared.expiresAt) <= Date.now() ? 'expired'
|
|
176
|
-
: preparedRequestRefusal(prepared.psp, prepared.environment, requestUrl, requestBody, requestHeaders, prepared.merchantOrigin);
|
|
189
|
+
: preparedRequestRefusal(prepared.psp, prepared.environment, requestUrl, requestBody, requestHeaders, prepared.merchantOrigin, prepared.currency);
|
|
177
190
|
this.invalidate(reason);
|
|
178
191
|
throw new CheckoutPreparationError(prepared.id, reason);
|
|
179
192
|
}
|
|
@@ -11,8 +11,13 @@
|
|
|
11
11
|
* amadeus_checkout: prepared only. Its SDK tunnel abandons the add after 30 seconds, too soon
|
|
12
12
|
* for an approval after Pay. Its environments are its hosts: paypages (production) and
|
|
13
13
|
* paypages.test (sandbox).
|
|
14
|
+
*
|
|
15
|
+
* cybersource_secure_acceptance: prepared only. The merchant's page asks its hidden frame for
|
|
16
|
+
* CyberSource's reply 30 times, a second apart, from Save (Arrow Electronics), too soon for an
|
|
17
|
+
* approval after Save. Its environments are its two hosts: secureacceptance (production) and
|
|
18
|
+
* testsecureacceptance (sandbox).
|
|
14
19
|
*/
|
|
15
|
-
export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen' | 'checkout_com' | 'paysafe' | 'fiserv' | 'cybersource_flex' | 'aci' | 'creditguard' | 'worldpay_eprotect' | 'amadeus_checkout' | 'verifone';
|
|
20
|
+
export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen' | 'checkout_com' | 'paysafe' | 'fiserv' | 'cybersource_flex' | 'aci' | 'creditguard' | 'worldpay_eprotect' | 'amadeus_checkout' | 'verifone' | 'cybersource_secure_acceptance';
|
|
16
21
|
export type PreparationEnvironment = 'production' | 'sandbox' | 'shared';
|
|
17
22
|
/**
|
|
18
23
|
* How the approved card reaches the processor: the device's own request (token), or
|
|
@@ -30,7 +35,8 @@ export declare function preparationRequestMethod(psp: string): 'GET' | 'POST';
|
|
|
30
35
|
* hosts too: connect (production) and connect-cert (sandbox). So are CyberSource
|
|
31
36
|
* Flex's: flex.cybersource.com is live, testflex.cybersource.com is test, and each has
|
|
32
37
|
* its own signing keys. A Verifone preparation pays in the environment of the key its reviewed
|
|
33
|
-
* profile pins.
|
|
38
|
+
* profile pins. CyberSource Secure Acceptance's are separate hosts too: secureacceptance is
|
|
39
|
+
* live, testsecureacceptance is test.
|
|
34
40
|
*/
|
|
35
41
|
export declare function validPreparationEnvironment(psp: string, environment: string): boolean;
|
|
36
42
|
/**
|
|
@@ -59,13 +65,16 @@ export declare function preparationEndpoint(psp: PreparationProcessor, environme
|
|
|
59
65
|
* What a preparation binds beyond its processor and environment. CreditGuard: the merchant page the
|
|
60
66
|
* cardholder approved (the submit's ppDomain names it) and the hosted page the checkout framed (the
|
|
61
67
|
* submit must pay that page's own transaction). Worldpay eProtect: the paypageId naming the Worldpay
|
|
62
|
-
* merchant the registration is for, fixed before its page sent anything.
|
|
63
|
-
*
|
|
68
|
+
* merchant the registration is for, fixed before its page sent anything. CyberSource Secure
|
|
69
|
+
* Acceptance: the profile id naming the CyberSource merchant profile the token is created for, and the
|
|
70
|
+
* page the cardholder approved, which the request's Origin, Referer and receipt page must be on. Every
|
|
71
|
+
* other processor ignores all four.
|
|
64
72
|
*/
|
|
65
73
|
export interface PreparedRequestBinding {
|
|
66
74
|
merchantOrigin?: string;
|
|
67
75
|
paymentPageUrl?: string;
|
|
68
76
|
paypageId?: string | null;
|
|
77
|
+
secureAcceptanceProfileId?: string | null;
|
|
69
78
|
}
|
|
70
79
|
/**
|
|
71
80
|
* The approved merchant page's host, with its port if it has one: what the Amadeus SDK sends in its
|
|
@@ -100,6 +109,9 @@ export declare function matchesPreparedMerchantRequest(profileId: string, reques
|
|
|
100
109
|
* A Worldpay eProtect preparation names the paypageId its registration must carry.
|
|
101
110
|
* An Amadeus preparation binds its SDK's Host header to the page the cardholder approved
|
|
102
111
|
* (merchantOrigin), as the API does.
|
|
112
|
+
* A CyberSource Secure Acceptance preparation names the profile id its token create must carry and
|
|
113
|
+
* the page it must come from, and its signed currency must be the approved one, as the API's create
|
|
114
|
+
* holds it.
|
|
103
115
|
*/
|
|
104
116
|
export declare function matchesPreparation(preparation: {
|
|
105
117
|
psp: PreparationProcessor;
|
|
@@ -111,6 +123,8 @@ export declare function matchesPreparation(preparation: {
|
|
|
111
123
|
merchantOrigin?: string;
|
|
112
124
|
paymentPageUrl?: string;
|
|
113
125
|
paypageId?: string;
|
|
126
|
+
secureAcceptanceProfileId?: string;
|
|
127
|
+
currency?: string;
|
|
114
128
|
}, requestUrl: string, method: string, body?: string | null, headers?: Record<string, string>): boolean;
|
|
115
129
|
/**
|
|
116
130
|
* The reason a claim reports for a request its preparation does not match. An Amadeus preparation
|
|
@@ -121,4 +135,4 @@ export declare function matchesPreparation(preparation: {
|
|
|
121
135
|
* header that does not name the approved page is `amadeus_header_binding`. Everything else, another
|
|
122
136
|
* processor's request or the other environment's host included, is `checkout_changed`.
|
|
123
137
|
*/
|
|
124
|
-
export declare function preparedRequestRefusal(psp: PreparationProcessor, environment: PreparationEnvironment, requestUrl: string, body?: string | null, headers?: Record<string, string>, merchantOrigin?: string | null): string;
|
|
138
|
+
export declare function preparedRequestRefusal(psp: PreparationProcessor, environment: PreparationEnvironment, requestUrl: string, body?: string | null, headers?: Record<string, string>, merchantOrigin?: string | null, currency?: string | null): string;
|