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.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/cli-vault.d.ts +7 -0
- package/dist/cli-vault.d.ts.map +1 -0
- package/dist/cli-vault.js +233 -0
- package/dist/cli-vault.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +159 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +68 -0
- package/dist/client.js.map +1 -0
- package/dist/core/models.d.ts +51 -0
- package/dist/core/models.d.ts.map +1 -0
- package/dist/core/models.js +19 -0
- package/dist/core/models.js.map +1 -0
- package/dist/core/state.d.ts +15 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/state.js +84 -0
- package/dist/core/state.js.map +1 -0
- package/dist/engine/guardrails.d.ts +6 -0
- package/dist/engine/guardrails.d.ts.map +1 -0
- package/dist/engine/guardrails.js +128 -0
- package/dist/engine/guardrails.js.map +1 -0
- package/dist/engine/injector.d.ts +87 -0
- package/dist/engine/injector.d.ts.map +1 -0
- package/dist/engine/injector.js +955 -0
- package/dist/engine/injector.js.map +1 -0
- package/dist/engine/known-processors.d.ts +11 -0
- package/dist/engine/known-processors.d.ts.map +1 -0
- package/dist/engine/known-processors.js +47 -0
- package/dist/engine/known-processors.js.map +1 -0
- package/dist/engine/llm-guardrails.d.ts +20 -0
- package/dist/engine/llm-guardrails.d.ts.map +1 -0
- package/dist/engine/llm-guardrails.js +89 -0
- package/dist/engine/llm-guardrails.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +334 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/providers/base.d.ts +5 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +3 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/byoc-local.d.ts +12 -0
- package/dist/providers/byoc-local.d.ts.map +1 -0
- package/dist/providers/byoc-local.js +56 -0
- package/dist/providers/byoc-local.js.map +1 -0
- package/dist/providers/stripe-mock.d.ts +6 -0
- package/dist/providers/stripe-mock.d.ts.map +1 -0
- package/dist/providers/stripe-mock.js +34 -0
- package/dist/providers/stripe-mock.js.map +1 -0
- package/dist/providers/stripe-real.d.ts +9 -0
- package/dist/providers/stripe-real.d.ts.map +1 -0
- package/dist/providers/stripe-real.js +84 -0
- package/dist/providers/stripe-real.js.map +1 -0
- package/dist/vault.d.ts +23 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +283 -0
- package/dist/vault.js.map +1 -0
- 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
|