@ggui-ai/mcp-server 0.6.3 → 0.8.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 (42) hide show
  1. package/dist/api-renders-routes.d.ts.map +1 -1
  2. package/dist/api-renders-routes.js +9 -1
  3. package/dist/browser-cors.d.ts +29 -0
  4. package/dist/browser-cors.d.ts.map +1 -0
  5. package/dist/browser-cors.js +64 -0
  6. package/dist/build-mcp.d.ts.map +1 -1
  7. package/dist/build-mcp.js +5 -1
  8. package/dist/code-store-fs.d.ts +3 -0
  9. package/dist/code-store-fs.d.ts.map +1 -1
  10. package/dist/code-store-fs.js +27 -3
  11. package/dist/console-session-routes.d.ts +10 -5
  12. package/dist/console-session-routes.d.ts.map +1 -1
  13. package/dist/console-session-routes.js +10 -5
  14. package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
  15. package/dist/ggui-session-channel/outbound.js +12 -0
  16. package/dist/ggui-session-channel/socket-router.d.ts +33 -0
  17. package/dist/ggui-session-channel/socket-router.d.ts.map +1 -1
  18. package/dist/ggui-session-channel/socket-router.js +155 -0
  19. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
  20. package/dist/ggui-session-channel/subscribe.js +43 -2
  21. package/dist/ggui-session-channel.d.ts +114 -0
  22. package/dist/ggui-session-channel.d.ts.map +1 -1
  23. package/dist/ggui-session-channel.js +77 -2
  24. package/dist/health-routes.d.ts +3 -0
  25. package/dist/health-routes.d.ts.map +1 -1
  26. package/dist/health-routes.js +11 -1
  27. package/dist/mcp-apps-outbound.d.ts +236 -31
  28. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  29. package/dist/mcp-apps-outbound.js +940 -261
  30. package/dist/origin-validation.d.ts +120 -0
  31. package/dist/origin-validation.d.ts.map +1 -0
  32. package/dist/origin-validation.js +199 -0
  33. package/dist/render-read-gate.d.ts +50 -0
  34. package/dist/render-read-gate.d.ts.map +1 -0
  35. package/dist/render-read-gate.js +36 -0
  36. package/dist/runtime-bundle-route.d.ts +15 -0
  37. package/dist/runtime-bundle-route.d.ts.map +1 -1
  38. package/dist/runtime-bundle-route.js +20 -4
  39. package/dist/server.d.ts +227 -9
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +292 -28
  42. package/package.json +12 -12
@@ -1 +1 @@
1
- {"version":3,"file":"api-renders-routes.d.ts","sourceRoot":"","sources":["../src/api-renders-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAO9F,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,sBAAsB,CAAC,WAAW,CAAC,CAAC;IACzD,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,KACV;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,oEAAoE;IACpE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,MAAM,CAAC;IACzC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAwU9D"}
1
+ {"version":3,"file":"api-renders-routes.d.ts","sourceRoot":"","sources":["../src/api-renders-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAO9F,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,sBAAsB,CAAC,WAAW,CAAC,CAAC;IACzD,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,KACV;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,oEAAoE;IACpE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,MAAM,CAAC;IACzC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAgV9D"}
@@ -179,7 +179,15 @@ export function mountApiRendersRoutes(opts) {
179
179
  renderCodeUrl = `${base}/code/${hash}.js`;
180
180
  }
181
181
  catch {
182
- // Silent — caller falls back to live-mode delivery.
182
+ // Silent — the caller falls back to live-mode delivery, and
183
+ // here that fallback is structural rather than hoped for:
184
+ // `mintBootstrap` is optional on the options type, but these
185
+ // routes mount only under `mcpAppsEnabled` + a render store
186
+ // + a token secret, which are the same preconditions that
187
+ // assign the minter. So a reader of THIS file cannot see the
188
+ // guarantee — it lives at the mount site, and it is what
189
+ // keeps the silence honest. Lose that pairing and this catch
190
+ // becomes a swallow.
183
191
  }
184
192
  try {
185
193
  const bundle = await deriveContractBundle(render);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * CORS for browser-resident MCP clients (ggui#438b).
3
+ *
4
+ * SCOPE: this is enablement, not conformance. No MCP spec layer requires
5
+ * CORS — the transport spec never mentions it, and the MCP Apps spec is
6
+ * fully host-mediated (the view postMessages its host; the host holds the
7
+ * MCP connection). claude.ai and ChatGPT connect to remote MCP servers
8
+ * from their BACKENDS, so they need none of this. It exists for clients
9
+ * where the browser itself holds the Streamable HTTP connection: guuey's
10
+ * browser host layer, local dev SPAs, Electron renderers.
11
+ *
12
+ * POSTURE: an origin ALLOWLIST, never `*`. The three existing `*` routes
13
+ * (runtime-bundle, code, api-renders) are credential-free or token-gated
14
+ * public reads; `/mcp` is different — `cookieAuthMiddleware` promotes the
15
+ * session cookie to a Bearer on it, so it belongs to the cookie-authed
16
+ * class. `Access-Control-Allow-Credentials` is NEVER set.
17
+ *
18
+ * LAYER RELATIONSHIP: origin-validation runs first and decides WHETHER a
19
+ * request executes at all (the spec-mandated 403); this layer decides HOW
20
+ * the browser is told it may read the response. A disallowed origin never
21
+ * reaches this layer's headers in production — its rejection branch is
22
+ * defense-in-depth. CORS is never the security boundary.
23
+ */
24
+ import type { RequestHandler } from "express";
25
+ import { type OriginHostPolicy } from "./origin-validation.js";
26
+ export declare function createBrowserCorsMiddleware(opts: {
27
+ readonly policy: OriginHostPolicy;
28
+ }): RequestHandler;
29
+ //# sourceMappingURL=browser-cors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-cors.d.ts","sourceRoot":"","sources":["../src/browser-cors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AA8BnF,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;CACnC,GAAG,cAAc,CA+CjB"}
@@ -0,0 +1,64 @@
1
+ import { validateOriginHost } from "./origin-validation.js";
2
+ /**
3
+ * Response headers the MCP client MUST be able to read.
4
+ *
5
+ * Both are silent-failure modes: the SDK client reads `Mcp-Session-Id`
6
+ * off every response and echoes it on subsequent requests — without
7
+ * exposure the browser hands back `null`, initialize appears to succeed,
8
+ * and every later request fails against a stateful server. It reads
9
+ * `WWW-Authenticate` off 401s to discover the OAuth resource metadata
10
+ * URL; without exposure, discovery silently degrades to guessing
11
+ * default well-known paths.
12
+ */
13
+ const EXPOSE_HEADERS = "Mcp-Session-Id, WWW-Authenticate";
14
+ /**
15
+ * Fallback allow-headers when the preflight omits
16
+ * `Access-Control-Request-Headers`. `Authorization` MUST be named
17
+ * explicitly — per the Fetch spec, `*` does not cover it.
18
+ */
19
+ const DEFAULT_ALLOW_HEADERS = "Authorization, Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Last-Event-ID";
20
+ /** GET and DELETE are permitted even though ggui answers them 405: the
21
+ * SDK client auto-attempts both and tolerates 405 — but only if the
22
+ * browser lets it read the status. */
23
+ const ALLOW_METHODS = "GET, POST, DELETE, OPTIONS";
24
+ const MAX_AGE_SECONDS = "600";
25
+ export function createBrowserCorsMiddleware(opts) {
26
+ const { policy } = opts;
27
+ return (req, res, next) => {
28
+ const origin = typeof req.headers.origin === "string" ? req.headers.origin : undefined;
29
+ // No Origin — a non-browser client. Nothing to negotiate.
30
+ if (origin === undefined || origin.length === 0) {
31
+ if (req.method === "OPTIONS") {
32
+ res.status(204).end();
33
+ return;
34
+ }
35
+ next();
36
+ return;
37
+ }
38
+ // The SAME allowlist that governs validation governs CORS, so an
39
+ // origin can never be CORS-allowed but Origin-rejected.
40
+ const allowed = validateOriginHost(req.headers.host, origin, policy) === null;
41
+ if (allowed) {
42
+ res.setHeader("Access-Control-Allow-Origin", origin);
43
+ // Echoing a specific origin makes the response origin-dependent.
44
+ res.setHeader("Vary", "Origin");
45
+ res.setHeader("Access-Control-Expose-Headers", EXPOSE_HEADERS);
46
+ // NOTE: Access-Control-Allow-Credentials is deliberately never set.
47
+ }
48
+ if (req.method === "OPTIONS") {
49
+ if (allowed) {
50
+ res.setHeader("Access-Control-Allow-Methods", ALLOW_METHODS);
51
+ // Reflect the requested headers so host-injected extras (a
52
+ // request-id, say) do not require a code change here.
53
+ const requested = req.headers["access-control-request-headers"];
54
+ res.setHeader("Access-Control-Allow-Headers", typeof requested === "string" && requested.length > 0 ? requested : DEFAULT_ALLOW_HEADERS);
55
+ res.setHeader("Access-Control-Max-Age", MAX_AGE_SECONDS);
56
+ }
57
+ // A disallowed preflight gets a bare 204 with no CORS headers —
58
+ // the browser fails the request, which is the intended outcome.
59
+ res.status(204).end();
60
+ return;
61
+ }
62
+ next();
63
+ };
64
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"build-mcp.d.ts","sourceRoot":"","sources":["../src/build-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAEL,KAAK,iCAAiC,EACvC,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iCAAiC,CAAC;IAC3D;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IAElE;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC,CAAC;CACtE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,EAChE,UAAU,EAAE,MAAM,cAAc,EAChC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,qBAA0B,GAC/B,SAAS,CAiLX"}
1
+ {"version":3,"file":"build-mcp.d.ts","sourceRoot":"","sources":["../src/build-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAEL,KAAK,iCAAiC,EACvC,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iCAAiC,CAAC;IAC3D;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IAElE;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC,CAAC;CACtE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,EAChE,UAAU,EAAE,MAAM,cAAc,EAChC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,qBAA0B,GAC/B,SAAS,CAqLX"}
package/dist/build-mcp.js CHANGED
@@ -31,8 +31,12 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
31
31
  if (opts.mcpAppsOutbound) {
32
32
  installMcpAppsOutbound(server, {
33
33
  ...(opts.shellHtml !== undefined ? { shellHtml: opts.shellHtml } : {}),
34
+ // Thread the same per-request context accessor + logger the tool
35
+ // path uses (`getContext`, param 3 of `buildMcpServer`) so the
36
+ // per-session resource handler's render-read gate sees the
37
+ // caller (render-read-gate.ts).
34
38
  ...(opts.selfContained !== undefined
35
- ? { selfContained: opts.selfContained }
39
+ ? { selfContained: { ...opts.selfContained, getContext, logger } }
36
40
  : {}),
37
41
  ...(opts.publicBaseUrl !== undefined
38
42
  ? { publicBaseUrl: opts.publicBaseUrl }
@@ -8,12 +8,15 @@ export interface FileSystemCodeStoreOptions {
8
8
  readonly root?: string;
9
9
  }
10
10
  export declare class FileSystemCodeStore implements CodeStore {
11
+ /** Bodies live on disk under the cache root — restart-safe (#457). */
12
+ readonly durability: "durable";
11
13
  private readonly root;
12
14
  constructor(opts?: FileSystemCodeStoreOptions);
13
15
  /** Absolute on-disk path for a given hash. Two-level sharded layout. */
14
16
  private absPath;
15
17
  put(hash: string, code: string): Promise<void>;
16
18
  get(hash: string): Promise<string | null>;
19
+ delete(hash: string): Promise<void>;
17
20
  hashOf(code: string): string;
18
21
  }
19
22
  //# sourceMappingURL=code-store-fs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"code-store-fs.d.ts","sourceRoot":"","sources":["../src/code-store-fs.ts"],"names":[],"mappings":"AAmDA,OAAO,EAEL,KAAK,SAAS,EACf,MAAM,0BAA0B,CAAC;AAElC,+CAA+C;AAC/C,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,mBAAoB,YAAW,SAAS;IACnD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAElB,IAAI,GAAE,0BAA+B;IAIjD,wEAAwE;IACxE,OAAO,CAAC,OAAO;IAMT,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB9C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAU/C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAG7B"}
1
+ {"version":3,"file":"code-store-fs.d.ts","sourceRoot":"","sources":["../src/code-store-fs.ts"],"names":[],"mappings":"AAuDA,OAAO,EAEL,KAAK,SAAS,EACf,MAAM,0BAA0B,CAAC;AAElC,+CAA+C;AAC/C,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,mBAAoB,YAAW,SAAS;IACnD,sEAAsE;IACtE,QAAQ,CAAC,UAAU,EAAG,SAAS,CAAU;IAEzC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAElB,IAAI,GAAE,0BAA+B;IAIjD,wEAAwE;IACxE,OAAO,CAAC,OAAO;IAOT,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB9C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUzC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBzC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAG7B"}
@@ -29,7 +29,10 @@
29
29
  * The store grows unbounded. Operators who care can periodically
30
30
  * `rm -rf ~/.ggui/code-cache/` — every entry is content-addressable
31
31
  * and immutable, so a re-fetch will repopulate from upstream code on
32
- * next render. There is no eviction policy in the seam itself.
32
+ * next render. `delete(hash)` removes one entry (unlink, ENOENT
33
+ * tolerated) for operators who need to act on a single bundle rather
34
+ * than the whole root. There is no eviction policy in the seam itself
35
+ * — nothing here decides what to remove or when.
33
36
  *
34
37
  * ## Permissions
35
38
  *
@@ -39,11 +42,13 @@
39
42
  * tighter perms wrap with their own umask.
40
43
  */
41
44
  import { createHash } from 'node:crypto';
42
- import { access, constants as fsConstants, mkdir, readFile, rename, writeFile, } from 'node:fs/promises';
45
+ import { access, constants as fsConstants, mkdir, readFile, rename, unlink, writeFile, } from 'node:fs/promises';
43
46
  import { homedir } from 'node:os';
44
47
  import { dirname, join } from 'node:path';
45
48
  import { CODE_HASH_REGEX, } from '@ggui-ai/mcp-server-core';
46
49
  export class FileSystemCodeStore {
50
+ /** Bodies live on disk under the cache root — restart-safe (#457). */
51
+ durability = 'durable';
47
52
  root;
48
53
  constructor(opts = {}) {
49
54
  this.root = opts.root ?? join(homedir(), '.ggui', 'code-cache');
@@ -51,7 +56,8 @@ export class FileSystemCodeStore {
51
56
  /** Absolute on-disk path for a given hash. Two-level sharded layout. */
52
57
  absPath(hash) {
53
58
  // Sharding: `<root>/<hash[0..2]>/<hash[2..]>.js`. Validation on
54
- // every put + get below means `hash` is always 64 lowercase hex.
59
+ // every put + get + delete below means `hash` is always 64
60
+ // lowercase hex.
55
61
  return join(this.root, hash.slice(0, 2), `${hash.slice(2)}.js`);
56
62
  }
57
63
  async put(hash, code) {
@@ -83,6 +89,24 @@ export class FileSystemCodeStore {
83
89
  throw err;
84
90
  }
85
91
  }
92
+ async delete(hash) {
93
+ // Validate before deriving a path: an unchecked hash could carry
94
+ // `..` segments and unlink a file outside the cache root. Same
95
+ // narrowing `get` applies, and like `get` a malformed key is not
96
+ // an error — there is simply nothing here under that name.
97
+ if (!CODE_HASH_REGEX.test(hash))
98
+ return;
99
+ try {
100
+ await unlink(this.absPath(hash));
101
+ }
102
+ catch (err) {
103
+ // Absent file = already removed. The shard directory is left in
104
+ // place; empty dirs cost one inode and the next put refills them.
105
+ if (err.code === 'ENOENT')
106
+ return;
107
+ throw err;
108
+ }
109
+ }
86
110
  hashOf(code) {
87
111
  return createHash('sha256').update(code, 'utf-8').digest('hex');
88
112
  }
@@ -8,11 +8,16 @@
8
8
  *
9
9
  * GET /ggui/console/session-resource?session=<sessionId>
10
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.
11
+ * blob (`text` IS the `GGUI_RENDER_SHELL_HTML` constant). On a
12
+ * default mount this matches what a host fetches via MCP
13
+ * `resources/read ui://ggui/render`; a mount opted into
14
+ * `mcpApps.inlineRuntimeShell` (or a custom `shellHtml`) serves
15
+ * a different body on the MCP path while THIS route keeps the
16
+ * thin shell — its first-party srcdoc/WebView consumers load
17
+ * external scripts fine and rely on the pinned CSP hash. NO
18
+ * inlined bootstrap — console fetches the bootstrap separately
19
+ * (meta route below) and replies to the iframe's
20
+ * `ui/initialize` postMessage with it.
16
21
  *
17
22
  * GET /ggui/console/sessions/:sessionId/meta
18
23
  * — slice-envelope JSON (`{ "ai.ggui/render": {...} }`, the same
@@ -1 +1 @@
1
- {"version":3,"file":"console-session-routes.d.ts","sourceRoot":"","sources":["../src/console-session-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAQ5F,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAQ1D,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAI1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,4DAA4D;IAC5D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,4DAA4D;IAC5D,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IACxC,qEAAqE;IACrE,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,KACV;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,uEAAuE;IACvE,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,mEAAmE;IACnE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,4BAA4B,CA0V1F"}
1
+ {"version":3,"file":"console-session-routes.d.ts","sourceRoot":"","sources":["../src/console-session-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAQ5F,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAQ1D,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAI1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,4DAA4D;IAC5D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,4DAA4D;IAC5D,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IACxC,qEAAqE;IACrE,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,KACV;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,uEAAuE;IACvE,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,mEAAmE;IACnE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,4BAA4B,CA0V1F"}
@@ -8,11 +8,16 @@
8
8
  *
9
9
  * GET /ggui/console/session-resource?session=<sessionId>
10
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.
11
+ * blob (`text` IS the `GGUI_RENDER_SHELL_HTML` constant). On a
12
+ * default mount this matches what a host fetches via MCP
13
+ * `resources/read ui://ggui/render`; a mount opted into
14
+ * `mcpApps.inlineRuntimeShell` (or a custom `shellHtml`) serves
15
+ * a different body on the MCP path while THIS route keeps the
16
+ * thin shell — its first-party srcdoc/WebView consumers load
17
+ * external scripts fine and rely on the pinned CSP hash. NO
18
+ * inlined bootstrap — console fetches the bootstrap separately
19
+ * (meta route below) and replies to the iframe's
20
+ * `ui/initialize` postMessage with it.
16
21
  *
17
22
  * GET /ggui/console/sessions/:sessionId/meta
18
23
  * — slice-envelope JSON (`{ "ai.ggui/render": {...} }`, the same
@@ -1 +1 @@
1
- {"version":3,"file":"outbound.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/outbound.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACvB,mBAAmB,EACnB,YAAY,EACb,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,UAAU,EACV,wBAAwB,EAEzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GACxB,iBAAiB,GACjB,mBAAmB,GACnB,mBAAmB,GACnB,wBAAwB,GACxB,aAAa,CAAC;AAElB,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,oEAAoE;IACpE,QAAQ,CAAC,YAAY,EAAE,uBAAuB,CAAC;IAC/C,0DAA0D;IAC1D,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;CAClF;AAED,+DAA+D;AAC/D,MAAM,WAAW,QAAQ;IACvB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjD,8DAA8D;IAC9D,SAAS,CACP,EAAE,EAAE,SAAS,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,GAChC,IAAI,CAAC;IACR;;;;;;;;;;OAUG;IACH,gBAAgB,CACd,EAAE,EAAE,SAAS,EACb,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,GAChC,IAAI,CAAC;IACR,sEAAsE;IACtE,iBAAiB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3E,4EAA4E;IAC5E,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1F,oEAAoE;IACpE,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,iEAAiE;IACjE,YAAY,CAAC,IAAI,EAAE;QACjB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC5B,GAAG,IAAI,CAAC;IACT,sEAAsE;IACtE,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACrE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAsM3D"}
1
+ {"version":3,"file":"outbound.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/outbound.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACvB,mBAAmB,EACnB,YAAY,EACb,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,UAAU,EACV,wBAAwB,EAEzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GACxB,iBAAiB,GACjB,mBAAmB,GACnB,mBAAmB,GACnB,wBAAwB,GACxB,aAAa,CAAC;AAElB,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,oEAAoE;IACpE,QAAQ,CAAC,YAAY,EAAE,uBAAuB,CAAC;IAC/C,0DAA0D;IAC1D,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;CAClF;AAED,+DAA+D;AAC/D,MAAM,WAAW,QAAQ;IACvB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjD,8DAA8D;IAC9D,SAAS,CACP,EAAE,EAAE,SAAS,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,GAChC,IAAI,CAAC;IACR;;;;;;;;;;OAUG;IACH,gBAAgB,CACd,EAAE,EAAE,SAAS,EACb,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,GAChC,IAAI,CAAC;IACR,sEAAsE;IACtE,iBAAiB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3E,4EAA4E;IAC5E,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1F,oEAAoE;IACpE,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,iEAAiE;IACjE,YAAY,CAAC,IAAI,EAAE;QACjB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC5B,GAAG,IAAI,CAAC;IACT,sEAAsE;IACtE,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACrE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAoN3D"}
@@ -158,6 +158,18 @@ export function createOutbound(deps) {
158
158
  for (const sub of deps.wsSubscribers) {
159
159
  if (sub.sessionId !== sessionId)
160
160
  continue;
161
+ // Mirror the in-process pump's replay-dedupe guard
162
+ // (subscriber-lifecycle.ts): a data frame whose per-session seq
163
+ // is <= the subscriber's replay snapshot was already delivered by
164
+ // subscribe-replay (policy 'all'/'latest') or is deliberately
165
+ // invisible to late subscribers (policy 'none') — never re-send.
166
+ // Unstamped frames were never buffered; replay cannot duplicate
167
+ // them, so they pass. Non-data frames carry no seq semantics.
168
+ if (frame.type === "data" &&
169
+ typeof frame.payload.seq === "number" &&
170
+ frame.payload.seq <= sub.replayCompletedSeq) {
171
+ continue;
172
+ }
161
173
  send(sub.ws, frame);
162
174
  }
163
175
  }
@@ -14,6 +14,35 @@ import type { Subscriber } from "./internal-types.js";
14
14
  import type { Outbound } from "./outbound.js";
15
15
  import type { SubscribeHandlers } from "./subscribe.js";
16
16
  import type { SubscriberLifecycle } from "./subscriber-lifecycle.js";
17
+ /**
18
+ * Monotonic counters for pre-subscribe cap-driven closures/refusals.
19
+ * The channel server owns the object (so its health getter can read a
20
+ * live snapshot) and hands the same reference to the router, which
21
+ * increments a field on every enforced cap. See {@link PreSubscribeCaps}.
22
+ */
23
+ export interface PreSubscribeRejectionCounters {
24
+ /** Oversized pre-subscribe frames rejected (1009). */
25
+ payload: number;
26
+ /** Pre-subscribe sockets closed for exceeding the idle window (1008). */
27
+ idle: number;
28
+ /** Upgrades refused for exceeding the pre-subscribe connection ceiling (1013). */
29
+ connection: number;
30
+ }
31
+ /**
32
+ * Resolved pre-subscribe cap thresholds (ggui#444). Each bounds only
33
+ * what a NOT-YET-SUBSCRIBED (unauthenticated / unregistered) socket can
34
+ * consume — a socket that has completed a valid `subscribe` is a
35
+ * legitimate long-lived subscriber and is exempt from all three. A
36
+ * value of `0` disables that cap.
37
+ */
38
+ export interface PreSubscribeCaps {
39
+ /** Per-frame byte ceiling on pre-subscribe frames. 0 = disabled. */
40
+ readonly maxPayloadBytes: number;
41
+ /** Idle window (ms) a socket has to complete a valid subscribe. 0 = disabled. */
42
+ readonly idleMs: number;
43
+ /** Concurrent pending (pre-subscribe) socket ceiling. 0 = disabled. */
44
+ readonly maxConnections: number;
45
+ }
17
46
  export interface SocketRouterDeps {
18
47
  readonly logger: Logger;
19
48
  /** GguiSession backing store — observation-message patches persist here. */
@@ -27,6 +56,10 @@ export interface SocketRouterDeps {
27
56
  readonly handleInboundAction: ActionIngress["handleInboundAction"];
28
57
  readonly handleChannelSubscribe: ChannelSubscriptions["handleChannelSubscribe"];
29
58
  readonly handleChannelUnsubscribe: ChannelSubscriptions["handleChannelUnsubscribe"];
59
+ /** Resolved pre-subscribe cap thresholds (ggui#444). */
60
+ readonly preSubscribeCaps: PreSubscribeCaps;
61
+ /** Shared monotonic counters incremented on each enforced cap. */
62
+ readonly preSubscribeRejections: PreSubscribeRejectionCounters;
30
63
  }
31
64
  /**
32
65
  * Wire the channel's `connection` handling onto `wss`. Reads the
@@ -1 +1 @@
1
- {"version":3,"file":"socket-router.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/socket-router.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAoB,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAInF,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,yEAAyE;IACzE,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACzD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC1C,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;IACvD,QAAQ,CAAC,eAAe,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IAC/D,QAAQ,CAAC,mBAAmB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IACnE,QAAQ,CAAC,sBAAsB,EAAE,oBAAoB,CAAC,wBAAwB,CAAC,CAAC;IAChF,QAAQ,CAAC,wBAAwB,EAAE,oBAAoB,CAAC,0BAA0B,CAAC,CAAC;CACrF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAkQrF"}
1
+ {"version":3,"file":"socket-router.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/socket-router.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAoB,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAInF,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE;;;;;GAKG;AACH,MAAM,WAAW,6BAA6B;IAC5C,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,yEAAyE;IACzE,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACzD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC1C,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;IACvD,QAAQ,CAAC,eAAe,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IAC/D,QAAQ,CAAC,mBAAmB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IACnE,QAAQ,CAAC,sBAAsB,EAAE,oBAAoB,CAAC,wBAAwB,CAAC,CAAC;IAChF,QAAQ,CAAC,wBAAwB,EAAE,oBAAoB,CAAC,0BAA0B,CAAC,CAAC;IACpF,wDAAwD;IACxD,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C,kEAAkE;IAClE,QAAQ,CAAC,sBAAsB,EAAE,6BAA6B,CAAC;CAChE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAmarF"}
@@ -12,6 +12,65 @@
12
12
  * frame processing per socket.
13
13
  */
14
14
  export function attachSocketRouter(wss, deps) {
15
+ const { preSubscribeCaps: caps, preSubscribeRejections: rejections } = deps;
16
+ // --- Pre-subscribe cap state (ggui#444) -------------------------------
17
+ //
18
+ // These bound what a socket can consume BEFORE it completes a valid
19
+ // `subscribe`. The distinguishing signal is `subscribersByWs.get(ws)`:
20
+ // unset = pre-subscribe (bounded), set = registered subscriber
21
+ // (exempt). All three caps target the pre-subscribe window only.
22
+ /** Live count of PENDING (pre-subscribe) sockets — never counts subscribers. */
23
+ let preSubscribeCount = 0;
24
+ /** Sockets currently counted against {@link preSubscribeCount}. */
25
+ const countedPending = new WeakSet();
26
+ /** Per-socket idle-timeout handle, cleared on subscribe or close. */
27
+ const idleTimers = new WeakMap();
28
+ /**
29
+ * Sockets already closed for the pre-subscribe payload cap. A single
30
+ * abusive burst can deliver several oversized frames in one read
31
+ * before `ws.close()` settles the connection (each already-parsed
32
+ * frame still fires its own `message` event); without this guard
33
+ * every one of them would re-run the payload branch and re-increment
34
+ * `rejections.payload` for what is really one abusive socket. Holds
35
+ * no resource of its own, so it needs no explicit cleanup — the
36
+ * entry is collected once the socket is.
37
+ */
38
+ const payloadRejected = new WeakSet();
39
+ /**
40
+ * Deduped cap-hit log keys. Capped so a flood cannot grow the set
41
+ * without bound; after the cap, rejections still enforce + count —
42
+ * they just stop logging. Mirrors the origin-rejection warn pattern.
43
+ */
44
+ const warnedCaps = new Set();
45
+ function warnCapOnce(event, fields) {
46
+ if (!warnedCaps.has(event) && warnedCaps.size < 100) {
47
+ warnedCaps.add(event);
48
+ deps.logger.warn(event, fields);
49
+ }
50
+ }
51
+ /** Drop a socket out of the pending count exactly once. */
52
+ function releasePending(ws) {
53
+ if (countedPending.has(ws)) {
54
+ countedPending.delete(ws);
55
+ preSubscribeCount -= 1;
56
+ }
57
+ }
58
+ function clearIdleTimer(ws) {
59
+ const timer = idleTimers.get(ws);
60
+ if (timer !== undefined) {
61
+ clearTimeout(timer);
62
+ idleTimers.delete(ws);
63
+ }
64
+ }
65
+ /**
66
+ * A socket that has become a registered subscriber is a legitimate
67
+ * long-lived connection: disarm its idle timer and stop counting it
68
+ * against the pre-subscribe ceiling. Idempotent.
69
+ */
70
+ function markSubscribed(ws) {
71
+ clearIdleTimer(ws);
72
+ releasePending(ws);
73
+ }
15
74
  /**
16
75
  * Tenancy guard for client-emitted observation messages
17
76
  * (`host_context_observed` today). Returns `false`
@@ -59,6 +118,37 @@ export function attachSocketRouter(wss, deps) {
59
118
  }
60
119
  async function onMessage(ws, raw) {
61
120
  const sub = deps.subscribersByWs.get(ws);
121
+ // Pre-subscribe per-frame payload cap. Bounds what a not-yet-
122
+ // subscribed (unauthenticated / unregistered) socket can buffer per
123
+ // frame. Subscribed sockets are exempt — their `action` frames have
124
+ // no protocol-level max size and are governed only by the coarse,
125
+ // global ws-level `maxPayload` memory backstop (see
126
+ // DEFAULT_WS_MAX_PAYLOAD_BYTES for its sizing rationale), never this
127
+ // tight handshake-sized ceiling. Measured before JSON.parse so a
128
+ // giant frame is dropped without being interpreted.
129
+ if (sub === undefined && caps.maxPayloadBytes > 0) {
130
+ // Already closed this socket for this cap: a same-segment burst
131
+ // can still deliver more already-parsed frames before
132
+ // `ws.close()` settles the connection. Drop them silently — one
133
+ // abusive socket is one rejection, not one per queued frame.
134
+ if (payloadRejected.has(ws)) {
135
+ return;
136
+ }
137
+ if (Buffer.byteLength(raw, "utf8") > caps.maxPayloadBytes) {
138
+ payloadRejected.add(ws);
139
+ rejections.payload += 1;
140
+ warnCapOnce("render_channel_pre_subscribe_payload_rejected", {
141
+ limitBytes: caps.maxPayloadBytes,
142
+ });
143
+ try {
144
+ ws.close(1009, "pre_subscribe_payload_exceeded");
145
+ }
146
+ catch {
147
+ /* best-effort: socket may already be closing */
148
+ }
149
+ return;
150
+ }
151
+ }
62
152
  let message;
63
153
  try {
64
154
  message = JSON.parse(raw);
@@ -103,6 +193,14 @@ export function attachSocketRouter(wss, deps) {
103
193
  await deps.handleSubscribe(ws, identity, message, cookieBound);
104
194
  pendingIdentity.delete(ws);
105
195
  pendingCookieBinding.delete(ws);
196
+ // A successful subscribe registers the subscriber (sets
197
+ // `subscribersByWs`). Only then does the socket graduate out of
198
+ // the pre-subscribe window: disarm its idle timer + free its
199
+ // ceiling slot. A REJECTED subscribe leaves it pending, so it
200
+ // stays bounded (and reapable) exactly as before.
201
+ if (deps.subscribersByWs.get(ws) !== undefined) {
202
+ markSubscribed(ws);
203
+ }
106
204
  return;
107
205
  }
108
206
  case "ping":
@@ -170,6 +268,58 @@ export function attachSocketRouter(wss, deps) {
170
268
  */
171
269
  const pendingCookieBinding = new WeakMap();
172
270
  wss.on("connection", (ws, req) => {
271
+ // Pre-subscribe connection ceiling. Count only sockets that have
272
+ // NOT yet completed a valid subscribe — legitimate subscriber
273
+ // fan-out can be large and is never bounded here. Beyond the
274
+ // ceiling the socket is cleanly closed (1013 "try again later")
275
+ // without wiring any per-socket handlers or consuming a slot.
276
+ if (caps.maxConnections > 0 && preSubscribeCount >= caps.maxConnections) {
277
+ rejections.connection += 1;
278
+ warnCapOnce("render_channel_pre_subscribe_connection_rejected", {
279
+ limit: caps.maxConnections,
280
+ });
281
+ // This early-return path skips the normal per-socket wiring below,
282
+ // so attach a minimal `error` listener first: a `ws` is an
283
+ // EventEmitter and an unhandled `error` (a refused peer resetting
284
+ // mid-close is the likely case here) would otherwise crash the
285
+ // process — the abuse path must never take the server down.
286
+ ws.on("error", (err) => {
287
+ deps.logger.warn("render_channel_socket_error", { error: String(err) });
288
+ });
289
+ try {
290
+ ws.close(1013, "pre_subscribe_connection_ceiling");
291
+ }
292
+ catch {
293
+ /* best-effort: socket may already be closing */
294
+ }
295
+ return;
296
+ }
297
+ preSubscribeCount += 1;
298
+ countedPending.add(ws);
299
+ // Pre-subscribe idle timeout. A socket that never completes a valid
300
+ // subscribe within the window is closed (1008). Armed here, cleared
301
+ // in `markSubscribed` on a successful subscribe, so a subscribed
302
+ // (long-idle / reconnecting) viewer is never reaped. `.unref()` so a
303
+ // pending timer never keeps the process alive on its own.
304
+ if (caps.idleMs > 0) {
305
+ const timer = setTimeout(() => {
306
+ idleTimers.delete(ws);
307
+ if (deps.subscribersByWs.get(ws) === undefined) {
308
+ rejections.idle += 1;
309
+ warnCapOnce("render_channel_pre_subscribe_idle_closed", {
310
+ idleMs: caps.idleMs,
311
+ });
312
+ try {
313
+ ws.close(1008, "pre_subscribe_idle_timeout");
314
+ }
315
+ catch {
316
+ /* best-effort: socket may already be closing */
317
+ }
318
+ }
319
+ }, caps.idleMs);
320
+ timer.unref?.();
321
+ idleTimers.set(ws, timer);
322
+ }
173
323
  // Bind the resolved identity from the upgrade phase. It was
174
324
  // attached to the request object in handleUpgrade.
175
325
  const identity = req.__gguiIdentity;
@@ -205,6 +355,11 @@ export function attachSocketRouter(wss, deps) {
205
355
  ws.on("close", () => {
206
356
  deps.unregister(ws);
207
357
  pendingIdentity.delete(ws);
358
+ // A pending socket that closes before subscribing frees its idle
359
+ // timer + ceiling slot. For a socket that already subscribed,
360
+ // `markSubscribed` cleared both — these are then no-ops.
361
+ clearIdleTimer(ws);
362
+ releasePending(ws);
208
363
  });
209
364
  ws.on("error", (err) => {
210
365
  deps.logger.warn("render_channel_socket_error", { error: String(err) });
@@ -1 +1 @@
1
- {"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/subscribe.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/subscribe.ts"],"names":[],"mappings":"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"}