@agent-cards/sdk 0.23.19 → 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 +61 -2
- package/PREFLIGHT.md +4 -0
- package/README.md +175 -7
- package/dist/amadeus.d.ts +4 -2
- package/dist/amadeus.js +16 -3
- package/dist/buy.d.ts +84 -2
- package/dist/buy.js +16 -1
- package/dist/card-endpoint-facts.d.ts +2 -1
- package/dist/card-endpoint-facts.js +18 -9
- package/dist/card-endpoint.d.ts +20 -4
- package/dist/card-endpoint.js +38 -4
- package/dist/card-fields.generated.js +1444 -51
- package/dist/cdp.d.ts +29 -0
- package/dist/cdp.js +444 -24
- package/dist/client.d.ts +134 -7
- package/dist/client.js +350 -25
- 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 +56 -0
- package/dist/hipay.generated.js +392 -0
- package/dist/hold.d.ts +180 -0
- package/dist/hold.js +270 -0
- package/dist/hosted-entry.d.ts +4 -0
- package/dist/hosted-entry.js +32 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/lifecycle.d.ts +10 -0
- package/dist/lifecycle.js +21 -0
- package/dist/merchant-card-endpoints.generated.js +102 -11
- package/dist/paddle-tokenex.generated.js +7 -3
- package/dist/paddle-vgs.generated.d.ts +3 -2
- package/dist/paddle-vgs.generated.js +9 -5
- package/dist/payrails.generated.js +7 -3
- package/dist/pre-claim.d.ts +20 -0
- package/dist/pre-claim.js +202 -22
- package/dist/preflight-catalog.json +58 -0
- package/dist/preflight-schemas.json +4 -0
- package/dist/preflight.generated.js +26 -2
- package/dist/prepared-processor.d.ts +6 -2
- package/dist/prepared-processor.js +18 -2
- package/dist/primer.generated.js +1487 -94
- package/dist/przelewy24.d.ts +47 -0
- package/dist/przelewy24.js +88 -0
- package/dist/quickpay.d.ts +44 -0
- package/dist/quickpay.js +65 -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 +744 -13
- 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.d.ts +54 -0
- package/dist/twoc2p.generated.d.ts +33 -0
- package/dist/twoc2p.generated.js +1149 -0
- package/dist/twoc2p.js +85 -0
- package/dist/voice.d.ts +16 -13
- package/dist/voice.js +13 -10
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// TokenEx hosted iframe v3 (psp `tokenex_iframe`), on htp.tokenex.com (live) and test-htp.tokenex.com (test):
|
|
2
|
+
// the card form JetBlue's booking checkout mounts for the card number and the CVV. The cardholder name and the
|
|
3
|
+
// expiry stay on JetBlue's own page. TokenEx's card-number frame, a cross-origin iframe the merchant page never
|
|
4
|
+
// reads, takes the CVV from its same-origin CVV frame and sends the whole card in one XHR: POST to its own
|
|
5
|
+
// document URL, query included, application/json. This module holds what the SDK decides about that frame's
|
|
6
|
+
// requests. The rules themselves are payment-core's (tokenex-iframe.generated.ts): the API applies the same ones
|
|
7
|
+
// at create, and the cardholder's device applies them again, with the account pin and the brand rule, before it
|
|
8
|
+
// writes a card.
|
|
9
|
+
//
|
|
10
|
+
// The processor ships dark. The API serves the tokenex_iframe recognizer only to a build that declares the
|
|
11
|
+
// `tokenex_iframe_tokenize` capability (client.ts SUPPORTED_CAPABILITIES) and only to an org Agentcard enabled
|
|
12
|
+
// for it, so an SDK the API never served it leaves every TokenEx request alone.
|
|
13
|
+
import { TOKENEX_IFRAME_CAPABILITY as CORE_CAPABILITY, TOKENEX_IFRAME_HOSTS, TOKENEX_IFRAME_PATH, TOKENEX_IFRAME_PSP as CORE_PSP, classifyTokenexIframeRequest, sanitizeTokenexIframeResponse, tokenexIframeBodyVerdict, tokenexIframeCardType, tokenexIframeResponseOutcome, tokenexIframeTarget, } from './tokenex-iframe.generated.js';
|
|
14
|
+
/** The PSP id the API and the recognizer name this lane. */
|
|
15
|
+
export const TOKENEX_IFRAME_PSP = CORE_PSP;
|
|
16
|
+
/** The dark-launch capability the API serves the tokenex_iframe recognizer behind. */
|
|
17
|
+
export const TOKENEX_IFRAME_CAPABILITY = CORE_CAPABILITY;
|
|
18
|
+
export const INVALID_TOKENEX_IFRAME_REQUEST = 'invalid_tokenex_iframe_request';
|
|
19
|
+
/** True when the URL parses onto one of TokenEx's two iframe hosts and the /iframe/v3 path, however it is spelled or queried. */
|
|
20
|
+
export function isTokenexIframePath(url) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = new URL(url);
|
|
23
|
+
return TOKENEX_IFRAME_HOSTS.includes(parsed.hostname.toLowerCase().replace(/\.$/, ''))
|
|
24
|
+
&& parsed.pathname.toLowerCase() === TOKENEX_IFRAME_PATH;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Whether this build's registry serves the TokenEx iframe: a company Agentcard turned it on for, on a build that declares it. */
|
|
31
|
+
export function servesTokenexIframe(vault) {
|
|
32
|
+
return typeof vault?.recognizesProcessor === 'function' && vault.recognizesProcessor(TOKENEX_IFRAME_PSP);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether an adapter must hand this request to the pre-claim gates: it is on the frame's /iframe/v3 path and this
|
|
36
|
+
* build's registry serves the TokenEx iframe. The recognizer matches only the exact card URL, so a variant (another
|
|
37
|
+
* query, a case or dot spelling) would otherwise never reach the gate that refuses it. attachToPlaywright's route
|
|
38
|
+
* predicate asks this; attachToCdp arms the two hosts from the recognizer's own host list.
|
|
39
|
+
*/
|
|
40
|
+
export function isArmedTokenexIframeUrl(vault, url) {
|
|
41
|
+
return isTokenexIframePath(url) && servesTokenexIframe(vault);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The pause decision for a POST on the frame's path, by payment-core's classifyTokenexIframeRequest:
|
|
45
|
+
* 'card' the exact card URL with the JSON content type and the sound pcicvv card post bound to its query: it pauses;
|
|
46
|
+
* 'invalid_tokenex_iframe_request' a post with a value in Data that is not that: it is refused in the browser;
|
|
47
|
+
* null not a card post (no value in Data, another path, BinLookup, Forter): the SDK's ordinary handling
|
|
48
|
+
* decides, and an empty-Data post continues untouched through the recognizer's passWithoutCard.
|
|
49
|
+
* A URL longer than payment-core reads (over 2,048 characters) classifies as null, as an unrelated request does, and
|
|
50
|
+
* the exact recognizer refuses it too, so a post on the frame's own path that carries a value in Data is refused here
|
|
51
|
+
* rather than sent to TokenEx with the agent's placeholder.
|
|
52
|
+
*/
|
|
53
|
+
export function tokenexIframePauseDecision(request) {
|
|
54
|
+
const verdict = classifyTokenexIframeRequest(request.url, 'POST', request.body ?? '', request.headers ?? {});
|
|
55
|
+
if (verdict === 'card')
|
|
56
|
+
return 'card';
|
|
57
|
+
if (verdict !== null)
|
|
58
|
+
return INVALID_TOKENEX_IFRAME_REQUEST;
|
|
59
|
+
return isTokenexIframePath(request.url) && tokenexIframeBodyVerdict(request.body ?? '', undefined).verdict !== 'continue'
|
|
60
|
+
? INVALID_TOKENEX_IFRAME_REQUEST : null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The answer the paused card post may receive, rebuilt from payment-core's allow-list under a JSON content type:
|
|
64
|
+
* only TokenEx's 200 with a sound sixTOKENfour token (tokenexIframeResponseOutcome reads it as approved), keys TokenEx
|
|
65
|
+
* documents for a success and nothing else (sanitizeTokenexIframeResponse). Null for anything else: an error answer,
|
|
66
|
+
* another status, a body out of bounds. The frame adds its own `cardType` before the page reads the answer.
|
|
67
|
+
*/
|
|
68
|
+
export function tokenexIframePageAnswer(status, body) {
|
|
69
|
+
try {
|
|
70
|
+
if (tokenexIframeResponseOutcome(status, body, undefined).outcome !== 'approved')
|
|
71
|
+
return null;
|
|
72
|
+
return { status: 200, headers: { 'content-type': 'application/json' }, body: sanitizeTokenexIframeResponse(body) };
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// ── The placeholder card ────────────────────────────────────────────────────
|
|
79
|
+
// The frame stamps `cardType` onto TokenEx's answer from ITS OWN reading of the digits the agent typed, not from the
|
|
80
|
+
// card TokenEx tokenized, and the page reads that brand (JetBlue's payment step goes on with it). So the cardholder's
|
|
81
|
+
// device refuses a card whose brand the placeholder does not share (payment-core tokenexIframeBrandRefusal), and it
|
|
82
|
+
// proves only Visa, Mastercard and American Express by the frame's own table. placeholderCard() therefore gives a
|
|
83
|
+
// TokenEx form only a number the frame reads as the stored card's own network, and refuses every other network.
|
|
84
|
+
/** The frame's cardType for each network GET /api/v2/vault_cards names, for the three this lane proves. */
|
|
85
|
+
const FRAME_CARD_TYPE = Object.freeze({ visa: 'visa', mastercard: 'masterCard', amex: 'americanExpress' });
|
|
86
|
+
/** Whether the frame reads `number` as the network `brand` (lowercase, as GET /api/v2/vault_cards names it). */
|
|
87
|
+
export function tokenexIframePlaceholderFits(brand, number) {
|
|
88
|
+
const type = Object.prototype.hasOwnProperty.call(FRAME_CARD_TYPE, brand) ? FRAME_CARD_TYPE[brand] : undefined;
|
|
89
|
+
return type !== undefined && tokenexIframeCardType(number) === type;
|
|
90
|
+
}
|
|
91
|
+
// ── The repeat rule ─────────────────────────────────────────────────────────
|
|
92
|
+
// TokenEx's frame answers a repeat itself: tokenize() hands the page its previous answer again, with no request,
|
|
93
|
+
// while Data and the CVV are unchanged and its five-minute timer (newAttemptTimeout, 300 s from the frame's load) has
|
|
94
|
+
// not set newAttempt. So the requests this build sees after an approval are of two kinds:
|
|
95
|
+
// - the same bytes again, from the same frame: the timer fired, or the frame was reset or reloaded with the same
|
|
96
|
+
// URL (and so the same AuthenticationKey and Timestamp). TokenEx would mint another token for the same card;
|
|
97
|
+
// - another Data or CVV: the agent typed another placeholder. The frame stamps cardType from the new digits, so an
|
|
98
|
+
// earlier token under them could carry another brand to the page than the card's.
|
|
99
|
+
// So once an approved answer is handed to a card post, a later post from the same frame with the same URL and the
|
|
100
|
+
// same body, byte for byte, is answered here with that stored answer: no create, no prompt, no second card sent.
|
|
101
|
+
// Any other post is a new question: another placeholder or CVV, another frame (JetBlue re-creating the iframe), another
|
|
102
|
+
// key or Timestamp (a reloaded frame JetBlue minted a new key for). The answer belongs to one approval's handoff (the
|
|
103
|
+
// gate in pre-claim.ts hands it out only while that approval is the attachment's current one, and once the answer
|
|
104
|
+
// reached the page only while the application's merchant resolver reports the order pending, as for Datatrans) and
|
|
105
|
+
// ends with the key the post carries (its Timestamp plus its ExpiresInSeconds, at most 1,200 s), so an approval that
|
|
106
|
+
// arrives ten minutes into a twenty-minute key keeps its answer for the remaining ten: past the key TokenEx would
|
|
107
|
+
// refuse the same post as "Invalid Authentication Key". There is no idle bound as Datatrans has: the frame re-sends
|
|
108
|
+
// unchanged bytes only on its own five-minute timer, so a five-minute quiet rule would end most repeats before they came.
|
|
109
|
+
/** The longest an iframe key lives, and so the longest a stored answer is handed out: TokenEx's ExpiresInSeconds maximum. */
|
|
110
|
+
export const TOKENEX_IFRAME_REPEAT_WINDOW_MS = 1_200_000;
|
|
111
|
+
/** The most answers one attachment keeps; the oldest goes first. */
|
|
112
|
+
const MAX_REMEMBERED = 8;
|
|
113
|
+
/** The millisecond the merchant's server minted the post's iframe key, from its `Timestamp` (yyyyMMddHHmmss, UTC). */
|
|
114
|
+
function keyMintedAtMs(stamp) {
|
|
115
|
+
if (stamp === undefined || !/^[0-9]{14}$/.test(stamp))
|
|
116
|
+
return null;
|
|
117
|
+
const at = Date.UTC(Number(stamp.slice(0, 4)), Number(stamp.slice(4, 6)) - 1, Number(stamp.slice(6, 8)), Number(stamp.slice(8, 10)), Number(stamp.slice(10, 12)), Number(stamp.slice(12, 14)));
|
|
118
|
+
return Number.isFinite(at) ? at : null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* When the post's own iframe key expires: its Timestamp plus its ExpiresInSeconds (TokenEx's default 1,200 s when
|
|
122
|
+
* the query names none), at most the window. An approval that reaches the page partway through a key's life therefore
|
|
123
|
+
* keeps its answer for the rest of that key and no longer, where a lifetime counted from the answer would outlive the
|
|
124
|
+
* key TokenEx checks. A Timestamp this clock has not reached counts from now, so the answer never outlives the key
|
|
125
|
+
* either way.
|
|
126
|
+
*/
|
|
127
|
+
function keyDeadlineMs(url, now) {
|
|
128
|
+
const target = tokenexIframeTarget(url);
|
|
129
|
+
if (!target)
|
|
130
|
+
return null;
|
|
131
|
+
const named = Number(target.query.get('ExpiresInSeconds') ?? '1200');
|
|
132
|
+
const minted = keyMintedAtMs(target.query.get('Timestamp'));
|
|
133
|
+
if (minted === null || !Number.isSafeInteger(named) || named <= 0)
|
|
134
|
+
return null;
|
|
135
|
+
return Math.min(minted, now) + Math.min(named * 1000, TOKENEX_IFRAME_REPEAT_WINDOW_MS);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The approved TokenEx answers one attachment handed to a card post, per frame. `frame` is the adapter's identity
|
|
139
|
+
* for the request's frame (the CDP frameId of the out-of-process iframe, the Playwright Frame); a request whose
|
|
140
|
+
* frame is unknown is never remembered and never answered. Only an answer payment-core reads as approved (a sound
|
|
141
|
+
* sixTOKENfour token) to the sound card post is kept: an error answer leaves the next post a new question, so the
|
|
142
|
+
* page can try again.
|
|
143
|
+
*/
|
|
144
|
+
export class TokenexIframeRepeats {
|
|
145
|
+
now;
|
|
146
|
+
remembered = [];
|
|
147
|
+
constructor(now = Date.now) {
|
|
148
|
+
this.now = now;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Keep the answer a token replay handed to this card post, when TokenEx approved it. Called after the answer
|
|
152
|
+
* reached the browser, so it never throws: a failure here only leaves the page's next post a new question.
|
|
153
|
+
*/
|
|
154
|
+
remember(frame, url, body, replay) {
|
|
155
|
+
try {
|
|
156
|
+
if (frame === undefined || frame === null || frame === '' || typeof body !== 'string')
|
|
157
|
+
return;
|
|
158
|
+
if (replay.mode !== 'token')
|
|
159
|
+
return;
|
|
160
|
+
if (tokenexIframeBodyVerdict(body, url).verdict !== 'card')
|
|
161
|
+
return;
|
|
162
|
+
const until = keyDeadlineMs(url, this.now());
|
|
163
|
+
if (until === null || until <= this.now())
|
|
164
|
+
return;
|
|
165
|
+
if (tokenexIframeResponseOutcome(replay.status, replay.body, undefined).outcome !== 'approved')
|
|
166
|
+
return;
|
|
167
|
+
this.prune();
|
|
168
|
+
this.remembered = this.remembered.filter((entry) => !(entry.frame === frame && entry.url === url && entry.body === body));
|
|
169
|
+
this.remembered.push({ frame, url, body, authorizationId: replay.authorizationId, status: replay.status,
|
|
170
|
+
headers: { ...replay.headers }, answer: replay.body, until });
|
|
171
|
+
if (this.remembered.length > MAX_REMEMBERED)
|
|
172
|
+
this.remembered.shift();
|
|
173
|
+
}
|
|
174
|
+
catch { /* see above: the next post is a new question */ }
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The stored answer for the same frame's same post, byte for byte, or null. Only an answer from `authorizationId`,
|
|
178
|
+
* the approval the attachment is handing off now, is handed out, and every answer from another approval is
|
|
179
|
+
* dropped: a later authorization ends the window of every earlier one.
|
|
180
|
+
*/
|
|
181
|
+
answerFor(frame, url, body, authorizationId) {
|
|
182
|
+
if (frame === undefined || frame === null || frame === '' || typeof body !== 'string' || !authorizationId)
|
|
183
|
+
return null;
|
|
184
|
+
this.prune();
|
|
185
|
+
this.remembered = this.remembered.filter((entry) => entry.authorizationId === authorizationId);
|
|
186
|
+
const entry = this.remembered.find((candidate) => candidate.frame === frame && candidate.url === url && candidate.body === body);
|
|
187
|
+
if (!entry)
|
|
188
|
+
return null;
|
|
189
|
+
return { authorizationId: entry.authorizationId, response: { status: entry.status, headers: { ...entry.headers }, body: entry.answer } };
|
|
190
|
+
}
|
|
191
|
+
prune() {
|
|
192
|
+
const now = this.now();
|
|
193
|
+
this.remembered = this.remembered.filter((entry) => now < entry.until);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transbank's Webpay Plus (webpay3g.transbank.cl, the card payment form most Chilean shops send a buyer to), as this
|
|
3
|
+
* SDK reads it.
|
|
4
|
+
*
|
|
5
|
+
* The merchant's server creates the transaction at Transbank and sends the buyer's browser to Webpay with a GET,
|
|
6
|
+
* https://webpay3g.transbank.cl/webpayserver/initTransaction?token_ws=<64 lowercase hex>, the way the official
|
|
7
|
+
* WooCommerce plugin redirects. That GET carries no card and nothing but Transbank's token, and it is the one request
|
|
8
|
+
* a build that declares Webpay's capability pauses for Webpay (hosted-entry.ts): the cardholder's device repeats it
|
|
9
|
+
* over the end-to-end TLS relay and sends Webpay's card steps inside the session it opens (payment-core
|
|
10
|
+
* processors/transbank.js). Everything else the agent's browser sends Webpay spends the token or carries a card the
|
|
11
|
+
* agent typed: the token's one submit (/webpayserver/init_transaction.cgi), the card lookup and card check under
|
|
12
|
+
* /webpayserver/wswebpay/services/, and the card post (/webpayserver/bp_control.cgi). Once Webpay is served this SDK
|
|
13
|
+
* aborts every one of them (pre-claim.ts transbankPreClaim), so the token is never spent in the agent's browser and
|
|
14
|
+
* no placeholder card reaches Webpay. Only the link exactly as the merchant's redirect writes it is the entry; any
|
|
15
|
+
* other spelling would be handed on to Webpay's own route, which these rules never see.
|
|
16
|
+
*
|
|
17
|
+
* TRANSBANK_HOSTS is payment-core's TRANSBANK_HOSTS, and isTransbankEntryUrl is payment-core's rule of the same name;
|
|
18
|
+
* transbank.test.mjs pins both to payment-core on every case payment-core's own tests list.
|
|
19
|
+
*/
|
|
20
|
+
export declare const TRANSBANK_PSP = "transbank_webpay";
|
|
21
|
+
/** The dark-launch capability the API serves Webpay's recognizer behind (payment-core definitions.js). */
|
|
22
|
+
export declare const TRANSBANK_CAPABILITY = "transbank_webpay_hosted_payment_page";
|
|
23
|
+
/** Webpay's live host and Transbank's integration host, each a host of its own entry. */
|
|
24
|
+
export declare const TRANSBANK_HOSTS: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* Whether a URL is Webpay's entry exactly as the merchant's redirect writes it: https, one of Transbank's two hosts in
|
|
27
|
+
* lower case, no port, credentials or fragment, the path /webpayserver/initTransaction, and a query of the one
|
|
28
|
+
* `token_ws` parameter holding 64 lowercase hex characters. A string that parses to another spelling is not it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isTransbankEntryUrl(raw: unknown): boolean;
|
|
31
|
+
/** Whether this URL is on one of Webpay's two hosts (https, no credentials, any port, a trailing dot or not). */
|
|
32
|
+
export declare function isTransbankHost(url: string): boolean;
|
|
33
|
+
/** Whether this build's registry serves Webpay's entry: a company Agentcard turned Webpay on for, on a build that declares it. */
|
|
34
|
+
export declare function servesTransbank(vault: {
|
|
35
|
+
isCardRequest(url: string, method?: string): boolean;
|
|
36
|
+
} | undefined): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Whether an adapter must hand this request to the pre-claim gates: it is on one of Webpay's hosts and this build's
|
|
39
|
+
* registry serves Webpay. attachToCdp pauses every request there through the recognizer's host patterns;
|
|
40
|
+
* attachToPlaywright's route predicate asks this, so both adapters show the Webpay gate every request to a Webpay host
|
|
41
|
+
* once Webpay is served.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isArmedTransbankUrl(vault: {
|
|
44
|
+
isCardRequest(url: string, method?: string): boolean;
|
|
45
|
+
} | undefined, url: string): boolean;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transbank's Webpay Plus (webpay3g.transbank.cl, the card payment form most Chilean shops send a buyer to), as this
|
|
3
|
+
* SDK reads it.
|
|
4
|
+
*
|
|
5
|
+
* The merchant's server creates the transaction at Transbank and sends the buyer's browser to Webpay with a GET,
|
|
6
|
+
* https://webpay3g.transbank.cl/webpayserver/initTransaction?token_ws=<64 lowercase hex>, the way the official
|
|
7
|
+
* WooCommerce plugin redirects. That GET carries no card and nothing but Transbank's token, and it is the one request
|
|
8
|
+
* a build that declares Webpay's capability pauses for Webpay (hosted-entry.ts): the cardholder's device repeats it
|
|
9
|
+
* over the end-to-end TLS relay and sends Webpay's card steps inside the session it opens (payment-core
|
|
10
|
+
* processors/transbank.js). Everything else the agent's browser sends Webpay spends the token or carries a card the
|
|
11
|
+
* agent typed: the token's one submit (/webpayserver/init_transaction.cgi), the card lookup and card check under
|
|
12
|
+
* /webpayserver/wswebpay/services/, and the card post (/webpayserver/bp_control.cgi). Once Webpay is served this SDK
|
|
13
|
+
* aborts every one of them (pre-claim.ts transbankPreClaim), so the token is never spent in the agent's browser and
|
|
14
|
+
* no placeholder card reaches Webpay. Only the link exactly as the merchant's redirect writes it is the entry; any
|
|
15
|
+
* other spelling would be handed on to Webpay's own route, which these rules never see.
|
|
16
|
+
*
|
|
17
|
+
* TRANSBANK_HOSTS is payment-core's TRANSBANK_HOSTS, and isTransbankEntryUrl is payment-core's rule of the same name;
|
|
18
|
+
* transbank.test.mjs pins both to payment-core on every case payment-core's own tests list.
|
|
19
|
+
*/
|
|
20
|
+
export const TRANSBANK_PSP = 'transbank_webpay';
|
|
21
|
+
/** The dark-launch capability the API serves Webpay's recognizer behind (payment-core definitions.js). */
|
|
22
|
+
export const TRANSBANK_CAPABILITY = 'transbank_webpay_hosted_payment_page';
|
|
23
|
+
/** Webpay's live host and Transbank's integration host, each a host of its own entry. */
|
|
24
|
+
export const TRANSBANK_HOSTS = Object.freeze(['webpay3g.transbank.cl', 'webpay3gint.transbank.cl']);
|
|
25
|
+
const TRANSBANK_ENTRY_PATH = '/webpayserver/initTransaction';
|
|
26
|
+
// A Webpay token as Transbank's REST reference defines it: 64 lowercase hex characters.
|
|
27
|
+
const TOKEN_QUERY = /^\?token_ws=[0-9a-f]{64}$/;
|
|
28
|
+
// An entry in the shape the merchant's redirect writes it: the registry reads Webpay as served when it takes this as a GET entry.
|
|
29
|
+
const TRANSBANK_SAMPLE_ENTRY = `https://webpay3g.transbank.cl${TRANSBANK_ENTRY_PATH}?token_ws=${'0'.repeat(64)}`;
|
|
30
|
+
/**
|
|
31
|
+
* Whether a URL is Webpay's entry exactly as the merchant's redirect writes it: https, one of Transbank's two hosts in
|
|
32
|
+
* lower case, no port, credentials or fragment, the path /webpayserver/initTransaction, and a query of the one
|
|
33
|
+
* `token_ws` parameter holding 64 lowercase hex characters. A string that parses to another spelling is not it.
|
|
34
|
+
*/
|
|
35
|
+
export function isTransbankEntryUrl(raw) {
|
|
36
|
+
if (typeof raw !== 'string' || raw.length > 256 || raw.includes('#'))
|
|
37
|
+
return false;
|
|
38
|
+
let url;
|
|
39
|
+
try {
|
|
40
|
+
url = new URL(raw);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (url.href !== raw || url.protocol !== 'https:' || url.port || url.username || url.password)
|
|
46
|
+
return false;
|
|
47
|
+
return TRANSBANK_HOSTS.includes(url.hostname) && url.pathname === TRANSBANK_ENTRY_PATH && TOKEN_QUERY.test(url.search);
|
|
48
|
+
}
|
|
49
|
+
/** Whether this URL is on one of Webpay's two hosts (https, no credentials, any port, a trailing dot or not). */
|
|
50
|
+
export function isTransbankHost(url) {
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = new URL(url);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return parsed.protocol === 'https:' && !parsed.username && !parsed.password
|
|
59
|
+
&& TRANSBANK_HOSTS.includes(parsed.hostname.toLowerCase().replace(/\.$/, ''));
|
|
60
|
+
}
|
|
61
|
+
/** Whether this build's registry serves Webpay's entry: a company Agentcard turned Webpay on for, on a build that declares it. */
|
|
62
|
+
export function servesTransbank(vault) {
|
|
63
|
+
return typeof vault?.isCardRequest === 'function' && vault.isCardRequest(TRANSBANK_SAMPLE_ENTRY, 'GET');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether an adapter must hand this request to the pre-claim gates: it is on one of Webpay's hosts and this build's
|
|
67
|
+
* registry serves Webpay. attachToCdp pauses every request there through the recognizer's host patterns;
|
|
68
|
+
* attachToPlaywright's route predicate asks this, so both adapters show the Webpay gate every request to a Webpay host
|
|
69
|
+
* once Webpay is served.
|
|
70
|
+
*/
|
|
71
|
+
export function isArmedTransbankUrl(vault, url) {
|
|
72
|
+
return isTransbankHost(url) && servesTransbank(vault);
|
|
73
|
+
}
|
package/dist/twoc2p.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** The PSP id the API and the recognizer name this lane, re-exported so the client dispatches by it. */
|
|
2
|
+
export { TWOC2P_PSP } from './twoc2p.generated.js';
|
|
3
|
+
/** The dark-launch capability a build declares when it fills a 2C2P Payment with the device's card field. */
|
|
4
|
+
export declare const TWOC2P_CAPABILITY = "twoc2p_secure_pay";
|
|
5
|
+
/** A cse substitution as the API serves it (substitute.ts Substitutions): here the one card field. */
|
|
6
|
+
interface TwoC2pServedSubstitution {
|
|
7
|
+
encoding: string;
|
|
8
|
+
at: string;
|
|
9
|
+
fields: Record<string, string>;
|
|
10
|
+
remove?: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Whether an approved 2C2P authorization's served substitution is one this SDK can continue: JSON at payment.data,
|
|
14
|
+
* no sibling to remove, and exactly the one card field, a non-empty string. substituteTwoC2p checks the field
|
|
15
|
+
* against the pinned key for the Payment's environment; this is the shape gate the continuation runs first.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isTwoC2pServedSubstitution(sub: unknown): sub is TwoC2pServedSubstitution;
|
|
18
|
+
/** The part of the client a refused continuation retires its approval through. */
|
|
19
|
+
interface TwoC2pRetiringClient {
|
|
20
|
+
cancelAuthorization?: (authorizationId: string, reason: 'merchant_never_retried') => Promise<unknown>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The retirement a refused 2C2P continuation runs (twoC2pContinuation's `retire`), or undefined for a client that
|
|
24
|
+
* cannot cancel: the approval is cancelled as merchant_never_retried, since its field went to no request, and the
|
|
25
|
+
* API stops serving the field. The paused request fails at once, so the cancel is not awaited; when the API does not
|
|
26
|
+
* confirm it, `onEvent` hears approval_retire_unconfirmed with the authorization's id. The API's sweep drops the field
|
|
27
|
+
* once its readable window passes either way.
|
|
28
|
+
*/
|
|
29
|
+
export declare function twoC2pRetire(client: TwoC2pRetiringClient | undefined, authorizationId: string, onEvent?: (event: {
|
|
30
|
+
type: string;
|
|
31
|
+
detail?: unknown;
|
|
32
|
+
}) => void): (() => void) | undefined;
|
|
33
|
+
/** The paused request an approval was created from: the URL and body the API reviewed at create. */
|
|
34
|
+
export interface TwoC2pApprovedRequest {
|
|
35
|
+
url: string;
|
|
36
|
+
body: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The continuation of an approved 2C2P Payment: the paused body with the device's card field written over the
|
|
40
|
+
* placeholder's (payment-core substituteTwoC2p, which refuses a field that is not one under the pinned key for the
|
|
41
|
+
* Payment's environment, or one already in the paused body).
|
|
42
|
+
*
|
|
43
|
+
* Only into `approved`, the paused Payment the approval was created from: `url` and `body` must equal it byte for
|
|
44
|
+
* byte. 2C2P's key is the same for every merchant, so the field would pay any later Payment of the same shape;
|
|
45
|
+
* binding it to the one request keeps this lane from ever writing one payment's field into another.
|
|
46
|
+
*
|
|
47
|
+
* Null when neither URL is a 2C2P Payment, so every other processor keeps its own swap. Throws for any other body or
|
|
48
|
+
* URL, and for a substitution the shape gate or the swap refuses, in each case before anything continues. A refusal
|
|
49
|
+
* means the device's field went to no request, so `retire` runs first: the caller retires the approval as
|
|
50
|
+
* merchant_never_retried, and the API stops serving the field (cdp.ts cseContinuation).
|
|
51
|
+
*/
|
|
52
|
+
export declare function twoC2pContinuation(url: string, body: string, sub: unknown, approved: TwoC2pApprovedRequest, retire?: () => void): {
|
|
53
|
+
body: string;
|
|
54
|
+
} | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare var TWOC2P_PSP: string;
|
|
2
|
+
declare var TWOC2P_CAPABILITY: string;
|
|
3
|
+
declare var TWOC2P_OPERATION: string;
|
|
4
|
+
declare var TWOC2P_SCHEME: string;
|
|
5
|
+
declare var TWOC2P_FIELD_AT: string;
|
|
6
|
+
declare var TWOC2P_FIELD: string;
|
|
7
|
+
declare var TWOC2P_REVIEWED_BUILDS: readonly string[];
|
|
8
|
+
declare var TWOC2P_REFUSAL_CODES: readonly string[];
|
|
9
|
+
declare function twoC2pPaymentTarget(url: any): Readonly<{
|
|
10
|
+
environment: "production" | "sandbox";
|
|
11
|
+
version: string;
|
|
12
|
+
reviewed: boolean;
|
|
13
|
+
}> | null;
|
|
14
|
+
declare function twoC2pPaymentCarriesCard(body: any): boolean;
|
|
15
|
+
declare function twoC2pBodyVerdict(body: any, keyBytes?: number, environment?: undefined): Readonly<{
|
|
16
|
+
verdict: "continue";
|
|
17
|
+
}> | Readonly<{
|
|
18
|
+
verdict: "refuse";
|
|
19
|
+
code: any;
|
|
20
|
+
}> | Readonly<{
|
|
21
|
+
verdict: "card";
|
|
22
|
+
}>;
|
|
23
|
+
declare function twoC2pRequestRefusal(request: any, environment: any): any;
|
|
24
|
+
declare function twoC2pRefusalMessage(code: any): any;
|
|
25
|
+
declare function encryptTwoC2pCard(card: any, environment: any, facilities?: {}): Promise<string>;
|
|
26
|
+
declare function twoC2pSubstitutionOf(value: any, environment: any): any;
|
|
27
|
+
declare function substituteTwoC2p(url: any, body: any, field: any): string;
|
|
28
|
+
declare function twoC2pSubstitutionsRefusal(template: any, substitution: any): {
|
|
29
|
+
code: string;
|
|
30
|
+
message: any;
|
|
31
|
+
} | null;
|
|
32
|
+
declare function twoC2pKeyUsable(authorization: any): boolean;
|
|
33
|
+
export { TWOC2P_CAPABILITY, TWOC2P_FIELD, TWOC2P_FIELD_AT, TWOC2P_OPERATION, TWOC2P_PSP, TWOC2P_REFUSAL_CODES, TWOC2P_REVIEWED_BUILDS, TWOC2P_SCHEME, encryptTwoC2pCard, substituteTwoC2p, twoC2pBodyVerdict, twoC2pKeyUsable, twoC2pPaymentCarriesCard, twoC2pPaymentTarget, twoC2pRefusalMessage, twoC2pRequestRefusal, twoC2pSubstitutionOf, twoC2pSubstitutionsRefusal };
|