@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 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.
@@ -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;
@@ -1,6 +1,174 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: c771bc4608bb5f6fd8db2cd15a071bec7cfc12c8d753d1e38403790599ddf300
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: 12336c60151bbdaa63f9d3e665fb53c8e3407ba942140bef2928e7ce3a8b3e2c
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;
@@ -1,6 +1,174 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 9ba65916c5dc5ee3ca830d630cafbde62d537e61991c33c80f1520ac93ef0b0d
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.27",
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",