@nodetool-ai/websocket 0.7.0-rc.11

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 (261) hide show
  1. package/dist/agent/agent-runtime.d.ts +47 -0
  2. package/dist/agent/agent-runtime.d.ts.map +1 -0
  3. package/dist/agent/agent-runtime.js +577 -0
  4. package/dist/agent/agent-runtime.js.map +1 -0
  5. package/dist/agent/codex-agent.d.ts +41 -0
  6. package/dist/agent/codex-agent.d.ts.map +1 -0
  7. package/dist/agent/codex-agent.js +334 -0
  8. package/dist/agent/codex-agent.js.map +1 -0
  9. package/dist/agent/index.d.ts +6 -0
  10. package/dist/agent/index.d.ts.map +1 -0
  11. package/dist/agent/index.js +4 -0
  12. package/dist/agent/index.js.map +1 -0
  13. package/dist/agent/llm-agent.d.ts +51 -0
  14. package/dist/agent/llm-agent.d.ts.map +1 -0
  15. package/dist/agent/llm-agent.js +526 -0
  16. package/dist/agent/llm-agent.js.map +1 -0
  17. package/dist/agent/mcp-tool-server.d.ts +33 -0
  18. package/dist/agent/mcp-tool-server.d.ts.map +1 -0
  19. package/dist/agent/mcp-tool-server.js +292 -0
  20. package/dist/agent/mcp-tool-server.js.map +1 -0
  21. package/dist/agent/opencode-agent.d.ts +32 -0
  22. package/dist/agent/opencode-agent.d.ts.map +1 -0
  23. package/dist/agent/opencode-agent.js +332 -0
  24. package/dist/agent/opencode-agent.js.map +1 -0
  25. package/dist/agent/pi-agent.d.ts +42 -0
  26. package/dist/agent/pi-agent.d.ts.map +1 -0
  27. package/dist/agent/pi-agent.js +429 -0
  28. package/dist/agent/pi-agent.js.map +1 -0
  29. package/dist/agent/sdk-provider.d.ts +46 -0
  30. package/dist/agent/sdk-provider.d.ts.map +1 -0
  31. package/dist/agent/sdk-provider.js +50 -0
  32. package/dist/agent/sdk-provider.js.map +1 -0
  33. package/dist/agent/socket-route.d.ts +11 -0
  34. package/dist/agent/socket-route.d.ts.map +1 -0
  35. package/dist/agent/socket-route.js +262 -0
  36. package/dist/agent/socket-route.js.map +1 -0
  37. package/dist/agent/transport.d.ts +39 -0
  38. package/dist/agent/transport.d.ts.map +1 -0
  39. package/dist/agent/transport.js +17 -0
  40. package/dist/agent/transport.js.map +1 -0
  41. package/dist/agent/types.d.ts +11 -0
  42. package/dist/agent/types.d.ts.map +1 -0
  43. package/dist/agent/types.js +10 -0
  44. package/dist/agent/types.js.map +1 -0
  45. package/dist/collection-api.d.ts +14 -0
  46. package/dist/collection-api.d.ts.map +1 -0
  47. package/dist/collection-api.js +75 -0
  48. package/dist/collection-api.js.map +1 -0
  49. package/dist/error-codes.d.ts +3 -0
  50. package/dist/error-codes.d.ts.map +1 -0
  51. package/dist/error-codes.js +2 -0
  52. package/dist/error-codes.js.map +1 -0
  53. package/dist/file-api.d.ts +12 -0
  54. package/dist/file-api.d.ts.map +1 -0
  55. package/dist/file-api.js +76 -0
  56. package/dist/file-api.js.map +1 -0
  57. package/dist/http-api.d.ts +94 -0
  58. package/dist/http-api.d.ts.map +1 -0
  59. package/dist/http-api.js +1472 -0
  60. package/dist/http-api.js.map +1 -0
  61. package/dist/index.d.ts +6 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +6 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/lib/asset-paths.d.ts +10 -0
  66. package/dist/lib/asset-paths.d.ts.map +1 -0
  67. package/dist/lib/asset-paths.js +45 -0
  68. package/dist/lib/asset-paths.js.map +1 -0
  69. package/dist/lib/bridge.d.ts +8 -0
  70. package/dist/lib/bridge.d.ts.map +1 -0
  71. package/dist/lib/bridge.js +70 -0
  72. package/dist/lib/bridge.js.map +1 -0
  73. package/dist/mcp-server.d.ts +32 -0
  74. package/dist/mcp-server.d.ts.map +1 -0
  75. package/dist/mcp-server.js +842 -0
  76. package/dist/mcp-server.js.map +1 -0
  77. package/dist/models-api.d.ts +12 -0
  78. package/dist/models-api.d.ts.map +1 -0
  79. package/dist/models-api.js +730 -0
  80. package/dist/models-api.js.map +1 -0
  81. package/dist/oauth-api.d.ts +28 -0
  82. package/dist/oauth-api.d.ts.map +1 -0
  83. package/dist/oauth-api.js +681 -0
  84. package/dist/oauth-api.js.map +1 -0
  85. package/dist/openai-api.d.ts +63 -0
  86. package/dist/openai-api.d.ts.map +1 -0
  87. package/dist/openai-api.js +359 -0
  88. package/dist/openai-api.js.map +1 -0
  89. package/dist/plugins/websocket.d.ts +14 -0
  90. package/dist/plugins/websocket.d.ts.map +1 -0
  91. package/dist/plugins/websocket.js +431 -0
  92. package/dist/plugins/websocket.js.map +1 -0
  93. package/dist/resolve-media-urls.d.ts +19 -0
  94. package/dist/resolve-media-urls.d.ts.map +1 -0
  95. package/dist/resolve-media-urls.js +109 -0
  96. package/dist/resolve-media-urls.js.map +1 -0
  97. package/dist/routes/assets.d.ts +22 -0
  98. package/dist/routes/assets.d.ts.map +1 -0
  99. package/dist/routes/assets.js +168 -0
  100. package/dist/routes/assets.js.map +1 -0
  101. package/dist/routes/collections.d.ts +16 -0
  102. package/dist/routes/collections.d.ts.map +1 -0
  103. package/dist/routes/collections.js +32 -0
  104. package/dist/routes/collections.js.map +1 -0
  105. package/dist/routes/files.d.ts +12 -0
  106. package/dist/routes/files.d.ts.map +1 -0
  107. package/dist/routes/files.js +11 -0
  108. package/dist/routes/files.js.map +1 -0
  109. package/dist/routes/health.d.ts +4 -0
  110. package/dist/routes/health.d.ts.map +1 -0
  111. package/dist/routes/health.js +41 -0
  112. package/dist/routes/health.js.map +1 -0
  113. package/dist/routes/jobs.d.ts +16 -0
  114. package/dist/routes/jobs.d.ts.map +1 -0
  115. package/dist/routes/jobs.js +25 -0
  116. package/dist/routes/jobs.js.map +1 -0
  117. package/dist/routes/nodes.d.ts +8 -0
  118. package/dist/routes/nodes.d.ts.map +1 -0
  119. package/dist/routes/nodes.js +124 -0
  120. package/dist/routes/nodes.js.map +1 -0
  121. package/dist/routes/oauth.d.ts +8 -0
  122. package/dist/routes/oauth.d.ts.map +1 -0
  123. package/dist/routes/oauth.js +19 -0
  124. package/dist/routes/oauth.js.map +1 -0
  125. package/dist/routes/openai.d.ts +8 -0
  126. package/dist/routes/openai.d.ts.map +1 -0
  127. package/dist/routes/openai.js +20 -0
  128. package/dist/routes/openai.js.map +1 -0
  129. package/dist/routes/storage.d.ts +12 -0
  130. package/dist/routes/storage.d.ts.map +1 -0
  131. package/dist/routes/storage.js +13 -0
  132. package/dist/routes/storage.js.map +1 -0
  133. package/dist/routes/workflows.d.ts +18 -0
  134. package/dist/routes/workflows.d.ts.map +1 -0
  135. package/dist/routes/workflows.js +25 -0
  136. package/dist/routes/workflows.js.map +1 -0
  137. package/dist/routes/workspace.d.ts +15 -0
  138. package/dist/routes/workspace.d.ts.map +1 -0
  139. package/dist/routes/workspace.js +24 -0
  140. package/dist/routes/workspace.js.map +1 -0
  141. package/dist/screenshot-server.d.ts +21 -0
  142. package/dist/screenshot-server.d.ts.map +1 -0
  143. package/dist/screenshot-server.js +222 -0
  144. package/dist/screenshot-server.js.map +1 -0
  145. package/dist/server.d.ts +10 -0
  146. package/dist/server.d.ts.map +1 -0
  147. package/dist/server.js +783 -0
  148. package/dist/server.js.map +1 -0
  149. package/dist/settings-registry.d.ts +29 -0
  150. package/dist/settings-registry.d.ts.map +1 -0
  151. package/dist/settings-registry.js +117 -0
  152. package/dist/settings-registry.js.map +1 -0
  153. package/dist/storage-api.d.ts +5 -0
  154. package/dist/storage-api.d.ts.map +1 -0
  155. package/dist/storage-api.js +221 -0
  156. package/dist/storage-api.js.map +1 -0
  157. package/dist/test-ui-server.d.ts +19 -0
  158. package/dist/test-ui-server.d.ts.map +1 -0
  159. package/dist/test-ui-server.js +1256 -0
  160. package/dist/test-ui-server.js.map +1 -0
  161. package/dist/trpc/context.d.ts +21 -0
  162. package/dist/trpc/context.d.ts.map +1 -0
  163. package/dist/trpc/context.js +10 -0
  164. package/dist/trpc/context.js.map +1 -0
  165. package/dist/trpc/error-formatter.d.ts +18 -0
  166. package/dist/trpc/error-formatter.d.ts.map +1 -0
  167. package/dist/trpc/error-formatter.js +41 -0
  168. package/dist/trpc/error-formatter.js.map +1 -0
  169. package/dist/trpc/index.d.ts +18 -0
  170. package/dist/trpc/index.d.ts.map +1 -0
  171. package/dist/trpc/index.js +13 -0
  172. package/dist/trpc/index.js.map +1 -0
  173. package/dist/trpc/middleware.d.ts +8 -0
  174. package/dist/trpc/middleware.d.ts.map +1 -0
  175. package/dist/trpc/middleware.js +12 -0
  176. package/dist/trpc/middleware.js.map +1 -0
  177. package/dist/trpc/router.d.ts +2543 -0
  178. package/dist/trpc/router.d.ts.map +1 -0
  179. package/dist/trpc/router.js +43 -0
  180. package/dist/trpc/router.js.map +1 -0
  181. package/dist/trpc/routers/assets.d.ts +229 -0
  182. package/dist/trpc/routers/assets.d.ts.map +1 -0
  183. package/dist/trpc/routers/assets.js +263 -0
  184. package/dist/trpc/routers/assets.js.map +1 -0
  185. package/dist/trpc/routers/collections.d.ts +88 -0
  186. package/dist/trpc/routers/collections.d.ts.map +1 -0
  187. package/dist/trpc/routers/collections.js +166 -0
  188. package/dist/trpc/routers/collections.js.map +1 -0
  189. package/dist/trpc/routers/costs.d.ts +129 -0
  190. package/dist/trpc/routers/costs.d.ts.map +1 -0
  191. package/dist/trpc/routers/costs.js +95 -0
  192. package/dist/trpc/routers/costs.js.map +1 -0
  193. package/dist/trpc/routers/files.d.ts +34 -0
  194. package/dist/trpc/routers/files.d.ts.map +1 -0
  195. package/dist/trpc/routers/files.js +92 -0
  196. package/dist/trpc/routers/files.js.map +1 -0
  197. package/dist/trpc/routers/jobs.d.ts +92 -0
  198. package/dist/trpc/routers/jobs.d.ts.map +1 -0
  199. package/dist/trpc/routers/jobs.js +100 -0
  200. package/dist/trpc/routers/jobs.js.map +1 -0
  201. package/dist/trpc/routers/mcp-config.d.ts +69 -0
  202. package/dist/trpc/routers/mcp-config.d.ts.map +1 -0
  203. package/dist/trpc/routers/mcp-config.js +290 -0
  204. package/dist/trpc/routers/mcp-config.js.map +1 -0
  205. package/dist/trpc/routers/messages.d.ts +121 -0
  206. package/dist/trpc/routers/messages.d.ts.map +1 -0
  207. package/dist/trpc/routers/messages.js +113 -0
  208. package/dist/trpc/routers/messages.js.map +1 -0
  209. package/dist/trpc/routers/models.d.ts +748 -0
  210. package/dist/trpc/routers/models.d.ts.map +1 -0
  211. package/dist/trpc/routers/models.js +1107 -0
  212. package/dist/trpc/routers/models.js.map +1 -0
  213. package/dist/trpc/routers/nodes.d.ts +48 -0
  214. package/dist/trpc/routers/nodes.d.ts.map +1 -0
  215. package/dist/trpc/routers/nodes.js +57 -0
  216. package/dist/trpc/routers/nodes.js.map +1 -0
  217. package/dist/trpc/routers/sandboxes.d.ts +64 -0
  218. package/dist/trpc/routers/sandboxes.d.ts.map +1 -0
  219. package/dist/trpc/routers/sandboxes.js +327 -0
  220. package/dist/trpc/routers/sandboxes.js.map +1 -0
  221. package/dist/trpc/routers/settings.d.ts +113 -0
  222. package/dist/trpc/routers/settings.d.ts.map +1 -0
  223. package/dist/trpc/routers/settings.js +193 -0
  224. package/dist/trpc/routers/settings.js.map +1 -0
  225. package/dist/trpc/routers/skills.d.ts +41 -0
  226. package/dist/trpc/routers/skills.d.ts.map +1 -0
  227. package/dist/trpc/routers/skills.js +194 -0
  228. package/dist/trpc/routers/skills.js.map +1 -0
  229. package/dist/trpc/routers/storage.d.ts +44 -0
  230. package/dist/trpc/routers/storage.d.ts.map +1 -0
  231. package/dist/trpc/routers/storage.js +153 -0
  232. package/dist/trpc/routers/storage.js.map +1 -0
  233. package/dist/trpc/routers/threads.d.ts +95 -0
  234. package/dist/trpc/routers/threads.d.ts.map +1 -0
  235. package/dist/trpc/routers/threads.js +150 -0
  236. package/dist/trpc/routers/threads.js.map +1 -0
  237. package/dist/trpc/routers/users.d.ts +79 -0
  238. package/dist/trpc/routers/users.d.ts.map +1 -0
  239. package/dist/trpc/routers/users.js +125 -0
  240. package/dist/trpc/routers/users.js.map +1 -0
  241. package/dist/trpc/routers/workflows.d.ts +681 -0
  242. package/dist/trpc/routers/workflows.d.ts.map +1 -0
  243. package/dist/trpc/routers/workflows.js +725 -0
  244. package/dist/trpc/routers/workflows.js.map +1 -0
  245. package/dist/trpc/routers/workspace.d.ts +127 -0
  246. package/dist/trpc/routers/workspace.d.ts.map +1 -0
  247. package/dist/trpc/routers/workspace.js +203 -0
  248. package/dist/trpc/routers/workspace.js.map +1 -0
  249. package/dist/unified-websocket-runner.d.ts +280 -0
  250. package/dist/unified-websocket-runner.d.ts.map +1 -0
  251. package/dist/unified-websocket-runner.js +3840 -0
  252. package/dist/unified-websocket-runner.js.map +1 -0
  253. package/dist/workspace-api.d.ts +14 -0
  254. package/dist/workspace-api.d.ts.map +1 -0
  255. package/dist/workspace-api.js +104 -0
  256. package/dist/workspace-api.js.map +1 -0
  257. package/dist/ws-adapter.d.ts +25 -0
  258. package/dist/ws-adapter.d.ts.map +1 -0
  259. package/dist/ws-adapter.js +63 -0
  260. package/dist/ws-adapter.js.map +1 -0
  261. package/package.json +92 -0
@@ -0,0 +1,1472 @@
1
+ import { createServer } from "node:http";
2
+ import { gzipSync } from "node:zlib";
3
+ import { mkdir, writeFile, stat, readFile } from "node:fs/promises";
4
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
5
+ import nodePath from "node:path";
6
+ import { createLogger, buildAssetUrl } from "@nodetool-ai/config";
7
+ import { workflowToDsl } from "@nodetool-ai/dsl";
8
+ import { Workflow, WorkflowVersion, Job, Asset } from "@nodetool-ai/models";
9
+ import { loadPythonPackageMetadata, NodeRegistry } from "@nodetool-ai/node-sdk";
10
+ import { registerBaseNodes } from "@nodetool-ai/base-nodes";
11
+ import { registerElevenLabsNodes } from "@nodetool-ai/elevenlabs-nodes";
12
+ import { registerTransformersJsNodes } from "@nodetool-ai/transformers-js-nodes";
13
+ import { registerFalNodes } from "@nodetool-ai/fal-nodes";
14
+ import { registerKieNodes } from "@nodetool-ai/kie-nodes";
15
+ import { registerReplicateNodes } from "@nodetool-ai/replicate-nodes";
16
+ import { PythonNodeExecutor, PythonStdioBridge } from "@nodetool-ai/runtime";
17
+ import { WorkflowRunner } from "@nodetool-ai/kernel";
18
+ import { handleOpenAIRequest } from "./openai-api.js";
19
+ import { handleOAuthRequest } from "./oauth-api.js";
20
+ import { createStorageHandler } from "./storage-api.js";
21
+ import { handleFileRequest } from "./file-api.js";
22
+ const log = createLogger("nodetool.websocket.http");
23
+ // ── Content type to file extension mapping ─────────────────────────
24
+ // Asset filename + storage-path helpers live in `./lib/asset-paths.ts` so the
25
+ // tRPC router can import them without dragging in the full http-api module
26
+ // graph. Re-exported here for any remaining REST callers.
27
+ import { getAssetFileName, getAssetStoragePath } from "./lib/asset-paths.js";
28
+ export { getAssetFileName, getAssetStoragePath };
29
+ // Lazily created storage handler — recreated if options change
30
+ let _storageHandler = null;
31
+ let _storageOpts;
32
+ let workflowRuntimePromise = null;
33
+ function getStorageHandler(opts) {
34
+ if (!_storageHandler || _storageOpts !== opts) {
35
+ _storageHandler = createStorageHandler(opts);
36
+ _storageOpts = opts;
37
+ }
38
+ return _storageHandler;
39
+ }
40
+ async function getWorkflowRuntimeEnvironment(options = {}) {
41
+ if (!workflowRuntimePromise) {
42
+ workflowRuntimePromise = (async () => {
43
+ const registry = options.registry ?? new NodeRegistry();
44
+ if (!options.registry) {
45
+ registry.loadPythonMetadata({
46
+ roots: options.metadataRoots,
47
+ maxDepth: options.metadataMaxDepth ?? 8
48
+ });
49
+ registerBaseNodes(registry);
50
+ registerElevenLabsNodes(registry);
51
+ registerTransformersJsNodes(registry);
52
+ registerFalNodes(registry);
53
+ registerKieNodes(registry);
54
+ registerReplicateNodes(registry);
55
+ }
56
+ const pythonBridge = new PythonStdioBridge({
57
+ workerArgs: process.env["NODETOOL_WORKER_NAMESPACES"]
58
+ ? ["--namespaces", process.env["NODETOOL_WORKER_NAMESPACES"]]
59
+ : []
60
+ });
61
+ const logPythonBridgeDiagnostics = (context) => {
62
+ const loadErrors = pythonBridge.getLoadErrors?.() ?? [];
63
+ if (loadErrors.length === 0)
64
+ return;
65
+ log.warn(`HTTP API Python bridge ${context} with ${loadErrors.length} load error(s)`);
66
+ for (const entry of loadErrors.slice(0, 10)) {
67
+ log.warn(`[python-worker][load-error] ${entry.module} (${entry.phase}): ${entry.error}`);
68
+ }
69
+ };
70
+ let pythonBridgeReady = false;
71
+ pythonBridge.on("stderr", (msg) => {
72
+ for (const line of msg.split("\n")) {
73
+ if (line.trim())
74
+ log.debug(`[python-worker] ${line}`);
75
+ }
76
+ });
77
+ pythonBridge.on("exit", (code) => {
78
+ log.warn(`HTTP API Python worker exited with code ${code}`);
79
+ pythonBridgeReady = false;
80
+ });
81
+ const ensurePythonBridge = async () => {
82
+ log.info("HTTP API lazily starting Python bridge");
83
+ try {
84
+ await pythonBridge.ensureConnected();
85
+ }
86
+ catch (err) {
87
+ log.warn("HTTP API Python bridge lazy start failed", err instanceof Error ? err : new Error(String(err)));
88
+ throw err;
89
+ }
90
+ pythonBridgeReady = true;
91
+ log.info("HTTP API Python bridge lazy start completed");
92
+ const meta = pythonBridge.getNodeMetadata();
93
+ log.info(`HTTP API Python bridge connected — ${meta.length} Python nodes`);
94
+ pythonBridge
95
+ .getWorkerStatus?.()
96
+ ?.then((status) => {
97
+ log.info("HTTP API Python bridge status", {
98
+ protocol_version: status.protocol_version,
99
+ node_count: status.node_count,
100
+ provider_count: status.provider_count,
101
+ namespaces: status.namespaces,
102
+ transport: status.transport,
103
+ max_frame_size: status.max_frame_size,
104
+ load_error_count: status.load_errors.length
105
+ });
106
+ })
107
+ .catch((err) => {
108
+ log.warn("HTTP API failed to fetch Python bridge status", err instanceof Error ? err : new Error(String(err)));
109
+ });
110
+ logPythonBridgeDiagnostics("connected");
111
+ };
112
+ const resolveExecutor = (node) => {
113
+ if (registry.has(node.type)) {
114
+ return registry.resolve(node);
115
+ }
116
+ if (pythonBridgeReady && pythonBridge.hasNodeType(node.type)) {
117
+ const meta = pythonBridge
118
+ .getNodeMetadata()
119
+ .find((n) => n.node_type === node.type);
120
+ const props = (node.properties ?? node.data ?? {});
121
+ return new PythonNodeExecutor(pythonBridge, node.type, props, Object.fromEntries((meta?.outputs ?? []).map((o) => [o.name, o.type.type])), meta?.required_settings ?? []);
122
+ }
123
+ if (registry.getMetadata(node.type) && !registry.has(node.type)) {
124
+ const stderrSummary = pythonBridge.getRecentStderrSummary?.() ?? null;
125
+ const loadErrors = pythonBridge.getLoadErrors?.() ?? [];
126
+ const matchingLoadError = loadErrors.find((entry) => entry.module.includes(node.type) ||
127
+ node.type.startsWith(entry.module.split(".").slice(2).join(".")));
128
+ throw new Error(pythonBridgeReady
129
+ ? `Python node "${node.type}" cannot execute: it is declared in metadata but was not loaded by the Python worker.${matchingLoadError ? ` Load error: ${matchingLoadError.module}: ${matchingLoadError.error}.` : stderrSummary ? ` Recent Python worker stderr: ${stderrSummary}` : " Check Python worker status/load errors for import failures."}`
130
+ : `Python node "${node.type}" cannot execute: Python worker is not connected.${stderrSummary ? ` Recent Python worker stderr: ${stderrSummary}` : ""}`);
131
+ }
132
+ return registry.resolve(node);
133
+ };
134
+ return { registry, pythonBridge, ensurePythonBridge, resolveExecutor };
135
+ })();
136
+ }
137
+ return workflowRuntimePromise;
138
+ }
139
+ // Rate-limit tracking for autosave: maps workflow_id -> last autosave timestamp (ms)
140
+ const lastAutosaveTime = new Map();
141
+ const AUTOSAVE_RATE_LIMIT_MS = 30_000;
142
+ function normalizePath(pathname) {
143
+ if (pathname.length > 1 && pathname.endsWith("/")) {
144
+ return pathname.slice(0, -1);
145
+ }
146
+ return pathname;
147
+ }
148
+ function jsonResponse(data, init) {
149
+ return new Response(JSON.stringify(data), {
150
+ status: init?.status ?? 200,
151
+ headers: {
152
+ "content-type": "application/json",
153
+ ...(init?.headers ?? {})
154
+ }
155
+ });
156
+ }
157
+ function errorResponse(status, detail) {
158
+ return jsonResponse({ detail }, { status });
159
+ }
160
+ export function getUserId(request, headerName) {
161
+ return (request.headers.get(headerName) ?? request.headers.get("x-user-id") ?? "1");
162
+ }
163
+ async function parseJsonBody(request) {
164
+ const contentType = request.headers.get("content-type") ?? "";
165
+ if (!contentType.toLowerCase().includes("application/json")) {
166
+ return null;
167
+ }
168
+ try {
169
+ return (await request.json());
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ export function toWorkflowResponse(workflow) {
176
+ return {
177
+ id: workflow.id,
178
+ access: workflow.access,
179
+ created_at: workflow.created_at,
180
+ updated_at: workflow.updated_at,
181
+ name: workflow.name,
182
+ tool_name: workflow.tool_name,
183
+ description: workflow.description,
184
+ tags: workflow.tags,
185
+ thumbnail: workflow.thumbnail,
186
+ thumbnail_url: workflow.thumbnail_url,
187
+ graph: workflow.graph,
188
+ input_schema: null,
189
+ output_schema: null,
190
+ settings: workflow.settings,
191
+ package_name: workflow.package_name,
192
+ path: workflow.path,
193
+ run_mode: workflow.run_mode,
194
+ workspace_id: workflow.workspace_id,
195
+ required_providers: null,
196
+ required_models: null,
197
+ html_app: workflow.html_app,
198
+ etag: workflow.getEtag()
199
+ };
200
+ }
201
+ export async function handleNodeMetadata(request, options) {
202
+ if (request.method !== "GET") {
203
+ return errorResponse(405, "Method not allowed");
204
+ }
205
+ const url = new URL(request.url);
206
+ const nodeType = url.searchParams.get("node_type");
207
+ const namespace = url.searchParams.get("namespace");
208
+ const queryParam = url.searchParams.get("query");
209
+ const fields = url.searchParams.get("fields") ?? "summary";
210
+ let nodes;
211
+ if (options.registry) {
212
+ nodes = options.registry.listMetadata();
213
+ }
214
+ else {
215
+ const loaded = loadPythonPackageMetadata({
216
+ roots: options.metadataRoots,
217
+ maxDepth: options.metadataMaxDepth
218
+ });
219
+ nodes = [...loaded.nodesByType.values()];
220
+ }
221
+ nodes.sort((a, b) => a.node_type.localeCompare(b.node_type));
222
+ // Exact node_type lookup returns the full metadata for that one node.
223
+ if (nodeType) {
224
+ const match = nodes.find((n) => n.node_type === nodeType);
225
+ if (!match)
226
+ return errorResponse(404, `Node type not found: ${nodeType}`);
227
+ return jsonResponse(match);
228
+ }
229
+ if (namespace) {
230
+ nodes = nodes.filter((n) => n.namespace?.startsWith(namespace));
231
+ }
232
+ if (queryParam) {
233
+ const terms = queryParam
234
+ .split(",")
235
+ .map((t) => t.trim().toLowerCase())
236
+ .filter((t) => t.length > 0);
237
+ if (terms.length > 0) {
238
+ const scored = nodes.map((n) => {
239
+ const haystack = `${n.title ?? ""} ${n.description ?? ""} ${n.node_type} ${n.namespace ?? ""}`.toLowerCase();
240
+ let score = 0;
241
+ for (const term of terms)
242
+ if (haystack.includes(term))
243
+ score++;
244
+ return { node: n, score };
245
+ });
246
+ nodes = scored
247
+ .filter((s) => s.score > 0)
248
+ .sort((a, b) => b.score - a.score)
249
+ .map((s) => s.node);
250
+ }
251
+ }
252
+ const limitRaw = url.searchParams.get("limit");
253
+ if (limitRaw) {
254
+ const parsed = Number.parseInt(limitRaw, 10);
255
+ if (Number.isFinite(parsed) && parsed > 0) {
256
+ nodes = nodes.slice(0, parsed);
257
+ }
258
+ }
259
+ // Default to slim summary to keep response size bounded. Full metadata is
260
+ // opt-in via `fields=full` or a specific node_type lookup.
261
+ if (fields === "full")
262
+ return jsonResponse(nodes);
263
+ return jsonResponse(nodes.map((n) => ({
264
+ node_type: n.node_type,
265
+ title: n.title,
266
+ description: n.description,
267
+ namespace: n.namespace
268
+ })));
269
+ }
270
+ export function parseLimit(url, defaultLimit = 100) {
271
+ const raw = url.searchParams.get("limit");
272
+ if (!raw)
273
+ return defaultLimit;
274
+ const parsed = Number.parseInt(raw, 10);
275
+ if (!Number.isFinite(parsed) || parsed <= 0)
276
+ return defaultLimit;
277
+ return Math.min(parsed, 500);
278
+ }
279
+ async function createWorkflow(body, userId) {
280
+ if (!body || typeof body.name !== "string") {
281
+ throw new Error("Invalid workflow");
282
+ }
283
+ if (!body.graph ||
284
+ !Array.isArray(body.graph.nodes) ||
285
+ !Array.isArray(body.graph.edges)) {
286
+ throw new Error("graph is required and must have nodes and edges arrays");
287
+ }
288
+ const graph = body.graph;
289
+ return (await Workflow.create({
290
+ user_id: userId,
291
+ name: body.name,
292
+ tool_name: body.tool_name ?? null,
293
+ package_name: body.package_name ?? null,
294
+ path: body.path ?? null,
295
+ tags: body.tags ?? [],
296
+ description: body.description ?? "",
297
+ thumbnail: body.thumbnail ?? null,
298
+ thumbnail_url: body.thumbnail_url ?? null,
299
+ access: body.access === "public" ? "public" : "private",
300
+ graph,
301
+ settings: body.settings ?? null,
302
+ run_mode: body.run_mode ?? "workflow",
303
+ workspace_id: body.workspace_id ?? null,
304
+ html_app: body.html_app ?? null
305
+ }));
306
+ }
307
+ async function updateWorkflow(id, body, userId) {
308
+ if (!body || typeof body.name !== "string") {
309
+ throw new Error("Invalid workflow");
310
+ }
311
+ if (!body.graph ||
312
+ !Array.isArray(body.graph.nodes) ||
313
+ !Array.isArray(body.graph.edges)) {
314
+ throw new Error("graph is required and must have nodes and edges arrays");
315
+ }
316
+ const graph = body.graph;
317
+ const existing = (await Workflow.get(id));
318
+ if (existing && existing.user_id !== userId) {
319
+ throw new Error("Workflow not found");
320
+ }
321
+ if (existing) {
322
+ existing.name = body.name;
323
+ existing.tool_name = body.tool_name ?? null;
324
+ existing.description = body.description ?? "";
325
+ existing.tags = body.tags ?? [];
326
+ existing.package_name = body.package_name ?? null;
327
+ if (body.thumbnail !== undefined)
328
+ existing.thumbnail = body.thumbnail;
329
+ existing.access = body.access === "public" ? "public" : "private";
330
+ existing.graph = graph;
331
+ existing.settings = body.settings ?? null;
332
+ if (body.run_mode !== undefined && body.run_mode !== null)
333
+ existing.run_mode = body.run_mode;
334
+ existing.workspace_id = body.workspace_id ?? null;
335
+ existing.html_app = body.html_app ?? null;
336
+ await existing.save();
337
+ return existing;
338
+ }
339
+ // Upsert: create the workflow if it doesn't exist
340
+ return (await Workflow.create({
341
+ id,
342
+ user_id: userId,
343
+ name: body.name,
344
+ tool_name: body.tool_name ?? null,
345
+ package_name: body.package_name ?? null,
346
+ path: body.path ?? null,
347
+ tags: body.tags ?? [],
348
+ description: body.description ?? "",
349
+ thumbnail: body.thumbnail ?? null,
350
+ thumbnail_url: body.thumbnail_url ?? null,
351
+ access: body.access === "public" ? "public" : "private",
352
+ graph: body.graph,
353
+ settings: body.settings ?? null,
354
+ run_mode: body.run_mode ?? "workflow",
355
+ workspace_id: body.workspace_id ?? null,
356
+ html_app: body.html_app ?? null
357
+ }));
358
+ }
359
+ export async function handleWorkflowRun(request, workflowId, options = {}) {
360
+ if (request.method !== "POST") {
361
+ return errorResponse(405, "Method not allowed");
362
+ }
363
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
364
+ const workflow = await Workflow.find(userId, workflowId);
365
+ if (!workflow) {
366
+ return errorResponse(404, "Workflow not found");
367
+ }
368
+ const runMode = workflow.run_mode ?? "workflow";
369
+ if (runMode !== "workflow") {
370
+ return errorResponse(400, `Workflow run mode "${runMode}" is not supported by the standalone backend`);
371
+ }
372
+ const body = await parseJsonBody(request);
373
+ const params = body?.params ?? {};
374
+ const graph = workflow.getGraph();
375
+ const runnableGraph = {
376
+ nodes: graph.nodes.map((node) => {
377
+ const record = node;
378
+ return {
379
+ ...record,
380
+ id: String(record.id ?? ""),
381
+ type: String(record.type ?? ""),
382
+ properties: (record.properties ?? record.data ?? {})
383
+ };
384
+ }),
385
+ edges: graph.edges.map((edge) => {
386
+ const record = edge;
387
+ return {
388
+ ...record,
389
+ id: typeof record.id === "string" || record.id == null
390
+ ? record.id
391
+ : String(record.id),
392
+ source: String(record.source ?? ""),
393
+ target: String(record.target ?? ""),
394
+ sourceHandle: String(record.sourceHandle ?? ""),
395
+ targetHandle: String(record.targetHandle ?? ""),
396
+ edge_type: record.edge_type === "control" ? "control" : "data"
397
+ };
398
+ })
399
+ };
400
+ const runtime = await getWorkflowRuntimeEnvironment(options);
401
+ const hasPythonNode = runnableGraph.nodes.some((node) => {
402
+ const nodeType = typeof node.type === "string" ? node.type : "";
403
+ return (nodeType !== "" &&
404
+ Boolean(runtime.registry.getMetadata(nodeType)) &&
405
+ !runtime.registry.has(nodeType));
406
+ });
407
+ if (hasPythonNode) {
408
+ await runtime.ensurePythonBridge();
409
+ }
410
+ const job = await Job.create({
411
+ workflow_id: workflowId,
412
+ user_id: userId,
413
+ status: "running",
414
+ params,
415
+ graph: runnableGraph
416
+ });
417
+ const runner = new WorkflowRunner(job.id, {
418
+ resolveExecutor: (node) => runtime.resolveExecutor(node)
419
+ });
420
+ const result = await runner.run({ job_id: job.id, workflow_id: workflowId, params }, runnableGraph);
421
+ if (result.status === "completed") {
422
+ job.markCompleted();
423
+ }
424
+ else if (result.status === "cancelled") {
425
+ job.markCancelled();
426
+ }
427
+ else {
428
+ job.markFailed(result.error ?? "Workflow run failed");
429
+ }
430
+ await job.save();
431
+ return jsonResponse({
432
+ job_id: job.id,
433
+ workflow_id: workflowId,
434
+ status: result.status,
435
+ outputs: result.outputs,
436
+ error: result.error ?? null,
437
+ message_count: result.messages.length,
438
+ background: body?.background ?? false
439
+ });
440
+ }
441
+ export async function handleWorkflowAutosave(request, workflowId, options) {
442
+ if (request.method !== "POST" && request.method !== "PUT") {
443
+ return errorResponse(405, "Method not allowed");
444
+ }
445
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
446
+ const workflow = (await Workflow.get(workflowId));
447
+ if (!workflow)
448
+ return errorResponse(404, "Workflow not found");
449
+ if (workflow.user_id !== userId)
450
+ return errorResponse(404, "Workflow not found");
451
+ const body = await parseJsonBody(request);
452
+ if (!body || !body.graph) {
453
+ return errorResponse(400, "Invalid body: graph is required");
454
+ }
455
+ const graph = body.graph;
456
+ const force = body?.force === true;
457
+ const maxVersions = typeof body?.max_versions === "number" ? body.max_versions : 10;
458
+ // Rate-limit: skip if last autosave < 30s ago and force is false
459
+ if (!force) {
460
+ const last = lastAutosaveTime.get(workflowId);
461
+ if (last !== undefined && Date.now() - last < AUTOSAVE_RATE_LIMIT_MS) {
462
+ return jsonResponse({
463
+ version: null,
464
+ message: "Autosave skipped (rate limited)",
465
+ skipped: true
466
+ });
467
+ }
468
+ }
469
+ workflow.graph = graph;
470
+ if (body.name !== undefined)
471
+ workflow.name = body.name;
472
+ if (body.description !== undefined)
473
+ workflow.description = body.description;
474
+ if (body.access === "public" || body.access === "private")
475
+ workflow.access = body.access;
476
+ await workflow.save();
477
+ lastAutosaveTime.set(workflowId, Date.now());
478
+ // Create a version and prune old ones if WorkflowVersion table is available
479
+ let version = null;
480
+ try {
481
+ const nextVer = await WorkflowVersion.nextVersion(workflowId);
482
+ const wv = new WorkflowVersion({
483
+ workflow_id: workflowId,
484
+ user_id: userId,
485
+ graph,
486
+ version: nextVer,
487
+ save_type: "autosave",
488
+ name: workflow.name,
489
+ description: workflow.description
490
+ });
491
+ await wv.save();
492
+ version = {
493
+ id: wv.id,
494
+ version: wv.version,
495
+ workflow_id: wv.workflow_id,
496
+ save_type: wv.save_type,
497
+ created_at: wv.created_at
498
+ };
499
+ await WorkflowVersion.pruneOldVersions(workflowId, maxVersions);
500
+ }
501
+ catch {
502
+ // non-fatal — version table may not exist
503
+ }
504
+ return jsonResponse({
505
+ version,
506
+ message: "Autosaved successfully",
507
+ skipped: false
508
+ });
509
+ }
510
+ // ── Workflow tools ─────────────────────────────────────────────────────
511
+ export async function handleWorkflowTools(request, options) {
512
+ if (request.method !== "GET") {
513
+ return errorResponse(405, "Method not allowed");
514
+ }
515
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
516
+ const url = new URL(request.url);
517
+ const limit = parseLimit(url, 100);
518
+ const [workflows] = await Workflow.paginateTools(userId, { limit });
519
+ return jsonResponse({
520
+ workflows: workflows.map((w) => ({
521
+ name: w.name,
522
+ tool_name: w.tool_name ?? null,
523
+ description: w.description ?? null
524
+ })),
525
+ next: null
526
+ });
527
+ }
528
+ // ── Workflow examples ──────────────────────────────────────────────────
529
+ /** URL prefix for example workflow thumbnail images served by this API. */
530
+ const EXAMPLES_THUMBNAILS_PREFIX = "/api/workflows/examples/thumbnails/";
531
+ /**
532
+ * Given an `examplesDir` (e.g. `.../nodetool/examples/nodetool-base`), return
533
+ * the sibling assets directory (`.../nodetool/assets/nodetool-base`).
534
+ */
535
+ function deriveAssetsDir(examplesDir) {
536
+ return nodePath.join(nodePath.dirname(nodePath.dirname(examplesDir)), "assets", nodePath.basename(examplesDir));
537
+ }
538
+ /**
539
+ * Read example workflow metadata from a directory of JSON files.
540
+ * Returns lightweight objects (no graph data) suitable for the /examples list.
541
+ *
542
+ * When the sibling `assets` directory exists (e.g.
543
+ * `packages/base-nodes/nodetool/assets/nodetool-base`), a `thumbnail_url`
544
+ * pointing to `/api/workflows/examples/thumbnails/<name>` is set so the
545
+ * frontend can display the pre-generated JPG thumbnails.
546
+ */
547
+ function buildExamplesFromDir(examplesDir) {
548
+ if (!existsSync(examplesDir))
549
+ return [];
550
+ // Derive the assets directory from the examples directory.
551
+ const assetsDir = deriveAssetsDir(examplesDir);
552
+ const now = new Date().toISOString();
553
+ const workflows = [];
554
+ let files;
555
+ try {
556
+ files = readdirSync(examplesDir)
557
+ .filter((f) => f.toLowerCase().endsWith(".json"))
558
+ .sort((a, b) => a.localeCompare(b));
559
+ }
560
+ catch (err) {
561
+ log.warn(`Failed to read examples directory ${examplesDir}: ${err}`);
562
+ return [];
563
+ }
564
+ for (const file of files) {
565
+ try {
566
+ const raw = readFileSync(nodePath.join(examplesDir, file), "utf8");
567
+ const parsed = JSON.parse(raw);
568
+ const name = typeof parsed.name === "string"
569
+ ? parsed.name
570
+ : file.replace(/\.json$/i, "");
571
+ // Point thumbnail_url to the served JPG when the file exists in assets.
572
+ const jpgFile = `${name}.jpg`;
573
+ const thumbnailUrl = existsSync(nodePath.join(assetsDir, jpgFile))
574
+ ? `${EXAMPLES_THUMBNAILS_PREFIX}${encodeURIComponent(jpgFile)}`
575
+ : null;
576
+ workflows.push({
577
+ id: file,
578
+ access: "public",
579
+ created_at: now,
580
+ updated_at: now,
581
+ name,
582
+ tool_name: null,
583
+ description: typeof parsed.description === "string" ? parsed.description : "",
584
+ tags: Array.isArray(parsed.tags)
585
+ ? parsed.tags.filter((t) => typeof t === "string")
586
+ : [],
587
+ thumbnail: thumbnailUrl ? jpgFile : null,
588
+ thumbnail_url: thumbnailUrl,
589
+ graph: { nodes: [], edges: [] },
590
+ input_schema: null,
591
+ output_schema: null,
592
+ settings: null,
593
+ package_name: typeof parsed.package_name === "string" ? parsed.package_name : null,
594
+ path: null,
595
+ run_mode: null,
596
+ workspace_id: null,
597
+ required_providers: null,
598
+ required_models: null,
599
+ html_app: null,
600
+ etag: null
601
+ });
602
+ }
603
+ catch (err) {
604
+ log.debug(`Skipping invalid example workflow file ${file}: ${err}`);
605
+ }
606
+ }
607
+ return workflows;
608
+ }
609
+ function buildExampleWorkflows(options) {
610
+ // If a static examples directory is configured, use it directly — no Python needed.
611
+ if (options.examplesDir) {
612
+ return buildExamplesFromDir(options.examplesDir);
613
+ }
614
+ const loaded = loadPythonPackageMetadata({
615
+ roots: options.metadataRoots,
616
+ maxDepth: options.metadataMaxDepth
617
+ });
618
+ const now = new Date().toISOString();
619
+ const workflows = [];
620
+ for (const pkg of loaded.packages) {
621
+ if (!pkg.examples || pkg.examples.length === 0)
622
+ continue;
623
+ for (const ex of pkg.examples) {
624
+ const meta = ex;
625
+ workflows.push({
626
+ id: meta.id ?? "",
627
+ access: "public",
628
+ created_at: now,
629
+ updated_at: now,
630
+ name: meta.name,
631
+ tool_name: null,
632
+ description: meta.description ?? "",
633
+ tags: meta.tags ?? [],
634
+ thumbnail: null,
635
+ thumbnail_url: null,
636
+ graph: { nodes: [], edges: [] },
637
+ input_schema: null,
638
+ output_schema: null,
639
+ settings: null,
640
+ package_name: pkg.name,
641
+ path: null,
642
+ run_mode: null,
643
+ workspace_id: null,
644
+ required_providers: null,
645
+ required_models: null,
646
+ html_app: null,
647
+ etag: null
648
+ });
649
+ }
650
+ }
651
+ return workflows;
652
+ }
653
+ function loadExampleGraph(packageName, exampleName, options) {
654
+ const loaded = loadPythonPackageMetadata({
655
+ roots: options.metadataRoots,
656
+ maxDepth: options.metadataMaxDepth
657
+ });
658
+ const pkg = loaded.packages.find((p) => p.name === packageName);
659
+ if (!pkg?.sourceFolder)
660
+ return null;
661
+ const examplePath = nodePath.join(pkg.sourceFolder, "nodetool", "examples", packageName, `${exampleName}.json`);
662
+ try {
663
+ const raw = readFileSync(examplePath, "utf8");
664
+ return JSON.parse(raw);
665
+ }
666
+ catch {
667
+ return null;
668
+ }
669
+ }
670
+ export async function handleWorkflowExamples(request, options) {
671
+ if (request.method !== "GET") {
672
+ return errorResponse(405, "Method not allowed");
673
+ }
674
+ const workflows = buildExampleWorkflows(options);
675
+ return jsonResponse({ workflows, next: null });
676
+ }
677
+ export async function handleWorkflowExamplesSearch(request, options) {
678
+ if (request.method !== "GET") {
679
+ return errorResponse(405, "Method not allowed");
680
+ }
681
+ const url = new URL(request.url);
682
+ const query = (url.searchParams.get("query") ?? "").toLowerCase().trim();
683
+ const workflows = buildExampleWorkflows(options);
684
+ const filtered = query
685
+ ? workflows.filter((w) => {
686
+ const wf = w;
687
+ const name = String(wf.name ?? "").toLowerCase();
688
+ const desc = String(wf.description ?? "").toLowerCase();
689
+ const tags = wf.tags ?? [];
690
+ return (name.includes(query) ||
691
+ desc.includes(query) ||
692
+ tags.some((t) => t.toLowerCase().includes(query)));
693
+ })
694
+ : workflows;
695
+ return jsonResponse({ workflows: filtered, next: null });
696
+ }
697
+ /**
698
+ * Serve a thumbnail JPG from the examples assets directory.
699
+ * URL: /api/workflows/examples/thumbnails/<encoded-filename>
700
+ */
701
+ export async function handleWorkflowExamplesThumbnail(request, filename, options) {
702
+ if (request.method !== "GET") {
703
+ return errorResponse(405, "Method not allowed");
704
+ }
705
+ if (!options.examplesDir) {
706
+ return errorResponse(404, "Examples not configured");
707
+ }
708
+ // Derive the assets directory from the examples directory.
709
+ const assetsDir = deriveAssetsDir(options.examplesDir);
710
+ // Prevent path traversal — only allow a plain filename (no slashes).
711
+ const safe = nodePath.basename(filename);
712
+ const safeLower = safe.toLowerCase();
713
+ if (!safeLower.endsWith(".jpg") && !safeLower.endsWith(".png")) {
714
+ return errorResponse(400, "Only .jpg and .png thumbnails are supported");
715
+ }
716
+ const filePath = nodePath.join(assetsDir, safe);
717
+ if (!existsSync(filePath)) {
718
+ return errorResponse(404, "Thumbnail not found");
719
+ }
720
+ const { createReadStream, statSync } = await import("node:fs");
721
+ let stat;
722
+ try {
723
+ stat = statSync(filePath);
724
+ }
725
+ catch {
726
+ return errorResponse(404, "Thumbnail not found");
727
+ }
728
+ const contentType = safeLower.endsWith(".png") ? "image/png" : "image/jpeg";
729
+ const stream = createReadStream(filePath);
730
+ const webStream = new ReadableStream({
731
+ start(controller) {
732
+ stream.on("data", (chunk) => controller.enqueue(chunk));
733
+ stream.on("end", () => controller.close());
734
+ stream.on("error", (err) => controller.error(err));
735
+ },
736
+ cancel() {
737
+ stream.destroy();
738
+ }
739
+ });
740
+ return new Response(webStream, {
741
+ status: 200,
742
+ headers: {
743
+ "content-type": contentType,
744
+ "content-length": String(stat.size),
745
+ "cache-control": "public, max-age=86400"
746
+ }
747
+ });
748
+ }
749
+ // ── Workflow app page ──────────────────────────────────────────────────
750
+ export async function handleWorkflowApp(request, workflowId, options) {
751
+ if (request.method !== "GET") {
752
+ return errorResponse(405, "Method not allowed");
753
+ }
754
+ const baseUrl = options.baseUrl ?? "http://127.0.0.1:7777";
755
+ const html = `<!DOCTYPE html><html><head><title>Workflow App</title>
756
+ <script>window.WORKFLOW_ID=${JSON.stringify(workflowId)};window.API_URL=${JSON.stringify(baseUrl)};</script>
757
+ </head><body><div id="app"></div></body></html>`;
758
+ return new Response(html, {
759
+ status: 200,
760
+ headers: { "content-type": "text/html" }
761
+ });
762
+ }
763
+ // ── Workflow generate-name ─────────────────────────────────────────────
764
+ /**
765
+ * Generate a descriptive name for a workflow based on its node types.
766
+ * The Python backend uses an LLM for this; the TS standalone mode derives
767
+ * a name from the workflow graph's node types instead.
768
+ *
769
+ * Node types follow the pattern `<root>.<category>[.<subcategory>...]`,
770
+ * e.g. `nodetool.text.Generate`. We collect unique category segments
771
+ * (parts[1]) to form a human-readable label.
772
+ */
773
+ function deriveWorkflowName(workflow) {
774
+ const graph = workflow.graph;
775
+ const nodes = graph?.nodes ?? [];
776
+ if (nodes.length === 0) {
777
+ return workflow.name || "Untitled Workflow";
778
+ }
779
+ // Collect unique category segments from node types (second dotted segment).
780
+ const categories = new Set();
781
+ for (const n of nodes) {
782
+ if (typeof n.type === "string") {
783
+ const parts = n.type.split(".");
784
+ // Require at least root.category format.
785
+ if (parts.length >= 2 && parts[1]) {
786
+ categories.add(parts[1]);
787
+ }
788
+ }
789
+ }
790
+ const segments = Array.from(categories).slice(0, 3);
791
+ if (segments.length === 0) {
792
+ return workflow.name || "Untitled Workflow";
793
+ }
794
+ const label = segments
795
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
796
+ .join(" + ");
797
+ return `${label} Workflow`;
798
+ }
799
+ export async function handleWorkflowGenerateName(request, workflowId, options) {
800
+ if (request.method !== "POST") {
801
+ return errorResponse(405, "Method not allowed");
802
+ }
803
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
804
+ const workflow = (await Workflow.get(workflowId));
805
+ if (!workflow)
806
+ return errorResponse(404, "Workflow not found");
807
+ if (workflow.user_id !== userId)
808
+ return errorResponse(404, "Workflow not found");
809
+ const name = deriveWorkflowName(workflow);
810
+ return jsonResponse({ name });
811
+ }
812
+ // ── Workflow DSL export (stub) ─────────────────────────────────────────
813
+ export async function handleWorkflowDslExport(request, workflowId, options) {
814
+ if (request.method !== "GET") {
815
+ return errorResponse(405, "Method not allowed");
816
+ }
817
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
818
+ const workflow = (await Workflow.get(workflowId));
819
+ if (!workflow) {
820
+ return errorResponse(404, "Workflow not found");
821
+ }
822
+ if (workflow.access !== "public" && workflow.user_id !== userId) {
823
+ return errorResponse(404, "Workflow not found");
824
+ }
825
+ if (!workflow.graph) {
826
+ return errorResponse(400, "Workflow has no graph to export");
827
+ }
828
+ try {
829
+ const source = workflowToDsl(workflow.graph, {
830
+ workflowName: workflow.name
831
+ });
832
+ return new Response(source, {
833
+ status: 200,
834
+ headers: { "content-type": "text/plain; charset=utf-8" }
835
+ });
836
+ }
837
+ catch (error) {
838
+ const message = error instanceof Error
839
+ ? error.message
840
+ : "Failed to export workflow as DSL";
841
+ return errorResponse(400, message);
842
+ }
843
+ }
844
+ // ── Workflow Gradio export (stub) ──────────────────────────────────────
845
+ export async function handleWorkflowGradioExport(request, workflowId, options) {
846
+ if (request.method !== "POST") {
847
+ return errorResponse(405, "Method not allowed");
848
+ }
849
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
850
+ const workflow = (await Workflow.get(workflowId));
851
+ if (!workflow)
852
+ return errorResponse(404, "Workflow not found");
853
+ if (workflow.user_id !== userId) {
854
+ return errorResponse(404, "Workflow not found");
855
+ }
856
+ // Gradio export requires the Python Gradio library; return 501 in standalone TS mode.
857
+ return errorResponse(501, "Workflow Gradio export is not available in standalone mode");
858
+ }
859
+ // ── Workflow versions ──────────────────────────────────────────────────
860
+ function toVersionResponse(v) {
861
+ return {
862
+ id: v.id,
863
+ workflow_id: v.workflow_id,
864
+ user_id: v.user_id,
865
+ name: v.name,
866
+ description: v.description,
867
+ graph: v.graph,
868
+ version: v.version,
869
+ save_type: v.save_type ?? "manual",
870
+ autosave_metadata: v.autosave_metadata ?? null,
871
+ created_at: v.created_at
872
+ };
873
+ }
874
+ export async function handleWorkflowVersions(request, workflowId, options) {
875
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
876
+ if (request.method === "POST") {
877
+ const workflow = (await Workflow.get(workflowId));
878
+ if (!workflow)
879
+ return errorResponse(404, "Workflow not found");
880
+ if (workflow.user_id !== userId)
881
+ return errorResponse(404, "Workflow not found");
882
+ const body = await parseJsonBody(request);
883
+ const nextVer = await WorkflowVersion.nextVersion(workflowId);
884
+ const version = (await WorkflowVersion.create({
885
+ workflow_id: workflowId,
886
+ user_id: userId,
887
+ name: body?.name ?? null,
888
+ description: body?.description ?? null,
889
+ graph: workflow.graph,
890
+ version: nextVer
891
+ }));
892
+ return jsonResponse(toVersionResponse(version));
893
+ }
894
+ if (request.method === "GET") {
895
+ const url = new URL(request.url);
896
+ const limit = parseLimit(url, 100);
897
+ const versions = await WorkflowVersion.listForWorkflow(workflowId, {
898
+ limit
899
+ });
900
+ return jsonResponse({ versions: versions.map(toVersionResponse) });
901
+ }
902
+ return errorResponse(405, "Method not allowed");
903
+ }
904
+ export async function handleWorkflowVersionByNumber(request, workflowId, versionNumber, options) {
905
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
906
+ if (request.method === "GET") {
907
+ const version = await WorkflowVersion.findByVersion(workflowId, versionNumber);
908
+ if (!version)
909
+ return errorResponse(404, "Version not found");
910
+ const workflow = (await Workflow.get(workflowId));
911
+ if (!workflow)
912
+ return errorResponse(404, "Workflow not found");
913
+ if (workflow.user_id !== userId)
914
+ return errorResponse(404, "Workflow not found");
915
+ return jsonResponse(toVersionResponse(version));
916
+ }
917
+ if (request.method === "POST") {
918
+ // restore: copy version graph back to workflow
919
+ const version = await WorkflowVersion.findByVersion(workflowId, versionNumber);
920
+ if (!version)
921
+ return errorResponse(404, "Version not found");
922
+ const workflow = (await Workflow.get(workflowId));
923
+ if (!workflow)
924
+ return errorResponse(404, "Workflow not found");
925
+ if (workflow.user_id !== userId)
926
+ return errorResponse(404, "Workflow not found");
927
+ workflow.graph = version.graph;
928
+ await workflow.save();
929
+ return jsonResponse(toWorkflowResponse(workflow));
930
+ }
931
+ return errorResponse(405, "Method not allowed");
932
+ }
933
+ export async function handleWorkflowVersionDeleteById(request, _workflowId, versionId, options) {
934
+ if (request.method !== "DELETE") {
935
+ return errorResponse(405, "Method not allowed");
936
+ }
937
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
938
+ const version = (await WorkflowVersion.get(versionId));
939
+ if (!version)
940
+ return errorResponse(404, "Version not found");
941
+ if (version.user_id !== userId)
942
+ return errorResponse(404, "Version not found");
943
+ await version.delete();
944
+ return new Response(null, { status: 204 });
945
+ }
946
+ export async function handleWorkflowsRoot(request, options) {
947
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
948
+ const url = new URL(request.url);
949
+ if (request.method === "GET") {
950
+ const limit = parseLimit(url, 100);
951
+ const runMode = url.searchParams.get("run_mode")?.trim() ?? undefined;
952
+ // cursor and columns params accepted for Python parity (cursor ignored in memory adapter)
953
+ // columns param is Python-specific (column selection) — ignored here
954
+ const [workflows, cursor] = await Workflow.paginate(userId, {
955
+ limit,
956
+ runMode
957
+ });
958
+ return jsonResponse({
959
+ workflows: workflows.map((w) => toWorkflowResponse(w)),
960
+ next: cursor || null
961
+ });
962
+ }
963
+ if (request.method === "POST") {
964
+ const body = await parseJsonBody(request);
965
+ if (!body)
966
+ return errorResponse(400, "Invalid JSON body");
967
+ try {
968
+ const fromPkg = url.searchParams.get("from_example_package")?.trim() ?? undefined;
969
+ const fromName = url.searchParams.get("from_example_name")?.trim() ?? undefined;
970
+ if (fromPkg &&
971
+ fromName &&
972
+ (!body.graph || body.graph.nodes?.length === 0)) {
973
+ const example = loadExampleGraph(fromPkg, fromName, options);
974
+ if (example?.graph) {
975
+ body.graph = example.graph;
976
+ }
977
+ }
978
+ const workflow = await createWorkflow(body, userId);
979
+ return jsonResponse(toWorkflowResponse(workflow));
980
+ }
981
+ catch (error) {
982
+ const message = error instanceof Error ? error.message : "Invalid workflow";
983
+ return errorResponse(400, message);
984
+ }
985
+ }
986
+ return errorResponse(405, "Method not allowed");
987
+ }
988
+ export async function handlePublicWorkflows(request) {
989
+ if (request.method !== "GET") {
990
+ return errorResponse(405, "Method not allowed");
991
+ }
992
+ const url = new URL(request.url);
993
+ const limit = parseLimit(url, 100);
994
+ const [workflows] = await Workflow.paginatePublic({ limit });
995
+ return jsonResponse({
996
+ workflows: workflows.map((w) => toWorkflowResponse(w)),
997
+ next: null
998
+ });
999
+ }
1000
+ export async function handlePublicWorkflowById(request, workflowId) {
1001
+ if (request.method !== "GET") {
1002
+ return errorResponse(405, "Method not allowed");
1003
+ }
1004
+ const workflow = (await Workflow.get(workflowId));
1005
+ if (!workflow || workflow.access !== "public") {
1006
+ return errorResponse(404, "Workflow not found");
1007
+ }
1008
+ return jsonResponse(toWorkflowResponse(workflow));
1009
+ }
1010
+ export async function handleWorkflowById(request, workflowId, options) {
1011
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
1012
+ if (request.method === "GET") {
1013
+ const workflow = (await Workflow.get(workflowId));
1014
+ if (!workflow)
1015
+ return errorResponse(404, "Workflow not found");
1016
+ if (workflow.access !== "public" && workflow.user_id !== userId) {
1017
+ return errorResponse(404, "Workflow not found");
1018
+ }
1019
+ return jsonResponse(toWorkflowResponse(workflow));
1020
+ }
1021
+ if (request.method === "PUT") {
1022
+ const body = await parseJsonBody(request);
1023
+ if (!body)
1024
+ return errorResponse(400, "Invalid JSON body");
1025
+ try {
1026
+ const workflow = await updateWorkflow(workflowId, body, userId);
1027
+ return jsonResponse(toWorkflowResponse(workflow));
1028
+ }
1029
+ catch (error) {
1030
+ const message = error instanceof Error ? error.message : "Invalid workflow";
1031
+ if (message === "Workflow not found")
1032
+ return errorResponse(404, message);
1033
+ return errorResponse(400, message);
1034
+ }
1035
+ }
1036
+ if (request.method === "DELETE") {
1037
+ const workflow = (await Workflow.get(workflowId));
1038
+ if (!workflow)
1039
+ return errorResponse(404, "Workflow not found");
1040
+ if (workflow.user_id !== userId)
1041
+ return errorResponse(404, "Workflow not found");
1042
+ await workflow.delete();
1043
+ return new Response(null, { status: 204 });
1044
+ }
1045
+ return errorResponse(405, "Method not allowed");
1046
+ }
1047
+ // ── Job types & helpers ───────────────────────────────────────────
1048
+ /**
1049
+ * Full job response — still exported here because `mcp-server.ts` consumes it
1050
+ * when serving job metadata over MCP. Consider relocating if MCP also migrates.
1051
+ */
1052
+ export function toJobResponse(job) {
1053
+ return {
1054
+ id: job.id,
1055
+ user_id: job.user_id,
1056
+ job_type: "workflow",
1057
+ status: job.status,
1058
+ workflow_id: job.workflow_id,
1059
+ started_at: job.started_at ?? null,
1060
+ finished_at: job.finished_at ?? null,
1061
+ error: job.error ?? null,
1062
+ cost: null
1063
+ };
1064
+ }
1065
+ // ── Trigger job stubs ─────────────────────────────────────────────
1066
+ export async function handleTriggersRunning(request) {
1067
+ if (request.method !== "GET") {
1068
+ return errorResponse(405, "Method not allowed");
1069
+ }
1070
+ return jsonResponse({ workflows: [] });
1071
+ }
1072
+ export async function handleTriggerStart(request, _workflowId) {
1073
+ if (request.method !== "POST") {
1074
+ return errorResponse(405, "Method not allowed");
1075
+ }
1076
+ return errorResponse(501, "Trigger workflows not available in standalone mode");
1077
+ }
1078
+ export async function handleTriggerStop(request, _workflowId) {
1079
+ if (request.method !== "POST") {
1080
+ return errorResponse(405, "Method not allowed");
1081
+ }
1082
+ return errorResponse(501, "Trigger workflows not available in standalone mode");
1083
+ }
1084
+ // ── Nodes dummy ───────────────────────────────────────────────────
1085
+ export async function handleNodesDummy(request) {
1086
+ if (request.method !== "GET") {
1087
+ return errorResponse(405, "Method not allowed");
1088
+ }
1089
+ return jsonResponse({
1090
+ type: "asset",
1091
+ uri: "",
1092
+ asset_id: null,
1093
+ data: null,
1094
+ metadata: null
1095
+ });
1096
+ }
1097
+ export function toAssetResponse(asset) {
1098
+ const isFolder = asset.content_type === "folder";
1099
+ const fileName = isFolder
1100
+ ? null
1101
+ : getAssetFileName(asset.id, asset.content_type);
1102
+ const getUrl = fileName ? buildAssetUrl(fileName) : null;
1103
+ const hasThumbnail = asset.content_type.startsWith("image/") ||
1104
+ asset.content_type.startsWith("video/");
1105
+ const updatedTs = asset.updated_at
1106
+ ? Math.floor(new Date(asset.updated_at).getTime() / 1000)
1107
+ : 0;
1108
+ const thumbUrl = hasThumbnail
1109
+ ? `/api/assets/${asset.id}/thumbnail?t=${updatedTs}`
1110
+ : null;
1111
+ return {
1112
+ id: asset.id,
1113
+ user_id: asset.user_id,
1114
+ workflow_id: asset.workflow_id ?? null,
1115
+ parent_id: asset.parent_id ?? null,
1116
+ name: asset.name,
1117
+ content_type: asset.content_type,
1118
+ size: asset.size ?? null,
1119
+ metadata: asset.metadata ?? null,
1120
+ created_at: asset.created_at,
1121
+ get_url: getUrl,
1122
+ thumb_url: thumbUrl,
1123
+ duration: asset.duration ?? null,
1124
+ node_id: asset.node_id ?? null,
1125
+ job_id: asset.job_id ?? null
1126
+ };
1127
+ }
1128
+ /**
1129
+ * Handle multipart file upload at POST /api/assets. JSON-only creation has
1130
+ * moved to the tRPC `assets.create` procedure.
1131
+ */
1132
+ export async function handleAssetsRoot(request, options) {
1133
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
1134
+ if (request.method === "POST") {
1135
+ const contentType = request.headers.get("content-type") ?? "";
1136
+ let body = null;
1137
+ let fileBuffer = null;
1138
+ let fileSize = null;
1139
+ if (contentType.toLowerCase().includes("multipart/form-data")) {
1140
+ try {
1141
+ const fd = await request.formData();
1142
+ const file = fd.get("file");
1143
+ // Frontend sends metadata as "json" field
1144
+ const assetJson = fd.get("json") ?? fd.get("asset");
1145
+ if (assetJson && typeof assetJson === "string") {
1146
+ body = JSON.parse(assetJson);
1147
+ }
1148
+ if (file) {
1149
+ const arrayBuffer = await file.arrayBuffer();
1150
+ fileBuffer = Buffer.from(arrayBuffer);
1151
+ fileSize = fileBuffer.byteLength;
1152
+ // Use file metadata if not supplied in the asset JSON field
1153
+ if (!body) {
1154
+ body = {
1155
+ name: file.name,
1156
+ content_type: file.type || "application/octet-stream",
1157
+ parent_id: userId
1158
+ };
1159
+ }
1160
+ else {
1161
+ body.name = body.name || file.name;
1162
+ body.content_type =
1163
+ body.content_type || file.type || "application/octet-stream";
1164
+ body.parent_id = body.parent_id || userId;
1165
+ }
1166
+ }
1167
+ }
1168
+ catch {
1169
+ return errorResponse(400, "Invalid multipart form data");
1170
+ }
1171
+ }
1172
+ else {
1173
+ body = await parseJsonBody(request);
1174
+ }
1175
+ if (!body ||
1176
+ typeof body.name !== "string" ||
1177
+ typeof body.content_type !== "string" ||
1178
+ typeof body.parent_id !== "string") {
1179
+ return errorResponse(400, "Invalid JSON body: name, content_type, and parent_id are required");
1180
+ }
1181
+ const metadata = body.metadata ?? {};
1182
+ const asset = (await Asset.create({
1183
+ user_id: userId,
1184
+ name: body.name,
1185
+ content_type: body.content_type,
1186
+ parent_id: body.parent_id,
1187
+ workflow_id: body.workflow_id ?? null,
1188
+ node_id: body.node_id ?? null,
1189
+ job_id: body.job_id ?? null,
1190
+ metadata: Object.keys(metadata).length > 0 ? metadata : (body.metadata ?? null),
1191
+ size: fileSize ?? body.size ?? null
1192
+ }));
1193
+ // Write file to storage directory so it's accessible via /api/storage/
1194
+ if (fileBuffer) {
1195
+ const storagePath = getAssetStoragePath(options.storage);
1196
+ const fileName = getAssetFileName(asset.id, asset.content_type);
1197
+ const filePath = nodePath.join(storagePath, fileName);
1198
+ await mkdir(storagePath, { recursive: true });
1199
+ await writeFile(filePath, fileBuffer);
1200
+ }
1201
+ return jsonResponse(toAssetResponse(asset));
1202
+ }
1203
+ return errorResponse(405, "Method not allowed");
1204
+ }
1205
+ export async function handleAssetThumbnail(request, assetId, options) {
1206
+ if (request.method === "POST") {
1207
+ return errorResponse(501, "Thumbnail generation not available");
1208
+ }
1209
+ if (request.method !== "GET")
1210
+ return errorResponse(405, "Method not allowed");
1211
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
1212
+ const asset = await Asset.find(userId, assetId);
1213
+ if (!asset)
1214
+ return errorResponse(404, "Asset not found");
1215
+ if (!asset.hasThumbnail) {
1216
+ return errorResponse(400, `Asset type '${asset.content_type}' does not support thumbnails`);
1217
+ }
1218
+ const storagePath = getAssetStoragePath(options.storage);
1219
+ // Try to serve a pre-generated thumbnail ({id}_thumb.jpg)
1220
+ const thumbFileName = `${asset.id}_thumb.jpg`;
1221
+ const thumbFilePath = nodePath.join(storagePath, thumbFileName);
1222
+ try {
1223
+ const thumbStat = await stat(thumbFilePath);
1224
+ const data = await readFile(thumbFilePath);
1225
+ return new Response(data, {
1226
+ status: 200,
1227
+ headers: {
1228
+ "Content-Type": "image/jpeg",
1229
+ "Content-Length": String(thumbStat.size),
1230
+ "Cache-Control": "no-cache",
1231
+ "Last-Modified": thumbStat.mtime.toUTCString()
1232
+ }
1233
+ });
1234
+ }
1235
+ catch {
1236
+ // No pre-generated thumbnail, fall through
1237
+ }
1238
+ // Fall back to serving the original file (for images)
1239
+ if (asset.content_type.startsWith("image/")) {
1240
+ const fileName = getAssetFileName(asset.id, asset.content_type);
1241
+ const filePath = nodePath.join(storagePath, fileName);
1242
+ try {
1243
+ const fileStat = await stat(filePath);
1244
+ const data = await readFile(filePath);
1245
+ return new Response(data, {
1246
+ status: 200,
1247
+ headers: {
1248
+ "Content-Type": asset.content_type,
1249
+ "Content-Length": String(fileStat.size),
1250
+ "Cache-Control": "no-cache",
1251
+ "Last-Modified": fileStat.mtime.toUTCString()
1252
+ }
1253
+ });
1254
+ }
1255
+ catch {
1256
+ // File not found on disk
1257
+ }
1258
+ }
1259
+ return errorResponse(404, "No thumbnail available");
1260
+ }
1261
+ export async function handleApiRequest(request, options = {}) {
1262
+ const url = new URL(request.url);
1263
+ const pathname = normalizePath(url.pathname);
1264
+ if (pathname.startsWith("/v1/")) {
1265
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
1266
+ const response = await handleOpenAIRequest(request, pathname, userId, options.openai);
1267
+ if (response)
1268
+ return response;
1269
+ }
1270
+ if (pathname.startsWith("/api/oauth/")) {
1271
+ const response = await handleOAuthRequest(request, pathname, () => getUserId(request, options.userIdHeader ?? "x-user-id"));
1272
+ if (response)
1273
+ return response;
1274
+ }
1275
+ if (pathname === "/api/users/validate_username") {
1276
+ if (request.method !== "GET")
1277
+ return errorResponse(405, "Method not allowed");
1278
+ const url = new URL(request.url);
1279
+ const username = url.searchParams.get("username")?.trim() ?? null;
1280
+ if (username === null)
1281
+ return errorResponse(400, "username parameter is required");
1282
+ if (!username)
1283
+ return errorResponse(400, "username cannot be empty");
1284
+ const valid = /^[a-zA-Z0-9_-]{3,32}$/.test(username);
1285
+ return jsonResponse({ valid, available: true });
1286
+ }
1287
+ if (pathname === "/api/nodes/dummy") {
1288
+ return handleNodesDummy(request);
1289
+ }
1290
+ if (pathname === "/api/nodes/metadata" || pathname === "/api/node/metadata") {
1291
+ return handleNodeMetadata(request, options);
1292
+ }
1293
+ if (pathname === "/api/workflows") {
1294
+ return handleWorkflowsRoot(request, options);
1295
+ }
1296
+ if (pathname === "/api/workflows/names") {
1297
+ if (request.method !== "GET")
1298
+ return errorResponse(405, "Method not allowed");
1299
+ const userId = getUserId(request, options.userIdHeader ?? "x-user-id");
1300
+ const [workflows] = await Workflow.paginate(userId, { limit: 1000 });
1301
+ const names = {};
1302
+ for (const wf of workflows)
1303
+ names[wf.id] = wf.name;
1304
+ return jsonResponse(names);
1305
+ }
1306
+ if (pathname === "/api/workflows/tools") {
1307
+ return handleWorkflowTools(request, options);
1308
+ }
1309
+ if (pathname === "/api/workflows/examples") {
1310
+ return handleWorkflowExamples(request, options);
1311
+ }
1312
+ if (pathname === "/api/workflows/examples/search") {
1313
+ return handleWorkflowExamplesSearch(request, options);
1314
+ }
1315
+ if (pathname.startsWith("/api/workflows/examples/thumbnails/")) {
1316
+ const filename = decodeURIComponent(pathname.slice("/api/workflows/examples/thumbnails/".length));
1317
+ return handleWorkflowExamplesThumbnail(request, filename, options);
1318
+ }
1319
+ if (pathname.startsWith("/api/workflows/examples/")) {
1320
+ return errorResponse(404, "Examples not available in standalone mode");
1321
+ }
1322
+ if (pathname === "/api/workflows/public") {
1323
+ return handlePublicWorkflows(request);
1324
+ }
1325
+ if (pathname.startsWith("/api/workflows/public/")) {
1326
+ const workflowId = decodeURIComponent(pathname.slice("/api/workflows/public/".length));
1327
+ if (!workflowId)
1328
+ return errorResponse(404, "Not found");
1329
+ return handlePublicWorkflowById(request, workflowId);
1330
+ }
1331
+ // ── Workflow sub-resource routes ───────────────────────────────────
1332
+ // Pattern: /api/workflows/{id}/...
1333
+ {
1334
+ const wfSubMatch = pathname.match(/^\/api\/workflows\/([^/]+)\/(.+)$/);
1335
+ if (wfSubMatch) {
1336
+ const workflowId = decodeURIComponent(wfSubMatch[1]);
1337
+ const subPath = wfSubMatch[2];
1338
+ if (subPath === "run") {
1339
+ return handleWorkflowRun(request, workflowId, options);
1340
+ }
1341
+ if (subPath === "autosave") {
1342
+ return handleWorkflowAutosave(request, workflowId, options);
1343
+ }
1344
+ if (subPath === "app") {
1345
+ return handleWorkflowApp(request, workflowId, options);
1346
+ }
1347
+ if (subPath === "generate-name") {
1348
+ return handleWorkflowGenerateName(request, workflowId, options);
1349
+ }
1350
+ if (subPath === "dsl-export") {
1351
+ return handleWorkflowDslExport(request, workflowId, options);
1352
+ }
1353
+ if (subPath === "gradio-export") {
1354
+ return handleWorkflowGradioExport(request, workflowId, options);
1355
+ }
1356
+ if (subPath === "versions") {
1357
+ return handleWorkflowVersions(request, workflowId, options);
1358
+ }
1359
+ // /api/workflows/{id}/versions/{version}/restore
1360
+ const versionRestoreMatch = subPath.match(/^versions\/(\d+)\/restore$/);
1361
+ if (versionRestoreMatch) {
1362
+ const versionNum = Number.parseInt(versionRestoreMatch[1], 10);
1363
+ return handleWorkflowVersionByNumber(request, workflowId, versionNum, options);
1364
+ }
1365
+ // /api/workflows/{id}/versions/{version} (GET by version number)
1366
+ const versionNumMatch = subPath.match(/^versions\/(\d+)$/);
1367
+ if (versionNumMatch) {
1368
+ const versionNum = Number.parseInt(versionNumMatch[1], 10);
1369
+ return handleWorkflowVersionByNumber(request, workflowId, versionNum, options);
1370
+ }
1371
+ // /api/workflows/{id}/versions/{version_id} (DELETE by id — version_id is not numeric)
1372
+ const versionIdMatch = subPath.match(/^versions\/([^/]+)$/);
1373
+ if (versionIdMatch) {
1374
+ const versionId = decodeURIComponent(versionIdMatch[1]);
1375
+ return handleWorkflowVersionDeleteById(request, workflowId, versionId, options);
1376
+ }
1377
+ }
1378
+ }
1379
+ if (pathname.startsWith("/api/workflows/")) {
1380
+ const workflowId = decodeURIComponent(pathname.slice("/api/workflows/".length));
1381
+ if (!workflowId)
1382
+ return errorResponse(404, "Not found");
1383
+ return handleWorkflowById(request, workflowId, options);
1384
+ }
1385
+ if (pathname.startsWith("/api/storage/")) {
1386
+ return getStorageHandler(options.storage)(request);
1387
+ }
1388
+ if (pathname === "/api/files" || pathname.startsWith("/api/files/")) {
1389
+ return handleFileRequest(request);
1390
+ }
1391
+ if (pathname === "/admin/secrets/import") {
1392
+ if (request.method !== "POST")
1393
+ return errorResponse(405, "Method not allowed");
1394
+ return errorResponse(501, "Secrets import not available in standalone mode");
1395
+ }
1396
+ return errorResponse(404, "Not found");
1397
+ }
1398
+ async function readNodeRequestBody(request) {
1399
+ const chunks = [];
1400
+ for await (const chunk of request) {
1401
+ if (chunk instanceof Uint8Array) {
1402
+ chunks.push(chunk);
1403
+ }
1404
+ else {
1405
+ chunks.push(Buffer.from(String(chunk)));
1406
+ }
1407
+ }
1408
+ const size = chunks.reduce((sum, c) => sum + c.byteLength, 0);
1409
+ const merged = new Uint8Array(size);
1410
+ let offset = 0;
1411
+ for (const chunk of chunks) {
1412
+ merged.set(chunk, offset);
1413
+ offset += chunk.byteLength;
1414
+ }
1415
+ return merged;
1416
+ }
1417
+ export async function handleNodeHttpRequest(req, res, options = {}) {
1418
+ const method = req.method ?? "GET";
1419
+ const baseUrl = options.baseUrl ?? "http://127.0.0.1:7777";
1420
+ const url = new URL(req.url ?? "/", baseUrl);
1421
+ const headers = new Headers();
1422
+ for (const [key, value] of Object.entries(req.headers)) {
1423
+ if (Array.isArray(value)) {
1424
+ for (const v of value)
1425
+ headers.append(key, v);
1426
+ }
1427
+ else if (value !== undefined) {
1428
+ headers.set(key, value);
1429
+ }
1430
+ }
1431
+ const hasBody = method !== "GET" && method !== "HEAD";
1432
+ const rawBody = hasBody ? await readNodeRequestBody(req) : undefined;
1433
+ const request = new Request(url.toString(), {
1434
+ method,
1435
+ headers,
1436
+ body: rawBody && rawBody.byteLength > 0 ? new Uint8Array(rawBody) : undefined
1437
+ });
1438
+ const response = await handleApiRequest(request, options);
1439
+ res.statusCode = response.status;
1440
+ response.headers.forEach((value, key) => {
1441
+ res.setHeader(key, value);
1442
+ });
1443
+ if (!response.body) {
1444
+ res.end();
1445
+ return;
1446
+ }
1447
+ const bodyBuffer = Buffer.from(await response.arrayBuffer());
1448
+ // Gzip-compress large JSON responses for performance (e.g. /api/nodes/metadata
1449
+ // is ~5 MB uncompressed, ~550 KB compressed).
1450
+ const GZIP_THRESHOLD = 256 * 1024;
1451
+ const acceptEncoding = req.headers["accept-encoding"] ?? "";
1452
+ if (bodyBuffer.length > GZIP_THRESHOLD && acceptEncoding.includes("gzip")) {
1453
+ const compressed = gzipSync(bodyBuffer);
1454
+ res.setHeader("content-encoding", "gzip");
1455
+ res.setHeader("content-length", compressed.length);
1456
+ res.end(compressed);
1457
+ return;
1458
+ }
1459
+ res.end(bodyBuffer);
1460
+ }
1461
+ export function createHttpApiServer(options = {}) {
1462
+ return createServer((req, res) => {
1463
+ void handleNodeHttpRequest(req, res, options).catch((error) => {
1464
+ log.error("Request failed", error instanceof Error ? error : new Error(String(error)));
1465
+ res.statusCode = 500;
1466
+ res.setHeader("content-type", "application/json");
1467
+ const detail = error instanceof Error ? error.message : String(error);
1468
+ res.end(JSON.stringify({ detail }));
1469
+ });
1470
+ });
1471
+ }
1472
+ //# sourceMappingURL=http-api.js.map