@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,385 @@
1
+ /**
2
+ * Console render-viewer session routes (cookie mint + resource +
3
+ * meta + observation).
4
+ *
5
+ * POST /ggui/console/session-cookie
6
+ * — resolve shortCode → render and mint the same-origin HTTP-only
7
+ * cookie the viewer authenticates to the live channel with.
8
+ *
9
+ * GET /ggui/console/session-resource?session=<sessionId>
10
+ * — production thin-shell HTML, wrapped as a ResourceContents
11
+ * blob (`text` IS `GGUI_RENDER_SHELL_HTML`, byte-identical to
12
+ * what Claude Desktop fetches via MCP `resources/read
13
+ * ui://ggui/render`). NO inlined bootstrap — console fetches
14
+ * the bootstrap separately (meta route below) and replies to
15
+ * the iframe's `ui/initialize` postMessage with it.
16
+ *
17
+ * GET /ggui/console/sessions/:sessionId/meta
18
+ * — slice-envelope JSON (`{ "ai.ggui/render": {...} }`, the same
19
+ * shape as the wire `_meta`). The console forwards this to the
20
+ * iframe as the `_meta` slice of a spec-canonical
21
+ * `ui/notifications/tool-result` notification (per
22
+ * `docs/protocol/extensions/ai.ggui-meta.md`). Requires
23
+ * `mcpApps: true` (mintBootstrap presence) — 503 otherwise.
24
+ *
25
+ * GET /ggui/console/session?session=<sessionId>
26
+ * — `{render, eventSequence}` JSON. Console-only observation
27
+ * surface for `<GguiSessionViewer>` to mount `<RenderInspector>`.
28
+ *
29
+ * Named parties (all routes):
30
+ * - console (SPA caller) — holds the same-origin cookie.
31
+ * - mcp-server (these handlers) — gates auth + scope; mints the
32
+ * bootstrap on the meta route.
33
+ * - host wrapper (bootstrap forwarder; `<AppRenderer>` on web,
34
+ * `<McpAppIframe>` on RN) — receives the bootstrap JSON and
35
+ * forwards it as a `ui/notifications/tool-result` `_meta` slice.
36
+ * - renderer bundle (inside the iframe) — runs the same boot
37
+ * code path as production; reads the single `ai.ggui/render`
38
+ * slice meta.
39
+ *
40
+ * Auth + scope obligations (uniform):
41
+ * - Cookie-auth via `readDevtoolCookieFromHeaders` +
42
+ * `verifyDevtoolCookie`. Invalid / missing → 401.
43
+ * - Scope: `cookie.sessionId` MUST equal the requested session.
44
+ * Cross-render access with a valid cookie → 403.
45
+ * - GguiSession existence + appId match: 404 / 403 respectively.
46
+ *
47
+ * The mount RETURNS the `GguiSessionChannelCookieAuth` binding the
48
+ * composer threads into `createGguiSessionChannelServer`, so the
49
+ * cookie minted here authenticates the live-channel WS upgrade.
50
+ */
51
+ import { deriveContractBundle } from "@ggui-ai/mcp-server-handlers/renders";
52
+ import { GGUI_RENDER_RESOURCE_MIME, GGUI_RENDER_RESOURCE_URI, MCP_APP_AI_GGUI_RENDER_META_KEY, } from "@ggui-ai/protocol/integrations/mcp-apps";
53
+ import { CONSOLE_COOKIE_NAME, mintDevtoolCookie, readDevtoolCookieFromHeaders, verifyDevtoolCookie, } from "./console-auth.js";
54
+ import { applyDevtoolSecurityHeaders } from "./console-headers.js";
55
+ import { GGUI_RENDER_SHELL_HTML } from "./mcp-apps-outbound.js";
56
+ import { singleParam } from "./route-param.js";
57
+ /**
58
+ * Mount the console session routes onto the express app and return
59
+ * the cookie-auth binding for the live channel.
60
+ */
61
+ export function mountConsoleSessionRoutes(opts) {
62
+ const { app, secret, shortCodeIndex, cookieTtlSec, cookieSecure, renderStore, mintBootstrap, runtimeBootstrapUrl, codeStore, publicBaseUrl, logger, } = opts;
63
+ const cookieLogger = logger.child({
64
+ component: "console-cookie",
65
+ });
66
+ app.post("/ggui/console/session-cookie", async (req, res) => {
67
+ applyDevtoolSecurityHeaders(res);
68
+ const body = (req.body ?? {});
69
+ if (typeof body.shortCode !== "string" || body.shortCode.length === 0) {
70
+ res.status(400).json({
71
+ error: "invalid_request",
72
+ message: "`shortCode` (string) is required",
73
+ });
74
+ return;
75
+ }
76
+ let binding;
77
+ try {
78
+ binding = await shortCodeIndex.lookup(body.shortCode);
79
+ }
80
+ catch (err) {
81
+ cookieLogger.error("short_code_lookup_failed", {
82
+ error: String(err),
83
+ });
84
+ res.status(500).json({ error: "internal_error" });
85
+ return;
86
+ }
87
+ if (!binding) {
88
+ res.status(404).json({
89
+ error: "unknown_short_code",
90
+ message: "Short-code does not resolve to any render on this server",
91
+ });
92
+ return;
93
+ }
94
+ const mint = mintDevtoolCookie({
95
+ sessionId: binding.sessionId,
96
+ appId: binding.appId,
97
+ secret,
98
+ ...(cookieTtlSec !== undefined ? { ttlSec: cookieTtlSec } : {}),
99
+ secure: cookieSecure,
100
+ });
101
+ res.setHeader("Set-Cookie", mint.setCookieHeader);
102
+ res.json({
103
+ sessionId: mint.sessionId,
104
+ appId: mint.appId,
105
+ expiresAt: mint.expiresAt,
106
+ });
107
+ });
108
+ /**
109
+ * Shared auth + scope gate for the console render routes.
110
+ * Returns the verified `(sessionId, appId)` pair on success or
111
+ * `null` after writing an HTTP error response on failure.
112
+ *
113
+ * Internal — closure-scoped to the route block; not exported.
114
+ */
115
+ const gateDevtoolRenderRequest = async (req, res, explicitSessionId) => {
116
+ const sessionIdRaw = explicitSessionId !== undefined ? explicitSessionId : req.query["session"];
117
+ if (typeof sessionIdRaw !== "string" || sessionIdRaw.length === 0) {
118
+ res.status(400).json({
119
+ error: "invalid_request",
120
+ message: "`render` query parameter (or :sessionId path parameter on the meta route) is required",
121
+ });
122
+ return null;
123
+ }
124
+ const rawCookie = readDevtoolCookieFromHeaders(req.headers);
125
+ if (!rawCookie) {
126
+ res.status(401).json({
127
+ error: "missing_cookie",
128
+ message: `${CONSOLE_COOKIE_NAME} cookie required (mint via POST /ggui/console/session-cookie first)`,
129
+ });
130
+ return null;
131
+ }
132
+ const claims = verifyDevtoolCookie(rawCookie, secret);
133
+ if (!claims) {
134
+ res.status(401).json({
135
+ error: "invalid_cookie",
136
+ message: "Console cookie is invalid, expired, or for another server",
137
+ });
138
+ return null;
139
+ }
140
+ if (claims.sessionId !== sessionIdRaw) {
141
+ res.status(403).json({
142
+ error: "cookie_session_mismatch",
143
+ message: `Console cookie is bound to render '${claims.sessionId}' but request targets '${sessionIdRaw}'`,
144
+ });
145
+ return null;
146
+ }
147
+ // GguiSession existence + appId match — even on the static-shell
148
+ // route we honestly answer 404 instead of leaking an HTML blob
149
+ // for a render the server doesn't know about.
150
+ let render = null;
151
+ if (renderStore) {
152
+ try {
153
+ render = await renderStore.get(claims.sessionId);
154
+ }
155
+ catch (err) {
156
+ cookieLogger.error("render_resource_store_failed", {
157
+ error: String(err),
158
+ sessionId: claims.sessionId,
159
+ });
160
+ res.status(500).json({ error: "internal_error" });
161
+ return null;
162
+ }
163
+ }
164
+ if (!render) {
165
+ res.status(404).json({
166
+ error: "session_not_found",
167
+ message: `GguiSession '${claims.sessionId}' is not on this server`,
168
+ });
169
+ return null;
170
+ }
171
+ if (render.appId !== claims.appId) {
172
+ res.status(403).json({
173
+ error: "app_mismatch",
174
+ message: `Cookie bound to app '${claims.appId}' but render is on app '${render.appId}'`,
175
+ });
176
+ return null;
177
+ }
178
+ return { sessionId: claims.sessionId, appId: claims.appId };
179
+ };
180
+ // GET /ggui/console/session-resource?session=<sessionId>
181
+ // → production thin-shell HTML, wrapped as a ResourceContents
182
+ // blob. NO inlined bootstrap — console fetches the bootstrap
183
+ // separately (route below) and replies to the iframe's
184
+ // `ui/initialize` postMessage with it.
185
+ app.get("/ggui/console/session-resource", async (req, res) => {
186
+ applyDevtoolSecurityHeaders(res);
187
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
188
+ const verified = await gateDevtoolRenderRequest(req, res);
189
+ if (!verified)
190
+ return;
191
+ res.status(200).json({
192
+ contents: [
193
+ {
194
+ uri: GGUI_RENDER_RESOURCE_URI,
195
+ mimeType: GGUI_RENDER_RESOURCE_MIME,
196
+ text: GGUI_RENDER_SHELL_HTML,
197
+ },
198
+ ],
199
+ });
200
+ });
201
+ // GET /ggui/console/sessions/:sessionId/meta
202
+ // → slice-envelope JSON (`{ "ai.ggui/render": {...} }`, the same
203
+ // shape as the wire `_meta`). Required when the console is
204
+ // hosting the renderer in a srcdoc iframe and needs to feed
205
+ // the iframe a meta payload via `ui/initialize`. `mcpApps:
206
+ // true` is required (mintWsToken/mintBootstrap presence) —
207
+ // 503 otherwise.
208
+ app.get("/ggui/console/sessions/:sessionId/meta", async (req, res) => {
209
+ applyDevtoolSecurityHeaders(res);
210
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
211
+ const sessionIdFromPath = singleParam(req.params["sessionId"]);
212
+ const verified = await gateDevtoolRenderRequest(req, res, sessionIdFromPath);
213
+ if (!verified)
214
+ return;
215
+ if (!mintBootstrap) {
216
+ res.status(503).json({
217
+ error: "mcp_apps_disabled",
218
+ message: "renders/:sessionId/meta requires mcpApps: true on the server so the renderer can receive a valid WS auth token. Enable `mcpApps` on createGguiServer() and retry.",
219
+ });
220
+ return;
221
+ }
222
+ const minted = mintBootstrap(verified.sessionId, verified.appId);
223
+ // Console/srcdoc absolute-URL fix: `<McpAppIframe>`
224
+ // mounts the resource via `srcdoc`, so the iframe's URL
225
+ // is `about:srcdoc` and any relative URL would resolve
226
+ // against that, not the dev server. Rewrite the
227
+ // same-origin `runtimeUrl` to absolute based on the
228
+ // request host. The `wsUrl` minted at server-boot
229
+ // defaults to `ws://localhost/ws` — also rewrite that
230
+ // to the request-host's actual host:port when its
231
+ // hostname is `localhost`, so the iframe's WebSocket
232
+ // open lands on the same listener. Operators who
233
+ // configured a CDN / external `wsUrl` / `runtime.url`
234
+ // are passed through unchanged.
235
+ const requestHost = req.get("host") ?? "";
236
+ const absoluteRendererUrl = /^https?:\/\//i.test(runtimeBootstrapUrl)
237
+ ? runtimeBootstrapUrl
238
+ : `${req.protocol}://${requestHost}${runtimeBootstrapUrl}`;
239
+ let resolvedWsUrl = minted.wsUrl;
240
+ try {
241
+ const wsParsed = new URL(minted.wsUrl);
242
+ if ((wsParsed.hostname === "localhost" || wsParsed.hostname === "127.0.0.1") &&
243
+ requestHost.length > 0) {
244
+ const wsScheme = req.protocol === "https" ? "wss" : "ws";
245
+ resolvedWsUrl = `${wsScheme}://${requestHost}${wsParsed.pathname}${wsParsed.search}`;
246
+ }
247
+ }
248
+ catch {
249
+ // Malformed `wsUrl` — leave it as-is so the renderer
250
+ // surfaces the failure through its own bootstrap-failed
251
+ // envelope rather than us silently rewriting a string
252
+ // we don't understand.
253
+ }
254
+ // Content-addressable contract-validator bundle (#109) for
255
+ // the active render. The renderer iframe's strict CSP
256
+ // forbids the `new Function` codegen `ajv.compile()` needs,
257
+ // so the server compiles + writes the bundle to its
258
+ // CodeStore at render time and threads the URL here. The
259
+ // iframe fetches the URL + dynamic-imports to resolve
260
+ // validators. Best-effort: a missing bundle degrades to no
261
+ // client-side validation; server-side `assertActionContract`
262
+ // remains authoritative.
263
+ let renderContractHash;
264
+ let renderValidatorsUrl;
265
+ if (renderStore && codeStore) {
266
+ try {
267
+ const stored = await renderStore.get(verified.sessionId);
268
+ if (stored !== null &&
269
+ stored.render.type !== "mcpApps" &&
270
+ stored.render.type !== "system" &&
271
+ typeof stored.render.componentCode === "string" &&
272
+ stored.render.componentCode.length > 0) {
273
+ const bundle = await deriveContractBundle(stored.render);
274
+ if (bundle) {
275
+ await codeStore.put(bundle.contractHash, bundle.bundleSource);
276
+ renderContractHash = bundle.contractHash;
277
+ const baseForValidators = publicBaseUrl
278
+ ? publicBaseUrl.replace(/\/$/, "")
279
+ : `${req.protocol}://${requestHost}`;
280
+ renderValidatorsUrl = `${baseForValidators}/contract/${bundle.contractHash}.js`;
281
+ }
282
+ }
283
+ }
284
+ catch (err) {
285
+ cookieLogger.warn("render_meta_validators_failed", {
286
+ error: String(err),
287
+ sessionId: verified.sessionId,
288
+ });
289
+ }
290
+ }
291
+ // Slice-envelope response (Phase B: single ai.ggui/render
292
+ // slice) — same shape as the wire `_meta` and the inline
293
+ // `__GGUI_META__` global the `/r/<shortCode>` shell carries.
294
+ // GguiSessionViewer parses with `parseMcpAppAiGguiRenderMeta`.
295
+ const renderMeta = {
296
+ sessionId: verified.sessionId,
297
+ appId: verified.appId,
298
+ runtimeUrl: absoluteRendererUrl,
299
+ wsUrl: resolvedWsUrl,
300
+ wsToken: minted.token,
301
+ expiresAt: minted.expiresAt,
302
+ ...(renderContractHash !== undefined && renderValidatorsUrl !== undefined
303
+ ? {
304
+ contractHash: renderContractHash,
305
+ validatorsUrl: renderValidatorsUrl,
306
+ }
307
+ : {}),
308
+ };
309
+ res.status(200).json({
310
+ [MCP_APP_AI_GGUI_RENDER_META_KEY]: renderMeta,
311
+ });
312
+ });
313
+ // GET /ggui/console/session?session=<sessionId>
314
+ // → `{render, eventSequence}` JSON.
315
+ //
316
+ // Console-only observation surface for `<GguiSessionViewer>` to mount
317
+ // `<RenderInspector>`. The iframe owns the live WS subscription
318
+ // + the bootstrap token (single-use), so the OUTER console DOM
319
+ // has no live source for render data — without this endpoint
320
+ // the inspector can't render contract / test-action panels.
321
+ //
322
+ // Failure modes:
323
+ // - 401 missing/invalid cookie · 403 cross-render/app · 404
324
+ // unknown render · 500 store failure (all delegated to
325
+ // `gateDevtoolRenderRequest`).
326
+ // - 503 if `renderStore` is not wired (zero-config server).
327
+ //
328
+ // Shape note: Phase B collapsed the prior session-stack array to
329
+ // a single `GguiSession` row. The response now returns the resolved
330
+ // `GguiSession` directly; console narrows on `render.type` before
331
+ // passing into `<RenderInspector>` (which only accepts the
332
+ // ComponentGguiSession variant since the inspector reads actionSpec /
333
+ // streamSpec / propsSpec — fields McpAppsGguiSession doesn't carry).
334
+ app.get("/ggui/console/session", async (req, res) => {
335
+ applyDevtoolSecurityHeaders(res);
336
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
337
+ const verified = await gateDevtoolRenderRequest(req, res);
338
+ if (!verified)
339
+ return;
340
+ if (!renderStore) {
341
+ res.status(503).json({
342
+ error: "session_store_unavailable",
343
+ message: "GguiSession observation requires renderChannel: true on the server so the render store is wired. Enable `renderChannel` on createGguiServer() and retry.",
344
+ });
345
+ return;
346
+ }
347
+ let stored = null;
348
+ try {
349
+ stored = await renderStore.get(verified.sessionId);
350
+ }
351
+ catch (err) {
352
+ cookieLogger.error("render_store_failed", {
353
+ error: String(err),
354
+ sessionId: verified.sessionId,
355
+ });
356
+ res.status(500).json({ error: "internal_error" });
357
+ return;
358
+ }
359
+ if (!stored) {
360
+ // Race: gate verified existence above but the render
361
+ // could expire between calls. Honest 404.
362
+ res.status(404).json({
363
+ error: "session_not_found",
364
+ message: `GguiSession '${verified.sessionId}' is not on this server`,
365
+ });
366
+ return;
367
+ }
368
+ res.status(200).json({
369
+ render: stored.render,
370
+ eventSequence: stored.eventSequence,
371
+ });
372
+ });
373
+ // Reference the cookie name to keep the export alive for
374
+ // downstream consumers + lint. The name is the single source
375
+ // of truth; avoid duplicating the string anywhere.
376
+ cookieLogger.debug?.("console_cookie_ready", {
377
+ cookieName: CONSOLE_COOKIE_NAME,
378
+ });
379
+ // Cookie-auth binding for the render channel — the composer
380
+ // threads this into `createGguiSessionChannelServer`.
381
+ return {
382
+ readCookie: readDevtoolCookieFromHeaders,
383
+ verify: (cookieValue) => verifyDevtoolCookie(cookieValue, secret),
384
+ };
385
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Console active-render list route.
3
+ *
4
+ * GET /ggui/console/sessions?limit=<n> — active-render list for
5
+ * the console SPA's `/admin/sessions` page. Operator-facing "what's
6
+ * live right now?" surface, enriched with each render's current
7
+ * shortCode so rows can link through to `/s/<shortCode>` (the
8
+ * existing render viewer).
9
+ *
10
+ * Scope: active renders only. The `GguiSessionFilter.status`
11
+ * taxonomy ('active' | 'completed' | 'expired') requires the
12
+ * store's private `closed` bucket flag to disambiguate completed
13
+ * from expired — that flag isn't on the `GguiSession` protocol type,
14
+ * so exposing mixed-status listings honestly requires a seam
15
+ * extension we don't need for the "live right now" use case.
16
+ * Future slices (historical renders, closed-renders triage)
17
+ * can opt in via query-param.
18
+ *
19
+ * Sources:
20
+ * - `renderStore.list({ status: 'active', limit })` — single
21
+ * page, limit default 25, clamped to [1, 100].
22
+ * - `shortCodeIndex.findBySessionId(render.id)` — best-effort
23
+ * enrichment; absent shortCode is a valid row (displays
24
+ * without a click-through link).
25
+ *
26
+ * Sort: most-recent `lastActivityAt` first — matches operator
27
+ * intent "show me what I was just looking at."
28
+ *
29
+ * Zero-config shape: `{ sessions: [], total: 0 }` when no
30
+ * renderStore is wired (e.g. pure-MCP dev boot with neither
31
+ * renderChannel nor mcpApps enabled).
32
+ */
33
+ import type { GguiSessionStore, ShortCodeIndex } from "@ggui-ai/mcp-server-core";
34
+ import type { Express } from "express";
35
+ import type { Logger } from "./logger.js";
36
+ interface MountOptions {
37
+ /** Express app to mount onto. */
38
+ readonly app: Express;
39
+ /** GguiSession store the list reads (absent = honest empty list). */
40
+ readonly renderStore?: GguiSessionStore;
41
+ /** shortCode index for best-effort row enrichment. */
42
+ readonly shortCodeIndex?: ShortCodeIndex;
43
+ /** Structured logger. */
44
+ readonly logger: Logger;
45
+ }
46
+ /**
47
+ * Mount `GET /ggui/console/sessions` onto the express app. Returns
48
+ * nothing — the route self-registers.
49
+ */
50
+ export declare function mountConsoleSessionsRoutes(opts: MountOptions): void;
51
+ export {};
52
+ //# sourceMappingURL=console-sessions-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-sessions-routes.d.ts","sourceRoot":"","sources":["../src/console-sessions-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AACjF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,qEAAqE;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IACxC,sDAAsD;IACtD,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA8EnE"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Console active-render list route.
3
+ *
4
+ * GET /ggui/console/sessions?limit=<n> — active-render list for
5
+ * the console SPA's `/admin/sessions` page. Operator-facing "what's
6
+ * live right now?" surface, enriched with each render's current
7
+ * shortCode so rows can link through to `/s/<shortCode>` (the
8
+ * existing render viewer).
9
+ *
10
+ * Scope: active renders only. The `GguiSessionFilter.status`
11
+ * taxonomy ('active' | 'completed' | 'expired') requires the
12
+ * store's private `closed` bucket flag to disambiguate completed
13
+ * from expired — that flag isn't on the `GguiSession` protocol type,
14
+ * so exposing mixed-status listings honestly requires a seam
15
+ * extension we don't need for the "live right now" use case.
16
+ * Future slices (historical renders, closed-renders triage)
17
+ * can opt in via query-param.
18
+ *
19
+ * Sources:
20
+ * - `renderStore.list({ status: 'active', limit })` — single
21
+ * page, limit default 25, clamped to [1, 100].
22
+ * - `shortCodeIndex.findBySessionId(render.id)` — best-effort
23
+ * enrichment; absent shortCode is a valid row (displays
24
+ * without a click-through link).
25
+ *
26
+ * Sort: most-recent `lastActivityAt` first — matches operator
27
+ * intent "show me what I was just looking at."
28
+ *
29
+ * Zero-config shape: `{ sessions: [], total: 0 }` when no
30
+ * renderStore is wired (e.g. pure-MCP dev boot with neither
31
+ * renderChannel nor mcpApps enabled).
32
+ */
33
+ import { applyDevtoolSecurityHeaders } from "./console-headers.js";
34
+ /**
35
+ * Mount `GET /ggui/console/sessions` onto the express app. Returns
36
+ * nothing — the route self-registers.
37
+ */
38
+ export function mountConsoleSessionsRoutes(opts) {
39
+ const { app, renderStore, shortCodeIndex, logger } = opts;
40
+ app.get("/ggui/console/sessions", async (req, res) => {
41
+ applyDevtoolSecurityHeaders(res);
42
+ const limitRaw = req.query["limit"];
43
+ let limit = 25;
44
+ if (typeof limitRaw === "string") {
45
+ const parsed = Number.parseInt(limitRaw, 10);
46
+ if (Number.isFinite(parsed) && parsed > 0) {
47
+ limit = Math.min(100, parsed);
48
+ }
49
+ }
50
+ if (!renderStore) {
51
+ res.json({ sessions: [], total: 0 });
52
+ return;
53
+ }
54
+ try {
55
+ const renders = await renderStore.list({
56
+ status: "active",
57
+ limit,
58
+ });
59
+ const summaries = [];
60
+ for (const render of renders) {
61
+ let shortCode = null;
62
+ if (shortCodeIndex) {
63
+ try {
64
+ shortCode = await shortCodeIndex.findBySessionId(render.id);
65
+ }
66
+ catch (err) {
67
+ // Best-effort — the render row is still honest
68
+ // without a shortCode.
69
+ logger.warn("console_renders_shortcode_lookup_failed", {
70
+ sessionId: render.id,
71
+ error: String(err),
72
+ });
73
+ }
74
+ }
75
+ summaries.push({
76
+ sessionId: render.id,
77
+ ...(shortCode ? { shortCode } : {}),
78
+ appId: render.appId,
79
+ lastActivityAt: render.lastActivityAt,
80
+ createdAt: render.createdAt,
81
+ status: "active",
82
+ });
83
+ }
84
+ // Most-recent activity first. Tiebreak on sessionId for
85
+ // stability when multiple rows share the same ms timestamp.
86
+ summaries.sort((a, b) => {
87
+ const byRecency = b.lastActivityAt - a.lastActivityAt;
88
+ if (byRecency !== 0)
89
+ return byRecency;
90
+ return a.sessionId.localeCompare(b.sessionId);
91
+ });
92
+ res.json({ sessions: summaries, total: summaries.length });
93
+ }
94
+ catch (err) {
95
+ logger.warn("console_renders_list_failed", {
96
+ error: String(err),
97
+ });
98
+ res.status(500).json({
99
+ error: "renders_unavailable",
100
+ message: err instanceof Error
101
+ ? `GguiSession store failed to list — ${err.message}`
102
+ : `GguiSession store failed to list — ${String(err)}`,
103
+ });
104
+ }
105
+ });
106
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Console static-bundle + SPA serving routes.
3
+ *
4
+ * GET <consolePath> — mode-meta-injected `index.html`,
5
+ * welcome page, or onboarding redirect.
6
+ * <consolePath>/* (static) — `express.static` over the package's
7
+ * built `dist/` (HTML + JS + CSS).
8
+ * GET /admin/* + /devtools/* — admin-HTML gate (302 to
9
+ * `/admin-login?next=…` on miss).
10
+ * GET <consolePath>/* (SPA) — fallback to the rewritten
11
+ * `index.html` so the React router
12
+ * takes over for client-side routes.
13
+ *
14
+ * When the `distDir` doesn't exist on disk (e.g. operator forgot
15
+ * to run `pnpm --filter @ggui-ai/console build`), the static
16
+ * route is replaced with a 503 that points at the missing build —
17
+ * silent 404 would be mistaken for "console is broken" rather
18
+ * than "console wasn't built yet," which is a real debugging
19
+ * trap for self-hosted operators.
20
+ */
21
+ import type { Express } from "express";
22
+ import express from "express";
23
+ import { type WelcomePageInputs } from "./console-welcome.js";
24
+ import type { Logger } from "./logger.js";
25
+ interface MountOptions {
26
+ /** Express app to mount onto. */
27
+ readonly app: Express;
28
+ /** Mount path for the console (default `/`). */
29
+ readonly consolePath: string;
30
+ /** Built console bundle directory on disk. */
31
+ readonly consoleDistDir: string;
32
+ /** Operator mode stamped into `<meta name="ggui-mode">`. */
33
+ readonly mode: "dev" | "prod";
34
+ /** Server identity name (welcome-page fallback title). */
35
+ readonly serverName: string;
36
+ /** Welcome-page inputs — enables the server-rendered landing. */
37
+ readonly welcomePage?: WelcomePageInputs;
38
+ /** Onboarding redirect resolver (runs before welcome/SPA index). */
39
+ readonly landingRedirect?: () => string | null;
40
+ /**
41
+ * Admin gate for the SPA's `/admin/*` + `/devtools/*` zones. `null`
42
+ * = console disabled / no admin token resolved → gate unmounted.
43
+ */
44
+ readonly requestHasAdminAuth: ((req: express.Request) => boolean) | null;
45
+ /** Structured logger for the missing-dist boot warning. */
46
+ readonly logger: Logger;
47
+ }
48
+ /**
49
+ * Mount the console static + SPA routes onto the express app.
50
+ * Returns nothing — the routes self-register.
51
+ */
52
+ export declare function mountConsoleStaticRoutes(opts: MountOptions): void;
53
+ export {};
54
+ //# sourceMappingURL=console-static-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-static-routes.d.ts","sourceRoot":"","sources":["../src/console-static-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAY,MAAM,SAAS,CAAC;AACjD,OAAO,OAAO,MAAM,SAAS,CAAC;AAI9B,OAAO,EAAqB,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,gDAAgD;IAChD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IAC9B,0DAA0D;IAC1D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IACzC,oEAAoE;IACpE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC/C;;;OAGG;IACH,QAAQ,CAAC,mBAAmB,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;IACzE,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA8KjE"}