@trusty-squire/mcp 0.9.19-rc.2 → 0.9.19-rc.21
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 +15 -33
- 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/bin.js +4 -10
- package/dist/bin.js.map +1 -1
- package/dist/bot/agent.d.ts +24 -2
- package/dist/bot/agent.d.ts.map +1 -1
- package/dist/bot/agent.js +634 -55
- package/dist/bot/agent.js.map +1 -1
- package/dist/bot/browser.d.ts +7 -0
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +182 -3
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/credential-extraction-flow.d.ts +2 -0
- package/dist/bot/credential-extraction-flow.d.ts.map +1 -1
- package/dist/bot/credential-extraction-flow.js +71 -1
- package/dist/bot/credential-extraction-flow.js.map +1 -1
- package/dist/bot/form-fill.d.ts.map +1 -1
- package/dist/bot/form-fill.js +11 -0
- package/dist/bot/form-fill.js.map +1 -1
- package/dist/bot/google-login.d.ts.map +1 -1
- package/dist/bot/google-login.js +37 -1
- package/dist/bot/google-login.js.map +1 -1
- package/dist/bot/index.d.ts +1 -0
- package/dist/bot/index.d.ts.map +1 -1
- package/dist/bot/index.js +1 -0
- package/dist/bot/index.js.map +1 -1
- package/dist/bot/login-state.d.ts +2 -1
- package/dist/bot/login-state.d.ts.map +1 -1
- package/dist/bot/login-state.js +22 -5
- package/dist/bot/login-state.js.map +1 -1
- package/dist/bot/nav-search.d.ts.map +1 -1
- package/dist/bot/nav-search.js +9 -0
- package/dist/bot/nav-search.js.map +1 -1
- package/dist/bot/post-signup-flow.d.ts.map +1 -1
- package/dist/bot/post-signup-flow.js +21 -0
- package/dist/bot/post-signup-flow.js.map +1 -1
- package/dist/bot/post-signup-recovery-state.d.ts +3 -0
- package/dist/bot/post-signup-recovery-state.d.ts.map +1 -1
- package/dist/bot/post-signup-recovery-state.js +3 -0
- package/dist/bot/post-signup-recovery-state.js.map +1 -1
- package/dist/bot/provision-session.d.ts +116 -1
- package/dist/bot/provision-session.d.ts.map +1 -1
- package/dist/bot/provision-session.js +885 -41
- package/dist/bot/provision-session.js.map +1 -1
- package/dist/bot/redact.d.ts.map +1 -1
- package/dist/bot/redact.js +25 -2
- package/dist/bot/redact.js.map +1 -1
- package/dist/bot/replay-skill.d.ts +6 -0
- package/dist/bot/replay-skill.d.ts.map +1 -1
- package/dist/bot/replay-skill.js +39 -5
- package/dist/bot/replay-skill.js.map +1 -1
- package/dist/bot/skill-hint.d.ts +7 -0
- package/dist/bot/skill-hint.d.ts.map +1 -0
- package/dist/bot/skill-hint.js +105 -0
- package/dist/bot/skill-hint.js.map +1 -0
- package/dist/bot/terminal-gate.d.ts +3 -1
- package/dist/bot/terminal-gate.d.ts.map +1 -1
- package/dist/bot/terminal-gate.js +19 -0
- package/dist/bot/terminal-gate.js.map +1 -1
- package/dist/install/agents.d.ts.map +1 -1
- package/dist/install/agents.js +12 -2
- package/dist/install/agents.js.map +1 -1
- package/dist/install/cli.d.ts +14 -2
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +346 -150
- package/dist/install/cli.js.map +1 -1
- package/dist/install/interactive.d.ts +9 -3
- package/dist/install/interactive.d.ts.map +1 -1
- package/dist/install/interactive.js +80 -140
- package/dist/install/interactive.js.map +1 -1
- package/dist/install/proxy-url.d.ts +2 -0
- package/dist/install/proxy-url.d.ts.map +1 -0
- package/dist/install/proxy-url.js +20 -0
- package/dist/install/proxy-url.js.map +1 -0
- package/dist/install/ui.js +1 -1
- package/dist/install/ui.js.map +1 -1
- package/dist/session.d.ts +3 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js.map +1 -1
- package/dist/skill-registry-client.d.ts +8 -0
- package/dist/skill-registry-client.d.ts.map +1 -1
- package/dist/skill-registry-client.js +70 -53
- package/dist/skill-registry-client.js.map +1 -1
- package/dist/tools/index.d.ts +1 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +10 -19
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/provision-any.d.ts +3 -0
- package/dist/tools/provision-any.d.ts.map +1 -1
- package/dist/tools/provision-any.js +162 -32
- package/dist/tools/provision-any.js.map +1 -1
- package/dist/tools/provision-drive.d.ts +121 -5
- package/dist/tools/provision-drive.d.ts.map +1 -1
- package/dist/tools/provision-drive.js +339 -48
- package/dist/tools/provision-drive.js.map +1 -1
- package/dist/tools/store-credential.d.ts +5 -0
- package/dist/tools/store-credential.d.ts.map +1 -1
- package/dist/tools/store-credential.js +5 -0
- package/dist/tools/store-credential.js.map +1 -1
- package/package.json +1 -3
- package/dist/bot/telegram-notify.d.ts +0 -8
- package/dist/bot/telegram-notify.d.ts.map +0 -1
- package/dist/bot/telegram-notify.js +0 -134
- package/dist/bot/telegram-notify.js.map +0 -1
package/dist/bot/agent.js
CHANGED
|
@@ -14,7 +14,6 @@ import { decideOAuthStep } from "./oauth-flow.js";
|
|
|
14
14
|
import { decideFormFillStep, FORM_FILL_BUDGETS as B_FF, findSignupLinkOnLoginPage, initialFormFillState, pickEmailCodeSubmitSelector, } from "./form-fill.js";
|
|
15
15
|
import { accumulateCandidate, hasFullHit, initialExtractionState, resolveExtraction, } from "./extraction.js";
|
|
16
16
|
import { notifyHeightenedAuth } from "./notify-api.js";
|
|
17
|
-
import { sendTelegramHeightenedAuth } from "./telegram-notify.js";
|
|
18
17
|
import { TwoCaptchaSolver } from "./captcha-solver-2captcha.js";
|
|
19
18
|
import { redactCredentials } from "./redact.js";
|
|
20
19
|
import { readOperatorOtp, fromDomainFromUrl } from "./read-otp.js";
|
|
@@ -23,18 +22,18 @@ import { SignupOAuthFlow } from "./signup-oauth-flow.js";
|
|
|
23
22
|
import { saveDebugSnapshot } from "./debug.js";
|
|
24
23
|
import { captureObservationFrame } from "./observation-frame.js";
|
|
25
24
|
import { classifyObservationFrame } from "./state-classifier.js";
|
|
26
|
-
import { classifyTerminalGate, isAtAccountReviewGate, isAtPaywall, isOnboardingReviewGate, isSignupsClosed, } from "./terminal-gate.js";
|
|
27
|
-
export { isAtAccountReviewGate, isAtPaywall, isOnboardingReviewGate, isSignupsClosed, } from "./terminal-gate.js";
|
|
25
|
+
import { classifyTerminalGate, isAtAccountReviewGate, isAtOAuthAccountLinkVerificationGate, isAtPaywall, isOnboardingReviewGate, isSignupsClosed, } from "./terminal-gate.js";
|
|
26
|
+
export { isAtAccountReviewGate, isAtOAuthAccountLinkVerificationGate, isAtPaywall, isOnboardingReviewGate, isSignupsClosed, } from "./terminal-gate.js";
|
|
28
27
|
import { PostSignupCredentialTracker, classifyNoCredentialPostSignup, } from "./post-signup-flow.js";
|
|
29
28
|
import { PostSignupActionExecutor, } from "./post-signup-action-executor.js";
|
|
30
29
|
import { PostSignupLoginFlow } from "./post-signup-login-flow.js";
|
|
31
30
|
import { MAX_POST_VERIFY_NAVIGATES, MAX_PREMATURE_DONE_FALLBACKS, MAX_UPSTREAM_BLIP_RETRIES, PostSignupRecoveryFlow, PostSignupRecoveryState, } from "./post-signup-recovery-state.js";
|
|
32
31
|
import { PostSignupSyntheticCapture } from "./post-signup-synthetic-capture.js";
|
|
33
|
-
import { CredentialExtractionFlow, DOM_LABEL_TO_KEY, NON_CREDENTIAL_KEYS, extractAllLabeledTokensFromReason, hasAnyExtractedCredential, hasUsableCredentialBundle, } from "./credential-extraction-flow.js";
|
|
34
|
-
export { extractAllLabeledTokensFromReason, hasAnyExtractedCredential, isMultiCredBundle, } from "./credential-extraction-flow.js";
|
|
32
|
+
import { CredentialExtractionFlow, DOM_LABEL_TO_KEY, NON_CREDENTIAL_KEYS, extractAllLabeledTokensFromReason, hasAnyExtractedCredential, hasUsableCredentialBundle, terminalReasonInvalidatesCredentialSuccess, } from "./credential-extraction-flow.js";
|
|
33
|
+
export { extractAllLabeledTokensFromReason, hasAnyExtractedCredential, isMultiCredBundle, terminalReasonInvalidatesCredentialSuccess, } from "./credential-extraction-flow.js";
|
|
35
34
|
import { captureOnboardingRound, hasCapturedAnyRound, hasCapturedExtractRound, nextCaptureRound, updateCapturedRoundSemantic, } from "./onboarding-capture.js";
|
|
36
35
|
import { evaluateSemanticTransition, inferSemanticTransition, } from "./semantic-transition.js";
|
|
37
|
-
import { KEYS_DESTINATION_URL, runNavSearch, } from "./nav-search.js";
|
|
36
|
+
import { KEYS_DESTINATION_URL, isRequiredAccountSetupForm, runNavSearch, } from "./nav-search.js";
|
|
38
37
|
import { wasRecentlyPrewarmed, recordPrewarmSuccess } from "./prewarm-cache.js";
|
|
39
38
|
import { pickLLMPair, } from "./llm-client.js";
|
|
40
39
|
import { getDomain } from "tldts";
|
|
@@ -401,6 +400,16 @@ const SERVICE_KEYS_PATHS = {
|
|
|
401
400
|
// /settings/* and /api-keys guesses miss it and the planner can get stuck
|
|
402
401
|
// in Business Account > Team Members.
|
|
403
402
|
paddle: ["/authentication-v2"],
|
|
403
|
+
// Together AI's project-scoped keys route is discoverable in the DOM, but the
|
|
404
|
+
// planner can loop between Create key and payment-skip affordances after it
|
|
405
|
+
// lands there. Pin the route so recovery starts at the credential surface.
|
|
406
|
+
togetherai: [
|
|
407
|
+
"https://api.together.ai/settings/projects/~current/api-keys",
|
|
408
|
+
"/settings/projects/~current/api-keys",
|
|
409
|
+
],
|
|
410
|
+
// Cartesia's authenticated root/dashboard can expose key rows with Reveal
|
|
411
|
+
// controls; generic /settings/* guesses hit 404/error shells first.
|
|
412
|
+
cartesia: ["/", "/dashboard", "/start", "/api-keys"],
|
|
404
413
|
};
|
|
405
414
|
// Normalize a service name to the slug used as a SERVICE_KEYS_PATHS key:
|
|
406
415
|
// lowercased, alphanumerics only. Mirrors guessSignupUrl's slug rule so
|
|
@@ -409,6 +418,80 @@ const SERVICE_KEYS_PATHS = {
|
|
|
409
418
|
export function serviceSlug(service) {
|
|
410
419
|
return service.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
411
420
|
}
|
|
421
|
+
export function hasCuratedServiceKeyPath(service) {
|
|
422
|
+
return SERVICE_KEYS_PATHS[serviceSlug(service)] !== undefined;
|
|
423
|
+
}
|
|
424
|
+
function elementLabel(el) {
|
|
425
|
+
return [
|
|
426
|
+
el.visibleText,
|
|
427
|
+
el.ariaLabel,
|
|
428
|
+
el.title,
|
|
429
|
+
el.labelText,
|
|
430
|
+
el.iconLabel,
|
|
431
|
+
el.href,
|
|
432
|
+
el.testId,
|
|
433
|
+
]
|
|
434
|
+
.filter((s) => s !== null && s !== undefined)
|
|
435
|
+
.join(" ")
|
|
436
|
+
.replace(/\s+/g, " ")
|
|
437
|
+
.trim();
|
|
438
|
+
}
|
|
439
|
+
export function shouldRevealBeforeCredentialSweep(input) {
|
|
440
|
+
const haystack = `${input.url}\n${input.pageText}`;
|
|
441
|
+
return (/\b(?:api[\s_-]*(?:keys?|tokens?)|access[\s_-]*tokens?|personal[\s_-]*access[\s_-]*tokens?|secret[\s_-]*keys?|auth[\s_-]*tokens?|credentials?|reveal|show\s+(?:key|token|secret)|copy\s+(?:key|token|secret))\b/i.test(haystack) &&
|
|
442
|
+
/(?:•{3,}|\*{3,}|[A-Za-z0-9]{2,4}[•*]{4,}|\b(?:reveal|show|unmask|view)\b)/i.test(haystack));
|
|
443
|
+
}
|
|
444
|
+
export function credentialActionSignature(step, inventory) {
|
|
445
|
+
if (!("selector" in step) || step.selector === undefined)
|
|
446
|
+
return null;
|
|
447
|
+
const target = inventory.find((e) => e.selector === step.selector);
|
|
448
|
+
const label = [
|
|
449
|
+
target?.visibleText,
|
|
450
|
+
target?.ariaLabel,
|
|
451
|
+
target?.title,
|
|
452
|
+
target?.labelText,
|
|
453
|
+
target?.iconLabel,
|
|
454
|
+
step.reason,
|
|
455
|
+
]
|
|
456
|
+
.filter((s) => s !== null && s !== undefined)
|
|
457
|
+
.join(" ")
|
|
458
|
+
.replace(/\s+/g, " ")
|
|
459
|
+
.trim();
|
|
460
|
+
if (label.length === 0)
|
|
461
|
+
return null;
|
|
462
|
+
const lower = label.toLowerCase();
|
|
463
|
+
if (/\b(?:create|generate|new|add|issue|mint)\b.*\b(?:api\s*)?(?:key|token|secret|credential)s?\b/.test(lower) ||
|
|
464
|
+
/\b(?:api\s*)?(?:key|token|secret|credential)s?\b.*\b(?:create|generate|new|add|issue|mint)\b/.test(lower)) {
|
|
465
|
+
return `${step.kind}:create-key`;
|
|
466
|
+
}
|
|
467
|
+
if (/\b(?:skip|maybe later|not now|without)\b.*\b(?:payment|billing|card|deposit|credits?)\b/.test(lower) ||
|
|
468
|
+
/\b(?:payment|billing|card|deposit|credits?)\b.*\b(?:skip|maybe later|not now|without)\b/.test(lower)) {
|
|
469
|
+
return `${step.kind}:skip-payment`;
|
|
470
|
+
}
|
|
471
|
+
if (/\b(?:add|make|initial)\b.*\b(?:payment|deposit|credits?)\b/.test(lower)) {
|
|
472
|
+
return `${step.kind}:payment`;
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
export function isRepeatingCredentialActionCycle(signatures, pageText = "") {
|
|
477
|
+
const interesting = signatures.some((s) => /(?:create-key|payment|deposit)/.test(s)) ||
|
|
478
|
+
/\b(?:api\s*keys?|tokens?|payment|billing|deposit|credits?)\b/i.test(pageText);
|
|
479
|
+
if (!interesting)
|
|
480
|
+
return false;
|
|
481
|
+
const last3 = signatures.slice(-3);
|
|
482
|
+
if (last3.length === 3 &&
|
|
483
|
+
last3.every((s) => s === last3[0]) &&
|
|
484
|
+
/(?:create-key|skip-payment|payment)/.test(last3[0] ?? "")) {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
const last5 = signatures.slice(-5);
|
|
488
|
+
return (last5.length === 5 &&
|
|
489
|
+
last5[0] === last5[2] &&
|
|
490
|
+
last5[2] === last5[4] &&
|
|
491
|
+
last5[1] === last5[3] &&
|
|
492
|
+
last5[0] !== last5[1] &&
|
|
493
|
+
last5.some((s) => /(?:create-key|skip-payment|payment)/.test(s)));
|
|
494
|
+
}
|
|
412
495
|
// 0.8.2-rc.10 — heuristic for "this account already exists on the
|
|
413
496
|
// service and its API keys are masked, with no path to reveal them."
|
|
414
497
|
// The test identity (methoxine@gmail.com) accumulates state across
|
|
@@ -706,6 +789,58 @@ export function findApiKeysNavLink(inventory, alreadyClicked = new Set()) {
|
|
|
706
789
|
candidates.sort((a, b) => b.score - a.score);
|
|
707
790
|
return candidates[0].el;
|
|
708
791
|
}
|
|
792
|
+
export function findRenderAccountSettingsLink(inventory, alreadyClicked = new Set()) {
|
|
793
|
+
for (const el of inventory) {
|
|
794
|
+
if (el.visible === false)
|
|
795
|
+
continue;
|
|
796
|
+
if (alreadyClicked.has(el.selector))
|
|
797
|
+
continue;
|
|
798
|
+
const clickable = el.tag === "a" ||
|
|
799
|
+
el.tag === "button" ||
|
|
800
|
+
el.role === "link" ||
|
|
801
|
+
el.role === "button";
|
|
802
|
+
if (!clickable)
|
|
803
|
+
continue;
|
|
804
|
+
const label = elementLabel(el);
|
|
805
|
+
if (/\baccount settings\b/i.test(label) || /\/u\/[^/\s]+\/settings(?:[#?\s]|$)/i.test(label)) {
|
|
806
|
+
return el;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
export function findRenderAccountMenuTrigger(inventory) {
|
|
812
|
+
const candidates = [];
|
|
813
|
+
for (const el of inventory) {
|
|
814
|
+
if (el.visible === false)
|
|
815
|
+
continue;
|
|
816
|
+
const clickable = el.tag === "button" || el.role === "button";
|
|
817
|
+
if (!clickable)
|
|
818
|
+
continue;
|
|
819
|
+
const label = elementLabel(el);
|
|
820
|
+
if (label.length === 0)
|
|
821
|
+
continue;
|
|
822
|
+
const normalized = label.toLowerCase().trim();
|
|
823
|
+
if (/\b(?:new|upgrade|search|settings|billing|projects?|blueprints?|notifications?|contact support|create|add)\b/i.test(normalized)) {
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
let score = 0;
|
|
827
|
+
if (el.landmark === "header")
|
|
828
|
+
score += 4;
|
|
829
|
+
if (/^[a-z]{1,3}$/i.test(normalized))
|
|
830
|
+
score += 3;
|
|
831
|
+
if (/\b(?:profile|account|user|avatar)\b/i.test(label))
|
|
832
|
+
score += 2;
|
|
833
|
+
if (el.inViewport === true)
|
|
834
|
+
score += 1;
|
|
835
|
+
if (score <= 0)
|
|
836
|
+
continue;
|
|
837
|
+
candidates.push({ el, score });
|
|
838
|
+
}
|
|
839
|
+
if (candidates.length === 0)
|
|
840
|
+
return null;
|
|
841
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
842
|
+
return candidates[0].el;
|
|
843
|
+
}
|
|
709
844
|
const RELOCATED_PAGE_TEXT = /\b(?:404|page not found|not found|does not exist|no longer exists|has moved|moved|migrated|now (?:lives|exists)|part of .* instead|teams? list|organisations? list|organizations? list)\b/i;
|
|
710
845
|
const CREDENTIAL_CONTEXT_TEXT = /\b(?:api[\s_-]*(?:keys?|tokens?)|access[\s_-]*tokens?|personal[\s_-]*access[\s_-]*tokens?|secret[\s_-]*keys?|auth[\s_-]*tokens?|credentials?|developers?)\b/i;
|
|
711
846
|
const RELOCATION_RECOVERY_TEXT = /\b(?:teams?|organisations?|organizations?|accounts?|settings|profile|dashboard|developers?|api|keys?|tokens?|credentials?)\b/i;
|
|
@@ -751,12 +886,17 @@ export function findRelocatedCredentialPageRecoveryLink(input) {
|
|
|
751
886
|
const haystack = `${href} ${text}`;
|
|
752
887
|
if (!RELOCATION_RECOVERY_TEXT.test(haystack))
|
|
753
888
|
continue;
|
|
889
|
+
const isScopeRecovery = /\b(?:teams?|organisations?|organizations?)\b/i.test(haystack);
|
|
890
|
+
const pageAdvertisesScopeList = /\b(?:teams? list|organisations? list|organizations? list|part of .* instead)\b/i.test(relocatedText);
|
|
891
|
+
if (isScopeRecovery && !pageAdvertisesScopeList && !SCOPE_ENTITY_HREF.test(href)) {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
754
894
|
let score = 0;
|
|
755
895
|
if (API_KEYS_HREF.test(href))
|
|
756
896
|
score += 6;
|
|
757
897
|
if (API_KEYS_TEXT.test(text))
|
|
758
898
|
score += 4;
|
|
759
|
-
if (
|
|
899
|
+
if (isScopeRecovery)
|
|
760
900
|
score += 3;
|
|
761
901
|
if (/\b(?:settings|accounts?|profile|developers?)\b/i.test(haystack))
|
|
762
902
|
score += 2;
|
|
@@ -1004,18 +1144,64 @@ function shouldComposeFallbackOnAppOrigin(current, app) {
|
|
|
1004
1144
|
// replace every current origin with the original signup URL's origin.
|
|
1005
1145
|
return /^(?:auth|authkit|login|accounts?|idp|sso|oauth|signin|signup)$/.test(firstLabel);
|
|
1006
1146
|
}
|
|
1147
|
+
const SERVICE_NAME_TLD_SUFFIXES = new Set([
|
|
1148
|
+
"ai",
|
|
1149
|
+
"app",
|
|
1150
|
+
"co",
|
|
1151
|
+
"dev",
|
|
1152
|
+
"io",
|
|
1153
|
+
"so",
|
|
1154
|
+
"xyz",
|
|
1155
|
+
]);
|
|
1007
1156
|
// Last-resort canonical signup URL when the caller passed none and no
|
|
1008
|
-
// promoted skill / model resolution applies
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1157
|
+
// promoted skill / model resolution applies. Plain service names still use
|
|
1158
|
+
// <name>.com/signup for the common dev-SaaS case (Resend, Postmark, IPInfo,
|
|
1159
|
+
// …). Service names written with a plausible TLD suffix (together-ai, fly-io,
|
|
1160
|
+
// x-ai) preserve that signal as <name>.<tld>/signup; dogfood showed the old
|
|
1161
|
+
// compact .com fallback can send these to a different registered domain.
|
|
1162
|
+
// Non-obvious entry points are still handled upstream by promoted-skill URLs,
|
|
1163
|
+
// the canonical map, or the model.
|
|
1015
1164
|
export function guessSignupUrl(service) {
|
|
1016
|
-
const
|
|
1165
|
+
const parts = service
|
|
1166
|
+
.toLowerCase()
|
|
1167
|
+
.split(/[^a-z0-9]+/)
|
|
1168
|
+
.filter((part) => part.length > 0);
|
|
1169
|
+
if (parts.length >= 2) {
|
|
1170
|
+
const tld = parts[parts.length - 1];
|
|
1171
|
+
const root = parts.slice(0, -1).join("");
|
|
1172
|
+
if (tld !== undefined && SERVICE_NAME_TLD_SUFFIXES.has(tld) && root.length > 0) {
|
|
1173
|
+
return `https://${root}.${tld}/signup`;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
const slug = parts.join("");
|
|
1017
1177
|
return `https://${slug}.com/signup`;
|
|
1018
1178
|
}
|
|
1179
|
+
function explicitServiceDomain(service) {
|
|
1180
|
+
const parts = service
|
|
1181
|
+
.toLowerCase()
|
|
1182
|
+
.split(/[^a-z0-9]+/)
|
|
1183
|
+
.filter((part) => part.length > 0);
|
|
1184
|
+
if (parts.length < 2)
|
|
1185
|
+
return null;
|
|
1186
|
+
const tld = parts[parts.length - 1];
|
|
1187
|
+
const root = parts.slice(0, -1).join("");
|
|
1188
|
+
if (tld === undefined || root.length === 0 || !SERVICE_NAME_TLD_SUFFIXES.has(tld)) {
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
return `${root}.${tld}`;
|
|
1192
|
+
}
|
|
1193
|
+
function modelUrlContradictsExplicitServiceDomain(service, url) {
|
|
1194
|
+
const expected = explicitServiceDomain(service);
|
|
1195
|
+
if (expected === null)
|
|
1196
|
+
return false;
|
|
1197
|
+
try {
|
|
1198
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
1199
|
+
return host !== expected && !host.endsWith(`.${expected}`);
|
|
1200
|
+
}
|
|
1201
|
+
catch {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1019
1205
|
const CANONICAL_SIGNUP_URLS = {
|
|
1020
1206
|
// The model repeatedly returns the stale/dead host
|
|
1021
1207
|
// console.cloud.clickhouse.com, which no longer resolves. ClickHouse's own
|
|
@@ -1042,6 +1228,13 @@ const CANONICAL_SIGNUP_URLS = {
|
|
|
1042
1228
|
// verification. Current Anyscale docs identify console.anyscale.com as the
|
|
1043
1229
|
// organization signup flow.
|
|
1044
1230
|
anyscale: "https://console.anyscale.com",
|
|
1231
|
+
// Cartesia's marketing /signup is currently a rendered 404/dead page. The
|
|
1232
|
+
// self-serve auth app exposes the registration surface under its sign-in SPA.
|
|
1233
|
+
cartesia: "https://play.cartesia.ai/sign-in/sign-up",
|
|
1234
|
+
// Pinecone Assistant is a product, not a standalone domain. Reusing the
|
|
1235
|
+
// generic service-name guess produced pineconeassistant.com, which does not
|
|
1236
|
+
// resolve; assistant keys are issued from the normal Pinecone console.
|
|
1237
|
+
pineconeassistant: "https://app.pinecone.io/signup",
|
|
1045
1238
|
};
|
|
1046
1239
|
function canonicalSignupUrl(service) {
|
|
1047
1240
|
const slug = service.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -1144,6 +1337,10 @@ export async function resolveSignupUrl(service, llm, opts = {}) {
|
|
|
1144
1337
|
});
|
|
1145
1338
|
const url = firstHttpsUrl(resp.text);
|
|
1146
1339
|
if (url !== null) {
|
|
1340
|
+
if (modelUrlContradictsExplicitServiceDomain(service, url)) {
|
|
1341
|
+
opts.log?.(`Model URL for "${service}" contradicts explicit service domain (${url}) — using guess`);
|
|
1342
|
+
return guessSignupUrl(service);
|
|
1343
|
+
}
|
|
1147
1344
|
opts.log?.(`Resolved signup URL for "${service}" via ${resp.backend}: ${url}`);
|
|
1148
1345
|
return url;
|
|
1149
1346
|
}
|
|
@@ -2108,6 +2305,26 @@ export function looksLike404(title, bodyText) {
|
|
|
2108
2305
|
// keys URLs that hit either are dead ends.
|
|
2109
2306
|
return has404Token || notFoundPhrase;
|
|
2110
2307
|
}
|
|
2308
|
+
export function looksLikeParkedDomainPage(title, bodyText) {
|
|
2309
|
+
const hay = `${title} ${bodyText}`.toLowerCase().replace(/\s+/g, " ").slice(0, 1200);
|
|
2310
|
+
const saleSignal = /\b(?:domain|website)\s+(?:name\s+)?(?:is\s+)?for sale\b/.test(hay) ||
|
|
2311
|
+
/\bbuy this domain\b/.test(hay) ||
|
|
2312
|
+
/\bthis domain may be for sale\b/.test(hay);
|
|
2313
|
+
const parkingSignal = /\b(?:domain parking|parked domain|sedo domain parking|hugedomains|dan\.com|afternic)\b/.test(hay) ||
|
|
2314
|
+
/\bsearch for information\b/.test(hay);
|
|
2315
|
+
return saleSignal && parkingSignal;
|
|
2316
|
+
}
|
|
2317
|
+
export function looksLikeBrowserNetworkError(title, bodyText) {
|
|
2318
|
+
const hay = `${title} ${bodyText}`.toLowerCase().replace(/\s+/g, " ").slice(0, 1200);
|
|
2319
|
+
return (/\bthis site can'?t be reached\b/.test(hay) ||
|
|
2320
|
+
/\bcheck if there is a typo in\b/.test(hay) ||
|
|
2321
|
+
/\b(?:dns_probe_finished|err_name_not_resolved|err_tunnel_connection_failed|err_connection_timed_out|err_connection_refused)\b/i.test(hay));
|
|
2322
|
+
}
|
|
2323
|
+
export function shouldAbortOAuthFirstOnDeadPage(title, bodyText) {
|
|
2324
|
+
return (looksLike404(title, bodyText) ||
|
|
2325
|
+
looksLikeParkedDomainPage(title, bodyText) ||
|
|
2326
|
+
looksLikeBrowserNetworkError(title, bodyText));
|
|
2327
|
+
}
|
|
2111
2328
|
// Services whose credential is the auto-created Firebase/GCP "Browser key"
|
|
2112
2329
|
// reachable from the Google Cloud API Credentials page. Both share the same
|
|
2113
2330
|
// project-creation flow and the SAME AIzaSy key (it's both the firebase web
|
|
@@ -2203,6 +2420,9 @@ export function classifySignupHtml(html, title) {
|
|
|
2203
2420
|
titleLower.includes("page not found")) {
|
|
2204
2421
|
return "other";
|
|
2205
2422
|
}
|
|
2423
|
+
if (looksLikeParkedDomainPage(title ?? "", text)) {
|
|
2424
|
+
return "other";
|
|
2425
|
+
}
|
|
2206
2426
|
// A password field is the structural prerequisite for an auth form. We
|
|
2207
2427
|
// regex the RAW html (not the stripped text) because attribute values
|
|
2208
2428
|
// live inside the tags the stripper removes. Either the input type or a
|
|
@@ -2420,6 +2640,45 @@ export function findCreateAccountCta(inventory) {
|
|
|
2420
2640
|
}
|
|
2421
2641
|
return null;
|
|
2422
2642
|
}
|
|
2643
|
+
// Post-OAuth account-link bridges (Auth0/Keycloak first-broker-login class)
|
|
2644
|
+
// can say "Account already exists" and offer a safe in-page action like
|
|
2645
|
+
// "Add to existing account". This is not a terminal verification wall: taking
|
|
2646
|
+
// the explicit link action usually completes the provider/session attachment
|
|
2647
|
+
// and redirects to the app. Conservative text gate + clickable CTA only.
|
|
2648
|
+
export function findOAuthAccountLinkCta(pageText, inventory) {
|
|
2649
|
+
const text = pageText.toLowerCase().replace(/\s+/g, " ");
|
|
2650
|
+
const accountLinkState = /\baccount already exists\b/.test(text) ||
|
|
2651
|
+
/\blink (?:your )?(?:google|github|oauth|social) account\b/.test(text) ||
|
|
2652
|
+
/\badd to existing account\b/.test(text);
|
|
2653
|
+
if (!accountLinkState)
|
|
2654
|
+
return null;
|
|
2655
|
+
const acceptLink = /\b(?:add to existing account|link (?:account|accounts|existing account)|continue with existing account|use existing account|connect (?:account|accounts))\b/i;
|
|
2656
|
+
const rejectLink = /\b(?:create new account|new account|review profile|cancel|back|not now|sign out|log out)\b/i;
|
|
2657
|
+
for (const el of inventory) {
|
|
2658
|
+
if (el.visible === false)
|
|
2659
|
+
continue;
|
|
2660
|
+
if (el.tag !== "a" && el.tag !== "button" && el.role !== "button")
|
|
2661
|
+
continue;
|
|
2662
|
+
const label = [
|
|
2663
|
+
el.visibleText,
|
|
2664
|
+
el.ariaLabel,
|
|
2665
|
+
el.labelText,
|
|
2666
|
+
el.title,
|
|
2667
|
+
el.name,
|
|
2668
|
+
el.id,
|
|
2669
|
+
]
|
|
2670
|
+
.filter((part) => typeof part === "string" && part.trim().length > 0)
|
|
2671
|
+
.join(" ")
|
|
2672
|
+
.trim();
|
|
2673
|
+
if (label === "")
|
|
2674
|
+
continue;
|
|
2675
|
+
if (rejectLink.test(label))
|
|
2676
|
+
continue;
|
|
2677
|
+
if (acceptLink.test(label))
|
|
2678
|
+
return el;
|
|
2679
|
+
}
|
|
2680
|
+
return null;
|
|
2681
|
+
}
|
|
2423
2682
|
// Conventional signup paths to probe, in priority order. Small + ordered
|
|
2424
2683
|
// on purpose — we want the FIRST real signup form, not a fan-out across
|
|
2425
2684
|
// dozens of guesses that each cost a round-trip over a residential
|
|
@@ -2650,6 +2909,16 @@ export function detectAntiBotBlock(html) {
|
|
|
2650
2909
|
return "Imperva";
|
|
2651
2910
|
return null;
|
|
2652
2911
|
}
|
|
2912
|
+
export function detectCredentialExtractionBlock(html, visibleText = "") {
|
|
2913
|
+
const vendor = detectAntiBotBlock(html);
|
|
2914
|
+
if (vendor !== null) {
|
|
2915
|
+
return `${vendor} anti-bot interstitial`;
|
|
2916
|
+
}
|
|
2917
|
+
if (visibleText.trim().length <= 600 && isAntiBotInterstitialText(visibleText)) {
|
|
2918
|
+
return "anti-bot interstitial";
|
|
2919
|
+
}
|
|
2920
|
+
return null;
|
|
2921
|
+
}
|
|
2653
2922
|
// F17 — True when the page looks like an authenticated dashboard
|
|
2654
2923
|
// rather than a sign-up page. Triggers when a prior OAuth bind
|
|
2655
2924
|
// already linked the account and the service auto-redirects past
|
|
@@ -3245,6 +3514,18 @@ export function detectSsoRestriction(pageText) {
|
|
|
3245
3514
|
// "Single Sign-On is required", "SSO organization membership".
|
|
3246
3515
|
return /(?:managed\s+via\s+(?:sso|single\s+sign-?on)|sso[\s-]?managed|sso\s+organization|single\s+sign-?on\s+is\s+required|enforced\s+by\s+(?:sso|saml))/.test(lower);
|
|
3247
3516
|
}
|
|
3517
|
+
export function detectOAuthRegistrationDisabled(url, bodyText) {
|
|
3518
|
+
let query = "";
|
|
3519
|
+
try {
|
|
3520
|
+
query = decodeURIComponent(new URL(url).search.replace(/\+/g, " ")).toLowerCase();
|
|
3521
|
+
}
|
|
3522
|
+
catch {
|
|
3523
|
+
query = "";
|
|
3524
|
+
}
|
|
3525
|
+
const hay = `${query} ${bodyText}`.toLowerCase().replace(/\s+/g, " ");
|
|
3526
|
+
return (/\b(?:sso|oauth|social|google|github)\s+account\s+registration\s+is\s+disabled\b/.test(hay) ||
|
|
3527
|
+
/\bregistration\s+(?:via|with)\s+(?:sso|oauth|social|google|github)\s+is\s+disabled\b/.test(hay));
|
|
3528
|
+
}
|
|
3248
3529
|
// Google-OAuth-is-LOGIN-ONLY (plunk class). Some services accept Google
|
|
3249
3530
|
// only to log an EXISTING account in; they do NOT auto-provision a new
|
|
3250
3531
|
// account for a first-time Google identity. The OAuth handshake
|
|
@@ -4366,6 +4647,23 @@ export function isSignupOrLoginRoute(url) {
|
|
|
4366
4647
|
return false;
|
|
4367
4648
|
}
|
|
4368
4649
|
}
|
|
4650
|
+
export function isPostVerifyAuthResetRoute(input) {
|
|
4651
|
+
return (input.round > 0 &&
|
|
4652
|
+
!input.hasExtractedCredential &&
|
|
4653
|
+
isSignupOrLoginRoute(input.url));
|
|
4654
|
+
}
|
|
4655
|
+
export function isGitHubOAuthRateLimitPage(url, htmlOrText) {
|
|
4656
|
+
try {
|
|
4657
|
+
const u = new URL(url);
|
|
4658
|
+
if (u.hostname !== "github.com" || !/^\/login\/oauth\/authorize\b/i.test(u.pathname)) {
|
|
4659
|
+
return false;
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
catch {
|
|
4663
|
+
return false;
|
|
4664
|
+
}
|
|
4665
|
+
return /\btoo many requests\b/i.test(htmlOrText) || /\bsecondary rate limit\b/i.test(htmlOrText);
|
|
4666
|
+
}
|
|
4369
4667
|
// The scheme://host root of a URL (no path/query) — the place a service
|
|
4370
4668
|
// redirects an authenticated user to their dashboard. Null on a malformed
|
|
4371
4669
|
// URL. Exported for unit tests.
|
|
@@ -5037,6 +5335,8 @@ export class SignupAgent {
|
|
|
5037
5335
|
// regression that produced the Security Code challenge on
|
|
5038
5336
|
// methoxine's account during the rc.30 Railway run.
|
|
5039
5337
|
let committedToEmailPath = forceFormFill;
|
|
5338
|
+
let lastGitHubAuthorizeUrl = null;
|
|
5339
|
+
let repeatedGitHubAuthorizeApprovals = 0;
|
|
5040
5340
|
const oauthCandidates = await this.resolveOAuthCandidates(task, steps);
|
|
5041
5341
|
for (;;) {
|
|
5042
5342
|
await this.browser.waitForFormReady();
|
|
@@ -5072,9 +5372,26 @@ export class SignupAgent {
|
|
|
5072
5372
|
};
|
|
5073
5373
|
}
|
|
5074
5374
|
if (/^https:\/\/github\.com\/login\/oauth\/authorize\b/i.test(state.url)) {
|
|
5375
|
+
if (isGitHubOAuthRateLimitPage(state.url, state.html)) {
|
|
5376
|
+
steps.push("OAuth: GitHub authorize page returned a secondary rate limit — stopping instead of retrying consent.");
|
|
5377
|
+
return {
|
|
5378
|
+
kind: "planning_failed",
|
|
5379
|
+
reason: "oauth_provider_rate_limited: GitHub returned a secondary rate limit during OAuth authorize",
|
|
5380
|
+
};
|
|
5381
|
+
}
|
|
5075
5382
|
const advanced = await this.browser.advanceOAuthConsent("github");
|
|
5076
5383
|
steps.push(`OAuth: GitHub authorize page appeared during form-fill — ${advanced ? "approved consent" : "no approve control found"}`);
|
|
5077
5384
|
if (advanced) {
|
|
5385
|
+
repeatedGitHubAuthorizeApprovals =
|
|
5386
|
+
lastGitHubAuthorizeUrl === state.url ? repeatedGitHubAuthorizeApprovals + 1 : 1;
|
|
5387
|
+
lastGitHubAuthorizeUrl = state.url;
|
|
5388
|
+
if (repeatedGitHubAuthorizeApprovals > 2) {
|
|
5389
|
+
steps.push("OAuth: GitHub authorize page repeated after consent approval — aborting instead of re-approving in a loop.");
|
|
5390
|
+
return {
|
|
5391
|
+
kind: "planning_failed",
|
|
5392
|
+
reason: "oauth_loop_detected: GitHub OAuth authorize page repeated after consent approval during form-fill",
|
|
5393
|
+
};
|
|
5394
|
+
}
|
|
5078
5395
|
await this.browser.wait(3);
|
|
5079
5396
|
continue;
|
|
5080
5397
|
}
|
|
@@ -5824,6 +6141,8 @@ export class SignupAgent {
|
|
|
5824
6141
|
}
|
|
5825
6142
|
};
|
|
5826
6143
|
const oauthCandidates = await this.resolveOAuthCandidates(task, steps);
|
|
6144
|
+
let lastGitHubAuthorizeUrl = null;
|
|
6145
|
+
let repeatedGitHubAuthorizeApprovals = 0;
|
|
5827
6146
|
for (;;) {
|
|
5828
6147
|
await this.browser.waitForFormReady();
|
|
5829
6148
|
const dismissed = await this.browser.dismissConsentBanner();
|
|
@@ -5837,6 +6156,20 @@ export class SignupAgent {
|
|
|
5837
6156
|
}
|
|
5838
6157
|
const browserState = frame.state;
|
|
5839
6158
|
const inventory = frame.inventory;
|
|
6159
|
+
if (looksLikeParkedDomainPage(browserState.title, browserState.html)) {
|
|
6160
|
+
steps.push("Form-fill: parked/domain-for-sale page detected — aborting signup automation.");
|
|
6161
|
+
return {
|
|
6162
|
+
kind: "planning_failed",
|
|
6163
|
+
reason: "parked_domain: signup URL resolved to a domain-for-sale or parking page",
|
|
6164
|
+
};
|
|
6165
|
+
}
|
|
6166
|
+
if (shouldAbortOAuthFirstOnDeadPage(browserState.title, browserState.html)) {
|
|
6167
|
+
steps.push("Form-fill: dead/404 signup page detected — aborting OAuth-first wait.");
|
|
6168
|
+
return {
|
|
6169
|
+
kind: "planning_failed",
|
|
6170
|
+
reason: "dead_signup_url: signup URL rendered a 404/dead page before any OAuth provider appeared",
|
|
6171
|
+
};
|
|
6172
|
+
}
|
|
5840
6173
|
const googleIdentifierAdvance = await this.advancePinnedGoogleIdentifierPage({
|
|
5841
6174
|
task,
|
|
5842
6175
|
url: browserState.url,
|
|
@@ -5853,9 +6186,28 @@ export class SignupAgent {
|
|
|
5853
6186
|
};
|
|
5854
6187
|
}
|
|
5855
6188
|
if (/^https:\/\/github\.com\/login\/oauth\/authorize\b/i.test(browserState.url)) {
|
|
6189
|
+
if (isGitHubOAuthRateLimitPage(browserState.url, browserState.html)) {
|
|
6190
|
+
steps.push("OAuth: GitHub authorize page returned a secondary rate limit — stopping instead of retrying consent.");
|
|
6191
|
+
return {
|
|
6192
|
+
kind: "planning_failed",
|
|
6193
|
+
reason: "oauth_provider_rate_limited: GitHub returned a secondary rate limit during OAuth authorize",
|
|
6194
|
+
};
|
|
6195
|
+
}
|
|
5856
6196
|
const advanced = await this.browser.advanceOAuthConsent("github");
|
|
5857
6197
|
steps.push(`OAuth: GitHub authorize page appeared during form-fill — ${advanced ? "approved consent" : "no approve control found"}`);
|
|
5858
6198
|
if (advanced) {
|
|
6199
|
+
repeatedGitHubAuthorizeApprovals =
|
|
6200
|
+
lastGitHubAuthorizeUrl === browserState.url
|
|
6201
|
+
? repeatedGitHubAuthorizeApprovals + 1
|
|
6202
|
+
: 1;
|
|
6203
|
+
lastGitHubAuthorizeUrl = browserState.url;
|
|
6204
|
+
if (repeatedGitHubAuthorizeApprovals > 2) {
|
|
6205
|
+
steps.push("OAuth: GitHub authorize page repeated after consent approval — aborting instead of re-approving in a loop.");
|
|
6206
|
+
return {
|
|
6207
|
+
kind: "planning_failed",
|
|
6208
|
+
reason: "oauth_loop_detected: GitHub OAuth authorize page repeated after consent approval during form-fill",
|
|
6209
|
+
};
|
|
6210
|
+
}
|
|
5859
6211
|
await this.browser.wait(3);
|
|
5860
6212
|
continue;
|
|
5861
6213
|
}
|
|
@@ -6758,7 +7110,7 @@ export class SignupAgent {
|
|
|
6758
7110
|
}
|
|
6759
7111
|
}
|
|
6760
7112
|
// 0.8.3-rc.1 — widened from 2 → 4 minutes. The 2-min window forced
|
|
6761
|
-
// the operator to drop everything immediately on a
|
|
7113
|
+
// the operator to drop everything immediately on a heightened-auth alert.
|
|
6762
7114
|
// For batch-harvest runs the operator is rarely staring at the
|
|
6763
7115
|
// phone; 4 minutes gives realistic time to switch devices, unlock,
|
|
6764
7116
|
// open the Google app, and tap. Matches the same wait window the
|
|
@@ -7118,10 +7470,11 @@ export class SignupAgent {
|
|
|
7118
7470
|
// landing as a wrong-URL signal: clear signedIn so the recovery path
|
|
7119
7471
|
// (Tier B CTA → real signup entry) runs instead of skipping signup.
|
|
7120
7472
|
const landedBodyText = await this.browser.extractText().catch(() => "");
|
|
7473
|
+
const landedDead = shouldAbortOAuthFirstOnDeadPage(landed.title, landedBodyText);
|
|
7121
7474
|
const landed404 = looksLike404(landed.title, landedBodyText);
|
|
7122
|
-
if (
|
|
7475
|
+
if (landedDead && signedIn) {
|
|
7123
7476
|
signedIn = false;
|
|
7124
|
-
steps.push(`${task.service}: landing ${pathOf(landed.url)} is a 404 shell, not an ` +
|
|
7477
|
+
steps.push(`${task.service}: landing ${pathOf(landed.url)} is a dead/404 shell, not an ` +
|
|
7125
7478
|
`authenticated dashboard — recovering the real signup entry`);
|
|
7126
7479
|
}
|
|
7127
7480
|
// SPA-settle re-check (returning-user cluster). An authenticated SPA
|
|
@@ -7160,6 +7513,10 @@ export class SignupAgent {
|
|
|
7160
7513
|
`skipping signup, routing straight to key extraction`);
|
|
7161
7514
|
alreadyAuthenticated = true;
|
|
7162
7515
|
}
|
|
7516
|
+
else if (landedDead) {
|
|
7517
|
+
needsRecovery = true;
|
|
7518
|
+
steps.push(`${task.service}: landing ${pathOf(landed.url)} is a dead/404 signup page — attempting recovery`);
|
|
7519
|
+
}
|
|
7163
7520
|
else if (task.signupUrl === undefined) {
|
|
7164
7521
|
needsRecovery = !(await this.looksLikeSignupPage());
|
|
7165
7522
|
}
|
|
@@ -7623,9 +7980,11 @@ export class SignupAgent {
|
|
|
7623
7980
|
...(task.scopeHint !== undefined ? { scopeHint: task.scopeHint } : {}),
|
|
7624
7981
|
...(task.machineToken !== undefined ? { machineToken: task.machineToken } : {}),
|
|
7625
7982
|
...(task.apiBase !== undefined ? { apiBase: task.apiBase } : {}),
|
|
7983
|
+
allowOperatorInboxOtp: task.allowOperatorInboxOtp === true,
|
|
7626
7984
|
});
|
|
7627
7985
|
}
|
|
7628
|
-
if (hasUsableCredentialBundle(credentials)
|
|
7986
|
+
if (hasUsableCredentialBundle(credentials) &&
|
|
7987
|
+
!terminalReasonInvalidatesCredentialSuccess(this.lastPostVerifyDoneReason)) {
|
|
7629
7988
|
// 0.8.3-rc.1 — when extractCredentials short-circuited
|
|
7630
7989
|
// before postVerifyLoop ran, no captures were written.
|
|
7631
7990
|
// Emit a synthetic extract round so auto-promote can
|
|
@@ -7640,6 +7999,10 @@ export class SignupAgent {
|
|
|
7640
7999
|
...this.resultTail(),
|
|
7641
8000
|
};
|
|
7642
8001
|
}
|
|
8002
|
+
if (terminalReasonInvalidatesCredentialSuccess(this.lastPostVerifyDoneReason)) {
|
|
8003
|
+
this.lastPostVerifyDoneReason =
|
|
8004
|
+
`[existing_account_no_extract] terminal planner reason invalidated the extracted candidate: ${this.lastPostVerifyDoneReason}`;
|
|
8005
|
+
}
|
|
7643
8006
|
// 0.8.2-rc.10 — same sentinel-pattern routing the runOAuthFlow
|
|
7644
8007
|
// path uses. The post-verify loop sets lastPostVerifyDoneReason
|
|
7645
8008
|
// with [stuck_loop] or [existing_account_no_extract] markers
|
|
@@ -7894,6 +8257,7 @@ export class SignupAgent {
|
|
|
7894
8257
|
...(task.scopeHint !== undefined ? { scopeHint: task.scopeHint } : {}),
|
|
7895
8258
|
...(task.machineToken !== undefined ? { machineToken: task.machineToken } : {}),
|
|
7896
8259
|
...(task.apiBase !== undefined ? { apiBase: task.apiBase } : {}),
|
|
8260
|
+
allowOperatorInboxOtp: task.allowOperatorInboxOtp === true,
|
|
7897
8261
|
});
|
|
7898
8262
|
}
|
|
7899
8263
|
}
|
|
@@ -7941,7 +8305,7 @@ export class SignupAgent {
|
|
|
7941
8305
|
}
|
|
7942
8306
|
}
|
|
7943
8307
|
}
|
|
7944
|
-
if (credentials
|
|
8308
|
+
if (hasUsableCredentialBundle(credentials)) {
|
|
7945
8309
|
// Form-fill + email success returns here WITHOUT entering the post-verify
|
|
7946
8310
|
// loop, so its extract-round salvage never ran — write one now (mailjet).
|
|
7947
8311
|
await this.salvageExtractCaptureIfNeeded(task.service, credentials, false);
|
|
@@ -8418,14 +8782,6 @@ export class SignupAgent {
|
|
|
8418
8782
|
machineToken: task.machineToken,
|
|
8419
8783
|
apiBase: task.apiBase,
|
|
8420
8784
|
});
|
|
8421
|
-
// rc.18 — opt-in Telegram fallback. Bypasses the email
|
|
8422
|
-
// path (which collapses to Sent only when GMAIL_USER ==
|
|
8423
|
-
// account.email). No-op without TELEGRAM_BOT_TOKEN env.
|
|
8424
|
-
void sendTelegramHeightenedAuth({
|
|
8425
|
-
service: task.service,
|
|
8426
|
-
digit: String(matchNum),
|
|
8427
|
-
windowSeconds: 240,
|
|
8428
|
-
});
|
|
8429
8785
|
}
|
|
8430
8786
|
else {
|
|
8431
8787
|
// Extractor missed the number — Google phrasing has
|
|
@@ -8443,11 +8799,6 @@ export class SignupAgent {
|
|
|
8443
8799
|
machineToken: task.machineToken,
|
|
8444
8800
|
apiBase: task.apiBase,
|
|
8445
8801
|
});
|
|
8446
|
-
void sendTelegramHeightenedAuth({
|
|
8447
|
-
service: task.service,
|
|
8448
|
-
digit: null,
|
|
8449
|
-
windowSeconds: 240,
|
|
8450
|
-
});
|
|
8451
8802
|
}
|
|
8452
8803
|
// Either way (number found or not), the user can still
|
|
8453
8804
|
// clear the challenge in the bot's browser window or by
|
|
@@ -8509,7 +8860,8 @@ export class SignupAgent {
|
|
|
8509
8860
|
// degrades to the phone-tap path below.
|
|
8510
8861
|
if (provider.id === "github" &&
|
|
8511
8862
|
task.machineToken !== undefined &&
|
|
8512
|
-
task.machineToken.length > 0
|
|
8863
|
+
task.machineToken.length > 0 &&
|
|
8864
|
+
task.allowOperatorInboxOtp === true) {
|
|
8513
8865
|
steps.push("GitHub: verify-it's-you challenge — polling operator inbox for a device-confirmation link (up to 60s)");
|
|
8514
8866
|
try {
|
|
8515
8867
|
const { readGitHubChallengeLink } = await import("./read-otp.js");
|
|
@@ -8538,8 +8890,8 @@ export class SignupAgent {
|
|
|
8538
8890
|
catch (err) {
|
|
8539
8891
|
steps.push(`GitHub: challenge-clearing import/call threw (${err instanceof Error ? err.message : String(err)})`);
|
|
8540
8892
|
}
|
|
8541
|
-
// 0.8.3-rc.1 — fall back to the phone-tap path: fire
|
|
8542
|
-
//
|
|
8893
|
+
// 0.8.3-rc.1 — fall back to the phone-tap path: fire a
|
|
8894
|
+
// heightened-auth notification and wait 4
|
|
8543
8895
|
// minutes for the operator to tap their phone. This is the
|
|
8544
8896
|
// same shape Google's challenge path already uses; without
|
|
8545
8897
|
// it the bot just times out silently with no operator
|
|
@@ -8553,11 +8905,6 @@ export class SignupAgent {
|
|
|
8553
8905
|
machineToken: task.machineToken,
|
|
8554
8906
|
apiBase: task.apiBase,
|
|
8555
8907
|
});
|
|
8556
|
-
void sendTelegramHeightenedAuth({
|
|
8557
|
-
service: task.service,
|
|
8558
|
-
digit: null,
|
|
8559
|
-
windowSeconds: 240,
|
|
8560
|
-
});
|
|
8561
8908
|
const cleared = await this.waitForGitHubChallenge(steps);
|
|
8562
8909
|
if (cleared) {
|
|
8563
8910
|
steps.push("GitHub: challenge cleared — re-classifying for the consent flow");
|
|
@@ -9090,6 +9437,15 @@ export class SignupAgent {
|
|
|
9090
9437
|
// detectManualLoginFallback would otherwise swallow it as
|
|
9091
9438
|
// oauth_session_not_persisted and abort. The account simply needs
|
|
9092
9439
|
// creating via email, so re-route to form-fill instead of bailing.
|
|
9440
|
+
if (detectOAuthRegistrationDisabled(gateState.url, gateText)) {
|
|
9441
|
+
return {
|
|
9442
|
+
success: false,
|
|
9443
|
+
error: `oauth_signup_disabled: ${task.service} rejected OAuth signup because account ` +
|
|
9444
|
+
`registration through this SSO/OAuth provider is disabled. Try a non-OAuth signup path manually.`,
|
|
9445
|
+
steps,
|
|
9446
|
+
...this.resultTail(),
|
|
9447
|
+
};
|
|
9448
|
+
}
|
|
9093
9449
|
if (detectGoogleNoAccount(gateState.url, gateText)) {
|
|
9094
9450
|
// Commit to email for the rest of the run — OAuth is login-only here, so
|
|
9095
9451
|
// the OAuth-first scan must not re-fire after the form-fill re-route.
|
|
@@ -9127,6 +9483,9 @@ export class SignupAgent {
|
|
|
9127
9483
|
machineToken.length === 0) {
|
|
9128
9484
|
otpResult = { code: null, reason: "no_machine_token" };
|
|
9129
9485
|
}
|
|
9486
|
+
else if (task.allowOperatorInboxOtp !== true) {
|
|
9487
|
+
otpResult = { code: null, reason: "operator_inbox_consent_missing" };
|
|
9488
|
+
}
|
|
9130
9489
|
else {
|
|
9131
9490
|
steps.push(`Email-OTP gate detected (${pathOf(gateState.url)}) — polling operator inbox for the code` +
|
|
9132
9491
|
(domain !== null ? ` (from_domain=${domain})` : ""));
|
|
@@ -9280,6 +9639,7 @@ export class SignupAgent {
|
|
|
9280
9639
|
...(task.scopeHint !== undefined ? { scopeHint: task.scopeHint } : {}),
|
|
9281
9640
|
...(task.machineToken !== undefined ? { machineToken: task.machineToken } : {}),
|
|
9282
9641
|
...(task.apiBase !== undefined ? { apiBase: task.apiBase } : {}),
|
|
9642
|
+
allowOperatorInboxOtp: task.allowOperatorInboxOtp === true,
|
|
9283
9643
|
});
|
|
9284
9644
|
}
|
|
9285
9645
|
catch (err) {
|
|
@@ -9314,7 +9674,8 @@ export class SignupAgent {
|
|
|
9314
9674
|
// complete, usable bundle. Require >=2 named (non-metadata) credentials
|
|
9315
9675
|
// so a lone ID (e.g. just application_id) still fails honestly: an ID
|
|
9316
9676
|
// without a secret isn't a usable credential.
|
|
9317
|
-
if (hasUsableCredentialBundle(credentials)
|
|
9677
|
+
if (hasUsableCredentialBundle(credentials) &&
|
|
9678
|
+
!terminalReasonInvalidatesCredentialSuccess(this.lastPostVerifyDoneReason)) {
|
|
9318
9679
|
return {
|
|
9319
9680
|
success: true,
|
|
9320
9681
|
credentials: { ...credentials },
|
|
@@ -9322,6 +9683,10 @@ export class SignupAgent {
|
|
|
9322
9683
|
...this.resultTail(),
|
|
9323
9684
|
};
|
|
9324
9685
|
}
|
|
9686
|
+
if (terminalReasonInvalidatesCredentialSuccess(this.lastPostVerifyDoneReason)) {
|
|
9687
|
+
this.lastPostVerifyDoneReason =
|
|
9688
|
+
`[existing_account_no_extract] terminal planner reason invalidated the extracted candidate: ${this.lastPostVerifyDoneReason}`;
|
|
9689
|
+
}
|
|
9325
9690
|
// No API key. Distinguish a billing/card wall (onboarding_blocked)
|
|
9326
9691
|
// from a generic navigation miss — never grep-loop a paid wall.
|
|
9327
9692
|
// rc.39 — fold the planner's `done` reason into the text we
|
|
@@ -9349,7 +9714,8 @@ export class SignupAgent {
|
|
|
9349
9714
|
};
|
|
9350
9715
|
}
|
|
9351
9716
|
if (postSignupGate.failure?.kind === "payment" ||
|
|
9352
|
-
postSignupGate.failure?.kind === "phone"
|
|
9717
|
+
postSignupGate.failure?.kind === "phone" ||
|
|
9718
|
+
postSignupGate.failure?.kind === "permission_denied") {
|
|
9353
9719
|
return {
|
|
9354
9720
|
success: false,
|
|
9355
9721
|
error: postSignupGate.failure.error,
|
|
@@ -9750,6 +10116,7 @@ ${formatInventory(input.inventory)}`,
|
|
|
9750
10116
|
...(task.scopeHint !== undefined ? { scopeHint: task.scopeHint } : {}),
|
|
9751
10117
|
...(task.machineToken !== undefined ? { machineToken: task.machineToken } : {}),
|
|
9752
10118
|
...(task.apiBase !== undefined ? { apiBase: task.apiBase } : {}),
|
|
10119
|
+
allowOperatorInboxOtp: task.allowOperatorInboxOtp === true,
|
|
9753
10120
|
});
|
|
9754
10121
|
}
|
|
9755
10122
|
// Drive the browser toward the API key after the account exists —
|
|
@@ -9784,15 +10151,10 @@ ${formatInventory(input.inventory)}`,
|
|
|
9784
10151
|
machineToken: this.currentMachineToken,
|
|
9785
10152
|
apiBase: this.currentApiBase,
|
|
9786
10153
|
});
|
|
9787
|
-
void sendTelegramHeightenedAuth({
|
|
9788
|
-
service,
|
|
9789
|
-
digit,
|
|
9790
|
-
windowSeconds: 240,
|
|
9791
|
-
});
|
|
9792
10154
|
// 0.8.3-rc.1 — vision-LLM fallback for the mid-post-verify path.
|
|
9793
10155
|
// When the planner's reason names a challenge but no digit, take
|
|
9794
10156
|
// a screenshot, ask Claude vision what number is on screen, and
|
|
9795
|
-
// fire a SECOND
|
|
10157
|
+
// fire a SECOND notification with the extracted number. The first
|
|
9796
10158
|
// notification went out immediately (so the operator knows to
|
|
9797
10159
|
// grab their phone); this follows up with the number as soon as
|
|
9798
10160
|
// vision returns (~2-5s).
|
|
@@ -9804,11 +10166,6 @@ ${formatInventory(input.inventory)}`,
|
|
|
9804
10166
|
const followUp = `Google challenge mid-post-verify: vision LLM read "${visionDigit}" from the screen — tap that on your phone.`;
|
|
9805
10167
|
console.error(`[universal-bot] ${followUp}`);
|
|
9806
10168
|
steps.push(`Post-verify: ${followUp}`);
|
|
9807
|
-
void sendTelegramHeightenedAuth({
|
|
9808
|
-
service,
|
|
9809
|
-
digit: visionDigit,
|
|
9810
|
-
windowSeconds: 240,
|
|
9811
|
-
});
|
|
9812
10169
|
void notifyHeightenedAuth({
|
|
9813
10170
|
service,
|
|
9814
10171
|
digit: visionDigit,
|
|
@@ -10370,9 +10727,17 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
10370
10727
|
if (!/^(0|false|off|no)$/i.test(process.env.NAV_SEARCH ?? "")) {
|
|
10371
10728
|
try {
|
|
10372
10729
|
const navResult = await this.runNavSearchPhase(args, oauth);
|
|
10373
|
-
if (
|
|
10730
|
+
if (hasUsableCredentialBundle(navResult)) {
|
|
10374
10731
|
return navResult;
|
|
10375
10732
|
}
|
|
10733
|
+
if (hasAnyExtractedCredential(navResult)) {
|
|
10734
|
+
for (const [k, v] of Object.entries(navResult)) {
|
|
10735
|
+
if (credentials[k] === undefined)
|
|
10736
|
+
credentials[k] = v;
|
|
10737
|
+
}
|
|
10738
|
+
args.steps.push("nav-search: found credential-shaped data, but not a final usable bundle yet — " +
|
|
10739
|
+
"continuing post-verify extraction instead of returning a false failure");
|
|
10740
|
+
}
|
|
10376
10741
|
if (this.resolvedSignupUrl !== undefined &&
|
|
10377
10742
|
KEYS_DESTINATION_URL.test(this.resolvedSignupUrl)) {
|
|
10378
10743
|
args.steps.push("nav-search: curated signup_url is a credential surface and no key was found — " +
|
|
@@ -10380,6 +10745,32 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
10380
10745
|
return navResult;
|
|
10381
10746
|
}
|
|
10382
10747
|
args.steps.push("nav-search: no key via navigation alone — handing off to the planner from the current surface");
|
|
10748
|
+
const afterNavText = await this.browser.extractText().catch(() => "");
|
|
10749
|
+
const afterNavInventory = await this.browser
|
|
10750
|
+
.extractInteractiveElements()
|
|
10751
|
+
.catch(() => []);
|
|
10752
|
+
const accountLinkCta = findOAuthAccountLinkCta(afterNavText, afterNavInventory);
|
|
10753
|
+
if (accountLinkCta !== null) {
|
|
10754
|
+
const label = (accountLinkCta.visibleText ??
|
|
10755
|
+
accountLinkCta.ariaLabel ??
|
|
10756
|
+
accountLinkCta.labelText ??
|
|
10757
|
+
"link existing account").trim();
|
|
10758
|
+
args.steps.push(`Post-verify: OAuth account-link bridge detected — clicking "${label}" before planner handoff.`);
|
|
10759
|
+
try {
|
|
10760
|
+
await this.browser.click(accountLinkCta.selector);
|
|
10761
|
+
await this.browser.wait(2);
|
|
10762
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
10763
|
+
}
|
|
10764
|
+
catch (err) {
|
|
10765
|
+
args.steps.push(`Post-verify: account-link bridge click threw (${err instanceof Error ? err.message : String(err)}) — continuing with planner handoff.`);
|
|
10766
|
+
}
|
|
10767
|
+
}
|
|
10768
|
+
if (isAtOAuthAccountLinkVerificationGate(afterNavText)) {
|
|
10769
|
+
this.lastPostVerifyDoneReason =
|
|
10770
|
+
`[oauth_account_link_verification] ${afterNavText.slice(0, 300)}`;
|
|
10771
|
+
args.steps.push("Post-verify: OAuth account-link email verification wall detected — stopping before fallback URL guesses.");
|
|
10772
|
+
return {};
|
|
10773
|
+
}
|
|
10383
10774
|
}
|
|
10384
10775
|
catch (err) {
|
|
10385
10776
|
args.steps.push(`nav-search: errored (${err instanceof Error ? err.message : String(err)}) — falling back to the planner`);
|
|
@@ -10409,6 +10800,43 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
10409
10800
|
// form-fill phase already captured (captureSignupFormRounds); 0 on the
|
|
10410
10801
|
// OAuth path, so this is unchanged for OAuth skills.
|
|
10411
10802
|
let capturedRound = this.captureChainRound;
|
|
10803
|
+
const prePlannerInventory = await this.browser.extractInteractiveElements().catch(() => []);
|
|
10804
|
+
const setupFormBlocksCuratedRoute = isRequiredAccountSetupForm(prePlannerInventory);
|
|
10805
|
+
if (hasCuratedServiceKeyPath(args.service) && !setupFormBlocksCuratedRoute) {
|
|
10806
|
+
const fallback = pickStuckLoopFallbackUrl(this.browser.currentUrl(), recovery.triedFallbackUrls, args.service, this.resolvedSignupUrl);
|
|
10807
|
+
if (fallback !== null) {
|
|
10808
|
+
recovery.triedFallbackUrls.add(fallback);
|
|
10809
|
+
args.steps.push(`Post-verify: nav-search exhausted but ${args.service} has a curated credential route — trying ${fallback} before greedy planning.`);
|
|
10810
|
+
try {
|
|
10811
|
+
await this.browser.goto(fallback);
|
|
10812
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
10813
|
+
const direct = await this.harvestVisibleCredentials();
|
|
10814
|
+
for (const [k, v] of Object.entries(direct)) {
|
|
10815
|
+
if (credentials[k] === undefined)
|
|
10816
|
+
credentials[k] = v;
|
|
10817
|
+
}
|
|
10818
|
+
if (hasAnyExtractedCredential(credentials)) {
|
|
10819
|
+
await this.writeFastPathSyntheticCapture(args.service, capturedRound, oauth, "curated credential route synthetic extract — nav-search exhausted, fallback route exposed credentials");
|
|
10820
|
+
return credentials;
|
|
10821
|
+
}
|
|
10822
|
+
const minted = await this.attemptMintNewKey(args.steps, args.service);
|
|
10823
|
+
if (minted !== null && hasAnyExtractedCredential(minted)) {
|
|
10824
|
+
for (const [k, v] of Object.entries(minted)) {
|
|
10825
|
+
if (credentials[k] === undefined)
|
|
10826
|
+
credentials[k] = v;
|
|
10827
|
+
}
|
|
10828
|
+
await this.writeFastPathSyntheticCapture(args.service, capturedRound, oauth, "curated credential route synthetic extract — minted/extracted after nav-search fallback");
|
|
10829
|
+
return credentials;
|
|
10830
|
+
}
|
|
10831
|
+
}
|
|
10832
|
+
catch (err) {
|
|
10833
|
+
args.steps.push(`Post-verify: curated credential route fallback errored (${err instanceof Error ? err.message : String(err)}) — continuing with greedy planner.`);
|
|
10834
|
+
}
|
|
10835
|
+
}
|
|
10836
|
+
}
|
|
10837
|
+
else if (setupFormBlocksCuratedRoute) {
|
|
10838
|
+
args.steps.push("Post-verify: required account setup form is still active — deferring curated credential route until setup is complete.");
|
|
10839
|
+
}
|
|
10412
10840
|
const credentialTracker = new PostSignupCredentialTracker(credentials);
|
|
10413
10841
|
// Gate URLs we've already polled the operator's gmail for, so a
|
|
10414
10842
|
// multi-round wait on the same email-OTP page doesn't re-poll.
|
|
@@ -10448,6 +10876,23 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
10448
10876
|
// the planner choosing the right click, so an on-screen key is never
|
|
10449
10877
|
// missed into a maxRounds bail. Merge-only (never overwrites a prior
|
|
10450
10878
|
// capture); both extractors are best-effort.
|
|
10879
|
+
try {
|
|
10880
|
+
const currentUrl = this.browser.currentUrl();
|
|
10881
|
+
if (!recovery.revealSweepUrls.has(currentUrl)) {
|
|
10882
|
+
const sweepText = await this.browser.extractVisibleText().catch(() => "");
|
|
10883
|
+
if (shouldRevealBeforeCredentialSweep({ url: currentUrl, pageText: sweepText })) {
|
|
10884
|
+
recovery.revealSweepUrls.add(currentUrl);
|
|
10885
|
+
const reveal = await this.browser.revealMaskedCredentials();
|
|
10886
|
+
if (reveal.clicked > 0) {
|
|
10887
|
+
args.steps.push(`Post-verify round ${round}: credential surface has reveal controls — clicked ${reveal.clicked} before sweeping.`);
|
|
10888
|
+
await this.browser.wait(1);
|
|
10889
|
+
}
|
|
10890
|
+
}
|
|
10891
|
+
}
|
|
10892
|
+
}
|
|
10893
|
+
catch {
|
|
10894
|
+
// reveal is opportunistic; normal extraction still runs below
|
|
10895
|
+
}
|
|
10451
10896
|
try {
|
|
10452
10897
|
const sweep = await this.extractCredentials();
|
|
10453
10898
|
for (const [k, v] of Object.entries(sweep)) {
|
|
@@ -10771,6 +11216,89 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
10771
11216
|
else {
|
|
10772
11217
|
lastReachablePostVerifyUrl = state.url;
|
|
10773
11218
|
}
|
|
11219
|
+
if (isPostVerifyAuthResetRoute({
|
|
11220
|
+
url: state.url,
|
|
11221
|
+
round,
|
|
11222
|
+
hasExtractedCredential: hasAnyExtractedCredential(credentials),
|
|
11223
|
+
})) {
|
|
11224
|
+
this.lastPostVerifyDoneReason =
|
|
11225
|
+
`[stuck_loop] post-verify credential search returned to auth/signup route ${state.url}; ` +
|
|
11226
|
+
`stopping instead of starting a second signup.`;
|
|
11227
|
+
args.steps.push(`Post-verify round ${round}: credential search returned to auth/signup route (${pathOf(state.url)}) — ` +
|
|
11228
|
+
`stopping instead of starting a second signup.`);
|
|
11229
|
+
break;
|
|
11230
|
+
}
|
|
11231
|
+
if (serviceSlug(args.service) === "render" &&
|
|
11232
|
+
!hasAnyExtractedCredential(credentials) &&
|
|
11233
|
+
/dashboard\.render\.com/i.test(state.url)) {
|
|
11234
|
+
const onRenderAccountSettings = /\/u\/[^/]+\/settings\b/i.test(state.url);
|
|
11235
|
+
if (!onRenderAccountSettings) {
|
|
11236
|
+
const accountSettingsLink = findRenderAccountSettingsLink(inventory, recovery.clickedKeysLinks);
|
|
11237
|
+
if (accountSettingsLink !== null) {
|
|
11238
|
+
recovery.clickedKeysLinks.add(accountSettingsLink.selector);
|
|
11239
|
+
const label = accountSettingsLink.visibleText ??
|
|
11240
|
+
accountSettingsLink.ariaLabel ??
|
|
11241
|
+
accountSettingsLink.href ??
|
|
11242
|
+
accountSettingsLink.selector;
|
|
11243
|
+
args.steps.push(`Post-verify round ${round}: Render account menu exposes "${label.slice(0, 60)}" — entering account settings before API-key navigation.`);
|
|
11244
|
+
try {
|
|
11245
|
+
await this.browser.click(accountSettingsLink.selector);
|
|
11246
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
11247
|
+
hint = undefined;
|
|
11248
|
+
recovery.prevSignature = null;
|
|
11249
|
+
recovery.prevInventorySize = -1;
|
|
11250
|
+
continue;
|
|
11251
|
+
}
|
|
11252
|
+
catch (err) {
|
|
11253
|
+
args.steps.push(`Post-verify round ${round}: Render account-settings click failed (${err instanceof Error ? err.message : String(err)}) — continuing.`);
|
|
11254
|
+
}
|
|
11255
|
+
}
|
|
11256
|
+
else if (!recovery.triedRenderAccountMenu) {
|
|
11257
|
+
const accountMenu = findRenderAccountMenuTrigger(inventory);
|
|
11258
|
+
if (accountMenu !== null) {
|
|
11259
|
+
recovery.triedRenderAccountMenu = true;
|
|
11260
|
+
const label = accountMenu.visibleText ??
|
|
11261
|
+
accountMenu.ariaLabel ??
|
|
11262
|
+
accountMenu.title ??
|
|
11263
|
+
accountMenu.selector;
|
|
11264
|
+
args.steps.push(`Post-verify round ${round}: Render dashboard has no API-key link visible — opening account menu "${label.slice(0, 40)}".`);
|
|
11265
|
+
try {
|
|
11266
|
+
await this.browser.click(accountMenu.selector);
|
|
11267
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
11268
|
+
hint = undefined;
|
|
11269
|
+
recovery.prevSignature = null;
|
|
11270
|
+
recovery.prevInventorySize = -1;
|
|
11271
|
+
continue;
|
|
11272
|
+
}
|
|
11273
|
+
catch (err) {
|
|
11274
|
+
args.steps.push(`Post-verify round ${round}: Render account-menu click failed (${err instanceof Error ? err.message : String(err)}) — continuing.`);
|
|
11275
|
+
}
|
|
11276
|
+
}
|
|
11277
|
+
}
|
|
11278
|
+
}
|
|
11279
|
+
else if (!/#api-keys\b/i.test(state.url)) {
|
|
11280
|
+
const keysLink = findApiKeysNavLink(inventory, recovery.clickedKeysLinks);
|
|
11281
|
+
if (keysLink !== null) {
|
|
11282
|
+
recovery.clickedKeysLinks.add(keysLink.selector);
|
|
11283
|
+
const label = keysLink.visibleText ??
|
|
11284
|
+
keysLink.ariaLabel ??
|
|
11285
|
+
keysLink.href ??
|
|
11286
|
+
keysLink.selector;
|
|
11287
|
+
args.steps.push(`Post-verify round ${round}: Render account settings reached — clicking API-key anchor "${label.slice(0, 60)}".`);
|
|
11288
|
+
try {
|
|
11289
|
+
await this.browser.click(keysLink.selector);
|
|
11290
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
11291
|
+
hint = undefined;
|
|
11292
|
+
recovery.prevSignature = null;
|
|
11293
|
+
recovery.prevInventorySize = -1;
|
|
11294
|
+
continue;
|
|
11295
|
+
}
|
|
11296
|
+
catch (err) {
|
|
11297
|
+
args.steps.push(`Post-verify round ${round}: Render API-key anchor click failed (${err instanceof Error ? err.message : String(err)}) — continuing.`);
|
|
11298
|
+
}
|
|
11299
|
+
}
|
|
11300
|
+
}
|
|
11301
|
+
}
|
|
10774
11302
|
const emailAlias = args.verificationEmailAlias ??
|
|
10775
11303
|
args.credentials?.email ??
|
|
10776
11304
|
args.oauthAccountEmail;
|
|
@@ -11168,6 +11696,7 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
11168
11696
|
if (hint === undefined &&
|
|
11169
11697
|
args.machineToken !== undefined &&
|
|
11170
11698
|
args.machineToken.length > 0 &&
|
|
11699
|
+
args.allowOperatorInboxOtp === true &&
|
|
11171
11700
|
!otpPolledUrls.has(state.url) &&
|
|
11172
11701
|
detectEmailOtpGate(state.url, state.title, await this.browser.extractText().catch(() => ""), inventory)) {
|
|
11173
11702
|
otpPolledUrls.add(state.url);
|
|
@@ -11735,6 +12264,46 @@ Prefer items naming keys / tokens / API / developer / secrets; then credentials
|
|
|
11735
12264
|
recovery.prevSignature = null;
|
|
11736
12265
|
recovery.prevInventorySize = inventory.length;
|
|
11737
12266
|
}
|
|
12267
|
+
const actionCycleSignature = credentialActionSignature(nextStep, inventory);
|
|
12268
|
+
if (actionCycleSignature !== null) {
|
|
12269
|
+
recovery.recentCredentialActionSignatures.push(actionCycleSignature);
|
|
12270
|
+
if (recovery.recentCredentialActionSignatures.length > 8) {
|
|
12271
|
+
recovery.recentCredentialActionSignatures =
|
|
12272
|
+
recovery.recentCredentialActionSignatures.slice(-8);
|
|
12273
|
+
}
|
|
12274
|
+
if (isRepeatingCredentialActionCycle(recovery.recentCredentialActionSignatures, visiblePageText)) {
|
|
12275
|
+
args.steps.push(`Post-verify: detected repeating credential-action cycle (${recovery.recentCredentialActionSignatures.slice(-5).join(" → ")}) — trying extraction/fallback instead of repeating it.`);
|
|
12276
|
+
const harvested = await this.harvestVisibleCredentials().catch(() => ({}));
|
|
12277
|
+
for (const [k, v] of Object.entries(harvested)) {
|
|
12278
|
+
if (credentials[k] === undefined)
|
|
12279
|
+
credentials[k] = v;
|
|
12280
|
+
}
|
|
12281
|
+
if (hasAnyExtractedCredential(credentials)) {
|
|
12282
|
+
await this.writeFastPathSyntheticCapture(args.service, capturedRound, oauth, "repeating credential-action cycle synthetic extract — credentials were already visible");
|
|
12283
|
+
return credentials;
|
|
12284
|
+
}
|
|
12285
|
+
const fallback = pickStuckLoopFallbackUrl(state.url, recovery.triedFallbackUrls, args.service, this.resolvedSignupUrl);
|
|
12286
|
+
if (fallback !== null) {
|
|
12287
|
+
recovery.triedFallbackUrls.add(fallback);
|
|
12288
|
+
args.steps.push(`Post-verify: credential-action cycle fallback — navigating to ${fallback}`);
|
|
12289
|
+
try {
|
|
12290
|
+
await this.browser.goto(fallback);
|
|
12291
|
+
await this.browser.waitForInteractiveDom(5, 15_000).catch(() => undefined);
|
|
12292
|
+
}
|
|
12293
|
+
catch (err) {
|
|
12294
|
+
args.steps.push(`Post-verify: credential-action cycle fallback navigate failed (${err instanceof Error ? err.message : String(err)}) — continuing.`);
|
|
12295
|
+
}
|
|
12296
|
+
recovery.prevSignature = null;
|
|
12297
|
+
recovery.prevInventorySize = -1;
|
|
12298
|
+
hint = undefined;
|
|
12299
|
+
continue;
|
|
12300
|
+
}
|
|
12301
|
+
this.lastPostVerifyDoneReason =
|
|
12302
|
+
`[stuck_loop] planner repeated credential/payment actions (${recovery.recentCredentialActionSignatures.slice(-5).join(" → ")}) with no usable credential and no fallback URL remaining.`;
|
|
12303
|
+
args.steps.push(`Post-verify: credential-action cycle unresolvable — breaking out with planner_stuck.`);
|
|
12304
|
+
break;
|
|
12305
|
+
}
|
|
12306
|
+
}
|
|
11738
12307
|
// Record the kind of the step we're ABOUT to execute (all re-plan
|
|
11739
12308
|
// `continue` guards are behind us here) so next round can judge
|
|
11740
12309
|
// whether it changed the page — the stalled-wizard breaker above.
|
|
@@ -12829,6 +13398,11 @@ ${formatInventory(input.inventory)}${input.hint !== undefined ? `\n\nIMPORTANT
|
|
|
12829
13398
|
if (typeof curUrl === "string" && isDocumentationUrl(curUrl)) {
|
|
12830
13399
|
return credentials;
|
|
12831
13400
|
}
|
|
13401
|
+
const currentState = await this.browser.getState().catch(() => null);
|
|
13402
|
+
if (currentState !== null &&
|
|
13403
|
+
detectCredentialExtractionBlock(currentState.html, titleFromHtml(currentState.html)) !== null) {
|
|
13404
|
+
return credentials;
|
|
13405
|
+
}
|
|
12832
13406
|
for (const candidate of await this.browser.extractCredentialCandidates()) {
|
|
12833
13407
|
const hit = extractApiKeyFromText(candidate);
|
|
12834
13408
|
if (hit === null)
|
|
@@ -12931,6 +13505,11 @@ ${formatInventory(input.inventory)}${input.hint !== undefined ? `\n\nIMPORTANT
|
|
|
12931
13505
|
const curUrl = typeof this.browser.currentUrl === "function" ? this.browser.currentUrl() : "";
|
|
12932
13506
|
if (typeof curUrl === "string" && isDocumentationUrl(curUrl))
|
|
12933
13507
|
return {};
|
|
13508
|
+
const currentState = await this.browser.getState().catch(() => null);
|
|
13509
|
+
if (currentState !== null &&
|
|
13510
|
+
detectCredentialExtractionBlock(currentState.html, titleFromHtml(currentState.html)) !== null) {
|
|
13511
|
+
return {};
|
|
13512
|
+
}
|
|
12934
13513
|
let st = initialExtractionState();
|
|
12935
13514
|
const classify = (text) => {
|
|
12936
13515
|
const hit = extractApiKeyFromText(text);
|