@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,98 @@
1
+ /**
2
+ * FileSystemCodeStore — node:fs-backed {@link CodeStore}. The OSS dev
3
+ * default for `ggui serve`.
4
+ *
5
+ * Default root is `~/.ggui/code-cache/`; operators can override per
6
+ * `createGguiServer({ codeStore })` for tests or project-local caches.
7
+ *
8
+ * ## Layout
9
+ *
10
+ * Two-level directory sharding by hash prefix to avoid stuffing one
11
+ * directory with thousands of files:
12
+ *
13
+ * <root>/<hash[0..2]>/<hash[2..]>.js
14
+ *
15
+ * e.g. `~/.ggui/code-cache/ab/cdef...0123.js`. The `.js` suffix isn't
16
+ * load-bearing for retrieval — `get(hash)` is the only read API and it
17
+ * derives the path from the hash — but operators occasionally `cat`
18
+ * one for debugging, and the suffix tells their editor to syntax-
19
+ * highlight as JavaScript.
20
+ *
21
+ * ## Atomicity
22
+ *
23
+ * `put` writes via temp-file + rename for atomic-or-throw — readers
24
+ * never see a half-written file. The mkdir-recursive call before write
25
+ * is idempotent and races safely (Linux `mkdir -p` semantics).
26
+ *
27
+ * ## Persistence + cleanup
28
+ *
29
+ * The store grows unbounded. Operators who care can periodically
30
+ * `rm -rf ~/.ggui/code-cache/` — every entry is content-addressable
31
+ * and immutable, so a re-fetch will repopulate from upstream code on
32
+ * next push. There is no eviction policy in the seam itself.
33
+ *
34
+ * ## Permissions
35
+ *
36
+ * Files are written with default umask. The cache contains compiled
37
+ * componentCode the agent generated; nothing more sensitive than what
38
+ * already shipped over the wire to the iframe. Operators who need
39
+ * tighter perms wrap with their own umask.
40
+ */
41
+ import { createHash } from 'node:crypto';
42
+ import { access, constants as fsConstants, mkdir, readFile, rename, writeFile, } from 'node:fs/promises';
43
+ import { homedir } from 'node:os';
44
+ import { dirname, join } from 'node:path';
45
+ import { CODE_HASH_REGEX, } from '@ggui-ai/mcp-server-core';
46
+ export class FileSystemCodeStore {
47
+ root;
48
+ constructor(opts = {}) {
49
+ this.root = opts.root ?? join(homedir(), '.ggui', 'code-cache');
50
+ }
51
+ /** Absolute on-disk path for a given hash. Two-level sharded layout. */
52
+ absPath(hash) {
53
+ // Sharding: `<root>/<hash[0..2]>/<hash[2..]>.js`. Validation on
54
+ // every put + get below means `hash` is always 64 lowercase hex.
55
+ return join(this.root, hash.slice(0, 2), `${hash.slice(2)}.js`);
56
+ }
57
+ async put(hash, code) {
58
+ if (!CODE_HASH_REGEX.test(hash)) {
59
+ throw new Error(`FileSystemCodeStore.put: hash must match ${CODE_HASH_REGEX.source}, got ${JSON.stringify(hash)}`);
60
+ }
61
+ const absPath = this.absPath(hash);
62
+ // Idempotent: if the file already exists, skip rewrite. Cheap fast
63
+ // path that avoids unnecessary disk churn when the same blueprint
64
+ // is pushed repeatedly.
65
+ if (await fileExists(absPath))
66
+ return;
67
+ await mkdir(dirname(absPath), { recursive: true });
68
+ // Temp-file + rename for atomic-or-throw write; readers never see
69
+ // partial bytes.
70
+ const tmpPath = `${absPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
71
+ await writeFile(tmpPath, code, { encoding: 'utf-8' });
72
+ await rename(tmpPath, absPath);
73
+ }
74
+ async get(hash) {
75
+ if (!CODE_HASH_REGEX.test(hash))
76
+ return null;
77
+ try {
78
+ return await readFile(this.absPath(hash), { encoding: 'utf-8' });
79
+ }
80
+ catch (err) {
81
+ if (err.code === 'ENOENT')
82
+ return null;
83
+ throw err;
84
+ }
85
+ }
86
+ hashOf(code) {
87
+ return createHash('sha256').update(code, 'utf-8').digest('hex');
88
+ }
89
+ }
90
+ async function fileExists(absPath) {
91
+ try {
92
+ await access(absPath, fsConstants.F_OK);
93
+ return true;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Embedded-ui cookie auth plane.
3
+ *
4
+ * This module owns the THIRD of the three token kinds the server
5
+ * tracks for the embedded UI:
6
+ *
7
+ * 1. bootstrap tokens — short-TTL, single-use, MCP Apps iframes.
8
+ * 2. session tokens — longer-TTL, reusable, post-bootstrap
9
+ * reconnect creds.
10
+ * 3. console cookies — longer-TTL, reusable, same-origin
11
+ * browser-only, issued by this server
12
+ * for ITS OWN console landing/viewer
13
+ * pages. **Scoped narrowly**: consumed
14
+ * ONLY at the live-channel WebSocket
15
+ * upgrade when `cookieAuth` is wired.
16
+ * NEVER authenticates `/mcp`, `/pair`,
17
+ * `/threads`, or any other ingress.
18
+ *
19
+ * Isolation invariant (load-bearing). The cookie uses the SAME HMAC
20
+ * shape as the bootstrap/session tokens but a distinct `kind` claim
21
+ * (`'console-session'`). That makes cross-kind confusion
22
+ * impossible: a cookie value CAN'T verify as a bootstrap or session
23
+ * token. Secrets are shared across kinds because they live in the
24
+ * same trust domain (server-minted same-origin creds) and kind
25
+ * discrimination is sufficient.
26
+ *
27
+ * Rationale for cookie-over-bearer. Browsers can't set `Authorization`
28
+ * on native WebSocket upgrades. A same-origin HTTP-only cookie travels
29
+ * automatically on the upgrade request, is unreadable from JS
30
+ * (blocks XSS exfil), and is scoped to this origin by the browser
31
+ * SameSite rules. The one genuine mismatch case (cookie authentication
32
+ * on cross-origin requests) is defense-in-depth: set `SameSite=Strict`
33
+ * so the cookie never leaves the console's origin in the first
34
+ * place.
35
+ *
36
+ * Lifecycle. Cookie is minted at `POST /ggui/console/session-cookie`
37
+ * (operator posts a shortCode → server resolves → sets Set-Cookie).
38
+ * Cookie is consumed at `GET /ws` upgrade (if the server composition
39
+ * wires `cookieAuth` to the session-channel). Never mutated in
40
+ * between.
41
+ */
42
+ import type { IncomingHttpHeaders } from 'node:http';
43
+ /**
44
+ * Cookie name. Chosen deliberately to NOT conflict with common framework
45
+ * cookies (Express session, CSRF, etc.). Change carries a compat
46
+ * concern — the console SPA + the session-channel upgrade read by
47
+ * this exact name. Keep this export as the single source of truth.
48
+ */
49
+ export declare const CONSOLE_COOKIE_NAME = "ggui_console_session";
50
+ /**
51
+ * Result of minting a cookie. Callers set the cookie on their response
52
+ * (the cookie endpoint) and echo the bound `sessionId`/`appId` back in
53
+ * the response body so the client SPA can bootstrap the viewer without
54
+ * a follow-up round-trip.
55
+ */
56
+ export interface DevtoolCookieMint {
57
+ readonly cookieValue: string;
58
+ readonly setCookieHeader: string;
59
+ readonly expiresAt: number;
60
+ readonly sessionId: string;
61
+ readonly appId: string;
62
+ }
63
+ export interface MintDevtoolCookieInput {
64
+ readonly sessionId: string;
65
+ readonly appId: string;
66
+ /** HMAC secret. Shared with bootstrap + session tokens by design. */
67
+ readonly secret: string;
68
+ /** Cookie TTL in seconds. Defaults to 8 hours. */
69
+ readonly ttlSec?: number;
70
+ /**
71
+ * When `true`, adds `Secure` to the Set-Cookie attribute string so
72
+ * the cookie is only sent over HTTPS. Operators fronting the server
73
+ * with TLS pass `true`; local dev / HTTP-only deployments leave it
74
+ * falsy. No auto-detect — explicit is safer.
75
+ */
76
+ readonly secure?: boolean;
77
+ /** Cookie path. Defaults to `/`. */
78
+ readonly path?: string;
79
+ /**
80
+ * SameSite policy. Defaults to `'Strict'` — same-origin operator
81
+ * convenience means we actively want the cookie NOT to travel on
82
+ * cross-origin navigations.
83
+ */
84
+ readonly sameSite?: 'Strict' | 'Lax' | 'None';
85
+ }
86
+ /**
87
+ * Mint a cookie value bound to `{ sessionId, appId }` and the formatted
88
+ * `Set-Cookie` header to send on the response. Caller does
89
+ * `res.setHeader('Set-Cookie', result.setCookieHeader)`.
90
+ */
91
+ export declare function mintDevtoolCookie(input: MintDevtoolCookieInput): DevtoolCookieMint;
92
+ /**
93
+ * Parse a raw `Cookie:` header and extract the console cookie
94
+ * value, or `null` if absent.
95
+ *
96
+ * Minimal manual parser — intentionally avoids pulling in a cookie
97
+ * library for a single cookie name. Handles the shapes we actually
98
+ * see (single cookie, multiple `; `-separated pairs, URL-encoded
99
+ * values); anything exotic returns null rather than trying to be
100
+ * clever.
101
+ */
102
+ export declare function extractDevtoolCookie(cookieHeader: string | undefined): string | null;
103
+ /**
104
+ * Read the console cookie off incoming headers. Thin wrapper
105
+ * around {@link extractDevtoolCookie} that accepts Node's
106
+ * `IncomingHttpHeaders` directly — the shape both the Express
107
+ * request and the raw `IncomingMessage` use.
108
+ *
109
+ * Node's declared `headers['cookie']` type is `string | undefined`;
110
+ * both Express (post-body-parser) and node:http (the WebSocket
111
+ * upgrade path) consistently deliver a single string. Multi-line
112
+ * `Cookie:` header arrays are not a real shape we see in practice,
113
+ * so we don't try to support them — if a future runtime produces
114
+ * that shape the cookie silently 404s, which is the correct
115
+ * degraded behavior for this auth plane.
116
+ */
117
+ export declare function readDevtoolCookieFromHeaders(headers: IncomingHttpHeaders): string | null;
118
+ /**
119
+ * Verified claims extracted from an console cookie. Scope is
120
+ * deliberately narrow: just the binding the session-channel upgrade
121
+ * needs to enforce `subscribe.sessionId === cookie.sessionId`.
122
+ */
123
+ export interface DevtoolCookieClaims {
124
+ readonly sessionId: string;
125
+ readonly appId: string;
126
+ }
127
+ /**
128
+ * Verify an console cookie value. Returns the claims on success,
129
+ * `null` on any failure (signature, expiry, wrong kind, malformed).
130
+ *
131
+ * Never throws. Failures collapse to `null` so callers don't have to
132
+ * distinguish failure reasons in the hot path — an invalid cookie
133
+ * means "no cookie" for auth purposes. A future refinement could
134
+ * differentiate "cookie expired" (prompt re-mint) from "cookie is
135
+ * for another server" (hard reject), but that distinction is not
136
+ * surfaced today.
137
+ */
138
+ export declare function verifyDevtoolCookie(cookieValue: string, secret: string): DevtoolCookieClaims | null;
139
+ //# sourceMappingURL=console-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-auth.d.ts","sourceRoot":"","sources":["../src/console-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAOrD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;CAC/C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,iBAAiB,CAqBnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAcf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,mBAAmB,GAC3B,MAAM,GAAG,IAAI,CAIf;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,mBAAmB,GAAG,IAAI,CAO5B"}
@@ -0,0 +1,102 @@
1
+ import { DEFAULT_DEVTOOL_SESSION_TTL_SEC, mintDevtoolSessionToken, verifyToken, } from '@ggui-ai/mcp-server-core';
2
+ /**
3
+ * Cookie name. Chosen deliberately to NOT conflict with common framework
4
+ * cookies (Express session, CSRF, etc.). Change carries a compat
5
+ * concern — the console SPA + the session-channel upgrade read by
6
+ * this exact name. Keep this export as the single source of truth.
7
+ */
8
+ export const CONSOLE_COOKIE_NAME = 'ggui_console_session';
9
+ /**
10
+ * Mint a cookie value bound to `{ sessionId, appId }` and the formatted
11
+ * `Set-Cookie` header to send on the response. Caller does
12
+ * `res.setHeader('Set-Cookie', result.setCookieHeader)`.
13
+ */
14
+ export function mintDevtoolCookie(input) {
15
+ const ttlSec = input.ttlSec ?? DEFAULT_DEVTOOL_SESSION_TTL_SEC;
16
+ const { token, claims } = mintDevtoolSessionToken({ sessionId: input.sessionId, appId: input.appId, ttlSec }, input.secret);
17
+ const attrs = [
18
+ `${CONSOLE_COOKIE_NAME}=${token}`,
19
+ `Path=${input.path ?? '/'}`,
20
+ `Max-Age=${ttlSec}`,
21
+ `SameSite=${input.sameSite ?? 'Strict'}`,
22
+ `HttpOnly`,
23
+ ];
24
+ if (input.secure)
25
+ attrs.push('Secure');
26
+ return {
27
+ cookieValue: token,
28
+ setCookieHeader: attrs.join('; '),
29
+ expiresAt: claims.exp * 1000,
30
+ sessionId: input.sessionId,
31
+ appId: input.appId,
32
+ };
33
+ }
34
+ /**
35
+ * Parse a raw `Cookie:` header and extract the console cookie
36
+ * value, or `null` if absent.
37
+ *
38
+ * Minimal manual parser — intentionally avoids pulling in a cookie
39
+ * library for a single cookie name. Handles the shapes we actually
40
+ * see (single cookie, multiple `; `-separated pairs, URL-encoded
41
+ * values); anything exotic returns null rather than trying to be
42
+ * clever.
43
+ */
44
+ export function extractDevtoolCookie(cookieHeader) {
45
+ if (!cookieHeader)
46
+ return null;
47
+ const pairs = cookieHeader.split(';');
48
+ for (const raw of pairs) {
49
+ const trimmed = raw.trim();
50
+ if (!trimmed)
51
+ continue;
52
+ const eq = trimmed.indexOf('=');
53
+ if (eq <= 0)
54
+ continue;
55
+ const name = trimmed.slice(0, eq);
56
+ if (name !== CONSOLE_COOKIE_NAME)
57
+ continue;
58
+ const value = trimmed.slice(eq + 1);
59
+ return value ? decodeURIComponent(value) : null;
60
+ }
61
+ return null;
62
+ }
63
+ /**
64
+ * Read the console cookie off incoming headers. Thin wrapper
65
+ * around {@link extractDevtoolCookie} that accepts Node's
66
+ * `IncomingHttpHeaders` directly — the shape both the Express
67
+ * request and the raw `IncomingMessage` use.
68
+ *
69
+ * Node's declared `headers['cookie']` type is `string | undefined`;
70
+ * both Express (post-body-parser) and node:http (the WebSocket
71
+ * upgrade path) consistently deliver a single string. Multi-line
72
+ * `Cookie:` header arrays are not a real shape we see in practice,
73
+ * so we don't try to support them — if a future runtime produces
74
+ * that shape the cookie silently 404s, which is the correct
75
+ * degraded behavior for this auth plane.
76
+ */
77
+ export function readDevtoolCookieFromHeaders(headers) {
78
+ const raw = headers['cookie'];
79
+ if (typeof raw === 'string')
80
+ return extractDevtoolCookie(raw);
81
+ return null;
82
+ }
83
+ /**
84
+ * Verify an console cookie value. Returns the claims on success,
85
+ * `null` on any failure (signature, expiry, wrong kind, malformed).
86
+ *
87
+ * Never throws. Failures collapse to `null` so callers don't have to
88
+ * distinguish failure reasons in the hot path — an invalid cookie
89
+ * means "no cookie" for auth purposes. A future refinement could
90
+ * differentiate "cookie expired" (prompt re-mint) from "cookie is
91
+ * for another server" (hard reject), but that distinction is not
92
+ * surfaced today.
93
+ */
94
+ export function verifyDevtoolCookie(cookieValue, secret) {
95
+ const result = verifyToken(cookieValue, secret, 'console-session');
96
+ if (!result.ok)
97
+ return null;
98
+ return {
99
+ sessionId: result.claims.sessionId,
100
+ appId: result.claims.appId,
101
+ };
102
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Console-facing blueprint-cache trace sink + REST/SSE endpoints
3
+ * powering `/devtools/cache` in the @ggui-ai/console SPA.
4
+ *
5
+ * This is the OSS-default sink the `ggui serve` process registers via
6
+ * {@link setCacheTraceSink}. A hosted closed runtime may swap in a
7
+ * durable sink (e.g. Redis-backed) — the matcher only knows about the
8
+ * {@link CacheTraceSink} contract.
9
+ *
10
+ * **Two surfaces, both admin-gated:**
11
+ * - `GET /ggui/console/cache/recent?limit=<n>` — JSON snapshot of the
12
+ * ring buffer's most recent N events, oldest-first within the page.
13
+ * Used for initial page load.
14
+ * - `GET /ggui/console/cache/stream` — SSE stream of new events as
15
+ * they fire. Heartbeat every 15s to keep proxies awake.
16
+ *
17
+ * **Memory bound.** Default capacity = 200 events. Each event carries
18
+ * a (truncated) intent + up to 5 candidate scores — at ~5KB/event that's
19
+ * ~1MB peak. Operator can override via
20
+ * `createGguiServer({ cacheTrace: { capacity }})` if running on a small
21
+ * box.
22
+ *
23
+ * **Why a separate file from console-llm-trace.** Both share the same
24
+ * shape (bounded buffer + SSE fanout + admin-gated REST), but:
25
+ *
26
+ * 1. The contract differ — `LlmTraceSink` carries provider/model/
27
+ * tokens, `CacheTraceSink` carries scope/intent/decision/
28
+ * candidates. Generic-typing one buffer over both narrows the
29
+ * operator UI's awareness of which fields to format.
30
+ * 2. The two can be swapped independently — a hosted closed runtime
31
+ * may want a durable LLM trace (cost analysis) but a lossy cache
32
+ * trace (ops signal only). One file per sink keeps that swap lean.
33
+ */
34
+ import type { Express } from 'express';
35
+ import type { CacheTraceEvent, CacheTraceSink } from '@ggui-ai/mcp-server-handlers/session-mutations';
36
+ /** SSE listener — receives one event per cache lookup. */
37
+ type SseListener = (event: CacheTraceEvent) => void;
38
+ /**
39
+ * In-memory ring buffer + listener fanout. Implements
40
+ * {@link CacheTraceSink} so it can be passed to
41
+ * {@link setCacheTraceSink}.
42
+ *
43
+ * **Why a class, not a closure.** Same reasoning as
44
+ * {@link BoundedLlmTraceSink} — tests + operators read state
45
+ * (`recent()`, listener count); subclass is the extension point if a
46
+ * hosted closed runtime wants to pipe events to Redis / DDB / S3 in
47
+ * addition to the ring buffer.
48
+ */
49
+ export declare class BoundedCacheTraceSink implements CacheTraceSink {
50
+ private readonly capacity;
51
+ private readonly buffer;
52
+ private readonly listeners;
53
+ constructor(opts?: {
54
+ readonly capacity?: number;
55
+ });
56
+ emit(event: CacheTraceEvent): void;
57
+ /**
58
+ * Snapshot of the most-recent `limit` events, oldest-first within
59
+ * the returned slice (so the operator UI can append in chronological
60
+ * order without re-sorting).
61
+ */
62
+ recent(limit: number): readonly CacheTraceEvent[];
63
+ /** Subscribe to live events. Returns an unsubscribe function. */
64
+ subscribe(listener: SseListener): () => void;
65
+ /** Listener count — for tests + the eventual `/devtools/info` view. */
66
+ listenerCount(): number;
67
+ /** Buffer size — for tests + future bound enforcement assertions. */
68
+ size(): number;
69
+ }
70
+ /**
71
+ * Mount the `/ggui/console/cache/recent` + `/.../stream` routes on
72
+ * `app`. Caller is responsible for installing the admin gate
73
+ * middleware on these paths beforehand — this function does not
74
+ * re-implement auth.
75
+ */
76
+ export declare function mountConsoleCacheRoutes(app: Express, sink: BoundedCacheTraceSink): void;
77
+ export {};
78
+ //# sourceMappingURL=console-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-cache.d.ts","sourceRoot":"","sources":["../src/console-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAqB,OAAO,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACf,MAAM,gDAAgD,CAAC;AAGxD,0DAA0D;AAC1D,KAAK,WAAW,GAAG,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,qBAAa,qBAAsB,YAAW,cAAc;IAC1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyB;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;gBAExC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAUjD,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI;IAclC;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,eAAe,EAAE;IAKjD,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,uBAAuB,CACrC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,qBAAqB,GAC1B,IAAI,CA8CN"}
@@ -0,0 +1,105 @@
1
+ import { applyDevtoolSecurityHeaders } from './console-headers.js';
2
+ /**
3
+ * In-memory ring buffer + listener fanout. Implements
4
+ * {@link CacheTraceSink} so it can be passed to
5
+ * {@link setCacheTraceSink}.
6
+ *
7
+ * **Why a class, not a closure.** Same reasoning as
8
+ * {@link BoundedLlmTraceSink} — tests + operators read state
9
+ * (`recent()`, listener count); subclass is the extension point if a
10
+ * hosted closed runtime wants to pipe events to Redis / DDB / S3 in
11
+ * addition to the ring buffer.
12
+ */
13
+ export class BoundedCacheTraceSink {
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(`BoundedCacheTraceSink: 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/cache/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 mountConsoleCacheRoutes(app, sink) {
70
+ // GET /ggui/console/cache/recent?limit=<n> — JSON snapshot.
71
+ app.get('/ggui/console/cache/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/cache/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/cache/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,124 @@
1
+ /**
2
+ * Security headers for console responses.
3
+ *
4
+ * Scope: applied ONLY to surfaces the console SPA itself owns —
5
+ * the `/ggui/console/*` API routes + the static mount + its SPA
6
+ * fallback. Deliberately NOT applied to `/mcp`, `/pair`, `/threads`,
7
+ * `/ggui/auth-check`, `/ggui/health` — those are API contract that
8
+ * browsers don't interpret as HTML, and bleeding CSP onto them adds
9
+ * noise without security value.
10
+ *
11
+ * The policy is the NARROWEST that keeps the current viewer working.
12
+ * Lifting any directive carries a real product decision; do NOT widen
13
+ * casually.
14
+ *
15
+ * - `default-src 'none'` — deny by default, opt into every source.
16
+ * - `script-src 'self' blob: data: <ggui-shell-hash>` — Vite emits
17
+ * `<script type="module" src="/assets/...">` with no inline scripts
18
+ * or eval (`'self'` covers that). `blob:` covers the outer dynamic
19
+ * `import(blob:URL)` call in `ReactComponentRenderer`'s
20
+ * `loadModule`: the renderer wraps compiled ESM in a Blob,
21
+ * `URL.createObjectURL`s it, and dynamically imports the resulting
22
+ * blob URL. `data:` covers the INNER bare-specifier rewrite path
23
+ * in `@ggui-ai/design/rendering/rewrite-imports.ts`: `import React
24
+ * from 'react'` inside the generated module becomes `import
25
+ * React from 'data:text/javascript,…shim…'`, and the browser
26
+ * imports that `data:` URL as a script subresource. Stripping
27
+ * either directive silently breaks the renderer — jsdom tests
28
+ * can't detect this because jsdom doesn't enforce CSP; the
29
+ * `live-generation.spec.ts` browser proof is what catches it.
30
+ * Both sources are produced by code running in THIS
31
+ * origin; neither permits third-party script loading, and
32
+ * neither enables `eval` / `new Function` (that needs
33
+ * `'unsafe-eval'`, which is explicitly absent).
34
+ *
35
+ * `<ggui-shell-hash>` is `GGUI_SESSION_SHELL_SCRIPT_HASH` from
36
+ * `mcp-apps-outbound.ts` — the sha-256 source-expression
37
+ * authorising the inline `<script>` block of the production thin
38
+ * shell `<McpAppIframe>` mounts via `srcdoc`. `srcdoc` iframes
39
+ * inherit the parent's CSP, so without the hash the shell's
40
+ * bootstrap script is blocked at parse time and the renderer is
41
+ * never fetched (lifecycle stuck at `mounting`, specs pinning
42
+ * `code-ready` time out). Hash CSP is the right shape because the
43
+ * shell body is static + known at build time; binding the policy
44
+ * to the exact bytes is narrower than `'unsafe-inline'` and
45
+ * narrower than a runtime nonce. Drift between this CSP and the
46
+ * actual script body is caught by `mcp-apps-outbound.test.ts` —
47
+ * edit the script, regenerate the hash.
48
+ *
49
+ * `'unsafe-inline'` is still NEVER allowed for scripts; the hash
50
+ * mechanism preserves the strict-no-inline posture for everything
51
+ * except the one known shell body whose bytes are pinned.
52
+ * - `style-src 'self' 'unsafe-inline'` — React `style={...}` props
53
+ * produce inline `style=""` attributes. `'unsafe-inline'` is
54
+ * required for those attribute-level styles; it does NOT open the
55
+ * door to inline `<script>` or `<style>` blocks (different CSP
56
+ * directive). Scoped, not dangerous.
57
+ * - `connect-src 'self'` — covers both same-origin `fetch()` to
58
+ * `/ggui/console/session-cookie` and the `new WebSocket('/ws')`
59
+ * upgrade. CSP Level 3 treats `'self'` as matching the document's
60
+ * origin across http/https/ws/wss, which is exactly the scope.
61
+ * - `img-src 'self' data:` — no images in the minimal viewer today,
62
+ * but data: URIs are a common future need (inline SVG, favicons).
63
+ * `'self'` alone would reject a minor future surface; adding data:
64
+ * now avoids a churn commit later.
65
+ * - `font-src 'self'` — no custom fonts in-tree, but a future design
66
+ * pass likely adds one. `'self'` matches that future without
67
+ * permissively allowing third-party font CDNs.
68
+ * - `frame-ancestors 'none'` — the console is NOT a frame target.
69
+ * The MCP Apps iframe shell is the framed surface (lives at a
70
+ * different URL with its own CSP); embedding THIS page inside
71
+ * another document would always be a clickjacking concern. Pairs
72
+ * with `X-Frame-Options: DENY` for legacy-browser coverage.
73
+ * - `base-uri 'none'` — no `<base>` tag in `index.html`; deny
74
+ * injection.
75
+ * - `form-action 'self'` — if a future slice adds a form, restrict
76
+ * submissions to same origin. Today there are no forms.
77
+ *
78
+ * What's deliberately NOT here:
79
+ *
80
+ * - `upgrade-insecure-requests` — operators run console on plain
81
+ * HTTP during local dev; forcing HTTPS would break that. TLS is
82
+ * the operator's choice via reverse proxy.
83
+ * - `report-uri` / `report-to` — no CSP telemetry ingestion today.
84
+ * A future hardening slice can add one; keeping this off avoids
85
+ * tying the OSS server to a specific reporting endpoint.
86
+ * - `Strict-Transport-Security` — same reasoning: operator owns HSTS
87
+ * via their reverse proxy / load balancer. The OSS server must
88
+ * stay HTTP-friendly for dev and self-hosted loopback.
89
+ *
90
+ * Header shape (in the order `setHeader` applies):
91
+ *
92
+ * Content-Security-Policy: <policy>
93
+ * X-Content-Type-Options: nosniff
94
+ * X-Frame-Options: DENY
95
+ * Referrer-Policy: strict-origin-when-cross-origin
96
+ * Cross-Origin-Opener-Policy: same-origin
97
+ */
98
+ import type { Response } from 'express';
99
+ import type { ServerResponse } from 'node:http';
100
+ /**
101
+ * The CSP directive string. Exported so tests can assert against the
102
+ * exact shape without hard-coding the directive order inside the test
103
+ * file. Change this string and a focused header test catches the
104
+ * regression.
105
+ */
106
+ export declare const DEVTOOL_CSP: string;
107
+ /**
108
+ * Other security headers applied alongside CSP. Split from `DEVTOOL_CSP`
109
+ * so tests can enumerate them separately. Keep this list small +
110
+ * well-justified — every header is a compat risk on the operator's
111
+ * browser matrix.
112
+ */
113
+ export declare const DEVTOOL_SECURITY_HEADERS: ReadonlyArray<readonly [string, string]>;
114
+ /**
115
+ * Apply the console security header set to a response. Pure
116
+ * side-effect on `res`; returns `void`. Safe to call multiple times on
117
+ * the same response — `setHeader` overwrites.
118
+ *
119
+ * Accepts both Express `Response` and raw Node `ServerResponse` so the
120
+ * `express.static` `setHeaders(res, path, stat)` callback (which passes
121
+ * `ServerResponse`) can reuse the same helper.
122
+ */
123
+ export declare function applyDevtoolSecurityHeaders(res: Response | ServerResponse): void;
124
+ //# sourceMappingURL=console-headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-headers.d.ts","sourceRoot":"","sources":["../src/console-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgGG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGhD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,MAcd,CAAC;AAEb;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,EAAE,aAAa,CAClD,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAO1B,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,QAAQ,GAAG,cAAc,GAC7B,IAAI,CAIN"}