@ggui-ai/mcp-server 0.1.0-rc.1

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 (141) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +48 -0
  3. package/dist/admin-blueprints-transport.d.ts +114 -0
  4. package/dist/admin-blueprints-transport.d.ts.map +1 -0
  5. package/dist/admin-blueprints-transport.js +118 -0
  6. package/dist/admin-oauth-providers-transport.d.ts +40 -0
  7. package/dist/admin-oauth-providers-transport.d.ts.map +1 -0
  8. package/dist/admin-oauth-providers-transport.js +263 -0
  9. package/dist/auth.d.ts +39 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +75 -0
  12. package/dist/build-mcp.d.ts +128 -0
  13. package/dist/build-mcp.d.ts.map +1 -0
  14. package/dist/build-mcp.js +113 -0
  15. package/dist/code-store-fs.d.ts +19 -0
  16. package/dist/code-store-fs.d.ts.map +1 -0
  17. package/dist/code-store-fs.js +98 -0
  18. package/dist/console-auth.d.ts +139 -0
  19. package/dist/console-auth.d.ts.map +1 -0
  20. package/dist/console-auth.js +102 -0
  21. package/dist/console-cache.d.ts +78 -0
  22. package/dist/console-cache.d.ts.map +1 -0
  23. package/dist/console-cache.js +105 -0
  24. package/dist/console-headers.d.ts +124 -0
  25. package/dist/console-headers.d.ts.map +1 -0
  26. package/dist/console-headers.js +49 -0
  27. package/dist/console-llm-trace.d.ts +66 -0
  28. package/dist/console-llm-trace.d.ts.map +1 -0
  29. package/dist/console-llm-trace.js +105 -0
  30. package/dist/console-payloads.d.ts +67 -0
  31. package/dist/console-payloads.d.ts.map +1 -0
  32. package/dist/console-payloads.js +105 -0
  33. package/dist/console-theme-routes.d.ts +111 -0
  34. package/dist/console-theme-routes.d.ts.map +1 -0
  35. package/dist/console-theme-routes.js +202 -0
  36. package/dist/console-timeline.d.ts +45 -0
  37. package/dist/console-timeline.d.ts.map +1 -0
  38. package/dist/console-timeline.js +169 -0
  39. package/dist/console-validator.d.ts +67 -0
  40. package/dist/console-validator.d.ts.map +1 -0
  41. package/dist/console-validator.js +105 -0
  42. package/dist/console-welcome.d.ts +7 -0
  43. package/dist/console-welcome.d.ts.map +1 -0
  44. package/dist/console-welcome.js +221 -0
  45. package/dist/csrf-middleware.d.ts +55 -0
  46. package/dist/csrf-middleware.d.ts.map +1 -0
  47. package/dist/csrf-middleware.js +138 -0
  48. package/dist/email-login.d.ts +174 -0
  49. package/dist/email-login.d.ts.map +1 -0
  50. package/dist/email-login.js +254 -0
  51. package/dist/email-resend.d.ts +29 -0
  52. package/dist/email-resend.d.ts.map +1 -0
  53. package/dist/email-resend.js +71 -0
  54. package/dist/email-sender-from-env.d.ts +34 -0
  55. package/dist/email-sender-from-env.d.ts.map +1 -0
  56. package/dist/email-sender-from-env.js +112 -0
  57. package/dist/email-smtp.d.ts +42 -0
  58. package/dist/email-smtp.d.ts.map +1 -0
  59. package/dist/email-smtp.js +81 -0
  60. package/dist/index.d.ts +102 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +122 -0
  63. package/dist/instructions-presets.d.ts +112 -0
  64. package/dist/instructions-presets.d.ts.map +1 -0
  65. package/dist/instructions-presets.js +195 -0
  66. package/dist/llm-backed-negotiator.d.ts +178 -0
  67. package/dist/llm-backed-negotiator.d.ts.map +1 -0
  68. package/dist/llm-backed-negotiator.js +579 -0
  69. package/dist/logger.d.ts +23 -0
  70. package/dist/logger.d.ts.map +1 -0
  71. package/dist/logger.js +41 -0
  72. package/dist/mcp-apps-inbound.d.ts +86 -0
  73. package/dist/mcp-apps-inbound.d.ts.map +1 -0
  74. package/dist/mcp-apps-inbound.js +278 -0
  75. package/dist/mcp-apps-outbound.d.ts +448 -0
  76. package/dist/mcp-apps-outbound.d.ts.map +1 -0
  77. package/dist/mcp-apps-outbound.js +1163 -0
  78. package/dist/mcp-mounts.d.ts +239 -0
  79. package/dist/mcp-mounts.d.ts.map +1 -0
  80. package/dist/mcp-mounts.js +222 -0
  81. package/dist/oauth-login-types.d.ts +160 -0
  82. package/dist/oauth-login-types.d.ts.map +1 -0
  83. package/dist/oauth-login-types.js +9 -0
  84. package/dist/oauth-login.d.ts +77 -0
  85. package/dist/oauth-login.d.ts.map +1 -0
  86. package/dist/oauth-login.js +455 -0
  87. package/dist/oauth-providers/github.d.ts +17 -0
  88. package/dist/oauth-providers/github.d.ts.map +1 -0
  89. package/dist/oauth-providers/github.js +89 -0
  90. package/dist/oauth-providers/google.d.ts +18 -0
  91. package/dist/oauth-providers/google.d.ts.map +1 -0
  92. package/dist/oauth-providers/google.js +59 -0
  93. package/dist/oauth-providers-store.d.ts +32 -0
  94. package/dist/oauth-providers-store.d.ts.map +1 -0
  95. package/dist/oauth-providers-store.js +291 -0
  96. package/dist/oauth.d.ts +347 -0
  97. package/dist/oauth.d.ts.map +1 -0
  98. package/dist/oauth.js +686 -0
  99. package/dist/pairing-transport.d.ts +99 -0
  100. package/dist/pairing-transport.d.ts.map +1 -0
  101. package/dist/pairing-transport.js +223 -0
  102. package/dist/rate-limit-middleware.d.ts +36 -0
  103. package/dist/rate-limit-middleware.d.ts.map +1 -0
  104. package/dist/rate-limit-middleware.js +57 -0
  105. package/dist/render-gate.d.ts +87 -0
  106. package/dist/render-gate.d.ts.map +1 -0
  107. package/dist/render-gate.js +77 -0
  108. package/dist/render-rate-limit.d.ts +59 -0
  109. package/dist/render-rate-limit.d.ts.map +1 -0
  110. package/dist/render-rate-limit.js +73 -0
  111. package/dist/render-signing.d.ts +98 -0
  112. package/dist/render-signing.d.ts.map +1 -0
  113. package/dist/render-signing.js +113 -0
  114. package/dist/request-context.d.ts +113 -0
  115. package/dist/request-context.d.ts.map +1 -0
  116. package/dist/request-context.js +154 -0
  117. package/dist/reserved-validators.d.ts +22 -0
  118. package/dist/reserved-validators.d.ts.map +1 -0
  119. package/dist/reserved-validators.js +101 -0
  120. package/dist/schema-compat.d.ts +167 -0
  121. package/dist/schema-compat.d.ts.map +1 -0
  122. package/dist/schema-compat.js +187 -0
  123. package/dist/security-headers-middleware.d.ts +38 -0
  124. package/dist/security-headers-middleware.d.ts.map +1 -0
  125. package/dist/security-headers-middleware.js +30 -0
  126. package/dist/server.d.ts +2060 -0
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +6338 -0
  129. package/dist/session-channel.d.ts +651 -0
  130. package/dist/session-channel.d.ts.map +1 -0
  131. package/dist/session-channel.js +1756 -0
  132. package/dist/storage.d.ts +89 -0
  133. package/dist/storage.d.ts.map +1 -0
  134. package/dist/storage.js +171 -0
  135. package/dist/thread-transport.d.ts +118 -0
  136. package/dist/thread-transport.d.ts.map +1 -0
  137. package/dist/thread-transport.js +478 -0
  138. package/dist/user-session-auth.d.ts +167 -0
  139. package/dist/user-session-auth.d.ts.map +1 -0
  140. package/dist/user-session-auth.js +148 -0
  141. package/package.json +76 -0
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Console theme picker routes — `GET /ggui/console/theme` +
3
+ * `POST /ggui/console/theme`.
4
+ *
5
+ * Shape:
6
+ *
7
+ * GET /ggui/console/theme
8
+ * → 200 {
9
+ * presets: ThemeEntry[], // from @ggui-ai/design#listThemes()
10
+ * current: ThemeConfig | null, // ggui.json#theme as parsed
11
+ * writerEnabled: boolean, // POST availability
12
+ * }
13
+ *
14
+ * POST /ggui/console/theme
15
+ * body: ThemeConfig | null // null = clear field, fall back to default
16
+ * → 200 { ok: true }
17
+ * → 400 { error: 'invalid_config', issue } — schema rejected
18
+ * → 501 { error: 'writer_not_configured' } — opts.themeWriter omitted
19
+ * → 500 { error: 'write_failed', message } — writer threw
20
+ *
21
+ * Authentication: piggy-backs on the same admin gate the LLM-keys
22
+ * routes use — operator-only since the value persists to ggui.json,
23
+ * which the OSS server treats as trusted manifest input. End-users
24
+ * who paired into a session don't get to mutate the project's
25
+ * theme. Multi-tenant deployments may relax this in a follow-up by
26
+ * scoping overrides per user (overrides become server state, not
27
+ * file state).
28
+ */
29
+ import { ThemeConfigSchema, safeParseThemeDocument, } from '@ggui-ai/project-config';
30
+ import { applyDevtoolSecurityHeaders } from './console-headers.js';
31
+ /**
32
+ * Filename gate for `POST /ggui/console/theme/upload`. Operators upload
33
+ * arbitrary names; we constrain to a small alphabet that can't escape
34
+ * the project directory or shadow tooling files.
35
+ */
36
+ const SAFE_FILENAME_RE = /^[a-zA-Z0-9._-]+\.json$/;
37
+ const FORBIDDEN_FILENAMES = new Set(['ggui.json', 'package.json']);
38
+ /**
39
+ * Mount `GET /ggui/console/theme` + `POST /ggui/console/theme` onto
40
+ * the express app. Returns nothing — the routes self-register.
41
+ *
42
+ * The current-config state is held in-process (mutable closure) so
43
+ * subsequent GETs reflect the latest POST without re-reading
44
+ * ggui.json. Writes are durable via the supplied `themeWriter`.
45
+ */
46
+ export function mountDevtoolThemeRoutes(opts) {
47
+ const { app, themeWriter, themeFileUploader, requestHasAdminAuth, onConfigChange } = opts;
48
+ let currentConfig = opts.initialConfig;
49
+ // Single helper for the two POST paths so the cell update + the
50
+ // change-notifier fire in lockstep. Errors from the notifier are
51
+ // swallowed — the operator's save already landed on disk, so a
52
+ // downstream subscription glitch shouldn't surface as a UI failure.
53
+ const updateConfig = (next) => {
54
+ currentConfig = next;
55
+ if (onConfigChange !== undefined) {
56
+ try {
57
+ onConfigChange(next);
58
+ }
59
+ catch {
60
+ /* observer-side failure is non-load-bearing */
61
+ }
62
+ }
63
+ };
64
+ const writerEnabled = themeWriter !== undefined;
65
+ const uploadEnabled = themeWriter !== undefined && themeFileUploader !== undefined;
66
+ // GET /ggui/console/theme — picker data
67
+ app.get('/ggui/console/theme', (req, res) => {
68
+ applyDevtoolSecurityHeaders(res);
69
+ if (!requestHasAdminAuth(req)) {
70
+ res.status(401).json({ error: 'admin_auth_required' });
71
+ return;
72
+ }
73
+ // The list of registered presets is owned by `@ggui-ai/design`,
74
+ // which the console client imports directly. The server only
75
+ // returns the resolved-from-manifest selection + the writer's
76
+ // posture; the client renders the preset grid from listThemes()
77
+ // on its own.
78
+ res.status(200).json({
79
+ current: currentConfig,
80
+ writerEnabled,
81
+ uploadEnabled,
82
+ });
83
+ });
84
+ // POST /ggui/console/theme — persist a selection
85
+ app.post('/ggui/console/theme', async (req, res) => {
86
+ applyDevtoolSecurityHeaders(res);
87
+ if (!requestHasAdminAuth(req)) {
88
+ res.status(401).json({ error: 'admin_auth_required' });
89
+ return;
90
+ }
91
+ if (themeWriter === undefined) {
92
+ res.status(501).json({
93
+ error: 'writer_not_configured',
94
+ message: 'Theme writes require the CLI to provide a themeWriter — ' +
95
+ 'launch via `ggui serve` (writer is wired) instead of using ' +
96
+ 'createGguiServer directly without the option.',
97
+ });
98
+ return;
99
+ }
100
+ // body is `ThemeConfig | null`. Empty body / `null` clears.
101
+ const raw = (req.body ?? null);
102
+ let parsed;
103
+ if (raw === null) {
104
+ parsed = null;
105
+ }
106
+ else {
107
+ const result = ThemeConfigSchema.safeParse(raw);
108
+ if (!result.success) {
109
+ res.status(400).json({
110
+ error: 'invalid_config',
111
+ issue: result.error.flatten(),
112
+ });
113
+ return;
114
+ }
115
+ parsed = result.data;
116
+ }
117
+ try {
118
+ await themeWriter(parsed);
119
+ updateConfig(parsed);
120
+ res.status(200).json({ ok: true, current: parsed });
121
+ }
122
+ catch (err) {
123
+ const message = err instanceof Error ? err.message : String(err);
124
+ res.status(500).json({ error: 'write_failed', message });
125
+ }
126
+ });
127
+ // POST /ggui/console/theme/upload — write a DTCG theme document
128
+ // alongside ggui.json and switch the manifest to point at it.
129
+ //
130
+ // Body shape: { filename: string, content: unknown, mode: 'light' | 'dark' }
131
+ //
132
+ // - filename is constrained by SAFE_FILENAME_RE — no path
133
+ // separators, no `ggui.json` / `package.json` collision.
134
+ // - content is validated via `safeParseThemeDocument` (plain DTCG
135
+ // v1) so a malformed paste is rejected before hitting disk.
136
+ // - On success we run `themeFileUploader(filename, content)`
137
+ // followed by `themeWriter({ file: './<filename>', mode })`,
138
+ // so a save lands the file AND updates the manifest atomically
139
+ // from the operator's POV. Partial-failure recovery is the
140
+ // CLI's responsibility — the seam contract says either both
141
+ // side-effects land or both throw.
142
+ app.post('/ggui/console/theme/upload', async (req, res) => {
143
+ applyDevtoolSecurityHeaders(res);
144
+ if (!requestHasAdminAuth(req)) {
145
+ res.status(401).json({ error: 'admin_auth_required' });
146
+ return;
147
+ }
148
+ if (themeWriter === undefined || themeFileUploader === undefined) {
149
+ res.status(501).json({
150
+ error: 'upload_not_configured',
151
+ message: 'Theme uploads require both themeWriter and themeFileUploader — ' +
152
+ 'launch via `ggui serve` (both are wired) instead of using ' +
153
+ 'createGguiServer directly without the options.',
154
+ });
155
+ return;
156
+ }
157
+ const body = (req.body ?? {});
158
+ const filename = typeof body.filename === 'string' ? body.filename : '';
159
+ if (!SAFE_FILENAME_RE.test(filename)) {
160
+ res.status(400).json({
161
+ error: 'invalid_filename',
162
+ message: 'filename must match /^[a-zA-Z0-9._-]+\\.json$/ — no path ' +
163
+ 'separators, ends with .json. Got: ' + JSON.stringify(filename),
164
+ });
165
+ return;
166
+ }
167
+ if (FORBIDDEN_FILENAMES.has(filename)) {
168
+ res.status(400).json({
169
+ error: 'forbidden_filename',
170
+ message: `filename "${filename}" would shadow a reserved file.`,
171
+ });
172
+ return;
173
+ }
174
+ const mode = body.mode;
175
+ if (mode !== 'light' && mode !== 'dark') {
176
+ res.status(400).json({
177
+ error: 'invalid_mode',
178
+ message: 'mode must be "light" or "dark".',
179
+ });
180
+ return;
181
+ }
182
+ const docResult = safeParseThemeDocument(body.content);
183
+ if (!docResult.success) {
184
+ res.status(400).json({
185
+ error: 'invalid_content',
186
+ issue: docResult.error.flatten(),
187
+ });
188
+ return;
189
+ }
190
+ try {
191
+ await themeFileUploader(filename, docResult.data);
192
+ const nextConfig = { file: `./${filename}`, mode };
193
+ await themeWriter(nextConfig);
194
+ updateConfig(nextConfig);
195
+ res.status(200).json({ ok: true, current: nextConfig });
196
+ }
197
+ catch (err) {
198
+ const message = err instanceof Error ? err.message : String(err);
199
+ res.status(500).json({ error: 'write_failed', message });
200
+ }
201
+ });
202
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Console-facing session-event timeline endpoints powering
3
+ * `/devtools/timeline` in the @ggui-ai/console SPA.
4
+ *
5
+ * Unlike `/devtools/llm-trace`, the timeline does NOT introduce a new
6
+ * sink — every event the operator wants to step through already
7
+ * exists in the {@link SessionStore} (inbound user
8
+ * actions + tool calls + UI mutations) and the
9
+ * {@link SessionStreamBuffer} (outbound stream cursor). This module is
10
+ * a thin read-only window over both.
11
+ *
12
+ * **Two surfaces, both admin-gated:**
13
+ *
14
+ * - `GET /ggui/console/timeline/sessions` — list of sessions visible
15
+ * to the timeline picker. All statuses (active / completed /
16
+ * expired) — operators frequently want to debug a session AFTER it
17
+ * terminated. Sorted most-recent-`lastActivityAt` first; defaults
18
+ * to 50 rows, clamped to [1, 200] via `?limit=`.
19
+ *
20
+ * - `GET /ggui/console/timeline/:sessionId/events` — the full event
21
+ * log for one session, oldest-first, drained from
22
+ * `sessionStore.observe(id, { tail: false })`. Also reports the
23
+ * outbound stream cursor (`streamBuffer.currentSeq`) so the
24
+ * operator can see live-channel progress without a separate fetch.
25
+ *
26
+ * Why REST-only (no SSE): replay is a snapshot. The operator picks a
27
+ * session and steps through what happened — they want a stable frozen
28
+ * view, not a live tail. SSE would force the scrubber to keep chasing
29
+ * a moving end-of-stream and complicate the UI without paying for it.
30
+ *
31
+ * Memory: bounded by whatever the underlying SessionStore retains.
32
+ * `InMemorySessionStore` keeps everything for the process lifetime —
33
+ * fine for OSS dev. A hosted closed runtime's persistent store is the
34
+ * durability surface.
35
+ */
36
+ import type { Express } from 'express';
37
+ import type { SessionStore, SessionStreamBuffer } from '@ggui-ai/mcp-server-core';
38
+ /**
39
+ * Mount `/ggui/console/timeline/*` routes on `app`. Caller is
40
+ * responsible for admin-gating the path prefix beforehand — this
41
+ * function does not re-implement auth (matches the
42
+ * `mountConsoleLlmTraceRoutes` shape in console-llm-trace.ts).
43
+ */
44
+ export declare function mountConsoleTimelineRoutes(app: Express, sessionStore: SessionStore | undefined, streamBuffer: SessionStreamBuffer | undefined): void;
45
+ //# sourceMappingURL=console-timeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-timeline.d.ts","sourceRoot":"","sources":["../src/console-timeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,KAAK,EAAqB,OAAO,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EAEV,YAAY,EACZ,mBAAmB,EACpB,MAAM,0BAA0B,CAAC;AAkElC;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,OAAO,EACZ,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,YAAY,EAAE,mBAAmB,GAAG,SAAS,GAC5C,IAAI,CA8JN"}
@@ -0,0 +1,169 @@
1
+ import { applyDevtoolSecurityHeaders } from './console-headers.js';
2
+ /**
3
+ * Drain {@link SessionStore.observe} into an array. The observe iterator
4
+ * with `{ tail: false }` resolves cleanly after replaying every stored
5
+ * event, so this is a plain `for await`. Bound by the store's per-
6
+ * session retention (in-memory: full history; hosted: store-defined).
7
+ */
8
+ async function drainSessionEvents(sessionStore, sessionId) {
9
+ const out = [];
10
+ for await (const event of sessionStore.observe(sessionId, {
11
+ fromSeq: 1,
12
+ tail: false,
13
+ })) {
14
+ out.push(event);
15
+ }
16
+ return out;
17
+ }
18
+ /**
19
+ * Mount `/ggui/console/timeline/*` routes on `app`. Caller is
20
+ * responsible for admin-gating the path prefix beforehand — this
21
+ * function does not re-implement auth (matches the
22
+ * `mountConsoleLlmTraceRoutes` shape in console-llm-trace.ts).
23
+ */
24
+ export function mountConsoleTimelineRoutes(app, sessionStore, streamBuffer) {
25
+ // GET /ggui/console/timeline/sessions?limit=<n>
26
+ app.get('/ggui/console/timeline/sessions', async (req, res) => {
27
+ applyDevtoolSecurityHeaders(res);
28
+ const limitRaw = req.query['limit'];
29
+ let limit = 50;
30
+ if (typeof limitRaw === 'string') {
31
+ const parsed = Number.parseInt(limitRaw, 10);
32
+ if (Number.isFinite(parsed) && parsed > 0) {
33
+ limit = Math.min(200, parsed);
34
+ }
35
+ }
36
+ // Zero-config shape: no store wired (pure-MCP boot) → empty list.
37
+ if (!sessionStore) {
38
+ const body = { sessions: [], total: 0 };
39
+ res.json(body);
40
+ return;
41
+ }
42
+ try {
43
+ // No status filter — operators want to debug completed +
44
+ // expired sessions, not just live ones. Limit is post-filter
45
+ // so the response stays bounded.
46
+ const sessions = await sessionStore.list({});
47
+ const summaries = [];
48
+ const now = Date.now();
49
+ for (const session of sessions) {
50
+ let streamSeq = 0;
51
+ if (streamBuffer) {
52
+ try {
53
+ streamSeq = await streamBuffer.currentSeq(session.id);
54
+ }
55
+ catch {
56
+ // Stream-cursor lookup is best-effort metadata; an
57
+ // adapter error must not blank the picker.
58
+ }
59
+ }
60
+ // Compute status from session state (the store doesn't
61
+ // surface its private `closed` flag on the Session type).
62
+ // We mirror the InMemorySessionStore's `computeStatus` rule:
63
+ // expired = expiresAt <= now, otherwise active. The
64
+ // 'completed' state needs the closed flag to disambiguate;
65
+ // we report 'active' until eviction. Operators reading the
66
+ // detail pane get the authoritative status from the store.
67
+ const status = session.expiresAt <= now ? 'expired' : 'active';
68
+ summaries.push({
69
+ sessionId: session.id,
70
+ appId: session.appId,
71
+ stackSize: session.stack.length,
72
+ createdAt: session.createdAt,
73
+ lastActivityAt: session.lastActivityAt,
74
+ status,
75
+ streamSeq,
76
+ });
77
+ }
78
+ // Most-recent activity first. Tiebreak on sessionId for
79
+ // stable ordering across reloads.
80
+ summaries.sort((a, b) => {
81
+ const byRecency = b.lastActivityAt - a.lastActivityAt;
82
+ if (byRecency !== 0)
83
+ return byRecency;
84
+ return a.sessionId.localeCompare(b.sessionId);
85
+ });
86
+ const trimmed = summaries.slice(0, limit);
87
+ const body = {
88
+ sessions: trimmed,
89
+ total: summaries.length,
90
+ };
91
+ res.json(body);
92
+ }
93
+ catch (err) {
94
+ res.status(500).json({
95
+ error: 'timeline_sessions_list_failed',
96
+ message: err instanceof Error
97
+ ? `Session store failed to list — ${err.message}`
98
+ : `Session store failed to list — ${String(err)}`,
99
+ });
100
+ }
101
+ });
102
+ // GET /ggui/console/timeline/:sessionId/events
103
+ app.get('/ggui/console/timeline/:sessionId/events', async (req, res) => {
104
+ applyDevtoolSecurityHeaders(res);
105
+ const sessionId = req.params['sessionId'];
106
+ if (!sessionId || sessionId.length === 0) {
107
+ res.status(400).json({
108
+ error: 'invalid_session_id',
109
+ message: 'sessionId path parameter is required',
110
+ });
111
+ return;
112
+ }
113
+ // Zero-config shape: no store wired → empty events.
114
+ if (!sessionStore) {
115
+ const body = {
116
+ sessionId,
117
+ events: [],
118
+ streamSeq: 0,
119
+ status: 'unknown',
120
+ };
121
+ res.json(body);
122
+ return;
123
+ }
124
+ try {
125
+ const session = await sessionStore.get(sessionId);
126
+ if (!session) {
127
+ // 404 is the right status — but we still return a well-
128
+ // formed body so the SPA can render an "expired/dropped"
129
+ // notice without a special-case branch. Body matches the
130
+ // schema; status code disambiguates.
131
+ const body = {
132
+ sessionId,
133
+ events: [],
134
+ streamSeq: 0,
135
+ status: 'unknown',
136
+ };
137
+ res.status(404).json(body);
138
+ return;
139
+ }
140
+ const events = await drainSessionEvents(sessionStore, sessionId);
141
+ let streamSeq = 0;
142
+ if (streamBuffer) {
143
+ try {
144
+ streamSeq = await streamBuffer.currentSeq(sessionId);
145
+ }
146
+ catch {
147
+ // Best-effort. Inbound events stand on their own.
148
+ }
149
+ }
150
+ const now = Date.now();
151
+ const status = session.expiresAt <= now ? 'expired' : 'active';
152
+ const body = {
153
+ sessionId,
154
+ events,
155
+ streamSeq,
156
+ status,
157
+ };
158
+ res.json(body);
159
+ }
160
+ catch (err) {
161
+ res.status(500).json({
162
+ error: 'timeline_events_drain_failed',
163
+ message: err instanceof Error
164
+ ? `Session store failed to drain — ${err.message}`
165
+ : `Session store failed to drain — ${String(err)}`,
166
+ });
167
+ }
168
+ });
169
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Console-facing validator-trace sink + REST/SSE endpoints powering
3
+ * `/devtools/validator` in the @ggui-ai/console SPA.
4
+ *
5
+ * This is the OSS-default sink the `ggui serve` process registers via
6
+ * {@link setValidatorTraceSink}. A hosted closed runtime may swap in a
7
+ * durable sink (e.g. Redis-backed) — the harness only knows about the
8
+ * {@link ValidatorTraceSink} contract.
9
+ *
10
+ * **Two surfaces, both admin-gated:**
11
+ * - `GET /ggui/console/validator/recent?limit=<n>` — JSON snapshot
12
+ * of the ring buffer's most recent N events, oldest-first within
13
+ * the page. Used for initial page load.
14
+ * - `GET /ggui/console/validator/stream` — SSE stream of new events
15
+ * as they fire. Heartbeat every 15s to keep proxies awake.
16
+ *
17
+ * **Memory bound.** Default capacity = 200 events. Each event carries
18
+ * the source-under-check (capped at ~16KB by truncateSourceForTrace)
19
+ * plus the full issues array — at ~20-30KB/event that's ~4-6MB peak.
20
+ * Operator can override via `createGguiServer({ validatorTrace: {
21
+ * capacity }})` if running on a small box.
22
+ */
23
+ import type { Express } from 'express';
24
+ import type { ValidatorTraceEvent, ValidatorTraceSink } from '@ggui-ai/ui-gen/harness/validator-trace-sink';
25
+ /** SSE listener — receives one event per runCheck invocation. */
26
+ type SseListener = (event: ValidatorTraceEvent) => void;
27
+ /**
28
+ * In-memory ring buffer + listener fanout. Implements
29
+ * {@link ValidatorTraceSink} so it can be passed to
30
+ * {@link setValidatorTraceSink}.
31
+ *
32
+ * **Why a class, not a closure.** Tests + operators read state
33
+ * (`recent()`, listener count) — instance methods on a class beat a
34
+ * pile of getter functions captured in scope. The shape is also the
35
+ * extension point if a hosted closed runtime wants to subclass and
36
+ * pipe events to Redis / DDB / S3 in addition to the ring buffer.
37
+ */
38
+ export declare class BoundedValidatorTraceSink implements ValidatorTraceSink {
39
+ private readonly capacity;
40
+ private readonly buffer;
41
+ private readonly listeners;
42
+ constructor(opts?: {
43
+ readonly capacity?: number;
44
+ });
45
+ emit(event: ValidatorTraceEvent): void;
46
+ /**
47
+ * Snapshot of the most-recent `limit` events, oldest-first within
48
+ * the returned slice (so the operator UI can append in chronological
49
+ * order without re-sorting).
50
+ */
51
+ recent(limit: number): readonly ValidatorTraceEvent[];
52
+ /** Subscribe to live events. Returns an unsubscribe function. */
53
+ subscribe(listener: SseListener): () => void;
54
+ /** Listener count — for tests + the eventual `/devtools/info` view. */
55
+ listenerCount(): number;
56
+ /** Buffer size — for tests + future bound enforcement assertions. */
57
+ size(): number;
58
+ }
59
+ /**
60
+ * Mount the `/ggui/console/validator/recent` + `/.../stream` routes on
61
+ * `app`. Caller is responsible for installing the admin gate
62
+ * middleware on these paths beforehand — this function does not
63
+ * re-implement auth.
64
+ */
65
+ export declare function mountConsoleValidatorRoutes(app: Express, sink: BoundedValidatorTraceSink): void;
66
+ export {};
67
+ //# sourceMappingURL=console-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-validator.d.ts","sourceRoot":"","sources":["../src/console-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAqB,OAAO,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,8CAA8C,CAAC;AAGtD,iEAAiE;AACjE,KAAK,WAAW,GAAG,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;AAExD;;;;;;;;;;GAUG;AACH,qBAAa,yBAA0B,YAAW,kBAAkB;IAClE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;gBAExC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAUjD,IAAI,CAAC,KAAK,EAAE,mBAAmB,GAAG,IAAI;IActC;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,mBAAmB,EAAE;IAKrD,iEAAiE;IACjE,SAAS,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,IAAI;IAO5C,uEAAuE;IACvE,aAAa,IAAI,MAAM;IAIvB,qEAAqE;IACrE,IAAI,IAAI,MAAM;CAGf;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,yBAAyB,GAC9B,IAAI,CA8CN"}
@@ -0,0 +1,105 @@
1
+ import { applyDevtoolSecurityHeaders } from './console-headers.js';
2
+ /**
3
+ * In-memory ring buffer + listener fanout. Implements
4
+ * {@link ValidatorTraceSink} so it can be passed to
5
+ * {@link setValidatorTraceSink}.
6
+ *
7
+ * **Why a class, not a closure.** Tests + operators read state
8
+ * (`recent()`, listener count) — instance methods on a class beat a
9
+ * pile of getter functions captured in scope. The shape is also the
10
+ * extension point if a hosted closed runtime wants to subclass and
11
+ * pipe events to Redis / DDB / S3 in addition to the ring buffer.
12
+ */
13
+ export class BoundedValidatorTraceSink {
14
+ capacity;
15
+ buffer = [];
16
+ listeners = new Set();
17
+ constructor(opts) {
18
+ const cap = opts?.capacity ?? 200;
19
+ if (!Number.isFinite(cap) || cap <= 0) {
20
+ throw new Error(`BoundedValidatorTraceSink: capacity must be a positive integer, got ${cap}`);
21
+ }
22
+ this.capacity = Math.floor(cap);
23
+ }
24
+ emit(event) {
25
+ this.buffer.push(event);
26
+ if (this.buffer.length > this.capacity) {
27
+ this.buffer.shift();
28
+ }
29
+ for (const listener of this.listeners) {
30
+ try {
31
+ listener(event);
32
+ }
33
+ catch {
34
+ // One bad listener must not block fan-out to others.
35
+ }
36
+ }
37
+ }
38
+ /**
39
+ * Snapshot of the most-recent `limit` events, oldest-first within
40
+ * the returned slice (so the operator UI can append in chronological
41
+ * order without re-sorting).
42
+ */
43
+ recent(limit) {
44
+ const n = Math.max(0, Math.min(limit, this.buffer.length));
45
+ return this.buffer.slice(-n);
46
+ }
47
+ /** Subscribe to live events. Returns an unsubscribe function. */
48
+ subscribe(listener) {
49
+ this.listeners.add(listener);
50
+ return () => {
51
+ this.listeners.delete(listener);
52
+ };
53
+ }
54
+ /** Listener count — for tests + the eventual `/devtools/info` view. */
55
+ listenerCount() {
56
+ return this.listeners.size;
57
+ }
58
+ /** Buffer size — for tests + future bound enforcement assertions. */
59
+ size() {
60
+ return this.buffer.length;
61
+ }
62
+ }
63
+ /**
64
+ * Mount the `/ggui/console/validator/recent` + `/.../stream` routes on
65
+ * `app`. Caller is responsible for installing the admin gate
66
+ * middleware on these paths beforehand — this function does not
67
+ * re-implement auth.
68
+ */
69
+ export function mountConsoleValidatorRoutes(app, sink) {
70
+ // GET /ggui/console/validator/recent?limit=<n> — JSON snapshot.
71
+ app.get('/ggui/console/validator/recent', (req, res) => {
72
+ applyDevtoolSecurityHeaders(res);
73
+ const limitRaw = req.query['limit'];
74
+ let limit = 100;
75
+ if (typeof limitRaw === 'string') {
76
+ const parsed = Number.parseInt(limitRaw, 10);
77
+ if (Number.isFinite(parsed) && parsed > 0) {
78
+ limit = Math.min(500, parsed);
79
+ }
80
+ }
81
+ res.json({ events: sink.recent(limit) });
82
+ });
83
+ // GET /ggui/console/validator/stream — SSE live stream.
84
+ // Heartbeat comment every 15s so reverse proxies don't kill the
85
+ // connection on idle. Client cleanup unregisters the listener.
86
+ app.get('/ggui/console/validator/stream', (req, res) => {
87
+ applyDevtoolSecurityHeaders(res);
88
+ res.setHeader('content-type', 'text/event-stream');
89
+ res.setHeader('cache-control', 'no-store');
90
+ res.setHeader('connection', 'keep-alive');
91
+ // Flush headers immediately so the EventSource starts receiving.
92
+ res.flushHeaders?.();
93
+ const heartbeat = setInterval(() => {
94
+ // SSE comment frame — clients ignore but proxies see traffic.
95
+ res.write(': ping\n\n');
96
+ }, 15000);
97
+ const off = sink.subscribe((event) => {
98
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
99
+ });
100
+ req.on('close', () => {
101
+ clearInterval(heartbeat);
102
+ off();
103
+ });
104
+ });
105
+ }
@@ -0,0 +1,7 @@
1
+ import type { OperatorConfig } from '@ggui-ai/project-config';
2
+ export interface WelcomePageInputs {
3
+ readonly operator?: OperatorConfig;
4
+ readonly appName?: string;
5
+ }
6
+ export declare const renderWelcomeHtml: (inputs: WelcomePageInputs, serverName: string) => string;
7
+ //# sourceMappingURL=console-welcome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-welcome.d.ts","sourceRoot":"","sources":["../src/console-welcome.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAoDD,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,iBAAiB,EACzB,YAAY,MAAM,KACjB,MA6KF,CAAC"}