@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.
- package/dist/api-renders-routes.d.ts.map +1 -1
- package/dist/api-renders-routes.js +9 -1
- package/dist/browser-cors.d.ts +29 -0
- package/dist/browser-cors.d.ts.map +1 -0
- package/dist/browser-cors.js +64 -0
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +5 -1
- package/dist/code-store-fs.d.ts +3 -0
- package/dist/code-store-fs.d.ts.map +1 -1
- package/dist/code-store-fs.js +27 -3
- package/dist/console-session-routes.d.ts +10 -5
- package/dist/console-session-routes.d.ts.map +1 -1
- package/dist/console-session-routes.js +10 -5
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.js +12 -0
- package/dist/ggui-session-channel/socket-router.d.ts +33 -0
- package/dist/ggui-session-channel/socket-router.d.ts.map +1 -1
- package/dist/ggui-session-channel/socket-router.js +155 -0
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscribe.js +43 -2
- package/dist/ggui-session-channel.d.ts +114 -0
- package/dist/ggui-session-channel.d.ts.map +1 -1
- package/dist/ggui-session-channel.js +77 -2
- package/dist/health-routes.d.ts +3 -0
- package/dist/health-routes.d.ts.map +1 -1
- package/dist/health-routes.js +11 -1
- package/dist/mcp-apps-outbound.d.ts +236 -31
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +940 -261
- package/dist/origin-validation.d.ts +120 -0
- package/dist/origin-validation.d.ts.map +1 -0
- package/dist/origin-validation.js +199 -0
- package/dist/render-read-gate.d.ts +50 -0
- package/dist/render-read-gate.d.ts.map +1 -0
- package/dist/render-read-gate.js +36 -0
- package/dist/runtime-bundle-route.d.ts +15 -0
- package/dist/runtime-bundle-route.d.ts.map +1 -1
- package/dist/runtime-bundle-route.js +20 -4
- package/dist/server.d.ts +227 -9
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +292 -28
- 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:
|
|
31
|
-
workspaceId:
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACd,MAAM,0BAA0B,CAAC;AAKlC,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAI1C,OAAO,EAEL,KAAK,mCAAmC,EACzC,MAAM,iDAAiD,CAAC;AAIzD,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,EAClC,MAAM,qCAAqC,CAAC;AAE7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,yEAAyE;AACzE,eAAO,MAAM,2BAA2B,QAAQ,CAAC;
|
|
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
|
-
|
|
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,
|
package/dist/health-routes.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/health-routes.js
CHANGED
|
@@ -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
|