pop-pay 0.3.3 → 0.4.1
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/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -1
- package/dist/client.js.map +1 -1
- package/dist/core/models.d.ts +2 -1
- package/dist/core/models.d.ts.map +1 -1
- package/dist/core/models.js +4 -0
- package/dist/core/models.js.map +1 -1
- package/dist/core/state.d.ts +6 -0
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/state.js +93 -2
- package/dist/core/state.js.map +1 -1
- package/dist/engine/guardrails.js +6 -6
- package/dist/engine/guardrails.js.map +1 -1
- package/dist/engine/injector.d.ts +44 -32
- package/dist/engine/injector.d.ts.map +1 -1
- package/dist/engine/injector.js +338 -644
- package/dist/engine/injector.js.map +1 -1
- package/dist/engine/llm-guardrails.d.ts.map +1 -1
- package/dist/engine/llm-guardrails.js +13 -4
- package/dist/engine/llm-guardrails.js.map +1 -1
- package/dist/mcp-server.js +201 -15
- package/dist/mcp-server.js.map +1 -1
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +3 -0
- package/dist/vault.js.map +1 -1
- package/package.json +6 -6
package/dist/engine/injector.js
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* PopBrowserInjector:
|
|
3
|
+
* PopBrowserInjector: Playwright-based browser injector for payment and billing fields.
|
|
4
4
|
*
|
|
5
5
|
* Connects to an already-running Chromium browser (via --remote-debugging-port)
|
|
6
|
-
* and auto-fills credit card fields
|
|
7
|
-
* Stripe and other third-party payment iframes. Also fills billing detail fields
|
|
8
|
-
* (name, address, email) that live in the main page frame.
|
|
6
|
+
* using playwright-core and auto-fills credit card fields across all frames.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
8
|
+
* This version replaces the raw CDP WebSocket implementation with Playwright's
|
|
9
|
+
* connectOverCDP, providing better isolation and cross-origin iframe support.
|
|
11
10
|
*/
|
|
12
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
12
|
exports.PopBrowserInjector = exports.CITY_SELECTORS = exports.STATE_SELECTORS = exports.COUNTRY_SELECTORS = exports.PHONE_COUNTRY_CODE_SELECTORS = exports.PHONE_SELECTORS = exports.EMAIL_SELECTORS = exports.ZIP_SELECTORS = exports.STREET_SELECTORS = exports.FULL_NAME_SELECTORS = exports.LAST_NAME_SELECTORS = exports.FIRST_NAME_SELECTORS = exports.CVV_SELECTORS = exports.EXPIRY_SELECTORS = exports.CARD_NUMBER_SELECTORS = void 0;
|
|
14
13
|
exports.ssrfValidateUrl = ssrfValidateUrl;
|
|
15
14
|
exports.verifyDomainToctou = verifyDomainToctou;
|
|
15
|
+
const playwright_core_1 = require("playwright-core");
|
|
16
16
|
const known_processors_js_1 = require("./known-processors.js");
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
|
+
// Structured logger
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const LOG_LEVEL = (process.env.POP_LOG_LEVEL ?? "info").toLowerCase();
|
|
21
|
+
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
22
|
+
function log(level, msg, data) {
|
|
23
|
+
if ((LEVELS[level] ?? 1) < (LEVELS[LOG_LEVEL] ?? 1))
|
|
24
|
+
return;
|
|
25
|
+
const entry = { ts: new Date().toISOString(), level, component: "PopBrowserInjector", msg, ...data };
|
|
26
|
+
const out = level === "error" ? process.stderr : process.stderr;
|
|
27
|
+
out.write(JSON.stringify(entry) + "\n");
|
|
28
|
+
}
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
18
30
|
// ISO 3166-1 alpha-2 -> E.164 dial prefix
|
|
19
31
|
// ---------------------------------------------------------------------------
|
|
20
32
|
const COUNTRY_DIAL_CODES = {
|
|
@@ -44,7 +56,7 @@ function nationalNumber(phoneE164, countryCode) {
|
|
|
44
56
|
return phoneE164;
|
|
45
57
|
}
|
|
46
58
|
// ---------------------------------------------------------------------------
|
|
47
|
-
// US state abbreviation -> full name
|
|
59
|
+
// US state abbreviation -> full name
|
|
48
60
|
// ---------------------------------------------------------------------------
|
|
49
61
|
const US_STATE_CODES = {
|
|
50
62
|
AL: "Alabama", AK: "Alaska", AZ: "Arizona", AR: "Arkansas",
|
|
@@ -62,7 +74,7 @@ const US_STATE_CODES = {
|
|
|
62
74
|
WV: "West Virginia", WI: "Wisconsin", WY: "Wyoming",
|
|
63
75
|
};
|
|
64
76
|
// ---------------------------------------------------------------------------
|
|
65
|
-
//
|
|
77
|
+
// Selectors
|
|
66
78
|
// ---------------------------------------------------------------------------
|
|
67
79
|
exports.CARD_NUMBER_SELECTORS = [
|
|
68
80
|
"input[autocomplete='cc-number']",
|
|
@@ -72,8 +84,8 @@ exports.CARD_NUMBER_SELECTORS = [
|
|
|
72
84
|
"input[id*='card'][id*='number']",
|
|
73
85
|
"input[placeholder*='Card number']",
|
|
74
86
|
"input[placeholder*='card number']",
|
|
75
|
-
"input[data-elements-stable-field-name='cardNumber']",
|
|
76
|
-
"input.__PrivateStripeElement",
|
|
87
|
+
"input[data-elements-stable-field-name='cardNumber']",
|
|
88
|
+
"input.__PrivateStripeElement",
|
|
77
89
|
];
|
|
78
90
|
exports.EXPIRY_SELECTORS = [
|
|
79
91
|
"input[autocomplete='cc-exp']",
|
|
@@ -83,7 +95,7 @@ exports.EXPIRY_SELECTORS = [
|
|
|
83
95
|
"input[placeholder*='MM / YY']",
|
|
84
96
|
"input[placeholder*='MM/YY']",
|
|
85
97
|
"input[placeholder*='Expiry']",
|
|
86
|
-
"input[data-elements-stable-field-name='cardExpiry']",
|
|
98
|
+
"input[data-elements-stable-field-name='cardExpiry']",
|
|
87
99
|
];
|
|
88
100
|
exports.CVV_SELECTORS = [
|
|
89
101
|
"input[autocomplete='cc-csc']",
|
|
@@ -94,11 +106,8 @@ exports.CVV_SELECTORS = [
|
|
|
94
106
|
"input[placeholder*='CVC']",
|
|
95
107
|
"input[placeholder*='CVV']",
|
|
96
108
|
"input[placeholder*='Security code']",
|
|
97
|
-
"input[data-elements-stable-field-name='cardCvc']",
|
|
109
|
+
"input[data-elements-stable-field-name='cardCvc']",
|
|
98
110
|
];
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
// CSS selectors for billing detail fields
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
111
|
exports.FIRST_NAME_SELECTORS = [
|
|
103
112
|
"input[autocomplete='given-name']",
|
|
104
113
|
"input[name='first_name']", "input[name='firstName']", "input[name='first-name']",
|
|
@@ -187,9 +196,6 @@ exports.CITY_SELECTORS = [
|
|
|
187
196
|
"input[aria-label*='City']",
|
|
188
197
|
"select[autocomplete='address-level2']", "select[name='city']",
|
|
189
198
|
];
|
|
190
|
-
// ---------------------------------------------------------------------------
|
|
191
|
-
// Known vendor domains (shared with guardrails)
|
|
192
|
-
// ---------------------------------------------------------------------------
|
|
193
199
|
const KNOWN_VENDOR_DOMAINS = {
|
|
194
200
|
aws: ["amazonaws.com", "aws.amazon.com"],
|
|
195
201
|
amazon: ["amazon.com", "amazon.co.uk", "amazon.co.jp"],
|
|
@@ -215,7 +221,6 @@ function ssrfValidateUrl(url) {
|
|
|
215
221
|
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
216
222
|
return "Only http/https URLs are allowed.";
|
|
217
223
|
}
|
|
218
|
-
// Block private/reserved IPs
|
|
219
224
|
const hostname = parsed.hostname;
|
|
220
225
|
if (hostname === "localhost" ||
|
|
221
226
|
hostname === "127.0.0.1" ||
|
|
@@ -225,7 +230,6 @@ function ssrfValidateUrl(url) {
|
|
|
225
230
|
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
|
|
226
231
|
hostname === "[::1]" ||
|
|
227
232
|
hostname.endsWith(".local")) {
|
|
228
|
-
// Allow localhost only for CDP URLs (checked separately)
|
|
229
233
|
return "Private/reserved IP addresses are not allowed.";
|
|
230
234
|
}
|
|
231
235
|
}
|
|
@@ -235,7 +239,7 @@ function ssrfValidateUrl(url) {
|
|
|
235
239
|
return null;
|
|
236
240
|
}
|
|
237
241
|
// ---------------------------------------------------------------------------
|
|
238
|
-
// TOCTOU domain verification
|
|
242
|
+
// TOCTOU domain verification
|
|
239
243
|
// ---------------------------------------------------------------------------
|
|
240
244
|
function verifyDomainToctou(pageUrl, approvedVendor) {
|
|
241
245
|
if (!pageUrl || !approvedVendor)
|
|
@@ -251,7 +255,6 @@ function verifyDomainToctou(pageUrl, approvedVendor) {
|
|
|
251
255
|
const vendorTokens = new Set(vendorLower.split(/[\s\-_./]+/).filter(Boolean));
|
|
252
256
|
let domainOk = false;
|
|
253
257
|
let vendorIsKnown = false;
|
|
254
|
-
// Check against KNOWN_VENDOR_DOMAINS using strict suffix matching
|
|
255
258
|
for (const [knownVendor, knownDomains] of Object.entries(KNOWN_VENDOR_DOMAINS)) {
|
|
256
259
|
if (vendorTokens.has(knownVendor) || knownVendor === vendorLower) {
|
|
257
260
|
vendorIsKnown = true;
|
|
@@ -261,7 +264,6 @@ function verifyDomainToctou(pageUrl, approvedVendor) {
|
|
|
261
264
|
break;
|
|
262
265
|
}
|
|
263
266
|
}
|
|
264
|
-
// Fallback for unknown vendors
|
|
265
267
|
if (!domainOk && !vendorIsKnown) {
|
|
266
268
|
const commonTlds = new Set(["com", "org", "net", "io", "co", "uk", "jp", "de", "fr"]);
|
|
267
269
|
const domainLabels = new Set(actualDomain.split(".").filter((l) => !commonTlds.has(l)));
|
|
@@ -270,7 +272,6 @@ function verifyDomainToctou(pageUrl, approvedVendor) {
|
|
|
270
272
|
[...vendorTokens].some((tok) => tok.length >= 4 &&
|
|
271
273
|
[...domainLabels].some((label) => label.includes(tok)));
|
|
272
274
|
}
|
|
273
|
-
// Payment processor passthrough
|
|
274
275
|
if (!domainOk) {
|
|
275
276
|
let userProcessors = [];
|
|
276
277
|
try {
|
|
@@ -288,699 +289,447 @@ function verifyDomainToctou(pageUrl, approvedVendor) {
|
|
|
288
289
|
return null;
|
|
289
290
|
}
|
|
290
291
|
// ---------------------------------------------------------------------------
|
|
291
|
-
// CDP connection helper using raw WebSocket
|
|
292
|
-
// ---------------------------------------------------------------------------
|
|
293
|
-
async function fetchJSON(url) {
|
|
294
|
-
const resp = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
295
|
-
return resp.json();
|
|
296
|
-
}
|
|
297
|
-
// Minimal WebSocket-based CDP client
|
|
298
|
-
class CDPClient {
|
|
299
|
-
ws;
|
|
300
|
-
msgId = 0;
|
|
301
|
-
pending = new Map();
|
|
302
|
-
eventHandlers = new Map();
|
|
303
|
-
static async connect(wsUrl) {
|
|
304
|
-
const client = new CDPClient();
|
|
305
|
-
const { WebSocket } = await import("ws").catch(() => {
|
|
306
|
-
// Fallback: use global WebSocket if available (Node 21+)
|
|
307
|
-
return { WebSocket: globalThis.WebSocket };
|
|
308
|
-
});
|
|
309
|
-
return new Promise((resolve, reject) => {
|
|
310
|
-
const ws = new WebSocket(wsUrl);
|
|
311
|
-
client.ws = ws;
|
|
312
|
-
ws.onopen = () => resolve(client);
|
|
313
|
-
ws.onerror = (e) => reject(new Error(`CDP WebSocket error: ${e.message ?? e}`));
|
|
314
|
-
ws.onmessage = (event) => {
|
|
315
|
-
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
|
316
|
-
const msg = JSON.parse(data);
|
|
317
|
-
if (msg.id !== undefined) {
|
|
318
|
-
const p = client.pending.get(msg.id);
|
|
319
|
-
if (p) {
|
|
320
|
-
client.pending.delete(msg.id);
|
|
321
|
-
if (msg.error)
|
|
322
|
-
p.reject(new Error(msg.error.message));
|
|
323
|
-
else
|
|
324
|
-
p.resolve(msg.result);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
else if (msg.method) {
|
|
328
|
-
const handlers = client.eventHandlers.get(msg.method);
|
|
329
|
-
if (handlers)
|
|
330
|
-
handlers.forEach((h) => h(msg.params));
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
ws.onclose = () => {
|
|
334
|
-
for (const p of client.pending.values()) {
|
|
335
|
-
p.reject(new Error("CDP connection closed"));
|
|
336
|
-
}
|
|
337
|
-
client.pending.clear();
|
|
338
|
-
};
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
async send(method, params = {}) {
|
|
342
|
-
const id = ++this.msgId;
|
|
343
|
-
return new Promise((resolve, reject) => {
|
|
344
|
-
this.pending.set(id, { resolve, reject });
|
|
345
|
-
const msg = JSON.stringify({ id, method, params });
|
|
346
|
-
this.ws.send(msg);
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
on(event, handler) {
|
|
350
|
-
const handlers = this.eventHandlers.get(event) ?? [];
|
|
351
|
-
handlers.push(handler);
|
|
352
|
-
this.eventHandlers.set(event, handlers);
|
|
353
|
-
}
|
|
354
|
-
close() {
|
|
355
|
-
try {
|
|
356
|
-
this.ws.close();
|
|
357
|
-
}
|
|
358
|
-
catch { }
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
// ---------------------------------------------------------------------------
|
|
362
292
|
// PopBrowserInjector
|
|
363
293
|
// ---------------------------------------------------------------------------
|
|
364
294
|
class PopBrowserInjector {
|
|
365
295
|
cdpUrl;
|
|
366
|
-
|
|
367
|
-
|
|
296
|
+
defaultBillingInfo;
|
|
297
|
+
browser = null;
|
|
298
|
+
constructor(cdpUrl = "http://localhost:9222", billingInfoOrHeadless) {
|
|
368
299
|
this.cdpUrl = cdpUrl;
|
|
369
|
-
|
|
300
|
+
if (typeof billingInfoOrHeadless === "object") {
|
|
301
|
+
this.defaultBillingInfo = billingInfoOrHeadless;
|
|
302
|
+
}
|
|
370
303
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Inject payment info into the current page.
|
|
306
|
+
* Supports both positional and object-based signatures for compatibility.
|
|
307
|
+
*/
|
|
308
|
+
async injectPaymentInfo(optsOrCard, expiry, cvv, vendor, pageUrl, billingInfo) {
|
|
309
|
+
let cardNumber;
|
|
310
|
+
let exp;
|
|
311
|
+
let cv;
|
|
312
|
+
let vend;
|
|
313
|
+
let url;
|
|
314
|
+
let billing;
|
|
315
|
+
if (typeof optsOrCard === "object") {
|
|
316
|
+
cardNumber = optsOrCard.cardNumber;
|
|
317
|
+
exp = optsOrCard.expiry || optsOrCard.expirationDate || "";
|
|
318
|
+
cv = optsOrCard.cvv;
|
|
319
|
+
vend = optsOrCard.vendor || optsOrCard.approvedVendor || "";
|
|
320
|
+
url = optsOrCard.pageUrl || "";
|
|
321
|
+
billing = optsOrCard.billingInfo || billingInfo;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
cardNumber = optsOrCard;
|
|
325
|
+
exp = expiry || "";
|
|
326
|
+
cv = cvv || "";
|
|
327
|
+
vend = vendor || "";
|
|
328
|
+
url = pageUrl || "";
|
|
329
|
+
billing = billingInfo;
|
|
330
|
+
}
|
|
375
331
|
const result = {
|
|
376
332
|
cardFilled: false,
|
|
377
333
|
billingFilled: false,
|
|
378
334
|
blockedReason: "",
|
|
379
335
|
};
|
|
380
336
|
// TOCTOU guard
|
|
381
|
-
const blocked = verifyDomainToctou(
|
|
337
|
+
const blocked = verifyDomainToctou(url, vend);
|
|
382
338
|
if (blocked) {
|
|
383
339
|
result.blockedReason = blocked;
|
|
384
340
|
return result;
|
|
385
341
|
}
|
|
386
|
-
const
|
|
387
|
-
const hasBilling = Object.values(
|
|
388
|
-
let client = null;
|
|
342
|
+
const finalBilling = billing || this.defaultBillingInfo || this.loadBillingFromEnv();
|
|
343
|
+
const hasBilling = Object.values(finalBilling).some((v) => v !== "");
|
|
389
344
|
try {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
345
|
+
this.browser = await playwright_core_1.chromium.connectOverCDP(this.cdpUrl);
|
|
346
|
+
const page = this.findBestPage(this.browser);
|
|
347
|
+
if (!page) {
|
|
348
|
+
result.blockedReason = "no_active_page";
|
|
393
349
|
return result;
|
|
394
350
|
}
|
|
395
|
-
|
|
396
|
-
// Enable DOM + Runtime
|
|
397
|
-
await client.send("Runtime.enable");
|
|
398
|
-
await client.send("DOM.enable");
|
|
399
|
-
await client.send("Page.enable");
|
|
351
|
+
await page.bringToFront();
|
|
400
352
|
// Blackout mode
|
|
401
353
|
const blackoutMode = (process.env.POP_BLACKOUT_MODE ?? "after").toLowerCase();
|
|
402
354
|
if (blackoutMode === "before") {
|
|
403
|
-
await this.enableBlackout(
|
|
355
|
+
await this.enableBlackout(page);
|
|
404
356
|
}
|
|
405
|
-
// Fill card fields across all frames
|
|
406
|
-
result.cardFilled = await this.
|
|
357
|
+
// Fill card fields across all frames
|
|
358
|
+
result.cardFilled = await this.fillAcrossFrames(page, cardNumber, exp, cv);
|
|
407
359
|
// Fill billing fields
|
|
408
360
|
if (hasBilling) {
|
|
409
|
-
const billingResult = await this.
|
|
361
|
+
const billingResult = await this.fillBillingFields(page, finalBilling);
|
|
410
362
|
result.billingFilled = billingResult.filled.length > 0;
|
|
411
363
|
result.billingDetails = billingResult;
|
|
412
364
|
}
|
|
413
365
|
if (blackoutMode === "after") {
|
|
414
|
-
await this.enableBlackout(
|
|
366
|
+
await this.enableBlackout(page);
|
|
415
367
|
}
|
|
416
368
|
return result;
|
|
417
369
|
}
|
|
418
370
|
catch (err) {
|
|
419
|
-
|
|
371
|
+
log("error", "injection failed", { error: err.message });
|
|
420
372
|
return result;
|
|
421
373
|
}
|
|
422
374
|
finally {
|
|
423
|
-
|
|
375
|
+
await this.close();
|
|
424
376
|
}
|
|
425
377
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Internal method used by mcp-server.ts. Kept for compatibility but marked internal.
|
|
380
|
+
* @internal
|
|
381
|
+
*/
|
|
429
382
|
async injectBillingOnly(opts) {
|
|
430
|
-
const result = {
|
|
431
|
-
cardFilled: false,
|
|
432
|
-
billingFilled: false,
|
|
433
|
-
blockedReason: "",
|
|
434
|
-
};
|
|
383
|
+
const result = { cardFilled: false, billingFilled: false, blockedReason: "" };
|
|
435
384
|
const blocked = verifyDomainToctou(opts.pageUrl ?? "", opts.approvedVendor ?? "");
|
|
436
385
|
if (blocked) {
|
|
437
386
|
result.blockedReason = blocked;
|
|
438
387
|
return result;
|
|
439
388
|
}
|
|
440
|
-
const
|
|
441
|
-
let client = null;
|
|
389
|
+
const billing = this.defaultBillingInfo || this.loadBillingFromEnv();
|
|
442
390
|
try {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
391
|
+
this.browser = await playwright_core_1.chromium.connectOverCDP(this.cdpUrl);
|
|
392
|
+
const page = this.findBestPage(this.browser);
|
|
393
|
+
if (!page) {
|
|
394
|
+
result.blockedReason = "no_active_page";
|
|
446
395
|
return result;
|
|
447
396
|
}
|
|
448
|
-
|
|
449
|
-
await client.send("Runtime.enable");
|
|
450
|
-
await client.send("DOM.enable");
|
|
451
|
-
const billingResult = await this.fillBillingAcrossFrames(client, billingInfo);
|
|
397
|
+
const billingResult = await this.fillBillingFields(page, billing);
|
|
452
398
|
result.billingFilled = billingResult.filled.length > 0;
|
|
453
399
|
result.billingDetails = billingResult;
|
|
454
400
|
return result;
|
|
455
401
|
}
|
|
456
402
|
catch (err) {
|
|
457
|
-
|
|
403
|
+
log("error", "billing injection failed", { error: err.message });
|
|
458
404
|
return result;
|
|
459
405
|
}
|
|
460
406
|
finally {
|
|
461
|
-
|
|
407
|
+
await this.close();
|
|
462
408
|
}
|
|
463
409
|
}
|
|
464
|
-
|
|
465
|
-
// Public API: page snapshot
|
|
466
|
-
// ------------------------------------------------------------------
|
|
467
|
-
async pageSnapshot(pageUrl) {
|
|
468
|
-
let client = null;
|
|
410
|
+
async pageSnapshot(url) {
|
|
469
411
|
try {
|
|
470
|
-
|
|
471
|
-
|
|
412
|
+
this.browser = await playwright_core_1.chromium.connectOverCDP(this.cdpUrl);
|
|
413
|
+
const page = this.findBestPage(this.browser);
|
|
414
|
+
if (!page)
|
|
472
415
|
return null;
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
await
|
|
476
|
-
await client.send("Page.enable");
|
|
477
|
-
// Get main frame info
|
|
478
|
-
const { result: titleResult } = await client.send("Runtime.evaluate", {
|
|
479
|
-
expression: "document.title",
|
|
480
|
-
});
|
|
481
|
-
const { result: urlResult } = await client.send("Runtime.evaluate", {
|
|
482
|
-
expression: "window.location.href",
|
|
483
|
-
});
|
|
484
|
-
const { result: htmlResult } = await client.send("Runtime.evaluate", {
|
|
485
|
-
expression: "document.documentElement.outerHTML",
|
|
486
|
-
});
|
|
487
|
-
// Get frame tree for iframe content
|
|
488
|
-
const { frameTree } = await client.send("Page.getFrameTree");
|
|
416
|
+
const title = await page.title();
|
|
417
|
+
const pageUrl = page.url();
|
|
418
|
+
const html = await page.content();
|
|
489
419
|
const frames = [];
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
contextId: undefined, // Would need execution context for each frame
|
|
497
|
-
});
|
|
498
|
-
frames.push({ url: child.frame.url, html: frameHtml?.value ?? "" });
|
|
499
|
-
}
|
|
500
|
-
catch { }
|
|
501
|
-
await collectFrames(child);
|
|
502
|
-
}
|
|
420
|
+
for (const frame of page.frames()) {
|
|
421
|
+
if (frame === page.mainFrame())
|
|
422
|
+
continue;
|
|
423
|
+
try {
|
|
424
|
+
const frameHtml = await frame.content();
|
|
425
|
+
frames.push({ url: frame.url(), html: frameHtml });
|
|
503
426
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
return {
|
|
507
|
-
url: urlResult?.value ?? target.url,
|
|
508
|
-
title: titleResult?.value ?? target.title,
|
|
509
|
-
html: htmlResult?.value ?? "",
|
|
510
|
-
frames,
|
|
511
|
-
};
|
|
427
|
+
catch { }
|
|
428
|
+
}
|
|
429
|
+
return { url: pageUrl, title, html, frames };
|
|
512
430
|
}
|
|
513
431
|
catch (err) {
|
|
514
|
-
|
|
432
|
+
log("error", "snapshot failed", { error: err.message });
|
|
515
433
|
return null;
|
|
516
434
|
}
|
|
517
435
|
finally {
|
|
518
|
-
|
|
436
|
+
await this.close();
|
|
519
437
|
}
|
|
520
438
|
}
|
|
439
|
+
async close() {
|
|
440
|
+
if (this.browser) {
|
|
441
|
+
try {
|
|
442
|
+
await this.browser.close();
|
|
443
|
+
}
|
|
444
|
+
catch { }
|
|
445
|
+
this.browser = null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
static maskedCard(cardNumber) {
|
|
449
|
+
const last4 = cardNumber.slice(-4);
|
|
450
|
+
return `****-****-****-${last4}`;
|
|
451
|
+
}
|
|
521
452
|
// ------------------------------------------------------------------
|
|
522
|
-
// Internal
|
|
453
|
+
// Internal Helpers
|
|
523
454
|
// ------------------------------------------------------------------
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
catch {
|
|
530
|
-
return null;
|
|
531
|
-
}
|
|
532
|
-
const pageTargets = targets.filter((t) => t.type === "page");
|
|
533
|
-
if (pageTargets.length === 0)
|
|
455
|
+
findBestPage(browser) {
|
|
456
|
+
const CHECKOUT_KEYWORDS = ["checkout", "payment", "donate", "pay", "purchase", "order", "gateway", "cart"];
|
|
457
|
+
const allPages = browser.contexts().flatMap((ctx) => ctx.pages());
|
|
458
|
+
if (allPages.length === 0)
|
|
534
459
|
return null;
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
for (const t of pageTargets) {
|
|
540
|
-
const urlLower = t.url.toLowerCase();
|
|
541
|
-
if (checkoutKeywords.some((kw) => urlLower.includes(kw))) {
|
|
542
|
-
return t;
|
|
460
|
+
for (const page of allPages) {
|
|
461
|
+
const url = page.url().toLowerCase();
|
|
462
|
+
if (CHECKOUT_KEYWORDS.some((kw) => url.includes(kw))) {
|
|
463
|
+
return page;
|
|
543
464
|
}
|
|
544
465
|
}
|
|
545
|
-
|
|
546
|
-
if (pageUrl) {
|
|
547
|
-
for (const t of pageTargets) {
|
|
548
|
-
if (t.url === pageUrl)
|
|
549
|
-
return t;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
// Fallback: last target
|
|
553
|
-
return pageTargets[pageTargets.length - 1];
|
|
466
|
+
return allPages[allPages.length - 1];
|
|
554
467
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
// ------------------------------------------------------------------
|
|
558
|
-
async fillCardAcrossFrames(client, cardNumber, expiry, cvv) {
|
|
559
|
-
// Track each field independently — Stripe splits fields across sibling iframes
|
|
468
|
+
async fillAcrossFrames(page, cardNumber, expiry, cvv) {
|
|
469
|
+
const allFrames = page.frames();
|
|
560
470
|
let cardFilled = false;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (r.expiry)
|
|
569
|
-
expiryFilled = true;
|
|
570
|
-
if (r.cvv)
|
|
571
|
-
cvvFilled = true;
|
|
572
|
-
};
|
|
573
|
-
// Process main frame
|
|
574
|
-
merge(await this.fillCardInContext(client, undefined, cardNumber, expiry, cvv));
|
|
575
|
-
// Process child frames (iframes) — keep going even after card found
|
|
576
|
-
// (expiry/CVV may be in sibling iframes, common in Stripe's multi-iframe layout)
|
|
577
|
-
const processFrame = async (tree) => {
|
|
578
|
-
if (tree.childFrames) {
|
|
579
|
-
for (const child of tree.childFrames) {
|
|
580
|
-
try {
|
|
581
|
-
// Create isolated world for cross-origin iframe access
|
|
582
|
-
const { executionContextId } = await client.send("Page.createIsolatedWorld", { frameId: child.frame.id, worldName: "pop-pay-injector" });
|
|
583
|
-
merge(await this.fillCardInContext(client, executionContextId, cardNumber, expiry, cvv));
|
|
584
|
-
}
|
|
585
|
-
catch {
|
|
586
|
-
// Cross-origin frame access may fail — continue
|
|
587
|
-
}
|
|
588
|
-
await processFrame(child);
|
|
471
|
+
for (const frame of allFrames) {
|
|
472
|
+
try {
|
|
473
|
+
log("debug", "scanning frame", { frameUrl: frame.url() });
|
|
474
|
+
if (await this.fillInFrame(frame, cardNumber, expiry, cvv)) {
|
|
475
|
+
log("info", "card fields filled in frame", { frameUrl: frame.url() });
|
|
476
|
+
cardFilled = true;
|
|
477
|
+
// Keep going for expiry/CVV in sibling iframes (Stripe)
|
|
589
478
|
}
|
|
590
479
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
// Shadow DOM piercing
|
|
480
|
+
catch { }
|
|
481
|
+
}
|
|
482
|
+
// Shadow DOM piercing fallback
|
|
594
483
|
if (!cardFilled) {
|
|
595
|
-
|
|
596
|
-
if (shadowFilled)
|
|
597
|
-
cardFilled = true;
|
|
484
|
+
cardFilled = await this.fillCardInShadowDom(page, cardNumber, expiry, cvv);
|
|
598
485
|
}
|
|
599
486
|
return cardFilled;
|
|
600
487
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const processFrame = async (tree) => {
|
|
621
|
-
if (tree.childFrames) {
|
|
622
|
-
for (const child of tree.childFrames) {
|
|
623
|
-
try {
|
|
624
|
-
// Create isolated world for cross-origin iframe access
|
|
625
|
-
const { executionContextId } = await client.send("Page.createIsolatedWorld", { frameId: child.frame.id, worldName: "pop-pay-billing-injector" });
|
|
626
|
-
merge(await this.fillBillingFields(client, info, { contextId: executionContextId }));
|
|
627
|
-
}
|
|
628
|
-
catch {
|
|
629
|
-
// Cross-origin frame access may fail — continue
|
|
630
|
-
}
|
|
631
|
-
await processFrame(child);
|
|
488
|
+
async fillInFrame(frame, cardNumber, expiry, cvv) {
|
|
489
|
+
const cardLocator = await this.findVisibleLocator(frame, exports.CARD_NUMBER_SELECTORS);
|
|
490
|
+
if (!cardLocator)
|
|
491
|
+
return false;
|
|
492
|
+
await cardLocator.fill(cardNumber);
|
|
493
|
+
const expiryLocator = await this.findVisibleLocator(frame, exports.EXPIRY_SELECTORS);
|
|
494
|
+
if (expiryLocator)
|
|
495
|
+
await expiryLocator.fill(expiry);
|
|
496
|
+
const cvvLocator = await this.findVisibleLocator(frame, exports.CVV_SELECTORS);
|
|
497
|
+
if (cvvLocator)
|
|
498
|
+
await cvvLocator.fill(cvv);
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
async findVisibleLocator(frame, selectors) {
|
|
502
|
+
for (const selector of selectors) {
|
|
503
|
+
try {
|
|
504
|
+
const locator = frame.locator(selector).first();
|
|
505
|
+
if (await locator.count() > 0) {
|
|
506
|
+
return locator;
|
|
632
507
|
}
|
|
633
508
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
result.filled = [...new Set(result.filled)];
|
|
638
|
-
result.failed = result.failed.filter((f) => !result.filled.some((filled) => f.startsWith(filled)));
|
|
639
|
-
result.skipped = result.skipped.filter((s) => !result.filled.includes(s));
|
|
640
|
-
return result;
|
|
641
|
-
}
|
|
642
|
-
// ------------------------------------------------------------------
|
|
643
|
-
// Internal: fill card fields in a single execution context
|
|
644
|
-
// ------------------------------------------------------------------
|
|
645
|
-
async fillCardInContext(client, contextId, cardNumber, expiry, cvv) {
|
|
646
|
-
const evalOpts = contextId !== undefined
|
|
647
|
-
? { contextId }
|
|
648
|
-
: {};
|
|
649
|
-
// Try each field independently — Stripe uses separate iframes per field
|
|
650
|
-
const cardSelector = exports.CARD_NUMBER_SELECTORS.join(", ");
|
|
651
|
-
const card = await this.fillInputViaEval(client, evalOpts, cardSelector, cardNumber);
|
|
652
|
-
const expirySelector = exports.EXPIRY_SELECTORS.join(", ");
|
|
653
|
-
const exp = await this.fillInputViaEval(client, evalOpts, expirySelector, expiry);
|
|
654
|
-
const cvvSelector = exports.CVV_SELECTORS.join(", ");
|
|
655
|
-
const cvvFilled = await this.fillInputViaEval(client, evalOpts, cvvSelector, cvv);
|
|
656
|
-
return { card, expiry: exp, cvv: cvvFilled };
|
|
509
|
+
catch { }
|
|
510
|
+
}
|
|
511
|
+
return null;
|
|
657
512
|
}
|
|
658
|
-
|
|
659
|
-
// Internal: Shadow DOM piercing support (new feature!)
|
|
660
|
-
// ------------------------------------------------------------------
|
|
661
|
-
async fillCardInShadowDom(client, cardNumber, expiry, cvv) {
|
|
513
|
+
async fillCardInShadowDom(page, cardNumber, expiry, cvv) {
|
|
662
514
|
try {
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
const found = root.querySelector(sel);
|
|
671
|
-
if (found) results.push(found);
|
|
672
|
-
}
|
|
673
|
-
// Recurse into shadow roots
|
|
674
|
-
const allElements = root.querySelectorAll('*');
|
|
675
|
-
for (const el of allElements) {
|
|
676
|
-
if (el.shadowRoot) {
|
|
677
|
-
results.push(...queryShadowAll(el.shadowRoot, selectors));
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
return results;
|
|
515
|
+
const script = `
|
|
516
|
+
([cardNumber, expiry, cvv, cardSels, expSels, cvvSels]) => {
|
|
517
|
+
function queryShadowFirst(root, selectors) {
|
|
518
|
+
const selectorList = selectors.split(', ');
|
|
519
|
+
for (const sel of selectorList) {
|
|
520
|
+
const found = root.querySelector(sel);
|
|
521
|
+
if (found) return found;
|
|
681
522
|
}
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
})()
|
|
687
|
-
`,
|
|
688
|
-
returnByValue: true,
|
|
689
|
-
});
|
|
690
|
-
if (!result?.value)
|
|
691
|
-
return false;
|
|
692
|
-
// Found shadow DOM card fields — fill them
|
|
693
|
-
const { result: fillResult } = await client.send("Runtime.evaluate", {
|
|
694
|
-
expression: `
|
|
695
|
-
(function() {
|
|
696
|
-
function queryShadowFirst(root, selectors) {
|
|
697
|
-
const selectorList = selectors.split(', ');
|
|
698
|
-
for (const sel of selectorList) {
|
|
699
|
-
const found = root.querySelector(sel);
|
|
523
|
+
const allElements = root.querySelectorAll('*');
|
|
524
|
+
for (const el of allElements) {
|
|
525
|
+
if (el.shadowRoot) {
|
|
526
|
+
const found = queryShadowFirst(el.shadowRoot, selectors);
|
|
700
527
|
if (found) return found;
|
|
701
528
|
}
|
|
702
|
-
const allElements = root.querySelectorAll('*');
|
|
703
|
-
for (const el of allElements) {
|
|
704
|
-
if (el.shadowRoot) {
|
|
705
|
-
const found = queryShadowFirst(el.shadowRoot, selectors);
|
|
706
|
-
if (found) return found;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
return null;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
function fillField(root, selectors, value) {
|
|
713
|
-
const el = queryShadowFirst(root, selectors);
|
|
714
|
-
if (!el) return false;
|
|
715
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
716
|
-
HTMLInputElement.prototype, 'value'
|
|
717
|
-
).set;
|
|
718
|
-
nativeSetter.call(el, value);
|
|
719
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
720
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
721
|
-
el.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
722
|
-
return true;
|
|
723
529
|
}
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
724
532
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
${JSON.stringify(exports.CARD_NUMBER_SELECTORS.join(", "))},
|
|
728
|
-
${JSON.stringify(cardNumber)}
|
|
729
|
-
);
|
|
730
|
-
if (cardFilled) {
|
|
731
|
-
fillField(document, ${JSON.stringify(exports.EXPIRY_SELECTORS.join(", "))}, ${JSON.stringify(expiry)});
|
|
732
|
-
fillField(document, ${JSON.stringify(exports.CVV_SELECTORS.join(", "))}, ${JSON.stringify(cvv)});
|
|
733
|
-
}
|
|
734
|
-
return cardFilled;
|
|
735
|
-
})()
|
|
736
|
-
`,
|
|
737
|
-
returnByValue: true,
|
|
738
|
-
});
|
|
739
|
-
return fillResult?.value === true;
|
|
740
|
-
}
|
|
741
|
-
catch {
|
|
742
|
-
return false;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
// ------------------------------------------------------------------
|
|
746
|
-
// Internal: fill a single input field via Runtime.evaluate
|
|
747
|
-
// ------------------------------------------------------------------
|
|
748
|
-
async fillInputViaEval(client, evalOpts, selector, value) {
|
|
749
|
-
try {
|
|
750
|
-
const { result } = await client.send("Runtime.evaluate", {
|
|
751
|
-
expression: `
|
|
752
|
-
(function() {
|
|
753
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
533
|
+
function fillField(root, selectors, value) {
|
|
534
|
+
const el = queryShadowFirst(root, selectors);
|
|
754
535
|
if (!el) return false;
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
: HTMLInputElement.prototype;
|
|
759
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
536
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
537
|
+
HTMLInputElement.prototype, 'value'
|
|
538
|
+
).set;
|
|
760
539
|
if (nativeSetter) {
|
|
761
|
-
nativeSetter.call(el,
|
|
540
|
+
nativeSetter.call(el, value);
|
|
762
541
|
} else {
|
|
763
|
-
el.value =
|
|
542
|
+
el.value = value;
|
|
764
543
|
}
|
|
765
544
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
766
545
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
767
|
-
el.dispatchEvent(new
|
|
546
|
+
el.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
768
547
|
return true;
|
|
769
|
-
}
|
|
770
|
-
`,
|
|
771
|
-
returnByValue: true,
|
|
772
|
-
...evalOpts,
|
|
773
|
-
});
|
|
774
|
-
return result?.value === true;
|
|
775
|
-
}
|
|
776
|
-
catch {
|
|
777
|
-
return false;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
// ------------------------------------------------------------------
|
|
781
|
-
// Internal: select an option from a <select> dropdown
|
|
782
|
-
// ------------------------------------------------------------------
|
|
783
|
-
async selectOption(client, evalOpts, selector, value) {
|
|
784
|
-
try {
|
|
785
|
-
const { result } = await client.send("Runtime.evaluate", {
|
|
786
|
-
expression: `
|
|
787
|
-
(function() {
|
|
788
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
789
|
-
if (!el || el.tagName !== 'SELECT') return false;
|
|
790
|
-
|
|
791
|
-
const options = Array.from(el.options).map(o => ({
|
|
792
|
-
value: o.value, text: o.text.trim()
|
|
793
|
-
}));
|
|
794
|
-
const valueLower = ${JSON.stringify(value.toLowerCase())};
|
|
795
|
-
|
|
796
|
-
let matchedValue = null;
|
|
797
|
-
// Exact value match
|
|
798
|
-
for (const opt of options) {
|
|
799
|
-
if (opt.value.toLowerCase() === valueLower) { matchedValue = opt.value; break; }
|
|
800
|
-
}
|
|
801
|
-
// Exact text match
|
|
802
|
-
if (!matchedValue) {
|
|
803
|
-
for (const opt of options) {
|
|
804
|
-
if (opt.text.toLowerCase() === valueLower) { matchedValue = opt.value; break; }
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
// Partial match
|
|
808
|
-
if (!matchedValue) {
|
|
809
|
-
for (const opt of options) {
|
|
810
|
-
const optText = opt.text.toLowerCase();
|
|
811
|
-
const optVal = opt.value.toLowerCase();
|
|
812
|
-
if ((valueLower.includes(optText) || optText.includes(valueLower) ||
|
|
813
|
-
valueLower.includes(optVal) || optVal.includes(valueLower)) && opt.value) {
|
|
814
|
-
matchedValue = opt.value; break;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
if (!matchedValue) return false;
|
|
819
|
-
|
|
820
|
-
// Native setter trick for React/Angular/Vue/Zoho
|
|
821
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
822
|
-
HTMLSelectElement.prototype, 'value'
|
|
823
|
-
).set;
|
|
824
|
-
nativeSetter.call(el, matchedValue);
|
|
548
|
+
}
|
|
825
549
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
});
|
|
842
|
-
return result?.value === true;
|
|
550
|
+
const cardFilled = fillField(document, cardSels, cardNumber);
|
|
551
|
+
if (cardFilled) {
|
|
552
|
+
fillField(document, expSels, expiry);
|
|
553
|
+
fillField(document, cvvSels, cvv);
|
|
554
|
+
}
|
|
555
|
+
return cardFilled;
|
|
556
|
+
}
|
|
557
|
+
`;
|
|
558
|
+
const result = await page.evaluate(script, [
|
|
559
|
+
cardNumber, expiry, cvv,
|
|
560
|
+
exports.CARD_NUMBER_SELECTORS.join(", "),
|
|
561
|
+
exports.EXPIRY_SELECTORS.join(", "),
|
|
562
|
+
exports.CVV_SELECTORS.join(", ")
|
|
563
|
+
]);
|
|
564
|
+
return !!result;
|
|
843
565
|
}
|
|
844
566
|
catch {
|
|
845
567
|
return false;
|
|
846
568
|
}
|
|
847
569
|
}
|
|
848
|
-
|
|
849
|
-
// Internal: fill a billing field (input or select)
|
|
850
|
-
// ------------------------------------------------------------------
|
|
851
|
-
async fillBillingField(client, evalOpts, selectors, value, fieldName) {
|
|
852
|
-
if (!value)
|
|
853
|
-
return false;
|
|
854
|
-
// Detect if first matching element is a <select> or <input>
|
|
855
|
-
const allSelector = selectors.join(", ");
|
|
856
|
-
try {
|
|
857
|
-
const { result } = await client.send("Runtime.evaluate", {
|
|
858
|
-
expression: `
|
|
859
|
-
(function() {
|
|
860
|
-
const el = document.querySelector(${JSON.stringify(allSelector)});
|
|
861
|
-
if (!el) return null;
|
|
862
|
-
return el.tagName.toLowerCase();
|
|
863
|
-
})()
|
|
864
|
-
`,
|
|
865
|
-
returnByValue: true,
|
|
866
|
-
...evalOpts,
|
|
867
|
-
});
|
|
868
|
-
if (!result?.value)
|
|
869
|
-
return false;
|
|
870
|
-
if (result.value === "select") {
|
|
871
|
-
return await this.selectOption(client, evalOpts, allSelector, value);
|
|
872
|
-
}
|
|
873
|
-
else {
|
|
874
|
-
return await this.fillInputViaEval(client, evalOpts, allSelector, value);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
catch {
|
|
878
|
-
return false;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
// ------------------------------------------------------------------
|
|
882
|
-
// Internal: fill all billing fields
|
|
883
|
-
// ------------------------------------------------------------------
|
|
884
|
-
async fillBillingFields(client, info, evalOpts) {
|
|
570
|
+
async fillBillingFields(page, info) {
|
|
885
571
|
const filled = [];
|
|
886
572
|
const failed = [];
|
|
887
573
|
const skipped = [];
|
|
888
|
-
|
|
889
|
-
const
|
|
890
|
-
? US_STATE_CODES[info.state.toUpperCase()] ?? info.state
|
|
891
|
-
: info.state;
|
|
892
|
-
const tryFill = async (selectors, value, name) => {
|
|
574
|
+
const state = info.state.length === 2 ? US_STATE_CODES[info.state.toUpperCase()] ?? info.state : info.state;
|
|
575
|
+
const tryFill = async (selectors, value, name, label) => {
|
|
893
576
|
if (!value) {
|
|
894
577
|
skipped.push(name);
|
|
895
578
|
return;
|
|
896
579
|
}
|
|
897
|
-
const ok = await this.
|
|
580
|
+
const ok = await this.fillField(page, selectors, value, name, label);
|
|
898
581
|
if (ok)
|
|
899
582
|
filled.push(name);
|
|
900
583
|
else
|
|
901
584
|
failed.push(`${name} (value='${value}')`);
|
|
902
585
|
};
|
|
903
|
-
//
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const fieldConfigs = [
|
|
907
|
-
{ selectors: exports.FIRST_NAME_SELECTORS, value: info.firstName, name: "first_name" },
|
|
908
|
-
{ selectors: exports.LAST_NAME_SELECTORS, value: info.lastName, name: "last_name" },
|
|
909
|
-
{ selectors: exports.STREET_SELECTORS, value: info.street, name: "street" },
|
|
910
|
-
{ selectors: exports.CITY_SELECTORS, value: info.city, name: "city" },
|
|
911
|
-
{ selectors: exports.STATE_SELECTORS, value: state, name: "state" },
|
|
912
|
-
{ selectors: exports.COUNTRY_SELECTORS, value: info.country, name: "country" },
|
|
913
|
-
{ selectors: exports.ZIP_SELECTORS, value: info.zip, name: "zip" },
|
|
914
|
-
{ selectors: exports.EMAIL_SELECTORS, value: info.email, name: "email" },
|
|
915
|
-
];
|
|
916
|
-
// Full name fallback
|
|
586
|
+
// Input fields first
|
|
587
|
+
await tryFill(exports.FIRST_NAME_SELECTORS, info.firstName, "first_name", "First name");
|
|
588
|
+
await tryFill(exports.LAST_NAME_SELECTORS, info.lastName, "last_name", "Last name");
|
|
917
589
|
if (info.firstName || info.lastName) {
|
|
918
590
|
const fullName = [info.firstName, info.lastName].filter(Boolean).join(" ");
|
|
919
|
-
|
|
591
|
+
await tryFill(exports.FULL_NAME_SELECTORS, fullName, "full_name", "Full name");
|
|
592
|
+
}
|
|
593
|
+
await tryFill(exports.STREET_SELECTORS, info.street, "street", "Address");
|
|
594
|
+
await tryFill(exports.CITY_SELECTORS, info.city, "city", "City");
|
|
595
|
+
await tryFill(exports.ZIP_SELECTORS, info.zip, "zip", "Zip");
|
|
596
|
+
await tryFill(exports.EMAIL_SELECTORS, info.email, "email", "Email");
|
|
597
|
+
// Selects last
|
|
598
|
+
await tryFill(exports.COUNTRY_SELECTORS, info.country, "country", "Country");
|
|
599
|
+
await tryFill(exports.STATE_SELECTORS, state, "state", "State");
|
|
600
|
+
// Phone
|
|
601
|
+
let ccFilled = false;
|
|
602
|
+
if (info.phoneCountryCode) {
|
|
603
|
+
ccFilled = await this.fillField(page, exports.PHONE_COUNTRY_CODE_SELECTORS, info.phoneCountryCode, "phone_country_code", "Country code");
|
|
604
|
+
if (ccFilled)
|
|
605
|
+
filled.push("phone_country_code");
|
|
920
606
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
607
|
+
const phoneValue = ccFilled ? nationalNumber(info.phone, info.phoneCountryCode) : info.phone;
|
|
608
|
+
await tryFill(exports.PHONE_SELECTORS, phoneValue, "phone", "Phone");
|
|
609
|
+
return { filled, failed, skipped };
|
|
610
|
+
}
|
|
611
|
+
async fillField(page, selectors, value, name, label) {
|
|
612
|
+
// Strategy 1: getByLabel
|
|
613
|
+
try {
|
|
614
|
+
const labelLocator = page.getByLabel(label, { exact: false });
|
|
615
|
+
if (await labelLocator.count() > 0) {
|
|
616
|
+
const tag = await labelLocator.first().evaluate("el => el.tagName.toLowerCase()");
|
|
617
|
+
if (tag === "select") {
|
|
618
|
+
return await this.selectOption(labelLocator.first(), value);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
await labelLocator.first().fill(value);
|
|
622
|
+
await this.dispatchEvents(labelLocator.first());
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
927
625
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
...evalOpts,
|
|
940
|
-
});
|
|
941
|
-
tagName = result?.value || null;
|
|
626
|
+
}
|
|
627
|
+
catch { }
|
|
628
|
+
// Strategy 2: CSS selectors
|
|
629
|
+
const frame = page.mainFrame();
|
|
630
|
+
const locator = await this.findVisibleLocator(frame, selectors);
|
|
631
|
+
if (!locator)
|
|
632
|
+
return false;
|
|
633
|
+
try {
|
|
634
|
+
const tag = await locator.evaluate("el => el.tagName.toLowerCase()");
|
|
635
|
+
if (tag === "select") {
|
|
636
|
+
return await this.selectOption(locator, value);
|
|
942
637
|
}
|
|
943
|
-
|
|
944
|
-
|
|
638
|
+
else {
|
|
639
|
+
await locator.fill(value);
|
|
640
|
+
await this.dispatchEvents(locator);
|
|
641
|
+
return true;
|
|
945
642
|
}
|
|
946
|
-
detections.push({ ...config, tagName });
|
|
947
643
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
if (ok)
|
|
951
|
-
filled.push(d.name);
|
|
952
|
-
else
|
|
953
|
-
failed.push(`${d.name} (value='${d.value}')`);
|
|
954
|
-
};
|
|
955
|
-
// Round 1: fill all non-select fields (inputs, textareas, etc.)
|
|
956
|
-
for (const d of detections) {
|
|
957
|
-
if (d.tagName !== "select") {
|
|
958
|
-
await doFill(d);
|
|
959
|
-
}
|
|
644
|
+
catch {
|
|
645
|
+
return false;
|
|
960
646
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
647
|
+
}
|
|
648
|
+
async selectOption(locator, value) {
|
|
649
|
+
try {
|
|
650
|
+
const options = await locator.evaluate(`el =>
|
|
651
|
+
Array.from(el.options).map(o => ({ value: o.value, text: o.text.trim() }))
|
|
652
|
+
`);
|
|
653
|
+
const valueLower = value.toLowerCase();
|
|
654
|
+
let matchedValue = null;
|
|
655
|
+
for (const opt of options) {
|
|
656
|
+
if (opt.value.toLowerCase() === valueLower) {
|
|
657
|
+
matchedValue = opt.value;
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (!matchedValue) {
|
|
662
|
+
for (const opt of options) {
|
|
663
|
+
if (opt.text.toLowerCase() === valueLower) {
|
|
664
|
+
matchedValue = opt.value;
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
965
668
|
}
|
|
669
|
+
if (!matchedValue) {
|
|
670
|
+
for (const opt of options) {
|
|
671
|
+
if ((valueLower.includes(opt.text.toLowerCase()) || opt.text.toLowerCase().includes(valueLower)) && opt.value) {
|
|
672
|
+
matchedValue = opt.value;
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (!matchedValue)
|
|
678
|
+
return false;
|
|
679
|
+
await locator.selectOption(matchedValue);
|
|
680
|
+
const actual = await locator.evaluate((el) => el.value);
|
|
681
|
+
if (actual === matchedValue) {
|
|
682
|
+
await this.dispatchEvents(locator);
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
return await locator.evaluate(`(el, val) => {
|
|
686
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value")?.set;
|
|
687
|
+
if (!nativeSetter) return false;
|
|
688
|
+
nativeSetter.call(el, val);
|
|
689
|
+
const events = ["focusin", "focus", "mousedown", "mouseup", "click", "input", "change", "blur", "focusout"];
|
|
690
|
+
events.forEach((evt) => el.dispatchEvent(new Event(evt, { bubbles: true })));
|
|
691
|
+
return el.value === val;
|
|
692
|
+
}`, matchedValue);
|
|
966
693
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
if (info.phoneCountryCode) {
|
|
970
|
-
ccFilled = await this.fillBillingField(client, evalOpts, exports.PHONE_COUNTRY_CODE_SELECTORS, info.phoneCountryCode, "phone_country_code");
|
|
971
|
-
if (ccFilled)
|
|
972
|
-
filled.push("phone_country_code");
|
|
694
|
+
catch {
|
|
695
|
+
return false;
|
|
973
696
|
}
|
|
974
|
-
const phoneValue = ccFilled
|
|
975
|
-
? nationalNumber(info.phone, info.phoneCountryCode)
|
|
976
|
-
: info.phone;
|
|
977
|
-
await tryFill(exports.PHONE_SELECTORS, phoneValue, "phone");
|
|
978
|
-
return { filled, failed, skipped };
|
|
979
697
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
698
|
+
async dispatchEvents(locator) {
|
|
699
|
+
try {
|
|
700
|
+
await locator.dispatchEvent("input");
|
|
701
|
+
await locator.dispatchEvent("change");
|
|
702
|
+
await locator.evaluate("el => el.dispatchEvent(new Event('blur', { bubbles: true }))");
|
|
703
|
+
}
|
|
704
|
+
catch { }
|
|
705
|
+
}
|
|
706
|
+
async enableBlackout(page) {
|
|
707
|
+
try {
|
|
708
|
+
for (const frame of page.frames()) {
|
|
709
|
+
try {
|
|
710
|
+
await frame.addStyleTag({
|
|
711
|
+
content: `
|
|
712
|
+
input[autocomplete*="cc-"],
|
|
713
|
+
input[name*="card"], input[name*="Card"],
|
|
714
|
+
input[name*="expir"], input[name*="cvc"], input[name*="cvv"],
|
|
715
|
+
input[data-elements-stable-field-name],
|
|
716
|
+
input.__PrivateStripeElement,
|
|
717
|
+
input[name="cardnumber"], input[name="cc-exp"],
|
|
718
|
+
input[name="security_code"], input[name="card_number"],
|
|
719
|
+
input[name="card_expiry"], input[name="card_cvc"] {
|
|
720
|
+
-webkit-text-security: disc !important;
|
|
721
|
+
color: transparent !important;
|
|
722
|
+
text-shadow: 0 0 8px rgba(0,0,0,0.5) !important;
|
|
723
|
+
}
|
|
724
|
+
`,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
catch { }
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
catch { }
|
|
731
|
+
}
|
|
732
|
+
loadBillingFromEnv() {
|
|
984
733
|
return {
|
|
985
734
|
firstName: (process.env.POP_BILLING_FIRST_NAME ?? "").trim(),
|
|
986
735
|
lastName: (process.env.POP_BILLING_LAST_NAME ?? "").trim(),
|
|
@@ -994,61 +743,6 @@ class PopBrowserInjector {
|
|
|
994
743
|
phoneCountryCode: (process.env.POP_BILLING_PHONE_COUNTRY_CODE ?? "").trim(),
|
|
995
744
|
};
|
|
996
745
|
}
|
|
997
|
-
// ------------------------------------------------------------------
|
|
998
|
-
// Internal: blackout mode (mask card fields)
|
|
999
|
-
// ------------------------------------------------------------------
|
|
1000
|
-
async enableBlackout(client) {
|
|
1001
|
-
try {
|
|
1002
|
-
await client.send("Runtime.evaluate", {
|
|
1003
|
-
expression: `
|
|
1004
|
-
(function() {
|
|
1005
|
-
// Inject into main frame
|
|
1006
|
-
function addBlackout(doc) {
|
|
1007
|
-
if (doc.getElementById('pop-pay-blackout')) return;
|
|
1008
|
-
const style = doc.createElement('style');
|
|
1009
|
-
style.id = 'pop-pay-blackout';
|
|
1010
|
-
style.textContent = \`
|
|
1011
|
-
input[autocomplete*="cc-"],
|
|
1012
|
-
input[name*="card"], input[name*="Card"],
|
|
1013
|
-
input[name*="expir"], input[name*="cvc"], input[name*="cvv"],
|
|
1014
|
-
input[data-elements-stable-field-name],
|
|
1015
|
-
input.__PrivateStripeElement,
|
|
1016
|
-
input[name="cardnumber"], input[name="cc-exp"],
|
|
1017
|
-
input[name="security_code"], input[name="card_number"],
|
|
1018
|
-
input[name="card_expiry"], input[name="card_cvc"] {
|
|
1019
|
-
-webkit-text-security: disc !important;
|
|
1020
|
-
color: transparent !important;
|
|
1021
|
-
text-shadow: 0 0 8px rgba(0,0,0,0.5) !important;
|
|
1022
|
-
}
|
|
1023
|
-
\`;
|
|
1024
|
-
doc.head.appendChild(style);
|
|
1025
|
-
}
|
|
1026
|
-
addBlackout(document);
|
|
1027
|
-
|
|
1028
|
-
// Try iframes (same-origin only)
|
|
1029
|
-
try {
|
|
1030
|
-
const iframes = document.querySelectorAll('iframe');
|
|
1031
|
-
for (const iframe of iframes) {
|
|
1032
|
-
try {
|
|
1033
|
-
if (iframe.contentDocument) {
|
|
1034
|
-
addBlackout(iframe.contentDocument);
|
|
1035
|
-
}
|
|
1036
|
-
} catch {}
|
|
1037
|
-
}
|
|
1038
|
-
} catch {}
|
|
1039
|
-
})()
|
|
1040
|
-
`,
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
catch { }
|
|
1044
|
-
}
|
|
1045
|
-
// ------------------------------------------------------------------
|
|
1046
|
-
// Masked card display helper
|
|
1047
|
-
// ------------------------------------------------------------------
|
|
1048
|
-
static maskedCard(cardNumber) {
|
|
1049
|
-
const last4 = cardNumber.slice(-4);
|
|
1050
|
-
return `****-****-****-${last4}`;
|
|
1051
|
-
}
|
|
1052
746
|
}
|
|
1053
747
|
exports.PopBrowserInjector = PopBrowserInjector;
|
|
1054
748
|
//# sourceMappingURL=injector.js.map
|