@agent-cards/sdk 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/PREFLIGHT.md +34 -0
- package/README.md +1217 -15
- package/dist/aci.d.ts +173 -0
- package/dist/aci.generated.d.ts +87 -0
- package/dist/aci.generated.js +306 -0
- package/dist/aci.js +371 -0
- package/dist/adyen-merchant-hosted.generated.d.ts +3 -3
- package/dist/adyen-merchant-hosted.generated.js +283 -49
- package/dist/agentcard.d.ts +9 -2
- package/dist/agentcard.js +4 -2
- package/dist/amadeus-checkout.generated.d.ts +57 -0
- package/dist/amadeus-checkout.generated.js +549 -0
- package/dist/amadeus.d.ts +41 -0
- package/dist/amadeus.js +81 -0
- package/dist/builtin-registry.generated.js +1 -1
- package/dist/card-endpoint.d.ts +177 -0
- package/dist/card-endpoint.js +286 -0
- package/dist/card-fields.generated.d.ts +5 -3
- package/dist/card-fields.generated.js +284 -7
- package/dist/cdp.d.ts +6 -1
- package/dist/cdp.js +983 -147
- package/dist/client.d.ts +493 -13
- package/dist/client.js +1100 -82
- package/dist/creditguard.generated.d.ts +79 -0
- package/dist/creditguard.generated.js +662 -0
- package/dist/cse-body.js +11 -1
- package/dist/cybersource-flex.d.ts +183 -0
- package/dist/cybersource-flex.generated.d.ts +92 -0
- package/dist/cybersource-flex.generated.js +689 -0
- package/dist/cybersource-flex.js +275 -0
- package/dist/datatrans.d.ts +96 -0
- package/dist/datatrans.generated.d.ts +64 -0
- package/dist/datatrans.generated.js +426 -0
- package/dist/datatrans.js +178 -0
- package/dist/elavon-converge.d.ts +37 -0
- package/dist/elavon-converge.generated.d.ts +49 -0
- package/dist/elavon-converge.generated.js +439 -0
- package/dist/elavon-converge.js +84 -0
- package/dist/eprotect.d.ts +152 -0
- package/dist/eprotect.generated.d.ts +77 -0
- package/dist/eprotect.generated.js +498 -0
- package/dist/eprotect.js +222 -0
- package/dist/gmo.d.ts +63 -0
- package/dist/gmo.generated.d.ts +60 -0
- package/dist/gmo.generated.js +586 -0
- package/dist/gmo.js +119 -0
- package/dist/hosted-form.d.ts +3 -2
- package/dist/hosted-form.js +3 -2
- package/dist/index.d.ts +19 -3
- package/dist/index.js +6 -1
- package/dist/juspay-gate.d.ts +82 -0
- package/dist/juspay-gate.js +126 -0
- package/dist/juspay.generated.d.ts +89 -0
- package/dist/juspay.generated.js +705 -0
- package/dist/lifecycle.d.ts +62 -6
- package/dist/lifecycle.js +123 -8
- package/dist/merchant-card-endpoints.generated.d.ts +63 -0
- package/dist/merchant-card-endpoints.generated.js +1103 -0
- package/dist/merchant-hosted.d.ts +4 -1
- package/dist/merchant-hosted.js +5 -2
- package/dist/merchant-session-probe.d.ts +48 -0
- package/dist/merchant-session-probe.js +53 -0
- package/dist/merchant-session.generated.d.ts +69 -0
- package/dist/merchant-session.generated.js +919 -0
- package/dist/payrails.d.ts +133 -0
- package/dist/payrails.generated.d.ts +73 -0
- package/dist/payrails.generated.js +596 -0
- package/dist/payrails.js +305 -0
- package/dist/pelecard.d.ts +32 -0
- package/dist/pelecard.generated.d.ts +32 -0
- package/dist/pelecard.generated.js +283 -0
- package/dist/pelecard.js +42 -0
- package/dist/pre-claim.d.ts +84 -3
- package/dist/pre-claim.js +987 -9
- package/dist/preflight-catalog.json +2310 -263
- package/dist/preflight-schemas.json +187 -6
- package/dist/preflight.generated.d.ts +1 -1
- package/dist/preflight.generated.js +247 -4
- package/dist/preparation.d.ts +5 -2
- package/dist/preparation.js +36 -10
- package/dist/prepared-processor.d.ts +86 -5
- package/dist/prepared-processor.js +157 -6
- package/dist/primer.d.ts +53 -0
- package/dist/primer.generated.d.ts +38 -0
- package/dist/primer.generated.js +624 -0
- package/dist/primer.js +82 -0
- package/dist/registry.d.ts +114 -12
- package/dist/registry.js +85 -11
- package/dist/shiji-sts.d.ts +144 -0
- package/dist/shiji-sts.generated.d.ts +72 -0
- package/dist/shiji-sts.generated.js +913 -0
- package/dist/shiji-sts.js +222 -0
- package/dist/stripe-checkout.generated.d.ts +1 -1
- package/dist/stripe-checkout.generated.js +735 -10
- package/dist/stripe-setup.d.ts +16 -0
- package/dist/stripe-setup.js +61 -0
- package/dist/substitutions.generated.js +187 -29
- package/dist/verifone.d.ts +68 -0
- package/dist/verifone.generated.d.ts +51 -0
- package/dist/verifone.generated.js +1401 -0
- package/dist/verifone.js +119 -0
- package/dist/voice-types.d.ts +27 -0
- package/dist/voice.d.ts +6 -1
- package/dist/voice.js +6 -1
- package/dist/wspay.d.ts +41 -0
- package/dist/wspay.generated.d.ts +41 -0
- package/dist/wspay.generated.js +370 -0
- package/dist/wspay.js +65 -0
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +3 -3
package/dist/payrails.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// Payrails web SDK v6 secure fields (Blacklane's consumer checkout), as the checkout SDK sees it.
|
|
2
|
+
//
|
|
3
|
+
// payrails.js 6.5.0 saves a card with one fetch from the merchant page: a POST to the session's
|
|
4
|
+
// vaultConfiguration.links.saveInstrument.href (https://api-pub.payrails.io/public/payment/instruments;
|
|
5
|
+
// test hosts api-pub.staging.payrails.io and sandbox-api-pub.staging.payrails.io). Its body carries
|
|
6
|
+
// the whole card as one JWE, made in Payrails' secure frame under a per-session RSA key. That key
|
|
7
|
+
// is not in the paused request: it comes in the init payload ({version, data}) the merchant's
|
|
8
|
+
// backend hands the page before the card form renders. So the runtime reads the page's own XHR and
|
|
9
|
+
// fetch answers for that payload and keeps, per attachment and in memory only, each session's
|
|
10
|
+
// token, holderReference, providerConfigId, saveInstrument href and key. When a card save pauses,
|
|
11
|
+
// exactly one captured session must match it on all four, seen at most nine minutes before. Its key
|
|
12
|
+
// rides on the create (request.payrails.encryption_public_key); the cardholder's device encrypts
|
|
13
|
+
// the real card under it and sends the save itself (mode device_encrypt); and the page receives
|
|
14
|
+
// only Payrails' 201, rebuilt from an allow-list. The device's ciphertext never reaches this runtime.
|
|
15
|
+
//
|
|
16
|
+
// The registry entry ships dark (capability payrails_device_encrypt). This build declares the
|
|
17
|
+
// capability and the mode on syncRegistry, and the API serves the entry only to an org Agentcard
|
|
18
|
+
// enabled for Payrails. For every other org nothing here runs: no answer is read, no Payrails host
|
|
19
|
+
// is intercepted, and Payrails' requests go through untouched, exactly as before.
|
|
20
|
+
//
|
|
21
|
+
// For an org in the dark launch, the pre-claim gate (pre-claim.ts) judges every POST to a Payrails
|
|
22
|
+
// host before the terminal arbiter would pause it:
|
|
23
|
+
// - a card save payment-core's guard refuses (not the SDK's exact body, a future usage with no
|
|
24
|
+
// approval wording, or a save that keeps the card on file without the cardholder's consent)
|
|
25
|
+
// aborts as an unsupported checkout before any create or prompt, and the attachment holds: the
|
|
26
|
+
// merchant's integration decides that shape, so the page's next save would be the same;
|
|
27
|
+
// - a card save with no captured session, or whose captured key is not as long as the key the
|
|
28
|
+
// page's frame wrapped, aborts alone: the agent reloads the checkout and tries again;
|
|
29
|
+
// - a payment that carries an encrypted card inside paymentComposition (paying with the card
|
|
30
|
+
// form, or a stored card with a CVC field) aborts as an unsupported checkout, so the agent's
|
|
31
|
+
// placeholder card never reaches an authorization;
|
|
32
|
+
// - a POST with no body the gate can read (none, an empty one, or one the adapter could not
|
|
33
|
+
// read) aborts alone, on both adapters alike (pre-claim.ts says why), except on Payrails'
|
|
34
|
+
// static host, which serves files and takes no payment;
|
|
35
|
+
// - every other request continues.
|
|
36
|
+
import { PAYRAILS_CAPABILITY, PAYRAILS_PSP, PAYRAILS_SAVE_HOSTS, PAYRAILS_SAVE_PATH, classifyPayrailsRequest, isPayrailsSaveUrl, payrailsAuthorizeCarriesCard, payrailsCardOnFileRefusal, payrailsResponseOutcome, payrailsSessionKeyRefusal, } from './payrails.generated.js';
|
|
37
|
+
export { PAYRAILS_CAPABILITY, PAYRAILS_PSP };
|
|
38
|
+
/**
|
|
39
|
+
* How the pre-claim gate reads a POST to a Payrails host: `card` for a card save on a save URL
|
|
40
|
+
* (data.encryptedData present, payment-core's pause predicate), `card_payment` for a payment that
|
|
41
|
+
* carries an encrypted card inside paymentComposition, and null for anything else, which continues.
|
|
42
|
+
*/
|
|
43
|
+
export function payrailsRequestKind(url, method, body) {
|
|
44
|
+
if (method.toUpperCase() !== 'POST')
|
|
45
|
+
return null;
|
|
46
|
+
if (classifyPayrailsRequest(url, 'POST', body) === 'card')
|
|
47
|
+
return 'card';
|
|
48
|
+
return payrailsAuthorizeCarriesCard(url, 'POST', body) ? 'card_payment' : null;
|
|
49
|
+
}
|
|
50
|
+
/** Refusals of the save's own shape: the page's next save would be the same, so the attachment holds. */
|
|
51
|
+
export const PAYRAILS_HOLDING_REASONS = [
|
|
52
|
+
'unsupported_payrails_card_payment', 'payrails_request_malformed', 'payrails_not_card_save',
|
|
53
|
+
'payrails_jwe_malformed', 'payrails_future_usage_refused', 'payrails_card_on_file_consent_required',
|
|
54
|
+
];
|
|
55
|
+
/** What the agent reads when a save's session cannot be bound. Not shown to the cardholder. */
|
|
56
|
+
export const PAYRAILS_SESSION_MESSAGE = 'Payrails session expired, reload the checkout';
|
|
57
|
+
/** Captured sessions kept per attachment; the oldest is dropped first. */
|
|
58
|
+
export const PAYRAILS_CAPTURE_LIMIT = 8;
|
|
59
|
+
/** How long a captured session is kept at all. */
|
|
60
|
+
export const PAYRAILS_CAPTURE_TTL_MS = 15 * 60_000;
|
|
61
|
+
/** How old a captured session may be when its save pauses: Payrails gives a session key ten minutes. */
|
|
62
|
+
export const PAYRAILS_SESSION_MAX_AGE_MS = 9 * 60_000;
|
|
63
|
+
/** The largest answer body read for an init payload. */
|
|
64
|
+
export const PAYRAILS_INIT_BODY_LIMIT = 256 * 1024;
|
|
65
|
+
/** How long a paused save waits for answer bodies still being read before it binds. */
|
|
66
|
+
export const PAYRAILS_PENDING_READ_MS = 1_000;
|
|
67
|
+
/** Fetch interception patterns for every Payrails host, armed only for an org the API served Payrails to. */
|
|
68
|
+
export const PAYRAILS_GUARD_PATTERNS = ['https://*.payrails.io/*'];
|
|
69
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
70
|
+
const text = (value, max) => typeof value === 'string' && value.length > 0 && value.length <= max
|
|
71
|
+
&& !/[\u0000-\u001f\u007f]/.test(value);
|
|
72
|
+
/** Standard base64 of UTF-8 JSON, as Payrails writes an init payload's `data`; null when it is not. */
|
|
73
|
+
function base64Json(value) {
|
|
74
|
+
if (value.length > PAYRAILS_INIT_BODY_LIMIT)
|
|
75
|
+
return null;
|
|
76
|
+
try {
|
|
77
|
+
const bytes = Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
|
|
78
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** The binding fields of one init payload `{version, data}`, or null when `candidate` is not one. */
|
|
85
|
+
function sessionOf(candidate) {
|
|
86
|
+
if (!record(candidate) || typeof candidate.version !== 'string' || typeof candidate.data !== 'string')
|
|
87
|
+
return null;
|
|
88
|
+
const data = base64Json(candidate.data);
|
|
89
|
+
if (!record(data) || !record(data.vaultConfiguration))
|
|
90
|
+
return null;
|
|
91
|
+
const vault = data.vaultConfiguration;
|
|
92
|
+
const save = record(vault.links) ? vault.links.saveInstrument : undefined;
|
|
93
|
+
const href = record(save) ? save.href : undefined;
|
|
94
|
+
if (!text(data.token, 8192) || !text(data.holderReference, 256) || !text(vault.providerConfigId, 64)
|
|
95
|
+
|| !text(vault.encryptionPublicKey, 1024) || !text(href, 2048) || !isPayrailsSaveUrl(href, { exact: true }))
|
|
96
|
+
return null;
|
|
97
|
+
return { token: data.token, holderReference: data.holderReference, providerConfigId: vault.providerConfigId,
|
|
98
|
+
saveInstrumentHref: href, encryptionPublicKey: vault.encryptionPublicKey };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Every Payrails init payload one JSON answer body carries: the body itself, or its members one
|
|
102
|
+
* level down (an object's values or an array's items). An init payload is `{version, data}` whose
|
|
103
|
+
* `data` decodes to JSON with a token, a holderReference and a vaultConfiguration naming its
|
|
104
|
+
* providerConfigId, its encryptionPublicKey and a saveInstrument href on a Payrails save URL.
|
|
105
|
+
*/
|
|
106
|
+
export function payrailsInitSessions(body) {
|
|
107
|
+
if (typeof body !== 'string' || body.length === 0 || body.length > PAYRAILS_INIT_BODY_LIMIT)
|
|
108
|
+
return [];
|
|
109
|
+
let parsed;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(body);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const top = sessionOf(parsed);
|
|
117
|
+
if (top)
|
|
118
|
+
return [top];
|
|
119
|
+
const members = Array.isArray(parsed) ? parsed : record(parsed) ? Object.values(parsed) : [];
|
|
120
|
+
return members.map(sessionOf).filter((session) => session !== null);
|
|
121
|
+
}
|
|
122
|
+
/** Whether an answer may carry an init payload worth reading: a 2xx XHR or fetch with a JSON type. */
|
|
123
|
+
export function payrailsInitCandidate(resourceType, status, contentType) {
|
|
124
|
+
return typeof resourceType === 'string' && ['xhr', 'fetch'].includes(resourceType.toLowerCase())
|
|
125
|
+
&& typeof status === 'number' && status >= 200 && status < 300
|
|
126
|
+
&& typeof contentType === 'string' && /json/i.test(contentType);
|
|
127
|
+
}
|
|
128
|
+
const sameSession = (a, b) => a.token === b.token && a.holderReference === b.holderReference
|
|
129
|
+
&& a.providerConfigId === b.providerConfigId && a.saveInstrumentHref === b.saveInstrumentHref
|
|
130
|
+
&& a.encryptionPublicKey === b.encryptionPublicKey;
|
|
131
|
+
/** The bearer token a paused save carries in `authorization`, any case of the name; null otherwise. */
|
|
132
|
+
function bearerOf(headers) {
|
|
133
|
+
const values = Object.entries(headers ?? {}).filter(([name]) => name.toLowerCase() === 'authorization').map(([, value]) => value);
|
|
134
|
+
if (values.length !== 1 || typeof values[0] !== 'string')
|
|
135
|
+
return null;
|
|
136
|
+
const match = /^Bearer ([^\s]+)$/.exec(values[0]);
|
|
137
|
+
return match ? match[1] : null;
|
|
138
|
+
}
|
|
139
|
+
/** The save body's holderReference and data.vaultProviderConfigId, read leniently; null when absent. */
|
|
140
|
+
function saveBindingOf(body) {
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(body);
|
|
143
|
+
const data = record(parsed) ? parsed.data : undefined;
|
|
144
|
+
if (!record(parsed) || !record(data) || typeof parsed.holderReference !== 'string' || typeof data.vaultProviderConfigId !== 'string')
|
|
145
|
+
return null;
|
|
146
|
+
return { holderReference: parsed.holderReference, providerConfigId: data.vaultProviderConfigId };
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The Payrails sessions one attachment has seen, in memory only: at most PAYRAILS_CAPTURE_LIMIT,
|
|
154
|
+
* each dropped PAYRAILS_CAPTURE_TTL_MS after it was first seen. Nothing here is ever logged or
|
|
155
|
+
* reported; the key leaves it only on the create for the one save it binds to.
|
|
156
|
+
*
|
|
157
|
+
* The capture runs on the agent's side and trusts nothing it reads: a page can hand it any key.
|
|
158
|
+
* The binding catches a stale session or a runtime bug. What bounds a key an agent supplies is
|
|
159
|
+
* elsewhere: the device sends the ciphertext only to a Payrails host, the API holds the token to
|
|
160
|
+
* the Payrails workspace Agentcard reviewed for the merchant, and the page never sees the
|
|
161
|
+
* ciphertext the device made.
|
|
162
|
+
*/
|
|
163
|
+
export class PayrailsSessionCapture {
|
|
164
|
+
sessions = [];
|
|
165
|
+
reads = new Set();
|
|
166
|
+
/** Record the init payloads an answer body carries; how many it carried. Never throws. */
|
|
167
|
+
observe(body, now = Date.now()) {
|
|
168
|
+
let found;
|
|
169
|
+
try {
|
|
170
|
+
found = payrailsInitSessions(body);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
this.prune(now);
|
|
176
|
+
for (const session of found) {
|
|
177
|
+
// A session seen again keeps its first sighting: its age is the session's, not the read's.
|
|
178
|
+
if (this.sessions.some((seen) => sameSession(seen, session)))
|
|
179
|
+
continue;
|
|
180
|
+
this.sessions.push({ ...session, seenAt: now });
|
|
181
|
+
if (this.sessions.length > PAYRAILS_CAPTURE_LIMIT)
|
|
182
|
+
this.sessions.shift();
|
|
183
|
+
}
|
|
184
|
+
return found.length;
|
|
185
|
+
}
|
|
186
|
+
/** An answer body still being read: a save that pauses meanwhile waits for it, briefly. */
|
|
187
|
+
reading(read) {
|
|
188
|
+
const tracked = read.then(() => { }, () => { }).finally(() => { this.reads.delete(tracked); });
|
|
189
|
+
this.reads.add(tracked);
|
|
190
|
+
}
|
|
191
|
+
/** Whether an answer body is still being read. */
|
|
192
|
+
get pending() { return this.reads.size > 0; }
|
|
193
|
+
/** Wait for the answer bodies being read, at most `ms`. */
|
|
194
|
+
async settle(ms = PAYRAILS_PENDING_READ_MS) {
|
|
195
|
+
if (!this.reads.size)
|
|
196
|
+
return;
|
|
197
|
+
let timer;
|
|
198
|
+
try {
|
|
199
|
+
await Promise.race([Promise.allSettled([...this.reads]), new Promise((resolve) => { timer = setTimeout(resolve, ms); })]);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
if (timer)
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The session key for a paused card save: the key of the captured session whose token is the
|
|
208
|
+
* save's bearer token, whose saveInstrument href is the save's URL, and whose holderReference and
|
|
209
|
+
* providerConfigId are the body's, first seen at most PAYRAILS_SESSION_MAX_AGE_MS before `now`.
|
|
210
|
+
* Null when none matches, or when matching sessions disagree on the key.
|
|
211
|
+
*/
|
|
212
|
+
keyFor(request, now = Date.now()) {
|
|
213
|
+
this.prune(now);
|
|
214
|
+
const token = bearerOf(request.headers);
|
|
215
|
+
const binding = typeof request.body === 'string' ? saveBindingOf(request.body) : null;
|
|
216
|
+
if (!token || !binding)
|
|
217
|
+
return null;
|
|
218
|
+
const keys = new Set(this.sessions.filter((session) => session.token === token && session.saveInstrumentHref === request.url
|
|
219
|
+
&& session.holderReference === binding.holderReference && session.providerConfigId === binding.providerConfigId
|
|
220
|
+
&& session.seenAt <= now && now - session.seenAt <= PAYRAILS_SESSION_MAX_AGE_MS).map((session) => session.encryptionPublicKey));
|
|
221
|
+
return keys.size === 1 ? [...keys][0] : null;
|
|
222
|
+
}
|
|
223
|
+
/** How many sessions are held; for tests. */
|
|
224
|
+
get size() { return this.sessions.length; }
|
|
225
|
+
prune(now) {
|
|
226
|
+
this.sessions = this.sessions.filter((session) => session.seenAt <= now && now - session.seenAt < PAYRAILS_CAPTURE_TTL_MS);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Whether the vault's current registry pauses Payrails' card save in mode device_encrypt: true only
|
|
231
|
+
* after syncRegistry() for an org the API serves the dark entry to. Everything Payrails-specific in
|
|
232
|
+
* the adapters (reading answers, intercepting Payrails hosts) runs only when this is true.
|
|
233
|
+
*/
|
|
234
|
+
export function recognizesPayrails(vault) {
|
|
235
|
+
if (!vault || typeof vault.isCardRequest !== 'function')
|
|
236
|
+
return false;
|
|
237
|
+
try {
|
|
238
|
+
return PAYRAILS_SAVE_HOSTS.some((host) => {
|
|
239
|
+
const url = `https://${host}${PAYRAILS_SAVE_PATH}`;
|
|
240
|
+
return vault.isCardRequest(url, 'POST')
|
|
241
|
+
&& (typeof vault.checkoutModeOf !== 'function' || vault.checkoutModeOf(url, 'POST') === 'device_encrypt');
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** A Payrails host: https on the standard port, no userinfo, a hostname under payrails.io. */
|
|
249
|
+
export function isPayrailsHost(rawUrl) {
|
|
250
|
+
let url;
|
|
251
|
+
try {
|
|
252
|
+
url = new URL(rawUrl);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
return url.protocol === 'https:' && !url.port && !url.username && !url.password
|
|
258
|
+
&& /(?:^|\.)payrails\.io$/.test(url.hostname.toLowerCase().replace(/\.$/, ''));
|
|
259
|
+
}
|
|
260
|
+
/** Payrails' static host: it serves the web SDK and the secure frame's files, and takes no payment. */
|
|
261
|
+
export const PAYRAILS_STATIC_HOST = 'assets.payrails.io';
|
|
262
|
+
/** A Payrails host (isPayrailsHost) that is exactly the static host. */
|
|
263
|
+
export function isPayrailsStaticHost(rawUrl) {
|
|
264
|
+
return isPayrailsHost(rawUrl) && new URL(rawUrl).hostname.toLowerCase().replace(/\.$/, '') === PAYRAILS_STATIC_HOST;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* payment-core's judgement of a card save's own shape, before any session is looked up: null when
|
|
268
|
+
* the save is the SDK's exact card save and keeps no card on file the cardholder has not consented
|
|
269
|
+
* to, else the refusal code. A guard that throws refuses.
|
|
270
|
+
*/
|
|
271
|
+
export function payrailsSaveRefusal(body) {
|
|
272
|
+
try {
|
|
273
|
+
return payrailsCardOnFileRefusal(body) ?? null;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return 'payrails_request_malformed';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The key check for a bound session: null when the captured key is an RSA-2048, 3072 or 4096 key as
|
|
281
|
+
* long as the key the page's frame wrapped for this save, else the refusal code.
|
|
282
|
+
*/
|
|
283
|
+
export function payrailsKeyRefusal(key, body) {
|
|
284
|
+
try {
|
|
285
|
+
return payrailsSessionKeyRefusal(key, body) ?? null;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return 'payrails_key_invalid';
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The only answer the page may receive for an approved Payrails save: Payrails' 201 for a card
|
|
293
|
+
* instrument, rebuilt from payment-core's allow-list (the BIN cut to six digits), under a JSON
|
|
294
|
+
* content type. Null for anything else.
|
|
295
|
+
*/
|
|
296
|
+
export function payrailsSaveAnswer(status, body) {
|
|
297
|
+
try {
|
|
298
|
+
const verdict = payrailsResponseOutcome(status, body);
|
|
299
|
+
const answer = verdict.outcome === 'approved' ? verdict.body : undefined;
|
|
300
|
+
return typeof answer === 'string' ? { status: 201, headers: { 'content-type': 'application/json' }, body: answer } : null;
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const PELECARD_PSP = "pelecard";
|
|
2
|
+
/** The dark-launch capability the API serves the pelecard recognizer behind. */
|
|
3
|
+
export declare const PELECARD_CAPABILITY = "pelecard_transaction";
|
|
4
|
+
/**
|
|
5
|
+
* Why the SDK refuses a Pelecard card submission before anything is claimed, created or prompted.
|
|
6
|
+
* The codes are the API's own for the same requests, so an integrator reads one code whether the
|
|
7
|
+
* browser or the API refused:
|
|
8
|
+
* - `invalid_pelecard_request`: not the page's own one-time card submission. That covers a saved
|
|
9
|
+
* token, a card track, a phone authorization, a wallet, Bit or bank field beside a card,
|
|
10
|
+
* installments, a buyer-typed total, a key the page never builds, the exact URL spelled any
|
|
11
|
+
* other way, or headers the page does not send. The cardholder's device could never fill it.
|
|
12
|
+
* - `pelecard_keep_on_file_unsupported`: a sound submission that asks Pelecard to keep the card
|
|
13
|
+
* on file (AskForTokenChecked true, or any CreateToken), so the merchant could charge it later
|
|
14
|
+
* without another approval. Only the cardholder can agree to that, and no approval asks them yet.
|
|
15
|
+
*/
|
|
16
|
+
export type PelecardRefusalReason = 'invalid_pelecard_request' | 'pelecard_keep_on_file_unsupported';
|
|
17
|
+
/** True when the URL's host is one of Pelecard's two payment page hosts. */
|
|
18
|
+
export declare function isPelecardHost(url: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The pause decision for a card-carrying POST the registry recognized on a Pelecard host. Null
|
|
21
|
+
* when it is the page's own one-time new-card submission, which pauses for approval; otherwise
|
|
22
|
+
* the reason it is refused. It judges the whole request (the exact URL, the page's JSON content
|
|
23
|
+
* type with Payment.js's own `charse0t` spelling, jQuery's X-Requested-With, and a body of the
|
|
24
|
+
* page's own keys) with payment-core's pelecardTransactionRefusal, the check the API runs at
|
|
25
|
+
* create and the device runs before it writes a card. It never passes keep-on-file consent:
|
|
26
|
+
* that waits for an approval that asks the cardholder.
|
|
27
|
+
*/
|
|
28
|
+
export declare function pelecardRefusal(request: {
|
|
29
|
+
url: string;
|
|
30
|
+
headers: Record<string, string>;
|
|
31
|
+
body: string | null;
|
|
32
|
+
}): PelecardRefusalReason | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
declare var PELECARD_TRANSACTION_HOSTS: readonly string[];
|
|
2
|
+
declare var PELECARD_TRANSACTION_PATH: string;
|
|
3
|
+
declare var PELECARD_REFUSAL_REASONS: readonly string[];
|
|
4
|
+
declare function isPelecardTransactionUrl(url: any, method?: string): boolean;
|
|
5
|
+
declare function pelecardBodyRefusal(body: any): "unsupported" | "keep_on_file" | null;
|
|
6
|
+
declare function isPelecardNewCardBody(body: any): boolean;
|
|
7
|
+
declare function pelecardKeepOnFileRequested(body: any): boolean | null;
|
|
8
|
+
declare function classifyPelecardRequest(url: any, method: any, body: any): "card" | "unsupported" | "keep_on_file" | null;
|
|
9
|
+
declare function pelecardTransactionRefusal(request: any): "unsupported" | "keep_on_file" | null;
|
|
10
|
+
declare function isPelecardTransactionRequest(request: any): boolean;
|
|
11
|
+
declare function pelecardRefusalMessage(reason: any): string;
|
|
12
|
+
declare function injectPelecardCard(body: any, card: any): {
|
|
13
|
+
body: string;
|
|
14
|
+
where: string[];
|
|
15
|
+
};
|
|
16
|
+
declare function pelecardResponseOutcome(status: any, body: any): {
|
|
17
|
+
outcome: string;
|
|
18
|
+
code: any;
|
|
19
|
+
reason?: undefined;
|
|
20
|
+
} | {
|
|
21
|
+
outcome: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
code?: undefined;
|
|
24
|
+
} | {
|
|
25
|
+
outcome: string;
|
|
26
|
+
};
|
|
27
|
+
declare function sanitizePelecardResponse(body: any): string;
|
|
28
|
+
declare function pelecardFacts(status: any, body: any): {
|
|
29
|
+
outcome: string;
|
|
30
|
+
refusalCode: any;
|
|
31
|
+
};
|
|
32
|
+
export { PELECARD_REFUSAL_REASONS, PELECARD_TRANSACTION_HOSTS, PELECARD_TRANSACTION_PATH, classifyPelecardRequest, injectPelecardCard, isPelecardNewCardBody, isPelecardTransactionRequest, isPelecardTransactionUrl, pelecardBodyRefusal, pelecardFacts, pelecardKeepOnFileRequested, pelecardRefusalMessage, pelecardResponseOutcome, pelecardTransactionRefusal, sanitizePelecardResponse };
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
+
// artifact-sha256: 7b1296f83822ae81c64835c270ff9048fddb6efe0d7cdbe37ba42b8a81ac03d0
|
|
4
|
+
// src/json-path.js
|
|
5
|
+
function roundTrips(parsed, body) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.stringify(parsed) === body;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// src/pelecard.js
|
|
14
|
+
var PELECARD_TRANSACTION_HOSTS = /* @__PURE__ */ Object.freeze(["gateway21.pelecard.biz", "gateway20.pelecard.biz"]);
|
|
15
|
+
var PELECARD_TRANSACTION_PATH = "/PaymentGW/Transaction";
|
|
16
|
+
var PELECARD_REFUSAL_REASONS = /* @__PURE__ */ Object.freeze(["unsupported", "keep_on_file"]);
|
|
17
|
+
var UNSUPPORTED_REFUSAL = "Refusing to pay: unsupported Pelecard card payment request.";
|
|
18
|
+
var KEEP_ON_FILE_REFUSAL = "Refusing to pay: this Pelecard payment would keep the card on file without your consent. Nothing was sent.";
|
|
19
|
+
var TEST_CARD_REFUSAL = "Refusing to pay: a Pelecard checkout takes only a documented test card, because the merchant can have Pelecard keep the card without asking you. Nothing was sent.";
|
|
20
|
+
var BODY_LIMIT = 32768;
|
|
21
|
+
var RESPONSE_LIMIT = 32768;
|
|
22
|
+
var MODEL_KEYS = [
|
|
23
|
+
"TransactionId",
|
|
24
|
+
"CreditCardNumber",
|
|
25
|
+
"CreditCardHolder",
|
|
26
|
+
"IdNumber",
|
|
27
|
+
"CreditCardDate",
|
|
28
|
+
"CvvNumber",
|
|
29
|
+
"FreeTotal",
|
|
30
|
+
"NumOfPayments",
|
|
31
|
+
"FirstPayment",
|
|
32
|
+
"EachPayment",
|
|
33
|
+
"Email",
|
|
34
|
+
"Phone",
|
|
35
|
+
"Address",
|
|
36
|
+
"City",
|
|
37
|
+
"Country",
|
|
38
|
+
"AddressIndex",
|
|
39
|
+
"Track2",
|
|
40
|
+
"Token",
|
|
41
|
+
"AgentCallTrxId",
|
|
42
|
+
"Platform",
|
|
43
|
+
"WalletType",
|
|
44
|
+
"Captcha",
|
|
45
|
+
"BankNumber",
|
|
46
|
+
"BranchNumber",
|
|
47
|
+
"AccountNumber"
|
|
48
|
+
];
|
|
49
|
+
var OPTIONAL_KEYS = ["IsIframe", "AskForTokenChecked", "BusinessNumber", "Remarks", "ReCaptchaToken", "CreateToken"];
|
|
50
|
+
var NON_CARD_KEYS = ["Track2", "Token", "AgentCallTrxId", "WalletType", "BankNumber", "BranchNumber", "AccountNumber", "Platform"];
|
|
51
|
+
var EMPTY_TERMS_KEYS = ["FreeTotal", "FirstPayment", "EachPayment"];
|
|
52
|
+
var TEXT_KEYS = ["CreditCardHolder", "IdNumber", "Email", "Phone", "Address", "City", "Country", "AddressIndex", "BusinessNumber", "Remarks"];
|
|
53
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
54
|
+
var owns = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
55
|
+
var exact = (pattern, value) => typeof value === "string" && pattern.exec(value)?.[0] === value;
|
|
56
|
+
var text = (value, max) => typeof value === "string" && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
57
|
+
var digits = (value, min, max) => typeof value === "string" && value.length >= min && value.length <= max && !/[^0-9]/.test(value);
|
|
58
|
+
var GUID = /^[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}$/;
|
|
59
|
+
var MMYY = /^(?:0[1-9]|1[0-2])[0-9]{2}$/;
|
|
60
|
+
var TYPED_PAN = /^[0-9](?:[0-9 -]{0,30}[0-9])?$/;
|
|
61
|
+
var typedPan = (value) => exact(TYPED_PAN, value) && digits(value.replace(/[ -]/g, ""), 12, 19);
|
|
62
|
+
var asksForToken = (value) => value === true || value === 1 || typeof value === "string" && /^(?:true|1)$/i.test(value.trim());
|
|
63
|
+
function isPelecardTransactionUrl(url, method = "POST") {
|
|
64
|
+
if (method !== "POST" || typeof url !== "string" || url.length > 256 || url.includes("?") || url.includes("#"))
|
|
65
|
+
return false;
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = new URL(url);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return parsed.href === url && parsed.protocol === "https:" && !parsed.port && !parsed.username && !parsed.password && !parsed.search && !parsed.hash && PELECARD_TRANSACTION_HOSTS.includes(parsed.hostname) && parsed.pathname === PELECARD_TRANSACTION_PATH;
|
|
74
|
+
}
|
|
75
|
+
function judgeBody(body) {
|
|
76
|
+
if (typeof body !== "string" || body === "" || body.length > BODY_LIMIT)
|
|
77
|
+
return null;
|
|
78
|
+
let value;
|
|
79
|
+
try {
|
|
80
|
+
value = JSON.parse(body);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (!record(value) || !roundTrips(value, body))
|
|
86
|
+
return null;
|
|
87
|
+
if (!Object.keys(value).every((key) => MODEL_KEYS.includes(key) || OPTIONAL_KEYS.includes(key)) || !MODEL_KEYS.every((key) => owns(value, key)))
|
|
88
|
+
return null;
|
|
89
|
+
if (!exact(GUID, value.TransactionId) || !typedPan(value.CreditCardNumber) || !exact(MMYY, value.CreditCardDate) || value.CvvNumber !== "" && !digits(value.CvvNumber, 3, 4))
|
|
90
|
+
return null;
|
|
91
|
+
if (value.NumOfPayments !== "1" || !EMPTY_TERMS_KEYS.every((key) => value[key] === "") || !NON_CARD_KEYS.every((key) => value[key] === ""))
|
|
92
|
+
return null;
|
|
93
|
+
if (!TEXT_KEYS.every((key) => !owns(value, key) || text(value[key], key === "Remarks" ? 1024 : 256)) || !exact(/^[A-Za-z0-9]{0,16}$/, value.Captcha) || owns(value, "ReCaptchaToken") && !exact(/^[A-Za-z0-9_-]{1,4096}$/, value.ReCaptchaToken) || owns(value, "IsIframe") && typeof value.IsIframe !== "boolean" || owns(value, "AskForTokenChecked") && typeof value.AskForTokenChecked !== "boolean" || owns(value, "CreateToken") && !asksForToken(value.CreateToken))
|
|
94
|
+
return null;
|
|
95
|
+
return { value, keepOnFile: value.AskForTokenChecked === true || owns(value, "CreateToken") };
|
|
96
|
+
}
|
|
97
|
+
function pelecardBodyRefusal(body) {
|
|
98
|
+
const judged = judgeBody(body);
|
|
99
|
+
if (!judged)
|
|
100
|
+
return "unsupported";
|
|
101
|
+
return judged.keepOnFile ? "keep_on_file" : null;
|
|
102
|
+
}
|
|
103
|
+
function isPelecardNewCardBody(body) {
|
|
104
|
+
return pelecardBodyRefusal(body) === null;
|
|
105
|
+
}
|
|
106
|
+
function pelecardKeepOnFileRequested(body) {
|
|
107
|
+
const judged = judgeBody(body);
|
|
108
|
+
return judged ? judged.keepOnFile : null;
|
|
109
|
+
}
|
|
110
|
+
function carriesCard(body) {
|
|
111
|
+
if (typeof body !== "string" || body === "")
|
|
112
|
+
return false;
|
|
113
|
+
let value;
|
|
114
|
+
try {
|
|
115
|
+
value = JSON.parse(body);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (!record(value) || !owns(value, "CreditCardNumber"))
|
|
121
|
+
return false;
|
|
122
|
+
const number = value.CreditCardNumber;
|
|
123
|
+
return typeof number === "string" && number !== "" || typeof number === "number";
|
|
124
|
+
}
|
|
125
|
+
function classifyPelecardRequest(url, method, body) {
|
|
126
|
+
if (!isPelecardTransactionUrl(url, method) || !carriesCard(body))
|
|
127
|
+
return null;
|
|
128
|
+
return pelecardBodyRefusal(body) ?? "card";
|
|
129
|
+
}
|
|
130
|
+
var CONTENT_TYPE = /^application\/json(?:;\s*(?:charset|charse0t)=utf-8)?$/i;
|
|
131
|
+
function pelecardTransactionRefusal(request) {
|
|
132
|
+
if (!record(request) || request.method !== "POST" || !isPelecardTransactionUrl(request.url) || !record(request.headers))
|
|
133
|
+
return "unsupported";
|
|
134
|
+
const headers = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
136
|
+
const lower = name.toLowerCase();
|
|
137
|
+
if (headers.has(lower) || typeof value !== "string")
|
|
138
|
+
return "unsupported";
|
|
139
|
+
headers.set(lower, value);
|
|
140
|
+
}
|
|
141
|
+
if (!exact(CONTENT_TYPE, headers.get("content-type")) || headers.has("x-requested-with") && headers.get("x-requested-with") !== "XMLHttpRequest")
|
|
142
|
+
return "unsupported";
|
|
143
|
+
return pelecardBodyRefusal(request.body);
|
|
144
|
+
}
|
|
145
|
+
function isPelecardTransactionRequest(request) {
|
|
146
|
+
return pelecardTransactionRefusal(request) === null;
|
|
147
|
+
}
|
|
148
|
+
function pelecardRefusalMessage(reason) {
|
|
149
|
+
if (reason === "test_card_required")
|
|
150
|
+
return TEST_CARD_REFUSAL;
|
|
151
|
+
return reason === "keep_on_file" ? KEEP_ON_FILE_REFUSAL : UNSUPPORTED_REFUSAL;
|
|
152
|
+
}
|
|
153
|
+
function injectPelecardCard(body, card) {
|
|
154
|
+
const judged = judgeBody(body);
|
|
155
|
+
if (!judged)
|
|
156
|
+
throw new Error(UNSUPPORTED_REFUSAL);
|
|
157
|
+
if (judged.keepOnFile)
|
|
158
|
+
throw new Error(KEEP_ON_FILE_REFUSAL);
|
|
159
|
+
const value = judged.value;
|
|
160
|
+
const withCvv = value.CvvNumber !== "";
|
|
161
|
+
const month = card && (Number.isInteger(card.month) || digits(card.month, 1, 2)) ? Number(card.month) : NaN;
|
|
162
|
+
const year = card && (Number.isInteger(card.year) || digits(card.year, 2, 4)) ? Number(card.year) : NaN;
|
|
163
|
+
if (!card || !digits(card.number, 12, 19) || !(month >= 1 && month <= 12) || !(year >= 0 && year <= 99 || year >= 2e3 && year <= 2099) || withCvv && !digits(card.cvc, 3, 4)) {
|
|
164
|
+
throw new Error("Could not fill the card fields for Pelecard. Nothing was sent.");
|
|
165
|
+
}
|
|
166
|
+
value.CreditCardNumber = card.number;
|
|
167
|
+
value.CreditCardDate = String(month).padStart(2, "0") + String(year % 100).padStart(2, "0");
|
|
168
|
+
const where = ["CreditCardNumber", "CreditCardDate"];
|
|
169
|
+
if (withCvv) {
|
|
170
|
+
value.CvvNumber = card.cvc;
|
|
171
|
+
where.push("CvvNumber");
|
|
172
|
+
}
|
|
173
|
+
if (value.CreditCardHolder !== "" && typeof card.name === "string" && card.name.trim() !== "" && text(card.name, 256)) {
|
|
174
|
+
value.CreditCardHolder = card.name;
|
|
175
|
+
where.push("CreditCardHolder");
|
|
176
|
+
}
|
|
177
|
+
return { body: JSON.stringify(value), where };
|
|
178
|
+
}
|
|
179
|
+
var REDIRECT_KEYS = [
|
|
180
|
+
"LandingURL",
|
|
181
|
+
"Target",
|
|
182
|
+
"StatusCode",
|
|
183
|
+
"StatusDescription",
|
|
184
|
+
"ApprovalNo",
|
|
185
|
+
"Token",
|
|
186
|
+
"ConfirmationKey",
|
|
187
|
+
"UserKey",
|
|
188
|
+
"UseEndOfProcess",
|
|
189
|
+
"FeedbackDataTransferMethod"
|
|
190
|
+
];
|
|
191
|
+
var panLike = (value) => /(?:[0-9][ _.:-]*){12,}/.test(value);
|
|
192
|
+
var markupSafe = (value, max) => text(value, max) && !/["'<>`\\]/.test(value);
|
|
193
|
+
var merchantValue = (value, max) => markupSafe(value, max) && !/[&=#?]/.test(value) && !panLike(value);
|
|
194
|
+
var absent = (value) => value === void 0 || value === null || value === "";
|
|
195
|
+
function readAnswer(body) {
|
|
196
|
+
if (typeof body !== "string" || body === "" || body.length > RESPONSE_LIMIT)
|
|
197
|
+
return null;
|
|
198
|
+
try {
|
|
199
|
+
const value = JSON.parse(body);
|
|
200
|
+
return record(value) ? value : null;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function landingUrl(value) {
|
|
207
|
+
if (!markupSafe(value, 2048) || panLike(value))
|
|
208
|
+
return null;
|
|
209
|
+
try {
|
|
210
|
+
const url = new URL(value);
|
|
211
|
+
return url.protocol === "https:" && !url.username && !url.password && url.href === value ? url : null;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function pelecardResponseOutcome(status, body) {
|
|
218
|
+
if (Number.isInteger(status) && status >= 400 && status < 500)
|
|
219
|
+
return clientErrorOutcome(status, body);
|
|
220
|
+
if (status !== 200)
|
|
221
|
+
return { outcome: "unknown", reason: "pelecard_status_unconfirmed" };
|
|
222
|
+
const value = readAnswer(body);
|
|
223
|
+
if (!value)
|
|
224
|
+
return { outcome: "unknown", reason: "pelecard_unreadable" };
|
|
225
|
+
const code = value.StatusCode;
|
|
226
|
+
if (!exact(/^[0-9]{3}$/, code))
|
|
227
|
+
return { outcome: "unknown", reason: "pelecard_unexpected_response" };
|
|
228
|
+
if (code !== "000") {
|
|
229
|
+
return absent(value.ApprovalNo) ? { outcome: "refused", code } : { outcome: "unknown", reason: "pelecard_unconfirmed" };
|
|
230
|
+
}
|
|
231
|
+
if (!REDIRECT_KEYS.every((key) => typeof value[key] === "string") || owns(value, "ParamX") && value.ParamX !== null && typeof value.ParamX !== "string") {
|
|
232
|
+
return { outcome: "unknown", reason: "pelecard_unexpected_response" };
|
|
233
|
+
}
|
|
234
|
+
if (value.DisplayIshurPopUp === true || !absent(value.UrlOpenFinance))
|
|
235
|
+
return { outcome: "unknown", reason: "pelecard_unconfirmed" };
|
|
236
|
+
const landing = landingUrl(value.LandingURL);
|
|
237
|
+
if (!landing)
|
|
238
|
+
return { outcome: "unknown", reason: "pelecard_unconfirmed" };
|
|
239
|
+
if (/(?:^|\.)pelecard\.biz$/.test(landing.hostname) && /threeds/i.test(landing.pathname)) {
|
|
240
|
+
return { outcome: "unknown", reason: "pelecard_three_ds_unconfirmed" };
|
|
241
|
+
}
|
|
242
|
+
return { outcome: "approved" };
|
|
243
|
+
}
|
|
244
|
+
function clientErrorOutcome(status, body) {
|
|
245
|
+
const value = readAnswer(body);
|
|
246
|
+
if (!value || !owns(value, "StatusCode") && !owns(value, "ApprovalNo"))
|
|
247
|
+
return { outcome: "refused", code: `http_${status}` };
|
|
248
|
+
const code = value.StatusCode;
|
|
249
|
+
return exact(/^[0-9]{3}$/, code) && code !== "000" && absent(value.ApprovalNo) ? { outcome: "refused", code } : { outcome: "unknown", reason: "pelecard_unconfirmed" };
|
|
250
|
+
}
|
|
251
|
+
function sanitizePelecardResponse(body) {
|
|
252
|
+
const fail = () => {
|
|
253
|
+
throw new Error("Invalid Pelecard payment response.");
|
|
254
|
+
};
|
|
255
|
+
if (pelecardResponseOutcome(200, body).outcome !== "approved")
|
|
256
|
+
return fail();
|
|
257
|
+
const value = readAnswer(body);
|
|
258
|
+
if (!exact(/^_?[A-Za-z]{0,16}$/, value.Target) || !exact(/^[A-Za-z0-9]{0,10}$/, value.ApprovalNo) || !(value.ConfirmationKey === "" || exact(/^[0-9a-fA-F]{32}$/, value.ConfirmationKey)) || !merchantValue(value.UserKey, 128) || !exact(/^(?:true|false|)$/i, value.UseEndOfProcess) || !exact(/^(?:get|post|)$/i, value.FeedbackDataTransferMethod) || typeof value.ParamX === "string" && !merchantValue(value.ParamX, 256))
|
|
259
|
+
return fail();
|
|
260
|
+
const safe = {
|
|
261
|
+
LandingURL: value.LandingURL,
|
|
262
|
+
Target: value.Target,
|
|
263
|
+
StatusCode: "000",
|
|
264
|
+
StatusDescription: markupSafe(value.StatusDescription, 256) && !panLike(value.StatusDescription) ? value.StatusDescription : "",
|
|
265
|
+
ApprovalNo: value.ApprovalNo,
|
|
266
|
+
Token: "",
|
|
267
|
+
ConfirmationKey: value.ConfirmationKey
|
|
268
|
+
};
|
|
269
|
+
if (owns(value, "ParamX"))
|
|
270
|
+
safe.ParamX = value.ParamX;
|
|
271
|
+
safe.UserKey = value.UserKey;
|
|
272
|
+
safe.UseEndOfProcess = value.UseEndOfProcess;
|
|
273
|
+
safe.FeedbackDataTransferMethod = value.FeedbackDataTransferMethod;
|
|
274
|
+
return JSON.stringify(safe);
|
|
275
|
+
}
|
|
276
|
+
function pelecardFacts(status, body) {
|
|
277
|
+
const verdict = pelecardResponseOutcome(status, body);
|
|
278
|
+
return {
|
|
279
|
+
outcome: verdict.outcome,
|
|
280
|
+
refusalCode: verdict.outcome === "refused" && /^[0-9]{3}$/.test(verdict.code) ? verdict.code : null
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export { PELECARD_REFUSAL_REASONS, PELECARD_TRANSACTION_HOSTS, PELECARD_TRANSACTION_PATH, classifyPelecardRequest, injectPelecardCard, isPelecardNewCardBody, isPelecardTransactionRequest, isPelecardTransactionUrl, pelecardBodyRefusal, pelecardFacts, pelecardKeepOnFileRequested, pelecardRefusalMessage, pelecardResponseOutcome, pelecardTransactionRefusal, sanitizePelecardResponse };
|