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.
@@ -1,18 +1,18 @@
1
1
  "use strict";
2
2
  /**
3
- * PopBrowserInjector: CDP-based browser injector with iframe + Shadow DOM traversal.
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 on the active page — including fields inside
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
- * New in TS port: Shadow DOM piercing support.
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 (for dropdowns that use full names)
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
- // CSS selectors for credit card fields across major payment providers
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']", // Stripe Elements
76
- "input.__PrivateStripeElement", // Stripe v2
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']", // Stripe Elements
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']", // Stripe Elements
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 (shared between payment + billing injection)
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
- headless;
367
- constructor(cdpUrl = "http://localhost:9222", headless = false) {
284
+ defaultBillingInfo;
285
+ browser = null;
286
+ constructor(cdpUrl = "http://localhost:9222", billingInfoOrHeadless) {
368
287
  this.cdpUrl = cdpUrl;
369
- this.headless = headless;
288
+ if (typeof billingInfoOrHeadless === "object") {
289
+ this.defaultBillingInfo = billingInfoOrHeadless;
290
+ }
370
291
  }
371
- // ------------------------------------------------------------------
372
- // Public API: inject payment info (card + billing)
373
- // ------------------------------------------------------------------
374
- async injectPaymentInfo(opts) {
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(opts.pageUrl ?? "", opts.approvedVendor ?? "");
325
+ const blocked = verifyDomainToctou(url, vend);
382
326
  if (blocked) {
383
327
  result.blockedReason = blocked;
384
328
  return result;
385
329
  }
386
- const billingInfo = this.loadBillingInfo();
387
- const hasBilling = Object.values(billingInfo).some((v) => v !== "");
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
- const target = await this.findBestTarget(opts.pageUrl);
391
- if (!target?.webSocketDebuggerUrl) {
392
- result.blockedReason = "no_target_found";
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
- client = await CDPClient.connect(target.webSocketDebuggerUrl);
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(client);
343
+ await this.enableBlackout(page);
404
344
  }
405
- // Fill card fields across all frames (including iframes + shadow DOM)
406
- result.cardFilled = await this.fillCardAcrossFrames(client, opts.cardNumber, opts.expirationDate, opts.cvv);
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.fillBillingAcrossFrames(client, billingInfo);
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(client);
354
+ await this.enableBlackout(page);
415
355
  }
416
356
  return result;
417
357
  }
418
358
  catch (err) {
419
- process.stderr.write(`PopBrowserInjector error: ${err.message}\n`);
359
+ console.error(`PopBrowserInjector error: ${err.message}`);
420
360
  return result;
421
361
  }
422
362
  finally {
423
- client?.close();
363
+ await this.close();
424
364
  }
425
365
  }
426
- // ------------------------------------------------------------------
427
- // Public API: inject billing info only (no card)
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 billingInfo = this.loadBillingInfo();
441
- let client = null;
377
+ const billing = this.defaultBillingInfo || this.loadBillingFromEnv();
442
378
  try {
443
- const target = await this.findBestTarget(opts.pageUrl);
444
- if (!target?.webSocketDebuggerUrl) {
445
- result.blockedReason = "no_target_found";
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
- client = await CDPClient.connect(target.webSocketDebuggerUrl);
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
- process.stderr.write(`PopBrowserInjector billing error: ${err.message}\n`);
391
+ console.error(`PopBrowserInjector billing error: ${err.message}`);
458
392
  return result;
459
393
  }
460
394
  finally {
461
- client?.close();
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
- const target = await this.findBestTarget(pageUrl);
471
- if (!target?.webSocketDebuggerUrl)
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
- client = await CDPClient.connect(target.webSocketDebuggerUrl);
474
- await client.send("Runtime.enable");
475
- await client.send("DOM.enable");
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 collectFrames = async (tree) => {
491
- if (tree.childFrames) {
492
- for (const child of tree.childFrames) {
493
- try {
494
- const { result: frameHtml } = await client.send("Runtime.evaluate", {
495
- expression: "document.documentElement.outerHTML",
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
- await collectFrames(frameTree);
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
- process.stderr.write(`PopBrowserInjector snapshot error: ${err.message}\n`);
420
+ console.error(`PopBrowserInjector snapshot error: ${err.message}`);
515
421
  return null;
516
422
  }
517
423
  finally {
518
- client?.close();
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: find the best CDP target (prefer checkout pages)
441
+ // Internal Helpers
523
442
  // ------------------------------------------------------------------
524
- async findBestTarget(pageUrl) {
525
- let targets;
526
- try {
527
- targets = await fetchJSON(`${this.cdpUrl}/json/list`);
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
- const pageTargets = targets.filter((t) => t.type === "page");
533
- if (pageTargets.length === 0)
534
- return null;
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
- // Fallback: last target
553
- return pageTargets[pageTargets.length - 1];
454
+ return allPages[allPages.length - 1];
554
455
  }
555
- // ------------------------------------------------------------------
556
- // Internal: fill card fields across all frames (iframes + shadow DOM)
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
- let expiryFilled = false;
562
- let cvvFilled = false;
563
- // Get frame tree
564
- const { frameTree } = await client.send("Page.getFrameTree");
565
- const merge = (r) => {
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
- await processFrame(frameTree);
593
- // Shadow DOM piercing: fallback if card not found in any frame
466
+ catch { }
467
+ }
468
+ // Shadow DOM piercing fallback
594
469
  if (!cardFilled) {
595
- const shadowFilled = await this.fillCardInShadowDom(client, cardNumber, expiry, cvv);
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
- // Internal: fill billing fields across all frames (iframes)
603
- // ------------------------------------------------------------------
604
- async fillBillingAcrossFrames(client, info) {
605
- const result = {
606
- filled: [],
607
- failed: [],
608
- skipped: [],
609
- };
610
- const merge = (frameResult) => {
611
- result.filled.push(...frameResult.filled);
612
- result.failed.push(...frameResult.failed);
613
- result.skipped.push(...frameResult.skipped);
614
- };
615
- // Try main frame first
616
- merge(await this.fillBillingFields(client, info, {}));
617
- // Get frame tree
618
- const { frameTree } = await client.send("Page.getFrameTree");
619
- // Process child frames (iframes)
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
- await processFrame(frameTree);
636
- // Deduplicate: if a field was filled in any frame, remove from failed/skipped
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 { result } = await client.send("Runtime.evaluate", {
664
- expression: `
665
- (function() {
666
- function queryShadowAll(root, selectors) {
667
- const results = [];
668
- const selectorList = selectors.split(', ');
669
- for (const sel of selectorList) {
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 cardSelectors = ${JSON.stringify(exports.CARD_NUMBER_SELECTORS.join(", "))};
684
- const cardFields = queryShadowAll(document, cardSelectors);
685
- return cardFields.length > 0;
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
- const cardFilled = fillField(
726
- document,
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
- // Use native setter to bypass framework interception
756
- const proto = el.tagName === 'SELECT'
757
- ? HTMLSelectElement.prototype
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, ${JSON.stringify(value)});
526
+ nativeSetter.call(el, value);
762
527
  } else {
763
- el.value = ${JSON.stringify(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 FocusEvent('blur', { bubbles: true }));
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
- const options = Array.from(el.options).map(o => ({
792
- value: o.value, text: o.text.trim()
793
- }));
794
- const valueLower = ${JSON.stringify(value.toLowerCase())};
795
-
796
- let matchedValue = null;
797
- // Exact value match
798
- for (const opt of options) {
799
- if (opt.value.toLowerCase() === valueLower) { matchedValue = opt.value; break; }
800
- }
801
- // Exact text match
802
- if (!matchedValue) {
803
- for (const opt of options) {
804
- if (opt.text.toLowerCase() === valueLower) { matchedValue = opt.value; break; }
805
- }
806
- }
807
- // Partial match
808
- if (!matchedValue) {
809
- for (const opt of options) {
810
- const optText = opt.text.toLowerCase();
811
- const optVal = opt.value.toLowerCase();
812
- if ((valueLower.includes(optText) || optText.includes(valueLower) ||
813
- valueLower.includes(optVal) || optVal.includes(valueLower)) && opt.value) {
814
- matchedValue = opt.value; break;
815
- }
816
- }
817
- }
818
- if (!matchedValue) return false;
819
-
820
- // Native setter trick for React/Angular/Vue/Zoho
821
- const nativeSetter = Object.getOwnPropertyDescriptor(
822
- HTMLSelectElement.prototype, 'value'
823
- ).set;
824
- nativeSetter.call(el, matchedValue);
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
- // Auto-expand US state abbreviations
889
- const state = info.state.length === 2
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.fillBillingField(client, evalOpts, selectors, value, name);
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
- // Fill ORDER matters: input fields first, then select dropdowns last.
904
- // Reason: filling inputs can trigger framework re-renders (React, Zoho)
905
- // which reset previously selected dropdowns. Selects go last to survive.
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
- fieldConfigs.push({ selectors: exports.FULL_NAME_SELECTORS, value: fullName, name: "full_name" });
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
- // Detect tagName for each non-empty field
922
- const detections = [];
923
- for (const config of fieldConfigs) {
924
- if (!config.value) {
925
- skipped.push(config.name);
926
- continue;
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
- let tagName = null;
929
- try {
930
- const allSelector = config.selectors.join(", ");
931
- const { result } = await client.send("Runtime.evaluate", {
932
- expression: `
933
- (function() {
934
- const el = document.querySelector(${JSON.stringify(allSelector)});
935
- return el ? el.tagName.toLowerCase() : null;
936
- })()
937
- `,
938
- returnByValue: true,
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
- catch {
944
- // Detection failed — treat as input (non-select)
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
- const doFill = async (d) => {
949
- const ok = await this.fillBillingField(client, evalOpts, d.selectors, d.value, d.name);
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
- // Round 2: fill all select dropdowns (survive re-renders)
962
- for (const d of detections) {
963
- if (d.tagName === "select") {
964
- await doFill(d);
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
- // Phone: country code dropdown first, then number
968
- let ccFilled = false;
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
- // Internal: load billing info from env vars
982
- // ------------------------------------------------------------------
983
- loadBillingInfo() {
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