@ggui-ai/mcp-server 0.2.0-alpha.4 → 0.3.0-rc.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/admin-blueprints-transport.d.ts.map +1 -1
- package/dist/admin-blueprints-transport.js +2 -1
- package/dist/admin-oauth-providers-transport.d.ts.map +1 -1
- package/dist/admin-oauth-providers-transport.js +7 -5
- package/dist/api-renders-routes.d.ts +85 -0
- package/dist/api-renders-routes.d.ts.map +1 -0
- package/dist/api-renders-routes.js +372 -0
- package/dist/build-mcp.d.ts +1 -1
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +39 -5
- package/dist/code-routes.d.ts +47 -0
- package/dist/code-routes.d.ts.map +1 -0
- package/dist/code-routes.js +81 -0
- package/dist/code-store-fs.js +2 -2
- package/dist/console-auth.d.ts +10 -10
- package/dist/console-auth.d.ts.map +1 -1
- package/dist/console-auth.js +5 -5
- package/dist/console-blueprint-routes.d.ts +71 -0
- package/dist/console-blueprint-routes.d.ts.map +1 -0
- package/dist/console-blueprint-routes.js +348 -0
- package/dist/console-chat-routes.d.ts +80 -0
- package/dist/console-chat-routes.d.ts.map +1 -0
- package/dist/console-chat-routes.js +182 -0
- package/dist/console-config-routes.d.ts +37 -0
- package/dist/console-config-routes.d.ts.map +1 -0
- package/dist/console-config-routes.js +91 -0
- package/dist/console-headers.d.ts +1 -1
- package/dist/console-info-routes.d.ts +84 -0
- package/dist/console-info-routes.d.ts.map +1 -0
- package/dist/console-info-routes.js +135 -0
- package/dist/console-keys-routes.d.ts +50 -0
- package/dist/console-keys-routes.d.ts.map +1 -0
- package/dist/console-keys-routes.js +222 -0
- package/dist/console-llm-keys-routes.d.ts +47 -0
- package/dist/console-llm-keys-routes.d.ts.map +1 -0
- package/dist/console-llm-keys-routes.js +443 -0
- package/dist/console-mcp-tools-routes.d.ts +41 -0
- package/dist/console-mcp-tools-routes.d.ts.map +1 -0
- package/dist/console-mcp-tools-routes.js +60 -0
- package/dist/console-registry-routes.d.ts +66 -0
- package/dist/console-registry-routes.d.ts.map +1 -0
- package/dist/console-registry-routes.js +276 -0
- package/dist/console-session-routes.d.ts +89 -0
- package/dist/console-session-routes.d.ts.map +1 -0
- package/dist/console-session-routes.js +385 -0
- package/dist/console-sessions-routes.d.ts +52 -0
- package/dist/console-sessions-routes.d.ts.map +1 -0
- package/dist/console-sessions-routes.js +106 -0
- package/dist/console-static-routes.d.ts +54 -0
- package/dist/console-static-routes.d.ts.map +1 -0
- package/dist/console-static-routes.js +190 -0
- package/dist/console-theme-routes.d.ts +3 -3
- package/dist/console-theme-routes.js +1 -1
- package/dist/console-timeline.d.ts +5 -5
- package/dist/console-timeline.d.ts.map +1 -1
- package/dist/console-timeline.js +27 -26
- package/dist/console-welcome.js +2 -2
- package/dist/email-login.d.ts.map +1 -1
- package/dist/email-login.js +2 -3
- package/dist/ggui-session-channel/action-ingress.d.ts +54 -0
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -0
- package/dist/ggui-session-channel/action-ingress.js +228 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +97 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -0
- package/dist/ggui-session-channel/channel-subscriptions.js +224 -0
- package/dist/ggui-session-channel/internal-types.d.ts +102 -0
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -0
- package/dist/ggui-session-channel/internal-types.js +6 -0
- package/dist/ggui-session-channel/outbound.d.ts +81 -0
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -0
- package/dist/ggui-session-channel/outbound.js +174 -0
- package/dist/ggui-session-channel/socket-router.d.ts +38 -0
- package/dist/ggui-session-channel/socket-router.d.ts.map +1 -0
- package/dist/ggui-session-channel/socket-router.js +213 -0
- package/dist/ggui-session-channel/subscribe.d.ts +165 -0
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscribe.js +370 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +40 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.js +123 -0
- package/dist/ggui-session-channel.d.ts +425 -0
- package/dist/ggui-session-channel.d.ts.map +1 -0
- package/dist/ggui-session-channel.js +262 -0
- package/dist/health-routes.d.ts +76 -0
- package/dist/health-routes.d.ts.map +1 -0
- package/dist/health-routes.js +145 -0
- package/dist/index.d.ts +10 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -9
- package/dist/instructions-presets.d.ts +3 -3
- package/dist/instructions-presets.js +24 -24
- package/dist/llm-backed-negotiator.d.ts +68 -67
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +82 -221
- package/dist/mcp-apps-outbound.d.ts +47 -48
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +154 -177
- package/dist/mcp-endpoint-routes.d.ts +88 -0
- package/dist/mcp-endpoint-routes.d.ts.map +1 -0
- package/dist/mcp-endpoint-routes.js +359 -0
- package/dist/mcp-mounts.d.ts +2 -76
- package/dist/mcp-mounts.d.ts.map +1 -1
- package/dist/mcp-mounts.js +0 -76
- package/dist/oauth-as-routes.d.ts +60 -0
- package/dist/oauth-as-routes.d.ts.map +1 -0
- package/dist/oauth-as-routes.js +82 -0
- package/dist/oauth-clients-routes.d.ts +39 -0
- package/dist/oauth-clients-routes.d.ts.map +1 -0
- package/dist/oauth-clients-routes.js +87 -0
- package/dist/oauth-login-types.d.ts +1 -20
- package/dist/oauth-login-types.d.ts.map +1 -1
- package/dist/oauth-login-types.js +30 -7
- package/dist/oauth-login.d.ts.map +1 -1
- package/dist/oauth-login.js +3 -2
- package/dist/oauth-providers-store.d.ts.map +1 -1
- package/dist/oauth-providers-store.js +5 -5
- package/dist/oauth.d.ts +9 -8
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +41 -19
- package/dist/pairing-transport.d.ts.map +1 -1
- package/dist/pairing-transport.js +2 -1
- package/dist/request-context.d.ts +2 -2
- package/dist/request-context.js +2 -2
- package/dist/reserved-validators.d.ts.map +1 -1
- package/dist/reserved-validators.js +9 -1
- package/dist/route-param.d.ts +9 -0
- package/dist/route-param.d.ts.map +1 -0
- package/dist/route-param.js +10 -0
- package/dist/runtime-bundle-route.d.ts +43 -0
- package/dist/runtime-bundle-route.d.ts.map +1 -0
- package/dist/runtime-bundle-route.js +80 -0
- package/dist/schema-compat.d.ts +64 -62
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +23 -51
- package/dist/server.d.ts +179 -193
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +644 -3759
- package/dist/storage.d.ts +5 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +5 -5
- package/dist/thread-transport.d.ts.map +1 -1
- package/dist/thread-transport.js +4 -3
- package/dist/user-session-auth.d.ts +7 -21
- package/dist/user-session-auth.d.ts.map +1 -1
- package/dist/user-session-auth.js +7 -28
- package/package.json +16 -15
- package/dist/mcp-apps-inbound.d.ts +0 -86
- package/dist/mcp-apps-inbound.d.ts.map +0 -1
- package/dist/mcp-apps-inbound.js +0 -283
- package/dist/render-channel.d.ts +0 -694
- package/dist/render-channel.d.ts.map +0 -1
- package/dist/render-channel.js +0 -1775
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console manifest-read route.
|
|
3
|
+
*
|
|
4
|
+
* GET /ggui/console/config — VSCode-settings-style read of the
|
|
5
|
+
* resolved `ggui.json`. Returns the parsed manifest, the raw
|
|
6
|
+
* file contents for display, and the introspected v1 JSON Schema
|
|
7
|
+
* (which carries field descriptions via the `.describe()` calls
|
|
8
|
+
* on `GguiJsonV1`).
|
|
9
|
+
*
|
|
10
|
+
* Source resolution: walks up from `process.cwd()` to find the
|
|
11
|
+
* nearest `ggui.json`. Honest about three states:
|
|
12
|
+
* - found + valid → `{source: {found:true, path}, manifest, raw, schema}`
|
|
13
|
+
* - found + invalid → `{source: {found:true, path, error: {message}},
|
|
14
|
+
* raw, schema}` (no manifest field — the operator inspects the raw
|
|
15
|
+
* bytes + sees the validation error so they can fix the file)
|
|
16
|
+
* - not found → `{source: {found:false, searchedFrom}, schema}`
|
|
17
|
+
* (the schema still ships so operators can browse what would be
|
|
18
|
+
* configurable IF a manifest existed)
|
|
19
|
+
*
|
|
20
|
+
* Read-only. Form controls on the same payload and a PATCH
|
|
21
|
+
* endpoint with atomic write + conflict detection layer on top.
|
|
22
|
+
*/
|
|
23
|
+
import { GguiJsonV1 } from "@ggui-ai/project-config";
|
|
24
|
+
import { findGguiJson, safeLoadGguiJson } from "@ggui-ai/project-config/node";
|
|
25
|
+
import { readFileSync } from "node:fs";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { applyDevtoolSecurityHeaders } from "./console-headers.js";
|
|
28
|
+
/**
|
|
29
|
+
* Mount `GET /ggui/console/config` onto the express app. Returns
|
|
30
|
+
* nothing — the route self-registers.
|
|
31
|
+
*/
|
|
32
|
+
export function mountConsoleConfigRoutes(opts) {
|
|
33
|
+
const { app, logger } = opts;
|
|
34
|
+
app.get("/ggui/console/config", async (_req, res) => {
|
|
35
|
+
applyDevtoolSecurityHeaders(res);
|
|
36
|
+
const searchedFrom = process.cwd();
|
|
37
|
+
const safeSchema = (() => {
|
|
38
|
+
try {
|
|
39
|
+
// `unrepresentable: 'any'` keeps fields backed by `.transform()`
|
|
40
|
+
// (e.g. `generation.model`, parsed at the schema boundary into
|
|
41
|
+
// a typed `LlmRoute`) in the JSON Schema as `{}` instead of
|
|
42
|
+
// throwing. Transforms are runtime-only; the JSON-Schema view
|
|
43
|
+
// serves the console SPA as documentation, so a permissive
|
|
44
|
+
// shape is the honest projection.
|
|
45
|
+
return z.toJSONSchema(GguiJsonV1, { unrepresentable: "any" });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
logger.warn("console_config_schema_conversion_failed", {
|
|
49
|
+
error: String(err),
|
|
50
|
+
});
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
const path = findGguiJson(searchedFrom);
|
|
55
|
+
if (path === null) {
|
|
56
|
+
res.json({
|
|
57
|
+
source: { found: false, searchedFrom },
|
|
58
|
+
schema: safeSchema,
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let raw = null;
|
|
63
|
+
try {
|
|
64
|
+
raw = readFileSync(path, "utf-8");
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
logger.warn("console_config_read_failed", { path, error: String(err) });
|
|
68
|
+
}
|
|
69
|
+
const result = safeLoadGguiJson(path);
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
const cause = result.error.cause;
|
|
72
|
+
const errorMessage = cause instanceof Error ? cause.message : result.error.message;
|
|
73
|
+
res.json({
|
|
74
|
+
source: {
|
|
75
|
+
found: true,
|
|
76
|
+
path,
|
|
77
|
+
error: { message: errorMessage },
|
|
78
|
+
},
|
|
79
|
+
...(raw !== null ? { raw } : {}),
|
|
80
|
+
schema: safeSchema,
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
res.json({
|
|
85
|
+
source: { found: true, path },
|
|
86
|
+
manifest: result.data,
|
|
87
|
+
...(raw !== null ? { raw } : {}),
|
|
88
|
+
schema: safeSchema,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* door to inline `<script>` or `<style>` blocks (different CSP
|
|
56
56
|
* directive). Scoped, not dangerous.
|
|
57
57
|
* - `connect-src 'self'` — covers both same-origin `fetch()` to
|
|
58
|
-
* `/ggui/console/
|
|
58
|
+
* `/ggui/console/session-cookie` and the `new WebSocket('/ws')`
|
|
59
59
|
* upgrade. CSP Level 3 treats `'self'` as matching the document's
|
|
60
60
|
* origin across http/https/ws/wss, which is exactly the scope.
|
|
61
61
|
* - `img-src 'self' data:` — no images in the minimal viewer today,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console server-info route.
|
|
3
|
+
*
|
|
4
|
+
* GET /ggui/console/info — JSON describing this server (name +
|
|
5
|
+
* version + mode + pairing block + capabilities + storage). Stable
|
|
6
|
+
* shape so the SPA client in `@ggui-ai/console` can fetch once on
|
|
7
|
+
* load.
|
|
8
|
+
*
|
|
9
|
+
* Pairing block: `enabled` reflects whether the server was composed
|
|
10
|
+
* with `pairing: true|{...}`. `pending` is the current `activeInit()`
|
|
11
|
+
* read — null when no code is pending OR when pairing is disabled.
|
|
12
|
+
* The landing page renders three distinct copy paths against this
|
|
13
|
+
* shape (disabled / enabled-but-idle / enabled-with-pending) so the
|
|
14
|
+
* client never has to compose state from multiple optional fields.
|
|
15
|
+
*
|
|
16
|
+
* Capabilities block (console status dashboard):
|
|
17
|
+
* - `toolCount`: the number of MCP tool handlers this server
|
|
18
|
+
* registered. Same value `server.toolCount` exposes and the
|
|
19
|
+
* banner prints.
|
|
20
|
+
* - `blueprintCount`: operator-scoped blueprint count from the
|
|
21
|
+
* wired `uiRegistry.list()` — 0 when no registry is bound.
|
|
22
|
+
* Best-effort on read (same contract as `/ggui/console/registry`).
|
|
23
|
+
* - `primitiveCount`: sum across `primitiveCatalogs`.
|
|
24
|
+
* - `agentWired`: whether the render-channel + mcpApps path is
|
|
25
|
+
* live (the server can accept `ggui_render` + live-channel joins).
|
|
26
|
+
* - `generation.wired`: whether generation deps were bound — the
|
|
27
|
+
* `ggui_render` LLM path is active. Absent = render returns
|
|
28
|
+
* `codeReady: false` honest placeholders.
|
|
29
|
+
* - `generation.hasCredentials`: actually resolves BYOK credentials
|
|
30
|
+
* via the same seam the render handler uses, so the
|
|
31
|
+
* operator-facing pill can distinguish three honest states:
|
|
32
|
+
* off / needs-key / ready.
|
|
33
|
+
*
|
|
34
|
+
* Storage block: 'memory' when the server fell back to the in-memory
|
|
35
|
+
* default, 'custom' when the operator passed one. Keeps the label
|
|
36
|
+
* taxonomy narrow — two states the operator can act on (swap to
|
|
37
|
+
* SQLite, swap to Postgres). Implementation-name leakage (e.g.
|
|
38
|
+
* "InMemoryGguiSessionStore") would couple the wire to class names
|
|
39
|
+
* that are not part of the public contract.
|
|
40
|
+
*/
|
|
41
|
+
import type { PairingService } from "@ggui-ai/mcp-server-core";
|
|
42
|
+
import type { GenerationDeps } from "@ggui-ai/mcp-server-handlers/renders";
|
|
43
|
+
import type { DiscoveredPrimitiveCatalog } from "@ggui-ai/project-config/node";
|
|
44
|
+
import type { UiRegistry } from "@ggui-ai/ui-registry";
|
|
45
|
+
import type { Express } from "express";
|
|
46
|
+
import type { ServerInfo } from "./build-mcp.js";
|
|
47
|
+
import type { Logger } from "./logger.js";
|
|
48
|
+
interface MountOptions {
|
|
49
|
+
/** Express app to mount onto. */
|
|
50
|
+
readonly app: Express;
|
|
51
|
+
/** Server identity (name / version / description). */
|
|
52
|
+
readonly info: ServerInfo;
|
|
53
|
+
/** Operator mode surfaced so the SPA shows/hides `/devtools`. */
|
|
54
|
+
readonly mode: "dev" | "prod";
|
|
55
|
+
/** Whether pairing was enabled at composition. */
|
|
56
|
+
readonly pairingEnabled: boolean;
|
|
57
|
+
/** Pairing service (null = pairing disabled / `pending` is null). */
|
|
58
|
+
readonly pairingService: PairingService | null;
|
|
59
|
+
/** Registered tool count. */
|
|
60
|
+
readonly toolCount: number;
|
|
61
|
+
/** Declared-blueprint registry for the blueprintCount probe. */
|
|
62
|
+
readonly uiRegistry?: UiRegistry;
|
|
63
|
+
/** Discovered primitive catalogs for the primitiveCount sum. */
|
|
64
|
+
readonly primitiveCatalogs?: ReadonlyArray<DiscoveredPrimitiveCatalog>;
|
|
65
|
+
/** Whether the mcpApps render path is live (`agentWired`). */
|
|
66
|
+
readonly mcpAppsEnabled: boolean;
|
|
67
|
+
/** Generation deps — presence drives `wired`; resolveLlm drives the
|
|
68
|
+
* `hasCredentials` probe. */
|
|
69
|
+
readonly generation?: GenerationDeps;
|
|
70
|
+
/** Pre-resolved storage labels (option plumbing in the composer). */
|
|
71
|
+
readonly storage: {
|
|
72
|
+
readonly renderStore: "memory" | "custom";
|
|
73
|
+
readonly vectorStore: "memory" | "custom";
|
|
74
|
+
};
|
|
75
|
+
/** Structured logger for best-effort probe warnings. */
|
|
76
|
+
readonly logger: Logger;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Mount `GET /ggui/console/info` onto the express app. Returns
|
|
80
|
+
* nothing — the route self-registers.
|
|
81
|
+
*/
|
|
82
|
+
export declare function mountConsoleInfoRoutes(opts: MountOptions): void;
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=console-info-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-info-routes.d.ts","sourceRoot":"","sources":["../src/console-info-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC/E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,qEAAqE;IACrE,QAAQ,CAAC,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/C,6BAA6B;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC,gEAAgE;IAChE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,aAAa,CAAC,0BAA0B,CAAC,CAAC;IACvE,8DAA8D;IAC9D,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;iCAC6B;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,CAAC,WAAW,EAAE,QAAQ,GAAG,QAAQ,CAAC;QAC1C,QAAQ,CAAC,WAAW,EAAE,QAAQ,GAAG,QAAQ,CAAC;KAC3C,CAAC;IACF,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAwG/D"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console server-info route.
|
|
3
|
+
*
|
|
4
|
+
* GET /ggui/console/info — JSON describing this server (name +
|
|
5
|
+
* version + mode + pairing block + capabilities + storage). Stable
|
|
6
|
+
* shape so the SPA client in `@ggui-ai/console` can fetch once on
|
|
7
|
+
* load.
|
|
8
|
+
*
|
|
9
|
+
* Pairing block: `enabled` reflects whether the server was composed
|
|
10
|
+
* with `pairing: true|{...}`. `pending` is the current `activeInit()`
|
|
11
|
+
* read — null when no code is pending OR when pairing is disabled.
|
|
12
|
+
* The landing page renders three distinct copy paths against this
|
|
13
|
+
* shape (disabled / enabled-but-idle / enabled-with-pending) so the
|
|
14
|
+
* client never has to compose state from multiple optional fields.
|
|
15
|
+
*
|
|
16
|
+
* Capabilities block (console status dashboard):
|
|
17
|
+
* - `toolCount`: the number of MCP tool handlers this server
|
|
18
|
+
* registered. Same value `server.toolCount` exposes and the
|
|
19
|
+
* banner prints.
|
|
20
|
+
* - `blueprintCount`: operator-scoped blueprint count from the
|
|
21
|
+
* wired `uiRegistry.list()` — 0 when no registry is bound.
|
|
22
|
+
* Best-effort on read (same contract as `/ggui/console/registry`).
|
|
23
|
+
* - `primitiveCount`: sum across `primitiveCatalogs`.
|
|
24
|
+
* - `agentWired`: whether the render-channel + mcpApps path is
|
|
25
|
+
* live (the server can accept `ggui_render` + live-channel joins).
|
|
26
|
+
* - `generation.wired`: whether generation deps were bound — the
|
|
27
|
+
* `ggui_render` LLM path is active. Absent = render returns
|
|
28
|
+
* `codeReady: false` honest placeholders.
|
|
29
|
+
* - `generation.hasCredentials`: actually resolves BYOK credentials
|
|
30
|
+
* via the same seam the render handler uses, so the
|
|
31
|
+
* operator-facing pill can distinguish three honest states:
|
|
32
|
+
* off / needs-key / ready.
|
|
33
|
+
*
|
|
34
|
+
* Storage block: 'memory' when the server fell back to the in-memory
|
|
35
|
+
* default, 'custom' when the operator passed one. Keeps the label
|
|
36
|
+
* taxonomy narrow — two states the operator can act on (swap to
|
|
37
|
+
* SQLite, swap to Postgres). Implementation-name leakage (e.g.
|
|
38
|
+
* "InMemoryGguiSessionStore") would couple the wire to class names
|
|
39
|
+
* that are not part of the public contract.
|
|
40
|
+
*/
|
|
41
|
+
import { randomUUID } from "node:crypto";
|
|
42
|
+
import { DEFAULT_BUILDER_APP_ID } from "./auth.js";
|
|
43
|
+
import { applyDevtoolSecurityHeaders } from "./console-headers.js";
|
|
44
|
+
/**
|
|
45
|
+
* Mount `GET /ggui/console/info` onto the express app. Returns
|
|
46
|
+
* nothing — the route self-registers.
|
|
47
|
+
*/
|
|
48
|
+
export function mountConsoleInfoRoutes(opts) {
|
|
49
|
+
const { app, info, mode, pairingEnabled, pairingService, toolCount, uiRegistry, primitiveCatalogs, mcpAppsEnabled, generation, storage, logger, } = opts;
|
|
50
|
+
app.get("/ggui/console/info", async (_req, res) => {
|
|
51
|
+
applyDevtoolSecurityHeaders(res);
|
|
52
|
+
let pending = null;
|
|
53
|
+
if (pairingService) {
|
|
54
|
+
try {
|
|
55
|
+
pending = await pairingService.activeInit();
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// `activeInit` is a read — failure here shouldn't 500 the
|
|
59
|
+
// landing page. Log + return null; operator sees "no pending
|
|
60
|
+
// pair code" which matches reality (we couldn't read one).
|
|
61
|
+
logger.warn("console_active_init_failed", {
|
|
62
|
+
error: String(err),
|
|
63
|
+
});
|
|
64
|
+
pending = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Provider/model specifics (`generation: { provider, model }`)
|
|
68
|
+
// are intentionally NOT surfaced yet — the `UiGenerator` contract
|
|
69
|
+
// doesn't expose a read-only identity on the handle. Adding that
|
|
70
|
+
// is a generator-package change; the dashboard card lands on the
|
|
71
|
+
// simpler `generation.wired` boolean until then.
|
|
72
|
+
let blueprintCount = 0;
|
|
73
|
+
if (uiRegistry) {
|
|
74
|
+
try {
|
|
75
|
+
const list = await uiRegistry.list();
|
|
76
|
+
blueprintCount = list.length;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
logger.warn("console_info_blueprint_count_failed", {
|
|
80
|
+
error: String(err),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const primitiveCount = (primitiveCatalogs ?? []).reduce((sum, c) => sum + c.manifest.primitives.length, 0);
|
|
85
|
+
// Generation probe — `wired` reports dep binding; `hasCredentials`
|
|
86
|
+
// actually resolves BYOK credentials via the same seam the render
|
|
87
|
+
// handler uses, so the operator-facing pill can distinguish
|
|
88
|
+
// three honest states: off / needs-key / ready. The split avoids
|
|
89
|
+
// a green "wired" pill next to a "text-only" meta misleading
|
|
90
|
+
// operators when creds are missing.
|
|
91
|
+
//
|
|
92
|
+
// Probe is best-effort: resolveLlm failure (filesystem hiccup,
|
|
93
|
+
// malformed `~/.ggui/credentials.json`) reports
|
|
94
|
+
// `hasCredentials: false`, matching operator expectation
|
|
95
|
+
// "whatever is on disk, this won't fire right now." Absence of
|
|
96
|
+
// creds is a non-error path per the GenerationDeps contract.
|
|
97
|
+
let generationHasCredentials = false;
|
|
98
|
+
if (generation) {
|
|
99
|
+
try {
|
|
100
|
+
const probeResult = await generation.resolveLlm({
|
|
101
|
+
appId: DEFAULT_BUILDER_APP_ID,
|
|
102
|
+
requestId: `console-info-probe-${randomUUID()}`,
|
|
103
|
+
});
|
|
104
|
+
generationHasCredentials = probeResult !== null;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
logger.warn("console_info_credential_probe_failed", {
|
|
108
|
+
error: String(err),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const capabilities = {
|
|
113
|
+
toolCount,
|
|
114
|
+
blueprintCount,
|
|
115
|
+
primitiveCount,
|
|
116
|
+
agentWired: mcpAppsEnabled,
|
|
117
|
+
generation: {
|
|
118
|
+
wired: generation !== undefined,
|
|
119
|
+
hasCredentials: generationHasCredentials,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
res.json({
|
|
123
|
+
server: info.name,
|
|
124
|
+
version: info.version,
|
|
125
|
+
...(info.description !== undefined ? { description: info.description } : {}),
|
|
126
|
+
mode,
|
|
127
|
+
pairing: {
|
|
128
|
+
enabled: pairingEnabled,
|
|
129
|
+
pending,
|
|
130
|
+
},
|
|
131
|
+
capabilities,
|
|
132
|
+
storage,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin-gated keys plane.
|
|
3
|
+
*
|
|
4
|
+
* POST /ggui/console/admin-login — bearer → cookie exchange.
|
|
5
|
+
* GET /ggui/console/keys — list pairings + plaintext token.
|
|
6
|
+
* POST /ggui/console/keys — mint a new pairing programmatically.
|
|
7
|
+
* DELETE /ggui/console/keys/:id — revoke a pairing (idempotent).
|
|
8
|
+
*
|
|
9
|
+
* Why the gate exists: the keys plane renders plaintext bearer
|
|
10
|
+
* tokens minted by the pairing service. The persistence file
|
|
11
|
+
* (`~/.ggui/keys.json` typically) already stores them in plaintext
|
|
12
|
+
* — single-operator local-host threat model — so showing them in
|
|
13
|
+
* a same-origin admin page is a UX, not a posture, change. BUT
|
|
14
|
+
* operators expose `ggui serve` over Cloudflare tunnels for
|
|
15
|
+
* claude.ai connector use, which removes "URL is unreachable from
|
|
16
|
+
* the open internet" from the threat model. The admin token gates
|
|
17
|
+
* the keys plane against random URL discovery.
|
|
18
|
+
*
|
|
19
|
+
* Scope discipline: the gate covers `/ggui/console/keys*` +
|
|
20
|
+
* `/ggui/console/admin-login` ONLY. Other console routes
|
|
21
|
+
* (registry, renders, cached blueprints, oauth-clients) are not
|
|
22
|
+
* re-gated here — that's a separate audit slice. Adding a single-
|
|
23
|
+
* path-prefix middleware avoids re-litigating the whole console
|
|
24
|
+
* posture in one go.
|
|
25
|
+
*
|
|
26
|
+
* Auth shape: `Authorization: Bearer <admin-token>` header OR the
|
|
27
|
+
* `ggui_console_admin` cookie (HttpOnly, sameSite=Lax, Secure when
|
|
28
|
+
* the request arrived over TLS). Cookie is minted by
|
|
29
|
+
* POST /ggui/console/admin-login on a successful token paste.
|
|
30
|
+
*/
|
|
31
|
+
import type { PairingService } from "@ggui-ai/mcp-server-core";
|
|
32
|
+
import type { Express } from "express";
|
|
33
|
+
import type { Logger } from "./logger.js";
|
|
34
|
+
interface MountOptions {
|
|
35
|
+
/** Express app to mount onto. */
|
|
36
|
+
readonly app: Express;
|
|
37
|
+
/** Resolved admin token gating the plane. */
|
|
38
|
+
readonly adminToken: string;
|
|
39
|
+
/** Pairing service the keys CRUD operates on. */
|
|
40
|
+
readonly pairing: PairingService;
|
|
41
|
+
/** Structured logger. */
|
|
42
|
+
readonly logger: Logger;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Mount the admin-login + keys routes onto the express app. Returns
|
|
46
|
+
* nothing — the routes self-register.
|
|
47
|
+
*/
|
|
48
|
+
export declare function mountConsoleKeysRoutes(opts: MountOptions): void;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=console-keys-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-keys-routes.d.ts","sourceRoot":"","sources":["../src/console-keys-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAW,MAAM,SAAS,CAAC;AAEhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAI1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,6CAA6C;IAC7C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA0L/D"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin-gated keys plane.
|
|
3
|
+
*
|
|
4
|
+
* POST /ggui/console/admin-login — bearer → cookie exchange.
|
|
5
|
+
* GET /ggui/console/keys — list pairings + plaintext token.
|
|
6
|
+
* POST /ggui/console/keys — mint a new pairing programmatically.
|
|
7
|
+
* DELETE /ggui/console/keys/:id — revoke a pairing (idempotent).
|
|
8
|
+
*
|
|
9
|
+
* Why the gate exists: the keys plane renders plaintext bearer
|
|
10
|
+
* tokens minted by the pairing service. The persistence file
|
|
11
|
+
* (`~/.ggui/keys.json` typically) already stores them in plaintext
|
|
12
|
+
* — single-operator local-host threat model — so showing them in
|
|
13
|
+
* a same-origin admin page is a UX, not a posture, change. BUT
|
|
14
|
+
* operators expose `ggui serve` over Cloudflare tunnels for
|
|
15
|
+
* claude.ai connector use, which removes "URL is unreachable from
|
|
16
|
+
* the open internet" from the threat model. The admin token gates
|
|
17
|
+
* the keys plane against random URL discovery.
|
|
18
|
+
*
|
|
19
|
+
* Scope discipline: the gate covers `/ggui/console/keys*` +
|
|
20
|
+
* `/ggui/console/admin-login` ONLY. Other console routes
|
|
21
|
+
* (registry, renders, cached blueprints, oauth-clients) are not
|
|
22
|
+
* re-gated here — that's a separate audit slice. Adding a single-
|
|
23
|
+
* path-prefix middleware avoids re-litigating the whole console
|
|
24
|
+
* posture in one go.
|
|
25
|
+
*
|
|
26
|
+
* Auth shape: `Authorization: Bearer <admin-token>` header OR the
|
|
27
|
+
* `ggui_console_admin` cookie (HttpOnly, sameSite=Lax, Secure when
|
|
28
|
+
* the request arrived over TLS). Cookie is minted by
|
|
29
|
+
* POST /ggui/console/admin-login on a successful token paste.
|
|
30
|
+
*/
|
|
31
|
+
import { applyDevtoolSecurityHeaders } from "./console-headers.js";
|
|
32
|
+
const ADMIN_COOKIE_NAME = "ggui_console_admin";
|
|
33
|
+
/**
|
|
34
|
+
* Mount the admin-login + keys routes onto the express app. Returns
|
|
35
|
+
* nothing — the routes self-register.
|
|
36
|
+
*/
|
|
37
|
+
export function mountConsoleKeysRoutes(opts) {
|
|
38
|
+
const { app, adminToken, pairing, logger } = opts;
|
|
39
|
+
const requestHasAdminAuth = (req) => {
|
|
40
|
+
// Header path — `Authorization: Bearer <token>`. Constant-time
|
|
41
|
+
// compare not needed: this is single-tenant local-host with a
|
|
42
|
+
// local network attacker model; the token also has 72 bits of
|
|
43
|
+
// entropy, so a timing-side-channel attack would still need
|
|
44
|
+
// ~2^36 attempts on average to materialize. Skip the cost.
|
|
45
|
+
const authHeader = req.headers["authorization"];
|
|
46
|
+
if (typeof authHeader === "string") {
|
|
47
|
+
const match = /^Bearer\s+(.+)$/i.exec(authHeader.trim());
|
|
48
|
+
if (match && match[1] === adminToken)
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// Cookie path — same name the admin-login route sets.
|
|
52
|
+
const cookieHeader = req.headers["cookie"];
|
|
53
|
+
if (typeof cookieHeader === "string") {
|
|
54
|
+
for (const raw of cookieHeader.split(";")) {
|
|
55
|
+
const trimmed = raw.trim();
|
|
56
|
+
const eq = trimmed.indexOf("=");
|
|
57
|
+
if (eq <= 0)
|
|
58
|
+
continue;
|
|
59
|
+
const name = trimmed.slice(0, eq);
|
|
60
|
+
if (name !== ADMIN_COOKIE_NAME)
|
|
61
|
+
continue;
|
|
62
|
+
const value = decodeURIComponent(trimmed.slice(eq + 1));
|
|
63
|
+
if (value === adminToken)
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
// Same-origin posture for cookie minting: req.secure is true when
|
|
70
|
+
// the connecting socket is TLS, OR when an upstream proxy set
|
|
71
|
+
// `X-Forwarded-Proto: https` AND express trust-proxy is enabled.
|
|
72
|
+
// For zero-config local-host, trust-proxy is OFF and req.secure
|
|
73
|
+
// reflects the literal socket. Operators behind a tunnel with
|
|
74
|
+
// TLS termination at the edge get the cookie WITHOUT Secure
|
|
75
|
+
// (intended — the in-pod request is plaintext HTTP). Browsers
|
|
76
|
+
// still scope it to the origin via SameSite, which is the
|
|
77
|
+
// primary CSRF protection here; Secure is a defense-in-depth
|
|
78
|
+
// attribute, not load-bearing for this token.
|
|
79
|
+
const buildAdminCookie = (req, value) => {
|
|
80
|
+
const attrs = [
|
|
81
|
+
`${ADMIN_COOKIE_NAME}=${encodeURIComponent(value)}`,
|
|
82
|
+
"Path=/",
|
|
83
|
+
// 8-hour TTL — same posture as the console session cookie.
|
|
84
|
+
// Operators staying in the keys page longer than that just
|
|
85
|
+
// re-paste the admin token (printed on the boot banner).
|
|
86
|
+
"Max-Age=28800",
|
|
87
|
+
"SameSite=Lax",
|
|
88
|
+
"HttpOnly",
|
|
89
|
+
];
|
|
90
|
+
if (req.secure)
|
|
91
|
+
attrs.push("Secure");
|
|
92
|
+
return attrs.join("; ");
|
|
93
|
+
};
|
|
94
|
+
// POST /ggui/console/admin-login — bearer-paste → cookie exchange.
|
|
95
|
+
// No auth gate: the request body IS the credential. On match,
|
|
96
|
+
// we set the cookie and 204; on mismatch we 401. Pre-launch no-
|
|
97
|
+
// backcompat: there's no rate-limiter wired in — this is a
|
|
98
|
+
// local-host route, lock-out via wider posture (tunnel access
|
|
99
|
+
// control, Cloudflare WAF) belongs to the operator.
|
|
100
|
+
app.post("/ggui/console/admin-login", (req, res) => {
|
|
101
|
+
applyDevtoolSecurityHeaders(res);
|
|
102
|
+
const body = req.body;
|
|
103
|
+
const candidate = typeof body?.token === "string" ? body.token : "";
|
|
104
|
+
if (candidate.length === 0 || candidate !== adminToken) {
|
|
105
|
+
res.status(401).json({ error: "invalid_token" });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
res.setHeader("Set-Cookie", buildAdminCookie(req, adminToken));
|
|
109
|
+
res.status(204).end();
|
|
110
|
+
});
|
|
111
|
+
// Path-prefix gate. `app.use(path, mw)` runs `mw` for every
|
|
112
|
+
// request whose path starts with `path` — express normalizes
|
|
113
|
+
// trailing slashes / sub-paths so `/keys/abc` + `/keys` both
|
|
114
|
+
// hit. We only mount the keys routes BELOW this so the gate is
|
|
115
|
+
// genuinely the only ingress.
|
|
116
|
+
app.use("/ggui/console/keys", (req, res, next) => {
|
|
117
|
+
if (requestHasAdminAuth(req))
|
|
118
|
+
return next();
|
|
119
|
+
applyDevtoolSecurityHeaders(res);
|
|
120
|
+
res.status(401).json({ error: "admin_auth_required" });
|
|
121
|
+
});
|
|
122
|
+
// GET /ggui/console/keys — list pairings + plaintext bearer.
|
|
123
|
+
// Wire shape: `{ keys: [{pairingId, deviceName, createdAt,
|
|
124
|
+
// lastUsedAt?, token}] }`. Plaintext exposure is intentional;
|
|
125
|
+
// see PairingWithToken JSDoc for the threat model.
|
|
126
|
+
app.get("/ggui/console/keys", async (_req, res) => {
|
|
127
|
+
applyDevtoolSecurityHeaders(res);
|
|
128
|
+
try {
|
|
129
|
+
const rows = await pairing.listPairingsWithTokens();
|
|
130
|
+
res.json({
|
|
131
|
+
keys: rows.map((row) => ({
|
|
132
|
+
pairingId: row.pairingId,
|
|
133
|
+
deviceName: row.deviceName,
|
|
134
|
+
createdAt: row.createdAt,
|
|
135
|
+
...(row.lastUsedAt !== undefined ? { lastUsedAt: row.lastUsedAt } : {}),
|
|
136
|
+
token: row.token,
|
|
137
|
+
})),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
logger.warn("console_keys_list_failed", {
|
|
142
|
+
error: String(err),
|
|
143
|
+
});
|
|
144
|
+
res.status(500).json({
|
|
145
|
+
error: "list_failed",
|
|
146
|
+
message: err instanceof Error
|
|
147
|
+
? `Keys list failed — ${err.message}`
|
|
148
|
+
: `Keys list failed — ${String(err)}`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// POST /ggui/console/keys — mint a fresh pairing without
|
|
153
|
+
// round-tripping `initPairing` + `completePairing` from the SPA.
|
|
154
|
+
// We do both server-side here: (1) initPairing to get a code,
|
|
155
|
+
// (2) completePairing to consume it. Idiomatic for an admin-only
|
|
156
|
+
// surface — the operator doesn't need a 6-digit-code typed in,
|
|
157
|
+
// they're already authenticated by the admin token. Returns the
|
|
158
|
+
// full `PairingCompletion` so the SPA can show the plaintext
|
|
159
|
+
// bearer in a one-time copy callout.
|
|
160
|
+
app.post("/ggui/console/keys", async (req, res) => {
|
|
161
|
+
applyDevtoolSecurityHeaders(res);
|
|
162
|
+
const body = req.body;
|
|
163
|
+
const deviceName = typeof body?.deviceName === "string" ? body.deviceName.trim() : "";
|
|
164
|
+
if (deviceName.length === 0 || deviceName.length > 256) {
|
|
165
|
+
res.status(400).json({
|
|
166
|
+
error: "invalid_device_name",
|
|
167
|
+
message: "`deviceName` is required (non-empty string, ≤256 chars).",
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const init = await pairing.initPairing();
|
|
173
|
+
const completion = await pairing.completePairing({
|
|
174
|
+
code: init.code,
|
|
175
|
+
deviceName,
|
|
176
|
+
});
|
|
177
|
+
res.json({
|
|
178
|
+
pairingId: completion.pairingId,
|
|
179
|
+
token: completion.token,
|
|
180
|
+
serverName: completion.serverName,
|
|
181
|
+
deviceName: completion.deviceName,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
logger.warn("console_keys_mint_failed", {
|
|
186
|
+
error: String(err),
|
|
187
|
+
});
|
|
188
|
+
res.status(500).json({
|
|
189
|
+
error: "mint_failed",
|
|
190
|
+
message: err instanceof Error ? `Mint failed — ${err.message}` : `Mint failed — ${String(err)}`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
// DELETE /ggui/console/keys/:pairingId — revoke (idempotent).
|
|
195
|
+
app.delete("/ggui/console/keys/:pairingId", async (req, res) => {
|
|
196
|
+
applyDevtoolSecurityHeaders(res);
|
|
197
|
+
const pairingId = req.params["pairingId"];
|
|
198
|
+
if (typeof pairingId !== "string" || pairingId.length === 0) {
|
|
199
|
+
res.status(400).json({
|
|
200
|
+
error: "missing_pairing_id",
|
|
201
|
+
message: "pairingId required in path segment (e.g. DELETE /ggui/console/keys/pair-1).",
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
await pairing.revokePairing(pairingId);
|
|
207
|
+
res.status(204).end();
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
logger.warn("console_keys_revoke_failed", {
|
|
211
|
+
error: String(err),
|
|
212
|
+
pairingId,
|
|
213
|
+
});
|
|
214
|
+
res.status(500).json({
|
|
215
|
+
error: "revoke_failed",
|
|
216
|
+
message: err instanceof Error
|
|
217
|
+
? `Revoke failed — ${err.message}`
|
|
218
|
+
: `Revoke failed — ${String(err)}`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BYOK LLM-keys plane — gated /ggui/console/llm-keys. Two postures:
|
|
3
|
+
*
|
|
4
|
+
* - `gateMode: 'admin-token'` (default — OSS-personal):
|
|
5
|
+
* gate accepts the admin bearer. Single global keyset; the
|
|
6
|
+
* /settings UI lets the operator paste keys for everyone.
|
|
7
|
+
* - `gateMode: 'auth-adapter'` (multi-tenant): gate
|
|
8
|
+
* calls the server's `AuthAdapter`. Each user's keys are
|
|
9
|
+
* scoped by `scopeFromRequest` (default: `userId`/`appId`).
|
|
10
|
+
*
|
|
11
|
+
* Wire shape (same in both gates):
|
|
12
|
+
* GET /ggui/console/llm-keys — list providers + presence
|
|
13
|
+
* POST /ggui/console/llm-keys — set { provider, key }
|
|
14
|
+
* DELETE /ggui/console/llm-keys/:provider — clear (idempotent)
|
|
15
|
+
* POST /ggui/console/llm-keys/:provider/probe — auth-validation probe
|
|
16
|
+
*
|
|
17
|
+
* Plaintext is NEVER returned on GET — unlike pairing tokens, an LLM
|
|
18
|
+
* key is a one-way paste (operator already has the key elsewhere; the
|
|
19
|
+
* server is just persisting it). The presence + source signal is
|
|
20
|
+
* enough for the /settings UI.
|
|
21
|
+
*/
|
|
22
|
+
import type { AuthAdapter, AuthResult, ProviderKeyStore } from "@ggui-ai/mcp-server-core";
|
|
23
|
+
import type { Express, Request } from "express";
|
|
24
|
+
import type { Logger } from "./logger.js";
|
|
25
|
+
interface MountOptions {
|
|
26
|
+
/** Express app to mount onto. */
|
|
27
|
+
readonly app: Express;
|
|
28
|
+
/** BYOK provider-key store backing list/set/delete/probe. */
|
|
29
|
+
readonly providerKeys: ProviderKeyStore;
|
|
30
|
+
/** Which gate guards the plane. */
|
|
31
|
+
readonly gateMode: "admin-token" | "auth-adapter";
|
|
32
|
+
/** Admin token for `admin-token` gate mode (null = gate never passes). */
|
|
33
|
+
readonly adminToken: string | null;
|
|
34
|
+
/** Auth adapter for `auth-adapter` gate mode. */
|
|
35
|
+
readonly auth: AuthAdapter;
|
|
36
|
+
/** Per-request scope resolver override (default: userId/appId/global). */
|
|
37
|
+
readonly scopeFromRequest?: (req: Request, identity: AuthResult | null) => string;
|
|
38
|
+
/** Structured logger. */
|
|
39
|
+
readonly logger: Logger;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Mount the LLM-keys routes onto the express app. Returns nothing —
|
|
43
|
+
* the routes self-register.
|
|
44
|
+
*/
|
|
45
|
+
export declare function mountConsoleLlmKeysRoutes(opts: MountOptions): void;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=console-llm-keys-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-llm-keys-routes.d.ts","sourceRoot":"","sources":["../src/console-llm-keys-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EAEV,gBAAgB,EACjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AA+B1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,mCAAmC;IACnC,QAAQ,CAAC,QAAQ,EAAE,aAAa,GAAG,cAAc,CAAC;IAClD,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,GAAG,IAAI,KAAK,MAAM,CAAC;IAClF,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAgZlE"}
|