perplexity-user-mcp 0.8.36

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 (125) hide show
  1. package/README.md +192 -0
  2. package/dist/attachments.d.ts +20 -0
  3. package/dist/attachments.mjs +43 -0
  4. package/dist/checks/browser.d.ts +100 -0
  5. package/dist/checks/browser.mjs +89 -0
  6. package/dist/checks/config.d.ts +91 -0
  7. package/dist/checks/config.mjs +88 -0
  8. package/dist/checks/ide.d.ts +89 -0
  9. package/dist/checks/ide.mjs +80 -0
  10. package/dist/checks/mcp.d.ts +61 -0
  11. package/dist/checks/mcp.mjs +56 -0
  12. package/dist/checks/native-deps.d.ts +131 -0
  13. package/dist/checks/native-deps.mjs +115 -0
  14. package/dist/checks/network.d.ts +71 -0
  15. package/dist/checks/network.mjs +70 -0
  16. package/dist/checks/probe.d.ts +93 -0
  17. package/dist/checks/probe.mjs +82 -0
  18. package/dist/checks/profiles.d.ts +99 -0
  19. package/dist/checks/profiles.mjs +90 -0
  20. package/dist/checks/runtime.d.ts +89 -0
  21. package/dist/checks/runtime.mjs +90 -0
  22. package/dist/checks/vault.d.ts +101 -0
  23. package/dist/checks/vault.mjs +90 -0
  24. package/dist/chunk-3B276PGG.mjs +115 -0
  25. package/dist/chunk-4UEJOM6W.mjs +9 -0
  26. package/dist/chunk-6EP2BLTV.mjs +205 -0
  27. package/dist/chunk-6YMQVLFX.mjs +146 -0
  28. package/dist/chunk-7JL36EBH.mjs +118 -0
  29. package/dist/chunk-DPGMKSSA.mjs +57 -0
  30. package/dist/chunk-H4BUAPPO.mjs +1950 -0
  31. package/dist/chunk-HMKLWVXB.mjs +109 -0
  32. package/dist/chunk-HTUAQRKH.mjs +125 -0
  33. package/dist/chunk-HU5B4FXS.mjs +139 -0
  34. package/dist/chunk-KCXM2M4B.mjs +1006 -0
  35. package/dist/chunk-LKJMLGFP.mjs +237 -0
  36. package/dist/chunk-LZPLNZ5U.mjs +67 -0
  37. package/dist/chunk-MTDFKNXX.mjs +19 -0
  38. package/dist/chunk-OF4DMAPJ.mjs +511 -0
  39. package/dist/chunk-PE23RMXY.mjs +43 -0
  40. package/dist/chunk-Q2VY4R5F.mjs +175 -0
  41. package/dist/chunk-S5VD7WTU.mjs +2540 -0
  42. package/dist/chunk-SVPRB62V.mjs +106 -0
  43. package/dist/chunk-TQLCLE4L.mjs +345 -0
  44. package/dist/chunk-U3DGFLXZ.mjs +43 -0
  45. package/dist/chunk-X45O6YD3.mjs +688 -0
  46. package/dist/chunk-XKSWCEGI.mjs +168 -0
  47. package/dist/chunk-Z7DAACGZ.mjs +534 -0
  48. package/dist/chunk-ZQFUZPLO.mjs +257 -0
  49. package/dist/cli.d.ts +952 -0
  50. package/dist/cli.mjs +827 -0
  51. package/dist/client.d.ts +355 -0
  52. package/dist/client.mjs +27 -0
  53. package/dist/cloud-sync.d-Cqt6y18U.d.ts +42 -0
  54. package/dist/cloud-sync.d.ts +42 -0
  55. package/dist/cloud-sync.mjs +17 -0
  56. package/dist/config.d.ts +186 -0
  57. package/dist/config.mjs +54 -0
  58. package/dist/daemon/attach.d.ts +36 -0
  59. package/dist/daemon/attach.mjs +25 -0
  60. package/dist/daemon/audit.d.ts +23 -0
  61. package/dist/daemon/audit.mjs +12 -0
  62. package/dist/daemon/client-http.d.ts +42 -0
  63. package/dist/daemon/client-http.mjs +29 -0
  64. package/dist/daemon/index.d.ts +14 -0
  65. package/dist/daemon/index.mjs +110 -0
  66. package/dist/daemon/install-tunnel.d.ts +46 -0
  67. package/dist/daemon/install-tunnel.mjs +14 -0
  68. package/dist/daemon/launcher.d.ts +163 -0
  69. package/dist/daemon/launcher.mjs +50 -0
  70. package/dist/daemon/lockfile.d.ts +29 -0
  71. package/dist/daemon/lockfile.mjs +18 -0
  72. package/dist/daemon/server.d.ts +159 -0
  73. package/dist/daemon/server.mjs +20 -0
  74. package/dist/daemon/token.d.ts +17 -0
  75. package/dist/daemon/token.mjs +17 -0
  76. package/dist/daemon/tunnel-providers/index.d.ts +330 -0
  77. package/dist/daemon/tunnel-providers/index.mjs +57 -0
  78. package/dist/daemon/tunnel.d.ts +23 -0
  79. package/dist/daemon/tunnel.mjs +9 -0
  80. package/dist/doctor-report.d.ts +24 -0
  81. package/dist/doctor-report.mjs +14 -0
  82. package/dist/doctor.d-CXmUqOXX.d.ts +43 -0
  83. package/dist/doctor.d.ts +44 -0
  84. package/dist/doctor.mjs +16 -0
  85. package/dist/export.d.ts +19 -0
  86. package/dist/export.mjs +15 -0
  87. package/dist/health-check.d.ts +108 -0
  88. package/dist/health-check.mjs +92 -0
  89. package/dist/history-store.d-BzjBF2m3.d.ts +65 -0
  90. package/dist/history-store.d.ts +65 -0
  91. package/dist/history-store.mjs +48 -0
  92. package/dist/impit-login-runner.d.ts +469 -0
  93. package/dist/impit-login-runner.mjs +685 -0
  94. package/dist/index.d.ts +159 -0
  95. package/dist/index.mjs +236 -0
  96. package/dist/login-runner.d.ts +333 -0
  97. package/dist/login-runner.mjs +320 -0
  98. package/dist/logout.d.ts +28 -0
  99. package/dist/logout.mjs +45 -0
  100. package/dist/manual-login-runner.d.ts +150 -0
  101. package/dist/manual-login-runner.mjs +146 -0
  102. package/dist/native-deps-BNThFHxa.d.ts +175 -0
  103. package/dist/native-deps-YNKXITRY.mjs +139 -0
  104. package/dist/profiles.d-DqS1oZWr.d.ts +41 -0
  105. package/dist/profiles.d.ts +41 -0
  106. package/dist/profiles.mjs +33 -0
  107. package/dist/redact.d.ts +159 -0
  108. package/dist/redact.mjs +11 -0
  109. package/dist/refresh.d.ts +118 -0
  110. package/dist/refresh.mjs +21 -0
  111. package/dist/reinit-watcher.d.ts +15 -0
  112. package/dist/reinit-watcher.mjs +8 -0
  113. package/dist/session-metadata-B9aV_n5g.d.ts +148 -0
  114. package/dist/tty-prompt.d.ts +44 -0
  115. package/dist/tty-prompt.mjs +39 -0
  116. package/dist/vault.d-BtRSLZiM.d.ts +8 -0
  117. package/dist/vault.d.ts +37 -0
  118. package/dist/vault.mjs +21 -0
  119. package/dist/viewer-detect.d-HWGnyFAA.d.ts +4 -0
  120. package/dist/viewer-detect.d.ts +4 -0
  121. package/dist/viewer-detect.mjs +37 -0
  122. package/dist/viewers.d-BGCK6sw6.d.ts +10 -0
  123. package/dist/viewers.d.ts +18 -0
  124. package/dist/viewers.mjs +122 -0
  125. package/package.json +152 -0
@@ -0,0 +1,469 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { Vault } from './vault.d-BtRSLZiM.js';
3
+ import { recordLoginSuccess, getActiveName, getProfilePaths } from './profiles.d-DqS1oZWr.js';
4
+ import { redact } from './redact.js';
5
+ import { b as buildRuntimeEndpoints } from './session-metadata-B9aV_n5g.js';
6
+ import { PlaywrightCookie } from './config.js';
7
+ import { isImpitAvailable, loadImpit } from './refresh.js';
8
+ import 'patchright';
9
+
10
+ /**
11
+ * Minimal RFC 6265-style cookie jar for the impit-driven login flow.
12
+ *
13
+ * Identity = `(name, domain, path)` triple — same triple replaces. Domain
14
+ * matching honours leading-dot semantics; path matching is prefix-based with
15
+ * default-path derived from the request URL. Honours `Expires`, `Max-Age`
16
+ * (Max-Age wins), `Secure`, and `HttpOnly` (round-tripped, not enforced).
17
+ *
18
+ * Round-trips cleanly to/from the `PlaywrightCookie` shape used by
19
+ * `getSavedCookies()` and the vault, so the impit login output can replace
20
+ * Patchright's cookie array drop-in.
21
+ */
22
+ declare class CookieJar {
23
+ constructor(initialCookies?: PlaywrightCookie[]);
24
+
25
+ /**
26
+ * Apply one or more `Set-Cookie` headers from a response. Accepts either a
27
+ * single string (joined header) or an array of values, e.g. from
28
+ * `res.headers.getSetCookie?.()`. Cookies whose Max-Age is `<= 0` or whose
29
+ * Expires is in the past are *deleted* from the jar (server-driven delete);
30
+ * cookies that merely become stale via wall-clock drift remain in the jar
31
+ * but are filtered out of `buildCookieHeader`.
32
+ */
33
+ consumeSetCookieHeader(header: string | string[] | null | undefined, requestUrl: string): void;
34
+
35
+ /**
36
+ * Build a `Cookie:` header value for `requestUrl`. Returns `""` when no
37
+ * cookies match (so callers can `if (h) headers.cookie = h`). Filters out
38
+ * cookies whose Secure flag is set when the request URL is `http://`, and
39
+ * cookies whose Expires/Max-Age is in the past. Sorted by path length
40
+ * descending, per RFC 6265 §5.4.
41
+ */
42
+ buildCookieHeader(requestUrl: string): string;
43
+
44
+ /**
45
+ * Snapshot of every cookie in the jar (including expired ones — callers
46
+ * that persist to disk want the full set so a later refresh can simply
47
+ * overwrite).
48
+ */
49
+ toPlaywrightShape(): PlaywrightCookie[];
50
+
51
+ /**
52
+ * Explicit setter, mostly for tests and for callers that already have a
53
+ * fully-formed cookie record (e.g. seeding `cf_clearance` from the vault).
54
+ */
55
+ set(
56
+ name: string,
57
+ value: string,
58
+ attributes?: Partial<PlaywrightCookie> & { hostOnly?: boolean },
59
+ ): void;
60
+ }
61
+
62
+ interface WarmCloudflareOptions {
63
+ /** Hard cap on the initial page.goto. Default 15_000ms. */
64
+ navigationTimeoutMs?: number;
65
+ /** How often to recheck context cookies for `cf_clearance`. Default 250ms. */
66
+ pollIntervalMs?: number;
67
+ /** Total wall-clock budget for the cookie poll loop after navigation. Default 10_000ms. */
68
+ cookieWaitMs?: number;
69
+ }
70
+ interface WarmCloudflareResult {
71
+ /**
72
+ * True iff the browser launched and navigation succeeded. May be true even
73
+ * when `hasCfClearance` is false — distinguishes "browser unusable" from
74
+ * "browser fine, CF just didn't issue clearance this time".
75
+ */
76
+ ok: boolean;
77
+ /** All cookies present in the ephemeral context at teardown. Empty on launch failure. */
78
+ cookies: PlaywrightCookie[];
79
+ /** Whether `cf_clearance` was observed in `cookies`. */
80
+ hasCfClearance: boolean;
81
+ /** Wall-clock duration of the warmup attempt, in milliseconds. */
82
+ elapsedMs: number;
83
+ /** Populated when `ok=false`. Single-line, human-readable. */
84
+ error?: string;
85
+ }
86
+ /**
87
+ * Brief headless launch to capture cf_clearance for the impit-login flow.
88
+ *
89
+ * Always returns a result object — never throws. Caller falls back to the
90
+ * full browser-based login when ok=false or hasCfClearance=false.
91
+ */
92
+ declare function warmCloudflare(opts?: WarmCloudflareOptions): Promise<WarmCloudflareResult>;
93
+
94
+ // Impit-driven Auto-OTP login runner. Same emit/IPC protocol as
95
+ // `login-runner.js` but drives the 6-step Perplexity email+OTP flow
96
+ // (csrf → sso check → signin/email → wait OTP → otp-redirect → callback)
97
+ // through impit (Rust HTTP client) instead of through a Patchright
98
+ // browser. The visible browser window is replaced with a brief headless
99
+ // CF warmup (only when the vault has no fresh `cf_clearance` cookie).
100
+ //
101
+ // Plan: docs/impit-coverage-plan.md §3 (Phase 1).
102
+ //
103
+ // Inputs (env):
104
+ // PERPLEXITY_EMAIL — required
105
+ // PERPLEXITY_PROFILE — profile name to write cookies to
106
+ // PERPLEXITY_OTP_TIMEOUT_MS — default 300_000
107
+ // PERPLEXITY_LOGIN_MAX_RETRIES — default 2
108
+ //
109
+ // Outputs:
110
+ // process.send({ phase: "awaiting_otp", attempt: N }) — IPC, parent prompts user
111
+ // process.stdin gets { otp: "123456" } — IPC, parent sends back
112
+ // process.stdout: JSON line `{ ok, ... }` final result
113
+ // exit codes: 0 success, 2 logical fail, 4 chrome/impit missing, 5 crash
114
+
115
+
116
+ const ORIGIN = process.env.PERPLEXITY_ORIGIN || "https://www.perplexity.ai";
117
+ const EMAIL = process.env.PERPLEXITY_EMAIL;
118
+ const OTP_TIMEOUT_MS = Number(process.env.PERPLEXITY_OTP_TIMEOUT_MS ?? 300_000);
119
+ const MAX_RETRIES = Number(process.env.PERPLEXITY_LOGIN_MAX_RETRIES ?? 2);
120
+ const USER_AGENT =
121
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
122
+ const CHROME_CLIENT_HINTS = {
123
+ "sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
124
+ "sec-ch-ua-mobile": "?0",
125
+ "sec-ch-ua-platform": '"Windows"',
126
+ };
127
+
128
+ function resolveProfile() {
129
+ return process.env.PERPLEXITY_PROFILE || getActiveName() || "default";
130
+ }
131
+
132
+ function ipc(msg) { if (process.send) process.send(msg); }
133
+ function emit(obj) { process.stdout.write(JSON.stringify(obj) + "\n"); }
134
+
135
+ async function awaitOtp() {
136
+ return new Promise((resolve, reject) => {
137
+ const timer = setTimeout(() => {
138
+ process.removeListener("message", handler);
139
+ reject(new Error("otp_timeout"));
140
+ }, OTP_TIMEOUT_MS);
141
+ const handler = (m) => {
142
+ if (m && typeof m.otp === "string") {
143
+ clearTimeout(timer);
144
+ process.removeListener("message", handler);
145
+ resolve(m.otp);
146
+ }
147
+ };
148
+ process.on("message", handler);
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Wrap an impit instance in a small fetch helper that round-trips the
154
+ * cookie jar. Returns { status, headers, json, text }.
155
+ */
156
+ async function impitJsonRequest(client, jar, url, init = {}) {
157
+ const cookieHeader = jar.buildCookieHeader(url);
158
+ const headers = {
159
+ "user-agent": USER_AGENT,
160
+ accept: "application/json, text/plain, */*",
161
+ "accept-language": "en-US,en;q=0.9",
162
+ ...CHROME_CLIENT_HINTS,
163
+ ...(cookieHeader ? { cookie: cookieHeader } : {}),
164
+ ...(init.body !== undefined ? { "content-type": "application/json" } : {}),
165
+ referer: `${ORIGIN}/`,
166
+ origin: ORIGIN,
167
+ ...(init.headers ?? {}),
168
+ };
169
+ const ctrl = new AbortController();
170
+ const to = setTimeout(() => ctrl.abort(), init.timeoutMs ?? 30_000);
171
+ try {
172
+ const res = await client.fetch(url, {
173
+ method: init.method ?? "GET",
174
+ headers,
175
+ body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
176
+ signal: ctrl.signal,
177
+ redirect: init.redirect ?? "manual",
178
+ });
179
+ const text = await res.text();
180
+ // Capture Set-Cookie before reading body — Node Headers carries them.
181
+ const setCookies = readSetCookies(res.headers);
182
+ if (setCookies.length) jar.consumeSetCookieHeader(setCookies, url);
183
+ let json;
184
+ if (text) {
185
+ try { json = JSON.parse(text); } catch { /* leave undefined */ }
186
+ }
187
+ return { status: res.status, headers: res.headers, text, json };
188
+ } finally {
189
+ clearTimeout(to);
190
+ }
191
+ }
192
+
193
+ function readSetCookies(headers) {
194
+ if (!headers) return [];
195
+ // Node 20+ standard
196
+ if (typeof headers.getSetCookie === "function") {
197
+ return headers.getSetCookie();
198
+ }
199
+ // Headers#raw() (undici)
200
+ if (typeof headers.raw === "function") {
201
+ const raw = headers.raw();
202
+ return raw["set-cookie"] ?? [];
203
+ }
204
+ // Plain object fallback
205
+ const lower = Object.keys(headers).find((k) => k.toLowerCase() === "set-cookie");
206
+ if (!lower) return [];
207
+ const v = headers[lower];
208
+ return Array.isArray(v) ? v : [v];
209
+ }
210
+
211
+ /**
212
+ * Follow redirects manually so we can capture Set-Cookie at every hop —
213
+ * the OTP callback chain depends on cookies set in 302 responses, which
214
+ * `redirect: "follow"` would swallow.
215
+ */
216
+ async function followRedirectsManually(client, jar, startUrl, init = {}, maxHops = 10) {
217
+ let url = startUrl;
218
+ let lastStatus = 0;
219
+ for (let hop = 0; hop < maxHops; hop++) {
220
+ const result = await impitJsonRequest(client, jar, url, { ...init, redirect: "manual", method: hop === 0 ? init.method ?? "GET" : "GET" });
221
+ lastStatus = result.status;
222
+ if (result.status >= 300 && result.status < 400) {
223
+ const loc = readHeader(result.headers, "location");
224
+ if (!loc) return { ok: false, status: result.status, finalUrl: url };
225
+ url = new URL(loc, url).toString();
226
+ // Subsequent hops must not re-send the body
227
+ init.body = undefined;
228
+ continue;
229
+ }
230
+ return { ok: result.status >= 200 && result.status < 300, status: result.status, finalUrl: url, json: result.json, text: result.text };
231
+ }
232
+ return { ok: false, status: lastStatus, finalUrl: url, error: "max_redirects" };
233
+ }
234
+
235
+ function readHeader(headers, name) {
236
+ if (!headers) return null;
237
+ if (typeof headers.get === "function") return headers.get(name);
238
+ const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
239
+ return key ? headers[key] : null;
240
+ }
241
+
242
+ /**
243
+ * Steps 1-3 of the auth flow: csrf → sso check → email signin.
244
+ * Returns either { kind: "live", redirectUrl } on success, or a structured
245
+ * failure that the caller maps to an exit reason.
246
+ */
247
+ async function startEmailFlow(client, jar) {
248
+ const endpoints = buildRuntimeEndpoints(ORIGIN);
249
+
250
+ const csrf = await impitJsonRequest(client, jar, endpoints.csrf);
251
+ if (csrf.status !== 200 || !csrf.json?.csrfToken) {
252
+ return { kind: "unsupported", detail: { step: "csrf", status: csrf.status } };
253
+ }
254
+
255
+ const sso = await impitJsonRequest(client, jar, endpoints.ssoDetails, {
256
+ method: "POST",
257
+ body: { email: EMAIL },
258
+ });
259
+ if (sso.status === 200 && sso.json?.organization) {
260
+ return { kind: "sso_required" };
261
+ }
262
+
263
+ const redirectUrl = `${ORIGIN}/account?login-source=settings`;
264
+ const signIn = await impitJsonRequest(client, jar, endpoints.signInEmail, {
265
+ method: "POST",
266
+ body: {
267
+ email: EMAIL,
268
+ useNumericOtp: "true",
269
+ csrfToken: csrf.json.csrfToken,
270
+ callbackUrl: `${redirectUrl}#locale=en-US`,
271
+ json: "true",
272
+ },
273
+ });
274
+
275
+ if (signIn.status !== 200 || !signIn.json?.url) {
276
+ return {
277
+ kind: signIn.status >= 400 && signIn.status < 500 ? "email_rejected" : "unsupported",
278
+ detail: { step: "signin_email", status: signIn.status },
279
+ };
280
+ }
281
+
282
+ return { kind: "live", redirectUrl, csrfToken: csrf.json.csrfToken };
283
+ }
284
+
285
+ async function submitOtp(client, jar, flow, otp) {
286
+ const endpoints = buildRuntimeEndpoints(ORIGIN);
287
+ const redirectResp = await impitJsonRequest(client, jar, endpoints.otpRedirectLink, {
288
+ method: "POST",
289
+ body: {
290
+ email: EMAIL,
291
+ otp,
292
+ redirectUrl: flow.redirectUrl,
293
+ emailLoginMethod: "web-otp",
294
+ loginSource: null,
295
+ },
296
+ });
297
+ if (redirectResp.status !== 200 || !redirectResp.json?.redirect) {
298
+ return { ok: false };
299
+ }
300
+ // Follow the callback redirect chain manually to capture session cookies.
301
+ const callbackUrl = new URL(redirectResp.json.redirect, ORIGIN).toString();
302
+ const callback = await followRedirectsManually(client, jar, callbackUrl, { method: "GET" });
303
+ if (!callback.ok) return { ok: false };
304
+ // After the chain, the jar should have __Secure-next-auth.session-token.
305
+ const cookies = jar.toPlaywrightShape();
306
+ const hasSession = cookies.some((c) => c.name === "__Secure-next-auth.session-token");
307
+ if (!hasSession) return { ok: false };
308
+ return { ok: true, cookies };
309
+ }
310
+
311
+ /**
312
+ * Probe an authenticated endpoint to fetch session/user metadata after a
313
+ * successful OTP. We don't have a page to evaluate in, so this is a small
314
+ * direct fetch — reusing the same jar.
315
+ */
316
+ async function fetchSessionInfo(client, jar) {
317
+ const session = await impitJsonRequest(client, jar, `${ORIGIN}/api/auth/session?version=2.18&source=default`);
318
+ return session.json ?? {};
319
+ }
320
+
321
+ async function fetchModelsCache(client, jar) {
322
+ // Mirrors what `refresh.ts` writes — but via this jar. Best-effort.
323
+ const result = {};
324
+ for (const [key, path] of [
325
+ ["models", "/rest/configs/models?version=2.18&source=default"],
326
+ ["asi", "/rest/configs/asi-access?version=2.18&source=default"],
327
+ ["rateLimits", "/rest/rate-limits?version=2.18&source=default"],
328
+ ["experiments", "/rest/experiments?version=2.18&source=default"],
329
+ ["userInfo", "/rest/user/info?version=2.18&source=default"],
330
+ ]) {
331
+ try {
332
+ const r = await impitJsonRequest(client, jar, `${ORIGIN}${path}`);
333
+ if (r.status === 200 && r.json) result[key] = r.json;
334
+ } catch { /* ignore — partial cache is fine */ }
335
+ }
336
+ return result;
337
+ }
338
+
339
+ function deriveTier(modelsCache) {
340
+ if (modelsCache.userInfo?.subscription_tier === "enterprise") return "Enterprise";
341
+ if (modelsCache.userInfo?.subscription_tier === "max") return "Max";
342
+ if (modelsCache.userInfo?.subscription_tier === "pro") return "Pro";
343
+ if (modelsCache.experiments?.server_is_enterprise) return "Enterprise";
344
+ if (modelsCache.experiments?.server_is_max) return "Max";
345
+ if (modelsCache.experiments?.server_is_pro) return "Pro";
346
+ if (modelsCache.asi?.can_use_computer) return "Pro";
347
+ return "Authenticated";
348
+ }
349
+
350
+ async function main() {
351
+ if (!EMAIL) {
352
+ emit({ ok: false, reason: "no_email" });
353
+ process.exit(1);
354
+ }
355
+ if (!isImpitAvailable()) {
356
+ emit({ ok: false, reason: "impit_missing", error: "Speed Boost (impit) not installed" });
357
+ process.exit(4);
358
+ }
359
+
360
+ const PROFILE = resolveProfile();
361
+
362
+ // 1. Seed jar from existing vault cookies (if any). Brand-new logins
363
+ // get an empty jar; subsequent re-logins inherit cf_clearance etc.
364
+ const vault = new Vault();
365
+ const jar = new CookieJar([]);
366
+ try {
367
+ const stored = await vault.get(PROFILE, "cookies").catch(() => null);
368
+ if (stored) {
369
+ const parsed = JSON.parse(stored);
370
+ if (Array.isArray(parsed)) {
371
+ for (const c of parsed) jar.set?.(c.name, c.value, { domain: c.domain, path: c.path, expires: c.expires, secure: c.secure, httpOnly: c.httpOnly, sameSite: c.sameSite });
372
+ }
373
+ }
374
+ } catch { /* ignore — seed is best-effort */ }
375
+
376
+ // 2. CF warmup if jar has no cf_clearance. ~1-2s headless launch.
377
+ if (!jar.toPlaywrightShape().some((c) => c.name === "cf_clearance")) {
378
+ const warmup = await warmCloudflare();
379
+ if (warmup.cookies.length) {
380
+ for (const c of warmup.cookies) {
381
+ jar.set?.(c.name, c.value, { domain: c.domain, path: c.path, expires: c.expires, secure: c.secure, httpOnly: c.httpOnly, sameSite: c.sameSite });
382
+ }
383
+ }
384
+ if (!warmup.hasCfClearance) {
385
+ // No cf_clearance after warmup → CF is challenging this IP. Bail to
386
+ // browser fallback which has the headed CF solver.
387
+ emit({ ok: false, reason: "cf_blocked", detail: { warmupOk: warmup.ok } });
388
+ process.exit(3);
389
+ }
390
+ }
391
+
392
+ // 3. Load impit and run the email→OTP→callback flow.
393
+ const impitMod = await loadImpit();
394
+ if (!impitMod) {
395
+ emit({ ok: false, reason: "impit_load_failed" });
396
+ process.exit(4);
397
+ }
398
+ const client = new impitMod.Impit({ browser: "chrome", ignoreTlsErrors: false });
399
+
400
+ let authFlow;
401
+ try {
402
+ authFlow = await startEmailFlow(client, jar);
403
+ } catch (err) {
404
+ emit({ ok: false, reason: "auto_unsupported", detail: { step: "start", error: redact(String(err?.message ?? err)) } });
405
+ process.exit(2);
406
+ }
407
+
408
+ if (authFlow.kind === "sso_required") {
409
+ emit({ ok: false, reason: "sso_required" });
410
+ process.exit(2);
411
+ }
412
+ if (authFlow.kind === "email_rejected") {
413
+ emit({ ok: false, reason: "email_rejected", detail: authFlow.detail });
414
+ process.exit(2);
415
+ }
416
+ if (authFlow.kind !== "live") {
417
+ emit({ ok: false, reason: "auto_unsupported", detail: authFlow.detail });
418
+ process.exit(2);
419
+ }
420
+
421
+ // 4. OTP loop with retry.
422
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
423
+ ipc({ phase: "awaiting_otp", attempt });
424
+ let otp;
425
+ try {
426
+ otp = await awaitOtp();
427
+ } catch {
428
+ emit({ ok: false, reason: "otp_timeout" });
429
+ process.exit(2);
430
+ }
431
+
432
+ const submitResp = await submitOtp(client, jar, authFlow, otp);
433
+ if (submitResp.ok) {
434
+ // 5. Persist cookies + session metadata.
435
+ const sessionInfo = await fetchSessionInfo(client, jar).catch(() => ({}));
436
+ const modelsCache = await fetchModelsCache(client, jar).catch(() => ({}));
437
+ const tier = deriveTier(modelsCache);
438
+
439
+ await vault.set(PROFILE, "cookies", JSON.stringify(submitResp.cookies));
440
+ if (sessionInfo?.user?.email) await vault.set(PROFILE, "email", sessionInfo.user.email);
441
+ if (sessionInfo?.user?.id) await vault.set(PROFILE, "userId", sessionInfo.user.id);
442
+
443
+ const paths = getProfilePaths(PROFILE);
444
+ if (!existsSync(paths.dir)) mkdirSync(paths.dir, { recursive: true });
445
+ writeFileSync(paths.modelsCache, JSON.stringify(modelsCache, null, 2));
446
+ recordLoginSuccess(PROFILE, { tier, loginMode: "auto", lastLogin: new Date().toISOString() });
447
+ writeFileSync(paths.reinit, String(Date.now()));
448
+
449
+ emit({ ok: true, tier, modelCount: Object.keys(modelsCache?.models?.models ?? {}).length, transport: "impit" });
450
+ process.exit(0);
451
+ }
452
+
453
+ if (attempt === MAX_RETRIES) {
454
+ emit({ ok: false, reason: "otp_rejected" });
455
+ process.exit(2);
456
+ }
457
+ }
458
+ }
459
+
460
+ main().catch((err) => {
461
+ const msg = err?.message ?? err;
462
+ emit({
463
+ ok: false,
464
+ reason: "crash",
465
+ error: redact(String(msg ?? "unknown error")),
466
+ ...(err?.stack ? { stack: redact(String(err.stack)) } : {}),
467
+ });
468
+ process.exit(5);
469
+ });