@trusty-squire/mcp 1.1.28 → 1.1.29-rc.2
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/dist/api-client.d.ts +6 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js.map +1 -1
- package/dist/bot/act/act.d.ts.map +1 -1
- package/dist/bot/act/act.js +15 -11
- package/dist/bot/act/act.js.map +1 -1
- package/dist/bot/browser.d.ts +0 -29
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +2 -747
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/capture/capture.d.ts +7 -15
- package/dist/bot/capture/capture.d.ts.map +1 -1
- package/dist/bot/capture/capture.js +71 -253
- package/dist/bot/capture/capture.js.map +1 -1
- package/dist/bot/card-release-approval.d.ts.map +1 -1
- package/dist/bot/card-release-approval.js +11 -2
- package/dist/bot/card-release-approval.js.map +1 -1
- package/dist/bot/credential-capture.d.ts +10 -0
- package/dist/bot/credential-capture.d.ts.map +1 -1
- package/dist/bot/credential-capture.js +5 -0
- package/dist/bot/credential-capture.js.map +1 -1
- package/dist/bot/credential-shape.d.ts +0 -5
- package/dist/bot/credential-shape.d.ts.map +1 -1
- package/dist/bot/credential-shape.js +8 -132
- package/dist/bot/credential-shape.js.map +1 -1
- package/dist/bot/drive-feedback.d.ts +2 -8
- package/dist/bot/drive-feedback.d.ts.map +1 -1
- package/dist/bot/drive-feedback.js +7 -49
- package/dist/bot/drive-feedback.js.map +1 -1
- package/dist/bot/jev-client.d.ts +3 -0
- package/dist/bot/jev-client.d.ts.map +1 -1
- package/dist/bot/jev-client.js +14 -5
- package/dist/bot/jev-client.js.map +1 -1
- package/dist/bot/operate-drive.d.ts +13 -63
- package/dist/bot/operate-drive.d.ts.map +1 -1
- package/dist/bot/operate-drive.js +136 -592
- package/dist/bot/operate-drive.js.map +1 -1
- package/dist/bot/page-driver.js +1 -1
- package/dist/bot/page-driver.js.map +1 -1
- package/dist/bot/provision-session.d.ts +3 -1
- package/dist/bot/provision-session.d.ts.map +1 -1
- package/dist/bot/provision-session.js +1 -1
- package/dist/bot/provision-session.js.map +1 -1
- package/dist/bot/session/lifecycle.d.ts.map +1 -1
- package/dist/bot/session/lifecycle.js +14 -13
- package/dist/bot/session/lifecycle.js.map +1 -1
- package/dist/bot/skill-hint.d.ts.map +1 -1
- package/dist/bot/skill-hint.js +4 -3
- package/dist/bot/skill-hint.js.map +1 -1
- package/dist/install/broker-service.d.ts.map +1 -1
- package/dist/install/broker-service.js +13 -15
- package/dist/install/broker-service.js.map +1 -1
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +43 -32
- package/dist/install/cli.js.map +1 -1
- package/dist/install/connect-report.d.ts +7 -7
- package/dist/install/connect-report.d.ts.map +1 -1
- package/dist/install/connect-report.js +31 -25
- package/dist/install/connect-report.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +42 -3
- package/dist/server.js.map +1 -1
- package/dist/tools/fetch-credential.d.ts.map +1 -1
- package/dist/tools/fetch-credential.js +3 -1
- package/dist/tools/fetch-credential.js.map +1 -1
- package/dist/tools/provision-drive-schema.d.ts +0 -1
- package/dist/tools/provision-drive-schema.d.ts.map +1 -1
- package/dist/tools/provision-drive-schema.js +125 -238
- package/dist/tools/provision-drive-schema.js.map +1 -1
- package/dist/tools/provision-drive.d.ts +217 -73
- package/dist/tools/provision-drive.d.ts.map +1 -1
- package/dist/tools/provision-drive.js +133 -180
- package/dist/tools/provision-drive.js.map +1 -1
- package/dist/tools/store-credential.d.ts.map +1 -1
- package/dist/tools/store-credential.js +5 -1
- package/dist/tools/store-credential.js.map +1 -1
- package/dist/tools/use-credential.js +2 -2
- package/package.json +1 -1
- package/dist/bot/credential-text.d.ts +0 -4
- package/dist/bot/credential-text.d.ts.map +0 -1
- package/dist/bot/credential-text.js +0 -296
- package/dist/bot/credential-text.js.map +0 -1
- package/dist/bot/extraction.d.ts +0 -18
- package/dist/bot/extraction.d.ts.map +0 -1
- package/dist/bot/extraction.js +0 -54
- package/dist/bot/extraction.js.map +0 -1
package/dist/bot/browser.js
CHANGED
|
@@ -513,8 +513,6 @@ export class BrowserController {
|
|
|
513
513
|
return "closed";
|
|
514
514
|
}
|
|
515
515
|
async initializePages(context, hardened, remoteMode) {
|
|
516
|
-
// Speed: optionally abort heavy/irrelevant requests before any navigation.
|
|
517
|
-
await this.installResourceBlocking();
|
|
518
516
|
const contextInitScripts = contextInitScriptsFor({ hardened, remoteMode });
|
|
519
517
|
// Never register context init scripts under patchright. Its injection path
|
|
520
518
|
// rewrites text/html after decoding the response as UTF-8, corrupting
|
|
@@ -740,54 +738,6 @@ export class BrowserController {
|
|
|
740
738
|
get stealthProfile() {
|
|
741
739
|
return this.processOwner.stealthProfile;
|
|
742
740
|
}
|
|
743
|
-
// Resource blocking for speed (BOT_BLOCK_RESOURCES, default OFF). Aborts
|
|
744
|
-
// image/media/font requests to cut page-load wall-clock. Exempt captcha/challenge + payment
|
|
745
|
-
// scripts (blocking those breaks the Turnstile/hCaptcha token poll and the
|
|
746
|
-
// signup form). CSS + first-party JS are never blocked (not in BLOCK_TYPES) —
|
|
747
|
-
// the SPA form renders from them and the vision planner reads the styled
|
|
748
|
-
// render. DUAL RISK, hence default-OFF + an OF#2 A/B before flipping on:
|
|
749
|
-
// (1) a browser that loads ZERO images is itself an anti-bot fingerprint;
|
|
750
|
-
// (2) the screenshot the vision planner reads loses detail — mitigated
|
|
751
|
-
// because the DOM inventory is the authoritative action space, but
|
|
752
|
-
// still a regression risk on image-only affordances.
|
|
753
|
-
// Registered on the CONTEXT so it covers OAuth popups + iframes.
|
|
754
|
-
async installResourceBlocking() {
|
|
755
|
-
const ctx = this.context;
|
|
756
|
-
if (ctx === null)
|
|
757
|
-
return;
|
|
758
|
-
if (!/^(1|true|on)$/i.test(process.env.BOT_BLOCK_RESOURCES ?? ""))
|
|
759
|
-
return;
|
|
760
|
-
const BLOCK_TYPES = new Set(["image", "media", "font"]);
|
|
761
|
-
// NEVER block — these break signup (captcha/challenge widgets + payment SDK).
|
|
762
|
-
const ALWAYS_ALLOW = [
|
|
763
|
-
"challenges.cloudflare.com",
|
|
764
|
-
"turnstile",
|
|
765
|
-
"hcaptcha.com",
|
|
766
|
-
"newassets.hcaptcha.com",
|
|
767
|
-
"recaptcha",
|
|
768
|
-
"gstatic.com/recaptcha",
|
|
769
|
-
"js.stripe.com",
|
|
770
|
-
];
|
|
771
|
-
await ctx.route("**/*", async (route) => {
|
|
772
|
-
try {
|
|
773
|
-
const url = route.request().url();
|
|
774
|
-
if (ALWAYS_ALLOW.some((h) => url.includes(h))) {
|
|
775
|
-
await route.continue();
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
const type = route.request().resourceType();
|
|
779
|
-
if (BLOCK_TYPES.has(type)) {
|
|
780
|
-
await route.abort();
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
await route.continue();
|
|
784
|
-
}
|
|
785
|
-
catch {
|
|
786
|
-
// Routing race / already-handled — never let a decision crash nav.
|
|
787
|
-
}
|
|
788
|
-
});
|
|
789
|
-
this.logOperatorDiagnostic("[operator] resource blocking ON (image/media/font aborted; captcha/CSS/JS allowed)");
|
|
790
|
-
}
|
|
791
741
|
async start() {
|
|
792
742
|
// A satellite's page is already attached by attachSessionPage() — there is
|
|
793
743
|
// no process for it to start.
|
|
@@ -3840,51 +3790,8 @@ export class BrowserController {
|
|
|
3840
3790
|
await cdp.detach().catch(() => undefined);
|
|
3841
3791
|
}
|
|
3842
3792
|
}
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
throw new Error("Browser not started");
|
|
3846
|
-
// page.content() / page.title() / screenshot() all throw
|
|
3847
|
-
// "Execution context was destroyed" when the page is mid-
|
|
3848
|
-
// navigation — common after an OAuth-button click that kicks off
|
|
3849
|
-
// a 3-5 hop redirect chain (sentry.io → accounts.google.com →
|
|
3850
|
-
// consent → callback → onboarding). Retry once after a short
|
|
3851
|
-
// settle: most navigations finish in <500ms even on slow links.
|
|
3852
|
-
try {
|
|
3853
|
-
return await this.snapshotState(page);
|
|
3854
|
-
}
|
|
3855
|
-
catch {
|
|
3856
|
-
await this.wait(0.8);
|
|
3857
|
-
return await this.snapshotState(page);
|
|
3858
|
-
}
|
|
3859
|
-
}
|
|
3860
|
-
async snapshotState(page = this.page) {
|
|
3861
|
-
if (!page)
|
|
3862
|
-
throw new Error("Browser not started");
|
|
3863
|
-
return {
|
|
3864
|
-
url: page.url(),
|
|
3865
|
-
title: await page.title(),
|
|
3866
|
-
html: await page.content(),
|
|
3867
|
-
screenshot: await page
|
|
3868
|
-
.screenshot({ fullPage: false, type: "jpeg", quality: 70, timeout: 8_000 })
|
|
3869
|
-
.then((shot) => shot.toString("base64"))
|
|
3870
|
-
.catch(() => ""),
|
|
3871
|
-
};
|
|
3872
|
-
}
|
|
3873
|
-
async extractText() {
|
|
3874
|
-
if (!this.page)
|
|
3875
|
-
throw new Error("Browser not started");
|
|
3876
|
-
return (await this.page.textContent("body")) || "";
|
|
3877
|
-
}
|
|
3878
|
-
// RENDERED, visibility-respecting body text. extractText() reads
|
|
3879
|
-
// textContent("body"), which includes display:none / visibility:hidden /
|
|
3880
|
-
// off-screen nodes — so a fully-rendered dashboard whose DOM merely
|
|
3881
|
-
// CONTAINS a hidden skeleton / "Loading…" / "Please wait 30 seconds…"
|
|
3882
|
-
// string (Next.js RSC inline payloads, lazy placeholders, aria-hidden
|
|
3883
|
-
// spinners) reads as still-loading and false-trips the loading-shell gate.
|
|
3884
|
-
// innerText is layout-aware: it omits hidden text and reflects what a user
|
|
3885
|
-
// would actually see. Use this for the SHELL decision ONLY — credential/key
|
|
3886
|
-
// extraction and wall-text checks deliberately read RAW text via
|
|
3887
|
-
// extractText() and must stay byte-identical, so this is purely additive.
|
|
3793
|
+
// Visible page text omits hidden skeletons and placeholders that could
|
|
3794
|
+
// otherwise make a rendered page look like it is still loading.
|
|
3888
3795
|
async extractVisibleText(page = this.page) {
|
|
3889
3796
|
if (page === null)
|
|
3890
3797
|
throw new Error("Browser not started");
|
|
@@ -4195,20 +4102,6 @@ export class BrowserController {
|
|
|
4195
4102
|
return false;
|
|
4196
4103
|
}
|
|
4197
4104
|
}
|
|
4198
|
-
// Discrete strings an API key might occupy — for credential
|
|
4199
|
-
// extraction. Gathered so a key is read WHOLE and un-glued from its
|
|
4200
|
-
// neighbours: extractText() concatenates the whole <body>, which
|
|
4201
|
-
// fuses a key to an adjacent "Copy"/"Done" button with no separator.
|
|
4202
|
-
//
|
|
4203
|
-
// Two surfaces:
|
|
4204
|
-
// 1. input/textarea VALUES — a copy-to-clipboard key field. An
|
|
4205
|
-
// input's value is not in textContent at all. Hidden and
|
|
4206
|
-
// password fields are excluded (captcha tokens / the signup
|
|
4207
|
-
// password), keeping this a clean credential surface.
|
|
4208
|
-
// 2. Each element's OWN direct text — the text nodes that are its
|
|
4209
|
-
// immediate children, excluding descendants. A key in a
|
|
4210
|
-
// <code>/<span>/<div> yields its clean value here even when a
|
|
4211
|
-
// sibling button shares the same parent.
|
|
4212
4105
|
// F10: read the clipboard contents (typically populated by the
|
|
4213
4106
|
// user-modal's Copy button — every modern API-key reveal modal puts
|
|
4214
4107
|
// the full secret here while displaying a masked stub). Requires
|
|
@@ -4247,644 +4140,6 @@ export class BrowserController {
|
|
|
4247
4140
|
}
|
|
4248
4141
|
});
|
|
4249
4142
|
}
|
|
4250
|
-
// F10 fallback: ALL <input> / <textarea> values, ignoring
|
|
4251
|
-
// visibility and type filters. extractCredentialCandidates
|
|
4252
|
-
// deliberately skips `type=hidden` / `type=password` / invisible
|
|
4253
|
-
// elements (correct for general candidate scanning), but some
|
|
4254
|
-
// API-key modals stash the full key in a hidden input the masked
|
|
4255
|
-
// display reads from — and that needs to be reachable when the
|
|
4256
|
-
// visible extraction comes back truncated.
|
|
4257
|
-
async extractAllInputValues(page = this.page) {
|
|
4258
|
-
if (!page)
|
|
4259
|
-
throw new Error("Browser not started");
|
|
4260
|
-
return await page.evaluate(() => {
|
|
4261
|
-
const out = [];
|
|
4262
|
-
document.querySelectorAll("input, textarea").forEach((el) => {
|
|
4263
|
-
if (!(el instanceof HTMLInputElement) && !(el instanceof HTMLTextAreaElement))
|
|
4264
|
-
return;
|
|
4265
|
-
// Only text-shaped controls RENDER a value. A checkbox/radio/button's
|
|
4266
|
-
// `value` is a markup constant (or the control's id) and must never be
|
|
4267
|
-
// read as a credential candidate.
|
|
4268
|
-
if (el instanceof HTMLInputElement &&
|
|
4269
|
-
!["text", "search", "url", "tel", "number", "email", ""].includes(el.type))
|
|
4270
|
-
return;
|
|
4271
|
-
const value = el.value;
|
|
4272
|
-
if (value.trim().length > 0)
|
|
4273
|
-
out.push(value.trim());
|
|
4274
|
-
});
|
|
4275
|
-
return out;
|
|
4276
|
-
});
|
|
4277
|
-
}
|
|
4278
|
-
// Last-resort scan: walk innerText looking for credential-shaped
|
|
4279
|
-
// tokens (UUIDs and other long alnum+hyphen blobs) inside any DOM
|
|
4280
|
-
// subtree that ALSO contains a "Copy" / "Copy token" / "Copy to
|
|
4281
|
-
// clipboard" affordance. The Copy-button colocation is what tells
|
|
4282
|
-
// us "the UI is presenting this string AS a credential" — without
|
|
4283
|
-
// it, we'd false-positive on session IDs in URLs, cache-buster
|
|
4284
|
-
// query params, etc. Returns every match it finds; the caller picks
|
|
4285
|
-
// the first that survives extractApiKeyFromText.
|
|
4286
|
-
async extractCredentialsNearCopyButtons(page = this.page) {
|
|
4287
|
-
if (!page)
|
|
4288
|
-
throw new Error("Browser not started");
|
|
4289
|
-
return await page.evaluate(() => {
|
|
4290
|
-
const out = [];
|
|
4291
|
-
const isVisible = (el) => {
|
|
4292
|
-
const r = el.getBoundingClientRect();
|
|
4293
|
-
return r.width > 2 && r.height > 2;
|
|
4294
|
-
};
|
|
4295
|
-
// Find every Copy- OR reveal-class affordance. A secret table-cell value
|
|
4296
|
-
// (deepinfra's keys table) lives in a row next to BOTH a copy and a
|
|
4297
|
-
// toggle-visibility/reveal control — but those are often icon buttons
|
|
4298
|
-
// whose accessible NAME is the row's date, not "copy". So match the
|
|
4299
|
-
// element's id / class / data-testid too, which carry the semantic name
|
|
4300
|
-
// ("copy-key", "toggle-token-visibility"). Reveal patterns are scoped to
|
|
4301
|
-
// key/token/secret/visibility context so a generic "Show more" doesn't
|
|
4302
|
-
// anchor a harvest.
|
|
4303
|
-
const copyButtons = Array.from(document.querySelectorAll('button, [role="button"], a, [aria-label]')).filter((el) => {
|
|
4304
|
-
if (!isVisible(el))
|
|
4305
|
-
return false;
|
|
4306
|
-
const name = `${el.textContent ?? ""} ${el.getAttribute("aria-label") ?? ""} ${el.getAttribute("title") ?? ""}`.toLowerCase();
|
|
4307
|
-
// el.className is an SVGAnimatedString on SVG elements — read via attr.
|
|
4308
|
-
const attrs = `${el.id} ${el.getAttribute("class") ?? ""} ${el.getAttribute("data-testid") ?? ""} ${el.getAttribute("data-test") ?? ""}`.toLowerCase();
|
|
4309
|
-
const hay = `${name} ${attrs}`;
|
|
4310
|
-
return /\bcopy\b|clipboard|reveal|toggle[-_ ]?(?:token|visibility)|show[-_ ]?(?:key|token|secret|api)/.test(hay);
|
|
4311
|
-
});
|
|
4312
|
-
// For each, walk up a few ancestors and dump the subtree's
|
|
4313
|
-
// innerText. The token is somewhere in there.
|
|
4314
|
-
const seen = new Set();
|
|
4315
|
-
const harvest = (text) => {
|
|
4316
|
-
if (text.length === 0 || text.length > 4096)
|
|
4317
|
-
return;
|
|
4318
|
-
// Tokenize by whitespace — each token is a separate candidate.
|
|
4319
|
-
text.split(/\s+/).forEach((tok) => {
|
|
4320
|
-
if (tok.length < 16 || tok.length > 256)
|
|
4321
|
-
return;
|
|
4322
|
-
if (seen.has(tok))
|
|
4323
|
-
return;
|
|
4324
|
-
seen.add(tok);
|
|
4325
|
-
out.push(tok);
|
|
4326
|
-
});
|
|
4327
|
-
};
|
|
4328
|
-
for (const btn of copyButtons) {
|
|
4329
|
-
// The value often lives in the copy button's OWN aria-label/title
|
|
4330
|
-
// ("Copy to clipboard: GOCSPX-…", "Copy api key sk-…") rather than in
|
|
4331
|
-
// any visible text node — GCP's new client-secret reveal does exactly
|
|
4332
|
-
// this, so the innerText-only walk below would miss it entirely.
|
|
4333
|
-
harvest(`${btn.getAttribute("aria-label") ?? ""} ${btn.getAttribute("title") ?? ""}`.trim());
|
|
4334
|
-
// Then walk up a few ancestors and dump the subtree's innerText.
|
|
4335
|
-
let anc = btn;
|
|
4336
|
-
for (let i = 0; i < 6 && anc !== null; i++) {
|
|
4337
|
-
anc = anc.parentElement;
|
|
4338
|
-
}
|
|
4339
|
-
if (anc === null)
|
|
4340
|
-
continue;
|
|
4341
|
-
harvest((anc.innerText ?? "").trim());
|
|
4342
|
-
}
|
|
4343
|
-
return out;
|
|
4344
|
-
});
|
|
4345
|
-
}
|
|
4346
|
-
// DOM-proximity labeled credential candidates. Walks every visible
|
|
4347
|
-
// input/code/text element looking for credential-shape strings,
|
|
4348
|
-
// pairs each one with its nearest credential-label text in the DOM
|
|
4349
|
-
// tree, and returns the labeled tuples for the multi-cred extractor
|
|
4350
|
-
// to fold into the credentials Record.
|
|
4351
|
-
//
|
|
4352
|
-
// Complements the Phase E planner-quoted extractor — when the
|
|
4353
|
-
// planner's prose doesn't explicitly label values (multi-cred page
|
|
4354
|
-
// where the planner missed one), this DOM-grounded pass picks them
|
|
4355
|
-
// up via the visible labels the page itself renders.
|
|
4356
|
-
//
|
|
4357
|
-
// Returns shape:
|
|
4358
|
-
// { value: "<credential-shape string>",
|
|
4359
|
-
// label: "<the closest matching label text>" | null,
|
|
4360
|
-
// isMasked: true if the value looks like a redacted display
|
|
4361
|
-
// (••••, ****, contains "•" or runs of "*") }
|
|
4362
|
-
//
|
|
4363
|
-
// The caller maps label
|
|
4364
|
-
// text to canonical credential keys using the same vocabulary the
|
|
4365
|
-
// Phase E parser uses.
|
|
4366
|
-
async extractLabeledCredentialCandidates(page = this.page) {
|
|
4367
|
-
if (!page)
|
|
4368
|
-
throw new Error("Browser not started");
|
|
4369
|
-
return await page.evaluate(() => {
|
|
4370
|
-
const LABEL_PHRASES = [
|
|
4371
|
-
// Generic
|
|
4372
|
-
"api key",
|
|
4373
|
-
"api token",
|
|
4374
|
-
"api secret",
|
|
4375
|
-
"secret key",
|
|
4376
|
-
"access key",
|
|
4377
|
-
"access token",
|
|
4378
|
-
"auth token",
|
|
4379
|
-
"bearer token",
|
|
4380
|
-
"personal access token",
|
|
4381
|
-
"client id",
|
|
4382
|
-
"client secret",
|
|
4383
|
-
"team id",
|
|
4384
|
-
"project id",
|
|
4385
|
-
"client key",
|
|
4386
|
-
// Cloudinary
|
|
4387
|
-
"cloud name",
|
|
4388
|
-
"cloudname",
|
|
4389
|
-
// Algolia
|
|
4390
|
-
"application id",
|
|
4391
|
-
"app id",
|
|
4392
|
-
"admin api key",
|
|
4393
|
-
"search api key",
|
|
4394
|
-
"monitoring api key",
|
|
4395
|
-
"search-only api key",
|
|
4396
|
-
// Twilio
|
|
4397
|
-
"account sid",
|
|
4398
|
-
"auth token",
|
|
4399
|
-
// Stripe
|
|
4400
|
-
"publishable key",
|
|
4401
|
-
"secret key",
|
|
4402
|
-
// AWS
|
|
4403
|
-
"access key id",
|
|
4404
|
-
"secret access key",
|
|
4405
|
-
// OAuth1
|
|
4406
|
-
"consumer key",
|
|
4407
|
-
"consumer secret",
|
|
4408
|
-
"access token secret",
|
|
4409
|
-
// Misc
|
|
4410
|
-
"project api key",
|
|
4411
|
-
"personal api key",
|
|
4412
|
-
"organization id",
|
|
4413
|
-
"org id",
|
|
4414
|
-
"app key",
|
|
4415
|
-
"app secret",
|
|
4416
|
-
// Pusher (and other keys tables) label fields bare: key / secret /
|
|
4417
|
-
// cluster. Without these the value inherits the nearest recognized
|
|
4418
|
-
// label (the app_id field), mislabeling key + secret as "app id".
|
|
4419
|
-
"cluster",
|
|
4420
|
-
"key",
|
|
4421
|
-
"secret",
|
|
4422
|
-
];
|
|
4423
|
-
const isVisible = (el) => {
|
|
4424
|
-
const r = el.getBoundingClientRect();
|
|
4425
|
-
return r.width > 2 && r.height > 2;
|
|
4426
|
-
};
|
|
4427
|
-
const isCredentialShape = (s) => {
|
|
4428
|
-
// Reasonable credential length range
|
|
4429
|
-
if (s.length < 6 || s.length > 256)
|
|
4430
|
-
return false;
|
|
4431
|
-
// Reject pure prose (spaces inside)
|
|
4432
|
-
if (/\s/.test(s))
|
|
4433
|
-
return false;
|
|
4434
|
-
// Must include some entropy markers: digit + letter combo OR
|
|
4435
|
-
// a credential prefix like sk_/pk_/api_/ etc.
|
|
4436
|
-
const hasDigit = /\d/.test(s);
|
|
4437
|
-
const hasLetter = /[A-Za-z]/.test(s);
|
|
4438
|
-
if (!hasDigit && !hasLetter)
|
|
4439
|
-
return false;
|
|
4440
|
-
// Reject pure URL fragments
|
|
4441
|
-
if (/^https?:\/\//i.test(s))
|
|
4442
|
-
return false;
|
|
4443
|
-
// Reject simple words / capitalized phrases
|
|
4444
|
-
if (/^[A-Za-z]+$/.test(s) && s.length < 12)
|
|
4445
|
-
return false;
|
|
4446
|
-
// Reject label-text masquerading as a value: a short token of only
|
|
4447
|
-
// letters + separators with NO digit (e.g. the literal "app_id" /
|
|
4448
|
-
// "secret" label text pusher renders next to the real value). Real
|
|
4449
|
-
// credentials carry a digit or are long; field labels don't.
|
|
4450
|
-
if (!hasDigit && /^[a-z][a-z_-]*$/i.test(s) && s.length < 16)
|
|
4451
|
-
return false;
|
|
4452
|
-
return true;
|
|
4453
|
-
};
|
|
4454
|
-
// Inline mirror of credential-shape.ts MASKED_DISPLAY_RE — page.evaluate
|
|
4455
|
-
// code can't import, so keep this regex byte-identical to the canonical.
|
|
4456
|
-
// Any mask glyph: bullet/circle, 3+ asterisks, ellipsis, or 3+ dots. (Was
|
|
4457
|
-
// `[•●⬤]{3,}|\*{4,}`, which MISSED the ellipsis masks GCP/Zilliz/S3 use.)
|
|
4458
|
-
const isMaskedShape = (s) => /[•●⬤]|\*{3,}|…|\.{3,}/.test(s);
|
|
4459
|
-
// Compute element-center coords for proximity matching.
|
|
4460
|
-
const centerOf = (el) => {
|
|
4461
|
-
const r = el.getBoundingClientRect();
|
|
4462
|
-
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
4463
|
-
};
|
|
4464
|
-
const labelHits = [];
|
|
4465
|
-
document.querySelectorAll("body *").forEach((el) => {
|
|
4466
|
-
if (el.tagName === "SCRIPT" || el.tagName === "STYLE")
|
|
4467
|
-
return;
|
|
4468
|
-
if (!isVisible(el))
|
|
4469
|
-
return;
|
|
4470
|
-
// Only consider DIRECT text content — child element text gets
|
|
4471
|
-
// claimed by THOSE elements' own label scans.
|
|
4472
|
-
let direct = "";
|
|
4473
|
-
el.childNodes.forEach((n) => {
|
|
4474
|
-
if (n.nodeType === Node.TEXT_NODE)
|
|
4475
|
-
direct += n.textContent ?? "";
|
|
4476
|
-
});
|
|
4477
|
-
direct = direct.trim().toLowerCase();
|
|
4478
|
-
if (direct.length === 0 || direct.length > 100)
|
|
4479
|
-
return;
|
|
4480
|
-
// Normalize separators so underscore/hyphen field labels match the
|
|
4481
|
-
// space-form phrases: pusher renders "app_id" / "app_key" / "app_secret"
|
|
4482
|
-
// and "app_id".includes("app id") is false, so every value used to
|
|
4483
|
-
// inherit a far "App keys" heading. With this, each field label matches
|
|
4484
|
-
// its own phrase and wins the proximity match.
|
|
4485
|
-
const directNorm = direct.replace(/[_-]+/g, " ");
|
|
4486
|
-
for (const phrase of LABEL_PHRASES) {
|
|
4487
|
-
if (directNorm.includes(phrase)) {
|
|
4488
|
-
const c = centerOf(el);
|
|
4489
|
-
labelHits.push({ phrase, x: c.x, y: c.y, el });
|
|
4490
|
-
break; // one label per element is enough
|
|
4491
|
-
}
|
|
4492
|
-
}
|
|
4493
|
-
});
|
|
4494
|
-
// Detect reveal buttons (eye / show / unmask icons) — any visible
|
|
4495
|
-
// button or [role=button] / svg whose aria-label / title / text
|
|
4496
|
-
// matches the reveal vocabulary. We only check WHETHER one exists
|
|
4497
|
-
// near a candidate; the clicker (revealMaskedCredentials below)
|
|
4498
|
-
// does the actual click pass.
|
|
4499
|
-
const REVEAL_PATTERN = /\b(?:reveal|show|unmask|view|toggle|copy)\b/i;
|
|
4500
|
-
const revealButtons = [];
|
|
4501
|
-
document
|
|
4502
|
-
.querySelectorAll('button, [role="button"], a, [aria-label], [title]')
|
|
4503
|
-
.forEach((el) => {
|
|
4504
|
-
if (!isVisible(el))
|
|
4505
|
-
return;
|
|
4506
|
-
const hay = `${el.textContent ?? ""} ${el.getAttribute("aria-label") ?? ""} ${el.getAttribute("title") ?? ""}`;
|
|
4507
|
-
if (!REVEAL_PATTERN.test(hay))
|
|
4508
|
-
return;
|
|
4509
|
-
const c = centerOf(el);
|
|
4510
|
-
revealButtons.push({ x: c.x, y: c.y, el });
|
|
4511
|
-
});
|
|
4512
|
-
// For each candidate, find nearest label by Euclidean distance.
|
|
4513
|
-
const findNearestLabel = (x, y) => {
|
|
4514
|
-
let best = null;
|
|
4515
|
-
for (const lh of labelHits) {
|
|
4516
|
-
const dx = lh.x - x;
|
|
4517
|
-
const dy = lh.y - y;
|
|
4518
|
-
const d = Math.sqrt(dx * dx + dy * dy);
|
|
4519
|
-
// Conservative cap — labels more than 400px away from the
|
|
4520
|
-
// value aren't visually grouped with it. Roughly: a typical
|
|
4521
|
-
// table-row width.
|
|
4522
|
-
if (d > 400)
|
|
4523
|
-
continue;
|
|
4524
|
-
if (best === null || d < best.d)
|
|
4525
|
-
best = { phrase: lh.phrase, d };
|
|
4526
|
-
}
|
|
4527
|
-
return best?.phrase ?? null;
|
|
4528
|
-
};
|
|
4529
|
-
const hasNearbyReveal = (x, y) => {
|
|
4530
|
-
for (const rb of revealButtons) {
|
|
4531
|
-
const dx = rb.x - x;
|
|
4532
|
-
const dy = rb.y - y;
|
|
4533
|
-
// Reveal/copy buttons are usually right next to the value —
|
|
4534
|
-
// 200px is generous.
|
|
4535
|
-
if (Math.sqrt(dx * dx + dy * dy) < 200)
|
|
4536
|
-
return true;
|
|
4537
|
-
}
|
|
4538
|
-
return false;
|
|
4539
|
-
};
|
|
4540
|
-
const seen = new Set();
|
|
4541
|
-
const out = [];
|
|
4542
|
-
const pushCandidate = (value, el) => {
|
|
4543
|
-
const trimmed = value.trim();
|
|
4544
|
-
if (trimmed.length === 0)
|
|
4545
|
-
return;
|
|
4546
|
-
const masked = isMaskedShape(trimmed);
|
|
4547
|
-
if (!masked && !isCredentialShape(trimmed)) {
|
|
4548
|
-
// 0.8.2-rc.17 — when the whole text-node string has
|
|
4549
|
-
// whitespace (Cloudinary's "Cloud name: dlq4xgrca" sits
|
|
4550
|
-
// in a SINGLE <div> with the label and value glued
|
|
4551
|
-
// together), isCredentialShape rejects the whole string.
|
|
4552
|
-
// Try to split on the canonical label-value separator
|
|
4553
|
-
// patterns ("Label: value", "Label = value", "Label\nvalue")
|
|
4554
|
-
// and re-evaluate each side. The token side gets the
|
|
4555
|
-
// candidate slot; the label side already lives on its own
|
|
4556
|
-
// (we don't need to push it). First-wins on duplicates.
|
|
4557
|
-
const split = /^([A-Za-z][A-Za-z _-]{1,40}?)\s*[:=]\s*([A-Za-z0-9._\-]{4,256})$/.exec(trimmed);
|
|
4558
|
-
if (split === null)
|
|
4559
|
-
return;
|
|
4560
|
-
const valueToken = split[2];
|
|
4561
|
-
if (valueToken === undefined)
|
|
4562
|
-
return;
|
|
4563
|
-
if (!isCredentialShape(valueToken))
|
|
4564
|
-
return;
|
|
4565
|
-
if (seen.has(valueToken))
|
|
4566
|
-
return;
|
|
4567
|
-
seen.add(valueToken);
|
|
4568
|
-
const c = centerOf(el);
|
|
4569
|
-
const label = findNearestLabel(c.x, c.y);
|
|
4570
|
-
out.push({
|
|
4571
|
-
value: valueToken,
|
|
4572
|
-
label,
|
|
4573
|
-
isMasked: false,
|
|
4574
|
-
hasRevealButton: false,
|
|
4575
|
-
});
|
|
4576
|
-
return;
|
|
4577
|
-
}
|
|
4578
|
-
if (seen.has(trimmed))
|
|
4579
|
-
return;
|
|
4580
|
-
seen.add(trimmed);
|
|
4581
|
-
const c = centerOf(el);
|
|
4582
|
-
const label = findNearestLabel(c.x, c.y);
|
|
4583
|
-
const hasReveal = masked ? hasNearbyReveal(c.x, c.y) : false;
|
|
4584
|
-
out.push({
|
|
4585
|
-
value: trimmed,
|
|
4586
|
-
label,
|
|
4587
|
-
isMasked: masked,
|
|
4588
|
-
hasRevealButton: hasReveal,
|
|
4589
|
-
});
|
|
4590
|
-
};
|
|
4591
|
-
// 0. Inline config snippets: a credential block listing multiple
|
|
4592
|
-
// label = "value" pairs in one text run (pusher's App Keys page:
|
|
4593
|
-
// app_id = "2164307" key = "..." secret = "..." cluster = "ap3").
|
|
4594
|
-
// No separate label ELEMENTS exist, so the proximity matcher mislabels
|
|
4595
|
-
// every value with whatever heading is nearest. Parse the label-value
|
|
4596
|
-
// pairs straight from the page text — each pair's own label is
|
|
4597
|
-
// authoritative. Runs FIRST so its correctly-labeled candidates win
|
|
4598
|
-
// the `seen` dedup over the proximity passes. Noise pairs are harmless:
|
|
4599
|
-
// a skill only matches the labels it asks for.
|
|
4600
|
-
const bodyText = document.body?.innerText ?? "";
|
|
4601
|
-
const INLINE_PAIR = /\b([A-Za-z][A-Za-z0-9_-]{1,40})\s*[:=]\s*["']?([A-Za-z0-9._-]{6,256})["']?/g;
|
|
4602
|
-
for (const m of bodyText.matchAll(INLINE_PAIR)) {
|
|
4603
|
-
const label = (m[1] ?? "").toLowerCase();
|
|
4604
|
-
const value = m[2] ?? "";
|
|
4605
|
-
if (!isCredentialShape(value))
|
|
4606
|
-
continue;
|
|
4607
|
-
if (seen.has(value))
|
|
4608
|
-
continue;
|
|
4609
|
-
seen.add(value);
|
|
4610
|
-
out.push({ value, label, isMasked: false, hasRevealButton: false });
|
|
4611
|
-
}
|
|
4612
|
-
// 1. <input> / <textarea> values (visible only). Only text-shaped
|
|
4613
|
-
// controls can RENDER a credential: a checkbox/radio/button's `value`
|
|
4614
|
-
// is a markup constant (or the control's id), not page content — an
|
|
4615
|
-
// offscreen `<input type="checkbox" id="secret" value="fallback">`
|
|
4616
|
-
// must never become a stored credential.
|
|
4617
|
-
document.querySelectorAll("input, textarea").forEach((el) => {
|
|
4618
|
-
if (el instanceof HTMLInputElement &&
|
|
4619
|
-
!["text", "search", "url", "tel", "number", "email", ""].includes(el.type))
|
|
4620
|
-
return;
|
|
4621
|
-
if (!isVisible(el))
|
|
4622
|
-
return;
|
|
4623
|
-
const value = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement ? el.value : "";
|
|
4624
|
-
if (value.length > 0)
|
|
4625
|
-
pushCandidate(value, el);
|
|
4626
|
-
});
|
|
4627
|
-
// 2. Direct text content in visible leaf elements.
|
|
4628
|
-
document.querySelectorAll("body *").forEach((el) => {
|
|
4629
|
-
if (el.tagName === "SCRIPT" || el.tagName === "STYLE")
|
|
4630
|
-
return;
|
|
4631
|
-
if (!isVisible(el))
|
|
4632
|
-
return;
|
|
4633
|
-
let direct = "";
|
|
4634
|
-
el.childNodes.forEach((n) => {
|
|
4635
|
-
if (n.nodeType === Node.TEXT_NODE)
|
|
4636
|
-
direct += n.textContent ?? "";
|
|
4637
|
-
});
|
|
4638
|
-
direct = direct.trim();
|
|
4639
|
-
if (direct.length === 0 || direct.length > 256)
|
|
4640
|
-
return;
|
|
4641
|
-
pushCandidate(direct, el);
|
|
4642
|
-
});
|
|
4643
|
-
// 3. Structural containers (code/pre/kbd) where the credential
|
|
4644
|
-
// is interpolated through nested spans.
|
|
4645
|
-
document.querySelectorAll('code, pre, kbd, samp, [role="textbox"]').forEach((el) => {
|
|
4646
|
-
if (!isVisible(el))
|
|
4647
|
-
return;
|
|
4648
|
-
const full = (el.textContent ?? "").trim();
|
|
4649
|
-
if (full.length === 0 || full.length > 256)
|
|
4650
|
-
return;
|
|
4651
|
-
pushCandidate(full, el);
|
|
4652
|
-
});
|
|
4653
|
-
return out;
|
|
4654
|
-
});
|
|
4655
|
-
}
|
|
4656
|
-
// Click every visible "Reveal" / "Show" / "Eye" / "Copy" button on
|
|
4657
|
-
// the page that sits next to a masked credential display. Used as a
|
|
4658
|
-
// pre-extract pass for services like Cloudinary that hide the
|
|
4659
|
-
// api_secret behind a click-to-reveal icon. Best-effort: failures
|
|
4660
|
-
// don't throw; subsequent extract pass tries whatever surfaced.
|
|
4661
|
-
// Returns the number of buttons successfully clicked.
|
|
4662
|
-
async revealMaskedCredentials(page = this.page) {
|
|
4663
|
-
if (page === null)
|
|
4664
|
-
throw new Error("Browser not started");
|
|
4665
|
-
const probe = await page.evaluate(() => {
|
|
4666
|
-
const isVisible = (el) => {
|
|
4667
|
-
const r = el.getBoundingClientRect();
|
|
4668
|
-
return r.width > 2 && r.height > 2;
|
|
4669
|
-
};
|
|
4670
|
-
// Walk up to the nearest "row-like" ancestor — a <tr>, a <li>,
|
|
4671
|
-
// or any container ≤ 800px wide with limited height. Cloudinary,
|
|
4672
|
-
// Algolia, Twilio all use table rows; clicking the reveal in
|
|
4673
|
-
// ROW X must populate the value in ROW X, not some neighbor row.
|
|
4674
|
-
const rowAncestor = (el) => {
|
|
4675
|
-
let cur = el;
|
|
4676
|
-
for (let i = 0; i < 8 && cur !== null; i++) {
|
|
4677
|
-
if (cur.tagName === "TR" || cur.tagName === "LI")
|
|
4678
|
-
return cur;
|
|
4679
|
-
const r = cur.getBoundingClientRect();
|
|
4680
|
-
if (r.width > 200 && r.width < 900 && r.height < 200)
|
|
4681
|
-
return cur;
|
|
4682
|
-
cur = cur.parentElement;
|
|
4683
|
-
}
|
|
4684
|
-
return el.parentElement;
|
|
4685
|
-
};
|
|
4686
|
-
const masked = [];
|
|
4687
|
-
document.querySelectorAll("body *").forEach((el) => {
|
|
4688
|
-
if (el.tagName === "SCRIPT" || el.tagName === "STYLE")
|
|
4689
|
-
return;
|
|
4690
|
-
if (!isVisible(el))
|
|
4691
|
-
return;
|
|
4692
|
-
let direct = "";
|
|
4693
|
-
el.childNodes.forEach((n) => {
|
|
4694
|
-
if (n.nodeType === Node.TEXT_NODE)
|
|
4695
|
-
direct += n.textContent ?? "";
|
|
4696
|
-
});
|
|
4697
|
-
const t = direct.trim();
|
|
4698
|
-
if (t.length < 3 || t.length > 100)
|
|
4699
|
-
return;
|
|
4700
|
-
// Canonical MASKED_DISPLAY_RE union (credential-shape.ts): bullet/
|
|
4701
|
-
// circle, 3+ asterisks, ellipsis, or 3+ dots. The old
|
|
4702
|
-
// `[•●⬤*]{3,}` missed the `…`/`...` masks a masked sibling key uses
|
|
4703
|
-
// (`vsk_sandbox_write_20af…`), so its Reveal was never clicked.
|
|
4704
|
-
if (!/[•●⬤]|\*{3,}|…|\.{3,}/.test(t))
|
|
4705
|
-
return;
|
|
4706
|
-
masked.push({ el, row: rowAncestor(el) });
|
|
4707
|
-
});
|
|
4708
|
-
document.querySelectorAll('input[type="password"]').forEach((el) => {
|
|
4709
|
-
if (!isVisible(el))
|
|
4710
|
-
return;
|
|
4711
|
-
masked.push({ el, row: rowAncestor(el) });
|
|
4712
|
-
});
|
|
4713
|
-
const selectorFor = (el) => {
|
|
4714
|
-
const parts = [];
|
|
4715
|
-
let current = el;
|
|
4716
|
-
while (current !== null && current !== document.body) {
|
|
4717
|
-
const tag = current.tagName.toLowerCase();
|
|
4718
|
-
const siblings = Array.from(current.parentElement?.children ?? []).filter((sibling) => sibling.tagName === current.tagName);
|
|
4719
|
-
parts.unshift(`${tag}:nth-of-type(${siblings.indexOf(current) + 1})`);
|
|
4720
|
-
current = current.parentElement;
|
|
4721
|
-
}
|
|
4722
|
-
return `body > ${parts.join(" > ")}`;
|
|
4723
|
-
};
|
|
4724
|
-
// No masked placeholder anywhere — but some consoles hide the key
|
|
4725
|
-
// ENTIRELY behind a "View/Show Key" button with no ••• shown at all
|
|
4726
|
-
// (Zilliz's "View My Personal Key"). The row-anchored pass below has
|
|
4727
|
-
// nothing to anchor on, so without this the reveal pass bails and the
|
|
4728
|
-
// extractor reports no_legit_credential on a page that DOES have a key.
|
|
4729
|
-
// Anchor-free fallback: click a button whose label pairs a SAFE reveal
|
|
4730
|
-
// verb with a credential noun, excluding destructive verbs (reset/
|
|
4731
|
-
// regenerate/delete/revoke/rotate would mint or destroy a key, not
|
|
4732
|
-
// reveal the existing one).
|
|
4733
|
-
const DESTRUCTIVE = /\b(?:reset|regenerat\w*|delete|revoke|rotate|create|new|remove|add|download)\b/i;
|
|
4734
|
-
if (masked.length === 0) {
|
|
4735
|
-
const KEY_NOUN = /\b(?:api\s*key|secret|token|credential|personal\s+key|access\s+key|key)\b/i;
|
|
4736
|
-
const SAFE_REVEAL = /\b(?:view|show|reveal|display|see)\b/i;
|
|
4737
|
-
const out = [];
|
|
4738
|
-
const diag = [];
|
|
4739
|
-
document
|
|
4740
|
-
.querySelectorAll('button, [role="button"], a[role="button"]')
|
|
4741
|
-
.forEach((el) => {
|
|
4742
|
-
if (!isVisible(el))
|
|
4743
|
-
return;
|
|
4744
|
-
const hay = `${el.textContent ?? ""} ${el.getAttribute("aria-label") ?? ""} ${el.getAttribute("title") ?? ""}`
|
|
4745
|
-
.replace(/\s+/g, " ")
|
|
4746
|
-
.trim();
|
|
4747
|
-
if (hay.length === 0 || hay.length > 60)
|
|
4748
|
-
return;
|
|
4749
|
-
if (!SAFE_REVEAL.test(hay) || !KEY_NOUN.test(hay))
|
|
4750
|
-
return;
|
|
4751
|
-
if (DESTRUCTIVE.test(hay))
|
|
4752
|
-
return;
|
|
4753
|
-
out.push(selectorFor(el));
|
|
4754
|
-
diag.push(`anchorless_key_reveal:"${hay.slice(0, 40)}"`);
|
|
4755
|
-
});
|
|
4756
|
-
return {
|
|
4757
|
-
selectors: out,
|
|
4758
|
-
diagnostic: out.length > 0 ? diag : ["no_masked_displays"],
|
|
4759
|
-
};
|
|
4760
|
-
}
|
|
4761
|
-
// 2. Classify candidate buttons. Prefer SHOW/REVEAL/EYE; fall
|
|
4762
|
-
// back to COPY only when no show button exists in the row.
|
|
4763
|
-
// (Copy generally puts value in clipboard, not in DOM —
|
|
4764
|
-
// which our extractor can't read in headless.)
|
|
4765
|
-
const SHOW_PATTERN = /\b(?:reveal|show|unmask|view|toggle|eye)\b/i;
|
|
4766
|
-
const COPY_PATTERN = /\bcopy\b/i;
|
|
4767
|
-
const collectButtonsInRow = (row) => {
|
|
4768
|
-
const showBtns = [];
|
|
4769
|
-
const copyBtns = [];
|
|
4770
|
-
if (row === null)
|
|
4771
|
-
return { showBtns, copyBtns };
|
|
4772
|
-
row
|
|
4773
|
-
.querySelectorAll('button, [role="button"], a[role="button"], [aria-label], [title]')
|
|
4774
|
-
.forEach((el) => {
|
|
4775
|
-
if (!isVisible(el))
|
|
4776
|
-
return;
|
|
4777
|
-
const hay = `${el.textContent ?? ""} ${el.getAttribute("aria-label") ?? ""} ${el.getAttribute("title") ?? ""} ${el.className ?? ""}`;
|
|
4778
|
-
if (DESTRUCTIVE.test(hay))
|
|
4779
|
-
return;
|
|
4780
|
-
if (SHOW_PATTERN.test(hay))
|
|
4781
|
-
showBtns.push(el);
|
|
4782
|
-
else if (COPY_PATTERN.test(hay))
|
|
4783
|
-
copyBtns.push(el);
|
|
4784
|
-
});
|
|
4785
|
-
return { showBtns, copyBtns };
|
|
4786
|
-
};
|
|
4787
|
-
const selectors = [];
|
|
4788
|
-
const diagnostic = [];
|
|
4789
|
-
const usedRows = new Set();
|
|
4790
|
-
for (const m of masked) {
|
|
4791
|
-
if (m.row === null)
|
|
4792
|
-
continue;
|
|
4793
|
-
if (usedRows.has(m.row))
|
|
4794
|
-
continue;
|
|
4795
|
-
usedRows.add(m.row);
|
|
4796
|
-
const { showBtns, copyBtns } = collectButtonsInRow(m.row);
|
|
4797
|
-
if (showBtns.length > 0) {
|
|
4798
|
-
const btn = showBtns[0];
|
|
4799
|
-
const sel = selectorFor(btn);
|
|
4800
|
-
selectors.push(sel);
|
|
4801
|
-
const label = (btn.textContent ??
|
|
4802
|
-
btn.getAttribute("aria-label") ??
|
|
4803
|
-
btn.getAttribute("title") ??
|
|
4804
|
-
"")
|
|
4805
|
-
.trim()
|
|
4806
|
-
.slice(0, 40);
|
|
4807
|
-
diagnostic.push(`row→show:"${label}"→${sel}`);
|
|
4808
|
-
}
|
|
4809
|
-
else if (copyBtns.length > 0) {
|
|
4810
|
-
diagnostic.push(`row→copy_only_no_show_button (copy='${copyBtns.length} found' — skipped, would only populate clipboard not DOM)`);
|
|
4811
|
-
}
|
|
4812
|
-
else {
|
|
4813
|
-
diagnostic.push("row→no_buttons_found");
|
|
4814
|
-
}
|
|
4815
|
-
}
|
|
4816
|
-
return { selectors, diagnostic };
|
|
4817
|
-
});
|
|
4818
|
-
let clicked = 0;
|
|
4819
|
-
for (const sel of probe.selectors) {
|
|
4820
|
-
try {
|
|
4821
|
-
await page.locator(sel).first().click({ timeout: 1500 });
|
|
4822
|
-
clicked += 1;
|
|
4823
|
-
// Reveal click often triggers a fetch (Cloudinary returns the
|
|
4824
|
-
// secret over an XHR before populating the DOM). Wait longer
|
|
4825
|
-
// than the previous 150ms.
|
|
4826
|
-
await this.sleep(800);
|
|
4827
|
-
}
|
|
4828
|
-
catch {
|
|
4829
|
-
// Click failed — best-effort.
|
|
4830
|
-
}
|
|
4831
|
-
}
|
|
4832
|
-
return { clicked, diagnostic: probe.diagnostic };
|
|
4833
|
-
}
|
|
4834
|
-
async extractCredentialCandidates() {
|
|
4835
|
-
if (!this.page)
|
|
4836
|
-
throw new Error("Browser not started");
|
|
4837
|
-
return await this.page.evaluate(() => {
|
|
4838
|
-
const out = [];
|
|
4839
|
-
const isVisible = (el) => {
|
|
4840
|
-
const r = el.getBoundingClientRect();
|
|
4841
|
-
return r.width > 2 && r.height > 2;
|
|
4842
|
-
};
|
|
4843
|
-
document.querySelectorAll("input, textarea").forEach((el) => {
|
|
4844
|
-
// Only text-shaped inputs can RENDER a credential. A checkbox/
|
|
4845
|
-
// radio/button's `value` is a markup constant, not page content —
|
|
4846
|
-
// zilliz's CookieScript banner ships `<input type="checkbox"
|
|
4847
|
-
// value="personalization">` and those words sit earlier in DOM
|
|
4848
|
-
// order than the real key, so the validator-shaped scan tier was
|
|
4849
|
-
// returning them as the "credential".
|
|
4850
|
-
if (el instanceof HTMLInputElement &&
|
|
4851
|
-
!["text", "search", "url", "tel", "number", "email", ""].includes(el.type)) {
|
|
4852
|
-
return;
|
|
4853
|
-
}
|
|
4854
|
-
const value = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement ? el.value : "";
|
|
4855
|
-
if (value.trim().length > 0 && isVisible(el))
|
|
4856
|
-
out.push(value.trim());
|
|
4857
|
-
});
|
|
4858
|
-
document.querySelectorAll("body *").forEach((el) => {
|
|
4859
|
-
if (el.tagName === "SCRIPT" || el.tagName === "STYLE")
|
|
4860
|
-
return;
|
|
4861
|
-
if (!isVisible(el))
|
|
4862
|
-
return;
|
|
4863
|
-
let direct = "";
|
|
4864
|
-
el.childNodes.forEach((n) => {
|
|
4865
|
-
if (n.nodeType === Node.TEXT_NODE)
|
|
4866
|
-
direct += n.textContent ?? "";
|
|
4867
|
-
});
|
|
4868
|
-
direct = direct.trim();
|
|
4869
|
-
// A real key is short; a long blob is a paragraph, not a key.
|
|
4870
|
-
if (direct.length > 0 && direct.length <= 256)
|
|
4871
|
-
out.push(direct);
|
|
4872
|
-
});
|
|
4873
|
-
// Structural containers (<code>, <pre>, kbd, samp, [role=textbox])
|
|
4874
|
-
// often render a credential by interpolating it through nested
|
|
4875
|
-
// <span>s — the loop above sees an empty direct-text and skips
|
|
4876
|
-
// them. Push the full textContent so a UUID built as
|
|
4877
|
-
// <code><span>7</span><span>5</span>…</code> is still scannable.
|
|
4878
|
-
document.querySelectorAll('code, pre, kbd, samp, [role="textbox"]').forEach((el) => {
|
|
4879
|
-
if (!isVisible(el))
|
|
4880
|
-
return;
|
|
4881
|
-
const full = (el.textContent ?? "").trim();
|
|
4882
|
-
if (full.length > 0 && full.length <= 256)
|
|
4883
|
-
out.push(full);
|
|
4884
|
-
});
|
|
4885
|
-
return out;
|
|
4886
|
-
});
|
|
4887
|
-
}
|
|
4888
4143
|
// Wait for the signup form to actually render before the planner
|
|
4889
4144
|
// screenshots the page (F1). SPA and two-stage signup pages render
|
|
4890
4145
|
// the form after JS executes; planning against a pre-render
|