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,168 @@
1
+ // src/profiles.js
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
5
+ var NAME_RE = /^[a-z0-9_-]{1,32}$/;
6
+ function getConfigDir() {
7
+ return process.env.PERPLEXITY_CONFIG_DIR || join(homedir(), ".perplexity-mcp");
8
+ }
9
+ function getProfilesDir() {
10
+ return join(getConfigDir(), "profiles");
11
+ }
12
+ function getProfilePaths(name) {
13
+ const dir = join(getProfilesDir(), name);
14
+ return {
15
+ dir,
16
+ meta: join(dir, "meta.json"),
17
+ vault: join(dir, "vault.enc"),
18
+ vaultPlain: join(dir, "vault.json"),
19
+ browserData: join(dir, "browser-data"),
20
+ modelsCache: join(dir, "models-cache.json"),
21
+ history: join(dir, "history"),
22
+ attachments: join(dir, "attachments"),
23
+ researches: join(dir, "researches"),
24
+ reinit: join(dir, ".reinit")
25
+ };
26
+ }
27
+ function validateName(name) {
28
+ if (!name || typeof name !== "string") return "Profile name is required.";
29
+ if (name.length > 32) return "Profile name must be 32 characters or fewer.";
30
+ if (name !== name.toLowerCase()) return "Profile name must be lowercase.";
31
+ if (!NAME_RE.test(name)) {
32
+ return "Profile name must contain only lowercase letters, digits, hyphens, and underscores. Invalid characters detected.";
33
+ }
34
+ return null;
35
+ }
36
+ function readMeta(name) {
37
+ const p = getProfilePaths(name).meta;
38
+ if (!existsSync(p)) return null;
39
+ try {
40
+ return JSON.parse(readFileSync(p, "utf8"));
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ function writeMeta(name, meta) {
46
+ const paths = getProfilePaths(name);
47
+ if (!existsSync(paths.dir)) mkdirSync(paths.dir, { recursive: true });
48
+ writeFileSync(paths.meta + ".tmp", JSON.stringify(meta, null, 2) + "\n");
49
+ renameSync(paths.meta + ".tmp", paths.meta);
50
+ }
51
+ function createProfile(name, opts = {}) {
52
+ const err = validateName(name);
53
+ if (err) throw new Error(err);
54
+ const paths = getProfilePaths(name);
55
+ if (existsSync(paths.dir)) throw new Error(`Profile '${name}' already exists.`);
56
+ mkdirSync(paths.dir, { recursive: true });
57
+ const meta = {
58
+ name,
59
+ displayName: opts.displayName ?? name,
60
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
61
+ loginMode: opts.loginMode ?? "manual"
62
+ };
63
+ writeMeta(name, meta);
64
+ return meta;
65
+ }
66
+ function listProfiles() {
67
+ const dir = getProfilesDir();
68
+ if (!existsSync(dir)) return [];
69
+ return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => readMeta(d.name)).filter(Boolean);
70
+ }
71
+ function getProfile(name) {
72
+ return readMeta(name);
73
+ }
74
+ function deleteProfile(name) {
75
+ const wasActive = getActiveName() === name;
76
+ const dir = getProfilePaths(name).dir;
77
+ if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
78
+ if (wasActive) {
79
+ const remaining = listProfiles();
80
+ if (remaining.length > 0) {
81
+ setActive(remaining[0].name);
82
+ } else {
83
+ const activePath = getActivePointerPath();
84
+ if (existsSync(activePath)) rmSync(activePath, { force: true });
85
+ }
86
+ }
87
+ import("./vault.mjs").then((m) => {
88
+ m.__resetKeyCache();
89
+ }).catch(() => {
90
+ });
91
+ }
92
+ function getActivePointerPath() {
93
+ return join(getConfigDir(), "active");
94
+ }
95
+ function getActiveName() {
96
+ const p = getActivePointerPath();
97
+ if (!existsSync(p)) return null;
98
+ try {
99
+ const name = readFileSync(p, "utf8").trim();
100
+ return name || null;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+ function getActive() {
106
+ const name = getActiveName();
107
+ if (!name) return null;
108
+ return getProfile(name);
109
+ }
110
+ function setActive(name) {
111
+ if (!getProfile(name)) throw new Error(`Profile '${name}' not found.`);
112
+ const cfg = getConfigDir();
113
+ if (!existsSync(cfg)) mkdirSync(cfg, { recursive: true });
114
+ const p = getActivePointerPath();
115
+ writeFileSync(p + ".tmp", name + "\n");
116
+ renameSync(p + ".tmp", p);
117
+ import("./vault.mjs").then((m) => {
118
+ m.__resetKeyCache();
119
+ }).catch(() => {
120
+ });
121
+ }
122
+ function suggestNextDefaultName() {
123
+ const names = listProfiles().map((p) => p.name);
124
+ let n = 1;
125
+ while (names.includes(`account-${n}`)) n++;
126
+ return `account-${n}`;
127
+ }
128
+ function renameProfile(oldName, newName) {
129
+ const err = validateName(newName);
130
+ if (err) throw new Error(err);
131
+ const oldPaths = getProfilePaths(oldName);
132
+ const newPaths = getProfilePaths(newName);
133
+ if (!existsSync(oldPaths.dir)) throw new Error(`Profile '${oldName}' not found.`);
134
+ if (existsSync(newPaths.dir)) throw new Error(`Profile '${newName}' already exists.`);
135
+ renameSync(oldPaths.dir, newPaths.dir);
136
+ const meta = readMeta(newName);
137
+ if (meta) {
138
+ meta.name = newName;
139
+ if (meta.displayName === oldName) meta.displayName = newName;
140
+ writeMeta(newName, meta);
141
+ }
142
+ if (getActiveName() === oldName) setActive(newName);
143
+ }
144
+ function recordLoginSuccess(name, { tier, loginMode, lastLogin }) {
145
+ const paths = getProfilePaths(name);
146
+ if (!existsSync(paths.dir)) mkdirSync(paths.dir, { recursive: true });
147
+ const current = readMeta(name) ?? { name, displayName: name, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
148
+ const meta = { ...current, loginMode, tier, lastLogin };
149
+ writeMeta(name, meta);
150
+ return meta;
151
+ }
152
+
153
+ export {
154
+ getConfigDir,
155
+ getProfilesDir,
156
+ getProfilePaths,
157
+ validateName,
158
+ createProfile,
159
+ listProfiles,
160
+ getProfile,
161
+ deleteProfile,
162
+ getActiveName,
163
+ getActive,
164
+ setActive,
165
+ suggestNextDefaultName,
166
+ renameProfile,
167
+ recordLoginSuccess
168
+ };
@@ -0,0 +1,534 @@
1
+ import {
2
+ ASI_ACCESS_ENDPOINT,
3
+ EXPERIMENTS_ENDPOINT,
4
+ MODELS_CONFIG_ENDPOINT,
5
+ PERPLEXITY_URL,
6
+ RATE_LIMIT_ENDPOINT,
7
+ USER_INFO_ENDPOINT,
8
+ findBrowser,
9
+ getOrCreateContext,
10
+ getSavedCookies,
11
+ resolveBrowserExecutable
12
+ } from "./chunk-LKJMLGFP.mjs";
13
+ import {
14
+ getActiveName,
15
+ getConfigDir,
16
+ getProfilePaths
17
+ } from "./chunk-XKSWCEGI.mjs";
18
+
19
+ // src/refresh.ts
20
+ import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync } from "fs";
21
+ import { dirname, join } from "path";
22
+ function getActiveProfileName() {
23
+ return process.env.PERPLEXITY_PROFILE || getActiveName() || "default";
24
+ }
25
+ function getModelsCacheFile() {
26
+ return getProfilePaths(getActiveProfileName()).modelsCache;
27
+ }
28
+ function getLegacyCookiesFile() {
29
+ return join(getProfilePaths(getActiveProfileName()).dir, "cookies.json");
30
+ }
31
+ function getImpitRuntimeDirPath() {
32
+ return join(getConfigDir(), "native-deps");
33
+ }
34
+ var STEALTH_ARGS = [
35
+ "--disable-blink-features=AutomationControlled",
36
+ // NOTE: `--disable-web-security` was removed (2026-04-27 public-hardening
37
+ // audit). All in-page `fetch()` calls used by the cookie-refresh tier hit
38
+ // the same Perplexity origin, so CORS is not a factor; keeping the flag
39
+ // would needlessly weaken the browser's same-origin policy. The off-origin
40
+ // ASI download path lives in client.ts and now uses APIRequestContext.
41
+ //
42
+ // NOTE: `--disable-features=IsolateOrigins,site-per-process` and
43
+ // `--disable-site-isolation-trials` were removed (2026-04-27 public-
44
+ // hardening audit). They disable Chromium's Site Isolation process model,
45
+ // which is a renderer-architecture feature invisible to JavaScript on the
46
+ // page (no documented fingerprint surface — Patchright's
47
+ // `chromiumSwitches.js` does not include them). The cookie-refresh path
48
+ // never enumerates frames or cross-origin iframes, so the only effect of
49
+ // keeping them was a silent reduction in the browser's Spectre/UXSS
50
+ // defense-in-depth.
51
+ "--no-first-run",
52
+ "--no-default-browser-check",
53
+ "--disable-infobars",
54
+ "--disable-extensions",
55
+ "--disable-popup-blocking"
56
+ ];
57
+ var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
58
+ var CHROME_CLIENT_HINTS = {
59
+ "sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
60
+ "sec-ch-ua-mobile": "?0",
61
+ "sec-ch-ua-platform": '"Windows"',
62
+ "sec-ch-ua-platform-version": '"15.0.0"',
63
+ "sec-ch-ua-arch": '"x86"',
64
+ "sec-ch-ua-bitness": '"64"',
65
+ "sec-ch-ua-full-version": '"138.0.7204.184"',
66
+ "sec-ch-ua-full-version-list": '"Not)A;Brand";v="8.0.0.0", "Chromium";v="138.0.7204.184", "Google Chrome";v="138.0.7204.184"',
67
+ "sec-ch-ua-model": '""',
68
+ "sec-fetch-dest": "empty",
69
+ "sec-fetch-mode": "cors",
70
+ "sec-fetch-site": "same-origin",
71
+ "x-app-apiclient": "default",
72
+ "x-app-apiversion": "2.18",
73
+ "x-perplexity-request-try-number": "1",
74
+ "x-perplexity-request-reason": "use-preferred-search-models",
75
+ accept: "*/*",
76
+ "accept-language": "en-US,en;q=0.9",
77
+ referer: `${PERPLEXITY_URL}/`,
78
+ origin: PERPLEXITY_URL
79
+ };
80
+ function noopLog(_) {
81
+ }
82
+ function buildCookieHeader(cookies) {
83
+ return cookies.filter((c) => c.domain?.includes("perplexity.ai")).map((c) => `${c.name}=${c.value}`).join("; ");
84
+ }
85
+ function detectCfChallenge(body) {
86
+ if (!body) return false;
87
+ const head = body.slice(0, 2e3).toLowerCase();
88
+ return head.includes("just a moment") || head.includes("cf-mitigated") || head.includes("enable javascript and cookies to continue") || head.includes("<html") && head.includes("cloudflare");
89
+ }
90
+ async function tierGotScraping(cookies, log, timeoutMs) {
91
+ const started = Date.now();
92
+ let gotScraping;
93
+ try {
94
+ ({ gotScraping } = await import("got-scraping"));
95
+ } catch (err) {
96
+ return { ok: false, error: `got-scraping missing: ${err.message}`, elapsedMs: Date.now() - started };
97
+ }
98
+ const cookieHeader = buildCookieHeader(cookies);
99
+ const headers = {
100
+ cookie: cookieHeader,
101
+ "user-agent": USER_AGENT,
102
+ ...CHROME_CLIENT_HINTS
103
+ };
104
+ const fetchJson = async (url, name) => {
105
+ try {
106
+ const res = await gotScraping({
107
+ url,
108
+ headers,
109
+ timeout: { request: timeoutMs },
110
+ throwHttpErrors: false,
111
+ responseType: "text",
112
+ headerGeneratorOptions: {
113
+ browsers: [{ name: "chrome", minVersion: 130 }],
114
+ operatingSystems: ["windows"]
115
+ }
116
+ });
117
+ const body = typeof res.body === "string" ? res.body : String(res.body ?? "");
118
+ if (res.statusCode !== 200) {
119
+ log(`got-scraping: ${name} status=${res.statusCode}`);
120
+ if (detectCfChallenge(body)) return { __challenged: true };
121
+ return null;
122
+ }
123
+ const ct = res.headers["content-type"] ?? "";
124
+ if (!String(ct).includes("application/json")) {
125
+ log(`got-scraping: ${name} non-JSON content-type=${ct}`);
126
+ if (detectCfChallenge(body)) return { __challenged: true };
127
+ return null;
128
+ }
129
+ return JSON.parse(body);
130
+ } catch (err) {
131
+ log(`got-scraping: ${name} threw ${err.message}`);
132
+ return null;
133
+ }
134
+ };
135
+ const [models, asi, rateLimits, experiments, userInfo] = await Promise.all([
136
+ fetchJson(MODELS_CONFIG_ENDPOINT, "models/config"),
137
+ fetchJson(ASI_ACCESS_ENDPOINT, "asi-access"),
138
+ fetchJson(RATE_LIMIT_ENDPOINT, "rate-limit"),
139
+ fetchJson(EXPERIMENTS_ENDPOINT, "experiments"),
140
+ fetchJson(USER_INFO_ENDPOINT, "user/info")
141
+ ]);
142
+ const challenged = !!(models && models.__challenged);
143
+ if (challenged) {
144
+ return { ok: false, challenged: true, error: "CF challenge via got-scraping", elapsedMs: Date.now() - started };
145
+ }
146
+ if (!models) {
147
+ return { ok: false, error: "got-scraping: models/config fetch failed", elapsedMs: Date.now() - started };
148
+ }
149
+ return {
150
+ ok: true,
151
+ payload: {
152
+ models,
153
+ asi: asi && !asi.__challenged ? asi : null,
154
+ rateLimits: rateLimits && !rateLimits.__challenged ? rateLimits : null,
155
+ experiments: experiments && !experiments.__challenged ? experiments : null,
156
+ userInfo: userInfo && !userInfo.__challenged ? userInfo : null
157
+ },
158
+ elapsedMs: Date.now() - started
159
+ };
160
+ }
161
+ async function loadImpit() {
162
+ const candidates = [
163
+ join(getImpitRuntimeDirPath(), "node_modules", "impit", "index.wrapper.js"),
164
+ join(getImpitRuntimeDirPath(), "node_modules", "impit", "index.js"),
165
+ join(getImpitRuntimeDirPath(), "node_modules", "impit", "dist", "index.js")
166
+ ];
167
+ const failures = [];
168
+ for (const p of candidates) {
169
+ if (!existsSync(p)) continue;
170
+ try {
171
+ const { createRequire } = await import("module");
172
+ const req = createRequire(p);
173
+ return req(p);
174
+ } catch (err) {
175
+ failures.push(`${p}: ${err.message ?? err}`);
176
+ }
177
+ }
178
+ if (failures.length) {
179
+ try {
180
+ console.error(`[impit] load failed: ${failures.join(" | ")}`);
181
+ } catch {
182
+ }
183
+ }
184
+ return null;
185
+ }
186
+ function isImpitAvailable() {
187
+ const marker = join(getImpitRuntimeDirPath(), "node_modules", "impit", "package.json");
188
+ return existsSync(marker);
189
+ }
190
+ async function impitFetchJson(url, init = {}, cookies, timeoutMs = 15e3) {
191
+ const impitMod = await loadImpit();
192
+ if (!impitMod) return null;
193
+ let client;
194
+ try {
195
+ client = new impitMod.Impit({ browser: "chrome", ignoreTlsErrors: false });
196
+ } catch {
197
+ return null;
198
+ }
199
+ const hasBody = init.body !== void 0 && init.body !== null;
200
+ const headers = {
201
+ cookie: buildCookieHeader(cookies),
202
+ "user-agent": USER_AGENT,
203
+ ...CHROME_CLIENT_HINTS,
204
+ accept: "application/json, text/plain, */*",
205
+ ...hasBody ? { "content-type": "application/json" } : {},
206
+ ...init.headers ?? {}
207
+ };
208
+ try {
209
+ const ctrl = new AbortController();
210
+ const to = setTimeout(() => ctrl.abort(), timeoutMs);
211
+ const res = await client.fetch(url, {
212
+ method: init.method ?? "GET",
213
+ headers,
214
+ body: hasBody ? JSON.stringify(init.body) : void 0,
215
+ signal: ctrl.signal
216
+ });
217
+ clearTimeout(to);
218
+ const text = await res.text();
219
+ if (detectCfChallenge(text)) {
220
+ return { status: res.status, data: null, challenged: true };
221
+ }
222
+ let data = text;
223
+ if (text) {
224
+ try {
225
+ data = JSON.parse(text);
226
+ } catch {
227
+ }
228
+ }
229
+ return { status: res.status, data };
230
+ } catch {
231
+ return null;
232
+ }
233
+ }
234
+ async function tierImpit(cookies, log, timeoutMs) {
235
+ const started = Date.now();
236
+ const impitMod = await loadImpit();
237
+ if (!impitMod) {
238
+ return { ok: false, error: "impit not installed (optional speed boost)", elapsedMs: Date.now() - started };
239
+ }
240
+ let client;
241
+ try {
242
+ client = new impitMod.Impit({ browser: "chrome", ignoreTlsErrors: false });
243
+ } catch (err) {
244
+ return { ok: false, error: `impit init failed: ${err.message}`, elapsedMs: Date.now() - started };
245
+ }
246
+ const cookieHeader = buildCookieHeader(cookies);
247
+ const headers = {
248
+ cookie: cookieHeader,
249
+ "user-agent": USER_AGENT,
250
+ ...CHROME_CLIENT_HINTS
251
+ };
252
+ const fetchJson = async (url, name) => {
253
+ try {
254
+ const ctrl = new AbortController();
255
+ const to = setTimeout(() => ctrl.abort(), timeoutMs);
256
+ const res = await client.fetch(url, { headers, signal: ctrl.signal });
257
+ clearTimeout(to);
258
+ const body = await res.text();
259
+ if (res.status !== 200) {
260
+ log(`impit: ${name} status=${res.status}`);
261
+ if (detectCfChallenge(body)) return { __challenged: true };
262
+ return null;
263
+ }
264
+ const rhdrs = res.headers;
265
+ const ct = typeof rhdrs?.get === "function" ? rhdrs.get("content-type") ?? "" : rhdrs?.["content-type"] ?? "";
266
+ if (!String(ct).includes("application/json")) {
267
+ log(`impit: ${name} non-JSON content-type=${ct}`);
268
+ if (detectCfChallenge(body)) return { __challenged: true };
269
+ return null;
270
+ }
271
+ return JSON.parse(body);
272
+ } catch (err) {
273
+ log(`impit: ${name} threw ${err.message}`);
274
+ return null;
275
+ }
276
+ };
277
+ const [models, asi, rateLimits, experiments, userInfo] = await Promise.all([
278
+ fetchJson(MODELS_CONFIG_ENDPOINT, "models/config"),
279
+ fetchJson(ASI_ACCESS_ENDPOINT, "asi-access"),
280
+ fetchJson(RATE_LIMIT_ENDPOINT, "rate-limit"),
281
+ fetchJson(EXPERIMENTS_ENDPOINT, "experiments"),
282
+ fetchJson(USER_INFO_ENDPOINT, "user/info")
283
+ ]);
284
+ const challenged = !!(models && models.__challenged);
285
+ if (challenged) {
286
+ return { ok: false, challenged: true, error: "CF challenge via impit", elapsedMs: Date.now() - started };
287
+ }
288
+ if (!models) {
289
+ return { ok: false, error: "impit: models/config fetch failed", elapsedMs: Date.now() - started };
290
+ }
291
+ return {
292
+ ok: true,
293
+ payload: {
294
+ models,
295
+ asi: asi && !asi.__challenged ? asi : null,
296
+ rateLimits: rateLimits && !rateLimits.__challenged ? rateLimits : null,
297
+ experiments: experiments && !experiments.__challenged ? experiments : null,
298
+ userInfo: userInfo && !userInfo.__challenged ? userInfo : null
299
+ },
300
+ elapsedMs: Date.now() - started
301
+ };
302
+ }
303
+ async function tierBrowser(cookies, log, timeoutMs) {
304
+ const started = Date.now();
305
+ try {
306
+ await resolveBrowserExecutable();
307
+ } catch (err) {
308
+ return { ok: false, error: err.message, elapsedMs: Date.now() - started };
309
+ }
310
+ let chromium;
311
+ try {
312
+ chromium = (await import("patchright")).chromium;
313
+ } catch (err) {
314
+ return { ok: false, error: `patchright missing: ${err.message}`, elapsedMs: Date.now() - started };
315
+ }
316
+ const probe = findBrowser();
317
+ const label = probe ? `${probe.channel}${probe.path.toLowerCase().includes("brave") ? " (brave)" : ""}` : "bundled";
318
+ log(`browser: launching headless ${label}`);
319
+ const browser = await chromium.launch({
320
+ headless: true,
321
+ args: STEALTH_ARGS,
322
+ ...probe ? { executablePath: probe.path } : {},
323
+ ...probe && ["chrome", "msedge", "chromium"].includes(probe.channel) ? { channel: probe.channel } : {},
324
+ ignoreDefaultArgs: ["--enable-automation"]
325
+ });
326
+ try {
327
+ const context = await getOrCreateContext(browser, {
328
+ viewport: { width: 1920, height: 1080 },
329
+ userAgent: USER_AGENT
330
+ });
331
+ await context.addCookies(cookies);
332
+ const page = await context.newPage();
333
+ const navResp = await page.goto(PERPLEXITY_URL, { waitUntil: "domcontentloaded", timeout: timeoutMs }).catch((e) => {
334
+ log(`browser: goto failed ${e.message}`);
335
+ return null;
336
+ });
337
+ const status = navResp?.status() ?? 0;
338
+ const title = await page.title().catch(() => "");
339
+ log(`browser: navigation status=${status}, title="${title}"`);
340
+ if (title.includes("Just a moment") || title.toLowerCase().includes("cloudflare")) {
341
+ return { ok: false, challenged: true, error: "CF challenge via browser \u2014 cookies expired", elapsedMs: Date.now() - started };
342
+ }
343
+ const fetchJson = async (url, name) => {
344
+ try {
345
+ const result = await page.evaluate(async (u) => {
346
+ try {
347
+ const r = await fetch(u, { credentials: "include" });
348
+ const ct = r.headers.get("content-type") ?? "";
349
+ if (!r.ok || !ct.includes("application/json")) {
350
+ return { ok: false, status: r.status, contentType: ct };
351
+ }
352
+ return { ok: true, body: await r.json() };
353
+ } catch (e) {
354
+ return { ok: false, error: e?.message ?? String(e) };
355
+ }
356
+ }, url);
357
+ if (!result?.ok) {
358
+ log(`browser: ${name} non-OK status=${result?.status ?? "n/a"} ct=${result?.contentType ?? "n/a"} err=${result?.error ?? "n/a"}`);
359
+ return null;
360
+ }
361
+ return result.body;
362
+ } catch (err) {
363
+ log(`browser: ${name} evaluate threw: ${err.message}`);
364
+ return null;
365
+ }
366
+ };
367
+ const [models, asi, rateLimits, experiments, userInfo] = await Promise.all([
368
+ fetchJson(MODELS_CONFIG_ENDPOINT, "models/config"),
369
+ fetchJson(ASI_ACCESS_ENDPOINT, "asi-access"),
370
+ fetchJson(RATE_LIMIT_ENDPOINT, "rate-limit"),
371
+ fetchJson(EXPERIMENTS_ENDPOINT, "experiments"),
372
+ fetchJson(USER_INFO_ENDPOINT, "user/info")
373
+ ]);
374
+ if (!models) {
375
+ return { ok: false, error: "browser: models/config fetch failed inside page", elapsedMs: Date.now() - started };
376
+ }
377
+ log(
378
+ `browser: fetched \u2014 models=${Object.keys(models.models || {}).length}, asi=${!!asi}, rateLimits=${!!rateLimits}, experiments=${experiments ? Object.keys(experiments).length + " keys" : "null"}, userInfo=${userInfo ? JSON.stringify(userInfo) : "null"}`
379
+ );
380
+ const freshCookies = await context.cookies(PERPLEXITY_URL).then(
381
+ (arr) => arr.map((c) => ({
382
+ name: c.name,
383
+ value: c.value,
384
+ domain: c.domain,
385
+ path: c.path,
386
+ secure: c.secure,
387
+ httpOnly: c.httpOnly,
388
+ sameSite: c.sameSite
389
+ }))
390
+ ).catch(() => void 0);
391
+ return {
392
+ ok: true,
393
+ payload: { models, asi, rateLimits, experiments, userInfo, freshCookies },
394
+ elapsedMs: Date.now() - started
395
+ };
396
+ } finally {
397
+ await browser.close().catch(() => void 0);
398
+ }
399
+ }
400
+ async function refreshAccountInfo(opts = {}) {
401
+ const log = opts.log ?? noopLog;
402
+ const started = Date.now();
403
+ const timeoutMs = opts.timeoutMs ?? 25e3;
404
+ const modelsCacheFile = getModelsCacheFile();
405
+ const legacyCookiesFile = getLegacyCookiesFile();
406
+ const savedCookies = await getSavedCookies();
407
+ if (savedCookies.length === 0) {
408
+ return {
409
+ ok: false,
410
+ source: "no-cookies",
411
+ tier: null,
412
+ modelCount: 0,
413
+ accountTier: "Unknown",
414
+ error: "No saved cookies \u2014 run Perplexity: Login first.",
415
+ cachePath: modelsCacheFile,
416
+ elapsedMs: Date.now() - started
417
+ };
418
+ }
419
+ const attempts = [];
420
+ const tierMap = {
421
+ "got-scraping": tierGotScraping,
422
+ impit: tierImpit,
423
+ browser: tierBrowser
424
+ };
425
+ const pipeline = opts.forceTier ? [[opts.forceTier, tierMap[opts.forceTier]]] : (() => {
426
+ const chain = [["got-scraping", tierGotScraping]];
427
+ if (isImpitAvailable()) chain.push(["impit", tierImpit]);
428
+ chain.push(["browser", tierBrowser]);
429
+ return chain;
430
+ })();
431
+ let successful = null;
432
+ let lastChallenged = false;
433
+ for (const [name, fn] of pipeline) {
434
+ log(`refresh: attempting tier=${name}`);
435
+ const result2 = await fn(savedCookies, log, timeoutMs);
436
+ attempts.push({ tier: name, ok: result2.ok, elapsedMs: result2.elapsedMs, error: result2.error });
437
+ if (result2.ok) {
438
+ successful = { tier: name, result: result2 };
439
+ log(`refresh: tier=${name} succeeded in ${result2.elapsedMs}ms`);
440
+ break;
441
+ }
442
+ lastChallenged = lastChallenged || !!result2.challenged;
443
+ log(`refresh: tier=${name} failed in ${result2.elapsedMs}ms (${result2.error ?? "unknown"})`);
444
+ }
445
+ if (!successful) {
446
+ return {
447
+ ok: false,
448
+ source: lastChallenged ? "cf-challenge" : "failed",
449
+ tier: null,
450
+ modelCount: 0,
451
+ accountTier: "Unknown",
452
+ error: lastChallenged ? "All tiers hit Cloudflare challenge. Run Perplexity: Login to re-solve Turnstile." : "All refresh tiers failed. See logs.",
453
+ cachePath: modelsCacheFile,
454
+ elapsedMs: Date.now() - started,
455
+ tierAttempts: attempts
456
+ };
457
+ }
458
+ const { tier, result } = successful;
459
+ const payload = result.payload;
460
+ const existing = readExistingCache(modelsCacheFile);
461
+ const isEnterpriseFromUser = payload.userInfo?.is_enterprise === true;
462
+ const isEnterpriseFromExp = payload.experiments?.server_is_enterprise === true;
463
+ const isProFromExp = payload.experiments?.server_is_pro === true;
464
+ const isMaxFromExp = payload.experiments?.server_is_max === true;
465
+ const canUseComputer = payload.asi?.can_use_computer ?? existing?.canUseComputer ?? false;
466
+ const info = {
467
+ // If Computer mode is accessible but no experiments flag fires, infer Pro —
468
+ // Computer is gated to paid tiers, so the account is at least Pro.
469
+ isPro: isProFromExp || canUseComputer && !isMaxFromExp && !isEnterpriseFromUser && !isEnterpriseFromExp,
470
+ isMax: isMaxFromExp,
471
+ isEnterprise: isEnterpriseFromUser || isEnterpriseFromExp,
472
+ canUseComputer,
473
+ modelsConfig: payload.models,
474
+ rateLimits: payload.rateLimits ?? existing?.rateLimits ?? null
475
+ };
476
+ mkdirSync(dirname(modelsCacheFile), { recursive: true });
477
+ writeFileSync(modelsCacheFile, JSON.stringify(info, null, 2));
478
+ log(`refresh: wrote ${modelsCacheFile} (${Object.keys(payload.models.models || {}).length} models)`);
479
+ if (payload.freshCookies && payload.freshCookies.length > 0) {
480
+ try {
481
+ mkdirSync(dirname(legacyCookiesFile), { recursive: true });
482
+ writeFileSync(
483
+ legacyCookiesFile,
484
+ JSON.stringify({ allCookies: payload.freshCookies, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
485
+ );
486
+ log(`refresh: persisted ${payload.freshCookies.length} fresh cookies to ${legacyCookiesFile}`);
487
+ } catch (err) {
488
+ log(`refresh: could not persist fresh cookies (non-fatal): ${err.message}`);
489
+ }
490
+ }
491
+ return {
492
+ ok: true,
493
+ source: "live",
494
+ tier,
495
+ modelCount: Object.keys(payload.models.models || {}).length,
496
+ accountTier: deriveTier(info),
497
+ cachePath: modelsCacheFile,
498
+ elapsedMs: Date.now() - started,
499
+ tierAttempts: attempts
500
+ };
501
+ }
502
+ function deriveTier(info) {
503
+ if (info.isMax) return "Max";
504
+ if (info.isEnterprise) return "Enterprise";
505
+ if (info.isPro) return "Pro";
506
+ return "Free";
507
+ }
508
+ function readExistingCache(modelsCacheFile = getModelsCacheFile()) {
509
+ if (!existsSync(modelsCacheFile)) return null;
510
+ try {
511
+ return JSON.parse(readFileSync(modelsCacheFile, "utf8"));
512
+ } catch {
513
+ return null;
514
+ }
515
+ }
516
+ function getModelsCacheInfo() {
517
+ const path = getModelsCacheFile();
518
+ const exists = existsSync(path);
519
+ const mtime = exists ? statSync(path).mtime : null;
520
+ const ageHours = mtime ? (Date.now() - mtime.getTime()) / 36e5 : null;
521
+ return { path, exists, mtime, ageHours };
522
+ }
523
+ function getImpitRuntimeDir() {
524
+ return getImpitRuntimeDirPath();
525
+ }
526
+
527
+ export {
528
+ loadImpit,
529
+ isImpitAvailable,
530
+ impitFetchJson,
531
+ refreshAccountInfo,
532
+ getModelsCacheInfo,
533
+ getImpitRuntimeDir
534
+ };