localant 1.0.2 → 1.1.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/README.ja.md +185 -0
- package/README.md +137 -20
- package/SECURITY.md +63 -8
- package/assets/hero.png +0 -0
- package/assets/localant-icon.png +0 -0
- package/examples/skills/article-publisher/README.md +41 -0
- package/examples/skills/article-publisher/package.json +9 -0
- package/examples/skills/article-publisher/skill.json +134 -0
- package/examples/skills/article-publisher/src/index.ts +186 -0
- package/examples/skills/article-publisher/tests/skill.test.ts +72 -0
- package/package.json +15 -5
- package/packages/cli/dist/autostart.d.ts +14 -0
- package/packages/cli/dist/autostart.d.ts.map +1 -0
- package/packages/cli/dist/autostart.js +98 -0
- package/packages/cli/dist/autostart.js.map +1 -0
- package/packages/cli/dist/bin.js +214 -2
- package/packages/cli/dist/bin.js.map +1 -1
- package/packages/cli/dist/runtime.d.ts.map +1 -1
- package/packages/cli/dist/runtime.js +56 -8
- package/packages/cli/dist/runtime.js.map +1 -1
- package/packages/cli/dist/serveo-setup.d.ts +37 -0
- package/packages/cli/dist/serveo-setup.d.ts.map +1 -0
- package/packages/cli/dist/serveo-setup.js +168 -0
- package/packages/cli/dist/serveo-setup.js.map +1 -0
- package/packages/cli/dist/util.d.ts +6 -0
- package/packages/cli/dist/util.d.ts.map +1 -1
- package/packages/cli/dist/util.js +20 -0
- package/packages/cli/dist/util.js.map +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/dashboard/dist/index.d.ts +5 -4
- package/packages/dashboard/dist/index.d.ts.map +1 -1
- package/packages/dashboard/dist/index.js +781 -44
- package/packages/dashboard/dist/index.js.map +1 -1
- package/packages/gateway/dist/gateway.d.ts +14 -1
- package/packages/gateway/dist/gateway.d.ts.map +1 -1
- package/packages/gateway/dist/gateway.js +59 -6
- package/packages/gateway/dist/gateway.js.map +1 -1
- package/packages/gateway/dist/index.d.ts +3 -0
- package/packages/gateway/dist/index.d.ts.map +1 -1
- package/packages/gateway/dist/index.js +3 -0
- package/packages/gateway/dist/index.js.map +1 -1
- package/packages/gateway/dist/managers/coding-agent-manager.d.ts +14 -0
- package/packages/gateway/dist/managers/coding-agent-manager.d.ts.map +1 -1
- package/packages/gateway/dist/managers/coding-agent-manager.js +21 -2
- package/packages/gateway/dist/managers/coding-agent-manager.js.map +1 -1
- package/packages/gateway/dist/managers/fs-manager.d.ts +73 -0
- package/packages/gateway/dist/managers/fs-manager.d.ts.map +1 -1
- package/packages/gateway/dist/managers/fs-manager.js +290 -6
- package/packages/gateway/dist/managers/fs-manager.js.map +1 -1
- package/packages/gateway/dist/managers/git-manager.d.ts +6 -0
- package/packages/gateway/dist/managers/git-manager.d.ts.map +1 -1
- package/packages/gateway/dist/managers/git-manager.js +24 -0
- package/packages/gateway/dist/managers/git-manager.js.map +1 -1
- package/packages/gateway/dist/managers/lsp-service.d.ts +88 -0
- package/packages/gateway/dist/managers/lsp-service.d.ts.map +1 -0
- package/packages/gateway/dist/managers/lsp-service.js +249 -0
- package/packages/gateway/dist/managers/lsp-service.js.map +1 -0
- package/packages/gateway/dist/managers/mcp-bridge.d.ts +2 -1
- package/packages/gateway/dist/managers/mcp-bridge.d.ts.map +1 -1
- package/packages/gateway/dist/managers/mcp-bridge.js +23 -2
- package/packages/gateway/dist/managers/mcp-bridge.js.map +1 -1
- package/packages/gateway/dist/managers/shell-manager.d.ts +19 -0
- package/packages/gateway/dist/managers/shell-manager.d.ts.map +1 -1
- package/packages/gateway/dist/managers/shell-manager.js +28 -0
- package/packages/gateway/dist/managers/shell-manager.js.map +1 -1
- package/packages/gateway/dist/managers/skill-runtime.d.ts +8 -0
- package/packages/gateway/dist/managers/skill-runtime.d.ts.map +1 -1
- package/packages/gateway/dist/managers/skill-runtime.js +15 -0
- package/packages/gateway/dist/managers/skill-runtime.js.map +1 -1
- package/packages/gateway/dist/managers/tunnel-manager.d.ts +19 -1
- package/packages/gateway/dist/managers/tunnel-manager.d.ts.map +1 -1
- package/packages/gateway/dist/managers/tunnel-manager.js +289 -8
- package/packages/gateway/dist/managers/tunnel-manager.js.map +1 -1
- package/packages/gateway/dist/security/command-guard.d.ts +3 -0
- package/packages/gateway/dist/security/command-guard.d.ts.map +1 -1
- package/packages/gateway/dist/security/command-guard.js +15 -7
- package/packages/gateway/dist/security/command-guard.js.map +1 -1
- package/packages/gateway/dist/security/path-guard.d.ts +3 -0
- package/packages/gateway/dist/security/path-guard.d.ts.map +1 -1
- package/packages/gateway/dist/security/path-guard.js +8 -2
- package/packages/gateway/dist/security/path-guard.js.map +1 -1
- package/packages/gateway/dist/stores/config-store.d.ts +10 -0
- package/packages/gateway/dist/stores/config-store.d.ts.map +1 -1
- package/packages/gateway/dist/stores/config-store.js +47 -3
- package/packages/gateway/dist/stores/config-store.js.map +1 -1
- package/packages/gateway/dist/stores/secret-vault.d.ts +19 -3
- package/packages/gateway/dist/stores/secret-vault.d.ts.map +1 -1
- package/packages/gateway/dist/stores/secret-vault.js +47 -6
- package/packages/gateway/dist/stores/secret-vault.js.map +1 -1
- package/packages/gateway/dist/tools/adapters.d.ts.map +1 -1
- package/packages/gateway/dist/tools/adapters.js +198 -7
- package/packages/gateway/dist/tools/adapters.js.map +1 -1
- package/packages/gateway/dist/tools/adb.d.ts.map +1 -1
- package/packages/gateway/dist/tools/adb.js +42 -0
- package/packages/gateway/dist/tools/adb.js.map +1 -1
- package/packages/gateway/dist/tools/agent.d.ts +10 -0
- package/packages/gateway/dist/tools/agent.d.ts.map +1 -0
- package/packages/gateway/dist/tools/agent.js +35 -0
- package/packages/gateway/dist/tools/agent.js.map +1 -0
- package/packages/gateway/dist/tools/aliases.d.ts +7 -0
- package/packages/gateway/dist/tools/aliases.d.ts.map +1 -0
- package/packages/gateway/dist/tools/aliases.js +64 -0
- package/packages/gateway/dist/tools/aliases.js.map +1 -0
- package/packages/gateway/dist/tools/bash.d.ts +10 -0
- package/packages/gateway/dist/tools/bash.d.ts.map +1 -0
- package/packages/gateway/dist/tools/bash.js +67 -0
- package/packages/gateway/dist/tools/bash.js.map +1 -0
- package/packages/gateway/dist/tools/browser.d.ts.map +1 -1
- package/packages/gateway/dist/tools/browser.js +9 -0
- package/packages/gateway/dist/tools/browser.js.map +1 -1
- package/packages/gateway/dist/tools/control.d.ts +8 -0
- package/packages/gateway/dist/tools/control.d.ts.map +1 -0
- package/packages/gateway/dist/tools/control.js +134 -0
- package/packages/gateway/dist/tools/control.js.map +1 -0
- package/packages/gateway/dist/tools/editing.d.ts +8 -0
- package/packages/gateway/dist/tools/editing.d.ts.map +1 -0
- package/packages/gateway/dist/tools/editing.js +102 -0
- package/packages/gateway/dist/tools/editing.js.map +1 -0
- package/packages/gateway/dist/tools/git.d.ts.map +1 -1
- package/packages/gateway/dist/tools/git.js +67 -0
- package/packages/gateway/dist/tools/git.js.map +1 -1
- package/packages/gateway/dist/tools/index.d.ts.map +1 -1
- package/packages/gateway/dist/tools/index.js +17 -2
- package/packages/gateway/dist/tools/index.js.map +1 -1
- package/packages/gateway/dist/tools/lsp.d.ts +10 -0
- package/packages/gateway/dist/tools/lsp.d.ts.map +1 -0
- package/packages/gateway/dist/tools/lsp.js +111 -0
- package/packages/gateway/dist/tools/lsp.js.map +1 -0
- package/packages/gateway/dist/tools/question.d.ts +10 -0
- package/packages/gateway/dist/tools/question.d.ts.map +1 -0
- package/packages/gateway/dist/tools/question.js +30 -0
- package/packages/gateway/dist/tools/question.js.map +1 -0
- package/packages/gateway/dist/tools/shell.d.ts +1 -1
- package/packages/gateway/dist/tools/shell.d.ts.map +1 -1
- package/packages/gateway/dist/tools/shell.js +15 -0
- package/packages/gateway/dist/tools/shell.js.map +1 -1
- package/packages/gateway/dist/tools/skill.d.ts.map +1 -1
- package/packages/gateway/dist/tools/skill.js +2 -7
- package/packages/gateway/dist/tools/skill.js.map +1 -1
- package/packages/gateway/dist/tools/system.js +2 -2
- package/packages/gateway/dist/tools/system.js.map +1 -1
- package/packages/gateway/dist/tools/validation.d.ts +3 -0
- package/packages/gateway/dist/tools/validation.d.ts.map +1 -0
- package/packages/gateway/dist/tools/validation.js +120 -0
- package/packages/gateway/dist/tools/validation.js.map +1 -0
- package/packages/mcp/dist/http-server.d.ts +1 -1
- package/packages/mcp/dist/http-server.d.ts.map +1 -1
- package/packages/mcp/dist/http-server.js +544 -20
- package/packages/mcp/dist/http-server.js.map +1 -1
- package/packages/mcp/dist/mcp-server.d.ts.map +1 -1
- package/packages/mcp/dist/mcp-server.js +5 -1
- package/packages/mcp/dist/mcp-server.js.map +1 -1
- package/packages/shared/dist/config.d.ts +146 -16
- package/packages/shared/dist/config.d.ts.map +1 -1
- package/packages/shared/dist/config.js +93 -7
- package/packages/shared/dist/config.js.map +1 -1
- package/packages/shared/dist/index.d.ts +2 -0
- package/packages/shared/dist/index.d.ts.map +1 -1
- package/packages/shared/dist/index.js +2 -0
- package/packages/shared/dist/index.js.map +1 -1
- package/packages/shared/dist/paths.d.ts +19 -2
- package/packages/shared/dist/paths.d.ts.map +1 -1
- package/packages/shared/dist/paths.js +50 -3
- package/packages/shared/dist/paths.js.map +1 -1
- package/packages/shared/dist/tool-profiles.d.ts +34 -0
- package/packages/shared/dist/tool-profiles.d.ts.map +1 -0
- package/packages/shared/dist/tool-profiles.js +188 -0
- package/packages/shared/dist/tool-profiles.js.map +1 -0
- package/packages/shared/dist/version.d.ts +9 -0
- package/packages/shared/dist/version.d.ts.map +1 -0
- package/packages/shared/dist/version.js +9 -0
- package/packages/shared/dist/version.js.map +1 -0
- package/assets/icon.svg +0 -25
- package/packages/gateway/dist/tools/article.d.ts +0 -3
- package/packages/gateway/dist/tools/article.d.ts.map +0 -1
- package/packages/gateway/dist/tools/article.js +0 -230
- package/packages/gateway/dist/tools/article.js.map +0 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
3
6
|
import express from "express";
|
|
4
7
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
-
import { createLogger, findAvailablePort } from "@localant/shared";
|
|
8
|
+
import { createLogger, findAvailablePort, APP_VERSION, ConfigSchema, isToolInProfile } from "@localant/shared";
|
|
9
|
+
import { commandExists } from "@localant/gateway";
|
|
6
10
|
import { dashboardHtml } from "@localant/dashboard";
|
|
7
11
|
import { buildMcpServer } from "./mcp-server.js";
|
|
8
12
|
const log = createLogger("http");
|
|
@@ -26,13 +30,61 @@ function extractToken(req) {
|
|
|
26
30
|
return headerKey;
|
|
27
31
|
return undefined;
|
|
28
32
|
}
|
|
33
|
+
/** Hostnames considered local. Used to defend the dashboard against
|
|
34
|
+
* DNS-rebinding (an attacker domain resolving to 127.0.0.1 carries its own
|
|
35
|
+
* Host header, which will not be in this set). */
|
|
36
|
+
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
37
|
+
function isLocalRequest(req) {
|
|
38
|
+
// express strips the port from req.hostname.
|
|
39
|
+
return LOCAL_HOSTS.has(req.hostname);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Minimal fixed-window in-memory rate limiter keyed by client IP. No external
|
|
43
|
+
* dependency; resets every `windowMs`. Returns false when the caller is over
|
|
44
|
+
* the limit.
|
|
45
|
+
*/
|
|
46
|
+
function createRateLimiter(limit, windowMs) {
|
|
47
|
+
let windowStart = Date.now();
|
|
48
|
+
let counts = new Map();
|
|
49
|
+
return (key) => {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
if (now - windowStart >= windowMs) {
|
|
52
|
+
windowStart = now;
|
|
53
|
+
counts = new Map();
|
|
54
|
+
}
|
|
55
|
+
const next = (counts.get(key) ?? 0) + 1;
|
|
56
|
+
counts.set(key, next);
|
|
57
|
+
return next <= limit;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Locate a bundled asset across the dev-tree and published-package layouts. */
|
|
61
|
+
function findAsset(file) {
|
|
62
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
63
|
+
const candidates = [
|
|
64
|
+
path.join(__dirname, "..", "..", "..", "assets", file),
|
|
65
|
+
path.join(__dirname, "..", "assets", file),
|
|
66
|
+
path.join(__dirname, "assets", file),
|
|
67
|
+
path.join(process.cwd(), "assets", file),
|
|
68
|
+
];
|
|
69
|
+
return candidates.find((c) => fs.existsSync(c));
|
|
70
|
+
}
|
|
71
|
+
function serveAsset(file, res) {
|
|
72
|
+
const found = findAsset(file);
|
|
73
|
+
if (found)
|
|
74
|
+
res.sendFile(found);
|
|
75
|
+
else
|
|
76
|
+
res.status(404).end();
|
|
77
|
+
}
|
|
29
78
|
/**
|
|
30
79
|
* Start the public gateway server (/healthz, /status, /mcp) and the local-only
|
|
31
80
|
* dashboard server. /mcp requires the auth token.
|
|
32
81
|
*/
|
|
33
82
|
export async function startHttpServers(gw) {
|
|
34
83
|
const cfg = gw.config();
|
|
35
|
-
|
|
84
|
+
// Read the token fresh on every check so rotating it from the dashboard/CLI
|
|
85
|
+
// takes effect immediately, without restarting the gateway.
|
|
86
|
+
const currentToken = () => gw.configStore.getToken();
|
|
87
|
+
const pendingCodes = new Map();
|
|
36
88
|
// ---------- Public gateway app ----------
|
|
37
89
|
const app = express();
|
|
38
90
|
app.use(express.json({ limit: "8mb" }));
|
|
@@ -40,14 +92,22 @@ export async function startHttpServers(gw) {
|
|
|
40
92
|
app.get("/status", (_req, res) => res.json(gw.runtimeInfo()));
|
|
41
93
|
const requireAuth = (req, res) => {
|
|
42
94
|
const provided = extractToken(req);
|
|
43
|
-
if (!provided || !tokenMatches(provided,
|
|
95
|
+
if (!provided || !tokenMatches(provided, currentToken())) {
|
|
44
96
|
res.status(401).json({ error: "Unauthorized. Provide the auth token via Authorization: Bearer <token> or ?key=<token>." });
|
|
45
97
|
return false;
|
|
46
98
|
}
|
|
47
99
|
return true;
|
|
48
100
|
};
|
|
101
|
+
// Rate limit the public MCP endpoint to blunt brute-force / abuse over the
|
|
102
|
+
// tunnel. Generous enough for normal ChatGPT use; keyed by client IP.
|
|
103
|
+
const mcpRateLimit = createRateLimiter(120, 60_000);
|
|
49
104
|
// Streamable HTTP MCP endpoint (stateless: one server+transport per request).
|
|
50
105
|
app.post("/mcp", async (req, res) => {
|
|
106
|
+
const ip = req.ip ?? req.socket.remoteAddress ?? "unknown";
|
|
107
|
+
if (!mcpRateLimit(ip)) {
|
|
108
|
+
res.status(429).json({ error: "Too many requests. Slow down." });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
51
111
|
if (!requireAuth(req, res))
|
|
52
112
|
return;
|
|
53
113
|
try {
|
|
@@ -69,6 +129,44 @@ export async function startHttpServers(gw) {
|
|
|
69
129
|
const methodNotAllowed = (_req, res) => res.status(405).json({ error: "Method not allowed. Use POST for /mcp." });
|
|
70
130
|
app.get("/mcp", methodNotAllowed);
|
|
71
131
|
app.delete("/mcp", methodNotAllowed);
|
|
132
|
+
// OAuth 認可エンドポイント
|
|
133
|
+
app.get("/oauth/authorize", (req, res) => {
|
|
134
|
+
const { redirect_uri, state } = req.query;
|
|
135
|
+
if (!redirect_uri) {
|
|
136
|
+
return res.status(400).send("Missing redirect_uri");
|
|
137
|
+
}
|
|
138
|
+
const rt = gw.runtimeInfo();
|
|
139
|
+
if (!rt.dashboard) {
|
|
140
|
+
return res.status(500).send("Dashboard is not running, cannot authorize.");
|
|
141
|
+
}
|
|
142
|
+
const target = `${rt.dashboard}/#oauth/approve?state=${encodeURIComponent(String(state || ""))}&redirect_uri=${encodeURIComponent(String(redirect_uri))}`;
|
|
143
|
+
res.redirect(target);
|
|
144
|
+
});
|
|
145
|
+
// OAuth トークンエンドポイント
|
|
146
|
+
app.post("/oauth/token", express.urlencoded({ extended: true }), (req, res) => {
|
|
147
|
+
const body = req.body || {};
|
|
148
|
+
const { grant_type, code } = body;
|
|
149
|
+
if (grant_type !== "authorization_code") {
|
|
150
|
+
return res.status(400).json({ error: "unsupported_grant_type" });
|
|
151
|
+
}
|
|
152
|
+
if (!code) {
|
|
153
|
+
return res.status(400).json({ error: "invalid_request", error_description: "Missing code" });
|
|
154
|
+
}
|
|
155
|
+
const pending = pendingCodes.get(code);
|
|
156
|
+
if (!pending) {
|
|
157
|
+
return res.status(400).json({ error: "invalid_grant", error_description: "Invalid or expired code" });
|
|
158
|
+
}
|
|
159
|
+
if (Date.now() - pending.createdAt > 600_000) {
|
|
160
|
+
pendingCodes.delete(code);
|
|
161
|
+
return res.status(400).json({ error: "invalid_grant", error_description: "Code expired" });
|
|
162
|
+
}
|
|
163
|
+
pendingCodes.delete(code);
|
|
164
|
+
res.json({
|
|
165
|
+
access_token: currentToken(),
|
|
166
|
+
token_type: "Bearer",
|
|
167
|
+
expires_in: 315360000,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
72
170
|
const gatewayPort = await findAvailablePort(cfg.gateway.port, cfg.gateway.host);
|
|
73
171
|
if (gatewayPort !== cfg.gateway.port) {
|
|
74
172
|
log.info(`port ${cfg.gateway.port} is busy — falling back to ${gatewayPort}`);
|
|
@@ -80,40 +178,218 @@ export async function startHttpServers(gw) {
|
|
|
80
178
|
let dashboardPort;
|
|
81
179
|
if (cfg.dashboard.enabled) {
|
|
82
180
|
dashboardPort = await findAvailablePort(cfg.dashboard.port, "127.0.0.1", [gatewayPort]);
|
|
181
|
+
// Per-process token embedded in the served HTML and required on /api/*.
|
|
182
|
+
// A cross-origin page cannot read the dashboard HTML (so it cannot learn
|
|
183
|
+
// the token) and cannot forge the custom header without a CORS preflight
|
|
184
|
+
// we never grant — this closes the CSRF / token-theft hole.
|
|
185
|
+
const dashToken = crypto.randomBytes(24).toString("base64url");
|
|
83
186
|
const dash = express();
|
|
84
187
|
dash.use(express.json({ limit: "2mb" }));
|
|
85
|
-
|
|
86
|
-
dash.
|
|
188
|
+
// DNS-rebinding defense: only serve requests whose Host is local.
|
|
189
|
+
dash.use((req, res, next) => {
|
|
190
|
+
if (!isLocalRequest(req)) {
|
|
191
|
+
res.status(403).json({ error: "Forbidden: dashboard is local-only." });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
next();
|
|
195
|
+
});
|
|
196
|
+
mountDashboardApi(dash, gw, dashToken, pendingCodes);
|
|
197
|
+
dash.get("/favicon.png", (_req, res) => serveAsset("hero.png", res));
|
|
198
|
+
dash.get("/favicon.ico", (_req, res) => serveAsset("hero.png", res));
|
|
199
|
+
dash.get("/hero.png", (_req, res) => serveAsset("hero.png", res));
|
|
200
|
+
dash.get("/", (_req, res) => res.type("html").send(dashboardHtml(dashToken)));
|
|
87
201
|
dashboardServer = await listen(dash, dashboardPort, "127.0.0.1");
|
|
88
202
|
log.info(`dashboard listening on http://127.0.0.1:${dashboardPort}`);
|
|
89
203
|
}
|
|
90
204
|
gw.setBoundPorts(gatewayPort, dashboardPort);
|
|
91
205
|
return { gateway: gatewayServer, dashboard: dashboardServer, gatewayPort, dashboardPort };
|
|
92
206
|
}
|
|
93
|
-
/**
|
|
94
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Dashboard API — bound to 127.0.0.1 only and additionally gated by a
|
|
209
|
+
* per-process token (defends against CSRF / DNS-rebinding from a browser tab).
|
|
210
|
+
*/
|
|
211
|
+
function mountDashboardApi(app, gw, dashToken, pendingCodes) {
|
|
95
212
|
const r = express.Router();
|
|
213
|
+
r.post("/oauth/approve", (q, s) => {
|
|
214
|
+
const { redirect_uri } = q.body;
|
|
215
|
+
if (!redirect_uri) {
|
|
216
|
+
s.status(400).json({ error: "Missing redirect_uri" });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const code = crypto.randomBytes(16).toString("hex");
|
|
220
|
+
pendingCodes.set(code, {
|
|
221
|
+
redirectUri: redirect_uri,
|
|
222
|
+
createdAt: Date.now(),
|
|
223
|
+
});
|
|
224
|
+
s.json({ code });
|
|
225
|
+
});
|
|
226
|
+
// Require the dashboard token on every /api/* call. The token is embedded in
|
|
227
|
+
// the served HTML, so the legitimate same-origin page always has it; a
|
|
228
|
+
// cross-origin attacker cannot read it nor forge the custom header.
|
|
229
|
+
r.use((req, res, next) => {
|
|
230
|
+
const provided = req.header("x-dashboard-token");
|
|
231
|
+
if (!provided || !tokenMatches(provided, dashToken)) {
|
|
232
|
+
res.status(401).json({ error: "Unauthorized dashboard request." });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
next();
|
|
236
|
+
});
|
|
237
|
+
r.get("/tools", (_q, s) => {
|
|
238
|
+
const profile = gw.config().tools.profile;
|
|
239
|
+
s.json(gw.registry.list().map((t) => {
|
|
240
|
+
const shape = t.inputSchema.shape ?? {};
|
|
241
|
+
const inputSchema = {};
|
|
242
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
243
|
+
const f = field;
|
|
244
|
+
inputSchema[key] = {
|
|
245
|
+
type: getZodTypeString(f),
|
|
246
|
+
description: f.description ?? f._def?.description ?? undefined,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
name: t.name,
|
|
251
|
+
description: t.description,
|
|
252
|
+
risk: t.risk,
|
|
253
|
+
inputSchema,
|
|
254
|
+
// Whether this tool is advertised to ChatGPT under the active profile.
|
|
255
|
+
active: isToolInProfile(t.name, profile),
|
|
256
|
+
};
|
|
257
|
+
}));
|
|
258
|
+
});
|
|
259
|
+
// --- Tool profile ---
|
|
260
|
+
r.get("/tools/profile", (_q, s) => s.json({ profile: gw.config().tools.profile }));
|
|
261
|
+
r.post("/tools/profile/:name", (q, s) => {
|
|
262
|
+
const name = q.params.name;
|
|
263
|
+
if (!["minimal", "coding", "full"].includes(name)) {
|
|
264
|
+
return s.status(400).json({ error: "profile must be minimal|coding|full" });
|
|
265
|
+
}
|
|
266
|
+
gw.saveConfig({ ...gw.config(), tools: { profile: name } });
|
|
267
|
+
s.json({ profile: name, note: "Restart the gateway for the MCP surface to refresh." });
|
|
268
|
+
});
|
|
269
|
+
// --- Agents detect ---
|
|
270
|
+
r.get("/agents/detect", async (_q, s) => s.json(await gw.agents.list()));
|
|
271
|
+
// --- MCP import-all ---
|
|
272
|
+
r.post("/mcp-servers/import-all", async (_q, s) => {
|
|
273
|
+
const res = await gw.executeTool("mcp_import_all_agent_configs", {}, { caller: "dashboard" });
|
|
274
|
+
s.json(res.data ?? { error: res.error });
|
|
275
|
+
});
|
|
276
|
+
// --- Running shell processes (local visibility; ChatGPT has no equivalent) ---
|
|
277
|
+
r.get("/processes", (_q, s) => s.json({ processes: gw.shell.listProcesses() }));
|
|
96
278
|
r.get("/status", (_q, s) => s.json(gw.runtimeInfo()));
|
|
97
|
-
r.get("/health", (_q, s) => s.json({ status: "ok", version:
|
|
279
|
+
r.get("/health", (_q, s) => s.json({ status: "ok", version: APP_VERSION, time: new Date().toISOString() }));
|
|
280
|
+
r.get("/doctor", async (_q, s) => {
|
|
281
|
+
const tools = ["git", "node", "pnpm", "npm", "npx", "claude", "codex", "openclaw", "agy", "hermes", "opencode", "cloudflared", "ngrok", "adb", "docker"];
|
|
282
|
+
const checks = await Promise.all(tools.map(async (name) => ({ name, available: await commandExists(name) })));
|
|
283
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
284
|
+
s.json({
|
|
285
|
+
node: process.version,
|
|
286
|
+
nodeOk: nodeMajor >= 20,
|
|
287
|
+
skillExecOk: nodeMajor >= 22,
|
|
288
|
+
platform: process.platform,
|
|
289
|
+
tools: checks,
|
|
290
|
+
});
|
|
291
|
+
});
|
|
98
292
|
r.get("/config", (_q, s) => s.json(gw.config()));
|
|
99
293
|
r.get("/mcp-endpoint", (_q, s) => {
|
|
100
294
|
const t = gw.tunnel.current();
|
|
101
295
|
s.json({ endpoint: t.url ? `${t.url.replace(/\/$/, "")}/mcp?key=${gw.configStore.getToken()}` : null, tunnel: t });
|
|
102
296
|
});
|
|
297
|
+
r.get("/token", (_q, s) => s.json({ token: gw.configStore.getToken() }));
|
|
298
|
+
r.post("/token/rotate", (_q, s) => {
|
|
299
|
+
const token = gw.configStore.rotateToken();
|
|
300
|
+
s.json({ token });
|
|
301
|
+
});
|
|
103
302
|
r.get("/approvals", (_q, s) => s.json(gw.approvals.listPending()));
|
|
104
303
|
r.post("/approvals/:id/approve", (q, s) => s.json(gw.approvals.approve(q.params.id, q.body?.scope === "session" ? "session" : "once") ?? { error: "not found" }));
|
|
105
304
|
r.post("/approvals/:id/deny", (q, s) => s.json(gw.approvals.deny(q.params.id) ?? { error: "not found" }));
|
|
106
|
-
r.get("/audit", (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
305
|
+
r.get("/audit", (q, s) => {
|
|
306
|
+
const query = typeof q.query.q === "string" ? q.query.q.trim() : "";
|
|
307
|
+
s.json(query ? gw.audit.search(query, 100) : gw.audit.list(100));
|
|
308
|
+
});
|
|
309
|
+
r.get("/audit/:id", (q, s) => {
|
|
310
|
+
const entry = gw.audit.get(q.params.id);
|
|
311
|
+
if (!entry) {
|
|
312
|
+
s.status(404).json({ error: "Audit entry not found." });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
s.json(entry);
|
|
316
|
+
});
|
|
317
|
+
r.get("/skills", (_q, s) => s.json({
|
|
318
|
+
skillsDir: gw.paths.skillsDir,
|
|
319
|
+
skills: gw.skills.list().map((sk) => ({
|
|
320
|
+
name: sk.manifest.name,
|
|
321
|
+
version: sk.manifest.version,
|
|
322
|
+
description: sk.manifest.description,
|
|
323
|
+
enabled: sk.enabled,
|
|
324
|
+
generated: sk.generated,
|
|
325
|
+
riskLevel: sk.manifest.riskLevel,
|
|
326
|
+
valid: sk.valid,
|
|
327
|
+
bundled: !sk.dir.startsWith(gw.paths.skillsDir),
|
|
328
|
+
tools: sk.manifest.tools.map((t) => t.name),
|
|
329
|
+
})),
|
|
330
|
+
}));
|
|
331
|
+
r.get("/skills/:name", (q, s) => {
|
|
332
|
+
const sk = gw.skills.get(q.params.name);
|
|
333
|
+
if (!sk) {
|
|
334
|
+
s.status(404).json({ error: `Skill not found: ${q.params.name}` });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
s.json({
|
|
338
|
+
...sk,
|
|
339
|
+
bundled: !sk.dir.startsWith(gw.paths.skillsDir),
|
|
340
|
+
validation: gw.skills.validate(q.params.name),
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
r.post("/skills", (q, s) => {
|
|
344
|
+
try {
|
|
345
|
+
const { name, description, riskLevel, requirements } = q.body ?? {};
|
|
346
|
+
if (!name || !description) {
|
|
347
|
+
s.status(400).json({ error: "name and description are required." });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const sk = gw.skills.generate({ name, description, riskLevel, requirements });
|
|
351
|
+
s.json({ ...sk, note: "Skill generated DISABLED. Review permissions, then enable it." });
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
s.status(400).json({ error: e.message });
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
r.post("/skills/install", async (q, s) => {
|
|
358
|
+
try {
|
|
359
|
+
const url = q.body?.url;
|
|
360
|
+
if (!url) {
|
|
361
|
+
s.status(400).json({ error: "url is required." });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const res = await gw.skills.installFromGit(url);
|
|
365
|
+
s.json({ ...res, note: "Cloned DISABLED. Review permissions, then enable it." });
|
|
366
|
+
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
s.status(400).json({ error: e.message });
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
r.post("/skills/:name/run", async (q, s) => {
|
|
372
|
+
try {
|
|
373
|
+
const { tool, input } = q.body ?? {};
|
|
374
|
+
if (!tool) {
|
|
375
|
+
s.status(400).json({ error: "tool is required." });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const result = await gw.skills.run(q.params.name, tool, input ?? {});
|
|
379
|
+
s.json({ result });
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
s.status(400).json({ error: e.message });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
r.delete("/skills/:name", (q, s) => {
|
|
386
|
+
try {
|
|
387
|
+
s.json({ removed: gw.skills.uninstall(q.params.name) });
|
|
388
|
+
}
|
|
389
|
+
catch (e) {
|
|
390
|
+
s.status(400).json({ error: e.message });
|
|
391
|
+
}
|
|
392
|
+
});
|
|
117
393
|
r.post("/skills/:name/enable", (q, s) => {
|
|
118
394
|
try {
|
|
119
395
|
s.json(gw.skills.setEnabled(q.params.name, true));
|
|
@@ -122,12 +398,239 @@ function mountDashboardApi(app, gw) {
|
|
|
122
398
|
s.status(400).json({ error: e.message });
|
|
123
399
|
}
|
|
124
400
|
});
|
|
125
|
-
r.post("/skills/:name/disable", (q, s) =>
|
|
401
|
+
r.post("/skills/:name/disable", (q, s) => {
|
|
402
|
+
try {
|
|
403
|
+
s.json(gw.skills.setEnabled(q.params.name, false));
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
s.status(400).json({ error: e.message });
|
|
407
|
+
}
|
|
408
|
+
});
|
|
126
409
|
r.get("/projects", (_q, s) => s.json(gw.projects.list()));
|
|
410
|
+
r.post("/projects", (q, s) => {
|
|
411
|
+
try {
|
|
412
|
+
const { path: projectPath, name } = q.body ?? {};
|
|
413
|
+
if (!projectPath) {
|
|
414
|
+
s.status(400).json({ error: "path is required." });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
s.json(gw.projects.register(projectPath, name));
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
s.status(400).json({ error: e.message });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
r.delete("/projects/:id", (q, s) => s.json({ removed: gw.projects.unregister(q.params.id) }));
|
|
127
424
|
r.get("/secrets", (_q, s) => s.json({ names: gw.vault.list() }));
|
|
425
|
+
r.post("/secrets", (q, s) => {
|
|
426
|
+
try {
|
|
427
|
+
const { name, value } = q.body;
|
|
428
|
+
if (!name || !value) {
|
|
429
|
+
s.status(400).json({ error: "Name and value are required." });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
gw.vault.set(name, value);
|
|
433
|
+
s.json({ ok: true });
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
s.status(400).json({ error: e.message });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
r.delete("/secrets/:name", (q, s) => {
|
|
440
|
+
try {
|
|
441
|
+
const ok = gw.vault.remove(q.params.name);
|
|
442
|
+
s.json({ ok });
|
|
443
|
+
}
|
|
444
|
+
catch (e) {
|
|
445
|
+
s.status(400).json({ error: e.message });
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
r.post("/config", (q, s) => {
|
|
449
|
+
try {
|
|
450
|
+
const current = gw.config();
|
|
451
|
+
const next = mergeConfig(current, q.body);
|
|
452
|
+
const parsed = ConfigSchema.parse(next);
|
|
453
|
+
const saved = gw.saveConfig(parsed);
|
|
454
|
+
s.json(saved);
|
|
455
|
+
}
|
|
456
|
+
catch (e) {
|
|
457
|
+
s.status(400).json({ error: e.message });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
128
460
|
r.get("/agents", async (_q, s) => s.json(await gw.agents.list()));
|
|
461
|
+
r.post("/agents/:name/enable", async (q, s) => setAgentEnabled(gw, q.params.name, true, s));
|
|
462
|
+
r.post("/agents/:name/disable", async (q, s) => setAgentEnabled(gw, q.params.name, false, s));
|
|
463
|
+
r.post("/agents/run", async (q, s) => {
|
|
464
|
+
try {
|
|
465
|
+
const { agent, projectId, task, mode } = q.body ?? {};
|
|
466
|
+
if (!agent || !projectId || !task) {
|
|
467
|
+
s.status(400).json({ error: "agent, projectId and task are required." });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (mode === "execute") {
|
|
471
|
+
s.json(await gw.agents.startTask(agent, projectId, task));
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
s.json(await gw.agents.plan(agent, projectId, task));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (e) {
|
|
478
|
+
s.status(400).json({ error: e.message });
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
r.get("/agents/tasks", (_q, s) => s.json(gw.agents.listTasks()));
|
|
482
|
+
r.get("/agents/tasks/:id/logs", (q, s) => {
|
|
483
|
+
try {
|
|
484
|
+
s.json({ logs: gw.agents.getLogs(q.params.id) });
|
|
485
|
+
}
|
|
486
|
+
catch (e) {
|
|
487
|
+
s.status(404).json({ error: e.message });
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
r.post("/agents/tasks/:id/stop", (q, s) => {
|
|
491
|
+
try {
|
|
492
|
+
s.json(gw.agents.stopTask(q.params.id));
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
s.status(404).json({ error: e.message });
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
r.get("/mcp-servers", (_q, s) => {
|
|
499
|
+
const servers = gw.config().mcpServers;
|
|
500
|
+
s.json(Object.entries(servers).map(([name, cfg]) => ({
|
|
501
|
+
name,
|
|
502
|
+
command: cfg.command,
|
|
503
|
+
args: cfg.args,
|
|
504
|
+
transport: cfg.transport,
|
|
505
|
+
enabled: cfg.enabled,
|
|
506
|
+
})));
|
|
507
|
+
});
|
|
508
|
+
r.post("/mcp-servers", (q, s) => {
|
|
509
|
+
try {
|
|
510
|
+
const { name, command, args, enabled } = q.body ?? {};
|
|
511
|
+
if (!name || !command) {
|
|
512
|
+
s.status(400).json({ error: "name and command are required." });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const cfg = gw.config();
|
|
516
|
+
const argList = Array.isArray(args) ? args : typeof args === "string" && args.trim() ? args.trim().split(/\s+/) : [];
|
|
517
|
+
gw.saveConfig({
|
|
518
|
+
...cfg,
|
|
519
|
+
mcpServers: {
|
|
520
|
+
...cfg.mcpServers,
|
|
521
|
+
[name]: { command, args: argList, transport: "stdio", enabled: enabled !== false },
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
s.json({ ok: true });
|
|
525
|
+
}
|
|
526
|
+
catch (e) {
|
|
527
|
+
s.status(400).json({ error: e.message });
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
r.delete("/mcp-servers/:name", (q, s) => {
|
|
531
|
+
const cfg = gw.config();
|
|
532
|
+
if (!cfg.mcpServers[q.params.name]) {
|
|
533
|
+
s.status(404).json({ error: `Unknown MCP server: ${q.params.name}` });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const next = { ...cfg.mcpServers };
|
|
537
|
+
delete next[q.params.name];
|
|
538
|
+
gw.saveConfig({ ...cfg, mcpServers: next });
|
|
539
|
+
s.json({ removed: true });
|
|
540
|
+
});
|
|
541
|
+
r.post("/mcp-servers/:name/enable", (q, s) => setMcpServerEnabled(gw, q.params.name, true, s));
|
|
542
|
+
r.post("/mcp-servers/:name/disable", (q, s) => setMcpServerEnabled(gw, q.params.name, false, s));
|
|
543
|
+
r.post("/mcp-servers/:name/test", async (q, s) => {
|
|
544
|
+
try {
|
|
545
|
+
const tools = await gw.bridge.listTools(q.params.name);
|
|
546
|
+
s.json({ ok: true, tools: tools.map((t) => t.name) });
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
s.json({ ok: false, reason: e.message });
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
r.get("/tunnel", (_q, s) => s.json(gw.tunnel.current()));
|
|
553
|
+
r.post("/tunnel/test", async (_q, s) => {
|
|
554
|
+
const t = gw.tunnel.current();
|
|
555
|
+
if (!t.url) {
|
|
556
|
+
s.json({ reachable: false, reason: "No tunnel URL — start the tunnel first." });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const target = `${t.url.replace(/\/$/, "")}/healthz`;
|
|
560
|
+
const started = Date.now();
|
|
561
|
+
try {
|
|
562
|
+
const ctrl = new AbortController();
|
|
563
|
+
const timer = setTimeout(() => ctrl.abort(), 8000);
|
|
564
|
+
const resp = await fetch(target, { signal: ctrl.signal, redirect: "manual" });
|
|
565
|
+
clearTimeout(timer);
|
|
566
|
+
s.json({ reachable: resp.ok, status: resp.status, ms: Date.now() - started, url: target });
|
|
567
|
+
}
|
|
568
|
+
catch (e) {
|
|
569
|
+
s.json({ reachable: false, ms: Date.now() - started, url: target, reason: e.message });
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
r.post("/tunnel/restart", async (_q, s) => {
|
|
573
|
+
try {
|
|
574
|
+
s.json(await gw.restartTunnel());
|
|
575
|
+
}
|
|
576
|
+
catch (e) {
|
|
577
|
+
s.status(500).json({ error: e.message });
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
r.post("/tunnel/stop", (_q, s) => {
|
|
581
|
+
gw.tunnel.stop();
|
|
582
|
+
s.json(gw.tunnel.current());
|
|
583
|
+
});
|
|
584
|
+
r.post("/tunnel/start", async (_q, s) => {
|
|
585
|
+
try {
|
|
586
|
+
s.json(await gw.tunnel.start(gw.gatewayPort()));
|
|
587
|
+
}
|
|
588
|
+
catch (e) {
|
|
589
|
+
s.status(500).json({ error: e.message });
|
|
590
|
+
}
|
|
591
|
+
});
|
|
129
592
|
app.use("/api", r);
|
|
130
593
|
}
|
|
594
|
+
/** Toggle a coding agent's `enabled` flag in config; 404 if the agent is unknown. */
|
|
595
|
+
async function setAgentEnabled(gw, name, enabled, s) {
|
|
596
|
+
const cfg = gw.config();
|
|
597
|
+
const agent = cfg.codingAgents[name];
|
|
598
|
+
if (!agent) {
|
|
599
|
+
s.status(404).json({ error: `Unknown coding agent: ${name}` });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
gw.saveConfig({
|
|
603
|
+
...cfg,
|
|
604
|
+
codingAgents: { ...cfg.codingAgents, [name]: { ...agent, enabled } },
|
|
605
|
+
});
|
|
606
|
+
s.json(await gw.agents.list());
|
|
607
|
+
}
|
|
608
|
+
/** Toggle a downstream MCP server's `enabled` flag in config; 404 if unknown. */
|
|
609
|
+
function setMcpServerEnabled(gw, name, enabled, s) {
|
|
610
|
+
const cfg = gw.config();
|
|
611
|
+
const server = cfg.mcpServers[name];
|
|
612
|
+
if (!server) {
|
|
613
|
+
s.status(404).json({ error: `Unknown MCP server: ${name}` });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
gw.saveConfig({
|
|
617
|
+
...cfg,
|
|
618
|
+
mcpServers: { ...cfg.mcpServers, [name]: { ...server, enabled } },
|
|
619
|
+
});
|
|
620
|
+
s.json({ ok: true, enabled });
|
|
621
|
+
}
|
|
622
|
+
function mergeConfig(current, update) {
|
|
623
|
+
const next = { ...current };
|
|
624
|
+
for (const key of Object.keys(update)) {
|
|
625
|
+
if (update[key] !== null && typeof update[key] === "object" && !Array.isArray(update[key])) {
|
|
626
|
+
next[key] = mergeConfig(next[key] || {}, update[key]);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
next[key] = update[key];
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return next;
|
|
633
|
+
}
|
|
131
634
|
function listen(app, port, host) {
|
|
132
635
|
return new Promise((resolve, reject) => {
|
|
133
636
|
const server = http.createServer(app);
|
|
@@ -135,4 +638,25 @@ function listen(app, port, host) {
|
|
|
135
638
|
server.listen(port, host, () => resolve(server));
|
|
136
639
|
});
|
|
137
640
|
}
|
|
641
|
+
function getZodTypeString(f) {
|
|
642
|
+
if (!f || !f._def)
|
|
643
|
+
return "unknown";
|
|
644
|
+
const typeName = f._def.typeName;
|
|
645
|
+
if (typeName === "ZodOptional") {
|
|
646
|
+
return `${getZodTypeString(f._def.innerType)} (optional)`;
|
|
647
|
+
}
|
|
648
|
+
if (typeName === "ZodNullable") {
|
|
649
|
+
return `${getZodTypeString(f._def.innerType)} (nullable)`;
|
|
650
|
+
}
|
|
651
|
+
if (typeName === "ZodArray") {
|
|
652
|
+
return `${getZodTypeString(f._def.type)}[]`;
|
|
653
|
+
}
|
|
654
|
+
if (typeName === "ZodEnum") {
|
|
655
|
+
return `enum (${f._def.values.join(" | ")})`;
|
|
656
|
+
}
|
|
657
|
+
if (typeName === "ZodEffects") {
|
|
658
|
+
return getZodTypeString(f._def.schema);
|
|
659
|
+
}
|
|
660
|
+
return typeName.replace(/^Zod/, "").toLowerCase();
|
|
661
|
+
}
|
|
138
662
|
//# sourceMappingURL=http-server.js.map
|