jishushell 0.6.18 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/anythingllm-container.yaml +1 -0
- package/apps/browserless-chromium-container.yaml +1 -0
- package/apps/filebrowser-container.yaml +1 -0
- package/apps/hermes-container.yaml +1 -7
- package/apps/immich-container-lite.yaml +337 -0
- package/apps/immich-container.yaml +371 -0
- package/apps/jishu-kb-container.yaml +26 -21
- package/apps/ollama-binary.yaml +1 -0
- package/apps/ollama-cpu-container.yaml +1 -0
- package/apps/ollama-with-hollama-binary.yaml +2 -0
- package/apps/openclaw-binary.yaml +4 -8
- package/apps/openclaw-container.yaml +1 -7
- package/apps/openclaw-with-ollama-container.yaml +1 -0
- package/apps/openclaw-with-searxng-container.yaml +20 -0
- package/apps/searxng-container.yaml +20 -0
- package/apps/weknora-container.yaml +5 -0
- package/dependencies/jishushell-panel-0.7.3.tgz +0 -0
- package/dist/cli/core.js +1 -1
- package/dist/cli/core.js.map +1 -1
- package/dist/cli/doctor.js +96 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/config.d.ts +9 -1
- package/dist/config.js +72 -2
- package/dist/config.js.map +1 -1
- package/dist/install.js +60 -19
- package/dist/install.js.map +1 -1
- package/dist/routes/admin.d.ts +2 -0
- package/dist/routes/admin.js +72 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/docker.d.ts +2 -0
- package/dist/routes/docker.js +58 -0
- package/dist/routes/docker.js.map +1 -0
- package/dist/routes/file-mounts.js +5 -8
- package/dist/routes/file-mounts.js.map +1 -1
- package/dist/routes/instances.d.ts +0 -14
- package/dist/routes/instances.js +44 -1184
- package/dist/routes/instances.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.js +53 -20
- package/dist/server.js.map +1 -1
- package/dist/services/app-common/catalog-service.js +15 -5
- package/dist/services/app-common/catalog-service.js.map +1 -1
- package/dist/services/app-common/delete-service.js +5 -0
- package/dist/services/app-common/delete-service.js.map +1 -1
- package/dist/services/app-common/instance-store.js +3 -0
- package/dist/services/app-common/instance-store.js.map +1 -1
- package/dist/services/app-common/lifecycle-service.js +12 -4
- package/dist/services/app-common/lifecycle-service.js.map +1 -1
- package/dist/services/app-common/ownership.d.ts +3 -0
- package/dist/services/app-common/ownership.js +11 -0
- package/dist/services/app-common/ownership.js.map +1 -0
- package/dist/services/app-common/runtime-facts.js +2 -0
- package/dist/services/app-common/runtime-facts.js.map +1 -1
- package/dist/services/app-common/spec-materializer.d.ts +0 -1
- package/dist/services/app-common/spec-materializer.js +21 -87
- package/dist/services/app-common/spec-materializer.js.map +1 -1
- package/dist/services/app-common/status-refresh.js +25 -13
- package/dist/services/app-common/status-refresh.js.map +1 -1
- package/dist/services/app-modules/browserless/routes.js +5 -3
- package/dist/services/app-modules/browserless/routes.js.map +1 -1
- package/dist/services/capabilities/contract.d.ts +1 -2
- package/dist/services/capabilities/contract.js +0 -10
- package/dist/services/capabilities/contract.js.map +1 -1
- package/dist/services/capabilities/endpoint-validator.js +0 -1
- package/dist/services/capabilities/endpoint-validator.js.map +1 -1
- package/dist/services/capabilities/health.js +1 -1
- package/dist/services/capabilities/health.js.map +1 -1
- package/dist/services/capability-proxy/http.d.ts +7 -0
- package/dist/services/capability-proxy/http.js +555 -0
- package/dist/services/capability-proxy/http.js.map +1 -0
- package/dist/services/capability-proxy/terminal.d.ts +4 -0
- package/dist/services/capability-proxy/terminal.js +179 -0
- package/dist/services/capability-proxy/terminal.js.map +1 -0
- package/dist/services/connections/admin.js +19 -9
- package/dist/services/connections/admin.js.map +1 -1
- package/dist/services/connections/apply.d.ts +3 -9
- package/dist/services/connections/apply.js +0 -29
- package/dist/services/connections/apply.js.map +1 -1
- package/dist/services/connections/transactor.js +2 -2
- package/dist/services/connections/transactor.js.map +1 -1
- package/dist/services/files/bootstrap.d.ts +7 -0
- package/dist/services/files/bootstrap.js +16 -0
- package/dist/services/files/bootstrap.js.map +1 -0
- package/dist/services/files/photos/upload-page.d.ts +2 -0
- package/dist/services/files/photos/upload-page.js +248 -0
- package/dist/services/files/photos/upload-page.js.map +1 -0
- package/dist/services/files/photos/upload-store.d.ts +74 -0
- package/dist/services/files/photos/upload-store.js +432 -0
- package/dist/services/files/photos/upload-store.js.map +1 -0
- package/dist/services/http/proxy-utils.d.ts +7 -0
- package/dist/services/http/proxy-utils.js +29 -0
- package/dist/services/http/proxy-utils.js.map +1 -0
- package/dist/services/http/request-utils.d.ts +3 -0
- package/dist/services/http/request-utils.js +23 -0
- package/dist/services/http/request-utils.js.map +1 -0
- package/dist/services/instances/manager.d.ts +6 -5
- package/dist/services/instances/manager.js +45 -51
- package/dist/services/instances/manager.js.map +1 -1
- package/dist/services/instances/pairing.d.ts +17 -0
- package/dist/services/instances/pairing.js +53 -0
- package/dist/services/instances/pairing.js.map +1 -0
- package/dist/services/instances/status.d.ts +2 -0
- package/dist/services/instances/status.js +11 -0
- package/dist/services/instances/status.js.map +1 -0
- package/dist/services/integrations/hermes/integration.d.ts +1 -1
- package/dist/services/integrations/hermes/integration.js +7 -8
- package/dist/services/integrations/hermes/integration.js.map +1 -1
- package/dist/services/integrations/immich/client.d.ts +93 -0
- package/dist/services/integrations/immich/client.js +458 -0
- package/dist/services/integrations/immich/client.js.map +1 -0
- package/dist/services/integrations/immich/config.d.ts +15 -0
- package/dist/services/integrations/immich/config.js +178 -0
- package/dist/services/integrations/immich/config.js.map +1 -0
- package/dist/services/integrations/immich/discovery.d.ts +9 -0
- package/dist/services/integrations/immich/discovery.js +101 -0
- package/dist/services/integrations/immich/discovery.js.map +1 -0
- package/dist/services/integrations/immich/gallery-renderer.d.ts +5 -0
- package/dist/services/integrations/immich/gallery-renderer.js +150 -0
- package/dist/services/integrations/immich/gallery-renderer.js.map +1 -0
- package/dist/services/integrations/immich/immich-shim.d.ts +11 -0
- package/dist/services/integrations/immich/immich-shim.js +439 -0
- package/dist/services/integrations/immich/immich-shim.js.map +1 -0
- package/dist/services/integrations/immich/integration.d.ts +18 -0
- package/dist/services/integrations/immich/integration.js +64 -0
- package/dist/services/integrations/immich/integration.js.map +1 -0
- package/dist/services/integrations/immich/photo-library.d.ts +4 -0
- package/dist/services/integrations/immich/photo-library.js +63 -0
- package/dist/services/integrations/immich/photo-library.js.map +1 -0
- package/dist/services/integrations/immich/review-executor.d.ts +3 -0
- package/dist/services/integrations/immich/review-executor.js +41 -0
- package/dist/services/integrations/immich/review-executor.js.map +1 -0
- package/dist/services/integrations/immich/review-session-service.d.ts +27 -0
- package/dist/services/integrations/immich/review-session-service.js +206 -0
- package/dist/services/integrations/immich/review-session-service.js.map +1 -0
- package/dist/services/integrations/immich/review-store.d.ts +47 -0
- package/dist/services/integrations/immich/review-store.js +347 -0
- package/dist/services/integrations/immich/review-store.js.map +1 -0
- package/dist/services/integrations/immich/routes.d.ts +7 -0
- package/dist/services/integrations/immich/routes.js +363 -0
- package/dist/services/integrations/immich/routes.js.map +1 -0
- package/dist/services/integrations/immich/types.d.ts +186 -0
- package/dist/services/integrations/immich/types.js +2 -0
- package/dist/services/integrations/immich/types.js.map +1 -0
- package/dist/services/integrations/index.d.ts +1 -0
- package/dist/services/integrations/index.js +1 -0
- package/dist/services/integrations/index.js.map +1 -1
- package/dist/services/integrations/installable/installers/integration.js +113 -7
- package/dist/services/integrations/installable/installers/integration.js.map +1 -1
- package/dist/services/integrations/jishukb/integration.d.ts +3 -1
- package/dist/services/integrations/jishukb/integration.js +121 -10
- package/dist/services/integrations/jishukb/integration.js.map +1 -1
- package/dist/services/integrations/openclaw/integration.d.ts +21 -7
- package/dist/services/integrations/openclaw/integration.js +385 -158
- package/dist/services/integrations/openclaw/integration.js.map +1 -1
- package/dist/services/integrations/openclaw/jishukb-native-mcp.d.ts +58 -0
- package/dist/services/integrations/openclaw/jishukb-native-mcp.js +373 -0
- package/dist/services/integrations/openclaw/jishukb-native-mcp.js.map +1 -0
- package/dist/services/integrations/openclaw/jishukb-shim.d.ts +8 -4
- package/dist/services/integrations/openclaw/jishukb-shim.js +624 -17
- package/dist/services/integrations/openclaw/jishukb-shim.js.map +1 -1
- package/dist/services/integrations/openclaw/mcporter.d.ts +13 -0
- package/dist/services/integrations/openclaw/mcporter.js +31 -0
- package/dist/services/integrations/openclaw/mcporter.js.map +1 -1
- package/dist/services/integrations/openclaw/native-mcp.d.ts +48 -0
- package/dist/services/integrations/openclaw/native-mcp.js +125 -0
- package/dist/services/integrations/openclaw/native-mcp.js.map +1 -0
- package/dist/services/integrations/openclaw/routes.js +4 -1
- package/dist/services/integrations/openclaw/routes.js.map +1 -1
- package/dist/services/integrations/types.d.ts +5 -17
- package/dist/services/repair/runtime-repair.js +68 -1
- package/dist/services/repair/runtime-repair.js.map +1 -1
- package/dist/services/runtime/docker-network.d.ts +8 -0
- package/dist/services/runtime/docker-network.js +123 -0
- package/dist/services/runtime/docker-network.js.map +1 -0
- package/dist/services/runtime/driver-registry.d.ts +4 -0
- package/dist/services/runtime/driver-registry.js.map +1 -1
- package/dist/services/runtime/drivers/nomad.d.ts +1 -0
- package/dist/services/runtime/drivers/nomad.js +35 -5
- package/dist/services/runtime/drivers/nomad.js.map +1 -1
- package/dist/services/runtime/service-manager.d.ts +2 -0
- package/dist/services/runtime/service-manager.js +18 -0
- package/dist/services/runtime/service-manager.js.map +1 -0
- package/dist/services/runtime/types.d.ts +1 -0
- package/dist/services/runtime/workload-compiler.js +29 -4
- package/dist/services/runtime/workload-compiler.js.map +1 -1
- package/dist/services/setup/setup-manager.js +29 -73
- package/dist/services/setup/setup-manager.js.map +1 -1
- package/dist/services/system/runtime-ownership.d.ts +36 -0
- package/dist/services/system/runtime-ownership.js +250 -0
- package/dist/services/system/runtime-ownership.js.map +1 -0
- package/dist/services/system/system-reconciler.js +53 -0
- package/dist/services/system/system-reconciler.js.map +1 -1
- package/dist/types.d.ts +19 -3
- package/dist/utils/path-safety.js +1 -1
- package/dist/utils/service-user.d.ts +13 -0
- package/dist/utils/service-user.js +129 -0
- package/dist/utils/service-user.js.map +1 -0
- package/install/jishu-install.sh +0 -1
- package/node_modules/brace-expansion/dist/commonjs/index.js +24 -14
- package/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -1
- package/node_modules/brace-expansion/dist/esm/index.js +24 -14
- package/node_modules/brace-expansion/dist/esm/index.js.map +1 -1
- package/node_modules/brace-expansion/package.json +2 -2
- package/node_modules/fast-uri/index.js +1 -1
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/fast-uri/test/security.test.js +28 -0
- package/node_modules/fastify/SECURITY.md +1 -1
- package/node_modules/fastify/SPONSORS.md +6 -4
- package/node_modules/fastify/docs/Guides/Database.md +0 -28
- package/node_modules/fastify/docs/Guides/Ecosystem.md +13 -2
- package/node_modules/fastify/docs/Guides/Serverless.md +2 -2
- package/node_modules/fastify/docs/Guides/Write-Plugin.md +1 -1
- package/node_modules/fastify/docs/Reference/Encapsulation.md +27 -26
- package/node_modules/fastify/docs/Reference/Errors.md +10 -4
- package/node_modules/fastify/docs/Reference/HTTP2.md +10 -10
- package/node_modules/fastify/docs/Reference/Hooks.md +4 -4
- package/node_modules/fastify/docs/Reference/Index.md +14 -16
- package/node_modules/fastify/docs/Reference/LTS.md +12 -13
- package/node_modules/fastify/docs/Reference/Lifecycle.md +9 -8
- package/node_modules/fastify/docs/Reference/Logging.md +44 -39
- package/node_modules/fastify/docs/Reference/Middleware.md +21 -25
- package/node_modules/fastify/docs/Reference/Principles.md +2 -2
- package/node_modules/fastify/docs/Reference/Reply.md +6 -1
- package/node_modules/fastify/docs/Reference/Request.md +27 -16
- package/node_modules/fastify/docs/Reference/Routes.md +5 -2
- package/node_modules/fastify/docs/Reference/Server.md +31 -3
- package/node_modules/fastify/docs/Reference/Type-Providers.md +29 -5
- package/node_modules/fastify/docs/Reference/Validation-and-Serialization.md +15 -2
- package/node_modules/fastify/docs/Reference/Warnings.md +7 -6
- package/node_modules/fastify/eslint.config.js +7 -2
- package/node_modules/fastify/fastify.d.ts +8 -3
- package/node_modules/fastify/fastify.js +43 -14
- package/node_modules/fastify/lib/content-type-parser.js +13 -1
- package/node_modules/fastify/lib/decorate.js +11 -3
- package/node_modules/fastify/lib/error-handler.js +4 -3
- package/node_modules/fastify/lib/error-serializer.js +59 -59
- package/node_modules/fastify/lib/errors.js +16 -1
- package/node_modules/fastify/lib/four-oh-four.js +14 -9
- package/node_modules/fastify/lib/handle-request.js +11 -5
- package/node_modules/fastify/lib/plugin-override.js +2 -1
- package/node_modules/fastify/lib/plugin-utils.js +5 -5
- package/node_modules/fastify/lib/reply.js +63 -8
- package/node_modules/fastify/lib/request.js +14 -4
- package/node_modules/fastify/lib/route.js +20 -6
- package/node_modules/fastify/lib/schema-controller.js +1 -1
- package/node_modules/fastify/lib/schemas.js +37 -30
- package/node_modules/fastify/lib/symbols.js +3 -1
- package/node_modules/fastify/lib/validation.js +1 -13
- package/node_modules/fastify/lib/warnings.js +3 -3
- package/node_modules/fastify/package.json +13 -15
- package/node_modules/fastify/scripts/validate-ecosystem-links.js +1 -0
- package/node_modules/fastify/test/bundler/esbuild/package.json +1 -1
- package/node_modules/fastify/test/close-pipelining.test.js +1 -2
- package/node_modules/fastify/test/custom-http-server.test.js +38 -0
- package/node_modules/fastify/test/decorator-instance-properties.test.js +63 -0
- package/node_modules/fastify/test/diagnostics-channel/async-error-handler.test.js +74 -0
- package/node_modules/fastify/test/hooks.test.js +23 -0
- package/node_modules/fastify/test/http-methods/get.test.js +1 -1
- package/node_modules/fastify/test/http2/plain.test.js +135 -0
- package/node_modules/fastify/test/http2/secure-with-fallback.test.js +1 -1
- package/node_modules/fastify/test/https/https.test.js +1 -2
- package/node_modules/fastify/test/internals/errors.test.js +31 -1
- package/node_modules/fastify/test/internals/plugin.test.js +3 -1
- package/node_modules/fastify/test/internals/request.test.js +27 -3
- package/node_modules/fastify/test/internals/schema-controller-perf.test.js +33 -0
- package/node_modules/fastify/test/logger/logging.test.js +18 -1
- package/node_modules/fastify/test/logger/options.test.js +38 -1
- package/node_modules/fastify/test/reply-error.test.js +1 -1
- package/node_modules/fastify/test/reply-trailers.test.js +70 -0
- package/node_modules/fastify/test/request-media-type.test.js +105 -0
- package/node_modules/fastify/test/route-prefix.test.js +34 -0
- package/node_modules/fastify/test/router-options.test.js +222 -11
- package/node_modules/fastify/test/schema-serialization.test.js +108 -0
- package/node_modules/fastify/test/schema-validation.test.js +24 -0
- package/node_modules/fastify/test/scripts/validate-ecosystem-links.test.js +40 -57
- package/node_modules/fastify/test/throw.test.js +14 -0
- package/node_modules/fastify/test/trust-proxy.test.js +21 -0
- package/node_modules/fastify/test/types/content-type-parser.tst.ts +70 -0
- package/node_modules/fastify/test/types/{decorate-request-reply.test-d.ts → decorate-request-reply.tst.ts} +4 -4
- package/node_modules/fastify/test/types/{dummy-plugin.ts → dummy-plugin.mts} +1 -1
- package/node_modules/fastify/test/types/errors.tst.ts +91 -0
- package/node_modules/fastify/test/types/fastify.tst.ts +351 -0
- package/node_modules/fastify/test/types/hooks.tst.ts +578 -0
- package/node_modules/fastify/test/types/instance.tst.ts +597 -0
- package/node_modules/fastify/test/types/{logger.test-d.ts → logger.tst.ts} +61 -62
- package/node_modules/fastify/test/types/plugin.tst.ts +96 -0
- package/node_modules/fastify/test/types/register.tst.ts +245 -0
- package/node_modules/fastify/test/types/reply.tst.ts +297 -0
- package/node_modules/fastify/test/types/request.tst.ts +199 -0
- package/node_modules/fastify/test/types/route.tst.ts +576 -0
- package/node_modules/fastify/test/types/{schema.test-d.ts → schema.tst.ts} +22 -22
- package/node_modules/fastify/test/types/{serverFactory.test-d.ts → serverFactory.tst.ts} +4 -4
- package/node_modules/fastify/test/types/tsconfig.json +9 -0
- package/node_modules/fastify/test/types/{type-provider.test-d.ts → type-provider.tst.ts} +256 -250
- package/node_modules/fastify/test/types/using.tst.ts +14 -0
- package/node_modules/fastify/types/errors.d.ts +3 -0
- package/node_modules/fastify/types/request.d.ts +23 -2
- package/node_modules/jishushell-panel/output/public/assets/{ApiKeyField-NKcbHjNz.js → ApiKeyField-Ce5d1xna.js} +1 -1
- package/node_modules/jishushell-panel/output/public/assets/{Dashboard-Da1fL38t.js → Dashboard-BXame3yg.js} +1 -1
- package/node_modules/jishushell-panel/output/public/assets/{HermesChatPanel-DZvmYsoh.js → HermesChatPanel-BHZtPCJd.js} +1 -1
- package/node_modules/jishushell-panel/output/public/assets/{HermesConfigForm-BLUWlKwm.js → HermesConfigForm-CB3GbNX9.js} +1 -1
- package/node_modules/jishushell-panel/output/public/assets/{InitPassword-BAKsshzk.js → InitPassword-Boab9F6g.js} +1 -1
- package/node_modules/jishushell-panel/output/public/assets/InstanceDetail-DrIWCqo-.js +14 -0
- package/node_modules/jishushell-panel/output/public/assets/{Login-DHeOmwI8.js → Login-CzpOkNau.js} +1 -1
- package/node_modules/jishushell-panel/output/public/assets/NewInstance-CANXyCcL.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/{ProviderRecommendations-H0ByEYF0.js → ProviderRecommendations-BABo9VOC.js} +1 -1
- package/node_modules/jishushell-panel/output/public/assets/Settings-CKp5XxFh.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/Setup-C7xVDPow.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/{WeixinLoginPanel-D-T6BxkQ.js → WeixinLoginPanel-B765Xz4C.js} +1 -1
- package/node_modules/jishushell-panel/output/public/assets/{index-ERt6_ngA.js → index-Bs6DSbiR.js} +6 -6
- package/node_modules/jishushell-panel/output/public/assets/{registry-DF93EzIb.js → registry-sWIZsIEF.js} +2 -2
- package/node_modules/jishushell-panel/output/public/assets/{usePolling-DeoThIQn.js → usePolling-D4IDOQd_.js} +1 -1
- package/node_modules/jishushell-panel/output/public/assets/vendor-i18n-Df8aUdv8.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/{vendor-react-Cc84NArf.js → vendor-react-0L0rjmYG.js} +3 -3
- package/node_modules/jishushell-panel/output/public/index.html +3 -3
- package/node_modules/jishushell-panel/package.json +1 -1
- package/node_modules/semver/classes/range.js +6 -2
- package/node_modules/semver/package.json +1 -1
- package/package.json +4 -4
- package/scripts/check-app-spec.mjs +4 -12
- package/scripts/check-architecture-boundaries.mjs +178 -0
- package/scripts/pack-gui-and-send-pi.sh +5 -3
- package/dependencies/jishushell-panel-0.6.18.tgz +0 -0
- package/dist/services/connections/suggestions.d.ts +0 -27
- package/dist/services/connections/suggestions.js +0 -124
- package/dist/services/connections/suggestions.js.map +0 -1
- package/node_modules/fastify/test/types/content-type-parser.test-d.ts +0 -72
- package/node_modules/fastify/test/types/errors.test-d.ts +0 -90
- package/node_modules/fastify/test/types/fastify.test-d.ts +0 -352
- package/node_modules/fastify/test/types/hooks.test-d.ts +0 -550
- package/node_modules/fastify/test/types/import.ts +0 -2
- package/node_modules/fastify/test/types/instance.test-d.ts +0 -588
- package/node_modules/fastify/test/types/plugin.test-d.ts +0 -97
- package/node_modules/fastify/test/types/register.test-d.ts +0 -237
- package/node_modules/fastify/test/types/reply.test-d.ts +0 -254
- package/node_modules/fastify/test/types/request.test-d.ts +0 -188
- package/node_modules/fastify/test/types/route.test-d.ts +0 -553
- package/node_modules/fastify/test/types/using.test-d.ts +0 -17
- package/node_modules/jishushell-panel/output/public/assets/InstanceDetail-Dgyc_TX5.js +0 -14
- package/node_modules/jishushell-panel/output/public/assets/NewInstance-CIy0cYtp.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/Settings-DAT-UMfP.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/Setup-g3uckFYR.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/vendor-i18n-CS8DFbkQ.js +0 -1
package/dist/routes/instances.js
CHANGED
|
@@ -1,839 +1,33 @@
|
|
|
1
|
-
import { getServiceManagerType } from "../config.js";
|
|
2
1
|
import { assertNotLocked } from "../services/backup/backup-manager.js";
|
|
3
2
|
import * as appService from "../services/app-common/service.js";
|
|
4
3
|
import * as appLifecycle from "../services/app-common/lifecycle-service.js";
|
|
5
|
-
import { cleanupInstance as cleanupProxyInstance, getLastProxyError,
|
|
4
|
+
import { cleanupInstance as cleanupProxyInstance, getLastProxyError, } from "../services/llm-proxy/instance-proxy.js";
|
|
6
5
|
import { createInstance, isInstanceAdminError, validateId, } from "../services/instances/admin.js";
|
|
7
6
|
import { cloneInstance, isInstanceCloneError } from "../services/instances/clone.js";
|
|
8
7
|
import { getConnectionStatus, getConnectionSummary, isConnectionAdminError, replaceConnections, } from "../services/connections/admin.js";
|
|
9
8
|
import { getAppConfigMeta, isAppConfigAdminError, readAppConfig, writeAppConfig, } from "../services/instances/config-admin.js";
|
|
10
|
-
import { augmentInstanceMetadata,
|
|
11
|
-
import { getIntegration
|
|
9
|
+
import { augmentInstanceMetadata, resolvePrimaryIntegrationKind, } from "../services/runtime/instance.js";
|
|
10
|
+
import { getIntegration } from "../services/integrations/index.js";
|
|
12
11
|
import { invalidateExecutionOwner } from "../services/app-common/execution-owner.js";
|
|
13
12
|
import { markRuntimeRepairRestartApplied } from "../services/repair/runtime-repair.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// Hop-by-hop headers that must not be forwarded by a proxy (RFC 2616 §13.5.1).
|
|
22
|
-
// Exported for integration-owned route modules that implement their own HTTP proxies.
|
|
23
|
-
export const HOP_BY_HOP = new Set([
|
|
24
|
-
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
|
|
25
|
-
"te", "trailer", "transfer-encoding", "upgrade",
|
|
26
|
-
]);
|
|
27
|
-
/**
|
|
28
|
-
* Strip the panel session cookie (`jishushell_session`) from a cookie header
|
|
29
|
-
* value, preserving any other cookies for upstream forwarding.
|
|
30
|
-
*/
|
|
31
|
-
function stripPanelSessionCookie(value) {
|
|
32
|
-
if (value === undefined)
|
|
33
|
-
return undefined;
|
|
34
|
-
const cookie = Array.isArray(value) ? value.join("; ") : value;
|
|
35
|
-
const preserved = cookie
|
|
36
|
-
.split(";")
|
|
37
|
-
.map((part) => part.trim())
|
|
38
|
-
.filter((part) => part && !/^jishushell_session=/i.test(part));
|
|
39
|
-
return preserved.length ? preserved.join("; ") : undefined;
|
|
40
|
-
}
|
|
41
|
-
function readHeaderValue(value) {
|
|
42
|
-
const header = Array.isArray(value) ? value[0] : value;
|
|
43
|
-
return typeof header === "string" && header.trim() ? header.trim() : null;
|
|
44
|
-
}
|
|
45
|
-
function parseHttpOrigin(value) {
|
|
46
|
-
if (!value)
|
|
47
|
-
return null;
|
|
48
|
-
try {
|
|
49
|
-
const parsed = new URL(value);
|
|
50
|
-
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.origin : null;
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
export function inferRequestOrigin(request) {
|
|
57
|
-
// Only trust browser-sent Origin/Referer for auto-allowlisting. Host and
|
|
58
|
-
// X-Forwarded-* are proxy metadata and should not become persisted origin
|
|
59
|
-
// policy by themselves.
|
|
60
|
-
return (parseHttpOrigin(readHeaderValue(request?.headers?.origin))
|
|
61
|
-
?? parseHttpOrigin(readHeaderValue(request?.headers?.referer)));
|
|
62
|
-
}
|
|
63
|
-
function capabilityProxyPath(instanceId, capability) {
|
|
64
|
-
return `/api/instances/${encodeURIComponent(instanceId)}/provides/${encodeURIComponent(capability)}`;
|
|
65
|
-
}
|
|
66
|
-
function joinProxyPath(basePath, suffix) {
|
|
67
|
-
const normalizedBase = basePath.replace(/\/+$/, "");
|
|
68
|
-
const normalizedSuffix = suffix.replace(/^\/+/, "");
|
|
69
|
-
if (!normalizedSuffix)
|
|
70
|
-
return normalizedBase;
|
|
71
|
-
return `${normalizedBase}/${normalizedSuffix}`;
|
|
72
|
-
}
|
|
73
|
-
function canonicalCapabilityProxyBase(basePath, capabilityPath) {
|
|
74
|
-
const normalizedCapabilityPath = typeof capabilityPath === "string" ? capabilityPath.trim() : "";
|
|
75
|
-
const needsTrailingSlash = !normalizedCapabilityPath || normalizedCapabilityPath.endsWith("/");
|
|
76
|
-
return needsTrailingSlash ? `${basePath}/` : basePath;
|
|
77
|
-
}
|
|
78
|
-
function rewriteCapabilityLocation(basePath, canonicalBasePath, pathname, search = "", hash = "") {
|
|
79
|
-
const rewrittenPath = pathname === "/"
|
|
80
|
-
? canonicalBasePath
|
|
81
|
-
: joinProxyPath(basePath, pathname);
|
|
82
|
-
return `${rewrittenPath}${search}${hash}`;
|
|
83
|
-
}
|
|
84
|
-
export function joinUpstreamPath(basePath, suffix) {
|
|
85
|
-
const normalizedBase = typeof basePath === "string" && basePath.trim()
|
|
86
|
-
? (basePath.startsWith("/") ? basePath : `/${basePath}`)
|
|
87
|
-
: "/";
|
|
88
|
-
const normalizedSuffix = suffix.replace(/^\/+/, "");
|
|
89
|
-
if (!normalizedSuffix)
|
|
90
|
-
return normalizedBase;
|
|
91
|
-
return `${normalizedBase.replace(/\/+$/, "")}/${normalizedSuffix}`;
|
|
92
|
-
}
|
|
93
|
-
function shouldRewriteProxyResponse(contentType) {
|
|
94
|
-
const value = (contentType ?? "").toLowerCase();
|
|
95
|
-
return value.includes("text/html") || value.includes("text/css");
|
|
96
|
-
}
|
|
97
|
-
function capabilityProxyCleanupToken(parts) {
|
|
98
|
-
const hash = createHash("sha256");
|
|
99
|
-
for (const part of parts) {
|
|
100
|
-
hash.update(part ?? "");
|
|
101
|
-
hash.update("\0");
|
|
102
|
-
}
|
|
103
|
-
return hash.digest("hex").slice(0, 32);
|
|
104
|
-
}
|
|
105
|
-
function hasCookieValue(cookieHeader, name, expectedValue) {
|
|
106
|
-
return cookieHeader
|
|
107
|
-
.split(";")
|
|
108
|
-
.map((part) => part.trim())
|
|
109
|
-
.some((part) => {
|
|
110
|
-
const eq = part.indexOf("=");
|
|
111
|
-
if (eq < 0)
|
|
112
|
-
return false;
|
|
113
|
-
return part.slice(0, eq) === name && part.slice(eq + 1) === expectedValue;
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
export async function resolveHttpCapability(instanceId, capabilityName) {
|
|
117
|
-
const capability = appService
|
|
118
|
-
.getProvidedCapabilitiesForApp(instanceId)
|
|
119
|
-
.find((entry) => entry.capability === capabilityName);
|
|
120
|
-
if (!capability)
|
|
121
|
-
throw new Error(`Capability '${capabilityName}' not found`);
|
|
122
|
-
if (capability.visibility === "internal") {
|
|
123
|
-
throw new Error(`Capability '${capabilityName}' is not externally accessible`);
|
|
124
|
-
}
|
|
125
|
-
if (capability.protocol !== "http" && capability.protocol !== "https") {
|
|
126
|
-
throw new Error(`Capability '${capabilityName}' does not use HTTP(S)`);
|
|
127
|
-
}
|
|
128
|
-
const runtimePort = appService.resolveRuntimeCapabilityPort(instanceId, capabilityName);
|
|
129
|
-
if (typeof runtimePort !== "number" || runtimePort < 1) {
|
|
130
|
-
throw new Error(`Capability '${capabilityName}' has no resolved port`);
|
|
131
|
-
}
|
|
132
|
-
const upstreamHost = await appService.getHostForAppPort(instanceId, runtimePort);
|
|
133
|
-
const proxyBase = capabilityProxyPath(instanceId, capabilityName);
|
|
134
|
-
const canonicalProxyBase = canonicalCapabilityProxyBase(proxyBase, capability.path);
|
|
135
|
-
return {
|
|
136
|
-
capability,
|
|
137
|
-
targetBaseUrl: `${capability.protocol}://${appService.urlHost(upstreamHost)}:${runtimePort}`,
|
|
138
|
-
proxyUrl: canonicalProxyBase,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Inject a tiny <script> that monkey-patches `fetch`, `XMLHttpRequest.open`,
|
|
143
|
-
* and `Element.prototype.{setAttribute,src,href}` so that same-origin absolute
|
|
144
|
-
* paths (e.g. `/api/v1/...`, `/static/...`) are transparently rewritten to go
|
|
145
|
-
* through the capability proxy prefix.
|
|
146
|
-
*
|
|
147
|
-
* The alternative — rewriting every JS bundle byte-for-byte — is fragile and
|
|
148
|
-
* expensive; a runtime shim at document load is the standard approach used by
|
|
149
|
-
* reverse-proxy front-ends (cf. Cloudflare Access, oauth2-proxy, etc.).
|
|
150
|
-
*/
|
|
151
|
-
function capabilityProxyBootstrap(proxyBasePath) {
|
|
152
|
-
const prefix = JSON.stringify(proxyBasePath.replace(/\/+$/, ""));
|
|
153
|
-
const workerPrelude = JSON.stringify([
|
|
154
|
-
"(function(){",
|
|
155
|
-
`var P=${prefix};`,
|
|
156
|
-
"function px(path){",
|
|
157
|
-
"if(!path||path.charAt(0)!=='/'||path.charAt(1)==='/'||path.indexOf(P)===0||path.indexOf('/api/instances/')===0)return path;",
|
|
158
|
-
"return P+path;",
|
|
159
|
-
"}",
|
|
160
|
-
"function str(u){return typeof u==='string'?u:(u&&typeof u.href==='string'?u.href:null);}",
|
|
161
|
-
"function rwWs(u){",
|
|
162
|
-
"var s0=str(u);if(!s0)return u;",
|
|
163
|
-
"var H=typeof __capProxyHost==='string'?__capProxyHost:self.location.host;",
|
|
164
|
-
"var L=typeof __capProxyProtocol==='string'?__capProxyProtocol:self.location.protocol;",
|
|
165
|
-
"var B=(L==='https:'?'https:':'http:')+'//'+H+'/';",
|
|
166
|
-
"try{var p=new URL(s0,B);",
|
|
167
|
-
"if(p.host===H&&(p.protocol==='ws:'||p.protocol==='wss:'||p.protocol===L)){",
|
|
168
|
-
"var pp=px(p.pathname);",
|
|
169
|
-
"if(pp!==p.pathname){var s=L==='https:'?'wss:':'ws:';return s+'//'+H+pp+p.search+p.hash;}",
|
|
170
|
-
"}}catch(_e){}",
|
|
171
|
-
"return u;",
|
|
172
|
-
"}",
|
|
173
|
-
"var _WS=self.WebSocket;",
|
|
174
|
-
"if(typeof _WS==='function')self.WebSocket=function(url,protocols){url=rwWs(url);return protocols!==undefined?new _WS(url,protocols):new _WS(url);};",
|
|
175
|
-
"if(typeof _WS==='function'){self.WebSocket.prototype=_WS.prototype;self.WebSocket.CONNECTING=_WS.CONNECTING;self.WebSocket.OPEN=_WS.OPEN;self.WebSocket.CLOSING=_WS.CLOSING;self.WebSocket.CLOSED=_WS.CLOSED;}",
|
|
176
|
-
"})();",
|
|
177
|
-
].join(""));
|
|
178
|
-
return [
|
|
179
|
-
"<script>(function(){",
|
|
180
|
-
`var P=${prefix};`,
|
|
181
|
-
"var O=window.location.origin;",
|
|
182
|
-
// --- Service Worker neutralization ---
|
|
183
|
-
// Embedded SPAs like OpenWebUI register a service worker that aggressively
|
|
184
|
-
// caches HTML and JS chunks at the proxy origin. After we update the proxy
|
|
185
|
-
// bootstrap, the SW would keep serving the old (unpatched) HTML, so socket.io
|
|
186
|
-
// never gets the WebSocket monkey-patch and the splash screen spins forever.
|
|
187
|
-
// Unregister any existing SW, purge its caches, and block future
|
|
188
|
-
// registrations for the lifetime of the iframe.
|
|
189
|
-
"try{if(navigator.serviceWorker){",
|
|
190
|
-
"if(navigator.serviceWorker.getRegistrations){",
|
|
191
|
-
"navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(r){try{r.unregister()}catch(_e){}})}).catch(function(){});",
|
|
192
|
-
"}",
|
|
193
|
-
"navigator.serviceWorker.register=function(){return Promise.reject(new Error('service worker disabled in capability proxy'))};",
|
|
194
|
-
"}}catch(_e){}",
|
|
195
|
-
"try{if(window.caches&&caches.keys){caches.keys().then(function(ks){ks.forEach(function(k){try{caches.delete(k)}catch(_e){}})}).catch(function(){});}}catch(_e){}",
|
|
196
|
-
// Only rewrite paths that do NOT already start with the proxy prefix and
|
|
197
|
-
// that are simple absolute paths (start with `/` but not `//`).
|
|
198
|
-
"function px(path){",
|
|
199
|
-
"if(!path||path.charAt(0)!=='/'||path.charAt(1)==='/'||path.indexOf(P)===0||path.indexOf('/api/instances/')===0)return path;",
|
|
200
|
-
"return P+path;",
|
|
201
|
-
"}",
|
|
202
|
-
"function str(u){return typeof u==='string'?u:(u&&typeof u.href==='string'?u.href:null);}",
|
|
203
|
-
"function rw(u){",
|
|
204
|
-
"var s0=str(u);if(!s0)return u;",
|
|
205
|
-
"var direct=px(s0);",
|
|
206
|
-
"if(direct!==s0)return direct;",
|
|
207
|
-
"try{var p=new URL(s0,window.location.href);",
|
|
208
|
-
"if(p.origin===O){var pp=px(p.pathname);if(pp!==p.pathname)return pp+p.search+p.hash;}",
|
|
209
|
-
"}catch(_e){}",
|
|
210
|
-
"return u;",
|
|
211
|
-
"}",
|
|
212
|
-
"function rwWs(u){",
|
|
213
|
-
"var s0=str(u);if(!s0)return u;",
|
|
214
|
-
"try{var p=new URL(s0,window.location.href);",
|
|
215
|
-
"if(p.host===window.location.host&&(p.protocol==='ws:'||p.protocol==='wss:'||p.protocol===window.location.protocol)){",
|
|
216
|
-
"var pp=px(p.pathname);",
|
|
217
|
-
"if(pp!==p.pathname){var s=window.location.protocol==='https:'?'wss:':'ws:';return s+'//'+window.location.host+pp+p.search+p.hash;}",
|
|
218
|
-
"}}catch(_e){}",
|
|
219
|
-
"return u;",
|
|
220
|
-
"}",
|
|
221
|
-
// --- fetch() ---
|
|
222
|
-
"var _f=window.fetch;",
|
|
223
|
-
"window.fetch=function(r,o){",
|
|
224
|
-
"if(typeof r==='string'||(r&&typeof r.href==='string'))r=rw(r);",
|
|
225
|
-
"else if(r instanceof Request){",
|
|
226
|
-
"var nr=rw(r.url);if(nr!==r.url)r=new Request(nr,r);}",
|
|
227
|
-
"return _f.call(this,r,o);};",
|
|
228
|
-
// --- XMLHttpRequest.open() ---
|
|
229
|
-
"var _xo=XMLHttpRequest.prototype.open;",
|
|
230
|
-
"XMLHttpRequest.prototype.open=function(m,u){",
|
|
231
|
-
"arguments[1]=rw(u);",
|
|
232
|
-
"return _xo.apply(this,arguments);};",
|
|
233
|
-
// --- history.pushState / replaceState ---
|
|
234
|
-
// SPA routers (SvelteKit, React Router, etc.) navigate via pushState.
|
|
235
|
-
// Without this, pushing "/" lands on the panel's own SPA.
|
|
236
|
-
"var _ps=history.pushState;",
|
|
237
|
-
"var _rs=history.replaceState;",
|
|
238
|
-
"history.pushState=function(s,t,u){",
|
|
239
|
-
"if(typeof u==='string')u=rw(u);",
|
|
240
|
-
"return _ps.call(this,s,t,u);};",
|
|
241
|
-
"history.replaceState=function(s,t,u){",
|
|
242
|
-
"if(typeof u==='string')u=rw(u);",
|
|
243
|
-
"return _rs.call(this,s,t,u);};",
|
|
244
|
-
// --- location.assign / location.replace ---
|
|
245
|
-
"var _la=location.assign.bind(location);",
|
|
246
|
-
"var _lr=location.replace.bind(location);",
|
|
247
|
-
"location.assign=function(u){return _la(rw(u));};",
|
|
248
|
-
"location.replace=function(u){return _lr(rw(u));};",
|
|
249
|
-
// --- frame-busting defense ---
|
|
250
|
-
// Embedded SPAs (e.g. WeKnora) frequently do
|
|
251
|
-
// window.top.location.href = '/login'
|
|
252
|
-
// when they see a 401, intending to log the user out. Inside our
|
|
253
|
-
// capability proxy iframe `top` is the panel's main window — that
|
|
254
|
-
// tears the user away from the instance detail page entirely.
|
|
255
|
-
// Redirect `top`/`parent` to the iframe's own window so the
|
|
256
|
-
// navigation stays inside the embed. Safe because the iframe IS
|
|
257
|
-
// same-origin as the panel (our reverse proxy serves it from the
|
|
258
|
-
// panel's host); cross-origin access would throw and fail closed.
|
|
259
|
-
"try{",
|
|
260
|
-
"Object.defineProperty(window,'top',{configurable:true,get:function(){return window;}});",
|
|
261
|
-
"Object.defineProperty(window,'parent',{configurable:true,get:function(){return window;}});",
|
|
262
|
-
"}catch(_e){}",
|
|
263
|
-
// --- dynamic property assignment: img.src = '/static/...' ---
|
|
264
|
-
"function patchProp(tag,prop){",
|
|
265
|
-
"var d=Object.getOwnPropertyDescriptor(tag.prototype,prop);",
|
|
266
|
-
"if(!d||!d.set)return;",
|
|
267
|
-
"var orig=d.set;",
|
|
268
|
-
"Object.defineProperty(tag.prototype,prop,{",
|
|
269
|
-
"set:function(v){return orig.call(this,rw(v));},",
|
|
270
|
-
"get:d.get,configurable:true,enumerable:true});",
|
|
271
|
-
"}",
|
|
272
|
-
"patchProp(HTMLImageElement,'src');",
|
|
273
|
-
"patchProp(HTMLScriptElement,'src');",
|
|
274
|
-
"patchProp(HTMLLinkElement,'href');",
|
|
275
|
-
"patchProp(HTMLSourceElement,'src');",
|
|
276
|
-
// --- Worker monkey-patch ---
|
|
277
|
-
// Some SPA clients create socket.io/WebSocket connections from workers.
|
|
278
|
-
// Patch same-origin workers so the same proxy-prefixing rule applies there.
|
|
279
|
-
"var _Worker=window.Worker;",
|
|
280
|
-
`var _workerPrelude=${workerPrelude};`,
|
|
281
|
-
"function shouldWrapWorker(url){",
|
|
282
|
-
"try{var p=new URL(String(url),window.location.href);return p.protocol==='blob:'||p.protocol==='data:'||p.origin===O;}catch(_e){return false;}",
|
|
283
|
-
"}",
|
|
284
|
-
"function wrapWorker(url,options){",
|
|
285
|
-
"if(typeof _Worker!=='function'||!shouldWrapWorker(url))return url;",
|
|
286
|
-
"try{var resolved=new URL(String(url),window.location.href).href;",
|
|
287
|
-
"var isModule=!!(options&&options.type==='module');",
|
|
288
|
-
"var workerEnv='var __capProxyHost='+JSON.stringify(window.location.host)+';var __capProxyProtocol='+JSON.stringify(window.location.protocol)+';';",
|
|
289
|
-
"var source=isModule?workerEnv+_workerPrelude+'\\nimport '+JSON.stringify(resolved)+';':workerEnv+_workerPrelude+'\\nimportScripts('+JSON.stringify(resolved)+');';",
|
|
290
|
-
"return URL.createObjectURL(new Blob([source],{type:'text/javascript'}));",
|
|
291
|
-
"}catch(_e){return url;}",
|
|
292
|
-
"}",
|
|
293
|
-
"if(typeof _Worker==='function'){",
|
|
294
|
-
"window.Worker=function(url,options){var wrapped=wrapWorker(url,options);return options!==undefined?new _Worker(wrapped,options):new _Worker(wrapped);};",
|
|
295
|
-
"window.Worker.prototype=_Worker.prototype;",
|
|
296
|
-
"}",
|
|
297
|
-
// --- WebSocket ---
|
|
298
|
-
// SPAs like OpenWebUI use socket.io over WebSocket. The socket.io client
|
|
299
|
-
// builds ws:// URLs from window.location and the configured path; the URL
|
|
300
|
-
// ends up pointing at the panel root (e.g. ws://panel:8090/ws/socket.io)
|
|
301
|
-
// instead of the capability proxy. Without this patch the WS upgrade
|
|
302
|
-
// request either gets destroyed (no route) or hits the wrong backend.
|
|
303
|
-
"var _WS=window.WebSocket;",
|
|
304
|
-
"if(typeof _WS==='function'){",
|
|
305
|
-
"window.WebSocket=function(url,protocols){",
|
|
306
|
-
"url=rwWs(url);",
|
|
307
|
-
"return protocols!==undefined?new _WS(url,protocols):new _WS(url);",
|
|
308
|
-
"};",
|
|
309
|
-
"window.WebSocket.prototype=_WS.prototype;",
|
|
310
|
-
"window.WebSocket.CONNECTING=_WS.CONNECTING;",
|
|
311
|
-
"window.WebSocket.OPEN=_WS.OPEN;",
|
|
312
|
-
"window.WebSocket.CLOSING=_WS.CLOSING;",
|
|
313
|
-
"window.WebSocket.CLOSED=_WS.CLOSED;",
|
|
314
|
-
"}",
|
|
315
|
-
// --- EventSource ---
|
|
316
|
-
// Some frameworks use SSE (Server-Sent Events) for real-time updates.
|
|
317
|
-
"var _ES=window.EventSource;",
|
|
318
|
-
"if(typeof _ES==='function'){",
|
|
319
|
-
"window.EventSource=function(url,opts){",
|
|
320
|
-
"if(typeof url==='string')url=rw(url);",
|
|
321
|
-
"return new _ES(url,opts);",
|
|
322
|
-
"};",
|
|
323
|
-
"window.EventSource.prototype=_ES.prototype;",
|
|
324
|
-
"window.EventSource.CONNECTING=_ES.CONNECTING;",
|
|
325
|
-
"window.EventSource.OPEN=_ES.OPEN;",
|
|
326
|
-
"window.EventSource.CLOSED=_ES.CLOSED;",
|
|
327
|
-
"}",
|
|
328
|
-
"})();</script>",
|
|
329
|
-
].join("");
|
|
330
|
-
}
|
|
331
|
-
function rewriteProxyTextBody(body, contentType, proxyBasePath, extraHeadHtml = "") {
|
|
332
|
-
const value = (contentType ?? "").toLowerCase();
|
|
333
|
-
const proxyBaseWithSlash = `${proxyBasePath.replace(/\/+$/, "")}/`;
|
|
334
|
-
let rewritten = body;
|
|
335
|
-
if (value.includes("text/html")) {
|
|
336
|
-
// Rewrite asset URLs FIRST, then optionally inject a <base> tag.
|
|
337
|
-
// Reversing the order would let the regex below match (and double-
|
|
338
|
-
// prefix) the leading slash of the just-inserted `<base href="/api/...">`,
|
|
339
|
-
// producing
|
|
340
|
-
// <base href="/api/instances/X/provides/Y/api/instances/X/provides/Y/">
|
|
341
|
-
// which then resolves every relative asset to a 404.
|
|
342
|
-
rewritten = rewritten.replace(/((?:href|src|action|poster)=['"])\/(?!\/)/gi, `$1${proxyBaseWithSlash}`);
|
|
343
|
-
// Rewrite dynamic import() paths inside inline <script> blocks so that
|
|
344
|
-
// SvelteKit (and similar frameworks) resolve JS modules through the proxy.
|
|
345
|
-
// Matches import("/_app/...") and import('/_app/...').
|
|
346
|
-
rewritten = rewritten.replace(/\bimport\(\s*(['"])\/(?!\/)/g, `import($1${proxyBaseWithSlash}`);
|
|
347
|
-
// Rewrite SvelteKit's client-side base path so that client-side routing
|
|
348
|
-
// and subsequent chunk fetches go through the capability proxy path.
|
|
349
|
-
// Older SvelteKit SSR output: __sveltekit_XXXXX = { base: "" };
|
|
350
|
-
rewritten = rewritten.replace(/(__sveltekit_\w+\s*=\s*\{\s*base\s*:\s*)(["'])["']/, `$1$2${proxyBasePath.replace(/\/+$/, "")}$2`);
|
|
351
|
-
// SvelteKit 2.x start() config: paths: { base: "", assets: "..." }
|
|
352
|
-
rewritten = rewritten.replace(/(paths\s*:\s*\{\s*base\s*:\s*)(["'])["'](\s*,\s*assets\s*:)/, `$1$2${proxyBasePath.replace(/\/+$/, "")}$2$3`);
|
|
353
|
-
if (!/<base\b/i.test(rewritten)) {
|
|
354
|
-
if (/<head[^>]*>/i.test(rewritten)) {
|
|
355
|
-
rewritten = rewritten.replace(/<head([^>]*)>/i, `<head$1><base href="${proxyBaseWithSlash}">`);
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
rewritten = `<base href="${proxyBaseWithSlash}">${rewritten}`;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (extraHeadHtml) {
|
|
362
|
-
if (/<base\b/i.test(rewritten)) {
|
|
363
|
-
rewritten = rewritten.replace(/<base\b[^>]*>/i, (match) => `${match}${extraHeadHtml}`);
|
|
364
|
-
}
|
|
365
|
-
else if (/<head[^>]*>/i.test(rewritten)) {
|
|
366
|
-
rewritten = rewritten.replace(/<head([^>]*)>/i, `<head$1>${extraHeadHtml}`);
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
rewritten = `${extraHeadHtml}${rewritten}`;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
if (value.includes("text/css")) {
|
|
374
|
-
rewritten = rewritten.replace(/url\((['"]?)\/(?!\/)/gi, `url($1${proxyBaseWithSlash}`);
|
|
375
|
-
}
|
|
376
|
-
else if (value.includes("text/html")) {
|
|
377
|
-
// HTML can contain inline scripts like new URL("/..."); only rewrite lowercase CSS url(...).
|
|
378
|
-
rewritten = rewritten.replace(/url\((['"]?)\/(?!\/)/g, `url($1${proxyBaseWithSlash}`);
|
|
379
|
-
}
|
|
380
|
-
return rewritten;
|
|
381
|
-
}
|
|
382
|
-
function isReadableBody(value) {
|
|
383
|
-
if (value instanceof Readable)
|
|
384
|
-
return true;
|
|
385
|
-
if (!value || typeof value !== "object")
|
|
386
|
-
return false;
|
|
387
|
-
const candidate = value;
|
|
388
|
-
return (typeof candidate.pipe === "function" &&
|
|
389
|
-
typeof candidate.on === "function" &&
|
|
390
|
-
typeof candidate[Symbol.asyncIterator] === "function");
|
|
391
|
-
}
|
|
392
|
-
async function readProxyBodyStream(stream) {
|
|
393
|
-
const chunks = [];
|
|
394
|
-
for await (const chunk of stream) {
|
|
395
|
-
if (typeof chunk === "string") {
|
|
396
|
-
chunks.push(Buffer.from(chunk));
|
|
397
|
-
}
|
|
398
|
-
else if (chunk instanceof Uint8Array) {
|
|
399
|
-
chunks.push(Buffer.from(chunk));
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
chunks.push(Buffer.from(String(chunk)));
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
return Buffer.concat(chunks);
|
|
406
|
-
}
|
|
407
|
-
function bytesToArrayBuffer(bytes) {
|
|
408
|
-
const out = new ArrayBuffer(bytes.byteLength);
|
|
409
|
-
new Uint8Array(out).set(bytes);
|
|
410
|
-
return out;
|
|
411
|
-
}
|
|
412
|
-
function isPlainRecord(value) {
|
|
413
|
-
if (!value || typeof value !== "object")
|
|
414
|
-
return false;
|
|
415
|
-
const proto = Object.getPrototypeOf(value);
|
|
416
|
-
return proto === Object.prototype || proto === null;
|
|
417
|
-
}
|
|
418
|
-
function encodeFormRecord(body) {
|
|
419
|
-
const params = new URLSearchParams();
|
|
420
|
-
for (const [key, value] of Object.entries(body)) {
|
|
421
|
-
if (Array.isArray(value)) {
|
|
422
|
-
for (const item of value)
|
|
423
|
-
params.append(key, String(item));
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
params.append(key, String(value));
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return params.toString();
|
|
430
|
-
}
|
|
431
|
-
async function buildProxyRequestBody(req) {
|
|
432
|
-
if (req.method === "GET" || req.method === "HEAD")
|
|
433
|
-
return undefined;
|
|
434
|
-
const body = req.body;
|
|
435
|
-
if (body == null)
|
|
436
|
-
return undefined;
|
|
437
|
-
if (typeof body === "string") {
|
|
438
|
-
return body;
|
|
439
|
-
}
|
|
440
|
-
if (body instanceof Uint8Array || Buffer.isBuffer(body))
|
|
441
|
-
return bytesToArrayBuffer(body);
|
|
442
|
-
if (isReadableBody(body)) {
|
|
443
|
-
return bytesToArrayBuffer(await readProxyBodyStream(body));
|
|
444
|
-
}
|
|
445
|
-
const contentType = String(req.headers["content-type"] ?? "").toLowerCase();
|
|
446
|
-
if (contentType.includes("application/x-www-form-urlencoded") && isPlainRecord(body)) {
|
|
447
|
-
return encodeFormRecord(body);
|
|
448
|
-
}
|
|
449
|
-
if (contentType.includes("application/json") && isPlainRecord(body)) {
|
|
450
|
-
return JSON.stringify(body);
|
|
451
|
-
}
|
|
452
|
-
if (isPlainRecord(body)) {
|
|
453
|
-
return JSON.stringify(body);
|
|
454
|
-
}
|
|
455
|
-
return undefined;
|
|
456
|
-
}
|
|
457
|
-
function parseCommandLine(input) {
|
|
458
|
-
const trimmed = input.trim();
|
|
459
|
-
if (!trimmed)
|
|
460
|
-
return [];
|
|
461
|
-
const args = [];
|
|
462
|
-
let current = "";
|
|
463
|
-
let quote = null;
|
|
464
|
-
let escaping = false;
|
|
465
|
-
for (const char of trimmed) {
|
|
466
|
-
if (escaping) {
|
|
467
|
-
current += char;
|
|
468
|
-
escaping = false;
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
if (char === "\\") {
|
|
472
|
-
escaping = true;
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
if (quote) {
|
|
476
|
-
if (char === quote) {
|
|
477
|
-
quote = null;
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
480
|
-
current += char;
|
|
481
|
-
}
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
if (char === '"' || char === "'") {
|
|
485
|
-
quote = char;
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
if (/\s/.test(char)) {
|
|
489
|
-
if (current) {
|
|
490
|
-
args.push(current);
|
|
491
|
-
current = "";
|
|
492
|
-
}
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
current += char;
|
|
496
|
-
}
|
|
497
|
-
if (escaping)
|
|
498
|
-
current += "\\";
|
|
499
|
-
if (quote)
|
|
500
|
-
throw new Error("Command contains an unterminated quote");
|
|
501
|
-
if (current)
|
|
502
|
-
args.push(current);
|
|
503
|
-
return args;
|
|
504
|
-
}
|
|
505
|
-
function isTerminalProvide(provide) {
|
|
506
|
-
return !!provide && String(provide.protocol).toLowerCase() === "terminal" && !!provide.terminal;
|
|
507
|
-
}
|
|
508
|
-
function resolveTerminalProvide(instanceId, capability) {
|
|
509
|
-
const provide = appService
|
|
510
|
-
.getProvidedCapabilitiesForApp(instanceId)
|
|
511
|
-
.find((entry) => entry.capability === capability);
|
|
512
|
-
if (!provide)
|
|
513
|
-
throw new Error(`Capability '${capability}' not found`);
|
|
514
|
-
if (!isTerminalProvide(provide)) {
|
|
515
|
-
throw new Error(`Capability '${capability}' is not a terminal provide`);
|
|
516
|
-
}
|
|
517
|
-
return provide;
|
|
518
|
-
}
|
|
519
|
-
function buildTerminalCommand(baseCommand, input) {
|
|
520
|
-
if (!Array.isArray(baseCommand) || baseCommand.length === 0 || baseCommand.some((part) => typeof part !== "string" || !part.trim())) {
|
|
521
|
-
throw new Error("Terminal provide is missing a valid base command");
|
|
522
|
-
}
|
|
523
|
-
const parsed = parseCommandLine(input);
|
|
524
|
-
if (!parsed.length)
|
|
525
|
-
throw new Error("Command cannot be empty");
|
|
526
|
-
const baseName = baseCommand[0].split("/").pop() || baseCommand[0];
|
|
527
|
-
const matchesBase = parsed.length >= baseCommand.length && baseCommand.every((part, index) => parsed[index] === part);
|
|
528
|
-
const matchesBaseName = parsed[0] === baseName;
|
|
529
|
-
if (matchesBase)
|
|
530
|
-
return parsed;
|
|
531
|
-
if (matchesBaseName)
|
|
532
|
-
return [baseCommand[0], ...parsed.slice(1)];
|
|
533
|
-
return [...baseCommand, ...parsed];
|
|
534
|
-
}
|
|
535
|
-
async function proxyProvidedCapability(req, reply) {
|
|
536
|
-
const idErr = validateId(req.params.id);
|
|
537
|
-
if (idErr)
|
|
538
|
-
return reply.status(400).send({ detail: idErr });
|
|
539
|
-
const rawInst = appService.getInstance(req.params.id);
|
|
540
|
-
if (!rawInst)
|
|
541
|
-
return reply.status(404).send({ detail: "Instance not found" });
|
|
542
|
-
if (!appService.getInstanceInstallRecord(req.params.id))
|
|
543
|
-
return reply.status(404).send({ detail: "App not found" });
|
|
544
|
-
const capabilities = appService.getProvidedCapabilitiesForApp(req.params.id);
|
|
545
|
-
const capability = capabilities.find((entry) => entry.capability === req.params.capability);
|
|
546
|
-
if (!capability) {
|
|
547
|
-
return reply.status(404).send({ detail: `Capability '${req.params.capability}' not found` });
|
|
548
|
-
}
|
|
549
|
-
if (capability.visibility === "internal") {
|
|
550
|
-
return reply.status(403).send({ detail: `Capability '${req.params.capability}' is not externally accessible` });
|
|
551
|
-
}
|
|
552
|
-
if (capability.protocol !== "http" && capability.protocol !== "https") {
|
|
553
|
-
return reply.status(400).send({ detail: `Capability '${req.params.capability}' does not use HTTP(S)` });
|
|
554
|
-
}
|
|
555
|
-
// Resolve only the canonical runtime hostPort. AppSpec declared ports are
|
|
556
|
-
// template metadata; using them here would route users to the wrong
|
|
557
|
-
// instance after reallocation or migration drift.
|
|
558
|
-
let runtimePort;
|
|
559
|
-
try {
|
|
560
|
-
runtimePort = appService.resolveRuntimeCapabilityPort(req.params.id, req.params.capability) ?? undefined;
|
|
561
|
-
}
|
|
562
|
-
catch (error) {
|
|
563
|
-
if (appService.isCanonicalRuntimePortRequiredError(error)) {
|
|
564
|
-
return reply.status(error.statusCode || 409).send({ detail: error.message, code: error.code });
|
|
565
|
-
}
|
|
566
|
-
throw error;
|
|
567
|
-
}
|
|
568
|
-
if (typeof runtimePort !== "number" || runtimePort < 1) {
|
|
569
|
-
return reply.status(500).send({ detail: `Capability '${req.params.capability}' has no resolved port` });
|
|
570
|
-
}
|
|
571
|
-
const upstreamHost = await appService.getHostForAppPort(req.params.id, runtimePort);
|
|
572
|
-
const upstreamOrigin = `${capability.protocol}://${appService.urlHost(upstreamHost)}:${runtimePort}`;
|
|
573
|
-
const wildcardSuffix = typeof req.params["*"] === "string" ? req.params["*"] : "";
|
|
574
|
-
const proxyBasePath = capabilityProxyPath(req.params.id, req.params.capability);
|
|
575
|
-
const querySuffix = req.raw.url?.includes("?") ? req.raw.url.slice(req.raw.url.indexOf("?")) : "";
|
|
576
|
-
const requestPath = req.raw.url?.split("?")[0] ?? "";
|
|
577
|
-
const canonicalProxyBase = canonicalCapabilityProxyBase(proxyBasePath, capability.path);
|
|
578
|
-
if (!wildcardSuffix && canonicalProxyBase !== proxyBasePath && !requestPath.endsWith("/")) {
|
|
579
|
-
reply.code(308).header("location", `${canonicalProxyBase}${querySuffix}`);
|
|
580
|
-
return reply.send();
|
|
581
|
-
}
|
|
582
|
-
const upstreamPath = joinUpstreamPath(capability.path, wildcardSuffix);
|
|
583
|
-
const targetUrl = `${upstreamOrigin}${upstreamPath}${querySuffix}`;
|
|
584
|
-
const headers = new Headers();
|
|
585
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
586
|
-
if (value == null)
|
|
587
|
-
continue;
|
|
588
|
-
const normalizedKey = key.toLowerCase();
|
|
589
|
-
if (HOP_BY_HOP.has(normalizedKey) || normalizedKey === "host" || normalizedKey === "content-length" || normalizedKey === "accept-encoding") {
|
|
590
|
-
continue;
|
|
591
|
-
}
|
|
592
|
-
// Strip panel session credentials to avoid leaking them upstream
|
|
593
|
-
// (consistent with the WebSocket capability proxy in server.ts)
|
|
594
|
-
if (normalizedKey === "authorization")
|
|
595
|
-
continue;
|
|
596
|
-
if (normalizedKey === "cookie") {
|
|
597
|
-
const stripped = stripPanelSessionCookie(value);
|
|
598
|
-
if (stripped)
|
|
599
|
-
headers.set(key, stripped);
|
|
600
|
-
continue;
|
|
601
|
-
}
|
|
602
|
-
if (Array.isArray(value)) {
|
|
603
|
-
for (const item of value)
|
|
604
|
-
headers.append(key, item);
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
headers.set(key, String(value));
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
headers.set("accept-encoding", "identity");
|
|
611
|
-
if (headers.has("origin"))
|
|
612
|
-
headers.set("origin", upstreamOrigin);
|
|
613
|
-
// `x-forwarded-prefix` is not a standard reverse-proxy header and some
|
|
614
|
-
// upstream frameworks (notably SvelteKit apps like Hollama) treat it as a
|
|
615
|
-
// deployment base path, which breaks `/_app/*` asset resolution under this
|
|
616
|
-
// generic proxy. The HTML/base rewrite below already handles path prefixing.
|
|
617
|
-
if (req.headers.host)
|
|
618
|
-
headers.set("x-forwarded-host", String(req.headers.host));
|
|
619
|
-
headers.set("x-forwarded-proto", req.protocol);
|
|
620
|
-
const clientIp = typeof req.ip === "string" && req.ip
|
|
621
|
-
? req.ip
|
|
622
|
-
: typeof req.raw?.socket?.remoteAddress === "string"
|
|
623
|
-
? req.raw.socket.remoteAddress
|
|
624
|
-
: "";
|
|
625
|
-
if (clientIp) {
|
|
626
|
-
const forwardedFor = headers.get("x-forwarded-for");
|
|
627
|
-
headers.set("x-forwarded-for", forwardedFor ? `${forwardedFor}, ${clientIp}` : clientIp);
|
|
628
|
-
headers.set("x-real-ip", clientIp);
|
|
629
|
-
}
|
|
630
|
-
// Intercept service worker scripts BEFORE talking to upstream. SPAs like
|
|
631
|
-
// OpenWebUI register a SvelteKit service worker that aggressively caches
|
|
632
|
-
// HTML/JS at the proxy origin; once installed, the SW serves stale bodies
|
|
633
|
-
// and the page never receives our latest bootstrap (WebSocket / fetch
|
|
634
|
-
// monkey-patch). We replace the SW body with a self-unregistration stub so
|
|
635
|
-
// the next browser update cycle removes the offending worker and restores
|
|
636
|
-
// network-backed loading.
|
|
637
|
-
if (/(?:^|\/)(?:service-worker|sw)\.js$/i.test(requestPath)) {
|
|
638
|
-
reply
|
|
639
|
-
.code(200)
|
|
640
|
-
.header("content-type", "application/javascript; charset=utf-8")
|
|
641
|
-
.header("cache-control", "no-store, no-cache, must-revalidate, max-age=0")
|
|
642
|
-
.header("pragma", "no-cache")
|
|
643
|
-
.header("service-worker-allowed", "/");
|
|
644
|
-
return reply.send("// Capability proxy: service worker intentionally disabled.\n" +
|
|
645
|
-
"self.addEventListener('install',function(e){self.skipWaiting()});\n" +
|
|
646
|
-
"self.addEventListener('activate',function(e){\n" +
|
|
647
|
-
" e.waitUntil((async function(){\n" +
|
|
648
|
-
" try{var cs=await caches.keys();for(var i=0;i<cs.length;i++){try{await caches.delete(cs[i])}catch(_e){}}}catch(_e){}\n" +
|
|
649
|
-
" try{var clients=await self.clients.matchAll({includeUncontrolled:true});clients.forEach(function(c){try{c.navigate(c.url)}catch(_e){}})}catch(_e){}\n" +
|
|
650
|
-
" try{await self.registration.unregister()}catch(_e){}\n" +
|
|
651
|
-
" })());\n" +
|
|
652
|
-
"});\n");
|
|
653
|
-
}
|
|
654
|
-
// Single AbortController so we can cancel the upstream when the client
|
|
655
|
-
// disconnects. AbortSignal.timeout() only limits connection establishment;
|
|
656
|
-
// long-poll/SSE bodies (e.g. socket.io) would otherwise pin the fetch
|
|
657
|
-
// promise indefinitely and starve the event loop.
|
|
658
|
-
const upstreamAbort = new AbortController();
|
|
659
|
-
const connectTimer = setTimeout(() => upstreamAbort.abort(new Error("upstream connect timeout")), 30_000);
|
|
660
|
-
const onClientClose = () => {
|
|
661
|
-
if (!reply.raw.writableEnded)
|
|
662
|
-
upstreamAbort.abort(new Error("client disconnected"));
|
|
663
|
-
};
|
|
664
|
-
reply.raw.once("close", onClientClose);
|
|
665
|
-
try {
|
|
666
|
-
console.error("[cap-proxy] PRE buildBody method=", req.method, "bodyType=", typeof req.body, req.body?.constructor?.name, "signalAborted=", upstreamAbort.signal.aborted);
|
|
667
|
-
const requestBody = await buildProxyRequestBody(req);
|
|
668
|
-
console.error("[cap-proxy] POST buildBody size=", requestBody instanceof ArrayBuffer ? requestBody.byteLength : typeof requestBody === "string" ? requestBody.length : "undef", "signalAborted=", upstreamAbort.signal.aborted, "reason=", upstreamAbort.signal.reason?.message);
|
|
669
|
-
const upstream = await fetch(targetUrl, {
|
|
670
|
-
method: req.method,
|
|
671
|
-
headers,
|
|
672
|
-
body: requestBody,
|
|
673
|
-
redirect: "manual",
|
|
674
|
-
signal: upstreamAbort.signal,
|
|
675
|
-
}).finally(() => clearTimeout(connectTimer));
|
|
676
|
-
console.error("[cap-proxy] POST fetch status=", upstream.status);
|
|
677
|
-
const upstreamContentType = upstream.headers.get("content-type");
|
|
678
|
-
const willRewriteBody = shouldRewriteProxyResponse(upstreamContentType);
|
|
679
|
-
const willInjectHtml = (upstreamContentType ?? "").toLowerCase().includes("text/html");
|
|
680
|
-
reply.code(upstream.status);
|
|
681
|
-
upstream.headers.forEach((value, key) => {
|
|
682
|
-
const normalizedKey = key.toLowerCase();
|
|
683
|
-
if (HOP_BY_HOP.has(normalizedKey) || normalizedKey === "content-length" || normalizedKey === "content-encoding") {
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
// When we rewrite the response body (HTML/CSS/JS), the upstream ETag /
|
|
687
|
-
// Cache-Control values describe the *original* upstream bytes — but the
|
|
688
|
-
// body the browser receives is post-rewrite (proxy-prefixed paths, JS
|
|
689
|
-
// hard-coded redirect targets, etc.). Honoring the upstream cache hints
|
|
690
|
-
// lets the browser pin a stale rewrite indefinitely: e.g. an early
|
|
691
|
-
// visit that pre-dated the JS rewrite gets cached and survives across
|
|
692
|
-
// app restarts, breaking the auth redirect logic until a hard refresh.
|
|
693
|
-
// Strip cache validators and force revalidation on every load.
|
|
694
|
-
if (willRewriteBody && (normalizedKey === "cache-control" ||
|
|
695
|
-
normalizedKey === "etag" ||
|
|
696
|
-
normalizedKey === "last-modified" ||
|
|
697
|
-
normalizedKey === "expires" ||
|
|
698
|
-
normalizedKey === "pragma")) {
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
if (willInjectHtml && (normalizedKey === "content-security-policy" ||
|
|
702
|
-
normalizedKey === "content-security-policy-report-only" ||
|
|
703
|
-
normalizedKey === "x-frame-options")) {
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
if (normalizedKey === "location") {
|
|
707
|
-
if (value.startsWith("/")) {
|
|
708
|
-
reply.header(key, rewriteCapabilityLocation(proxyBasePath, canonicalProxyBase, value));
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
try {
|
|
712
|
-
const parsed = new URL(value);
|
|
713
|
-
const upstreamBase = new URL(upstreamOrigin);
|
|
714
|
-
if (parsed.origin === upstreamBase.origin) {
|
|
715
|
-
reply.header(key, rewriteCapabilityLocation(proxyBasePath, canonicalProxyBase, parsed.pathname, parsed.search, parsed.hash));
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
catch {
|
|
720
|
-
// fall through to raw location header
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
reply.header(key, value);
|
|
724
|
-
});
|
|
725
|
-
if (req.method === "HEAD") {
|
|
726
|
-
reply.raw.off("close", onClientClose);
|
|
727
|
-
return reply.send();
|
|
728
|
-
}
|
|
729
|
-
if (willRewriteBody) {
|
|
730
|
-
// Pair with the cache-validator strip above.
|
|
731
|
-
reply.header("cache-control", "no-cache, no-store, must-revalidate");
|
|
732
|
-
reply.header("pragma", "no-cache");
|
|
733
|
-
reply.header("expires", "0");
|
|
734
|
-
let extraHeadHtml = "";
|
|
735
|
-
const htmlRewriter = getCapabilityHtmlRewriter(capability);
|
|
736
|
-
extraHeadHtml += (await htmlRewriter?.buildHeadHtml({
|
|
737
|
-
instanceId: req.params.id,
|
|
738
|
-
capability,
|
|
739
|
-
upstreamOrigin,
|
|
740
|
-
signal: upstreamAbort.signal,
|
|
741
|
-
willInjectHtml,
|
|
742
|
-
})) ?? "";
|
|
743
|
-
// Inject a generic fetch/XHR monkey-patch for all capability-proxied
|
|
744
|
-
// HTML pages. SPA frameworks like SvelteKit compile absolute API paths
|
|
745
|
-
// (e.g. `/api/v1/...`, `/ollama/...`) into JS bundles at build time.
|
|
746
|
-
// When the page is served under the proxy path those requests bypass
|
|
747
|
-
// the proxy and hit the panel's own `/api/` routes instead. The patch
|
|
748
|
-
// intercepts fetch() and XMLHttpRequest.open() and rewrites same-origin
|
|
749
|
-
// absolute paths that do NOT already start with the proxy prefix.
|
|
750
|
-
extraHeadHtml += capabilityProxyBootstrap(proxyBasePath);
|
|
751
|
-
const rawBody = await upstream.text();
|
|
752
|
-
// First-visit/content-change cleanup: if the browser still has a stale
|
|
753
|
-
// ServiceWorker registered from an earlier panel build (which would
|
|
754
|
-
// intercept this navigation and serve cached HTML *without* the
|
|
755
|
-
// bootstrap patches), emit Clear-Site-Data so the browser drops the SW
|
|
756
|
-
// + its cache and reloads through the proxy. We mark the success with a
|
|
757
|
-
// long-lived cookie scoped to the proxy path to avoid a reload loop.
|
|
758
|
-
// The cookie value is tied to the upstream HTML bytes and validators,
|
|
759
|
-
// not a fixed boolean, so same-tag image rebuilds such as jishu-kb:0.1.1
|
|
760
|
-
// can still trigger one fresh cleanup after the embedded UI changes.
|
|
761
|
-
// Gate to HTML only — JS/CSS sub-resources also flow through this branch
|
|
762
|
-
// now that we rewrite JS bundles, and emitting Clear-Site-Data on a JS
|
|
763
|
-
// response would clear storage mid-page-load.
|
|
764
|
-
if (willInjectHtml) {
|
|
765
|
-
const cleanupToken = capabilityProxyCleanupToken([
|
|
766
|
-
req.params.id,
|
|
767
|
-
req.params.capability,
|
|
768
|
-
upstreamOrigin,
|
|
769
|
-
upstreamContentType,
|
|
770
|
-
upstream.headers.get("etag"),
|
|
771
|
-
upstream.headers.get("last-modified"),
|
|
772
|
-
upstream.headers.get("content-length"),
|
|
773
|
-
rawBody,
|
|
774
|
-
]);
|
|
775
|
-
const cookieHeader = (req.headers.cookie || "").toString();
|
|
776
|
-
const swCleaned = hasCookieValue(cookieHeader, "cap_proxy_sw_clean", cleanupToken);
|
|
777
|
-
if (!swCleaned) {
|
|
778
|
-
reply.header("Clear-Site-Data", '"cache", "storage"');
|
|
779
|
-
reply.header("Set-Cookie", `cap_proxy_sw_clean=${cleanupToken}; Path=${proxyBasePath}; Max-Age=2592000; SameSite=Lax`);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
reply.raw.off("close", onClientClose);
|
|
783
|
-
const rewritten = rewriteProxyTextBody(rawBody, upstreamContentType, proxyBasePath, extraHeadHtml);
|
|
784
|
-
return reply.send(rewritten);
|
|
785
|
-
}
|
|
786
|
-
if (!upstream.body) {
|
|
787
|
-
reply.raw.off("close", onClientClose);
|
|
788
|
-
return reply.send();
|
|
789
|
-
}
|
|
790
|
-
const readable = Readable.fromWeb(upstream.body);
|
|
791
|
-
readable.once("close", () => reply.raw.off("close", onClientClose));
|
|
792
|
-
return reply.send(readable);
|
|
793
|
-
}
|
|
794
|
-
catch (error) {
|
|
795
|
-
console.error("[cap-proxy] CATCH", error?.constructor?.name, error?.name, error?.message, "signalAborted=", upstreamAbort.signal.aborted, "reason=", upstreamAbort.signal.reason?.message, "stack=", error?.stack?.split("\n").slice(0, 4).join(" | "));
|
|
796
|
-
reply.raw.off("close", onClientClose);
|
|
797
|
-
return reply.status(502).send({ detail: error?.message || `Failed to proxy capability '${req.params.capability}'` });
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
// Resolve service manager once at route registration, re-resolve on config change
|
|
801
|
-
let _svc = null;
|
|
802
|
-
let _svcType = "";
|
|
803
|
-
export async function getSvc() {
|
|
804
|
-
const currentType = getServiceManagerType();
|
|
805
|
-
if (_svc && _svcType === currentType)
|
|
806
|
-
return _svc;
|
|
807
|
-
if (currentType !== "nomad") {
|
|
808
|
-
// Step 13: process-manager / host-mode legacy runtime is unsupported.
|
|
809
|
-
throw new Error(`service_manager='${currentType}' is no longer supported. ` +
|
|
810
|
-
"Only 'nomad' runtimes are accepted on the runtime path. " +
|
|
811
|
-
"Run `jishushell migrate legacy` to convert legacy instances.");
|
|
812
|
-
}
|
|
813
|
-
_svc = await import("../services/runtime/drivers/nomad.js");
|
|
814
|
-
_svcType = currentType;
|
|
815
|
-
return _svc;
|
|
816
|
-
}
|
|
13
|
+
import { proxyProvidedCapability } from "../services/capability-proxy/http.js";
|
|
14
|
+
import { sendTerminalCapabilityInput, startTerminalCapabilitySession, stopTerminalCapabilitySession, streamTerminalCapabilitySession, } from "../services/capability-proxy/terminal.js";
|
|
15
|
+
import { inferRequestOrigin } from "../services/http/request-utils.js";
|
|
16
|
+
import { getCachedStatus } from "../services/instances/status.js";
|
|
17
|
+
import { approvePairingRequest, isInstancePairingError, listPairingRequests, } from "../services/instances/pairing.js";
|
|
18
|
+
// Generic instance API only. Product-specific HTTP behavior belongs in
|
|
19
|
+
// integrations/<kind> or app-modules/<app>, reached through explicit hooks.
|
|
817
20
|
function getLifecycleActionSource(req) {
|
|
818
21
|
const raw = req.headers["x-jishushell-action-source"];
|
|
819
22
|
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
820
23
|
const source = typeof value === "string" ? value.trim() : "";
|
|
821
24
|
return /^[a-z0-9._:-]{1,80}$/i.test(source) ? source : "unknown";
|
|
822
25
|
}
|
|
823
|
-
export async function getCachedStatus(_svc, instanceId, reason) {
|
|
824
|
-
const snapshot = await refreshInstanceStatus(instanceId, {
|
|
825
|
-
reason,
|
|
826
|
-
force: true,
|
|
827
|
-
minIntervalMs: 0,
|
|
828
|
-
});
|
|
829
|
-
return snapshot?.service ?? { status: "unknown", pid: null, uptime: null, memory_mb: null, cpu_percent: null };
|
|
830
|
-
}
|
|
831
26
|
export async function instanceRoutes(app) {
|
|
832
27
|
// List
|
|
833
28
|
app.get("/api/instances", async () => {
|
|
834
|
-
const svc = await getSvc();
|
|
835
29
|
const instances = appService.listInstances();
|
|
836
|
-
const statuses = await Promise.all(instances.map(inst => getCachedStatus(
|
|
30
|
+
const statuses = await Promise.all(instances.map(inst => getCachedStatus(inst.id, "api-list").catch(() => ({ status: "unknown", pid: null, uptime: null, memory_mb: null, cpu_percent: null }))));
|
|
837
31
|
return Promise.all(instances.map(async (inst, i) => ({
|
|
838
32
|
...(await augmentInstanceMetadata(inst.id, inst)),
|
|
839
33
|
service: statuses[i],
|
|
@@ -845,7 +39,7 @@ export async function instanceRoutes(app) {
|
|
|
845
39
|
return await createInstance(req.body);
|
|
846
40
|
}
|
|
847
41
|
catch (e) {
|
|
848
|
-
// Structured rejection from
|
|
42
|
+
// Structured rejection from integration create policy — return 409 with code
|
|
849
43
|
if (e && e.name === "InstanceCreationRejected") {
|
|
850
44
|
return reply.status(409).send({
|
|
851
45
|
detail: e.hint,
|
|
@@ -901,11 +95,10 @@ export async function instanceRoutes(app) {
|
|
|
901
95
|
const idErr = validateId(req.params.id);
|
|
902
96
|
if (idErr)
|
|
903
97
|
return reply.status(400).send({ detail: idErr });
|
|
904
|
-
const svc = await getSvc();
|
|
905
98
|
const inst = appService.getInstance(req.params.id);
|
|
906
99
|
if (!inst)
|
|
907
100
|
return reply.status(404).send({ detail: "Instance not found" });
|
|
908
|
-
const status = await getCachedStatus(
|
|
101
|
+
const status = await getCachedStatus(req.params.id, "api-detail");
|
|
909
102
|
return {
|
|
910
103
|
...(await augmentInstanceMetadata(req.params.id, inst)),
|
|
911
104
|
service: status,
|
|
@@ -1135,36 +328,19 @@ export async function instanceRoutes(app) {
|
|
|
1135
328
|
const idErr = validateId(req.params.id);
|
|
1136
329
|
if (idErr)
|
|
1137
330
|
return reply.status(400).send({ detail: idErr });
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
return reply.status(404).send({ detail: "Instance not found" });
|
|
1141
|
-
}
|
|
1142
|
-
const capabilities = await getInstanceCapabilities(req.params.id, inst);
|
|
1143
|
-
if (!capabilities.pairing.list) {
|
|
1144
|
-
return reply.status(501).send({ detail: "Pairing list is not supported for this runtime" });
|
|
331
|
+
try {
|
|
332
|
+
return await listPairingRequests(req.params.id);
|
|
1145
333
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (isInstancePairingError(error))
|
|
336
|
+
return reply.status(error.statusCode).send({ detail: error.message });
|
|
337
|
+
throw error;
|
|
1149
338
|
}
|
|
1150
|
-
const svc = await getSvc();
|
|
1151
|
-
// Pure integration dispatch — no hardcoded kind fallback.
|
|
1152
|
-
const cmd = await getIntegration(integrationKind).buildPairingListCommand(req.params.id);
|
|
1153
|
-
const result = await svc.exec(req.params.id, cmd, 15_000);
|
|
1154
|
-
return { output: result.stdout + result.stderr, exitCode: result.exitCode };
|
|
1155
339
|
});
|
|
1156
340
|
app.post("/api/instances/:id/pairing/approve", async (req, reply) => {
|
|
1157
341
|
const idErr = validateId(req.params.id);
|
|
1158
342
|
if (idErr)
|
|
1159
343
|
return reply.status(400).send({ detail: idErr });
|
|
1160
|
-
const inst = appService.getInstance(req.params.id);
|
|
1161
|
-
if (!inst) {
|
|
1162
|
-
return reply.status(404).send({ detail: "Instance not found" });
|
|
1163
|
-
}
|
|
1164
|
-
const capabilities = await getInstanceCapabilities(req.params.id, inst);
|
|
1165
|
-
if (!capabilities.pairing.approve) {
|
|
1166
|
-
return reply.status(501).send({ detail: "Pairing approve is not supported for this runtime" });
|
|
1167
|
-
}
|
|
1168
344
|
const { channel, code, notify } = req.body ?? {};
|
|
1169
345
|
if (!channel || !PAIRING_CHANNEL_RE.test(channel)) {
|
|
1170
346
|
return reply.status(400).send({ detail: "Invalid channel: must be lowercase alphanumeric/hyphen/underscore" });
|
|
@@ -1172,29 +348,23 @@ export async function instanceRoutes(app) {
|
|
|
1172
348
|
if (!code || !PAIRING_CODE_RE.test(code)) {
|
|
1173
349
|
return reply.status(400).send({ detail: "Invalid pairing code: must be 4-16 uppercase alphanumeric characters" });
|
|
1174
350
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
return reply.status(501).send({ detail: "Pairing approve is not supported for this runtime" });
|
|
351
|
+
try {
|
|
352
|
+
return await approvePairingRequest(req.params.id, { channel, code, notify });
|
|
1178
353
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
});
|
|
1184
|
-
const svc = await getSvc();
|
|
1185
|
-
const result = await svc.exec(req.params.id, cmd, 15_000);
|
|
1186
|
-
if (result.exitCode !== 0) {
|
|
1187
|
-
return reply.status(400).send({ detail: (result.stderr || result.stdout || "Approval failed").trim() });
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (isInstancePairingError(error))
|
|
356
|
+
return reply.status(error.statusCode).send({ detail: error.message });
|
|
357
|
+
throw error;
|
|
1188
358
|
}
|
|
1189
|
-
return { ok: true, output: (result.stdout + result.stderr).trim() };
|
|
1190
359
|
});
|
|
1191
|
-
//
|
|
1192
|
-
//
|
|
1193
|
-
//
|
|
360
|
+
// Inline Chat surface for runtimes that choose the JishuShell-rendered Chat
|
|
361
|
+
// tab instead of an iframe-backed app UI. The runtime declares how Core can
|
|
362
|
+
// reach its chat endpoint via `inlineChatDescriptor`; this route owns only
|
|
363
|
+
// the shared forwarding/SSE wrapper.
|
|
1194
364
|
//
|
|
1195
|
-
// Flow: panel JWT auth
|
|
1196
|
-
// read allocated host port from runtime.ports
|
|
1197
|
-
//
|
|
365
|
+
// Flow: panel JWT auth -> read the declared per-instance secret from
|
|
366
|
+
// integration-home/.env -> read allocated host port from runtime.ports ->
|
|
367
|
+
// POST forward to the declared runtime chat endpoint.
|
|
1198
368
|
//
|
|
1199
369
|
// The response is framed as Server-Sent Events with periodic `: ping`
|
|
1200
370
|
// heartbeats while we wait for the agent to finish. Long-running agent
|
|
@@ -1206,9 +376,8 @@ export async function instanceRoutes(app) {
|
|
|
1206
376
|
// Errors before headers are sent fall back to HTTP 5xx JSON; errors
|
|
1207
377
|
// after hijack go out as an `event: error` SSE payload.
|
|
1208
378
|
//
|
|
1209
|
-
// This is a thin server-side forwarder, NOT a new LLM proxy.
|
|
1210
|
-
//
|
|
1211
|
-
// upstream provider.
|
|
379
|
+
// This is a thin server-side forwarder, NOT a new LLM proxy. Managed model
|
|
380
|
+
// traffic still goes through the runtime's configured Core proxy binding.
|
|
1212
381
|
app.post("/api/instances/:id/agent/chat", async (req, reply) => {
|
|
1213
382
|
const idErr = validateId(req.params.id);
|
|
1214
383
|
if (idErr)
|
|
@@ -1228,10 +397,9 @@ export async function instanceRoutes(app) {
|
|
|
1228
397
|
detail: `Runtime "${integrationKind ?? "generic"}" does not support inline chat (chatPanel=${chatPanel})`,
|
|
1229
398
|
});
|
|
1230
399
|
}
|
|
1231
|
-
// Integration-owned dispatch:
|
|
1232
|
-
//
|
|
1233
|
-
//
|
|
1234
|
-
// optional path/header/timeout) so this forwarder can reach its agent.
|
|
400
|
+
// Integration-owned dispatch: any integration that declares chatPanel
|
|
401
|
+
// "inline" MUST also supply `inlineChatDescriptor` (secret env var plus
|
|
402
|
+
// optional path/header/timeout) so this forwarder can reach its runtime.
|
|
1235
403
|
if (!integrationKind) {
|
|
1236
404
|
return reply.status(500).send({
|
|
1237
405
|
detail: `Instance "${req.params.id}" has no integration identity but declared chatPanel=inline`,
|
|
@@ -1257,8 +425,8 @@ export async function instanceRoutes(app) {
|
|
|
1257
425
|
}
|
|
1258
426
|
throw error;
|
|
1259
427
|
}
|
|
1260
|
-
//
|
|
1261
|
-
// declares which env var holds it
|
|
428
|
+
// The runtime API key lives in the integration-managed secretEnv file.
|
|
429
|
+
// Integration declares which env var holds it.
|
|
1262
430
|
const secretEnv = inst?.paths?.secretEnv;
|
|
1263
431
|
if (!secretEnv) {
|
|
1264
432
|
return reply.status(500).send({ detail: "Instance has no secretEnv path" });
|
|
@@ -1273,7 +441,7 @@ export async function instanceRoutes(app) {
|
|
|
1273
441
|
const endpointPath = desc.endpointPath ?? "/v1/chat/completions";
|
|
1274
442
|
const authHeader = desc.authHeader ?? "Authorization";
|
|
1275
443
|
const authScheme = desc.authScheme ?? "Bearer ";
|
|
1276
|
-
// Upstream budget: the
|
|
444
|
+
// Upstream budget: the runtime call still gets a hard ceiling so
|
|
1277
445
|
// a wedged container can't hold the connection forever. Integration can
|
|
1278
446
|
// extend this via inlineChatDescriptor.timeoutMs; default is 30 min
|
|
1279
447
|
// which comfortably covers multi-step tool loops.
|
|
@@ -1346,101 +514,16 @@ export async function instanceRoutes(app) {
|
|
|
1346
514
|
}
|
|
1347
515
|
});
|
|
1348
516
|
app.post("/api/instances/:id/provides/:capability/terminal/session", async (req, reply) => {
|
|
1349
|
-
|
|
1350
|
-
if (idErr)
|
|
1351
|
-
return reply.status(400).send({ detail: idErr });
|
|
1352
|
-
if (!appService.getInstance(req.params.id)) {
|
|
1353
|
-
return reply.status(404).send({ detail: "Instance not found" });
|
|
1354
|
-
}
|
|
1355
|
-
try {
|
|
1356
|
-
const provide = resolveTerminalProvide(req.params.id, req.params.capability);
|
|
1357
|
-
const terminal = provide.terminal;
|
|
1358
|
-
const input = typeof req.body?.input === "string" ? req.body.input : "";
|
|
1359
|
-
const command = buildTerminalCommand(terminal.command, input);
|
|
1360
|
-
const session = startTerminalSession({
|
|
1361
|
-
instanceId: req.params.id,
|
|
1362
|
-
capability: req.params.capability,
|
|
1363
|
-
terminal,
|
|
1364
|
-
command,
|
|
1365
|
-
});
|
|
1366
|
-
return reply.send(session);
|
|
1367
|
-
}
|
|
1368
|
-
catch (error) {
|
|
1369
|
-
return reply.status(400).send({ detail: error?.message || "Failed to start terminal session" });
|
|
1370
|
-
}
|
|
517
|
+
return startTerminalCapabilitySession(req, reply);
|
|
1371
518
|
});
|
|
1372
519
|
app.get("/api/instances/:id/provides/:capability/terminal/session/:sessionId/stream", async (req, reply) => {
|
|
1373
|
-
|
|
1374
|
-
if (idErr)
|
|
1375
|
-
return reply.status(400).send({ detail: idErr });
|
|
1376
|
-
if (!assertTerminalSessionOwner(req.params.sessionId, req.params.id, req.params.capability)) {
|
|
1377
|
-
return reply.status(404).send({ detail: "Terminal session not found" });
|
|
1378
|
-
}
|
|
1379
|
-
const session = getTerminalSession(req.params.sessionId);
|
|
1380
|
-
if (!session)
|
|
1381
|
-
return reply.status(404).send({ detail: "Terminal session not found" });
|
|
1382
|
-
const since = Math.max(parseInt(req.query.since || "0", 10) || 0, 0);
|
|
1383
|
-
reply.hijack();
|
|
1384
|
-
const raw = reply.raw;
|
|
1385
|
-
raw.writeHead(200, {
|
|
1386
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
1387
|
-
"Cache-Control": "no-cache, no-transform",
|
|
1388
|
-
"Connection": "keep-alive",
|
|
1389
|
-
"X-Accel-Buffering": "no",
|
|
1390
|
-
});
|
|
1391
|
-
const writeEvent = (event, data) => {
|
|
1392
|
-
raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
1393
|
-
};
|
|
1394
|
-
for (const event of getTerminalSessionEvents(req.params.sessionId, since)) {
|
|
1395
|
-
writeEvent(event.type, event);
|
|
1396
|
-
}
|
|
1397
|
-
if (!session.running) {
|
|
1398
|
-
writeEvent("done", { sessionId: req.params.sessionId });
|
|
1399
|
-
raw.end();
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
const unsubscribe = subscribeTerminalSession(req.params.sessionId, (event) => {
|
|
1403
|
-
writeEvent(event.type, event);
|
|
1404
|
-
if (event.type === "exit" || event.type === "error") {
|
|
1405
|
-
writeEvent("done", { sessionId: req.params.sessionId });
|
|
1406
|
-
unsubscribe?.();
|
|
1407
|
-
raw.end();
|
|
1408
|
-
}
|
|
1409
|
-
});
|
|
1410
|
-
req.raw.on("close", () => {
|
|
1411
|
-
unsubscribe?.();
|
|
1412
|
-
});
|
|
1413
|
-
raw.write(": ping\n\n");
|
|
520
|
+
return streamTerminalCapabilitySession(req, reply);
|
|
1414
521
|
});
|
|
1415
522
|
app.post("/api/instances/:id/provides/:capability/terminal/session/:sessionId/input", async (req, reply) => {
|
|
1416
|
-
|
|
1417
|
-
if (idErr)
|
|
1418
|
-
return reply.status(400).send({ detail: idErr });
|
|
1419
|
-
if (!assertTerminalSessionOwner(req.params.sessionId, req.params.id, req.params.capability)) {
|
|
1420
|
-
return reply.status(404).send({ detail: "Terminal session not found" });
|
|
1421
|
-
}
|
|
1422
|
-
try {
|
|
1423
|
-
sendTerminalSessionInput(req.params.sessionId, typeof req.body?.input === "string" ? req.body.input : "");
|
|
1424
|
-
return reply.send({ ok: true });
|
|
1425
|
-
}
|
|
1426
|
-
catch (error) {
|
|
1427
|
-
return reply.status(400).send({ detail: error?.message || "Failed to send terminal input" });
|
|
1428
|
-
}
|
|
523
|
+
return sendTerminalCapabilityInput(req, reply);
|
|
1429
524
|
});
|
|
1430
525
|
app.post("/api/instances/:id/provides/:capability/terminal/session/:sessionId/stop", async (req, reply) => {
|
|
1431
|
-
|
|
1432
|
-
if (idErr)
|
|
1433
|
-
return reply.status(400).send({ detail: idErr });
|
|
1434
|
-
if (!assertTerminalSessionOwner(req.params.sessionId, req.params.id, req.params.capability)) {
|
|
1435
|
-
return reply.status(404).send({ detail: "Terminal session not found" });
|
|
1436
|
-
}
|
|
1437
|
-
try {
|
|
1438
|
-
stopTerminalSession(req.params.sessionId);
|
|
1439
|
-
return reply.send({ ok: true });
|
|
1440
|
-
}
|
|
1441
|
-
catch (error) {
|
|
1442
|
-
return reply.status(400).send({ detail: error?.message || "Failed to stop terminal session" });
|
|
1443
|
-
}
|
|
526
|
+
return stopTerminalCapabilitySession(req, reply);
|
|
1444
527
|
});
|
|
1445
528
|
app.all("/api/instances/:id/provides/:capability", async (req, reply) => proxyProvidedCapability(req, reply));
|
|
1446
529
|
app.all("/api/instances/:id/provides/:capability/*", async (req, reply) => proxyProvidedCapability(req, reply));
|
|
@@ -1461,128 +544,6 @@ export async function instanceRoutes(app) {
|
|
|
1461
544
|
}
|
|
1462
545
|
return { lines: logLines };
|
|
1463
546
|
});
|
|
1464
|
-
// Admin: re-encrypt all instance secrets with current AES key
|
|
1465
|
-
app.post("/api/admin/migrate-secrets", async (_req, _reply) => {
|
|
1466
|
-
const { getAesKey, getJwtSecret } = await import("../config.js");
|
|
1467
|
-
const { scryptSync, createDecipheriv, createCipheriv, randomBytes } = await import("crypto");
|
|
1468
|
-
const { readFileSync, existsSync: fsExistsSync } = await import("fs");
|
|
1469
|
-
const instances = appService.listInstances();
|
|
1470
|
-
const results = [];
|
|
1471
|
-
const currentKey = getAesKey();
|
|
1472
|
-
const legacyKey = scryptSync(getJwtSecret(), "jishushell-apikey-v1", 32);
|
|
1473
|
-
for (const inst of instances) {
|
|
1474
|
-
const envFiles = appService.getRuntimeEnvFiles(inst.id);
|
|
1475
|
-
const providerEnvFile = envFiles[0]?.replace(/model\.env$/, "provider.env");
|
|
1476
|
-
if (!providerEnvFile || !fsExistsSync(providerEnvFile)) {
|
|
1477
|
-
results.push({ id: inst.id, status: "skipped", error: "no provider.env" });
|
|
1478
|
-
continue;
|
|
1479
|
-
}
|
|
1480
|
-
const envContent = readFileSync(providerEnvFile, "utf-8");
|
|
1481
|
-
const match = envContent.match(/UPSTREAM_API_KEY=(.+)/);
|
|
1482
|
-
if (!match || !match[1]?.startsWith("enc:")) {
|
|
1483
|
-
results.push({ id: inst.id, status: "skipped", error: "no encrypted key" });
|
|
1484
|
-
continue;
|
|
1485
|
-
}
|
|
1486
|
-
const encrypted = match[1];
|
|
1487
|
-
const raw = Buffer.from(encrypted.slice(4), "base64");
|
|
1488
|
-
if (raw.length < 29) {
|
|
1489
|
-
results.push({ id: inst.id, status: "error", error: "encrypted data too short" });
|
|
1490
|
-
continue;
|
|
1491
|
-
}
|
|
1492
|
-
const iv = raw.subarray(0, 12);
|
|
1493
|
-
const tag = raw.subarray(12, 28);
|
|
1494
|
-
const ciphertext = raw.subarray(28);
|
|
1495
|
-
// Try decrypt with current key first
|
|
1496
|
-
let plaintext = null;
|
|
1497
|
-
let needsReEncrypt = false;
|
|
1498
|
-
try {
|
|
1499
|
-
const d = createDecipheriv("aes-256-gcm", currentKey, iv);
|
|
1500
|
-
d.setAuthTag(tag);
|
|
1501
|
-
plaintext = d.update(ciphertext, undefined, "utf-8") + d.final("utf-8");
|
|
1502
|
-
// Already encrypted with current key — no migration needed
|
|
1503
|
-
results.push({ id: inst.id, status: "ok" });
|
|
1504
|
-
continue;
|
|
1505
|
-
}
|
|
1506
|
-
catch {
|
|
1507
|
-
// Current key failed — try legacy
|
|
1508
|
-
needsReEncrypt = true;
|
|
1509
|
-
}
|
|
1510
|
-
if (needsReEncrypt) {
|
|
1511
|
-
try {
|
|
1512
|
-
const d = createDecipheriv("aes-256-gcm", legacyKey, iv);
|
|
1513
|
-
d.setAuthTag(tag);
|
|
1514
|
-
plaintext = d.update(ciphertext, undefined, "utf-8") + d.final("utf-8");
|
|
1515
|
-
}
|
|
1516
|
-
catch {
|
|
1517
|
-
results.push({ id: inst.id, status: "error", error: "decrypt failed with both keys" });
|
|
1518
|
-
continue;
|
|
1519
|
-
}
|
|
1520
|
-
// Re-encrypt with current key
|
|
1521
|
-
const newIv = randomBytes(12);
|
|
1522
|
-
const c = createCipheriv("aes-256-gcm", currentKey, newIv);
|
|
1523
|
-
const enc = Buffer.concat([c.update(plaintext, "utf-8"), c.final()]);
|
|
1524
|
-
const newTag = c.getAuthTag();
|
|
1525
|
-
const newEncrypted = "enc:" + Buffer.concat([newIv, newTag, enc]).toString("base64");
|
|
1526
|
-
// Write back
|
|
1527
|
-
const newContent = envContent.replace(/UPSTREAM_API_KEY=.+/, `UPSTREAM_API_KEY=${newEncrypted}`);
|
|
1528
|
-
writeSecretFile(providerEnvFile, newContent);
|
|
1529
|
-
results.push({ id: inst.id, status: "migrated" });
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
// Invalidate proxy config cache for migrated instances
|
|
1533
|
-
for (const r of results) {
|
|
1534
|
-
if (r.status === "migrated")
|
|
1535
|
-
invalidateConfigCache(r.id);
|
|
1536
|
-
}
|
|
1537
|
-
return { ok: true, results };
|
|
1538
|
-
});
|
|
1539
|
-
// ── Docker image check & pull ───────────────────────────────────────────
|
|
1540
|
-
// Generic Docker operations used by NewInstance UI to verify / pull runtime
|
|
1541
|
-
// images before creating an instance. Framework-level (not integration-scoped)
|
|
1542
|
-
// because every container runtime shares the same docker CLI here.
|
|
1543
|
-
app.get("/api/docker/image-check", async (req, reply) => {
|
|
1544
|
-
const image = req.query.image;
|
|
1545
|
-
if (!image || typeof image !== "string") {
|
|
1546
|
-
return reply.status(400).send({ detail: "Missing 'image' query parameter" });
|
|
1547
|
-
}
|
|
1548
|
-
// Validate image name to prevent command injection
|
|
1549
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9\-_.:/@]*$/.test(image) || image.length > 256) {
|
|
1550
|
-
return reply.status(400).send({ detail: "Invalid Docker image name" });
|
|
1551
|
-
}
|
|
1552
|
-
const { inspectDockerImage, toDockerInspectUserError } = await import("../utils/docker-inspect.js");
|
|
1553
|
-
const result = inspectDockerImage(image, { timeout: 10000 });
|
|
1554
|
-
if (result.ok) {
|
|
1555
|
-
return { exists: true, image };
|
|
1556
|
-
}
|
|
1557
|
-
if (result.reason === "not_found")
|
|
1558
|
-
return { exists: false, image };
|
|
1559
|
-
const inspectError = toDockerInspectUserError(image, result);
|
|
1560
|
-
return reply.status(inspectError.statusCode).send({
|
|
1561
|
-
detail: inspectError.message,
|
|
1562
|
-
code: inspectError.code,
|
|
1563
|
-
exists: false,
|
|
1564
|
-
image,
|
|
1565
|
-
});
|
|
1566
|
-
});
|
|
1567
|
-
app.post("/api/docker/image-pull", async (req, reply) => {
|
|
1568
|
-
const image = req.body?.image;
|
|
1569
|
-
if (!image || typeof image !== "string") {
|
|
1570
|
-
return reply.status(400).send({ detail: "Missing 'image' in request body" });
|
|
1571
|
-
}
|
|
1572
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9\-_.:/@]*$/.test(image) || image.length > 256) {
|
|
1573
|
-
return reply.status(400).send({ detail: "Invalid Docker image name" });
|
|
1574
|
-
}
|
|
1575
|
-
try {
|
|
1576
|
-
const { execFile } = await import("child_process");
|
|
1577
|
-
const { promisify } = await import("util");
|
|
1578
|
-
const execFileAsync = promisify(execFile);
|
|
1579
|
-
await execFileAsync("docker", ["pull", image], { timeout: 600_000 });
|
|
1580
|
-
return { ok: true, image };
|
|
1581
|
-
}
|
|
1582
|
-
catch (e) {
|
|
1583
|
-
return reply.status(500).send({ detail: `Failed to pull image: ${e.message}` });
|
|
1584
|
-
}
|
|
1585
|
-
});
|
|
1586
547
|
// ── Connections REST API ────────────────────────────────────────────────
|
|
1587
548
|
/** GET /api/instances/:id/connections — view spec.requires + bindings. */
|
|
1588
549
|
app.get("/api/instances/:id/connections", async (req, reply) => {
|
|
@@ -1632,106 +593,5 @@ export async function instanceRoutes(app) {
|
|
|
1632
593
|
return { state: "error", unboundRequired: [], unboundOptional: [], bindable: 0, error: e.message };
|
|
1633
594
|
}
|
|
1634
595
|
});
|
|
1635
|
-
// ── Suggestions API (PR 6) ─────────────────────────────────────────────
|
|
1636
|
-
app.get("/api/suggestions", async () => {
|
|
1637
|
-
const { computeSuggestions } = await import("../services/connections/suggestions.js");
|
|
1638
|
-
return { suggestions: computeSuggestions() };
|
|
1639
|
-
});
|
|
1640
|
-
app.post("/api/suggestions/:id/apply", async (req, reply) => {
|
|
1641
|
-
const { computeSuggestions } = await import("../services/connections/suggestions.js");
|
|
1642
|
-
const all = computeSuggestions();
|
|
1643
|
-
const target = all.find((s) => s.id === req.params.id);
|
|
1644
|
-
if (!target) {
|
|
1645
|
-
return reply.status(404).send({ detail: "Suggestion no longer applies" });
|
|
1646
|
-
}
|
|
1647
|
-
// Apply by issuing the equivalent PUT /connections — read current
|
|
1648
|
-
// bindings, splice in the new one for `slot`, persist via the
|
|
1649
|
-
// transactor.
|
|
1650
|
-
const meta = appService.getInstance(target.consumerInstanceId);
|
|
1651
|
-
if (!meta)
|
|
1652
|
-
return reply.status(404).send({ detail: "Consumer instance not found" });
|
|
1653
|
-
// Resolve consumer spec from canonical sources only. The suggestion
|
|
1654
|
-
// target is an instance id; AppSpec id is only the template identity.
|
|
1655
|
-
const appData = appService.getInstanceInstallRecord(target.consumerInstanceId);
|
|
1656
|
-
let consumerSpec = appData?.spec ?? null;
|
|
1657
|
-
if (!consumerSpec) {
|
|
1658
|
-
const { readCanonicalSpecForInstance } = await import("../services/app-common/instance-store.js");
|
|
1659
|
-
consumerSpec = readCanonicalSpecForInstance(target.consumerInstanceId);
|
|
1660
|
-
}
|
|
1661
|
-
if (!consumerSpec) {
|
|
1662
|
-
return reply.status(409).send({
|
|
1663
|
-
detail: "Instance has no canonical AppSpec; run `jishushell migrate legacy --yes` first",
|
|
1664
|
-
code: "INSTANCE_NEEDS_MIGRATION",
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
const newConnections = {
|
|
1668
|
-
...(meta.connections ?? {}),
|
|
1669
|
-
[target.slot]: {
|
|
1670
|
-
kind: "single",
|
|
1671
|
-
providerId: target.candidate.providerId,
|
|
1672
|
-
capability: target.candidate.capability,
|
|
1673
|
-
},
|
|
1674
|
-
};
|
|
1675
|
-
const { applyConnections } = await import("../services/connections/transactor.js");
|
|
1676
|
-
const safeJson = await import("../utils/safe-json.js");
|
|
1677
|
-
const fs = await import("fs");
|
|
1678
|
-
const path = await import("path");
|
|
1679
|
-
const instancePath = appService.instanceMetaPath(target.consumerInstanceId);
|
|
1680
|
-
const readInstanceJson = async () => (safeJson.safeReadJson(instancePath, `instance:${target.consumerInstanceId}`) ?? {});
|
|
1681
|
-
const saveInstanceJson = async (_id, mutator) => {
|
|
1682
|
-
const cur = (safeJson.safeReadJson(instancePath, `instance:${target.consumerInstanceId}`) ?? {});
|
|
1683
|
-
try {
|
|
1684
|
-
fs.mkdirSync(path.dirname(instancePath), { recursive: true });
|
|
1685
|
-
}
|
|
1686
|
-
catch {
|
|
1687
|
-
/* noop */
|
|
1688
|
-
}
|
|
1689
|
-
safeJson.safeWriteJson(instancePath, mutator(cur));
|
|
1690
|
-
};
|
|
1691
|
-
let integration = null;
|
|
1692
|
-
const hasIntegrationMetadata = Array.isArray(meta.integrations)
|
|
1693
|
-
&& meta.integrations.length > 0;
|
|
1694
|
-
if (hasIntegrationMetadata) {
|
|
1695
|
-
const integrationKind = resolvePrimaryIntegrationKind(meta);
|
|
1696
|
-
if (!integrationKind || !hasIntegration(integrationKind)) {
|
|
1697
|
-
return reply.status(409).send({
|
|
1698
|
-
detail: "Instance is missing canonical integration identity; run migration or repair first",
|
|
1699
|
-
code: "INSTANCE_NEEDS_MIGRATION",
|
|
1700
|
-
});
|
|
1701
|
-
}
|
|
1702
|
-
const resolvedIntegration = getIntegration(integrationKind);
|
|
1703
|
-
integration =
|
|
1704
|
-
typeof resolvedIntegration.applyConnectionEnv === "function"
|
|
1705
|
-
? resolvedIntegration
|
|
1706
|
-
: null;
|
|
1707
|
-
}
|
|
1708
|
-
try {
|
|
1709
|
-
const result = await applyConnections({
|
|
1710
|
-
instance: meta,
|
|
1711
|
-
spec: consumerSpec,
|
|
1712
|
-
newConnections,
|
|
1713
|
-
saveInstanceJson,
|
|
1714
|
-
readInstanceJson,
|
|
1715
|
-
integration,
|
|
1716
|
-
});
|
|
1717
|
-
return { ok: true, applied: target.id, resolved: result.resolved.length };
|
|
1718
|
-
}
|
|
1719
|
-
catch (e) {
|
|
1720
|
-
return reply.status(e?.statusCode ?? 500).send({
|
|
1721
|
-
detail: e?.message ?? "Suggestion apply failed",
|
|
1722
|
-
code: e?.code,
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
|
-
});
|
|
1726
|
-
app.post("/api/suggestions/:id/dismiss", async (req, reply) => {
|
|
1727
|
-
const { dismissSuggestion } = await import("../services/connections/suggestions.js");
|
|
1728
|
-
try {
|
|
1729
|
-
await dismissSuggestion(req.params.id);
|
|
1730
|
-
return { ok: true };
|
|
1731
|
-
}
|
|
1732
|
-
catch (e) {
|
|
1733
|
-
return reply.status(400).send({ detail: e?.message ?? "Invalid suggestion id" });
|
|
1734
|
-
}
|
|
1735
|
-
});
|
|
1736
596
|
}
|
|
1737
597
|
//# sourceMappingURL=instances.js.map
|