enpilink 1.0.2
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/LICENSE +21 -0
- package/README.md +289 -0
- package/bin/run.js +5 -0
- package/dist/cli/build-helpers.d.ts +8 -0
- package/dist/cli/build-helpers.js +105 -0
- package/dist/cli/build-helpers.js.map +1 -0
- package/dist/cli/build-helpers.test.d.ts +1 -0
- package/dist/cli/build-helpers.test.js +100 -0
- package/dist/cli/build-helpers.test.js.map +1 -0
- package/dist/cli/detect-port.d.ts +18 -0
- package/dist/cli/detect-port.js +50 -0
- package/dist/cli/detect-port.js.map +1 -0
- package/dist/cli/ensure-ssh-key.d.ts +17 -0
- package/dist/cli/ensure-ssh-key.js +45 -0
- package/dist/cli/ensure-ssh-key.js.map +1 -0
- package/dist/cli/ensure-ssh-key.test.d.ts +1 -0
- package/dist/cli/ensure-ssh-key.test.js +68 -0
- package/dist/cli/ensure-ssh-key.test.js.map +1 -0
- package/dist/cli/header.d.ts +4 -0
- package/dist/cli/header.js +6 -0
- package/dist/cli/header.js.map +1 -0
- package/dist/cli/resolve-views-dir.d.ts +1 -0
- package/dist/cli/resolve-views-dir.js +17 -0
- package/dist/cli/resolve-views-dir.js.map +1 -0
- package/dist/cli/run-command.d.ts +2 -0
- package/dist/cli/run-command.js +43 -0
- package/dist/cli/run-command.js.map +1 -0
- package/dist/cli/telemetry.d.ts +14 -0
- package/dist/cli/telemetry.js +24 -0
- package/dist/cli/telemetry.js.map +1 -0
- package/dist/cli/tunnel-control-server.d.ts +11 -0
- package/dist/cli/tunnel-control-server.js +35 -0
- package/dist/cli/tunnel-control-server.js.map +1 -0
- package/dist/cli/tunnel-control-server.test.d.ts +1 -0
- package/dist/cli/tunnel-control-server.test.js +39 -0
- package/dist/cli/tunnel-control-server.test.js.map +1 -0
- package/dist/cli/tunnel-handler.d.ts +3 -0
- package/dist/cli/tunnel-handler.js +48 -0
- package/dist/cli/tunnel-handler.js.map +1 -0
- package/dist/cli/tunnel-handler.test.d.ts +1 -0
- package/dist/cli/tunnel-handler.test.js +107 -0
- package/dist/cli/tunnel-handler.test.js.map +1 -0
- package/dist/cli/tunnel-providers/index.d.ts +5 -0
- package/dist/cli/tunnel-providers/index.js +5 -0
- package/dist/cli/tunnel-providers/index.js.map +1 -0
- package/dist/cli/tunnel-providers/srv-us.d.ts +18 -0
- package/dist/cli/tunnel-providers/srv-us.js +66 -0
- package/dist/cli/tunnel-providers/srv-us.js.map +1 -0
- package/dist/cli/tunnel-providers/srv-us.test.d.ts +1 -0
- package/dist/cli/tunnel-providers/srv-us.test.js +74 -0
- package/dist/cli/tunnel-providers/srv-us.test.js.map +1 -0
- package/dist/cli/tunnel-providers/types.d.ts +49 -0
- package/dist/cli/tunnel-providers/types.js +2 -0
- package/dist/cli/tunnel-providers/types.js.map +1 -0
- package/dist/cli/tunnel.d.ts +75 -0
- package/dist/cli/tunnel.js +254 -0
- package/dist/cli/tunnel.js.map +1 -0
- package/dist/cli/tunnel.test.d.ts +1 -0
- package/dist/cli/tunnel.test.js +255 -0
- package/dist/cli/tunnel.test.js.map +1 -0
- package/dist/cli/types.d.ts +5 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/use-execute-steps.d.ts +11 -0
- package/dist/cli/use-execute-steps.js +36 -0
- package/dist/cli/use-execute-steps.js.map +1 -0
- package/dist/cli/use-messages.d.ts +3 -0
- package/dist/cli/use-messages.js +11 -0
- package/dist/cli/use-messages.js.map +1 -0
- package/dist/cli/use-nodemon.d.ts +2 -0
- package/dist/cli/use-nodemon.js +73 -0
- package/dist/cli/use-nodemon.js.map +1 -0
- package/dist/cli/use-open-browser.d.ts +1 -0
- package/dist/cli/use-open-browser.js +44 -0
- package/dist/cli/use-open-browser.js.map +1 -0
- package/dist/cli/use-open-tunnel-browser.d.ts +6 -0
- package/dist/cli/use-open-tunnel-browser.js +19 -0
- package/dist/cli/use-open-tunnel-browser.js.map +1 -0
- package/dist/cli/use-tunnel.d.ts +17 -0
- package/dist/cli/use-tunnel.js +131 -0
- package/dist/cli/use-tunnel.js.map +1 -0
- package/dist/cli/use-typescript-check.d.ts +9 -0
- package/dist/cli/use-typescript-check.js +94 -0
- package/dist/cli/use-typescript-check.js.map +1 -0
- package/dist/commands/build.d.ts +8 -0
- package/dist/commands/build.js +97 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/create.d.ts +9 -0
- package/dist/commands/create.js +30 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dev.d.ts +13 -0
- package/dist/commands/dev.js +112 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/start.d.ts +10 -0
- package/dist/commands/start.js +76 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/telemetry/disable.d.ts +5 -0
- package/dist/commands/telemetry/disable.js +12 -0
- package/dist/commands/telemetry/disable.js.map +1 -0
- package/dist/commands/telemetry/enable.d.ts +5 -0
- package/dist/commands/telemetry/enable.js +12 -0
- package/dist/commands/telemetry/enable.js.map +1 -0
- package/dist/commands/telemetry/status.d.ts +5 -0
- package/dist/commands/telemetry/status.js +12 -0
- package/dist/commands/telemetry/status.js.map +1 -0
- package/dist/server/admin.d.ts +79 -0
- package/dist/server/admin.js +239 -0
- package/dist/server/admin.js.map +1 -0
- package/dist/server/admin.test.d.ts +1 -0
- package/dist/server/admin.test.js +226 -0
- package/dist/server/admin.test.js.map +1 -0
- package/dist/server/analytics.d.ts +60 -0
- package/dist/server/analytics.js +168 -0
- package/dist/server/analytics.js.map +1 -0
- package/dist/server/analytics.test.d.ts +1 -0
- package/dist/server/analytics.test.js +179 -0
- package/dist/server/analytics.test.js.map +1 -0
- package/dist/server/asset-base-url-transform-plugin.d.ts +11 -0
- package/dist/server/asset-base-url-transform-plugin.js +48 -0
- package/dist/server/asset-base-url-transform-plugin.js.map +1 -0
- package/dist/server/asset-base-url-transform-plugin.test.d.ts +1 -0
- package/dist/server/asset-base-url-transform-plugin.test.js +134 -0
- package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -0
- package/dist/server/auth.d.ts +20 -0
- package/dist/server/auth.js +28 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/build-manifest.test.d.ts +1 -0
- package/dist/server/build-manifest.test.js +27 -0
- package/dist/server/build-manifest.test.js.map +1 -0
- package/dist/server/config/config.test.d.ts +1 -0
- package/dist/server/config/config.test.js +214 -0
- package/dist/server/config/config.test.js.map +1 -0
- package/dist/server/config/index.d.ts +3 -0
- package/dist/server/config/index.js +4 -0
- package/dist/server/config/index.js.map +1 -0
- package/dist/server/config/resolve.d.ts +73 -0
- package/dist/server/config/resolve.js +167 -0
- package/dist/server/config/resolve.js.map +1 -0
- package/dist/server/config/router.d.ts +23 -0
- package/dist/server/config/router.js +119 -0
- package/dist/server/config/router.js.map +1 -0
- package/dist/server/config/schema.d.ts +78 -0
- package/dist/server/config/schema.js +158 -0
- package/dist/server/config/schema.js.map +1 -0
- package/dist/server/content-helpers.d.ts +67 -0
- package/dist/server/content-helpers.js +79 -0
- package/dist/server/content-helpers.js.map +1 -0
- package/dist/server/content-helpers.test.d.ts +1 -0
- package/dist/server/content-helpers.test.js +70 -0
- package/dist/server/content-helpers.test.js.map +1 -0
- package/dist/server/express.d.ts +11 -0
- package/dist/server/express.js +129 -0
- package/dist/server/express.js.map +1 -0
- package/dist/server/express.test.d.ts +1 -0
- package/dist/server/express.test.js +464 -0
- package/dist/server/express.test.js.map +1 -0
- package/dist/server/file-ref.d.ts +28 -0
- package/dist/server/file-ref.js +27 -0
- package/dist/server/file-ref.js.map +1 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.js +14 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/inferUtilityTypes.d.ts +64 -0
- package/dist/server/inferUtilityTypes.js +2 -0
- package/dist/server/inferUtilityTypes.js.map +1 -0
- package/dist/server/log-sink.d.ts +16 -0
- package/dist/server/log-sink.js +66 -0
- package/dist/server/log-sink.js.map +1 -0
- package/dist/server/metric.d.ts +12 -0
- package/dist/server/metric.js +13 -0
- package/dist/server/metric.js.map +1 -0
- package/dist/server/middleware.d.ts +137 -0
- package/dist/server/middleware.js +93 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/middleware.test-d.d.ts +1 -0
- package/dist/server/middleware.test-d.js +75 -0
- package/dist/server/middleware.test-d.js.map +1 -0
- package/dist/server/middleware.test.d.ts +1 -0
- package/dist/server/middleware.test.js +493 -0
- package/dist/server/middleware.test.js.map +1 -0
- package/dist/server/mock-seed.d.ts +62 -0
- package/dist/server/mock-seed.js +251 -0
- package/dist/server/mock-seed.js.map +1 -0
- package/dist/server/mock-seed.test.d.ts +1 -0
- package/dist/server/mock-seed.test.js +122 -0
- package/dist/server/mock-seed.test.js.map +1 -0
- package/dist/server/observability.d.ts +149 -0
- package/dist/server/observability.js +340 -0
- package/dist/server/observability.js.map +1 -0
- package/dist/server/observability.test.d.ts +1 -0
- package/dist/server/observability.test.js +251 -0
- package/dist/server/observability.test.js.map +1 -0
- package/dist/server/otel.d.ts +45 -0
- package/dist/server/otel.js +117 -0
- package/dist/server/otel.js.map +1 -0
- package/dist/server/otel.test.d.ts +1 -0
- package/dist/server/otel.test.js +122 -0
- package/dist/server/otel.test.js.map +1 -0
- package/dist/server/server.d.ts +422 -0
- package/dist/server/server.js +684 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/storage/index.d.ts +23 -0
- package/dist/server/storage/index.js +46 -0
- package/dist/server/storage/index.js.map +1 -0
- package/dist/server/storage/memory.d.ts +30 -0
- package/dist/server/storage/memory.js +98 -0
- package/dist/server/storage/memory.js.map +1 -0
- package/dist/server/storage/memory.test.d.ts +1 -0
- package/dist/server/storage/memory.test.js +81 -0
- package/dist/server/storage/memory.test.js.map +1 -0
- package/dist/server/storage/postgres.d.ts +65 -0
- package/dist/server/storage/postgres.js +242 -0
- package/dist/server/storage/postgres.js.map +1 -0
- package/dist/server/storage/postgres.test.d.ts +1 -0
- package/dist/server/storage/postgres.test.js +182 -0
- package/dist/server/storage/postgres.test.js.map +1 -0
- package/dist/server/storage/sqlite.d.ts +33 -0
- package/dist/server/storage/sqlite.js +250 -0
- package/dist/server/storage/sqlite.js.map +1 -0
- package/dist/server/storage/sqlite.test.d.ts +1 -0
- package/dist/server/storage/sqlite.test.js +133 -0
- package/dist/server/storage/sqlite.test.js.map +1 -0
- package/dist/server/storage/types.d.ts +119 -0
- package/dist/server/storage/types.js +11 -0
- package/dist/server/storage/types.js.map +1 -0
- package/dist/server/templateHelper.d.ts +16 -0
- package/dist/server/templateHelper.js +11 -0
- package/dist/server/templateHelper.js.map +1 -0
- package/dist/server/templates.generated.d.ts +4 -0
- package/dist/server/templates.generated.js +47 -0
- package/dist/server/templates.generated.js.map +1 -0
- package/dist/server/tunnel-proxy-router.d.ts +7 -0
- package/dist/server/tunnel-proxy-router.js +110 -0
- package/dist/server/tunnel-proxy-router.js.map +1 -0
- package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
- package/dist/server/tunnel-proxy-router.test.js +229 -0
- package/dist/server/tunnel-proxy-router.test.js.map +1 -0
- package/dist/server/viewsDevServer.d.ts +14 -0
- package/dist/server/viewsDevServer.js +45 -0
- package/dist/server/viewsDevServer.js.map +1 -0
- package/dist/test/utils.d.ts +127 -0
- package/dist/test/utils.js +247 -0
- package/dist/test/utils.js.map +1 -0
- package/dist/test/view.test.d.ts +1 -0
- package/dist/test/view.test.js +568 -0
- package/dist/test/view.test.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/web/bridges/apps-sdk/adaptor.d.ts +54 -0
- package/dist/web/bridges/apps-sdk/adaptor.js +164 -0
- package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -0
- package/dist/web/bridges/apps-sdk/bridge.d.ts +11 -0
- package/dist/web/bridges/apps-sdk/bridge.js +47 -0
- package/dist/web/bridges/apps-sdk/bridge.js.map +1 -0
- package/dist/web/bridges/apps-sdk/index.d.ts +5 -0
- package/dist/web/bridges/apps-sdk/index.js +5 -0
- package/dist/web/bridges/apps-sdk/index.js.map +1 -0
- package/dist/web/bridges/apps-sdk/types.d.ts +147 -0
- package/dist/web/bridges/apps-sdk/types.js +10 -0
- package/dist/web/bridges/apps-sdk/types.js.map +1 -0
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +13 -0
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +18 -0
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -0
- package/dist/web/bridges/get-adaptor.d.ts +9 -0
- package/dist/web/bridges/get-adaptor.js +15 -0
- package/dist/web/bridges/get-adaptor.js.map +1 -0
- package/dist/web/bridges/index.d.ts +5 -0
- package/dist/web/bridges/index.js +6 -0
- package/dist/web/bridges/index.js.map +1 -0
- package/dist/web/bridges/mcp-app/adaptor.d.ts +81 -0
- package/dist/web/bridges/mcp-app/adaptor.js +346 -0
- package/dist/web/bridges/mcp-app/adaptor.js.map +1 -0
- package/dist/web/bridges/mcp-app/bridge.d.ts +28 -0
- package/dist/web/bridges/mcp-app/bridge.js +124 -0
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -0
- package/dist/web/bridges/mcp-app/index.d.ts +4 -0
- package/dist/web/bridges/mcp-app/index.js +4 -0
- package/dist/web/bridges/mcp-app/index.js.map +1 -0
- package/dist/web/bridges/mcp-app/types.d.ts +8 -0
- package/dist/web/bridges/mcp-app/types.js +2 -0
- package/dist/web/bridges/mcp-app/types.js.map +1 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +19 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js +19 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.d.ts +1 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +26 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -0
- package/dist/web/bridges/mcp-app/view-tools.test.d.ts +1 -0
- package/dist/web/bridges/mcp-app/view-tools.test.js +144 -0
- package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -0
- package/dist/web/bridges/types.d.ts +243 -0
- package/dist/web/bridges/types.js +2 -0
- package/dist/web/bridges/types.js.map +1 -0
- package/dist/web/bridges/use-host-context.d.ts +7 -0
- package/dist/web/bridges/use-host-context.js +13 -0
- package/dist/web/bridges/use-host-context.js.map +1 -0
- package/dist/web/components/modal-provider.d.ts +4 -0
- package/dist/web/components/modal-provider.js +45 -0
- package/dist/web/components/modal-provider.js.map +1 -0
- package/dist/web/create-store.d.ts +29 -0
- package/dist/web/create-store.js +64 -0
- package/dist/web/create-store.js.map +1 -0
- package/dist/web/create-store.test.d.ts +1 -0
- package/dist/web/create-store.test.js +129 -0
- package/dist/web/create-store.test.js.map +1 -0
- package/dist/web/data-llm.d.ts +47 -0
- package/dist/web/data-llm.js +100 -0
- package/dist/web/data-llm.js.map +1 -0
- package/dist/web/data-llm.test.d.ts +1 -0
- package/dist/web/data-llm.test.js +142 -0
- package/dist/web/data-llm.test.js.map +1 -0
- package/dist/web/generate-helpers.d.ts +120 -0
- package/dist/web/generate-helpers.js +115 -0
- package/dist/web/generate-helpers.js.map +1 -0
- package/dist/web/generate-helpers.test-d.d.ts +1 -0
- package/dist/web/generate-helpers.test-d.js +211 -0
- package/dist/web/generate-helpers.test-d.js.map +1 -0
- package/dist/web/generate-helpers.test.d.ts +1 -0
- package/dist/web/generate-helpers.test.js +17 -0
- package/dist/web/generate-helpers.test.js.map +1 -0
- package/dist/web/helpers/state.d.ts +7 -0
- package/dist/web/helpers/state.js +45 -0
- package/dist/web/helpers/state.js.map +1 -0
- package/dist/web/helpers/state.test.d.ts +1 -0
- package/dist/web/helpers/state.test.js +53 -0
- package/dist/web/helpers/state.test.js.map +1 -0
- package/dist/web/hooks/index.d.ts +17 -0
- package/dist/web/hooks/index.js +18 -0
- package/dist/web/hooks/index.js.map +1 -0
- package/dist/web/hooks/test/utils.d.ts +20 -0
- package/dist/web/hooks/test/utils.js +75 -0
- package/dist/web/hooks/test/utils.js.map +1 -0
- package/dist/web/hooks/use-call-tool.d.ts +146 -0
- package/dist/web/hooks/use-call-tool.js +96 -0
- package/dist/web/hooks/use-call-tool.js.map +1 -0
- package/dist/web/hooks/use-call-tool.test-d.d.ts +1 -0
- package/dist/web/hooks/use-call-tool.test-d.js +104 -0
- package/dist/web/hooks/use-call-tool.test-d.js.map +1 -0
- package/dist/web/hooks/use-call-tool.test.d.ts +1 -0
- package/dist/web/hooks/use-call-tool.test.js +211 -0
- package/dist/web/hooks/use-call-tool.test.js.map +1 -0
- package/dist/web/hooks/use-display-mode.d.ts +24 -0
- package/dist/web/hooks/use-display-mode.js +29 -0
- package/dist/web/hooks/use-display-mode.js.map +1 -0
- package/dist/web/hooks/use-display-mode.test-d.d.ts +1 -0
- package/dist/web/hooks/use-display-mode.test-d.js +8 -0
- package/dist/web/hooks/use-display-mode.test-d.js.map +1 -0
- package/dist/web/hooks/use-display-mode.test.d.ts +1 -0
- package/dist/web/hooks/use-display-mode.test.js +41 -0
- package/dist/web/hooks/use-display-mode.test.js.map +1 -0
- package/dist/web/hooks/use-download.d.ts +5 -0
- package/dist/web/hooks/use-download.js +8 -0
- package/dist/web/hooks/use-download.js.map +1 -0
- package/dist/web/hooks/use-download.test.d.ts +1 -0
- package/dist/web/hooks/use-download.test.js +95 -0
- package/dist/web/hooks/use-download.test.js.map +1 -0
- package/dist/web/hooks/use-files.d.ts +39 -0
- package/dist/web/hooks/use-files.js +42 -0
- package/dist/web/hooks/use-files.js.map +1 -0
- package/dist/web/hooks/use-files.test.d.ts +1 -0
- package/dist/web/hooks/use-files.test.js +54 -0
- package/dist/web/hooks/use-files.test.js.map +1 -0
- package/dist/web/hooks/use-intent.d.ts +30 -0
- package/dist/web/hooks/use-intent.js +34 -0
- package/dist/web/hooks/use-intent.js.map +1 -0
- package/dist/web/hooks/use-intent.test.d.ts +1 -0
- package/dist/web/hooks/use-intent.test.js +85 -0
- package/dist/web/hooks/use-intent.test.js.map +1 -0
- package/dist/web/hooks/use-layout.d.ts +24 -0
- package/dist/web/hooks/use-layout.js +25 -0
- package/dist/web/hooks/use-layout.js.map +1 -0
- package/dist/web/hooks/use-layout.test.d.ts +1 -0
- package/dist/web/hooks/use-layout.test.js +96 -0
- package/dist/web/hooks/use-layout.test.js.map +1 -0
- package/dist/web/hooks/use-notify.d.ts +29 -0
- package/dist/web/hooks/use-notify.js +33 -0
- package/dist/web/hooks/use-notify.js.map +1 -0
- package/dist/web/hooks/use-notify.test.d.ts +1 -0
- package/dist/web/hooks/use-notify.test.js +105 -0
- package/dist/web/hooks/use-notify.test.js.map +1 -0
- package/dist/web/hooks/use-open-external.d.ts +20 -0
- package/dist/web/hooks/use-open-external.js +24 -0
- package/dist/web/hooks/use-open-external.js.map +1 -0
- package/dist/web/hooks/use-open-external.test.d.ts +1 -0
- package/dist/web/hooks/use-open-external.test.js +65 -0
- package/dist/web/hooks/use-open-external.test.js.map +1 -0
- package/dist/web/hooks/use-register-view-tool.d.ts +38 -0
- package/dist/web/hooks/use-register-view-tool.js +50 -0
- package/dist/web/hooks/use-register-view-tool.js.map +1 -0
- package/dist/web/hooks/use-request-close.d.ts +16 -0
- package/dist/web/hooks/use-request-close.js +21 -0
- package/dist/web/hooks/use-request-close.js.map +1 -0
- package/dist/web/hooks/use-request-close.test.d.ts +1 -0
- package/dist/web/hooks/use-request-close.test.js +52 -0
- package/dist/web/hooks/use-request-close.test.js.map +1 -0
- package/dist/web/hooks/use-request-modal.d.ts +24 -0
- package/dist/web/hooks/use-request-modal.js +31 -0
- package/dist/web/hooks/use-request-modal.js.map +1 -0
- package/dist/web/hooks/use-request-modal.test.d.ts +1 -0
- package/dist/web/hooks/use-request-modal.test.js +61 -0
- package/dist/web/hooks/use-request-modal.test.js.map +1 -0
- package/dist/web/hooks/use-request-size.d.ts +20 -0
- package/dist/web/hooks/use-request-size.js +24 -0
- package/dist/web/hooks/use-request-size.js.map +1 -0
- package/dist/web/hooks/use-request-size.test.d.ts +1 -0
- package/dist/web/hooks/use-request-size.test.js +65 -0
- package/dist/web/hooks/use-request-size.test.js.map +1 -0
- package/dist/web/hooks/use-send-follow-up-message.d.ts +19 -0
- package/dist/web/hooks/use-send-follow-up-message.js +25 -0
- package/dist/web/hooks/use-send-follow-up-message.js.map +1 -0
- package/dist/web/hooks/use-set-open-in-app-url.d.ts +18 -0
- package/dist/web/hooks/use-set-open-in-app-url.js +25 -0
- package/dist/web/hooks/use-set-open-in-app-url.js.map +1 -0
- package/dist/web/hooks/use-set-open-in-app-url.test.d.ts +1 -0
- package/dist/web/hooks/use-set-open-in-app-url.test.js +43 -0
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -0
- package/dist/web/hooks/use-tool-info.d.ts +87 -0
- package/dist/web/hooks/use-tool-info.js +49 -0
- package/dist/web/hooks/use-tool-info.js.map +1 -0
- package/dist/web/hooks/use-tool-info.test-d.d.ts +1 -0
- package/dist/web/hooks/use-tool-info.test-d.js +91 -0
- package/dist/web/hooks/use-tool-info.test-d.js.map +1 -0
- package/dist/web/hooks/use-tool-info.test.d.ts +1 -0
- package/dist/web/hooks/use-tool-info.test.js +130 -0
- package/dist/web/hooks/use-tool-info.test.js.map +1 -0
- package/dist/web/hooks/use-user.d.ts +20 -0
- package/dist/web/hooks/use-user.js +37 -0
- package/dist/web/hooks/use-user.js.map +1 -0
- package/dist/web/hooks/use-user.test.d.ts +1 -0
- package/dist/web/hooks/use-user.test.js +122 -0
- package/dist/web/hooks/use-user.test.js.map +1 -0
- package/dist/web/hooks/use-view-state.d.ts +25 -0
- package/dist/web/hooks/use-view-state.js +32 -0
- package/dist/web/hooks/use-view-state.js.map +1 -0
- package/dist/web/hooks/use-view-state.test.d.ts +1 -0
- package/dist/web/hooks/use-view-state.test.js +177 -0
- package/dist/web/hooks/use-view-state.test.js.map +1 -0
- package/dist/web/index.d.ts +7 -0
- package/dist/web/index.js +8 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/mount-view.d.ts +20 -0
- package/dist/web/mount-view.js +46 -0
- package/dist/web/mount-view.js.map +1 -0
- package/dist/web/plugin/data-llm.test.d.ts +1 -0
- package/dist/web/plugin/data-llm.test.js +81 -0
- package/dist/web/plugin/data-llm.test.js.map +1 -0
- package/dist/web/plugin/plugin.d.ts +33 -0
- package/dist/web/plugin/plugin.js +189 -0
- package/dist/web/plugin/plugin.js.map +1 -0
- package/dist/web/plugin/scan-views.d.ts +16 -0
- package/dist/web/plugin/scan-views.js +88 -0
- package/dist/web/plugin/scan-views.js.map +1 -0
- package/dist/web/plugin/scan-views.test.d.ts +1 -0
- package/dist/web/plugin/scan-views.test.js +99 -0
- package/dist/web/plugin/scan-views.test.js.map +1 -0
- package/dist/web/plugin/transform-data-llm.d.ts +12 -0
- package/dist/web/plugin/transform-data-llm.js +96 -0
- package/dist/web/plugin/transform-data-llm.js.map +1 -0
- package/dist/web/plugin/transform-data-llm.test.d.ts +1 -0
- package/dist/web/plugin/transform-data-llm.test.js +81 -0
- package/dist/web/plugin/transform-data-llm.test.js.map +1 -0
- package/dist/web/plugin/validate-view.d.ts +1 -0
- package/dist/web/plugin/validate-view.js +9 -0
- package/dist/web/plugin/validate-view.js.map +1 -0
- package/dist/web/plugin/validate-view.test.d.ts +1 -0
- package/dist/web/plugin/validate-view.test.js +24 -0
- package/dist/web/plugin/validate-view.test.js.map +1 -0
- package/dist/web/proxy.d.ts +1 -0
- package/dist/web/proxy.js +52 -0
- package/dist/web/proxy.js.map +1 -0
- package/dist/web/types.d.ts +20 -0
- package/dist/web/types.js +2 -0
- package/dist/web/types.js.map +1 -0
- package/package.json +125 -0
- package/scripts/postinstall.mjs +45 -0
- package/tsconfig.base.json +36 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { coerceBool, coerceNumber, configSchema, ENV_VARS, isRuntimeKey, isSecretKey, keyMeta, RUNTIME_KEYS, schemaForKey, } from "./schema.js";
|
|
4
|
+
/** A masked placeholder returned in place of any secret value. */
|
|
5
|
+
export const MASKED = "••••••••";
|
|
6
|
+
const FILE_NAMES = ["enpilink.config.json", "enpilink.config.ts"];
|
|
7
|
+
/**
|
|
8
|
+
* Load the optional config file (JSON only — a `.ts` file is acknowledged but
|
|
9
|
+
* not executed here to avoid a runtime transpile dependency; M6 can add TS
|
|
10
|
+
* loading). Returns a flat partial keyed by {@link ConfigKey}. Never throws —
|
|
11
|
+
* a malformed/absent file yields `{}`.
|
|
12
|
+
*/
|
|
13
|
+
export function loadConfigFile(cwd = process.cwd()) {
|
|
14
|
+
for (const name of FILE_NAMES) {
|
|
15
|
+
const full = path.join(cwd, name);
|
|
16
|
+
if (!fs.existsSync(full)) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (name.endsWith(".json")) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = fs.readFileSync(full, "utf8");
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
if (parsed && typeof parsed === "object") {
|
|
24
|
+
return { source: "file", values: parsed };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Malformed file → ignore (defaults/db/env still apply).
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// A `.ts` config file is recognized but not executed in M4.
|
|
32
|
+
}
|
|
33
|
+
return { source: null, values: {} };
|
|
34
|
+
}
|
|
35
|
+
/** Read the raw env value for a key (the mapped env var), or undefined. */
|
|
36
|
+
function envValue(key) {
|
|
37
|
+
const raw = process.env[ENV_VARS[key]];
|
|
38
|
+
return raw === undefined || raw === "" ? undefined : raw;
|
|
39
|
+
}
|
|
40
|
+
/** Default-typed sample used to learn each key's expected runtime type. */
|
|
41
|
+
const TYPE_SAMPLE = configSchema.parse({});
|
|
42
|
+
/** Coerce a raw value (e.g. an env string) to the key's expected type. */
|
|
43
|
+
function coerceForKey(key, raw) {
|
|
44
|
+
const expected = typeof TYPE_SAMPLE[key];
|
|
45
|
+
if (expected === "boolean") {
|
|
46
|
+
return coerceBool(raw) ?? raw;
|
|
47
|
+
}
|
|
48
|
+
if (expected === "number") {
|
|
49
|
+
return coerceNumber(raw) ?? raw;
|
|
50
|
+
}
|
|
51
|
+
return raw;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve all config. Reads the DB (runtime keys only) when a storage adapter
|
|
55
|
+
* is provided; secrets are never read from the DB. Falls back to defaults when
|
|
56
|
+
* there is no storage. Never throws on a storage error — it degrades to
|
|
57
|
+
* file/env/default.
|
|
58
|
+
*
|
|
59
|
+
* @param storage active storage adapter, or `null` when analytics/admin is off.
|
|
60
|
+
*/
|
|
61
|
+
export async function resolveConfig(storage, cwd = process.cwd()) {
|
|
62
|
+
const file = loadConfigFile(cwd);
|
|
63
|
+
// DB values for runtime keys only (secrets are never persisted/read).
|
|
64
|
+
let db = {};
|
|
65
|
+
if (storage) {
|
|
66
|
+
try {
|
|
67
|
+
const all = await storage.allConfig();
|
|
68
|
+
for (const key of RUNTIME_KEYS) {
|
|
69
|
+
if (key in all && !isSecretKey(key)) {
|
|
70
|
+
db[key] = all[key];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
db = {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const rawValues = {};
|
|
79
|
+
const sources = new Map();
|
|
80
|
+
const keys = Object.keys(configSchema.shape);
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
const env = envValue(key);
|
|
83
|
+
if (env !== undefined) {
|
|
84
|
+
rawValues[key] = coerceForKey(key, env);
|
|
85
|
+
sources.set(key, "env");
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (key in file.values) {
|
|
89
|
+
rawValues[key] = file.values[key];
|
|
90
|
+
sources.set(key, "file");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Secrets are never read from the DB.
|
|
94
|
+
if (isRuntimeKey(key) && !isSecretKey(key) && key in db) {
|
|
95
|
+
rawValues[key] = db[key];
|
|
96
|
+
sources.set(key, "db");
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
sources.set(key, "default");
|
|
100
|
+
}
|
|
101
|
+
// Validate + fill defaults. If a supplied value is invalid, zod throws; we
|
|
102
|
+
// fall back per-key to the default so a bad DB/file value can't crash reads.
|
|
103
|
+
let values;
|
|
104
|
+
try {
|
|
105
|
+
values = configSchema.parse(rawValues);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Re-resolve key-by-key, dropping any invalid override.
|
|
109
|
+
const safe = {};
|
|
110
|
+
for (const key of keys) {
|
|
111
|
+
const single = schemaForKey(key).safeParse(rawValues[key]);
|
|
112
|
+
if (single.success && key in rawValues) {
|
|
113
|
+
safe[key] = rawValues[key];
|
|
114
|
+
}
|
|
115
|
+
else if (sources.get(key) !== "default") {
|
|
116
|
+
sources.set(key, "default");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
values = configSchema.parse(safe);
|
|
120
|
+
}
|
|
121
|
+
const settings = keys.map((key) => {
|
|
122
|
+
const meta = keyMeta(key);
|
|
123
|
+
const source = sources.get(key) ?? "default";
|
|
124
|
+
const secret = meta.secret;
|
|
125
|
+
// Env-locked: bootstrap keys are always read-only; runtime keys are locked
|
|
126
|
+
// when pinned by env or file (the DB write would be shadowed).
|
|
127
|
+
const envLocked = meta.tier === "bootstrap" || source === "env" || source === "file";
|
|
128
|
+
const value = secret
|
|
129
|
+
? values[key] !== undefined && values[key] !== ""
|
|
130
|
+
? MASKED
|
|
131
|
+
: null
|
|
132
|
+
: values[key];
|
|
133
|
+
return {
|
|
134
|
+
key,
|
|
135
|
+
tier: meta.tier,
|
|
136
|
+
value,
|
|
137
|
+
source,
|
|
138
|
+
secret,
|
|
139
|
+
envLocked,
|
|
140
|
+
env: meta.env,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
return { values, settings };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Validate + coerce a single RUNTIME, non-secret value before persisting.
|
|
147
|
+
* Returns the coerced value or an error string. Rejects unknown/bootstrap/
|
|
148
|
+
* secret keys (those are handled by the caller with a clear 4xx).
|
|
149
|
+
*/
|
|
150
|
+
export function validateRuntimeWrite(key, rawValue) {
|
|
151
|
+
if (!isRuntimeKey(key)) {
|
|
152
|
+
return { ok: false, error: `"${key}" is not a runtime key` };
|
|
153
|
+
}
|
|
154
|
+
if (isSecretKey(key)) {
|
|
155
|
+
return { ok: false, error: `"${key}" is a secret and cannot be set here` };
|
|
156
|
+
}
|
|
157
|
+
const coerced = coerceForKey(key, rawValue);
|
|
158
|
+
const parsed = schemaForKey(key).safeParse(coerced);
|
|
159
|
+
if (!parsed.success) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
error: `invalid value for "${key}": ${parsed.error.issues[0]?.message ?? "validation failed"}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return { ok: true, value: parsed.data };
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/server/config/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { StorageAdapter } from \"../storage/types.js\";\nimport {\n type Config,\n type ConfigKey,\n coerceBool,\n coerceNumber,\n configSchema,\n ENV_VARS,\n isRuntimeKey,\n isSecretKey,\n keyMeta,\n RUNTIME_KEYS,\n schemaForKey,\n} from \"./schema.js\";\n\n/**\n * Config resolution (M4). Merges sources with precedence\n * **env > file (`enpilink.config.{json,ts}`) > db (runtime only) > default**\n * and reports, per key, which source supplied the value and whether the key is\n * secret / env-locked (read-only).\n *\n * Secrets (e.g. `adminAuthToken`) are NEVER read from the DB and NEVER returned\n * in plaintext — {@link resolveConfig} masks them to a placeholder and flags\n * `secret: true`. Bootstrap keys and any key pinned by env/file are\n * `envLocked: true` (the admin UI must render them read-only).\n */\n\nexport type ConfigSource = \"env\" | \"file\" | \"db\" | \"default\";\n\n/** A masked placeholder returned in place of any secret value. */\nexport const MASKED = \"••••••••\";\n\n/** A single resolved setting as exposed by the config API. */\nexport interface ResolvedSetting {\n key: ConfigKey;\n /** Tier: `bootstrap` (env/file only) or `runtime` (DB-editable). */\n tier: \"bootstrap\" | \"runtime\";\n /** The resolved value, or {@link MASKED} when secret. */\n value: unknown;\n /** Which source supplied the value. */\n source: ConfigSource;\n /** Whether this key is a secret (never returned in plaintext). */\n secret: boolean;\n /**\n * Whether this key is read-only in the admin UI. True for all bootstrap keys,\n * and for any runtime key pinned via env or file (the DB value is shadowed).\n */\n envLocked: boolean;\n /** The env var that drives / can pin this key. */\n env: string;\n}\n\n/** The full resolved config plus per-key reporting. */\nexport interface ResolvedConfig {\n /** Effective typed values (secrets present in-process, masked only at the API). */\n values: Config;\n /** Per-key source + secret/env-lock reporting. */\n settings: ResolvedSetting[];\n}\n\nconst FILE_NAMES = [\"enpilink.config.json\", \"enpilink.config.ts\"];\n\n/**\n * Load the optional config file (JSON only — a `.ts` file is acknowledged but\n * not executed here to avoid a runtime transpile dependency; M6 can add TS\n * loading). Returns a flat partial keyed by {@link ConfigKey}. Never throws —\n * a malformed/absent file yields `{}`.\n */\nexport function loadConfigFile(cwd: string = process.cwd()): {\n source: \"file\" | null;\n values: Partial<Record<ConfigKey, unknown>>;\n} {\n for (const name of FILE_NAMES) {\n const full = path.join(cwd, name);\n if (!fs.existsSync(full)) {\n continue;\n }\n if (name.endsWith(\".json\")) {\n try {\n const raw = fs.readFileSync(full, \"utf8\");\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") {\n return { source: \"file\", values: parsed as Record<string, unknown> };\n }\n } catch {\n // Malformed file → ignore (defaults/db/env still apply).\n }\n }\n // A `.ts` config file is recognized but not executed in M4.\n }\n return { source: null, values: {} };\n}\n\n/** Read the raw env value for a key (the mapped env var), or undefined. */\nfunction envValue(key: ConfigKey): string | undefined {\n const raw = process.env[ENV_VARS[key]];\n return raw === undefined || raw === \"\" ? undefined : raw;\n}\n\n/** Default-typed sample used to learn each key's expected runtime type. */\nconst TYPE_SAMPLE = configSchema.parse({}) as Record<string, unknown>;\n\n/** Coerce a raw value (e.g. an env string) to the key's expected type. */\nfunction coerceForKey(key: ConfigKey, raw: unknown): unknown {\n const expected = typeof TYPE_SAMPLE[key];\n if (expected === \"boolean\") {\n return coerceBool(raw) ?? raw;\n }\n if (expected === \"number\") {\n return coerceNumber(raw) ?? raw;\n }\n return raw;\n}\n\n/**\n * Resolve all config. Reads the DB (runtime keys only) when a storage adapter\n * is provided; secrets are never read from the DB. Falls back to defaults when\n * there is no storage. Never throws on a storage error — it degrades to\n * file/env/default.\n *\n * @param storage active storage adapter, or `null` when analytics/admin is off.\n */\nexport async function resolveConfig(\n storage: StorageAdapter | null,\n cwd: string = process.cwd(),\n): Promise<ResolvedConfig> {\n const file = loadConfigFile(cwd);\n\n // DB values for runtime keys only (secrets are never persisted/read).\n let db: Record<string, unknown> = {};\n if (storage) {\n try {\n const all = await storage.allConfig();\n for (const key of RUNTIME_KEYS) {\n if (key in all && !isSecretKey(key)) {\n db[key] = all[key];\n }\n }\n } catch {\n db = {};\n }\n }\n\n const rawValues: Record<string, unknown> = {};\n const sources = new Map<ConfigKey, ConfigSource>();\n\n const keys = Object.keys(configSchema.shape) as ConfigKey[];\n for (const key of keys) {\n const env = envValue(key);\n if (env !== undefined) {\n rawValues[key] = coerceForKey(key, env);\n sources.set(key, \"env\");\n continue;\n }\n if (key in file.values) {\n rawValues[key] = file.values[key];\n sources.set(key, \"file\");\n continue;\n }\n // Secrets are never read from the DB.\n if (isRuntimeKey(key) && !isSecretKey(key) && key in db) {\n rawValues[key] = db[key];\n sources.set(key, \"db\");\n continue;\n }\n sources.set(key, \"default\");\n }\n\n // Validate + fill defaults. If a supplied value is invalid, zod throws; we\n // fall back per-key to the default so a bad DB/file value can't crash reads.\n let values: Config;\n try {\n values = configSchema.parse(rawValues);\n } catch {\n // Re-resolve key-by-key, dropping any invalid override.\n const safe: Record<string, unknown> = {};\n for (const key of keys) {\n const single = schemaForKey(key).safeParse(rawValues[key]);\n if (single.success && key in rawValues) {\n safe[key] = rawValues[key];\n } else if (sources.get(key) !== \"default\") {\n sources.set(key, \"default\");\n }\n }\n values = configSchema.parse(safe);\n }\n\n const settings: ResolvedSetting[] = keys.map((key) => {\n const meta = keyMeta(key);\n const source = sources.get(key) ?? \"default\";\n const secret = meta.secret;\n // Env-locked: bootstrap keys are always read-only; runtime keys are locked\n // when pinned by env or file (the DB write would be shadowed).\n const envLocked =\n meta.tier === \"bootstrap\" || source === \"env\" || source === \"file\";\n const value = secret\n ? values[key] !== undefined && values[key] !== \"\"\n ? MASKED\n : null\n : (values[key] as unknown);\n return {\n key,\n tier: meta.tier,\n value,\n source,\n secret,\n envLocked,\n env: meta.env,\n };\n });\n\n return { values, settings };\n}\n\n/**\n * Validate + coerce a single RUNTIME, non-secret value before persisting.\n * Returns the coerced value or an error string. Rejects unknown/bootstrap/\n * secret keys (those are handled by the caller with a clear 4xx).\n */\nexport function validateRuntimeWrite(\n key: string,\n rawValue: unknown,\n): { ok: true; value: unknown } | { ok: false; error: string } {\n if (!isRuntimeKey(key)) {\n return { ok: false, error: `\"${key}\" is not a runtime key` };\n }\n if (isSecretKey(key)) {\n return { ok: false, error: `\"${key}\" is a secret and cannot be set here` };\n }\n const coerced = coerceForKey(key as ConfigKey, rawValue);\n const parsed = schemaForKey(key as ConfigKey).safeParse(coerced);\n if (!parsed.success) {\n return {\n ok: false,\n error: `invalid value for \"${key}\": ${parsed.error.issues[0]?.message ?? \"validation failed\"}`,\n };\n }\n return { ok: true, value: parsed.data };\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Router } from "express";
|
|
2
|
+
import type { StorageAdapter } from "../storage/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Config admin API (M4). Pure core — reads the SAME active
|
|
5
|
+
* {@link StorageAdapter} the analytics middleware writes to, via
|
|
6
|
+
* {@link getActiveStorage}. Does NOT depend on `@enpilink/console`.
|
|
7
|
+
*
|
|
8
|
+
* Mounted dev-only (under the `NODE_ENV !== "production"` block in
|
|
9
|
+
* `express.ts`) at `/__enpilink/config`. Prod admin mounting (behind bearer
|
|
10
|
+
* auth) is M5.
|
|
11
|
+
*
|
|
12
|
+
* Routes:
|
|
13
|
+
* - `GET /__enpilink/config` — all settings (value-or-masked + source +
|
|
14
|
+
* secret/envLocked flags).
|
|
15
|
+
* - `PUT /__enpilink/config/:key` — set a RUNTIME, non-secret key. Rejects
|
|
16
|
+
* bootstrap / secret / env-locked / unknown keys with a clear 4xx.
|
|
17
|
+
* - `GET /__enpilink/config/audit` — recent config-change history.
|
|
18
|
+
*
|
|
19
|
+
* Disabled-safe: when there is no active storage, reads fall back to env/file/
|
|
20
|
+
* defaults (runtime keys show their defaults) and NEVER 500. Writes require a
|
|
21
|
+
* storage adapter (409 when none) — there is nowhere to persist otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createConfigRouter(getStorage?: () => StorageAdapter | null): Router;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import express, {} from "express";
|
|
2
|
+
import { getActiveStorage } from "../log-sink.js";
|
|
3
|
+
import { resolveConfig, validateRuntimeWrite } from "./resolve.js";
|
|
4
|
+
import { isBootstrapKey, isKnownKey, isRuntimeKey, isSecretKey, } from "./schema.js";
|
|
5
|
+
/**
|
|
6
|
+
* Config admin API (M4). Pure core — reads the SAME active
|
|
7
|
+
* {@link StorageAdapter} the analytics middleware writes to, via
|
|
8
|
+
* {@link getActiveStorage}. Does NOT depend on `@enpilink/console`.
|
|
9
|
+
*
|
|
10
|
+
* Mounted dev-only (under the `NODE_ENV !== "production"` block in
|
|
11
|
+
* `express.ts`) at `/__enpilink/config`. Prod admin mounting (behind bearer
|
|
12
|
+
* auth) is M5.
|
|
13
|
+
*
|
|
14
|
+
* Routes:
|
|
15
|
+
* - `GET /__enpilink/config` — all settings (value-or-masked + source +
|
|
16
|
+
* secret/envLocked flags).
|
|
17
|
+
* - `PUT /__enpilink/config/:key` — set a RUNTIME, non-secret key. Rejects
|
|
18
|
+
* bootstrap / secret / env-locked / unknown keys with a clear 4xx.
|
|
19
|
+
* - `GET /__enpilink/config/audit` — recent config-change history.
|
|
20
|
+
*
|
|
21
|
+
* Disabled-safe: when there is no active storage, reads fall back to env/file/
|
|
22
|
+
* defaults (runtime keys show their defaults) and NEVER 500. Writes require a
|
|
23
|
+
* storage adapter (409 when none) — there is nowhere to persist otherwise.
|
|
24
|
+
*/
|
|
25
|
+
export function createConfigRouter(getStorage = getActiveStorage) {
|
|
26
|
+
const router = express.Router();
|
|
27
|
+
const base = "/__enpilink/config";
|
|
28
|
+
// GET /config — full resolved settings. Secrets masked; never 500.
|
|
29
|
+
router.get(base, async (_req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const resolved = await resolveConfig(getStorage());
|
|
32
|
+
res.json({ settings: resolved.settings });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Last-resort: resolve with no storage so reads never fail.
|
|
36
|
+
const resolved = await resolveConfig(null);
|
|
37
|
+
res.json({ settings: resolved.settings });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// GET /config/audit — change history (most recent first). Never 500.
|
|
41
|
+
router.get(`${base}/audit`, async (_req, res) => {
|
|
42
|
+
const storage = getStorage();
|
|
43
|
+
if (!storage) {
|
|
44
|
+
res.json({ enabled: false, audit: [] });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const audit = await storage.getConfigAudit();
|
|
49
|
+
res.json({ enabled: true, audit });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
res.json({ enabled: false, audit: [] });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// PUT /config/:key — set a runtime, non-secret key. Reject everything else.
|
|
56
|
+
router.put(`${base}/:key`, async (req, res) => {
|
|
57
|
+
const key = req.params.key;
|
|
58
|
+
if (!isKnownKey(key)) {
|
|
59
|
+
res.status(404).json({ error: `Unknown config key "${key}"` });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (isSecretKey(key)) {
|
|
63
|
+
res.status(403).json({
|
|
64
|
+
error: `"${key}" is a secret and is set via environment only`,
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (isBootstrapKey(key)) {
|
|
69
|
+
res.status(403).json({
|
|
70
|
+
error: `"${key}" is a bootstrap setting (env/file only) and is read-only here`,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!isRuntimeKey(key)) {
|
|
75
|
+
res.status(403).json({ error: `"${key}" is not editable` });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Reject if this runtime key is currently pinned (env-locked) by env/file.
|
|
79
|
+
const resolved = await resolveConfig(getStorage());
|
|
80
|
+
const setting = resolved.settings.find((s) => s.key === key);
|
|
81
|
+
if (setting?.envLocked) {
|
|
82
|
+
res.status(409).json({
|
|
83
|
+
error: `"${key}" is pinned via ${setting.source} and cannot be changed here`,
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const body = req.body;
|
|
88
|
+
const rawValue = body?.value;
|
|
89
|
+
const check = validateRuntimeWrite(key, rawValue);
|
|
90
|
+
if (!check.ok) {
|
|
91
|
+
res.status(400).json({ error: check.error });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const storage = getStorage();
|
|
95
|
+
if (!storage) {
|
|
96
|
+
res.status(409).json({
|
|
97
|
+
error: "No active storage; cannot persist runtime config",
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const actor = actorOf(req);
|
|
103
|
+
await storage.setConfig(key, check.value, actor);
|
|
104
|
+
res.json({ ok: true, key, value: check.value });
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
res.status(500).json({
|
|
108
|
+
error: err instanceof Error ? err.message : "Failed to write config",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return router;
|
|
113
|
+
}
|
|
114
|
+
/** Best-effort actor attribution for audit rows (no auth in dev → "dev"). */
|
|
115
|
+
function actorOf(req) {
|
|
116
|
+
const header = req.header("x-enpilink-actor");
|
|
117
|
+
return typeof header === "string" && header.length > 0 ? header : "dev";
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../../src/server/config/router.ts"],"names":[],"mappings":"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","sourcesContent":["import express, { type Router } from \"express\";\nimport { getActiveStorage } from \"../log-sink.js\";\nimport type { ConfigAuditEntry, StorageAdapter } from \"../storage/types.js\";\nimport { resolveConfig, validateRuntimeWrite } from \"./resolve.js\";\nimport {\n isBootstrapKey,\n isKnownKey,\n isRuntimeKey,\n isSecretKey,\n} from \"./schema.js\";\n\n/**\n * Config admin API (M4). Pure core — reads the SAME active\n * {@link StorageAdapter} the analytics middleware writes to, via\n * {@link getActiveStorage}. Does NOT depend on `@enpilink/console`.\n *\n * Mounted dev-only (under the `NODE_ENV !== \"production\"` block in\n * `express.ts`) at `/__enpilink/config`. Prod admin mounting (behind bearer\n * auth) is M5.\n *\n * Routes:\n * - `GET /__enpilink/config` — all settings (value-or-masked + source +\n * secret/envLocked flags).\n * - `PUT /__enpilink/config/:key` — set a RUNTIME, non-secret key. Rejects\n * bootstrap / secret / env-locked / unknown keys with a clear 4xx.\n * - `GET /__enpilink/config/audit` — recent config-change history.\n *\n * Disabled-safe: when there is no active storage, reads fall back to env/file/\n * defaults (runtime keys show their defaults) and NEVER 500. Writes require a\n * storage adapter (409 when none) — there is nowhere to persist otherwise.\n */\nexport function createConfigRouter(\n getStorage: () => StorageAdapter | null = getActiveStorage,\n): Router {\n const router = express.Router();\n const base = \"/__enpilink/config\";\n\n // GET /config — full resolved settings. Secrets masked; never 500.\n router.get(base, async (_req, res) => {\n try {\n const resolved = await resolveConfig(getStorage());\n res.json({ settings: resolved.settings });\n } catch {\n // Last-resort: resolve with no storage so reads never fail.\n const resolved = await resolveConfig(null);\n res.json({ settings: resolved.settings });\n }\n });\n\n // GET /config/audit — change history (most recent first). Never 500.\n router.get(`${base}/audit`, async (_req, res) => {\n const storage = getStorage();\n if (!storage) {\n res.json({ enabled: false, audit: [] as ConfigAuditEntry[] });\n return;\n }\n try {\n const audit = await storage.getConfigAudit();\n res.json({ enabled: true, audit });\n } catch {\n res.json({ enabled: false, audit: [] as ConfigAuditEntry[] });\n }\n });\n\n // PUT /config/:key — set a runtime, non-secret key. Reject everything else.\n router.put(`${base}/:key`, async (req, res) => {\n const key = req.params.key;\n\n if (!isKnownKey(key)) {\n res.status(404).json({ error: `Unknown config key \"${key}\"` });\n return;\n }\n if (isSecretKey(key)) {\n res.status(403).json({\n error: `\"${key}\" is a secret and is set via environment only`,\n });\n return;\n }\n if (isBootstrapKey(key)) {\n res.status(403).json({\n error: `\"${key}\" is a bootstrap setting (env/file only) and is read-only here`,\n });\n return;\n }\n if (!isRuntimeKey(key)) {\n res.status(403).json({ error: `\"${key}\" is not editable` });\n return;\n }\n\n // Reject if this runtime key is currently pinned (env-locked) by env/file.\n const resolved = await resolveConfig(getStorage());\n const setting = resolved.settings.find((s) => s.key === key);\n if (setting?.envLocked) {\n res.status(409).json({\n error: `\"${key}\" is pinned via ${setting.source} and cannot be changed here`,\n });\n return;\n }\n\n const body = req.body as { value?: unknown } | undefined;\n const rawValue = body?.value;\n const check = validateRuntimeWrite(key, rawValue);\n if (!check.ok) {\n res.status(400).json({ error: check.error });\n return;\n }\n\n const storage = getStorage();\n if (!storage) {\n res.status(409).json({\n error: \"No active storage; cannot persist runtime config\",\n });\n return;\n }\n\n try {\n const actor = actorOf(req);\n await storage.setConfig(key, check.value, actor);\n res.json({ ok: true, key, value: check.value });\n } catch (err) {\n res.status(500).json({\n error: err instanceof Error ? err.message : \"Failed to write config\",\n });\n }\n });\n\n return router;\n}\n\n/** Best-effort actor attribution for audit rows (no auth in dev → \"dev\"). */\nfunction actorOf(req: express.Request): string {\n const header = req.header(\"x-enpilink-actor\");\n return typeof header === \"string\" && header.length > 0 ? header : \"dev\";\n}\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Coerce an env string (or already-typed value) into a boolean. */
|
|
3
|
+
export declare function coerceBool(raw: unknown): boolean | undefined;
|
|
4
|
+
/** Coerce an env string (or number) into a finite number. */
|
|
5
|
+
export declare function coerceNumber(raw: unknown): number | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Bootstrap (env/file-only) settings. Not DB-editable. Defaults keep the
|
|
8
|
+
* framework off-by-default and secure-by-default.
|
|
9
|
+
*/
|
|
10
|
+
export declare const bootstrapSchema: z.ZodObject<{
|
|
11
|
+
storage: z.ZodDefault<z.ZodString>;
|
|
12
|
+
dbPath: z.ZodDefault<z.ZodString>;
|
|
13
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
admin: z.ZodDefault<z.ZodBoolean>;
|
|
15
|
+
adminAuthToken: z.ZodOptional<z.ZodString>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
/** Runtime (DB-editable) settings. */
|
|
18
|
+
export declare const runtimeSchema: z.ZodObject<{
|
|
19
|
+
"analytics.enabled": z.ZodDefault<z.ZodBoolean>;
|
|
20
|
+
"analytics.sampleRate": z.ZodDefault<z.ZodNumber>;
|
|
21
|
+
"retention.events": z.ZodDefault<z.ZodNumber>;
|
|
22
|
+
"retention.logs": z.ZodDefault<z.ZodNumber>;
|
|
23
|
+
"flags.liveLogs": z.ZodDefault<z.ZodBoolean>;
|
|
24
|
+
"display.bucketMs": z.ZodDefault<z.ZodNumber>;
|
|
25
|
+
}, z.core.$strip>;
|
|
26
|
+
export declare const configSchema: z.ZodObject<{
|
|
27
|
+
storage: z.ZodDefault<z.ZodString>;
|
|
28
|
+
dbPath: z.ZodDefault<z.ZodString>;
|
|
29
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
30
|
+
admin: z.ZodDefault<z.ZodBoolean>;
|
|
31
|
+
adminAuthToken: z.ZodOptional<z.ZodString>;
|
|
32
|
+
"analytics.enabled": z.ZodDefault<z.ZodBoolean>;
|
|
33
|
+
"analytics.sampleRate": z.ZodDefault<z.ZodNumber>;
|
|
34
|
+
"retention.events": z.ZodDefault<z.ZodNumber>;
|
|
35
|
+
"retention.logs": z.ZodDefault<z.ZodNumber>;
|
|
36
|
+
"flags.liveLogs": z.ZodDefault<z.ZodBoolean>;
|
|
37
|
+
"display.bucketMs": z.ZodDefault<z.ZodNumber>;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
export type BootstrapConfig = z.infer<typeof bootstrapSchema>;
|
|
40
|
+
export type RuntimeConfig = z.infer<typeof runtimeSchema>;
|
|
41
|
+
export type Config = z.infer<typeof configSchema>;
|
|
42
|
+
/** All known config keys. */
|
|
43
|
+
export type ConfigKey = keyof Config;
|
|
44
|
+
export type BootstrapKey = keyof BootstrapConfig;
|
|
45
|
+
export type RuntimeKey = keyof RuntimeConfig;
|
|
46
|
+
/** Per-key metadata describing tier / secret / env-lock semantics. */
|
|
47
|
+
export interface KeyMeta {
|
|
48
|
+
key: ConfigKey;
|
|
49
|
+
/** `bootstrap` (env/file only) or `runtime` (DB-editable). */
|
|
50
|
+
tier: "bootstrap" | "runtime";
|
|
51
|
+
/** Secret keys are env-only and NEVER persisted/returned in plaintext. */
|
|
52
|
+
secret: boolean;
|
|
53
|
+
/** The env var that drives this key (for the "set via env" hint). */
|
|
54
|
+
env: string;
|
|
55
|
+
}
|
|
56
|
+
/** Bootstrap keys (env/file only). */
|
|
57
|
+
export declare const BOOTSTRAP_KEYS: readonly ["storage", "dbPath", "port", "admin", "adminAuthToken"];
|
|
58
|
+
/** Runtime keys (DB-editable). */
|
|
59
|
+
export declare const RUNTIME_KEYS: readonly ["analytics.enabled", "analytics.sampleRate", "retention.events", "retention.logs", "flags.liveLogs", "display.bucketMs"];
|
|
60
|
+
/** Secret keys: env-only, masked + never persisted/returned in plaintext. */
|
|
61
|
+
export declare const SECRET_KEYS: readonly ["adminAuthToken"];
|
|
62
|
+
/**
|
|
63
|
+
* Env var mapping per key. Bootstrap keys map to dedicated env vars that match
|
|
64
|
+
* the framework's existing env surface; runtime keys use an
|
|
65
|
+
* `ENPILINK_CFG_<KEY>` convention so an operator can pin (env-lock) any runtime
|
|
66
|
+
* value without colliding with the bootstrap vars.
|
|
67
|
+
*/
|
|
68
|
+
export declare const ENV_VARS: Record<ConfigKey, string>;
|
|
69
|
+
export declare function isSecretKey(key: string): boolean;
|
|
70
|
+
export declare function isRuntimeKey(key: string): key is RuntimeKey;
|
|
71
|
+
export declare function isBootstrapKey(key: string): key is BootstrapKey;
|
|
72
|
+
export declare function isKnownKey(key: string): key is ConfigKey;
|
|
73
|
+
/** Metadata for every known key. */
|
|
74
|
+
export declare function keyMeta(key: ConfigKey): KeyMeta;
|
|
75
|
+
/** All keys with metadata. */
|
|
76
|
+
export declare function allKeyMeta(): KeyMeta[];
|
|
77
|
+
/** The per-key zod schema (for validating a single runtime write). */
|
|
78
|
+
export declare function schemaForKey(key: ConfigKey): z.ZodTypeAny;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Config schema for enpilink's admin / control plane (M4).
|
|
4
|
+
*
|
|
5
|
+
* Settings are split into two tiers:
|
|
6
|
+
*
|
|
7
|
+
* - **Bootstrap** keys are env/file ONLY — they configure how the process
|
|
8
|
+
* starts (storage engine, port, whether the prod admin is enabled, the admin
|
|
9
|
+
* auth secret). They are NOT editable from the DB / admin UI. Some are
|
|
10
|
+
* **secret** (`adminAuthToken`) and are never persisted to the DB nor
|
|
11
|
+
* returned in plaintext by any API.
|
|
12
|
+
* - **Runtime** keys live in the DB and are editable from the Configuration
|
|
13
|
+
* admin page (analytics on/off + sample rate, retention, feature flags,
|
|
14
|
+
* display prefs). They still honour the env > file > db precedence so an
|
|
15
|
+
* operator can pin a value via env/file and lock it in the UI.
|
|
16
|
+
*
|
|
17
|
+
* Resolution precedence is **env > file > db > default** (see `resolve.ts`).
|
|
18
|
+
*/
|
|
19
|
+
/** A boolean parsed from an env string: `1`/`true`/`yes`/`on` (ci) → true. */
|
|
20
|
+
const TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
21
|
+
const FALSY = new Set(["0", "false", "no", "off"]);
|
|
22
|
+
/** Coerce an env string (or already-typed value) into a boolean. */
|
|
23
|
+
export function coerceBool(raw) {
|
|
24
|
+
if (typeof raw === "boolean") {
|
|
25
|
+
return raw;
|
|
26
|
+
}
|
|
27
|
+
if (typeof raw === "string") {
|
|
28
|
+
const v = raw.trim().toLowerCase();
|
|
29
|
+
if (TRUTHY.has(v)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (FALSY.has(v)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/** Coerce an env string (or number) into a finite number. */
|
|
39
|
+
export function coerceNumber(raw) {
|
|
40
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
44
|
+
const n = Number(raw);
|
|
45
|
+
if (Number.isFinite(n)) {
|
|
46
|
+
return n;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Bootstrap (env/file-only) settings. Not DB-editable. Defaults keep the
|
|
53
|
+
* framework off-by-default and secure-by-default.
|
|
54
|
+
*/
|
|
55
|
+
export const bootstrapSchema = z.object({
|
|
56
|
+
/** Storage engine name (`memory` | `sqlite` | custom). */
|
|
57
|
+
storage: z.string().default("memory"),
|
|
58
|
+
/** SQLite database path (only meaningful for the sqlite engine). */
|
|
59
|
+
dbPath: z.string().default("./enpilink.db"),
|
|
60
|
+
/** HTTP port the server listens on. */
|
|
61
|
+
port: z.number().int().positive().default(3000),
|
|
62
|
+
/** Whether the production admin plane is enabled (M5). Off by default. */
|
|
63
|
+
admin: z.boolean().default(false),
|
|
64
|
+
/**
|
|
65
|
+
* Bearer token guarding the prod admin plane (M5). SECRET — env-only, never
|
|
66
|
+
* persisted to the DB nor returned in plaintext.
|
|
67
|
+
*/
|
|
68
|
+
adminAuthToken: z.string().optional(),
|
|
69
|
+
});
|
|
70
|
+
/** Runtime (DB-editable) settings. */
|
|
71
|
+
export const runtimeSchema = z.object({
|
|
72
|
+
/** Whether analytics/event capture is enabled. */
|
|
73
|
+
"analytics.enabled": z.boolean().default(false),
|
|
74
|
+
/** Fraction of requests sampled `[0, 1]`. */
|
|
75
|
+
"analytics.sampleRate": z.number().min(0).max(1).default(1),
|
|
76
|
+
/** Max events retained (retention cap). */
|
|
77
|
+
"retention.events": z.number().int().nonnegative().default(5000),
|
|
78
|
+
/** Max logs retained (retention cap). */
|
|
79
|
+
"retention.logs": z.number().int().nonnegative().default(5000),
|
|
80
|
+
/** Feature flag: expose the live log stream in the dashboard. */
|
|
81
|
+
"flags.liveLogs": z.boolean().default(true),
|
|
82
|
+
/** Display preference: dashboard time-bucket width (ms). */
|
|
83
|
+
"display.bucketMs": z.number().int().positive().default(60_000),
|
|
84
|
+
});
|
|
85
|
+
export const configSchema = bootstrapSchema.merge(runtimeSchema);
|
|
86
|
+
/** Bootstrap keys (env/file only). */
|
|
87
|
+
export const BOOTSTRAP_KEYS = [
|
|
88
|
+
"storage",
|
|
89
|
+
"dbPath",
|
|
90
|
+
"port",
|
|
91
|
+
"admin",
|
|
92
|
+
"adminAuthToken",
|
|
93
|
+
];
|
|
94
|
+
/** Runtime keys (DB-editable). */
|
|
95
|
+
export const RUNTIME_KEYS = [
|
|
96
|
+
"analytics.enabled",
|
|
97
|
+
"analytics.sampleRate",
|
|
98
|
+
"retention.events",
|
|
99
|
+
"retention.logs",
|
|
100
|
+
"flags.liveLogs",
|
|
101
|
+
"display.bucketMs",
|
|
102
|
+
];
|
|
103
|
+
/** Secret keys: env-only, masked + never persisted/returned in plaintext. */
|
|
104
|
+
export const SECRET_KEYS = [
|
|
105
|
+
"adminAuthToken",
|
|
106
|
+
];
|
|
107
|
+
/**
|
|
108
|
+
* Env var mapping per key. Bootstrap keys map to dedicated env vars that match
|
|
109
|
+
* the framework's existing env surface; runtime keys use an
|
|
110
|
+
* `ENPILINK_CFG_<KEY>` convention so an operator can pin (env-lock) any runtime
|
|
111
|
+
* value without colliding with the bootstrap vars.
|
|
112
|
+
*/
|
|
113
|
+
export const ENV_VARS = {
|
|
114
|
+
storage: "ENPILINK_STORAGE",
|
|
115
|
+
dbPath: "ENPILINK_DB_PATH",
|
|
116
|
+
port: "PORT",
|
|
117
|
+
admin: "ENPILINK_ADMIN",
|
|
118
|
+
adminAuthToken: "ENPILINK_ADMIN_TOKEN",
|
|
119
|
+
"analytics.enabled": "ENPILINK_ANALYTICS",
|
|
120
|
+
"analytics.sampleRate": "ENPILINK_CFG_ANALYTICS_SAMPLE_RATE",
|
|
121
|
+
"retention.events": "ENPILINK_CFG_RETENTION_EVENTS",
|
|
122
|
+
"retention.logs": "ENPILINK_CFG_RETENTION_LOGS",
|
|
123
|
+
"flags.liveLogs": "ENPILINK_CFG_FLAGS_LIVE_LOGS",
|
|
124
|
+
"display.bucketMs": "ENPILINK_CFG_DISPLAY_BUCKET_MS",
|
|
125
|
+
};
|
|
126
|
+
const SECRET_SET = new Set(SECRET_KEYS);
|
|
127
|
+
const RUNTIME_SET = new Set(RUNTIME_KEYS);
|
|
128
|
+
const BOOTSTRAP_SET = new Set(BOOTSTRAP_KEYS);
|
|
129
|
+
export function isSecretKey(key) {
|
|
130
|
+
return SECRET_SET.has(key);
|
|
131
|
+
}
|
|
132
|
+
export function isRuntimeKey(key) {
|
|
133
|
+
return RUNTIME_SET.has(key);
|
|
134
|
+
}
|
|
135
|
+
export function isBootstrapKey(key) {
|
|
136
|
+
return BOOTSTRAP_SET.has(key);
|
|
137
|
+
}
|
|
138
|
+
export function isKnownKey(key) {
|
|
139
|
+
return RUNTIME_SET.has(key) || BOOTSTRAP_SET.has(key);
|
|
140
|
+
}
|
|
141
|
+
/** Metadata for every known key. */
|
|
142
|
+
export function keyMeta(key) {
|
|
143
|
+
return {
|
|
144
|
+
key,
|
|
145
|
+
tier: isBootstrapKey(key) ? "bootstrap" : "runtime",
|
|
146
|
+
secret: isSecretKey(key),
|
|
147
|
+
env: ENV_VARS[key],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** All keys with metadata. */
|
|
151
|
+
export function allKeyMeta() {
|
|
152
|
+
return [...BOOTSTRAP_KEYS, ...RUNTIME_KEYS].map((k) => keyMeta(k));
|
|
153
|
+
}
|
|
154
|
+
/** The per-key zod schema (for validating a single runtime write). */
|
|
155
|
+
export function schemaForKey(key) {
|
|
156
|
+
return (configSchema.shape[key] ?? z.unknown());
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/server/config/schema.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\n\n/**\n * Config schema for enpilink's admin / control plane (M4).\n *\n * Settings are split into two tiers:\n *\n * - **Bootstrap** keys are env/file ONLY — they configure how the process\n * starts (storage engine, port, whether the prod admin is enabled, the admin\n * auth secret). They are NOT editable from the DB / admin UI. Some are\n * **secret** (`adminAuthToken`) and are never persisted to the DB nor\n * returned in plaintext by any API.\n * - **Runtime** keys live in the DB and are editable from the Configuration\n * admin page (analytics on/off + sample rate, retention, feature flags,\n * display prefs). They still honour the env > file > db precedence so an\n * operator can pin a value via env/file and lock it in the UI.\n *\n * Resolution precedence is **env > file > db > default** (see `resolve.ts`).\n */\n\n/** A boolean parsed from an env string: `1`/`true`/`yes`/`on` (ci) → true. */\nconst TRUTHY = new Set([\"1\", \"true\", \"yes\", \"on\"]);\nconst FALSY = new Set([\"0\", \"false\", \"no\", \"off\"]);\n\n/** Coerce an env string (or already-typed value) into a boolean. */\nexport function coerceBool(raw: unknown): boolean | undefined {\n if (typeof raw === \"boolean\") {\n return raw;\n }\n if (typeof raw === \"string\") {\n const v = raw.trim().toLowerCase();\n if (TRUTHY.has(v)) {\n return true;\n }\n if (FALSY.has(v)) {\n return false;\n }\n }\n return undefined;\n}\n\n/** Coerce an env string (or number) into a finite number. */\nexport function coerceNumber(raw: unknown): number | undefined {\n if (typeof raw === \"number\" && Number.isFinite(raw)) {\n return raw;\n }\n if (typeof raw === \"string\" && raw.trim() !== \"\") {\n const n = Number(raw);\n if (Number.isFinite(n)) {\n return n;\n }\n }\n return undefined;\n}\n\n/**\n * Bootstrap (env/file-only) settings. Not DB-editable. Defaults keep the\n * framework off-by-default and secure-by-default.\n */\nexport const bootstrapSchema = z.object({\n /** Storage engine name (`memory` | `sqlite` | custom). */\n storage: z.string().default(\"memory\"),\n /** SQLite database path (only meaningful for the sqlite engine). */\n dbPath: z.string().default(\"./enpilink.db\"),\n /** HTTP port the server listens on. */\n port: z.number().int().positive().default(3000),\n /** Whether the production admin plane is enabled (M5). Off by default. */\n admin: z.boolean().default(false),\n /**\n * Bearer token guarding the prod admin plane (M5). SECRET — env-only, never\n * persisted to the DB nor returned in plaintext.\n */\n adminAuthToken: z.string().optional(),\n});\n\n/** Runtime (DB-editable) settings. */\nexport const runtimeSchema = z.object({\n /** Whether analytics/event capture is enabled. */\n \"analytics.enabled\": z.boolean().default(false),\n /** Fraction of requests sampled `[0, 1]`. */\n \"analytics.sampleRate\": z.number().min(0).max(1).default(1),\n /** Max events retained (retention cap). */\n \"retention.events\": z.number().int().nonnegative().default(5000),\n /** Max logs retained (retention cap). */\n \"retention.logs\": z.number().int().nonnegative().default(5000),\n /** Feature flag: expose the live log stream in the dashboard. */\n \"flags.liveLogs\": z.boolean().default(true),\n /** Display preference: dashboard time-bucket width (ms). */\n \"display.bucketMs\": z.number().int().positive().default(60_000),\n});\n\nexport const configSchema = bootstrapSchema.merge(runtimeSchema);\n\nexport type BootstrapConfig = z.infer<typeof bootstrapSchema>;\nexport type RuntimeConfig = z.infer<typeof runtimeSchema>;\nexport type Config = z.infer<typeof configSchema>;\n\n/** All known config keys. */\nexport type ConfigKey = keyof Config;\nexport type BootstrapKey = keyof BootstrapConfig;\nexport type RuntimeKey = keyof RuntimeConfig;\n\n/** Per-key metadata describing tier / secret / env-lock semantics. */\nexport interface KeyMeta {\n key: ConfigKey;\n /** `bootstrap` (env/file only) or `runtime` (DB-editable). */\n tier: \"bootstrap\" | \"runtime\";\n /** Secret keys are env-only and NEVER persisted/returned in plaintext. */\n secret: boolean;\n /** The env var that drives this key (for the \"set via env\" hint). */\n env: string;\n}\n\n/** Bootstrap keys (env/file only). */\nexport const BOOTSTRAP_KEYS = [\n \"storage\",\n \"dbPath\",\n \"port\",\n \"admin\",\n \"adminAuthToken\",\n] as const satisfies readonly BootstrapKey[];\n\n/** Runtime keys (DB-editable). */\nexport const RUNTIME_KEYS = [\n \"analytics.enabled\",\n \"analytics.sampleRate\",\n \"retention.events\",\n \"retention.logs\",\n \"flags.liveLogs\",\n \"display.bucketMs\",\n] as const satisfies readonly RuntimeKey[];\n\n/** Secret keys: env-only, masked + never persisted/returned in plaintext. */\nexport const SECRET_KEYS = [\n \"adminAuthToken\",\n] as const satisfies readonly ConfigKey[];\n\n/**\n * Env var mapping per key. Bootstrap keys map to dedicated env vars that match\n * the framework's existing env surface; runtime keys use an\n * `ENPILINK_CFG_<KEY>` convention so an operator can pin (env-lock) any runtime\n * value without colliding with the bootstrap vars.\n */\nexport const ENV_VARS: Record<ConfigKey, string> = {\n storage: \"ENPILINK_STORAGE\",\n dbPath: \"ENPILINK_DB_PATH\",\n port: \"PORT\",\n admin: \"ENPILINK_ADMIN\",\n adminAuthToken: \"ENPILINK_ADMIN_TOKEN\",\n \"analytics.enabled\": \"ENPILINK_ANALYTICS\",\n \"analytics.sampleRate\": \"ENPILINK_CFG_ANALYTICS_SAMPLE_RATE\",\n \"retention.events\": \"ENPILINK_CFG_RETENTION_EVENTS\",\n \"retention.logs\": \"ENPILINK_CFG_RETENTION_LOGS\",\n \"flags.liveLogs\": \"ENPILINK_CFG_FLAGS_LIVE_LOGS\",\n \"display.bucketMs\": \"ENPILINK_CFG_DISPLAY_BUCKET_MS\",\n};\n\nconst SECRET_SET = new Set<string>(SECRET_KEYS);\nconst RUNTIME_SET = new Set<string>(RUNTIME_KEYS);\nconst BOOTSTRAP_SET = new Set<string>(BOOTSTRAP_KEYS);\n\nexport function isSecretKey(key: string): boolean {\n return SECRET_SET.has(key);\n}\nexport function isRuntimeKey(key: string): key is RuntimeKey {\n return RUNTIME_SET.has(key);\n}\nexport function isBootstrapKey(key: string): key is BootstrapKey {\n return BOOTSTRAP_SET.has(key);\n}\nexport function isKnownKey(key: string): key is ConfigKey {\n return RUNTIME_SET.has(key) || BOOTSTRAP_SET.has(key);\n}\n\n/** Metadata for every known key. */\nexport function keyMeta(key: ConfigKey): KeyMeta {\n return {\n key,\n tier: isBootstrapKey(key) ? \"bootstrap\" : \"runtime\",\n secret: isSecretKey(key),\n env: ENV_VARS[key],\n };\n}\n\n/** All keys with metadata. */\nexport function allKeyMeta(): KeyMeta[] {\n return [...BOOTSTRAP_KEYS, ...RUNTIME_KEYS].map((k) => keyMeta(k));\n}\n\n/** The per-key zod schema (for validating a single runtime write). */\nexport function schemaForKey(key: ConfigKey): z.ZodTypeAny {\n return (\n (configSchema.shape as Record<string, z.ZodTypeAny>)[key] ?? z.unknown()\n );\n}\n"]}
|