@ggui-ai/mcp-server 0.2.0-alpha.3 → 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 +25 -25
- 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 -248
- 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 +204 -183
- 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,443 @@
|
|
|
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 { resolveIdentity, UnauthenticatedError } from "./auth.js";
|
|
23
|
+
import { applyDevtoolSecurityHeaders } from "./console-headers.js";
|
|
24
|
+
const ADMIN_COOKIE_NAME_LLM = "ggui_console_admin";
|
|
25
|
+
/**
|
|
26
|
+
* The LLM_PROVIDERS allowlist matches `LlmProvider` minus `bedrock`
|
|
27
|
+
* (which is IAM-based and never paste-resolvable — see byok-resolver
|
|
28
|
+
* PROVIDER_ENV_NAMES bedrock note). Order is the operator-facing
|
|
29
|
+
* display order: anthropic first since the OSS triad ships claude
|
|
30
|
+
* as the default model.
|
|
31
|
+
*/
|
|
32
|
+
const LLM_PROVIDERS = [
|
|
33
|
+
"anthropic",
|
|
34
|
+
"openai",
|
|
35
|
+
"google",
|
|
36
|
+
"openrouter",
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Mirror of `byok-resolver.ts::PROVIDER_ENV_NAMES` — env-var name
|
|
40
|
+
* ordered list per provider, first non-empty wins. Mirrored rather
|
|
41
|
+
* than imported because `@ggui-ai/cli` is downstream of this
|
|
42
|
+
* package and we can't depend back the other direction.
|
|
43
|
+
*/
|
|
44
|
+
const PROVIDER_ENV_NAMES = {
|
|
45
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
46
|
+
google: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
|
47
|
+
openai: ["OPENAI_API_KEY"],
|
|
48
|
+
openrouter: ["OPENROUTER_API_KEY"],
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Mount the LLM-keys routes onto the express app. Returns nothing —
|
|
52
|
+
* the routes self-register.
|
|
53
|
+
*/
|
|
54
|
+
export function mountConsoleLlmKeysRoutes(opts) {
|
|
55
|
+
const { app, providerKeys, gateMode, adminToken, auth, logger } = opts;
|
|
56
|
+
const defaultScope = (_req, identity) => {
|
|
57
|
+
if (identity) {
|
|
58
|
+
if (identity.identity.kind === "user") {
|
|
59
|
+
return identity.identity.userId;
|
|
60
|
+
}
|
|
61
|
+
if (identity.identity.kind === "app") {
|
|
62
|
+
return identity.identity.appId;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return "global";
|
|
66
|
+
};
|
|
67
|
+
const scopeFromRequest = opts.scopeFromRequest ?? defaultScope;
|
|
68
|
+
const requestHasAdminAuthLlm = (req) => {
|
|
69
|
+
if (adminToken === null)
|
|
70
|
+
return false;
|
|
71
|
+
const authHeader = req.headers["authorization"];
|
|
72
|
+
if (typeof authHeader === "string") {
|
|
73
|
+
const match = /^Bearer\s+(.+)$/i.exec(authHeader.trim());
|
|
74
|
+
if (match && match[1] === adminToken)
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const cookieHeader = req.headers["cookie"];
|
|
78
|
+
if (typeof cookieHeader === "string") {
|
|
79
|
+
for (const raw of cookieHeader.split(";")) {
|
|
80
|
+
const trimmed = raw.trim();
|
|
81
|
+
const eq = trimmed.indexOf("=");
|
|
82
|
+
if (eq <= 0)
|
|
83
|
+
continue;
|
|
84
|
+
const name = trimmed.slice(0, eq);
|
|
85
|
+
if (name !== ADMIN_COOKIE_NAME_LLM)
|
|
86
|
+
continue;
|
|
87
|
+
const value = decodeURIComponent(trimmed.slice(eq + 1));
|
|
88
|
+
if (value === adminToken)
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
};
|
|
94
|
+
// Per-request identity stash. Populated by the gate when in
|
|
95
|
+
// 'auth-adapter' mode; left null under 'admin-token'. Route
|
|
96
|
+
// handlers read it via `getRequestIdentity(req)` so the scope
|
|
97
|
+
// resolver receives the resolved AuthResult without re-running
|
|
98
|
+
// `resolveIdentity`.
|
|
99
|
+
const llmKeysIdentityByRequest = new WeakMap();
|
|
100
|
+
const getRequestIdentity = (req) => llmKeysIdentityByRequest.get(req) ?? null;
|
|
101
|
+
// Path-prefix gate.
|
|
102
|
+
//
|
|
103
|
+
// admin-token → same shape as /keys (Bearer admin OR
|
|
104
|
+
// ggui_console_admin cookie).
|
|
105
|
+
// auth-adapter → calls `resolveIdentity(auth, req)`. `kind:'builder'`
|
|
106
|
+
// is rejected with 401 — the multi-tenant posture
|
|
107
|
+
// is meaningless without a real per-caller id.
|
|
108
|
+
app.use("/ggui/console/llm-keys", (req, res, next) => {
|
|
109
|
+
if (gateMode === "admin-token") {
|
|
110
|
+
if (requestHasAdminAuthLlm(req))
|
|
111
|
+
return next();
|
|
112
|
+
applyDevtoolSecurityHeaders(res);
|
|
113
|
+
res.status(401).json({ error: "admin_auth_required" });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// 'auth-adapter' — the multi-tenant gate.
|
|
117
|
+
resolveIdentity(auth, req)
|
|
118
|
+
.then((identity) => {
|
|
119
|
+
if (identity.identity.kind === "builder") {
|
|
120
|
+
applyDevtoolSecurityHeaders(res);
|
|
121
|
+
res.status(401).json({
|
|
122
|
+
error: "tenant_required",
|
|
123
|
+
message: "Multi-tenant /llm-keys requires an end-user or app identity. " +
|
|
124
|
+
'The configured AuthAdapter resolved kind:"builder" — pair a ' +
|
|
125
|
+
'real user/app bearer or use providerKeysGate:"admin-token".',
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
llmKeysIdentityByRequest.set(req, identity);
|
|
130
|
+
next();
|
|
131
|
+
})
|
|
132
|
+
.catch((err) => {
|
|
133
|
+
applyDevtoolSecurityHeaders(res);
|
|
134
|
+
if (err instanceof UnauthenticatedError) {
|
|
135
|
+
res.status(401).json({ error: "unauthenticated" });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
logger.warn("console_llm_keys_auth_failed", { error: String(err) });
|
|
139
|
+
res.status(500).json({ error: "auth_unexpected_error" });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
// GET /ggui/console/llm-keys — list providers + presence.
|
|
143
|
+
// Each row reports:
|
|
144
|
+
// - name: 'anthropic' | 'openai' | 'google' | 'openrouter'
|
|
145
|
+
// - configured: boolean — true when EITHER env OR file has a key
|
|
146
|
+
// - source: 'env' | 'file' | null — env wins on collision
|
|
147
|
+
// - envName: which env var fired (only present when source='env')
|
|
148
|
+
// - envNames: the env-var names this provider accepts
|
|
149
|
+
// (informational — the /settings UI shows them so
|
|
150
|
+
// operators know they can `export` instead of pasting)
|
|
151
|
+
app.get("/ggui/console/llm-keys", async (req, res) => {
|
|
152
|
+
applyDevtoolSecurityHeaders(res);
|
|
153
|
+
try {
|
|
154
|
+
const scope = scopeFromRequest(req, getRequestIdentity(req));
|
|
155
|
+
const filePresent = new Set(await providerKeys.listProviders(scope));
|
|
156
|
+
// Build rows with an optional 12-char prefix preview so the
|
|
157
|
+
// operator can confirm WHICH key is loaded without exposing
|
|
158
|
+
// the secret. 12 chars covers the discriminating prefix
|
|
159
|
+
// (`sk-ant-api03`, `sk-ant-oat01`, `sk-or-v1-…`, etc.) while
|
|
160
|
+
// staying safely under the entropy floor — these prefixes
|
|
161
|
+
// are not secret on their own.
|
|
162
|
+
const rows = await Promise.all(LLM_PROVIDERS.map(async (provider) => {
|
|
163
|
+
const envNames = PROVIDER_ENV_NAMES[provider];
|
|
164
|
+
let envHit;
|
|
165
|
+
let envValue;
|
|
166
|
+
for (const name of envNames) {
|
|
167
|
+
const value = process.env[name];
|
|
168
|
+
if (value !== undefined && value.length > 0) {
|
|
169
|
+
envHit = name;
|
|
170
|
+
envValue = value;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const inFile = filePresent.has(provider);
|
|
175
|
+
const source = envHit !== undefined ? "env" : inFile ? "file" : null;
|
|
176
|
+
let keyPreview;
|
|
177
|
+
if (envValue !== undefined) {
|
|
178
|
+
keyPreview = envValue.slice(0, 12);
|
|
179
|
+
}
|
|
180
|
+
else if (inFile) {
|
|
181
|
+
try {
|
|
182
|
+
const ref = await providerKeys.get(scope, provider);
|
|
183
|
+
if (ref)
|
|
184
|
+
keyPreview = ref.key.slice(0, 12);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Best-effort — preview is advisory; never block the GET.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
name: provider,
|
|
192
|
+
configured: source !== null,
|
|
193
|
+
source,
|
|
194
|
+
...(envHit !== undefined ? { envName: envHit } : {}),
|
|
195
|
+
envNames: [...envNames],
|
|
196
|
+
inFile,
|
|
197
|
+
...(keyPreview !== undefined ? { keyPreview } : {}),
|
|
198
|
+
};
|
|
199
|
+
}));
|
|
200
|
+
res.json({
|
|
201
|
+
providers: rows,
|
|
202
|
+
scope,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
logger.warn("console_llm_keys_list_failed", {
|
|
207
|
+
error: String(err),
|
|
208
|
+
});
|
|
209
|
+
res.status(500).json({
|
|
210
|
+
error: "list_failed",
|
|
211
|
+
message: err instanceof Error
|
|
212
|
+
? `LLM keys list failed — ${err.message}`
|
|
213
|
+
: `LLM keys list failed — ${String(err)}`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
// POST /ggui/console/llm-keys — set a provider's key.
|
|
218
|
+
// Body: { provider: 'anthropic'|'openai'|..., key: string }.
|
|
219
|
+
// Returns: { provider, source: 'file', envOverridden: boolean }.
|
|
220
|
+
// `envOverridden: true` means the operator pasted a key but env
|
|
221
|
+
// var also set — the resolver still picks env. The /settings UI
|
|
222
|
+
// surfaces this so operators don't think their paste is in effect.
|
|
223
|
+
app.post("/ggui/console/llm-keys", async (req, res) => {
|
|
224
|
+
applyDevtoolSecurityHeaders(res);
|
|
225
|
+
const body = req.body;
|
|
226
|
+
const provider = typeof body?.provider === "string" ? body.provider : "";
|
|
227
|
+
const key = typeof body?.key === "string" ? body.key.trim() : "";
|
|
228
|
+
if (!LLM_PROVIDERS.includes(provider)) {
|
|
229
|
+
res.status(400).json({
|
|
230
|
+
error: "invalid_provider",
|
|
231
|
+
message: `provider must be one of ${LLM_PROVIDERS.join(", ")}.`,
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (key.length === 0 || key.length > 4096) {
|
|
236
|
+
res.status(400).json({
|
|
237
|
+
error: "invalid_key",
|
|
238
|
+
message: "`key` is required (non-empty string, ≤4096 chars).",
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const typedProvider = provider;
|
|
244
|
+
const scope = scopeFromRequest(req, getRequestIdentity(req));
|
|
245
|
+
await providerKeys.set(scope, typedProvider, key);
|
|
246
|
+
const envNames = PROVIDER_ENV_NAMES[typedProvider];
|
|
247
|
+
let envOverride;
|
|
248
|
+
for (const name of envNames) {
|
|
249
|
+
const value = process.env[name];
|
|
250
|
+
if (value !== undefined && value.length > 0) {
|
|
251
|
+
envOverride = name;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
res.json({
|
|
256
|
+
provider: typedProvider,
|
|
257
|
+
source: "file",
|
|
258
|
+
envOverridden: envOverride !== undefined,
|
|
259
|
+
...(envOverride !== undefined ? { envName: envOverride } : {}),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
logger.warn("console_llm_keys_set_failed", {
|
|
264
|
+
error: String(err),
|
|
265
|
+
provider,
|
|
266
|
+
});
|
|
267
|
+
res.status(500).json({
|
|
268
|
+
error: "set_failed",
|
|
269
|
+
message: err instanceof Error
|
|
270
|
+
? `LLM key set failed — ${err.message}`
|
|
271
|
+
: `LLM key set failed — ${String(err)}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
// DELETE /ggui/console/llm-keys/:provider — clear (idempotent).
|
|
276
|
+
// 204 even when the key wasn't set; mirrors `ProviderKeyStore.delete`
|
|
277
|
+
// contract. NEVER touches env — the operator owns env separately.
|
|
278
|
+
app.delete("/ggui/console/llm-keys/:provider", async (req, res) => {
|
|
279
|
+
applyDevtoolSecurityHeaders(res);
|
|
280
|
+
const provider = req.params["provider"];
|
|
281
|
+
if (!LLM_PROVIDERS.includes(provider)) {
|
|
282
|
+
res.status(400).json({
|
|
283
|
+
error: "invalid_provider",
|
|
284
|
+
message: `provider must be one of ${LLM_PROVIDERS.join(", ")}.`,
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const typedProvider = provider;
|
|
290
|
+
const scope = scopeFromRequest(req, getRequestIdentity(req));
|
|
291
|
+
await providerKeys.delete(scope, typedProvider);
|
|
292
|
+
res.status(204).end();
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
logger.warn("console_llm_keys_delete_failed", {
|
|
296
|
+
error: String(err),
|
|
297
|
+
provider,
|
|
298
|
+
});
|
|
299
|
+
res.status(500).json({
|
|
300
|
+
error: "delete_failed",
|
|
301
|
+
message: err instanceof Error
|
|
302
|
+
? `LLM key delete failed — ${err.message}`
|
|
303
|
+
: `LLM key delete failed — ${String(err)}`,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
// POST /ggui/console/llm-keys/:provider/probe — auth-validation health
|
|
308
|
+
// probe for the configured key. Hits each provider's cheapest
|
|
309
|
+
// auth-checking endpoint with a 5s timeout. Status code is always
|
|
310
|
+
// 200 — the `ok` flag carries the verdict so the UI can paint a dot
|
|
311
|
+
// without branching on HTTP status. NEVER returns or logs the key
|
|
312
|
+
// value (latency + ok-flag + provider name only).
|
|
313
|
+
const probeProvider = async (provider, key) => {
|
|
314
|
+
const start = Date.now();
|
|
315
|
+
const ac = new AbortController();
|
|
316
|
+
const timer = setTimeout(() => {
|
|
317
|
+
ac.abort();
|
|
318
|
+
}, 5000);
|
|
319
|
+
try {
|
|
320
|
+
let url;
|
|
321
|
+
const headers = {};
|
|
322
|
+
if (provider === "anthropic") {
|
|
323
|
+
url = "https://api.anthropic.com/v1/models?limit=1";
|
|
324
|
+
headers["x-api-key"] = key;
|
|
325
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
326
|
+
}
|
|
327
|
+
else if (provider === "openai") {
|
|
328
|
+
url = "https://api.openai.com/v1/models";
|
|
329
|
+
headers["Authorization"] = `Bearer ${key}`;
|
|
330
|
+
}
|
|
331
|
+
else if (provider === "google") {
|
|
332
|
+
// Google uses query-param key auth — no Authorization header.
|
|
333
|
+
url = `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1&key=${encodeURIComponent(key)}`;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// openrouter — auth/key endpoint validates without listing.
|
|
337
|
+
url = "https://openrouter.ai/api/v1/auth/key";
|
|
338
|
+
headers["Authorization"] = `Bearer ${key}`;
|
|
339
|
+
}
|
|
340
|
+
const res = await globalThis.fetch(url, {
|
|
341
|
+
method: "GET",
|
|
342
|
+
headers,
|
|
343
|
+
signal: ac.signal,
|
|
344
|
+
});
|
|
345
|
+
const latencyMs = Date.now() - start;
|
|
346
|
+
if (res.ok) {
|
|
347
|
+
return { ok: true, latencyMs };
|
|
348
|
+
}
|
|
349
|
+
let detail = "";
|
|
350
|
+
try {
|
|
351
|
+
const body = (await res.json());
|
|
352
|
+
const errField = body.error;
|
|
353
|
+
const errMessage = typeof errField === "string"
|
|
354
|
+
? errField
|
|
355
|
+
: errField !== null &&
|
|
356
|
+
typeof errField === "object" &&
|
|
357
|
+
"message" in errField &&
|
|
358
|
+
typeof errField.message === "string"
|
|
359
|
+
? errField.message
|
|
360
|
+
: typeof body.message === "string"
|
|
361
|
+
? body.message
|
|
362
|
+
: "";
|
|
363
|
+
if (errMessage.length > 0)
|
|
364
|
+
detail = ` ${errMessage}`;
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Non-JSON body — error code alone is enough for the dot.
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
ok: false,
|
|
371
|
+
latencyMs,
|
|
372
|
+
error: `HTTP ${res.status}${detail}`.slice(0, 200),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
const latencyMs = Date.now() - start;
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
latencyMs,
|
|
380
|
+
error: String(err).slice(0, 200),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
app.post("/ggui/console/llm-keys/:provider/probe", async (req, res) => {
|
|
388
|
+
applyDevtoolSecurityHeaders(res);
|
|
389
|
+
const provider = req.params["provider"];
|
|
390
|
+
if (!LLM_PROVIDERS.includes(provider)) {
|
|
391
|
+
res.status(400).json({
|
|
392
|
+
error: "invalid_provider",
|
|
393
|
+
message: `provider must be one of ${LLM_PROVIDERS.join(", ")}.`,
|
|
394
|
+
});
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const typedProvider = provider;
|
|
399
|
+
// Resolve env-first-then-file, mirroring the GET handler's
|
|
400
|
+
// source rule. Env wins on collision so the probe matches what
|
|
401
|
+
// the generation pipeline would actually send.
|
|
402
|
+
const envNames = PROVIDER_ENV_NAMES[typedProvider];
|
|
403
|
+
let resolvedKey = null;
|
|
404
|
+
for (const name of envNames) {
|
|
405
|
+
const value = process.env[name];
|
|
406
|
+
if (value !== undefined && value.length > 0) {
|
|
407
|
+
resolvedKey = value;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (resolvedKey === null) {
|
|
412
|
+
const scope = scopeFromRequest(req, getRequestIdentity(req));
|
|
413
|
+
const ref = await providerKeys.get(scope, typedProvider);
|
|
414
|
+
if (ref !== null && ref.key.length > 0) {
|
|
415
|
+
resolvedKey = ref.key;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (resolvedKey === null) {
|
|
419
|
+
res.status(400).json({ ok: false, error: "not_configured" });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const result = await probeProvider(typedProvider, resolvedKey);
|
|
423
|
+
logger.info("console_llm_keys_probe", {
|
|
424
|
+
provider: typedProvider,
|
|
425
|
+
ok: result.ok,
|
|
426
|
+
latencyMs: result.latencyMs,
|
|
427
|
+
});
|
|
428
|
+
res.json(result);
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
logger.warn("console_llm_keys_probe_failed", {
|
|
432
|
+
error: String(err),
|
|
433
|
+
provider,
|
|
434
|
+
});
|
|
435
|
+
res.status(500).json({
|
|
436
|
+
error: "probe_failed",
|
|
437
|
+
message: err instanceof Error
|
|
438
|
+
? `LLM key probe failed — ${err.message}`
|
|
439
|
+
: `LLM key probe failed — ${String(err)}`,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console MCP-tool inventory route.
|
|
3
|
+
*
|
|
4
|
+
* GET /ggui/console/mcp/tools — registered MCP-tool inventory for
|
|
5
|
+
* the console SPA's `/mcp` page. Operator-facing "what tools does
|
|
6
|
+
* my server expose?" — same handler set the `/mcp` JSON-RPC
|
|
7
|
+
* endpoint surfaces via `tools/list`, but rendered as cards
|
|
8
|
+
* instead of curl output.
|
|
9
|
+
*
|
|
10
|
+
* Currently LIST-only — name + title? + description + input/output
|
|
11
|
+
* JSON Schema. A "test invoke" form is deferred — invoking a tool
|
|
12
|
+
* from the console needs a same-origin bearer claim story (console
|
|
13
|
+
* session cookie currently authenticates only the live-channel WS
|
|
14
|
+
* upgrade).
|
|
15
|
+
*
|
|
16
|
+
* Schema conversion: handlers carry Zod raw shapes; the wire
|
|
17
|
+
* needs JSON Schema. `z.toJSONSchema(z.object(rawShape))` does
|
|
18
|
+
* the conversion. Failure (e.g. an exotic Zod type the v4
|
|
19
|
+
* converter doesn't yet support) reports `{}` for that field
|
|
20
|
+
* and warn-logs — operators still see name + description, just
|
|
21
|
+
* without typed input/output detail.
|
|
22
|
+
*/
|
|
23
|
+
import type { SharedHandler } from "@ggui-ai/mcp-server-handlers";
|
|
24
|
+
import type { Express } from "express";
|
|
25
|
+
import { type ZodRawShape } from "zod";
|
|
26
|
+
import type { Logger } from "./logger.js";
|
|
27
|
+
interface MountOptions {
|
|
28
|
+
/** Express app to mount onto. */
|
|
29
|
+
readonly app: Express;
|
|
30
|
+
/** Composed handler list the inventory projects. */
|
|
31
|
+
readonly handlers: ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>;
|
|
32
|
+
/** Structured logger for schema-conversion warnings. */
|
|
33
|
+
readonly logger: Logger;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Mount `GET /ggui/console/mcp/tools` onto the express app. Returns
|
|
37
|
+
* nothing — the route self-registers.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mountConsoleMcpToolsRoutes(opts: MountOptions): void;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=console-mcp-tools-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-mcp-tools-routes.d.ts","sourceRoot":"","sources":["../src/console-mcp-tools-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAE1C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1E,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAwCnE"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console MCP-tool inventory route.
|
|
3
|
+
*
|
|
4
|
+
* GET /ggui/console/mcp/tools — registered MCP-tool inventory for
|
|
5
|
+
* the console SPA's `/mcp` page. Operator-facing "what tools does
|
|
6
|
+
* my server expose?" — same handler set the `/mcp` JSON-RPC
|
|
7
|
+
* endpoint surfaces via `tools/list`, but rendered as cards
|
|
8
|
+
* instead of curl output.
|
|
9
|
+
*
|
|
10
|
+
* Currently LIST-only — name + title? + description + input/output
|
|
11
|
+
* JSON Schema. A "test invoke" form is deferred — invoking a tool
|
|
12
|
+
* from the console needs a same-origin bearer claim story (console
|
|
13
|
+
* session cookie currently authenticates only the live-channel WS
|
|
14
|
+
* upgrade).
|
|
15
|
+
*
|
|
16
|
+
* Schema conversion: handlers carry Zod raw shapes; the wire
|
|
17
|
+
* needs JSON Schema. `z.toJSONSchema(z.object(rawShape))` does
|
|
18
|
+
* the conversion. Failure (e.g. an exotic Zod type the v4
|
|
19
|
+
* converter doesn't yet support) reports `{}` for that field
|
|
20
|
+
* and warn-logs — operators still see name + description, just
|
|
21
|
+
* without typed input/output detail.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { applyDevtoolSecurityHeaders } from "./console-headers.js";
|
|
25
|
+
/**
|
|
26
|
+
* Mount `GET /ggui/console/mcp/tools` onto the express app. Returns
|
|
27
|
+
* nothing — the route self-registers.
|
|
28
|
+
*/
|
|
29
|
+
export function mountConsoleMcpToolsRoutes(opts) {
|
|
30
|
+
const { app, handlers, logger } = opts;
|
|
31
|
+
app.get("/ggui/console/mcp/tools", async (_req, res) => {
|
|
32
|
+
applyDevtoolSecurityHeaders(res);
|
|
33
|
+
const safeToJsonSchema = (shape) => {
|
|
34
|
+
try {
|
|
35
|
+
return z.toJSONSchema(z.object(shape));
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
logger.warn("console_mcp_tools_schema_conversion_failed", {
|
|
39
|
+
error: String(err),
|
|
40
|
+
});
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const tools = handlers.map((h) => {
|
|
45
|
+
const summary = {
|
|
46
|
+
name: h.name,
|
|
47
|
+
...(h.title !== undefined ? { title: h.title } : {}),
|
|
48
|
+
description: h.description,
|
|
49
|
+
inputSchema: safeToJsonSchema(h.inputSchema),
|
|
50
|
+
outputSchema: safeToJsonSchema(h.outputSchema),
|
|
51
|
+
};
|
|
52
|
+
return summary;
|
|
53
|
+
});
|
|
54
|
+
// Stable sort by name for deterministic operator reading order
|
|
55
|
+
// — handlers are registered in module-import order, which is
|
|
56
|
+
// arbitrary from the operator's perspective.
|
|
57
|
+
tools.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
|
+
res.json({ tools, total: tools.length });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console blueprint-catalog read/maintenance routes.
|
|
3
|
+
*
|
|
4
|
+
* GET /ggui/console/registry — declared-blueprint +
|
|
5
|
+
* primitive catalog (boot-time content).
|
|
6
|
+
* GET /ggui/console/blueprints/cached — intent-keyed
|
|
7
|
+
* generation-cache listing (legacy surface).
|
|
8
|
+
* DELETE /ggui/console/blueprints/cached/:id — invalidate one entry.
|
|
9
|
+
* POST /ggui/console/blueprints/cached/clear — bulk-clear the scope.
|
|
10
|
+
* GET /ggui/console/blueprints/registry — contract-keyed runtime
|
|
11
|
+
* registry the matcher actually consults (per-row tier
|
|
12
|
+
* diagnostics: kind, contractKey, hitCount, lastHitAt,
|
|
13
|
+
* provenance).
|
|
14
|
+
*
|
|
15
|
+
* `/registry` sources:
|
|
16
|
+
* - `blueprints[]` ← `uiRegistry?.list()` (full manifest entries;
|
|
17
|
+
* surfaces `name` + `description?` + `category?`). Same registry
|
|
18
|
+
* the `/ggui/console/blueprint/:id` endpoint resolves against, so
|
|
19
|
+
* the /registry click-through to /preview/<id> is guaranteed to
|
|
20
|
+
* hit the same dataset.
|
|
21
|
+
* - `primitives[]` ← `primitiveCatalogs` (`DiscoveredPrimitiveCatalog`
|
|
22
|
+
* shape from `@ggui-ai/project-config`). Each entry flattens
|
|
23
|
+
* one primitive per row, tagging it with its catalog's
|
|
24
|
+
* `source` ('package' | 'local') + `import` specifier.
|
|
25
|
+
*
|
|
26
|
+
* Scope: read-only on `/registry` (authoring happens on disk and the
|
|
27
|
+
* server re-reads on boot; zero-config is an honest empty shape).
|
|
28
|
+
* The cache/registry surfaces are scoped to `DEFAULT_BUILDER_APP_ID`
|
|
29
|
+
* because the OSS server is single-tenant by construction — same
|
|
30
|
+
* scope the render handler writes to, so list/invalidate/clear see
|
|
31
|
+
* what the real cache writes.
|
|
32
|
+
*
|
|
33
|
+
* Enumeration gate: `listGenerationCache` + `clearGenerationCache` +
|
|
34
|
+
* `listBlueprints` require an EnumerableVectorStore. Every OSS
|
|
35
|
+
* default satisfies it (`InMemoryVectorStore`, `SqliteVectorStore`);
|
|
36
|
+
* deployments that wire a non-enumerable backend get an honest `501`
|
|
37
|
+
* with a reason code rather than a silent empty list or a runtime
|
|
38
|
+
* throw.
|
|
39
|
+
*
|
|
40
|
+
* No bearer auth (same rule as the other console endpoints —
|
|
41
|
+
* same-origin operator-facing).
|
|
42
|
+
*/
|
|
43
|
+
import type { VectorStore } from "@ggui-ai/mcp-server-core";
|
|
44
|
+
import type { DiscoveredPrimitiveCatalog } from "@ggui-ai/project-config/node";
|
|
45
|
+
import type { UiRegistry } from "@ggui-ai/ui-registry";
|
|
46
|
+
import type { Express } from "express";
|
|
47
|
+
import type { Logger } from "./logger.js";
|
|
48
|
+
interface MountOptions {
|
|
49
|
+
/** Express app to mount onto. */
|
|
50
|
+
readonly app: Express;
|
|
51
|
+
/** Declared-blueprint registry (absent = empty `blueprints[]`). */
|
|
52
|
+
readonly uiRegistry?: UiRegistry;
|
|
53
|
+
/** Discovered primitive catalogs (absent = empty `primitives[]`). */
|
|
54
|
+
readonly primitiveCatalogs?: ReadonlyArray<DiscoveredPrimitiveCatalog>;
|
|
55
|
+
/** Vector store backing the generation cache + runtime registry. */
|
|
56
|
+
readonly vectors: VectorStore;
|
|
57
|
+
/** Structured logger. */
|
|
58
|
+
readonly logger: Logger;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Mount the console catalog routes onto the express app. Returns
|
|
62
|
+
* nothing — the routes self-register.
|
|
63
|
+
*/
|
|
64
|
+
export declare function mountConsoleRegistryRoutes(opts: MountOptions): void;
|
|
65
|
+
export {};
|
|
66
|
+
//# sourceMappingURL=console-registry-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-registry-routes.d.ts","sourceRoot":"","sources":["../src/console-registry-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAS5D,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,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC,qEAAqE;IACrE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,aAAa,CAAC,0BAA0B,CAAC,CAAC;IACvE,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA4PnE"}
|