@trusty-squire/mcp 1.0.0 → 1.0.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +9 -15
  2. package/dist/bot/credential-text.d.ts +4 -0
  3. package/dist/bot/credential-text.d.ts.map +1 -0
  4. package/dist/bot/credential-text.js +297 -0
  5. package/dist/bot/credential-text.js.map +1 -0
  6. package/dist/bot/email-verification.d.ts +2 -0
  7. package/dist/bot/email-verification.d.ts.map +1 -0
  8. package/dist/bot/email-verification.js +47 -0
  9. package/dist/bot/email-verification.js.map +1 -0
  10. package/dist/bot/google-login.d.ts +1 -0
  11. package/dist/bot/google-login.d.ts.map +1 -1
  12. package/dist/bot/google-login.js +47 -2
  13. package/dist/bot/google-login.js.map +1 -1
  14. package/dist/bot/index.d.ts +0 -40
  15. package/dist/bot/index.d.ts.map +1 -1
  16. package/dist/bot/index.js +8 -156
  17. package/dist/bot/index.js.map +1 -1
  18. package/dist/bot/login-state.d.ts +2 -0
  19. package/dist/bot/login-state.d.ts.map +1 -1
  20. package/dist/bot/login-state.js +44 -0
  21. package/dist/bot/login-state.js.map +1 -1
  22. package/dist/bot/oauth-detect.d.ts +8 -0
  23. package/dist/bot/oauth-detect.d.ts.map +1 -0
  24. package/dist/bot/oauth-detect.js +329 -0
  25. package/dist/bot/oauth-detect.js.map +1 -0
  26. package/dist/bot/onboarding-capture.d.ts +1 -1
  27. package/dist/bot/onboarding-capture.d.ts.map +1 -1
  28. package/dist/bot/operator-recipe.d.ts +24 -24
  29. package/dist/bot/promote-to-skill.d.ts +1 -1
  30. package/dist/bot/promote-to-skill.d.ts.map +1 -1
  31. package/dist/bot/promote-to-skill.js +7 -5
  32. package/dist/bot/promote-to-skill.js.map +1 -1
  33. package/dist/bot/provision-session.d.ts +10 -0
  34. package/dist/bot/provision-session.d.ts.map +1 -1
  35. package/dist/bot/provision-session.js +126 -5
  36. package/dist/bot/provision-session.js.map +1 -1
  37. package/dist/bot/provision-types.d.ts +69 -0
  38. package/dist/bot/provision-types.d.ts.map +1 -0
  39. package/dist/bot/provision-types.js +9 -0
  40. package/dist/bot/provision-types.js.map +1 -0
  41. package/dist/bot/semantic-transition.d.ts +1 -1
  42. package/dist/bot/semantic-transition.d.ts.map +1 -1
  43. package/dist/skill-cli/cli.d.ts +1 -7
  44. package/dist/skill-cli/cli.d.ts.map +1 -1
  45. package/dist/skill-cli/cli.js +1 -69
  46. package/dist/skill-cli/cli.js.map +1 -1
  47. package/dist/tools/provision-drive.d.ts +48 -8
  48. package/dist/tools/provision-drive.d.ts.map +1 -1
  49. package/dist/tools/provision-drive.js +127 -2
  50. package/dist/tools/provision-drive.js.map +1 -1
  51. package/dist/tools/store-credential.d.ts +4 -4
  52. package/dist/tools/use-credential.d.ts +6 -6
  53. package/package.json +2 -6
  54. package/dist/bot/affordance-probe.d.ts +0 -15
  55. package/dist/bot/affordance-probe.d.ts.map +0 -1
  56. package/dist/bot/affordance-probe.js +0 -61
  57. package/dist/bot/affordance-probe.js.map +0 -1
  58. package/dist/bot/agent.d.ts +0 -449
  59. package/dist/bot/agent.d.ts.map +0 -1
  60. package/dist/bot/agent.js +0 -13634
  61. package/dist/bot/agent.js.map +0 -1
  62. package/dist/bot/debug.d.ts +0 -3
  63. package/dist/bot/debug.d.ts.map +0 -1
  64. package/dist/bot/debug.js +0 -35
  65. package/dist/bot/debug.js.map +0 -1
  66. package/dist/bot/email-code-fetcher.d.ts +0 -5
  67. package/dist/bot/email-code-fetcher.d.ts.map +0 -1
  68. package/dist/bot/email-code-fetcher.js +0 -33
  69. package/dist/bot/email-code-fetcher.js.map +0 -1
  70. package/dist/bot/flow-engine.d.ts +0 -7
  71. package/dist/bot/flow-engine.d.ts.map +0 -1
  72. package/dist/bot/flow-engine.js +0 -13
  73. package/dist/bot/flow-engine.js.map +0 -1
  74. package/dist/bot/form-fill.d.ts +0 -172
  75. package/dist/bot/form-fill.d.ts.map +0 -1
  76. package/dist/bot/form-fill.js +0 -444
  77. package/dist/bot/form-fill.js.map +0 -1
  78. package/dist/bot/inbox-client.d.ts +0 -45
  79. package/dist/bot/inbox-client.d.ts.map +0 -1
  80. package/dist/bot/inbox-client.js +0 -220
  81. package/dist/bot/inbox-client.js.map +0 -1
  82. package/dist/bot/llm-client.d.ts +0 -89
  83. package/dist/bot/llm-client.d.ts.map +0 -1
  84. package/dist/bot/llm-client.js +0 -493
  85. package/dist/bot/llm-client.js.map +0 -1
  86. package/dist/bot/nav-search.d.ts +0 -114
  87. package/dist/bot/nav-search.d.ts.map +0 -1
  88. package/dist/bot/nav-search.js +0 -670
  89. package/dist/bot/nav-search.js.map +0 -1
  90. package/dist/bot/notify-api.d.ts +0 -9
  91. package/dist/bot/notify-api.d.ts.map +0 -1
  92. package/dist/bot/notify-api.js +0 -49
  93. package/dist/bot/notify-api.js.map +0 -1
  94. package/dist/bot/oauth-flow.d.ts +0 -48
  95. package/dist/bot/oauth-flow.d.ts.map +0 -1
  96. package/dist/bot/oauth-flow.js +0 -111
  97. package/dist/bot/oauth-flow.js.map +0 -1
  98. package/dist/bot/observation-frame.d.ts +0 -16
  99. package/dist/bot/observation-frame.d.ts.map +0 -1
  100. package/dist/bot/observation-frame.js +0 -22
  101. package/dist/bot/observation-frame.js.map +0 -1
  102. package/dist/bot/post-signup-action-executor.d.ts +0 -53
  103. package/dist/bot/post-signup-action-executor.d.ts.map +0 -1
  104. package/dist/bot/post-signup-action-executor.js +0 -104
  105. package/dist/bot/post-signup-action-executor.js.map +0 -1
  106. package/dist/bot/post-signup-flow.d.ts +0 -51
  107. package/dist/bot/post-signup-flow.d.ts.map +0 -1
  108. package/dist/bot/post-signup-flow.js +0 -155
  109. package/dist/bot/post-signup-flow.js.map +0 -1
  110. package/dist/bot/post-signup-login-flow.d.ts +0 -28
  111. package/dist/bot/post-signup-login-flow.d.ts.map +0 -1
  112. package/dist/bot/post-signup-login-flow.js +0 -35
  113. package/dist/bot/post-signup-login-flow.js.map +0 -1
  114. package/dist/bot/post-signup-recovery-state.d.ts +0 -150
  115. package/dist/bot/post-signup-recovery-state.d.ts.map +0 -1
  116. package/dist/bot/post-signup-recovery-state.js +0 -253
  117. package/dist/bot/post-signup-recovery-state.js.map +0 -1
  118. package/dist/bot/post-signup-synthetic-capture.d.ts +0 -43
  119. package/dist/bot/post-signup-synthetic-capture.d.ts.map +0 -1
  120. package/dist/bot/post-signup-synthetic-capture.js +0 -84
  121. package/dist/bot/post-signup-synthetic-capture.js.map +0 -1
  122. package/dist/bot/prewarm-cache.d.ts +0 -3
  123. package/dist/bot/prewarm-cache.d.ts.map +0 -1
  124. package/dist/bot/prewarm-cache.js +0 -91
  125. package/dist/bot/prewarm-cache.js.map +0 -1
  126. package/dist/bot/read-otp.d.ts +0 -21
  127. package/dist/bot/read-otp.d.ts.map +0 -1
  128. package/dist/bot/read-otp.js +0 -170
  129. package/dist/bot/read-otp.js.map +0 -1
  130. package/dist/bot/redact.d.ts +0 -3
  131. package/dist/bot/redact.d.ts.map +0 -1
  132. package/dist/bot/redact.js +0 -132
  133. package/dist/bot/redact.js.map +0 -1
  134. package/dist/bot/rejection-record.d.ts +0 -70
  135. package/dist/bot/rejection-record.d.ts.map +0 -1
  136. package/dist/bot/rejection-record.js +0 -76
  137. package/dist/bot/rejection-record.js.map +0 -1
  138. package/dist/bot/replay-skill.d.ts +0 -180
  139. package/dist/bot/replay-skill.d.ts.map +0 -1
  140. package/dist/bot/replay-skill.js +0 -4361
  141. package/dist/bot/replay-skill.js.map +0 -1
  142. package/dist/bot/signup-lock.d.ts +0 -17
  143. package/dist/bot/signup-lock.d.ts.map +0 -1
  144. package/dist/bot/signup-lock.js +0 -174
  145. package/dist/bot/signup-lock.js.map +0 -1
  146. package/dist/bot/signup-oauth-flow.d.ts +0 -16
  147. package/dist/bot/signup-oauth-flow.d.ts.map +0 -1
  148. package/dist/bot/signup-oauth-flow.js +0 -31
  149. package/dist/bot/signup-oauth-flow.js.map +0 -1
  150. package/dist/bot/state-classifier.d.ts +0 -18
  151. package/dist/bot/state-classifier.d.ts.map +0 -1
  152. package/dist/bot/state-classifier.js +0 -127
  153. package/dist/bot/state-classifier.js.map +0 -1
  154. package/dist/bot/terminal-gate.d.ts +0 -22
  155. package/dist/bot/terminal-gate.d.ts.map +0 -1
  156. package/dist/bot/terminal-gate.js +0 -184
  157. package/dist/bot/terminal-gate.js.map +0 -1
  158. package/dist/bot/unwinnable-services.d.ts +0 -7
  159. package/dist/bot/unwinnable-services.d.ts.map +0 -1
  160. package/dist/bot/unwinnable-services.js +0 -69
  161. package/dist/bot/unwinnable-services.js.map +0 -1
  162. package/dist/data/service-categories.d.ts +0 -11
  163. package/dist/data/service-categories.d.ts.map +0 -1
  164. package/dist/data/service-categories.js +0 -149
  165. package/dist/data/service-categories.js.map +0 -1
  166. package/dist/provision-run.d.ts +0 -45
  167. package/dist/provision-run.d.ts.map +0 -1
  168. package/dist/provision-run.js +0 -99
  169. package/dist/provision-run.js.map +0 -1
  170. package/dist/tools/provision-any.d.ts +0 -207
  171. package/dist/tools/provision-any.d.ts.map +0 -1
  172. package/dist/tools/provision-any.js +0 -1519
  173. package/dist/tools/provision-any.js.map +0 -1
  174. package/dist/tools/signup-telemetry.d.ts +0 -38
  175. package/dist/tools/signup-telemetry.d.ts.map +0 -1
  176. package/dist/tools/signup-telemetry.js +0 -103
  177. package/dist/tools/signup-telemetry.js.map +0 -1
package/README.md CHANGED
@@ -60,14 +60,12 @@ The agent drives the browser step by step; the key lands in your vault, never in
60
60
 
61
61
  ## Tools
62
62
 
63
- | Tool | What it does |
64
- |---|---|
65
- | `operate_start` / `observe` / `act` | Open a scoped browser session, read the page, take one action at a time. |
66
- | `operate_extract` | Reveal and capture credentials from the current page. |
67
- | `operate_remember` / `operate_use` | Save a successful run as a skill; replay it by name in ~30s. |
68
- | `operate_finish_task` / `operate_finish` | Close with an outcome (store credentials) or just release the browser. |
69
- | `grant_app_access` | Mint a scoped, revocable key for a deployed app (egress grant). |
70
- | `list_credentials` / `use_credential` | Browse the vault; call an API through it without exposing the raw secret. |
63
+ - `operate_start` / `operate_observe` / `operate_act` — open a scoped browser session, read the page, act one step at a time.
64
+ - `operate_extract` — reveal and capture credentials from the current page.
65
+ - `operate_remember` / `operate_use` save a successful run as a skill; replay it by name in ~30s.
66
+ - `operate_finish_task` / `operate_finish` finish with an outcome (store credentials) or just release the browser.
67
+ - `grant_app_access` mint a scoped, revocable key for a deployed app (egress grant).
68
+ - `list_credentials` / `use_credential` browse the vault; call an API through it without exposing the raw secret.
71
69
 
72
70
  ## How it works
73
71
 
@@ -86,13 +84,9 @@ A thin local MCP server that drives a real browser on your machine:
86
84
 
87
85
  ## Configuration
88
86
 
89
- | Env var | Default | Purpose |
90
- |---|---|---|
91
- | `TRUSTY_SQUIRE_API_BASE` | `https://trusty-squire-api.fly.dev` | API gateway URL |
92
- | `UNIVERSAL_BOT_PREFER_CHEAP` | `true` | Cheap-tier vision planning; premium fallback only on parse failure |
93
- | `UNIVERSAL_BOT_MAX_LLM_CALLS` | `15` | Per-run circuit breaker |
94
- | `UNIVERSAL_BOT_PROXY_URL` | — | Residential proxy (`socks5://…`); datacenter egress only unless `UNIVERSAL_BOT_PROXY_ALWAYS=true` |
95
- | `UNIVERSAL_BOT_HEADLESS` | `false` | Force Chromium-headless (only when you want no display surface) |
87
+ No setup needed for normal use `connect` wires everything up. Advanced bot
88
+ knobs (residential proxy, LLM tier, per-run call cap, API gateway override) are
89
+ documented in [the repo](https://github.com/Trusty-Squire/trusty-squire).
96
90
 
97
91
  ## Security & privacy
98
92
 
@@ -0,0 +1,4 @@
1
+ export declare function isTruncatedCapture(sourceText: string, capturedKey: string): boolean;
2
+ export declare function extractApiKeyFromText(text: string): string | null;
3
+ export declare function isCredentialNoiseCandidate(candidate: string): boolean;
4
+ //# sourceMappingURL=credential-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-text.d.ts","sourceRoot":"","sources":["../../src/bot/credential-text.ts"],"names":[],"mappings":"AAwDA,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAYnF;AAeD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkJjE;AAkED,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAKrE"}
@@ -0,0 +1,297 @@
1
+ // credential-text.ts — pull a credential VALUE out of a blob of visible page
2
+ // text, and reject candidates that are truncated displays or password-manager
3
+ // UI noise. Carved out of agent.ts (the retired universal-bot monolith); these
4
+ // are the browser-free, regex-driven extraction predicates the provision
5
+ // session + skill replay paths share.
6
+ //
7
+ // Distinct from credential-shape.ts: that module answers "is this string
8
+ // shaped like a credential" (a tight host-side gate); this one answers "given a
9
+ // page of text, WHICH substring is the credential" (service-prefix + labeled
10
+ // regex library) plus the two candidate-rejection helpers replay uses.
11
+ // Real API keys / bearer tokens are short (Stripe ~32, JWT ~hundreds
12
+ // but our labeled patterns don't target JWTs). Captcha challenge
13
+ // tokens are very long: g-recaptcha-response runs ~500-2000 chars and
14
+ // cf-turnstile-response is similar. A 100-char ceiling cleanly admits
15
+ // real keys and rejects every captcha token shape we've seen.
16
+ const MAX_CREDENTIAL_LENGTH = 100;
17
+ // Substrings that, if present in a candidate, mark it as a
18
+ // challenge/cookie token rather than a credential. Cloudflare clearance
19
+ // cookies (`__cf`, `cf_clearance`), CDN challenge paths (`cdn-cgi`),
20
+ // and the visible field/param names of the two captcha widgets.
21
+ const CAPTCHA_TOKEN_MARKERS = [
22
+ "__cf",
23
+ "cf_clearance",
24
+ "cdn-cgi",
25
+ "cf-turnstile-response",
26
+ "g-recaptcha-response",
27
+ "h-captcha-response",
28
+ ];
29
+ // Distinctive service key prefixes. If a *labeled* match's value
30
+ // embeds one of these NOT at its start, the regex straddled glued UI
31
+ // text on a dense dashboard (e.g. Render's API-keys list rendered as
32
+ // "...Name bot-key Menu Key rnd_xxxx" with no separators) — the real
33
+ // key starts at the prefix, so the labeled match is contaminated and
34
+ // must be rejected. A clean labeled key either starts with its prefix
35
+ // (then the prefixed patterns above already caught it) or carries no
36
+ // known prefix at all.
37
+ const EMBEDDED_KEY_PREFIXES = [
38
+ "rnd_",
39
+ "phc_",
40
+ "sk_live_",
41
+ "sk_test_",
42
+ "pk_live_",
43
+ "pk_test_",
44
+ ];
45
+ // True when `capturedKey` is followed by a truncation marker (`...`
46
+ // or the Unicode ellipsis `…`) in `sourceText`. That marker is the
47
+ // signal that the visible display masked the full secret — the
48
+ // regex captured everything up to but not including the marker, so
49
+ // the value LOOKS valid but is short. Used by F10's
50
+ // extract-via-Copy-button recovery path; without this check, the
51
+ // bot accepts the truncated value, stores it, and the user discovers
52
+ // the failure only when their next API call returns 401.
53
+ export function isTruncatedCapture(sourceText, capturedKey) {
54
+ const idx = sourceText.indexOf(capturedKey);
55
+ if (idx < 0)
56
+ return false;
57
+ const after = sourceText.slice(idx + capturedKey.length, idx + capturedKey.length + 10);
58
+ // Whitespace OK between key and ellipsis (some modals render as
59
+ // "sk-or-v1-xxxx ..."). Three OR MORE dots; two dots are ordinary
60
+ // punctuation and would false-positive on e.g. "key value.." in
61
+ // help text.
62
+ return /^\s*(?:\.{3,}|…)/.test(after);
63
+ }
64
+ // Pull an API key out of the *visible* page text.
65
+ //
66
+ // Two strategies, in priority order:
67
+ // 1. Known service-specific prefixes (re_, sk_live_, …) — high
68
+ // confidence, the prefix itself is the proof.
69
+ // 2. Labeled patterns ("api key: <value>") — lower confidence, so
70
+ // they carry guard rails: the value must sit IMMEDIATELY after
71
+ // the label (a small bounded gap of spaces/colon/equals, NOT
72
+ // arbitrary whitespace that could span unrelated page sections),
73
+ // must be under MAX_CREDENTIAL_LENGTH, and must not look like a
74
+ // captcha/cookie token. Without these, a `g-recaptcha-response`
75
+ // value or a session token elsewhere in the body could be
76
+ // mistaken for `credentials.api_key`.
77
+ export function extractApiKeyFromText(text) {
78
+ const prefixed = [
79
+ /\bre_[a-zA-Z0-9_]{20,}\b/, // Resend (key body contains underscores)
80
+ /\bsk_(?:live|test)_[a-zA-Z0-9]{20,}\b/, // Stripe secret
81
+ // NOTE: client-embedded PUBLIC keys are deliberately NOT matched —
82
+ // Stripe publishable (pk_live_/pk_test_) and PostHog project
83
+ // (phc_) keys ship in the client-side JS of every site that uses
84
+ // those vendors, so finding one on a page means "this service
85
+ // embeds Stripe/PostHog", not "here is the user's credential".
86
+ // Each produced a false success on Mistral (its billing pk_live_,
87
+ // then its analytics phc_, surfaced as the api_key).
88
+ // Plunk is the EXCEPTION: its API key is `pk_<hex>` (e.g.
89
+ // pk_e063df9b5…) and IS the user's credential, shown in plaintext on
90
+ // the dashboard under an "API Key" label. The pure-hex body after the
91
+ // prefix distinguishes it from Stripe publishable keys (pk_live_/
92
+ // pk_test_ — a "live"/"test" segment, not hex), so this can't re-open
93
+ // the embedded-public-key false-positive the note above warns about.
94
+ /\bpk_[a-f0-9]{24,}\b/, // Plunk (hex public/API key — NOT Stripe pk_live_/pk_test_)
95
+ /\bkey-[a-f0-9]{32}\b/, // Mailgun
96
+ /\bSG\.[a-zA-Z0-9_\-]{20,}\.[a-zA-Z0-9_\-]{20,}\b/, // SendGrid
97
+ /\brnd_[a-zA-Z0-9]{20,}\b/, // Render
98
+ /\bsntry[su]_[A-Za-z0-9_=\-]{20,}/, // Sentry org/user auth token
99
+ // Neon serverless Postgres. Modal renders `napi_<48-char-alnum>` and
100
+ // also shows a truncated `napi_xxx…` in the visible text below the
101
+ // input field. Without the prefix here, the bot saw the truncated
102
+ // display, isTruncatedCapture rejected the partial value, every
103
+ // pass returned null, and the planner gave up despite the full key
104
+ // being in the input field's `value` attribute. rc.14 — surfaced
105
+ // during the harvester rc.13 pass on Neon.
106
+ /\bnapi_[a-zA-Z0-9]{30,80}\b/, // Neon
107
+ // 0.8.3-rc.1 — typeform personal access tokens. Shape
108
+ // `tfp_<alnum-with-underscore>` length 40-80. Surfaced during the
109
+ // 2026-05-29 retest where the planner SAW the token (quoted in
110
+ // reason) but no regex matched, so extractCredentials returned
111
+ // null and the bot bailed `oauth_onboarding_failed`.
112
+ /\btfp_[A-Za-z0-9_]{40,80}\b/, // Typeform
113
+ // Replicate API tokens. `r8_<40-char alnum>` per their docs. Shown
114
+ // in the table row after Create. The post-verify loop iterates,
115
+ // adds rows, but extractCredentials returned null every round
116
+ // until rc.20 because no regex matched. Added defensively after
117
+ // the rc.13 verification pass showed Replicate burning the full
118
+ // 12-round budget filling-creating tokens nobody could extract.
119
+ /\br8_[a-zA-Z0-9]{30,60}\b/, // Replicate
120
+ // rc.23 — added after the post-rc.22 registry-snapshot review of
121
+ // 200 failed signups. Each pattern matches a token shape the
122
+ // bot's planner had already QUOTED in its `reason` field (i.e.
123
+ // the credential was visible on the page, just not in a shape
124
+ // any prior regex recognised). The redact.{ts,mjs} pattern set
125
+ // stays in lockstep with these.
126
+ /\bpscale_tkn_[A-Za-z0-9]{30,60}\b/, // PlanetScale Service Token
127
+ /\bsbp_[a-zA-Z0-9]{30,80}\b/, // Supabase Personal Access Token
128
+ // Baseten: `<6-12 alnum>.<30+ alnum>`. The dot separator + length
129
+ // bounds on both sides distinguish it from version strings (too
130
+ // short on either side). rc.35 — relaxed the prefix to mixed-case
131
+ // after the rc.33 broad sweep showed a Baseten key whose prefix
132
+ // had uppercase letters: `HP9tFTtm.txDl4vv7ayYsTwx9dQea47ylRdN4Brk3`.
133
+ /\b[A-Za-z0-9]{6,12}\.[A-Za-z0-9]{30,50}\b/, // Baseten
134
+ // Qdrant Cloud: `<UUID>|<55-char opaque>` — a literal pipe between
135
+ // a key id and the secret body. Unique enough that no false-
136
+ // positive guard is needed.
137
+ //
138
+ // rc.34/rc.36 — extended the secret-body character class to
139
+ // include underscore + hyphen. rc.35 broad sweep surfaced
140
+ // another Qdrant shape with mid-body hyphens:
141
+ // `<UUID>|e8L7oyi-5fHa327u7x-IQN6WivtPlpIVjT-giIsrXDZW7P-8i2G9Pw`.
142
+ // [A-Za-z0-9_-] covers both observed shapes; the {30,80} length
143
+ // bound + UUID prefix keep false-positive risk near zero.
144
+ /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\|[A-Za-z0-9_\-]{30,80}\b/i, // Qdrant
145
+ // JWT (eyJ...eyJ...sig) — Convex's API "token" is a JWT. Other
146
+ // services may emit JWTs as bearer secrets too. Three-segment
147
+ // base64url with literal dots. Conservative bounds — under 20
148
+ // chars per segment is almost never a real JWT.
149
+ /\beyJ[A-Za-z0-9_\-]{20,}\.eyJ[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\b/, // JWT
150
+ // Zeabur's API key — `sk-<28-40 lowercase alnum>`. Shorter than
151
+ // OpenAI legacy (which is 40+ mixed-case). The lowercase-only
152
+ // character class differentiates from OpenAI legacy so this
153
+ // pattern only fires on Zeabur-style keys. Surfaced from the
154
+ // rc.23 snapshot review.
155
+ /\bsk-[a-z0-9]{28,38}\b/, // Zeabur
156
+ // PostHog PERSONAL API key — `phx_<43+ alnum>`. MEASURED 2026-06-24: the
157
+ // post-OAuth create-personal-api-key flow mints a `phx_`-prefixed secret —
158
+ // the planner SAW it (and even OCR-transcribed it with errors), but no regex
159
+ // matched so the DOM extractor stored nothing and the run bailed
160
+ // oauth_onboarding_failed while a real key sat on the page. The {40,60}
161
+ // bound keeps it clear of short prefixed noise. Deliberately NOT matching
162
+ // the `phc_` PROJECT key —
163
+ // that's a PUBLIC analytics key embedded in client JS on every PostHog site
164
+ // (see extract-credentials.test "ignores a PostHog project key").
165
+ /\bphx_[A-Za-z0-9]{40,60}\b/, // PostHog personal API key
166
+ // OpenRouter, Anthropic, OpenAI — these are the dominant
167
+ // OAuth-completed-then-copy-needed services. Specific-prefix
168
+ // patterns first so a labeled-pattern fallback isn't load-
169
+ // bearing for them. Putting `sk-or-v1-` before `sk-` so it wins
170
+ // when both could match (cosmetic; both capture the same value).
171
+ // 0.6.15-rc.8 — character-class anchored so the greedy match
172
+ // doesn't glue trailing modal text. extractText() returns body
173
+ // as one concatenated string, so a key followed by "Please copy
174
+ // this" with no separator looked like `sk-or-v1-<64hex>Please…`.
175
+ // The hex-only class terminates at the first non-hex char.
176
+ /\bsk-or-v1-[a-f0-9]{40,80}/i, // OpenRouter (sk-or-v1-<64hex>)
177
+ /\bsk-ant-[a-zA-Z0-9_-]{40,120}/, // Anthropic (sk-ant-<urlsafe-b64>)
178
+ /\bsk-proj-[a-zA-Z0-9_-]{40,200}/, // OpenAI project key
179
+ /\bsk-[a-zA-Z0-9]{40,60}/, // OpenAI legacy (`sk-` + ~48 chars, no dashes)
180
+ ];
181
+ for (const pattern of prefixed) {
182
+ const match = text.match(pattern);
183
+ if (match !== null)
184
+ return match[0];
185
+ }
186
+ // Labeled patterns. The label and value MUST be separated by a real
187
+ // separator — a colon/equals, or whitespace — `(?:[ \t]*[:=][ \t]*|[ \t]+)`,
188
+ // never a newline. A MANDATORY separator is what keeps the regex from
189
+ // latching the label onto glued dashboard nav text: a sidebar
190
+ // rendering "API Keys" "Webhooks" "Settings" as adjacent links
191
+ // concatenates in textContent to "API KeysWebhooksSettings…", and an
192
+ // optional-gap regex would capture "sWebhooksSettings…" as the key
193
+ // (Resend false-positive). Requiring `:`/`=`/space means "API Key"
194
+ // followed immediately by a letter does not match.
195
+ const labeled = [
196
+ /(?:api[_\s-]?key|access[_\s-]?token|secret[_\s-]?key)(?:[ \t]*[:=][ \t]*|[ \t]+)([a-zA-Z0-9_\-]{20,})/i,
197
+ /\b[Bb]earer[ \t]+([a-zA-Z0-9_\-.]{30,})/,
198
+ // UUID-style tokens (Railway uses bare UUIDs for API tokens; some
199
+ // smaller services do too). MUST be labeled — bare UUIDs appear all
200
+ // over dashboards as trace IDs, project IDs, and request IDs, so an
201
+ // unlabeled UUID regex would false-positive on the first error page
202
+ // the bot lands on. The label set is broader than the prefix-style
203
+ // labeled regex above because Railway specifically renders "Token"
204
+ // (not "API key") next to the value.
205
+ /(?:api[_\s-]?token|api[_\s-]?key|access[_\s-]?token|new[_\s-]?token|\btoken|\bsecret)(?:[ \t\n]*[:=][ \t\n]*|[ \t\n]+)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/i,
206
+ ];
207
+ for (const pattern of labeled) {
208
+ const match = text.match(pattern);
209
+ const candidate = match?.[1];
210
+ if (candidate === undefined)
211
+ continue;
212
+ // A captcha challenge token: too long for a real key, and/or
213
+ // carries a known cookie/widget marker.
214
+ if (candidate.length > MAX_CREDENTIAL_LENGTH)
215
+ continue;
216
+ const lower = candidate.toLowerCase();
217
+ if (CAPTCHA_TOKEN_MARKERS.some((marker) => lower.includes(marker)))
218
+ continue;
219
+ // Contaminated: the labeled match straddled glued dashboard text
220
+ // onto a real key (the key prefix sits mid-candidate, not at 0).
221
+ if (EMBEDDED_KEY_PREFIXES.some((p) => lower.indexOf(p) > 0))
222
+ continue;
223
+ return candidate;
224
+ }
225
+ return null;
226
+ }
227
+ // Password-manager / autofill UI affordances that render as short
228
+ // word-tokens on credential pages. A render API-keys page ships a
229
+ // "Save to 1Password" / "1Password" autofill button next to the real
230
+ // `rnd_…` key; LastPass, Bitwarden, and Dashlane do the same. These
231
+ // strings are alphanumeric, often carry a digit ("1Password"), and sit
232
+ // EARLIER in DOM order than the credential — so the validator-blind
233
+ // candidate-scan tiers (replay-skill.ts) used to return them as the
234
+ // "credential" and the downstream length validator then rejected them
235
+ // (the 0DTW2V66 render skill: `got="1Password" length 9 below min 32`).
236
+ // They are never credentials; reject them at the candidate layer so the
237
+ // scan moves on to the real key instead of the right key being shadowed
238
+ // by a UI word. Matched case-insensitively as a whole token (the
239
+ // candidates the scan tiers feed in are already whitespace-trimmed
240
+ // single tokens).
241
+ const CREDENTIAL_NOISE_TOKENS = [
242
+ "1password",
243
+ "lastpass",
244
+ "bitwarden",
245
+ "dashlane",
246
+ "keepass",
247
+ "keeper",
248
+ "nordpass",
249
+ "proton pass",
250
+ "protonpass",
251
+ "autofill",
252
+ "passwords",
253
+ // Cookie-consent widget vocabulary (CookieScript/OneTrust-class banners
254
+ // render these as checkbox values and category labels on EVERY page,
255
+ // earlier in DOM order than any credential — zilliz's banner fed
256
+ // "personalization" to the validator-shaped scan tier as the "key").
257
+ // Whole-token equality with a generic English word is never a real
258
+ // credential, so rejecting these costs nothing.
259
+ "necessary",
260
+ "analytics",
261
+ "personalization",
262
+ "personalisation",
263
+ "advertising",
264
+ "advertisement",
265
+ "marketing",
266
+ "functional",
267
+ "preferences",
268
+ "statistics",
269
+ "performance",
270
+ "targeting",
271
+ "unclassified",
272
+ "security",
273
+ ];
274
+ // Verb-prefixed UI affordances ("Save to 1Password", "Copy to
275
+ // clipboard", "Add to vault"). The candidate-scan tiers tokenize on
276
+ // whitespace so a multi-word affordance rarely survives as one
277
+ // candidate — but extractText()/innerText passes glue it together, so
278
+ // guard the leading verbs too.
279
+ const CREDENTIAL_NOISE_PREFIXES = [
280
+ "save to ",
281
+ "copy to ",
282
+ "add to ",
283
+ "store in ",
284
+ ];
285
+ // True when a candidate string is a password-manager / autofill UI
286
+ // affordance rather than a real credential value. Used by the replay
287
+ // engine's raw-candidate scan tiers to keep "1Password"-class words
288
+ // out of the credential slot.
289
+ export function isCredentialNoiseCandidate(candidate) {
290
+ const lower = candidate.trim().toLowerCase();
291
+ if (lower.length === 0)
292
+ return false;
293
+ if (CREDENTIAL_NOISE_TOKENS.includes(lower))
294
+ return true;
295
+ return CREDENTIAL_NOISE_PREFIXES.some((p) => lower.startsWith(p));
296
+ }
297
+ //# sourceMappingURL=credential-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-text.js","sourceRoot":"","sources":["../../src/bot/credential-text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function pickVerificationLink(links: readonly string[]): string | null;
2
+ //# sourceMappingURL=email-verification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-verification.d.ts","sourceRoot":"","sources":["../../src/bot/email-verification.ts"],"names":[],"mappings":"AAYA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAgB5E"}
@@ -0,0 +1,47 @@
1
+ // email-verification.ts — choose which link in a verification email to click.
2
+ // Carved out of agent.ts (the retired universal-bot monolith); the provision
3
+ // session uses this to follow the confirm/activate link without navigating to
4
+ // an unsubscribe or asset URL.
5
+ // Choose which link in a verification email to click. Scores each URL
6
+ // by keyword and picks the best — but only if it scored positive.
7
+ //
8
+ // The all-negative case is the bug this guards: an email whose only
9
+ // links are unsubscribe/preferences scores <= 0 everywhere, and an
10
+ // earlier version returned links[0] anyway, navigating the bot straight
11
+ // to an unsubscribe URL.
12
+ export function pickVerificationLink(links) {
13
+ const scored = links.map((url) => {
14
+ const lower = url.toLowerCase();
15
+ let score = 0;
16
+ if (isEmailAssetLink(lower))
17
+ score -= 50;
18
+ if (lower.includes("verify") || lower.includes("confirm"))
19
+ score += 10;
20
+ if (lower.includes("activate"))
21
+ score += 8;
22
+ if (lower.includes("welcome"))
23
+ score += 3;
24
+ if (lower.includes("unsubscribe") || lower.includes("preferences"))
25
+ score -= 10;
26
+ return { url, score };
27
+ });
28
+ scored.sort((a, b) => b.score - a.score);
29
+ const top = scored[0];
30
+ return top !== undefined && top.score > 0
31
+ ? top.url.replace(/&amp;/gi, "&")
32
+ : null;
33
+ }
34
+ function isEmailAssetLink(rawUrl) {
35
+ try {
36
+ const u = new URL(rawUrl.replace(/&amp;/g, "&"));
37
+ const host = u.hostname.toLowerCase();
38
+ const path = u.pathname.toLowerCase();
39
+ return (/\.(?:png|jpe?g|gif|webp|svg|ico|css|woff2?|ttf|otf)(?:$|[?#])/.test(path) ||
40
+ /^(?:static|cdn|assets|images|img|media)\./.test(host) ||
41
+ /\/(?:static|assets|images|img|media)\//.test(path));
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ //# sourceMappingURL=email-verification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-verification.js","sourceRoot":"","sources":["../../src/bot/email-verification.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,6EAA6E;AAC7E,8EAA8E;AAC9E,+BAA+B;AAE/B,sEAAsE;AACtE,kEAAkE;AAClE,EAAE;AACF,oEAAoE;AACpE,mEAAmE;AACnE,wEAAwE;AACxE,yBAAyB;AACzB,MAAM,UAAU,oBAAoB,CAAC,KAAwB;IAC3D,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QAC/B,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QAChC,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,gBAAgB,CAAC,KAAK,CAAC;YAAE,KAAK,IAAI,EAAE,CAAC;QACzC,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE,KAAK,IAAI,EAAE,CAAC;QACvE,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC;YAAE,KAAK,IAAI,CAAC,CAAC;QAC3C,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE,KAAK,IAAI,CAAC,CAAC;QAC1C,IAAI,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC;YAAE,KAAK,IAAI,EAAE,CAAC;QAChF,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACtB,OAAO,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC;QACvC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;QACjC,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAc;IACtC,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;QACjD,MAAM,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;QACtC,OAAO,CACL,+DAA+D,CAAC,IAAI,CAAC,IAAI,CAAC;YAC1E,2CAA2C,CAAC,IAAI,CAAC,IAAI,CAAC;YACtD,wCAAwC,CAAC,IAAI,CAAC,IAAI,CAAC,CACpD,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -34,6 +34,7 @@ export declare function ensureOAuthSession(opts?: {
34
34
  apiBaseUrl?: string;
35
35
  forceOpen?: boolean;
36
36
  }): Promise<LoginResult>;
37
+ export declare function extractGoogleAccountEmail(pageText: string): string | null;
37
38
  export declare function openInstallConfirmInBotChrome(opts: {
38
39
  confirmUrl: string;
39
40
  pollUntilClaimed: (context: BrowserContext) => Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"google-login.d.ts","sourceRoot":"","sources":["../../src/bot/google-login.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAsG5D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,GAAG,eAAe,GAAG,SAAS,GAAG,OAAO,CAAC;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAmDD,wBAAsB,4BAA4B,CAChD,UAAU,GAAE,MAA2B,GACtC,OAAO,CAAC,eAAe,EAAE,CAAC,CAyB5B;AAUD,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,aAAa,GACb,WAAW,GACX,YAAY,CAAC;AAEjB,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,eAAe,CAwCtF;AA+BD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CA2BlE;AAMD,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAEjE;AAaD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAiB/D;AAUD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBpE;AAGD,wBAAgB,UAAU,IAAI,OAAO,CAsBpC;AAWD,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAe9C;AAaD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGjD;AAiJD,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IAGjB,aAAa,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAG7D,WAAW,EAAE,MAAM,CAAC;IAIpB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAO1D,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAKvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,cAAc,CAClC,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC;IAAE,MAAM,EAAE,WAAW,GAAG,qBAAqB,GAAG,SAAS,CAAA;CAAE,CAAC,CAsBtE;AAuRD,wBAAsB,kBAAkB,CAAC,IAAI,CAAC,EAAE;IAC9C,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IAQpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,OAAO,CAAC,WAAW,CAAC,CA8DvB;AASD,wBAAsB,6BAA6B,CAAC,IAAI,EAAE;IACxD,UAAU,EAAE,MAAM,CAAC;IAOnB,gBAAgB,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IAIxB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAoDxE"}
1
+ {"version":3,"file":"google-login.d.ts","sourceRoot":"","sources":["../../src/bot/google-login.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAsG5D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,GAAG,eAAe,GAAG,SAAS,GAAG,OAAO,CAAC;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAmDD,wBAAsB,4BAA4B,CAChD,UAAU,GAAE,MAA2B,GACtC,OAAO,CAAC,eAAe,EAAE,CAAC,CAyB5B;AAUD,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,aAAa,GACb,WAAW,GACX,YAAY,CAAC;AAEjB,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,eAAe,CAwCtF;AA+BD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CA2BlE;AAMD,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAEjE;AAaD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAiB/D;AAUD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBpE;AAGD,wBAAgB,UAAU,IAAI,OAAO,CAsBpC;AAWD,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAe9C;AAaD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGjD;AAiJD,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IAGjB,aAAa,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAG7D,WAAW,EAAE,MAAM,CAAC;IAIpB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAO1D,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAKvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,cAAc,CAClC,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC;IAAE,MAAM,EAAE,WAAW,GAAG,qBAAqB,GAAG,SAAS,CAAA;CAAE,CAAC,CAsBtE;AAuRD,wBAAsB,kBAAkB,CAAC,IAAI,CAAC,EAAE;IAC9C,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IAQpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,OAAO,CAAC,WAAW,CAAC,CA8DvB;AASD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQzE;AAgCD,wBAAsB,6BAA6B,CAAC,IAAI,EAAE;IACxD,UAAU,EAAE,MAAM,CAAC;IAOnB,gBAAgB,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IAIxB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAyDxE"}
@@ -32,7 +32,7 @@ import boxen from "boxen";
32
32
  import chalk from "chalk";
33
33
  import { shortenVncUrl } from "../api-client.js";
34
34
  import { CHROME_PROFILE_DIR, launchWithProfileGate, ProfileBusyError, waitForProfileFree } from "./profile.js";
35
- import { markProviderLoggedIn } from "./login-state.js";
35
+ import { markProviderLoggedIn, recordProviderEmail } from "./login-state.js";
36
36
  import { randomBytes } from "node:crypto";
37
37
  const require = createRequire(import.meta.url);
38
38
  // Route the INTERACTIVE login through the same proxy the bot uses, so the
@@ -871,6 +871,46 @@ export async function ensureOAuthSession(opts) {
871
871
  return { status: "error", detail: err instanceof Error ? err.message : String(err) };
872
872
  }
873
873
  }
874
+ // PR3 signin-vault: pull the signed-in Google address out of an account page's
875
+ // text. The OneGoogle account chip carries an aria-label like
876
+ // "Google Account: Ada Lovelace (ada@example.com)" on every Google surface, so
877
+ // prefer that anchored match; fall back to the first email-shaped token (the
878
+ // myaccount.google.com page renders the address prominently). Pure + exported
879
+ // for unit tests — the live navigation that feeds it is captureGoogleEmail.
880
+ const EMAIL_TOKEN_RE = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i;
881
+ export function extractGoogleAccountEmail(pageText) {
882
+ const chip = /Google Account:[^()]*\(([^)]+)\)/i.exec(pageText);
883
+ if (chip?.[1] !== undefined) {
884
+ const m = EMAIL_TOKEN_RE.exec(chip[1]);
885
+ if (m !== null)
886
+ return m[0].trim();
887
+ }
888
+ const any = EMAIL_TOKEN_RE.exec(pageText);
889
+ return any !== null ? any[0].trim() : null;
890
+ }
891
+ // Capture-at-login: with the profile's live Google session, read the account's
892
+ // email once and persist it to the profile marker so provision can fill it as
893
+ // the signup email (user-owned accounts) without a per-run scrape. Best-effort:
894
+ // any failure leaves the marker unset and provision proceeds without a pre-known
895
+ // email. Only meaningful for google (the inbox identity); github has no inbox.
896
+ async function captureGoogleEmail(context, profileDir) {
897
+ let page = null;
898
+ try {
899
+ page = await context.newPage();
900
+ await page.goto("https://myaccount.google.com/", { waitUntil: "domcontentloaded", timeout: 15_000 });
901
+ const text = await page.evaluate(() => document.body.innerText).catch(() => "");
902
+ const email = extractGoogleAccountEmail(text);
903
+ if (email !== null)
904
+ recordProviderEmail("google", email, profileDir);
905
+ }
906
+ catch {
907
+ /* best-effort — no pre-known email this run */
908
+ }
909
+ finally {
910
+ if (page !== null)
911
+ await page.close().catch(() => undefined);
912
+ }
913
+ }
874
914
  // Public entry for the install flow: opens the trustysquire /install
875
915
  // confirm URL in the bot's persistent Chrome profile, runs the
876
916
  // user-supplied check until the install is claimed (or the deadline
@@ -917,8 +957,13 @@ export async function openInstallConfirmInBotChrome(opts) {
917
957
  break;
918
958
  }
919
959
  }
920
- if (hit)
960
+ if (hit) {
921
961
  markProviderLoggedIn(provider, profileDir);
962
+ // Capture-at-login: store the Google email now (the one moment it's
963
+ // certain) so provision fills it as the user-owned signup email.
964
+ if (provider === "google")
965
+ await captureGoogleEmail(context, profileDir);
966
+ }
922
967
  }
923
968
  },
924
969
  });