pop-pay 0.1.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/cli-vault.d.ts +7 -0
  4. package/dist/cli-vault.d.ts.map +1 -0
  5. package/dist/cli-vault.js +233 -0
  6. package/dist/cli-vault.js.map +1 -0
  7. package/dist/cli.d.ts +6 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +159 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/client.d.ts +18 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +68 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/core/models.d.ts +51 -0
  16. package/dist/core/models.d.ts.map +1 -0
  17. package/dist/core/models.js +19 -0
  18. package/dist/core/models.js.map +1 -0
  19. package/dist/core/state.d.ts +15 -0
  20. package/dist/core/state.d.ts.map +1 -0
  21. package/dist/core/state.js +84 -0
  22. package/dist/core/state.js.map +1 -0
  23. package/dist/engine/guardrails.d.ts +6 -0
  24. package/dist/engine/guardrails.d.ts.map +1 -0
  25. package/dist/engine/guardrails.js +128 -0
  26. package/dist/engine/guardrails.js.map +1 -0
  27. package/dist/engine/injector.d.ts +87 -0
  28. package/dist/engine/injector.d.ts.map +1 -0
  29. package/dist/engine/injector.js +955 -0
  30. package/dist/engine/injector.js.map +1 -0
  31. package/dist/engine/known-processors.d.ts +11 -0
  32. package/dist/engine/known-processors.d.ts.map +1 -0
  33. package/dist/engine/known-processors.js +47 -0
  34. package/dist/engine/known-processors.js.map +1 -0
  35. package/dist/engine/llm-guardrails.d.ts +20 -0
  36. package/dist/engine/llm-guardrails.d.ts.map +1 -0
  37. package/dist/engine/llm-guardrails.js +89 -0
  38. package/dist/engine/llm-guardrails.js.map +1 -0
  39. package/dist/index.d.ts +15 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +41 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/mcp-server.d.ts +7 -0
  44. package/dist/mcp-server.d.ts.map +1 -0
  45. package/dist/mcp-server.js +334 -0
  46. package/dist/mcp-server.js.map +1 -0
  47. package/dist/providers/base.d.ts +5 -0
  48. package/dist/providers/base.d.ts.map +1 -0
  49. package/dist/providers/base.js +3 -0
  50. package/dist/providers/base.js.map +1 -0
  51. package/dist/providers/byoc-local.d.ts +12 -0
  52. package/dist/providers/byoc-local.d.ts.map +1 -0
  53. package/dist/providers/byoc-local.js +56 -0
  54. package/dist/providers/byoc-local.js.map +1 -0
  55. package/dist/providers/stripe-mock.d.ts +6 -0
  56. package/dist/providers/stripe-mock.d.ts.map +1 -0
  57. package/dist/providers/stripe-mock.js +34 -0
  58. package/dist/providers/stripe-mock.js.map +1 -0
  59. package/dist/providers/stripe-real.d.ts +9 -0
  60. package/dist/providers/stripe-real.d.ts.map +1 -0
  61. package/dist/providers/stripe-real.js +84 -0
  62. package/dist/providers/stripe-real.js.map +1 -0
  63. package/dist/vault.d.ts +23 -0
  64. package/dist/vault.d.ts.map +1 -0
  65. package/dist/vault.js +283 -0
  66. package/dist/vault.js.map +1 -0
  67. package/package.json +71 -0
@@ -0,0 +1,955 @@
1
+ "use strict";
2
+ /**
3
+ * PopBrowserInjector: CDP-based browser injector with iframe + Shadow DOM traversal.
4
+ *
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.
9
+ *
10
+ * New in TS port: Shadow DOM piercing support.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ 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
+ exports.ssrfValidateUrl = ssrfValidateUrl;
15
+ exports.verifyDomainToctou = verifyDomainToctou;
16
+ const known_processors_js_1 = require("./known-processors.js");
17
+ // ---------------------------------------------------------------------------
18
+ // ISO 3166-1 alpha-2 -> E.164 dial prefix
19
+ // ---------------------------------------------------------------------------
20
+ const COUNTRY_DIAL_CODES = {
21
+ US: "+1", CA: "+1", GB: "+44", AU: "+61", DE: "+49",
22
+ FR: "+33", JP: "+81", CN: "+86", IN: "+91", BR: "+55",
23
+ TW: "+886", HK: "+852", SG: "+65", KR: "+82", MX: "+52",
24
+ NL: "+31", SE: "+46", NO: "+47", DK: "+45", FI: "+358",
25
+ CH: "+41", AT: "+43", BE: "+32", IT: "+39", ES: "+34",
26
+ PT: "+351", PL: "+48", RU: "+7", UA: "+380", NZ: "+64",
27
+ ZA: "+27", NG: "+234", EG: "+20", IL: "+972", AE: "+971",
28
+ SA: "+966", TR: "+90", AR: "+54", CO: "+57", CL: "+56",
29
+ TH: "+66", VN: "+84", ID: "+62", MY: "+60", PH: "+63",
30
+ };
31
+ function nationalNumber(phoneE164, countryCode) {
32
+ if (!phoneE164.startsWith("+"))
33
+ return phoneE164;
34
+ const cc = countryCode.trim();
35
+ let dial;
36
+ if (!cc.startsWith("+")) {
37
+ dial = COUNTRY_DIAL_CODES[cc.toUpperCase()] ?? `+${cc}`;
38
+ }
39
+ else {
40
+ dial = cc;
41
+ }
42
+ if (phoneE164.startsWith(dial))
43
+ return phoneE164.slice(dial.length);
44
+ return phoneE164;
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // US state abbreviation -> full name (for dropdowns that use full names)
48
+ // ---------------------------------------------------------------------------
49
+ const US_STATE_CODES = {
50
+ AL: "Alabama", AK: "Alaska", AZ: "Arizona", AR: "Arkansas",
51
+ CA: "California", CO: "Colorado", CT: "Connecticut", DE: "Delaware",
52
+ DC: "District of Columbia", FL: "Florida", GA: "Georgia", HI: "Hawaii",
53
+ ID: "Idaho", IL: "Illinois", IN: "Indiana", IA: "Iowa",
54
+ KS: "Kansas", KY: "Kentucky", LA: "Louisiana", ME: "Maine",
55
+ MD: "Maryland", MA: "Massachusetts", MI: "Michigan", MN: "Minnesota",
56
+ MS: "Mississippi", MO: "Missouri", MT: "Montana", NE: "Nebraska",
57
+ NV: "Nevada", NH: "New Hampshire", NJ: "New Jersey", NM: "New Mexico",
58
+ NY: "New York", NC: "North Carolina", ND: "North Dakota", OH: "Ohio",
59
+ OK: "Oklahoma", OR: "Oregon", PA: "Pennsylvania", RI: "Rhode Island",
60
+ SC: "South Carolina", SD: "South Dakota", TN: "Tennessee", TX: "Texas",
61
+ UT: "Utah", VT: "Vermont", VA: "Virginia", WA: "Washington",
62
+ WV: "West Virginia", WI: "Wisconsin", WY: "Wyoming",
63
+ };
64
+ // ---------------------------------------------------------------------------
65
+ // CSS selectors for credit card fields across major payment providers
66
+ // ---------------------------------------------------------------------------
67
+ exports.CARD_NUMBER_SELECTORS = [
68
+ "input[autocomplete='cc-number']",
69
+ "input[name='cardnumber']",
70
+ "input[name='card_number']",
71
+ "input[name='card-number']",
72
+ "input[id*='card'][id*='number']",
73
+ "input[placeholder*='Card number']",
74
+ "input[placeholder*='card number']",
75
+ "input[data-elements-stable-field-name='cardNumber']", // Stripe Elements
76
+ "input.__PrivateStripeElement", // Stripe v2
77
+ ];
78
+ exports.EXPIRY_SELECTORS = [
79
+ "input[autocomplete='cc-exp']",
80
+ "input[name='cc-exp']",
81
+ "input[name='expiry']",
82
+ "input[name='card_expiry']",
83
+ "input[placeholder*='MM / YY']",
84
+ "input[placeholder*='MM/YY']",
85
+ "input[placeholder*='Expiry']",
86
+ "input[data-elements-stable-field-name='cardExpiry']", // Stripe Elements
87
+ ];
88
+ exports.CVV_SELECTORS = [
89
+ "input[autocomplete='cc-csc']",
90
+ "input[name='cvc']",
91
+ "input[name='cvv']",
92
+ "input[name='security_code']",
93
+ "input[name='card_cvc']",
94
+ "input[placeholder*='CVC']",
95
+ "input[placeholder*='CVV']",
96
+ "input[placeholder*='Security code']",
97
+ "input[data-elements-stable-field-name='cardCvc']", // Stripe Elements
98
+ ];
99
+ // ---------------------------------------------------------------------------
100
+ // CSS selectors for billing detail fields
101
+ // ---------------------------------------------------------------------------
102
+ exports.FIRST_NAME_SELECTORS = [
103
+ "input[autocomplete='given-name']",
104
+ "input[name='first_name']", "input[name='firstName']", "input[name='first-name']",
105
+ "input[id*='first'][id*='name']", "input[id='first_name']", "input[id='firstName']",
106
+ "input[placeholder*='First name']", "input[placeholder*='first name']",
107
+ "input[aria-label*='First name']", "input[aria-label*='first name']",
108
+ ];
109
+ exports.LAST_NAME_SELECTORS = [
110
+ "input[autocomplete='family-name']",
111
+ "input[name='last_name']", "input[name='lastName']", "input[name='last-name']",
112
+ "input[id*='last'][id*='name']", "input[id='last_name']", "input[id='lastName']",
113
+ "input[placeholder*='Last name']", "input[placeholder*='last name']",
114
+ "input[aria-label*='Last name']", "input[aria-label*='last name']",
115
+ ];
116
+ exports.FULL_NAME_SELECTORS = [
117
+ "input[autocomplete='name']",
118
+ "input[name='full_name']", "input[name='fullName']", "input[name='name']",
119
+ "input[id='full_name']", "input[id='fullName']",
120
+ "input[placeholder*='Full name']", "input[placeholder*='full name']",
121
+ "input[aria-label*='Full name']", "input[aria-label*='full name']",
122
+ ];
123
+ exports.STREET_SELECTORS = [
124
+ "input[autocomplete='street-address']", "input[autocomplete='address-line1']",
125
+ "input[name='address']", "input[name='address1']", "input[name='street']",
126
+ "input[name='street_address']", "input[name='billing_address']",
127
+ "input[id*='address']", "input[id*='street']",
128
+ "input[placeholder*='Street']", "input[placeholder*='street']",
129
+ "input[placeholder*='Address']", "input[placeholder*='address']",
130
+ "input[aria-label*='Street']", "input[aria-label*='street']",
131
+ ];
132
+ exports.ZIP_SELECTORS = [
133
+ "input[autocomplete='postal-code']",
134
+ "input[name='zip']", "input[name='postal_code']", "input[name='postcode']",
135
+ "input[name='zipcode']", "input[name='zip_code']",
136
+ "input[id*='zip']", "input[id*='postal']",
137
+ "input[placeholder*='Zip']", "input[placeholder*='zip']",
138
+ "input[placeholder*='Postal']", "input[placeholder*='postal']",
139
+ "input[aria-label*='Zip']", "input[aria-label*='zip']", "input[aria-label*='Postal']",
140
+ ];
141
+ exports.EMAIL_SELECTORS = [
142
+ "input[autocomplete='email']", "input[type='email']",
143
+ "input[name='email']", "input[name='email_address']",
144
+ "input[id='email']", "input[id*='email']",
145
+ "input[placeholder*='Email']", "input[placeholder*='email']",
146
+ "input[aria-label*='Email']", "input[aria-label*='email']",
147
+ ];
148
+ exports.PHONE_SELECTORS = [
149
+ "input[autocomplete='tel']", "input[type='tel']",
150
+ "input[name='phone']", "input[name='phone_number']", "input[name='phoneNumber']",
151
+ "input[name='telephone']", "input[name='mobile']",
152
+ "input[id*='phone']", "input[id*='tel']", "input[id*='mobile']",
153
+ "input[placeholder*='Phone']", "input[placeholder*='phone']",
154
+ "input[placeholder*='Mobile']",
155
+ "input[aria-label*='Phone']", "input[aria-label*='phone']",
156
+ ];
157
+ exports.PHONE_COUNTRY_CODE_SELECTORS = [
158
+ "select[autocomplete='tel-country-code']",
159
+ "select[name='phone_country_code']", "select[name='phoneCountryCode']",
160
+ "select[name='dialCode']", "select[name='dial_code']",
161
+ "select[name='country_code']", "select[name='countryCode']",
162
+ "select[id*='country_code']", "select[id*='dialCode']", "select[id*='dial_code']",
163
+ "select[aria-label*='Country code']", "select[aria-label*='country code']",
164
+ "select[aria-label*='Dial code']",
165
+ ];
166
+ exports.COUNTRY_SELECTORS = [
167
+ "select[autocomplete='country']", "select[autocomplete='country-name']",
168
+ "select[name='country']", "select[name='billing_country']", "select[name='billingCountry']",
169
+ "select[id='country']", "select[id*='country']",
170
+ "select[aria-label*='Country']", "select[aria-label*='country']",
171
+ "input[autocomplete='country']", "input[autocomplete='country-name']", "input[name='country']",
172
+ ];
173
+ exports.STATE_SELECTORS = [
174
+ "select[autocomplete='address-level1']",
175
+ "select[name='state']", "select[name='province']", "select[name='region']",
176
+ "select[name='billing_state']",
177
+ "select[id='state']", "select[id*='state']", "select[id*='province']",
178
+ "select[aria-label*='State']", "select[aria-label*='state']",
179
+ "select[aria-label*='Province']",
180
+ "input[autocomplete='address-level1']", "input[name='state']", "input[name='province']",
181
+ ];
182
+ exports.CITY_SELECTORS = [
183
+ "input[autocomplete='address-level2']",
184
+ "input[name='city']", "input[name='town']", "input[name='billing_city']",
185
+ "input[id='city']", "input[id*='city']",
186
+ "input[placeholder*='City']", "input[placeholder*='city']",
187
+ "input[aria-label*='City']",
188
+ "select[autocomplete='address-level2']", "select[name='city']",
189
+ ];
190
+ // ---------------------------------------------------------------------------
191
+ // Known vendor domains (shared with guardrails)
192
+ // ---------------------------------------------------------------------------
193
+ const KNOWN_VENDOR_DOMAINS = {
194
+ aws: ["amazonaws.com", "aws.amazon.com"],
195
+ amazon: ["amazon.com", "amazon.co.uk", "amazon.co.jp"],
196
+ github: ["github.com"],
197
+ cloudflare: ["cloudflare.com"],
198
+ openai: ["openai.com", "platform.openai.com"],
199
+ stripe: ["stripe.com", "dashboard.stripe.com"],
200
+ anthropic: ["anthropic.com", "claude.ai"],
201
+ google: ["google.com", "cloud.google.com", "console.cloud.google.com"],
202
+ microsoft: ["microsoft.com", "azure.microsoft.com", "portal.azure.com"],
203
+ wikipedia: ["wikipedia.org", "wikimedia.org", "donate.wikimedia.org"],
204
+ digitalocean: ["digitalocean.com", "cloud.digitalocean.com"],
205
+ heroku: ["heroku.com", "dashboard.heroku.com"],
206
+ vercel: ["vercel.com", "app.vercel.com"],
207
+ netlify: ["netlify.com", "app.netlify.com"],
208
+ };
209
+ // ---------------------------------------------------------------------------
210
+ // SSRF guard
211
+ // ---------------------------------------------------------------------------
212
+ function ssrfValidateUrl(url) {
213
+ try {
214
+ const parsed = new URL(url);
215
+ if (!["http:", "https:"].includes(parsed.protocol)) {
216
+ return "Only http/https URLs are allowed.";
217
+ }
218
+ // Block private/reserved IPs
219
+ const hostname = parsed.hostname;
220
+ if (hostname === "localhost" ||
221
+ hostname === "127.0.0.1" ||
222
+ hostname === "0.0.0.0" ||
223
+ hostname.startsWith("10.") ||
224
+ hostname.startsWith("192.168.") ||
225
+ /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
226
+ hostname === "[::1]" ||
227
+ hostname.endsWith(".local")) {
228
+ // Allow localhost only for CDP URLs (checked separately)
229
+ return "Private/reserved IP addresses are not allowed.";
230
+ }
231
+ }
232
+ catch {
233
+ return "Invalid URL.";
234
+ }
235
+ return null;
236
+ }
237
+ // ---------------------------------------------------------------------------
238
+ // TOCTOU domain verification (shared between payment + billing injection)
239
+ // ---------------------------------------------------------------------------
240
+ function verifyDomainToctou(pageUrl, approvedVendor) {
241
+ if (!pageUrl || !approvedVendor)
242
+ return null;
243
+ let actualDomain;
244
+ try {
245
+ actualDomain = new URL(pageUrl).hostname.toLowerCase().replace(/^www\./, "");
246
+ }
247
+ catch {
248
+ return "invalid_url";
249
+ }
250
+ const vendorLower = approvedVendor.toLowerCase();
251
+ const vendorTokens = new Set(vendorLower.split(/[\s\-_./]+/).filter(Boolean));
252
+ let domainOk = false;
253
+ let vendorIsKnown = false;
254
+ // Check against KNOWN_VENDOR_DOMAINS using strict suffix matching
255
+ for (const [knownVendor, knownDomains] of Object.entries(KNOWN_VENDOR_DOMAINS)) {
256
+ if (vendorTokens.has(knownVendor) || knownVendor === vendorLower) {
257
+ vendorIsKnown = true;
258
+ if (knownDomains.some((d) => actualDomain === d || actualDomain.endsWith("." + d))) {
259
+ domainOk = true;
260
+ }
261
+ break;
262
+ }
263
+ }
264
+ // Fallback for unknown vendors
265
+ if (!domainOk && !vendorIsKnown) {
266
+ const commonTlds = new Set(["com", "org", "net", "io", "co", "uk", "jp", "de", "fr"]);
267
+ const domainLabels = new Set(actualDomain.split(".").filter((l) => !commonTlds.has(l)));
268
+ domainOk =
269
+ [...vendorTokens].some((tok) => domainLabels.has(tok)) ||
270
+ [...vendorTokens].some((tok) => tok.length >= 4 &&
271
+ [...domainLabels].some((label) => label.includes(tok)));
272
+ }
273
+ // Payment processor passthrough
274
+ if (!domainOk) {
275
+ let userProcessors = [];
276
+ try {
277
+ userProcessors = JSON.parse(process.env.POP_ALLOWED_PAYMENT_PROCESSORS ?? "[]");
278
+ }
279
+ catch { }
280
+ const allProcessors = new Set([...known_processors_js_1.KNOWN_PAYMENT_PROCESSORS, ...userProcessors]);
281
+ if ([...allProcessors].some((p) => actualDomain === p || actualDomain.endsWith("." + p))) {
282
+ domainOk = true;
283
+ }
284
+ }
285
+ if (!domainOk) {
286
+ return `domain_mismatch:${actualDomain}`;
287
+ }
288
+ return null;
289
+ }
290
+ // ---------------------------------------------------------------------------
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
+ // PopBrowserInjector
363
+ // ---------------------------------------------------------------------------
364
+ class PopBrowserInjector {
365
+ cdpUrl;
366
+ headless;
367
+ constructor(cdpUrl = "http://localhost:9222", headless = false) {
368
+ this.cdpUrl = cdpUrl;
369
+ this.headless = headless;
370
+ }
371
+ // ------------------------------------------------------------------
372
+ // Public API: inject payment info (card + billing)
373
+ // ------------------------------------------------------------------
374
+ async injectPaymentInfo(opts) {
375
+ const result = {
376
+ cardFilled: false,
377
+ billingFilled: false,
378
+ blockedReason: "",
379
+ };
380
+ // TOCTOU guard
381
+ const blocked = verifyDomainToctou(opts.pageUrl ?? "", opts.approvedVendor ?? "");
382
+ if (blocked) {
383
+ result.blockedReason = blocked;
384
+ return result;
385
+ }
386
+ const billingInfo = this.loadBillingInfo();
387
+ const hasBilling = Object.values(billingInfo).some((v) => v !== "");
388
+ let client = null;
389
+ try {
390
+ const target = await this.findBestTarget(opts.pageUrl);
391
+ if (!target?.webSocketDebuggerUrl) {
392
+ result.blockedReason = "no_target_found";
393
+ return result;
394
+ }
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");
400
+ // Blackout mode
401
+ const blackoutMode = (process.env.POP_BLACKOUT_MODE ?? "after").toLowerCase();
402
+ if (blackoutMode === "before") {
403
+ await this.enableBlackout(client);
404
+ }
405
+ // Fill card fields across all frames (including iframes + shadow DOM)
406
+ result.cardFilled = await this.fillCardAcrossFrames(client, opts.cardNumber, opts.expirationDate, opts.cvv);
407
+ // Fill billing fields
408
+ if (hasBilling) {
409
+ const billingResult = await this.fillBillingFields(client, billingInfo);
410
+ result.billingFilled = billingResult.filled.length > 0;
411
+ result.billingDetails = billingResult;
412
+ }
413
+ if (blackoutMode === "after") {
414
+ await this.enableBlackout(client);
415
+ }
416
+ return result;
417
+ }
418
+ catch (err) {
419
+ process.stderr.write(`PopBrowserInjector error: ${err.message}\n`);
420
+ return result;
421
+ }
422
+ finally {
423
+ client?.close();
424
+ }
425
+ }
426
+ // ------------------------------------------------------------------
427
+ // Public API: inject billing info only (no card)
428
+ // ------------------------------------------------------------------
429
+ async injectBillingOnly(opts) {
430
+ const result = {
431
+ cardFilled: false,
432
+ billingFilled: false,
433
+ blockedReason: "",
434
+ };
435
+ const blocked = verifyDomainToctou(opts.pageUrl ?? "", opts.approvedVendor ?? "");
436
+ if (blocked) {
437
+ result.blockedReason = blocked;
438
+ return result;
439
+ }
440
+ const billingInfo = this.loadBillingInfo();
441
+ let client = null;
442
+ try {
443
+ const target = await this.findBestTarget(opts.pageUrl);
444
+ if (!target?.webSocketDebuggerUrl) {
445
+ result.blockedReason = "no_target_found";
446
+ return result;
447
+ }
448
+ client = await CDPClient.connect(target.webSocketDebuggerUrl);
449
+ await client.send("Runtime.enable");
450
+ await client.send("DOM.enable");
451
+ const billingResult = await this.fillBillingFields(client, billingInfo);
452
+ result.billingFilled = billingResult.filled.length > 0;
453
+ result.billingDetails = billingResult;
454
+ return result;
455
+ }
456
+ catch (err) {
457
+ process.stderr.write(`PopBrowserInjector billing error: ${err.message}\n`);
458
+ return result;
459
+ }
460
+ finally {
461
+ client?.close();
462
+ }
463
+ }
464
+ // ------------------------------------------------------------------
465
+ // Public API: page snapshot
466
+ // ------------------------------------------------------------------
467
+ async pageSnapshot(pageUrl) {
468
+ let client = null;
469
+ try {
470
+ const target = await this.findBestTarget(pageUrl);
471
+ if (!target?.webSocketDebuggerUrl)
472
+ 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");
489
+ 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
+ }
503
+ }
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
+ };
512
+ }
513
+ catch (err) {
514
+ process.stderr.write(`PopBrowserInjector snapshot error: ${err.message}\n`);
515
+ return null;
516
+ }
517
+ finally {
518
+ client?.close();
519
+ }
520
+ }
521
+ // ------------------------------------------------------------------
522
+ // Internal: find the best CDP target (prefer checkout pages)
523
+ // ------------------------------------------------------------------
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)
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;
550
+ }
551
+ }
552
+ // Fallback: last target
553
+ return pageTargets[pageTargets.length - 1];
554
+ }
555
+ // ------------------------------------------------------------------
556
+ // Internal: fill card fields across all frames (iframes + shadow DOM)
557
+ // ------------------------------------------------------------------
558
+ async fillCardAcrossFrames(client, cardNumber, expiry, cvv) {
559
+ 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);
581
+ }
582
+ }
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;
589
+ return cardFilled;
590
+ }
591
+ // ------------------------------------------------------------------
592
+ // Internal: fill card fields in a single execution context
593
+ // ------------------------------------------------------------------
594
+ async fillCardInContext(client, contextId, cardNumber, expiry, cvv) {
595
+ const evalOpts = contextId !== undefined
596
+ ? { contextId }
597
+ : {};
598
+ // Try to fill card number
599
+ const cardSelector = exports.CARD_NUMBER_SELECTORS.join(", ");
600
+ const cardFilled = await this.fillInputViaEval(client, evalOpts, cardSelector, cardNumber);
601
+ if (!cardFilled)
602
+ return false;
603
+ // Fill expiry
604
+ const expirySelector = exports.EXPIRY_SELECTORS.join(", ");
605
+ await this.fillInputViaEval(client, evalOpts, expirySelector, expiry);
606
+ // Fill CVV
607
+ const cvvSelector = exports.CVV_SELECTORS.join(", ");
608
+ await this.fillInputViaEval(client, evalOpts, cvvSelector, cvv);
609
+ return true;
610
+ }
611
+ // ------------------------------------------------------------------
612
+ // Internal: Shadow DOM piercing support (new feature!)
613
+ // ------------------------------------------------------------------
614
+ async fillCardInShadowDom(client, cardNumber, expiry, cvv) {
615
+ try {
616
+ const { result } = await client.send("Runtime.evaluate", {
617
+ expression: `
618
+ (function() {
619
+ function queryShadowAll(root, selectors) {
620
+ const results = [];
621
+ const selectorList = selectors.split(', ');
622
+ for (const sel of selectorList) {
623
+ const found = root.querySelector(sel);
624
+ if (found) results.push(found);
625
+ }
626
+ // Recurse into shadow roots
627
+ const allElements = root.querySelectorAll('*');
628
+ for (const el of allElements) {
629
+ if (el.shadowRoot) {
630
+ results.push(...queryShadowAll(el.shadowRoot, selectors));
631
+ }
632
+ }
633
+ return results;
634
+ }
635
+
636
+ const cardSelectors = ${JSON.stringify(exports.CARD_NUMBER_SELECTORS.join(", "))};
637
+ const cardFields = queryShadowAll(document, cardSelectors);
638
+ return cardFields.length > 0;
639
+ })()
640
+ `,
641
+ returnByValue: true,
642
+ });
643
+ if (!result?.value)
644
+ return false;
645
+ // Found shadow DOM card fields — fill them
646
+ const { result: fillResult } = await client.send("Runtime.evaluate", {
647
+ expression: `
648
+ (function() {
649
+ function queryShadowFirst(root, selectors) {
650
+ const selectorList = selectors.split(', ');
651
+ for (const sel of selectorList) {
652
+ const found = root.querySelector(sel);
653
+ if (found) return found;
654
+ }
655
+ const allElements = root.querySelectorAll('*');
656
+ for (const el of allElements) {
657
+ if (el.shadowRoot) {
658
+ const found = queryShadowFirst(el.shadowRoot, selectors);
659
+ if (found) return found;
660
+ }
661
+ }
662
+ return null;
663
+ }
664
+
665
+ function fillField(root, selectors, value) {
666
+ const el = queryShadowFirst(root, selectors);
667
+ if (!el) return false;
668
+ const nativeSetter = Object.getOwnPropertyDescriptor(
669
+ HTMLInputElement.prototype, 'value'
670
+ ).set;
671
+ nativeSetter.call(el, value);
672
+ el.dispatchEvent(new Event('input', { bubbles: true }));
673
+ el.dispatchEvent(new Event('change', { bubbles: true }));
674
+ el.dispatchEvent(new Event('blur', { bubbles: true }));
675
+ return true;
676
+ }
677
+
678
+ const cardFilled = fillField(
679
+ document,
680
+ ${JSON.stringify(exports.CARD_NUMBER_SELECTORS.join(", "))},
681
+ ${JSON.stringify(cardNumber)}
682
+ );
683
+ if (cardFilled) {
684
+ fillField(document, ${JSON.stringify(exports.EXPIRY_SELECTORS.join(", "))}, ${JSON.stringify(expiry)});
685
+ fillField(document, ${JSON.stringify(exports.CVV_SELECTORS.join(", "))}, ${JSON.stringify(cvv)});
686
+ }
687
+ return cardFilled;
688
+ })()
689
+ `,
690
+ returnByValue: true,
691
+ });
692
+ return fillResult?.value === true;
693
+ }
694
+ catch {
695
+ return false;
696
+ }
697
+ }
698
+ // ------------------------------------------------------------------
699
+ // Internal: fill a single input field via Runtime.evaluate
700
+ // ------------------------------------------------------------------
701
+ async fillInputViaEval(client, evalOpts, selector, value) {
702
+ try {
703
+ const { result } = await client.send("Runtime.evaluate", {
704
+ expression: `
705
+ (function() {
706
+ const el = document.querySelector(${JSON.stringify(selector)});
707
+ if (!el) return false;
708
+ // Use native setter to bypass framework interception
709
+ const proto = el.tagName === 'SELECT'
710
+ ? HTMLSelectElement.prototype
711
+ : HTMLInputElement.prototype;
712
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
713
+ if (nativeSetter) {
714
+ nativeSetter.call(el, ${JSON.stringify(value)});
715
+ } else {
716
+ el.value = ${JSON.stringify(value)};
717
+ }
718
+ el.dispatchEvent(new Event('input', { bubbles: true }));
719
+ el.dispatchEvent(new Event('change', { bubbles: true }));
720
+ el.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
721
+ return true;
722
+ })()
723
+ `,
724
+ returnByValue: true,
725
+ ...evalOpts,
726
+ });
727
+ return result?.value === true;
728
+ }
729
+ catch {
730
+ return false;
731
+ }
732
+ }
733
+ // ------------------------------------------------------------------
734
+ // Internal: select an option from a <select> dropdown
735
+ // ------------------------------------------------------------------
736
+ async selectOption(client, evalOpts, selector, value) {
737
+ try {
738
+ const { result } = await client.send("Runtime.evaluate", {
739
+ expression: `
740
+ (function() {
741
+ const el = document.querySelector(${JSON.stringify(selector)});
742
+ if (!el || el.tagName !== 'SELECT') return false;
743
+
744
+ const options = Array.from(el.options).map(o => ({
745
+ value: o.value, text: o.text.trim()
746
+ }));
747
+ const valueLower = ${JSON.stringify(value.toLowerCase())};
748
+
749
+ let matchedValue = null;
750
+ // Exact value match
751
+ for (const opt of options) {
752
+ if (opt.value.toLowerCase() === valueLower) { matchedValue = opt.value; break; }
753
+ }
754
+ // Exact text match
755
+ if (!matchedValue) {
756
+ for (const opt of options) {
757
+ if (opt.text.toLowerCase() === valueLower) { matchedValue = opt.value; break; }
758
+ }
759
+ }
760
+ // Partial match
761
+ if (!matchedValue) {
762
+ for (const opt of options) {
763
+ const optText = opt.text.toLowerCase();
764
+ const optVal = opt.value.toLowerCase();
765
+ if ((valueLower.includes(optText) || optText.includes(valueLower) ||
766
+ valueLower.includes(optVal) || optVal.includes(valueLower)) && opt.value) {
767
+ matchedValue = opt.value; break;
768
+ }
769
+ }
770
+ }
771
+ if (!matchedValue) return false;
772
+
773
+ // Native setter trick for React/Angular/Vue/Zoho
774
+ const nativeSetter = Object.getOwnPropertyDescriptor(
775
+ HTMLSelectElement.prototype, 'value'
776
+ ).set;
777
+ nativeSetter.call(el, matchedValue);
778
+
779
+ el.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
780
+ el.dispatchEvent(new FocusEvent('focus', { bubbles: false }));
781
+ el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
782
+ el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
783
+ el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
784
+ el.dispatchEvent(new Event('input', { bubbles: true }));
785
+ el.dispatchEvent(new Event('change', { bubbles: true }));
786
+ el.dispatchEvent(new FocusEvent('blur', { bubbles: false }));
787
+ el.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
788
+
789
+ return el.value === matchedValue;
790
+ })()
791
+ `,
792
+ returnByValue: true,
793
+ ...evalOpts,
794
+ });
795
+ return result?.value === true;
796
+ }
797
+ catch {
798
+ return false;
799
+ }
800
+ }
801
+ // ------------------------------------------------------------------
802
+ // Internal: fill a billing field (input or select)
803
+ // ------------------------------------------------------------------
804
+ async fillBillingField(client, selectors, value, fieldName) {
805
+ if (!value)
806
+ return false;
807
+ // Detect if first matching element is a <select> or <input>
808
+ const allSelector = selectors.join(", ");
809
+ try {
810
+ const { result } = await client.send("Runtime.evaluate", {
811
+ expression: `
812
+ (function() {
813
+ const el = document.querySelector(${JSON.stringify(allSelector)});
814
+ if (!el) return null;
815
+ return el.tagName.toLowerCase();
816
+ })()
817
+ `,
818
+ returnByValue: true,
819
+ });
820
+ if (!result?.value)
821
+ return false;
822
+ if (result.value === "select") {
823
+ return await this.selectOption(client, {}, allSelector, value);
824
+ }
825
+ else {
826
+ return await this.fillInputViaEval(client, {}, allSelector, value);
827
+ }
828
+ }
829
+ catch {
830
+ return false;
831
+ }
832
+ }
833
+ // ------------------------------------------------------------------
834
+ // Internal: fill all billing fields
835
+ // ------------------------------------------------------------------
836
+ async fillBillingFields(client, info) {
837
+ const filled = [];
838
+ const failed = [];
839
+ const skipped = [];
840
+ // Auto-expand US state abbreviations
841
+ const state = info.state.length === 2
842
+ ? US_STATE_CODES[info.state.toUpperCase()] ?? info.state
843
+ : info.state;
844
+ const tryFill = async (selectors, value, name) => {
845
+ if (!value) {
846
+ skipped.push(name);
847
+ return;
848
+ }
849
+ const ok = await this.fillBillingField(client, selectors, value, name);
850
+ if (ok)
851
+ filled.push(name);
852
+ else
853
+ failed.push(`${name} (value='${value}')`);
854
+ };
855
+ await tryFill(exports.FIRST_NAME_SELECTORS, info.firstName, "first_name");
856
+ await tryFill(exports.LAST_NAME_SELECTORS, info.lastName, "last_name");
857
+ // Full name fallback
858
+ if (info.firstName || info.lastName) {
859
+ const fullName = [info.firstName, info.lastName].filter(Boolean).join(" ");
860
+ await tryFill(exports.FULL_NAME_SELECTORS, fullName, "full_name");
861
+ }
862
+ await tryFill(exports.STREET_SELECTORS, info.street, "street");
863
+ await tryFill(exports.CITY_SELECTORS, info.city, "city");
864
+ await tryFill(exports.STATE_SELECTORS, state, "state");
865
+ await tryFill(exports.COUNTRY_SELECTORS, info.country, "country");
866
+ await tryFill(exports.ZIP_SELECTORS, info.zip, "zip");
867
+ await tryFill(exports.EMAIL_SELECTORS, info.email, "email");
868
+ // Phone: country code dropdown first, then number
869
+ let ccFilled = false;
870
+ if (info.phoneCountryCode) {
871
+ ccFilled = await this.fillBillingField(client, exports.PHONE_COUNTRY_CODE_SELECTORS, info.phoneCountryCode, "phone_country_code");
872
+ if (ccFilled)
873
+ filled.push("phone_country_code");
874
+ }
875
+ const phoneValue = ccFilled
876
+ ? nationalNumber(info.phone, info.phoneCountryCode)
877
+ : info.phone;
878
+ await tryFill(exports.PHONE_SELECTORS, phoneValue, "phone");
879
+ return { filled, failed, skipped };
880
+ }
881
+ // ------------------------------------------------------------------
882
+ // Internal: load billing info from env vars
883
+ // ------------------------------------------------------------------
884
+ loadBillingInfo() {
885
+ return {
886
+ firstName: (process.env.POP_BILLING_FIRST_NAME ?? "").trim(),
887
+ lastName: (process.env.POP_BILLING_LAST_NAME ?? "").trim(),
888
+ street: (process.env.POP_BILLING_STREET ?? "").trim(),
889
+ city: (process.env.POP_BILLING_CITY ?? "").trim(),
890
+ state: (process.env.POP_BILLING_STATE ?? "").trim(),
891
+ country: (process.env.POP_BILLING_COUNTRY ?? "").trim(),
892
+ zip: (process.env.POP_BILLING_ZIP ?? "").trim(),
893
+ email: (process.env.POP_BILLING_EMAIL ?? "").trim(),
894
+ phone: (process.env.POP_BILLING_PHONE ?? "").trim(),
895
+ phoneCountryCode: (process.env.POP_BILLING_PHONE_COUNTRY_CODE ?? "").trim(),
896
+ };
897
+ }
898
+ // ------------------------------------------------------------------
899
+ // Internal: blackout mode (mask card fields)
900
+ // ------------------------------------------------------------------
901
+ async enableBlackout(client) {
902
+ try {
903
+ await client.send("Runtime.evaluate", {
904
+ expression: `
905
+ (function() {
906
+ // Inject into main frame
907
+ function addBlackout(doc) {
908
+ if (doc.getElementById('pop-pay-blackout')) return;
909
+ const style = doc.createElement('style');
910
+ style.id = 'pop-pay-blackout';
911
+ style.textContent = \`
912
+ input[autocomplete*="cc-"],
913
+ input[name*="card"], input[name*="Card"],
914
+ input[name*="expir"], input[name*="cvc"], input[name*="cvv"],
915
+ input[data-elements-stable-field-name],
916
+ input.__PrivateStripeElement,
917
+ input[name="cardnumber"], input[name="cc-exp"],
918
+ input[name="security_code"], input[name="card_number"],
919
+ input[name="card_expiry"], input[name="card_cvc"] {
920
+ -webkit-text-security: disc !important;
921
+ color: transparent !important;
922
+ text-shadow: 0 0 8px rgba(0,0,0,0.5) !important;
923
+ }
924
+ \`;
925
+ doc.head.appendChild(style);
926
+ }
927
+ addBlackout(document);
928
+
929
+ // Try iframes (same-origin only)
930
+ try {
931
+ const iframes = document.querySelectorAll('iframe');
932
+ for (const iframe of iframes) {
933
+ try {
934
+ if (iframe.contentDocument) {
935
+ addBlackout(iframe.contentDocument);
936
+ }
937
+ } catch {}
938
+ }
939
+ } catch {}
940
+ })()
941
+ `,
942
+ });
943
+ }
944
+ catch { }
945
+ }
946
+ // ------------------------------------------------------------------
947
+ // Masked card display helper
948
+ // ------------------------------------------------------------------
949
+ static maskedCard(cardNumber) {
950
+ const last4 = cardNumber.slice(-4);
951
+ return `****-****-****-${last4}`;
952
+ }
953
+ }
954
+ exports.PopBrowserInjector = PopBrowserInjector;
955
+ //# sourceMappingURL=injector.js.map