@agent-cards/sdk 0.23.20 → 0.23.21
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 +41 -2
- package/PREFLIGHT.md +2 -0
- package/README.md +143 -5
- package/dist/card-fields.generated.js +984 -57
- package/dist/cdp.js +146 -10
- package/dist/client.d.ts +104 -3
- package/dist/client.js +233 -23
- package/dist/comgate.d.ts +50 -0
- package/dist/comgate.js +80 -0
- package/dist/cybersource-secure-acceptance.generated.d.ts +1 -1
- package/dist/eprotect.generated.d.ts +1 -1
- package/dist/gmo.generated.js +7 -3
- package/dist/hipay.generated.d.ts +5 -5
- package/dist/hipay.generated.js +5 -2
- package/dist/hold.d.ts +1 -1
- package/dist/hold.js +10 -4
- package/dist/hosted-entry.d.ts +4 -0
- package/dist/hosted-entry.js +32 -0
- package/dist/index.d.ts +1 -0
- package/dist/paddle-tokenex.generated.js +7 -3
- package/dist/payrails.generated.js +7 -3
- package/dist/pre-claim.d.ts +7 -1
- package/dist/pre-claim.js +127 -20
- package/dist/preflight-catalog.json +58 -0
- package/dist/preflight-schemas.json +4 -0
- package/dist/preflight.generated.js +26 -2
- package/dist/primer.generated.js +1027 -100
- package/dist/przelewy24.d.ts +47 -0
- package/dist/przelewy24.js +88 -0
- package/dist/registry.d.ts +6 -5
- package/dist/registry.js +27 -7
- package/dist/shiji-sts.generated.js +7 -3
- package/dist/stripe-checkout.generated.js +621 -12
- package/dist/tokenex-iframe.d.ts +93 -0
- package/dist/tokenex-iframe.generated.d.ts +81 -0
- package/dist/tokenex-iframe.generated.js +403 -0
- package/dist/tokenex-iframe.js +195 -0
- package/dist/transbank.d.ts +45 -0
- package/dist/transbank.js +73 -0
- package/dist/twoc2p.generated.js +7 -3
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ReplayResponse } from './client.js';
|
|
2
|
+
/** The PSP id the API and the recognizer name this lane. */
|
|
3
|
+
export declare const TOKENEX_IFRAME_PSP: string;
|
|
4
|
+
/** The dark-launch capability the API serves the tokenex_iframe recognizer behind. */
|
|
5
|
+
export declare const TOKENEX_IFRAME_CAPABILITY: string;
|
|
6
|
+
/**
|
|
7
|
+
* Why the SDK refuses a TokenEx card post before anything is claimed, created or prompted: a post on the frame's
|
|
8
|
+
* /iframe/v3 path that carries a value in Data and is not the PCI card-number frame's own card post (a cvvOnly or
|
|
9
|
+
* PCI-only frame, a Token key, another key, order or type, a query this lane has not read or that names another
|
|
10
|
+
* account than the body, a URL variant or one longer than the recognizer reads, a content type other than JSON).
|
|
11
|
+
* The API answers the same code at create, so the refusal here only keeps the agent's placeholder from reaching
|
|
12
|
+
* TokenEx and saves the round trip.
|
|
13
|
+
*/
|
|
14
|
+
export type TokenexIframeRefusalReason = 'invalid_tokenex_iframe_request';
|
|
15
|
+
export declare const INVALID_TOKENEX_IFRAME_REQUEST: TokenexIframeRefusalReason;
|
|
16
|
+
/** True when the URL parses onto one of TokenEx's two iframe hosts and the /iframe/v3 path, however it is spelled or queried. */
|
|
17
|
+
export declare function isTokenexIframePath(url: string): boolean;
|
|
18
|
+
/** Whether this build's registry serves the TokenEx iframe: a company Agentcard turned it on for, on a build that declares it. */
|
|
19
|
+
export declare function servesTokenexIframe(vault: {
|
|
20
|
+
recognizesProcessor?(psp: string): boolean;
|
|
21
|
+
} | undefined): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Whether an adapter must hand this request to the pre-claim gates: it is on the frame's /iframe/v3 path and this
|
|
24
|
+
* build's registry serves the TokenEx iframe. The recognizer matches only the exact card URL, so a variant (another
|
|
25
|
+
* query, a case or dot spelling) would otherwise never reach the gate that refuses it. attachToPlaywright's route
|
|
26
|
+
* predicate asks this; attachToCdp arms the two hosts from the recognizer's own host list.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isArmedTokenexIframeUrl(vault: {
|
|
29
|
+
recognizesProcessor?(psp: string): boolean;
|
|
30
|
+
} | undefined, url: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The pause decision for a POST on the frame's path, by payment-core's classifyTokenexIframeRequest:
|
|
33
|
+
* 'card' the exact card URL with the JSON content type and the sound pcicvv card post bound to its query: it pauses;
|
|
34
|
+
* 'invalid_tokenex_iframe_request' a post with a value in Data that is not that: it is refused in the browser;
|
|
35
|
+
* null not a card post (no value in Data, another path, BinLookup, Forter): the SDK's ordinary handling
|
|
36
|
+
* decides, and an empty-Data post continues untouched through the recognizer's passWithoutCard.
|
|
37
|
+
* A URL longer than payment-core reads (over 2,048 characters) classifies as null, as an unrelated request does, and
|
|
38
|
+
* the exact recognizer refuses it too, so a post on the frame's own path that carries a value in Data is refused here
|
|
39
|
+
* rather than sent to TokenEx with the agent's placeholder.
|
|
40
|
+
*/
|
|
41
|
+
export declare function tokenexIframePauseDecision(request: {
|
|
42
|
+
url: string;
|
|
43
|
+
headers: Record<string, string>;
|
|
44
|
+
body: string | null;
|
|
45
|
+
}): 'card' | TokenexIframeRefusalReason | null;
|
|
46
|
+
/**
|
|
47
|
+
* The answer the paused card post may receive, rebuilt from payment-core's allow-list under a JSON content type:
|
|
48
|
+
* only TokenEx's 200 with a sound sixTOKENfour token (tokenexIframeResponseOutcome reads it as approved), keys TokenEx
|
|
49
|
+
* documents for a success and nothing else (sanitizeTokenexIframeResponse). Null for anything else: an error answer,
|
|
50
|
+
* another status, a body out of bounds. The frame adds its own `cardType` before the page reads the answer.
|
|
51
|
+
*/
|
|
52
|
+
export declare function tokenexIframePageAnswer(status: number, body: string): {
|
|
53
|
+
status: number;
|
|
54
|
+
headers: Record<string, string>;
|
|
55
|
+
body: string;
|
|
56
|
+
} | null;
|
|
57
|
+
/** Whether the frame reads `number` as the network `brand` (lowercase, as GET /api/v2/vault_cards names it). */
|
|
58
|
+
export declare function tokenexIframePlaceholderFits(brand: string, number: string): boolean;
|
|
59
|
+
/** The longest an iframe key lives, and so the longest a stored answer is handed out: TokenEx's ExpiresInSeconds maximum. */
|
|
60
|
+
export declare const TOKENEX_IFRAME_REPEAT_WINDOW_MS = 1200000;
|
|
61
|
+
/** A stored answer handed to a repeat, with the approval it came from. */
|
|
62
|
+
export interface TokenexIframeRepeatAnswer {
|
|
63
|
+
authorizationId: string;
|
|
64
|
+
response: {
|
|
65
|
+
status: number;
|
|
66
|
+
headers: Record<string, string>;
|
|
67
|
+
body: string;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The approved TokenEx answers one attachment handed to a card post, per frame. `frame` is the adapter's identity
|
|
72
|
+
* for the request's frame (the CDP frameId of the out-of-process iframe, the Playwright Frame); a request whose
|
|
73
|
+
* frame is unknown is never remembered and never answered. Only an answer payment-core reads as approved (a sound
|
|
74
|
+
* sixTOKENfour token) to the sound card post is kept: an error answer leaves the next post a new question, so the
|
|
75
|
+
* page can try again.
|
|
76
|
+
*/
|
|
77
|
+
export declare class TokenexIframeRepeats {
|
|
78
|
+
private readonly now;
|
|
79
|
+
private remembered;
|
|
80
|
+
constructor(now?: () => number);
|
|
81
|
+
/**
|
|
82
|
+
* Keep the answer a token replay handed to this card post, when TokenEx approved it. Called after the answer
|
|
83
|
+
* reached the browser, so it never throws: a failure here only leaves the page's next post a new question.
|
|
84
|
+
*/
|
|
85
|
+
remember(frame: unknown, url: string, body: string | null, replay: ReplayResponse): void;
|
|
86
|
+
/**
|
|
87
|
+
* The stored answer for the same frame's same post, byte for byte, or null. Only an answer from `authorizationId`,
|
|
88
|
+
* the approval the attachment is handing off now, is handed out, and every answer from another approval is
|
|
89
|
+
* dropped: a later authorization ends the window of every earlier one.
|
|
90
|
+
*/
|
|
91
|
+
answerFor(frame: unknown, url: string, body: string | null, authorizationId: string | null): TokenexIframeRepeatAnswer | null;
|
|
92
|
+
private prune;
|
|
93
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
declare var TOKENEX_IFRAME_PSP: string;
|
|
2
|
+
declare var TOKENEX_IFRAME_CAPABILITY: string;
|
|
3
|
+
declare var TOKENEX_IFRAME_OPERATION: string;
|
|
4
|
+
declare var TOKENEX_IFRAME_HOSTS: readonly string[];
|
|
5
|
+
declare var TOKENEX_IFRAME_PATH: string;
|
|
6
|
+
declare var TOKENEX_IFRAME_REFUSAL_REASONS: readonly string[];
|
|
7
|
+
declare var TOKENEX_IFRAME_BODY_KEYS: readonly string[];
|
|
8
|
+
declare var TOKENEX_IFRAME_TOKEN_SCHEMES: readonly string[];
|
|
9
|
+
declare var TOKENEX_IFRAME_REVIEWED_ACCOUNTS: Readonly<{
|
|
10
|
+
"htp.tokenex.com": Readonly<{}>;
|
|
11
|
+
"test-htp.tokenex.com": Readonly<{}>;
|
|
12
|
+
}>;
|
|
13
|
+
declare var TOKENEX_IFRAME_ACCOUNT_NAMES: Readonly<{
|
|
14
|
+
"htp.tokenex.com": Readonly<{}>;
|
|
15
|
+
"test-htp.tokenex.com": Readonly<{}>;
|
|
16
|
+
}>;
|
|
17
|
+
declare function tokenexIframeTarget(url: any): {
|
|
18
|
+
host: string;
|
|
19
|
+
query: Map<any, any>;
|
|
20
|
+
} | null;
|
|
21
|
+
declare function isTokenexIframeUrl(url: any, method?: string): boolean;
|
|
22
|
+
declare function tokenexIframeBodyRefusal(body: any): "unsupported" | null;
|
|
23
|
+
declare function tokenexIframeRequestRefusal(request: any): "unsupported" | null;
|
|
24
|
+
declare function isTokenexIframeRequest(request: any): boolean;
|
|
25
|
+
declare var TOKENEX_IFRAME_GATE_CODES: readonly string[];
|
|
26
|
+
declare function tokenexIframeBodyVerdict(body: any, url: any): {
|
|
27
|
+
verdict: string;
|
|
28
|
+
code?: undefined;
|
|
29
|
+
} | {
|
|
30
|
+
verdict: string;
|
|
31
|
+
code: string;
|
|
32
|
+
};
|
|
33
|
+
declare function classifyTokenexIframeRequest(url: any, method: any, body: any, headers: any): "card" | "unsupported" | null;
|
|
34
|
+
declare function tokenexIframeAccount(request: any): {
|
|
35
|
+
host: string;
|
|
36
|
+
tokenExId: any;
|
|
37
|
+
origin: any;
|
|
38
|
+
} | null;
|
|
39
|
+
declare function tokenexIframeAccountRefusal(request: any, accounts?: Readonly<{
|
|
40
|
+
"htp.tokenex.com": Readonly<{}>;
|
|
41
|
+
"test-htp.tokenex.com": Readonly<{}>;
|
|
42
|
+
}>): "unreviewed_account" | null;
|
|
43
|
+
declare function tokenexIframeAccountName(request: any, accounts?: Readonly<{
|
|
44
|
+
"htp.tokenex.com": Readonly<{}>;
|
|
45
|
+
"test-htp.tokenex.com": Readonly<{}>;
|
|
46
|
+
}>, names?: Readonly<{
|
|
47
|
+
"htp.tokenex.com": Readonly<{}>;
|
|
48
|
+
"test-htp.tokenex.com": Readonly<{}>;
|
|
49
|
+
}>): string | null;
|
|
50
|
+
declare function tokenexIframeCardType(number: any): string | RegExp | null;
|
|
51
|
+
declare function tokenexIframeBrandRefusal(body: any, card: any): "unsupported" | "brand_mismatch" | null;
|
|
52
|
+
declare function tokenexIframeDeviceRefusal(request: any, card: any, accounts?: Readonly<{
|
|
53
|
+
"htp.tokenex.com": Readonly<{}>;
|
|
54
|
+
"test-htp.tokenex.com": Readonly<{}>;
|
|
55
|
+
}>): "unsupported" | "brand_mismatch" | "unreviewed_account" | null;
|
|
56
|
+
declare function tokenexIframeRefusalMessage(reason: any): string;
|
|
57
|
+
declare function injectTokenexIframeCard(body: any, card: any): {
|
|
58
|
+
body: string;
|
|
59
|
+
where: string[];
|
|
60
|
+
};
|
|
61
|
+
declare function isTokenexIframeDeviceRequest(templateBody: any, deviceBody: any): boolean;
|
|
62
|
+
declare function tokenexIframeErrorCode(message: any): string | RegExp;
|
|
63
|
+
declare function tokenexIframeResponseOutcome(status: any, body: any, card: any): {
|
|
64
|
+
outcome: string;
|
|
65
|
+
code: string | RegExp;
|
|
66
|
+
reason?: undefined;
|
|
67
|
+
} | {
|
|
68
|
+
outcome: string;
|
|
69
|
+
reason: string | undefined;
|
|
70
|
+
code?: undefined;
|
|
71
|
+
} | {
|
|
72
|
+
outcome: string;
|
|
73
|
+
code?: undefined;
|
|
74
|
+
reason?: undefined;
|
|
75
|
+
};
|
|
76
|
+
declare function sanitizeTokenexIframeResponse(body: any): string;
|
|
77
|
+
declare function tokenexIframeFacts(status: any, body: any): {
|
|
78
|
+
outcome: string;
|
|
79
|
+
refusalCode: string | RegExp | null | undefined;
|
|
80
|
+
};
|
|
81
|
+
export { TOKENEX_IFRAME_ACCOUNT_NAMES, TOKENEX_IFRAME_BODY_KEYS, TOKENEX_IFRAME_CAPABILITY, TOKENEX_IFRAME_GATE_CODES, TOKENEX_IFRAME_HOSTS, TOKENEX_IFRAME_OPERATION, TOKENEX_IFRAME_PATH, TOKENEX_IFRAME_PSP, TOKENEX_IFRAME_REFUSAL_REASONS, TOKENEX_IFRAME_REVIEWED_ACCOUNTS, TOKENEX_IFRAME_TOKEN_SCHEMES, classifyTokenexIframeRequest, injectTokenexIframeCard, isTokenexIframeDeviceRequest, isTokenexIframeRequest, isTokenexIframeUrl, sanitizeTokenexIframeResponse, tokenexIframeAccount, tokenexIframeAccountName, tokenexIframeAccountRefusal, tokenexIframeBodyRefusal, tokenexIframeBodyVerdict, tokenexIframeBrandRefusal, tokenexIframeCardType, tokenexIframeDeviceRefusal, tokenexIframeErrorCode, tokenexIframeFacts, tokenexIframeRefusalMessage, tokenexIframeRequestRefusal, tokenexIframeResponseOutcome, tokenexIframeTarget };
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
+
// artifact-sha256: 79eec39d9ea40ae7687f24e640623f854bc1c0754a2553663c0662bd6cb86218
|
|
4
|
+
// src/tokenex-iframe.js
|
|
5
|
+
var TOKENEX_IFRAME_PSP = "tokenex_iframe";
|
|
6
|
+
var TOKENEX_IFRAME_CAPABILITY = "tokenex_iframe_tokenize";
|
|
7
|
+
var TOKENEX_IFRAME_OPERATION = "tokenex_iframe.card_request";
|
|
8
|
+
var TOKENEX_IFRAME_HOSTS = Object.freeze(["htp.tokenex.com", "test-htp.tokenex.com"]);
|
|
9
|
+
var TOKENEX_IFRAME_PATH = "/iframe/v3";
|
|
10
|
+
var TOKENEX_IFRAME_REFUSAL_REASONS = Object.freeze(["unsupported", "brand_mismatch", "unreviewed_account"]);
|
|
11
|
+
var TOKENEX_IFRAME_BODY_KEYS = Object.freeze([
|
|
12
|
+
"TokenExID",
|
|
13
|
+
"Origin",
|
|
14
|
+
"AuthenticationKey",
|
|
15
|
+
"Timestamp",
|
|
16
|
+
"Data",
|
|
17
|
+
"CvvValue",
|
|
18
|
+
"TokenScheme",
|
|
19
|
+
"CvvOnly",
|
|
20
|
+
"PCI",
|
|
21
|
+
"ReturnHash",
|
|
22
|
+
"use3DS",
|
|
23
|
+
"ThreeDSMethodNotificationUrl",
|
|
24
|
+
"kountSessionId",
|
|
25
|
+
"fraudMerchantId",
|
|
26
|
+
"UseKount",
|
|
27
|
+
"kountMode",
|
|
28
|
+
"kountAnId",
|
|
29
|
+
"FraudServicesRiskRequestModel",
|
|
30
|
+
"EnforceLuhnCompliance",
|
|
31
|
+
"CustomDataLuhnCheck"
|
|
32
|
+
]);
|
|
33
|
+
var TOKENEX_IFRAME_TOKEN_SCHEMES = Object.freeze(["sixTOKENfour"]);
|
|
34
|
+
var TOKENEX_IFRAME_REVIEWED_ACCOUNTS = Object.freeze({
|
|
35
|
+
"htp.tokenex.com": Object.freeze({}),
|
|
36
|
+
"test-htp.tokenex.com": Object.freeze({})
|
|
37
|
+
});
|
|
38
|
+
var TOKENEX_IFRAME_ACCOUNT_NAMES = Object.freeze({
|
|
39
|
+
"htp.tokenex.com": Object.freeze({}),
|
|
40
|
+
"test-htp.tokenex.com": Object.freeze({})
|
|
41
|
+
});
|
|
42
|
+
var UNSUPPORTED_REFUSAL = "Refusing to pay: unsupported TokenEx card request.";
|
|
43
|
+
var BRAND_MISMATCH_REFUSAL = "Refusing to pay: this TokenEx payment was started with a card of another brand than yours. Nothing was sent.";
|
|
44
|
+
var UNREVIEWED_ACCOUNT_REFUSAL = "Refusing to pay: this TokenEx payment goes to an account Agentcard has not reviewed. Nothing was sent.";
|
|
45
|
+
var FILL_REFUSAL = "Could not fill the card fields for TokenEx. Nothing was sent.";
|
|
46
|
+
var BODY_LIMIT = 8192;
|
|
47
|
+
var RESPONSE_LIMIT = 4096;
|
|
48
|
+
var URL_LIMIT = 2048;
|
|
49
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
50
|
+
var owns = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
51
|
+
var exact = (pattern, value) => typeof value === "string" && pattern.exec(value)?.[0] === value;
|
|
52
|
+
var text = (value, max) => typeof value === "string" && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
53
|
+
var digits = (value, min, max) => typeof value === "string" && value.length >= min && value.length <= max && !/[^0-9]/.test(value);
|
|
54
|
+
var panLike = (value) => (value.match(/\p{Nd}/gu) ?? []).length >= 12;
|
|
55
|
+
var plain = (value, max) => text(value, max) && !panLike(value);
|
|
56
|
+
var TOKENEX_ID = /^[0-9]{16}$/;
|
|
57
|
+
var AUTHENTICATION_KEY = /^[A-Za-z0-9+/]{43}=$/;
|
|
58
|
+
var TIMESTAMP = /^20[0-9]{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12][0-9]|3[01])(?:[01][0-9]|2[0-3])[0-5][0-9][0-5][0-9]$/;
|
|
59
|
+
function isOrigin(value) {
|
|
60
|
+
if (typeof value !== "string" || value.length > 255 || !value.startsWith("https://"))
|
|
61
|
+
return false;
|
|
62
|
+
try {
|
|
63
|
+
return new URL(value).origin === value;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
var BOUND = {
|
|
70
|
+
TokenExID: (value) => exact(TOKENEX_ID, value),
|
|
71
|
+
Origin: isOrigin,
|
|
72
|
+
AuthenticationKey: (value) => exact(AUTHENTICATION_KEY, value),
|
|
73
|
+
Timestamp: (value) => exact(TIMESTAMP, value),
|
|
74
|
+
TokenScheme: (value) => TOKENEX_IFRAME_TOKEN_SCHEMES.includes(value)
|
|
75
|
+
};
|
|
76
|
+
var BOUND_KEYS = Object.keys(BOUND);
|
|
77
|
+
var BOOLEAN = /^(?:true|false)$/i;
|
|
78
|
+
var QUERY_RULES = {
|
|
79
|
+
...BOUND,
|
|
80
|
+
Mode: (value) => value === "Data",
|
|
81
|
+
PCI: (value) => exact(/^true$/i, value),
|
|
82
|
+
CVV: (value) => exact(/^true$/i, value),
|
|
83
|
+
Container: (value) => exact(/^[A-Za-z][A-Za-z0-9_-]{0,63}$/, value),
|
|
84
|
+
CvvContainer: (value) => exact(/^[A-Za-z][A-Za-z0-9_-]{0,63}$/, value),
|
|
85
|
+
EnforceLuhnCompliance: (value) => exact(BOOLEAN, value),
|
|
86
|
+
CustomDataLabel: (value) => plain(value, 64),
|
|
87
|
+
CustomCvvDataLabel: (value) => plain(value, 64),
|
|
88
|
+
ExpiresInSeconds: (value) => exact(/^[1-9][0-9]{0,3}$/, value),
|
|
89
|
+
ReturnAutoCompleteValues: (value) => exact(BOOLEAN, value)
|
|
90
|
+
};
|
|
91
|
+
var QUERY_REQUIRED = [...BOUND_KEYS, "Mode", "PCI", "CVV"];
|
|
92
|
+
function readQuery(search) {
|
|
93
|
+
if (typeof search !== "string" || !search.startsWith("?") || search.length < 2)
|
|
94
|
+
return null;
|
|
95
|
+
const query = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const chunk of search.slice(1).split("&")) {
|
|
97
|
+
const eq = chunk.indexOf("=");
|
|
98
|
+
if (eq < 1)
|
|
99
|
+
return null;
|
|
100
|
+
let key, value;
|
|
101
|
+
try {
|
|
102
|
+
key = decodeURIComponent(chunk.slice(0, eq));
|
|
103
|
+
value = decodeURIComponent(chunk.slice(eq + 1));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (query.has(key) || !owns(QUERY_RULES, key) || !QUERY_RULES[key](value))
|
|
109
|
+
return null;
|
|
110
|
+
query.set(key, value);
|
|
111
|
+
}
|
|
112
|
+
return QUERY_REQUIRED.every((key) => query.has(key)) ? query : null;
|
|
113
|
+
}
|
|
114
|
+
function tokenexIframeTarget(url) {
|
|
115
|
+
if (typeof url !== "string" || url.length > URL_LIMIT || url.includes("#"))
|
|
116
|
+
return null;
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = new URL(url);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (parsed.href !== url || parsed.protocol !== "https:" || parsed.port || parsed.username || parsed.password)
|
|
125
|
+
return null;
|
|
126
|
+
if (!TOKENEX_IFRAME_HOSTS.includes(parsed.hostname) || parsed.pathname !== TOKENEX_IFRAME_PATH)
|
|
127
|
+
return null;
|
|
128
|
+
const query = readQuery(parsed.search);
|
|
129
|
+
return query ? { host: parsed.hostname, query } : null;
|
|
130
|
+
}
|
|
131
|
+
function isTokenexIframeUrl(url, method = "POST") {
|
|
132
|
+
return method === "POST" && tokenexIframeTarget(url) !== null;
|
|
133
|
+
}
|
|
134
|
+
function onIframePath(url) {
|
|
135
|
+
if (typeof url !== "string" || url.length > URL_LIMIT)
|
|
136
|
+
return false;
|
|
137
|
+
try {
|
|
138
|
+
const parsed = new URL(url);
|
|
139
|
+
return TOKENEX_IFRAME_HOSTS.includes(parsed.hostname.replace(/\.$/, "")) && parsed.pathname.toLowerCase() === TOKENEX_IFRAME_PATH;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
var PLACEHOLDER = /^[0-9]{1,19}(?: [0-9]{1,19}){0,5}$/;
|
|
146
|
+
var placeholderDigits = (value) => exact(PLACEHOLDER, value) && value.length <= 23 ? value.replace(/ /g, "") : null;
|
|
147
|
+
var RULES = {
|
|
148
|
+
...BOUND,
|
|
149
|
+
Data: (value) => {
|
|
150
|
+
const pan = placeholderDigits(value);
|
|
151
|
+
return pan !== null && pan.length >= 13 && pan.length <= 19;
|
|
152
|
+
},
|
|
153
|
+
CvvValue: (value) => digits(value, 3, 4),
|
|
154
|
+
CvvOnly: (value) => value === "False",
|
|
155
|
+
PCI: (value) => value === "True",
|
|
156
|
+
ReturnHash: (value) => value === null,
|
|
157
|
+
use3DS: (value) => value === "False",
|
|
158
|
+
ThreeDSMethodNotificationUrl: (value) => value === "",
|
|
159
|
+
kountSessionId: (value) => value === "",
|
|
160
|
+
fraudMerchantId: (value) => value === "",
|
|
161
|
+
UseKount: (value) => value === "False",
|
|
162
|
+
kountMode: (value) => value === "",
|
|
163
|
+
kountAnId: (value) => value === "",
|
|
164
|
+
FraudServicesRiskRequestModel: (value) => value === "",
|
|
165
|
+
EnforceLuhnCompliance: (value) => value === "true" || value === "false",
|
|
166
|
+
CustomDataLuhnCheck: (value) => value === true
|
|
167
|
+
};
|
|
168
|
+
function readJson(body, limit) {
|
|
169
|
+
if (typeof body !== "string" || body === "" || body.length > limit)
|
|
170
|
+
return null;
|
|
171
|
+
try {
|
|
172
|
+
const value = JSON.parse(body);
|
|
173
|
+
return record(value) ? value : null;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function judgeBody(body) {
|
|
180
|
+
const value = readJson(body, BODY_LIMIT);
|
|
181
|
+
if (!value || JSON.stringify(value) !== body)
|
|
182
|
+
return null;
|
|
183
|
+
const keys = Object.keys(value);
|
|
184
|
+
if (keys.length !== TOKENEX_IFRAME_BODY_KEYS.length || keys.some((key, index) => key !== TOKENEX_IFRAME_BODY_KEYS[index]))
|
|
185
|
+
return null;
|
|
186
|
+
return keys.every((key) => RULES[key](value[key])) ? value : null;
|
|
187
|
+
}
|
|
188
|
+
function tokenexIframeBodyRefusal(body) {
|
|
189
|
+
return judgeBody(body) ? null : "unsupported";
|
|
190
|
+
}
|
|
191
|
+
function carriesCard(body) {
|
|
192
|
+
const value = readJson(body, BODY_LIMIT * 4);
|
|
193
|
+
return value !== null && typeof value.Data === "string" && value.Data !== "";
|
|
194
|
+
}
|
|
195
|
+
var CONTENT_TYPE = /^application\/json(?:;\s*charset=utf-8)?$/i;
|
|
196
|
+
function headersRefusal(headers) {
|
|
197
|
+
if (!record(headers))
|
|
198
|
+
return "unsupported";
|
|
199
|
+
const folded = /* @__PURE__ */ new Map();
|
|
200
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
201
|
+
const lower = name.toLowerCase();
|
|
202
|
+
if (folded.has(lower) || typeof value !== "string")
|
|
203
|
+
return "unsupported";
|
|
204
|
+
folded.set(lower, value);
|
|
205
|
+
}
|
|
206
|
+
return exact(CONTENT_TYPE, folded.get("content-type")) ? null : "unsupported";
|
|
207
|
+
}
|
|
208
|
+
function bindingRefusal(target, value) {
|
|
209
|
+
return BOUND_KEYS.every((key) => target.query.get(key) === value[key]) ? null : "unsupported";
|
|
210
|
+
}
|
|
211
|
+
function tokenexIframeRequestRefusal(request) {
|
|
212
|
+
if (!record(request) || request.method !== "POST")
|
|
213
|
+
return "unsupported";
|
|
214
|
+
const target = tokenexIframeTarget(request.url);
|
|
215
|
+
if (!target)
|
|
216
|
+
return "unsupported";
|
|
217
|
+
const headers = headersRefusal(request.headers);
|
|
218
|
+
if (headers)
|
|
219
|
+
return headers;
|
|
220
|
+
const value = judgeBody(request.body);
|
|
221
|
+
return value ? bindingRefusal(target, value) : "unsupported";
|
|
222
|
+
}
|
|
223
|
+
function isTokenexIframeRequest(request) {
|
|
224
|
+
return tokenexIframeRequestRefusal(request) === null;
|
|
225
|
+
}
|
|
226
|
+
var TOKENEX_IFRAME_GATE_CODES = Object.freeze(["tokenex_iframe_url_refused", "tokenex_iframe_body_unsupported", "tokenex_iframe_query_mismatch"]);
|
|
227
|
+
function tokenexIframeBodyVerdict(body, url) {
|
|
228
|
+
if (!carriesCard(body))
|
|
229
|
+
return { verdict: "continue" };
|
|
230
|
+
const target = url !== void 0 ? tokenexIframeTarget(url) : null;
|
|
231
|
+
if (url !== void 0 && !target)
|
|
232
|
+
return { verdict: "refuse", code: "tokenex_iframe_url_refused" };
|
|
233
|
+
const value = judgeBody(body);
|
|
234
|
+
if (!value)
|
|
235
|
+
return { verdict: "refuse", code: "tokenex_iframe_body_unsupported" };
|
|
236
|
+
if (target && bindingRefusal(target, value))
|
|
237
|
+
return { verdict: "refuse", code: "tokenex_iframe_query_mismatch" };
|
|
238
|
+
return { verdict: "card" };
|
|
239
|
+
}
|
|
240
|
+
function classifyTokenexIframeRequest(url, method, body, headers) {
|
|
241
|
+
if (method !== "POST" || !onIframePath(url) || !carriesCard(body))
|
|
242
|
+
return null;
|
|
243
|
+
return tokenexIframeRequestRefusal({ url, method, body, headers }) ?? "card";
|
|
244
|
+
}
|
|
245
|
+
function tokenexIframeAccount(request) {
|
|
246
|
+
if (tokenexIframeRequestRefusal(request) !== null)
|
|
247
|
+
return null;
|
|
248
|
+
const value = judgeBody(request.body);
|
|
249
|
+
return { host: tokenexIframeTarget(request.url).host, tokenExId: value.TokenExID, origin: value.Origin };
|
|
250
|
+
}
|
|
251
|
+
function tokenexIframeAccountRefusal(request, accounts = TOKENEX_IFRAME_REVIEWED_ACCOUNTS) {
|
|
252
|
+
const account = tokenexIframeAccount(request);
|
|
253
|
+
if (!account)
|
|
254
|
+
return "unreviewed_account";
|
|
255
|
+
const listed = record(accounts) && owns(accounts, account.host) && record(accounts[account.host]) ? accounts[account.host] : null;
|
|
256
|
+
return listed && owns(listed, account.tokenExId) && listed[account.tokenExId] === account.origin ? null : "unreviewed_account";
|
|
257
|
+
}
|
|
258
|
+
function tokenexIframeAccountName(request, accounts = TOKENEX_IFRAME_REVIEWED_ACCOUNTS, names = TOKENEX_IFRAME_ACCOUNT_NAMES) {
|
|
259
|
+
if (tokenexIframeAccountRefusal(request, accounts) !== null)
|
|
260
|
+
return null;
|
|
261
|
+
const { host, tokenExId } = tokenexIframeAccount(request);
|
|
262
|
+
const named = record(names) && owns(names, host) && record(names[host]) ? names[host] : null;
|
|
263
|
+
const name = named && owns(named, tokenExId) ? named[tokenExId] : null;
|
|
264
|
+
return typeof name === "string" && name.trim() !== "" && plain(name, 64) ? name : null;
|
|
265
|
+
}
|
|
266
|
+
var CARD_TYPES = [
|
|
267
|
+
["visa", /^4\d{12}(\d{3})?(\d{3})?$/],
|
|
268
|
+
["masterCard", /^(5[1-5]\d{4}|677189|222[1-9]\d{2}|22[3-9]\d{3}|2[3-6]\d{4}|27[01]\d{3}|2720\d{2})\d{10,13}$/],
|
|
269
|
+
["americanExpress", /^3[47]\d{13,17}$/]
|
|
270
|
+
];
|
|
271
|
+
var CVV_LENGTH = { visa: 3, masterCard: 3, americanExpress: 4 };
|
|
272
|
+
function tokenexIframeCardType(number) {
|
|
273
|
+
if (!digits(number, 13, 19))
|
|
274
|
+
return null;
|
|
275
|
+
for (const [type, pattern] of CARD_TYPES)
|
|
276
|
+
if (pattern.test(number))
|
|
277
|
+
return type;
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
function tokenexIframeBrandRefusal(body, card) {
|
|
281
|
+
const value = judgeBody(body);
|
|
282
|
+
if (!value)
|
|
283
|
+
return "unsupported";
|
|
284
|
+
const placeholder = tokenexIframeCardType(placeholderDigits(value.Data));
|
|
285
|
+
const real = tokenexIframeCardType(card && card.number);
|
|
286
|
+
return placeholder !== null && placeholder === real ? null : "brand_mismatch";
|
|
287
|
+
}
|
|
288
|
+
function tokenexIframeDeviceRefusal(request, card, accounts = TOKENEX_IFRAME_REVIEWED_ACCOUNTS) {
|
|
289
|
+
const refusal = tokenexIframeRequestRefusal(request);
|
|
290
|
+
if (refusal)
|
|
291
|
+
return refusal;
|
|
292
|
+
if (record(card) && digits(card.number, 13, 19) && tokenexIframeBrandRefusal(request.body, card) !== null)
|
|
293
|
+
return "brand_mismatch";
|
|
294
|
+
return tokenexIframeAccountRefusal(request, accounts);
|
|
295
|
+
}
|
|
296
|
+
function tokenexIframeRefusalMessage(reason) {
|
|
297
|
+
if (reason === "brand_mismatch")
|
|
298
|
+
return BRAND_MISMATCH_REFUSAL;
|
|
299
|
+
if (reason === "unreviewed_account")
|
|
300
|
+
return UNREVIEWED_ACCOUNT_REFUSAL;
|
|
301
|
+
return UNSUPPORTED_REFUSAL;
|
|
302
|
+
}
|
|
303
|
+
function injectTokenexIframeCard(body, card) {
|
|
304
|
+
const value = judgeBody(body);
|
|
305
|
+
if (!value)
|
|
306
|
+
throw new Error(UNSUPPORTED_REFUSAL);
|
|
307
|
+
if (!record(card) || !digits(card.number, 13, 19))
|
|
308
|
+
throw new Error(FILL_REFUSAL);
|
|
309
|
+
if (tokenexIframeBrandRefusal(body, card) !== null)
|
|
310
|
+
throw new Error(BRAND_MISMATCH_REFUSAL);
|
|
311
|
+
if (!digits(card.cvc, 3, 4) || card.cvc.length !== CVV_LENGTH[tokenexIframeCardType(card.number)])
|
|
312
|
+
throw new Error(FILL_REFUSAL);
|
|
313
|
+
return { body: JSON.stringify({ ...value, Data: card.number, CvvValue: card.cvc }), where: ["Data", "CvvValue"] };
|
|
314
|
+
}
|
|
315
|
+
function isTokenexIframeDeviceRequest(templateBody, deviceBody) {
|
|
316
|
+
const template = judgeBody(templateBody);
|
|
317
|
+
const device = judgeBody(deviceBody);
|
|
318
|
+
if (!template || !device || device.Data === placeholderDigits(template.Data))
|
|
319
|
+
return false;
|
|
320
|
+
try {
|
|
321
|
+
return injectTokenexIframeCard(templateBody, { number: device.Data, cvc: device.CvvValue }).body === deviceBody;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
var SUCCESS_RULES = {
|
|
328
|
+
token: (value) => digits(value, 13, 19),
|
|
329
|
+
firstSix: (value) => digits(value, 6, 6),
|
|
330
|
+
lastFour: (value) => digits(value, 4, 4),
|
|
331
|
+
referenceNumber: (value) => exact(/^[A-Za-z0-9-]{1,64}$/, value),
|
|
332
|
+
tokenHMAC: (value) => exact(/^[A-Za-z0-9+/]{1,128}={0,2}$/, value),
|
|
333
|
+
cvvIncluded: (value) => typeof value === "boolean"
|
|
334
|
+
};
|
|
335
|
+
var SUCCESS_KEYS = Object.keys(SUCCESS_RULES);
|
|
336
|
+
var ERROR_CODES = [
|
|
337
|
+
[/^Invalid Card Number\b/, "invalid_card_number"],
|
|
338
|
+
[/^Invalid Authentication Key\b/, "invalid_authentication_key"]
|
|
339
|
+
];
|
|
340
|
+
function rebuildSuccess(value) {
|
|
341
|
+
if (!owns(value, "token"))
|
|
342
|
+
return { reason: "tokenex_iframe_no_token" };
|
|
343
|
+
const answer = {};
|
|
344
|
+
for (const key of SUCCESS_KEYS) {
|
|
345
|
+
if (!owns(value, key))
|
|
346
|
+
continue;
|
|
347
|
+
if (!SUCCESS_RULES[key](value[key]))
|
|
348
|
+
return { reason: key === "token" ? "tokenex_iframe_unexpected_token" : "tokenex_iframe_unexpected_field" };
|
|
349
|
+
answer[key] = value[key];
|
|
350
|
+
}
|
|
351
|
+
return { answer };
|
|
352
|
+
}
|
|
353
|
+
function rebuildError(value) {
|
|
354
|
+
if (owns(value, "token") || !plain(value.error, 256) || value.error.trim() === "")
|
|
355
|
+
return null;
|
|
356
|
+
if (owns(value, "referenceNumber") && !SUCCESS_RULES.referenceNumber(value.referenceNumber))
|
|
357
|
+
return null;
|
|
358
|
+
return owns(value, "referenceNumber") ? { error: value.error, referenceNumber: value.referenceNumber } : { error: value.error };
|
|
359
|
+
}
|
|
360
|
+
function tokenexIframeErrorCode(message) {
|
|
361
|
+
for (const [pattern, code] of ERROR_CODES)
|
|
362
|
+
if (typeof message === "string" && pattern.test(message))
|
|
363
|
+
return code;
|
|
364
|
+
return "tokenex_iframe_error";
|
|
365
|
+
}
|
|
366
|
+
function tokenexIframeResponseOutcome(status, body, card) {
|
|
367
|
+
if (status !== 200)
|
|
368
|
+
return { outcome: "unknown", reason: "tokenex_iframe_status_unconfirmed" };
|
|
369
|
+
const value = readJson(body, RESPONSE_LIMIT);
|
|
370
|
+
if (!value)
|
|
371
|
+
return { outcome: "unknown", reason: "tokenex_iframe_unreadable" };
|
|
372
|
+
if (owns(value, "error")) {
|
|
373
|
+
const error = rebuildError(value);
|
|
374
|
+
return error ? { outcome: "refused", code: tokenexIframeErrorCode(error.error) } : { outcome: "unknown", reason: "tokenex_iframe_unexpected_error" };
|
|
375
|
+
}
|
|
376
|
+
const { answer, reason } = rebuildSuccess(value);
|
|
377
|
+
if (!answer)
|
|
378
|
+
return { outcome: "unknown", reason };
|
|
379
|
+
if (record(card) && digits(card.number, 13, 19)) {
|
|
380
|
+
const number = card.number;
|
|
381
|
+
const sixTokenFour = answer.token.length === number.length && answer.token.startsWith(number.slice(0, 6)) && answer.token.endsWith(number.slice(-4));
|
|
382
|
+
if (!sixTokenFour || answer.token.includes(number) || (answer.referenceNumber ?? "").includes(number) || owns(answer, "firstSix") && answer.firstSix !== number.slice(0, 6) || owns(answer, "lastFour") && answer.lastFour !== number.slice(-4)) {
|
|
383
|
+
return { outcome: "unknown", reason: "tokenex_iframe_token_mismatch" };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return { outcome: "approved" };
|
|
387
|
+
}
|
|
388
|
+
function sanitizeTokenexIframeResponse(body) {
|
|
389
|
+
const value = readJson(body, RESPONSE_LIMIT);
|
|
390
|
+
const fail = () => {
|
|
391
|
+
throw new Error("Invalid TokenEx tokenization response.");
|
|
392
|
+
};
|
|
393
|
+
if (!value)
|
|
394
|
+
return fail();
|
|
395
|
+
if (owns(value, "error"))
|
|
396
|
+
return JSON.stringify(rebuildError(value) ?? fail());
|
|
397
|
+
return JSON.stringify(rebuildSuccess(value).answer ?? fail());
|
|
398
|
+
}
|
|
399
|
+
function tokenexIframeFacts(status, body) {
|
|
400
|
+
const verdict = tokenexIframeResponseOutcome(status, body);
|
|
401
|
+
return { outcome: verdict.outcome, refusalCode: verdict.outcome === "refused" ? verdict.code : null };
|
|
402
|
+
}
|
|
403
|
+
export { TOKENEX_IFRAME_ACCOUNT_NAMES, TOKENEX_IFRAME_BODY_KEYS, TOKENEX_IFRAME_CAPABILITY, TOKENEX_IFRAME_GATE_CODES, TOKENEX_IFRAME_HOSTS, TOKENEX_IFRAME_OPERATION, TOKENEX_IFRAME_PATH, TOKENEX_IFRAME_PSP, TOKENEX_IFRAME_REFUSAL_REASONS, TOKENEX_IFRAME_REVIEWED_ACCOUNTS, TOKENEX_IFRAME_TOKEN_SCHEMES, classifyTokenexIframeRequest, injectTokenexIframeCard, isTokenexIframeDeviceRequest, isTokenexIframeRequest, isTokenexIframeUrl, sanitizeTokenexIframeResponse, tokenexIframeAccount, tokenexIframeAccountName, tokenexIframeAccountRefusal, tokenexIframeBodyRefusal, tokenexIframeBodyVerdict, tokenexIframeBrandRefusal, tokenexIframeCardType, tokenexIframeDeviceRefusal, tokenexIframeErrorCode, tokenexIframeFacts, tokenexIframeRefusalMessage, tokenexIframeRequestRefusal, tokenexIframeResponseOutcome, tokenexIframeTarget };
|