@humain/terminal 0.0.3-beta.0 → 0.0.3-beta.2
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/CHANGELOG.md +13 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -3
- package/dist/config.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +13 -0
- package/dist/humain/mcp-adapter/vendor/README.md +4 -1
- package/dist/humain/mcp-adapter/vendor/agent-dir.ts +12 -4
- package/dist/humain/mcp-adapter/vendor/cli.js +25 -4
- package/dist/humain/mcp-adapter/vendor/config.ts +4 -4
- package/dist/humain/mcp-adapter/vendor/direct-tools.ts +18 -15
- package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +416 -60
- package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +2 -1
- package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +18 -8
- package/dist/humain/mcp-adapter/vendor/package.json +177 -176
- package/dist/humain/mcp-adapter/vendor/request-headers-command.ts +336 -0
- package/dist/humain/mcp-adapter/vendor/server-manager.ts +5 -0
- package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +38 -18
- package/dist/humain/mcp-adapter/vendor/types.ts +93 -10
- package/dist/humain/web-access/vendor/auth-fetch.d.ts +13 -0
- package/dist/humain/web-access/vendor/auth-fetch.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/auth-fetch.js +134 -0
- package/dist/humain/web-access/vendor/auth-fetch.js.map +1 -0
- package/dist/humain/web-access/vendor/chrome-cookies.d.ts +13 -0
- package/dist/humain/web-access/vendor/chrome-cookies.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/chrome-cookies.js +106 -22
- package/dist/humain/web-access/vendor/chrome-cookies.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.d.ts +1 -0
- package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.js +3 -1
- package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-server.js +2 -0
- package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts +2 -0
- package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/extract.js +79 -17
- package/dist/humain/web-access/vendor/extract.js.map +1 -1
- package/dist/humain/web-access/vendor/fetch-params.d.ts +2 -0
- package/dist/humain/web-access/vendor/fetch-params.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js +17 -3
- package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
- package/dist/humain/web-access/vendor/firecrawl.d.ts +7 -0
- package/dist/humain/web-access/vendor/firecrawl.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/firecrawl.js +141 -10
- package/dist/humain/web-access/vendor/firecrawl.js.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
- package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.js +22 -4
- package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
- package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/index.js +122 -48
- package/dist/humain/web-access/vendor/index.js.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts +3 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.js +6 -3
- package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
- package/dist/humain/web-access/vendor/youtube-extract.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/youtube-extract.js +2 -2
- package/dist/humain/web-access/vendor/youtube-extract.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +2 -0
- package/dist/utils/clipboard-image.js.map +1 -1
- package/npm-shrinkwrap.json +7 -7
- package/package.json +6 -6
- package/src/humain/mcp-adapter/UPSTREAM.md +1 -1
- package/src/humain/mcp-adapter/upstream.json +3 -3
- package/src/humain/mcp-adapter/vendor/CHANGELOG.md +13 -0
- package/src/humain/web-access/UPSTREAM.md +1 -1
- package/src/humain/web-access/upstream.json +4 -4
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import type { FetchLike } from "@modelcontextprotocol/client";
|
|
4
|
+
import type { HttpRequestHeadersCommand } from "./types.ts";
|
|
5
|
+
import { interpolateEnvVars } from "./utils.ts";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
8
|
+
const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
9
|
+
const USE_PROCESS_GROUP = process.platform !== "win32";
|
|
10
|
+
const CLEANUP_TOKEN_ENV = "PI_MCP_REQUEST_HEADERS_CLEANUP_TOKEN";
|
|
11
|
+
|
|
12
|
+
function isNoSuchProcessError(error: unknown): boolean {
|
|
13
|
+
return typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ESRCH";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runPosixPs(args: string[]): { status: number | null; stdout: string } {
|
|
17
|
+
if (process.env.PI_MCP_ADAPTER_TEST_FAIL_PS === "1") return { status: 1, stdout: "" };
|
|
18
|
+
return spawnSync("ps", args, { encoding: "utf8" });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectPosixDescendantPids(rootPid: number): number[] {
|
|
22
|
+
const result = runPosixPs(["-axo", "pid=,ppid="]);
|
|
23
|
+
if (result.status !== 0) {
|
|
24
|
+
throw new Error(`HTTP request headers command cleanup failed: ps exited with code ${result.status ?? "unknown"}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const childrenByParent = new Map<number, number[]>();
|
|
28
|
+
for (const line of result.stdout.split("\n")) {
|
|
29
|
+
const [pidText, ppidText] = line.trim().split(/\s+/, 2);
|
|
30
|
+
const pid = Number(pidText);
|
|
31
|
+
const ppid = Number(ppidText);
|
|
32
|
+
if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue;
|
|
33
|
+
const children = childrenByParent.get(ppid);
|
|
34
|
+
if (children) children.push(pid);
|
|
35
|
+
else childrenByParent.set(ppid, [pid]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const descendants: number[] = [];
|
|
39
|
+
const stack = [...(childrenByParent.get(rootPid) ?? [])];
|
|
40
|
+
while (stack.length > 0) {
|
|
41
|
+
const pid = stack.pop()!;
|
|
42
|
+
descendants.push(pid);
|
|
43
|
+
stack.push(...(childrenByParent.get(pid) ?? []));
|
|
44
|
+
}
|
|
45
|
+
return descendants;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collectPosixCleanupTokenPids(cleanupToken: string): number[] {
|
|
49
|
+
const result = runPosixPs(["axeww", "-o", "pid=,command="]);
|
|
50
|
+
if (result.status !== 0) {
|
|
51
|
+
throw new Error(`HTTP request headers command cleanup failed: ps exited with code ${result.status ?? "unknown"}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const needle = `${CLEANUP_TOKEN_ENV}=${cleanupToken}`;
|
|
55
|
+
const pids: number[] = [];
|
|
56
|
+
for (const line of result.stdout.split("\n")) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed.includes(needle)) continue;
|
|
59
|
+
const [pidText] = trimmed.split(/\s+/, 1);
|
|
60
|
+
const pid = Number(pidText);
|
|
61
|
+
if (Number.isInteger(pid) && pid !== process.pid) pids.push(pid);
|
|
62
|
+
}
|
|
63
|
+
return pids;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function assertPosixProcessDiscoveryAvailable(): void {
|
|
67
|
+
collectPosixDescendantPids(process.pid);
|
|
68
|
+
collectPosixCleanupTokenPids(`${process.pid}-preflight`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isTaskkillNoSuchProcess(result: ReturnType<typeof spawnSync>): boolean {
|
|
72
|
+
return `${result.stdout ?? ""}\n${result.stderr ?? ""}`.toLowerCase().includes("not found");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function signalPid(pid: number, signal: NodeJS.Signals): void {
|
|
76
|
+
try {
|
|
77
|
+
process.kill(pid, signal);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (!isNoSuchProcessError(error)) throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function signalProcessGroup(pid: number, signal: NodeJS.Signals): void {
|
|
84
|
+
try {
|
|
85
|
+
process.kill(-pid, signal);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (!isNoSuchProcessError(error)) throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function killRequestHeadersCommand(child: ChildProcess, trackedPosixDescendantPids = new Set<number>(), cleanupToken?: string): void {
|
|
92
|
+
if (process.platform === "win32" && child.pid !== undefined) {
|
|
93
|
+
const result = spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
94
|
+
encoding: "utf8",
|
|
95
|
+
windowsHide: true,
|
|
96
|
+
});
|
|
97
|
+
if (result.status === 0 || isTaskkillNoSuchProcess(result)) return;
|
|
98
|
+
throw new Error(`HTTP request headers command cleanup failed: taskkill exited with code ${result.status ?? "unknown"}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (USE_PROCESS_GROUP && child.pid !== undefined) {
|
|
102
|
+
const frozenPids = new Set<number>();
|
|
103
|
+
let cleanupError: Error | undefined;
|
|
104
|
+
try {
|
|
105
|
+
signalProcessGroup(child.pid, "SIGSTOP");
|
|
106
|
+
for (const pid of trackedPosixDescendantPids) {
|
|
107
|
+
signalPid(pid, "SIGSTOP");
|
|
108
|
+
frozenPids.add(pid);
|
|
109
|
+
}
|
|
110
|
+
let stablePasses = 0;
|
|
111
|
+
for (let pass = 0; pass < 16; pass++) {
|
|
112
|
+
const candidates = [
|
|
113
|
+
...collectPosixDescendantPids(child.pid),
|
|
114
|
+
...(cleanupToken ? collectPosixCleanupTokenPids(cleanupToken) : []),
|
|
115
|
+
];
|
|
116
|
+
const newPids = candidates.filter(pid => !frozenPids.has(pid));
|
|
117
|
+
if (newPids.length === 0) {
|
|
118
|
+
stablePasses++;
|
|
119
|
+
if (stablePasses >= 2) return;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
stablePasses = 0;
|
|
123
|
+
for (const pid of newPids) {
|
|
124
|
+
signalPid(pid, "SIGSTOP");
|
|
125
|
+
frozenPids.add(pid);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
cleanupError = new Error("HTTP request headers command cleanup failed: descendant process tree did not stabilize");
|
|
129
|
+
} catch (error) {
|
|
130
|
+
cleanupError = error instanceof Error ? error : new Error(String(error));
|
|
131
|
+
} finally {
|
|
132
|
+
signalProcessGroup(child.pid, "SIGKILL");
|
|
133
|
+
for (const pid of frozenPids) signalPid(pid, "SIGKILL");
|
|
134
|
+
}
|
|
135
|
+
if (cleanupError) throw cleanupError;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
child.kill("SIGKILL");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface HttpRequestCommandEnvelope {
|
|
143
|
+
version: 1;
|
|
144
|
+
method: string;
|
|
145
|
+
url: string;
|
|
146
|
+
bodyBase64: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
type CommandResult =
|
|
150
|
+
| { status: "error"; error: Error }
|
|
151
|
+
| { status: "success"; headers: Headers };
|
|
152
|
+
|
|
153
|
+
function resolvedCommand(config: HttpRequestHeadersCommand): {
|
|
154
|
+
command: string;
|
|
155
|
+
args: string[];
|
|
156
|
+
env: NodeJS.ProcessEnv;
|
|
157
|
+
timeoutMs: number;
|
|
158
|
+
} {
|
|
159
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
160
|
+
throw new Error("HTTP request headers command must be an object");
|
|
161
|
+
}
|
|
162
|
+
if (typeof config.command !== "string" || config.command.trim() === "") {
|
|
163
|
+
throw new Error("HTTP request headers command requires a non-empty command");
|
|
164
|
+
}
|
|
165
|
+
if (config.args !== undefined && (!Array.isArray(config.args) || config.args.some(arg => typeof arg !== "string"))) {
|
|
166
|
+
throw new Error("HTTP request headers command args must be strings");
|
|
167
|
+
}
|
|
168
|
+
if (config.env !== undefined && (
|
|
169
|
+
typeof config.env !== "object"
|
|
170
|
+
|| Array.isArray(config.env)
|
|
171
|
+
|| Object.values(config.env).some(value => typeof value !== "string")
|
|
172
|
+
)) {
|
|
173
|
+
throw new Error("HTTP request headers command env values must be strings");
|
|
174
|
+
}
|
|
175
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
176
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60_000) {
|
|
177
|
+
throw new Error("HTTP request headers command timeoutMs must be an integer between 1 and 60000");
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
command: interpolateEnvVars(config.command),
|
|
181
|
+
args: (config.args ?? []).map(interpolateEnvVars),
|
|
182
|
+
env: {
|
|
183
|
+
...process.env,
|
|
184
|
+
...Object.fromEntries(
|
|
185
|
+
Object.entries(config.env ?? {}).map(([key, value]) => [key, interpolateEnvVars(value)]),
|
|
186
|
+
),
|
|
187
|
+
},
|
|
188
|
+
timeoutMs,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function invokeRequestHeadersCommand(
|
|
193
|
+
config: HttpRequestHeadersCommand,
|
|
194
|
+
envelope: HttpRequestCommandEnvelope,
|
|
195
|
+
signal: AbortSignal,
|
|
196
|
+
): Promise<Headers> {
|
|
197
|
+
const resolved = resolvedCommand(config);
|
|
198
|
+
if (USE_PROCESS_GROUP) assertPosixProcessDiscoveryAvailable();
|
|
199
|
+
return new Promise<Headers>((resolve, reject) => {
|
|
200
|
+
let stdout = Buffer.alloc(0);
|
|
201
|
+
let settled = false;
|
|
202
|
+
const cleanupToken = randomUUID();
|
|
203
|
+
const child = spawn(resolved.command, resolved.args, {
|
|
204
|
+
env: { ...resolved.env, [CLEANUP_TOKEN_ENV]: cleanupToken },
|
|
205
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
206
|
+
windowsHide: true,
|
|
207
|
+
detached: USE_PROCESS_GROUP,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const trackedPosixDescendantPids = new Set<number>();
|
|
211
|
+
let trackingError: Error | undefined;
|
|
212
|
+
const trackPosixDescendants = () => {
|
|
213
|
+
if (!USE_PROCESS_GROUP || child.pid === undefined || settled || trackingError) return;
|
|
214
|
+
try {
|
|
215
|
+
for (const pid of collectPosixDescendantPids(child.pid)) trackedPosixDescendantPids.add(pid);
|
|
216
|
+
for (const pid of collectPosixCleanupTokenPids(cleanupToken)) trackedPosixDescendantPids.add(pid);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
trackingError = error instanceof Error ? error : new Error(String(error));
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const descendantTracker = USE_PROCESS_GROUP ? setInterval(trackPosixDescendants, 50) : undefined;
|
|
222
|
+
descendantTracker?.unref();
|
|
223
|
+
|
|
224
|
+
const finish = (result: CommandResult) => {
|
|
225
|
+
if (settled) return;
|
|
226
|
+
settled = true;
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
if (descendantTracker) clearInterval(descendantTracker);
|
|
229
|
+
signal.removeEventListener("abort", abort);
|
|
230
|
+
if (result.status === "error") reject(result.error);
|
|
231
|
+
else resolve(result.headers);
|
|
232
|
+
};
|
|
233
|
+
const finishAfterKill = (result: CommandResult) => {
|
|
234
|
+
try {
|
|
235
|
+
killRequestHeadersCommand(child, trackedPosixDescendantPids, cleanupToken);
|
|
236
|
+
finish(result);
|
|
237
|
+
} catch (cleanupError) {
|
|
238
|
+
finish({ status: "error", error: cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)) });
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const failAfterKill = (message: string) => {
|
|
242
|
+
finishAfterKill({ status: "error", error: trackingError ?? new Error(message) });
|
|
243
|
+
};
|
|
244
|
+
const abort = () => {
|
|
245
|
+
failAfterKill("HTTP request headers command aborted");
|
|
246
|
+
};
|
|
247
|
+
const timer = setTimeout(() => {
|
|
248
|
+
failAfterKill(`HTTP request headers command timed out after ${resolved.timeoutMs}ms`);
|
|
249
|
+
}, resolved.timeoutMs);
|
|
250
|
+
|
|
251
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
252
|
+
if (signal.aborted) {
|
|
253
|
+
abort();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
child.on("error", () => finish({ status: "error", error: new Error("HTTP request headers command failed to start") }));
|
|
258
|
+
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
259
|
+
if (settled) return;
|
|
260
|
+
stdout = Buffer.concat([stdout, Buffer.from(chunk)]);
|
|
261
|
+
if (stdout.byteLength > MAX_OUTPUT_BYTES) {
|
|
262
|
+
failAfterKill("HTTP request headers command output exceeded 64 KiB");
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
child.on("close", code => {
|
|
266
|
+
if (settled) return;
|
|
267
|
+
if (trackingError) {
|
|
268
|
+
failAfterKill(trackingError.message);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (code !== 0) {
|
|
272
|
+
failAfterKill(`HTTP request headers command exited with code ${code ?? "unknown"}`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
let parsed: unknown;
|
|
276
|
+
try {
|
|
277
|
+
parsed = JSON.parse(stdout.toString("utf8"));
|
|
278
|
+
} catch {
|
|
279
|
+
finishAfterKill({ status: "error", error: new Error("HTTP request headers command returned invalid JSON") });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
283
|
+
finishAfterKill({ status: "error", error: new Error("HTTP request headers command must return a JSON object") });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const entries = Object.entries(parsed);
|
|
287
|
+
if (entries.some(([, value]) => typeof value !== "string")) {
|
|
288
|
+
finishAfterKill({ status: "error", error: new Error("HTTP request headers command values must be strings") });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
finishAfterKill({ status: "success", headers: new Headers(entries as Array<[string, string]>) });
|
|
293
|
+
} catch {
|
|
294
|
+
finishAfterKill({ status: "error", error: new Error("HTTP request headers command returned an invalid header") });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
child.stdin.on("error", () => {});
|
|
299
|
+
child.stdin.end(JSON.stringify(envelope));
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Wrap fetch so a trusted command can derive headers from the exact request. */
|
|
304
|
+
export function createRequestHeadersCommandFetch(
|
|
305
|
+
config: HttpRequestHeadersCommand,
|
|
306
|
+
delegate: FetchLike = globalThis.fetch,
|
|
307
|
+
): FetchLike {
|
|
308
|
+
// Validate static configuration before the first request.
|
|
309
|
+
resolvedCommand(config);
|
|
310
|
+
return async (input: URL | RequestInfo, init?: RequestInit): Promise<Response> => {
|
|
311
|
+
const request = new Request(input, init);
|
|
312
|
+
const body = Buffer.from(await request.clone().arrayBuffer());
|
|
313
|
+
const derived = await invokeRequestHeadersCommand(config, {
|
|
314
|
+
version: 1,
|
|
315
|
+
method: request.method.toUpperCase(),
|
|
316
|
+
url: request.url,
|
|
317
|
+
bodyBase64: body.toString("base64"),
|
|
318
|
+
}, request.signal);
|
|
319
|
+
const headers = new Headers(request.headers);
|
|
320
|
+
derived.forEach((value, name) => headers.set(name, value));
|
|
321
|
+
return delegate(new URL(request.url), {
|
|
322
|
+
method: request.method,
|
|
323
|
+
headers,
|
|
324
|
+
...(request.method === "GET" || request.method === "HEAD" ? {} : { body }),
|
|
325
|
+
signal: request.signal,
|
|
326
|
+
cache: request.cache,
|
|
327
|
+
credentials: request.credentials,
|
|
328
|
+
integrity: request.integrity,
|
|
329
|
+
keepalive: request.keepalive,
|
|
330
|
+
mode: request.mode,
|
|
331
|
+
redirect: request.redirect,
|
|
332
|
+
referrer: request.referrer,
|
|
333
|
+
referrerPolicy: request.referrerPolicy,
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
}
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
traceTransportKind,
|
|
57
57
|
wrapTransportWithMcpTrace,
|
|
58
58
|
} from "./mcp-trace.ts";
|
|
59
|
+
import { createRequestHeadersCommandFetch } from "./request-headers-command.ts";
|
|
59
60
|
|
|
60
61
|
const MAX_CAPTURED_STDERR_BYTES = 8 * 1024;
|
|
61
62
|
const MAX_CAPTURED_STDERR_LINES = 3;
|
|
@@ -746,6 +747,9 @@ export class McpServerManager {
|
|
|
746
747
|
}
|
|
747
748
|
|
|
748
749
|
const requestInit = Object.keys(headers).length > 0 ? { headers } : undefined;
|
|
750
|
+
const requestFetch = definition.requestHeadersCommand
|
|
751
|
+
? createRequestHeadersCommandFetch(definition.requestHeadersCommand)
|
|
752
|
+
: undefined;
|
|
749
753
|
const createAuthProvider = (): McpOAuthProvider => new McpOAuthProvider(
|
|
750
754
|
serverName,
|
|
751
755
|
serverUrl,
|
|
@@ -772,6 +776,7 @@ export class McpServerManager {
|
|
|
772
776
|
const authProvider = "provider" in authState ? authState.provider : undefined;
|
|
773
777
|
const transportOptions = {
|
|
774
778
|
...(requestInit !== undefined ? { requestInit } : {}),
|
|
779
|
+
...(requestFetch !== undefined ? { fetch: requestFetch } : {}),
|
|
775
780
|
...(authProvider !== undefined ? { authProvider } : {}),
|
|
776
781
|
...(authProvider !== undefined
|
|
777
782
|
&& definition.oauth !== false
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getToolUiResourceUri } from "./ui-app-bridge-helpers.ts";
|
|
2
2
|
import type { McpExtensionState } from "./state.ts";
|
|
3
3
|
import type { ToolMetadata, McpTool, McpResource, ServerEntry, ToolPrefix } from "./types.ts";
|
|
4
|
-
import { formatToolName, getToolNameCandidates, isToolAllowed, resolveToolPrefix } from "./types.ts";
|
|
4
|
+
import { createToolSelectorCandidateIndex, formatToolName, getToolNameCandidates, isToolAllowed, resolveToolPrefix } from "./types.ts";
|
|
5
5
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
6
6
|
import { extractToolUiStreamMode } from "./utils.ts";
|
|
7
7
|
import { extractUiToolVisibility, isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
|
|
@@ -20,48 +20,68 @@ export function buildToolMetadata(
|
|
|
20
20
|
const failedTools: string[] = [];
|
|
21
21
|
const seenNames = new Set<string>();
|
|
22
22
|
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
const hasToolFilters =
|
|
24
|
+
(Array.isArray(definition.includeTools) && definition.includeTools.length > 0) ||
|
|
25
|
+
(Array.isArray(definition.excludeTools) && definition.excludeTools.length > 0);
|
|
26
|
+
const selectorCandidateIndex = hasToolFilters && configuredServers ? (() => {
|
|
25
27
|
const candidates = new Set<string>();
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
+
const additionalCandidatesByToolName = new Map<string, Set<string>>();
|
|
29
|
+
const evaluatedToolNames = new Set<string>();
|
|
30
|
+
const addCandidates = (
|
|
31
|
+
target: Set<string>,
|
|
32
|
+
originalName: string,
|
|
33
|
+
candidateServerName: string,
|
|
34
|
+
candidatePrefix: ToolPrefix,
|
|
35
|
+
) => {
|
|
36
|
+
for (const candidate of getToolNameCandidates(originalName, candidateServerName, candidatePrefix, false)) target.add(candidate);
|
|
28
37
|
};
|
|
29
38
|
|
|
30
39
|
for (const tool of tools) {
|
|
31
|
-
if (tool?.name)
|
|
40
|
+
if (!tool?.name) continue;
|
|
41
|
+
evaluatedToolNames.add(tool.name);
|
|
42
|
+
addCandidates(candidates, tool.name, serverName, effectivePrefix);
|
|
32
43
|
}
|
|
33
44
|
if (definition.exposeResources !== false) {
|
|
34
45
|
for (const resource of resources) {
|
|
35
|
-
|
|
46
|
+
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
47
|
+
evaluatedToolNames.add(baseName);
|
|
48
|
+
if (resource?.name && resource?.uri) addCandidates(candidates, baseName, serverName, effectivePrefix);
|
|
36
49
|
}
|
|
37
50
|
}
|
|
38
51
|
for (const [otherServerName, otherDefinition] of Object.entries(configuredServers)) {
|
|
39
52
|
if (otherServerName === serverName) continue;
|
|
53
|
+
const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
|
|
40
54
|
const knownTools = knownMetadata?.get(otherServerName);
|
|
41
55
|
if (knownTools) {
|
|
42
|
-
const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
|
|
43
56
|
for (const tool of knownTools) {
|
|
44
57
|
candidates.add(tool.name);
|
|
45
|
-
addCandidates(tool.originalName, otherServerName, otherPrefix);
|
|
58
|
+
addCandidates(candidates, tool.originalName, otherServerName, otherPrefix);
|
|
46
59
|
}
|
|
47
60
|
} else if (!knownMetadata || includeMissingConfiguredCandidates) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
for (const toolName of evaluatedToolNames) {
|
|
62
|
+
let additionalCandidates = additionalCandidatesByToolName.get(toolName);
|
|
63
|
+
if (!additionalCandidates) {
|
|
64
|
+
additionalCandidates = new Set<string>();
|
|
65
|
+
additionalCandidatesByToolName.set(toolName, additionalCandidates);
|
|
66
|
+
}
|
|
67
|
+
addCandidates(additionalCandidates, toolName, otherServerName, otherPrefix);
|
|
68
|
+
if (includeMissingConfiguredCandidates) {
|
|
69
|
+
for (const candidate of getToolNameCandidates(toolName, otherServerName, otherPrefix, false)) {
|
|
70
|
+
additionalCandidates.add(candidate.replace(/-/g, "_"));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
52
73
|
}
|
|
53
74
|
}
|
|
54
75
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
76
|
+
return createToolSelectorCandidateIndex(candidates, additionalCandidatesByToolName);
|
|
77
|
+
})() : undefined;
|
|
58
78
|
|
|
59
79
|
for (const tool of tools) {
|
|
60
80
|
if (!tool?.name) {
|
|
61
81
|
failedTools.push("(unnamed)");
|
|
62
82
|
continue;
|
|
63
83
|
}
|
|
64
|
-
if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools,
|
|
84
|
+
if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) {
|
|
65
85
|
continue;
|
|
66
86
|
}
|
|
67
87
|
|
|
@@ -97,7 +117,7 @@ export function buildToolMetadata(
|
|
|
97
117
|
if (definition.exposeResources !== false) {
|
|
98
118
|
for (const resource of resources) {
|
|
99
119
|
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
100
|
-
if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools,
|
|
120
|
+
if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) {
|
|
101
121
|
continue;
|
|
102
122
|
}
|
|
103
123
|
|
|
@@ -356,6 +356,19 @@ export interface OAuthConfig {
|
|
|
356
356
|
skipIssuerMetadataValidation?: boolean;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Trusted executable invoked for every outbound HTTP request. The adapter
|
|
361
|
+
* writes a versioned JSON request envelope to stdin and expects a JSON object
|
|
362
|
+
* containing headers on stdout. This is intended for caller-bound signing
|
|
363
|
+
* schemes whose headers depend on the exact request body.
|
|
364
|
+
*/
|
|
365
|
+
export interface HttpRequestHeadersCommand {
|
|
366
|
+
command: string;
|
|
367
|
+
args?: string[];
|
|
368
|
+
env?: Record<string, string>;
|
|
369
|
+
timeoutMs?: number;
|
|
370
|
+
}
|
|
371
|
+
|
|
359
372
|
// Server configuration
|
|
360
373
|
export interface ServerEntry {
|
|
361
374
|
command?: string;
|
|
@@ -367,6 +380,8 @@ export interface ServerEntry {
|
|
|
367
380
|
// HTTP fields
|
|
368
381
|
url?: string;
|
|
369
382
|
headers?: Record<string, string>;
|
|
383
|
+
/** Add or replace HTTP headers by running a trusted command for each request. */
|
|
384
|
+
requestHeadersCommand?: HttpRequestHeadersCommand;
|
|
370
385
|
/**
|
|
371
386
|
* Authentication type:
|
|
372
387
|
* - 'oauth' - Use OAuth 2.1 (auto-discovers endpoints, supports dynamic client registration)
|
|
@@ -489,6 +504,8 @@ export interface McpSettings {
|
|
|
489
504
|
idleTimeout?: number; // minutes, default 10, 0 to disable
|
|
490
505
|
requestTimeoutMs?: number; // milliseconds, overrides the SDK request timeout when > 0
|
|
491
506
|
directTools?: boolean;
|
|
507
|
+
/** Show the advisory when 75 or more direct tools resolve. Defaults to true. */
|
|
508
|
+
warnOnLargeDirectTools?: boolean;
|
|
492
509
|
/** Register the trusted MCP-only JavaScript scripting tool. Defaults to true; set false to hide it. */
|
|
493
510
|
scriptMode?: boolean;
|
|
494
511
|
/** Render MCP tool results as compact self-rendered rows by default, or as the legacy boxed row. */
|
|
@@ -789,6 +806,25 @@ function globToRegExp(pattern: string): RegExp {
|
|
|
789
806
|
return new RegExp(`^${escaped}$`);
|
|
790
807
|
}
|
|
791
808
|
|
|
809
|
+
export interface ToolSelectorCandidateIndex {
|
|
810
|
+
readonly allCurrentCandidates: ReadonlySet<string>;
|
|
811
|
+
readonly matchingCountByPattern: Map<string, number>;
|
|
812
|
+
readonly matcherByPattern: Map<string, RegExp>;
|
|
813
|
+
readonly additionalCurrentCandidatesByToolName?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export function createToolSelectorCandidateIndex(
|
|
817
|
+
allCurrentCandidates: Set<string>,
|
|
818
|
+
additionalCurrentCandidatesByToolName?: ReadonlyMap<string, ReadonlySet<string>>,
|
|
819
|
+
): ToolSelectorCandidateIndex {
|
|
820
|
+
return {
|
|
821
|
+
allCurrentCandidates,
|
|
822
|
+
matchingCountByPattern: new Map<string, number>(),
|
|
823
|
+
matcherByPattern: new Map<string, RegExp>(),
|
|
824
|
+
...(additionalCurrentCandidatesByToolName ? { additionalCurrentCandidatesByToolName } : {}),
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
792
828
|
export function matchesToolPattern(candidates: Set<string>, patterns?: unknown): boolean {
|
|
793
829
|
if (!Array.isArray(patterns) || patterns.length === 0) return false;
|
|
794
830
|
|
|
@@ -805,24 +841,71 @@ export function matchesToolPattern(candidates: Set<string>, patterns?: unknown):
|
|
|
805
841
|
return false;
|
|
806
842
|
}
|
|
807
843
|
|
|
844
|
+
export type ToolSelectorCandidateContext = Set<string> | ToolSelectorCandidateIndex;
|
|
845
|
+
|
|
846
|
+
function indexHasOtherCurrentMatch(
|
|
847
|
+
index: ToolSelectorCandidateIndex,
|
|
848
|
+
toolName: string,
|
|
849
|
+
currentCandidates: Set<string>,
|
|
850
|
+
pattern: string,
|
|
851
|
+
): boolean {
|
|
852
|
+
const additionalCandidates = index.additionalCurrentCandidatesByToolName?.get(toolName);
|
|
853
|
+
const hasCandidate = (candidate: string): boolean =>
|
|
854
|
+
index.allCurrentCandidates.has(candidate) || additionalCandidates?.has(candidate) === true;
|
|
855
|
+
const isGlob = pattern.includes("*") || pattern.includes("?");
|
|
856
|
+
if (!isGlob) {
|
|
857
|
+
return hasCandidate(pattern) && !currentCandidates.has(pattern);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
let matcher = index.matcherByPattern.get(pattern);
|
|
861
|
+
if (!matcher) {
|
|
862
|
+
matcher = globToRegExp(pattern);
|
|
863
|
+
index.matcherByPattern.set(pattern, matcher);
|
|
864
|
+
}
|
|
865
|
+
let matchingCount = index.matchingCountByPattern.get(pattern);
|
|
866
|
+
if (matchingCount === undefined) {
|
|
867
|
+
matchingCount = 0;
|
|
868
|
+
for (const candidate of index.allCurrentCandidates) {
|
|
869
|
+
if (matcher.test(candidate)) matchingCount++;
|
|
870
|
+
}
|
|
871
|
+
index.matchingCountByPattern.set(pattern, matchingCount);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
let totalMatchingCount = matchingCount;
|
|
875
|
+
if (additionalCandidates) {
|
|
876
|
+
for (const candidate of additionalCandidates) {
|
|
877
|
+
if (!index.allCurrentCandidates.has(candidate) && matcher.test(candidate)) totalMatchingCount++;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (totalMatchingCount === 0) return false;
|
|
881
|
+
|
|
882
|
+
let currentMatchingCount = 0;
|
|
883
|
+
for (const candidate of currentCandidates) {
|
|
884
|
+
if (hasCandidate(candidate) && matcher.test(candidate)) currentMatchingCount++;
|
|
885
|
+
}
|
|
886
|
+
return totalMatchingCount > currentMatchingCount;
|
|
887
|
+
}
|
|
888
|
+
|
|
808
889
|
function matchesToolSelector(
|
|
809
890
|
toolName: string,
|
|
810
891
|
serverName: string,
|
|
811
892
|
prefix: ToolPrefix,
|
|
812
893
|
patterns: unknown,
|
|
813
|
-
otherCurrentCandidates?:
|
|
894
|
+
otherCurrentCandidates?: ToolSelectorCandidateContext,
|
|
814
895
|
): boolean {
|
|
896
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return false;
|
|
815
897
|
const currentCandidates = getToolNameCandidates(toolName, serverName, prefix, false);
|
|
816
898
|
if (matchesToolPattern(currentCandidates, patterns)) return true;
|
|
817
899
|
if (!otherCurrentCandidates) return matchesToolPattern(getToolNameCandidates(toolName, serverName, prefix), patterns);
|
|
818
900
|
const legacyCandidates = getToolNameCandidates(toolName, serverName, prefix);
|
|
819
901
|
for (const candidate of currentCandidates) legacyCandidates.delete(candidate);
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
902
|
+
return patterns.some(pattern => {
|
|
903
|
+
if (typeof pattern !== "string" || !matchesToolPattern(legacyCandidates, [pattern])) return false;
|
|
904
|
+
const hasCollision = otherCurrentCandidates instanceof Set
|
|
905
|
+
? matchesToolPattern(otherCurrentCandidates, [pattern])
|
|
906
|
+
: indexHasOtherCurrentMatch(otherCurrentCandidates, toolName, currentCandidates, pattern);
|
|
907
|
+
return !hasCollision;
|
|
908
|
+
});
|
|
826
909
|
}
|
|
827
910
|
|
|
828
911
|
export function isToolIncluded(
|
|
@@ -830,7 +913,7 @@ export function isToolIncluded(
|
|
|
830
913
|
serverName: string,
|
|
831
914
|
prefix: ToolPrefix,
|
|
832
915
|
includeTools?: unknown,
|
|
833
|
-
otherCurrentCandidates?:
|
|
916
|
+
otherCurrentCandidates?: ToolSelectorCandidateContext,
|
|
834
917
|
): boolean {
|
|
835
918
|
if (!Array.isArray(includeTools) || includeTools.length === 0) return true;
|
|
836
919
|
return matchesToolSelector(toolName, serverName, prefix, includeTools, otherCurrentCandidates);
|
|
@@ -841,7 +924,7 @@ export function isToolExcluded(
|
|
|
841
924
|
serverName: string,
|
|
842
925
|
prefix: ToolPrefix,
|
|
843
926
|
excludeTools?: unknown,
|
|
844
|
-
otherCurrentCandidates?:
|
|
927
|
+
otherCurrentCandidates?: ToolSelectorCandidateContext,
|
|
845
928
|
): boolean {
|
|
846
929
|
return matchesToolSelector(toolName, serverName, prefix, excludeTools, otherCurrentCandidates);
|
|
847
930
|
}
|
|
@@ -852,7 +935,7 @@ export function isToolAllowed(
|
|
|
852
935
|
prefix: ToolPrefix,
|
|
853
936
|
includeTools?: unknown,
|
|
854
937
|
excludeTools?: unknown,
|
|
855
|
-
otherCurrentCandidates?:
|
|
938
|
+
otherCurrentCandidates?: ToolSelectorCandidateContext,
|
|
856
939
|
): boolean {
|
|
857
940
|
return isToolIncluded(toolName, serverName, prefix, includeTools, otherCurrentCandidates)
|
|
858
941
|
&& !isToolExcluded(toolName, serverName, prefix, excludeTools, otherCurrentCandidates);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type AuthFetchRequest = true | string;
|
|
2
|
+
export type AuthFetchCache = "session" | "off";
|
|
3
|
+
export interface AuthFetchProfile {
|
|
4
|
+
name: string;
|
|
5
|
+
hosts: string[];
|
|
6
|
+
chromeProfile?: string;
|
|
7
|
+
redirects: "same-origin";
|
|
8
|
+
cache: AuthFetchCache;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveAuthFetchProfile(request: AuthFetchRequest): AuthFetchProfile;
|
|
11
|
+
export declare function assertAuthFetchUrl(profile: AuthFetchProfile, rawUrl: string): URL;
|
|
12
|
+
export declare function authFetchRedirectGuard(profile: AuthFetchProfile, from: URL, to: URL): void;
|
|
13
|
+
//# sourceMappingURL=auth-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-fetch.d.ts","sourceRoot":"","sources":["../../../../src/humain/web-access/vendor/auth-fetch.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,MAAM,CAAC;AAC7C,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,KAAK,CAAC;AAE/C,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,aAAa,CAAC;IACzB,KAAK,EAAE,cAAc,CAAC;CACtB;AAMD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,gBAAgB,GAAG,gBAAgB,CAenF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,CAcjF;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,GAAG,IAAI,CAI1F","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { getWebSearchConfigPath } from \"./utils.ts\";\n\nconst WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();\nconst AUTH_PROFILE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;\n\nexport type AuthFetchRequest = true | string;\nexport type AuthFetchCache = \"session\" | \"off\";\n\nexport interface AuthFetchProfile {\n\tname: string;\n\thosts: string[];\n\tchromeProfile?: string;\n\tredirects: \"same-origin\";\n\tcache: AuthFetchCache;\n}\n\ninterface AuthFetchConfigRoot {\n\tauthFetch?: unknown;\n}\n\nexport function resolveAuthFetchProfile(request: AuthFetchRequest): AuthFetchProfile {\n\tconst profiles = loadAuthFetchProfiles();\n\tif (profiles.length === 0) {\n\t\tthrow new Error(`auth requires at least one authFetch profile in ${WEB_SEARCH_CONFIG_PATH}`);\n\t}\n\tif (request === true) {\n\t\tif (profiles.length !== 1) {\n\t\t\tthrow new Error(\"auth: true requires exactly one authFetch profile; use a profile name instead\");\n\t\t}\n\t\treturn profiles[0];\n\t}\n\tconst name = request.trim();\n\tconst profile = profiles.find(candidate => candidate.name === name);\n\tif (!profile) throw new Error(`Unknown authFetch profile: ${name}`);\n\treturn profile;\n}\n\nexport function assertAuthFetchUrl(profile: AuthFetchProfile, rawUrl: string): URL {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(rawUrl);\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tthrow new Error(`Authenticated fetch requires an HTTPS URL: ${message}`);\n\t}\n\tif (url.protocol !== \"https:\") throw new Error(\"Authenticated fetch requires an HTTPS URL\");\n\tconst hostname = normalizeHostname(url.hostname);\n\tif (!profile.hosts.some(host => hostMatches(hostname, host))) {\n\t\tthrow new Error(`URL host ${hostname} is not allowed by authFetch profile ${profile.name}`);\n\t}\n\treturn url;\n}\n\nexport function authFetchRedirectGuard(profile: AuthFetchProfile, from: URL, to: URL): void {\n\tif (profile.redirects === \"same-origin\" && to.origin !== from.origin) {\n\t\tthrow new Error(`Authenticated fetch refused cross-origin redirect: ${from.origin} -> ${to.origin}`);\n\t}\n}\n\nfunction loadAuthFetchProfiles(): AuthFetchProfile[] {\n\tif (!existsSync(WEB_SEARCH_CONFIG_PATH)) return [];\n\tconst raw = readFileSync(WEB_SEARCH_CONFIG_PATH, \"utf-8\");\n\tlet parsed: AuthFetchConfigRoot;\n\ttry {\n\t\tconst value: unknown = JSON.parse(raw);\n\t\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"expected a JSON object\");\n\t\tparsed = value as AuthFetchConfigRoot;\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tthrow new Error(`Failed to parse ${WEB_SEARCH_CONFIG_PATH}: ${message}`);\n\t}\n\tif (parsed.authFetch === undefined || parsed.authFetch === null) return [];\n\tif (typeof parsed.authFetch !== \"object\" || Array.isArray(parsed.authFetch)) {\n\t\tthrow new Error(`authFetch in ${WEB_SEARCH_CONFIG_PATH} must be an object`);\n\t}\n\treturn Object.entries(parsed.authFetch as Record<string, unknown>).map(([name, value]) => parseProfile(name, value));\n}\n\nfunction parseProfile(name: string, value: unknown): AuthFetchProfile {\n\tif (!AUTH_PROFILE_NAME_PATTERN.test(name)) {\n\t\tthrow new Error(`authFetch profile name ${JSON.stringify(name)} must start with a letter and contain only letters, numbers, underscores, or hyphens`);\n\t}\n\tif (Array.isArray(value)) {\n\t\treturn { name, hosts: parseHosts(value, `authFetch.${name}`), redirects: \"same-origin\", cache: \"session\" };\n\t}\n\tif (!value || typeof value !== \"object\") {\n\t\tthrow new Error(`authFetch.${name} in ${WEB_SEARCH_CONFIG_PATH} must be an array of hosts or an object`);\n\t}\n\tconst config = value as Record<string, unknown>;\n\tif (!Array.isArray(config.hosts)) {\n\t\tthrow new Error(`authFetch.${name}.hosts in ${WEB_SEARCH_CONFIG_PATH} must be a non-empty array of hostnames`);\n\t}\n\tconst redirects = config.redirects ?? \"same-origin\";\n\tif (redirects !== \"same-origin\") {\n\t\tthrow new Error(`authFetch.${name}.redirects in ${WEB_SEARCH_CONFIG_PATH} must be \"same-origin\"`);\n\t}\n\tconst cache = config.cache ?? \"session\";\n\tif (cache !== \"session\" && cache !== \"off\") {\n\t\tthrow new Error(`authFetch.${name}.cache in ${WEB_SEARCH_CONFIG_PATH} must be \"session\" or \"off\"`);\n\t}\n\tconst chromeProfile = parseChromeProfile(config.chromeProfile, `authFetch.${name}.chromeProfile`);\n\treturn {\n\t\tname,\n\t\thosts: parseHosts(config.hosts, `authFetch.${name}.hosts`),\n\t\t...(chromeProfile ? { chromeProfile } : {}),\n\t\tredirects,\n\t\tcache,\n\t};\n}\n\nfunction parseHosts(value: unknown[], label: string): string[] {\n\tif (value.length === 0) throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a non-empty array of hostnames`);\n\tconst hosts = value.map((entry) => {\n\t\tif (typeof entry !== \"string\") throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must contain only hostnames`);\n\t\treturn parseHost(entry, label);\n\t});\n\treturn [...new Set(hosts)];\n}\n\nfunction parseHost(value: string, label: string): string {\n\tconst host = normalizeHostname(value.trim());\n\tif (!host || host.startsWith(\".\") || host.endsWith(\".\") || /\\s|[\\\\/?:#@*]/.test(host)) {\n\t\tthrow new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} contains an invalid hostname: ${JSON.stringify(value)}`);\n\t}\n\tif (host.length > 253 || !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(host)) {\n\t\tthrow new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} contains an invalid hostname: ${JSON.stringify(value)}`);\n\t}\n\treturn host;\n}\n\nfunction parseChromeProfile(value: unknown, label: string): string | undefined {\n\tif (value === undefined || value === null) return undefined;\n\tif (typeof value !== \"string\") throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a string`);\n\tconst normalized = value.trim();\n\tif (!normalized || normalized === \".\" || normalized === \"..\" || normalized.includes(\"/\") || normalized.includes(\"\\\\\")) {\n\t\tthrow new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a profile directory name, not a path`);\n\t}\n\treturn normalized;\n}\n\nfunction normalizeHostname(hostname: string): string {\n\treturn hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\").replace(/\\.$/, \"\");\n}\n\nfunction hostMatches(hostname: string, allowedHost: string): boolean {\n\treturn hostname === allowedHost || hostname.endsWith(`.${allowedHost}`);\n}\n"]}
|