pop-pay 0.3.3 → 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/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 +324 -644
- package/dist/engine/injector.js.map +1 -1
- package/dist/mcp-server.js +199 -13
- 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,699 +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 {
|
|
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)
|
|
530
447
|
return null;
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const checkoutKeywords = [
|
|
536
|
-
"checkout", "payment", "donate", "pay", "purchase", "order", "gateway", "cart",
|
|
537
|
-
];
|
|
538
|
-
// Prefer pages whose URL looks like a checkout/payment page
|
|
539
|
-
for (const t of pageTargets) {
|
|
540
|
-
const urlLower = t.url.toLowerCase();
|
|
541
|
-
if (checkoutKeywords.some((kw) => urlLower.includes(kw))) {
|
|
542
|
-
return t;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
// If pageUrl is provided, try to find a matching target
|
|
546
|
-
if (pageUrl) {
|
|
547
|
-
for (const t of pageTargets) {
|
|
548
|
-
if (t.url === pageUrl)
|
|
549
|
-
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;
|
|
550
452
|
}
|
|
551
453
|
}
|
|
552
|
-
|
|
553
|
-
return pageTargets[pageTargets.length - 1];
|
|
454
|
+
return allPages[allPages.length - 1];
|
|
554
455
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
// ------------------------------------------------------------------
|
|
558
|
-
async fillCardAcrossFrames(client, cardNumber, expiry, cvv) {
|
|
559
|
-
// Track each field independently — Stripe splits fields across sibling iframes
|
|
456
|
+
async fillAcrossFrames(page, cardNumber, expiry, cvv) {
|
|
457
|
+
const allFrames = page.frames();
|
|
560
458
|
let cardFilled = false;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (r.card)
|
|
567
|
-
cardFilled = true;
|
|
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);
|
|
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)
|
|
589
464
|
}
|
|
590
465
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
// Shadow DOM piercing
|
|
466
|
+
catch { }
|
|
467
|
+
}
|
|
468
|
+
// Shadow DOM piercing fallback
|
|
594
469
|
if (!cardFilled) {
|
|
595
|
-
|
|
596
|
-
if (shadowFilled)
|
|
597
|
-
cardFilled = true;
|
|
470
|
+
cardFilled = await this.fillCardInShadowDom(page, cardNumber, expiry, cvv);
|
|
598
471
|
}
|
|
599
472
|
return cardFilled;
|
|
600
473
|
}
|
|
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);
|
|
474
|
+
async fillInFrame(frame, cardNumber, expiry, cvv) {
|
|
475
|
+
const cardLocator = await this.findVisibleLocator(frame, exports.CARD_NUMBER_SELECTORS);
|
|
476
|
+
if (!cardLocator)
|
|
477
|
+
return false;
|
|
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);
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
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;
|
|
632
493
|
}
|
|
633
494
|
}
|
|
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 };
|
|
495
|
+
catch { }
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
657
498
|
}
|
|
658
|
-
|
|
659
|
-
// Internal: Shadow DOM piercing support (new feature!)
|
|
660
|
-
// ------------------------------------------------------------------
|
|
661
|
-
async fillCardInShadowDom(client, cardNumber, expiry, cvv) {
|
|
499
|
+
async fillCardInShadowDom(page, cardNumber, expiry, cvv) {
|
|
662
500
|
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;
|
|
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;
|
|
681
508
|
}
|
|
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);
|
|
509
|
+
const allElements = root.querySelectorAll('*');
|
|
510
|
+
for (const el of allElements) {
|
|
511
|
+
if (el.shadowRoot) {
|
|
512
|
+
const found = queryShadowFirst(el.shadowRoot, selectors);
|
|
700
513
|
if (found) return found;
|
|
701
514
|
}
|
|
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
515
|
}
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
724
518
|
|
|
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)});
|
|
519
|
+
function fillField(root, selectors, value) {
|
|
520
|
+
const el = queryShadowFirst(root, selectors);
|
|
754
521
|
if (!el) return false;
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
: HTMLInputElement.prototype;
|
|
759
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
522
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
523
|
+
HTMLInputElement.prototype, 'value'
|
|
524
|
+
).set;
|
|
760
525
|
if (nativeSetter) {
|
|
761
|
-
nativeSetter.call(el,
|
|
526
|
+
nativeSetter.call(el, value);
|
|
762
527
|
} else {
|
|
763
|
-
el.value =
|
|
528
|
+
el.value = value;
|
|
764
529
|
}
|
|
765
530
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
766
531
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
767
|
-
el.dispatchEvent(new
|
|
532
|
+
el.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
768
533
|
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;
|
|
534
|
+
}
|
|
790
535
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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);
|
|
825
|
-
|
|
826
|
-
el.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
|
|
827
|
-
el.dispatchEvent(new FocusEvent('focus', { bubbles: false }));
|
|
828
|
-
el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
|
829
|
-
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
|
830
|
-
el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
831
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
832
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
833
|
-
el.dispatchEvent(new FocusEvent('blur', { bubbles: false }));
|
|
834
|
-
el.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
|
|
835
|
-
|
|
836
|
-
return el.value === matchedValue;
|
|
837
|
-
})()
|
|
838
|
-
`,
|
|
839
|
-
returnByValue: true,
|
|
840
|
-
...evalOpts,
|
|
841
|
-
});
|
|
842
|
-
return result?.value === true;
|
|
843
|
-
}
|
|
844
|
-
catch {
|
|
845
|
-
return false;
|
|
846
|
-
}
|
|
847
|
-
}
|
|
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
|
-
}
|
|
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;
|
|
876
551
|
}
|
|
877
552
|
catch {
|
|
878
553
|
return false;
|
|
879
554
|
}
|
|
880
555
|
}
|
|
881
|
-
|
|
882
|
-
// Internal: fill all billing fields
|
|
883
|
-
// ------------------------------------------------------------------
|
|
884
|
-
async fillBillingFields(client, info, evalOpts) {
|
|
556
|
+
async fillBillingFields(page, info) {
|
|
885
557
|
const filled = [];
|
|
886
558
|
const failed = [];
|
|
887
559
|
const skipped = [];
|
|
888
|
-
|
|
889
|
-
const
|
|
890
|
-
? US_STATE_CODES[info.state.toUpperCase()] ?? info.state
|
|
891
|
-
: info.state;
|
|
892
|
-
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) => {
|
|
893
562
|
if (!value) {
|
|
894
563
|
skipped.push(name);
|
|
895
564
|
return;
|
|
896
565
|
}
|
|
897
|
-
const ok = await this.
|
|
566
|
+
const ok = await this.fillField(page, selectors, value, name, label);
|
|
898
567
|
if (ok)
|
|
899
568
|
filled.push(name);
|
|
900
569
|
else
|
|
901
570
|
failed.push(`${name} (value='${value}')`);
|
|
902
571
|
};
|
|
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
|
|
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");
|
|
917
575
|
if (info.firstName || info.lastName) {
|
|
918
576
|
const fullName = [info.firstName, info.lastName].filter(Boolean).join(" ");
|
|
919
|
-
|
|
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");
|
|
920
592
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
+
}
|
|
927
611
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
...evalOpts,
|
|
940
|
-
});
|
|
941
|
-
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);
|
|
942
623
|
}
|
|
943
|
-
|
|
944
|
-
|
|
624
|
+
else {
|
|
625
|
+
await locator.fill(value);
|
|
626
|
+
await this.dispatchEvents(locator);
|
|
627
|
+
return true;
|
|
945
628
|
}
|
|
946
|
-
detections.push({ ...config, tagName });
|
|
947
629
|
}
|
|
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
|
-
}
|
|
630
|
+
catch {
|
|
631
|
+
return false;
|
|
960
632
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
+
}
|
|
965
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);
|
|
966
679
|
}
|
|
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");
|
|
680
|
+
catch {
|
|
681
|
+
return false;
|
|
973
682
|
}
|
|
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
683
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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() {
|
|
984
719
|
return {
|
|
985
720
|
firstName: (process.env.POP_BILLING_FIRST_NAME ?? "").trim(),
|
|
986
721
|
lastName: (process.env.POP_BILLING_LAST_NAME ?? "").trim(),
|
|
@@ -994,61 +729,6 @@ class PopBrowserInjector {
|
|
|
994
729
|
phoneCountryCode: (process.env.POP_BILLING_PHONE_COUNTRY_CODE ?? "").trim(),
|
|
995
730
|
};
|
|
996
731
|
}
|
|
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
732
|
}
|
|
1053
733
|
exports.PopBrowserInjector = PopBrowserInjector;
|
|
1054
734
|
//# sourceMappingURL=injector.js.map
|