@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
@@ -8,6 +8,20 @@
8
8
  */
9
9
  import { PROTOCOL_SCHEMA_VERSION, UPGRADE_REQUIRED } from "@ggui-ai/protocol";
10
10
  import { defaultAppIdFromIdentity, resolveIdentityFromHeaders, UnauthenticatedError, } from "../auth.js";
11
+ /**
12
+ * Marker `userId` (and `workspaceId`) `resolveIdentityFromUpgrade`
13
+ * returns for a bootstrap-gated upgrade — see the "WS-token gate"
14
+ * comment there. It is a placeholder, not a verified identity:
15
+ * `handleSubscribe` MUST reject a subscribe on this identity outright
16
+ * unless the subscribe payload itself presents a `wsToken` that goes on
17
+ * to verify (ggui#438a security review, C1). Never treat this marker as
18
+ * authenticated for identity-default resolution or render provisioning.
19
+ */
20
+ const BOOTSTRAP_PENDING_MARKER = "__bootstrap_pending__";
21
+ /** Is `identity` the unverified upgrade-time placeholder above? */
22
+ function isBootstrapPendingIdentity(identity) {
23
+ return identity.identity.kind === "user" && identity.identity.userId === BOOTSTRAP_PENDING_MARKER;
24
+ }
11
25
  export function createSubscribeHandlers(deps) {
12
26
  async function resolveIdentityFromUpgrade(req) {
13
27
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -27,8 +41,8 @@ export function createSubscribeHandlers(deps) {
27
41
  return {
28
42
  identity: {
29
43
  kind: "user",
30
- userId: "__bootstrap_pending__",
31
- workspaceId: "__bootstrap_pending__",
44
+ userId: BOOTSTRAP_PENDING_MARKER,
45
+ workspaceId: BOOTSTRAP_PENDING_MARKER,
32
46
  roles: [],
33
47
  },
34
48
  source: "apikey",
@@ -127,6 +141,24 @@ export function createSubscribeHandlers(deps) {
127
141
  }
128
142
  return;
129
143
  }
144
+ // C1 (ggui#438a security review): `identity` may be the
145
+ // upgrade-time bootstrap-pending PLACEHOLDER (see
146
+ // `resolveIdentityFromUpgrade`'s "WS-token gate" comment and
147
+ // `isBootstrapPendingIdentity` above) — a "don't reject the
148
+ // upgrade" signal, not a verified identity. If the subscribe
149
+ // payload does not itself carry a `wsToken` to verify, this socket
150
+ // has proven nothing about itself and MUST be rejected here —
151
+ // never fall through to identity-default appId resolution or
152
+ // dev-mode render provisioning on an unverified placeholder. A
153
+ // present `payload.wsToken` is verified by the block immediately
154
+ // below, which returns on any verify failure — so by the time this
155
+ // function reaches appId resolution, every bootstrap-pending
156
+ // identity has either been rejected or replaced by a verified one.
157
+ if (isBootstrapPendingIdentity(identity) &&
158
+ !(typeof payload.wsToken === "string" && payload.wsToken.length > 0)) {
159
+ deps.sendError(ws, "UNAUTHENTICATED", "Bootstrap-gated socket did not present a wsToken to verify at subscribe", message.requestId);
160
+ return;
161
+ }
130
162
  // WS-token-auth path. When `payload.wsToken` is present, the MCP
131
163
  // Apps iframe is asking us to authenticate it via the short-lived
132
164
  // token minted by `ggui_render`. This REPLACES the upgrade-time
@@ -226,6 +258,15 @@ export function createSubscribeHandlers(deps) {
226
258
  }
227
259
  else {
228
260
  try {
261
+ // NO `userId` here, deliberately (#446). `effectiveIdentity`
262
+ // above may be the bootstrap-synthesized identity whose
263
+ // `userId` IS the sessionId — a render-scoped credential every
264
+ // holder of the bootstrap token presents, not a person.
265
+ // Threading it onto the row would bind the row's SUBJECT to
266
+ // that credential, and the render-read gate would then pass
267
+ // every bearer of the token as "the subject". The row acquires
268
+ // its real subject later, from the agent's commit, via the
269
+ // stores' if-not-exists semantics.
229
270
  stored = await deps.renderStore.create({
230
271
  id: payload.sessionId,
231
272
  appId: effectiveAppId,
@@ -60,6 +60,54 @@ import { type GguiSessionChannelBootstrap, type GguiSessionChannelCookieAuth } f
60
60
  import type { Logger } from "./logger.js";
61
61
  /** Default URL path for the channel endpoint. Operators can override. */
62
62
  export declare const DEFAULT_RENDER_CHANNEL_PATH = "/ws";
63
+ /**
64
+ * Coarse ws-level `maxPayload` memory backstop (1 MiB) applied to EVERY
65
+ * inbound frame on EVERY socket, INCLUDING already-subscribed ones —
66
+ * unlike the other three pre-subscribe caps in this file, this one is
67
+ * NOT subscriber-exempt. It bounds the absolute per-frame buffer a
68
+ * hostile peer can force before the frame is even assembled: a 100x
69
+ * reduction from the `ws` library's own ~100 MiB default, applied
70
+ * uniformly regardless of subscribe state.
71
+ *
72
+ * `ActionEnvelope` (the post-subscribe `action` payload) has no
73
+ * protocol-level max size, so this ceiling can NOT be sized to provably
74
+ * never clip a legitimate frame — it is a deliberately generous-but-
75
+ * finite backstop, not a guarantee. The closest documented protocol-
76
+ * level payload bound is `CONTEXT_SNAPSHOT_MAX_BYTES` (64 KiB, the
77
+ * `ggui_runtime_sync_context` MCP tool call — a related but distinct
78
+ * transport from this WS channel), which puts 1 MiB at roughly 16x
79
+ * headroom over the largest structured payload the protocol formally
80
+ * bounds. Operators whose legitimate `action` frames exceed 1 MiB
81
+ * (e.g. large form submissions) should raise {@link maxPayloadBytes},
82
+ * or set it to `0` to fall back to the `ws` library default. Distinct from
83
+ * the tight, pre-subscribe-only, subscriber-exempt
84
+ * {@link DEFAULT_PRE_SUBSCRIBE_MAX_PAYLOAD_BYTES}. Disable this one
85
+ * with `0`.
86
+ */
87
+ export declare const DEFAULT_WS_MAX_PAYLOAD_BYTES = 1048576;
88
+ /**
89
+ * Per-frame byte ceiling on PRE-SUBSCRIBE frames (64 KiB). Pre-subscribe
90
+ * traffic is tiny (a subscribe carrying a JWT is under 1 KiB), so this
91
+ * is ~70x headroom yet caps a pre-auth memory-abuse frame. Enforced
92
+ * per-frame, keyed on "not yet a subscriber", so post-subscribe frames
93
+ * are exempt. Disable with `0`.
94
+ */
95
+ export declare const DEFAULT_PRE_SUBSCRIBE_MAX_PAYLOAD_BYTES = 65536;
96
+ /**
97
+ * Grace window (30 s) an unauthenticated socket has to complete a valid
98
+ * subscribe before it is closed. Open→subscribe is normally sub-second;
99
+ * 30 s tolerates a slow bootstrap-token verify while bounding how long a
100
+ * credential-less socket holds a slot. Disable with `0`.
101
+ */
102
+ export declare const DEFAULT_PRE_SUBSCRIBE_IDLE_MS = 30000;
103
+ /**
104
+ * Concurrent PENDING (pre-subscribe) socket ceiling (1024). Honest
105
+ * clients leave the pending state in under a second, so 1024 in-flight
106
+ * handshakes is far beyond any real burst yet bounds a credential-less
107
+ * connection flood. Counts pending sockets only — never subscribers.
108
+ * Disable with `0`.
109
+ */
110
+ export declare const DEFAULT_MAX_PRE_SUBSCRIBE_CONNECTIONS = 1024;
63
111
  export type { GguiSessionChannelLocalToolsOptions } from "./ggui-session-channel/channel-subscriptions.js";
64
112
  export type { GguiSessionChannelBootstrap, GguiSessionChannelBootstrapRefreshResult, GguiSessionChannelBootstrapVerifyResult, GguiSessionChannelCookieAuth, } from "./ggui-session-channel/subscribe.js";
65
113
  export interface GguiSessionChannelOptions {
@@ -228,6 +276,58 @@ export interface GguiSessionChannelOptions {
228
276
  * identically in both modes.
229
277
  */
230
278
  readonly versionPolicy?: "advisory" | "reject";
279
+ /**
280
+ * Coarse ws-level `maxPayload` memory backstop (bytes) applied to
281
+ * EVERY inbound frame on EVERY socket, INCLUDING already-subscribed
282
+ * ones — the largest single frame the server will assemble before
283
+ * rejecting it (ws closes 1009). Distinct from
284
+ * {@link maxPreSubscribePayloadBytes}: that one is the tight,
285
+ * subscriber-exempt, credential-less bound; this one is a generous-
286
+ * but-finite global ceiling applied uniformly regardless of subscribe
287
+ * state.
288
+ *
289
+ * `ActionEnvelope` (the post-subscribe `action` payload) has no
290
+ * protocol-level max size, so this default can NOT be sized to
291
+ * provably never clip a legitimate frame — see
292
+ * {@link DEFAULT_WS_MAX_PAYLOAD_BYTES} for the sizing rationale.
293
+ * Operators expecting `action` frames larger than the default should
294
+ * raise this value.
295
+ *
296
+ * Defaults to {@link DEFAULT_WS_MAX_PAYLOAD_BYTES} (1 MiB). `0`
297
+ * disables the ws-level check (falls back to the ws library's own
298
+ * ~100 MiB default).
299
+ */
300
+ readonly maxPayloadBytes?: number;
301
+ /**
302
+ * Per-frame byte ceiling for PRE-SUBSCRIBE (not-yet-registered)
303
+ * sockets. A pre-subscribe frame larger than this closes the socket
304
+ * (1009) before it is parsed. Subscribed sockets are exempt — only
305
+ * the coarse {@link maxPayloadBytes} backstop applies to them.
306
+ *
307
+ * Defaults to {@link DEFAULT_PRE_SUBSCRIBE_MAX_PAYLOAD_BYTES}
308
+ * (64 KiB). `0` disables the pre-subscribe payload cap.
309
+ */
310
+ readonly maxPreSubscribePayloadBytes?: number;
311
+ /**
312
+ * Grace window (ms) a socket has to complete a valid subscribe before
313
+ * it is closed (1008). Armed on connection, cleared on successful
314
+ * subscribe — a registered subscriber is never reaped, so long-idle
315
+ * and reconnecting viewers keep the existing keepalive behavior.
316
+ *
317
+ * Defaults to {@link DEFAULT_PRE_SUBSCRIBE_IDLE_MS} (30 s). `0`
318
+ * disables the idle timeout.
319
+ */
320
+ readonly preSubscribeIdleMs?: number;
321
+ /**
322
+ * Concurrent PENDING (pre-subscribe) socket ceiling. A new upgrade
323
+ * that would exceed it is cleanly closed (1013). Counts only sockets
324
+ * that have not completed a valid subscribe — subscriber fan-out is
325
+ * never bounded here.
326
+ *
327
+ * Defaults to {@link DEFAULT_MAX_PRE_SUBSCRIBE_CONNECTIONS} (1024).
328
+ * `0` disables the ceiling.
329
+ */
330
+ readonly maxPreSubscribeConnections?: number;
231
331
  /**
232
332
  * Optional hook fired synchronously when the local subscriber count
233
333
  * for `sessionId` transitions 0 → 1 (the first subscriber for that
@@ -412,6 +512,20 @@ export interface GguiSessionChannelServer {
412
512
  readonly subscriberCount: number;
413
513
  /** Number of distinct renders with at least one subscriber. */
414
514
  readonly renderCount: number;
515
+ /**
516
+ * Monotonic counts of pre-subscribe cap-driven closures/refusals
517
+ * (ggui#444), surfaced on `/ggui/health` for operator diagnosis of a
518
+ * credential-less abuse burst. Each field only ever increases over
519
+ * the channel's lifetime:
520
+ * - `payload` — oversized pre-subscribe frames rejected (1009).
521
+ * - `idle` — sockets closed for never subscribing in time (1008).
522
+ * - `connection` — upgrades refused past the pending ceiling (1013).
523
+ */
524
+ readonly preSubscribeRejections: {
525
+ readonly payload: number;
526
+ readonly idle: number;
527
+ readonly connection: number;
528
+ };
415
529
  /**
416
530
  * Close every live subscriber + the underlying ws server. Idempotent.
417
531
  */
@@ -1 +1 @@
1
- {"version":3,"file":"ggui-session-channel.d.ts","sourceRoot":"","sources":["../src/ggui-session-channel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ggui-session-channel.d.ts","sourceRoot":"","sources":["../src/ggui-session-channel.ts"],"names":[],"mappings":"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"}
@@ -61,6 +61,54 @@ import { createSubscribeHandlers, } from "./ggui-session-channel/subscribe.js";
61
61
  import { createSubscriberLifecycle } from "./ggui-session-channel/subscriber-lifecycle.js";
62
62
  /** Default URL path for the channel endpoint. Operators can override. */
63
63
  export const DEFAULT_RENDER_CHANNEL_PATH = "/ws";
64
+ /**
65
+ * Coarse ws-level `maxPayload` memory backstop (1 MiB) applied to EVERY
66
+ * inbound frame on EVERY socket, INCLUDING already-subscribed ones —
67
+ * unlike the other three pre-subscribe caps in this file, this one is
68
+ * NOT subscriber-exempt. It bounds the absolute per-frame buffer a
69
+ * hostile peer can force before the frame is even assembled: a 100x
70
+ * reduction from the `ws` library's own ~100 MiB default, applied
71
+ * uniformly regardless of subscribe state.
72
+ *
73
+ * `ActionEnvelope` (the post-subscribe `action` payload) has no
74
+ * protocol-level max size, so this ceiling can NOT be sized to provably
75
+ * never clip a legitimate frame — it is a deliberately generous-but-
76
+ * finite backstop, not a guarantee. The closest documented protocol-
77
+ * level payload bound is `CONTEXT_SNAPSHOT_MAX_BYTES` (64 KiB, the
78
+ * `ggui_runtime_sync_context` MCP tool call — a related but distinct
79
+ * transport from this WS channel), which puts 1 MiB at roughly 16x
80
+ * headroom over the largest structured payload the protocol formally
81
+ * bounds. Operators whose legitimate `action` frames exceed 1 MiB
82
+ * (e.g. large form submissions) should raise {@link maxPayloadBytes},
83
+ * or set it to `0` to fall back to the `ws` library default. Distinct from
84
+ * the tight, pre-subscribe-only, subscriber-exempt
85
+ * {@link DEFAULT_PRE_SUBSCRIBE_MAX_PAYLOAD_BYTES}. Disable this one
86
+ * with `0`.
87
+ */
88
+ export const DEFAULT_WS_MAX_PAYLOAD_BYTES = 1_048_576;
89
+ /**
90
+ * Per-frame byte ceiling on PRE-SUBSCRIBE frames (64 KiB). Pre-subscribe
91
+ * traffic is tiny (a subscribe carrying a JWT is under 1 KiB), so this
92
+ * is ~70x headroom yet caps a pre-auth memory-abuse frame. Enforced
93
+ * per-frame, keyed on "not yet a subscriber", so post-subscribe frames
94
+ * are exempt. Disable with `0`.
95
+ */
96
+ export const DEFAULT_PRE_SUBSCRIBE_MAX_PAYLOAD_BYTES = 65_536;
97
+ /**
98
+ * Grace window (30 s) an unauthenticated socket has to complete a valid
99
+ * subscribe before it is closed. Open→subscribe is normally sub-second;
100
+ * 30 s tolerates a slow bootstrap-token verify while bounding how long a
101
+ * credential-less socket holds a slot. Disable with `0`.
102
+ */
103
+ export const DEFAULT_PRE_SUBSCRIBE_IDLE_MS = 30_000;
104
+ /**
105
+ * Concurrent PENDING (pre-subscribe) socket ceiling (1024). Honest
106
+ * clients leave the pending state in under a second, so 1024 in-flight
107
+ * handshakes is far beyond any real burst yet bounds a credential-less
108
+ * connection flood. Counts pending sockets only — never subscribers.
109
+ * Disable with `0`.
110
+ */
111
+ export const DEFAULT_MAX_PRE_SUBSCRIBE_CONNECTIONS = 1_024;
64
112
  /**
65
113
  * Build an OSS live-channel server. The returned object is designed to be
66
114
  * composed into `createGguiServer` — see `server.ts` for the wire-up.
@@ -73,9 +121,25 @@ export function createGguiSessionChannelServer(opts) {
73
121
  // Live-tail pub/sub. Default in-process; multi-process deployments
74
122
  // bind a pubsub-backed StreamFanout via `opts.streamFanout`.
75
123
  const streamFanout = opts.streamFanout ?? new InProcessStreamFanout();
124
+ // Pre-subscribe caps (ggui#444) — bound what a not-yet-subscribed
125
+ // (credential-less) socket can consume. Resolved once here; `0`
126
+ // disables a given cap. See the DEFAULT_* constants for rationale.
127
+ const wsMaxPayloadBytes = opts.maxPayloadBytes ?? DEFAULT_WS_MAX_PAYLOAD_BYTES;
128
+ const preSubscribeCaps = {
129
+ maxPayloadBytes: opts.maxPreSubscribePayloadBytes ?? DEFAULT_PRE_SUBSCRIBE_MAX_PAYLOAD_BYTES,
130
+ idleMs: opts.preSubscribeIdleMs ?? DEFAULT_PRE_SUBSCRIBE_IDLE_MS,
131
+ maxConnections: opts.maxPreSubscribeConnections ?? DEFAULT_MAX_PRE_SUBSCRIBE_CONNECTIONS,
132
+ };
133
+ // Live monotonic counters surfaced on `/ggui/health`. The router owns
134
+ // the increments; the health getter below reads a snapshot.
135
+ const preSubscribeRejections = { payload: 0, idle: 0, connection: 0 };
76
136
  // `noServer: true` means we own the upgrade wiring (see handleUpgrade);
77
- // ws won't try to bind its own port.
78
- const wss = new WebSocketServer({ noServer: true });
137
+ // ws won't try to bind its own port. `maxPayload` is the coarse global
138
+ // memory backstop; the tight pre-subscribe payload cap is enforced
139
+ // per-frame in the socket router. `0` → fall back to the ws default.
140
+ const wss = wsMaxPayloadBytes > 0
141
+ ? new WebSocketServer({ noServer: true, maxPayload: wsMaxPayloadBytes })
142
+ : new WebSocketServer({ noServer: true });
79
143
  /**
80
144
  * Flat set of all live WS subscribers. Replaces the per-render
81
145
  * `subscribersByRender` Map — routing is now StreamFanout's job;
@@ -162,6 +226,8 @@ export function createGguiSessionChannelServer(opts) {
162
226
  handleInboundAction,
163
227
  handleChannelSubscribe,
164
228
  handleChannelUnsubscribe,
229
+ preSubscribeCaps,
230
+ preSubscribeRejections,
165
231
  });
166
232
  return {
167
233
  path,
@@ -218,6 +284,15 @@ export function createGguiSessionChannelServer(opts) {
218
284
  renders.add(sub.sessionId);
219
285
  return renders.size;
220
286
  },
287
+ get preSubscribeRejections() {
288
+ // Snapshot so callers can't mutate the live counters through the
289
+ // getter's return value.
290
+ return {
291
+ payload: preSubscribeRejections.payload,
292
+ idle: preSubscribeRejections.idle,
293
+ connection: preSubscribeRejections.connection,
294
+ };
295
+ },
221
296
  async close() {
222
297
  // Close every open socket + drain its StreamFanout subscription.
223
298
  // `wss.close` terminates the server but not in-flight sockets,
@@ -66,6 +66,9 @@ interface MountOptions {
66
66
  readonly oauthIssuerUrl?: string;
67
67
  /** Structured logger for unexpected auth-check failures. */
68
68
  readonly logger: Logger;
69
+ /** Effective browser-origin allowlist, for operator diagnosis of CORS
70
+ * failures — which are otherwise invisible server-side. */
71
+ readonly corsOrigins?: ReadonlyArray<string>;
69
72
  }
70
73
  /**
71
74
  * Mount `/ggui/live` + `/ggui/health` + `/ggui/auth-check` onto the
@@ -1 +1 @@
1
- {"version":3,"file":"health-routes.d.ts","sourceRoot":"","sources":["../src/health-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAE1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAU1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC;QACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;KAClD,CAAC,CAAC;IACH;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,wBAAwB,GAAG,IAAI,CAAC;IAC3D;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,GAAG,WAAW,CAAA;KAAE,CAAC;IACrE,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAmH1D"}
1
+ {"version":3,"file":"health-routes.d.ts","sourceRoot":"","sources":["../src/health-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAE1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAU1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC;QACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;KAClD,CAAC,CAAC;IACH;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,wBAAwB,GAAG,IAAI,CAAC;IAC3D;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,GAAG,WAAW,CAAA;KAAE,CAAC;IACrE,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;gEAC4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC9C;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAuI1D"}
@@ -40,7 +40,7 @@ const READINESS_CHECK_TIMEOUT_MS = 1_000;
40
40
  * express app. Returns nothing — the routes self-register.
41
41
  */
42
42
  export function mountHealthRoutes(opts) {
43
- const { app, info, toolCount, readinessChecks, getChannel, threads, auth, oauthEnabled, logger } = opts;
43
+ const { app, info, toolCount, readinessChecks, getChannel, threads, auth, oauthEnabled, logger, corsOrigins, } = opts;
44
44
  async function runReadinessChecks() {
45
45
  if (readinessChecks.length === 0) {
46
46
  return { allReady: true, results: {} };
@@ -86,8 +86,18 @@ export function mountHealthRoutes(opts) {
86
86
  path: channel.path,
87
87
  subscribers: channel.subscriberCount,
88
88
  renders: channel.renderCount,
89
+ // Pre-subscribe cap-hit counters (ggui#444) — operator
90
+ // signal for a credential-less abuse burst. Monotonic; all
91
+ // zero on a healthy server.
92
+ caps: {
93
+ preSubscribeRejections: channel.preSubscribeRejections,
94
+ },
89
95
  };
90
96
  }
97
+ body.cors = {
98
+ loopback: true,
99
+ origins: corsOrigins === undefined ? [] : [...corsOrigins],
100
+ };
91
101
  // Thread-transport presence + durability claim. When present,
92
102
  // `durability` is exactly what the caller declared — 'ephemeral'
93
103
  // by default so Portal does not silently hide its non-durable