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,1006 @@
|
|
|
1
|
+
import {
|
|
2
|
+
startTunnel
|
|
3
|
+
} from "./chunk-6YMQVLFX.mjs";
|
|
4
|
+
import {
|
|
5
|
+
getTunnelBinaryPath
|
|
6
|
+
} from "./chunk-3B276PGG.mjs";
|
|
7
|
+
import {
|
|
8
|
+
safeAtomicWriteFileSync
|
|
9
|
+
} from "./chunk-MTDFKNXX.mjs";
|
|
10
|
+
|
|
11
|
+
// src/daemon/tunnel-providers/index.ts
|
|
12
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "fs";
|
|
13
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
14
|
+
|
|
15
|
+
// src/daemon/tunnel-providers/cloudflared-quick.ts
|
|
16
|
+
import { existsSync } from "fs";
|
|
17
|
+
var cloudflaredQuickProvider = {
|
|
18
|
+
id: "cf-quick",
|
|
19
|
+
displayName: "Cloudflare Quick Tunnel",
|
|
20
|
+
description: "Zero-setup ephemeral *.trycloudflare.com URL. Changes on every restart.",
|
|
21
|
+
async isSetupComplete(configDir) {
|
|
22
|
+
const binaryPath = getTunnelBinaryPath(configDir);
|
|
23
|
+
if (!existsSync(binaryPath)) {
|
|
24
|
+
return {
|
|
25
|
+
ready: false,
|
|
26
|
+
reason: "cloudflared binary not installed.",
|
|
27
|
+
action: { label: "Install cloudflared", kind: "install-binary" }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return { ready: true };
|
|
31
|
+
},
|
|
32
|
+
async start(options) {
|
|
33
|
+
const binaryPath = getTunnelBinaryPath(options.configDir);
|
|
34
|
+
if (!existsSync(binaryPath)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"cloudflared is not installed. Run `npx perplexity-user-mcp daemon install-tunnel` first."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return startTunnel({
|
|
40
|
+
command: binaryPath,
|
|
41
|
+
port: options.port,
|
|
42
|
+
onStateChange: options.onStateChange
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/daemon/tunnel-providers/cloudflared-named.ts
|
|
48
|
+
import { existsSync as existsSync3 } from "fs";
|
|
49
|
+
import { spawn as nodeSpawn2 } from "child_process";
|
|
50
|
+
import { createInterface } from "readline";
|
|
51
|
+
import { execFile as execFileCallback } from "child_process";
|
|
52
|
+
import { promisify } from "util";
|
|
53
|
+
import { homedir as homedir2 } from "os";
|
|
54
|
+
import { join as join2 } from "path";
|
|
55
|
+
|
|
56
|
+
// src/daemon/tunnel-providers/cloudflared-named-setup.ts
|
|
57
|
+
import {
|
|
58
|
+
chmodSync,
|
|
59
|
+
existsSync as existsSync2,
|
|
60
|
+
mkdirSync,
|
|
61
|
+
readFileSync,
|
|
62
|
+
rmSync
|
|
63
|
+
} from "fs";
|
|
64
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
65
|
+
import { dirname, join } from "path";
|
|
66
|
+
import { homedir } from "os";
|
|
67
|
+
import { spawnSync } from "child_process";
|
|
68
|
+
var DeleteNamedTunnelError = class extends Error {
|
|
69
|
+
constructor(message, reason) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.reason = reason;
|
|
72
|
+
this.name = "DeleteNamedTunnelError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var CONFIG_FILENAME = "cloudflared-named.yml";
|
|
76
|
+
var DEFAULT_LOGIN_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
77
|
+
var CERT_POLL_INTERVAL_MS = 250;
|
|
78
|
+
function runCloudflaredLogin(options) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const binaryPath = options.binaryPath ?? getTunnelBinaryPath(options.configDir);
|
|
81
|
+
try {
|
|
82
|
+
assertBinaryExists(binaryPath);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
reject(err);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const certPath = options.certPath ?? defaultCertPath();
|
|
88
|
+
if (existsSync2(certPath)) {
|
|
89
|
+
reject(
|
|
90
|
+
new Error(
|
|
91
|
+
`cert already exists at ${certPath}; rename or delete it to re-run login.`
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const spawnImpl = options.dependencies?.spawn ?? nodeSpawn;
|
|
97
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS;
|
|
98
|
+
const child = spawnImpl(binaryPath, ["tunnel", "login"], {
|
|
99
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
100
|
+
windowsHide: true
|
|
101
|
+
});
|
|
102
|
+
const stderrChunks = [];
|
|
103
|
+
child.stderr?.on("data", (chunk) => {
|
|
104
|
+
const text = chunk.toString("utf8");
|
|
105
|
+
stderrChunks.push(text);
|
|
106
|
+
if (options.forwardOutput) process.stderr.write(text);
|
|
107
|
+
});
|
|
108
|
+
child.stdout?.on("data", (chunk) => {
|
|
109
|
+
const text = chunk.toString("utf8");
|
|
110
|
+
stderrChunks.push(text);
|
|
111
|
+
if (options.forwardOutput) process.stderr.write(text);
|
|
112
|
+
});
|
|
113
|
+
let settled = false;
|
|
114
|
+
let pollTimer = null;
|
|
115
|
+
let overallTimer = null;
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
118
|
+
if (overallTimer) clearTimeout(overallTimer);
|
|
119
|
+
if (options.signal) options.signal.removeEventListener("abort", onAbort);
|
|
120
|
+
killChild(child);
|
|
121
|
+
};
|
|
122
|
+
const resolveOk = () => {
|
|
123
|
+
if (settled) return;
|
|
124
|
+
settled = true;
|
|
125
|
+
cleanup();
|
|
126
|
+
resolve({ ok: true, certPath, stderr: stderrChunks.join("") });
|
|
127
|
+
};
|
|
128
|
+
const rejectWith = (err) => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
settled = true;
|
|
131
|
+
cleanup();
|
|
132
|
+
reject(err);
|
|
133
|
+
};
|
|
134
|
+
const onAbort = () => {
|
|
135
|
+
rejectWith(new Error("cloudflared login aborted by caller."));
|
|
136
|
+
};
|
|
137
|
+
if (options.signal) {
|
|
138
|
+
if (options.signal.aborted) {
|
|
139
|
+
rejectWith(new Error("cloudflared login aborted by caller."));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
143
|
+
}
|
|
144
|
+
pollTimer = setInterval(() => {
|
|
145
|
+
try {
|
|
146
|
+
if (existsSync2(certPath)) resolveOk();
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}, CERT_POLL_INTERVAL_MS);
|
|
150
|
+
overallTimer = setTimeout(() => {
|
|
151
|
+
rejectWith(
|
|
152
|
+
new Error(
|
|
153
|
+
`cloudflared login timed out after ${Math.round(timeoutMs / 1e3)}s \u2014 cert not written to ${certPath}.`
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
}, timeoutMs);
|
|
157
|
+
child.on("error", (err) => {
|
|
158
|
+
rejectWith(new Error(`cloudflared login failed to start: ${err.message}`));
|
|
159
|
+
});
|
|
160
|
+
child.on("exit", () => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
if (existsSync2(certPath)) {
|
|
163
|
+
resolveOk();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function listNamedTunnels(options) {
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
const binaryPath = options.binaryPath ?? getTunnelBinaryPath(options.configDir);
|
|
171
|
+
try {
|
|
172
|
+
assertBinaryExists(binaryPath);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
reject(err);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const spawnImpl = options.dependencies?.spawn ?? nodeSpawn;
|
|
178
|
+
const child = spawnImpl(binaryPath, ["tunnel", "list", "--output=json"], {
|
|
179
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
180
|
+
windowsHide: true
|
|
181
|
+
});
|
|
182
|
+
const stdoutChunks = [];
|
|
183
|
+
const stderrChunks = [];
|
|
184
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk.toString("utf8")));
|
|
185
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(chunk.toString("utf8")));
|
|
186
|
+
child.on("error", (err) => {
|
|
187
|
+
reject(new Error(`cloudflared list failed to start: ${err.message}`));
|
|
188
|
+
});
|
|
189
|
+
child.on("exit", (code) => {
|
|
190
|
+
const stdout = stdoutChunks.join("");
|
|
191
|
+
const stderr = stderrChunks.join("");
|
|
192
|
+
if (code !== 0) {
|
|
193
|
+
reject(new Error(`cloudflared tunnel list exited with code ${code}: ${stderr.trim() || stdout.trim()}`));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const trimmed = stdout.trim();
|
|
197
|
+
if (trimmed === "" || trimmed === "null") {
|
|
198
|
+
resolve([]);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let parsed;
|
|
202
|
+
try {
|
|
203
|
+
parsed = JSON.parse(trimmed);
|
|
204
|
+
} catch {
|
|
205
|
+
reject(
|
|
206
|
+
new Error(
|
|
207
|
+
`cloudflared output not parseable: ${trimmed.slice(0, 200)}`
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!Array.isArray(parsed)) {
|
|
213
|
+
resolve([]);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const summaries = parsed.filter((entry) => !!entry && typeof entry === "object").map((entry) => {
|
|
217
|
+
const id = typeof entry.id === "string" ? entry.id : "";
|
|
218
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
219
|
+
const createdAt = typeof entry.created_at === "string" ? entry.created_at : void 0;
|
|
220
|
+
const connArr = Array.isArray(entry.connections) ? entry.connections : [];
|
|
221
|
+
return { uuid: id, name, createdAt, connections: connArr.length };
|
|
222
|
+
}).filter((entry) => entry.uuid.length > 0);
|
|
223
|
+
resolve(summaries);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
var CREATE_ID_REGEX = /Created tunnel\s+(\S+)\s+with id\s+([0-9a-f-]{8,})/i;
|
|
228
|
+
var CREDENTIALS_REGEX = /Tunnel credentials written to\s+(.+?\.json)/i;
|
|
229
|
+
async function createNamedTunnel(options) {
|
|
230
|
+
if (!options.name) throw new Error("createNamedTunnel: name is required.");
|
|
231
|
+
if (!options.hostname) throw new Error("createNamedTunnel: hostname is required.");
|
|
232
|
+
const binaryPath = options.binaryPath ?? getTunnelBinaryPath(options.configDir);
|
|
233
|
+
assertBinaryExists(binaryPath);
|
|
234
|
+
const spawnImpl = options.dependencies?.spawn ?? nodeSpawn;
|
|
235
|
+
return runCapture(spawnImpl, binaryPath, ["tunnel", "create", options.name], options.signal).then(({ code, stdout, stderr }) => {
|
|
236
|
+
if (code !== 0) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`cloudflared tunnel create exited with code ${code}: ${stderr.trim() || stdout.trim()}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const idMatch = stdout.match(CREATE_ID_REGEX) ?? stderr.match(CREATE_ID_REGEX);
|
|
242
|
+
if (!idMatch) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`cloudflared create output missing tunnel id line. Output: ${(stdout + stderr).slice(0, 400)}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const credMatch = stdout.match(CREDENTIALS_REGEX) ?? stderr.match(CREDENTIALS_REGEX);
|
|
248
|
+
if (!credMatch) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`cloudflared create output missing credentials path line. Output: ${(stdout + stderr).slice(0, 400)}`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const uuid = idMatch[2];
|
|
254
|
+
const credentialsPath = credMatch[1].trim();
|
|
255
|
+
const parsedName = idMatch[1];
|
|
256
|
+
return { uuid, name: parsedName || options.name, credentialsPath };
|
|
257
|
+
}).then(async (tunnel) => {
|
|
258
|
+
const { code, stdout, stderr } = await runCapture(
|
|
259
|
+
spawnImpl,
|
|
260
|
+
binaryPath,
|
|
261
|
+
["tunnel", "route", "dns", tunnel.uuid, options.hostname],
|
|
262
|
+
options.signal
|
|
263
|
+
);
|
|
264
|
+
if (code !== 0) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`cloudflared route dns exited with code ${code}: ${stderr.trim() || stdout.trim()}`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return tunnel;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async function deleteNamedTunnel(options) {
|
|
273
|
+
const uuid = options.uuid.trim();
|
|
274
|
+
if (!uuid) throw new Error("deleteNamedTunnel: uuid is required.");
|
|
275
|
+
const binaryPath = options.binaryPath ?? getTunnelBinaryPath(options.configDir);
|
|
276
|
+
assertBinaryExists(binaryPath);
|
|
277
|
+
const spawnImpl = options.dependencies?.spawn ?? nodeSpawn;
|
|
278
|
+
const { code, stdout, stderr } = await runCapture(
|
|
279
|
+
spawnImpl,
|
|
280
|
+
binaryPath,
|
|
281
|
+
["tunnel", "delete", "--force", uuid],
|
|
282
|
+
options.signal
|
|
283
|
+
);
|
|
284
|
+
if (code !== 0) {
|
|
285
|
+
const combined = `${stderr}
|
|
286
|
+
${stdout}`.trim();
|
|
287
|
+
if (isActiveConnectionDeleteFailure(combined)) {
|
|
288
|
+
throw new DeleteNamedTunnelError(
|
|
289
|
+
"cloudflared could not delete the tunnel because it still has active connections. Remove the DNS route/CNAME for this hostname, wait for connections to drain, then retry delete.",
|
|
290
|
+
"active-connections"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
throw new DeleteNamedTunnelError(
|
|
294
|
+
`cloudflared tunnel delete exited with code ${code}: ${combined}`,
|
|
295
|
+
"unknown"
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return { uuid };
|
|
299
|
+
}
|
|
300
|
+
function isActiveConnectionDeleteFailure(output) {
|
|
301
|
+
const normalized = output.toLowerCase();
|
|
302
|
+
return /active connection/.test(normalized) || /still has connections/.test(normalized) || /cannot.*delete.*connections/.test(normalized) || /unable.*delete.*connections/.test(normalized);
|
|
303
|
+
}
|
|
304
|
+
function runCapture(spawnImpl, command, args, signal) {
|
|
305
|
+
return new Promise((resolve, reject) => {
|
|
306
|
+
const child = spawnImpl(command, args, {
|
|
307
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
308
|
+
windowsHide: true
|
|
309
|
+
});
|
|
310
|
+
const stdout = [];
|
|
311
|
+
const stderr = [];
|
|
312
|
+
child.stdout?.on("data", (chunk) => stdout.push(chunk.toString("utf8")));
|
|
313
|
+
child.stderr?.on("data", (chunk) => stderr.push(chunk.toString("utf8")));
|
|
314
|
+
const onAbort = () => killChild(child);
|
|
315
|
+
if (signal) {
|
|
316
|
+
if (signal.aborted) {
|
|
317
|
+
killChild(child);
|
|
318
|
+
reject(new Error("cloudflared command aborted by caller."));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
322
|
+
}
|
|
323
|
+
child.on("error", (err) => {
|
|
324
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
325
|
+
reject(new Error(`cloudflared command failed to start: ${err.message}`));
|
|
326
|
+
});
|
|
327
|
+
child.on("exit", (code) => {
|
|
328
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
329
|
+
resolve({ code, stdout: stdout.join(""), stderr: stderr.join("") });
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function getNamedTunnelConfigPath(configDir) {
|
|
334
|
+
return join(configDir, CONFIG_FILENAME);
|
|
335
|
+
}
|
|
336
|
+
function writeTunnelConfig(options) {
|
|
337
|
+
if (!options.uuid) throw new Error("writeTunnelConfig: uuid is required.");
|
|
338
|
+
if (!options.hostname) throw new Error("writeTunnelConfig: hostname is required.");
|
|
339
|
+
if (!Number.isFinite(options.port) || options.port <= 0) {
|
|
340
|
+
throw new Error("writeTunnelConfig: port must be a positive number.");
|
|
341
|
+
}
|
|
342
|
+
if (!options.credentialsPath) {
|
|
343
|
+
throw new Error("writeTunnelConfig: credentialsPath is required.");
|
|
344
|
+
}
|
|
345
|
+
const configPath = getNamedTunnelConfigPath(options.configDir);
|
|
346
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
347
|
+
const yaml = serializeConfigYaml({
|
|
348
|
+
uuid: options.uuid,
|
|
349
|
+
hostname: options.hostname,
|
|
350
|
+
port: options.port,
|
|
351
|
+
credentialsPath: options.credentialsPath
|
|
352
|
+
});
|
|
353
|
+
safeAtomicWriteFileSync(configPath, yaml, { encoding: "utf8", mode: 384 });
|
|
354
|
+
applyPrivatePermissions(configPath);
|
|
355
|
+
return {
|
|
356
|
+
uuid: options.uuid,
|
|
357
|
+
hostname: options.hostname,
|
|
358
|
+
port: options.port,
|
|
359
|
+
configPath,
|
|
360
|
+
credentialsPath: options.credentialsPath
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function readNamedTunnelConfig(configDir) {
|
|
364
|
+
const configPath = getNamedTunnelConfigPath(configDir);
|
|
365
|
+
if (!existsSync2(configPath)) return null;
|
|
366
|
+
let raw;
|
|
367
|
+
try {
|
|
368
|
+
raw = readFileSync(configPath, "utf8");
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
const parsed = parseConfigYaml(raw);
|
|
373
|
+
if (!parsed) return null;
|
|
374
|
+
return {
|
|
375
|
+
uuid: parsed.uuid,
|
|
376
|
+
hostname: parsed.hostname,
|
|
377
|
+
port: parsed.port,
|
|
378
|
+
credentialsPath: parsed.credentialsPath,
|
|
379
|
+
configPath
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function clearNamedTunnelConfig(configDir) {
|
|
383
|
+
const configPath = getNamedTunnelConfigPath(configDir);
|
|
384
|
+
const existed = existsSync2(configPath);
|
|
385
|
+
rmSync(configPath, { force: true });
|
|
386
|
+
return existed;
|
|
387
|
+
}
|
|
388
|
+
function assertBinaryExists(binaryPath) {
|
|
389
|
+
if (!existsSync2(binaryPath)) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`cloudflared not installed; run "daemon install-tunnel" first (expected at ${binaryPath}).`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function defaultCertPath() {
|
|
396
|
+
return join(homedir(), ".cloudflared", "cert.pem");
|
|
397
|
+
}
|
|
398
|
+
function killChild(child) {
|
|
399
|
+
if (!child || child.killed) return;
|
|
400
|
+
try {
|
|
401
|
+
child.kill("SIGTERM");
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function serializeConfigYaml(opts) {
|
|
406
|
+
return [
|
|
407
|
+
`tunnel: ${opts.uuid}`,
|
|
408
|
+
`credentials-file: ${yamlQuoteIfNeeded(opts.credentialsPath)}`,
|
|
409
|
+
"ingress:",
|
|
410
|
+
` - hostname: ${opts.hostname}`,
|
|
411
|
+
` service: http://127.0.0.1:${opts.port}`,
|
|
412
|
+
" - service: http_status:404",
|
|
413
|
+
""
|
|
414
|
+
].join("\n");
|
|
415
|
+
}
|
|
416
|
+
function yamlQuoteIfNeeded(value) {
|
|
417
|
+
if (/^[A-Za-z0-9._/\\:-]+$/u.test(value)) return value;
|
|
418
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
419
|
+
}
|
|
420
|
+
function parseConfigYaml(raw) {
|
|
421
|
+
const lines = raw.split(/\r?\n/);
|
|
422
|
+
let uuid = "";
|
|
423
|
+
let credentialsPath = "";
|
|
424
|
+
let hostname = "";
|
|
425
|
+
let port = 0;
|
|
426
|
+
for (const line of lines) {
|
|
427
|
+
const trimmed = line.trim();
|
|
428
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
429
|
+
const tunnelMatch = trimmed.match(/^tunnel:\s*(.+)$/u);
|
|
430
|
+
if (tunnelMatch) {
|
|
431
|
+
uuid = unquoteYaml(tunnelMatch[1].trim());
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const credsMatch = trimmed.match(/^credentials-file:\s*(.+)$/u);
|
|
435
|
+
if (credsMatch) {
|
|
436
|
+
credentialsPath = unquoteYaml(credsMatch[1].trim());
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const hostnameMatch = trimmed.match(/^- hostname:\s*(.+)$/u);
|
|
440
|
+
if (hostnameMatch) {
|
|
441
|
+
hostname = unquoteYaml(hostnameMatch[1].trim());
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const serviceMatch = trimmed.match(/^service:\s*http:\/\/127\.0\.0\.1:(\d+)\s*$/u);
|
|
445
|
+
if (serviceMatch) {
|
|
446
|
+
port = Number.parseInt(serviceMatch[1], 10);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (!uuid || !hostname || !credentialsPath || !Number.isFinite(port) || port <= 0) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return { uuid, hostname, port, credentialsPath };
|
|
454
|
+
}
|
|
455
|
+
function unquoteYaml(value) {
|
|
456
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
457
|
+
return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
458
|
+
}
|
|
459
|
+
return value;
|
|
460
|
+
}
|
|
461
|
+
function applyPrivatePermissions(path) {
|
|
462
|
+
if (process.platform === "win32") {
|
|
463
|
+
const username = process.env.USERNAME;
|
|
464
|
+
const domain = process.env.USERDOMAIN;
|
|
465
|
+
const target = domain && username ? `${domain}\\${username}` : username ?? "";
|
|
466
|
+
if (!target) return;
|
|
467
|
+
spawnSync("icacls", [path, "/inheritance:r", "/grant:r", `${target}:(R,W)`], {
|
|
468
|
+
encoding: "utf8",
|
|
469
|
+
windowsHide: true
|
|
470
|
+
});
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
chmodSync(path, 384);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/daemon/tunnel-providers/cloudflared-named.ts
|
|
477
|
+
var execFile = promisify(execFileCallback);
|
|
478
|
+
var STOP_GRACE_MS = 3e3;
|
|
479
|
+
var READY_LINE_REGEX = /Registered tunnel connection/i;
|
|
480
|
+
function createCloudflaredNamedProvider(options = {}) {
|
|
481
|
+
const spawnImpl = options.dependencies?.spawn ?? nodeSpawn2;
|
|
482
|
+
const homedirImpl = options.dependencies?.homedir ?? homedir2;
|
|
483
|
+
return {
|
|
484
|
+
id: "cf-named",
|
|
485
|
+
displayName: "Cloudflare Named Tunnel",
|
|
486
|
+
description: "Persistent URL on your own Cloudflare-managed zone. Requires one-time `cloudflared login` + tunnel create.",
|
|
487
|
+
async isSetupComplete(configDir) {
|
|
488
|
+
const binaryPath = getTunnelBinaryPath(configDir);
|
|
489
|
+
if (!existsSync3(binaryPath)) {
|
|
490
|
+
return {
|
|
491
|
+
ready: false,
|
|
492
|
+
reason: "cloudflared binary not installed.",
|
|
493
|
+
action: { label: "Install cloudflared", kind: "install-binary" }
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const certPath = join2(homedirImpl(), ".cloudflared", "cert.pem");
|
|
497
|
+
if (!existsSync3(certPath)) {
|
|
498
|
+
return {
|
|
499
|
+
ready: false,
|
|
500
|
+
reason: "cloudflared login required \u2014 origin cert not found.",
|
|
501
|
+
// 8.4.2 emitted `kind: "open-url"` here but the action has no URL —
|
|
502
|
+
// it spawns `cloudflared tunnel login` on the host. 8.4.3 migrates
|
|
503
|
+
// this to the generic run-command contract: the UI dispatches the
|
|
504
|
+
// webview message identified by `command` (here: `cf-named-login`
|
|
505
|
+
// → `daemon:cf-named-login`), which the extension modal-confirms
|
|
506
|
+
// before spawning the child process.
|
|
507
|
+
action: {
|
|
508
|
+
label: "Run cloudflared login",
|
|
509
|
+
kind: "run-command",
|
|
510
|
+
command: "cf-named-login"
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const config = readNamedTunnelConfig(configDir);
|
|
515
|
+
if (!config) {
|
|
516
|
+
return {
|
|
517
|
+
ready: false,
|
|
518
|
+
reason: "named tunnel not configured \u2014 run the setup flow."
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
if (!existsSync3(config.credentialsPath)) {
|
|
522
|
+
return {
|
|
523
|
+
ready: false,
|
|
524
|
+
reason: `credentials file not found at ${config.credentialsPath}.`
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
return { ready: true };
|
|
528
|
+
},
|
|
529
|
+
async start(startOptions) {
|
|
530
|
+
const binaryPath = getTunnelBinaryPath(startOptions.configDir);
|
|
531
|
+
if (!existsSync3(binaryPath)) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
"cloudflared is not installed. Run `npx perplexity-user-mcp daemon install-tunnel` first."
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
const certPath = join2(homedirImpl(), ".cloudflared", "cert.pem");
|
|
537
|
+
if (!existsSync3(certPath)) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
"cloudflared named tunnel not set up \u2014 origin cert missing. Run `cloudflared tunnel login`."
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const existing = readNamedTunnelConfig(startOptions.configDir);
|
|
543
|
+
if (!existing) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
"named tunnel not configured \u2014 run the cf-named setup flow from the dashboard or CLI."
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
if (!existsSync3(existing.credentialsPath)) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
`named tunnel credentials file missing at ${existing.credentialsPath}.`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
const refreshed = writeTunnelConfig({
|
|
554
|
+
configDir: startOptions.configDir,
|
|
555
|
+
uuid: existing.uuid,
|
|
556
|
+
hostname: existing.hostname,
|
|
557
|
+
port: startOptions.port,
|
|
558
|
+
credentialsPath: existing.credentialsPath
|
|
559
|
+
});
|
|
560
|
+
return spawnNamedTunnel({
|
|
561
|
+
binaryPath,
|
|
562
|
+
configPath: refreshed.configPath,
|
|
563
|
+
hostname: refreshed.hostname,
|
|
564
|
+
onStateChange: startOptions.onStateChange,
|
|
565
|
+
spawnImpl
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
var cloudflaredNamedProvider = createCloudflaredNamedProvider();
|
|
571
|
+
function spawnNamedTunnel(options) {
|
|
572
|
+
const args = [
|
|
573
|
+
"tunnel",
|
|
574
|
+
"--no-autoupdate",
|
|
575
|
+
"--config",
|
|
576
|
+
options.configPath,
|
|
577
|
+
"run"
|
|
578
|
+
];
|
|
579
|
+
const child = options.spawnImpl(options.binaryPath, args, {
|
|
580
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
581
|
+
detached: false,
|
|
582
|
+
windowsHide: true
|
|
583
|
+
});
|
|
584
|
+
const hostnameUrl = `https://${options.hostname}`;
|
|
585
|
+
let stopping = false;
|
|
586
|
+
let settled = false;
|
|
587
|
+
let resolveExited;
|
|
588
|
+
const exited = new Promise((resolve) => {
|
|
589
|
+
resolveExited = resolve;
|
|
590
|
+
});
|
|
591
|
+
let state = {
|
|
592
|
+
status: "starting",
|
|
593
|
+
url: null,
|
|
594
|
+
pid: child.pid ?? null,
|
|
595
|
+
error: null
|
|
596
|
+
};
|
|
597
|
+
const updateState = (next) => {
|
|
598
|
+
state = next;
|
|
599
|
+
options.onStateChange(state);
|
|
600
|
+
};
|
|
601
|
+
updateState(state);
|
|
602
|
+
let resolveReady;
|
|
603
|
+
let rejectReady;
|
|
604
|
+
const waitUntilReady = new Promise((resolve, reject) => {
|
|
605
|
+
resolveReady = resolve;
|
|
606
|
+
rejectReady = reject;
|
|
607
|
+
});
|
|
608
|
+
const handleLine = (line) => {
|
|
609
|
+
if (settled) return;
|
|
610
|
+
if (!READY_LINE_REGEX.test(line)) return;
|
|
611
|
+
settled = true;
|
|
612
|
+
updateState({
|
|
613
|
+
status: "enabled",
|
|
614
|
+
url: hostnameUrl,
|
|
615
|
+
pid: child.pid ?? null,
|
|
616
|
+
error: null
|
|
617
|
+
});
|
|
618
|
+
resolveReady(hostnameUrl);
|
|
619
|
+
};
|
|
620
|
+
if (child.stderr) createInterface({ input: child.stderr }).on("line", handleLine);
|
|
621
|
+
if (child.stdout) createInterface({ input: child.stdout }).on("line", handleLine);
|
|
622
|
+
child.on("error", (error) => {
|
|
623
|
+
if (!settled) {
|
|
624
|
+
settled = true;
|
|
625
|
+
rejectReady(error);
|
|
626
|
+
}
|
|
627
|
+
updateState({
|
|
628
|
+
status: stopping ? "disabled" : "crashed",
|
|
629
|
+
url: null,
|
|
630
|
+
pid: child.pid ?? null,
|
|
631
|
+
error: error.message
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
child.on("exit", (code, signal) => {
|
|
635
|
+
if (!settled) {
|
|
636
|
+
settled = true;
|
|
637
|
+
rejectReady(
|
|
638
|
+
new Error(
|
|
639
|
+
`cloudflared exited before the named tunnel came online (code=${code ?? "null"} signal=${signal ?? "null"}).`
|
|
640
|
+
)
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
updateState({
|
|
644
|
+
status: stopping ? "disabled" : "crashed",
|
|
645
|
+
url: stopping ? null : state.url,
|
|
646
|
+
pid: null,
|
|
647
|
+
error: stopping ? null : `cloudflared exited (code=${code ?? "null"} signal=${signal ?? "null"}).`
|
|
648
|
+
});
|
|
649
|
+
resolveExited();
|
|
650
|
+
});
|
|
651
|
+
const stop = async () => {
|
|
652
|
+
if (stopping) return;
|
|
653
|
+
stopping = true;
|
|
654
|
+
if (child.exitCode !== null || child.killed) {
|
|
655
|
+
updateState({ status: "disabled", url: null, pid: null, error: null });
|
|
656
|
+
resolveExited();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (process.platform === "win32") {
|
|
660
|
+
await execFile("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
|
|
661
|
+
windowsHide: true
|
|
662
|
+
}).catch(() => void 0);
|
|
663
|
+
await exited;
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
child.kill("SIGTERM");
|
|
667
|
+
const escalate = setTimeout(() => {
|
|
668
|
+
if (!killedOrExited(child)) {
|
|
669
|
+
try {
|
|
670
|
+
child.kill("SIGKILL");
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}, STOP_GRACE_MS);
|
|
675
|
+
try {
|
|
676
|
+
await exited;
|
|
677
|
+
} finally {
|
|
678
|
+
clearTimeout(escalate);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
return {
|
|
682
|
+
pid: child.pid ?? 0,
|
|
683
|
+
waitUntilReady,
|
|
684
|
+
stop,
|
|
685
|
+
getState: () => state
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function killedOrExited(child) {
|
|
689
|
+
return child.killed || child.exitCode !== null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/daemon/tunnel-providers/ngrok-config.ts
|
|
693
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
|
|
694
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
695
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
696
|
+
function getNgrokConfigPath(configDir) {
|
|
697
|
+
return join3(configDir, "ngrok.json");
|
|
698
|
+
}
|
|
699
|
+
function readNgrokSettings(configDir) {
|
|
700
|
+
const path = getNgrokConfigPath(configDir);
|
|
701
|
+
if (!existsSync4(path)) return null;
|
|
702
|
+
try {
|
|
703
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
704
|
+
if (typeof parsed.authtoken === "string" && parsed.authtoken.length > 0) {
|
|
705
|
+
return {
|
|
706
|
+
authtoken: parsed.authtoken,
|
|
707
|
+
domain: typeof parsed.domain === "string" && parsed.domain.length > 0 ? parsed.domain : void 0,
|
|
708
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
return null;
|
|
712
|
+
} catch {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function writeNgrokSettings(configDir, next) {
|
|
717
|
+
const path = getNgrokConfigPath(configDir);
|
|
718
|
+
const prev = readNgrokSettings(configDir);
|
|
719
|
+
const merged = {
|
|
720
|
+
authtoken: next.authtoken ?? prev?.authtoken ?? "",
|
|
721
|
+
domain: next.domain === null ? void 0 : next.domain ?? prev?.domain,
|
|
722
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
723
|
+
};
|
|
724
|
+
if (!merged.authtoken) {
|
|
725
|
+
throw new Error("ngrok authtoken is required.");
|
|
726
|
+
}
|
|
727
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
728
|
+
safeAtomicWriteFileSync(path, JSON.stringify(merged, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
729
|
+
applyPrivatePermissions2(path);
|
|
730
|
+
return merged;
|
|
731
|
+
}
|
|
732
|
+
function clearNgrokSettings(configDir) {
|
|
733
|
+
const path = getNgrokConfigPath(configDir);
|
|
734
|
+
rmSync2(path, { force: true });
|
|
735
|
+
}
|
|
736
|
+
function applyPrivatePermissions2(path) {
|
|
737
|
+
if (process.platform === "win32") {
|
|
738
|
+
const username = process.env.USERNAME;
|
|
739
|
+
const domain = process.env.USERDOMAIN;
|
|
740
|
+
const target = domain && username ? `${domain}\\${username}` : username ?? "";
|
|
741
|
+
if (!target) return;
|
|
742
|
+
spawnSync2("icacls", [path, "/inheritance:r", "/grant:r", `${target}:(R,W)`], {
|
|
743
|
+
encoding: "utf8",
|
|
744
|
+
windowsHide: true
|
|
745
|
+
});
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
chmodSync2(path, 384);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/daemon/tunnel-providers/ngrok.ts
|
|
752
|
+
var DASHBOARD_AUTHTOKEN_URL = "https://dashboard.ngrok.com/get-started/your-authtoken";
|
|
753
|
+
var NgrokNativeMissingError = class extends Error {
|
|
754
|
+
platform;
|
|
755
|
+
arch;
|
|
756
|
+
cause;
|
|
757
|
+
constructor(cause) {
|
|
758
|
+
const platform = process.platform;
|
|
759
|
+
const arch = process.arch;
|
|
760
|
+
const message = `@ngrok/ngrok native binding for ${platform}-${arch} is not available in this VSIX. Reinstall the extension (or install @ngrok/ngrok manually) to use the ngrok provider, or switch to the cloudflared provider in the dashboard.`;
|
|
761
|
+
super(message);
|
|
762
|
+
this.name = "NgrokNativeMissingError";
|
|
763
|
+
this.platform = platform;
|
|
764
|
+
this.arch = arch;
|
|
765
|
+
if (cause !== void 0) {
|
|
766
|
+
this.cause = cause;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
var cachedNgrok = null;
|
|
771
|
+
function isNativeMissingError(err) {
|
|
772
|
+
if (!err) return false;
|
|
773
|
+
const code = err.code;
|
|
774
|
+
if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") return true;
|
|
775
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
776
|
+
return /Cannot find module ['"]@ngrok\/ngrok[-/]/i.test(message);
|
|
777
|
+
}
|
|
778
|
+
async function loadNgrokNative() {
|
|
779
|
+
if (cachedNgrok) return cachedNgrok;
|
|
780
|
+
try {
|
|
781
|
+
const mod = await import("@ngrok/ngrok");
|
|
782
|
+
const resolved = mod.default ?? mod;
|
|
783
|
+
if (typeof resolved?.forward !== "function") {
|
|
784
|
+
throw new Error("@ngrok/ngrok module did not expose forward(); API changed?");
|
|
785
|
+
}
|
|
786
|
+
cachedNgrok = resolved;
|
|
787
|
+
return resolved;
|
|
788
|
+
} catch (err) {
|
|
789
|
+
if (isNativeMissingError(err)) {
|
|
790
|
+
throw new NgrokNativeMissingError(err);
|
|
791
|
+
}
|
|
792
|
+
throw err;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
async function isNgrokNativeAvailable() {
|
|
796
|
+
try {
|
|
797
|
+
await loadNgrokNative();
|
|
798
|
+
return { available: true };
|
|
799
|
+
} catch (err) {
|
|
800
|
+
if (err instanceof NgrokNativeMissingError) {
|
|
801
|
+
return { available: false, error: err };
|
|
802
|
+
}
|
|
803
|
+
throw err;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
var ngrokProvider = {
|
|
807
|
+
id: "ngrok",
|
|
808
|
+
displayName: "ngrok",
|
|
809
|
+
description: "Persistent URL via ngrok. Requires a free ngrok account authtoken.",
|
|
810
|
+
async isSetupComplete(configDir) {
|
|
811
|
+
const probe = await isNgrokNativeAvailable();
|
|
812
|
+
if (!probe.available) {
|
|
813
|
+
return {
|
|
814
|
+
ready: false,
|
|
815
|
+
reason: probe.error.message
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
const settings = readNgrokSettings(configDir);
|
|
819
|
+
if (!settings?.authtoken) {
|
|
820
|
+
return {
|
|
821
|
+
ready: false,
|
|
822
|
+
reason: "ngrok authtoken not set.",
|
|
823
|
+
action: {
|
|
824
|
+
label: "Get authtoken",
|
|
825
|
+
kind: "open-url",
|
|
826
|
+
url: DASHBOARD_AUTHTOKEN_URL
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
return { ready: true };
|
|
831
|
+
},
|
|
832
|
+
async start(options) {
|
|
833
|
+
const settings = readNgrokSettings(options.configDir);
|
|
834
|
+
if (!settings?.authtoken) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
`ngrok authtoken not configured. Paste your authtoken from ${DASHBOARD_AUTHTOKEN_URL} into the dashboard, or run \`perplexity-user-mcp daemon set-ngrok-authtoken <token>\`.`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const ngrok = await loadNgrokNative();
|
|
840
|
+
let state = { status: "starting", url: null, pid: null, error: null };
|
|
841
|
+
const updateState = (next) => {
|
|
842
|
+
state = next;
|
|
843
|
+
options.onStateChange(state);
|
|
844
|
+
};
|
|
845
|
+
updateState(state);
|
|
846
|
+
try {
|
|
847
|
+
if (typeof ngrok.kill === "function") {
|
|
848
|
+
await ngrok.kill();
|
|
849
|
+
}
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
let listener = null;
|
|
853
|
+
let resolveExited;
|
|
854
|
+
const exited = new Promise((resolve) => {
|
|
855
|
+
resolveExited = resolve;
|
|
856
|
+
});
|
|
857
|
+
try {
|
|
858
|
+
listener = await ngrok.forward({
|
|
859
|
+
addr: options.port,
|
|
860
|
+
authtoken: settings.authtoken,
|
|
861
|
+
...settings.domain ? { domain: settings.domain } : {},
|
|
862
|
+
// Human-readable label in the ngrok dashboard.
|
|
863
|
+
forwards_to: `perplexity-mcp (port ${options.port})`
|
|
864
|
+
});
|
|
865
|
+
} catch (err) {
|
|
866
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
867
|
+
const friendly = translateNgrokError(raw, settings.domain);
|
|
868
|
+
updateState({ status: "crashed", url: null, pid: null, error: friendly });
|
|
869
|
+
throw new Error(friendly);
|
|
870
|
+
}
|
|
871
|
+
const url = typeof listener?.url === "function" ? listener.url() : null;
|
|
872
|
+
if (!url) {
|
|
873
|
+
await safeClose(listener);
|
|
874
|
+
updateState({ status: "crashed", url: null, pid: null, error: "ngrok returned no URL" });
|
|
875
|
+
throw new Error("ngrok did not publish a URL.");
|
|
876
|
+
}
|
|
877
|
+
updateState({ status: "enabled", url, pid: null, error: null });
|
|
878
|
+
let stopping = false;
|
|
879
|
+
const stop = async () => {
|
|
880
|
+
if (stopping) return;
|
|
881
|
+
stopping = true;
|
|
882
|
+
await safeClose(listener);
|
|
883
|
+
listener = null;
|
|
884
|
+
updateState({ status: "disabled", url: null, pid: null, error: null });
|
|
885
|
+
resolveExited();
|
|
886
|
+
};
|
|
887
|
+
return {
|
|
888
|
+
pid: 0,
|
|
889
|
+
waitUntilReady: Promise.resolve(url),
|
|
890
|
+
stop,
|
|
891
|
+
getState: () => state
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
async function safeClose(listener) {
|
|
896
|
+
if (!listener) return;
|
|
897
|
+
try {
|
|
898
|
+
if (typeof listener.close === "function") {
|
|
899
|
+
await listener.close();
|
|
900
|
+
}
|
|
901
|
+
} catch {
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
function translateNgrokError(raw, domain) {
|
|
905
|
+
if (/ERR_NGROK_334/i.test(raw) || /already online/i.test(raw)) {
|
|
906
|
+
const which = domain ? ` for "${domain}"` : "";
|
|
907
|
+
return `ngrok refused the bind${which}: the reserved domain is still registered from a previous session. Wait ~60 seconds for ngrok's server to release it, then click Enable again. Or: use the Kill daemon button to force-cleanup, then try a different domain (or leave the domain blank for an ephemeral URL). Upstream code: ERR_NGROK_334.`;
|
|
908
|
+
}
|
|
909
|
+
if (/ERR_NGROK_105/i.test(raw) || /authentication failed/i.test(raw) || /authtoken/i.test(raw)) {
|
|
910
|
+
return `ngrok rejected the authtoken. Check it at ${DASHBOARD_AUTHTOKEN_URL} and paste it into the dashboard, then try Enable again.`;
|
|
911
|
+
}
|
|
912
|
+
if (/ERR_NGROK_108/i.test(raw) || /limited to 1 simultaneous/i.test(raw)) {
|
|
913
|
+
return `ngrok free tier allows one session per account. Another device or app is already using this authtoken \u2014 stop it in the ngrok dashboard (Cloud Edge \u2192 Tunnels), then click Enable.`;
|
|
914
|
+
}
|
|
915
|
+
return `ngrok forward failed: ${raw}`;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/daemon/tunnel-providers/index.ts
|
|
919
|
+
var REGISTRY = {
|
|
920
|
+
"cf-quick": cloudflaredQuickProvider,
|
|
921
|
+
ngrok: ngrokProvider,
|
|
922
|
+
"cf-named": cloudflaredNamedProvider
|
|
923
|
+
};
|
|
924
|
+
function getTunnelProvider(id) {
|
|
925
|
+
const provider = REGISTRY[id];
|
|
926
|
+
if (!provider) {
|
|
927
|
+
throw new Error(`Unknown tunnel provider: ${id}`);
|
|
928
|
+
}
|
|
929
|
+
return provider;
|
|
930
|
+
}
|
|
931
|
+
function listTunnelProviders() {
|
|
932
|
+
return Object.values(REGISTRY);
|
|
933
|
+
}
|
|
934
|
+
var DEFAULT_PROVIDER = "cf-quick";
|
|
935
|
+
function getTunnelSettingsPath(configDir) {
|
|
936
|
+
return join4(configDir, "tunnel-settings.json");
|
|
937
|
+
}
|
|
938
|
+
function readTunnelSettings(configDir) {
|
|
939
|
+
const path = getTunnelSettingsPath(configDir);
|
|
940
|
+
if (!existsSync5(path)) {
|
|
941
|
+
return { activeProvider: DEFAULT_PROVIDER, updatedAt: (/* @__PURE__ */ new Date(0)).toISOString() };
|
|
942
|
+
}
|
|
943
|
+
try {
|
|
944
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
945
|
+
const activeProvider = parsed.activeProvider && parsed.activeProvider in REGISTRY ? parsed.activeProvider : DEFAULT_PROVIDER;
|
|
946
|
+
return {
|
|
947
|
+
activeProvider,
|
|
948
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
949
|
+
};
|
|
950
|
+
} catch {
|
|
951
|
+
return { activeProvider: DEFAULT_PROVIDER, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function writeTunnelSettings(configDir, patch) {
|
|
955
|
+
const path = getTunnelSettingsPath(configDir);
|
|
956
|
+
const prev = readTunnelSettings(configDir);
|
|
957
|
+
const next = {
|
|
958
|
+
activeProvider: patch.activeProvider && patch.activeProvider in REGISTRY ? patch.activeProvider : prev.activeProvider,
|
|
959
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
960
|
+
};
|
|
961
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
962
|
+
safeAtomicWriteFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
963
|
+
return next;
|
|
964
|
+
}
|
|
965
|
+
async function listTunnelProviderStatuses(configDir) {
|
|
966
|
+
const { activeProvider } = readTunnelSettings(configDir);
|
|
967
|
+
const results = [];
|
|
968
|
+
for (const provider of listTunnelProviders()) {
|
|
969
|
+
const setup = await provider.isSetupComplete(configDir);
|
|
970
|
+
results.push({
|
|
971
|
+
id: provider.id,
|
|
972
|
+
displayName: provider.displayName,
|
|
973
|
+
description: provider.description,
|
|
974
|
+
setup,
|
|
975
|
+
isActive: provider.id === activeProvider
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
return results;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export {
|
|
982
|
+
DeleteNamedTunnelError,
|
|
983
|
+
runCloudflaredLogin,
|
|
984
|
+
listNamedTunnels,
|
|
985
|
+
createNamedTunnel,
|
|
986
|
+
deleteNamedTunnel,
|
|
987
|
+
isActiveConnectionDeleteFailure,
|
|
988
|
+
getNamedTunnelConfigPath,
|
|
989
|
+
writeTunnelConfig,
|
|
990
|
+
readNamedTunnelConfig,
|
|
991
|
+
clearNamedTunnelConfig,
|
|
992
|
+
createCloudflaredNamedProvider,
|
|
993
|
+
getNgrokConfigPath,
|
|
994
|
+
readNgrokSettings,
|
|
995
|
+
writeNgrokSettings,
|
|
996
|
+
clearNgrokSettings,
|
|
997
|
+
NgrokNativeMissingError,
|
|
998
|
+
loadNgrokNative,
|
|
999
|
+
isNgrokNativeAvailable,
|
|
1000
|
+
getTunnelProvider,
|
|
1001
|
+
listTunnelProviders,
|
|
1002
|
+
getTunnelSettingsPath,
|
|
1003
|
+
readTunnelSettings,
|
|
1004
|
+
writeTunnelSettings,
|
|
1005
|
+
listTunnelProviderStatuses
|
|
1006
|
+
};
|