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