@ggui-ai/mcp-server 0.2.0-alpha.4 → 0.4.0-rc.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 (152) hide show
  1. package/dist/admin-blueprints-transport.d.ts.map +1 -1
  2. package/dist/admin-blueprints-transport.js +2 -1
  3. package/dist/admin-oauth-providers-transport.d.ts.map +1 -1
  4. package/dist/admin-oauth-providers-transport.js +7 -5
  5. package/dist/api-renders-routes.d.ts +85 -0
  6. package/dist/api-renders-routes.d.ts.map +1 -0
  7. package/dist/api-renders-routes.js +372 -0
  8. package/dist/build-mcp.d.ts +2 -2
  9. package/dist/build-mcp.d.ts.map +1 -1
  10. package/dist/build-mcp.js +64 -5
  11. package/dist/code-routes.d.ts +47 -0
  12. package/dist/code-routes.d.ts.map +1 -0
  13. package/dist/code-routes.js +81 -0
  14. package/dist/code-store-fs.js +2 -2
  15. package/dist/console-auth.d.ts +10 -10
  16. package/dist/console-auth.d.ts.map +1 -1
  17. package/dist/console-auth.js +5 -5
  18. package/dist/console-blueprint-routes.d.ts +71 -0
  19. package/dist/console-blueprint-routes.d.ts.map +1 -0
  20. package/dist/console-blueprint-routes.js +348 -0
  21. package/dist/console-chat-routes.d.ts +80 -0
  22. package/dist/console-chat-routes.d.ts.map +1 -0
  23. package/dist/console-chat-routes.js +204 -0
  24. package/dist/console-config-routes.d.ts +37 -0
  25. package/dist/console-config-routes.d.ts.map +1 -0
  26. package/dist/console-config-routes.js +91 -0
  27. package/dist/console-headers.d.ts +1 -1
  28. package/dist/console-info-routes.d.ts +84 -0
  29. package/dist/console-info-routes.d.ts.map +1 -0
  30. package/dist/console-info-routes.js +135 -0
  31. package/dist/console-keys-routes.d.ts +50 -0
  32. package/dist/console-keys-routes.d.ts.map +1 -0
  33. package/dist/console-keys-routes.js +222 -0
  34. package/dist/console-llm-keys-routes.d.ts +47 -0
  35. package/dist/console-llm-keys-routes.d.ts.map +1 -0
  36. package/dist/console-llm-keys-routes.js +443 -0
  37. package/dist/console-mcp-tools-routes.d.ts +41 -0
  38. package/dist/console-mcp-tools-routes.d.ts.map +1 -0
  39. package/dist/console-mcp-tools-routes.js +60 -0
  40. package/dist/console-registry-routes.d.ts +66 -0
  41. package/dist/console-registry-routes.d.ts.map +1 -0
  42. package/dist/console-registry-routes.js +276 -0
  43. package/dist/console-session-routes.d.ts +89 -0
  44. package/dist/console-session-routes.d.ts.map +1 -0
  45. package/dist/console-session-routes.js +385 -0
  46. package/dist/console-sessions-routes.d.ts +52 -0
  47. package/dist/console-sessions-routes.d.ts.map +1 -0
  48. package/dist/console-sessions-routes.js +106 -0
  49. package/dist/console-static-routes.d.ts +54 -0
  50. package/dist/console-static-routes.d.ts.map +1 -0
  51. package/dist/console-static-routes.js +190 -0
  52. package/dist/console-theme-routes.d.ts +3 -3
  53. package/dist/console-theme-routes.js +1 -1
  54. package/dist/console-timeline.d.ts +5 -5
  55. package/dist/console-timeline.d.ts.map +1 -1
  56. package/dist/console-timeline.js +27 -26
  57. package/dist/console-welcome.js +2 -2
  58. package/dist/email-login.d.ts.map +1 -1
  59. package/dist/email-login.js +2 -3
  60. package/dist/ggui-session-channel/action-ingress.d.ts +54 -0
  61. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -0
  62. package/dist/ggui-session-channel/action-ingress.js +228 -0
  63. package/dist/ggui-session-channel/channel-subscriptions.d.ts +97 -0
  64. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -0
  65. package/dist/ggui-session-channel/channel-subscriptions.js +224 -0
  66. package/dist/ggui-session-channel/internal-types.d.ts +102 -0
  67. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -0
  68. package/dist/ggui-session-channel/internal-types.js +6 -0
  69. package/dist/ggui-session-channel/outbound.d.ts +81 -0
  70. package/dist/ggui-session-channel/outbound.d.ts.map +1 -0
  71. package/dist/ggui-session-channel/outbound.js +174 -0
  72. package/dist/ggui-session-channel/socket-router.d.ts +38 -0
  73. package/dist/ggui-session-channel/socket-router.d.ts.map +1 -0
  74. package/dist/ggui-session-channel/socket-router.js +213 -0
  75. package/dist/ggui-session-channel/subscribe.d.ts +165 -0
  76. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -0
  77. package/dist/ggui-session-channel/subscribe.js +370 -0
  78. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +40 -0
  79. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -0
  80. package/dist/ggui-session-channel/subscriber-lifecycle.js +123 -0
  81. package/dist/ggui-session-channel.d.ts +425 -0
  82. package/dist/ggui-session-channel.d.ts.map +1 -0
  83. package/dist/ggui-session-channel.js +262 -0
  84. package/dist/health-routes.d.ts +76 -0
  85. package/dist/health-routes.d.ts.map +1 -0
  86. package/dist/health-routes.js +145 -0
  87. package/dist/index.d.ts +10 -11
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +8 -9
  90. package/dist/instructions-presets.d.ts +3 -3
  91. package/dist/instructions-presets.js +24 -24
  92. package/dist/llm-backed-negotiator.d.ts +68 -67
  93. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  94. package/dist/llm-backed-negotiator.js +82 -221
  95. package/dist/mcp-apps-outbound.d.ts +47 -48
  96. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  97. package/dist/mcp-apps-outbound.js +154 -177
  98. package/dist/mcp-endpoint-routes.d.ts +88 -0
  99. package/dist/mcp-endpoint-routes.d.ts.map +1 -0
  100. package/dist/mcp-endpoint-routes.js +359 -0
  101. package/dist/mcp-mounts.d.ts +2 -76
  102. package/dist/mcp-mounts.d.ts.map +1 -1
  103. package/dist/mcp-mounts.js +0 -76
  104. package/dist/oauth-as-routes.d.ts +60 -0
  105. package/dist/oauth-as-routes.d.ts.map +1 -0
  106. package/dist/oauth-as-routes.js +82 -0
  107. package/dist/oauth-clients-routes.d.ts +39 -0
  108. package/dist/oauth-clients-routes.d.ts.map +1 -0
  109. package/dist/oauth-clients-routes.js +87 -0
  110. package/dist/oauth-login-types.d.ts +1 -20
  111. package/dist/oauth-login-types.d.ts.map +1 -1
  112. package/dist/oauth-login-types.js +30 -7
  113. package/dist/oauth-login.d.ts.map +1 -1
  114. package/dist/oauth-login.js +3 -2
  115. package/dist/oauth-providers-store.d.ts.map +1 -1
  116. package/dist/oauth-providers-store.js +5 -5
  117. package/dist/oauth.d.ts +9 -8
  118. package/dist/oauth.d.ts.map +1 -1
  119. package/dist/oauth.js +41 -19
  120. package/dist/pairing-transport.d.ts.map +1 -1
  121. package/dist/pairing-transport.js +2 -1
  122. package/dist/request-context.d.ts +2 -2
  123. package/dist/request-context.js +2 -2
  124. package/dist/reserved-validators.d.ts.map +1 -1
  125. package/dist/reserved-validators.js +9 -1
  126. package/dist/route-param.d.ts +9 -0
  127. package/dist/route-param.d.ts.map +1 -0
  128. package/dist/route-param.js +10 -0
  129. package/dist/runtime-bundle-route.d.ts +43 -0
  130. package/dist/runtime-bundle-route.d.ts.map +1 -0
  131. package/dist/runtime-bundle-route.js +80 -0
  132. package/dist/schema-compat.d.ts +64 -62
  133. package/dist/schema-compat.d.ts.map +1 -1
  134. package/dist/schema-compat.js +23 -51
  135. package/dist/server.d.ts +179 -193
  136. package/dist/server.d.ts.map +1 -1
  137. package/dist/server.js +644 -3759
  138. package/dist/storage.d.ts +5 -5
  139. package/dist/storage.d.ts.map +1 -1
  140. package/dist/storage.js +5 -5
  141. package/dist/thread-transport.d.ts.map +1 -1
  142. package/dist/thread-transport.js +4 -3
  143. package/dist/user-session-auth.d.ts +7 -21
  144. package/dist/user-session-auth.d.ts.map +1 -1
  145. package/dist/user-session-auth.js +7 -28
  146. package/package.json +16 -15
  147. package/dist/mcp-apps-inbound.d.ts +0 -86
  148. package/dist/mcp-apps-inbound.d.ts.map +0 -1
  149. package/dist/mcp-apps-inbound.js +0 -283
  150. package/dist/render-channel.d.ts +0 -694
  151. package/dist/render-channel.d.ts.map +0 -1
  152. package/dist/render-channel.js +0 -1775
@@ -0,0 +1,123 @@
1
+ /**
2
+ * WS subscriber lifecycle for the live channel — registration into the
3
+ * shared subscriber set, the per-subscriber live-tail pump loop, and
4
+ * the symmetric teardown path (`unregister`) that ends the pump,
5
+ * unhooks the StreamFanout subscription, and clears every
6
+ * `channel_subscribe` polling loop the subscriber owned.
7
+ *
8
+ * Owns the per-render subscriber counter that drives the
9
+ * `onFirstSubscriber` / `onLastSubscriberGone` 0↔1 transition hooks —
10
+ * no other module reads it.
11
+ */
12
+ export function createSubscriberLifecycle(deps) {
13
+ /**
14
+ * Per-render local subscriber count. Drives the
15
+ * `onFirstSubscriber` / `onLastSubscriberGone` 0↔1 transition hooks
16
+ * multi-process deployments use for per-render cross-process pub/sub
17
+ * channel scoping. Distinct from the channel server's `renderCount`
18
+ * getter — that walks `wsSubscribers` on demand; this map is the
19
+ * registration-time counter the hooks key off.
20
+ */
21
+ const renderCountById = new Map();
22
+ /**
23
+ * Pump live frames from the StreamFanout iterator out to this
24
+ * subscriber's WS. Started fire-and-forget by `register`; ends when
25
+ * the iterator yields done (close() on the seam) OR `unregister`
26
+ * calls `iter.return()`. Per-subscriber seq filter applied here:
27
+ * frames with `seq <= replayCompletedSeq` were (or will be)
28
+ * delivered via the replay path on subscribe.
29
+ *
30
+ * The pump's first action is `await iter.next()`, which yields
31
+ * control back to the event loop. This is what preserves the
32
+ * subscribe-handler ordering invariant: ack → replay frames →
33
+ * live frames. The replay-frame send loop completes synchronously
34
+ * before the pump can ever send anything, regardless of fanout
35
+ * timing.
36
+ */
37
+ async function pumpSubscriber(sub) {
38
+ try {
39
+ for (;;) {
40
+ const { value, done } = await sub.iter.next();
41
+ if (done)
42
+ return;
43
+ if (value.seq <= sub.replayCompletedSeq)
44
+ continue;
45
+ if (sub.ws.readyState !== sub.ws.OPEN) {
46
+ await sub.iter.return?.();
47
+ return;
48
+ }
49
+ deps.send(sub.ws, { type: "data", payload: value });
50
+ }
51
+ }
52
+ catch (err) {
53
+ deps.logger.warn("render_channel_pump_failed", {
54
+ sessionId: sub.sessionId,
55
+ error: String(err),
56
+ });
57
+ }
58
+ }
59
+ function register(sub) {
60
+ deps.wsSubscribers.add(sub);
61
+ deps.subscribersByWs.set(sub.ws, sub);
62
+ // Per-render count bookkeeping + 0→1 hook for cloud pubsub
63
+ // adapter scoping. Increment FIRST so the hook sees the up-to-date
64
+ // state; hook fires only on the transition (prevCount === 0).
65
+ const prevCount = renderCountById.get(sub.sessionId) ?? 0;
66
+ renderCountById.set(sub.sessionId, prevCount + 1);
67
+ if (prevCount === 0 && deps.onFirstSubscriber) {
68
+ try {
69
+ deps.onFirstSubscriber(sub.sessionId);
70
+ }
71
+ catch (err) {
72
+ // Best-effort: a thrown hook MUST NOT corrupt the
73
+ // wsSubscribers set vs the real socket lifecycle.
74
+ deps.logger.warn("render_channel_on_first_subscriber_threw", {
75
+ sessionId: sub.sessionId,
76
+ error: String(err),
77
+ });
78
+ }
79
+ }
80
+ // Start the pump loop. Fire-and-forget — pump errors are logged
81
+ // inside pumpSubscriber, never propagated.
82
+ void pumpSubscriber(sub);
83
+ }
84
+ function unregister(ws) {
85
+ const sub = deps.subscribersByWs.get(ws);
86
+ if (!sub)
87
+ return;
88
+ deps.subscribersByWs.delete(ws);
89
+ deps.wsSubscribers.delete(sub);
90
+ // Per-render count bookkeeping + 1→0 hook (symmetric with register).
91
+ const prevCount = renderCountById.get(sub.sessionId) ?? 0;
92
+ if (prevCount <= 1) {
93
+ renderCountById.delete(sub.sessionId);
94
+ if (prevCount === 1 && deps.onLastSubscriberGone) {
95
+ try {
96
+ deps.onLastSubscriberGone(sub.sessionId);
97
+ }
98
+ catch (err) {
99
+ deps.logger.warn("render_channel_on_last_subscriber_gone_threw", {
100
+ sessionId: sub.sessionId,
101
+ error: String(err),
102
+ });
103
+ }
104
+ }
105
+ }
106
+ else {
107
+ renderCountById.set(sub.sessionId, prevCount - 1);
108
+ }
109
+ // Ending the iter terminates pumpSubscriber AND unregisters this
110
+ // subscriber from the StreamFanout. Idempotent on the seam side
111
+ // (close-after-return is a no-op).
112
+ void sub.iter.return?.();
113
+ // Tear down every `channel_subscribe` polling loop owned
114
+ // by this subscriber. Symmetric with stream-iterator teardown
115
+ // above. clearInterval is idempotent on already-cleared handles,
116
+ // so a concurrent channel_unsubscribe + WS close is safe.
117
+ for (const state of sub.channelSubs.values()) {
118
+ clearInterval(state.timer);
119
+ }
120
+ sub.channelSubs.clear();
121
+ }
122
+ return { register, unregister };
123
+ }
@@ -0,0 +1,425 @@
1
+ /**
2
+ * OSS live channel — live render plane over WebSocket.
3
+ *
4
+ * The live channel is where the typed-channel contract is enforced on
5
+ * live traffic between the server and the user. It co-hosts on the
6
+ * same Express server as `/mcp` and reuses the
7
+ * `@ggui-ai/mcp-server-handlers/renders` helpers, so every
8
+ * deployment of this server family enforces the same contracts.
9
+ *
10
+ * Scope:
11
+ *
12
+ * - `subscribe` → auth, resolve-or-create render, register subscriber,
13
+ * reply `ack` with the render's current snapshot + sequence.
14
+ * - `action` → inbound user action carried as an {@link ActionEnvelope}.
15
+ * Gated through `assertActionContract` (payload, for data:submit).
16
+ * Persisted to GguiSessionStore as a typed render event.
17
+ * - `ping`/`pong` → heartbeat parity with hosted.
18
+ * - `close`/socket-close → clean subscriber teardown.
19
+ * - `sendToGguiSession(sessionId, data)` → outbound fan-out API for
20
+ * mutation handlers (ggui_emit / connector `ctx.send`). Validated
21
+ * through `assertStreamContract` before delivery.
22
+ *
23
+ * `props_update`: the agent-driven `ggui_update` handler calls
24
+ * `channel.sendPropsUpdate(sessionId, props)` (wired as its
25
+ * `propsUpdateNotifier`) to fan a `{type:'props_update'}` frame to
26
+ * live subscribers. Reaches the renderer's existing `props_update`
27
+ * branch in `iframe-runtime` and applies new props in-place.
28
+ *
29
+ * Not handled here:
30
+ *
31
+ * - Pattern-B daemon-agent notification stream (GET /mcp SSE).
32
+ * - Short-lived render-token mint/consume — that's ggui_render-gated.
33
+ * Dev-mode auth (any bearer via existing AuthAdapter) matches the
34
+ * `/mcp` endpoint's shape and is operator-replaceable.
35
+ *
36
+ * Module layout: this file owns the public surface (options, server
37
+ * interface, composer). The handler families live in
38
+ * `./ggui-session-channel/` as factories taking explicit typed deps:
39
+ *
40
+ * - `outbound.ts` — send/fan-out primitives + the public fan-out
41
+ * surfaces (`sendToGguiSession`, `sendPropsUpdate`, …).
42
+ * - `subscriber-lifecycle.ts` — register / live-tail pump /
43
+ * unregister + the 0↔1 subscriber-count hooks.
44
+ * - `subscribe.ts` — upgrade-time identity resolution (bearer /
45
+ * bootstrap / console cookie) + the `subscribe` handler.
46
+ * - `action-ingress.ts` — `action` contract gate + ledger/pipe
47
+ * dual-write + ack.
48
+ * - `channel-subscriptions.ts` — `channel_subscribe` source-tool
49
+ * polling loops.
50
+ * - `socket-router.ts` — `connection` wiring with the per-socket
51
+ * inbound ordering chain + the message-type dispatcher.
52
+ */
53
+ import type { AuthAdapter, AuthResult, PendingEventConsumer, GguiSessionStore, GguiSessionStreamBuffer, StreamEnvelopeInput, StreamFanout, TelemetrySink } from "@ggui-ai/mcp-server-core";
54
+ import type { JsonObject, GguiSession, ReservedChannelValidator } from "@ggui-ai/protocol";
55
+ import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
56
+ import type { IncomingMessage } from "node:http";
57
+ import type { Duplex } from "node:stream";
58
+ import { type GguiSessionChannelLocalToolsOptions } from "./ggui-session-channel/channel-subscriptions.js";
59
+ import { type GguiSessionChannelBootstrap, type GguiSessionChannelCookieAuth } from "./ggui-session-channel/subscribe.js";
60
+ import type { Logger } from "./logger.js";
61
+ /** Default URL path for the channel endpoint. Operators can override. */
62
+ export declare const DEFAULT_RENDER_CHANNEL_PATH = "/ws";
63
+ export type { GguiSessionChannelLocalToolsOptions } from "./ggui-session-channel/channel-subscriptions.js";
64
+ export type { GguiSessionChannelBootstrap, GguiSessionChannelBootstrapRefreshResult, GguiSessionChannelBootstrapVerifyResult, GguiSessionChannelCookieAuth, } from "./ggui-session-channel/subscribe.js";
65
+ export interface GguiSessionChannelOptions {
66
+ /** Required — the render backing store (typically `InMemoryGguiSessionStore`). */
67
+ readonly renderStore: GguiSessionStore;
68
+ /**
69
+ * Optional pending-events pipe — the SAME `PendingEventConsumer`
70
+ * instance the `ggui_consume` handler drains and the
71
+ * `ggui_runtime_submit_action` relay appends to. When wired, the WS
72
+ * `action` ingress dual-writes every accepted `data:submit` envelope:
73
+ *
74
+ * 1. `renderStore.appendEvent({type:'user.submitted', …})` — the
75
+ * append-only retained ledger (load-bearing for the ack `seq`
76
+ * and reconnect resume, exactly as before).
77
+ * 2. `pendingEventConsumer.append(sessionId, {id, envelope,
78
+ * createdAt})` — the queue that wakes the agent's `ggui_consume`
79
+ * long-poll. The entry mirrors the relay's consume-entry shape
80
+ * (`ConsumeEventEntry`), so WS-originated gestures and
81
+ * tools/call-relayed gestures drain identically.
82
+ *
83
+ * A failed pipe append (e.g. the pipe was never opened because the
84
+ * render didn't come from `ggui_render`) degrades to ledger-only with
85
+ * a `render_channel_consume_append_failed` warn — ack semantics are
86
+ * unchanged either way.
87
+ *
88
+ * Absent → ledger-only ingress (audit-only deployments, conformance
89
+ * harnesses, and composers that registered no `ggui_consume`).
90
+ * `createGguiServer` threads its shared instance here whenever it
91
+ * composed the default handler set.
92
+ */
93
+ readonly pendingEventConsumer?: PendingEventConsumer;
94
+ /**
95
+ * Required — the same `AuthAdapter` the `/mcp` endpoint uses. Any
96
+ * failure during `subscribe` rejects the upgrade with HTTP 401.
97
+ */
98
+ readonly auth: AuthAdapter;
99
+ /**
100
+ * Maps resolved identity → tenant appId for subscribes that omit
101
+ * `payload.appId` (SPEC §12.2 identity-default resolution). Defaults
102
+ * to `defaultAppIdFromIdentity` — same mapping the `/mcp` endpoint
103
+ * uses. Token-bound subscribes (wsToken / console cookie) never
104
+ * consult this seam: their credential already binds the appId.
105
+ */
106
+ readonly appIdFromIdentity?: (result: AuthResult) => string;
107
+ /** Structured logger. */
108
+ readonly logger: Logger;
109
+ /** URL path to mount on. Defaults to `/ws`. */
110
+ readonly path?: string;
111
+ /**
112
+ * Outbound stream replay buffer. Defaults to a fresh
113
+ * `InMemoryGguiSessionStreamBuffer` when omitted — fine for OSS
114
+ * zero-config / dev. Persistent adapters bind via the same
115
+ * `GguiSessionStreamBuffer` interface when they land.
116
+ *
117
+ * Each channel instance owns its own seq cursor space; sharing a
118
+ * buffer across two channels in the same process would couple their
119
+ * sequences in confusing ways.
120
+ */
121
+ readonly streamBuffer?: GguiSessionStreamBuffer;
122
+ /**
123
+ * Live-tail pub/sub for outbound live-channel frames. Defaults to a
124
+ * fresh `InProcessStreamFanout` (in-memory, single-process).
125
+ * Multi-process deployments bind a pubsub-backed `StreamFanout`
126
+ * implementation (e.g. Redis) behind this seam for cross-process
127
+ * fan-out.
128
+ *
129
+ * The channel server uses the seam to publish every fanout-eligible
130
+ * envelope and to subscribe one async iterator per WebSocket
131
+ * subscriber — no in-process Map walk; the seam owns routing.
132
+ */
133
+ readonly streamFanout?: StreamFanout;
134
+ /**
135
+ * Optional bootstrap-auth plumbing. When present, the channel
136
+ * accepts `SubscribePayload.wsToken` and issues reconnect
137
+ * credentials in `AckPayload.sessionToken`. When absent, bootstrap
138
+ * tokens are rejected with `BOOTSTRAP_NOT_SUPPORTED`.
139
+ */
140
+ readonly bootstrap?: GguiSessionChannelBootstrap;
141
+ /**
142
+ * Optional console cookie-auth plumbing. When present, the
143
+ * channel upgrade looks for the configured cookie on the incoming
144
+ * request. A valid cookie binds the identity as a `builder` and
145
+ * scopes the subscriber to the cookie's `sessionId` — any
146
+ * `subscribe.sessionId` mismatch is rejected with
147
+ * `DEVTOOL_COOKIE_SESSION_MISMATCH`.
148
+ *
149
+ * Absent = cookie auth disabled on this channel. Cookies are never
150
+ * auto-enabled; the server's console composition decides.
151
+ *
152
+ * Design boundary: this auth plane is MUTUALLY EXCLUSIVE with
153
+ * bootstrap auth at upgrade time. When both are configured, the
154
+ * bootstrap path (via `?bootstrap=` query) wins; cookie is only
155
+ * consulted for standard upgrades.
156
+ */
157
+ readonly cookieAuth?: GguiSessionChannelCookieAuth;
158
+ /**
159
+ * Reserved-channel payload validators for channels whose shape is
160
+ * NOT protocol-owned (Item 4 injection pattern). The primary
161
+ * consumer is `_ggui:preview` — the server composes a
162
+ * {@link ReservedChannelValidator} adapting
163
+ * `@ggui-ai/preview-a2ui::parseServerMessage` so malformed A2UI
164
+ * frames emitted on the preview channel reject at the fan-out
165
+ * boundary instead of landing in the subscriber's renderer.
166
+ *
167
+ * Lookup inside {@link validateStreamData} consults this map FIRST,
168
+ * then the protocol-shipped `BUILTIN_RESERVED_VALIDATORS` (which
169
+ * validates `_ggui:lifecycle`), then falls through to
170
+ * `{valid: true}` when a known reserved channel has no validator.
171
+ *
172
+ * Absent = no `_ggui:preview` validation (documented degradation for
173
+ * implementations without the preview package); `_ggui:lifecycle`
174
+ * is always validated via the built-in.
175
+ */
176
+ readonly extraReservedValidators?: ReadonlyMap<string, ReservedChannelValidator>;
177
+ /**
178
+ * Optional {@link TelemetrySink} for live-channel operational
179
+ * signals (C12) — operational counts + durations for OTLP /
180
+ * CloudWatch / Datadog forwarders. Reserved seam: the channel
181
+ * defines the binding point but emits no signals of its own today;
182
+ * future operational counts (e.g. subscribe / poll failure rates)
183
+ * bind here so operator sink wiring composes uniformly with the
184
+ * server's `server.composed` signal.
185
+ *
186
+ * Deliberately separate from the renderer's client-side
187
+ * `ObservabilityEvent` surface — two independent consumers (backend
188
+ * metrics vs host inspector UI). See the TelemetrySink docstring
189
+ * for the sync/lossy contract.
190
+ */
191
+ readonly telemetry?: TelemetrySink;
192
+ /**
193
+ * Opt-in `channel_subscribe` plumbing for `streamSpec[*].source.tool`
194
+ * fan-out. When present, channel subscribes whose
195
+ * `source.tool` is in `allowlist` are accepted and the server begins
196
+ * polling. When absent, every `channel_subscribe` returns
197
+ * `CHANNEL_NOT_LOCAL` so the iframe falls back to direct polling via
198
+ * the MCP host proxy.
199
+ *
200
+ * Same `allowlist` MUST be advertised on
201
+ * `handshake.serverCapabilities.streamWebSocketLocalTools` so iframe
202
+ * + server agree on which channels use the WS fan-out path.
203
+ */
204
+ readonly streamWebSocketLocalTools?: GguiSessionChannelLocalToolsOptions;
205
+ /**
206
+ * Protocol-version handshake policy. Governs server behavior when a
207
+ * subscribe declares a `supportedVersions` list that does NOT
208
+ * contain this server's {@link PROTOCOL_SCHEMA_VERSION}.
209
+ *
210
+ * - `'reject'` (default): server emits
211
+ * `{type:'error', payload.code: 'UPGRADE_REQUIRED'}` AND closes
212
+ * the underlying WebSocket. The caller cannot accidentally
213
+ * proceed against a version-mismatched render. This is the
214
+ * canonical posture for first-party servers.
215
+ * - `'advisory'` (opt-out): server emits `UPGRADE_REQUIRED`
216
+ * but keeps the connection open — the subscribe stops (no ack,
217
+ * no snapshot, no replay). Existing clients that ignore the error
218
+ * code continue to interoperate exactly as pre-handshake.
219
+ * Use only for controlled migration windows during which
220
+ * legacy-version clients must remain attached.
221
+ *
222
+ * Absent `payload.supportedVersions` always passes through — the
223
+ * handshake is fully opt-in on the client side. `serverVersion` is
224
+ * stamped into every successful ack regardless of policy.
225
+ *
226
+ * Switching between `'advisory'` and `'reject'` is a config change,
227
+ * not a schema change — the wire fields and error code ship
228
+ * identically in both modes.
229
+ */
230
+ readonly versionPolicy?: "advisory" | "reject";
231
+ /**
232
+ * Optional hook fired synchronously when the local subscriber count
233
+ * for `sessionId` transitions 0 → 1 (the first subscriber for that
234
+ * render connects to this server instance).
235
+ *
236
+ * Hosted deployments use this to lazily SUBSCRIBE to the per-render
237
+ * cross-pod broadcast channel (e.g. Redis pub/sub); OSS has no use
238
+ * for it (in-process broadcasts already route via
239
+ * {@link GguiSessionChannelServer.sendPropsUpdate}). Bounding pubsub
240
+ * fan-in to only renders a pod actually holds connections for is a
241
+ * correctness requirement, not an optimization — without it every
242
+ * pod receives every other pod's broadcast for every active render.
243
+ *
244
+ * Best-effort: a thrown callback is logged and swallowed.
245
+ * `register()` MUST NOT fail because of a hook error or the
246
+ * `wsSubscribers` set would drift out of sync with the real socket
247
+ * lifecycle.
248
+ *
249
+ * Concurrent register/unregister for the same sessionId are serialized
250
+ * by the channel's single-threaded WS event loop; hook implementations
251
+ * do not need their own mutex for the 0↔1 transition.
252
+ */
253
+ readonly onFirstSubscriber?: (sessionId: string) => void;
254
+ /**
255
+ * Optional hook fired synchronously when the local subscriber count
256
+ * for `sessionId` transitions 1 → 0 (the last subscriber for that
257
+ * render disconnects).
258
+ *
259
+ * Symmetric with {@link onFirstSubscriber}; same best-effort posture
260
+ * and single-threaded serialization guarantee.
261
+ */
262
+ readonly onLastSubscriberGone?: (sessionId: string) => void;
263
+ }
264
+ export interface GguiSessionChannelServer {
265
+ /** The URL path the channel accepts upgrade requests on. */
266
+ readonly path: string;
267
+ /**
268
+ * Wire this into the HTTP server's `upgrade` event. Rejects with 401
269
+ * on auth failure; otherwise completes the WS handshake and wires
270
+ * the subscriber.
271
+ */
272
+ handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void;
273
+ /**
274
+ * Deliver a stream envelope to every subscriber of `delivery.sessionId`.
275
+ *
276
+ * Outbound fan-out enforcement point — the delivery's `payload` is
277
+ * validated against the active render's streamSpec via
278
+ * `assertStreamContract` before any subscriber receives it.
279
+ *
280
+ * Sequencing + replay behavior:
281
+ * 1. Payload validated against the active render's streamSpec.
282
+ * 2. The replay buffer assigns a render-scoped monotonic `seq` and
283
+ * (conditionally) stores the stamped envelope per the channel's
284
+ * replay policy (`'none'` skip / `'latest'` single-slot /
285
+ * `'all'` FIFO ring).
286
+ * 3. The stamped envelope fans out to every subscriber for the
287
+ * render, skipping any whose initial replay already covered
288
+ * this seq (prevents double delivery on reconnect).
289
+ *
290
+ * The caller supplies `StreamEnvelopeInput` (no seq); the server
291
+ * stamps. Returns the stamped `seq` for callers that need to thread
292
+ * ordering back to their own response (e.g., `ggui_emit`'s wire
293
+ * output). When the channel's replay policy was `'none'`, seq is
294
+ * still assigned (so fan-out has a stable cursor) but nothing is
295
+ * stored — the seq still surfaces here so the caller has the same
296
+ * shape regardless of replay policy.
297
+ *
298
+ * Throws `ContractViolationError` on payload mismatch; transport
299
+ * errors are logged but not propagated (per-subscriber best-effort).
300
+ */
301
+ sendToGguiSession(delivery: StreamEnvelopeInput): Promise<{
302
+ seq: number;
303
+ }>;
304
+ /**
305
+ * Fan a `{type:'render', payload:{render, matchType?}}` wire frame
306
+ * to every subscriber currently bound to `sessionId`. Use this to
307
+ * notify already-subscribed clients about a render-commit that
308
+ * happened AFTER they subscribed — the initial `ack.render` snapshot
309
+ * covers state at subscribe time only, so without an explicit notify
310
+ * a second-turn `commit` is invisible to the live client.
311
+ *
312
+ * The `B1` regression context (2026-04-22 QA pass): the chat surface
313
+ * in `/chat` reuses one render across turns. The first turn's render
314
+ * subscribed AFTER `commit` ran, so the ack carried the
315
+ * entry. The second turn's commit landed on the live render — the
316
+ * subscriber never heard about the new entry, the inline UI slot
317
+ * stayed in "Waiting for render channel replay…" indefinitely.
318
+ * `notifyGguiSessionCommit` closes that gap. Best-effort: per-subscriber
319
+ * send failures are swallowed (same posture as `sendToGguiSession`).
320
+ *
321
+ * NOT durable. Frames are not stamped through the replay buffer —
322
+ * fresh subscribers still get the current render via `ack.render` on
323
+ * subscribe. A new tab opening mid-render reads the latest render
324
+ * from the snapshot; live tabs read the delta from this notify.
325
+ *
326
+ * Subscribers received via `register()` are tracked in
327
+ * `subscribersByRender`; this helper iterates the bound set and
328
+ * skips closed sockets via the `send()` helper's existing guard.
329
+ * Callers ARE responsible for ordering — call after the underlying
330
+ * `renderStore.commit` resolves so the snapshot a
331
+ * concurrent fresh subscriber observes still includes the entry.
332
+ */
333
+ notifyGguiSessionCommit(sessionId: string, render: GguiSession, matchType?: string): void;
334
+ /**
335
+ * Fan a `{type:'props_update', payload:{sessionId, props}}` wire frame
336
+ * to every subscriber currently bound to `sessionId`. The agent-driven
337
+ * `ggui_update` handler calls this (wired as its `propsUpdateNotifier`)
338
+ * so a props patch replaces renderer props in-place on live
339
+ * subscribers without waiting for a resubscribe.
340
+ *
341
+ * Validation posture (mirrors `notifyGguiSessionCommit`'s "best-effort orphan
342
+ * no-op"):
343
+ * 1. Look up the render via `renderStore.get`. Absent → log
344
+ * `render_channel_props_update_orphan` and return — the wire
345
+ * validator on the renderer side would reject a frame for an
346
+ * unknown render anyway.
347
+ * 2. Iterate the flat WS-subscriber set, filter to subscribers
348
+ * whose `sessionId` matches, and `send()` the frame. Closed
349
+ * sockets are skipped silently by `send()`.
350
+ *
351
+ * NOT routed through StreamFanout — `type: 'props_update'` is a
352
+ * distinct WebSocket message type, not a stream envelope. Stream
353
+ * envelopes flow on `data` frames and have a `seq` cursor; props
354
+ * updates are ephemeral and follow `notifyGguiSessionCommit`'s pattern
355
+ * (live-only, no replay-buffer stamping). A new subscriber that
356
+ * connects mid-render reads current `props` from the render
357
+ * snapshot delivered in `ack.render`.
358
+ *
359
+ * Schema validation against `propsSpec`: NOT enforced server-side
360
+ * here. The `ggui_update` handler validates the patch against the
361
+ * render's `propsSpec` BEFORE persisting and notifying, and the
362
+ * renderer re-validates inbound props via
363
+ * `validateInboundPropsPayload` against the cached
364
+ * `render.propsSpec` before applying — defense-in-depth at the
365
+ * receiving boundary.
366
+ */
367
+ sendPropsUpdate(sessionId: string, props: JsonObject): Promise<void>;
368
+ /**
369
+ * Fan a `{type:'drain_ack', payload:{sessionId, appId,
370
+ * eventId, drainedAt}}` wire frame to every subscriber currently
371
+ * bound to `sessionId`.
372
+ *
373
+ * Fired by `createGguiConsumeHandler` once per drained
374
+ * `PendingEvent` so the iframe-runtime can cancel the matching
375
+ * per-action 10s claim timer + resolve the toast as `consumed`.
376
+ * Implements the `DrainAckNotifier` contract from
377
+ * `@ggui-ai/mcp-server-handlers`.
378
+ *
379
+ * Same posture as `notifyGguiSessionCommit` / `sendPropsUpdate` — live-only,
380
+ * no replay-buffer stamping. Subscribers that connect AFTER the
381
+ * drain see the next consume's snapshot rather than the missed
382
+ * frame; the iframe's claim timer + atomic-pop primitive backstop
383
+ * any frame loss.
384
+ */
385
+ sendDrainAck(args: {
386
+ readonly sessionId: string;
387
+ readonly appId: string;
388
+ readonly eventId: string;
389
+ readonly drainedAt: string;
390
+ }): void;
391
+ /**
392
+ * Fan a server-frame to every local WS subscriber bound to
393
+ * `sessionId`. Skips replay-buffer stamping, GguiSessionStore lookups,
394
+ * and contract validation — the caller is the one that originally
395
+ * validated + persisted the underlying mutation. This surface is the
396
+ * delivery path for already-validated frames that arrived via an
397
+ * external pubsub layer (e.g. Redis from another process of a
398
+ * multi-process deployment).
399
+ *
400
+ * Internal adapter use only. NOT part of the published ggui
401
+ * protocol, NOT stable across versions, NOT exposed to MCP / wire
402
+ * callers. The publisher is responsible for ensuring `frame` is
403
+ * wire-valid; this method does not re-validate.
404
+ *
405
+ * No-op when no local subscriber is bound to `sessionId`. Closed
406
+ * sockets are skipped silently by the underlying `send()` helper —
407
+ * same posture as `sendPropsUpdate` / `notifyGguiSessionCommit`. Per-
408
+ * subscriber send failures are logged but never propagated.
409
+ */
410
+ externalBroadcast(sessionId: string, frame: WebSocketMessage): void;
411
+ /** Number of live subscribers. Useful for health / debug introspection. */
412
+ readonly subscriberCount: number;
413
+ /** Number of distinct renders with at least one subscriber. */
414
+ readonly renderCount: number;
415
+ /**
416
+ * Close every live subscriber + the underlying ws server. Idempotent.
417
+ */
418
+ close(): Promise<void>;
419
+ }
420
+ /**
421
+ * Build an OSS live-channel server. The returned object is designed to be
422
+ * composed into `createGguiServer` — see `server.ts` for the wire-up.
423
+ */
424
+ export declare function createGguiSessionChannelServer(opts: GguiSessionChannelOptions): GguiSessionChannelServer;
425
+ //# sourceMappingURL=ggui-session-channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ggui-session-channel.d.ts","sourceRoot":"","sources":["../src/ggui-session-channel.ts"],"names":[],"mappings":"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"}