terra-mcp-google 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/admin/api.d.ts +35 -0
- package/dist/admin/api.js +106 -0
- package/dist/admin/api.js.map +1 -0
- package/dist/admin/server.d.ts +14 -0
- package/dist/admin/server.js +138 -0
- package/dist/admin/server.js.map +1 -0
- package/dist/admin/ui/assets/index--VIJVbeC.js +41 -0
- package/dist/admin/ui/assets/index-BykS1oj0.css +1 -0
- package/dist/admin/ui/index.html +23 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/constants.d.ts +61 -3
- package/dist/config/constants.js +65 -13
- package/dist/config/constants.js.map +1 -1
- package/dist/core/local-file.js +4 -4
- package/dist/core/local-file.js.map +1 -1
- package/dist/core/tool.d.ts +5 -0
- package/dist/core/tool.js +11 -4
- package/dist/core/tool.js.map +1 -1
- package/dist/google/auth.js +2 -2
- package/dist/google/auth.js.map +1 -1
- package/dist/policy/engine.d.ts +29 -0
- package/dist/policy/engine.js +95 -0
- package/dist/policy/engine.js.map +1 -0
- package/dist/policy/guard.d.ts +31 -0
- package/dist/policy/guard.js +116 -0
- package/dist/policy/guard.js.map +1 -0
- package/dist/policy/resolver.d.ts +27 -0
- package/dist/policy/resolver.js +46 -0
- package/dist/policy/resolver.js.map +1 -0
- package/dist/policy/store.d.ts +32 -0
- package/dist/policy/store.js +135 -0
- package/dist/policy/store.js.map +1 -0
- package/dist/policy/types.d.ts +77 -0
- package/dist/policy/types.js +9 -0
- package/dist/policy/types.js.map +1 -0
- package/dist/services/drive/tools.js +45 -3
- package/dist/services/drive/tools.js.map +1 -1
- package/dist/services/sheets/tools.js +24 -0
- package/dist/services/sheets/tools.js.map +1 -1
- package/dist/setup/setup.js +2 -2
- package/dist/setup/setup.js.map +1 -1
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -18,8 +18,57 @@ terra-mcp client codex # print safe (read-only) MCP config
|
|
|
18
18
|
| `auth login` / `logout` / `status` | sign in / out / show account, scopes, expiry |
|
|
19
19
|
| `setup` | check config dir + auth, print MCP config for all clients |
|
|
20
20
|
| `client [codex\|claude\|copilot\|kiro\|all]` | print MCP config with **mutating tools disabled** (`--include-dangerous` keeps them) |
|
|
21
|
+
| `admin` | open the web console to manage which resources the agent may access (`-p` port, `--no-open`) |
|
|
21
22
|
| *(no command)* | start the stdio server |
|
|
22
23
|
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
Set these in your MCP client's `env` block (or the launching shell):
|
|
27
|
+
|
|
28
|
+
| Variable | Default | Purpose |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `TERRA_MCP_DIR` | `~/.terra-mcp` | Directory holding the OAuth client config + cached token |
|
|
31
|
+
| `GOOGLE_OAUTH_CREDENTIALS` | `<TERRA_MCP_DIR>/client_secret.json` | Google OAuth client JSON; overrides the embedded client |
|
|
32
|
+
| `GOOGLE_OAUTH_TOKEN` | `<TERRA_MCP_DIR>/token.json` | Cached access/refresh token |
|
|
33
|
+
| `TERRA_MCP_SAFE_MODE` | unset | `1` → register **only read-only tools**; drop every mutating tool |
|
|
34
|
+
| `TERRA_MCP_LOCAL_FILE_ROOT` | unset | Only directory `local_path`/`save_path` may touch. Unset = local file up/download disabled |
|
|
35
|
+
| `TERRA_MCP_POLICY_DB` | `<TERRA_MCP_DIR>/policy.db` | SQLite database holding the resource allowlist + policy mode |
|
|
36
|
+
| `TERRA_MCP_POLICY_MODE` | `read_open` | Initial mode before one is chosen in the console: `off` \| `read_open` \| `strict` |
|
|
37
|
+
| `TERRA_MCP_ADMIN_PORT` | `4717` | Port the `admin` web console listens on |
|
|
38
|
+
| `TERRA_MCP_TOKEN_PROXY_URL` | bundled proxy | OAuth token-exchange proxy (advanced; only when self-hosting it) |
|
|
39
|
+
| `TERRA_MCP_PROXY_KEY` | bundled key | Deterrent key sent to the proxy (ships in the package — not a secret) |
|
|
40
|
+
|
|
41
|
+
**Restricting tools.** Disable the mutating tools server-side with `TERRA_MCP_SAFE_MODE=1`, or
|
|
42
|
+
per-client with `terra-mcp client <agent>` (pass `--include-dangerous` to keep them on). Tools are
|
|
43
|
+
also gated by the **OAuth scopes granted at login** — a Sheets-only grant exposes no `drive_*` tools.
|
|
44
|
+
|
|
45
|
+
## Permission gate (per-resource allowlist)
|
|
46
|
+
|
|
47
|
+
Beyond on/off tool gating, the server enforces a **per-resource allowlist**: you decide exactly
|
|
48
|
+
which Drive files/folders and Sheets the agent may touch, and with what power (read / write / delete).
|
|
49
|
+
Every tool call is checked against the allowlist (stored in a small local SQLite database) before it
|
|
50
|
+
runs; created resources are auto-added so the agent can keep working with what it just made.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
terra-mcp admin # opens the web console at http://localhost:4717
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Three modes** (switch any time in the console):
|
|
57
|
+
|
|
58
|
+
| Mode | The agent can… |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `off` | …do anything your account can (gate disabled). |
|
|
61
|
+
| `read_open` *(default)* | …**read** anything and **create** new resources, but only **write/delete existing** resources you've granted. Useful out of the box — nothing to configure to start reading. |
|
|
62
|
+
| `strict` | …only **see and use** resources in the allowlist (and inside granted folders). Everything else is invisible, even to list/search. |
|
|
63
|
+
|
|
64
|
+
**Grants** carry independent read / write / delete flags (new grants default to read-only — the safe
|
|
65
|
+
starting point), and a grant on a **folder cascades** to everything inside it. Manage them — search
|
|
66
|
+
your Drive to pick a file, or paste an ID — in the `admin` console. The console and the running
|
|
67
|
+
server share the same database, so changes take effect on the agent's next call; no restart needed.
|
|
68
|
+
|
|
69
|
+
> Requires **Node 23.4+** (or Node 22.5+ launched with `--experimental-sqlite`) for the gate. On
|
|
70
|
+
> older Node the server still runs, with the gate disabled.
|
|
71
|
+
|
|
23
72
|
## Tools
|
|
24
73
|
|
|
25
74
|
**Auth** — sign-in/out are CLI-only (`terra-mcp auth login`/`logout`).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { drive_v3 } from "googleapis";
|
|
2
|
+
import type { PolicyStore } from "../policy/store.js";
|
|
3
|
+
import type { ResourceKind } from "../policy/types.js";
|
|
4
|
+
/** A Drive resource as surfaced to the admin console's "search & pick" flow. */
|
|
5
|
+
export interface DriveItem {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
kind: ResourceKind;
|
|
10
|
+
}
|
|
11
|
+
/** Collaborators the API routes need, injected so the router stays testable. */
|
|
12
|
+
export interface ApiDeps {
|
|
13
|
+
store: PolicyStore;
|
|
14
|
+
searchDrive: (query: string) => Promise<DriveItem[]>;
|
|
15
|
+
authInfo: () => Promise<{
|
|
16
|
+
signedIn: boolean;
|
|
17
|
+
email: string | null;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
/** A plain HTTP-ish response the transport layer serializes. */
|
|
21
|
+
export interface ApiResponse {
|
|
22
|
+
status: number;
|
|
23
|
+
body: unknown;
|
|
24
|
+
}
|
|
25
|
+
/** Classify a Drive mimeType into the grant `kind` the gate understands. */
|
|
26
|
+
export declare function kindOfMime(mimeType: string | null | undefined): ResourceKind;
|
|
27
|
+
/** Build the `searchDrive` dependency backed by a live Drive client. */
|
|
28
|
+
export declare function driveSearcher(drive: drive_v3.Drive): (query: string) => Promise<DriveItem[]>;
|
|
29
|
+
/**
|
|
30
|
+
* The admin console's HTTP surface, as a pure function over (method, path, body)
|
|
31
|
+
* — no `node:http` objects — so it unit-tests against an in-memory store and
|
|
32
|
+
* fake deps. Returns `null` for non-`/api` paths so the caller can fall back to
|
|
33
|
+
* serving the SPA's static assets.
|
|
34
|
+
*/
|
|
35
|
+
export declare function routeApi(method: string, path: string, query: URLSearchParams, body: unknown, deps: ApiDeps): Promise<ApiResponse | null>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DriveFileAdapter } from "../services/drive/adapter.js";
|
|
3
|
+
const kindSchema = z.enum(["file", "folder", "spreadsheet"]);
|
|
4
|
+
const modeSchema = z.enum(["off", "read_open", "strict"]);
|
|
5
|
+
const createGrantSchema = z.object({
|
|
6
|
+
kind: kindSchema,
|
|
7
|
+
googleId: z.string().min(1),
|
|
8
|
+
name: z.string().nullish(),
|
|
9
|
+
canRead: z.boolean(),
|
|
10
|
+
canWrite: z.boolean(),
|
|
11
|
+
canDelete: z.boolean(),
|
|
12
|
+
});
|
|
13
|
+
const patchGrantSchema = z.object({
|
|
14
|
+
name: z.string().nullish(),
|
|
15
|
+
canRead: z.boolean().optional(),
|
|
16
|
+
canWrite: z.boolean().optional(),
|
|
17
|
+
canDelete: z.boolean().optional(),
|
|
18
|
+
});
|
|
19
|
+
/** Classify a Drive mimeType into the grant `kind` the gate understands. */
|
|
20
|
+
export function kindOfMime(mimeType) {
|
|
21
|
+
if (mimeType === "application/vnd.google-apps.folder")
|
|
22
|
+
return "folder";
|
|
23
|
+
if (mimeType === "application/vnd.google-apps.spreadsheet")
|
|
24
|
+
return "spreadsheet";
|
|
25
|
+
return "file";
|
|
26
|
+
}
|
|
27
|
+
/** Build the `searchDrive` dependency backed by a live Drive client. */
|
|
28
|
+
export function driveSearcher(drive) {
|
|
29
|
+
return async (query) => {
|
|
30
|
+
const trimmed = query.trim();
|
|
31
|
+
// Escape single quotes for the Drive `q` string literal.
|
|
32
|
+
const q = trimmed ? `name contains '${trimmed.replace(/'/g, "\\'")}'` : undefined;
|
|
33
|
+
const { files } = await new DriveFileAdapter(drive).listFiles({
|
|
34
|
+
query: q,
|
|
35
|
+
pageSize: 25,
|
|
36
|
+
orderBy: "modifiedTime desc",
|
|
37
|
+
includeTrashed: false,
|
|
38
|
+
});
|
|
39
|
+
return files
|
|
40
|
+
.filter((f) => typeof f.id === "string")
|
|
41
|
+
.map((f) => ({
|
|
42
|
+
id: f.id,
|
|
43
|
+
name: f.name ?? "(untitled)",
|
|
44
|
+
mimeType: f.mimeType ?? "application/octet-stream",
|
|
45
|
+
kind: kindOfMime(f.mimeType),
|
|
46
|
+
}));
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The admin console's HTTP surface, as a pure function over (method, path, body)
|
|
51
|
+
* — no `node:http` objects — so it unit-tests against an in-memory store and
|
|
52
|
+
* fake deps. Returns `null` for non-`/api` paths so the caller can fall back to
|
|
53
|
+
* serving the SPA's static assets.
|
|
54
|
+
*/
|
|
55
|
+
export async function routeApi(method, path, query, body, deps) {
|
|
56
|
+
if (path !== "/api" && !path.startsWith("/api/"))
|
|
57
|
+
return null;
|
|
58
|
+
try {
|
|
59
|
+
if (path === "/api/health" && method === "GET") {
|
|
60
|
+
const auth = await deps.authInfo();
|
|
61
|
+
return ok({ ok: true, mode: deps.store.getMode(), ...auth });
|
|
62
|
+
}
|
|
63
|
+
if (path === "/api/grants") {
|
|
64
|
+
if (method === "GET")
|
|
65
|
+
return ok({ grants: deps.store.listGrants() });
|
|
66
|
+
if (method === "POST") {
|
|
67
|
+
const input = createGrantSchema.parse(body);
|
|
68
|
+
return ok({ grant: deps.store.upsertGrant(input) }, 201);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const grantId = path.match(/^\/api\/grants\/(\d+)$/);
|
|
72
|
+
if (grantId) {
|
|
73
|
+
const id = Number(grantId[1]);
|
|
74
|
+
if (method === "PATCH") {
|
|
75
|
+
const patch = patchGrantSchema.parse(body);
|
|
76
|
+
const grant = deps.store.updateGrant(id, patch);
|
|
77
|
+
return grant ? ok({ grant }) : fail(404, "Grant not found");
|
|
78
|
+
}
|
|
79
|
+
if (method === "DELETE") {
|
|
80
|
+
return deps.store.deleteGrant(id) ? ok({ ok: true }) : fail(404, "Grant not found");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (path === "/api/mode" && method === "PUT") {
|
|
84
|
+
const { mode } = z.object({ mode: modeSchema }).parse(body);
|
|
85
|
+
deps.store.setMode(mode);
|
|
86
|
+
return ok({ mode });
|
|
87
|
+
}
|
|
88
|
+
if (path === "/api/drive/search" && method === "GET") {
|
|
89
|
+
return ok({ files: await deps.searchDrive(query.get("q") ?? "") });
|
|
90
|
+
}
|
|
91
|
+
return fail(404, "Not found");
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof z.ZodError) {
|
|
95
|
+
return fail(400, error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "));
|
|
96
|
+
}
|
|
97
|
+
return fail(500, error instanceof Error ? error.message : String(error));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function ok(body, status = 200) {
|
|
101
|
+
return { status, body };
|
|
102
|
+
}
|
|
103
|
+
function fail(status, error) {
|
|
104
|
+
return { status, body: { error } };
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/admin/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface AdminServerHandle {
|
|
2
|
+
port: number;
|
|
3
|
+
url: string;
|
|
4
|
+
close: () => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Start the admin web console: a tiny `node:http` server that exposes the policy
|
|
8
|
+
* REST API under `/api` and serves the built SPA for everything else. No
|
|
9
|
+
* framework, no extra runtime dependency.
|
|
10
|
+
*/
|
|
11
|
+
export declare function startAdminServer(opts?: {
|
|
12
|
+
port?: number;
|
|
13
|
+
staticDir?: string;
|
|
14
|
+
}): Promise<AdminServerHandle>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, extname, join, normalize, resolve } from "node:path";
|
|
5
|
+
import { ADMIN_PORT, getAdminStaticDir } from "../config/constants.js";
|
|
6
|
+
import { getGoogleClients } from "../google/client.js";
|
|
7
|
+
import { getAuthStatus } from "../google/auth.js";
|
|
8
|
+
import { getPolicyStoreOrThrow } from "../policy/guard.js";
|
|
9
|
+
import { driveSearcher, routeApi } from "./api.js";
|
|
10
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const MIME = {
|
|
12
|
+
".html": "text/html; charset=utf-8",
|
|
13
|
+
".js": "text/javascript; charset=utf-8",
|
|
14
|
+
".css": "text/css; charset=utf-8",
|
|
15
|
+
".json": "application/json; charset=utf-8",
|
|
16
|
+
".svg": "image/svg+xml",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".jpg": "image/jpeg",
|
|
19
|
+
".ico": "image/x-icon",
|
|
20
|
+
".woff2": "font/woff2",
|
|
21
|
+
".woff": "font/woff",
|
|
22
|
+
};
|
|
23
|
+
/** Build the API dependencies from the live policy store + Google clients. */
|
|
24
|
+
function buildDeps() {
|
|
25
|
+
return {
|
|
26
|
+
store: getPolicyStoreOrThrow(),
|
|
27
|
+
searchDrive: async (query) => driveSearcher((await getGoogleClients()).drive)(query),
|
|
28
|
+
authInfo: async () => {
|
|
29
|
+
const status = await getAuthStatus().catch(() => ({ authenticated: false, email: null }));
|
|
30
|
+
return { signedIn: status.authenticated, email: status.email ?? null };
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** First existing candidate directory holding the built SPA, or undefined. */
|
|
35
|
+
async function resolveStaticDir(override) {
|
|
36
|
+
const candidates = [
|
|
37
|
+
override,
|
|
38
|
+
getAdminStaticDir(),
|
|
39
|
+
join(HERE, "ui"), // production: copied beside the compiled server (dist/admin/ui)
|
|
40
|
+
join(HERE, "../../admin/dist"), // dev via tsx: the repo's built SPA
|
|
41
|
+
].filter((c) => typeof c === "string");
|
|
42
|
+
const exists = await Promise.all(candidates.map(isDir));
|
|
43
|
+
return candidates.find((_, index) => exists[index]);
|
|
44
|
+
}
|
|
45
|
+
async function isDir(path) {
|
|
46
|
+
try {
|
|
47
|
+
return (await stat(path)).isDirectory();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function readBody(req) {
|
|
54
|
+
const chunks = [];
|
|
55
|
+
for await (const chunk of req)
|
|
56
|
+
chunks.push(chunk);
|
|
57
|
+
if (chunks.length === 0)
|
|
58
|
+
return undefined;
|
|
59
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
60
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
61
|
+
}
|
|
62
|
+
function sendJson(res, status, body) {
|
|
63
|
+
const payload = JSON.stringify(body);
|
|
64
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
65
|
+
res.end(payload);
|
|
66
|
+
}
|
|
67
|
+
async function serveStatic(res, staticDir, urlPath) {
|
|
68
|
+
if (!staticDir) {
|
|
69
|
+
res.writeHead(503, { "content-type": "text/plain; charset=utf-8" });
|
|
70
|
+
res.end("Admin UI is not built. Run `pnpm admin:build` (or set TERRA_MCP_ADMIN_STATIC_DIR).");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Resolve the request to a file inside staticDir; fall back to index.html so
|
|
74
|
+
// the single-page app can handle its own routing. Reject path traversal.
|
|
75
|
+
const relative = normalize(urlPath === "/" ? "/index.html" : urlPath).replace(/^(\.\.[/\\])+/, "");
|
|
76
|
+
let filePath = resolve(staticDir, "." + relative);
|
|
77
|
+
if (!filePath.startsWith(resolve(staticDir)))
|
|
78
|
+
filePath = join(staticDir, "index.html");
|
|
79
|
+
if (!(await fileExists(filePath)))
|
|
80
|
+
filePath = join(staticDir, "index.html");
|
|
81
|
+
try {
|
|
82
|
+
const data = await readFile(filePath);
|
|
83
|
+
res.writeHead(200, { "content-type": MIME[extname(filePath)] ?? "application/octet-stream" });
|
|
84
|
+
res.end(data);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
88
|
+
res.end("Not found");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function fileExists(path) {
|
|
92
|
+
try {
|
|
93
|
+
return (await stat(path)).isFile();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Start the admin web console: a tiny `node:http` server that exposes the policy
|
|
101
|
+
* REST API under `/api` and serves the built SPA for everything else. No
|
|
102
|
+
* framework, no extra runtime dependency.
|
|
103
|
+
*/
|
|
104
|
+
export async function startAdminServer(opts = {}) {
|
|
105
|
+
const port = opts.port ?? ADMIN_PORT;
|
|
106
|
+
const deps = buildDeps();
|
|
107
|
+
const staticDir = await resolveStaticDir(opts.staticDir);
|
|
108
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
109
|
+
const server = createServer((req, res) => {
|
|
110
|
+
void handle(req, res, deps, staticDir);
|
|
111
|
+
});
|
|
112
|
+
server.on("error", rejectPromise);
|
|
113
|
+
server.listen(port, () => {
|
|
114
|
+
resolvePromise({
|
|
115
|
+
port,
|
|
116
|
+
url: `http://localhost:${port}`,
|
|
117
|
+
close: () => new Promise((done) => server.close(() => done())),
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async function handle(req, res, deps, staticDir) {
|
|
123
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
124
|
+
const method = req.method ?? "GET";
|
|
125
|
+
try {
|
|
126
|
+
if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
|
|
127
|
+
const body = method === "GET" || method === "DELETE" ? undefined : await readBody(req);
|
|
128
|
+
const result = await routeApi(method, url.pathname, url.searchParams, body, deps);
|
|
129
|
+
if (result)
|
|
130
|
+
return sendJson(res, result.status, result.body);
|
|
131
|
+
}
|
|
132
|
+
await serveStatic(res, staticDir, url.pathname);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/admin/server.ts"],"names":[],"mappings":"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"}
|