pop-pay 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,693 +277,445 @@ function verifyDomainToctou(pageUrl, approvedVendor) {
288
277
  return null;
289
278
  }
290
279
  // ---------------------------------------------------------------------------
291
- // CDP connection helper using raw WebSocket
292
- // ---------------------------------------------------------------------------
293
- async function fetchJSON(url) {
294
- const resp = await fetch(url, { signal: AbortSignal.timeout(5000) });
295
- return resp.json();
296
- }
297
- // Minimal WebSocket-based CDP client
298
- class CDPClient {
299
- ws;
300
- msgId = 0;
301
- pending = new Map();
302
- eventHandlers = new Map();
303
- static async connect(wsUrl) {
304
- const client = new CDPClient();
305
- const { WebSocket } = await import("ws").catch(() => {
306
- // Fallback: use global WebSocket if available (Node 21+)
307
- return { WebSocket: globalThis.WebSocket };
308
- });
309
- return new Promise((resolve, reject) => {
310
- const ws = new WebSocket(wsUrl);
311
- client.ws = ws;
312
- ws.onopen = () => resolve(client);
313
- ws.onerror = (e) => reject(new Error(`CDP WebSocket error: ${e.message ?? e}`));
314
- ws.onmessage = (event) => {
315
- const data = typeof event.data === "string" ? event.data : event.data.toString();
316
- const msg = JSON.parse(data);
317
- if (msg.id !== undefined) {
318
- const p = client.pending.get(msg.id);
319
- if (p) {
320
- client.pending.delete(msg.id);
321
- if (msg.error)
322
- p.reject(new Error(msg.error.message));
323
- else
324
- p.resolve(msg.result);
325
- }
326
- }
327
- else if (msg.method) {
328
- const handlers = client.eventHandlers.get(msg.method);
329
- if (handlers)
330
- handlers.forEach((h) => h(msg.params));
331
- }
332
- };
333
- ws.onclose = () => {
334
- for (const p of client.pending.values()) {
335
- p.reject(new Error("CDP connection closed"));
336
- }
337
- client.pending.clear();
338
- };
339
- });
340
- }
341
- async send(method, params = {}) {
342
- const id = ++this.msgId;
343
- return new Promise((resolve, reject) => {
344
- this.pending.set(id, { resolve, reject });
345
- const msg = JSON.stringify({ id, method, params });
346
- this.ws.send(msg);
347
- });
348
- }
349
- on(event, handler) {
350
- const handlers = this.eventHandlers.get(event) ?? [];
351
- handlers.push(handler);
352
- this.eventHandlers.set(event, handlers);
353
- }
354
- close() {
355
- try {
356
- this.ws.close();
357
- }
358
- catch { }
359
- }
360
- }
361
- // ---------------------------------------------------------------------------
362
280
  // PopBrowserInjector
363
281
  // ---------------------------------------------------------------------------
364
282
  class PopBrowserInjector {
365
283
  cdpUrl;
366
- 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 {
530
- return null;
531
- }
532
- const pageTargets = targets.filter((t) => t.type === "page");
533
- if (pageTargets.length === 0)
443
+ findBestPage(browser) {
444
+ const CHECKOUT_KEYWORDS = ["checkout", "payment", "donate", "pay", "purchase", "order", "gateway", "cart"];
445
+ const allPages = browser.contexts().flatMap((ctx) => ctx.pages());
446
+ if (allPages.length === 0)
534
447
  return null;
535
- const 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;
448
+ for (const page of allPages) {
449
+ const url = page.url().toLowerCase();
450
+ if (CHECKOUT_KEYWORDS.some((kw) => url.includes(kw))) {
451
+ return page;
543
452
  }
544
453
  }
545
- // 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;
550
- }
551
- }
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) {
456
+ async fillAcrossFrames(page, cardNumber, expiry, cvv) {
457
+ const allFrames = page.frames();
559
458
  let cardFilled = false;
560
- // Get frame tree
561
- const { frameTree } = await client.send("Page.getFrameTree");
562
- // Process main frame
563
- const mainFilled = await this.fillCardInContext(client, undefined, cardNumber, expiry, cvv);
564
- if (mainFilled)
565
- cardFilled = true;
566
- // Process child frames (iframes)
567
- const processFrame = async (tree) => {
568
- if (tree.childFrames) {
569
- for (const child of tree.childFrames) {
570
- try {
571
- // Create isolated world for cross-origin iframe access
572
- const { executionContextId } = await client.send("Page.createIsolatedWorld", { frameId: child.frame.id, worldName: "pop-pay-injector" });
573
- const filled = await this.fillCardInContext(client, executionContextId, cardNumber, expiry, cvv);
574
- if (filled)
575
- cardFilled = true;
576
- }
577
- catch {
578
- // Cross-origin frame access may fail — continue
579
- }
580
- await processFrame(child);
459
+ for (const frame of allFrames) {
460
+ try {
461
+ if (await this.fillInFrame(frame, cardNumber, expiry, cvv)) {
462
+ cardFilled = true;
463
+ // Keep going for expiry/CVV in sibling iframes (Stripe)
581
464
  }
582
465
  }
583
- };
584
- await processFrame(frameTree);
585
- // Shadow DOM piercing: search for shadow roots in main frame
586
- const shadowFilled = await this.fillCardInShadowDom(client, cardNumber, expiry, cvv);
587
- if (shadowFilled)
588
- cardFilled = true;
466
+ catch { }
467
+ }
468
+ // Shadow DOM piercing fallback
469
+ if (!cardFilled) {
470
+ cardFilled = await this.fillCardInShadowDom(page, cardNumber, expiry, cvv);
471
+ }
589
472
  return cardFilled;
590
473
  }
591
- // ------------------------------------------------------------------
592
- // Internal: fill billing fields across all frames (iframes)
593
- // ------------------------------------------------------------------
594
- async fillBillingAcrossFrames(client, info) {
595
- const result = {
596
- filled: [],
597
- failed: [],
598
- skipped: [],
599
- };
600
- const merge = (frameResult) => {
601
- result.filled.push(...frameResult.filled);
602
- result.failed.push(...frameResult.failed);
603
- result.skipped.push(...frameResult.skipped);
604
- };
605
- // Try main frame first
606
- merge(await this.fillBillingFields(client, info, {}));
607
- // Get frame tree
608
- const { frameTree } = await client.send("Page.getFrameTree");
609
- // Process child frames (iframes)
610
- const processFrame = async (tree) => {
611
- if (tree.childFrames) {
612
- for (const child of tree.childFrames) {
613
- try {
614
- // Create isolated world for cross-origin iframe access
615
- const { executionContextId } = await client.send("Page.createIsolatedWorld", { frameId: child.frame.id, worldName: "pop-pay-billing-injector" });
616
- merge(await this.fillBillingFields(client, info, { contextId: executionContextId }));
617
- }
618
- catch {
619
- // Cross-origin frame access may fail — continue
620
- }
621
- await processFrame(child);
622
- }
623
- }
624
- };
625
- await processFrame(frameTree);
626
- // Deduplicate: if a field was filled in any frame, remove from failed/skipped
627
- result.filled = [...new Set(result.filled)];
628
- result.failed = result.failed.filter((f) => !result.filled.some((filled) => f.startsWith(filled)));
629
- result.skipped = result.skipped.filter((s) => !result.filled.includes(s));
630
- return result;
631
- }
632
- // ------------------------------------------------------------------
633
- // Internal: fill card fields in a single execution context
634
- // ------------------------------------------------------------------
635
- async fillCardInContext(client, contextId, cardNumber, expiry, cvv) {
636
- const evalOpts = contextId !== undefined
637
- ? { contextId }
638
- : {};
639
- // Try to fill card number
640
- const cardSelector = exports.CARD_NUMBER_SELECTORS.join(", ");
641
- const cardFilled = await this.fillInputViaEval(client, evalOpts, cardSelector, cardNumber);
642
- if (!cardFilled)
474
+ async fillInFrame(frame, cardNumber, expiry, cvv) {
475
+ const cardLocator = await this.findVisibleLocator(frame, exports.CARD_NUMBER_SELECTORS);
476
+ if (!cardLocator)
643
477
  return false;
644
- // Fill expiry
645
- const expirySelector = exports.EXPIRY_SELECTORS.join(", ");
646
- await this.fillInputViaEval(client, evalOpts, expirySelector, expiry);
647
- // Fill CVV
648
- const cvvSelector = exports.CVV_SELECTORS.join(", ");
649
- await this.fillInputViaEval(client, evalOpts, cvvSelector, cvv);
478
+ await cardLocator.fill(cardNumber);
479
+ const expiryLocator = await this.findVisibleLocator(frame, exports.EXPIRY_SELECTORS);
480
+ if (expiryLocator)
481
+ await expiryLocator.fill(expiry);
482
+ const cvvLocator = await this.findVisibleLocator(frame, exports.CVV_SELECTORS);
483
+ if (cvvLocator)
484
+ await cvvLocator.fill(cvv);
650
485
  return true;
651
486
  }
652
- // ------------------------------------------------------------------
653
- // Internal: Shadow DOM piercing support (new feature!)
654
- // ------------------------------------------------------------------
655
- async fillCardInShadowDom(client, cardNumber, expiry, cvv) {
656
- try {
657
- const { result } = await client.send("Runtime.evaluate", {
658
- expression: `
659
- (function() {
660
- function queryShadowAll(root, selectors) {
661
- const results = [];
662
- const selectorList = selectors.split(', ');
663
- for (const sel of selectorList) {
664
- const found = root.querySelector(sel);
665
- if (found) results.push(found);
666
- }
667
- // Recurse into shadow roots
668
- const allElements = root.querySelectorAll('*');
669
- for (const el of allElements) {
670
- if (el.shadowRoot) {
671
- results.push(...queryShadowAll(el.shadowRoot, selectors));
672
- }
673
- }
674
- return results;
675
- }
676
-
677
- const cardSelectors = ${JSON.stringify(exports.CARD_NUMBER_SELECTORS.join(", "))};
678
- const cardFields = queryShadowAll(document, cardSelectors);
679
- return cardFields.length > 0;
680
- })()
681
- `,
682
- returnByValue: true,
683
- });
684
- if (!result?.value)
685
- return false;
686
- // Found shadow DOM card fields — fill them
687
- const { result: fillResult } = await client.send("Runtime.evaluate", {
688
- expression: `
689
- (function() {
690
- function queryShadowFirst(root, selectors) {
691
- const selectorList = selectors.split(', ');
692
- for (const sel of selectorList) {
693
- const found = root.querySelector(sel);
694
- if (found) return found;
695
- }
696
- const allElements = root.querySelectorAll('*');
697
- for (const el of allElements) {
698
- if (el.shadowRoot) {
699
- const found = queryShadowFirst(el.shadowRoot, selectors);
700
- if (found) return found;
487
+ async findVisibleLocator(frame, selectors) {
488
+ for (const selector of selectors) {
489
+ try {
490
+ const locator = frame.locator(selector).first();
491
+ if (await locator.count() > 0) {
492
+ return locator;
701
493
  }
702
- }
703
- return null;
704
- }
705
-
706
- function fillField(root, selectors, value) {
707
- const el = queryShadowFirst(root, selectors);
708
- if (!el) return false;
709
- const nativeSetter = Object.getOwnPropertyDescriptor(
710
- HTMLInputElement.prototype, 'value'
711
- ).set;
712
- nativeSetter.call(el, value);
713
- el.dispatchEvent(new Event('input', { bubbles: true }));
714
- el.dispatchEvent(new Event('change', { bubbles: true }));
715
- el.dispatchEvent(new Event('blur', { bubbles: true }));
716
- return true;
717
- }
718
-
719
- const cardFilled = fillField(
720
- document,
721
- ${JSON.stringify(exports.CARD_NUMBER_SELECTORS.join(", "))},
722
- ${JSON.stringify(cardNumber)}
723
- );
724
- if (cardFilled) {
725
- fillField(document, ${JSON.stringify(exports.EXPIRY_SELECTORS.join(", "))}, ${JSON.stringify(expiry)});
726
- fillField(document, ${JSON.stringify(exports.CVV_SELECTORS.join(", "))}, ${JSON.stringify(cvv)});
727
494
  }
728
- return cardFilled;
729
- })()
730
- `,
731
- returnByValue: true,
732
- });
733
- return fillResult?.value === true;
734
- }
735
- catch {
736
- return false;
495
+ catch { }
737
496
  }
497
+ return null;
738
498
  }
739
- // ------------------------------------------------------------------
740
- // Internal: fill a single input field via Runtime.evaluate
741
- // ------------------------------------------------------------------
742
- async fillInputViaEval(client, evalOpts, selector, value) {
499
+ async fillCardInShadowDom(page, cardNumber, expiry, cvv) {
743
500
  try {
744
- const { result } = await client.send("Runtime.evaluate", {
745
- expression: `
746
- (function() {
747
- const el = document.querySelector(${JSON.stringify(selector)});
748
- if (!el) return false;
749
- // Use native setter to bypass framework interception
750
- const proto = el.tagName === 'SELECT'
751
- ? HTMLSelectElement.prototype
752
- : HTMLInputElement.prototype;
753
- const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
754
- if (nativeSetter) {
755
- nativeSetter.call(el, ${JSON.stringify(value)});
756
- } else {
757
- el.value = ${JSON.stringify(value)};
501
+ const script = `
502
+ ([cardNumber, expiry, cvv, cardSels, expSels, cvvSels]) => {
503
+ function queryShadowFirst(root, selectors) {
504
+ const selectorList = selectors.split(', ');
505
+ for (const sel of selectorList) {
506
+ const found = root.querySelector(sel);
507
+ if (found) return found;
758
508
  }
759
- el.dispatchEvent(new Event('input', { bubbles: true }));
760
- el.dispatchEvent(new Event('change', { bubbles: true }));
761
- el.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
762
- return true;
763
- })()
764
- `,
765
- returnByValue: true,
766
- ...evalOpts,
767
- });
768
- return result?.value === true;
769
- }
770
- catch {
771
- return false;
772
- }
773
- }
774
- // ------------------------------------------------------------------
775
- // Internal: select an option from a <select> dropdown
776
- // ------------------------------------------------------------------
777
- async selectOption(client, evalOpts, selector, value) {
778
- try {
779
- const { result } = await client.send("Runtime.evaluate", {
780
- expression: `
781
- (function() {
782
- const el = document.querySelector(${JSON.stringify(selector)});
783
- if (!el || el.tagName !== 'SELECT') return false;
784
-
785
- const options = Array.from(el.options).map(o => ({
786
- value: o.value, text: o.text.trim()
787
- }));
788
- const valueLower = ${JSON.stringify(value.toLowerCase())};
789
-
790
- let matchedValue = null;
791
- // Exact value match
792
- for (const opt of options) {
793
- if (opt.value.toLowerCase() === valueLower) { matchedValue = opt.value; break; }
794
- }
795
- // Exact text match
796
- if (!matchedValue) {
797
- for (const opt of options) {
798
- if (opt.text.toLowerCase() === valueLower) { matchedValue = opt.value; break; }
799
- }
800
- }
801
- // Partial match
802
- if (!matchedValue) {
803
- for (const opt of options) {
804
- const optText = opt.text.toLowerCase();
805
- const optVal = opt.value.toLowerCase();
806
- if ((valueLower.includes(optText) || optText.includes(valueLower) ||
807
- valueLower.includes(optVal) || optVal.includes(valueLower)) && opt.value) {
808
- matchedValue = opt.value; break;
809
- }
509
+ const allElements = root.querySelectorAll('*');
510
+ for (const el of allElements) {
511
+ if (el.shadowRoot) {
512
+ const found = queryShadowFirst(el.shadowRoot, selectors);
513
+ if (found) return found;
810
514
  }
811
515
  }
812
- if (!matchedValue) return false;
516
+ return null;
517
+ }
813
518
 
814
- // Native setter trick for React/Angular/Vue/Zoho
519
+ function fillField(root, selectors, value) {
520
+ const el = queryShadowFirst(root, selectors);
521
+ if (!el) return false;
815
522
  const nativeSetter = Object.getOwnPropertyDescriptor(
816
- HTMLSelectElement.prototype, 'value'
523
+ HTMLInputElement.prototype, 'value'
817
524
  ).set;
818
- nativeSetter.call(el, matchedValue);
819
-
820
- el.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
821
- el.dispatchEvent(new FocusEvent('focus', { bubbles: false }));
822
- el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
823
- el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
824
- el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
525
+ if (nativeSetter) {
526
+ nativeSetter.call(el, value);
527
+ } else {
528
+ el.value = value;
529
+ }
825
530
  el.dispatchEvent(new Event('input', { bubbles: true }));
826
531
  el.dispatchEvent(new Event('change', { bubbles: true }));
827
- el.dispatchEvent(new FocusEvent('blur', { bubbles: false }));
828
- el.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
532
+ el.dispatchEvent(new Event('blur', { bubbles: true }));
533
+ return true;
534
+ }
829
535
 
830
- return el.value === matchedValue;
831
- })()
832
- `,
833
- returnByValue: true,
834
- ...evalOpts,
835
- });
836
- return result?.value === true;
536
+ const cardFilled = fillField(document, cardSels, cardNumber);
537
+ if (cardFilled) {
538
+ fillField(document, expSels, expiry);
539
+ fillField(document, cvvSels, cvv);
540
+ }
541
+ return cardFilled;
542
+ }
543
+ `;
544
+ const result = await page.evaluate(script, [
545
+ cardNumber, expiry, cvv,
546
+ exports.CARD_NUMBER_SELECTORS.join(", "),
547
+ exports.EXPIRY_SELECTORS.join(", "),
548
+ exports.CVV_SELECTORS.join(", ")
549
+ ]);
550
+ return !!result;
837
551
  }
838
552
  catch {
839
553
  return false;
840
554
  }
841
555
  }
842
- // ------------------------------------------------------------------
843
- // Internal: fill a billing field (input or select)
844
- // ------------------------------------------------------------------
845
- async fillBillingField(client, evalOpts, selectors, value, fieldName) {
846
- if (!value)
847
- return false;
848
- // Detect if first matching element is a <select> or <input>
849
- const allSelector = selectors.join(", ");
850
- try {
851
- const { result } = await client.send("Runtime.evaluate", {
852
- expression: `
853
- (function() {
854
- const el = document.querySelector(${JSON.stringify(allSelector)});
855
- if (!el) return null;
856
- return el.tagName.toLowerCase();
857
- })()
858
- `,
859
- returnByValue: true,
860
- ...evalOpts,
861
- });
862
- if (!result?.value)
863
- return false;
864
- if (result.value === "select") {
865
- return await this.selectOption(client, evalOpts, allSelector, value);
866
- }
867
- else {
868
- return await this.fillInputViaEval(client, evalOpts, allSelector, value);
869
- }
870
- }
871
- catch {
872
- return false;
873
- }
874
- }
875
- // ------------------------------------------------------------------
876
- // Internal: fill all billing fields
877
- // ------------------------------------------------------------------
878
- async fillBillingFields(client, info, evalOpts) {
556
+ async fillBillingFields(page, info) {
879
557
  const filled = [];
880
558
  const failed = [];
881
559
  const skipped = [];
882
- // Auto-expand US state abbreviations
883
- const state = info.state.length === 2
884
- ? US_STATE_CODES[info.state.toUpperCase()] ?? info.state
885
- : info.state;
886
- const tryFill = async (selectors, value, name) => {
560
+ const state = info.state.length === 2 ? US_STATE_CODES[info.state.toUpperCase()] ?? info.state : info.state;
561
+ const tryFill = async (selectors, value, name, label) => {
887
562
  if (!value) {
888
563
  skipped.push(name);
889
564
  return;
890
565
  }
891
- const ok = await this.fillBillingField(client, evalOpts, selectors, value, name);
566
+ const ok = await this.fillField(page, selectors, value, name, label);
892
567
  if (ok)
893
568
  filled.push(name);
894
569
  else
895
570
  failed.push(`${name} (value='${value}')`);
896
571
  };
897
- // Fill ORDER matters: input fields first, then select dropdowns last.
898
- // Reason: filling inputs can trigger framework re-renders (React, Zoho)
899
- // which reset previously selected dropdowns. Selects go last to survive.
900
- const fieldConfigs = [
901
- { selectors: exports.FIRST_NAME_SELECTORS, value: info.firstName, name: "first_name" },
902
- { selectors: exports.LAST_NAME_SELECTORS, value: info.lastName, name: "last_name" },
903
- { selectors: exports.STREET_SELECTORS, value: info.street, name: "street" },
904
- { selectors: exports.CITY_SELECTORS, value: info.city, name: "city" },
905
- { selectors: exports.STATE_SELECTORS, value: state, name: "state" },
906
- { selectors: exports.COUNTRY_SELECTORS, value: info.country, name: "country" },
907
- { selectors: exports.ZIP_SELECTORS, value: info.zip, name: "zip" },
908
- { selectors: exports.EMAIL_SELECTORS, value: info.email, name: "email" },
909
- ];
910
- // Full name fallback
572
+ // Input fields first
573
+ await tryFill(exports.FIRST_NAME_SELECTORS, info.firstName, "first_name", "First name");
574
+ await tryFill(exports.LAST_NAME_SELECTORS, info.lastName, "last_name", "Last name");
911
575
  if (info.firstName || info.lastName) {
912
576
  const fullName = [info.firstName, info.lastName].filter(Boolean).join(" ");
913
- 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");
914
592
  }
915
- // Detect tagName for each non-empty field
916
- const detections = [];
917
- for (const config of fieldConfigs) {
918
- if (!config.value) {
919
- skipped.push(config.name);
920
- 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
+ }
921
611
  }
922
- let tagName = null;
923
- try {
924
- const allSelector = config.selectors.join(", ");
925
- const { result } = await client.send("Runtime.evaluate", {
926
- expression: `
927
- (function() {
928
- const el = document.querySelector(${JSON.stringify(allSelector)});
929
- return el ? el.tagName.toLowerCase() : null;
930
- })()
931
- `,
932
- returnByValue: true,
933
- ...evalOpts,
934
- });
935
- tagName = result?.value || null;
612
+ }
613
+ catch { }
614
+ // Strategy 2: CSS selectors
615
+ const frame = page.mainFrame();
616
+ const locator = await this.findVisibleLocator(frame, selectors);
617
+ if (!locator)
618
+ return false;
619
+ try {
620
+ const tag = await locator.evaluate("el => el.tagName.toLowerCase()");
621
+ if (tag === "select") {
622
+ return await this.selectOption(locator, value);
936
623
  }
937
- catch {
938
- // Detection failed — treat as input (non-select)
624
+ else {
625
+ await locator.fill(value);
626
+ await this.dispatchEvents(locator);
627
+ return true;
939
628
  }
940
- detections.push({ ...config, tagName });
941
629
  }
942
- const doFill = async (d) => {
943
- const ok = await this.fillBillingField(client, evalOpts, d.selectors, d.value, d.name);
944
- if (ok)
945
- filled.push(d.name);
946
- else
947
- failed.push(`${d.name} (value='${d.value}')`);
948
- };
949
- // Round 1: fill all non-select fields (inputs, textareas, etc.)
950
- for (const d of detections) {
951
- if (d.tagName !== "select") {
952
- await doFill(d);
953
- }
630
+ catch {
631
+ return false;
954
632
  }
955
- // Round 2: fill all select dropdowns (survive re-renders)
956
- for (const d of detections) {
957
- if (d.tagName === "select") {
958
- 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
+ }
959
654
  }
655
+ if (!matchedValue) {
656
+ for (const opt of options) {
657
+ if ((valueLower.includes(opt.text.toLowerCase()) || opt.text.toLowerCase().includes(valueLower)) && opt.value) {
658
+ matchedValue = opt.value;
659
+ break;
660
+ }
661
+ }
662
+ }
663
+ if (!matchedValue)
664
+ return false;
665
+ await locator.selectOption(matchedValue);
666
+ const actual = await locator.evaluate((el) => el.value);
667
+ if (actual === matchedValue) {
668
+ await this.dispatchEvents(locator);
669
+ return true;
670
+ }
671
+ return await locator.evaluate(`(el, val) => {
672
+ const nativeSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value")?.set;
673
+ if (!nativeSetter) return false;
674
+ nativeSetter.call(el, val);
675
+ const events = ["focusin", "focus", "mousedown", "mouseup", "click", "input", "change", "blur", "focusout"];
676
+ events.forEach((evt) => el.dispatchEvent(new Event(evt, { bubbles: true })));
677
+ return el.value === val;
678
+ }`, matchedValue);
960
679
  }
961
- // Phone: country code dropdown first, then number
962
- let ccFilled = false;
963
- if (info.phoneCountryCode) {
964
- ccFilled = await this.fillBillingField(client, evalOpts, exports.PHONE_COUNTRY_CODE_SELECTORS, info.phoneCountryCode, "phone_country_code");
965
- if (ccFilled)
966
- filled.push("phone_country_code");
680
+ catch {
681
+ return false;
967
682
  }
968
- const phoneValue = ccFilled
969
- ? nationalNumber(info.phone, info.phoneCountryCode)
970
- : info.phone;
971
- await tryFill(exports.PHONE_SELECTORS, phoneValue, "phone");
972
- return { filled, failed, skipped };
973
683
  }
974
- // ------------------------------------------------------------------
975
- // Internal: load billing info from env vars
976
- // ------------------------------------------------------------------
977
- 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() {
978
719
  return {
979
720
  firstName: (process.env.POP_BILLING_FIRST_NAME ?? "").trim(),
980
721
  lastName: (process.env.POP_BILLING_LAST_NAME ?? "").trim(),
@@ -988,61 +729,6 @@ class PopBrowserInjector {
988
729
  phoneCountryCode: (process.env.POP_BILLING_PHONE_COUNTRY_CODE ?? "").trim(),
989
730
  };
990
731
  }
991
- // ------------------------------------------------------------------
992
- // Internal: blackout mode (mask card fields)
993
- // ------------------------------------------------------------------
994
- async enableBlackout(client) {
995
- try {
996
- await client.send("Runtime.evaluate", {
997
- expression: `
998
- (function() {
999
- // Inject into main frame
1000
- function addBlackout(doc) {
1001
- if (doc.getElementById('pop-pay-blackout')) return;
1002
- const style = doc.createElement('style');
1003
- style.id = 'pop-pay-blackout';
1004
- style.textContent = \`
1005
- input[autocomplete*="cc-"],
1006
- input[name*="card"], input[name*="Card"],
1007
- input[name*="expir"], input[name*="cvc"], input[name*="cvv"],
1008
- input[data-elements-stable-field-name],
1009
- input.__PrivateStripeElement,
1010
- input[name="cardnumber"], input[name="cc-exp"],
1011
- input[name="security_code"], input[name="card_number"],
1012
- input[name="card_expiry"], input[name="card_cvc"] {
1013
- -webkit-text-security: disc !important;
1014
- color: transparent !important;
1015
- text-shadow: 0 0 8px rgba(0,0,0,0.5) !important;
1016
- }
1017
- \`;
1018
- doc.head.appendChild(style);
1019
- }
1020
- addBlackout(document);
1021
-
1022
- // Try iframes (same-origin only)
1023
- try {
1024
- const iframes = document.querySelectorAll('iframe');
1025
- for (const iframe of iframes) {
1026
- try {
1027
- if (iframe.contentDocument) {
1028
- addBlackout(iframe.contentDocument);
1029
- }
1030
- } catch {}
1031
- }
1032
- } catch {}
1033
- })()
1034
- `,
1035
- });
1036
- }
1037
- catch { }
1038
- }
1039
- // ------------------------------------------------------------------
1040
- // Masked card display helper
1041
- // ------------------------------------------------------------------
1042
- static maskedCard(cardNumber) {
1043
- const last4 = cardNumber.slice(-4);
1044
- return `****-****-****-${last4}`;
1045
- }
1046
732
  }
1047
733
  exports.PopBrowserInjector = PopBrowserInjector;
1048
734
  //# sourceMappingURL=injector.js.map