@agent-cards/sdk 0.23.27 → 0.23.28
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 +8 -0
- package/dist/juspay-gate.js +2 -2
- package/dist/juspay.generated.d.ts +2 -2
- package/dist/juspay.generated.js +174 -4
- package/dist/payrexx.generated.js +176 -2
- package/dist/wspay.generated.d.ts +1 -1
- package/dist/wspay.generated.js +175 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ An unreleased change waits as a file of its own in `changes/` (`changes/README.m
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.23.28
|
|
8
|
+
|
|
9
|
+
- Hand the page no Juspay `/txns` answer whose bank redirect could carry the card. The page receives only Juspay's order id, transaction id, status and bank redirect, and the SDK now hands over an approved answer only when the redirect's pay/start link and its parameters name no 3-D Secure PaReq and hold no compressed value. A PaReq carries the card deflated, and any compressed value can, where a check that reads text never finds it. For any other answer, `authorize()` throws `PaymentOutcomeUnknownError` with reason `juspay_response_unconfirmed`, and the page never gets it. Agentcard's API checks the answer the same way before it stores it, whatever SDK version asks, so this is a second check behind it.
|
|
10
|
+
|
|
11
|
+
- Read Juspay's transaction id the same way. The page gets the transaction id beside the bank redirect, so a `/txns` answer whose transaction id names a 3-D Secure PaReq or holds a compressed value now ends the same way, with `PaymentOutcomeUnknownError` and reason `juspay_response_unconfirmed`.
|
|
12
|
+
|
|
13
|
+
- Stop Payrexx Pay's 3-D Secure posts after an answer whose 3-D Secure step could carry the card. When Payrexx answers the card request by starting 3-D Secure, the frame posts the card again for the authentication and the post-authentication, and the cardholder's device sends each post with the real card. An answer whose method or ACS address, or any of its members, names a 3-D Secure PaReq or holds a compressed value no longer opens those posts: each one fails in the browser, and the checkout reads `unsupported` with reason `unsupported_payrexx_card_resend`. Agentcard's API judges the answer the same way before it hands one over, so this is a second check behind it.
|
|
14
|
+
|
|
7
15
|
## 0.23.27
|
|
8
16
|
|
|
9
17
|
- Pause Pin Payments' card request for the cardholder's device, for a company Agentcard turned Pin Payments on for. Pin's Hosted Fields (its control frame on `cdn.pinpayments.com`) and Pin.js on a merchant's own page send the card as JSON to `https://api.pinpayments.com/1/cards` under the merchant's publishable key, and Pin answers with a card token the merchant's server charges afterwards. The sync now also asks for `capabilities=pin_payments_card_token`, and the API serves Pin's recognizer only for a company Agentcard turned Pin on for. The SDK pauses only the card fields' own request at that exact URL: a secret key, an amount or any other body there fails in your browser before anything is created, and Pin's JSONP form and a card's own record are never paused. The cardholder's device sends the request to Pin itself with the real card, and the page gets Pin's card token. Pin's request names no price, so pass `amount` and `currency`, above zero: without them `authorize()` throws `AmountRequiredError` before anything is created. The API pays only a Pin account Agentcard reviewed for the checkout page the request comes from. Matching API and Vault releases are required. Pin Payments stays off for every company Agentcard has not turned it on for.
|
package/dist/juspay-gate.js
CHANGED
|
@@ -116,9 +116,9 @@ export class JuspayCheckoutGate {
|
|
|
116
116
|
export function juspayPageAnswer(status, body, request) {
|
|
117
117
|
if (status !== 200)
|
|
118
118
|
return null;
|
|
119
|
-
// No token argument: like the API, the SDK never holds the real token the device's tokenize returned.
|
|
119
|
+
// No token or card argument: like the API, the SDK never holds the real token the device's tokenize returned, or the card.
|
|
120
120
|
try {
|
|
121
|
-
return sanitizeJuspayTxnsResponse(body, request, undefined);
|
|
121
|
+
return sanitizeJuspayTxnsResponse(body, request, undefined, undefined);
|
|
122
122
|
}
|
|
123
123
|
catch {
|
|
124
124
|
return null;
|
|
@@ -69,8 +69,8 @@ declare function readJuspayTokenizeReply(status: any, text: any): {
|
|
|
69
69
|
token?: undefined;
|
|
70
70
|
};
|
|
71
71
|
declare function isJuspayTokenizeRefusal(code: any): boolean;
|
|
72
|
-
declare function sanitizeJuspayTxnsResponse(text: any, template: any, token: any): string;
|
|
73
|
-
declare function juspayTxnsOutcome(status: any, text: any, template: any, token: any): {
|
|
72
|
+
declare function sanitizeJuspayTxnsResponse(text: any, template: any, token: any, card: any): string;
|
|
73
|
+
declare function juspayTxnsOutcome(status: any, text: any, template: any, token: any, card: any): {
|
|
74
74
|
outcome: string;
|
|
75
75
|
reason: string;
|
|
76
76
|
code?: undefined;
|
package/dist/juspay.generated.js
CHANGED
|
@@ -1,6 +1,174 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
-
// artifact-sha256:
|
|
3
|
+
// artifact-sha256: e7c7e538df5795d38e76bb0d005b7c053e5aecd796034c2195c57a1541901827
|
|
4
|
+
// src/three-ds-step.js
|
|
5
|
+
var RUN = /[A-Za-z0-9+/_-]{12,}/g;
|
|
6
|
+
var RUN_LENGTH = 12;
|
|
7
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
8
|
+
var LAYERS = 2;
|
|
9
|
+
var PAREQ = /pareq/i;
|
|
10
|
+
var PAGE_PAREQ = /\bname\s*=\s*["']?[^"'\s>]*pareq/i;
|
|
11
|
+
var THREE_DS_STEP_UNREADABLE = "unreadable_three_ds_step";
|
|
12
|
+
var THREE_DS_STEP_HOLDS_CARD = "card_in_three_ds_step";
|
|
13
|
+
var PERCENT_LAYERS = 8;
|
|
14
|
+
function percentDecoded(text) {
|
|
15
|
+
let out = text;
|
|
16
|
+
for (let layer = 0; layer < PERCENT_LAYERS; layer += 1) {
|
|
17
|
+
const next = out.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
18
|
+
if (next === out)
|
|
19
|
+
break;
|
|
20
|
+
out = next;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
var NAMED = /* @__PURE__ */ Object.freeze({
|
|
25
|
+
amp: "&",
|
|
26
|
+
percnt: "%",
|
|
27
|
+
num: "#",
|
|
28
|
+
quest: "?",
|
|
29
|
+
semi: ";",
|
|
30
|
+
excl: "!",
|
|
31
|
+
dollar: "$",
|
|
32
|
+
lpar: "(",
|
|
33
|
+
rpar: ")",
|
|
34
|
+
ast: "*",
|
|
35
|
+
comma: ",",
|
|
36
|
+
commat: "@",
|
|
37
|
+
lsqb: "[",
|
|
38
|
+
rsqb: "]",
|
|
39
|
+
lcub: "{",
|
|
40
|
+
rcub: "}",
|
|
41
|
+
verbar: "|",
|
|
42
|
+
bsol: "\\",
|
|
43
|
+
hat: "^",
|
|
44
|
+
grave: "`",
|
|
45
|
+
plus: "+",
|
|
46
|
+
sol: "/",
|
|
47
|
+
equals: "=",
|
|
48
|
+
lowbar: "_",
|
|
49
|
+
period: ".",
|
|
50
|
+
colon: ":",
|
|
51
|
+
quot: '"',
|
|
52
|
+
apos: "'",
|
|
53
|
+
lt: "<",
|
|
54
|
+
gt: ">"
|
|
55
|
+
});
|
|
56
|
+
var NUMERIC = "#(?:[xX]([0-9a-fA-F]+)|([0-9]+));?";
|
|
57
|
+
var BARE_NAME = "(sup[123]|frac(?:12|14|34)|[A-Za-z]+);?";
|
|
58
|
+
var referenceText = (hex, decimal, name) => {
|
|
59
|
+
if (name !== void 0)
|
|
60
|
+
return Object.prototype.hasOwnProperty.call(NAMED, name.toLowerCase()) ? NAMED[name.toLowerCase()] : " ";
|
|
61
|
+
const point = hex === void 0 ? parseInt(decimal, 10) : parseInt(hex, 16);
|
|
62
|
+
return point <= 1114111 ? String.fromCodePoint(point) : "";
|
|
63
|
+
};
|
|
64
|
+
function referenceReadings(text) {
|
|
65
|
+
const read = (pattern) => text.replace(new RegExp(`&(?:${NUMERIC}|${pattern})`, "g"), (_, hex, decimal, name) => referenceText(hex, decimal, name));
|
|
66
|
+
return [read("([A-Za-z][A-Za-z0-9]*);"), read(BARE_NAME)];
|
|
67
|
+
}
|
|
68
|
+
function readings(text) {
|
|
69
|
+
const out = /* @__PURE__ */ new Set([text, percentDecoded(text)]);
|
|
70
|
+
for (const reading of [...out])
|
|
71
|
+
for (const decoded of referenceReadings(reading))
|
|
72
|
+
out.add(decoded).add(percentDecoded(decoded));
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function base64Bytes(run, skip) {
|
|
76
|
+
let bits = 0;
|
|
77
|
+
let value = 0;
|
|
78
|
+
let out = "";
|
|
79
|
+
for (let i = skip; i < run.length; i += 1) {
|
|
80
|
+
const char = run[i];
|
|
81
|
+
value = value << 6 | (char === "-" ? 62 : char === "_" ? 63 : ALPHABET.indexOf(char));
|
|
82
|
+
bits += 6;
|
|
83
|
+
if (bits >= 8) {
|
|
84
|
+
bits -= 8;
|
|
85
|
+
out += String.fromCharCode(value >> bits);
|
|
86
|
+
value &= (1 << bits) - 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
var compressed = (bytes) => bytes.charCodeAt(0) === 31 && bytes.charCodeAt(1) === 139 || bytes.charCodeAt(0) === 120 && (30720 + bytes.charCodeAt(1)) % 31 === 0;
|
|
92
|
+
function holdsStream(text, layers) {
|
|
93
|
+
for (const reading of readings(text)) {
|
|
94
|
+
for (const [run] of reading.matchAll(RUN)) {
|
|
95
|
+
const starts = [0];
|
|
96
|
+
for (let slash = run.indexOf("/"); slash >= 0; slash = run.indexOf("/", slash + 1))
|
|
97
|
+
starts.push(slash + 1);
|
|
98
|
+
if (starts.some((start) => run.length - start >= RUN_LENGTH && compressed(base64Bytes(run.slice(start, start + 4), 0))))
|
|
99
|
+
return true;
|
|
100
|
+
for (let skip = 0; layers > 0 && skip < 4; skip += 1)
|
|
101
|
+
if (holdsStream(base64Bytes(run, skip), layers - 1))
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
function numberPattern(card) {
|
|
108
|
+
const number = card && (typeof card.number === "string" || typeof card.number === "number") ? String(card.number).replace(/[^0-9]/g, "") : "";
|
|
109
|
+
return number.length >= 12 && number.length <= 19 ? new RegExp(number.split("").join("[^0-9A-Za-z]{0,3}")) : null;
|
|
110
|
+
}
|
|
111
|
+
function holdsNumber(text, pattern, layers) {
|
|
112
|
+
for (const reading of readings(text)) {
|
|
113
|
+
if (pattern.test(reading))
|
|
114
|
+
return true;
|
|
115
|
+
if (layers === 0)
|
|
116
|
+
continue;
|
|
117
|
+
for (const [run] of reading.matchAll(RUN)) {
|
|
118
|
+
for (let skip = 0; skip < 4; skip += 1)
|
|
119
|
+
if (holdsNumber(base64Bytes(run, skip), pattern, layers - 1))
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function addressTexts(value) {
|
|
126
|
+
if (typeof value !== "string")
|
|
127
|
+
return null;
|
|
128
|
+
let url;
|
|
129
|
+
try {
|
|
130
|
+
url = new URL(value);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const names = [];
|
|
136
|
+
const texts = [value, ...url.pathname.split("/")];
|
|
137
|
+
for (const pair of `${url.search.slice(1)}&${url.hash.slice(1)}`.split("&")) {
|
|
138
|
+
if (pair === "")
|
|
139
|
+
continue;
|
|
140
|
+
const at = pair.indexOf("=");
|
|
141
|
+
names.push(at < 0 ? pair : pair.slice(0, at));
|
|
142
|
+
if (at >= 0)
|
|
143
|
+
texts.push(pair.slice(at + 1));
|
|
144
|
+
}
|
|
145
|
+
return { names, texts: [...texts, ...names] };
|
|
146
|
+
}
|
|
147
|
+
var textOf = (value) => typeof value === "string" ? value : JSON.stringify(value) ?? "";
|
|
148
|
+
function threeDsStepLeak(card, step) {
|
|
149
|
+
const names = [];
|
|
150
|
+
const texts = [];
|
|
151
|
+
for (const url of step.urls ?? []) {
|
|
152
|
+
const address = addressTexts(url);
|
|
153
|
+
if (!address)
|
|
154
|
+
return THREE_DS_STEP_UNREADABLE;
|
|
155
|
+
names.push(...address.names);
|
|
156
|
+
texts.push(...address.texts);
|
|
157
|
+
}
|
|
158
|
+
for (const [name, value] of step.fields ?? []) {
|
|
159
|
+
names.push(textOf(name));
|
|
160
|
+
texts.push(textOf(name), textOf(value));
|
|
161
|
+
}
|
|
162
|
+
for (const page of step.pages ?? []) {
|
|
163
|
+
if ([...readings(textOf(page))].some((reading) => PAGE_PAREQ.test(reading)))
|
|
164
|
+
return THREE_DS_STEP_UNREADABLE;
|
|
165
|
+
texts.push(textOf(page));
|
|
166
|
+
}
|
|
167
|
+
if (names.some((name) => [...readings(name)].some((reading) => PAREQ.test(reading))) || texts.some((text) => holdsStream(text, LAYERS)))
|
|
168
|
+
return THREE_DS_STEP_UNREADABLE;
|
|
169
|
+
const pattern = numberPattern(card);
|
|
170
|
+
return pattern && texts.some((text) => holdsNumber(text, pattern, LAYERS)) ? THREE_DS_STEP_HOLDS_CARD : null;
|
|
171
|
+
}
|
|
4
172
|
// src/juspay.js
|
|
5
173
|
var ENDPOINTS = Object.freeze({
|
|
6
174
|
production: Object.freeze({ host: "api.juspay.in", txns: "https://api.juspay.in/txns", tokenize: "https://api.juspay.in/card/tokenize" }),
|
|
@@ -631,7 +799,7 @@ function payStartLink(url, host, merchantOk) {
|
|
|
631
799
|
return false;
|
|
632
800
|
return !url.pathname.split("/").some((segment) => /^\d{12,19}$/.test(segment) && luhn(segment));
|
|
633
801
|
}
|
|
634
|
-
function sanitizeJuspayTxnsResponse(text, template, token) {
|
|
802
|
+
function sanitizeJuspayTxnsResponse(text, template, token, card) {
|
|
635
803
|
const paused = templateOf(template);
|
|
636
804
|
const value = readJson(text, MAX_RESPONSE);
|
|
637
805
|
const forms = tokenForms(token);
|
|
@@ -675,9 +843,11 @@ function sanitizeJuspayTxnsResponse(text, template, token) {
|
|
|
675
843
|
}
|
|
676
844
|
authentication.params = safe;
|
|
677
845
|
}
|
|
846
|
+
if (threeDsStepLeak(card, { urls: [authentication.url], fields: [["txn_id", txnId], ...Object.entries(authentication.params ?? {})] }))
|
|
847
|
+
return invalid();
|
|
678
848
|
return JSON.stringify({ order_id: orderId, txn_id: txnId, status, payment: { authentication } });
|
|
679
849
|
}
|
|
680
|
-
function juspayTxnsOutcome(status, text, template, token) {
|
|
850
|
+
function juspayTxnsOutcome(status, text, template, token, card) {
|
|
681
851
|
if (!Number.isInteger(status))
|
|
682
852
|
return { outcome: "unknown", reason: "juspay_unconfirmed" };
|
|
683
853
|
if (status >= 500)
|
|
@@ -696,7 +866,7 @@ function juspayTxnsOutcome(status, text, template, token) {
|
|
|
696
866
|
if (!JUSPAY_APPROVED_STATUSES.includes(value.status))
|
|
697
867
|
return { outcome: "unknown", reason: "juspay_status_unknown" };
|
|
698
868
|
try {
|
|
699
|
-
return { outcome: "approved", body: sanitizeJuspayTxnsResponse(text, template, token) };
|
|
869
|
+
return { outcome: "approved", body: sanitizeJuspayTxnsResponse(text, template, token, card) };
|
|
700
870
|
}
|
|
701
871
|
catch {
|
|
702
872
|
return { outcome: "unknown", reason: "juspay_unsafe_response" };
|
|
@@ -1,6 +1,174 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
-
// artifact-sha256:
|
|
3
|
+
// artifact-sha256: 5166082ae6c8f452fbcd2df5f679aa40e78fe7fc9f9dff20d9d50512ae7b4487
|
|
4
|
+
// src/three-ds-step.js
|
|
5
|
+
var RUN = /[A-Za-z0-9+/_-]{12,}/g;
|
|
6
|
+
var RUN_LENGTH = 12;
|
|
7
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
8
|
+
var LAYERS = 2;
|
|
9
|
+
var PAREQ = /pareq/i;
|
|
10
|
+
var PAGE_PAREQ = /\bname\s*=\s*["']?[^"'\s>]*pareq/i;
|
|
11
|
+
var THREE_DS_STEP_UNREADABLE = "unreadable_three_ds_step";
|
|
12
|
+
var THREE_DS_STEP_HOLDS_CARD = "card_in_three_ds_step";
|
|
13
|
+
var PERCENT_LAYERS = 8;
|
|
14
|
+
function percentDecoded(text) {
|
|
15
|
+
let out = text;
|
|
16
|
+
for (let layer = 0; layer < PERCENT_LAYERS; layer += 1) {
|
|
17
|
+
const next = out.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
18
|
+
if (next === out)
|
|
19
|
+
break;
|
|
20
|
+
out = next;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
var NAMED = /* @__PURE__ */ Object.freeze({
|
|
25
|
+
amp: "&",
|
|
26
|
+
percnt: "%",
|
|
27
|
+
num: "#",
|
|
28
|
+
quest: "?",
|
|
29
|
+
semi: ";",
|
|
30
|
+
excl: "!",
|
|
31
|
+
dollar: "$",
|
|
32
|
+
lpar: "(",
|
|
33
|
+
rpar: ")",
|
|
34
|
+
ast: "*",
|
|
35
|
+
comma: ",",
|
|
36
|
+
commat: "@",
|
|
37
|
+
lsqb: "[",
|
|
38
|
+
rsqb: "]",
|
|
39
|
+
lcub: "{",
|
|
40
|
+
rcub: "}",
|
|
41
|
+
verbar: "|",
|
|
42
|
+
bsol: "\\",
|
|
43
|
+
hat: "^",
|
|
44
|
+
grave: "`",
|
|
45
|
+
plus: "+",
|
|
46
|
+
sol: "/",
|
|
47
|
+
equals: "=",
|
|
48
|
+
lowbar: "_",
|
|
49
|
+
period: ".",
|
|
50
|
+
colon: ":",
|
|
51
|
+
quot: '"',
|
|
52
|
+
apos: "'",
|
|
53
|
+
lt: "<",
|
|
54
|
+
gt: ">"
|
|
55
|
+
});
|
|
56
|
+
var NUMERIC = "#(?:[xX]([0-9a-fA-F]+)|([0-9]+));?";
|
|
57
|
+
var BARE_NAME = "(sup[123]|frac(?:12|14|34)|[A-Za-z]+);?";
|
|
58
|
+
var referenceText = (hex, decimal, name) => {
|
|
59
|
+
if (name !== void 0)
|
|
60
|
+
return Object.prototype.hasOwnProperty.call(NAMED, name.toLowerCase()) ? NAMED[name.toLowerCase()] : " ";
|
|
61
|
+
const point = hex === void 0 ? parseInt(decimal, 10) : parseInt(hex, 16);
|
|
62
|
+
return point <= 1114111 ? String.fromCodePoint(point) : "";
|
|
63
|
+
};
|
|
64
|
+
function referenceReadings(text) {
|
|
65
|
+
const read = (pattern) => text.replace(new RegExp(`&(?:${NUMERIC}|${pattern})`, "g"), (_, hex, decimal, name) => referenceText(hex, decimal, name));
|
|
66
|
+
return [read("([A-Za-z][A-Za-z0-9]*);"), read(BARE_NAME)];
|
|
67
|
+
}
|
|
68
|
+
function readings(text) {
|
|
69
|
+
const out = /* @__PURE__ */ new Set([text, percentDecoded(text)]);
|
|
70
|
+
for (const reading of [...out])
|
|
71
|
+
for (const decoded of referenceReadings(reading))
|
|
72
|
+
out.add(decoded).add(percentDecoded(decoded));
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function base64Bytes(run, skip) {
|
|
76
|
+
let bits = 0;
|
|
77
|
+
let value = 0;
|
|
78
|
+
let out = "";
|
|
79
|
+
for (let i = skip; i < run.length; i += 1) {
|
|
80
|
+
const char = run[i];
|
|
81
|
+
value = value << 6 | (char === "-" ? 62 : char === "_" ? 63 : ALPHABET.indexOf(char));
|
|
82
|
+
bits += 6;
|
|
83
|
+
if (bits >= 8) {
|
|
84
|
+
bits -= 8;
|
|
85
|
+
out += String.fromCharCode(value >> bits);
|
|
86
|
+
value &= (1 << bits) - 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
var compressed = (bytes) => bytes.charCodeAt(0) === 31 && bytes.charCodeAt(1) === 139 || bytes.charCodeAt(0) === 120 && (30720 + bytes.charCodeAt(1)) % 31 === 0;
|
|
92
|
+
function holdsStream(text, layers) {
|
|
93
|
+
for (const reading of readings(text)) {
|
|
94
|
+
for (const [run] of reading.matchAll(RUN)) {
|
|
95
|
+
const starts = [0];
|
|
96
|
+
for (let slash = run.indexOf("/"); slash >= 0; slash = run.indexOf("/", slash + 1))
|
|
97
|
+
starts.push(slash + 1);
|
|
98
|
+
if (starts.some((start) => run.length - start >= RUN_LENGTH && compressed(base64Bytes(run.slice(start, start + 4), 0))))
|
|
99
|
+
return true;
|
|
100
|
+
for (let skip = 0; layers > 0 && skip < 4; skip += 1)
|
|
101
|
+
if (holdsStream(base64Bytes(run, skip), layers - 1))
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
function numberPattern(card) {
|
|
108
|
+
const number = card && (typeof card.number === "string" || typeof card.number === "number") ? String(card.number).replace(/[^0-9]/g, "") : "";
|
|
109
|
+
return number.length >= 12 && number.length <= 19 ? new RegExp(number.split("").join("[^0-9A-Za-z]{0,3}")) : null;
|
|
110
|
+
}
|
|
111
|
+
function holdsNumber(text, pattern, layers) {
|
|
112
|
+
for (const reading of readings(text)) {
|
|
113
|
+
if (pattern.test(reading))
|
|
114
|
+
return true;
|
|
115
|
+
if (layers === 0)
|
|
116
|
+
continue;
|
|
117
|
+
for (const [run] of reading.matchAll(RUN)) {
|
|
118
|
+
for (let skip = 0; skip < 4; skip += 1)
|
|
119
|
+
if (holdsNumber(base64Bytes(run, skip), pattern, layers - 1))
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function addressTexts(value) {
|
|
126
|
+
if (typeof value !== "string")
|
|
127
|
+
return null;
|
|
128
|
+
let url;
|
|
129
|
+
try {
|
|
130
|
+
url = new URL(value);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const names = [];
|
|
136
|
+
const texts = [value, ...url.pathname.split("/")];
|
|
137
|
+
for (const pair of `${url.search.slice(1)}&${url.hash.slice(1)}`.split("&")) {
|
|
138
|
+
if (pair === "")
|
|
139
|
+
continue;
|
|
140
|
+
const at = pair.indexOf("=");
|
|
141
|
+
names.push(at < 0 ? pair : pair.slice(0, at));
|
|
142
|
+
if (at >= 0)
|
|
143
|
+
texts.push(pair.slice(at + 1));
|
|
144
|
+
}
|
|
145
|
+
return { names, texts: [...texts, ...names] };
|
|
146
|
+
}
|
|
147
|
+
var textOf = (value) => typeof value === "string" ? value : JSON.stringify(value) ?? "";
|
|
148
|
+
function threeDsStepLeak(card, step) {
|
|
149
|
+
const names = [];
|
|
150
|
+
const texts = [];
|
|
151
|
+
for (const url of step.urls ?? []) {
|
|
152
|
+
const address = addressTexts(url);
|
|
153
|
+
if (!address)
|
|
154
|
+
return THREE_DS_STEP_UNREADABLE;
|
|
155
|
+
names.push(...address.names);
|
|
156
|
+
texts.push(...address.texts);
|
|
157
|
+
}
|
|
158
|
+
for (const [name, value] of step.fields ?? []) {
|
|
159
|
+
names.push(textOf(name));
|
|
160
|
+
texts.push(textOf(name), textOf(value));
|
|
161
|
+
}
|
|
162
|
+
for (const page of step.pages ?? []) {
|
|
163
|
+
if ([...readings(textOf(page))].some((reading) => PAGE_PAREQ.test(reading)))
|
|
164
|
+
return THREE_DS_STEP_UNREADABLE;
|
|
165
|
+
texts.push(textOf(page));
|
|
166
|
+
}
|
|
167
|
+
if (names.some((name) => [...readings(name)].some((reading) => PAREQ.test(reading))) || texts.some((text) => holdsStream(text, LAYERS)))
|
|
168
|
+
return THREE_DS_STEP_UNREADABLE;
|
|
169
|
+
const pattern = numberPattern(card);
|
|
170
|
+
return pattern && texts.some((text) => holdsNumber(text, pattern, LAYERS)) ? THREE_DS_STEP_HOLDS_CARD : null;
|
|
171
|
+
}
|
|
4
172
|
// src/payrexx.js
|
|
5
173
|
var PAYREXX_PSP = "payrexx";
|
|
6
174
|
var PAYREXX_CAPABILITY = "payrexx_card_frame";
|
|
@@ -287,8 +455,14 @@ function payrexxAnswerOutcome(status, text, card) {
|
|
|
287
455
|
return { outcome: "refused", code: "payrexx_three_ds_declined" };
|
|
288
456
|
if (step === "tokenless")
|
|
289
457
|
return { outcome: "refused", code: "payrexx_three_ds_error" };
|
|
290
|
-
if (step !== null)
|
|
458
|
+
if (step !== null) {
|
|
459
|
+
const urls = [answer.threeDSMethodURL, answer.acsURL].filter((url) => typeof url === "string" && url !== "");
|
|
460
|
+
const members = Object.entries(answer).map(([key, value]) => [key, typeof value === "string" ? value : ""]);
|
|
461
|
+
const leak = threeDsStepLeak(card, { urls, fields: [...members, ["", text]] });
|
|
462
|
+
if (leak)
|
|
463
|
+
return { outcome: "unknown", reason: leak === THREE_DS_STEP_UNREADABLE ? "payrexx_three_ds_unreadable" : "payrexx_card_in_answer" };
|
|
291
464
|
return { outcome: "approved", body: text, pending: "three_ds" };
|
|
465
|
+
}
|
|
292
466
|
}
|
|
293
467
|
return { outcome: "unknown", reason: "payrexx_answer_unconfirmed" };
|
|
294
468
|
}
|
|
@@ -17,7 +17,7 @@ declare function injectWspayCard(body: any, card: any, options: any): {
|
|
|
17
17
|
body: string;
|
|
18
18
|
where: string[];
|
|
19
19
|
};
|
|
20
|
-
declare function wspayResponseOutcome(status: any, text2: any): {
|
|
20
|
+
declare function wspayResponseOutcome(status: any, text2: any, card: any): {
|
|
21
21
|
outcome: string;
|
|
22
22
|
reason: string;
|
|
23
23
|
code?: undefined;
|
package/dist/wspay.generated.js
CHANGED
|
@@ -1,6 +1,174 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
-
// artifact-sha256:
|
|
3
|
+
// artifact-sha256: f3d9221468606ad3d7512105a7eb6c2eef4b813d5772cb2a5c31042874cb4984
|
|
4
|
+
// src/three-ds-step.js
|
|
5
|
+
var RUN = /[A-Za-z0-9+/_-]{12,}/g;
|
|
6
|
+
var RUN_LENGTH = 12;
|
|
7
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
8
|
+
var LAYERS = 2;
|
|
9
|
+
var PAREQ = /pareq/i;
|
|
10
|
+
var PAGE_PAREQ = /\bname\s*=\s*["']?[^"'\s>]*pareq/i;
|
|
11
|
+
var THREE_DS_STEP_UNREADABLE = "unreadable_three_ds_step";
|
|
12
|
+
var THREE_DS_STEP_HOLDS_CARD = "card_in_three_ds_step";
|
|
13
|
+
var PERCENT_LAYERS = 8;
|
|
14
|
+
function percentDecoded(text2) {
|
|
15
|
+
let out = text2;
|
|
16
|
+
for (let layer = 0; layer < PERCENT_LAYERS; layer += 1) {
|
|
17
|
+
const next = out.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
18
|
+
if (next === out)
|
|
19
|
+
break;
|
|
20
|
+
out = next;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
var NAMED = /* @__PURE__ */ Object.freeze({
|
|
25
|
+
amp: "&",
|
|
26
|
+
percnt: "%",
|
|
27
|
+
num: "#",
|
|
28
|
+
quest: "?",
|
|
29
|
+
semi: ";",
|
|
30
|
+
excl: "!",
|
|
31
|
+
dollar: "$",
|
|
32
|
+
lpar: "(",
|
|
33
|
+
rpar: ")",
|
|
34
|
+
ast: "*",
|
|
35
|
+
comma: ",",
|
|
36
|
+
commat: "@",
|
|
37
|
+
lsqb: "[",
|
|
38
|
+
rsqb: "]",
|
|
39
|
+
lcub: "{",
|
|
40
|
+
rcub: "}",
|
|
41
|
+
verbar: "|",
|
|
42
|
+
bsol: "\\",
|
|
43
|
+
hat: "^",
|
|
44
|
+
grave: "`",
|
|
45
|
+
plus: "+",
|
|
46
|
+
sol: "/",
|
|
47
|
+
equals: "=",
|
|
48
|
+
lowbar: "_",
|
|
49
|
+
period: ".",
|
|
50
|
+
colon: ":",
|
|
51
|
+
quot: '"',
|
|
52
|
+
apos: "'",
|
|
53
|
+
lt: "<",
|
|
54
|
+
gt: ">"
|
|
55
|
+
});
|
|
56
|
+
var NUMERIC = "#(?:[xX]([0-9a-fA-F]+)|([0-9]+));?";
|
|
57
|
+
var BARE_NAME = "(sup[123]|frac(?:12|14|34)|[A-Za-z]+);?";
|
|
58
|
+
var referenceText = (hex, decimal, name) => {
|
|
59
|
+
if (name !== void 0)
|
|
60
|
+
return Object.prototype.hasOwnProperty.call(NAMED, name.toLowerCase()) ? NAMED[name.toLowerCase()] : " ";
|
|
61
|
+
const point = hex === void 0 ? parseInt(decimal, 10) : parseInt(hex, 16);
|
|
62
|
+
return point <= 1114111 ? String.fromCodePoint(point) : "";
|
|
63
|
+
};
|
|
64
|
+
function referenceReadings(text2) {
|
|
65
|
+
const read = (pattern) => text2.replace(new RegExp(`&(?:${NUMERIC}|${pattern})`, "g"), (_, hex, decimal, name) => referenceText(hex, decimal, name));
|
|
66
|
+
return [read("([A-Za-z][A-Za-z0-9]*);"), read(BARE_NAME)];
|
|
67
|
+
}
|
|
68
|
+
function readings(text2) {
|
|
69
|
+
const out = /* @__PURE__ */ new Set([text2, percentDecoded(text2)]);
|
|
70
|
+
for (const reading of [...out])
|
|
71
|
+
for (const decoded of referenceReadings(reading))
|
|
72
|
+
out.add(decoded).add(percentDecoded(decoded));
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function base64Bytes(run, skip) {
|
|
76
|
+
let bits = 0;
|
|
77
|
+
let value = 0;
|
|
78
|
+
let out = "";
|
|
79
|
+
for (let i = skip; i < run.length; i += 1) {
|
|
80
|
+
const char = run[i];
|
|
81
|
+
value = value << 6 | (char === "-" ? 62 : char === "_" ? 63 : ALPHABET.indexOf(char));
|
|
82
|
+
bits += 6;
|
|
83
|
+
if (bits >= 8) {
|
|
84
|
+
bits -= 8;
|
|
85
|
+
out += String.fromCharCode(value >> bits);
|
|
86
|
+
value &= (1 << bits) - 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
var compressed = (bytes) => bytes.charCodeAt(0) === 31 && bytes.charCodeAt(1) === 139 || bytes.charCodeAt(0) === 120 && (30720 + bytes.charCodeAt(1)) % 31 === 0;
|
|
92
|
+
function holdsStream(text2, layers) {
|
|
93
|
+
for (const reading of readings(text2)) {
|
|
94
|
+
for (const [run] of reading.matchAll(RUN)) {
|
|
95
|
+
const starts = [0];
|
|
96
|
+
for (let slash = run.indexOf("/"); slash >= 0; slash = run.indexOf("/", slash + 1))
|
|
97
|
+
starts.push(slash + 1);
|
|
98
|
+
if (starts.some((start) => run.length - start >= RUN_LENGTH && compressed(base64Bytes(run.slice(start, start + 4), 0))))
|
|
99
|
+
return true;
|
|
100
|
+
for (let skip = 0; layers > 0 && skip < 4; skip += 1)
|
|
101
|
+
if (holdsStream(base64Bytes(run, skip), layers - 1))
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
function numberPattern(card) {
|
|
108
|
+
const number = card && (typeof card.number === "string" || typeof card.number === "number") ? String(card.number).replace(/[^0-9]/g, "") : "";
|
|
109
|
+
return number.length >= 12 && number.length <= 19 ? new RegExp(number.split("").join("[^0-9A-Za-z]{0,3}")) : null;
|
|
110
|
+
}
|
|
111
|
+
function holdsNumber(text2, pattern, layers) {
|
|
112
|
+
for (const reading of readings(text2)) {
|
|
113
|
+
if (pattern.test(reading))
|
|
114
|
+
return true;
|
|
115
|
+
if (layers === 0)
|
|
116
|
+
continue;
|
|
117
|
+
for (const [run] of reading.matchAll(RUN)) {
|
|
118
|
+
for (let skip = 0; skip < 4; skip += 1)
|
|
119
|
+
if (holdsNumber(base64Bytes(run, skip), pattern, layers - 1))
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function addressTexts(value) {
|
|
126
|
+
if (typeof value !== "string")
|
|
127
|
+
return null;
|
|
128
|
+
let url;
|
|
129
|
+
try {
|
|
130
|
+
url = new URL(value);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const names = [];
|
|
136
|
+
const texts = [value, ...url.pathname.split("/")];
|
|
137
|
+
for (const pair of `${url.search.slice(1)}&${url.hash.slice(1)}`.split("&")) {
|
|
138
|
+
if (pair === "")
|
|
139
|
+
continue;
|
|
140
|
+
const at = pair.indexOf("=");
|
|
141
|
+
names.push(at < 0 ? pair : pair.slice(0, at));
|
|
142
|
+
if (at >= 0)
|
|
143
|
+
texts.push(pair.slice(at + 1));
|
|
144
|
+
}
|
|
145
|
+
return { names, texts: [...texts, ...names] };
|
|
146
|
+
}
|
|
147
|
+
var textOf = (value) => typeof value === "string" ? value : JSON.stringify(value) ?? "";
|
|
148
|
+
function threeDsStepLeak(card, step) {
|
|
149
|
+
const names = [];
|
|
150
|
+
const texts = [];
|
|
151
|
+
for (const url of step.urls ?? []) {
|
|
152
|
+
const address = addressTexts(url);
|
|
153
|
+
if (!address)
|
|
154
|
+
return THREE_DS_STEP_UNREADABLE;
|
|
155
|
+
names.push(...address.names);
|
|
156
|
+
texts.push(...address.texts);
|
|
157
|
+
}
|
|
158
|
+
for (const [name, value] of step.fields ?? []) {
|
|
159
|
+
names.push(textOf(name));
|
|
160
|
+
texts.push(textOf(name), textOf(value));
|
|
161
|
+
}
|
|
162
|
+
for (const page of step.pages ?? []) {
|
|
163
|
+
if ([...readings(textOf(page))].some((reading) => PAGE_PAREQ.test(reading)))
|
|
164
|
+
return THREE_DS_STEP_UNREADABLE;
|
|
165
|
+
texts.push(textOf(page));
|
|
166
|
+
}
|
|
167
|
+
if (names.some((name) => [...readings(name)].some((reading) => PAREQ.test(reading))) || texts.some((text2) => holdsStream(text2, LAYERS)))
|
|
168
|
+
return THREE_DS_STEP_UNREADABLE;
|
|
169
|
+
const pattern = numberPattern(card);
|
|
170
|
+
return pattern && texts.some((text2) => holdsNumber(text2, pattern, LAYERS)) ? THREE_DS_STEP_HOLDS_CARD : null;
|
|
171
|
+
}
|
|
4
172
|
// src/wspay.js
|
|
5
173
|
var WSPAY_CONFIRM_HOSTS = Object.freeze(["proxy.wspay.biz", "proxy-test.wspay.biz"]);
|
|
6
174
|
var WSPAY_TEST_HOST = "proxy-test.wspay.biz";
|
|
@@ -303,7 +471,7 @@ function responseCode(result) {
|
|
|
303
471
|
const code = record(trx) ? trx.response_code : null;
|
|
304
472
|
return exact(/^[0-9]{4}$/, code) ? code : null;
|
|
305
473
|
}
|
|
306
|
-
function wspayResponseOutcome(status, text2) {
|
|
474
|
+
function wspayResponseOutcome(status, text2, card) {
|
|
307
475
|
if (status !== 200)
|
|
308
476
|
return { outcome: "unknown", reason: "wspay_status_unconfirmed" };
|
|
309
477
|
const value = readJson(text2);
|
|
@@ -319,8 +487,12 @@ function wspayResponseOutcome(status, text2) {
|
|
|
319
487
|
}
|
|
320
488
|
if (value.status === "approved" && approvedResult(result))
|
|
321
489
|
return { outcome: "approved" };
|
|
322
|
-
if (value.status === "action_required" && (result === null || result === void 0) && keysWithin(value.action_required, ["acs_url", "redirect_to"]) && httpsUrl(value.action_required.acs_url) && httpsUrl(value.action_required.redirect_to))
|
|
490
|
+
if (value.status === "action_required" && (result === null || result === void 0) && keysWithin(value.action_required, ["acs_url", "redirect_to"]) && httpsUrl(value.action_required.acs_url) && httpsUrl(value.action_required.redirect_to)) {
|
|
491
|
+
const leak = threeDsStepLeak(card, { urls: [value.action_required.acs_url, value.action_required.redirect_to] });
|
|
492
|
+
if (leak)
|
|
493
|
+
return { outcome: "unknown", reason: leak === THREE_DS_STEP_UNREADABLE ? "wspay_three_ds_unreadable" : "wspay_answer_holds_card" };
|
|
323
494
|
return { outcome: "approved", pending: "three_ds" };
|
|
495
|
+
}
|
|
324
496
|
return { outcome: "unknown", reason: "wspay_unconfirmed" };
|
|
325
497
|
}
|
|
326
498
|
function sanitizeWspayResponse(text2, options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-cards/sdk",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.28",
|
|
4
4
|
"description": "Agentcard's SDK: one client, Agentcard, with two modules. checkout pauses a browser purchase and pays it with the person's own card; voice connects a person to your product once and approves a purchase by voice inside it. Card data never enters your process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|