@ggui-ai/mcp-server 0.8.0 → 0.10.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 +21 -0
- package/dist/api-renders-routes.d.ts.map +1 -1
- package/dist/api-renders-routes.js +54 -28
- package/dist/api-renders-stream-route.d.ts +80 -0
- package/dist/api-renders-stream-route.d.ts.map +1 -0
- package/dist/api-renders-stream-route.js +311 -0
- package/dist/build-mcp.d.ts +48 -7
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +87 -6
- package/dist/code-module-variant.d.ts +150 -0
- package/dist/code-module-variant.d.ts.map +1 -0
- package/dist/code-module-variant.js +243 -0
- package/dist/code-routes.d.ts +12 -2
- package/dist/code-routes.d.ts.map +1 -1
- package/dist/code-routes.js +12 -2
- package/dist/console-session-routes.d.ts.map +1 -1
- package/dist/console-session-routes.js +11 -0
- package/dist/control-service.d.ts +29 -3
- package/dist/control-service.d.ts.map +1 -1
- package/dist/control-service.js +26 -2
- package/dist/ggui-session-channel/action-ingress.d.ts +2 -2
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +4 -4
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
- package/dist/ggui-session-channel/internal-types.d.ts +63 -9
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.d.ts +15 -5
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.js +64 -24
- package/dist/ggui-session-channel/socket-router.d.ts +8 -3
- package/dist/ggui-session-channel/socket-router.d.ts.map +1 -1
- package/dist/ggui-session-channel/socket-router.js +6 -1
- package/dist/ggui-session-channel/subscribe.d.ts +48 -3
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscribe.js +97 -36
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +23 -13
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscriber-lifecycle.js +24 -11
- package/dist/ggui-session-channel.d.ts +58 -11
- package/dist/ggui-session-channel.d.ts.map +1 -1
- package/dist/ggui-session-channel.js +55 -19
- package/dist/health-routes.d.ts +19 -3
- package/dist/health-routes.d.ts.map +1 -1
- package/dist/health-routes.js +26 -18
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/instructions-presets.js +10 -10
- package/dist/mcp-apps-outbound.d.ts +88 -11
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +470 -77
- package/dist/mcp-endpoint-routes.d.ts +23 -5
- package/dist/mcp-endpoint-routes.d.ts.map +1 -1
- package/dist/mcp-endpoint-routes.js +69 -1
- package/dist/oauth-as-routes.d.ts +11 -0
- package/dist/oauth-as-routes.d.ts.map +1 -1
- package/dist/oauth-as-routes.js +45 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +8 -1
- package/dist/runtime-bundle-hash.d.ts +55 -0
- package/dist/runtime-bundle-hash.d.ts.map +1 -0
- package/dist/runtime-bundle-hash.js +85 -0
- package/dist/runtime-bundle-route.js +1 -1
- package/dist/server.d.ts +239 -61
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +355 -143
- package/package.json +13 -12
package/dist/code-routes.js
CHANGED
|
@@ -28,12 +28,22 @@
|
|
|
28
28
|
* store sees the parameter.
|
|
29
29
|
*/
|
|
30
30
|
import { CODE_HASH_REGEX } from "@ggui-ai/mcp-server-core";
|
|
31
|
+
import { mountCodeModuleVariantRoute, } from "./code-module-variant.js";
|
|
31
32
|
/**
|
|
32
|
-
* Mount `GET /code/:hash.js` + `GET /contract/:hash.js`
|
|
33
|
-
*
|
|
33
|
+
* Mount `GET /code/:hash.js` + `GET /contract/:hash.js` (+ the
|
|
34
|
+
* module-variant route when configured) onto the express app. Returns
|
|
35
|
+
* nothing — the routes self-register.
|
|
34
36
|
*/
|
|
35
37
|
export function mountCodeRoutes(opts) {
|
|
36
38
|
const { app, codeStore, logger } = opts;
|
|
39
|
+
if (opts.moduleVariant !== undefined) {
|
|
40
|
+
mountCodeModuleVariantRoute({
|
|
41
|
+
app,
|
|
42
|
+
codeStore,
|
|
43
|
+
variant: opts.moduleVariant,
|
|
44
|
+
logger,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
37
47
|
const mountContentAddressableRoute = (mountPath, label) => {
|
|
38
48
|
app.get(mountPath, async (req, res) => {
|
|
39
49
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,CAqW1F"}
|
|
@@ -267,9 +267,19 @@ export function mountConsoleSessionRoutes(opts) {
|
|
|
267
267
|
// remains authoritative.
|
|
268
268
|
let renderContractHash;
|
|
269
269
|
let renderValidatorsUrl;
|
|
270
|
+
// Freeze-latch self-epoch (#483): the console inspector views the
|
|
271
|
+
// LIVE HEAD, so its boot meta carries the row's current epoch —
|
|
272
|
+
// without it the viewer boots believing epoch 0 and the first
|
|
273
|
+
// update on the session would freeze the inspection surface.
|
|
274
|
+
// (Follow-up: on a `ui.reminted` boundary the viewer should
|
|
275
|
+
// re-mount to the new head rather than stay frozen.)
|
|
276
|
+
let renderEpoch;
|
|
270
277
|
if (renderStore && codeStore) {
|
|
271
278
|
try {
|
|
272
279
|
const stored = await renderStore.get(verified.sessionId);
|
|
280
|
+
if (stored !== null) {
|
|
281
|
+
renderEpoch = stored.render.epoch;
|
|
282
|
+
}
|
|
273
283
|
if (stored !== null &&
|
|
274
284
|
stored.render.type !== "mcpApps" &&
|
|
275
285
|
stored.render.type !== "system" &&
|
|
@@ -304,6 +314,7 @@ export function mountConsoleSessionRoutes(opts) {
|
|
|
304
314
|
wsUrl: resolvedWsUrl,
|
|
305
315
|
wsToken: minted.token,
|
|
306
316
|
expiresAt: minted.expiresAt,
|
|
317
|
+
...(renderEpoch !== undefined ? { epoch: renderEpoch } : {}),
|
|
307
318
|
...(renderContractHash !== undefined && renderValidatorsUrl !== undefined
|
|
308
319
|
? {
|
|
309
320
|
contractHash: renderContractHash,
|
|
@@ -88,6 +88,18 @@ export declare function filterHandlersByAudience(set: ReadonlyArray<AnyHandler>,
|
|
|
88
88
|
* mutations. Confirm-gating it would break the card's inline render on
|
|
89
89
|
* the first call, because the confirmation preview does not carry the
|
|
90
90
|
* card's discriminant.
|
|
91
|
+
*
|
|
92
|
+
* `ggui_ops_save_library_blueprint` is the one MUTATING entry, by owner
|
|
93
|
+
* ruling (ggui#525, 2026-08-16). The argument that it is safe on the
|
|
94
|
+
* first call: the save is idempotent under the name-collapse rule (same
|
|
95
|
+
* slug ⇒ one row, a repeat is a no-op), it writes only to the CALLER's
|
|
96
|
+
* own library (no cross-tenant or account-wide effect), and its caller
|
|
97
|
+
* is the console helper agent auto-saving after a render — the
|
|
98
|
+
* "confirm" it performed was an agent self-confirm with no human in
|
|
99
|
+
* the loop, which bought no safety and cost one extra tool round trip
|
|
100
|
+
* on every helper render. A future entry that lacks any of those three
|
|
101
|
+
* properties (idempotent, own-scope, no human to protect) does NOT
|
|
102
|
+
* inherit this ruling — it argues its own.
|
|
91
103
|
*/
|
|
92
104
|
export declare const SINGLE_CALL_OPS: ReadonlySet<string>;
|
|
93
105
|
/**
|
|
@@ -110,8 +122,12 @@ export declare function stripAudience<I extends ZodRawShape, O extends ZodRawSha
|
|
|
110
122
|
* populated: every deployment tier proves identity differently
|
|
111
123
|
* (single-user builder, per-app key, per-user key, OIDC), and only the
|
|
112
124
|
* source distinguishes "the adapter authenticated this request" from
|
|
113
|
-
* "the transport let it through".
|
|
114
|
-
*
|
|
125
|
+
* "the transport let it through". This gate is defense-in-depth
|
|
126
|
+
* behind the transport's anonymous-ops 401 challenge (ggui#505, see
|
|
127
|
+
* `mcp-endpoint-routes.ts`): anonymous callers normally never reach
|
|
128
|
+
* dispatch on an ops tool; a throw HERE surfaces as an in-band
|
|
129
|
+
* `isError` tool result, which is the contract for callers that
|
|
130
|
+
* authenticated but lack what the handler needs.
|
|
115
131
|
*/
|
|
116
132
|
export declare function withAuthGate<I extends ZodRawShape, O extends ZodRawShape, D>(h: SharedHandler<I, O, D>): SharedHandler<I, O, D>;
|
|
117
133
|
/**
|
|
@@ -155,6 +171,16 @@ export interface BuildControlServiceArgs {
|
|
|
155
171
|
* design-time tools, and the per-handler auth gate is what keeps the
|
|
156
172
|
* ops half closed.
|
|
157
173
|
*/
|
|
158
|
-
export declare function buildControlService(args: BuildControlServiceArgs):
|
|
174
|
+
export declare function buildControlService(args: BuildControlServiceArgs): ControlService;
|
|
175
|
+
/**
|
|
176
|
+
* {@link McpService} plus the control plane's ops-tool name set — the
|
|
177
|
+
* membership the transport-level OAuth challenge reads. A separate
|
|
178
|
+
* field (rather than audience tags on the handlers) because service
|
|
179
|
+
* handlers deliberately carry no audience tag (see
|
|
180
|
+
* {@link stripAudience}).
|
|
181
|
+
*/
|
|
182
|
+
export interface ControlService extends McpService {
|
|
183
|
+
readonly opsToolNames: ReadonlySet<string>;
|
|
184
|
+
}
|
|
159
185
|
export {};
|
|
160
186
|
//# sourceMappingURL=control-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control-service.d.ts","sourceRoot":"","sources":["../src/control-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAK,KAAK,WAAW,EAAmB,MAAM,KAAK,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,KAAK,UAAU,GAAG,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAE1D,gDAAgD;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,CAAC;AAEnE,6CAA6C;AAC7C,eAAO,MAAM,YAAY,aAAa,CAAC;AAEvC,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,YAAY,CAAC;AAE9C,sDAAsD;AACtD,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,WAAW,CAAwB,CAAC;AAErF,0CAA0C;AAC1C,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,WAAW,CAAuB,CAAC;AAEvF;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,aAAa,CAAC,UAAU,CAAC,EAC9B,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,GAClC,aAAa,CAAC,UAAU,CAAC,CAK3B;AAED
|
|
1
|
+
{"version":3,"file":"control-service.d.ts","sourceRoot":"","sources":["../src/control-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAK,KAAK,WAAW,EAAmB,MAAM,KAAK,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,KAAK,UAAU,GAAG,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAE1D,gDAAgD;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,CAAC;AAEnE,6CAA6C;AAC7C,eAAO,MAAM,YAAY,aAAa,CAAC;AAEvC,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,YAAY,CAAC;AAE9C,sDAAsD;AACtD,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,WAAW,CAAwB,CAAC;AAErF,0CAA0C;AAC1C,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,WAAW,CAAuB,CAAC;AAEvF;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,aAAa,CAAC,UAAU,CAAC,EAC9B,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,GAClC,aAAa,CAAC,UAAU,CAAC,CAK3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAgB9C,CAAC;AAEH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,EAC3E,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAGxB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,EAC1E,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAYxB;AASD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,EAC7E,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CA+BzC;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAC7C;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChD;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,cAAc,CAwBjF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5C"}
|
package/dist/control-service.js
CHANGED
|
@@ -89,11 +89,24 @@ export function filterHandlersByAudience(set, allowed) {
|
|
|
89
89
|
* mutations. Confirm-gating it would break the card's inline render on
|
|
90
90
|
* the first call, because the confirmation preview does not carry the
|
|
91
91
|
* card's discriminant.
|
|
92
|
+
*
|
|
93
|
+
* `ggui_ops_save_library_blueprint` is the one MUTATING entry, by owner
|
|
94
|
+
* ruling (ggui#525, 2026-08-16). The argument that it is safe on the
|
|
95
|
+
* first call: the save is idempotent under the name-collapse rule (same
|
|
96
|
+
* slug ⇒ one row, a repeat is a no-op), it writes only to the CALLER's
|
|
97
|
+
* own library (no cross-tenant or account-wide effect), and its caller
|
|
98
|
+
* is the console helper agent auto-saving after a render — the
|
|
99
|
+
* "confirm" it performed was an agent self-confirm with no human in
|
|
100
|
+
* the loop, which bought no safety and cost one extra tool round trip
|
|
101
|
+
* on every helper render. A future entry that lacks any of those three
|
|
102
|
+
* properties (idempotent, own-scope, no human to protect) does NOT
|
|
103
|
+
* inherit this ruling — it argues its own.
|
|
92
104
|
*/
|
|
93
105
|
export const SINGLE_CALL_OPS = new Set([
|
|
94
106
|
"ggui_ops_get_credit_balance",
|
|
95
107
|
"ggui_ops_get_my_blueprint_source",
|
|
96
108
|
"ggui_ops_get_org_balance",
|
|
109
|
+
"ggui_ops_get_render_source",
|
|
97
110
|
"ggui_ops_list_apps",
|
|
98
111
|
"ggui_ops_list_blueprints",
|
|
99
112
|
"ggui_ops_list_connector_keys",
|
|
@@ -102,6 +115,8 @@ export const SINGLE_CALL_OPS = new Set([
|
|
|
102
115
|
"ggui_ops_list_my_blueprints",
|
|
103
116
|
"ggui_ops_list_orgs",
|
|
104
117
|
"ggui_ops_list_provider_keys",
|
|
118
|
+
"ggui_ops_list_recent_renders",
|
|
119
|
+
"ggui_ops_save_library_blueprint",
|
|
105
120
|
"ggui_ops_setup_byok",
|
|
106
121
|
]);
|
|
107
122
|
/**
|
|
@@ -127,8 +142,12 @@ export function stripAudience(h) {
|
|
|
127
142
|
* populated: every deployment tier proves identity differently
|
|
128
143
|
* (single-user builder, per-app key, per-user key, OIDC), and only the
|
|
129
144
|
* source distinguishes "the adapter authenticated this request" from
|
|
130
|
-
* "the transport let it through".
|
|
131
|
-
*
|
|
145
|
+
* "the transport let it through". This gate is defense-in-depth
|
|
146
|
+
* behind the transport's anonymous-ops 401 challenge (ggui#505, see
|
|
147
|
+
* `mcp-endpoint-routes.ts`): anonymous callers normally never reach
|
|
148
|
+
* dispatch on an ops tool; a throw HERE surfaces as an in-band
|
|
149
|
+
* `isError` tool result, which is the contract for callers that
|
|
150
|
+
* authenticated but lack what the handler needs.
|
|
132
151
|
*/
|
|
133
152
|
export function withAuthGate(h) {
|
|
134
153
|
return {
|
|
@@ -216,5 +235,10 @@ export function buildControlService(args) {
|
|
|
216
235
|
path: CONTROL_PATH,
|
|
217
236
|
handlers,
|
|
218
237
|
anonymous: true,
|
|
238
|
+
// Captured BEFORE stripAudience erases the tags — the transport's
|
|
239
|
+
// anonymous-ops 401 challenge (ggui#505) needs the ops-name set,
|
|
240
|
+
// and the stripped handlers can no longer answer "which of you are
|
|
241
|
+
// ops". This service is the single authority on that membership.
|
|
242
|
+
opsToolNames: new Set(opsTools.map((h) => h.name)),
|
|
219
243
|
};
|
|
220
244
|
}
|
|
@@ -9,7 +9,7 @@ import type { GguiSessionStore, PendingEventConsumer } from "@ggui-ai/mcp-server
|
|
|
9
9
|
import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
|
|
10
10
|
import type { WebSocket } from "ws";
|
|
11
11
|
import type { Logger } from "../logger.js";
|
|
12
|
-
import type {
|
|
12
|
+
import type { WsSubscriber } from "./internal-types.js";
|
|
13
13
|
import type { Outbound } from "./outbound.js";
|
|
14
14
|
export interface ActionIngressDeps {
|
|
15
15
|
readonly logger: Logger;
|
|
@@ -46,7 +46,7 @@ export interface ActionIngress {
|
|
|
46
46
|
* mid-turn. Pipe failure degrades to ledger-only with a warn;
|
|
47
47
|
* it never changes the ack.
|
|
48
48
|
*/
|
|
49
|
-
handleInboundAction(ws: WebSocket, sub:
|
|
49
|
+
handleInboundAction(ws: WebSocket, sub: WsSubscriber, message: WebSocketMessage & {
|
|
50
50
|
type: "action";
|
|
51
51
|
}): Promise<void>;
|
|
52
52
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-ingress.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/action-ingress.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAIvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"action-ingress.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/action-ingress.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAIvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AA+G9C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IACrD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,mBAAmB,CACjB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,GAC7C,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,CAiJ1E"}
|
|
@@ -10,7 +10,7 @@ import type { GguiSessionStore } from "@ggui-ai/mcp-server-core";
|
|
|
10
10
|
import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
|
|
11
11
|
import type { WebSocket } from "ws";
|
|
12
12
|
import type { Logger } from "../logger.js";
|
|
13
|
-
import type {
|
|
13
|
+
import type { WsSubscriber } from "./internal-types.js";
|
|
14
14
|
import type { Outbound } from "./outbound.js";
|
|
15
15
|
/**
|
|
16
16
|
* Opt-in plumbing for the `channel_subscribe` polling loop. When this
|
|
@@ -68,7 +68,7 @@ export interface ChannelSubscriptionsDeps {
|
|
|
68
68
|
* callback fire-time to self-clean intervals that outlived their
|
|
69
69
|
* subscriber.
|
|
70
70
|
*/
|
|
71
|
-
readonly subscribersByWs: WeakMap<WebSocket,
|
|
71
|
+
readonly subscribersByWs: WeakMap<WebSocket, WsSubscriber>;
|
|
72
72
|
readonly send: Outbound["send"];
|
|
73
73
|
readonly sendChannelError: Outbound["sendChannelError"];
|
|
74
74
|
}
|
|
@@ -80,7 +80,7 @@ export interface ChannelSubscriptions {
|
|
|
80
80
|
* `${sessionId}:${channelName}` — a re-subscribe replaces any
|
|
81
81
|
* existing interval rather than running two in parallel.
|
|
82
82
|
*/
|
|
83
|
-
handleChannelSubscribe(ws: WebSocket, sub:
|
|
83
|
+
handleChannelSubscribe(ws: WebSocket, sub: WsSubscriber, message: WebSocketMessage & {
|
|
84
84
|
type: "channel_subscribe";
|
|
85
85
|
}): Promise<void>;
|
|
86
86
|
/**
|
|
@@ -89,7 +89,7 @@ export interface ChannelSubscriptions {
|
|
|
89
89
|
* implicitly unsubscribes every channel; this message is for
|
|
90
90
|
* mid-session cancellation.
|
|
91
91
|
*/
|
|
92
|
-
handleChannelUnsubscribe(ws: WebSocket, sub:
|
|
92
|
+
handleChannelUnsubscribe(ws: WebSocket, sub: WsSubscriber, message: WebSocketMessage & {
|
|
93
93
|
type: "channel_unsubscribe";
|
|
94
94
|
}): void;
|
|
95
95
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-subscriptions.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/channel-subscriptions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,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,EAA4B,
|
|
1
|
+
{"version":3,"file":"channel-subscriptions.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/channel-subscriptions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,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,EAA4B,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAClF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAa9C;;;;;;;GAOG;AACH,MAAM,WAAW,mCAAmC;IAClD;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;;;;;;OAUG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;CACH;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,mCAAmC,GAAG,SAAS,CAAC;IACrE;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC3D,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,sBAAsB,CACpB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,mBAAmB,CAAA;KAAE,GACxD,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;;;OAKG;IACH,wBAAwB,CACtB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,qBAAqB,CAAA;KAAE,GAC1D,IAAI,CAAC;CACT;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,wBAAwB,GAAG,oBAAoB,CAiQ/F"}
|
|
@@ -5,14 +5,44 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { AuthResult, BufferedStreamEnvelope } from "@ggui-ai/mcp-server-core";
|
|
7
7
|
import type { JsonObject } from "@ggui-ai/protocol";
|
|
8
|
+
import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
|
|
8
9
|
import type { WebSocket } from "ws";
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Transport-neutral write surface for one subscriber's outbound plane.
|
|
12
|
+
* The fan-out walks (`props_update` / `render` / `drain_ack` / external
|
|
13
|
+
* broadcasts), the live-tail pump, and the subscribe tail all write
|
|
14
|
+
* through this seam so a subscriber can ride WS (`createWsSink`) or SSE
|
|
15
|
+
* (`SseSink` in `../api-renders-stream-route.ts`) without per-frame
|
|
16
|
+
* transport branching.
|
|
13
17
|
*/
|
|
14
|
-
export interface
|
|
15
|
-
|
|
18
|
+
export interface SubscriberSink {
|
|
19
|
+
/** WS: `ws.readyState === OPEN`; SSE: `!res.writableEnded && !res.destroyed`. */
|
|
20
|
+
isOpen(): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Write one wire frame. `resumeId` is the durable-cursor stamp for
|
|
23
|
+
* ledger-backed replay frames: SSE emits it as the `id:` field ahead
|
|
24
|
+
* of `data:` (so the browser's `Last-Event-ID` lands on the
|
|
25
|
+
* GguiSessionEvent ledger cursor); WS ignores it — WS resume rides
|
|
26
|
+
* `SubscribePayload.sinceSequence` instead.
|
|
27
|
+
*/
|
|
28
|
+
write(frame: WebSocketMessage, opts?: {
|
|
29
|
+
readonly resumeId?: string;
|
|
30
|
+
}): void;
|
|
31
|
+
/**
|
|
32
|
+
* Terminate the transport. WS maps `service_restart` → close 1012
|
|
33
|
+
* (reconnect immediately — same server family is still up) and
|
|
34
|
+
* `session_expired` → close 1008 (policy violation — do not blind-
|
|
35
|
+
* reconnect); SSE ends the response, letting `EventSource`
|
|
36
|
+
* auto-reconnect hit the HTTP pre-gate for the authoritative verdict.
|
|
37
|
+
*/
|
|
38
|
+
end(reason: "service_restart" | "session_expired"): void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Fields shared by every subscriber regardless of transport (one
|
|
42
|
+
* client, one render). Held live in the channel's flat subscriber set;
|
|
43
|
+
* torn down on transport close or explicit `close` message.
|
|
44
|
+
*/
|
|
45
|
+
interface SubscriberBase {
|
|
16
46
|
readonly sessionId: string;
|
|
17
47
|
readonly appId: string;
|
|
18
48
|
readonly identity: AuthResult;
|
|
@@ -32,7 +62,7 @@ export interface Subscriber {
|
|
|
32
62
|
* Per-subscriber live-tail iterator from `streamFanout.subscribe`.
|
|
33
63
|
* Owned by the subscriber for its full lifetime; ending it (via
|
|
34
64
|
* `iter.return()`) terminates the pump loop AND unregisters from
|
|
35
|
-
* the StreamFanout. `unregister(
|
|
65
|
+
* the StreamFanout. `unregister(sub)` is the single point that
|
|
36
66
|
* does this teardown.
|
|
37
67
|
*/
|
|
38
68
|
readonly iter: AsyncIterator<BufferedStreamEnvelope>;
|
|
@@ -41,15 +71,38 @@ export interface Subscriber {
|
|
|
41
71
|
* Keyed by `${sessionId}:${channelName}` so a reconnect that
|
|
42
72
|
* re-subscribes to the same (render, channel) pair replaces the
|
|
43
73
|
* existing timer rather than minting a duplicate (idempotent
|
|
44
|
-
* semantics on the wire). Torn down en masse by `unregister(
|
|
45
|
-
*
|
|
74
|
+
* semantics on the wire). Torn down en masse by `unregister(sub)`
|
|
75
|
+
* on transport close.
|
|
46
76
|
*
|
|
47
77
|
* Populated by the `channel_subscribe` handler when the composing
|
|
48
78
|
* host wired a `streamWebSocketLocalTools` allowlist; empty
|
|
49
|
-
* otherwise.
|
|
79
|
+
* otherwise. SSE subscribers have no inbound channel, so theirs
|
|
80
|
+
* stays empty structurally.
|
|
50
81
|
*/
|
|
51
82
|
readonly channelSubs: Map<string, ChannelSubscriptionState>;
|
|
83
|
+
/** Outbound write surface — the ONLY transport coupling on the fan-out path. */
|
|
84
|
+
readonly sink: SubscriberSink;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* WebSocket-attached subscriber. The `ws` handle stays for the inbound
|
|
88
|
+
* / control planes (socket-router dispatch, action acks, subscribe
|
|
89
|
+
* rejects) which remain WS-only — SSE has no inbound channel.
|
|
90
|
+
*/
|
|
91
|
+
export interface WsSubscriber extends SubscriberBase {
|
|
92
|
+
readonly transport: "ws";
|
|
93
|
+
readonly ws: WebSocket;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* SSE-attached subscriber (`GET /api/sessions/:sessionId/stream`),
|
|
97
|
+
* registered via `GguiSessionChannelServer.attachExternalSubscriber`.
|
|
98
|
+
* Outbound-only: it shares the fan-out planes with WS subscribers but
|
|
99
|
+
* never appears in the ws→subscriber reverse index.
|
|
100
|
+
*/
|
|
101
|
+
export interface SseSubscriber extends SubscriberBase {
|
|
102
|
+
readonly transport: "sse";
|
|
52
103
|
}
|
|
104
|
+
/** A single connected subscriber, discriminated on `transport`. */
|
|
105
|
+
export type Subscriber = WsSubscriber | SseSubscriber;
|
|
53
106
|
/**
|
|
54
107
|
* Per-(subscriber, sessionId, channelName) polling-loop state.
|
|
55
108
|
* Created on `channel_subscribe` accept, torn down on `channel_unsubscribe`
|
|
@@ -99,4 +152,5 @@ export interface UpgradeBindings {
|
|
|
99
152
|
appId: string;
|
|
100
153
|
};
|
|
101
154
|
}
|
|
155
|
+
export {};
|
|
102
156
|
//# sourceMappingURL=internal-types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal-types.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/internal-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC
|
|
1
|
+
{"version":3,"file":"internal-types.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/internal-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,MAAM,IAAI,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5E;;;;;;OAMG;IACH,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,IAAI,CAAC;CAC1D;AAED;;;;GAIG;AACH,UAAU,cAAc;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;;;;;OASG;IACH,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,sBAAsB,CAAC,CAAC;IACrD;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;IAC5D,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;CAC3B;AAED,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,aAAa,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,wBAAwB;IACvC,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;CAChD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B,iBAAiB,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1D"}
|
|
@@ -16,7 +16,7 @@ import type { ErrorPayload, GguiSession, JsonObject, ReservedChannelValidator }
|
|
|
16
16
|
import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
|
|
17
17
|
import type { WebSocket } from "ws";
|
|
18
18
|
import type { Logger } from "../logger.js";
|
|
19
|
-
import type { Subscriber } from "./internal-types.js";
|
|
19
|
+
import type { Subscriber, SubscriberSink } from "./internal-types.js";
|
|
20
20
|
/** Channel-scoped error codes carried on `channel_error` frames. */
|
|
21
21
|
export type ChannelErrorCode = "CHANNEL_UNKNOWN" | "CHANNEL_NOT_LOCAL" | "SESSION_NOT_FOUND" | "SUBSCRIBE_UNAUTHORIZED" | "POLL_FAILED";
|
|
22
22
|
export interface OutboundDeps {
|
|
@@ -28,10 +28,11 @@ export interface OutboundDeps {
|
|
|
28
28
|
/** Live-tail pub/sub seam for stream-envelope fan-out. */
|
|
29
29
|
readonly streamFanout: StreamFanout;
|
|
30
30
|
/**
|
|
31
|
-
* Flat set of all live WS
|
|
31
|
+
* Flat set of all live subscribers (WS + SSE) — shared with the
|
|
32
32
|
* subscriber-lifecycle module (which owns membership); read-only
|
|
33
|
-
* here. Direct
|
|
34
|
-
* external broadcasts) iterate + filter by `sessionId
|
|
33
|
+
* here. Direct frames (`props_update`, `render`, `drain_ack`,
|
|
34
|
+
* external broadcasts) iterate + filter by `sessionId`, writing
|
|
35
|
+
* through each subscriber's transport-neutral `sink`.
|
|
35
36
|
*/
|
|
36
37
|
readonly wsSubscribers: ReadonlySet<Subscriber>;
|
|
37
38
|
/**
|
|
@@ -41,6 +42,15 @@ export interface OutboundDeps {
|
|
|
41
42
|
*/
|
|
42
43
|
readonly extraReservedValidators?: ReadonlyMap<string, ReservedChannelValidator>;
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* WS-backed {@link SubscriberSink}. `write` wraps the existing send
|
|
47
|
+
* guard (readyState check + warn-log on failure); `resumeId` is ignored
|
|
48
|
+
* — WS resume rides `SubscribePayload.sinceSequence`, not a per-frame
|
|
49
|
+
* id. `end` maps `service_restart` → 1012 (RFC 6455 "Service Restart":
|
|
50
|
+
* reconnect immediately) and `session_expired` → 1008 (policy
|
|
51
|
+
* violation: do not blind-reconnect).
|
|
52
|
+
*/
|
|
53
|
+
export declare function createWsSink(ws: WebSocket, logger: Logger): SubscriberSink;
|
|
44
54
|
/** The send/fan-out helpers, bound to one channel instance. */
|
|
45
55
|
export interface Outbound {
|
|
46
56
|
/** Low-level wire write — skips closed sockets, warn-logs send failures. */
|
|
@@ -66,7 +76,7 @@ export interface Outbound {
|
|
|
66
76
|
/** Impl behind {@link GguiSessionChannelServer.notifyGguiSessionCommit}. */
|
|
67
77
|
notifyGguiSessionCommit(sessionId: string, render: GguiSession, matchType?: string): void;
|
|
68
78
|
/** Impl behind {@link GguiSessionChannelServer.sendPropsUpdate}. */
|
|
69
|
-
sendPropsUpdate(sessionId: string, props: JsonObject): Promise<void>;
|
|
79
|
+
sendPropsUpdate(sessionId: string, props: JsonObject, epoch: number): Promise<void>;
|
|
70
80
|
/** Impl behind {@link GguiSessionChannelServer.sendDrainAck}. */
|
|
71
81
|
sendDrainAck(args: {
|
|
72
82
|
readonly sessionId: string;
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"outbound.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/outbound.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,16 +12,48 @@
|
|
|
12
12
|
* inline comments here cover impl-level decisions only.
|
|
13
13
|
*/
|
|
14
14
|
import { assertStreamContract } from "@ggui-ai/mcp-server-handlers/renders";
|
|
15
|
+
/**
|
|
16
|
+
* Low-level ws wire write shared by the closure-bound `send` helper and
|
|
17
|
+
* {@link createWsSink} — skips closed sockets, warn-logs send failures.
|
|
18
|
+
* One definition so the guard cannot drift between the two surfaces.
|
|
19
|
+
*/
|
|
20
|
+
function wsWrite(ws, logger, msg) {
|
|
21
|
+
if (ws.readyState !== ws.OPEN)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
ws.send(JSON.stringify(msg));
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
logger.warn("render_channel_send_failed", { error: String(err) });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* WS-backed {@link SubscriberSink}. `write` wraps the existing send
|
|
32
|
+
* guard (readyState check + warn-log on failure); `resumeId` is ignored
|
|
33
|
+
* — WS resume rides `SubscribePayload.sinceSequence`, not a per-frame
|
|
34
|
+
* id. `end` maps `service_restart` → 1012 (RFC 6455 "Service Restart":
|
|
35
|
+
* reconnect immediately) and `session_expired` → 1008 (policy
|
|
36
|
+
* violation: do not blind-reconnect).
|
|
37
|
+
*/
|
|
38
|
+
export function createWsSink(ws, logger) {
|
|
39
|
+
return {
|
|
40
|
+
isOpen: () => ws.readyState === ws.OPEN,
|
|
41
|
+
write: (frame) => {
|
|
42
|
+
wsWrite(ws, logger, frame);
|
|
43
|
+
},
|
|
44
|
+
end: (reason) => {
|
|
45
|
+
try {
|
|
46
|
+
ws.close(reason === "service_restart" ? 1012 : 1008, reason);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* best-effort — socket may already be closing */
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
15
54
|
export function createOutbound(deps) {
|
|
16
55
|
function send(ws, msg) {
|
|
17
|
-
|
|
18
|
-
return;
|
|
19
|
-
try {
|
|
20
|
-
ws.send(JSON.stringify(msg));
|
|
21
|
-
}
|
|
22
|
-
catch (err) {
|
|
23
|
-
deps.logger.warn("render_channel_send_failed", { error: String(err) });
|
|
24
|
-
}
|
|
56
|
+
wsWrite(ws, deps.logger, msg);
|
|
25
57
|
}
|
|
26
58
|
function sendError(ws, code, message, requestId, details) {
|
|
27
59
|
send(ws, {
|
|
@@ -88,20 +120,22 @@ export function createOutbound(deps) {
|
|
|
88
120
|
// `notifyGguiSessionCommit` JSDoc on the public interface for why
|
|
89
121
|
// fresh subscribers rely on `ack.render` instead of a replay frame.
|
|
90
122
|
// NOT routed through StreamFanout either — `type: 'render'` is a
|
|
91
|
-
// distinct WebSocket message type. Filter the flat
|
|
123
|
+
// distinct WebSocket message type. Filter the flat subscriber
|
|
92
124
|
// set by sessionId; N is typically 1-2 (multi-tab render sharing).
|
|
125
|
+
// `sink.write` is the transport-neutral seam — WS and SSE
|
|
126
|
+
// subscribers receive the identical frame.
|
|
93
127
|
const payload = matchType !== undefined ? { session: render, matchType } : { session: render };
|
|
94
128
|
for (const sub of deps.wsSubscribers) {
|
|
95
129
|
if (sub.sessionId !== sessionId)
|
|
96
130
|
continue;
|
|
97
|
-
|
|
131
|
+
sub.sink.write({ type: "render", payload });
|
|
98
132
|
}
|
|
99
133
|
}
|
|
100
134
|
/**
|
|
101
135
|
* Best-effort + orphan-tolerant per the docstring on the public
|
|
102
136
|
* `sendPropsUpdate` method.
|
|
103
137
|
*/
|
|
104
|
-
async function sendPropsUpdate(sessionId, props) {
|
|
138
|
+
async function sendPropsUpdate(sessionId, props, epoch) {
|
|
105
139
|
let stored;
|
|
106
140
|
try {
|
|
107
141
|
stored = await deps.renderStore.get(sessionId);
|
|
@@ -119,29 +153,35 @@ export function createOutbound(deps) {
|
|
|
119
153
|
});
|
|
120
154
|
return;
|
|
121
155
|
}
|
|
122
|
-
// Filter the flat
|
|
123
|
-
// `notifyGguiSessionCommit`. `
|
|
124
|
-
// and logs (but doesn't throw on) per-subscriber
|
|
125
|
-
// the calling handler can't be made to fail by a
|
|
156
|
+
// Filter the flat subscriber set by sessionId; same posture as
|
|
157
|
+
// `notifyGguiSessionCommit`. `sink.write` already silently skips
|
|
158
|
+
// closed transports and logs (but doesn't throw on) per-subscriber
|
|
159
|
+
// send failures, so the calling handler can't be made to fail by a
|
|
160
|
+
// dead transport.
|
|
161
|
+
// Freeze-latch epoch (#483): the caller passes the COMMIT-TIME
|
|
162
|
+
// epoch — an update fans its freshly-advanced value, an amend the
|
|
163
|
+
// unchanged head. Deliberately not re-read from the row here: a
|
|
164
|
+
// re-read races a concurrent update and could stamp an amend's
|
|
165
|
+
// frame with a newer epoch, freezing the live head it belongs to.
|
|
126
166
|
for (const sub of deps.wsSubscribers) {
|
|
127
167
|
if (sub.sessionId !== sessionId)
|
|
128
168
|
continue;
|
|
129
|
-
|
|
169
|
+
sub.sink.write({
|
|
130
170
|
type: "props_update",
|
|
131
|
-
payload: { sessionId, props },
|
|
171
|
+
payload: { sessionId, props, epoch },
|
|
132
172
|
});
|
|
133
173
|
}
|
|
134
174
|
}
|
|
135
175
|
function sendDrainAck({ sessionId, appId, eventId, drainedAt, }) {
|
|
136
176
|
// Server-side fan-out for the action-drain ack.
|
|
137
|
-
// Filter the flat
|
|
177
|
+
// Filter the flat subscriber set by sessionId (same posture
|
|
138
178
|
// as `sendPropsUpdate`). No persistence; subscribers that
|
|
139
179
|
// missed the frame fall back to their 10s claim timer, which
|
|
140
180
|
// the atomic pop resolves cleanly.
|
|
141
181
|
for (const sub of deps.wsSubscribers) {
|
|
142
182
|
if (sub.sessionId !== sessionId)
|
|
143
183
|
continue;
|
|
144
|
-
|
|
184
|
+
sub.sink.write({
|
|
145
185
|
type: "drain_ack",
|
|
146
186
|
payload: { sessionId, appId, eventId, drainedAt },
|
|
147
187
|
});
|
|
@@ -149,10 +189,10 @@ export function createOutbound(deps) {
|
|
|
149
189
|
}
|
|
150
190
|
function externalBroadcast(sessionId, frame) {
|
|
151
191
|
// Walk the flat subscriber set; filter to matching sessionId.
|
|
152
|
-
// `
|
|
153
|
-
// throw on) per-subscriber failures, so the caller (an
|
|
154
|
-
// pubsub on-message handler) can't be made to fail by a
|
|
155
|
-
//
|
|
192
|
+
// `sink.write` already guards closed transports and logs (but
|
|
193
|
+
// doesn't throw on) per-subscriber failures, so the caller (an
|
|
194
|
+
// external pubsub on-message handler) can't be made to fail by a
|
|
195
|
+
// dead transport. No GguiSessionStore lookup — the publisher already
|
|
156
196
|
// validated; this seam is the cross-process delivery path, not
|
|
157
197
|
// the re-validation point.
|
|
158
198
|
for (const sub of deps.wsSubscribers) {
|
|
@@ -170,7 +210,7 @@ export function createOutbound(deps) {
|
|
|
170
210
|
frame.payload.seq <= sub.replayCompletedSeq) {
|
|
171
211
|
continue;
|
|
172
212
|
}
|
|
173
|
-
|
|
213
|
+
sub.sink.write(frame);
|
|
174
214
|
}
|
|
175
215
|
}
|
|
176
216
|
return {
|
|
@@ -10,7 +10,7 @@ import type { WebSocket, WebSocketServer } from "ws";
|
|
|
10
10
|
import type { Logger } from "../logger.js";
|
|
11
11
|
import type { ActionIngress } from "./action-ingress.js";
|
|
12
12
|
import type { ChannelSubscriptions } from "./channel-subscriptions.js";
|
|
13
|
-
import type {
|
|
13
|
+
import type { WsSubscriber } 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";
|
|
@@ -47,8 +47,13 @@ export interface SocketRouterDeps {
|
|
|
47
47
|
readonly logger: Logger;
|
|
48
48
|
/** GguiSession backing store — observation-message patches persist here. */
|
|
49
49
|
readonly renderStore: GguiSessionStore;
|
|
50
|
-
/**
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* ws → subscriber reverse index — the dispatcher's per-frame lookup.
|
|
52
|
+
* WS-only by construction (the lifecycle module populates it for
|
|
53
|
+
* `transport: 'ws'` subscribers only); SSE subscribers have no
|
|
54
|
+
* inbound socket to route.
|
|
55
|
+
*/
|
|
56
|
+
readonly subscribersByWs: WeakMap<WebSocket, WsSubscriber>;
|
|
52
57
|
readonly send: Outbound["send"];
|
|
53
58
|
readonly sendError: Outbound["sendError"];
|
|
54
59
|
readonly unregister: SubscriberLifecycle["unregister"];
|
|
@@ -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,
|
|
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,EAAmB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACzE,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;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC3D,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,CAuarF"}
|