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.
- package/README.md +192 -0
- package/dist/attachments.d.ts +20 -0
- package/dist/attachments.mjs +43 -0
- package/dist/checks/browser.d.ts +100 -0
- package/dist/checks/browser.mjs +89 -0
- package/dist/checks/config.d.ts +91 -0
- package/dist/checks/config.mjs +88 -0
- package/dist/checks/ide.d.ts +89 -0
- package/dist/checks/ide.mjs +80 -0
- package/dist/checks/mcp.d.ts +61 -0
- package/dist/checks/mcp.mjs +56 -0
- package/dist/checks/native-deps.d.ts +131 -0
- package/dist/checks/native-deps.mjs +115 -0
- package/dist/checks/network.d.ts +71 -0
- package/dist/checks/network.mjs +70 -0
- package/dist/checks/probe.d.ts +93 -0
- package/dist/checks/probe.mjs +82 -0
- package/dist/checks/profiles.d.ts +99 -0
- package/dist/checks/profiles.mjs +90 -0
- package/dist/checks/runtime.d.ts +89 -0
- package/dist/checks/runtime.mjs +90 -0
- package/dist/checks/vault.d.ts +101 -0
- package/dist/checks/vault.mjs +90 -0
- package/dist/chunk-3B276PGG.mjs +115 -0
- package/dist/chunk-4UEJOM6W.mjs +9 -0
- package/dist/chunk-6EP2BLTV.mjs +205 -0
- package/dist/chunk-6YMQVLFX.mjs +146 -0
- package/dist/chunk-7JL36EBH.mjs +118 -0
- package/dist/chunk-DPGMKSSA.mjs +57 -0
- package/dist/chunk-H4BUAPPO.mjs +1950 -0
- package/dist/chunk-HMKLWVXB.mjs +109 -0
- package/dist/chunk-HTUAQRKH.mjs +125 -0
- package/dist/chunk-HU5B4FXS.mjs +139 -0
- package/dist/chunk-KCXM2M4B.mjs +1006 -0
- package/dist/chunk-LKJMLGFP.mjs +237 -0
- package/dist/chunk-LZPLNZ5U.mjs +67 -0
- package/dist/chunk-MTDFKNXX.mjs +19 -0
- package/dist/chunk-OF4DMAPJ.mjs +511 -0
- package/dist/chunk-PE23RMXY.mjs +43 -0
- package/dist/chunk-Q2VY4R5F.mjs +175 -0
- package/dist/chunk-S5VD7WTU.mjs +2540 -0
- package/dist/chunk-SVPRB62V.mjs +106 -0
- package/dist/chunk-TQLCLE4L.mjs +345 -0
- package/dist/chunk-U3DGFLXZ.mjs +43 -0
- package/dist/chunk-X45O6YD3.mjs +688 -0
- package/dist/chunk-XKSWCEGI.mjs +168 -0
- package/dist/chunk-Z7DAACGZ.mjs +534 -0
- package/dist/chunk-ZQFUZPLO.mjs +257 -0
- package/dist/cli.d.ts +952 -0
- package/dist/cli.mjs +827 -0
- package/dist/client.d.ts +355 -0
- package/dist/client.mjs +27 -0
- package/dist/cloud-sync.d-Cqt6y18U.d.ts +42 -0
- package/dist/cloud-sync.d.ts +42 -0
- package/dist/cloud-sync.mjs +17 -0
- package/dist/config.d.ts +186 -0
- package/dist/config.mjs +54 -0
- package/dist/daemon/attach.d.ts +36 -0
- package/dist/daemon/attach.mjs +25 -0
- package/dist/daemon/audit.d.ts +23 -0
- package/dist/daemon/audit.mjs +12 -0
- package/dist/daemon/client-http.d.ts +42 -0
- package/dist/daemon/client-http.mjs +29 -0
- package/dist/daemon/index.d.ts +14 -0
- package/dist/daemon/index.mjs +110 -0
- package/dist/daemon/install-tunnel.d.ts +46 -0
- package/dist/daemon/install-tunnel.mjs +14 -0
- package/dist/daemon/launcher.d.ts +163 -0
- package/dist/daemon/launcher.mjs +50 -0
- package/dist/daemon/lockfile.d.ts +29 -0
- package/dist/daemon/lockfile.mjs +18 -0
- package/dist/daemon/server.d.ts +159 -0
- package/dist/daemon/server.mjs +20 -0
- package/dist/daemon/token.d.ts +17 -0
- package/dist/daemon/token.mjs +17 -0
- package/dist/daemon/tunnel-providers/index.d.ts +330 -0
- package/dist/daemon/tunnel-providers/index.mjs +57 -0
- package/dist/daemon/tunnel.d.ts +23 -0
- package/dist/daemon/tunnel.mjs +9 -0
- package/dist/doctor-report.d.ts +24 -0
- package/dist/doctor-report.mjs +14 -0
- package/dist/doctor.d-CXmUqOXX.d.ts +43 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.mjs +16 -0
- package/dist/export.d.ts +19 -0
- package/dist/export.mjs +15 -0
- package/dist/health-check.d.ts +108 -0
- package/dist/health-check.mjs +92 -0
- package/dist/history-store.d-BzjBF2m3.d.ts +65 -0
- package/dist/history-store.d.ts +65 -0
- package/dist/history-store.mjs +48 -0
- package/dist/impit-login-runner.d.ts +469 -0
- package/dist/impit-login-runner.mjs +685 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.mjs +236 -0
- package/dist/login-runner.d.ts +333 -0
- package/dist/login-runner.mjs +320 -0
- package/dist/logout.d.ts +28 -0
- package/dist/logout.mjs +45 -0
- package/dist/manual-login-runner.d.ts +150 -0
- package/dist/manual-login-runner.mjs +146 -0
- package/dist/native-deps-BNThFHxa.d.ts +175 -0
- package/dist/native-deps-YNKXITRY.mjs +139 -0
- package/dist/profiles.d-DqS1oZWr.d.ts +41 -0
- package/dist/profiles.d.ts +41 -0
- package/dist/profiles.mjs +33 -0
- package/dist/redact.d.ts +159 -0
- package/dist/redact.mjs +11 -0
- package/dist/refresh.d.ts +118 -0
- package/dist/refresh.mjs +21 -0
- package/dist/reinit-watcher.d.ts +15 -0
- package/dist/reinit-watcher.mjs +8 -0
- package/dist/session-metadata-B9aV_n5g.d.ts +148 -0
- package/dist/tty-prompt.d.ts +44 -0
- package/dist/tty-prompt.mjs +39 -0
- package/dist/vault.d-BtRSLZiM.d.ts +8 -0
- package/dist/vault.d.ts +37 -0
- package/dist/vault.mjs +21 -0
- package/dist/viewer-detect.d-HWGnyFAA.d.ts +4 -0
- package/dist/viewer-detect.d.ts +4 -0
- package/dist/viewer-detect.mjs +37 -0
- package/dist/viewers.d-BGCK6sw6.d.ts +10 -0
- package/dist/viewers.d.ts +18 -0
- package/dist/viewers.mjs +122 -0
- package/package.json +152 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureDaemon
|
|
3
|
+
} from "./chunk-X45O6YD3.mjs";
|
|
4
|
+
|
|
5
|
+
// src/daemon/attach.ts
|
|
6
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
async function attachToDaemon(options = {}) {
|
|
9
|
+
const ensure = options.dependencies?.ensureDaemon ?? ensureDaemon;
|
|
10
|
+
const sourceIn = options.stdin ?? process.stdin;
|
|
11
|
+
const sourceOut = options.stdout ?? process.stdout;
|
|
12
|
+
let daemon;
|
|
13
|
+
try {
|
|
14
|
+
daemon = await ensure({
|
|
15
|
+
configDir: options.configDir,
|
|
16
|
+
startTimeoutMs: options.ensureTimeoutMs ?? 15e3
|
|
17
|
+
});
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (options.fallbackStdio) {
|
|
20
|
+
await runFallback(error, options);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
const stdio = new StdioServerTransport(sourceIn, sourceOut);
|
|
26
|
+
const http = new StreamableHTTPClientTransport(new URL(`${daemon.url}/mcp`), {
|
|
27
|
+
requestInit: {
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${daemon.bearerToken}`,
|
|
30
|
+
"x-perplexity-client-id": options.clientId ?? `daemon-attach-${process.pid}`,
|
|
31
|
+
"x-perplexity-source": "loopback"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const completion = new Promise((resolve, reject) => {
|
|
36
|
+
let settled = false;
|
|
37
|
+
const handleInputClosed = () => settle();
|
|
38
|
+
const settle = (error) => {
|
|
39
|
+
if (settled) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
settled = true;
|
|
43
|
+
sourceIn.off("end", handleInputClosed);
|
|
44
|
+
sourceIn.off("close", handleInputClosed);
|
|
45
|
+
void Promise.all([
|
|
46
|
+
http.close().catch(() => void 0),
|
|
47
|
+
stdio.close().catch(() => void 0)
|
|
48
|
+
]).finally(() => {
|
|
49
|
+
if (error) {
|
|
50
|
+
reject(error);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
stdio.onmessage = (message) => {
|
|
57
|
+
void http.send(message).catch((error) => settle(asError(error)));
|
|
58
|
+
};
|
|
59
|
+
stdio.onclose = () => settle();
|
|
60
|
+
stdio.onerror = (error) => settle(error);
|
|
61
|
+
http.onmessage = (message) => {
|
|
62
|
+
void stdio.send(message).catch((error) => settle(asError(error)));
|
|
63
|
+
};
|
|
64
|
+
http.onclose = () => settle();
|
|
65
|
+
http.onerror = (error) => settle(error);
|
|
66
|
+
sourceIn.on("end", handleInputClosed);
|
|
67
|
+
sourceIn.on("close", handleInputClosed);
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
await Promise.all([stdio.start(), http.start()]);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
await Promise.all([
|
|
73
|
+
stdio.close().catch(() => void 0),
|
|
74
|
+
http.close().catch(() => void 0)
|
|
75
|
+
]);
|
|
76
|
+
if (options.fallbackStdio) {
|
|
77
|
+
await runFallback(error, options);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
await completion;
|
|
83
|
+
}
|
|
84
|
+
async function runFallback(error, options) {
|
|
85
|
+
const reason = truncate(asError(error).message, 120);
|
|
86
|
+
process.stderr.write(
|
|
87
|
+
`[perplexity-mcp] daemon unreachable (${reason}); falling back to in-process stdio
|
|
88
|
+
`
|
|
89
|
+
);
|
|
90
|
+
const runStdioMain = options.dependencies?.runStdioMain ?? (async () => {
|
|
91
|
+
const mod = await import("./index.mjs");
|
|
92
|
+
await mod.main();
|
|
93
|
+
});
|
|
94
|
+
await runStdioMain();
|
|
95
|
+
}
|
|
96
|
+
function truncate(value, max) {
|
|
97
|
+
if (value.length <= max) return value;
|
|
98
|
+
return value.slice(0, max);
|
|
99
|
+
}
|
|
100
|
+
function asError(error) {
|
|
101
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export {
|
|
105
|
+
attachToDaemon
|
|
106
|
+
};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import {
|
|
2
|
+
safeAtomicWriteFileSync
|
|
3
|
+
} from "./chunk-MTDFKNXX.mjs";
|
|
4
|
+
import {
|
|
5
|
+
getProfilePaths
|
|
6
|
+
} from "./chunk-XKSWCEGI.mjs";
|
|
7
|
+
|
|
8
|
+
// src/vault.js
|
|
9
|
+
import { createCipheriv, createDecipheriv, randomBytes, hkdfSync, scrypt as nodeScrypt } from "crypto";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
import { existsSync, readFileSync, mkdirSync, rmSync } from "fs";
|
|
12
|
+
import { dirname } from "path";
|
|
13
|
+
var MAGIC = Buffer.from("PXVT");
|
|
14
|
+
var VERSION_V1 = 1;
|
|
15
|
+
var VERSION_V2 = 2;
|
|
16
|
+
var VERSION_V3 = 3;
|
|
17
|
+
var IV_LEN = 12;
|
|
18
|
+
var AUTHTAG_LEN = 16;
|
|
19
|
+
var SALT_LEN = 16;
|
|
20
|
+
var KDF_ID_SCRYPT = 1;
|
|
21
|
+
var SCRYPT_LOGN_DEFAULT = 17;
|
|
22
|
+
var SCRYPT_R_DEFAULT = 8;
|
|
23
|
+
var SCRYPT_P_DEFAULT = 1;
|
|
24
|
+
var SCRYPT_LOGN_FLOOR = 16;
|
|
25
|
+
var SCRYPT_MAXMEM = 256 * 1024 * 1024;
|
|
26
|
+
var SCRYPT_PARAMS_LEN = 3;
|
|
27
|
+
var LEGACY_STATIC_SALT = Buffer.from("perplexity-user-mcp:v1:salt");
|
|
28
|
+
var HKDF_INFO = Buffer.from("vault-master-key");
|
|
29
|
+
var V1_HEADER_LEN = 4 + 1 + IV_LEN;
|
|
30
|
+
var V2_HEADER_LEN = 4 + 1 + 1 + SALT_LEN + IV_LEN;
|
|
31
|
+
var V3_HEADER_FIXED_PREAMBLE = 4 + 1 + 1 + 1;
|
|
32
|
+
var scryptAsync = promisify(nodeScrypt);
|
|
33
|
+
var _kdfParamsOverride = null;
|
|
34
|
+
var _kdfTestModeActive = false;
|
|
35
|
+
function __setKdfParamsForTest(params) {
|
|
36
|
+
if (!params || typeof params.logN !== "number" || typeof params.r !== "number" || typeof params.p !== "number") {
|
|
37
|
+
throw new Error("__setKdfParamsForTest requires {logN, r, p} numbers.");
|
|
38
|
+
}
|
|
39
|
+
_kdfParamsOverride = { logN: params.logN, r: params.r, p: params.p };
|
|
40
|
+
_kdfTestModeActive = true;
|
|
41
|
+
}
|
|
42
|
+
function getActiveKdfParams() {
|
|
43
|
+
return _kdfParamsOverride ?? {
|
|
44
|
+
logN: SCRYPT_LOGN_DEFAULT,
|
|
45
|
+
r: SCRYPT_R_DEFAULT,
|
|
46
|
+
p: SCRYPT_P_DEFAULT
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function parseVaultHeader(blob) {
|
|
50
|
+
if (!Buffer.isBuffer(blob) || blob.length < 5) {
|
|
51
|
+
throw new Error(`Vault file too short / truncated (${blob ? blob.length : 0} bytes).`);
|
|
52
|
+
}
|
|
53
|
+
if (!blob.slice(0, 4).equals(MAGIC)) {
|
|
54
|
+
if (blob.length < V1_HEADER_LEN + AUTHTAG_LEN) {
|
|
55
|
+
throw new Error(`Vault file too short / truncated (${blob.length} bytes, no valid header).`);
|
|
56
|
+
}
|
|
57
|
+
throw new Error("Vault file has wrong magic header \u2014 not a Perplexity vault.");
|
|
58
|
+
}
|
|
59
|
+
const version = blob[4];
|
|
60
|
+
if (version === VERSION_V1) {
|
|
61
|
+
if (blob.length < V1_HEADER_LEN + AUTHTAG_LEN) {
|
|
62
|
+
throw new Error(`Vault file too short / truncated (${blob.length} bytes, v1).`);
|
|
63
|
+
}
|
|
64
|
+
const iv = blob.slice(5, 5 + IV_LEN);
|
|
65
|
+
const tag = blob.slice(blob.length - AUTHTAG_LEN);
|
|
66
|
+
const ct = blob.slice(5 + IV_LEN, blob.length - AUTHTAG_LEN);
|
|
67
|
+
return { version, kdfId: null, kdfParams: null, salt: null, iv, ct, tag };
|
|
68
|
+
}
|
|
69
|
+
if (version === VERSION_V2) {
|
|
70
|
+
if (blob.length < 6) {
|
|
71
|
+
throw new Error(`Vault file too short / truncated (${blob.length} bytes, v2 header).`);
|
|
72
|
+
}
|
|
73
|
+
const saltLen = blob[5];
|
|
74
|
+
if (saltLen !== SALT_LEN) {
|
|
75
|
+
throw new Error(`Vault has invalid salt length: ${saltLen} (expected ${SALT_LEN}). Possible corruption.`);
|
|
76
|
+
}
|
|
77
|
+
if (blob.length < V2_HEADER_LEN + AUTHTAG_LEN) {
|
|
78
|
+
throw new Error(`Vault file too short / truncated (${blob.length} bytes, v2).`);
|
|
79
|
+
}
|
|
80
|
+
const salt = blob.slice(6, 6 + SALT_LEN);
|
|
81
|
+
const iv = blob.slice(6 + SALT_LEN, 6 + SALT_LEN + IV_LEN);
|
|
82
|
+
const tag = blob.slice(blob.length - AUTHTAG_LEN);
|
|
83
|
+
const ct = blob.slice(V2_HEADER_LEN, blob.length - AUTHTAG_LEN);
|
|
84
|
+
return { version, kdfId: null, kdfParams: null, salt, iv, ct, tag };
|
|
85
|
+
}
|
|
86
|
+
if (version === VERSION_V3) {
|
|
87
|
+
if (blob.length < V3_HEADER_FIXED_PREAMBLE) {
|
|
88
|
+
throw new Error(`Vault file too short / truncated (${blob.length} bytes, v3 preamble).`);
|
|
89
|
+
}
|
|
90
|
+
const kdfId = blob[5];
|
|
91
|
+
const kdfParamsLen = blob[6];
|
|
92
|
+
if (kdfId === KDF_ID_SCRYPT) {
|
|
93
|
+
if (kdfParamsLen !== SCRYPT_PARAMS_LEN) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Vault has invalid KDF params length: ${kdfParamsLen} (expected ${SCRYPT_PARAMS_LEN} for scrypt). Possible corruption.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Vault uses unsupported KDF id: 0x${kdfId.toString(16).padStart(2, "0")}.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const kdfParamsStart = V3_HEADER_FIXED_PREAMBLE;
|
|
104
|
+
const kdfParamsEnd = kdfParamsStart + kdfParamsLen;
|
|
105
|
+
if (blob.length < kdfParamsEnd + 1) {
|
|
106
|
+
throw new Error(`Vault file too short / truncated (${blob.length} bytes, v3 KDF params).`);
|
|
107
|
+
}
|
|
108
|
+
const kdfParamsBytes = blob.slice(kdfParamsStart, kdfParamsEnd);
|
|
109
|
+
let kdfParams;
|
|
110
|
+
if (kdfId === KDF_ID_SCRYPT) {
|
|
111
|
+
kdfParams = {
|
|
112
|
+
logN: kdfParamsBytes[0],
|
|
113
|
+
r: kdfParamsBytes[1],
|
|
114
|
+
p: kdfParamsBytes[2]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const saltLenOffset = kdfParamsEnd;
|
|
118
|
+
const saltLen = blob[saltLenOffset];
|
|
119
|
+
if (saltLen !== SALT_LEN) {
|
|
120
|
+
throw new Error(`Vault has invalid salt length: ${saltLen} (expected ${SALT_LEN}). Possible corruption.`);
|
|
121
|
+
}
|
|
122
|
+
const saltStart = saltLenOffset + 1;
|
|
123
|
+
const ivStart = saltStart + SALT_LEN;
|
|
124
|
+
const ctStart = ivStart + IV_LEN;
|
|
125
|
+
const fullHeaderAndTag = ctStart + AUTHTAG_LEN;
|
|
126
|
+
if (blob.length < fullHeaderAndTag) {
|
|
127
|
+
throw new Error(`Vault file too short / truncated (${blob.length} bytes, v3).`);
|
|
128
|
+
}
|
|
129
|
+
const salt = blob.slice(saltStart, saltStart + SALT_LEN);
|
|
130
|
+
const iv = blob.slice(ivStart, ctStart);
|
|
131
|
+
const tag = blob.slice(blob.length - AUTHTAG_LEN);
|
|
132
|
+
const ct = blob.slice(ctStart, blob.length - AUTHTAG_LEN);
|
|
133
|
+
return { version, kdfId, kdfParams, salt, iv, ct, tag };
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`Vault uses unsupported version byte: ${version}. Upgrade required.`);
|
|
136
|
+
}
|
|
137
|
+
function hkdfFromPassphrase(passphrase, salt) {
|
|
138
|
+
return Buffer.from(hkdfSync("sha256", Buffer.from(passphrase, "utf8"), salt, HKDF_INFO, 32));
|
|
139
|
+
}
|
|
140
|
+
async function scryptDerive(passphrase, salt, params) {
|
|
141
|
+
const { logN, r, p } = params;
|
|
142
|
+
if (!_kdfTestModeActive && logN < SCRYPT_LOGN_FLOOR) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Vault scrypt parameters below security floor (logN=${logN} < ${SCRYPT_LOGN_FLOOR}). Refusing to derive.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (r < 1 || p < 1) {
|
|
148
|
+
throw new Error(`Vault scrypt parameters invalid (r=${r}, p=${p}).`);
|
|
149
|
+
}
|
|
150
|
+
const N = 1 << logN;
|
|
151
|
+
const key = await scryptAsync(Buffer.from(passphrase, "utf8"), salt, 32, {
|
|
152
|
+
N,
|
|
153
|
+
r,
|
|
154
|
+
p,
|
|
155
|
+
maxmem: SCRYPT_MAXMEM
|
|
156
|
+
});
|
|
157
|
+
return Buffer.from(key);
|
|
158
|
+
}
|
|
159
|
+
function encryptBlob(plaintext, key) {
|
|
160
|
+
if (!Buffer.isBuffer(key) || key.length !== 32) {
|
|
161
|
+
throw new Error("Vault key must be 32 bytes.");
|
|
162
|
+
}
|
|
163
|
+
const salt = randomBytes(SALT_LEN);
|
|
164
|
+
const iv = randomBytes(IV_LEN);
|
|
165
|
+
const params = getActiveKdfParams();
|
|
166
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
167
|
+
const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
168
|
+
const tag = cipher.getAuthTag();
|
|
169
|
+
return Buffer.concat([
|
|
170
|
+
MAGIC,
|
|
171
|
+
Buffer.from([VERSION_V3, KDF_ID_SCRYPT, SCRYPT_PARAMS_LEN, params.logN, params.r, params.p, SALT_LEN]),
|
|
172
|
+
salt,
|
|
173
|
+
iv,
|
|
174
|
+
ct,
|
|
175
|
+
tag
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
function decryptBlob(blob, key) {
|
|
179
|
+
if (!Buffer.isBuffer(key) || key.length !== 32) {
|
|
180
|
+
throw new Error("Vault key must be 32 bytes.");
|
|
181
|
+
}
|
|
182
|
+
const header = parseVaultHeader(blob);
|
|
183
|
+
return aesGcmOpen(header, key);
|
|
184
|
+
}
|
|
185
|
+
function aesGcmOpen({ iv, ct, tag }, key) {
|
|
186
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
187
|
+
decipher.setAuthTag(tag);
|
|
188
|
+
try {
|
|
189
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
190
|
+
} catch {
|
|
191
|
+
throw new Error(
|
|
192
|
+
"Vault decrypt failed: wrong passphrase or corrupted ciphertext. If you recently rotated PERPLEXITY_VAULT_PASSPHRASE or VS Code SecretStorage, restore the original passphrase."
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
var KEYTAR_SERVICE = "perplexity-user-mcp";
|
|
197
|
+
var KEYTAR_ACCOUNT = "vault-master-key";
|
|
198
|
+
var _keyCache = null;
|
|
199
|
+
var _unsealMaterialCache = null;
|
|
200
|
+
function __resetKeyCache() {
|
|
201
|
+
_keyCache = null;
|
|
202
|
+
_unsealMaterialCache = null;
|
|
203
|
+
_kdfParamsOverride = null;
|
|
204
|
+
_kdfTestModeActive = false;
|
|
205
|
+
}
|
|
206
|
+
async function tryKeytar() {
|
|
207
|
+
try {
|
|
208
|
+
const mod = await import("keytar");
|
|
209
|
+
return mod.default ?? mod;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function keyFromKeychain() {
|
|
215
|
+
const keytar = await tryKeytar();
|
|
216
|
+
if (!keytar) return null;
|
|
217
|
+
try {
|
|
218
|
+
let hex = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
219
|
+
if (!hex) {
|
|
220
|
+
const fresh = randomBytes(32);
|
|
221
|
+
hex = fresh.toString("hex");
|
|
222
|
+
await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT, hex);
|
|
223
|
+
}
|
|
224
|
+
const buf = Buffer.from(hex, "hex");
|
|
225
|
+
if (buf.length !== 32) return null;
|
|
226
|
+
return buf;
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function isStdioServerMode() {
|
|
232
|
+
return process.env.PERPLEXITY_MCP_STDIO === "1" || process.stdin && process.stdin.isTTY === false;
|
|
233
|
+
}
|
|
234
|
+
async function getUnsealMaterial() {
|
|
235
|
+
if (_unsealMaterialCache) return _unsealMaterialCache;
|
|
236
|
+
const fromKc = await keyFromKeychain();
|
|
237
|
+
if (fromKc) {
|
|
238
|
+
_unsealMaterialCache = { kind: "key", key: fromKc };
|
|
239
|
+
return _unsealMaterialCache;
|
|
240
|
+
}
|
|
241
|
+
const envPass = process.env.PERPLEXITY_VAULT_PASSPHRASE;
|
|
242
|
+
if (envPass) {
|
|
243
|
+
_unsealMaterialCache = { kind: "passphrase", passphrase: envPass };
|
|
244
|
+
return _unsealMaterialCache;
|
|
245
|
+
}
|
|
246
|
+
if (!isStdioServerMode() && process.stdin.isTTY) {
|
|
247
|
+
const { promptSecret } = await import("./tty-prompt.mjs");
|
|
248
|
+
const pass = await promptSecret({ prompt: "Perplexity vault passphrase: " });
|
|
249
|
+
if (pass) {
|
|
250
|
+
_unsealMaterialCache = { kind: "passphrase", passphrase: pass };
|
|
251
|
+
return _unsealMaterialCache;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
throw new Error(
|
|
255
|
+
"Vault locked: no keychain, no env var, no TTY. Three unseal paths on Linux/headless: (a) install an OS keychain (libsecret + gnome-keyring) so the MCP process can read it, (b) set PERPLEXITY_VAULT_PASSPHRASE in your IDE's MCP server env block, or (c) run the VS Code extension's daemon and connect over HTTP transport instead of stdio. Codex CLI setup: docs/codex-cli-setup.md. Generic vault-unseal docs: docs/vault-unseal.md."
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
async function getMasterKey() {
|
|
259
|
+
if (_keyCache) return _keyCache;
|
|
260
|
+
const unseal = await getUnsealMaterial();
|
|
261
|
+
if (unseal.kind === "key") {
|
|
262
|
+
_keyCache = unseal.key;
|
|
263
|
+
} else {
|
|
264
|
+
_keyCache = hkdfFromPassphrase(unseal.passphrase, LEGACY_STATIC_SALT);
|
|
265
|
+
}
|
|
266
|
+
return _keyCache;
|
|
267
|
+
}
|
|
268
|
+
async function deriveKeyForHeader(header, unseal) {
|
|
269
|
+
if (unseal.kind === "key") return unseal.key;
|
|
270
|
+
if (header.version === VERSION_V1) {
|
|
271
|
+
return hkdfFromPassphrase(unseal.passphrase, LEGACY_STATIC_SALT);
|
|
272
|
+
}
|
|
273
|
+
if (header.version === VERSION_V2) {
|
|
274
|
+
return hkdfFromPassphrase(unseal.passphrase, header.salt);
|
|
275
|
+
}
|
|
276
|
+
return scryptDerive(unseal.passphrase, header.salt, header.kdfParams);
|
|
277
|
+
}
|
|
278
|
+
async function readVaultObject(profileName) {
|
|
279
|
+
const p = getProfilePaths(profileName).vault;
|
|
280
|
+
if (!existsSync(p)) return {};
|
|
281
|
+
const blob = readFileSync(p);
|
|
282
|
+
const header = parseVaultHeader(blob);
|
|
283
|
+
const unseal = await getUnsealMaterial();
|
|
284
|
+
const key = await deriveKeyForHeader(header, unseal);
|
|
285
|
+
const plain = aesGcmOpen(header, key);
|
|
286
|
+
try {
|
|
287
|
+
return JSON.parse(plain.toString("utf8"));
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const { redact } = await import("./redact.mjs");
|
|
290
|
+
console.error(`[vault] Corrupt vault JSON for profile ${redact(profileName)}: ${redact(err.message)}`);
|
|
291
|
+
throw new Error(`Vault for profile '${profileName}' is corrupt or unreadable.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function writeVaultObject(profileName, obj) {
|
|
295
|
+
const paths = getProfilePaths(profileName);
|
|
296
|
+
if (!existsSync(paths.dir)) mkdirSync(paths.dir, { recursive: true });
|
|
297
|
+
const unseal = await getUnsealMaterial();
|
|
298
|
+
const salt = randomBytes(SALT_LEN);
|
|
299
|
+
const params = getActiveKdfParams();
|
|
300
|
+
const key = unseal.kind === "key" ? unseal.key : await scryptDerive(unseal.passphrase, salt, params);
|
|
301
|
+
const iv = randomBytes(IV_LEN);
|
|
302
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
303
|
+
const plaintext = Buffer.from(JSON.stringify(obj));
|
|
304
|
+
const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
305
|
+
const tag = cipher.getAuthTag();
|
|
306
|
+
const blob = Buffer.concat([
|
|
307
|
+
MAGIC,
|
|
308
|
+
Buffer.from([VERSION_V3, KDF_ID_SCRYPT, SCRYPT_PARAMS_LEN, params.logN, params.r, params.p, SALT_LEN]),
|
|
309
|
+
salt,
|
|
310
|
+
iv,
|
|
311
|
+
ct,
|
|
312
|
+
tag
|
|
313
|
+
]);
|
|
314
|
+
safeAtomicWriteFileSync(paths.vault, blob);
|
|
315
|
+
}
|
|
316
|
+
var Vault = class {
|
|
317
|
+
async get(profile, key) {
|
|
318
|
+
const obj = await readVaultObject(profile);
|
|
319
|
+
return obj[key] ?? null;
|
|
320
|
+
}
|
|
321
|
+
async set(profile, key, value) {
|
|
322
|
+
const obj = await readVaultObject(profile);
|
|
323
|
+
obj[key] = value;
|
|
324
|
+
await writeVaultObject(profile, obj);
|
|
325
|
+
}
|
|
326
|
+
async delete(profile, key) {
|
|
327
|
+
const obj = await readVaultObject(profile);
|
|
328
|
+
delete obj[key];
|
|
329
|
+
await writeVaultObject(profile, obj);
|
|
330
|
+
}
|
|
331
|
+
async deleteAll(profile) {
|
|
332
|
+
const p = getProfilePaths(profile).vault;
|
|
333
|
+
if (existsSync(p)) rmSync(p, { force: true });
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export {
|
|
338
|
+
__setKdfParamsForTest,
|
|
339
|
+
encryptBlob,
|
|
340
|
+
decryptBlob,
|
|
341
|
+
__resetKeyCache,
|
|
342
|
+
getUnsealMaterial,
|
|
343
|
+
getMasterKey,
|
|
344
|
+
Vault
|
|
345
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getProfilePaths
|
|
3
|
+
} from "./chunk-XKSWCEGI.mjs";
|
|
4
|
+
|
|
5
|
+
// src/reinit-watcher.js
|
|
6
|
+
import { existsSync, mkdirSync, watch } from "fs";
|
|
7
|
+
import { dirname } from "path";
|
|
8
|
+
function watchReinit(profileName, callback, opts = {}) {
|
|
9
|
+
const { debounceMs = 200 } = opts;
|
|
10
|
+
const target = getProfilePaths(profileName).reinit;
|
|
11
|
+
const parent = dirname(target);
|
|
12
|
+
if (!existsSync(parent)) mkdirSync(parent, { recursive: true });
|
|
13
|
+
let timer = null;
|
|
14
|
+
const w = watch(parent, { persistent: false }, (event, filename) => {
|
|
15
|
+
if (!filename) return;
|
|
16
|
+
if (!String(filename).endsWith(".reinit")) return;
|
|
17
|
+
if (!existsSync(target)) return;
|
|
18
|
+
if (timer) clearTimeout(timer);
|
|
19
|
+
timer = setTimeout(() => {
|
|
20
|
+
timer = null;
|
|
21
|
+
try {
|
|
22
|
+
callback();
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
}, debounceMs);
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
dispose() {
|
|
29
|
+
if (timer) {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
timer = null;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
w.close();
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
watchReinit
|
|
43
|
+
};
|