jishushell 0.4.24 → 0.5.15

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 (281) hide show
  1. package/INSTALL-NOTICE +11 -0
  2. package/apps/anythingllm-container.yaml +287 -0
  3. package/apps/browserless-chromium-container.yaml +90 -0
  4. package/apps/filebrowser-container.yaml +163 -0
  5. package/apps/hermes-container.yaml +36 -2
  6. package/apps/ollama-binary.yaml +91 -90
  7. package/apps/ollama-cpu-container.yaml +8 -1
  8. package/apps/ollama-with-hollama-binary.yaml +91 -90
  9. package/apps/openclaw-binary.yaml +38 -1
  10. package/apps/openclaw-container.yaml +45 -2
  11. package/apps/openclaw-with-ollama-container.yaml +11 -2
  12. package/apps/openclaw-with-searxng-container.yaml +26 -2
  13. package/apps/openwebui-container.yaml +45 -1
  14. package/apps/playwright-container.yaml +7 -1
  15. package/apps/searxng-container.yaml +58 -7
  16. package/apps/weknora-container.yaml +471 -0
  17. package/dist/cli/app.js +79 -9
  18. package/dist/cli/app.js.map +1 -1
  19. package/dist/cli/doctor.d.ts +12 -12
  20. package/dist/cli/doctor.js +242 -55
  21. package/dist/cli/doctor.js.map +1 -1
  22. package/dist/cli/llm.d.ts +4 -3
  23. package/dist/cli/llm.js +4 -3
  24. package/dist/cli/llm.js.map +1 -1
  25. package/dist/cli/panel.d.ts +6 -5
  26. package/dist/cli/panel.js +10 -9
  27. package/dist/cli/panel.js.map +1 -1
  28. package/dist/config.d.ts +19 -0
  29. package/dist/config.js +99 -1
  30. package/dist/config.js.map +1 -1
  31. package/dist/control.d.ts +7 -6
  32. package/dist/control.js +7 -6
  33. package/dist/control.js.map +1 -1
  34. package/dist/install.js +3 -3
  35. package/dist/install.js.map +1 -1
  36. package/dist/routes/agent-apps.d.ts +1 -1
  37. package/dist/routes/agent-apps.js +1 -1
  38. package/dist/routes/apps.js +44 -11
  39. package/dist/routes/apps.js.map +1 -1
  40. package/dist/routes/auth.js +5 -2
  41. package/dist/routes/auth.js.map +1 -1
  42. package/dist/routes/backup.js +64 -11
  43. package/dist/routes/backup.js.map +1 -1
  44. package/dist/routes/external-mounts.d.ts +17 -0
  45. package/dist/routes/external-mounts.js +73 -0
  46. package/dist/routes/external-mounts.js.map +1 -0
  47. package/dist/routes/file-mounts.d.ts +13 -0
  48. package/dist/routes/file-mounts.js +90 -0
  49. package/dist/routes/file-mounts.js.map +1 -0
  50. package/dist/routes/files-organize.d.ts +28 -0
  51. package/dist/routes/files-organize.js +167 -0
  52. package/dist/routes/files-organize.js.map +1 -0
  53. package/dist/routes/files.d.ts +31 -0
  54. package/dist/routes/files.js +321 -0
  55. package/dist/routes/files.js.map +1 -0
  56. package/dist/routes/instances.js +826 -17
  57. package/dist/routes/instances.js.map +1 -1
  58. package/dist/routes/internal.d.ts +2 -0
  59. package/dist/routes/internal.js +59 -0
  60. package/dist/routes/internal.js.map +1 -0
  61. package/dist/routes/llm.js +24 -35
  62. package/dist/routes/llm.js.map +1 -1
  63. package/dist/routes/setup.js +10 -10
  64. package/dist/routes/setup.js.map +1 -1
  65. package/dist/routes/system.js +1 -1
  66. package/dist/routes/system.js.map +1 -1
  67. package/dist/routes/webdav.d.ts +17 -0
  68. package/dist/routes/webdav.js +114 -0
  69. package/dist/routes/webdav.js.map +1 -0
  70. package/dist/server.d.ts +9 -0
  71. package/dist/server.js +751 -20
  72. package/dist/server.js.map +1 -1
  73. package/dist/services/agent-apps/catalog.js +4 -3
  74. package/dist/services/agent-apps/catalog.js.map +1 -1
  75. package/dist/services/agent-apps/index.d.ts +1 -1
  76. package/dist/services/agent-apps/index.js +1 -1
  77. package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
  78. package/dist/services/agent-apps/installers/adapter.js +1 -1
  79. package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
  80. package/dist/services/agent-apps/installers/shell-script.js +3 -3
  81. package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
  82. package/dist/services/agent-apps/types.d.ts +2 -2
  83. package/dist/services/agent-apps/types.js +1 -1
  84. package/dist/services/app/app-compiler.d.ts +1 -1
  85. package/dist/services/app/app-compiler.js +5 -5
  86. package/dist/services/app/app-compiler.js.map +1 -1
  87. package/dist/services/app/app-manager.d.ts +25 -1
  88. package/dist/services/app/app-manager.js +829 -150
  89. package/dist/services/app/app-manager.js.map +1 -1
  90. package/dist/services/app/custom-manager.js.map +1 -1
  91. package/dist/services/app/hermes-agent-manager.js +7 -4
  92. package/dist/services/app/hermes-agent-manager.js.map +1 -1
  93. package/dist/services/app/ollama-manager.js +1 -1
  94. package/dist/services/app/ollama-manager.js.map +1 -1
  95. package/dist/services/app/openclaw-manager.js +20 -3
  96. package/dist/services/app/openclaw-manager.js.map +1 -1
  97. package/dist/services/app/platform-transform.d.ts +32 -0
  98. package/dist/services/app/platform-transform.js +65 -0
  99. package/dist/services/app/platform-transform.js.map +1 -0
  100. package/dist/services/app/provide-resolver.d.ts +29 -0
  101. package/dist/services/app/provide-resolver.js +112 -0
  102. package/dist/services/app/provide-resolver.js.map +1 -0
  103. package/dist/services/app-passwords.d.ts +61 -0
  104. package/dist/services/app-passwords.js +173 -0
  105. package/dist/services/app-passwords.js.map +1 -0
  106. package/dist/services/backup-manager.d.ts +11 -0
  107. package/dist/services/backup-manager.js +177 -4
  108. package/dist/services/backup-manager.js.map +1 -1
  109. package/dist/services/capability-endpoint-validator.d.ts +41 -0
  110. package/dist/services/capability-endpoint-validator.js +104 -0
  111. package/dist/services/capability-endpoint-validator.js.map +1 -0
  112. package/dist/services/capability-health.d.ts +16 -0
  113. package/dist/services/capability-health.js +121 -0
  114. package/dist/services/capability-health.js.map +1 -0
  115. package/dist/services/capability-registry.d.ts +106 -0
  116. package/dist/services/capability-registry.js +313 -0
  117. package/dist/services/capability-registry.js.map +1 -0
  118. package/dist/services/connection-apply.d.ts +91 -0
  119. package/dist/services/connection-apply.js +475 -0
  120. package/dist/services/connection-apply.js.map +1 -0
  121. package/dist/services/connection-resolver.d.ts +65 -0
  122. package/dist/services/connection-resolver.js +281 -0
  123. package/dist/services/connection-resolver.js.map +1 -0
  124. package/dist/services/connection-transactor.d.ts +39 -0
  125. package/dist/services/connection-transactor.js +351 -0
  126. package/dist/services/connection-transactor.js.map +1 -0
  127. package/dist/services/external-mounts.d.ts +40 -0
  128. package/dist/services/external-mounts.js +187 -0
  129. package/dist/services/external-mounts.js.map +1 -0
  130. package/dist/services/files-manager.d.ts +252 -0
  131. package/dist/services/files-manager.js +1075 -0
  132. package/dist/services/files-manager.js.map +1 -0
  133. package/dist/services/files-mounts.d.ts +42 -0
  134. package/dist/services/files-mounts.js +207 -0
  135. package/dist/services/files-mounts.js.map +1 -0
  136. package/dist/services/instance-manager.d.ts +13 -0
  137. package/dist/services/instance-manager.js +138 -46
  138. package/dist/services/instance-manager.js.map +1 -1
  139. package/dist/services/llm-proxy/index.d.ts +16 -2
  140. package/dist/services/llm-proxy/index.js +48 -44
  141. package/dist/services/llm-proxy/index.js.map +1 -1
  142. package/dist/services/llm-proxy/probe.d.ts +6 -0
  143. package/dist/services/llm-proxy/probe.js +85 -0
  144. package/dist/services/llm-proxy/probe.js.map +1 -0
  145. package/dist/services/llm-proxy/ssrf.d.ts +1 -0
  146. package/dist/services/llm-proxy/ssrf.js +24 -9
  147. package/dist/services/llm-proxy/ssrf.js.map +1 -1
  148. package/dist/services/nomad-manager.d.ts +4 -0
  149. package/dist/services/nomad-manager.js +428 -35
  150. package/dist/services/nomad-manager.js.map +1 -1
  151. package/dist/services/organize/applier.d.ts +46 -0
  152. package/dist/services/organize/applier.js +218 -0
  153. package/dist/services/organize/applier.js.map +1 -0
  154. package/dist/services/organize/rules.d.ts +57 -0
  155. package/dist/services/organize/rules.js +286 -0
  156. package/dist/services/organize/rules.js.map +1 -0
  157. package/dist/services/organize/scanner.d.ts +50 -0
  158. package/dist/services/organize/scanner.js +366 -0
  159. package/dist/services/organize/scanner.js.map +1 -0
  160. package/dist/services/organize/store.d.ts +14 -0
  161. package/dist/services/organize/store.js +82 -0
  162. package/dist/services/organize/store.js.map +1 -0
  163. package/dist/services/panel-manager.js +20 -1
  164. package/dist/services/panel-manager.js.map +1 -1
  165. package/dist/services/process-manager.js +4 -3
  166. package/dist/services/process-manager.js.map +1 -1
  167. package/dist/services/runtime/adapters/hermes.d.ts +30 -1
  168. package/dist/services/runtime/adapters/hermes.js +219 -6
  169. package/dist/services/runtime/adapters/hermes.js.map +1 -1
  170. package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
  171. package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
  172. package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
  173. package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
  174. package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
  175. package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
  176. package/dist/services/runtime/adapters/openclaw.d.ts +177 -0
  177. package/dist/services/runtime/adapters/openclaw.js +1171 -11
  178. package/dist/services/runtime/adapters/openclaw.js.map +1 -1
  179. package/dist/services/runtime/instance.d.ts +1 -1
  180. package/dist/services/runtime/instance.js +1 -1
  181. package/dist/services/runtime/instance.js.map +1 -1
  182. package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
  183. package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
  184. package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
  185. package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
  186. package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
  187. package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
  188. package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
  189. package/dist/services/runtime/mcp-shims/firewall.js +129 -0
  190. package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
  191. package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
  192. package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
  193. package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
  194. package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
  195. package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
  196. package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
  197. package/dist/services/runtime/migrations.d.ts +8 -0
  198. package/dist/services/runtime/migrations.js +100 -0
  199. package/dist/services/runtime/migrations.js.map +1 -1
  200. package/dist/services/runtime/types.d.ts +46 -0
  201. package/dist/services/setup-manager.js +99 -24
  202. package/dist/services/setup-manager.js.map +1 -1
  203. package/dist/services/suggestions.d.ts +27 -0
  204. package/dist/services/suggestions.js +133 -0
  205. package/dist/services/suggestions.js.map +1 -0
  206. package/dist/services/task-registry.js +4 -2
  207. package/dist/services/task-registry.js.map +1 -1
  208. package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
  209. package/dist/services/telemetry/device-fingerprint.js +1 -1
  210. package/dist/services/types-shim.d.ts +16 -0
  211. package/dist/services/types-shim.js +2 -0
  212. package/dist/services/types-shim.js.map +1 -0
  213. package/dist/services/webdav/server.d.ts +24 -0
  214. package/dist/services/webdav/server.js +420 -0
  215. package/dist/services/webdav/server.js.map +1 -0
  216. package/dist/services/webdav/xml-builder.d.ts +73 -0
  217. package/dist/services/webdav/xml-builder.js +156 -0
  218. package/dist/services/webdav/xml-builder.js.map +1 -0
  219. package/dist/services/workspace-builder.d.ts +29 -0
  220. package/dist/services/workspace-builder.js +188 -0
  221. package/dist/services/workspace-builder.js.map +1 -0
  222. package/dist/types.d.ts +231 -1
  223. package/dist/utils/instance-lock.d.ts +22 -0
  224. package/dist/utils/instance-lock.js +48 -0
  225. package/dist/utils/instance-lock.js.map +1 -0
  226. package/dist/utils/path-locks.d.ts +30 -0
  227. package/dist/utils/path-locks.js +63 -0
  228. package/dist/utils/path-locks.js.map +1 -0
  229. package/dist/utils/path-safety.d.ts +41 -0
  230. package/dist/utils/path-safety.js +119 -0
  231. package/dist/utils/path-safety.js.map +1 -0
  232. package/dist/utils/safe-json.js +55 -22
  233. package/dist/utils/safe-json.js.map +1 -1
  234. package/dist/utils/safe-write.d.ts +24 -0
  235. package/dist/utils/safe-write.js +82 -0
  236. package/dist/utils/safe-write.js.map +1 -0
  237. package/install/jishu-install.sh +323 -27
  238. package/install/jishu-uninstall.sh +353 -20
  239. package/package.json +18 -1
  240. package/public/assets/Dashboard-BdWPtroF.js +1 -0
  241. package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-B_2HlVBQ.js} +1 -1
  242. package/public/assets/HermesConfigForm-DVlhg3WV.js +4 -0
  243. package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-D7glTExX.js} +1 -1
  244. package/public/assets/InstanceDetail-CxSy2cpe.js +92 -0
  245. package/public/assets/{Login-BWsZH2mu.js → Login-Cfr5c2sv.js} +1 -1
  246. package/public/assets/NewInstance-BIYDmJis.js +1 -0
  247. package/public/assets/ProviderRecommendations-BuRnvRcI.js +1 -0
  248. package/public/assets/Settings-Cc-tYBil.js +1 -0
  249. package/public/assets/Setup-lGZEk5jq.js +1 -0
  250. package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-CoGqzxeV.js} +2 -2
  251. package/public/assets/index-87IJXG-w.css +1 -0
  252. package/public/assets/index-BZc5zH7u.js +19 -0
  253. package/public/assets/providers-DtNXh9JD.js +1 -0
  254. package/public/assets/registry-BWnkJgZ1.js +2 -0
  255. package/public/assets/{usePolling-Do5Erqm_.js → usePolling-CwwT9KrC.js} +1 -1
  256. package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-y9V7Sfuu.js} +1 -1
  257. package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-BWrEVJVb.js} +6 -6
  258. package/public/index.html +4 -4
  259. package/scripts/check-app-spec.mjs +457 -0
  260. package/scripts/check-i18n.mjs +154 -0
  261. package/scripts/check-new-file-tests.mjs +230 -0
  262. package/scripts/check-quarantine-expiry.mjs +105 -0
  263. package/scripts/perf/README.md +49 -0
  264. package/scripts/perf/auth.js +99 -0
  265. package/scripts/perf/config.js +63 -0
  266. package/scripts/perf/instances.js +143 -0
  267. package/scripts/perf/proxy.js +96 -0
  268. package/scripts/run.sh +4 -4
  269. package/scripts/smoke/files-w1.sh +142 -0
  270. package/scripts/smoke-backend.mjs +122 -0
  271. package/scripts/smoke-post-publish.mjs +346 -0
  272. package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
  273. package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
  274. package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
  275. package/public/assets/NewInstance-BCIrAd86.js +0 -1
  276. package/public/assets/Settings-xkDcduFz.js +0 -1
  277. package/public/assets/Setup-Cfuwj4gV.js +0 -1
  278. package/public/assets/index-CPhVFEsx.css +0 -1
  279. package/public/assets/index-DQsM6Joa.js +0 -19
  280. package/public/assets/providers-V-vwrExZ.js +0 -1
  281. package/public/assets/registry-B4UFJdpA.js +0 -2
package/dist/server.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import Fastify from "fastify";
2
2
  import fastifyStatic from "@fastify/static";
3
3
  import multipart from "@fastify/multipart";
4
- import { join, dirname } from "path";
4
+ import { basename, dirname, join } from "path";
5
5
  import { existsSync, realpathSync } from "fs";
6
6
  import { fileURLToPath } from "url";
7
7
  import { request as httpRequest } from "http";
8
+ import { request as httpsRequest } from "https";
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = dirname(__filename);
10
- import { ensureDirs, ensurePanelConfig, getPanelConfig, migrateCatalogSchemaIfNeeded, migrateHermesShimOutIfNeeded, migrateOpenclawCatalogIfNeeded, migrateOpenclawImageTagIfNeeded, } from "./config.js";
11
+ import { ensureDirs, ensurePanelConfig, getPanelConfig, migrateCatalogSchemaIfNeeded, migrateHermesShimOutIfNeeded, migrateOpenclawCatalogIfNeeded, migrateOpenclawImageTagIfNeeded, getInternalMcpToken, } from "./config.js";
12
+ import { timingSafeEqual } from "node:crypto";
11
13
  import { runStartupMigrations } from "./services/runtime/index.js";
12
14
  import { verifyToken } from "./auth.js";
13
15
  import { authRoutes } from "./routes/auth.js";
@@ -18,7 +20,15 @@ import { runtimeCatalogRoutes } from "./routes/runtime.js";
18
20
  import { appRoutes } from "./routes/apps.js";
19
21
  import { agentAppRoutes } from "./routes/agent-apps.js";
20
22
  import { llmRoutes } from "./routes/llm.js";
23
+ import { filesRoutes } from "./routes/files.js";
24
+ import { internalRoutes } from "./routes/internal.js";
25
+ import { organizeRoutes } from "./routes/files-organize.js";
26
+ import { fileMountsRoutes } from "./routes/file-mounts.js";
27
+ import { webdavRoutes } from "./routes/webdav.js";
28
+ import { externalMountsRoutes } from "./routes/external-mounts.js";
29
+ import { FilesManager } from "./services/files-manager.js";
21
30
  import * as appManager from "./services/app/app-manager.js";
31
+ import * as capabilityRegistry from "./services/capability-registry.js";
22
32
  import backupRoutes from "./routes/backup.js";
23
33
  import * as llmProxy from "./services/llm-proxy/index.js";
24
34
  import * as nomadManager from "./services/nomad-manager.js";
@@ -29,6 +39,107 @@ import { checkUpdate } from "./services/panel-manager.js";
29
39
  import { PROXY_IDENTITY_HEADERS } from "./constants.js";
30
40
  import { supportsGatewayWebsocket } from "./services/runtime/instance.js";
31
41
  const PUBLIC_PATHS = new Set(["/api/auth/status", "/api/auth/init", "/api/auth/login", "/api/setup/status", "/api/apps/validate-sudo-password"]);
42
+ /** Slug charset for /apps/:slug — same as instance ids. */
43
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
44
+ /** Regex for top-level app WebSocket upgrades (AI-FS v1 W2.5 PR-8). */
45
+ const APP_SLUG_WS_RE = /^\/apps\/([a-z0-9][a-z0-9-]{0,62})(\/.*)?$/;
46
+ /**
47
+ * Resolve the upstream host/port for a top-level /apps/:slug request using
48
+ * the capability registry. Resolution order:
49
+ * 1. `<slug>-ui` — canonical convention (e.g. filebrowser-ui)
50
+ * 2. `web-<slug>` — web-* convention (e.g. web-filebrowser)
51
+ * Returns the first entry with status === "running", or null if none found.
52
+ */
53
+ function resolveAppSlugUpstream(slug) {
54
+ const candidates = [`${slug}-ui`, `web-${slug}`];
55
+ for (const cap of candidates) {
56
+ const providers = capabilityRegistry.listProviders(cap);
57
+ const running = providers.find((p) => p.status === "running");
58
+ if (running) {
59
+ return { host: running.host, port: running.hostPort };
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+ function hasValidPanelUpgradeToken(req) {
65
+ const authHeader = req.headers.authorization || "";
66
+ const match = authHeader.match(/^Bearer\s+(\S+)$/);
67
+ let token = match?.[1] || "";
68
+ if (!token) {
69
+ const cookie = req.headers.cookie || "";
70
+ const cookieMatch = cookie.match(/(?:^|;\s*)jishushell_session=([^;]*)/);
71
+ token = cookieMatch?.[1] || "";
72
+ }
73
+ return Boolean(token && token.length <= 4096 && verifyToken(token));
74
+ }
75
+ function isLoopbackSocket(socket) {
76
+ const remoteIp = socket.remoteAddress || "";
77
+ return remoteIp === "127.0.0.1" || remoteIp === "::1" || remoteIp === "::ffff:127.0.0.1";
78
+ }
79
+ function joinCapabilityUpstreamPath(basePath, suffix) {
80
+ const normalizedBase = typeof basePath === "string" && basePath.trim()
81
+ ? (basePath.startsWith("/") ? basePath : `/${basePath}`)
82
+ : "/";
83
+ const normalizedSuffix = suffix.replace(/^\/+/, "");
84
+ if (!normalizedSuffix)
85
+ return normalizedBase;
86
+ return `${normalizedBase.replace(/\/+$/, "")}/${normalizedSuffix}`;
87
+ }
88
+ export function stripPanelSessionCookie(value) {
89
+ if (value === undefined)
90
+ return undefined;
91
+ const cookie = Array.isArray(value) ? value.join("; ") : value;
92
+ const preserved = cookie
93
+ .split(";")
94
+ .map((part) => part.trim())
95
+ .filter((part) => part && !/^jishushell_session=/i.test(part));
96
+ return preserved.length ? preserved.join("; ") : undefined;
97
+ }
98
+ export function buildCapabilityWebSocketHeaders(headers, upstreamOrigin) {
99
+ const upstreamHeaders = {};
100
+ for (const [key, value] of Object.entries(headers)) {
101
+ if (value === undefined)
102
+ continue;
103
+ const lower = key.toLowerCase();
104
+ if (lower === "host" || PROXY_IDENTITY_HEADERS.has(lower))
105
+ continue;
106
+ if (lower === "authorization")
107
+ continue;
108
+ if (lower === "origin") {
109
+ upstreamHeaders[key] = upstreamOrigin;
110
+ continue;
111
+ }
112
+ if (lower === "cookie") {
113
+ const cookie = stripPanelSessionCookie(value);
114
+ if (cookie)
115
+ upstreamHeaders[key] = cookie;
116
+ continue;
117
+ }
118
+ upstreamHeaders[key] = value;
119
+ }
120
+ return upstreamHeaders;
121
+ }
122
+ function forwardWebSocketRejection(socket, res) {
123
+ const statusCode = res.statusCode || 502;
124
+ const statusMessage = res.statusMessage || "Bad Gateway";
125
+ try {
126
+ socket.write(`HTTP/1.1 ${statusCode} ${statusMessage}\r\nConnection: close\r\n\r\n`);
127
+ }
128
+ catch { }
129
+ res.resume();
130
+ socket.destroy();
131
+ }
132
+ export function selectRefererWebSocketCapability(capabilities, refererCapability, wsPath) {
133
+ const matches = capabilities.filter((capability) => {
134
+ if (capability.visibility === "internal")
135
+ return false;
136
+ const capabilityPath = (capability.path || "/").replace(/\/+$/, "") || "/";
137
+ return wsPath === capabilityPath || wsPath.startsWith(capabilityPath === "/" ? "/" : `${capabilityPath}/`);
138
+ });
139
+ return matches.find((capability) => capability.capability !== refererCapability)
140
+ ?? matches.find((capability) => capability.capability === refererCapability)
141
+ ?? null;
142
+ }
32
143
  export async function createServer(options = {}) {
33
144
  const port = options.port ?? 8090;
34
145
  const host = options.host || "0.0.0.0";
@@ -42,7 +153,7 @@ export async function createServer(options = {}) {
42
153
  bodyLimit: 1048576,
43
154
  });
44
155
  // Allow empty JSON body for POST requests
45
- app.addContentTypeParser("application/json", { parseAs: "string" }, (req, body, done) => {
156
+ app.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
46
157
  if (!body || body.length === 0) {
47
158
  done(null, {});
48
159
  }
@@ -56,9 +167,49 @@ export async function createServer(options = {}) {
56
167
  }
57
168
  });
58
169
  // Parse text/plain and application/x-yaml as raw string (for app install endpoint)
59
- app.addContentTypeParser(["text/plain", "application/x-yaml"], { parseAs: "string" }, (req, body, done) => {
170
+ app.addContentTypeParser(["text/plain", "application/x-yaml"], { parseAs: "string" }, (_req, body, done) => {
60
171
  done(null, body);
61
172
  });
173
+ // Fallback: pass any other content type through as the raw stream. Without
174
+ // this, /apps/:slug/* (W2.5 PR-8) would reject POST/PUT uploads with 415
175
+ // because Fastify's default behavior is to reject unparseable bodies.
176
+ // Specific routes that need typed bodies (octet-stream upload, etc.)
177
+ // register their own narrower parsers — those win over this catch-all.
178
+ app.addContentTypeParser("*", (_req, payload, done) => {
179
+ done(null, payload);
180
+ });
181
+ // Eager-init the internal MCP token so the file exists on disk before
182
+ // any agent container starts and tries to read it via env injection.
183
+ // Lazy-init would only fire on the first request that carries an
184
+ // X-Jishushell-Internal-Token header, which is too late for the
185
+ // adapter onBeforeStart that mounts the value into the container.
186
+ try {
187
+ getInternalMcpToken();
188
+ }
189
+ catch (e) {
190
+ console.warn(`[server] internal MCP token init skipped: ${e?.message ?? e}`);
191
+ }
192
+ // ── Auth middleware for /apps/* (AI-FS v1 W2.5 PR-8) ──────────────────
193
+ // Top-level reverse proxy for installed apps. Added in AI-FS v1 W2.5 PR-8.
194
+ // Pairs with `/api/instances/:id/provides/:cap/*` which is instance-scoped.
195
+ // This hook is scoped to /apps/* only so it does not double-fire with the
196
+ // existing /api/* hook below.
197
+ app.addHook("onRequest", async (request, reply) => {
198
+ const path = request.url.split("?")[0];
199
+ if (!path.startsWith("/apps/"))
200
+ return;
201
+ const authHeader = request.headers.authorization || "";
202
+ const match = authHeader.match(/^Bearer\s+(\S+)$/);
203
+ let token = match?.[1] || "";
204
+ if (!token) {
205
+ const cookie = request.headers.cookie || "";
206
+ const cookieMatch = cookie.match(/(?:^|;\s*)jishushell_session=([^;]*)/);
207
+ token = cookieMatch?.[1] || "";
208
+ }
209
+ if (!token || token.length > 4096 || !verifyToken(token)) {
210
+ return reply.status(401).send({ detail: "Unauthorized" });
211
+ }
212
+ });
62
213
  // Auth middleware
63
214
  app.addHook("onRequest", async (request, reply) => {
64
215
  const path = request.url.split("?")[0];
@@ -73,6 +224,35 @@ export async function createServer(options = {}) {
73
224
  }
74
225
  return;
75
226
  }
227
+ // Internal token path — used by:
228
+ // (a) MCP shims spawned inside agent containers
229
+ // (need BOTH X-Jishushell-Internal-Token + X-Jishushell-Instance)
230
+ // (b) Panel app lifecycle scripts (post_start, etc.) calling back into
231
+ // /api/internal/* on the same host
232
+ // (need ONLY X-Jishushell-Internal-Token; no instance binding —
233
+ // apps aren't instances)
234
+ // Combined with the standard JWT path below; we never require both.
235
+ const internalToken = request.headers["x-jishushell-internal-token"];
236
+ if (typeof internalToken === "string" && internalToken.length >= 32) {
237
+ const expected = getInternalMcpToken();
238
+ if (internalToken.length === expected.length &&
239
+ timingSafeEqual(Buffer.from(internalToken), Buffer.from(expected))) {
240
+ const instanceId = request.headers["x-jishushell-instance"];
241
+ if (typeof instanceId === "string" && instanceManager.getInstance(instanceId)) {
242
+ request.internalCallerInstance = instanceId;
243
+ return;
244
+ }
245
+ // Path (b): app-lifecycle / internal-only routes. Allowed without
246
+ // an instance header. The route is responsible for asserting the
247
+ // (request as any).internalCallerScope === "panel" tag before
248
+ // returning sensitive data.
249
+ if (path.startsWith("/api/internal/")) {
250
+ request.internalCallerScope = "panel";
251
+ return;
252
+ }
253
+ }
254
+ return reply.status(401).send({ detail: "Unauthorized" });
255
+ }
76
256
  // Check Authorization header first, then fall back to httpOnly cookie
77
257
  const authHeader = request.headers.authorization || "";
78
258
  const match = authHeader.match(/^Bearer\s+(\S+)$/);
@@ -120,6 +300,122 @@ export async function createServer(options = {}) {
120
300
  await app.register(agentAppRoutes);
121
301
  await app.register(llmRoutes);
122
302
  await app.register(backupRoutes);
303
+ await app.register(internalRoutes);
304
+ // Single FilesManager instance shared across /api/files, /webdav, and
305
+ // /api/files/external-mounts so a mutation on the mount list (PUT
306
+ // /external-mounts) is visible to every layer immediately.
307
+ const panelCfgForFiles = getPanelConfig();
308
+ const initialExternalMounts = Array.isArray(panelCfgForFiles.external_mounts)
309
+ ? panelCfgForFiles.external_mounts
310
+ : [];
311
+ const sharedFilesManager = new FilesManager({
312
+ externalMounts: initialExternalMounts,
313
+ });
314
+ await app.register(async (scope) => {
315
+ // Encapsulated registration so the route-scoped octet-stream parser
316
+ // does not leak to other routes (matches plan G6/D6 — Files is the
317
+ // only route that needs raw-stream uploads at W1).
318
+ await filesRoutes(scope, { filesManager: sharedFilesManager });
319
+ await organizeRoutes(scope, { filesManager: sharedFilesManager });
320
+ await fileMountsRoutes(scope);
321
+ await externalMountsRoutes(scope, { filesManager: sharedFilesManager });
322
+ });
323
+ // WebDAV runs in its own scope — it has independent auth (app password
324
+ // Basic Auth, NOT the panel JWT) and registers extra HTTP methods that
325
+ // we don't want leaking to other routes.
326
+ await app.register(async (scope) => {
327
+ await webdavRoutes(scope, { filesManager: sharedFilesManager });
328
+ });
329
+ // ── Top-level /apps/:slug/* reverse proxy (AI-FS v1 W2.5 PR-8) ───────────
330
+ // Reverse-proxies HTTP requests for installed apps to their capability
331
+ // upstream. Auth is handled by the onRequest hook above. The /apps/:slug
332
+ // prefix is NOT stripped — apps like Filebrowser are configured with
333
+ // --baseURL=/apps/filebrowser and expect the full path.
334
+ //
335
+ // Resolution order: `<slug>-ui` → `web-<slug>` (first running entry wins).
336
+ // 503 if no running provider found. 400 if slug is malformed.
337
+ //
338
+ // Registered BEFORE the SPA static fallback so explicit routes win.
339
+ const appsProxyHandler = async (req, reply) => {
340
+ const rawSlug = req.params.slug ?? "";
341
+ if (!SLUG_RE.test(rawSlug)) {
342
+ return reply.status(400).send({ detail: "Invalid app slug", slug: rawSlug });
343
+ }
344
+ const upstream = resolveAppSlugUpstream(rawSlug);
345
+ if (!upstream) {
346
+ return reply.status(503).send({ detail: "app not running", slug: rawSlug });
347
+ }
348
+ const { host: upstreamHost, port: upstreamPort } = upstream;
349
+ // Pass the full original path unchanged (including /apps/<slug> prefix).
350
+ const rawUrl = req.raw.url ?? "/";
351
+ const targetPath = rawUrl;
352
+ const hostHeader = `${upstreamHost}:${upstreamPort}`;
353
+ // Build forwarded headers: strip cookie and authorization to avoid
354
+ // leaking panel session credentials to third-party apps.
355
+ const forwardHeaders = {};
356
+ for (const [key, value] of Object.entries(req.headers)) {
357
+ if (value === undefined)
358
+ continue;
359
+ const lower = key.toLowerCase();
360
+ if (lower === "host")
361
+ continue;
362
+ if (lower === "cookie") {
363
+ const stripped = stripPanelSessionCookie(value);
364
+ if (stripped)
365
+ forwardHeaders[key] = stripped;
366
+ continue;
367
+ }
368
+ if (lower === "authorization")
369
+ continue;
370
+ forwardHeaders[key] = value;
371
+ }
372
+ forwardHeaders["host"] = hostHeader;
373
+ // Hijack before writing to reply.raw so Fastify does not attempt its own
374
+ // lifecycle send after the handler returns.
375
+ reply.hijack();
376
+ await new Promise((resolve) => {
377
+ const proxyReq = httpRequest({
378
+ hostname: upstreamHost,
379
+ port: upstreamPort,
380
+ path: targetPath,
381
+ method: req.method,
382
+ headers: forwardHeaders,
383
+ }, (proxyRes) => {
384
+ // Stream the response back without buffering.
385
+ reply.raw.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
386
+ proxyRes.pipe(reply.raw, { end: true });
387
+ proxyRes.on("end", resolve);
388
+ proxyRes.on("error", () => {
389
+ try {
390
+ reply.raw.end();
391
+ }
392
+ catch { }
393
+ resolve();
394
+ });
395
+ });
396
+ proxyReq.on("error", (err) => {
397
+ console.error(`[apps-proxy] upstream error for slug=${rawSlug}:`, err.message);
398
+ if (!reply.raw.headersSent) {
399
+ reply.raw.writeHead(502, { "content-type": "application/json" });
400
+ reply.raw.end(JSON.stringify({ detail: "upstream error", slug: rawSlug }));
401
+ }
402
+ resolve();
403
+ });
404
+ // Forward request body if present (POST, PUT, PATCH, etc.)
405
+ if (req.rawBody) {
406
+ proxyReq.end(req.rawBody);
407
+ }
408
+ else if (req.raw.readable) {
409
+ req.raw.pipe(proxyReq, { end: true });
410
+ req.raw.on("end", () => { }); // ensure event fires
411
+ }
412
+ else {
413
+ proxyReq.end();
414
+ }
415
+ });
416
+ };
417
+ app.all("/apps/:slug", appsProxyHandler);
418
+ app.all("/apps/:slug/*", appsProxyHandler);
123
419
  // Serve frontend static files
124
420
  // Look for frontend dist in multiple locations
125
421
  const resolvedDir = existsSync(__dirname) ? realpathSync(__dirname) : __dirname;
@@ -142,6 +438,10 @@ export async function createServer(options = {}) {
142
438
  root: assetsDir,
143
439
  prefix: "/assets/",
144
440
  decorateReply: false,
441
+ cacheControl: false,
442
+ setHeaders: (res) => {
443
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
444
+ },
145
445
  });
146
446
  }
147
447
  // Serve other static files and SPA fallback
@@ -150,6 +450,16 @@ export async function createServer(options = {}) {
150
450
  prefix: "/",
151
451
  decorateReply: true,
152
452
  wildcard: false,
453
+ globIgnore: ["assets/**"],
454
+ cacheControl: false,
455
+ setHeaders: (res, filePath) => {
456
+ if (basename(filePath) === "index.html") {
457
+ res.setHeader("Cache-Control", "no-store");
458
+ }
459
+ else {
460
+ res.setHeader("Cache-Control", "public, max-age=0");
461
+ }
462
+ },
153
463
  });
154
464
  // SPA fallback: serve index.html for non-API, non-asset routes
155
465
  app.setNotFoundHandler(async (request, reply) => {
@@ -162,6 +472,10 @@ export async function createServer(options = {}) {
162
472
  error: { message: "proxy route not found", type: "server_error" },
163
473
  });
164
474
  }
475
+ if (path.startsWith("/assets/")) {
476
+ return reply.status(404).type("text/plain").send("Asset not found");
477
+ }
478
+ reply.header("Cache-Control", "no-store");
165
479
  return reply.sendFile("index.html");
166
480
  });
167
481
  }
@@ -242,20 +556,23 @@ export async function createServer(options = {}) {
242
556
  // pinned digest tag rewrite can now succeed.
243
557
  runStartupMigrations();
244
558
  }
245
- // Rebuild capability registry from running app instances
559
+ // Rebuild capability registry from app instances. PR 3 sub-step 3f
560
+ // of the app-interconnect design drops the legacy `portOverride`
561
+ // arg — each provide now resolves through `resolveProvideEndpoint`
562
+ // which reads the actual allocated port from runtime metadata, so
563
+ // multi-provide apps no longer collapse all entries onto the
564
+ // gateway port. For instances whose underlying job is currently
565
+ // not running, we still re-register the entries (so the
566
+ // Connections UI keeps them as greyed candidates) and immediately
567
+ // mark them stopped via `setProviderStatus` (§5.4 of the design).
246
568
  try {
247
- for (const inst of instanceManager.listInstances()) {
248
- if (!inst.id || !inst.app_id)
249
- continue;
250
- const appData = appManager.getApp(inst.app_id);
251
- if (!appData || !appData.spec.provides?.length)
569
+ for (const appData of appManager.listApps()) {
570
+ if (!appData.spec.provides?.length)
252
571
  continue;
253
- // Use the generic framework helper — it dispatches through
254
- // the runtime adapter and handles both OpenClaw (env var)
255
- // and adapter-based (runtime.ports[]) port discovery.
256
- const port = instanceManager.getGatewayPort(inst.id);
257
- if (port > 0) {
258
- appManager.registerCapabilities(inst.id, appData.spec, port);
572
+ appManager.registerCapabilities(appData.manifest.id, appData.spec);
573
+ const status = await nomadManager.getStatus(appData.manifest.id);
574
+ if (status.status === "stopped") {
575
+ capabilityRegistry.setProviderStatus(appData.manifest.id, "stopped");
259
576
  }
260
577
  }
261
578
  }
@@ -296,11 +613,156 @@ export async function createServer(options = {}) {
296
613
  // request lifecycle. Fastify's internal headersTimeout (73s) kills hijacked
297
614
  // sockets, so we intercept upgrades before Fastify sees them.
298
615
  const GATEWAY_WS_RE = /^\/api\/instances\/([a-z0-9][a-z0-9-]{0,62})\/gateway(?:\/(.*))?$/;
616
+ const CAPABILITY_WS_RE = /^\/api\/instances\/([a-z0-9][a-z0-9-]{0,62})\/provides\/([^/?]+)(?:\/(.*))?$/;
617
+ // Match Referer pointing at a capability proxy page (used to resolve
618
+ // root-level WS upgrades initiated by embedded pages like Browserless debugger
619
+ // whose Web Workers create WS connections to the panel origin directly).
620
+ const CAPABILITY_REF_RE = /\/api\/instances\/([a-z0-9][a-z0-9-]{0,62})\/provides\/([^/?]+)/;
299
621
  app.server.on("upgrade", async (req, socket, head) => {
300
622
  const url = req.url || "";
301
623
  const path = url.split("?")[0];
302
- const match = path.match(GATEWAY_WS_RE);
303
- if (!match) {
624
+ const gatewayMatch = path.match(GATEWAY_WS_RE);
625
+ const capabilityMatch = path.match(CAPABILITY_WS_RE);
626
+ // ── /apps/:slug/* WebSocket upgrade (AI-FS v1 W2.5 PR-8) ──────────────
627
+ // Proxy WS upgrades for top-level installed apps. Same auth gate as HTTP.
628
+ const appSlugWsMatch = path.match(APP_SLUG_WS_RE);
629
+ if (appSlugWsMatch && !gatewayMatch && !capabilityMatch) {
630
+ const slug = appSlugWsMatch[1];
631
+ // Auth gate: same JWT cookie/bearer check as the HTTP handler above.
632
+ if (!hasValidPanelUpgradeToken(req)) {
633
+ socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
634
+ socket.destroy();
635
+ return;
636
+ }
637
+ const upstream = resolveAppSlugUpstream(slug);
638
+ if (!upstream) {
639
+ socket.write("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n");
640
+ socket.destroy();
641
+ return;
642
+ }
643
+ const { host: upstreamHost, port: upstreamPort } = upstream;
644
+ const hostHeader = `${upstreamHost}:${upstreamPort}`;
645
+ const upstreamOrigin = `http://${hostHeader}`;
646
+ const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
647
+ const targetPath = `${path}${qs}`;
648
+ // Build upstream WS headers: strip cookie (panel session) and authorization.
649
+ const upstreamHeaders = buildCapabilityWebSocketHeaders(req.headers, upstreamOrigin);
650
+ socket.setTimeout?.(0);
651
+ socket.setNoDelay?.(true);
652
+ socket.setKeepAlive?.(true, 30000);
653
+ let proxyReq;
654
+ try {
655
+ proxyReq = httpRequest({
656
+ hostname: upstreamHost,
657
+ port: upstreamPort,
658
+ path: targetPath,
659
+ method: "GET",
660
+ timeout: 10_000,
661
+ headers: {
662
+ ...upstreamHeaders,
663
+ host: hostHeader,
664
+ },
665
+ });
666
+ }
667
+ catch {
668
+ try {
669
+ socket.destroy();
670
+ }
671
+ catch { }
672
+ activeWsSockets.delete(socket);
673
+ return;
674
+ }
675
+ proxyReq.on("timeout", () => {
676
+ proxyReq.destroy();
677
+ socket.destroy();
678
+ });
679
+ proxyReq.on("upgrade", (_res, proxySocket, proxyHead) => {
680
+ proxySocket.setTimeout(0);
681
+ proxySocket.setNoDelay(true);
682
+ proxySocket.setKeepAlive(true, 30000);
683
+ proxySocket.pause();
684
+ let handshake = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n";
685
+ for (const [k, v] of Object.entries(_res.headers)) {
686
+ if (v !== undefined && k.toLowerCase().startsWith("sec-websocket-")) {
687
+ handshake += `${k}: ${v}\r\n`;
688
+ }
689
+ }
690
+ handshake += "\r\n";
691
+ socket.write(handshake);
692
+ if (head.length > 0)
693
+ proxySocket.write(head);
694
+ if (proxyHead.length > 0)
695
+ socket.write(proxyHead);
696
+ proxySocket.pipe(socket);
697
+ socket.pipe(proxySocket);
698
+ proxySocket.resume();
699
+ activeWsSockets.add(socket);
700
+ activeWsSockets.add(proxySocket);
701
+ const release = (s) => { activeWsSockets.delete(s); };
702
+ const fail = () => {
703
+ try {
704
+ proxySocket.destroy();
705
+ }
706
+ catch { }
707
+ try {
708
+ socket.destroy();
709
+ }
710
+ catch { }
711
+ release(socket);
712
+ release(proxySocket);
713
+ };
714
+ proxySocket.on("close", () => {
715
+ release(proxySocket);
716
+ try {
717
+ if (!socket.destroyed && !socket.writableEnded)
718
+ socket.end();
719
+ }
720
+ catch { }
721
+ });
722
+ socket.on("close", () => {
723
+ release(socket);
724
+ try {
725
+ if (!proxySocket.destroyed && !proxySocket.writableEnded)
726
+ proxySocket.end();
727
+ }
728
+ catch { }
729
+ });
730
+ proxySocket.on("error", fail);
731
+ socket.on("error", fail);
732
+ });
733
+ proxyReq.on("response", (res) => {
734
+ forwardWebSocketRejection(socket, res);
735
+ activeWsSockets.delete(socket);
736
+ });
737
+ proxyReq.on("error", () => {
738
+ try {
739
+ socket.write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
740
+ }
741
+ catch { }
742
+ socket.destroy();
743
+ activeWsSockets.delete(socket);
744
+ });
745
+ proxyReq.end();
746
+ return;
747
+ }
748
+ // ── end /apps/:slug/* WS upgrade ───────────────────────────────────────
749
+ // When an embedded page (e.g. Browserless debugger's puppeteer Worker)
750
+ // opens a WS to the panel origin at a non-API path (e.g. /?launch=...),
751
+ // resolve the target instance via the Referer header pointing at the
752
+ // capability proxy page that served the embedded UI.
753
+ let refererCapabilityMatch = null;
754
+ if (!gatewayMatch && !capabilityMatch) {
755
+ const referer = req.headers.referer || "";
756
+ try {
757
+ const refUrl = new URL(referer, "http://localhost");
758
+ const m = refUrl.pathname.match(CAPABILITY_REF_RE);
759
+ if (m) {
760
+ refererCapabilityMatch = { instanceId: m[1], capability: decodeURIComponent(m[2]) };
761
+ }
762
+ }
763
+ catch { /* ignore malformed referer */ }
764
+ }
765
+ if (!gatewayMatch && !capabilityMatch && !refererCapabilityMatch) {
304
766
  // Not a gateway WebSocket — let it drop (Fastify doesn't handle WS natively)
305
767
  socket.destroy();
306
768
  return;
@@ -308,8 +770,7 @@ export async function createServer(options = {}) {
308
770
  // Defense-in-depth: validate Origin header for WebSocket upgrades.
309
771
  // Require Origin for non-loopback clients; gateway has its own token auth as primary.
310
772
  const origin = req.headers.origin || "";
311
- const remoteIp = socket.remoteAddress || "";
312
- const isLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1" || remoteIp === "::ffff:127.0.0.1";
773
+ const isLoopback = isLoopbackSocket(socket);
313
774
  if (!origin && !isLoopback) {
314
775
  socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
315
776
  socket.destroy();
@@ -331,6 +792,276 @@ export async function createServer(options = {}) {
331
792
  return;
332
793
  }
333
794
  }
795
+ if ((capabilityMatch || refererCapabilityMatch) && !hasValidPanelUpgradeToken(req)) {
796
+ socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
797
+ socket.destroy();
798
+ return;
799
+ }
800
+ // Referer-based WS proxy: embedded pages (like Browserless debugger's Web
801
+ // Worker) open WS connections to the panel origin (e.g. ws://panel:8090/?launch=...).
802
+ // We resolve the correct upstream by finding a capability on the same
803
+ // instance that serves the request path (e.g. path "/" with http protocol).
804
+ if (refererCapabilityMatch) {
805
+ const id = refererCapabilityMatch.instanceId;
806
+ const appData = instanceManager.getApp(id);
807
+ if (!appData) {
808
+ socket.destroy();
809
+ return;
810
+ }
811
+ const capabilities = instanceManager.getProvidedCapabilitiesForApp(id);
812
+ // Find a capability whose path prefix matches the WS request path.
813
+ // Prefer capabilities with path "/" (root API) that aren't the debugger UI itself.
814
+ const wsPath = path || "/";
815
+ const capability = selectRefererWebSocketCapability(capabilities, refererCapabilityMatch.capability, wsPath);
816
+ if (!capability || typeof capability.port !== "number" || capability.port < 1) {
817
+ try {
818
+ socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
819
+ }
820
+ catch { }
821
+ socket.destroy();
822
+ return;
823
+ }
824
+ const protocol = String(capability.protocol || "http").toLowerCase();
825
+ const upstreamRequest = protocol === "https" || protocol === "wss" ? httpsRequest : httpRequest;
826
+ const targetHost = await instanceManager.getHostForAppPort(id, capability.port);
827
+ const hostHeader = `${instanceManager.urlHost(targetHost)}:${capability.port}`;
828
+ const upstreamOrigin = `${protocol === "https" || protocol === "wss" ? "https" : "http"}://${hostHeader}`;
829
+ const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
830
+ const targetPath = `${joinCapabilityUpstreamPath(capability.path, path.replace(/^\/+/, ""))}${qs}`;
831
+ const upstreamHeaders = buildCapabilityWebSocketHeaders(req.headers, upstreamOrigin);
832
+ socket.setTimeout?.(0);
833
+ socket.setNoDelay?.(true);
834
+ socket.setKeepAlive?.(true, 30000);
835
+ let proxyReq;
836
+ try {
837
+ proxyReq = upstreamRequest({
838
+ hostname: targetHost,
839
+ port: capability.port,
840
+ path: targetPath,
841
+ method: "GET",
842
+ timeout: 10_000,
843
+ headers: {
844
+ ...upstreamHeaders,
845
+ host: hostHeader,
846
+ },
847
+ });
848
+ }
849
+ catch {
850
+ try {
851
+ socket.destroy();
852
+ }
853
+ catch { }
854
+ activeWsSockets.delete(socket);
855
+ return;
856
+ }
857
+ proxyReq.on("timeout", () => {
858
+ proxyReq.destroy();
859
+ socket.destroy();
860
+ });
861
+ proxyReq.on("upgrade", (_res, proxySocket, proxyHead) => {
862
+ proxySocket.setTimeout(0);
863
+ proxySocket.setNoDelay(true);
864
+ proxySocket.setKeepAlive(true, 30000);
865
+ proxySocket.pause();
866
+ let handshake = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n";
867
+ for (const [k, v] of Object.entries(_res.headers)) {
868
+ if (v !== undefined && k.toLowerCase().startsWith("sec-websocket-")) {
869
+ handshake += `${k}: ${v}\r\n`;
870
+ }
871
+ }
872
+ handshake += "\r\n";
873
+ socket.write(handshake);
874
+ if (head.length > 0)
875
+ proxySocket.write(head);
876
+ if (proxyHead.length > 0)
877
+ socket.write(proxyHead);
878
+ proxySocket.pipe(socket);
879
+ socket.pipe(proxySocket);
880
+ proxySocket.resume();
881
+ activeWsSockets.add(socket);
882
+ activeWsSockets.add(proxySocket);
883
+ const release = (s) => { activeWsSockets.delete(s); };
884
+ const fail = () => {
885
+ try {
886
+ proxySocket.destroy();
887
+ }
888
+ catch { }
889
+ try {
890
+ socket.destroy();
891
+ }
892
+ catch { }
893
+ release(socket);
894
+ release(proxySocket);
895
+ };
896
+ proxySocket.on("close", () => {
897
+ release(proxySocket);
898
+ try {
899
+ if (!socket.destroyed && !socket.writableEnded)
900
+ socket.end();
901
+ }
902
+ catch { }
903
+ });
904
+ socket.on("close", () => {
905
+ release(socket);
906
+ try {
907
+ if (!proxySocket.destroyed && !proxySocket.writableEnded)
908
+ proxySocket.end();
909
+ }
910
+ catch { }
911
+ });
912
+ proxySocket.on("error", fail);
913
+ socket.on("error", fail);
914
+ });
915
+ proxyReq.on("response", (res) => {
916
+ forwardWebSocketRejection(socket, res);
917
+ activeWsSockets.delete(socket);
918
+ });
919
+ proxyReq.on("error", () => {
920
+ try {
921
+ socket.write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
922
+ }
923
+ catch { }
924
+ socket.destroy();
925
+ activeWsSockets.delete(socket);
926
+ });
927
+ proxyReq.end();
928
+ return;
929
+ }
930
+ if (capabilityMatch) {
931
+ const id = capabilityMatch[1];
932
+ const capabilityName = decodeURIComponent(capabilityMatch[2] || "");
933
+ const appData = instanceManager.getApp(id);
934
+ if (!appData) {
935
+ socket.destroy();
936
+ return;
937
+ }
938
+ const capability = instanceManager.getProvidedCapabilitiesForApp(id).find((entry) => entry.capability === capabilityName);
939
+ if (!capability || capability.visibility === "internal") {
940
+ try {
941
+ socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
942
+ }
943
+ catch { }
944
+ socket.destroy();
945
+ return;
946
+ }
947
+ if (typeof capability.port !== "number" || capability.port < 1) {
948
+ try {
949
+ socket.write("HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n");
950
+ }
951
+ catch { }
952
+ socket.destroy();
953
+ return;
954
+ }
955
+ const protocol = String(capability.protocol || "http").toLowerCase();
956
+ const upstreamRequest = protocol === "https" || protocol === "wss" ? httpsRequest : httpRequest;
957
+ const targetHost = await instanceManager.getHostForAppPort(id, capability.port);
958
+ const hostHeader = `${instanceManager.urlHost(targetHost)}:${capability.port}`;
959
+ const upstreamOrigin = `${protocol === "https" || protocol === "wss" ? "https" : "http"}://${hostHeader}`;
960
+ const suffix = capabilityMatch[3] || "";
961
+ const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
962
+ const targetPath = `${joinCapabilityUpstreamPath(capability.path, suffix)}${qs}`;
963
+ const upstreamHeaders = buildCapabilityWebSocketHeaders(req.headers, upstreamOrigin);
964
+ socket.setTimeout?.(0);
965
+ socket.setNoDelay?.(true);
966
+ socket.setKeepAlive?.(true, 30000);
967
+ let proxyReq;
968
+ try {
969
+ proxyReq = upstreamRequest({
970
+ hostname: targetHost,
971
+ port: capability.port,
972
+ path: targetPath,
973
+ method: "GET",
974
+ timeout: 10_000,
975
+ headers: {
976
+ ...upstreamHeaders,
977
+ host: hostHeader,
978
+ },
979
+ });
980
+ }
981
+ catch {
982
+ try {
983
+ socket.destroy();
984
+ }
985
+ catch { }
986
+ activeWsSockets.delete(socket);
987
+ return;
988
+ }
989
+ proxyReq.on("timeout", () => {
990
+ proxyReq.destroy();
991
+ socket.destroy();
992
+ });
993
+ proxyReq.on("upgrade", (_res, proxySocket, proxyHead) => {
994
+ proxySocket.setTimeout(0);
995
+ proxySocket.setNoDelay(true);
996
+ proxySocket.setKeepAlive(true, 30000);
997
+ proxySocket.pause();
998
+ let handshake = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n";
999
+ for (const [k, v] of Object.entries(_res.headers)) {
1000
+ if (v !== undefined && k.toLowerCase().startsWith("sec-websocket-")) {
1001
+ handshake += `${k}: ${v}\r\n`;
1002
+ }
1003
+ }
1004
+ handshake += "\r\n";
1005
+ socket.write(handshake);
1006
+ if (head.length > 0)
1007
+ proxySocket.write(head);
1008
+ if (proxyHead.length > 0)
1009
+ socket.write(proxyHead);
1010
+ proxySocket.pipe(socket);
1011
+ socket.pipe(proxySocket);
1012
+ proxySocket.resume();
1013
+ activeWsSockets.add(socket);
1014
+ activeWsSockets.add(proxySocket);
1015
+ const release = (s) => {
1016
+ activeWsSockets.delete(s);
1017
+ };
1018
+ const fail = () => {
1019
+ try {
1020
+ proxySocket.destroy();
1021
+ }
1022
+ catch { }
1023
+ try {
1024
+ socket.destroy();
1025
+ }
1026
+ catch { }
1027
+ release(socket);
1028
+ release(proxySocket);
1029
+ };
1030
+ proxySocket.on("close", () => {
1031
+ release(proxySocket);
1032
+ try {
1033
+ if (!socket.destroyed && !socket.writableEnded)
1034
+ socket.end();
1035
+ }
1036
+ catch { }
1037
+ });
1038
+ socket.on("close", () => {
1039
+ release(socket);
1040
+ try {
1041
+ if (!proxySocket.destroyed && !proxySocket.writableEnded)
1042
+ proxySocket.end();
1043
+ }
1044
+ catch { }
1045
+ });
1046
+ proxySocket.on("error", fail);
1047
+ socket.on("error", fail);
1048
+ });
1049
+ proxyReq.on("response", (res) => {
1050
+ forwardWebSocketRejection(socket, res);
1051
+ activeWsSockets.delete(socket);
1052
+ });
1053
+ proxyReq.on("error", () => {
1054
+ try {
1055
+ socket.write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
1056
+ }
1057
+ catch { }
1058
+ socket.destroy();
1059
+ activeWsSockets.delete(socket);
1060
+ });
1061
+ proxyReq.end();
1062
+ return;
1063
+ }
1064
+ const match = gatewayMatch;
334
1065
  const id = match[1];
335
1066
  const inst = instanceManager.getInstance(id);
336
1067
  if (!inst) {