@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.
Files changed (152) hide show
  1. package/dist/admin-blueprints-transport.d.ts.map +1 -1
  2. package/dist/admin-blueprints-transport.js +2 -1
  3. package/dist/admin-oauth-providers-transport.d.ts.map +1 -1
  4. package/dist/admin-oauth-providers-transport.js +7 -5
  5. package/dist/api-renders-routes.d.ts +85 -0
  6. package/dist/api-renders-routes.d.ts.map +1 -0
  7. package/dist/api-renders-routes.js +372 -0
  8. package/dist/build-mcp.d.ts +1 -1
  9. package/dist/build-mcp.d.ts.map +1 -1
  10. package/dist/build-mcp.js +39 -5
  11. package/dist/code-routes.d.ts +47 -0
  12. package/dist/code-routes.d.ts.map +1 -0
  13. package/dist/code-routes.js +81 -0
  14. package/dist/code-store-fs.js +2 -2
  15. package/dist/console-auth.d.ts +10 -10
  16. package/dist/console-auth.d.ts.map +1 -1
  17. package/dist/console-auth.js +5 -5
  18. package/dist/console-blueprint-routes.d.ts +71 -0
  19. package/dist/console-blueprint-routes.d.ts.map +1 -0
  20. package/dist/console-blueprint-routes.js +348 -0
  21. package/dist/console-chat-routes.d.ts +80 -0
  22. package/dist/console-chat-routes.d.ts.map +1 -0
  23. package/dist/console-chat-routes.js +182 -0
  24. package/dist/console-config-routes.d.ts +37 -0
  25. package/dist/console-config-routes.d.ts.map +1 -0
  26. package/dist/console-config-routes.js +91 -0
  27. package/dist/console-headers.d.ts +1 -1
  28. package/dist/console-info-routes.d.ts +84 -0
  29. package/dist/console-info-routes.d.ts.map +1 -0
  30. package/dist/console-info-routes.js +135 -0
  31. package/dist/console-keys-routes.d.ts +50 -0
  32. package/dist/console-keys-routes.d.ts.map +1 -0
  33. package/dist/console-keys-routes.js +222 -0
  34. package/dist/console-llm-keys-routes.d.ts +47 -0
  35. package/dist/console-llm-keys-routes.d.ts.map +1 -0
  36. package/dist/console-llm-keys-routes.js +443 -0
  37. package/dist/console-mcp-tools-routes.d.ts +41 -0
  38. package/dist/console-mcp-tools-routes.d.ts.map +1 -0
  39. package/dist/console-mcp-tools-routes.js +60 -0
  40. package/dist/console-registry-routes.d.ts +66 -0
  41. package/dist/console-registry-routes.d.ts.map +1 -0
  42. package/dist/console-registry-routes.js +276 -0
  43. package/dist/console-session-routes.d.ts +89 -0
  44. package/dist/console-session-routes.d.ts.map +1 -0
  45. package/dist/console-session-routes.js +385 -0
  46. package/dist/console-sessions-routes.d.ts +52 -0
  47. package/dist/console-sessions-routes.d.ts.map +1 -0
  48. package/dist/console-sessions-routes.js +106 -0
  49. package/dist/console-static-routes.d.ts +54 -0
  50. package/dist/console-static-routes.d.ts.map +1 -0
  51. package/dist/console-static-routes.js +190 -0
  52. package/dist/console-theme-routes.d.ts +3 -3
  53. package/dist/console-theme-routes.js +1 -1
  54. package/dist/console-timeline.d.ts +5 -5
  55. package/dist/console-timeline.d.ts.map +1 -1
  56. package/dist/console-timeline.js +27 -26
  57. package/dist/console-welcome.js +2 -2
  58. package/dist/email-login.d.ts.map +1 -1
  59. package/dist/email-login.js +2 -3
  60. package/dist/ggui-session-channel/action-ingress.d.ts +54 -0
  61. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -0
  62. package/dist/ggui-session-channel/action-ingress.js +228 -0
  63. package/dist/ggui-session-channel/channel-subscriptions.d.ts +97 -0
  64. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -0
  65. package/dist/ggui-session-channel/channel-subscriptions.js +224 -0
  66. package/dist/ggui-session-channel/internal-types.d.ts +102 -0
  67. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -0
  68. package/dist/ggui-session-channel/internal-types.js +6 -0
  69. package/dist/ggui-session-channel/outbound.d.ts +81 -0
  70. package/dist/ggui-session-channel/outbound.d.ts.map +1 -0
  71. package/dist/ggui-session-channel/outbound.js +174 -0
  72. package/dist/ggui-session-channel/socket-router.d.ts +38 -0
  73. package/dist/ggui-session-channel/socket-router.d.ts.map +1 -0
  74. package/dist/ggui-session-channel/socket-router.js +213 -0
  75. package/dist/ggui-session-channel/subscribe.d.ts +165 -0
  76. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -0
  77. package/dist/ggui-session-channel/subscribe.js +370 -0
  78. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +40 -0
  79. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -0
  80. package/dist/ggui-session-channel/subscriber-lifecycle.js +123 -0
  81. package/dist/ggui-session-channel.d.ts +425 -0
  82. package/dist/ggui-session-channel.d.ts.map +1 -0
  83. package/dist/ggui-session-channel.js +262 -0
  84. package/dist/health-routes.d.ts +76 -0
  85. package/dist/health-routes.d.ts.map +1 -0
  86. package/dist/health-routes.js +145 -0
  87. package/dist/index.d.ts +10 -11
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +8 -9
  90. package/dist/instructions-presets.d.ts +3 -3
  91. package/dist/instructions-presets.js +24 -24
  92. package/dist/llm-backed-negotiator.d.ts +68 -67
  93. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  94. package/dist/llm-backed-negotiator.js +82 -221
  95. package/dist/mcp-apps-outbound.d.ts +47 -48
  96. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  97. package/dist/mcp-apps-outbound.js +154 -177
  98. package/dist/mcp-endpoint-routes.d.ts +88 -0
  99. package/dist/mcp-endpoint-routes.d.ts.map +1 -0
  100. package/dist/mcp-endpoint-routes.js +359 -0
  101. package/dist/mcp-mounts.d.ts +2 -76
  102. package/dist/mcp-mounts.d.ts.map +1 -1
  103. package/dist/mcp-mounts.js +0 -76
  104. package/dist/oauth-as-routes.d.ts +60 -0
  105. package/dist/oauth-as-routes.d.ts.map +1 -0
  106. package/dist/oauth-as-routes.js +82 -0
  107. package/dist/oauth-clients-routes.d.ts +39 -0
  108. package/dist/oauth-clients-routes.d.ts.map +1 -0
  109. package/dist/oauth-clients-routes.js +87 -0
  110. package/dist/oauth-login-types.d.ts +1 -20
  111. package/dist/oauth-login-types.d.ts.map +1 -1
  112. package/dist/oauth-login-types.js +30 -7
  113. package/dist/oauth-login.d.ts.map +1 -1
  114. package/dist/oauth-login.js +3 -2
  115. package/dist/oauth-providers-store.d.ts.map +1 -1
  116. package/dist/oauth-providers-store.js +5 -5
  117. package/dist/oauth.d.ts +9 -8
  118. package/dist/oauth.d.ts.map +1 -1
  119. package/dist/oauth.js +41 -19
  120. package/dist/pairing-transport.d.ts.map +1 -1
  121. package/dist/pairing-transport.js +2 -1
  122. package/dist/request-context.d.ts +2 -2
  123. package/dist/request-context.js +2 -2
  124. package/dist/reserved-validators.d.ts.map +1 -1
  125. package/dist/reserved-validators.js +9 -1
  126. package/dist/route-param.d.ts +9 -0
  127. package/dist/route-param.d.ts.map +1 -0
  128. package/dist/route-param.js +10 -0
  129. package/dist/runtime-bundle-route.d.ts +43 -0
  130. package/dist/runtime-bundle-route.d.ts.map +1 -0
  131. package/dist/runtime-bundle-route.js +80 -0
  132. package/dist/schema-compat.d.ts +64 -62
  133. package/dist/schema-compat.d.ts.map +1 -1
  134. package/dist/schema-compat.js +23 -51
  135. package/dist/server.d.ts +179 -193
  136. package/dist/server.d.ts.map +1 -1
  137. package/dist/server.js +644 -3759
  138. package/dist/storage.d.ts +5 -5
  139. package/dist/storage.d.ts.map +1 -1
  140. package/dist/storage.js +5 -5
  141. package/dist/thread-transport.d.ts.map +1 -1
  142. package/dist/thread-transport.js +4 -3
  143. package/dist/user-session-auth.d.ts +7 -21
  144. package/dist/user-session-auth.d.ts.map +1 -1
  145. package/dist/user-session-auth.js +7 -28
  146. package/package.json +16 -15
  147. package/dist/mcp-apps-inbound.d.ts +0 -86
  148. package/dist/mcp-apps-inbound.d.ts.map +0 -1
  149. package/dist/mcp-apps-inbound.js +0 -283
  150. package/dist/render-channel.d.ts +0 -694
  151. package/dist/render-channel.d.ts.map +0 -1
  152. 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/render-cookie` and the `new WebSocket('/ws')`
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"}