multicorn-shield 0.1.15 → 0.2.0
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/dist/multicorn-proxy.js +599 -72
- package/dist/openclaw-hook/handler.js +89 -26
- package/dist/openclaw-plugin/multicorn-shield.js +111 -39
- package/package.json +17 -22
package/dist/multicorn-proxy.js
CHANGED
|
@@ -1,17 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { readFile, writeFile, mkdir, unlink } from 'fs/promises';
|
|
3
4
|
import { join } from 'path';
|
|
4
5
|
import { homedir } from 'os';
|
|
5
6
|
import { createInterface } from 'readline';
|
|
6
7
|
import { spawn } from 'child_process';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
7
9
|
import 'stream';
|
|
8
10
|
|
|
11
|
+
var style = {
|
|
12
|
+
violet: (s) => `\x1B[38;2;124;58;237m${s}\x1B[0m`,
|
|
13
|
+
violetLight: (s) => `\x1B[38;2;167;139;250m${s}\x1B[0m`,
|
|
14
|
+
green: (s) => `\x1B[38;2;34;197;94m${s}\x1B[0m`,
|
|
15
|
+
yellow: (s) => `\x1B[38;2;245;158;11m${s}\x1B[0m`,
|
|
16
|
+
red: (s) => `\x1B[38;2;239;68;68m${s}\x1B[0m`,
|
|
17
|
+
cyan: (s) => `\x1B[38;2;6;182;212m${s}\x1B[0m`,
|
|
18
|
+
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
19
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`
|
|
20
|
+
};
|
|
21
|
+
var BANNER = [
|
|
22
|
+
" \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588 \u2588\u2588\u2584 ",
|
|
23
|
+
" \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
|
|
24
|
+
" \u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588 \u2588\u2588 \u2588 \u2588 \u2588",
|
|
25
|
+
" \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
|
|
26
|
+
" \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2580 "
|
|
27
|
+
].map((line) => style.violet(line)).join("\n");
|
|
28
|
+
function withSpinner(message) {
|
|
29
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
30
|
+
let i = 0;
|
|
31
|
+
const interval = setInterval(() => {
|
|
32
|
+
const frame = frames[i % frames.length];
|
|
33
|
+
process.stderr.write(`\r${style.violet(frame ?? "\u280B")} ${message}`);
|
|
34
|
+
i++;
|
|
35
|
+
}, 80);
|
|
36
|
+
return {
|
|
37
|
+
stop(success, result) {
|
|
38
|
+
clearInterval(interval);
|
|
39
|
+
const icon = success ? style.green("\u2713") : style.red("\u2717");
|
|
40
|
+
process.stderr.write(`\r\x1B[2K${icon} ${result}
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
9
45
|
var CONFIG_DIR = join(homedir(), ".multicorn");
|
|
10
46
|
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
11
47
|
var OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
|
|
12
|
-
var
|
|
13
|
-
var OPENCLAW_PARSE_WARNING = "Multicorn Shield: Could not update ~/.openclaw/openclaw.json - please set MULTICORN_API_KEY manually.\n";
|
|
14
|
-
var OPENCLAW_UPDATED_MESSAGE = "OpenClaw config updated at ~/.openclaw/openclaw.json\n";
|
|
48
|
+
var OPENCLAW_MIN_VERSION = "2026.2.26";
|
|
15
49
|
async function loadConfig() {
|
|
16
50
|
try {
|
|
17
51
|
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
@@ -32,14 +66,13 @@ async function saveConfig(config) {
|
|
|
32
66
|
function isErrnoException(e) {
|
|
33
67
|
return typeof e === "object" && e !== null && "code" in e;
|
|
34
68
|
}
|
|
35
|
-
async function updateOpenClawConfigIfPresent(apiKey, baseUrl) {
|
|
69
|
+
async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
|
|
36
70
|
let raw;
|
|
37
71
|
try {
|
|
38
72
|
raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
39
73
|
} catch (e) {
|
|
40
74
|
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
41
|
-
|
|
42
|
-
return;
|
|
75
|
+
return "not-found";
|
|
43
76
|
}
|
|
44
77
|
throw e;
|
|
45
78
|
}
|
|
@@ -47,8 +80,7 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl) {
|
|
|
47
80
|
try {
|
|
48
81
|
obj = JSON.parse(raw);
|
|
49
82
|
} catch {
|
|
50
|
-
|
|
51
|
-
return;
|
|
83
|
+
return "parse-error";
|
|
52
84
|
}
|
|
53
85
|
let hooks = obj["hooks"];
|
|
54
86
|
if (hooks === void 0 || typeof hooks !== "object") {
|
|
@@ -77,10 +109,66 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl) {
|
|
|
77
109
|
}
|
|
78
110
|
env["MULTICORN_API_KEY"] = apiKey;
|
|
79
111
|
env["MULTICORN_BASE_URL"] = baseUrl;
|
|
112
|
+
if (agentName !== void 0) {
|
|
113
|
+
env["MULTICORN_AGENT_NAME"] = agentName;
|
|
114
|
+
const agentsList = obj["agents"];
|
|
115
|
+
const list = agentsList?.["list"];
|
|
116
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
117
|
+
const first = list[0];
|
|
118
|
+
if (first["id"] !== agentName) {
|
|
119
|
+
first["id"] = agentName;
|
|
120
|
+
first["name"] = agentName;
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
if (agentsList !== void 0 && typeof agentsList === "object") {
|
|
124
|
+
agentsList["list"] = [{ id: agentName, name: agentName }];
|
|
125
|
+
} else {
|
|
126
|
+
obj["agents"] = { list: [{ id: agentName, name: agentName }] };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
80
130
|
await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n", {
|
|
81
131
|
encoding: "utf8"
|
|
82
132
|
});
|
|
83
|
-
|
|
133
|
+
return "updated";
|
|
134
|
+
}
|
|
135
|
+
async function detectOpenClaw() {
|
|
136
|
+
let raw;
|
|
137
|
+
try {
|
|
138
|
+
raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
139
|
+
} catch (e) {
|
|
140
|
+
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
141
|
+
return { status: "not-found", version: null };
|
|
142
|
+
}
|
|
143
|
+
throw e;
|
|
144
|
+
}
|
|
145
|
+
let obj;
|
|
146
|
+
try {
|
|
147
|
+
obj = JSON.parse(raw);
|
|
148
|
+
} catch {
|
|
149
|
+
return { status: "parse-error", version: null };
|
|
150
|
+
}
|
|
151
|
+
const meta = obj["meta"];
|
|
152
|
+
if (typeof meta === "object" && meta !== null) {
|
|
153
|
+
const v = meta["lastTouchedVersion"];
|
|
154
|
+
if (typeof v === "string" && v.length > 0) {
|
|
155
|
+
return { status: "detected", version: v };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { status: "detected", version: null };
|
|
159
|
+
}
|
|
160
|
+
function isVersionAtLeast(version, minimum) {
|
|
161
|
+
const vParts = version.split(".").map(Number);
|
|
162
|
+
const mParts = minimum.split(".").map(Number);
|
|
163
|
+
const len = Math.max(vParts.length, mParts.length);
|
|
164
|
+
for (let i = 0; i < len; i++) {
|
|
165
|
+
const v = vParts[i] ?? 0;
|
|
166
|
+
const m = mParts[i] ?? 0;
|
|
167
|
+
if (Number.isNaN(v) || Number.isNaN(m)) return false;
|
|
168
|
+
if (v > m) return true;
|
|
169
|
+
if (v < m) return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
84
172
|
}
|
|
85
173
|
async function validateApiKey(apiKey, baseUrl) {
|
|
86
174
|
try {
|
|
@@ -106,7 +194,116 @@ async function validateApiKey(apiKey, baseUrl) {
|
|
|
106
194
|
};
|
|
107
195
|
}
|
|
108
196
|
}
|
|
197
|
+
function normalizeAgentName(raw) {
|
|
198
|
+
return raw.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
199
|
+
}
|
|
200
|
+
async function isOpenClawConnected() {
|
|
201
|
+
try {
|
|
202
|
+
const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
203
|
+
const obj = JSON.parse(raw);
|
|
204
|
+
const hooks = obj["hooks"];
|
|
205
|
+
const internal = hooks?.["internal"];
|
|
206
|
+
const entries = internal?.["entries"];
|
|
207
|
+
const shield = entries?.["multicorn-shield"];
|
|
208
|
+
const env = shield?.["env"];
|
|
209
|
+
const key = env?.["MULTICORN_API_KEY"];
|
|
210
|
+
return typeof key === "string" && key.length > 0;
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function isClaudeCodeConnected() {
|
|
216
|
+
try {
|
|
217
|
+
return existsSync(join(homedir(), ".claude", "plugins", "cache", "multicorn-shield"));
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function getClaudeDesktopConfigPath() {
|
|
223
|
+
switch (process.platform) {
|
|
224
|
+
case "win32":
|
|
225
|
+
return join(
|
|
226
|
+
process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"),
|
|
227
|
+
"Claude",
|
|
228
|
+
"claude_desktop_config.json"
|
|
229
|
+
);
|
|
230
|
+
case "linux":
|
|
231
|
+
return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
232
|
+
default:
|
|
233
|
+
return join(
|
|
234
|
+
homedir(),
|
|
235
|
+
"Library",
|
|
236
|
+
"Application Support",
|
|
237
|
+
"Claude",
|
|
238
|
+
"claude_desktop_config.json"
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function updateClaudeDesktopConfig(agentName, mcpServerCommand, overwrite = false) {
|
|
243
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(agentName)) {
|
|
244
|
+
throw new Error("Agent name must contain only letters, numbers, hyphens, and underscores");
|
|
245
|
+
}
|
|
246
|
+
const configPath = getClaudeDesktopConfigPath();
|
|
247
|
+
let obj = {};
|
|
248
|
+
let fileExists = false;
|
|
249
|
+
try {
|
|
250
|
+
const raw = await readFile(configPath, "utf8");
|
|
251
|
+
fileExists = true;
|
|
252
|
+
try {
|
|
253
|
+
obj = JSON.parse(raw);
|
|
254
|
+
} catch {
|
|
255
|
+
return "parse-error";
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
259
|
+
fileExists = false;
|
|
260
|
+
} else {
|
|
261
|
+
throw e;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
let mcpServers = obj["mcpServers"];
|
|
265
|
+
if (mcpServers === void 0 || typeof mcpServers !== "object") {
|
|
266
|
+
mcpServers = {};
|
|
267
|
+
obj["mcpServers"] = mcpServers;
|
|
268
|
+
}
|
|
269
|
+
if (mcpServers[agentName] !== void 0 && !overwrite) {
|
|
270
|
+
return "skipped";
|
|
271
|
+
}
|
|
272
|
+
const commandParts = mcpServerCommand.trim().split(/\s+/);
|
|
273
|
+
mcpServers[agentName] = {
|
|
274
|
+
command: "npx",
|
|
275
|
+
args: ["multicorn-proxy", "--wrap", ...commandParts, "--agent-name", agentName]
|
|
276
|
+
};
|
|
277
|
+
const configDir = join(configPath, "..");
|
|
278
|
+
if (!fileExists) {
|
|
279
|
+
await mkdir(configDir, { recursive: true });
|
|
280
|
+
}
|
|
281
|
+
await writeFile(configPath, JSON.stringify(obj, null, 2) + "\n", { encoding: "utf8" });
|
|
282
|
+
return fileExists ? "updated" : "created";
|
|
283
|
+
}
|
|
284
|
+
async function isClaudeDesktopConnected() {
|
|
285
|
+
try {
|
|
286
|
+
const raw = await readFile(getClaudeDesktopConfigPath(), "utf8");
|
|
287
|
+
const obj = JSON.parse(raw);
|
|
288
|
+
const mcpServers = obj["mcpServers"];
|
|
289
|
+
if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
|
|
290
|
+
for (const entry of Object.values(mcpServers)) {
|
|
291
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
292
|
+
const args = entry["args"];
|
|
293
|
+
if (Array.isArray(args) && args.includes("multicorn-proxy")) return true;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
109
300
|
async function runInit(baseUrl = "https://api.multicorn.ai") {
|
|
301
|
+
if (!process.stdin.isTTY) {
|
|
302
|
+
process.stderr.write(
|
|
303
|
+
style.red("Error: interactive terminal required. Cannot run init with piped input.") + "\n"
|
|
304
|
+
);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
110
307
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
111
308
|
function ask(question) {
|
|
112
309
|
return new Promise((resolve) => {
|
|
@@ -115,39 +312,332 @@ async function runInit(baseUrl = "https://api.multicorn.ai") {
|
|
|
115
312
|
});
|
|
116
313
|
});
|
|
117
314
|
}
|
|
118
|
-
process.stderr.write("
|
|
119
|
-
process.stderr.write("
|
|
120
|
-
|
|
121
|
-
|
|
315
|
+
process.stderr.write("\n" + BANNER + "\n");
|
|
316
|
+
process.stderr.write(style.dim("Agent governance for the AI era") + "\n\n");
|
|
317
|
+
process.stderr.write(style.bold(style.violet("Multicorn Shield proxy setup")) + "\n\n");
|
|
318
|
+
process.stderr.write(
|
|
319
|
+
style.dim("Get your API key at https://app.multicorn.ai/settings/api-keys") + "\n\n"
|
|
320
|
+
);
|
|
321
|
+
let apiKey = "";
|
|
322
|
+
const existing = await loadConfig().catch(() => null);
|
|
323
|
+
if (existing !== null && existing.apiKey.startsWith("mcs_") && existing.apiKey.length >= 8) {
|
|
324
|
+
const masked = "mcs_..." + existing.apiKey.slice(-4);
|
|
325
|
+
process.stderr.write("Found existing API key: " + style.cyan(masked) + "\n");
|
|
326
|
+
const answer = await ask("Use this key? (Y/n) ");
|
|
327
|
+
if (answer.trim().toLowerCase() !== "n") {
|
|
328
|
+
apiKey = existing.apiKey;
|
|
329
|
+
if (baseUrl === "https://api.multicorn.ai") {
|
|
330
|
+
baseUrl = existing.baseUrl;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
while (apiKey.length === 0) {
|
|
122
335
|
const input = await ask("API key (starts with mcs_): ");
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
process.stderr.write("API key is required
|
|
336
|
+
const key = input.trim();
|
|
337
|
+
if (key.length === 0) {
|
|
338
|
+
process.stderr.write(style.red("API key is required.") + "\n");
|
|
126
339
|
continue;
|
|
127
340
|
}
|
|
128
|
-
|
|
129
|
-
|
|
341
|
+
const spinner = withSpinner("Validating key...");
|
|
342
|
+
let result;
|
|
343
|
+
try {
|
|
344
|
+
result = await validateApiKey(key, baseUrl);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
spinner.stop(false, "Validation failed");
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
130
349
|
if (!result.valid) {
|
|
131
|
-
|
|
132
|
-
`);
|
|
350
|
+
spinner.stop(false, result.error ?? "Validation failed. Try again.");
|
|
133
351
|
continue;
|
|
134
352
|
}
|
|
135
|
-
|
|
353
|
+
spinner.stop(true, "Key validated");
|
|
354
|
+
apiKey = key;
|
|
136
355
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
} catch {
|
|
356
|
+
const configuredPlatforms = /* @__PURE__ */ new Set();
|
|
357
|
+
let lastConfig = { apiKey, baseUrl };
|
|
358
|
+
let configuring = true;
|
|
359
|
+
while (configuring) {
|
|
142
360
|
process.stderr.write(
|
|
143
|
-
"
|
|
361
|
+
"\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n"
|
|
144
362
|
);
|
|
363
|
+
const platformLabels = ["OpenClaw", "Claude Code", "Claude Desktop", "Other MCP Agent"];
|
|
364
|
+
const openClawConnected = await isOpenClawConnected();
|
|
365
|
+
const claudeCodeConnected = isClaudeCodeConnected();
|
|
366
|
+
const claudeDesktopConnected = await isClaudeDesktopConnected();
|
|
367
|
+
for (let i = 0; i < platformLabels.length; i++) {
|
|
368
|
+
const sessionMarker = configuredPlatforms.has(i + 1) ? " " + style.green("\u2713") : "";
|
|
369
|
+
let connectedMarker = "";
|
|
370
|
+
if (!configuredPlatforms.has(i + 1)) {
|
|
371
|
+
if (i === 0 && openClawConnected) {
|
|
372
|
+
connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
|
|
373
|
+
} else if (i === 1 && claudeCodeConnected) {
|
|
374
|
+
connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
|
|
375
|
+
} else if (i === 2 && claudeDesktopConnected) {
|
|
376
|
+
connectedMarker = " " + style.green("\u2713") + style.dim(" connected");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
process.stderr.write(
|
|
380
|
+
` ${style.violet(String(i + 1))}. ${platformLabels[i] ?? ""}${sessionMarker}${connectedMarker}
|
|
381
|
+
`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
let selection = 0;
|
|
385
|
+
while (selection === 0) {
|
|
386
|
+
const input = await ask("Select (1-4): ");
|
|
387
|
+
const num = parseInt(input.trim(), 10);
|
|
388
|
+
if (num >= 1 && num <= 4) {
|
|
389
|
+
selection = num;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
let agentName = "";
|
|
393
|
+
while (agentName.length === 0) {
|
|
394
|
+
const input = await ask("\nWhat would you like to call this agent? ");
|
|
395
|
+
if (input.trim().length === 0) continue;
|
|
396
|
+
const transformed = normalizeAgentName(input);
|
|
397
|
+
if (transformed.length === 0) {
|
|
398
|
+
process.stderr.write(
|
|
399
|
+
style.red("Agent name must contain letters or numbers. Please try again.") + "\n"
|
|
400
|
+
);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (transformed !== input.trim()) {
|
|
404
|
+
process.stderr.write(style.yellow("Agent name set to: ") + style.cyan(transformed) + "\n");
|
|
405
|
+
}
|
|
406
|
+
agentName = transformed;
|
|
407
|
+
}
|
|
408
|
+
if (selection === 1) {
|
|
409
|
+
let detection;
|
|
410
|
+
try {
|
|
411
|
+
detection = await detectOpenClaw();
|
|
412
|
+
} catch (error) {
|
|
413
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
414
|
+
process.stderr.write(style.red("\u2717") + ` Failed to read OpenClaw config: ${detail}
|
|
415
|
+
`);
|
|
416
|
+
rl.close();
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
if (detection.status === "not-found") {
|
|
420
|
+
process.stderr.write(
|
|
421
|
+
style.red("\u2717") + " OpenClaw is not installed. Install OpenClaw first, then run npx multicorn-proxy init again.\n"
|
|
422
|
+
);
|
|
423
|
+
rl.close();
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
if (detection.status === "parse-error") {
|
|
427
|
+
process.stderr.write(
|
|
428
|
+
style.red("\u2717") + " Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually.\n"
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
if (detection.status === "detected") {
|
|
432
|
+
if (detection.version !== null) {
|
|
433
|
+
process.stderr.write(
|
|
434
|
+
style.green("\u2713") + ` OpenClaw detected ${style.dim(`(${detection.version})`)}
|
|
435
|
+
`
|
|
436
|
+
);
|
|
437
|
+
if (isVersionAtLeast(detection.version, OPENCLAW_MIN_VERSION)) {
|
|
438
|
+
process.stderr.write(
|
|
439
|
+
style.green("\u2713") + " " + style.green("Version compatible") + "\n"
|
|
440
|
+
);
|
|
441
|
+
} else {
|
|
442
|
+
process.stderr.write(
|
|
443
|
+
style.yellow("\u26A0") + ` Shield has been tested with OpenClaw ${style.cyan(OPENCLAW_MIN_VERSION)} and above. Your version (${detection.version}) may work but is untested. We recommend upgrading to at least ${style.cyan(OPENCLAW_MIN_VERSION)}.
|
|
444
|
+
`
|
|
445
|
+
);
|
|
446
|
+
const answer = await ask("Continue anyway? (y/N) ");
|
|
447
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
448
|
+
rl.close();
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
process.stderr.write(
|
|
454
|
+
style.yellow("\u26A0") + " Could not detect OpenClaw version. Continuing anyway.\n"
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const spinner = withSpinner("Updating OpenClaw config...");
|
|
458
|
+
try {
|
|
459
|
+
const result = await updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName);
|
|
460
|
+
if (result === "not-found") {
|
|
461
|
+
spinner.stop(false, "OpenClaw config disappeared unexpectedly.");
|
|
462
|
+
rl.close();
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
if (result === "parse-error") {
|
|
466
|
+
spinner.stop(
|
|
467
|
+
false,
|
|
468
|
+
"Could not update OpenClaw config. Set MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually."
|
|
469
|
+
);
|
|
470
|
+
} else {
|
|
471
|
+
spinner.stop(
|
|
472
|
+
true,
|
|
473
|
+
"OpenClaw config updated at " + style.cyan("~/.openclaw/openclaw.json")
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
478
|
+
spinner.stop(false, `Failed to update OpenClaw config: ${detail}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
} else if (selection === 2) {
|
|
482
|
+
process.stderr.write("\nTo connect Claude Code to Shield:\n\n");
|
|
483
|
+
process.stderr.write(
|
|
484
|
+
" " + style.bold("Step 1") + " - Add the Multicorn marketplace:\n " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n\n"
|
|
485
|
+
);
|
|
486
|
+
process.stderr.write(
|
|
487
|
+
" " + style.bold("Step 2") + " - Install the plugin:\n " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n\n"
|
|
488
|
+
);
|
|
489
|
+
process.stderr.write(
|
|
490
|
+
" " + style.bold("Step 3") + " - Start Claude Code:\n " + style.cyan("claude") + "\n\n"
|
|
491
|
+
);
|
|
492
|
+
process.stderr.write(
|
|
493
|
+
style.dim("Run /plugin inside Claude Code to confirm multicorn-shield is installed.") + "\n"
|
|
494
|
+
);
|
|
495
|
+
process.stderr.write(
|
|
496
|
+
style.dim("Requires Claude Code to be installed. Get it at https://code.claude.com") + "\n"
|
|
497
|
+
);
|
|
498
|
+
} else if (selection === 3) {
|
|
499
|
+
const mcpCommand = await ask(
|
|
500
|
+
"\nWhat MCP server should Shield govern for this agent?\nThis is the command you'd normally use to start your MCP server.\nExample: npx -y @modelcontextprotocol/server-filesystem /tmp\nLeave blank to skip and configure later: "
|
|
501
|
+
);
|
|
502
|
+
if (mcpCommand.trim().length === 0) {
|
|
503
|
+
const configPath = getClaudeDesktopConfigPath();
|
|
504
|
+
process.stderr.write("\n" + style.dim("Add this to your Claude Desktop config at:") + "\n");
|
|
505
|
+
process.stderr.write(" " + style.cyan(configPath) + "\n\n");
|
|
506
|
+
const snippet = JSON.stringify(
|
|
507
|
+
{
|
|
508
|
+
mcpServers: {
|
|
509
|
+
[agentName]: {
|
|
510
|
+
command: "npx",
|
|
511
|
+
args: [
|
|
512
|
+
"multicorn-proxy",
|
|
513
|
+
"--wrap",
|
|
514
|
+
"<your-mcp-server-command>",
|
|
515
|
+
"--agent-name",
|
|
516
|
+
agentName
|
|
517
|
+
]
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
null,
|
|
522
|
+
2
|
|
523
|
+
);
|
|
524
|
+
process.stderr.write(style.cyan(snippet) + "\n\n");
|
|
525
|
+
} else {
|
|
526
|
+
let shouldWrite = true;
|
|
527
|
+
const spinner = withSpinner("Updating Claude Desktop config...");
|
|
528
|
+
try {
|
|
529
|
+
let result = await updateClaudeDesktopConfig(agentName, mcpCommand.trim());
|
|
530
|
+
if (result === "skipped") {
|
|
531
|
+
spinner.stop(false, `Agent "${agentName}" already exists in Claude Desktop config.`);
|
|
532
|
+
const overwrite = await ask("Overwrite the existing entry? (y/N) ");
|
|
533
|
+
if (overwrite.trim().toLowerCase() === "y") {
|
|
534
|
+
const retrySpinner = withSpinner("Updating Claude Desktop config...");
|
|
535
|
+
result = await updateClaudeDesktopConfig(agentName, mcpCommand.trim(), true);
|
|
536
|
+
retrySpinner.stop(
|
|
537
|
+
true,
|
|
538
|
+
"Claude Desktop config updated at " + style.cyan(getClaudeDesktopConfigPath())
|
|
539
|
+
);
|
|
540
|
+
} else {
|
|
541
|
+
shouldWrite = false;
|
|
542
|
+
process.stderr.write(style.dim("Skipped. Existing config left unchanged.") + "\n");
|
|
543
|
+
}
|
|
544
|
+
} else if (result === "parse-error") {
|
|
545
|
+
spinner.stop(false, "Claude Desktop config file contains invalid JSON.");
|
|
546
|
+
const configPath = getClaudeDesktopConfigPath();
|
|
547
|
+
process.stderr.write(
|
|
548
|
+
style.yellow("\u26A0") + " Fix the JSON in " + style.cyan(configPath) + " or add this entry manually:\n\n"
|
|
549
|
+
);
|
|
550
|
+
const snippet = JSON.stringify(
|
|
551
|
+
{
|
|
552
|
+
mcpServers: {
|
|
553
|
+
[agentName]: {
|
|
554
|
+
command: "npx",
|
|
555
|
+
args: [
|
|
556
|
+
"multicorn-proxy",
|
|
557
|
+
"--wrap",
|
|
558
|
+
...mcpCommand.trim().split(/\s+/),
|
|
559
|
+
"--agent-name",
|
|
560
|
+
agentName
|
|
561
|
+
]
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
null,
|
|
566
|
+
2
|
|
567
|
+
);
|
|
568
|
+
process.stderr.write(style.cyan(snippet) + "\n\n");
|
|
569
|
+
} else {
|
|
570
|
+
const verb = result === "created" ? "Created" : "Updated";
|
|
571
|
+
spinner.stop(
|
|
572
|
+
true,
|
|
573
|
+
verb + " Claude Desktop config at " + style.cyan(getClaudeDesktopConfigPath())
|
|
574
|
+
);
|
|
575
|
+
process.stderr.write(style.dim("Restart Claude Desktop to pick up changes.") + "\n");
|
|
576
|
+
}
|
|
577
|
+
} catch (error) {
|
|
578
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
579
|
+
spinner.stop(false, `Failed to update Claude Desktop config: ${detail}`);
|
|
580
|
+
shouldWrite = false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
process.stderr.write("\n" + style.dim("Start the Shield proxy with:") + "\n");
|
|
585
|
+
process.stderr.write(
|
|
586
|
+
" " + style.cyan(
|
|
587
|
+
`npx multicorn-proxy --wrap <your-mcp-server-command> --agent-name ${agentName}`
|
|
588
|
+
) + "\n\n"
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
configuredPlatforms.add(selection);
|
|
592
|
+
lastConfig = { apiKey, baseUrl, agentName };
|
|
593
|
+
try {
|
|
594
|
+
await saveConfig(lastConfig);
|
|
595
|
+
process.stderr.write(style.green("\u2713") + ` Config saved to ${style.cyan(CONFIG_PATH)}
|
|
596
|
+
`);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
599
|
+
process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
|
|
600
|
+
}
|
|
601
|
+
if (configuredPlatforms.size >= 4) {
|
|
602
|
+
configuring = false;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const another = await ask("\nWould you like to configure another agent? (y/N) ");
|
|
606
|
+
if (another.trim().toLowerCase() !== "y") {
|
|
607
|
+
configuring = false;
|
|
608
|
+
}
|
|
145
609
|
}
|
|
146
|
-
|
|
147
|
-
|
|
610
|
+
rl.close();
|
|
611
|
+
process.stderr.write("\n" + style.bold(style.violet("Setup complete")) + "\n\n");
|
|
612
|
+
const allPlatforms = ["OpenClaw", "Claude Code", "Claude Desktop", "Other MCP Agent"];
|
|
613
|
+
for (const idx of configuredPlatforms) {
|
|
614
|
+
process.stderr.write(` ${style.green("\u2713")} ${allPlatforms[idx - 1] ?? ""}
|
|
148
615
|
`);
|
|
149
|
-
|
|
150
|
-
|
|
616
|
+
}
|
|
617
|
+
process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
|
|
618
|
+
const blocks = [];
|
|
619
|
+
if (configuredPlatforms.has(1)) {
|
|
620
|
+
blocks.push(
|
|
621
|
+
"\n" + style.bold("To complete your OpenClaw setup:") + "\n \u2192 Restart your gateway: " + style.cyan("openclaw gateway restart") + "\n \u2192 Start a session: " + style.cyan("openclaw tui") + "\n"
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
if (configuredPlatforms.has(2)) {
|
|
625
|
+
blocks.push(
|
|
626
|
+
"\n" + style.bold("To complete your Claude Code setup:") + "\n \u2192 Add marketplace: " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n \u2192 Install plugin: " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n"
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
if (configuredPlatforms.has(3)) {
|
|
630
|
+
blocks.push(
|
|
631
|
+
"\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (configuredPlatforms.has(4)) {
|
|
635
|
+
blocks.push(
|
|
636
|
+
"\n" + style.bold("To complete your Other MCP Agent setup:") + "\n \u2192 Start your agent with: " + style.cyan("npx multicorn-proxy --wrap <your-server> --agent-name <name>") + "\n"
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
process.stderr.write(blocks.join("") + "\n");
|
|
640
|
+
return lastConfig;
|
|
151
641
|
}
|
|
152
642
|
function isProxyConfig(value) {
|
|
153
643
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -610,51 +1100,53 @@ function capitalize(str) {
|
|
|
610
1100
|
}
|
|
611
1101
|
var MULTICORN_DIR = join(homedir(), ".multicorn");
|
|
612
1102
|
var SCOPES_PATH = join(MULTICORN_DIR, "scopes.json");
|
|
613
|
-
var
|
|
614
|
-
|
|
615
|
-
|
|
1103
|
+
var CACHE_META_PATH = join(MULTICORN_DIR, "cache-meta.json");
|
|
1104
|
+
function cacheKey(agentName, apiKey) {
|
|
1105
|
+
return createHash("sha256").update(`${agentName}:${apiKey}`).digest("hex").slice(0, 16);
|
|
1106
|
+
}
|
|
1107
|
+
async function ensureCacheIdentity(apiKey) {
|
|
1108
|
+
const currentHash = createHash("sha256").update(apiKey).digest("hex");
|
|
1109
|
+
let storedHash = null;
|
|
616
1110
|
try {
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
return url.toString();
|
|
622
|
-
}
|
|
623
|
-
if (url.hostname === "api.multicorn.ai") {
|
|
624
|
-
url.hostname = "app.multicorn.ai";
|
|
625
|
-
return url.toString();
|
|
626
|
-
}
|
|
627
|
-
if (url.hostname.includes("api")) {
|
|
628
|
-
url.hostname = url.hostname.replace("api", "app");
|
|
629
|
-
return url.toString();
|
|
1111
|
+
const raw = await readFile(CACHE_META_PATH, "utf8");
|
|
1112
|
+
const meta = JSON.parse(raw);
|
|
1113
|
+
if (typeof meta === "object" && meta !== null && "apiKeyHash" in meta) {
|
|
1114
|
+
storedHash = meta.apiKeyHash;
|
|
630
1115
|
}
|
|
631
|
-
if (url.protocol === "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
|
|
632
|
-
return "https://app.multicorn.ai";
|
|
633
|
-
}
|
|
634
|
-
return "https://app.multicorn.ai";
|
|
635
1116
|
} catch {
|
|
636
|
-
return "https://app.multicorn.ai";
|
|
637
1117
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
Object.setPrototypeOf(this, _ShieldAuthError.prototype);
|
|
1118
|
+
if (storedHash === null || storedHash !== currentHash) {
|
|
1119
|
+
try {
|
|
1120
|
+
await unlink(SCOPES_PATH);
|
|
1121
|
+
} catch {
|
|
1122
|
+
}
|
|
644
1123
|
}
|
|
645
|
-
|
|
646
|
-
|
|
1124
|
+
if (storedHash !== currentHash) {
|
|
1125
|
+
await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
|
|
1126
|
+
await writeFile(CACHE_META_PATH, JSON.stringify({ apiKeyHash: currentHash }, null, 2) + "\n", {
|
|
1127
|
+
encoding: "utf8",
|
|
1128
|
+
mode: 384
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
async function loadCachedScopes(agentName, apiKey) {
|
|
1133
|
+
if (apiKey.length === 0) return null;
|
|
1134
|
+
await ensureCacheIdentity(apiKey);
|
|
1135
|
+
const key = cacheKey(agentName, apiKey);
|
|
647
1136
|
try {
|
|
648
1137
|
const raw = await readFile(SCOPES_PATH, "utf8");
|
|
649
1138
|
const parsed = JSON.parse(raw);
|
|
650
1139
|
if (!isScopesCacheFile(parsed)) return null;
|
|
651
|
-
const entry = parsed[
|
|
1140
|
+
const entry = parsed[key];
|
|
652
1141
|
return entry?.scopes ?? null;
|
|
653
1142
|
} catch {
|
|
654
1143
|
return null;
|
|
655
1144
|
}
|
|
656
1145
|
}
|
|
657
|
-
async function saveCachedScopes(agentName, agentId, scopes) {
|
|
1146
|
+
async function saveCachedScopes(agentName, agentId, scopes, apiKey) {
|
|
1147
|
+
if (apiKey.length === 0) return;
|
|
1148
|
+
await ensureCacheIdentity(apiKey);
|
|
1149
|
+
const key = cacheKey(agentName, apiKey);
|
|
658
1150
|
await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
|
|
659
1151
|
let existing = {};
|
|
660
1152
|
try {
|
|
@@ -665,7 +1157,7 @@ async function saveCachedScopes(agentName, agentId, scopes) {
|
|
|
665
1157
|
}
|
|
666
1158
|
const updated = {
|
|
667
1159
|
...existing,
|
|
668
|
-
[
|
|
1160
|
+
[key]: {
|
|
669
1161
|
agentId,
|
|
670
1162
|
scopes,
|
|
671
1163
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -676,6 +1168,44 @@ async function saveCachedScopes(agentName, agentId, scopes) {
|
|
|
676
1168
|
mode: 384
|
|
677
1169
|
});
|
|
678
1170
|
}
|
|
1171
|
+
function isScopesCacheFile(value) {
|
|
1172
|
+
return typeof value === "object" && value !== null;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/proxy/consent.ts
|
|
1176
|
+
var CONSENT_POLL_INTERVAL_MS = 3e3;
|
|
1177
|
+
var CONSENT_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1178
|
+
function deriveDashboardUrl(baseUrl) {
|
|
1179
|
+
try {
|
|
1180
|
+
const url = new URL(baseUrl);
|
|
1181
|
+
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
1182
|
+
url.port = "5173";
|
|
1183
|
+
url.protocol = "http:";
|
|
1184
|
+
return url.toString();
|
|
1185
|
+
}
|
|
1186
|
+
if (url.hostname === "api.multicorn.ai") {
|
|
1187
|
+
url.hostname = "app.multicorn.ai";
|
|
1188
|
+
return url.toString();
|
|
1189
|
+
}
|
|
1190
|
+
if (url.hostname.includes("api")) {
|
|
1191
|
+
url.hostname = url.hostname.replace("api", "app");
|
|
1192
|
+
return url.toString();
|
|
1193
|
+
}
|
|
1194
|
+
if (url.protocol === "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
|
|
1195
|
+
return "https://app.multicorn.ai";
|
|
1196
|
+
}
|
|
1197
|
+
return "https://app.multicorn.ai";
|
|
1198
|
+
} catch {
|
|
1199
|
+
return "https://app.multicorn.ai";
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
var ShieldAuthError = class _ShieldAuthError extends Error {
|
|
1203
|
+
constructor(message) {
|
|
1204
|
+
super(message);
|
|
1205
|
+
this.name = "ShieldAuthError";
|
|
1206
|
+
Object.setPrototypeOf(this, _ShieldAuthError.prototype);
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
679
1209
|
async function findAgentByName(agentName, apiKey, baseUrl) {
|
|
680
1210
|
let response;
|
|
681
1211
|
try {
|
|
@@ -796,7 +1326,7 @@ Waiting for you to grant access in the Multicorn dashboard...
|
|
|
796
1326
|
);
|
|
797
1327
|
}
|
|
798
1328
|
async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
|
|
799
|
-
const cachedScopes = await loadCachedScopes(agentName);
|
|
1329
|
+
const cachedScopes = await loadCachedScopes(agentName, apiKey);
|
|
800
1330
|
if (cachedScopes !== null && cachedScopes.length > 0) {
|
|
801
1331
|
logger.debug("Loaded scopes from cache.", { agent: agentName, count: cachedScopes.length });
|
|
802
1332
|
return { id: "", name: agentName, scopes: cachedScopes };
|
|
@@ -824,7 +1354,7 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
|
|
|
824
1354
|
}
|
|
825
1355
|
const scopes = await fetchGrantedScopes(agent.id, apiKey, baseUrl);
|
|
826
1356
|
if (scopes.length > 0) {
|
|
827
|
-
await saveCachedScopes(agentName, agent.id, scopes);
|
|
1357
|
+
await saveCachedScopes(agentName, agent.id, scopes, apiKey);
|
|
828
1358
|
}
|
|
829
1359
|
return { ...agent, scopes };
|
|
830
1360
|
}
|
|
@@ -862,9 +1392,6 @@ function isPermissionShape(value) {
|
|
|
862
1392
|
const obj = value;
|
|
863
1393
|
return typeof obj["service"] === "string" && typeof obj["read"] === "boolean" && typeof obj["write"] === "boolean" && typeof obj["execute"] === "boolean" && (obj["revoked_at"] === null || typeof obj["revoked_at"] === "string");
|
|
864
1394
|
}
|
|
865
|
-
function isScopesCacheFile(value) {
|
|
866
|
-
return typeof value === "object" && value !== null;
|
|
867
|
-
}
|
|
868
1395
|
|
|
869
1396
|
// src/proxy/index.ts
|
|
870
1397
|
var DEFAULT_SCOPE_REFRESH_INTERVAL_MS = 6e4;
|
|
@@ -892,7 +1419,7 @@ function createProxyServer(config) {
|
|
|
892
1419
|
const scopes = await fetchGrantedScopes(agentId, config.apiKey, config.baseUrl);
|
|
893
1420
|
grantedScopes = scopes;
|
|
894
1421
|
if (scopes.length > 0) {
|
|
895
|
-
await saveCachedScopes(config.agentName, agentId, scopes);
|
|
1422
|
+
await saveCachedScopes(config.agentName, agentId, scopes, config.apiKey);
|
|
896
1423
|
}
|
|
897
1424
|
config.logger.debug("Scopes refreshed.", { count: scopes.length });
|
|
898
1425
|
} catch (error) {
|
|
@@ -921,7 +1448,7 @@ function createProxyServer(config) {
|
|
|
921
1448
|
scopeParam
|
|
922
1449
|
);
|
|
923
1450
|
grantedScopes = scopes;
|
|
924
|
-
await saveCachedScopes(config.agentName, agentId, scopes);
|
|
1451
|
+
await saveCachedScopes(config.agentName, agentId, scopes, config.apiKey);
|
|
925
1452
|
} finally {
|
|
926
1453
|
consentInProgress = false;
|
|
927
1454
|
}
|
|
@@ -1286,7 +1813,7 @@ Use https:// or http://localhost for local development.
|
|
|
1286
1813
|
);
|
|
1287
1814
|
process.exit(1);
|
|
1288
1815
|
}
|
|
1289
|
-
const agentName = cli.agentName.length > 0 ? cli.agentName : deriveAgentName(cli.wrapCommand);
|
|
1816
|
+
const agentName = cli.agentName.length > 0 ? cli.agentName : config.agentName !== void 0 && config.agentName.length > 0 ? config.agentName : deriveAgentName(cli.wrapCommand);
|
|
1290
1817
|
const finalBaseUrl = cli.baseUrl !== "https://api.multicorn.ai" ? cli.baseUrl : config.baseUrl;
|
|
1291
1818
|
const finalDashboardUrl = cli.dashboardUrl !== "" ? cli.dashboardUrl : deriveDashboardUrl(finalBaseUrl);
|
|
1292
1819
|
const proxy = createProxyServer({
|