@unoverse-platform/studio 0.1.89 → 0.1.91

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 (132) hide show
  1. package/local/plugin.ts +20 -12
  2. package/package.json +13 -3
  3. package/screens/TemplatesLibraryView.tsx +31 -6
  4. package/screens/TemplatesView.tsx +1 -1
  5. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  6. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  7. package/vendor/base/src/agent-mcp/index.ts +44 -0
  8. package/vendor/base/src/agent-mcp/invoke.ts +140 -0
  9. package/vendor/base/src/agent-mcp/lean.ts +111 -0
  10. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  11. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  12. package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
  13. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  14. package/vendor/base/src/agent-mcp/types.ts +130 -0
  15. package/vendor/base/src/auth/auth.ts +278 -0
  16. package/vendor/base/src/auth/authConfig.ts +73 -0
  17. package/vendor/base/src/boot.ts +245 -0
  18. package/vendor/base/src/definitions/definitions.ts +18 -0
  19. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  20. package/vendor/base/src/definitions/fsCache.ts +128 -0
  21. package/vendor/base/src/definitions/index.ts +19 -0
  22. package/vendor/base/src/definitions/inputs.ts +30 -0
  23. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  24. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  25. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  26. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  27. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  28. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  29. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  30. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  31. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  32. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  33. package/vendor/base/src/definitions/load/load.ts +487 -0
  34. package/vendor/base/src/definitions/model.ts +238 -0
  35. package/vendor/base/src/definitions/registry/catalog.ts +643 -0
  36. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  37. package/vendor/base/src/definitions/theme.ts +383 -0
  38. package/vendor/base/src/executor.ts +284 -0
  39. package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
  40. package/vendor/base/src/items/baseVersion.ts +96 -0
  41. package/vendor/base/src/items/collect.ts +199 -0
  42. package/vendor/base/src/items/fingerprint.ts +34 -0
  43. package/vendor/base/src/items/publish.ts +198 -0
  44. package/vendor/base/src/lint/context.mjs +56 -0
  45. package/vendor/base/src/lint/design/defs.mjs +46 -0
  46. package/vendor/base/src/lint/design/file.mjs +737 -0
  47. package/vendor/base/src/lint/design/index.d.mts +30 -0
  48. package/vendor/base/src/lint/design/index.mjs +996 -0
  49. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  50. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  51. package/vendor/base/src/lint/design/walk.mjs +444 -0
  52. package/vendor/base/src/lint/index.mjs +28 -0
  53. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  54. package/vendor/base/src/lint/nodes/index.mjs +64 -0
  55. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  56. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  57. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  58. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  59. package/vendor/base/src/logger.ts +33 -0
  60. package/vendor/base/src/manifests/compose.ts +539 -0
  61. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  62. package/vendor/base/src/manifests/executor/context.ts +107 -0
  63. package/vendor/base/src/manifests/executor/index.ts +47 -0
  64. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  65. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  66. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  67. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  68. package/vendor/base/src/manifests/executor/send.ts +65 -0
  69. package/vendor/base/src/manifests/executor/session.ts +46 -0
  70. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  71. package/vendor/base/src/manifests/index.ts +280 -0
  72. package/vendor/base/src/manifests/integrity.ts +93 -0
  73. package/vendor/base/src/manifests/rows.ts +33 -0
  74. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  75. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  76. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  77. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  78. package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
  79. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  80. package/vendor/base/src/manifests/runtime/context.ts +150 -0
  81. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  82. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  83. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  84. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  85. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  86. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  87. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  88. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  89. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  90. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  91. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  92. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  93. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  94. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  95. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  96. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  97. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  98. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  99. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  100. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  101. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  102. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
  103. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  104. package/vendor/base/src/manifests/source.ts +187 -0
  105. package/vendor/base/src/manifests/watch.ts +67 -0
  106. package/vendor/base/src/markdown/document.ts +141 -0
  107. package/vendor/base/src/paths.ts +180 -0
  108. package/vendor/base/src/platform/audioManager.ts +100 -0
  109. package/vendor/base/src/platform/clientTransport.ts +65 -0
  110. package/vendor/base/src/platform/executionContext.ts +154 -0
  111. package/vendor/base/src/platform/index.ts +10 -0
  112. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  113. package/vendor/base/src/platform/redis.ts +54 -0
  114. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  115. package/vendor/base/src/pluginBase.ts +248 -0
  116. package/vendor/base/src/plugins/README.md +57 -0
  117. package/vendor/base/src/plugins/discovery.ts +87 -0
  118. package/vendor/base/src/plugins/index.ts +16 -0
  119. package/vendor/base/src/plugins/loader.ts +179 -0
  120. package/vendor/base/src/plugins/redis.ts +36 -0
  121. package/vendor/base/src/plugins/state.ts +53 -0
  122. package/vendor/base/src/registry.ts +95 -0
  123. package/vendor/base/src/security/permissions.ts +93 -0
  124. package/vendor/base/src/security/rules.ts +73 -0
  125. package/vendor/base/src/template/SafeExpression.ts +556 -0
  126. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  127. package/vendor/base/src/template/index.ts +22 -0
  128. package/vendor/base/src/types.ts +50 -0
  129. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  130. package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
  131. package/vendor/base/src/workflow/routing.ts +122 -0
  132. package/vendor/sdk/lib/core/store.ts +26 -6
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Node Execution - Handles executing nodes (PromiseNode, CallbackNode, ServiceCall)
3
+ */
4
+
5
+ import type { NodeDefinition } from "./types.js";
6
+ import { getNode } from "./registry.js";
7
+ import { buildExecutionContext } from "./platform/index.js";
8
+
9
+ /**
10
+ * Create a logger for a node type - outputs structured JSON for Loki/Promtail
11
+ */
12
+ function createLogger(nodeType: string) {
13
+ const log = (level: string, ...args: any[]) => {
14
+ const msg = args.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))).join(" ");
15
+ console.log(JSON.stringify({ level, nodeType, msg, service: "node-service" }));
16
+ };
17
+ return {
18
+ info: (...args: any[]) => log("info", ...args),
19
+ error: (...args: any[]) => log("error", ...args),
20
+ debug: (...args: any[]) => log("debug", ...args),
21
+ warn: (...args: any[]) => log("warn", ...args),
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Execute a PromiseNode (single execution, returns result)
27
+ */
28
+ async function executePromiseNode(nodeDef: NodeDefinition, inputs: any, config: any, context: any) {
29
+ const ExecutorClass = nodeDef.executor.executor || nodeDef.executor;
30
+ const executor = new ExecutorClass(nodeDef.type);
31
+
32
+ // Inject logger into executor (base classes expect this.logger)
33
+ executor.logger = createLogger(nodeDef.type);
34
+
35
+ // Ensure base class methods are available (some executors call this.validateAndGetContext)
36
+ if (!executor.validateAndGetContext) {
37
+ executor.validateAndGetContext = (ctx: any) => ({
38
+ workflowId: ctx?.workflowId || ctx?.workflow?.id || "",
39
+ executionId: ctx?.executionId || ctx?.workflow?.runId || "",
40
+ nodeId: ctx?.nodeId || "",
41
+ });
42
+ }
43
+
44
+ if (!executor.execute || typeof executor.execute !== "function") {
45
+ throw new Error(`Node ${nodeDef.type} executor does not have an execute method`);
46
+ }
47
+
48
+ const executionContext = buildExecutionContext(nodeDef.type, inputs, config, context);
49
+ return await executor.execute(inputs, config, executionContext);
50
+ }
51
+
52
+ /**
53
+ * Execute a CallbackNode (generator, emits multiple outputs)
54
+ * Returns an async generator that yields outputs
55
+ */
56
+ export async function* executeCallbackNode(nodeDef: NodeDefinition, inputs: any, config: any, context: any) {
57
+ const ExecutorClass = nodeDef.executor.executor || nodeDef.executor;
58
+ const executor = new ExecutorClass(nodeDef.type);
59
+
60
+ // Inject logger into executor (base classes expect this.logger)
61
+ executor.logger = createLogger(nodeDef.type);
62
+
63
+ // Ensure base class methods are available (some executors call this.validateAndGetContext)
64
+ if (!executor.validateAndGetContext) {
65
+ executor.validateAndGetContext = (ctx: any) => ({
66
+ workflowId: ctx?.workflowId || ctx?.workflow?.id || "",
67
+ executionId: ctx?.executionId || ctx?.workflow?.runId || "",
68
+ nodeId: ctx?.nodeId || "",
69
+ });
70
+ }
71
+
72
+ if (!executor.initializeState || !executor.handleEvent) {
73
+ // A PromiseNode on the stream path: design-system nodes ride the callback-actor
74
+ // lifecycle (template uiComponent) but their universal executor is a PromiseNode
75
+ // that outputs at the end (UNOVERSE_NODE_RUNTIME.md). One output, then done.
76
+ if (typeof (executor as any).execute === "function") {
77
+ yield await executePromiseNode(nodeDef, inputs, config, context);
78
+ return;
79
+ }
80
+ throw new Error(`Node ${nodeDef.type} is a CallbackNode but missing initializeState or handleEvent`);
81
+ }
82
+
83
+ const executionContext = buildExecutionContext(nodeDef.type, inputs, config, context);
84
+
85
+ // Set executionContext on executor for nodes that need it (e.g., OpenAIStream)
86
+ executor.executionContext = executionContext;
87
+
88
+ // Initialize state
89
+ let state = executor.initializeState(inputs);
90
+
91
+ // Queue-based streaming: emit() pushes to queue, generator yields from queue
92
+ const outputQueue: any[] = [];
93
+ const state_: { resolver: ((v: { value: any; done: boolean }) => void) | null; done: boolean } = {
94
+ resolver: null,
95
+ done: false,
96
+ };
97
+
98
+ // emit() either resolves a waiting consumer or queues the output
99
+ let emitCount = 0;
100
+ const emit = (output: any) => {
101
+ emitCount++;
102
+ if (state_.resolver) {
103
+ const resolve = state_.resolver;
104
+ state_.resolver = null;
105
+ resolve({ value: output, done: false });
106
+ } else {
107
+ outputQueue.push(output);
108
+ }
109
+ };
110
+
111
+ // Run execution in background - don't await it
112
+ const executionPromise = (async () => {
113
+ try {
114
+ // Pass executionContext as 4th parameter for nodes that need it (e.g., AWSNovaSpeech)
115
+ state = await executor.handleEvent({ type: "EXECUTE", inputs, config }, state, emit, executionContext);
116
+
117
+ // Only auto-continue if node is NOT yielding for user input (focusInputRequired).
118
+ // Guard against a node that never completes and makes no progress: a signal-driven
119
+ // node (e.g. Loop) run standalone takes "no action" on CONTINUE and never sets
120
+ // isComplete, which would spin forever and flood logs. Break after repeated
121
+ // no-progress iterations (no new emit AND unchanged state), plus a hard backstop.
122
+ const stateSig = (s: any) => {
123
+ try {
124
+ return JSON.stringify(s ?? null);
125
+ } catch {
126
+ return String(emitCount); // uncomparable state → fall back to emit progress
127
+ }
128
+ };
129
+ // The signature is consulted ONLY when emits have stalled: stringifying the full
130
+ // node state per iteration is O(state size) per chunk — O(n²) over a long stream
131
+ // for a state-accumulating node (message history, streamed text). Emit progress
132
+ // alone proves progress on the common path, so skip the stringify entirely there.
133
+ let prevSig: string | null = null;
134
+ let prevEmit = emitCount;
135
+ let stalls = 0;
136
+ let total = 0;
137
+ while (!state.isComplete && !state.focusInputRequired) {
138
+ if (++total > 100000) break; // hard backstop
139
+ state = await executor.handleEvent(
140
+ { type: "CONTINUE", inputs: { continue: true }, config },
141
+ state,
142
+ emit,
143
+ executionContext,
144
+ );
145
+ if (emitCount !== prevEmit) {
146
+ stalls = 0;
147
+ prevSig = null; // stale — recompute only if emits stall again
148
+ } else {
149
+ const sig = stateSig(state);
150
+ if (prevSig !== null && sig !== prevSig) {
151
+ stalls = 0;
152
+ } else if (prevSig !== null && ++stalls >= 3) {
153
+ // No new emit and unchanged state across a few CONTINUEs → the node is
154
+ // signal-driven (e.g. Loop) and won't advance standalone. Stop quietly.
155
+ break;
156
+ }
157
+ prevSig = sig;
158
+ }
159
+ prevEmit = emitCount;
160
+ }
161
+ } finally {
162
+ state_.done = true;
163
+ // Signal completion to any waiting consumer
164
+ if (state_.resolver) {
165
+ const resolve = state_.resolver;
166
+ state_.resolver = null;
167
+ resolve({ value: undefined, done: true });
168
+ }
169
+ }
170
+ })();
171
+
172
+ // Yield outputs as they arrive
173
+ try {
174
+ while (true) {
175
+ // First drain the queue
176
+ if (outputQueue.length > 0) {
177
+ yield outputQueue.shift();
178
+ continue;
179
+ }
180
+
181
+ // If execution is done, drain any remaining queue items before finishing
182
+ if (state_.done) {
183
+ // CRITICAL: Yield any remaining queued outputs before exiting
184
+ // This ensures the final emit() is always sent to the client
185
+ while (outputQueue.length > 0) {
186
+ yield outputQueue.shift();
187
+ }
188
+ break;
189
+ }
190
+
191
+ // Wait for next output or completion
192
+ const result = await new Promise<{ value: any; done: boolean }>((resolve) => {
193
+ state_.resolver = resolve;
194
+ });
195
+
196
+ if (result.done) {
197
+ break;
198
+ }
199
+
200
+ yield result.value;
201
+ }
202
+ } finally {
203
+ // Ensure execution completes even if generator is abandoned
204
+ await executionPromise;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Execute a service call on a node (calls handleServiceCall method)
210
+ * Used for service nodes like OpenAIEmbeddingService, BedrockEmbeddingService
211
+ */
212
+ export async function executeServiceCall(
213
+ nodeType: string,
214
+ method: string,
215
+ params: any,
216
+ config: any,
217
+ context: any,
218
+ ): Promise<any> {
219
+ const nodeDef = getNode(nodeType);
220
+
221
+ if (!nodeDef) {
222
+ throw new Error(`Node type not found: ${nodeType}`);
223
+ }
224
+
225
+ if (!nodeDef.executor) {
226
+ throw new Error(`No executor found for node type: ${nodeType}`);
227
+ }
228
+
229
+ const ExecutorClass = nodeDef.executor.executor || nodeDef.executor;
230
+ /**
231
+ * WITH THE NODE TYPE, like the two other construction sites in this file.
232
+ *
233
+ * A code node's executor hardcodes its own type in its constructor, so `new ExecutorClass()`
234
+ * worked for every one of them and the missing argument sat here unnoticed. The two SHARED
235
+ * manifest classes cannot: one class serves every manifest node, so the type IS the
236
+ * argument, and without it `this.nodeType` is undefined and the lookup fails with
237
+ * `No manifest loaded for node type "undefined"`.
238
+ *
239
+ * It broke the SERVICE channel only, which is why it survived the whole migration: every
240
+ * manifest node so far is reached by the graph (executePromiseNode / executeCallbackNode
241
+ * above, both of which pass it). SpatialSearch is the first manifest node that is ONLY ever
242
+ * called as a service, so it is the first to hit this line.
243
+ */
244
+ const executor = new ExecutorClass(nodeDef.type);
245
+
246
+ // Inject logger into executor (base classes expect this.logger)
247
+ executor.logger = createLogger(nodeType);
248
+
249
+ if (!executor.handleServiceCall || typeof executor.handleServiceCall !== "function") {
250
+ throw new Error(`Node ${nodeType} executor does not have a handleServiceCall method`);
251
+ }
252
+
253
+ const executionContext = buildExecutionContext(nodeType, {}, config, context);
254
+ return await executor.handleServiceCall(method, params, config, executionContext);
255
+ }
256
+
257
+ /**
258
+ * Execute a node (dispatches to correct executor based on type)
259
+ */
260
+ export async function executeNode(nodeType: string, inputs: any, config: any, context: any) {
261
+ const nodeDef = getNode(nodeType);
262
+
263
+ if (!nodeDef) {
264
+ throw new Error(`Node type not found: ${nodeType}`);
265
+ }
266
+
267
+ if (!nodeDef.executor) {
268
+ throw new Error(`No executor found for node type: ${nodeType}`);
269
+ }
270
+
271
+ // PromiseNode - single execution
272
+ if (nodeDef.executionMode === "single") {
273
+ return await executePromiseNode(nodeDef, inputs, config, context);
274
+ }
275
+
276
+ // CallbackNode - collect all outputs and return final result
277
+ const outputs: any[] = [];
278
+ for await (const output of executeCallbackNode(nodeDef, inputs, config, context)) {
279
+ outputs.push(output);
280
+ }
281
+
282
+ // Return the last output (final result)
283
+ return outputs[outputs.length - 1] || {};
284
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * uWS ↔ Web-Fetch adapter — the ONLY bespoke transport code.
3
+ *
4
+ * The MCP SDK's transport is framework-agnostic: `WebStandardStreamableHTTPServerTransport`
5
+ * exposes `handleRequest(req: Request) => Promise<Response>` — the exact entry the SDK
6
+ * documents for Hono / Cloudflare Workers / Deno / Bun. So we do NOT hand-roll the MCP
7
+ * protocol: this adapter just converts a uWS request into a Web `Request`, and writes the
8
+ * Web `Response` (single-shot JSON, or a long-lived SSE stream) back onto the uWS
9
+ * `HttpResponse`. All MCP protocol logic stays inside the SDK transport.
10
+ *
11
+ * uWS rules honored here (these are the easy things to get wrong):
12
+ * - `HttpRequest` is valid only synchronously → method/url/headers are read up-front.
13
+ * - `onAborted` MUST be registered before the first `await`, or uWS throws on disconnect.
14
+ * - every write happens inside `res.cork()`.
15
+ * - body chunks are copied synchronously (uWS reuses the chunk's backing memory).
16
+ */
17
+ import { gzipSync } from "node:zlib";
18
+ import type { HttpRequest, HttpResponse } from "uWebSockets.js";
19
+
20
+ const STATUS_TEXT: Record<number, string> = {
21
+ 200: "OK",
22
+ 202: "Accepted",
23
+ 204: "No Content",
24
+ 304: "Not Modified",
25
+ 400: "Bad Request",
26
+ 404: "Not Found",
27
+ 405: "Method Not Allowed",
28
+ 406: "Not Acceptable",
29
+ 500: "Internal Server Error",
30
+ };
31
+ const statusLine = (code: number): string => `${code} ${STATUS_TEXT[code] ?? "OK"}`;
32
+
33
+ /** Text-ish payloads compress ~4:1; anything else (images, audio) is already packed. */
34
+ const COMPRESSIBLE = /^(text\/|application\/(json|javascript|xml|vnd\.unoverse))/;
35
+ /** Below this, the gzip header + CPU outweigh the shrink. */
36
+ const COMPRESS_MIN_BYTES = 1024;
37
+
38
+ function writeCors(res: HttpResponse): void {
39
+ // The workbench is same-origin (via the Vite proxy) so needs none of this; kept so
40
+ // external MCP clients / the Inspector / the canvas can connect cross-origin.
41
+ res.writeHeader("Access-Control-Allow-Origin", "*");
42
+ res.writeHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
43
+ // `ngrok-skip-browser-warning` (2026-08-28): the webSDK sends it on ngrok-hosted dev
44
+ // tunnels to bypass the free tier's interstitial; a custom header makes the browser
45
+ // PREFLIGHT, and a preflight that doesn't allow it fails the whole request ("blocked
46
+ // by CORS: header field not allowed") — observed live inside ChatGPT's widget sandbox.
47
+ // Harmless to allow always: it's only ever SENT to ngrok hosts.
48
+ res.writeHeader(
49
+ "Access-Control-Allow-Headers",
50
+ "Content-Type, Authorization, mcp-session-id, mcp-protocol-version, ngrok-skip-browser-warning",
51
+ );
52
+ res.writeHeader("Access-Control-Expose-Headers", "mcp-session-id, mcp-protocol-version");
53
+ // A PREFLIGHT IS A ROUND TRIP THAT CARRIES NO DATA, AND WITHOUT THIS THE BROWSER REPEATS
54
+ // IT CONSTANTLY. Chrome's default preflight cache is 5 seconds, so an app making a series
55
+ // of MCP calls pays OPTIONS → 204 → the real request, doubling the round trips on nearly
56
+ // every one. On a universe served from one region that is the dominant cost of loading an
57
+ // app: measured London → UAE, a round trip is ~150ms and the server's own processing is
58
+ // ~0ms, so the preflights were most of the wait, not the work.
59
+ //
60
+ // 24h is the practical ceiling (Chromium caps at 2h, Firefox at 24h; both clamp rather
61
+ // than ignore). The response being cached says only which methods and headers are allowed,
62
+ // which is static in this file — no authorization decision is cached, because auth is
63
+ // checked on the real request every time.
64
+ res.writeHeader("Access-Control-Max-Age", "86400");
65
+ }
66
+
67
+ /** Buffer the full request body; resolves on the last chunk (copies as it goes). */
68
+ function readBody(res: HttpResponse): Promise<Buffer> {
69
+ return new Promise((resolve) => {
70
+ // Collect copies and concat ONCE at the end. The old per-chunk
71
+ // `Buffer.concat([buf, chunk])` reallocated+copied the whole accumulator on every
72
+ // chunk — O(n²) in body size, which hurts large /execute payloads. Each chunk is
73
+ // still copied synchronously (uWS reuses the chunk's backing memory after return).
74
+ const chunks: Buffer[] = [];
75
+ res.onData((chunk, isLast) => {
76
+ // COPY each chunk synchronously NOW — uWS detaches/reuses the chunk's backing
77
+ // ArrayBuffer after this callback returns. `Buffer.from(arrayBuffer)` is only a
78
+ // VIEW (no copy), so wrap in a Uint8Array, which Buffer.from then copies; deferring
79
+ // a bare view to the final concat would read detached memory.
80
+ chunks.push(Buffer.from(new Uint8Array(chunk)));
81
+ if (isLast) resolve(chunks.length === 1 ? chunks[0] : Buffer.concat(chunks));
82
+ });
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Drive a uWS request through a Web-fetch handler: OPTIONS preflight, body buffering,
88
+ * and pumping the Response (single-shot JSON or long-lived SSE) back to uWS.
89
+ */
90
+ export async function handleFetch(
91
+ res: HttpResponse,
92
+ req: HttpRequest,
93
+ handler: (request: Request) => Promise<Response>,
94
+ ): Promise<void> {
95
+ const state: { aborted: boolean; reader?: ReadableStreamDefaultReader<Uint8Array> } = { aborted: false };
96
+ res.onAborted(() => {
97
+ state.aborted = true;
98
+ void state.reader?.cancel().catch(() => {});
99
+ });
100
+
101
+ // HttpRequest is only valid synchronously — capture everything now.
102
+ const method = req.getMethod().toUpperCase();
103
+ const query = req.getQuery();
104
+ const url = `http://localhost${req.getUrl()}${query ? `?${query}` : ""}`;
105
+ const headers: Record<string, string> = {};
106
+ req.forEach((k, v) => {
107
+ headers[k] = v;
108
+ });
109
+
110
+ if (method === "OPTIONS") {
111
+ res.cork(() => {
112
+ res.writeStatus(statusLine(204));
113
+ writeCors(res);
114
+ res.endWithoutBody();
115
+ });
116
+ return;
117
+ }
118
+
119
+ const body = method !== "GET" && method !== "HEAD" ? await readBody(res) : undefined;
120
+ if (state.aborted) return;
121
+
122
+ let response: Response;
123
+ try {
124
+ const init: RequestInit = { method, headers };
125
+ if (body && body.length) init.body = new Uint8Array(body); // BodyInit-compatible copy of the Buffer
126
+ response = await handler(new Request(url, init));
127
+ } catch (err) {
128
+ // Log the detail server-side; return a GENERIC message to the client (no internal
129
+ // detail leakage — MCP security best-practices).
130
+ console.error("[unoverse] request handler error:", err);
131
+ if (!state.aborted) {
132
+ res.cork(() => {
133
+ res.writeStatus(statusLine(500));
134
+ res.writeHeader("Content-Type", "application/json");
135
+ writeCors(res);
136
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32603, message: "Internal error" }, id: null }));
137
+ });
138
+ }
139
+ return;
140
+ }
141
+ if (state.aborted) return;
142
+
143
+ const isSse = (response.headers.get("content-type") ?? "").startsWith("text/event-stream");
144
+
145
+ // Single-shot responses (everything except SSE) are buffered so large text payloads
146
+ // can be gzipped. Neither uWS nor the DO load balancer compresses, so without this
147
+ // every byte travels identity-encoded — the widget boot alone was ~660KB of JS/JSON
148
+ // that gzips to ~150KB. SSE must stream, so it is never buffered or compressed.
149
+ if (!isSse) {
150
+ const contentType = response.headers.get("content-type") ?? "";
151
+ const acceptsGzip = /(^|,)\s*gzip\s*(;|,|$)/.test(headers["accept-encoding"] ?? "");
152
+ let payload: Buffer | null = null;
153
+ try {
154
+ payload = Buffer.from(await response.arrayBuffer());
155
+ } catch {
156
+ payload = null; // body unreadable — fall through and end empty below
157
+ }
158
+ if (state.aborted) return;
159
+ const shouldCompress =
160
+ acceptsGzip &&
161
+ payload !== null &&
162
+ payload.length >= COMPRESS_MIN_BYTES &&
163
+ COMPRESSIBLE.test(contentType) &&
164
+ !response.headers.get("content-encoding");
165
+ const body = shouldCompress && payload ? gzipSync(payload) : payload;
166
+ res.cork(() => {
167
+ res.writeStatus(statusLine(response.status));
168
+ response.headers.forEach((value, key) => {
169
+ const k = key.toLowerCase();
170
+ // uWS generates these itself on every response. Forwarding an upstream copy
171
+ // (e.g. the engine's own `date`) yields a DUPLICATE header — nginx just warns
172
+ // and serves 200, but HTTP/3 (QUIC) rejects the malformed header block with
173
+ // QUIC_PACKET_READ_ERROR. Skip them so uWS's single copy is authoritative.
174
+ // access-control-*: writeCors() below is authoritative — forwarding an upstream
175
+ // copy too produced the doubled `access-control-allow-origin: *,*` seen live.
176
+ if (k === "content-length" || k === "date" || k === "server" || k.startsWith("access-control-")) return;
177
+ res.writeHeader(key, value);
178
+ });
179
+ if (shouldCompress) {
180
+ res.writeHeader("Content-Encoding", "gzip");
181
+ // Encoding varies by the request's Accept-Encoding, so shared caches must key on it.
182
+ res.writeHeader("Vary", "Accept-Encoding");
183
+ }
184
+ writeCors(res);
185
+ // `res.end()` for the empty case, NEVER `endWithoutBody()`. On HTTP/1.1 with no
186
+ // Content-Length, endWithoutBody() leaves the response unterminated: the client
187
+ // waits on a reply that never finishes until the socket's idle timeout kills the
188
+ // connection ~12s later. Every bodyless response paid it — the MCP handshake's
189
+ // 202 ack among them, so every app open stalled ~10s before its first paint
190
+ // (observed live on bppunoverse 2026-08-21; reproduced in isolation: a JSON
191
+ // response 28ms, a null-body 202 11.9s). end() closes with content-length: 0.
192
+ if (body && body.length) res.end(body);
193
+ else res.end();
194
+ });
195
+ return;
196
+ }
197
+
198
+ // SSE: write head, then pump chunks until the transport closes the stream.
199
+ const reader = response.body?.getReader();
200
+ state.reader = reader;
201
+ res.cork(() => {
202
+ res.writeStatus(statusLine(response.status));
203
+ response.headers.forEach((value, key) => {
204
+ const k = key.toLowerCase();
205
+ // See the single-shot branch for why these are skipped.
206
+ if (k === "content-length" || k === "date" || k === "server" || k.startsWith("access-control-")) return;
207
+ res.writeHeader(key, value);
208
+ });
209
+ writeCors(res);
210
+ });
211
+
212
+ if (!reader) {
213
+ if (!state.aborted) res.cork(() => res.end());
214
+ return;
215
+ }
216
+
217
+ // SSE pump lifecycle. A GET stream that dies leaves its session registered while
218
+ // every push silently drops (the SDK sends nothing to a missing standalone stream),
219
+ // so these logs are the ground truth for whether a client's inbound stream exists.
220
+ const path = new URL(url).pathname;
221
+ // Session id ties a pump to its MCP session (request header on established sessions,
222
+ // response header on initialize).
223
+ const sid = headers["mcp-session-id"] ?? response.headers.get("mcp-session-id") ?? "-";
224
+ if (isSse) console.log(`[unoverse:sse] pump OPEN ${method} ${path} sid=${sid}`);
225
+
226
+ // KEEPALIVE — an idle SSE stream writes NOTHING, and a silent connection is fair game
227
+ // for any middlebox (the Vite dev proxy kills one after ~2 min; observed as "client
228
+ // aborted" mid-run, after which every push into the still-registered session drops
229
+ // silently). An SSE comment line every 25s keeps every hop alive; parsers ignore
230
+ // comments by spec, so no client sees them.
231
+ // 10s, not 25s: the client's zombie watchdog declares death after ~1.5 missed
232
+ // beats, so the beat interval directly sets how long a silently-dead stream can
233
+ // blind the canvas. Comment lines are ~18 bytes — frequency is free.
234
+ const keepalive = isSse
235
+ ? setInterval(() => {
236
+ if (!state.aborted) res.cork(() => res.write(": keepalive\n\n"));
237
+ }, 10_000)
238
+ : null;
239
+
240
+ try {
241
+ for (;;) {
242
+ const { done, value } = await reader.read();
243
+ if (state.aborted) {
244
+ void reader.cancel().catch(() => {});
245
+ if (isSse) console.log(`[unoverse:sse] pump CLOSED (client aborted) ${method} ${path} sid=${sid}`);
246
+ return;
247
+ }
248
+ if (done) break;
249
+ if (value) res.cork(() => res.write(value));
250
+ }
251
+ if (!state.aborted) res.cork(() => res.end());
252
+ if (isSse) console.log(`[unoverse:sse] pump CLOSED (stream ended) ${method} ${path} sid=${sid}`);
253
+ } catch {
254
+ void reader.cancel().catch(() => {});
255
+ if (!state.aborted) res.cork(() => res.end());
256
+ if (isSse) console.log(`[unoverse:sse] pump CLOSED (pump error) ${method} ${path} sid=${sid}`);
257
+ } finally {
258
+ if (keepalive) clearInterval(keepalive);
259
+ }
260
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The design system version an asset was authored against.
3
+ *
4
+ * TWO PARTS, because neither alone is enough:
5
+ *
6
+ * 1.4.2+a3f1
7
+ * └───┘ └──┘
8
+ * │ └── HASH over every member's content. Derived, so it cannot be forgotten or
9
+ * │ bumped wrongly, and it moves the instant anything moves. This is what
10
+ * │ actually answers "did it change".
11
+ * └──────── PACKAGE VERSION, when there is one. Declared, so it can be stale, but it
12
+ * is ORDERED, which a hash never is. This is what answers "which way".
13
+ *
14
+ * A hash alone tells you two things differ and not which is newer. A version alone can be
15
+ * wrong, and is absent entirely in the monorepo where the design system is uncommitted
16
+ * source on disk rather than an installed package. So: hash always, version when known.
17
+ *
18
+ * WHAT IT IS FOR. Every published asset is built ON the design system: a component composes
19
+ * marketplace atoms, an app composes components, an org's tokens override a foundation
20
+ * scale. When the foundation moves, an item authored against the older one may reference
21
+ * something that no longer exists. The symptom is a hole where a component should be, seen
22
+ * by an end user and traceable to nothing. This is the record that makes it traceable.
23
+ *
24
+ * It cannot be backfilled: nothing else anywhere records what a definition was written
25
+ * against. See migration 018 and DECLARATIVE_NODES.md §9.12.
26
+ */
27
+ import { createHash } from "crypto";
28
+ import { existsSync, readFileSync, readdirSync } from "fs";
29
+ import { join } from "path";
30
+ import { fingerprintOf } from "./fingerprint.js";
31
+
32
+ /** Short, readable, and only ever compared for equality. */
33
+ function shortHash(s: string): string {
34
+ return createHash("sha256").update(s).digest("hex").slice(0, 8);
35
+ }
36
+
37
+ /** Every file under a directory, sorted, so the hash is order-independent. */
38
+ function filesUnder(dir: string, prefix = "", out: Record<string, string> = {}): Record<string, string> {
39
+ if (!existsSync(dir)) return out;
40
+ for (const e of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
41
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
42
+ if (e.isDirectory()) filesUnder(join(dir, e.name), rel, out);
43
+ else if (/\.(ya?ml|json)$/.test(e.name)) out[rel] = readFileSync(join(dir, e.name), "utf8");
44
+ }
45
+ return out;
46
+ }
47
+
48
+ /**
49
+ * The declared version, if the design system arrived as a package.
50
+ *
51
+ * Absent in the monorepo, where it is source on disk. That absence is honest rather than a
52
+ * gap: there is no version, because nobody cut one.
53
+ */
54
+ function packageVersion(designSystemDir: string): string | null {
55
+ // The bundle lives at <package>/definitions, so its package.json is one level up.
56
+ for (const candidate of [join(designSystemDir, "..", "package.json"), join(designSystemDir, "package.json")]) {
57
+ if (!existsSync(candidate)) continue;
58
+ try {
59
+ const v = JSON.parse(readFileSync(candidate, "utf8"))?.version;
60
+ if (typeof v === "string") return v;
61
+ } catch {
62
+ /* unreadable package.json: fall through to the hash alone */
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * Compute the version of the design system at `designSystemDir` (the resolved marketplace
70
+ * directory: on-disk design/marketplace, or the installed bundle).
71
+ *
72
+ * Hashes components, atoms and styles together, because they only mean anything as a set:
73
+ * a universe holding half of them renders against a contract with holes.
74
+ */
75
+ export function designSystemVersion(designSystemDir: string): string {
76
+ const parts: string[] = [];
77
+
78
+ for (const kind of ["components", "atoms"] as const) {
79
+ const home = join(designSystemDir, kind);
80
+ if (!existsSync(home)) continue;
81
+ for (const e of readdirSync(home, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
82
+ if (e.name.startsWith(".") || e.name === "README.md") continue;
83
+ const files = e.isDirectory()
84
+ ? filesUnder(join(home, e.name))
85
+ : { [e.name]: readFileSync(join(home, e.name), "utf8") };
86
+ parts.push(`${kind}/${e.name}:${fingerprintOf(files)}`);
87
+ }
88
+ }
89
+
90
+ const styles = filesUnder(join(designSystemDir, "styles"));
91
+ if (Object.keys(styles).length) parts.push(`styles:${fingerprintOf(styles)}`);
92
+
93
+ const hash = shortHash(parts.join("|"));
94
+ const declared = packageVersion(designSystemDir);
95
+ return declared ? `${declared}+${hash}` : hash;
96
+ }