@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/build-mcp.d.ts
CHANGED
|
@@ -60,6 +60,16 @@ export interface BuildMcpServerOptions {
|
|
|
60
60
|
* host that owns the iframe CSP itself.
|
|
61
61
|
*/
|
|
62
62
|
readonly publicBaseUrl?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Live-channel origins for the static shell's CSP declaration —
|
|
65
|
+
* forwarded to `installMcpAppsOutbound`. Deployments that set no
|
|
66
|
+
* `publicBaseUrl` (the cloud pod) pass their `wsUrl` + its ws→http
|
|
67
|
+
* origin flip here so the mounted iframe's `connect-src` covers the
|
|
68
|
+
* SSE / HTTP-polling session API and the WebSocket; otherwise
|
|
69
|
+
* cross-origin hosts CSP-block every network rung of the failover
|
|
70
|
+
* ladder (#471 round 11).
|
|
71
|
+
*/
|
|
72
|
+
readonly extraConnectUrls?: readonly (string | undefined)[];
|
|
63
73
|
/**
|
|
64
74
|
* Identity-kind allowlist for tool registration. When set, handlers
|
|
65
75
|
* whose `allowedFor` field is non-empty AND does NOT intersect this
|
|
@@ -116,13 +126,44 @@ export interface BuildMcpServerOptions {
|
|
|
116
126
|
* `resources/read` later.
|
|
117
127
|
*/
|
|
118
128
|
readonly extraResources?: ReadonlyArray<(server: McpServer) => void>;
|
|
129
|
+
/**
|
|
130
|
+
* Withhold every handler's per-result bootstrap MATERIAL from tool
|
|
131
|
+
* results — the read-plane-only posture.
|
|
132
|
+
*
|
|
133
|
+
* By default a successful tool result carries the handler's
|
|
134
|
+
* `resultMeta` — for `ggui_render` / `ggui_update` that is the
|
|
135
|
+
* `ai.ggui/render` bootstrap a host may mount DIRECTLY without any
|
|
136
|
+
* further round-trip, plus the spec-canonical pointer `_meta.ui.
|
|
137
|
+
* resourceUri`. Setting this makes the server publish only the
|
|
138
|
+
* durable IDENTITY: `structuredContent.resourceUri` (the `ui://`
|
|
139
|
+
* locator) and — the same value, on the wire slot MCP Apps hosts
|
|
140
|
+
* read — `_meta.ui.resourceUri` (+ the legacy flat `ui/resourceUri`).
|
|
141
|
+
* `resultMeta` is never invoked (no bootstrap token is minted for a
|
|
142
|
+
* slice nobody receives); the pointer is derived from the validated
|
|
143
|
+
* OUTPUT itself, the single source of truth `resultMeta` reuses.
|
|
144
|
+
* A host MUST resolve every view by an authenticated `resources/read`
|
|
145
|
+
* — the persisted-locator path — before it can mount anything.
|
|
146
|
+
*
|
|
147
|
+
* Why the pointer stays (ggui#537): the identity IS the pointer.
|
|
148
|
+
* Spec-canonical hosts (claude.ai, Claude Desktop, `@ggui-ai/
|
|
149
|
+
* mcp-apps-react`'s chat-helpers, the OSS samples) mount the
|
|
150
|
+
* per-render self-contained shell that `_meta.ui.resourceUri` names —
|
|
151
|
+
* a `resources/read` the HOST performs, exactly the read-plane path
|
|
152
|
+
* this posture wants. The first arm (f8c93405d) stripped `_meta`
|
|
153
|
+
* wholesale, which took the pointer with it and left every such host
|
|
154
|
+
* with the declaration-level static shell and a result it could not
|
|
155
|
+
* mount from ("Waiting for tool result…", prod 2026-08-16→17).
|
|
156
|
+
*
|
|
157
|
+
* That is a deployment posture, not a debug switch: a hosted
|
|
158
|
+
* deployment that wants "views mount only through the read plane"
|
|
159
|
+
* (thread-scoped ownership checks, fresh per-read credentials, no
|
|
160
|
+
* inlined bootstrap material crossing a chat transcript) states it
|
|
161
|
+
* here by construction, and any host that still expects the inlined
|
|
162
|
+
* bootstrap fails loudly (a locator it cannot resolve) instead of
|
|
163
|
+
* silently mounting stale material. `structuredContent` and `content`
|
|
164
|
+
* are untouched; only `_meta` is withheld.
|
|
165
|
+
*/
|
|
166
|
+
readonly withholdResultMeta?: boolean;
|
|
119
167
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Build a fresh MCP server with every handler registered.
|
|
122
|
-
*
|
|
123
|
-
* `getContext` is a late-binding accessor so the HTTP layer can thread
|
|
124
|
-
* per-request context (via AsyncLocalStorage or a closure) without
|
|
125
|
-
* leaking the shape into this module.
|
|
126
|
-
*/
|
|
127
168
|
export declare function buildMcpServer(info: ServerInfo, handlers: ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>, getContext: () => HandlerContext, logger: Logger, opts?: BuildMcpServerOptions): McpServer;
|
|
128
169
|
//# sourceMappingURL=build-mcp.d.ts.map
|
package/dist/build-mcp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-mcp.d.ts","sourceRoot":"","sources":["../src/build-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"build-mcp.d.ts","sourceRoot":"","sources":["../src/build-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAEL,KAAK,iCAAiC,EACvC,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iCAAiC,CAAC;IAC3D;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAC5D;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IAElE;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC,CAAC;IAErE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CACvC;AAyBD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,EAChE,UAAU,EAAE,MAAM,cAAc,EAChC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,qBAA0B,GAC/B,SAAS,CAwPX"}
|
package/dist/build-mcp.js
CHANGED
|
@@ -13,6 +13,7 @@ import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
|
|
|
13
13
|
import { isRecord } from '@ggui-ai/protocol';
|
|
14
14
|
import { z } from 'zod';
|
|
15
15
|
import { isHandlerFailure, } from '@ggui-ai/mcp-server-handlers';
|
|
16
|
+
import { GGUI_RENDER_RESOURCE_URI } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
16
17
|
import { installMcpAppsOutbound, } from './mcp-apps-outbound.js';
|
|
17
18
|
/**
|
|
18
19
|
* Build a fresh MCP server with every handler registered.
|
|
@@ -21,6 +22,23 @@ import { installMcpAppsOutbound, } from './mcp-apps-outbound.js';
|
|
|
21
22
|
* per-request context (via AsyncLocalStorage or a closure) without
|
|
22
23
|
* leaking the shape into this module.
|
|
23
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* The identity-only `_meta` a withholding server publishes (ggui#537):
|
|
27
|
+
* when the validated output carries a `ui://` `resourceUri` (the durable
|
|
28
|
+
* locator `ggui_render` / `ggui_update` surface on structuredContent),
|
|
29
|
+
* mirror it onto the spec-canonical `_meta.ui.resourceUri` slot MCP
|
|
30
|
+
* Apps hosts read (+ the legacy flat key), and nothing else. Same value
|
|
31
|
+
* `resultMeta` would have stamped, without invoking it — no bootstrap
|
|
32
|
+
* material, no minted token. `undefined` for outputs without a locator.
|
|
33
|
+
*/
|
|
34
|
+
function identityPointerMeta(validated) {
|
|
35
|
+
if (!isRecord(validated))
|
|
36
|
+
return undefined;
|
|
37
|
+
const uri = validated['resourceUri'];
|
|
38
|
+
if (typeof uri !== 'string' || !uri.startsWith('ui://'))
|
|
39
|
+
return undefined;
|
|
40
|
+
return { ui: { resourceUri: uri }, 'ui/resourceUri': uri };
|
|
41
|
+
}
|
|
24
42
|
export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
25
43
|
const server = new McpServer({
|
|
26
44
|
name: info.name,
|
|
@@ -28,8 +46,13 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
28
46
|
...(info.description ? { description: info.description } : {}),
|
|
29
47
|
...(opts.instructions ? { instructions: opts.instructions } : {}),
|
|
30
48
|
});
|
|
49
|
+
// Content-addressed shell URI (stale-shell bust) — when MCP Apps
|
|
50
|
+
// outbound wiring registers the shell, declarations advertising the
|
|
51
|
+
// BARE `ui://ggui/render` are rewritten to the versioned twin below
|
|
52
|
+
// so host prefetch caches key on content, not on a constant string.
|
|
53
|
+
let shellResourceUri;
|
|
31
54
|
if (opts.mcpAppsOutbound) {
|
|
32
|
-
installMcpAppsOutbound(server, {
|
|
55
|
+
({ shellResourceUri } = installMcpAppsOutbound(server, {
|
|
33
56
|
...(opts.shellHtml !== undefined ? { shellHtml: opts.shellHtml } : {}),
|
|
34
57
|
// Thread the same per-request context accessor + logger the tool
|
|
35
58
|
// path uses (`getContext`, param 3 of `buildMcpServer`) so the
|
|
@@ -41,7 +64,10 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
41
64
|
...(opts.publicBaseUrl !== undefined
|
|
42
65
|
? { publicBaseUrl: opts.publicBaseUrl }
|
|
43
66
|
: {}),
|
|
44
|
-
|
|
67
|
+
...(opts.extraConnectUrls !== undefined
|
|
68
|
+
? { extraConnectUrls: opts.extraConnectUrls }
|
|
69
|
+
: {}),
|
|
70
|
+
}));
|
|
45
71
|
}
|
|
46
72
|
// Per-request resource registrars supplied by the host. Run BEFORE
|
|
47
73
|
// tool registration so `tools/list` ordering is unaffected and any
|
|
@@ -119,17 +145,64 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
119
145
|
const validated = z.object(handler.outputSchema).parse(data);
|
|
120
146
|
// Per-result `_meta` — NOT merged into structuredContent, so
|
|
121
147
|
// agents that typecheck against the tool signature never see
|
|
122
|
-
// it. This is where view-only bootstrap material lives.
|
|
123
|
-
|
|
148
|
+
// it. This is where view-only bootstrap material lives. Under
|
|
149
|
+
// the withhold posture only the identity pointer is published,
|
|
150
|
+
// derived from the output (see `withholdResultMeta`).
|
|
151
|
+
const meta = opts.withholdResultMeta === true
|
|
152
|
+
? identityPointerMeta(validated)
|
|
153
|
+
: await handler.resultMeta?.(data, input, ctx);
|
|
124
154
|
logger.info('tool_invoked', {
|
|
125
155
|
tool: handler.name,
|
|
126
156
|
appId: ctx.appId,
|
|
127
157
|
outcome: 'success',
|
|
128
158
|
elapsedMs: Date.now() - start,
|
|
129
159
|
});
|
|
160
|
+
// When the handler's output carries a `nextStep`, lead the
|
|
161
|
+
// model-visible content with the imperative in PLAIN TEXT.
|
|
162
|
+
// Burying the chain cue inside the JSON block proved fragile
|
|
163
|
+
// on live hosts (the first claude.ai #471 test: the agent
|
|
164
|
+
// rendered, never noticed `nextStep`, ended its turn, and the
|
|
165
|
+
// user's click had no listener). The JSON stays second —
|
|
166
|
+
// structured consumers read `structuredContent` anyway.
|
|
167
|
+
const nextStepHint = validated !== null &&
|
|
168
|
+
typeof validated === 'object' &&
|
|
169
|
+
'nextStep' in validated &&
|
|
170
|
+
validated.nextStep &&
|
|
171
|
+
typeof validated.nextStep
|
|
172
|
+
.example === 'string'
|
|
173
|
+
? validated.nextStep.example
|
|
174
|
+
: undefined;
|
|
175
|
+
// The gesture-poll wrapper below describes ggui_consume's
|
|
176
|
+
// semantics ("catch an immediate gesture", "waits up to 25s")
|
|
177
|
+
// — it is ONLY true when the nextStep IS the consume hint.
|
|
178
|
+
// Ungated, it decorated ggui_handshake results too (whose
|
|
179
|
+
// nextStep is a ggui_render example), telling agents a
|
|
180
|
+
// not-yet-rendered UI "has interactive actions" — a live agent
|
|
181
|
+
// flagged the contradiction against an actions=∅ contract
|
|
182
|
+
// (2026-08-12).
|
|
183
|
+
const gestureHint = nextStepHint !== undefined && nextStepHint.includes('ggui_consume')
|
|
184
|
+
? nextStepHint
|
|
185
|
+
: undefined;
|
|
130
186
|
return {
|
|
131
187
|
structuredContent: validated,
|
|
132
188
|
content: [
|
|
189
|
+
...(gestureHint !== undefined
|
|
190
|
+
? [
|
|
191
|
+
{
|
|
192
|
+
type: 'text',
|
|
193
|
+
// Gentle + bounded, deliberately: a forcing
|
|
194
|
+
// imperative hijacked live agents into polling
|
|
195
|
+
// instead of acting (matrix scenario 6), and an
|
|
196
|
+
// unbounded "re-call on empty" looped them past
|
|
197
|
+
// their turn budget. The poll is a latency
|
|
198
|
+
// optimization, not the delivery guarantee — when
|
|
199
|
+
// nobody is polling, a gesture rings the chat via
|
|
200
|
+
// ui/message and arrives as a new user message
|
|
201
|
+
// carrying its own consume directive.
|
|
202
|
+
text: `The UI has interactive actions. After this turn's work, you may call ${gestureHint} once to catch an immediate gesture (waits up to 25s); if events is empty, end your turn — later gestures arrive as new user messages.`,
|
|
203
|
+
},
|
|
204
|
+
]
|
|
205
|
+
: []),
|
|
133
206
|
{ type: 'text', text: JSON.stringify(validated) },
|
|
134
207
|
],
|
|
135
208
|
...(meta !== undefined ? { _meta: meta } : {}),
|
|
@@ -164,11 +237,19 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
164
237
|
// error on the handler author's side — fail loud rather than
|
|
165
238
|
// silently registering the tool without its UI surface.
|
|
166
239
|
if (handler._meta && 'ui' in handler._meta) {
|
|
167
|
-
const
|
|
168
|
-
if (!isMcpUiToolMeta(
|
|
240
|
+
const uiRaw = handler._meta['ui'];
|
|
241
|
+
if (!isMcpUiToolMeta(uiRaw)) {
|
|
169
242
|
throw new Error(`Tool ${handler.name} declares _meta.ui with an invalid shape — ` +
|
|
170
243
|
`expected { resourceUri?: string; visibility?: ('model' | 'app')[] }.`);
|
|
171
244
|
}
|
|
245
|
+
// Declarations author the STABLE `ui://ggui/render` constant;
|
|
246
|
+
// registration swaps in the content-addressed twin so hosts
|
|
247
|
+
// prefetch (and cache) the shell by its content hash. Handlers
|
|
248
|
+
// stay host-cache-agnostic; the swap lives in ONE place.
|
|
249
|
+
const ui = shellResourceUri !== undefined &&
|
|
250
|
+
uiRaw.resourceUri === GGUI_RENDER_RESOURCE_URI
|
|
251
|
+
? { ...uiRaw, resourceUri: shellResourceUri }
|
|
252
|
+
: uiRaw;
|
|
172
253
|
registerAppTool(server, handler.name, {
|
|
173
254
|
...baseConfig,
|
|
174
255
|
_meta: { ...handler._meta, ui },
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict-CSP module-variant delivery (ggui#522 slice 2).
|
|
3
|
+
*
|
|
4
|
+
* The `/code/<hash>.js` route serves STORED component bytes — compiled
|
|
5
|
+
* ESM whose imports are still BARE specifiers (`react`,
|
|
6
|
+
* `@ggui-ai/design/primitives`, …). A browser cannot import that
|
|
7
|
+
* standalone; the renderer historically bridged the gap in-frame by
|
|
8
|
+
* rewriting bare specifiers to `data:` shims and instantiating the
|
|
9
|
+
* module from a `blob:` URL — both of which a strict host CSP refuses.
|
|
10
|
+
*
|
|
11
|
+
* This module adds the server-side twin of that bridge: a SECOND,
|
|
12
|
+
* immutable route `/code/<hash>.m<rt>.js` serving the SAME stored bytes
|
|
13
|
+
* with the rewrite already applied — every bare specifier resolved to a
|
|
14
|
+
* static shim asset under `/_ggui/shims/<rt>/<name>.js` on the same
|
|
15
|
+
* origin. A frame whose `script-src` allows only the asset origin can
|
|
16
|
+
* `import()` the variant URL directly: no `blob:`, no `data:`, no
|
|
17
|
+
* `'unsafe-eval'`.
|
|
18
|
+
*
|
|
19
|
+
* `<rt>` — the 12-hex content hash of the runtime bundle — is the
|
|
20
|
+
* variant KEY, and it lives in the PATH deliberately: the asset CDN
|
|
21
|
+
* (CloudFront `CACHING_OPTIMIZED`) ignores query strings, so a
|
|
22
|
+
* query-keyed variant would collapse to one cached body across
|
|
23
|
+
* runtime versions. The hash versions the whole family: the shim
|
|
24
|
+
* files ship in the runtime bundle's dist, so any shim-affecting
|
|
25
|
+
* change also changes the bundle bytes, which changes `<rt>`, which
|
|
26
|
+
* changes every variant URL — immutable caching stays sound.
|
|
27
|
+
*
|
|
28
|
+
* The route serves only the CURRENT `<rt>`: after a redeploy, a
|
|
29
|
+
* CDN-evicted old-variant URL 404s (`no-store`) and the renderer falls
|
|
30
|
+
* back to its raw-bytes ladder — honest degradation, never stale shims.
|
|
31
|
+
*
|
|
32
|
+
* 3rd-party gadget packages (GG.8.2) have no static shim (their
|
|
33
|
+
* exports are derived per-render from the generated code); a module
|
|
34
|
+
* importing one is NOT asset-deliverable. Both ends respect that: the
|
|
35
|
+
* minter declines to stamp `codeModuleUrl`, and the route declines
|
|
36
|
+
* (404) any body whose rewrite leaves a bare specifier standing.
|
|
37
|
+
*/
|
|
38
|
+
import type { Express } from "express";
|
|
39
|
+
import type { Logger } from "./logger.js";
|
|
40
|
+
/**
|
|
41
|
+
* Mint the `codeModuleUrl` for one render's component code, or decline.
|
|
42
|
+
*
|
|
43
|
+
* `undefined` ⇒ the code is not asset-deliverable (it imports a
|
|
44
|
+
* package with no static shim) — the slice then carries only the raw
|
|
45
|
+
* `codeUrl`/`codeB64` carriers and the renderer uses its blob ladder.
|
|
46
|
+
*/
|
|
47
|
+
export type MintCodeModuleUrl = (args: {
|
|
48
|
+
/** RAW stored component code (markers included — stripped here). */
|
|
49
|
+
readonly code: string;
|
|
50
|
+
/** `sha256(code)` — the same hash `codeUrl` is composed from. */
|
|
51
|
+
readonly hash: string;
|
|
52
|
+
/** Code base URL, no trailing slash (the emitter already trims). */
|
|
53
|
+
readonly base: string;
|
|
54
|
+
}) => string | undefined;
|
|
55
|
+
/** The variant URL for (hash, rt) under `base` — ONE composition site. */
|
|
56
|
+
export declare function composeCodeModuleUrl(args: {
|
|
57
|
+
readonly base: string;
|
|
58
|
+
readonly hash: string;
|
|
59
|
+
readonly runtimeHash: string;
|
|
60
|
+
}): string;
|
|
61
|
+
/**
|
|
62
|
+
* Build the minter every `codeUrl` emitter injects. Coverability is
|
|
63
|
+
* decided from the code's own import list: every bare specifier must
|
|
64
|
+
* have a static shim ({@link ASSET_SHIM_FOR_SPECIFIER}) or the mint
|
|
65
|
+
* declines. Markers are stripped first — their embedded JSON can
|
|
66
|
+
* contain strings the import scanner would misread.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createCodeModuleUrlMinter(args: {
|
|
69
|
+
readonly runtimeHash: string;
|
|
70
|
+
}): MintCodeModuleUrl;
|
|
71
|
+
/**
|
|
72
|
+
* Rewrite stored component bytes into the servable asset-module
|
|
73
|
+
* variant — the same preprocessing the renderer's blob ladder applies
|
|
74
|
+
* (strip markers, hoist imports) plus the `asset-url` specifier
|
|
75
|
+
* rewrite. Declines when any bare specifier survives (no static shim
|
|
76
|
+
* exists for it) instead of serving a module that fails at eval.
|
|
77
|
+
*/
|
|
78
|
+
export declare function rewriteToAssetModule(args: {
|
|
79
|
+
readonly code: string;
|
|
80
|
+
readonly shimBaseUrl: string;
|
|
81
|
+
}): {
|
|
82
|
+
readonly ok: true;
|
|
83
|
+
readonly code: string;
|
|
84
|
+
} | {
|
|
85
|
+
readonly ok: false;
|
|
86
|
+
readonly leftover: readonly string[];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Read the static shim files emitted by the iframe-runtime build
|
|
90
|
+
* (`dist/shims/<name>.js`) into memory, keyed by file name. Captured
|
|
91
|
+
* ONCE at composition — the immutable shim route must keep serving the
|
|
92
|
+
* exact bytes its URL family was stamped for, even across an in-place
|
|
93
|
+
* rebuild of the dist (same guarantee the hashed bundle route makes by
|
|
94
|
+
* serving captured bytes).
|
|
95
|
+
*
|
|
96
|
+
* `undefined` when the directory is missing/empty (bundle built by an
|
|
97
|
+
* older toolchain) — callers then skip the whole variant family.
|
|
98
|
+
*/
|
|
99
|
+
export declare function captureShimSources(shimsDir: string): ReadonlyMap<string, Buffer> | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Mount `GET <urlPrefix>/:rt/:file` serving the captured shim sources
|
|
102
|
+
* with the immutable posture of the hashed-bundle route. Only the
|
|
103
|
+
* CURRENT `runtimeHash` is served; any other `rt` 404s `no-store` so a
|
|
104
|
+
* stale URL can never lock wrong bytes into a cache.
|
|
105
|
+
*/
|
|
106
|
+
export declare function mountShimRoutes(opts: {
|
|
107
|
+
readonly app: Express;
|
|
108
|
+
readonly urlPrefix: string;
|
|
109
|
+
readonly runtimeHash: string;
|
|
110
|
+
readonly shims: ReadonlyMap<string, Buffer>;
|
|
111
|
+
}): void;
|
|
112
|
+
/**
|
|
113
|
+
* Options for the `/code/<hash>.m<rt>.js` variant route
|
|
114
|
+
* (mounted by `mountCodeRoutes` BEFORE the plain routes).
|
|
115
|
+
*/
|
|
116
|
+
export interface CodeModuleVariantOptions {
|
|
117
|
+
/** 12-hex content hash of the runtime bundle currently served. */
|
|
118
|
+
readonly runtimeHash: string;
|
|
119
|
+
/**
|
|
120
|
+
* Absolute base of THIS build's shim directory
|
|
121
|
+
* (`<codeBaseUrl><urlPrefix>/<runtimeHash>`), no trailing slash —
|
|
122
|
+
* the rewrite embeds it into every import.
|
|
123
|
+
*/
|
|
124
|
+
readonly shimBaseUrl: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* RegExp route for the variant path. A RegExp (not a string pattern)
|
|
128
|
+
* on purpose: two params separated by literal dots inside one path
|
|
129
|
+
* segment sit exactly on path-to-regexp's least-stable parsing ground,
|
|
130
|
+
* and a silent non-match here would read as "variant never works".
|
|
131
|
+
*/
|
|
132
|
+
export declare const CODE_MODULE_VARIANT_ROUTE: RegExp;
|
|
133
|
+
/**
|
|
134
|
+
* Mount the module-variant route. Serving posture mirrors the plain
|
|
135
|
+
* code route (immutable on success, `no-store` on every failure); the
|
|
136
|
+
* two decline arms are:
|
|
137
|
+
*
|
|
138
|
+
* - `rt` ≠ the current runtime hash → 404. The shims for another
|
|
139
|
+
* build are unknowable here; the renderer falls back to raw bytes.
|
|
140
|
+
* - leftover bare specifier after rewrite → 404. The module imports
|
|
141
|
+
* a package with no static shim (3rd-party gadget); serving it
|
|
142
|
+
* would fail at eval inside the frame instead of falling back.
|
|
143
|
+
*/
|
|
144
|
+
export declare function mountCodeModuleVariantRoute(opts: {
|
|
145
|
+
readonly app: Express;
|
|
146
|
+
readonly codeStore: import("@ggui-ai/mcp-server-core").CodeStore;
|
|
147
|
+
readonly variant: CodeModuleVariantOptions;
|
|
148
|
+
readonly logger: Logger;
|
|
149
|
+
}): void;
|
|
150
|
+
//# sourceMappingURL=code-module-variant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-module-variant.d.ts","sourceRoot":"","sources":["../src/code-module-variant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAU1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE;IACrC,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,KAAK,MAAM,GAAG,SAAS,CAAC;AAEzB,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,GAAG,MAAM,CAET;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,GAAG,iBAAiB,CASpB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAQ9G;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GACf,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAkBzC;AAQD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C,GAAG,IAAI,CAyCP;AAED;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,QAAkD,CAAC;AAEzF;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,OAAO,0BAA0B,EAAE,SAAS,CAAC;IACjE,QAAQ,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GAAG,IAAI,CA8DP"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict-CSP module-variant delivery (ggui#522 slice 2).
|
|
3
|
+
*
|
|
4
|
+
* The `/code/<hash>.js` route serves STORED component bytes — compiled
|
|
5
|
+
* ESM whose imports are still BARE specifiers (`react`,
|
|
6
|
+
* `@ggui-ai/design/primitives`, …). A browser cannot import that
|
|
7
|
+
* standalone; the renderer historically bridged the gap in-frame by
|
|
8
|
+
* rewriting bare specifiers to `data:` shims and instantiating the
|
|
9
|
+
* module from a `blob:` URL — both of which a strict host CSP refuses.
|
|
10
|
+
*
|
|
11
|
+
* This module adds the server-side twin of that bridge: a SECOND,
|
|
12
|
+
* immutable route `/code/<hash>.m<rt>.js` serving the SAME stored bytes
|
|
13
|
+
* with the rewrite already applied — every bare specifier resolved to a
|
|
14
|
+
* static shim asset under `/_ggui/shims/<rt>/<name>.js` on the same
|
|
15
|
+
* origin. A frame whose `script-src` allows only the asset origin can
|
|
16
|
+
* `import()` the variant URL directly: no `blob:`, no `data:`, no
|
|
17
|
+
* `'unsafe-eval'`.
|
|
18
|
+
*
|
|
19
|
+
* `<rt>` — the 12-hex content hash of the runtime bundle — is the
|
|
20
|
+
* variant KEY, and it lives in the PATH deliberately: the asset CDN
|
|
21
|
+
* (CloudFront `CACHING_OPTIMIZED`) ignores query strings, so a
|
|
22
|
+
* query-keyed variant would collapse to one cached body across
|
|
23
|
+
* runtime versions. The hash versions the whole family: the shim
|
|
24
|
+
* files ship in the runtime bundle's dist, so any shim-affecting
|
|
25
|
+
* change also changes the bundle bytes, which changes `<rt>`, which
|
|
26
|
+
* changes every variant URL — immutable caching stays sound.
|
|
27
|
+
*
|
|
28
|
+
* The route serves only the CURRENT `<rt>`: after a redeploy, a
|
|
29
|
+
* CDN-evicted old-variant URL 404s (`no-store`) and the renderer falls
|
|
30
|
+
* back to its raw-bytes ladder — honest degradation, never stale shims.
|
|
31
|
+
*
|
|
32
|
+
* 3rd-party gadget packages (GG.8.2) have no static shim (their
|
|
33
|
+
* exports are derived per-render from the generated code); a module
|
|
34
|
+
* importing one is NOT asset-deliverable. Both ends respect that: the
|
|
35
|
+
* minter declines to stamp `codeModuleUrl`, and the route declines
|
|
36
|
+
* (404) any body whose rewrite leaves a bare specifier standing.
|
|
37
|
+
*/
|
|
38
|
+
import fs from "node:fs";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import { rewriteImports, findBareImportSpecifiers, stripMarkers, ASSET_SHIM_FOR_SPECIFIER, } from "@ggui-ai/design/rendering";
|
|
41
|
+
import { hoistImports } from "@ggui-ai/design/module-loader";
|
|
42
|
+
/** The variant URL for (hash, rt) under `base` — ONE composition site. */
|
|
43
|
+
export function composeCodeModuleUrl(args) {
|
|
44
|
+
return `${args.base}/code/${args.hash}.m${args.runtimeHash}.js`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the minter every `codeUrl` emitter injects. Coverability is
|
|
48
|
+
* decided from the code's own import list: every bare specifier must
|
|
49
|
+
* have a static shim ({@link ASSET_SHIM_FOR_SPECIFIER}) or the mint
|
|
50
|
+
* declines. Markers are stripped first — their embedded JSON can
|
|
51
|
+
* contain strings the import scanner would misread.
|
|
52
|
+
*/
|
|
53
|
+
export function createCodeModuleUrlMinter(args) {
|
|
54
|
+
const { runtimeHash } = args;
|
|
55
|
+
return ({ code, hash, base }) => {
|
|
56
|
+
const bare = findBareImportSpecifiers(stripMarkers(code));
|
|
57
|
+
if (!bare.every((spec) => spec in ASSET_SHIM_FOR_SPECIFIER)) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return composeCodeModuleUrl({ base, hash, runtimeHash });
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Rewrite stored component bytes into the servable asset-module
|
|
65
|
+
* variant — the same preprocessing the renderer's blob ladder applies
|
|
66
|
+
* (strip markers, hoist imports) plus the `asset-url` specifier
|
|
67
|
+
* rewrite. Declines when any bare specifier survives (no static shim
|
|
68
|
+
* exists for it) instead of serving a module that fails at eval.
|
|
69
|
+
*/
|
|
70
|
+
export function rewriteToAssetModule(args) {
|
|
71
|
+
const rewritten = rewriteImports(hoistImports(stripMarkers(args.code)), {
|
|
72
|
+
mode: "asset-url",
|
|
73
|
+
shimBaseUrl: args.shimBaseUrl,
|
|
74
|
+
});
|
|
75
|
+
const leftover = findBareImportSpecifiers(rewritten);
|
|
76
|
+
if (leftover.length > 0)
|
|
77
|
+
return { ok: false, leftover };
|
|
78
|
+
return { ok: true, code: rewritten };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Read the static shim files emitted by the iframe-runtime build
|
|
82
|
+
* (`dist/shims/<name>.js`) into memory, keyed by file name. Captured
|
|
83
|
+
* ONCE at composition — the immutable shim route must keep serving the
|
|
84
|
+
* exact bytes its URL family was stamped for, even across an in-place
|
|
85
|
+
* rebuild of the dist (same guarantee the hashed bundle route makes by
|
|
86
|
+
* serving captured bytes).
|
|
87
|
+
*
|
|
88
|
+
* `undefined` when the directory is missing/empty (bundle built by an
|
|
89
|
+
* older toolchain) — callers then skip the whole variant family.
|
|
90
|
+
*/
|
|
91
|
+
export function captureShimSources(shimsDir) {
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = fs.readdirSync(shimsDir);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const out = new Map();
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (!entry.endsWith(".js"))
|
|
102
|
+
continue;
|
|
103
|
+
try {
|
|
104
|
+
out.set(entry, fs.readFileSync(path.join(shimsDir, entry)));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// A vanishing file mid-capture degrades to "that shim 404s";
|
|
108
|
+
// the renderer's fallback ladder covers it.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out.size > 0 ? out : undefined;
|
|
112
|
+
}
|
|
113
|
+
/** File-segment shape of a shim asset (`react.js`, `jsx-runtime.js`). */
|
|
114
|
+
const SHIM_FILE_REGEX = /^[a-z][a-z-]*\.js$/;
|
|
115
|
+
/** 12-hex runtime-hash path segment. */
|
|
116
|
+
const RUNTIME_HASH_REGEX = /^[a-f0-9]{12}$/;
|
|
117
|
+
/**
|
|
118
|
+
* Mount `GET <urlPrefix>/:rt/:file` serving the captured shim sources
|
|
119
|
+
* with the immutable posture of the hashed-bundle route. Only the
|
|
120
|
+
* CURRENT `runtimeHash` is served; any other `rt` 404s `no-store` so a
|
|
121
|
+
* stale URL can never lock wrong bytes into a cache.
|
|
122
|
+
*/
|
|
123
|
+
export function mountShimRoutes(opts) {
|
|
124
|
+
const { app, urlPrefix, runtimeHash, shims } = opts;
|
|
125
|
+
app.get(`${urlPrefix}/:rt/:file`, (req, res) => {
|
|
126
|
+
// Same CORS rationale as the runtime bundle: module fetches from a
|
|
127
|
+
// sandboxed `srcdoc` iframe (`null` origin) always run CORS mode.
|
|
128
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
129
|
+
const rt = req.params["rt"];
|
|
130
|
+
const file = req.params["file"];
|
|
131
|
+
if (typeof rt !== "string" ||
|
|
132
|
+
!RUNTIME_HASH_REGEX.test(rt) ||
|
|
133
|
+
typeof file !== "string" ||
|
|
134
|
+
!SHIM_FILE_REGEX.test(file)) {
|
|
135
|
+
res.setHeader("Cache-Control", "no-store");
|
|
136
|
+
res.status(400).json({
|
|
137
|
+
error: {
|
|
138
|
+
code: "invalid_request",
|
|
139
|
+
message: "shim path must be /<12-hex runtime hash>/<name>.js",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const source = rt === runtimeHash ? shims.get(file) : undefined;
|
|
145
|
+
if (source === undefined) {
|
|
146
|
+
res.setHeader("Cache-Control", "no-store");
|
|
147
|
+
res.status(404).json({
|
|
148
|
+
error: {
|
|
149
|
+
code: "not_found",
|
|
150
|
+
message: rt === runtimeHash
|
|
151
|
+
? "unknown shim module"
|
|
152
|
+
: "shim family not served by this runtime build",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
158
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
159
|
+
res.status(200).send(source);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* RegExp route for the variant path. A RegExp (not a string pattern)
|
|
164
|
+
* on purpose: two params separated by literal dots inside one path
|
|
165
|
+
* segment sit exactly on path-to-regexp's least-stable parsing ground,
|
|
166
|
+
* and a silent non-match here would read as "variant never works".
|
|
167
|
+
*/
|
|
168
|
+
export const CODE_MODULE_VARIANT_ROUTE = /^\/code\/([a-f0-9]{64})\.m([a-f0-9]{12})\.js$/;
|
|
169
|
+
/**
|
|
170
|
+
* Mount the module-variant route. Serving posture mirrors the plain
|
|
171
|
+
* code route (immutable on success, `no-store` on every failure); the
|
|
172
|
+
* two decline arms are:
|
|
173
|
+
*
|
|
174
|
+
* - `rt` ≠ the current runtime hash → 404. The shims for another
|
|
175
|
+
* build are unknowable here; the renderer falls back to raw bytes.
|
|
176
|
+
* - leftover bare specifier after rewrite → 404. The module imports
|
|
177
|
+
* a package with no static shim (3rd-party gadget); serving it
|
|
178
|
+
* would fail at eval inside the frame instead of falling back.
|
|
179
|
+
*/
|
|
180
|
+
export function mountCodeModuleVariantRoute(opts) {
|
|
181
|
+
const { app, codeStore, variant, logger } = opts;
|
|
182
|
+
app.get(CODE_MODULE_VARIANT_ROUTE, async (req, res) => {
|
|
183
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
184
|
+
// RegExp routes surface capture groups as positional params.
|
|
185
|
+
const hash = req.params["0"];
|
|
186
|
+
const rt = req.params["1"];
|
|
187
|
+
if (typeof hash !== "string" || typeof rt !== "string") {
|
|
188
|
+
res.setHeader("Cache-Control", "no-store");
|
|
189
|
+
res.status(400).json({
|
|
190
|
+
error: { code: "invalid_request", message: "malformed variant path" },
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (rt !== variant.runtimeHash) {
|
|
195
|
+
res.setHeader("Cache-Control", "no-store");
|
|
196
|
+
res.status(404).json({
|
|
197
|
+
error: {
|
|
198
|
+
code: "not_found",
|
|
199
|
+
message: "variant family not served by this runtime build",
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const code = await codeStore.get(hash);
|
|
206
|
+
if (code === null) {
|
|
207
|
+
res.setHeader("Cache-Control", "no-store");
|
|
208
|
+
res.status(404).json({
|
|
209
|
+
error: { code: "not_found", message: "unknown code hash" },
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const rewritten = rewriteToAssetModule({
|
|
214
|
+
code,
|
|
215
|
+
shimBaseUrl: variant.shimBaseUrl,
|
|
216
|
+
});
|
|
217
|
+
if (!rewritten.ok) {
|
|
218
|
+
logger.warn("code_module_variant_declined", {
|
|
219
|
+
hash,
|
|
220
|
+
leftover: [...rewritten.leftover],
|
|
221
|
+
});
|
|
222
|
+
res.setHeader("Cache-Control", "no-store");
|
|
223
|
+
res.status(404).json({
|
|
224
|
+
error: {
|
|
225
|
+
code: "not_found",
|
|
226
|
+
message: "module not asset-deliverable (bare imports without static shims)",
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
232
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
233
|
+
res.status(200).send(rewritten.code);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
logger.warn("code_module_variant_failed", { hash, error: String(err) });
|
|
237
|
+
res.setHeader("Cache-Control", "no-store");
|
|
238
|
+
res.status(500).json({
|
|
239
|
+
error: { code: "internal", message: "variant fetch failed" },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
package/dist/code-routes.d.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import type { CodeStore } from "@ggui-ai/mcp-server-core";
|
|
31
31
|
import type { Express } from "express";
|
|
32
32
|
import type { Logger } from "./logger.js";
|
|
33
|
+
import { type CodeModuleVariantOptions } from "./code-module-variant.js";
|
|
33
34
|
interface MountOptions {
|
|
34
35
|
/** Express app to mount onto. */
|
|
35
36
|
readonly app: Express;
|
|
@@ -37,10 +38,19 @@ interface MountOptions {
|
|
|
37
38
|
readonly codeStore: CodeStore;
|
|
38
39
|
/** Structured logger for fetch-failure warnings. */
|
|
39
40
|
readonly logger: Logger;
|
|
41
|
+
/**
|
|
42
|
+
* Strict-CSP module-variant family (`/code/<hash>.m<rt>.js`,
|
|
43
|
+
* ggui#522 slice 2). Present ⇒ the variant route mounts BEFORE the
|
|
44
|
+
* plain routes — ordering is load-bearing: `/code/:hash.js`'s param
|
|
45
|
+
* would otherwise swallow the variant path (`:hash` = `<hash>.m<rt>`)
|
|
46
|
+
* and 400 it at the hash gate.
|
|
47
|
+
*/
|
|
48
|
+
readonly moduleVariant?: CodeModuleVariantOptions;
|
|
40
49
|
}
|
|
41
50
|
/**
|
|
42
|
-
* Mount `GET /code/:hash.js` + `GET /contract/:hash.js`
|
|
43
|
-
*
|
|
51
|
+
* Mount `GET /code/:hash.js` + `GET /contract/:hash.js` (+ the
|
|
52
|
+
* module-variant route when configured) onto the express app. Returns
|
|
53
|
+
* nothing — the routes self-register.
|
|
44
54
|
*/
|
|
45
55
|
export declare function mountCodeRoutes(opts: MountOptions): void;
|
|
46
56
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"code-routes.d.ts","sourceRoot":"","sources":["../src/code-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"code-routes.d.ts","sourceRoot":"","sources":["../src/code-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,0BAA0B,CAAC;AAElC,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,wBAAwB,CAAC;CACnD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAqDxD"}
|