@trusty-squire/mcp 1.0.50 → 1.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/README.md +27 -4
- package/dist/api-client.d.ts +44 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +23 -0
- package/dist/api-client.js.map +1 -1
- package/dist/bot/browser.d.ts +33 -0
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +457 -143
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/pay-operator.d.ts +27 -0
- package/dist/bot/pay-operator.d.ts.map +1 -0
- package/dist/bot/pay-operator.js +356 -0
- package/dist/bot/pay-operator.js.map +1 -0
- package/dist/bot/payment-hpke.d.ts +8 -0
- package/dist/bot/payment-hpke.d.ts.map +1 -0
- package/dist/bot/payment-hpke.js +59 -0
- package/dist/bot/payment-hpke.js.map +1 -0
- package/dist/bot/provision-session.d.ts +2 -1
- package/dist/bot/provision-session.d.ts.map +1 -1
- package/dist/bot/provision-session.js +94 -26
- package/dist/bot/provision-session.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +10 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/index.d.ts +6 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/operate-pay.d.ts +37 -0
- package/dist/tools/operate-pay.d.ts.map +1 -0
- package/dist/tools/operate-pay.js +83 -0
- package/dist/tools/operate-pay.js.map +1 -0
- package/dist/tools/provision-drive.d.ts +8 -8
- package/dist/tools/store-credential.d.ts +4 -4
- package/package.json +3 -1
package/dist/bot/browser.js
CHANGED
|
@@ -109,6 +109,90 @@ function getChromium() {
|
|
|
109
109
|
}
|
|
110
110
|
return cachedChromium;
|
|
111
111
|
}
|
|
112
|
+
const CURRENCY_SYMBOLS = {
|
|
113
|
+
$: "USD",
|
|
114
|
+
"€": "EUR",
|
|
115
|
+
"£": "GBP",
|
|
116
|
+
"¥": "JPY",
|
|
117
|
+
};
|
|
118
|
+
function currencyMinorDigits(currency) {
|
|
119
|
+
return new Intl.NumberFormat(undefined, {
|
|
120
|
+
style: "currency",
|
|
121
|
+
currency,
|
|
122
|
+
}).resolvedOptions().maximumFractionDigits;
|
|
123
|
+
}
|
|
124
|
+
function parseDisplayedNumber(raw, minorDigits) {
|
|
125
|
+
const value = raw.replace(/\s/g, "");
|
|
126
|
+
const comma = value.lastIndexOf(",");
|
|
127
|
+
const dot = value.lastIndexOf(".");
|
|
128
|
+
let normalized = value;
|
|
129
|
+
if (comma >= 0 && dot >= 0) {
|
|
130
|
+
const decimalIndex = Math.max(comma, dot);
|
|
131
|
+
const fractionLength = value.length - decimalIndex - 1;
|
|
132
|
+
if (minorDigits > 0 && fractionLength > 0 && fractionLength <= minorDigits) {
|
|
133
|
+
const integer = value.slice(0, decimalIndex).replace(/[.,]/g, "");
|
|
134
|
+
normalized = `${integer}.${value.slice(decimalIndex + 1)}`;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
normalized = value.replace(/[.,]/g, "");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (comma >= 0) {
|
|
141
|
+
const commaCount = (value.match(/,/g) ?? []).length;
|
|
142
|
+
const fractionLength = value.length - comma - 1;
|
|
143
|
+
normalized =
|
|
144
|
+
commaCount === 1 && minorDigits > 0 && fractionLength > 0 && fractionLength <= minorDigits
|
|
145
|
+
? value.replace(",", ".")
|
|
146
|
+
: value.replaceAll(",", "");
|
|
147
|
+
}
|
|
148
|
+
else if ((value.match(/\./g) ?? []).length > 1) {
|
|
149
|
+
normalized = value.replaceAll(".", "");
|
|
150
|
+
}
|
|
151
|
+
else if (dot >= 0) {
|
|
152
|
+
const fractionLength = value.length - dot - 1;
|
|
153
|
+
normalized =
|
|
154
|
+
minorDigits > 0 && fractionLength > 0 && fractionLength <= minorDigits
|
|
155
|
+
? value
|
|
156
|
+
: value.replace(".", "");
|
|
157
|
+
}
|
|
158
|
+
const parsed = Number(normalized);
|
|
159
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
160
|
+
}
|
|
161
|
+
export function parseCheckoutAmount(texts, fallbackCurrency) {
|
|
162
|
+
const totalPattern = /\b(?:order\s+total|grand\s+total|total\s+due|amount\s+due|total)\b\s*:?\s*(?:(USD|EUR|GBP|CAD|AUD|JPY|NZD|CHF|SEK|NOK|DKK)\s*)?([$€£¥])?\s*([0-9][0-9.,]*)(?:\s*(USD|EUR|GBP|CAD|AUD|JPY|NZD|CHF|SEK|NOK|DKK))?/gi;
|
|
163
|
+
for (const text of texts) {
|
|
164
|
+
totalPattern.lastIndex = 0;
|
|
165
|
+
for (const match of text.matchAll(totalPattern)) {
|
|
166
|
+
const currency = (match[1] ??
|
|
167
|
+
match[4] ??
|
|
168
|
+
CURRENCY_SYMBOLS[match[2] ?? ""] ??
|
|
169
|
+
fallbackCurrency)?.toUpperCase();
|
|
170
|
+
if (currency === undefined || !/^[A-Z]{3}$/.test(currency))
|
|
171
|
+
continue;
|
|
172
|
+
const minorDigits = currencyMinorDigits(currency);
|
|
173
|
+
const amount = parseDisplayedNumber(match[3] ?? "", minorDigits);
|
|
174
|
+
if (amount === null)
|
|
175
|
+
continue;
|
|
176
|
+
const scale = 10 ** minorDigits;
|
|
177
|
+
const minor = Math.round(amount * scale);
|
|
178
|
+
if (Math.abs(amount * scale - minor) > 1e-6)
|
|
179
|
+
continue;
|
|
180
|
+
return { amount_cents: minor, currency };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function merchantFromPage(title, siteName, url) {
|
|
186
|
+
if (siteName.trim().length > 0)
|
|
187
|
+
return siteName.trim().slice(0, 256);
|
|
188
|
+
const titlePart = title
|
|
189
|
+
.split(/\s+[|—–-]\s+/)
|
|
190
|
+
.find((part) => !/\b(checkout|payment|cart|order)\b/i.test(part));
|
|
191
|
+
if (titlePart !== undefined && titlePart.trim().length > 0) {
|
|
192
|
+
return titlePart.trim().slice(0, 256);
|
|
193
|
+
}
|
|
194
|
+
return new URL(url).hostname.replace(/^www\./, "").slice(0, 256);
|
|
195
|
+
}
|
|
112
196
|
const HCAPTCHA_UUID_RE = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
113
197
|
export function extractHcaptchaSitekeyFromHtml(html) {
|
|
114
198
|
if (!/hcaptcha\.com|h-captcha|hcaptcha/i.test(html))
|
|
@@ -207,10 +291,7 @@ function isCaptchaVariant(v) {
|
|
|
207
291
|
function pngDimensions(buf) {
|
|
208
292
|
if (buf.length < 24)
|
|
209
293
|
return null;
|
|
210
|
-
if (buf[0] !== 0x89 ||
|
|
211
|
-
buf[1] !== 0x50 ||
|
|
212
|
-
buf[2] !== 0x4e ||
|
|
213
|
-
buf[3] !== 0x47) {
|
|
294
|
+
if (buf[0] !== 0x89 || buf[1] !== 0x50 || buf[2] !== 0x4e || buf[3] !== 0x47) {
|
|
214
295
|
return null;
|
|
215
296
|
}
|
|
216
297
|
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
@@ -226,12 +307,7 @@ function pngDimensions(buf) {
|
|
|
226
307
|
// hardest to fingerprint as automation. Stable Chrome > Edge >
|
|
227
308
|
// Beta/Canary > Brave. Brave isn't a Playwright channel but its
|
|
228
309
|
// binary path is well-known; we resolve it explicitly below.
|
|
229
|
-
const PREFERRED_CHANNELS = [
|
|
230
|
-
"chrome",
|
|
231
|
-
"msedge",
|
|
232
|
-
"chrome-beta",
|
|
233
|
-
"chrome-canary",
|
|
234
|
-
];
|
|
310
|
+
const PREFERRED_CHANNELS = ["chrome", "msedge", "chrome-beta", "chrome-canary"];
|
|
235
311
|
// Per-channel binary search paths. Playwright's `executablePath()` is
|
|
236
312
|
// argumentless (returns the bundled Chromium path), so we can't ask it
|
|
237
313
|
// "is Chrome installed?" — we have to look ourselves. These are the
|
|
@@ -784,9 +860,7 @@ export class BrowserController {
|
|
|
784
860
|
this.humanize = opts.humanize ?? true;
|
|
785
861
|
this.profileDir = opts.profileDir ?? CHROME_PROFILE_DIR;
|
|
786
862
|
this.proxyOverride =
|
|
787
|
-
opts.proxyUrl !== undefined && opts.proxyUrl.trim().length > 0
|
|
788
|
-
? opts.proxyUrl.trim()
|
|
789
|
-
: null;
|
|
863
|
+
opts.proxyUrl !== undefined && opts.proxyUrl.trim().length > 0 ? opts.proxyUrl.trim() : null;
|
|
790
864
|
}
|
|
791
865
|
// Per-launch egress override (verify-fleet identities each get their own IP).
|
|
792
866
|
// null → use the env-global proxy. See resolveProxy().
|
|
@@ -870,7 +944,10 @@ export class BrowserController {
|
|
|
870
944
|
...(params.headless ? ["--headless=new"] : []),
|
|
871
945
|
"about:blank",
|
|
872
946
|
];
|
|
873
|
-
const child = spawn(params.binary, argv, {
|
|
947
|
+
const child = spawn(params.binary, argv, {
|
|
948
|
+
env: params.env,
|
|
949
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
950
|
+
});
|
|
874
951
|
this.childChrome = child;
|
|
875
952
|
registerSelfManagedChrome(child);
|
|
876
953
|
let chromeStderr = "";
|
|
@@ -943,17 +1020,35 @@ export class BrowserController {
|
|
|
943
1020
|
return;
|
|
944
1021
|
const BLOCK_TYPES = new Set(["image", "media", "font"]);
|
|
945
1022
|
const BLOCK_HOSTS = [
|
|
946
|
-
"google-analytics.com",
|
|
947
|
-
"
|
|
948
|
-
"
|
|
949
|
-
"
|
|
950
|
-
"
|
|
951
|
-
"
|
|
1023
|
+
"google-analytics.com",
|
|
1024
|
+
"googletagmanager.com",
|
|
1025
|
+
"analytics.google.com",
|
|
1026
|
+
"doubleclick.net",
|
|
1027
|
+
"static.hotjar.com",
|
|
1028
|
+
"script.hotjar.com",
|
|
1029
|
+
"segment.com",
|
|
1030
|
+
"segment.io",
|
|
1031
|
+
"cdn.segment.com",
|
|
1032
|
+
"fullstory.com",
|
|
1033
|
+
"mixpanel.com",
|
|
1034
|
+
"bugsnag.com",
|
|
1035
|
+
"intercom.io",
|
|
1036
|
+
"intercomcdn.com",
|
|
1037
|
+
"widget.intercom.io",
|
|
1038
|
+
"connect.facebook.net",
|
|
1039
|
+
"analytics.tiktok.com",
|
|
1040
|
+
"clarity.ms",
|
|
1041
|
+
"cdn.heapanalytics.com",
|
|
1042
|
+
"wistia.com",
|
|
952
1043
|
];
|
|
953
1044
|
// NEVER block — these break signup (captcha/challenge widgets + payment SDK).
|
|
954
1045
|
const ALWAYS_ALLOW = [
|
|
955
|
-
"challenges.cloudflare.com",
|
|
956
|
-
"
|
|
1046
|
+
"challenges.cloudflare.com",
|
|
1047
|
+
"turnstile",
|
|
1048
|
+
"hcaptcha.com",
|
|
1049
|
+
"newassets.hcaptcha.com",
|
|
1050
|
+
"recaptcha",
|
|
1051
|
+
"gstatic.com/recaptcha",
|
|
957
1052
|
"js.stripe.com",
|
|
958
1053
|
];
|
|
959
1054
|
await ctx.route("**/*", async (route) => {
|
|
@@ -1393,7 +1488,6 @@ export class BrowserController {
|
|
|
1393
1488
|
void (async () => {
|
|
1394
1489
|
if (trace) {
|
|
1395
1490
|
const before = await frame.evaluate(RENDERER_PROBE).catch(() => "eval-fail");
|
|
1396
|
-
// eslint-disable-next-line no-console
|
|
1397
1491
|
console.error(`[captcha-fp] ${cfHost} renderer BEFORE spoof: ${before}`);
|
|
1398
1492
|
}
|
|
1399
1493
|
// Retry until the spoof STICKS. The first framenavigated commonly
|
|
@@ -1412,7 +1506,6 @@ export class BrowserController {
|
|
|
1412
1506
|
await new Promise((res) => setTimeout(res, 150));
|
|
1413
1507
|
}
|
|
1414
1508
|
if (trace) {
|
|
1415
|
-
// eslint-disable-next-line no-console
|
|
1416
1509
|
console.error(`[captcha-fp] ${cfHost} renderer AFTER spoof: ${landed ? "Intel (landed)" : "FAILED to land in budget"}`);
|
|
1417
1510
|
}
|
|
1418
1511
|
})();
|
|
@@ -1439,8 +1532,7 @@ export class BrowserController {
|
|
|
1439
1532
|
/api\.hcaptcha\.com\/(?:checksiteconfig|getcaptcha|checkcaptcha)/.test(url)) {
|
|
1440
1533
|
try {
|
|
1441
1534
|
const body = await resp.text();
|
|
1442
|
-
bodyPreview =
|
|
1443
|
-
body.length > 400 ? body.slice(0, 400) + "…" : body;
|
|
1535
|
+
bodyPreview = body.length > 400 ? body.slice(0, 400) + "…" : body;
|
|
1444
1536
|
}
|
|
1445
1537
|
catch {
|
|
1446
1538
|
// body may be evicted; ignore
|
|
@@ -1590,8 +1682,7 @@ export class BrowserController {
|
|
|
1590
1682
|
// not fire. MEASURED 2026-07-01 (Loops "Login link": the results list has no
|
|
1591
1683
|
// /api/auth/callback href; opening the row reveals it).
|
|
1592
1684
|
const els = await this.extractInteractiveElements();
|
|
1593
|
-
const row = els.find((e) => e.role === "link" &&
|
|
1594
|
-
(e.visibleText ?? e.ariaLabel ?? e.labelText ?? "").trim().length > 25);
|
|
1685
|
+
const row = els.find((e) => e.role === "link" && (e.visibleText ?? e.ariaLabel ?? e.labelText ?? "").trim().length > 25);
|
|
1595
1686
|
if (row === undefined)
|
|
1596
1687
|
return false;
|
|
1597
1688
|
await this.click(row.selector).catch(() => { });
|
|
@@ -1626,7 +1717,9 @@ export class BrowserController {
|
|
|
1626
1717
|
try {
|
|
1627
1718
|
const left = new URL(a);
|
|
1628
1719
|
const right = new URL(b);
|
|
1629
|
-
return left.origin === right.origin &&
|
|
1720
|
+
return (left.origin === right.origin &&
|
|
1721
|
+
left.pathname === right.pathname &&
|
|
1722
|
+
left.search === right.search);
|
|
1630
1723
|
}
|
|
1631
1724
|
catch {
|
|
1632
1725
|
return false;
|
|
@@ -1678,7 +1771,9 @@ export class BrowserController {
|
|
|
1678
1771
|
if (landedAuthGateForTarget(this.page.url(), url))
|
|
1679
1772
|
break;
|
|
1680
1773
|
await this.page
|
|
1681
|
-
.waitForURL((landed) => sameOriginPathAndSearch(landed.toString(), url), {
|
|
1774
|
+
.waitForURL((landed) => sameOriginPathAndSearch(landed.toString(), url), {
|
|
1775
|
+
timeout: 5000,
|
|
1776
|
+
})
|
|
1682
1777
|
.then(() => undefined)
|
|
1683
1778
|
.catch(() => undefined);
|
|
1684
1779
|
if (sameOriginPathAndSearch(this.page.url(), url))
|
|
@@ -2052,7 +2147,7 @@ export class BrowserController {
|
|
|
2052
2147
|
// — the trigger updates + Next un-gates — but only when we target the
|
|
2053
2148
|
// option element, not its child span (which a raw coordinate click drops).
|
|
2054
2149
|
const optEl = el.closest('[role="option"],[role="menuitem"],[role="menuitemradio"],[cmdk-item]');
|
|
2055
|
-
const optRole = optEl !== null ? optEl.getAttribute("role") ?? "option" : "";
|
|
2150
|
+
const optRole = optEl !== null ? (optEl.getAttribute("role") ?? "option") : "";
|
|
2056
2151
|
const optText = optEl !== null ? (optEl.textContent ?? "").trim().slice(0, 80) : "";
|
|
2057
2152
|
return {
|
|
2058
2153
|
inputKind,
|
|
@@ -2076,7 +2171,9 @@ export class BrowserController {
|
|
|
2076
2171
|
// NOT the anti-bot-scored gate.
|
|
2077
2172
|
const optRole = probe.role === "option" || probe.role === "menuitem" || probe.role === "menuitemradio"
|
|
2078
2173
|
? probe.role
|
|
2079
|
-
: probe.optRole === "option" ||
|
|
2174
|
+
: probe.optRole === "option" ||
|
|
2175
|
+
probe.optRole === "menuitem" ||
|
|
2176
|
+
probe.optRole === "menuitemradio"
|
|
2080
2177
|
? probe.optRole
|
|
2081
2178
|
: "";
|
|
2082
2179
|
const optName = probe.role !== "" ? probe.text : probe.optText;
|
|
@@ -2197,9 +2294,7 @@ export class BrowserController {
|
|
|
2197
2294
|
// Escape regex metacharacters in the user-supplied label text so
|
|
2198
2295
|
// a literal "(2FA)" or "." doesn't get interpreted as a pattern.
|
|
2199
2296
|
const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2200
|
-
const locator = this.page
|
|
2201
|
-
.getByText(new RegExp(escaped, "i"))
|
|
2202
|
-
.first();
|
|
2297
|
+
const locator = this.page.getByText(new RegExp(escaped, "i")).first();
|
|
2203
2298
|
await locator.waitFor({ state: "visible", timeout: timeoutMs });
|
|
2204
2299
|
if (this.humanize) {
|
|
2205
2300
|
await this.humanClickLocator(locator);
|
|
@@ -2322,10 +2417,16 @@ export class BrowserController {
|
|
|
2322
2417
|
async ensureChecked(selector) {
|
|
2323
2418
|
if (!this.page)
|
|
2324
2419
|
return false;
|
|
2325
|
-
if (await this.page
|
|
2420
|
+
if (await this.page
|
|
2421
|
+
.locator(selector)
|
|
2422
|
+
.isChecked()
|
|
2423
|
+
.catch(() => false))
|
|
2326
2424
|
return true;
|
|
2327
2425
|
await this.clickAssociatedLabel(selector).catch(() => false);
|
|
2328
|
-
if (await this.page
|
|
2426
|
+
if (await this.page
|
|
2427
|
+
.locator(selector)
|
|
2428
|
+
.isChecked()
|
|
2429
|
+
.catch(() => false))
|
|
2329
2430
|
return true;
|
|
2330
2431
|
const domChecked = await this.page
|
|
2331
2432
|
.locator(selector)
|
|
@@ -2350,7 +2451,10 @@ export class BrowserController {
|
|
|
2350
2451
|
.catch(() => false);
|
|
2351
2452
|
if (!domChecked)
|
|
2352
2453
|
return false;
|
|
2353
|
-
return await this.page
|
|
2454
|
+
return await this.page
|
|
2455
|
+
.locator(selector)
|
|
2456
|
+
.isChecked()
|
|
2457
|
+
.catch(() => false);
|
|
2354
2458
|
}
|
|
2355
2459
|
// Click the <label> associated with a checkbox/radio input — either a
|
|
2356
2460
|
// `<label for="<id>">` or the wrapping `<label>` ancestor. Mantine/Radix
|
|
@@ -2552,9 +2656,7 @@ export class BrowserController {
|
|
|
2552
2656
|
if (!box.checked)
|
|
2553
2657
|
return false;
|
|
2554
2658
|
const text = associatedText(box);
|
|
2555
|
-
return
|
|
2556
|
-
!marketingRe.test(text) &&
|
|
2557
|
-
!riskyChoiceRe.test(text));
|
|
2659
|
+
return !agreementRe.test(text) && !marketingRe.test(text) && !riskyChoiceRe.test(text);
|
|
2558
2660
|
});
|
|
2559
2661
|
if (alreadyChoseCategory)
|
|
2560
2662
|
return [];
|
|
@@ -2699,8 +2801,7 @@ export class BrowserController {
|
|
|
2699
2801
|
// the disabled button is NOT scroll position (Railway iter ≥2 on
|
|
2700
2802
|
// the second ToS modal: planner kept asking for scroll when the
|
|
2701
2803
|
// form was actually waiting on something else).
|
|
2702
|
-
if (target.scrollTop + target.clientHeight >=
|
|
2703
|
-
target.scrollHeight - 4) {
|
|
2804
|
+
if (target.scrollTop + target.clientHeight >= target.scrollHeight - 4) {
|
|
2704
2805
|
return {
|
|
2705
2806
|
scrolled: false,
|
|
2706
2807
|
container: selector ?? "auto-detected",
|
|
@@ -2995,8 +3096,8 @@ export class BrowserController {
|
|
|
2995
3096
|
'[role="option"]:visible',
|
|
2996
3097
|
'[role="menuitem"]:visible',
|
|
2997
3098
|
'[role="menuitemradio"]:visible',
|
|
2998
|
-
|
|
2999
|
-
|
|
3099
|
+
"mat-option:visible",
|
|
3100
|
+
".mat-mdc-option:visible",
|
|
3000
3101
|
'[id^="react-select-"][role*="menu"]:visible',
|
|
3001
3102
|
'[role="listbox"]:visible li:visible',
|
|
3002
3103
|
];
|
|
@@ -3092,10 +3193,10 @@ export class BrowserController {
|
|
|
3092
3193
|
// CSS-escape the id so unusual characters (Sentry's `--` separator
|
|
3093
3194
|
// is fine, but the helper is defensive against future ids that
|
|
3094
3195
|
// include `.`, spaces, …) don't break the locator.
|
|
3095
|
-
const escaped =
|
|
3196
|
+
const escaped = typeof globalThis.CSS?.escape ===
|
|
3096
3197
|
"function"
|
|
3097
3198
|
? globalThis.CSS.escape(resolvedId)
|
|
3098
|
-
: resolvedId.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, "\\$1")
|
|
3199
|
+
: resolvedId.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, "\\$1");
|
|
3099
3200
|
return `#${escaped}`;
|
|
3100
3201
|
}
|
|
3101
3202
|
catch {
|
|
@@ -3124,9 +3225,7 @@ export class BrowserController {
|
|
|
3124
3225
|
throw new Error("Browser not started");
|
|
3125
3226
|
const triggerLocator = this.page.locator(triggerSelector);
|
|
3126
3227
|
try {
|
|
3127
|
-
const tagName = await triggerLocator
|
|
3128
|
-
.first()
|
|
3129
|
-
.evaluate((node) => node.tagName.toLowerCase());
|
|
3228
|
+
const tagName = await triggerLocator.first().evaluate((node) => node.tagName.toLowerCase());
|
|
3130
3229
|
// Limit this path to input-typed triggers; native <select> and
|
|
3131
3230
|
// <button role="combobox"> are handled by other tiers. The
|
|
3132
3231
|
// selectFromCombobox caller has already returned for matching
|
|
@@ -3471,9 +3570,7 @@ export class BrowserController {
|
|
|
3471
3570
|
out.webglUnmaskedVendor = gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL);
|
|
3472
3571
|
out.webglUnmaskedRenderer = gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL);
|
|
3473
3572
|
}
|
|
3474
|
-
out.webglExtensions = (gl.getSupportedExtensions() ?? [])
|
|
3475
|
-
.slice(0, 6)
|
|
3476
|
-
.join(",");
|
|
3573
|
+
out.webglExtensions = (gl.getSupportedExtensions() ?? []).slice(0, 6).join(",");
|
|
3477
3574
|
}
|
|
3478
3575
|
else {
|
|
3479
3576
|
out.webglVendor = null;
|
|
@@ -3520,8 +3617,7 @@ export class BrowserController {
|
|
|
3520
3617
|
console.error("[fingerprint] " + JSON.stringify(fp));
|
|
3521
3618
|
}
|
|
3522
3619
|
catch (err) {
|
|
3523
|
-
console.error("[fingerprint] probe failed: " +
|
|
3524
|
-
(err instanceof Error ? err.message : String(err)));
|
|
3620
|
+
console.error("[fingerprint] probe failed: " + (err instanceof Error ? err.message : String(err)));
|
|
3525
3621
|
}
|
|
3526
3622
|
}
|
|
3527
3623
|
// Click at the checkbox position. Turnstile's checkbox sits at
|
|
@@ -3656,7 +3752,10 @@ export class BrowserController {
|
|
|
3656
3752
|
{ kind: "turnstile", selector: 'iframe[src*="challenges.cloudflare.com"]' },
|
|
3657
3753
|
// Visible reCAPTCHA only — the size=invisible anchor (score-mode badge)
|
|
3658
3754
|
// is handled by the recaptchaInvisibleOnly skip above.
|
|
3659
|
-
{
|
|
3755
|
+
{
|
|
3756
|
+
kind: "recaptcha",
|
|
3757
|
+
selector: 'iframe[src*="recaptcha/api2/anchor"]:not([src*="size=invisible"])',
|
|
3758
|
+
},
|
|
3660
3759
|
// hCaptcha's checkbox iframe (the anchor frame). Plausible and other
|
|
3661
3760
|
// hCaptcha sites render this; clicking it ticks the box the same way
|
|
3662
3761
|
// Turnstile/reCAPTCHA do.
|
|
@@ -3897,7 +3996,8 @@ export class BrowserController {
|
|
|
3897
3996
|
for (const [, v] of Object.entries(obj)) {
|
|
3898
3997
|
if (v === null || typeof v !== "object")
|
|
3899
3998
|
continue;
|
|
3900
|
-
if ("callback" in v &&
|
|
3999
|
+
if ("callback" in v &&
|
|
4000
|
+
typeof v.callback === "function") {
|
|
3901
4001
|
try {
|
|
3902
4002
|
v.callback(tok);
|
|
3903
4003
|
}
|
|
@@ -4123,8 +4223,7 @@ export class BrowserController {
|
|
|
4123
4223
|
const fromDom = await this.page.evaluate(() => {
|
|
4124
4224
|
const div = document.querySelector(".h-captcha[data-sitekey], [data-hcaptcha-sitekey]");
|
|
4125
4225
|
if (div !== null) {
|
|
4126
|
-
const k = div.getAttribute("data-sitekey") ??
|
|
4127
|
-
div.getAttribute("data-hcaptcha-sitekey");
|
|
4226
|
+
const k = div.getAttribute("data-sitekey") ?? div.getAttribute("data-hcaptcha-sitekey");
|
|
4128
4227
|
if (k !== null && k.length > 10)
|
|
4129
4228
|
return k;
|
|
4130
4229
|
}
|
|
@@ -4381,8 +4480,7 @@ export class BrowserController {
|
|
|
4381
4480
|
return {
|
|
4382
4481
|
found: true,
|
|
4383
4482
|
solved: false,
|
|
4384
|
-
reason: `2captcha_${solveRes.kind}` +
|
|
4385
|
-
("reason" in solveRes ? `:${solveRes.reason}` : ""),
|
|
4483
|
+
reason: `2captcha_${solveRes.kind}` + ("reason" in solveRes ? `:${solveRes.reason}` : ""),
|
|
4386
4484
|
clicks: 0,
|
|
4387
4485
|
...("durationMs" in solveRes ? { durationMs: solveRes.durationMs } : {}),
|
|
4388
4486
|
};
|
|
@@ -4563,7 +4661,7 @@ export class BrowserController {
|
|
|
4563
4661
|
async extractText() {
|
|
4564
4662
|
if (!this.page)
|
|
4565
4663
|
throw new Error("Browser not started");
|
|
4566
|
-
return await this.page.textContent("body") || "";
|
|
4664
|
+
return (await this.page.textContent("body")) || "";
|
|
4567
4665
|
}
|
|
4568
4666
|
// RENDERED, visibility-respecting body text. extractText() reads
|
|
4569
4667
|
// textContent("body"), which includes display:none / visibility:hidden /
|
|
@@ -4580,6 +4678,201 @@ export class BrowserController {
|
|
|
4580
4678
|
throw new Error("Browser not started");
|
|
4581
4679
|
return await this.page.evaluate(() => document.body?.innerText ?? "");
|
|
4582
4680
|
}
|
|
4681
|
+
async readCheckoutSummary(fallbackCurrency) {
|
|
4682
|
+
if (!this.page)
|
|
4683
|
+
throw new Error("Browser not started");
|
|
4684
|
+
const page = this.page;
|
|
4685
|
+
const identity = await page.evaluate(() => ({
|
|
4686
|
+
title: document.title,
|
|
4687
|
+
siteName: document.querySelector('meta[property="og:site_name"]')?.content ??
|
|
4688
|
+
document.querySelector('[itemprop="merchant"]')?.textContent ??
|
|
4689
|
+
"",
|
|
4690
|
+
}));
|
|
4691
|
+
const texts = await Promise.all(page
|
|
4692
|
+
.frames()
|
|
4693
|
+
.map(async (frame) => await frame.evaluate(() => document.body?.innerText ?? "").catch(() => "")));
|
|
4694
|
+
const amount = parseCheckoutAmount(texts, fallbackCurrency);
|
|
4695
|
+
if (amount === null)
|
|
4696
|
+
throw new Error("payment_checkout_total_not_found");
|
|
4697
|
+
return {
|
|
4698
|
+
merchant: merchantFromPage(identity.title, identity.siteName, page.url()),
|
|
4699
|
+
checkout_origin: new URL(page.url()).origin,
|
|
4700
|
+
...amount,
|
|
4701
|
+
};
|
|
4702
|
+
}
|
|
4703
|
+
// Common autocomplete/name selectors across all CDP-reachable frames,
|
|
4704
|
+
// including cross-origin hosted fields. No PSP-specific adapters.
|
|
4705
|
+
async fillAndSubmitCheckout(card) {
|
|
4706
|
+
if (!this.page)
|
|
4707
|
+
throw new Error("Browser not started");
|
|
4708
|
+
const frames = this.page.frames();
|
|
4709
|
+
const filled = new Set();
|
|
4710
|
+
const fillFirst = async (field, value, selectors) => {
|
|
4711
|
+
if (value === undefined || value.length === 0)
|
|
4712
|
+
return false;
|
|
4713
|
+
for (const frame of frames) {
|
|
4714
|
+
const matches = frame.locator(selectors);
|
|
4715
|
+
const count = Math.min(await matches.count().catch(() => 0), 10);
|
|
4716
|
+
for (let i = 0; i < count; i += 1) {
|
|
4717
|
+
const input = matches.nth(i);
|
|
4718
|
+
if (!(await input.isVisible().catch(() => false)))
|
|
4719
|
+
continue;
|
|
4720
|
+
if (!(await input.isEnabled().catch(() => false)))
|
|
4721
|
+
continue;
|
|
4722
|
+
const tag = await input.evaluate((el) => el.tagName.toLowerCase()).catch(() => "");
|
|
4723
|
+
await input.evaluate((el) => el.setAttribute("data-ts-sealed-payment", "1"));
|
|
4724
|
+
if (tag === "select") {
|
|
4725
|
+
const selected = (await input
|
|
4726
|
+
.selectOption({ value })
|
|
4727
|
+
.then(() => true)
|
|
4728
|
+
.catch(() => false)) ||
|
|
4729
|
+
(await input
|
|
4730
|
+
.selectOption({ label: value })
|
|
4731
|
+
.then(() => true)
|
|
4732
|
+
.catch(() => false));
|
|
4733
|
+
if (!selected)
|
|
4734
|
+
continue;
|
|
4735
|
+
}
|
|
4736
|
+
else {
|
|
4737
|
+
await input.fill(value);
|
|
4738
|
+
}
|
|
4739
|
+
filled.add(field);
|
|
4740
|
+
return true;
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
return false;
|
|
4744
|
+
};
|
|
4745
|
+
try {
|
|
4746
|
+
await fillFirst("pan", card.pan, 'input[autocomplete~="cc-number"],input[name*="cardnumber" i],input[id*="card-number" i],input[id*="cardnumber" i]');
|
|
4747
|
+
const combinedExpiry = `${card.exp_month.padStart(2, "0")}/${card.exp_year.slice(-2)}`;
|
|
4748
|
+
const combined = await fillFirst("expiry", combinedExpiry, 'input[autocomplete~="cc-exp"],input[name*="expir" i]:not([name*="month" i]):not([name*="year" i]),input[name="exp" i],input[name*="exp-date" i],input[id*="expir" i]:not([id*="month" i]):not([id*="year" i]),input[id="exp" i],input[id*="exp-date" i]');
|
|
4749
|
+
if (!combined) {
|
|
4750
|
+
await fillFirst("exp_month", card.exp_month.padStart(2, "0"), '[autocomplete~="cc-exp-month"],[name*="exp_month" i],[name*="expmonth" i]');
|
|
4751
|
+
await fillFirst("exp_year", card.exp_year, '[autocomplete~="cc-exp-year"],[name*="exp_year" i],[name*="expyear" i]');
|
|
4752
|
+
}
|
|
4753
|
+
const fields = [
|
|
4754
|
+
[
|
|
4755
|
+
"cvv",
|
|
4756
|
+
card.cvv,
|
|
4757
|
+
'input[autocomplete~="cc-csc"],input[name*="cvv" i],input[name*="cvc" i],input[name*="security-code" i],input[id*="cvv" i],input[id*="cvc" i]',
|
|
4758
|
+
],
|
|
4759
|
+
[
|
|
4760
|
+
"name",
|
|
4761
|
+
card.name,
|
|
4762
|
+
'input[autocomplete~="cc-name"],input[name*="cardholder" i],input[name*="card-name" i],input[id*="cardholder" i]',
|
|
4763
|
+
],
|
|
4764
|
+
[
|
|
4765
|
+
"line1",
|
|
4766
|
+
card.billing.line1,
|
|
4767
|
+
'[autocomplete~="address-line1"],[name*="address_line1" i],[name*="address1" i],[name="line1" i]',
|
|
4768
|
+
],
|
|
4769
|
+
[
|
|
4770
|
+
"line2",
|
|
4771
|
+
card.billing.line2,
|
|
4772
|
+
'[autocomplete~="address-line2"],[name*="address_line2" i],[name*="address2" i],[name="line2" i]',
|
|
4773
|
+
],
|
|
4774
|
+
[
|
|
4775
|
+
"city",
|
|
4776
|
+
card.billing.city,
|
|
4777
|
+
'[autocomplete~="address-level2"],[name*="city" i],[name*="locality" i]',
|
|
4778
|
+
],
|
|
4779
|
+
[
|
|
4780
|
+
"state",
|
|
4781
|
+
card.billing.state,
|
|
4782
|
+
'[autocomplete~="address-level1"],[name*="state" i],[name*="region" i]',
|
|
4783
|
+
],
|
|
4784
|
+
[
|
|
4785
|
+
"postal_code",
|
|
4786
|
+
card.billing.postal_code,
|
|
4787
|
+
'[autocomplete~="postal-code"],[name*="postal" i],[name*="zip" i]',
|
|
4788
|
+
],
|
|
4789
|
+
["country", card.billing.country, '[autocomplete~="country"],[name*="country" i]'],
|
|
4790
|
+
];
|
|
4791
|
+
for (const [field, value, selectors] of fields) {
|
|
4792
|
+
await fillFirst(field, value, selectors);
|
|
4793
|
+
}
|
|
4794
|
+
for (const required of ["pan", "expiry", "cvv", "name"]) {
|
|
4795
|
+
if (required === "expiry" && filled.has("exp_month") && filled.has("exp_year"))
|
|
4796
|
+
continue;
|
|
4797
|
+
if (!filled.has(required))
|
|
4798
|
+
throw new Error(`payment_field_not_found:${required}`);
|
|
4799
|
+
}
|
|
4800
|
+
const submitName = /^(?:pay(?:\s+now)?|place\s+order|complete\s+(?:order|purchase|payment)|submit\s+payment|buy\s+now|confirm\s+(?:order|payment))\b/i;
|
|
4801
|
+
let submitted = false;
|
|
4802
|
+
for (const frame of frames) {
|
|
4803
|
+
const matches = frame.locator('button,input[type="submit"],[role="button"]');
|
|
4804
|
+
const count = Math.min(await matches.count().catch(() => 0), 100);
|
|
4805
|
+
for (let i = 0; i < count; i += 1) {
|
|
4806
|
+
const candidate = matches.nth(i);
|
|
4807
|
+
if (!(await candidate.isVisible().catch(() => false)))
|
|
4808
|
+
continue;
|
|
4809
|
+
if (!(await candidate.isEnabled().catch(() => false)))
|
|
4810
|
+
continue;
|
|
4811
|
+
const label = await candidate
|
|
4812
|
+
.evaluate((el) => (el.getAttribute("aria-label") ||
|
|
4813
|
+
(el instanceof HTMLInputElement ? el.value : el.textContent) ||
|
|
4814
|
+
"").trim())
|
|
4815
|
+
.catch(() => "");
|
|
4816
|
+
if (!submitName.test(label))
|
|
4817
|
+
continue;
|
|
4818
|
+
await candidate.click();
|
|
4819
|
+
submitted = true;
|
|
4820
|
+
break;
|
|
4821
|
+
}
|
|
4822
|
+
if (submitted)
|
|
4823
|
+
break;
|
|
4824
|
+
}
|
|
4825
|
+
if (!submitted)
|
|
4826
|
+
throw new Error("payment_submit_not_found");
|
|
4827
|
+
const challengeDeadline = Date.now() + 15_000;
|
|
4828
|
+
while (Date.now() < challengeDeadline) {
|
|
4829
|
+
const challenge = await this.detectThreeDsChallenge();
|
|
4830
|
+
if (challenge.three_ds_required)
|
|
4831
|
+
return challenge;
|
|
4832
|
+
await this.page.waitForTimeout(250).catch(() => undefined);
|
|
4833
|
+
}
|
|
4834
|
+
return { three_ds_required: false };
|
|
4835
|
+
}
|
|
4836
|
+
finally {
|
|
4837
|
+
for (const frame of this.page.frames()) {
|
|
4838
|
+
await frame
|
|
4839
|
+
.locator('[data-ts-sealed-payment="1"]')
|
|
4840
|
+
.evaluateAll((elements) => {
|
|
4841
|
+
for (const element of elements) {
|
|
4842
|
+
if (element instanceof HTMLInputElement ||
|
|
4843
|
+
element instanceof HTMLTextAreaElement ||
|
|
4844
|
+
element instanceof HTMLSelectElement) {
|
|
4845
|
+
element.value = "";
|
|
4846
|
+
}
|
|
4847
|
+
element.removeAttribute("data-ts-sealed-payment");
|
|
4848
|
+
}
|
|
4849
|
+
})
|
|
4850
|
+
.catch(() => undefined);
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
async detectThreeDsChallenge() {
|
|
4855
|
+
if (!this.page)
|
|
4856
|
+
throw new Error("Browser not started");
|
|
4857
|
+
const urlPattern = /(?:3d[-_ ]?secure|three[-_ ]?d[-_ ]?secure|\/3ds(?:2)?\/|\/acs\/|challenge)/i;
|
|
4858
|
+
for (const frame of this.page.frames()) {
|
|
4859
|
+
const detected = urlPattern.test(frame.url()) ||
|
|
4860
|
+
(await frame
|
|
4861
|
+
.evaluate(() => {
|
|
4862
|
+
if (document.querySelector('iframe[name*="challenge" i],iframe[title*="3d secure" i],input[name="creq" i],form[action*="acs" i]') !== null)
|
|
4863
|
+
return true;
|
|
4864
|
+
return /\b(?:3d secure|authenticate (?:this )?payment|verify (?:your )?identity|security code sent to)\b/i.test(document.body?.innerText ?? "");
|
|
4865
|
+
})
|
|
4866
|
+
.catch(() => false));
|
|
4867
|
+
if (detected) {
|
|
4868
|
+
return {
|
|
4869
|
+
three_ds_required: true,
|
|
4870
|
+
challenge_url: frame.url() || this.page.url(),
|
|
4871
|
+
};
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
return { three_ds_required: false };
|
|
4875
|
+
}
|
|
4583
4876
|
// Deterministic Firebase/GCP credential extraction. Every Firebase project
|
|
4584
4877
|
// auto-creates a "Browser key (auto created by Firebase)" in its underlying
|
|
4585
4878
|
// Google Cloud project — the SAME AIzaSy value as firebaseConfig.apiKey AND a
|
|
@@ -4607,8 +4900,7 @@ export class BrowserController {
|
|
|
4607
4900
|
}
|
|
4608
4901
|
// Locate the Firebase Browser-key row; return its AIzaSy if already shown,
|
|
4609
4902
|
// else click the row's "Show key" button to reveal it.
|
|
4610
|
-
const readRowKey = () => this.page
|
|
4611
|
-
.evaluate(() => {
|
|
4903
|
+
const readRowKey = () => this.page.evaluate(() => {
|
|
4612
4904
|
const rows = Array.from(document.querySelectorAll("tr"));
|
|
4613
4905
|
const row = rows.find((r) => /browser key \(auto created by firebase\)/i.test(r.textContent ?? "")) ??
|
|
4614
4906
|
rows.find((r) => /browser key/i.test(r.textContent ?? ""));
|
|
@@ -4621,8 +4913,7 @@ export class BrowserController {
|
|
|
4621
4913
|
if (btn !== undefined)
|
|
4622
4914
|
btn.click();
|
|
4623
4915
|
return null;
|
|
4624
|
-
})
|
|
4625
|
-
.catch(() => null);
|
|
4916
|
+
}).catch(() => null);
|
|
4626
4917
|
const first = await readRowKey();
|
|
4627
4918
|
if (first !== null && KEY_RE.test(first))
|
|
4628
4919
|
return first;
|
|
@@ -4702,8 +4993,7 @@ export class BrowserController {
|
|
|
4702
4993
|
// "Open Source" but stay uncommitted, so Next stays disabled); (2) empty
|
|
4703
4994
|
// text; (3) a clear "Select…/Choose…/Pick…" placeholder. NOT
|
|
4704
4995
|
// "search"/"add"/"type" — those are filter inputs we must not auto-pick.
|
|
4705
|
-
const hasPlaceholderAttr = el.hasAttribute("data-placeholder") ||
|
|
4706
|
-
el.querySelector("[data-placeholder]") !== null;
|
|
4996
|
+
const hasPlaceholderAttr = el.hasAttribute("data-placeholder") || el.querySelector("[data-placeholder]") !== null;
|
|
4707
4997
|
const placeholderish = hasPlaceholderAttr ||
|
|
4708
4998
|
txt.length === 0 ||
|
|
4709
4999
|
/^(?:please\s+)?(?:select|choose|pick)\b/i.test(txt);
|
|
@@ -4826,7 +5116,9 @@ export class BrowserController {
|
|
|
4826
5116
|
// triggers (aria-haspopup) — those are handled in (2); a selected preset
|
|
4827
5117
|
// trigger can also read "All access" and we must not re-open it here.
|
|
4828
5118
|
try {
|
|
4829
|
-
const allAccess = root.locator('button:not([aria-haspopup="true"])', {
|
|
5119
|
+
const allAccess = root.locator('button:not([aria-haspopup="true"])', {
|
|
5120
|
+
hasText: /^(?:all access|full access|all scopes)$/i,
|
|
5121
|
+
});
|
|
4830
5122
|
const n = Math.min(await allAccess.count().catch(() => 0), 3);
|
|
4831
5123
|
for (let i = 0; i < n; i += 1) {
|
|
4832
5124
|
const b = allAccess.nth(i);
|
|
@@ -4848,9 +5140,7 @@ export class BrowserController {
|
|
|
4848
5140
|
const t = triggers.nth(i);
|
|
4849
5141
|
if (!(await t.isVisible().catch(() => false)))
|
|
4850
5142
|
continue;
|
|
4851
|
-
const txt = ((await t.textContent().catch(() => "")) ?? "")
|
|
4852
|
-
.replace(/\s+/g, " ")
|
|
4853
|
-
.trim();
|
|
5143
|
+
const txt = ((await t.textContent().catch(() => "")) ?? "").replace(/\s+/g, " ").trim();
|
|
4854
5144
|
// Only act on an UNSELECTED select (a "Select…/Choose…/Pick…"
|
|
4855
5145
|
// placeholder) — never re-pick one that already holds a value.
|
|
4856
5146
|
if (!/^(?:please\s+)?(?:select|choose|pick)\b/i.test(txt))
|
|
@@ -4919,7 +5209,9 @@ export class BrowserController {
|
|
|
4919
5209
|
if (!this.page)
|
|
4920
5210
|
throw new Error("Browser not started");
|
|
4921
5211
|
return await this.page.evaluate(async (rawPrefix) => {
|
|
4922
|
-
const prefix = String(rawPrefix ?? "")
|
|
5212
|
+
const prefix = String(rawPrefix ?? "")
|
|
5213
|
+
.replace(/^\/+|\/+$/g, "")
|
|
5214
|
+
.toLowerCase();
|
|
4923
5215
|
const candidates = [];
|
|
4924
5216
|
const seen = new Set();
|
|
4925
5217
|
const add = (value) => {
|
|
@@ -5182,29 +5474,54 @@ export class BrowserController {
|
|
|
5182
5474
|
return await this.page.evaluate(() => {
|
|
5183
5475
|
const LABEL_PHRASES = [
|
|
5184
5476
|
// Generic
|
|
5185
|
-
"api key",
|
|
5186
|
-
"
|
|
5187
|
-
"
|
|
5477
|
+
"api key",
|
|
5478
|
+
"api token",
|
|
5479
|
+
"api secret",
|
|
5480
|
+
"secret key",
|
|
5481
|
+
"access key",
|
|
5482
|
+
"access token",
|
|
5483
|
+
"auth token",
|
|
5484
|
+
"bearer token",
|
|
5485
|
+
"personal access token",
|
|
5486
|
+
"client id",
|
|
5487
|
+
"client secret",
|
|
5488
|
+
"client key",
|
|
5188
5489
|
// Cloudinary
|
|
5189
|
-
"cloud name",
|
|
5490
|
+
"cloud name",
|
|
5491
|
+
"cloudname",
|
|
5190
5492
|
// Algolia
|
|
5191
|
-
"application id",
|
|
5192
|
-
"
|
|
5493
|
+
"application id",
|
|
5494
|
+
"app id",
|
|
5495
|
+
"admin api key",
|
|
5496
|
+
"search api key",
|
|
5497
|
+
"monitoring api key",
|
|
5498
|
+
"search-only api key",
|
|
5193
5499
|
// Twilio
|
|
5194
|
-
"account sid",
|
|
5500
|
+
"account sid",
|
|
5501
|
+
"auth token",
|
|
5195
5502
|
// Stripe
|
|
5196
|
-
"publishable key",
|
|
5503
|
+
"publishable key",
|
|
5504
|
+
"secret key",
|
|
5197
5505
|
// AWS
|
|
5198
|
-
"access key id",
|
|
5506
|
+
"access key id",
|
|
5507
|
+
"secret access key",
|
|
5199
5508
|
// OAuth1
|
|
5200
|
-
"consumer key",
|
|
5509
|
+
"consumer key",
|
|
5510
|
+
"consumer secret",
|
|
5511
|
+
"access token secret",
|
|
5201
5512
|
// Misc
|
|
5202
|
-
"project api key",
|
|
5203
|
-
"
|
|
5513
|
+
"project api key",
|
|
5514
|
+
"personal api key",
|
|
5515
|
+
"organization id",
|
|
5516
|
+
"org id",
|
|
5517
|
+
"app key",
|
|
5518
|
+
"app secret",
|
|
5204
5519
|
// Pusher (and other keys tables) label fields bare: key / secret /
|
|
5205
5520
|
// cluster. Without these the value inherits the nearest recognized
|
|
5206
5521
|
// label (the app_id field), mislabeling key + secret as "app id".
|
|
5207
|
-
"cluster",
|
|
5522
|
+
"cluster",
|
|
5523
|
+
"key",
|
|
5524
|
+
"secret",
|
|
5208
5525
|
];
|
|
5209
5526
|
const isVisible = (el) => {
|
|
5210
5527
|
const r = el.getBoundingClientRect();
|
|
@@ -5397,14 +5714,11 @@ export class BrowserController {
|
|
|
5397
5714
|
}
|
|
5398
5715
|
// 1. <input> / <textarea> values (visible only).
|
|
5399
5716
|
document.querySelectorAll("input, textarea").forEach((el) => {
|
|
5400
|
-
if (el instanceof HTMLInputElement &&
|
|
5401
|
-
(el.type === "hidden" || el.type === "password"))
|
|
5717
|
+
if (el instanceof HTMLInputElement && (el.type === "hidden" || el.type === "password"))
|
|
5402
5718
|
return;
|
|
5403
5719
|
if (!isVisible(el))
|
|
5404
5720
|
return;
|
|
5405
|
-
const value = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement
|
|
5406
|
-
? el.value
|
|
5407
|
-
: "";
|
|
5721
|
+
const value = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement ? el.value : "";
|
|
5408
5722
|
if (value.length > 0)
|
|
5409
5723
|
pushCandidate(value, el);
|
|
5410
5724
|
});
|
|
@@ -5426,9 +5740,7 @@ export class BrowserController {
|
|
|
5426
5740
|
});
|
|
5427
5741
|
// 3. Structural containers (code/pre/kbd) where the credential
|
|
5428
5742
|
// is interpolated through nested spans.
|
|
5429
|
-
document
|
|
5430
|
-
.querySelectorAll('code, pre, kbd, samp, [role="textbox"]')
|
|
5431
|
-
.forEach((el) => {
|
|
5743
|
+
document.querySelectorAll('code, pre, kbd, samp, [role="textbox"]').forEach((el) => {
|
|
5432
5744
|
if (!isVisible(el))
|
|
5433
5745
|
return;
|
|
5434
5746
|
const full = (el.textContent ?? "").trim();
|
|
@@ -5488,9 +5800,7 @@ export class BrowserController {
|
|
|
5488
5800
|
return;
|
|
5489
5801
|
masked.push({ el, row: rowAncestor(el) });
|
|
5490
5802
|
});
|
|
5491
|
-
document
|
|
5492
|
-
.querySelectorAll('input[type="password"]')
|
|
5493
|
-
.forEach((el) => {
|
|
5803
|
+
document.querySelectorAll('input[type="password"]').forEach((el) => {
|
|
5494
5804
|
if (!isVisible(el))
|
|
5495
5805
|
return;
|
|
5496
5806
|
masked.push({ el, row: rowAncestor(el) });
|
|
@@ -5576,7 +5886,12 @@ export class BrowserController {
|
|
|
5576
5886
|
const btn = showBtns[0];
|
|
5577
5887
|
const sel = selectorFor(btn);
|
|
5578
5888
|
selectors.push(sel);
|
|
5579
|
-
const label = (btn.textContent ??
|
|
5889
|
+
const label = (btn.textContent ??
|
|
5890
|
+
btn.getAttribute("aria-label") ??
|
|
5891
|
+
btn.getAttribute("title") ??
|
|
5892
|
+
"")
|
|
5893
|
+
.trim()
|
|
5894
|
+
.slice(0, 40);
|
|
5580
5895
|
diagnostic.push(`row→show:"${label}"→${sel}`);
|
|
5581
5896
|
}
|
|
5582
5897
|
else if (copyBtns.length > 0) {
|
|
@@ -5624,9 +5939,7 @@ export class BrowserController {
|
|
|
5624
5939
|
!["text", "search", "url", "tel", "number", "email", ""].includes(el.type)) {
|
|
5625
5940
|
return;
|
|
5626
5941
|
}
|
|
5627
|
-
const value = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement
|
|
5628
|
-
? el.value
|
|
5629
|
-
: "";
|
|
5942
|
+
const value = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement ? el.value : "";
|
|
5630
5943
|
if (value.trim().length > 0 && isVisible(el))
|
|
5631
5944
|
out.push(value.trim());
|
|
5632
5945
|
});
|
|
@@ -5650,9 +5963,7 @@ export class BrowserController {
|
|
|
5650
5963
|
// <span>s — the loop above sees an empty direct-text and skips
|
|
5651
5964
|
// them. Push the full textContent so a UUID built as
|
|
5652
5965
|
// <code><span>7</span><span>5</span>…</code> is still scannable.
|
|
5653
|
-
document
|
|
5654
|
-
.querySelectorAll('code, pre, kbd, samp, [role="textbox"]')
|
|
5655
|
-
.forEach((el) => {
|
|
5966
|
+
document.querySelectorAll('code, pre, kbd, samp, [role="textbox"]').forEach((el) => {
|
|
5656
5967
|
if (!isVisible(el))
|
|
5657
5968
|
return;
|
|
5658
5969
|
const full = (el.textContent ?? "").trim();
|
|
@@ -5881,9 +6192,7 @@ export class BrowserController {
|
|
|
5881
6192
|
// (e.g. lazy-rendering the previously-blocked OAuth chooser).
|
|
5882
6193
|
// Try networkidle first for SPA re-renders, fall back to a
|
|
5883
6194
|
// fixed dwell.
|
|
5884
|
-
await this.page
|
|
5885
|
-
.waitForLoadState("networkidle", { timeout: 3000 })
|
|
5886
|
-
.catch(() => undefined);
|
|
6195
|
+
await this.page.waitForLoadState("networkidle", { timeout: 3000 }).catch(() => undefined);
|
|
5887
6196
|
await this.page.waitForTimeout(800);
|
|
5888
6197
|
return target.text;
|
|
5889
6198
|
}
|
|
@@ -6110,9 +6419,7 @@ export class BrowserController {
|
|
|
6110
6419
|
if (r.width < 2 || r.height < 2)
|
|
6111
6420
|
return false;
|
|
6112
6421
|
const s = window.getComputedStyle(el);
|
|
6113
|
-
return (s.display !== "none" &&
|
|
6114
|
-
s.visibility !== "hidden" &&
|
|
6115
|
-
parseFloat(s.opacity || "1") > 0.01);
|
|
6422
|
+
return (s.display !== "none" && s.visibility !== "hidden" && parseFloat(s.opacity || "1") > 0.01);
|
|
6116
6423
|
};
|
|
6117
6424
|
// G12 — visually-hidden checkbox/radio surfacing. Custom-styled
|
|
6118
6425
|
// TOS checkboxes are real `<input type=checkbox>` elements with
|
|
@@ -6246,11 +6553,15 @@ export class BrowserController {
|
|
|
6246
6553
|
const id = el.getAttribute("id");
|
|
6247
6554
|
const name = el.getAttribute("name");
|
|
6248
6555
|
if (testId !== null && testId.length > 0) {
|
|
6249
|
-
const attr = el.hasAttribute("data-testid")
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6556
|
+
const attr = el.hasAttribute("data-testid")
|
|
6557
|
+
? "data-testid"
|
|
6558
|
+
: el.hasAttribute("data-test-id")
|
|
6559
|
+
? "data-test-id"
|
|
6560
|
+
: el.hasAttribute("data-test")
|
|
6561
|
+
? "data-test"
|
|
6562
|
+
: el.hasAttribute("data-cy")
|
|
6563
|
+
? "data-cy"
|
|
6564
|
+
: "data-qa";
|
|
6254
6565
|
base = `[${attr}="${CSS.escape(testId)}"]`;
|
|
6255
6566
|
}
|
|
6256
6567
|
else if (id !== null && /^[A-Za-z][\w-]*$/.test(id)) {
|
|
@@ -6390,8 +6701,12 @@ export class BrowserController {
|
|
|
6390
6701
|
// cursor:pointer but no button/role/input semantics and no div/section
|
|
6391
6702
|
// wrapper, so the SELECTOR walk AND the old div-only scan both missed
|
|
6392
6703
|
// it, leaving the planner no clickable target.
|
|
6393
|
-
const isCardTag = (t) => t === "div" ||
|
|
6394
|
-
t === "
|
|
6704
|
+
const isCardTag = (t) => t === "div" ||
|
|
6705
|
+
t === "li" ||
|
|
6706
|
+
t === "article" ||
|
|
6707
|
+
t === "section" ||
|
|
6708
|
+
t === "label" ||
|
|
6709
|
+
t.includes("-");
|
|
6395
6710
|
// Walk the light DOM AND every open shadow root — a UI-kit chip can
|
|
6396
6711
|
// live inside a web component's shadow tree.
|
|
6397
6712
|
const scanRoot = (root) => {
|
|
@@ -6516,22 +6831,18 @@ export class BrowserController {
|
|
|
6516
6831
|
const pathLabel = isGoogleGSIIframe
|
|
6517
6832
|
? "Continue with Google"
|
|
6518
6833
|
: isFormControlElement(el)
|
|
6519
|
-
? labelFor(el) ?? directLabel(el) ?? iconLabelFor(el)
|
|
6520
|
-
: directLabel(el) ?? labelFor(el) ?? iconLabelFor(el);
|
|
6834
|
+
? (labelFor(el) ?? directLabel(el) ?? iconLabelFor(el))
|
|
6835
|
+
: (directLabel(el) ?? labelFor(el) ?? iconLabelFor(el));
|
|
6521
6836
|
out.push({
|
|
6522
6837
|
tag: isGoogleGSIIframe ? "button" : el.tagName.toLowerCase(),
|
|
6523
6838
|
type: el.getAttribute("type"),
|
|
6524
6839
|
id: el.getAttribute("id"),
|
|
6525
6840
|
name: el.getAttribute("name"),
|
|
6526
6841
|
placeholder: el.getAttribute("placeholder"),
|
|
6527
|
-
ariaLabel: isGoogleGSIIframe
|
|
6528
|
-
? "Continue with Google"
|
|
6529
|
-
: el.getAttribute("aria-label"),
|
|
6842
|
+
ariaLabel: isGoogleGSIIframe ? "Continue with Google" : el.getAttribute("aria-label"),
|
|
6530
6843
|
role: isGoogleGSIIframe ? "button" : el.getAttribute("role"),
|
|
6531
6844
|
labelText: labelFor(el),
|
|
6532
|
-
visibleText: isGoogleGSIIframe
|
|
6533
|
-
? "Continue with Google"
|
|
6534
|
-
: clean(el.textContent),
|
|
6845
|
+
visibleText: isGoogleGSIIframe ? "Continue with Google" : clean(el.textContent),
|
|
6535
6846
|
selector: selectorFor(el),
|
|
6536
6847
|
visible: true,
|
|
6537
6848
|
inViewport: r.top >= 0 &&
|
|
@@ -6573,8 +6884,7 @@ export class BrowserController {
|
|
|
6573
6884
|
// caller wanting to find UNCHECKED checkboxes needs `checked`
|
|
6574
6885
|
// explicitly. The submit-disabled re-plan hint uses this to
|
|
6575
6886
|
// surface concrete unticked candidates to the planner.
|
|
6576
|
-
checked: el instanceof HTMLInputElement &&
|
|
6577
|
-
(el.type === "checkbox" || el.type === "radio")
|
|
6887
|
+
checked: el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")
|
|
6578
6888
|
? el.checked
|
|
6579
6889
|
: null,
|
|
6580
6890
|
// For <select>: the currently-selected option's visible text
|
|
@@ -6600,6 +6910,7 @@ export class BrowserController {
|
|
|
6600
6910
|
? clean(el.options[el.selectedIndex]?.textContent ?? null)
|
|
6601
6911
|
: null,
|
|
6602
6912
|
interactedThisRun: el.getAttribute("data-ts-touched") === "1",
|
|
6913
|
+
sealed: el.getAttribute("data-ts-sealed-payment") === "1",
|
|
6603
6914
|
screenPath: `${container ?? "body:root"} > ${elementKind(el)}:` +
|
|
6604
6915
|
slug(pathLabel, `${elementKind(el)}-${out.length}`),
|
|
6605
6916
|
container,
|
|
@@ -6658,9 +6969,7 @@ export class BrowserController {
|
|
|
6658
6969
|
}
|
|
6659
6970
|
// Race a popup `page` event against the click. context-level
|
|
6660
6971
|
// "page" fires for both window.open popups and target=_blank.
|
|
6661
|
-
const popupPromise = this.context
|
|
6662
|
-
.waitForEvent("page", { timeout: 8000 })
|
|
6663
|
-
.catch(() => null);
|
|
6972
|
+
const popupPromise = this.context.waitForEvent("page", { timeout: 8000 }).catch(() => null);
|
|
6664
6973
|
await this.click(selector);
|
|
6665
6974
|
const popup = await popupPromise;
|
|
6666
6975
|
if (popup !== null && popup !== this.page && !popup.isClosed()) {
|
|
@@ -6706,7 +7015,8 @@ export class BrowserController {
|
|
|
6706
7015
|
// Capture the body of a Clerk/Stytch/WorkOS sign-in/up/callback error
|
|
6707
7016
|
// (>=400) — its error code is the definitive tell (captcha_invalid vs
|
|
6708
7017
|
// transfer vs identifier_*). JSON only, bounded.
|
|
6709
|
-
if (res.status() >= 400 &&
|
|
7018
|
+
if (res.status() >= 400 &&
|
|
7019
|
+
/\/v1\/client\/(sign_ins|sign_ups)|oauth_callback|\/session/i.test(url)) {
|
|
6710
7020
|
entry.body = (await res.text().catch(() => "")).slice(0, 800);
|
|
6711
7021
|
}
|
|
6712
7022
|
this.oauthNetLog.push(entry);
|
|
@@ -6770,7 +7080,15 @@ export class BrowserController {
|
|
|
6770
7080
|
mkdirSync(dir, { recursive: true });
|
|
6771
7081
|
const ts = process.env.OAUTH_DEBUG_TS ?? String(this.oauthNetLog.length);
|
|
6772
7082
|
const path = join(dir, `${service}-${label}-${ts}.json`);
|
|
6773
|
-
writeFileSync(path, JSON.stringify({
|
|
7083
|
+
writeFileSync(path, JSON.stringify({
|
|
7084
|
+
service,
|
|
7085
|
+
label,
|
|
7086
|
+
finalUrl: url,
|
|
7087
|
+
clerkState,
|
|
7088
|
+
cookies: cookieSummary,
|
|
7089
|
+
netLog: this.oauthNetLog,
|
|
7090
|
+
pageText: consoleText.slice(0, 600),
|
|
7091
|
+
}, null, 2));
|
|
6774
7092
|
console.error(`[oauth-debug] wrote ${path} (${cookieSummary.length} cookies, ${this.oauthNetLog.length} net entries)`);
|
|
6775
7093
|
}
|
|
6776
7094
|
catch (err) {
|
|
@@ -6785,9 +7103,7 @@ export class BrowserController {
|
|
|
6785
7103
|
return null;
|
|
6786
7104
|
try {
|
|
6787
7105
|
return await this.page.evaluate(() => {
|
|
6788
|
-
const c = document
|
|
6789
|
-
.querySelector('meta[name="csrf-token"]')
|
|
6790
|
-
?.getAttribute("content");
|
|
7106
|
+
const c = document.querySelector('meta[name="csrf-token"]')?.getAttribute("content");
|
|
6791
7107
|
return c !== null && c !== undefined && c.length > 0 ? c : null;
|
|
6792
7108
|
});
|
|
6793
7109
|
}
|
|
@@ -6959,9 +7275,7 @@ export class BrowserController {
|
|
|
6959
7275
|
// undefined (no-op) and any failure must degrade to the popup/none path.
|
|
6960
7276
|
if (cdp !== null) {
|
|
6961
7277
|
const promptDeadline = Date.now() + Math.min(4_000, timeoutMs);
|
|
6962
|
-
while (Date.now() < promptDeadline &&
|
|
6963
|
-
!fedcmResolved &&
|
|
6964
|
-
this.context.pages().length <= 1) {
|
|
7278
|
+
while (Date.now() < promptDeadline && !fedcmResolved && this.context.pages().length <= 1) {
|
|
6965
7279
|
await this.sleep(250);
|
|
6966
7280
|
}
|
|
6967
7281
|
if (!fedcmResolved && this.context.pages().length <= 1) {
|
|
@@ -7252,7 +7566,7 @@ export class BrowserController {
|
|
|
7252
7566
|
const bad = /\b(settings|marketplace|learn more|cancel|skip|back|terms|privacy)\b/i;
|
|
7253
7567
|
const candidates = Array.from(document.querySelectorAll('a[href], button, [role="button"], [role="link"]')).filter((el) => visible(el));
|
|
7254
7568
|
const byHref = candidates.find((el) => {
|
|
7255
|
-
const href = el instanceof HTMLAnchorElement ? el.href : el.getAttribute("href") ?? "";
|
|
7569
|
+
const href = el instanceof HTMLAnchorElement ? el.href : (el.getAttribute("href") ?? "");
|
|
7256
7570
|
return /\/installations\/(?:new|permissions)\b/.test(href);
|
|
7257
7571
|
});
|
|
7258
7572
|
const target = byHref ??
|
|
@@ -7562,7 +7876,9 @@ export class BrowserController {
|
|
|
7562
7876
|
try {
|
|
7563
7877
|
this.xvfb.stop();
|
|
7564
7878
|
}
|
|
7565
|
-
catch {
|
|
7879
|
+
catch {
|
|
7880
|
+
/* best-effort */
|
|
7881
|
+
}
|
|
7566
7882
|
this.xvfb = null;
|
|
7567
7883
|
}
|
|
7568
7884
|
}
|
|
@@ -7880,9 +8196,7 @@ export function scoreSignupButton(text, oauthProviders) {
|
|
|
7880
8196
|
// Bump weight from +5 to +12 so the combined-flow button outranks
|
|
7881
8197
|
// generic nav anchors that score 0. The compensating auth-verb
|
|
7882
8198
|
// penalty below is also suppressed when email is present.
|
|
7883
|
-
const hasEmail = t.includes("continue with email") ||
|
|
7884
|
-
t.includes("sign up with email") ||
|
|
7885
|
-
t.includes("email");
|
|
8199
|
+
const hasEmail = t.includes("continue with email") || t.includes("sign up with email") || t.includes("email");
|
|
7886
8200
|
if (hasEmail) {
|
|
7887
8201
|
score += 12;
|
|
7888
8202
|
}
|