noodleseed-cli 0.1.4
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 +176 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +625 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +52 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +77 -0
- package/dist/config.js.map +1 -0
- package/dist/control-plane.d.ts +33 -0
- package/dist/control-plane.d.ts.map +1 -0
- package/dist/control-plane.js +223 -0
- package/dist/control-plane.js.map +1 -0
- package/dist/deploy.d.ts +62 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +182 -0
- package/dist/deploy.js.map +1 -0
- package/dist/dev.d.ts +50 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +223 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/validate.d.ts +37 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +46 -0
- package/dist/validate.js.map +1 -0
- package/node_modules/@noodle-borg/auth/dist/index.d.ts +14 -0
- package/node_modules/@noodle-borg/auth/dist/index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/auth/dist/index.js +14 -0
- package/node_modules/@noodle-borg/auth/dist/index.js.map +1 -0
- package/node_modules/@noodle-borg/auth/dist/jwt-issuer.d.ts +21 -0
- package/node_modules/@noodle-borg/auth/dist/jwt-issuer.d.ts.map +1 -0
- package/node_modules/@noodle-borg/auth/dist/jwt-issuer.js +24 -0
- package/node_modules/@noodle-borg/auth/dist/jwt-issuer.js.map +1 -0
- package/node_modules/@noodle-borg/auth/dist/metadata.d.ts +27 -0
- package/node_modules/@noodle-borg/auth/dist/metadata.d.ts.map +1 -0
- package/node_modules/@noodle-borg/auth/dist/metadata.js +21 -0
- package/node_modules/@noodle-borg/auth/dist/metadata.js.map +1 -0
- package/node_modules/@noodle-borg/auth/dist/signer.d.ts +45 -0
- package/node_modules/@noodle-borg/auth/dist/signer.d.ts.map +1 -0
- package/node_modules/@noodle-borg/auth/dist/signer.js +47 -0
- package/node_modules/@noodle-borg/auth/dist/signer.js.map +1 -0
- package/node_modules/@noodle-borg/auth/dist/verify.d.ts +42 -0
- package/node_modules/@noodle-borg/auth/dist/verify.d.ts.map +1 -0
- package/node_modules/@noodle-borg/auth/dist/verify.js +48 -0
- package/node_modules/@noodle-borg/auth/dist/verify.js.map +1 -0
- package/node_modules/@noodle-borg/auth/package.json +27 -0
- package/node_modules/@noodle-borg/authoring/dist/index.d.ts +200 -0
- package/node_modules/@noodle-borg/authoring/dist/index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/authoring/dist/index.js +504 -0
- package/node_modules/@noodle-borg/authoring/dist/index.js.map +1 -0
- package/node_modules/@noodle-borg/authoring/package.json +29 -0
- package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +203 -0
- package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/artifact/types.js +20 -0
- package/node_modules/@noodle-borg/compiler/dist/artifact/types.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/in-memory.d.ts +13 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/in-memory.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/in-memory.js +19 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/in-memory.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/signature.d.ts +11 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/signature.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/signature.js +31 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/signature.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/types.d.ts +43 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/types.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/types.js +11 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/types.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/cli.d.ts +3 -0
- package/node_modules/@noodle-borg/compiler/dist/cli.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/cli.js +19 -0
- package/node_modules/@noodle-borg/compiler/dist/cli.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/compile.d.ts +50 -0
- package/node_modules/@noodle-borg/compiler/dist/compile.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/compile.js +719 -0
- package/node_modules/@noodle-borg/compiler/dist/compile.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +27 -0
- package/node_modules/@noodle-borg/compiler/dist/errors.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/errors.js +2 -0
- package/node_modules/@noodle-borg/compiler/dist/errors.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/index.d.ts +13 -0
- package/node_modules/@noodle-borg/compiler/dist/index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/index.js +11 -0
- package/node_modules/@noodle-borg/compiler/dist/index.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/expression.d.ts +136 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/expression.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/expression.js +552 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/expression.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/naming.d.ts +14 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/naming.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/naming.js +18 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/naming.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema-refs.d.ts +24 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema-refs.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema-refs.js +149 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema-refs.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +97 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +157 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/schema-export.d.ts +6 -0
- package/node_modules/@noodle-borg/compiler/dist/schema-export.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/schema-export.js +28 -0
- package/node_modules/@noodle-borg/compiler/dist/schema-export.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/suggest.d.ts +41 -0
- package/node_modules/@noodle-borg/compiler/dist/suggest.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/suggest.js +105 -0
- package/node_modules/@noodle-borg/compiler/dist/suggest.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/uri-template.d.ts +33 -0
- package/node_modules/@noodle-borg/compiler/dist/uri-template.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compiler/dist/uri-template.js +83 -0
- package/node_modules/@noodle-borg/compiler/dist/uri-template.js.map +1 -0
- package/node_modules/@noodle-borg/compiler/package.json +32 -0
- package/node_modules/@noodle-borg/compute/dist/code-connector.d.ts +45 -0
- package/node_modules/@noodle-borg/compute/dist/code-connector.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compute/dist/code-connector.js +53 -0
- package/node_modules/@noodle-borg/compute/dist/code-connector.js.map +1 -0
- package/node_modules/@noodle-borg/compute/dist/engine.d.ts +73 -0
- package/node_modules/@noodle-borg/compute/dist/engine.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compute/dist/engine.js +31 -0
- package/node_modules/@noodle-borg/compute/dist/engine.js.map +1 -0
- package/node_modules/@noodle-borg/compute/dist/index.d.ts +4 -0
- package/node_modules/@noodle-borg/compute/dist/index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compute/dist/index.js +4 -0
- package/node_modules/@noodle-borg/compute/dist/index.js.map +1 -0
- package/node_modules/@noodle-borg/compute/dist/quickjs-engine.d.ts +31 -0
- package/node_modules/@noodle-borg/compute/dist/quickjs-engine.d.ts.map +1 -0
- package/node_modules/@noodle-borg/compute/dist/quickjs-engine.js +271 -0
- package/node_modules/@noodle-borg/compute/dist/quickjs-engine.js.map +1 -0
- package/node_modules/@noodle-borg/compute/package.json +29 -0
- package/node_modules/@noodle-borg/connector-defs/dist/compile.d.ts +46 -0
- package/node_modules/@noodle-borg/connector-defs/dist/compile.d.ts.map +1 -0
- package/node_modules/@noodle-borg/connector-defs/dist/compile.js +289 -0
- package/node_modules/@noodle-borg/connector-defs/dist/compile.js.map +1 -0
- package/node_modules/@noodle-borg/connector-defs/dist/index.d.ts +3 -0
- package/node_modules/@noodle-borg/connector-defs/dist/index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/connector-defs/dist/index.js +3 -0
- package/node_modules/@noodle-borg/connector-defs/dist/index.js.map +1 -0
- package/node_modules/@noodle-borg/connector-defs/dist/schema.d.ts +332 -0
- package/node_modules/@noodle-borg/connector-defs/dist/schema.d.ts.map +1 -0
- package/node_modules/@noodle-borg/connector-defs/dist/schema.js +105 -0
- package/node_modules/@noodle-borg/connector-defs/dist/schema.js.map +1 -0
- package/node_modules/@noodle-borg/connector-defs/package.json +32 -0
- package/node_modules/@noodle-borg/connector-http/dist/http-connector.d.ts +86 -0
- package/node_modules/@noodle-borg/connector-http/dist/http-connector.d.ts.map +1 -0
- package/node_modules/@noodle-borg/connector-http/dist/http-connector.js +138 -0
- package/node_modules/@noodle-borg/connector-http/dist/http-connector.js.map +1 -0
- package/node_modules/@noodle-borg/connector-http/dist/index.d.ts +3 -0
- package/node_modules/@noodle-borg/connector-http/dist/index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/connector-http/dist/index.js +3 -0
- package/node_modules/@noodle-borg/connector-http/dist/index.js.map +1 -0
- package/node_modules/@noodle-borg/connector-http/dist/ssrf.d.ts +45 -0
- package/node_modules/@noodle-borg/connector-http/dist/ssrf.d.ts.map +1 -0
- package/node_modules/@noodle-borg/connector-http/dist/ssrf.js +57 -0
- package/node_modules/@noodle-borg/connector-http/dist/ssrf.js.map +1 -0
- package/node_modules/@noodle-borg/connector-http/package.json +30 -0
- package/node_modules/@noodle-borg/protocol/dist/adapter.d.ts +73 -0
- package/node_modules/@noodle-borg/protocol/dist/adapter.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/adapter.js +2 -0
- package/node_modules/@noodle-borg/protocol/dist/adapter.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/adapters/mcp-2025-11-25.d.ts +19 -0
- package/node_modules/@noodle-borg/protocol/dist/adapters/mcp-2025-11-25.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/adapters/mcp-2025-11-25.js +34 -0
- package/node_modules/@noodle-borg/protocol/dist/adapters/mcp-2025-11-25.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/index.d.ts +5 -0
- package/node_modules/@noodle-borg/protocol/dist/index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/index.js +5 -0
- package/node_modules/@noodle-borg/protocol/dist/index.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/jsonrpc.d.ts +19 -0
- package/node_modules/@noodle-borg/protocol/dist/jsonrpc.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/jsonrpc.js +14 -0
- package/node_modules/@noodle-borg/protocol/dist/jsonrpc.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/mapping.d.ts +133 -0
- package/node_modules/@noodle-borg/protocol/dist/mapping.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/mapping.js +181 -0
- package/node_modules/@noodle-borg/protocol/dist/mapping.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/negotiate.d.ts +13 -0
- package/node_modules/@noodle-borg/protocol/dist/negotiate.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/negotiate.js +18 -0
- package/node_modules/@noodle-borg/protocol/dist/negotiate.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/registry.d.ts +17 -0
- package/node_modules/@noodle-borg/protocol/dist/registry.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/registry.js +33 -0
- package/node_modules/@noodle-borg/protocol/dist/registry.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/sdk-server.d.ts +22 -0
- package/node_modules/@noodle-borg/protocol/dist/sdk-server.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/sdk-server.js +91 -0
- package/node_modules/@noodle-borg/protocol/dist/sdk-server.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/server.d.ts +38 -0
- package/node_modules/@noodle-borg/protocol/dist/server.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/server.js +89 -0
- package/node_modules/@noodle-borg/protocol/dist/server.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/stateless.d.ts +14 -0
- package/node_modules/@noodle-borg/protocol/dist/stateless.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/stateless.js +28 -0
- package/node_modules/@noodle-borg/protocol/dist/stateless.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/bootstrap.d.ts +24 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/bootstrap.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/bootstrap.js +165 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/bootstrap.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/ext-apps-bundle.d.ts +4 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/ext-apps-bundle.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/ext-apps-bundle.js +10 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/ext-apps-bundle.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/inject.d.ts +8 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/inject.d.ts.map +1 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/inject.js +36 -0
- package/node_modules/@noodle-borg/protocol/dist/widget/inject.js.map +1 -0
- package/node_modules/@noodle-borg/protocol/package.json +29 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/map.d.ts +29 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/map.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/map.js +38 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/map.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/secret-box.d.ts +103 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/secret-box.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/secret-box.js +118 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/secret-box.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/static.d.ts +12 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/static.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/static.js +15 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/static.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/types.d.ts +28 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/types.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/types.js +2 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/types.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/connector/in-memory.d.ts +29 -0
- package/node_modules/@noodle-borg/runtime/dist/connector/in-memory.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/connector/in-memory.js +37 -0
- package/node_modules/@noodle-borg/runtime/dist/connector/in-memory.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +41 -0
- package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/connector/types.js +2 -0
- package/node_modules/@noodle-borg/runtime/dist/connector/types.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/eval/evaluate.d.ts +39 -0
- package/node_modules/@noodle-borg/runtime/dist/eval/evaluate.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/eval/evaluate.js +117 -0
- package/node_modules/@noodle-borg/runtime/dist/eval/evaluate.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +47 -0
- package/node_modules/@noodle-borg/runtime/dist/execute.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/execute.js +297 -0
- package/node_modules/@noodle-borg/runtime/dist/execute.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/index.d.ts +12 -0
- package/node_modules/@noodle-borg/runtime/dist/index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/index.js +8 -0
- package/node_modules/@noodle-borg/runtime/dist/index.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/policy/allow-all.d.ts +10 -0
- package/node_modules/@noodle-borg/runtime/dist/policy/allow-all.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/policy/allow-all.js +13 -0
- package/node_modules/@noodle-borg/runtime/dist/policy/allow-all.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/policy/types.d.ts +25 -0
- package/node_modules/@noodle-borg/runtime/dist/policy/types.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/policy/types.js +2 -0
- package/node_modules/@noodle-borg/runtime/dist/policy/types.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/result.d.ts +19 -0
- package/node_modules/@noodle-borg/runtime/dist/result.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/result.js +2 -0
- package/node_modules/@noodle-borg/runtime/dist/result.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/package.json +27 -0
- package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.d.ts +48 -0
- package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.js +79 -0
- package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/index.d.ts +7 -0
- package/node_modules/@noodle-borg/service/dist/index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/index.js +7 -0
- package/node_modules/@noodle-borg/service/dist/index.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/main.d.ts +3 -0
- package/node_modules/@noodle-borg/service/dist/main.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/main.js +171 -0
- package/node_modules/@noodle-borg/service/dist/main.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/app.d.ts +14 -0
- package/node_modules/@noodle-borg/service/dist/oauth/app.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/app.js +48 -0
- package/node_modules/@noodle-borg/service/dist/oauth/app.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/consent.d.ts +18 -0
- package/node_modules/@noodle-borg/service/dist/oauth/consent.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/consent.js +55 -0
- package/node_modules/@noodle-borg/service/dist/oauth/consent.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/google.d.ts +31 -0
- package/node_modules/@noodle-borg/service/dist/oauth/google.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/google.js +39 -0
- package/node_modules/@noodle-borg/service/dist/oauth/google.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/paths.d.ts +3 -0
- package/node_modules/@noodle-borg/service/dist/oauth/paths.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/paths.js +19 -0
- package/node_modules/@noodle-borg/service/dist/oauth/paths.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/provider.d.ts +61 -0
- package/node_modules/@noodle-borg/service/dist/oauth/provider.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/provider.js +313 -0
- package/node_modules/@noodle-borg/service/dist/oauth/provider.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/store-postgres.d.ts +29 -0
- package/node_modules/@noodle-borg/service/dist/oauth/store-postgres.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/store-postgres.js +176 -0
- package/node_modules/@noodle-borg/service/dist/oauth/store-postgres.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/store.d.ts +85 -0
- package/node_modules/@noodle-borg/service/dist/oauth/store.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/store.js +57 -0
- package/node_modules/@noodle-borg/service/dist/oauth/store.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/tokens.d.ts +8 -0
- package/node_modules/@noodle-borg/service/dist/oauth/tokens.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/oauth/tokens.js +13 -0
- package/node_modules/@noodle-borg/service/dist/oauth/tokens.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/secret/kms-master-key.d.ts +36 -0
- package/node_modules/@noodle-borg/service/dist/secret/kms-master-key.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/secret/kms-master-key.js +51 -0
- package/node_modules/@noodle-borg/service/dist/secret/kms-master-key.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/service.d.ts +221 -0
- package/node_modules/@noodle-borg/service/dist/service.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/service.js +1163 -0
- package/node_modules/@noodle-borg/service/dist/service.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/store/cloudsql-pool.d.ts +34 -0
- package/node_modules/@noodle-borg/service/dist/store/cloudsql-pool.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/store/cloudsql-pool.js +38 -0
- package/node_modules/@noodle-borg/service/dist/store/cloudsql-pool.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/store/postgres.d.ts +56 -0
- package/node_modules/@noodle-borg/service/dist/store/postgres.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/store/postgres.js +372 -0
- package/node_modules/@noodle-borg/service/dist/store/postgres.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/store.d.ts +192 -0
- package/node_modules/@noodle-borg/service/dist/store.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/store.js +230 -0
- package/node_modules/@noodle-borg/service/dist/store.js.map +1 -0
- package/node_modules/@noodle-borg/service/package.json +44 -0
- package/node_modules/@noodle-borg/transport-http/dist/caller-auth.d.ts +15 -0
- package/node_modules/@noodle-borg/transport-http/dist/caller-auth.d.ts.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/caller-auth.js +38 -0
- package/node_modules/@noodle-borg/transport-http/dist/caller-auth.js.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/examples/serve-demo.d.ts +2 -0
- package/node_modules/@noodle-borg/transport-http/dist/examples/serve-demo.d.ts.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/examples/serve-demo.js +129 -0
- package/node_modules/@noodle-borg/transport-http/dist/examples/serve-demo.js.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/front-door.d.ts +46 -0
- package/node_modules/@noodle-borg/transport-http/dist/front-door.d.ts.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/front-door.js +75 -0
- package/node_modules/@noodle-borg/transport-http/dist/front-door.js.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/handler.d.ts +142 -0
- package/node_modules/@noodle-borg/transport-http/dist/handler.d.ts.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/handler.js +387 -0
- package/node_modules/@noodle-borg/transport-http/dist/handler.js.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/index.d.ts +6 -0
- package/node_modules/@noodle-borg/transport-http/dist/index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/index.js +6 -0
- package/node_modules/@noodle-borg/transport-http/dist/index.js.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/logging.d.ts +41 -0
- package/node_modules/@noodle-borg/transport-http/dist/logging.d.ts.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/logging.js +71 -0
- package/node_modules/@noodle-borg/transport-http/dist/logging.js.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/serve.d.ts +22 -0
- package/node_modules/@noodle-borg/transport-http/dist/serve.d.ts.map +1 -0
- package/node_modules/@noodle-borg/transport-http/dist/serve.js +25 -0
- package/node_modules/@noodle-borg/transport-http/dist/serve.js.map +1 -0
- package/node_modules/@noodle-borg/transport-http/package.json +30 -0
- package/package.json +78 -0
|
@@ -0,0 +1,1163 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { createJwtVerifier, protectedResourceMetadata, } from '@noodle-borg/auth';
|
|
4
|
+
import { compile, InMemoryCatalog } from '@noodle-borg/compiler';
|
|
5
|
+
import { compileConnectors } from '@noodle-borg/connector-defs';
|
|
6
|
+
import { InMemoryConnectorRegistry, MapServiceBroker, SecretBox, staticMasterKeyProvider, } from '@noodle-borg/runtime';
|
|
7
|
+
import { applySecurityHeaders, createMcpRouter, effectiveProto, enforceHttps, mintCallerKey, noopLogger, verifyCallerKey, } from '@noodle-borg/transport-http';
|
|
8
|
+
import { allowAllGate, GoogleControlPlaneGate, } from './auth/deploy-gate.js';
|
|
9
|
+
import { isAuthServerPath } from './oauth/paths.js';
|
|
10
|
+
import { InMemoryControlPlaneStore, JsonFileArtifactStore, validateOrgRole, validateSlug, validateTenantRef, } from './store.js';
|
|
11
|
+
const DEFAULT_MAX_BODY = 1 << 20;
|
|
12
|
+
/** Well-known prefix for OAuth 2.0 Protected Resource Metadata (RFC 9728); the resource path follows it. */
|
|
13
|
+
const PRM_PREFIX = '/.well-known/oauth-protected-resource';
|
|
14
|
+
/**
|
|
15
|
+
* Holds deployed servers in memory and serves them by id. Each deploy compiles an optional declarative
|
|
16
|
+
* connector catalog and the manifest (against that catalog) to a resolved artifact, then registers the
|
|
17
|
+
* resolved {@link ServedArtifact} (artifact + execution deps). The wire is stateless: the transport
|
|
18
|
+
* builds a fresh SDK server per request.
|
|
19
|
+
*
|
|
20
|
+
* With an {@link ArtifactStore} (Slice 25, ADR 0027), each deploy is also persisted as its **inputs**
|
|
21
|
+
* (the compiled artifact's deps are live closures and are not serializable) so deploys survive a process
|
|
22
|
+
* bounce. Recovery is **lazy by default** (ADR 0036): {@link get} recompiles a server from the store on its
|
|
23
|
+
* first request (a cache miss), which makes any instance able to serve any deploy on a multi-instance
|
|
24
|
+
* platform. {@link recover} eagerly recompiles **all** persisted servers and is used only for the opt-in
|
|
25
|
+
* warm-all path (`serveService({ warmAll })`). Without a store, behaviour is exactly as before (in-memory
|
|
26
|
+
* only; deploys are lost on restart).
|
|
27
|
+
*/
|
|
28
|
+
export class ServerRegistry {
|
|
29
|
+
#servers = new Map();
|
|
30
|
+
#records = new Map();
|
|
31
|
+
#activeTenants = new Map();
|
|
32
|
+
#store;
|
|
33
|
+
#secretBox;
|
|
34
|
+
/** Single-flight: concurrent first-hits for an uncached id share one in-flight lazy compile (ADR 0036). */
|
|
35
|
+
#inflight = new Map();
|
|
36
|
+
/**
|
|
37
|
+
* @param store optional durable store; when set, deploys are persisted and {@link recover} replays them.
|
|
38
|
+
* @param secretBox encrypts persisted secrets at rest (ADR 0028). **A file-backed `store` must be paired
|
|
39
|
+
* with a `secretBox`** — without one, persisted secrets fall back to the interim cleartext `{enc:'none'}`
|
|
40
|
+
* envelope. `serveService` enforces this (a `dataDir` fails closed without a master key); a caller that
|
|
41
|
+
* constructs this directly is responsible for the same pairing.
|
|
42
|
+
*/
|
|
43
|
+
constructor(store, secretBox) {
|
|
44
|
+
this.#store = store;
|
|
45
|
+
this.#secretBox = secretBox;
|
|
46
|
+
}
|
|
47
|
+
async deploy(tenant, manifest, connectors, secrets, actor, accessMode = 'caller-key',
|
|
48
|
+
/**
|
|
49
|
+
* When true and a prior active deployment exists for this tenant, reuse its caller-key **hash** instead
|
|
50
|
+
* of minting a new key (the plaintext is unrecoverable, so none is returned). Used by `mrdn dev`
|
|
51
|
+
* to keep the local key stable across hot-reloads; the HTTP deploy route never sets it, so production
|
|
52
|
+
* redeploys keep rotating the key.
|
|
53
|
+
*/
|
|
54
|
+
reuseCallerKey = false) {
|
|
55
|
+
const safeTenant = validateTenantRef(tenant);
|
|
56
|
+
// Identity-based access modes gate the data plane on verified identity, so the deploy must have an
|
|
57
|
+
// authenticated actor to establish ownership/audit provenance and avoid unauthored protected servers.
|
|
58
|
+
if (isIdentityAccessMode(accessMode) && actor === undefined) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
errors: [
|
|
62
|
+
{
|
|
63
|
+
code: 'identity_access_requires_identity',
|
|
64
|
+
path: 'accessMode',
|
|
65
|
+
message: `${accessMode} deployments require an authenticated deployer`,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const built = this.#compileTarget(manifest, connectors, secrets ?? {});
|
|
71
|
+
if (!built.ok)
|
|
72
|
+
return { ok: false, errors: built.errors };
|
|
73
|
+
const deploymentId = mintDeploymentId(built.served.artifact.server.name);
|
|
74
|
+
// `caller-key`: mint a per-server caller key, store only its hash, return the key once — unless
|
|
75
|
+
// `reuseCallerKey` is set and a prior active deployment exists, in which case keep its hash (no plaintext
|
|
76
|
+
// to return). Identity modes: no shared key at all — callers authenticate with verified identity.
|
|
77
|
+
const reusedHash = accessMode === 'caller-key' && reuseCallerKey
|
|
78
|
+
? await this.#activeCallerKeyHash(safeTenant)
|
|
79
|
+
: undefined;
|
|
80
|
+
const callerKey = accessMode === 'caller-key' && reusedHash === undefined ? mintCallerKey() : undefined;
|
|
81
|
+
const callerKeyHash = callerKey?.hash ?? reusedHash;
|
|
82
|
+
const version = Date.now();
|
|
83
|
+
const ownerSubject = accessMode === 'owner-only' ? actor?.subject : undefined;
|
|
84
|
+
// Persist before registering so a write failure fails the deploy closed (nothing is served that would
|
|
85
|
+
// silently vanish on restart). The plaintext caller key is never persisted — only its hash (ADR 0030).
|
|
86
|
+
if (this.#store) {
|
|
87
|
+
const record = {
|
|
88
|
+
schemaVersion: 1,
|
|
89
|
+
deploymentId,
|
|
90
|
+
orgSlug: safeTenant.org,
|
|
91
|
+
appSlug: safeTenant.app,
|
|
92
|
+
environment: safeTenant.env,
|
|
93
|
+
deploymentVersion: version,
|
|
94
|
+
active: true,
|
|
95
|
+
serverName: built.served.artifact.server.name,
|
|
96
|
+
createdAt: new Date().toISOString(),
|
|
97
|
+
...(actor ? { createdBySubject: actor.subject, createdByEmail: actor.email } : {}),
|
|
98
|
+
accessMode,
|
|
99
|
+
...(callerKeyHash ? { callerKeyHash } : {}),
|
|
100
|
+
manifest,
|
|
101
|
+
...(connectors !== undefined ? { connectors } : {}),
|
|
102
|
+
secrets: await sealSecrets(secrets ?? {}, this.#secretBox),
|
|
103
|
+
};
|
|
104
|
+
await this.#store.append(record);
|
|
105
|
+
this.#records.set(record.deploymentId, record);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
for (const [id, existing] of this.#records) {
|
|
109
|
+
if (existing.active &&
|
|
110
|
+
existing.orgSlug === safeTenant.org &&
|
|
111
|
+
existing.appSlug === safeTenant.app &&
|
|
112
|
+
existing.environment === safeTenant.env) {
|
|
113
|
+
this.#records.set(id, { ...existing, active: false });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this.#records.set(deploymentId, {
|
|
117
|
+
schemaVersion: 1,
|
|
118
|
+
deploymentId,
|
|
119
|
+
orgSlug: safeTenant.org,
|
|
120
|
+
appSlug: safeTenant.app,
|
|
121
|
+
environment: safeTenant.env,
|
|
122
|
+
deploymentVersion: version,
|
|
123
|
+
active: true,
|
|
124
|
+
serverName: built.served.artifact.server.name,
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
...(actor ? { createdBySubject: actor.subject, createdByEmail: actor.email } : {}),
|
|
127
|
+
accessMode,
|
|
128
|
+
...(callerKeyHash ? { callerKeyHash } : {}),
|
|
129
|
+
manifest,
|
|
130
|
+
...(connectors !== undefined ? { connectors } : {}),
|
|
131
|
+
secrets: { enc: 'none', values: secrets ?? {} },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
this.#servers.set(deploymentId, {
|
|
135
|
+
served: built.served,
|
|
136
|
+
accessMode,
|
|
137
|
+
org: safeTenant.org,
|
|
138
|
+
...(callerKeyHash ? { callerKeyHash } : {}),
|
|
139
|
+
...(ownerSubject !== undefined ? { ownerSubject } : {}),
|
|
140
|
+
});
|
|
141
|
+
this.#activeTenants.set(tenantKey(safeTenant), deploymentId);
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
deploymentId,
|
|
145
|
+
accessMode,
|
|
146
|
+
...(callerKey ? { callerKey: callerKey.key } : {}),
|
|
147
|
+
...(reusedHash !== undefined ? { callerKeyReused: true } : {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The caller-key **hash** of the tenant's current active deployment, or `undefined` if none. Checks the
|
|
152
|
+
* in-memory registry first (the `dev` hot-reload path), then the durable store. Used only by the
|
|
153
|
+
* `reuseCallerKey` deploy path to keep a key stable across redeploys without ever handling its plaintext.
|
|
154
|
+
*/
|
|
155
|
+
async #activeCallerKeyHash(tenant) {
|
|
156
|
+
const cachedId = this.#activeTenants.get(tenantKey(tenant));
|
|
157
|
+
if (cachedId !== undefined) {
|
|
158
|
+
const hash = this.#servers.get(cachedId)?.callerKeyHash;
|
|
159
|
+
if (hash !== undefined)
|
|
160
|
+
return hash;
|
|
161
|
+
}
|
|
162
|
+
if (this.#store) {
|
|
163
|
+
const record = await this.#store.getActiveByTenant(tenant);
|
|
164
|
+
return record?.callerKeyHash;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Rebuild every persisted server into memory on startup. Each record is replayed through the same
|
|
170
|
+
* compile path as a fresh deploy, reusing its stored id and caller-key hash. A record that no longer
|
|
171
|
+
* compiles (e.g. source/catalog drift) is skipped and reported — the remaining servers still recover.
|
|
172
|
+
*/
|
|
173
|
+
async recover() {
|
|
174
|
+
if (!this.#store)
|
|
175
|
+
return { recovered: 0, failed: [] };
|
|
176
|
+
const records = await this.#store.loadAll();
|
|
177
|
+
const failed = [];
|
|
178
|
+
let recovered = 0;
|
|
179
|
+
for (const record of records) {
|
|
180
|
+
// Decrypt the persisted secrets first; a wrong/missing master key (or tampered ciphertext) must be
|
|
181
|
+
// fail-soft (skip + report this server), not crash the whole recovery. The error message carries no
|
|
182
|
+
// secret material (SecretDecryptError is deliberately opaque).
|
|
183
|
+
let secrets;
|
|
184
|
+
try {
|
|
185
|
+
secrets = await secretsOf(record.secrets, this.#secretBox);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
failed.push({
|
|
189
|
+
deploymentId: record.deploymentId,
|
|
190
|
+
errors: [
|
|
191
|
+
{ code: 'secret_decrypt_failed', path: 'secrets', message: error.message },
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const built = this.#compileTarget(record.manifest, record.connectors, secrets);
|
|
197
|
+
if (!built.ok) {
|
|
198
|
+
failed.push({ deploymentId: record.deploymentId, errors: built.errors });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
this.#servers.set(record.deploymentId, servedTargetFor(record, built.served));
|
|
202
|
+
this.#records.set(record.deploymentId, record);
|
|
203
|
+
if (record.active) {
|
|
204
|
+
this.#activeTenants.set(tenantKey({ org: record.orgSlug, app: record.appSlug, env: record.environment }), record.deploymentId);
|
|
205
|
+
}
|
|
206
|
+
recovered++;
|
|
207
|
+
}
|
|
208
|
+
return { recovered, failed };
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Compile inputs into a served artifact (artifact + execution deps) or deploy errors. Pure: no id
|
|
212
|
+
* minting, no `#servers` mutation, no persistence — the shared path for {@link deploy} and
|
|
213
|
+
* {@link recover}. Fails closed when a referenced secret has no value; the error names only the
|
|
214
|
+
* reference, never a value.
|
|
215
|
+
*/
|
|
216
|
+
#compileTarget(manifest, connectors, secrets) {
|
|
217
|
+
let catalogConnectors = [];
|
|
218
|
+
let httpConnectors = [];
|
|
219
|
+
let secretBindings = [];
|
|
220
|
+
if (connectors !== undefined && connectors.trim() !== '') {
|
|
221
|
+
const cc = compileConnectors(connectors);
|
|
222
|
+
if (!cc.ok)
|
|
223
|
+
return { ok: false, errors: cc.errors };
|
|
224
|
+
catalogConnectors = cc.catalog;
|
|
225
|
+
httpConnectors = cc.connectors;
|
|
226
|
+
secretBindings = cc.secretBindings;
|
|
227
|
+
}
|
|
228
|
+
const broker = buildBroker(secretBindings, secrets);
|
|
229
|
+
if (!broker.ok)
|
|
230
|
+
return { ok: false, errors: broker.errors };
|
|
231
|
+
const compiled = compile(manifest, { catalog: new InMemoryCatalog(catalogConnectors) });
|
|
232
|
+
if (!compiled.ok)
|
|
233
|
+
return { ok: false, errors: compiled.errors };
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
served: {
|
|
237
|
+
artifact: compiled.artifact,
|
|
238
|
+
deps: {
|
|
239
|
+
connectors: new InMemoryConnectorRegistry(httpConnectors),
|
|
240
|
+
broker: broker.broker,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Resolve a served server by id. A cache hit returns immediately. On a cache miss **with a durable
|
|
247
|
+
* store**, the record is point-read ({@link ArtifactStore.get}), recompiled through the shared
|
|
248
|
+
* {@link #compileTarget} path, cached, and returned — so **any instance can serve any deploy**, including
|
|
249
|
+
* one it never handled (multi-instance correctness, ADR 0036). A miss with no record resolves to
|
|
250
|
+
* `undefined` (→ `404`). A record that no longer compiles, or whose secret will not decrypt, **throws**
|
|
251
|
+
* (→ `500`) — failing only its own request, never the instance. Concurrent first-hits for the same id
|
|
252
|
+
* share one in-flight compile (single-flight).
|
|
253
|
+
*/
|
|
254
|
+
async get(deploymentId) {
|
|
255
|
+
const cached = this.#servers.get(deploymentId);
|
|
256
|
+
if (cached)
|
|
257
|
+
return cached;
|
|
258
|
+
if (!this.#store)
|
|
259
|
+
return undefined;
|
|
260
|
+
const inflight = this.#inflight.get(deploymentId);
|
|
261
|
+
if (inflight)
|
|
262
|
+
return inflight;
|
|
263
|
+
const promise = this.#loadAndCompile(this.#store, deploymentId).finally(() => {
|
|
264
|
+
this.#inflight.delete(deploymentId);
|
|
265
|
+
});
|
|
266
|
+
this.#inflight.set(deploymentId, promise);
|
|
267
|
+
return promise;
|
|
268
|
+
}
|
|
269
|
+
async getActiveByTenant(ref) {
|
|
270
|
+
const safe = validateTenantRef(ref);
|
|
271
|
+
const cachedId = this.#activeTenants.get(tenantKey(safe));
|
|
272
|
+
if (cachedId !== undefined)
|
|
273
|
+
return this.get(cachedId);
|
|
274
|
+
if (!this.#store)
|
|
275
|
+
return undefined;
|
|
276
|
+
const record = await this.#store.getActiveByTenant(safe);
|
|
277
|
+
return record ? this.get(record.deploymentId) : undefined;
|
|
278
|
+
}
|
|
279
|
+
async listDeployments(filter) {
|
|
280
|
+
const org = validateSlug('org', filter.org);
|
|
281
|
+
const app = filter.app !== undefined ? validateSlug('app', filter.app) : undefined;
|
|
282
|
+
const env = filter.env !== undefined ? validateSlug('env', filter.env) : undefined;
|
|
283
|
+
if (this.#store) {
|
|
284
|
+
return this.#store.listDeployments({
|
|
285
|
+
org,
|
|
286
|
+
...(app !== undefined ? { app } : {}),
|
|
287
|
+
...(env !== undefined ? { env } : {}),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return [...this.#records.values()]
|
|
291
|
+
.filter((record) => record.orgSlug === org &&
|
|
292
|
+
(app === undefined || record.appSlug === app) &&
|
|
293
|
+
(env === undefined || record.environment === env))
|
|
294
|
+
.sort((a, b) => b.deploymentVersion - a.deploymentVersion)
|
|
295
|
+
.map(recordSummary);
|
|
296
|
+
}
|
|
297
|
+
async rotateCallerKey(tenant) {
|
|
298
|
+
const safe = validateTenantRef(tenant);
|
|
299
|
+
const active = this.#store
|
|
300
|
+
? await this.#store.getActiveByTenant(safe)
|
|
301
|
+
: [...this.#records.values()]
|
|
302
|
+
.filter((record) => record.active &&
|
|
303
|
+
record.orgSlug === safe.org &&
|
|
304
|
+
record.appSlug === safe.app &&
|
|
305
|
+
record.environment === safe.env)
|
|
306
|
+
.sort((a, b) => b.deploymentVersion - a.deploymentVersion)[0];
|
|
307
|
+
if (!active || (active.accessMode ?? 'caller-key') !== 'caller-key')
|
|
308
|
+
return undefined;
|
|
309
|
+
const minted = mintCallerKey();
|
|
310
|
+
const updated = this.#store
|
|
311
|
+
? await this.#store.updateActiveCallerKey(safe, minted.hash)
|
|
312
|
+
: { ...active, callerKeyHash: minted.hash };
|
|
313
|
+
if (!updated)
|
|
314
|
+
return undefined;
|
|
315
|
+
this.#records.set(updated.deploymentId, updated);
|
|
316
|
+
const cached = this.#servers.get(updated.deploymentId);
|
|
317
|
+
if (cached) {
|
|
318
|
+
this.#servers.set(updated.deploymentId, { ...cached, callerKeyHash: minted.hash });
|
|
319
|
+
}
|
|
320
|
+
return { key: minted.key, deployment: recordSummary(updated) };
|
|
321
|
+
}
|
|
322
|
+
async verifyActiveCallerKey(tenant, token) {
|
|
323
|
+
const safe = validateTenantRef(tenant);
|
|
324
|
+
const active = this.#store
|
|
325
|
+
? await this.#store.getActiveByTenant(safe)
|
|
326
|
+
: [...this.#records.values()]
|
|
327
|
+
.filter((record) => record.active &&
|
|
328
|
+
record.orgSlug === safe.org &&
|
|
329
|
+
record.appSlug === safe.app &&
|
|
330
|
+
record.environment === safe.env)
|
|
331
|
+
.sort((a, b) => b.deploymentVersion - a.deploymentVersion)[0];
|
|
332
|
+
const hash = active?.callerKeyHash;
|
|
333
|
+
return hash !== undefined && verifyCallerKey(token, hash);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The lazy half of {@link get}: point-read one persisted record and recompile it on a cache miss. `store`
|
|
337
|
+
* is passed in (resolved by the caller) so this stays free of `#store`-narrowing. No record → `undefined`
|
|
338
|
+
* (a `404`); a decrypt or compile failure throws (a `500`) rather than masquerading as "not found".
|
|
339
|
+
*/
|
|
340
|
+
async #loadAndCompile(store, deploymentId) {
|
|
341
|
+
const record = await store.get(deploymentId);
|
|
342
|
+
if (!record)
|
|
343
|
+
return undefined;
|
|
344
|
+
// A wrong/missing master key (or tampered ciphertext) throws here and propagates: the record exists but
|
|
345
|
+
// cannot be materialised (possibly transiently, e.g. KMS unavailable), so it is a `500`, not a `404`.
|
|
346
|
+
// `SecretDecryptError` is deliberately opaque — no secret material reaches the (swallowed) 500 body.
|
|
347
|
+
const secrets = await secretsOf(record.secrets, this.#secretBox);
|
|
348
|
+
const built = this.#compileTarget(record.manifest, record.connectors, secrets);
|
|
349
|
+
if (!built.ok) {
|
|
350
|
+
// The inputs compiled at deploy time; failing now is source/catalog drift — a server error. Codes
|
|
351
|
+
// only (never a secret name or value) in case this is ever logged; the client gets a generic 500.
|
|
352
|
+
throw new Error(`deployment ${deploymentId} failed to recompile (${built.errors.map((e) => e.code).join(',')})`);
|
|
353
|
+
}
|
|
354
|
+
const target = servedTargetFor(record, built.served);
|
|
355
|
+
this.#servers.set(deploymentId, target);
|
|
356
|
+
this.#records.set(deploymentId, record);
|
|
357
|
+
return target;
|
|
358
|
+
}
|
|
359
|
+
get size() {
|
|
360
|
+
return this.#servers.size;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Build the front-door {@link ServedTarget} for a persisted record: its access mode plus the credential the
|
|
365
|
+
* front-door checks — a caller-key hash (`caller-key`), the owner subject (`owner-only`), or the tenant org
|
|
366
|
+
* (`org-members`). A legacy record with no `accessMode` reads as `caller-key`.
|
|
367
|
+
*/
|
|
368
|
+
function servedTargetFor(record, served) {
|
|
369
|
+
const accessMode = record.accessMode ?? 'caller-key';
|
|
370
|
+
return {
|
|
371
|
+
served,
|
|
372
|
+
accessMode,
|
|
373
|
+
org: record.orgSlug,
|
|
374
|
+
...(record.callerKeyHash !== undefined ? { callerKeyHash: record.callerKeyHash } : {}),
|
|
375
|
+
...(accessMode === 'owner-only' && record.createdBySubject !== undefined
|
|
376
|
+
? { ownerSubject: record.createdBySubject }
|
|
377
|
+
: {}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function recordSummary(record) {
|
|
381
|
+
return {
|
|
382
|
+
deploymentId: record.deploymentId,
|
|
383
|
+
orgSlug: record.orgSlug,
|
|
384
|
+
appSlug: record.appSlug,
|
|
385
|
+
environment: record.environment,
|
|
386
|
+
active: record.active,
|
|
387
|
+
serverName: record.serverName,
|
|
388
|
+
createdAt: record.createdAt,
|
|
389
|
+
...(record.createdByEmail !== undefined ? { createdByEmail: record.createdByEmail } : {}),
|
|
390
|
+
accessMode: record.accessMode ?? 'caller-key',
|
|
391
|
+
hasCallerKey: record.callerKeyHash !== undefined,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Encrypt deploy-supplied secret values into the at-rest envelope. With a {@link SecretBox} the whole
|
|
396
|
+
* map is AES-256-GCM-sealed (`aes-256-gcm`); without one — non-persistent / in-memory paths only — it
|
|
397
|
+
* falls back to the interim plaintext form (`none`). The live in-memory broker still holds plaintext;
|
|
398
|
+
* only the persisted projection is sealed.
|
|
399
|
+
*/
|
|
400
|
+
async function sealSecrets(values, box) {
|
|
401
|
+
if (!box)
|
|
402
|
+
return { enc: 'none', values: { ...values } };
|
|
403
|
+
return { enc: 'aes-256-gcm', sealed: await box.seal(JSON.stringify(values)) };
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Recover plaintext secret values from a persisted envelope for replay. A `none` envelope is read
|
|
407
|
+
* directly (non-persistent/in-memory); an `aes-256-gcm` envelope is decrypted with the {@link SecretBox} —
|
|
408
|
+
* fail-closed if no box (no master key) is available, and a `SecretDecryptError` on a wrong key or
|
|
409
|
+
* tampered ciphertext.
|
|
410
|
+
*/
|
|
411
|
+
async function secretsOf(envelope, box) {
|
|
412
|
+
if (envelope.enc === 'none')
|
|
413
|
+
return { ...envelope.values };
|
|
414
|
+
if (!box) {
|
|
415
|
+
throw new Error('cannot recover an encrypted secret envelope without a secret master key');
|
|
416
|
+
}
|
|
417
|
+
return JSON.parse(await box.open(envelope.sealed));
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Resolve declared secret references against the deploy-supplied values into a per-server
|
|
421
|
+
* {@link MapServiceBroker}. Each binding keyed by `(connectorId, operation?)` gets a `{token}` credential;
|
|
422
|
+
* operations with no declared auth fall back to the broker's empty-token default (preserving public-API
|
|
423
|
+
* behavior). A reference with no supplied value fails closed — the server is not registered — and the
|
|
424
|
+
* error names only the reference (never a value), keeping secrets out of error bodies and logs.
|
|
425
|
+
*/
|
|
426
|
+
function buildBroker(bindings, secrets) {
|
|
427
|
+
const entries = new Map();
|
|
428
|
+
const errors = [];
|
|
429
|
+
for (const binding of bindings) {
|
|
430
|
+
const value = secrets[binding.secretRef];
|
|
431
|
+
if (value === undefined) {
|
|
432
|
+
errors.push({
|
|
433
|
+
code: 'missing_secret',
|
|
434
|
+
path: `secrets.${binding.secretRef}`,
|
|
435
|
+
message: `no value supplied for required secret "${binding.secretRef}"`,
|
|
436
|
+
});
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
entries.set(MapServiceBroker.key(binding.connectorId, binding.operation), { token: value });
|
|
440
|
+
}
|
|
441
|
+
if (errors.length > 0)
|
|
442
|
+
return { ok: false, errors };
|
|
443
|
+
return { ok: true, broker: new MapServiceBroker(entries) };
|
|
444
|
+
}
|
|
445
|
+
function tenantKey(ref) {
|
|
446
|
+
return `${ref.org}/${ref.app}/${ref.env}`;
|
|
447
|
+
}
|
|
448
|
+
/** Build a URL-safe, human-readable id from the manifest's server name plus a short random suffix. */
|
|
449
|
+
function mintDeploymentId(name) {
|
|
450
|
+
const slug = name
|
|
451
|
+
.toLowerCase()
|
|
452
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
453
|
+
.replace(/^-+|-+$/g, '') || 'server';
|
|
454
|
+
return `${slug}-${randomUUID().slice(0, 8)}`;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* The combined service listener: tenant-scoped deploy management plus the multi-tenant MCP router.
|
|
458
|
+
*/
|
|
459
|
+
export function createServiceHandler(registry, options = {}) {
|
|
460
|
+
const logger = options.logger ?? noopLogger;
|
|
461
|
+
const tls = options.tls ?? {};
|
|
462
|
+
const controlPlane = options.controlPlaneStore ?? new InMemoryControlPlaneStore();
|
|
463
|
+
const router = createMcpRouter(() => Promise.resolve(undefined), {
|
|
464
|
+
logger,
|
|
465
|
+
tls,
|
|
466
|
+
...(options.verifyOwnerToken ? { verifyOwnerToken: options.verifyOwnerToken } : {}),
|
|
467
|
+
authorizeDataPlaneIdentity: (input) => {
|
|
468
|
+
if (input.accessMode !== 'org-members')
|
|
469
|
+
return Promise.resolve(false);
|
|
470
|
+
return controlPlane.isOrgMember({ org: input.org, subject: input.subject });
|
|
471
|
+
},
|
|
472
|
+
verifyCallerKeyForTenant: (input) => registry.verifyActiveCallerKey(input.tenant, input.token),
|
|
473
|
+
admissionGate: createAdmissionGate(logger),
|
|
474
|
+
tenantLookup: (ref) => registry.getActiveByTenant(ref),
|
|
475
|
+
});
|
|
476
|
+
const maxBody = options.maxBodyBytes ?? DEFAULT_MAX_BODY;
|
|
477
|
+
const gate = options.deployGate ?? allowAllGate();
|
|
478
|
+
return (req, res) => {
|
|
479
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
480
|
+
// Liveness/readiness probes: un-gated and **not** HTTPS-enforced — Cloud Run's internal probe is plain
|
|
481
|
+
// HTTP, so enforcing HTTPS here would `426` the probe and the revision would never go healthy (ADR 0034).
|
|
482
|
+
// `/healthz` is a pure liveness 200 (no store touch); `/readyz` reflects the injected readiness probe.
|
|
483
|
+
if (req.method === 'GET' && (url.pathname === '/healthz' || url.pathname === '/readyz')) {
|
|
484
|
+
applySecurityHeaders(res, tls);
|
|
485
|
+
if (url.pathname === '/healthz')
|
|
486
|
+
return sendJson(res, 200, { status: 'ok' });
|
|
487
|
+
void Promise.resolve(options.readinessProbe ? options.readinessProbe() : true)
|
|
488
|
+
.then((ready) => sendJson(res, ready ? 200 : 503, { status: ready ? 'ready' : 'unready' }))
|
|
489
|
+
.catch(() => sendJson(res, 503, { status: 'unready' }));
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
// Protected-resource metadata (RFC 9728 / MCP authorization): an MCP client fetches this after a `401`
|
|
493
|
+
// from an owner-only endpoint to discover the authorization server (OA-1). The resource path follows the
|
|
494
|
+
// well-known prefix, e.g. `/.well-known/oauth-protected-resource/o/{org}/{app}/mcp`.
|
|
495
|
+
if (req.method === 'GET' && url.pathname.startsWith(PRM_PREFIX)) {
|
|
496
|
+
applySecurityHeaders(res, tls);
|
|
497
|
+
if (enforceHttps(req, res, tls))
|
|
498
|
+
return;
|
|
499
|
+
const base = options.publicBaseUrl ?? baseFromRequest(req, tls);
|
|
500
|
+
const doc = protectedResourceMetadata({
|
|
501
|
+
resource: `${base}${url.pathname.slice(PRM_PREFIX.length)}`,
|
|
502
|
+
...(options.authServerIssuer ? { authorizationServers: [options.authServerIssuer] } : {}),
|
|
503
|
+
});
|
|
504
|
+
return sendJson(res, 200, doc);
|
|
505
|
+
}
|
|
506
|
+
// Authorization server (OA-2): delegate the AS paths to the Express sub-app. Same HTTPS posture +
|
|
507
|
+
// security headers as the rest of the control plane; the sub-app owns content negotiation and its own
|
|
508
|
+
// CORS/rate-limit/body parsing. Delegated before the body is read so the SDK handlers can stream it.
|
|
509
|
+
if (options.authServerApp !== undefined && isAuthServerPath(url.pathname)) {
|
|
510
|
+
applySecurityHeaders(res, tls);
|
|
511
|
+
if (enforceHttps(req, res, tls))
|
|
512
|
+
return;
|
|
513
|
+
options.authServerApp(req, res);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (url.pathname === '/v1/auth/google' && req.method === 'GET') {
|
|
517
|
+
applySecurityHeaders(res, tls);
|
|
518
|
+
if (enforceHttps(req, res, tls))
|
|
519
|
+
return;
|
|
520
|
+
const base = options.publicBaseUrl ?? baseFromRequest(req, tls);
|
|
521
|
+
return sendJson(res, 200, {
|
|
522
|
+
ok: true,
|
|
523
|
+
service: base,
|
|
524
|
+
googleClientId: options.controlPlaneGoogleClientId ?? null,
|
|
525
|
+
allowedEmailDomain: options.controlPlaneAllowedEmailDomain ?? '@noodleseed.com',
|
|
526
|
+
authType: options.controlPlaneGoogleClientId ? 'google-oauth-pkce' : 'open-dev',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
const deployRef = parseTenantDeployPath(url.pathname);
|
|
530
|
+
if (req.method === 'POST' && deployRef !== undefined) {
|
|
531
|
+
// Same HTTPS posture + security headers as the data plane (the router applies them for /{id}/mcp).
|
|
532
|
+
applySecurityHeaders(res, tls);
|
|
533
|
+
if (enforceHttps(req, res, tls))
|
|
534
|
+
return;
|
|
535
|
+
// Admin boundary: authenticate the deployer before reading the body. The gate guards only
|
|
536
|
+
// management deploys — tenant MCP routes are governed separately (caller key, Slice 24).
|
|
537
|
+
handleDeploy(req, res, registry, options, maxBody, deployRef, gate, controlPlane).catch((error) => {
|
|
538
|
+
// A thrown deploy failure (compile/seal/persist) must be observable, not a silent 500. The message
|
|
539
|
+
// is operational detail (a DB/KMS/connector fault), not a secret — values/keys are never included.
|
|
540
|
+
logger.error('deploy.error', {
|
|
541
|
+
name: error instanceof Error ? error.name : 'unknown',
|
|
542
|
+
message: error instanceof Error ? error.message : String(error),
|
|
543
|
+
stack: error instanceof Error ? (error.stack ?? '').split('\n').slice(0, 4).join(' | ') : '',
|
|
544
|
+
});
|
|
545
|
+
if (!res.headersSent)
|
|
546
|
+
sendJson(res, 500, { error: 'internal error' });
|
|
547
|
+
});
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (url.pathname === '/v1/whoami' && req.method === 'GET') {
|
|
551
|
+
applySecurityHeaders(res, tls);
|
|
552
|
+
if (enforceHttps(req, res, tls))
|
|
553
|
+
return;
|
|
554
|
+
handleWhoami(req, res, gate, controlPlane).catch(() => {
|
|
555
|
+
if (!res.headersSent)
|
|
556
|
+
sendJson(res, 500, { error: 'internal error' });
|
|
557
|
+
});
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (url.pathname === '/v1/orgs' && (req.method === 'GET' || req.method === 'POST')) {
|
|
561
|
+
applySecurityHeaders(res, tls);
|
|
562
|
+
if (enforceHttps(req, res, tls))
|
|
563
|
+
return;
|
|
564
|
+
handleOrgs(req, res, gate, controlPlane, maxBody).catch(() => {
|
|
565
|
+
if (!res.headersSent)
|
|
566
|
+
sendJson(res, 500, { error: 'internal error' });
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const membersRef = parseMembersPath(url.pathname);
|
|
571
|
+
if (membersRef !== undefined) {
|
|
572
|
+
applySecurityHeaders(res, tls);
|
|
573
|
+
if (enforceHttps(req, res, tls))
|
|
574
|
+
return;
|
|
575
|
+
handleMembers(req, res, gate, controlPlane, maxBody, membersRef).catch(() => {
|
|
576
|
+
if (!res.headersSent)
|
|
577
|
+
sendJson(res, 500, { error: 'internal error' });
|
|
578
|
+
});
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const deploymentsRef = parseDeploymentsPath(url.pathname);
|
|
582
|
+
if (deploymentsRef !== undefined && req.method === 'GET') {
|
|
583
|
+
applySecurityHeaders(res, tls);
|
|
584
|
+
if (enforceHttps(req, res, tls))
|
|
585
|
+
return;
|
|
586
|
+
handleDeployments(req, res, registry, gate, controlPlane, deploymentsRef, url).catch(() => {
|
|
587
|
+
if (!res.headersSent)
|
|
588
|
+
sendJson(res, 500, { error: 'internal error' });
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const keysRef = parseKeysPath(url.pathname);
|
|
593
|
+
if (keysRef !== undefined && (req.method === 'GET' || req.method === 'POST')) {
|
|
594
|
+
applySecurityHeaders(res, tls);
|
|
595
|
+
if (enforceHttps(req, res, tls))
|
|
596
|
+
return;
|
|
597
|
+
handleKeys(req, res, registry, gate, controlPlane, keysRef).catch(() => {
|
|
598
|
+
if (!res.headersSent)
|
|
599
|
+
sendJson(res, 500, { error: 'internal error' });
|
|
600
|
+
});
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
router(req, res);
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
async function handleDeploy(req, res, registry, options, maxBody, tenant, gate, controlPlane) {
|
|
607
|
+
const identity = await authorizeControlPlane(req, res, gate, { requireIdentity: false });
|
|
608
|
+
if (identity === false)
|
|
609
|
+
return;
|
|
610
|
+
if (identity && !identity.superAdmin) {
|
|
611
|
+
const member = await controlPlane.isOrgMember({ org: tenant.org, subject: identity.subject });
|
|
612
|
+
if (!member)
|
|
613
|
+
return sendForbidden(res, 'forbidden');
|
|
614
|
+
}
|
|
615
|
+
const body = await readBody(req, maxBody);
|
|
616
|
+
if (!body.ok)
|
|
617
|
+
return sendJson(res, 413, { error: 'request body too large' });
|
|
618
|
+
let manifest;
|
|
619
|
+
let connectors;
|
|
620
|
+
let secrets;
|
|
621
|
+
let accessMode = 'caller-key';
|
|
622
|
+
try {
|
|
623
|
+
const parsed = JSON.parse(body.text);
|
|
624
|
+
if (typeof parsed.manifest !== 'string')
|
|
625
|
+
throw new Error('body must be {"manifest": "<yaml>"}');
|
|
626
|
+
manifest = parsed.manifest;
|
|
627
|
+
if (parsed.connectors !== undefined) {
|
|
628
|
+
if (typeof parsed.connectors !== 'string')
|
|
629
|
+
throw new Error('"connectors" must be a string');
|
|
630
|
+
connectors = parsed.connectors;
|
|
631
|
+
}
|
|
632
|
+
if (parsed.secrets !== undefined) {
|
|
633
|
+
secrets = parseSecrets(parsed.secrets);
|
|
634
|
+
}
|
|
635
|
+
if (parsed.accessMode !== undefined) {
|
|
636
|
+
if (!isAccessMode(parsed.accessMode)) {
|
|
637
|
+
throw new Error('"accessMode" must be "caller-key", "owner-only", or "org-members"');
|
|
638
|
+
}
|
|
639
|
+
accessMode = parsed.accessMode;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
return sendJson(res, 400, { error: `invalid deploy request: ${error.message}` });
|
|
644
|
+
}
|
|
645
|
+
// Identity access modes bind the data plane to verified identity, so the deployer must be authenticated.
|
|
646
|
+
// (Localhost dev runs the gate open; such deploys are rejected for lack of an owner/audit actor.)
|
|
647
|
+
if (isIdentityAccessMode(accessMode) && !identity) {
|
|
648
|
+
return sendJson(res, 401, {
|
|
649
|
+
error: `${accessMode} deployments require an authenticated deployer`,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
const logger = options.logger ?? noopLogger;
|
|
653
|
+
const result = await registry.deploy(tenant, manifest, connectors, secrets, identity || undefined, accessMode);
|
|
654
|
+
if (!result.ok) {
|
|
655
|
+
// Codes are safe enum strings; never a path-with-value. No secret name or value is logged.
|
|
656
|
+
logger.warn('deploy.rejected', {
|
|
657
|
+
status: 400,
|
|
658
|
+
errorCount: result.errors.length,
|
|
659
|
+
codes: result.errors.map((error) => error.code).join(','),
|
|
660
|
+
});
|
|
661
|
+
return sendJson(res, 400, { ok: false, errors: result.errors });
|
|
662
|
+
}
|
|
663
|
+
// Counts only — never secret names or values, never the minted caller key or its hash.
|
|
664
|
+
logger.info('deploy.ok', {
|
|
665
|
+
deploymentId: result.deploymentId,
|
|
666
|
+
org: tenant.org,
|
|
667
|
+
app: tenant.app,
|
|
668
|
+
env: tenant.env,
|
|
669
|
+
hasConnectors: connectors !== undefined,
|
|
670
|
+
secretCount: secrets ? Object.keys(secrets).length : 0,
|
|
671
|
+
});
|
|
672
|
+
const base = options.publicBaseUrl ?? baseFromRequest(req, options.tls ?? {});
|
|
673
|
+
return sendJson(res, 201, {
|
|
674
|
+
ok: true,
|
|
675
|
+
org: tenant.org,
|
|
676
|
+
app: tenant.app,
|
|
677
|
+
env: tenant.env,
|
|
678
|
+
deploymentId: result.deploymentId,
|
|
679
|
+
accessMode: result.accessMode,
|
|
680
|
+
url: tenant.env === 'prod'
|
|
681
|
+
? `${base}/o/${tenant.org}/${tenant.app}/mcp`
|
|
682
|
+
: `${base}/o/${tenant.org}/${tenant.app}/${tenant.env}/mcp`,
|
|
683
|
+
// Caller-key deploys only: shown once, callers send `Authorization: Bearer <callerKey>` (only the hash
|
|
684
|
+
// is stored). Identity-based deploys mint no key — callers authenticate with their own identity.
|
|
685
|
+
...(result.callerKey !== undefined ? { callerKey: result.callerKey } : {}),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function isAccessMode(value) {
|
|
689
|
+
return value === 'caller-key' || value === 'owner-only' || value === 'org-members';
|
|
690
|
+
}
|
|
691
|
+
function isIdentityAccessMode(accessMode) {
|
|
692
|
+
return accessMode === 'owner-only' || accessMode === 'org-members';
|
|
693
|
+
}
|
|
694
|
+
async function handleWhoami(req, res, gate, controlPlane) {
|
|
695
|
+
const identity = await authorizeControlPlane(req, res, gate, { requireIdentity: true });
|
|
696
|
+
if (identity === false)
|
|
697
|
+
return;
|
|
698
|
+
const orgs = identity.superAdmin
|
|
699
|
+
? await controlPlane.listOrgs()
|
|
700
|
+
: await controlPlane.listOrgsForSubject(identity.subject);
|
|
701
|
+
return sendJson(res, 200, { ok: true, identity, orgs });
|
|
702
|
+
}
|
|
703
|
+
async function handleOrgs(req, res, gate, controlPlane, maxBody) {
|
|
704
|
+
const identity = await authorizeControlPlane(req, res, gate, { requireIdentity: true });
|
|
705
|
+
if (identity === false)
|
|
706
|
+
return;
|
|
707
|
+
if (req.method === 'GET') {
|
|
708
|
+
const orgs = identity.superAdmin
|
|
709
|
+
? await controlPlane.listOrgs()
|
|
710
|
+
: await controlPlane.listOrgsForSubject(identity.subject);
|
|
711
|
+
return sendJson(res, 200, { ok: true, orgs });
|
|
712
|
+
}
|
|
713
|
+
if (!identity.superAdmin)
|
|
714
|
+
return sendForbidden(res, 'super-admin required');
|
|
715
|
+
const body = await readJsonBody(req, maxBody);
|
|
716
|
+
if (!body.ok)
|
|
717
|
+
return sendJson(res, body.status, { error: body.error });
|
|
718
|
+
const parsed = body.value;
|
|
719
|
+
if (typeof parsed.slug !== 'string')
|
|
720
|
+
return sendJson(res, 400, { error: '"slug" must be a string' });
|
|
721
|
+
if (parsed.displayName !== undefined && typeof parsed.displayName !== 'string') {
|
|
722
|
+
return sendJson(res, 400, { error: '"displayName" must be a string' });
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
const org = await controlPlane.createOrg({
|
|
726
|
+
slug: parsed.slug,
|
|
727
|
+
...(typeof parsed.displayName === 'string' ? { displayName: parsed.displayName } : {}),
|
|
728
|
+
});
|
|
729
|
+
return sendJson(res, 201, { ok: true, org });
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
return sendJson(res, 400, { error: error.message });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
async function handleMembers(req, res, gate, controlPlane, maxBody, ref) {
|
|
736
|
+
const identity = await authorizeControlPlane(req, res, gate, { requireIdentity: true });
|
|
737
|
+
if (identity === false)
|
|
738
|
+
return;
|
|
739
|
+
if (!identity.superAdmin)
|
|
740
|
+
return sendForbidden(res, 'super-admin required');
|
|
741
|
+
if (req.method === 'GET' && ref.subject === undefined) {
|
|
742
|
+
try {
|
|
743
|
+
return sendJson(res, 200, { ok: true, members: await controlPlane.listOrgMembers(ref.org) });
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
return sendJson(res, 400, { error: error.message });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (req.method === 'POST' && ref.subject === undefined) {
|
|
750
|
+
const body = await readJsonBody(req, maxBody);
|
|
751
|
+
if (!body.ok)
|
|
752
|
+
return sendJson(res, body.status, { error: body.error });
|
|
753
|
+
const parsed = body.value;
|
|
754
|
+
if (typeof parsed.subject !== 'string') {
|
|
755
|
+
return sendJson(res, 400, { error: '"subject" must be a string' });
|
|
756
|
+
}
|
|
757
|
+
if (typeof parsed.email !== 'string') {
|
|
758
|
+
return sendJson(res, 400, { error: '"email" must be a string' });
|
|
759
|
+
}
|
|
760
|
+
let role;
|
|
761
|
+
try {
|
|
762
|
+
role = validateOrgRole(typeof parsed.role === 'string' ? parsed.role : 'developer');
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
return sendJson(res, 400, { error: error.message });
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
const member = await controlPlane.addOrgMember({
|
|
769
|
+
org: ref.org,
|
|
770
|
+
subject: parsed.subject,
|
|
771
|
+
email: parsed.email,
|
|
772
|
+
role,
|
|
773
|
+
});
|
|
774
|
+
return sendJson(res, 201, { ok: true, member });
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
return sendJson(res, 400, { error: error.message });
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (req.method === 'DELETE' && ref.subject !== undefined) {
|
|
781
|
+
await controlPlane.removeOrgMember({ org: ref.org, subject: ref.subject });
|
|
782
|
+
res.writeHead(204);
|
|
783
|
+
res.end();
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
return sendJson(res, 404, { error: 'not found' });
|
|
787
|
+
}
|
|
788
|
+
async function handleDeployments(req, res, registry, gate, controlPlane, ref, url) {
|
|
789
|
+
const identity = await authorizeControlPlane(req, res, gate, { requireIdentity: true });
|
|
790
|
+
if (identity === false)
|
|
791
|
+
return;
|
|
792
|
+
if (!identity.superAdmin) {
|
|
793
|
+
const member = await controlPlane.isOrgMember({ org: ref.org, subject: identity.subject });
|
|
794
|
+
if (!member)
|
|
795
|
+
return sendForbidden(res, 'forbidden');
|
|
796
|
+
}
|
|
797
|
+
const app = url.searchParams.get('app') ?? undefined;
|
|
798
|
+
const env = url.searchParams.get('env') ?? undefined;
|
|
799
|
+
try {
|
|
800
|
+
const deployments = await registry.listDeployments({
|
|
801
|
+
org: ref.org,
|
|
802
|
+
...(app !== undefined ? { app } : {}),
|
|
803
|
+
...(env !== undefined ? { env } : {}),
|
|
804
|
+
});
|
|
805
|
+
return sendJson(res, 200, { ok: true, deployments });
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
return sendJson(res, 400, { error: error.message });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
async function handleKeys(req, res, registry, gate, controlPlane, ref) {
|
|
812
|
+
const identity = await authorizeControlPlane(req, res, gate, { requireIdentity: true });
|
|
813
|
+
if (identity === false)
|
|
814
|
+
return;
|
|
815
|
+
if (!identity.superAdmin) {
|
|
816
|
+
const member = await controlPlane.isOrgMember({ org: ref.org, subject: identity.subject });
|
|
817
|
+
if (!member)
|
|
818
|
+
return sendForbidden(res, 'forbidden');
|
|
819
|
+
}
|
|
820
|
+
if (req.method === 'GET' && ref.action === undefined) {
|
|
821
|
+
const deployments = await registry.listDeployments({
|
|
822
|
+
org: ref.org,
|
|
823
|
+
app: ref.app,
|
|
824
|
+
env: ref.env,
|
|
825
|
+
});
|
|
826
|
+
return sendJson(res, 200, {
|
|
827
|
+
ok: true,
|
|
828
|
+
keys: deployments
|
|
829
|
+
.filter((deployment) => deployment.active && deployment.hasCallerKey)
|
|
830
|
+
.map((deployment) => ({
|
|
831
|
+
org: deployment.orgSlug,
|
|
832
|
+
app: deployment.appSlug,
|
|
833
|
+
env: deployment.environment,
|
|
834
|
+
deploymentId: deployment.deploymentId,
|
|
835
|
+
active: deployment.active,
|
|
836
|
+
createdAt: deployment.createdAt,
|
|
837
|
+
})),
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
if (req.method === 'POST' && ref.action === 'rotate') {
|
|
841
|
+
const rotated = await registry.rotateCallerKey(ref);
|
|
842
|
+
if (!rotated) {
|
|
843
|
+
return sendJson(res, 404, {
|
|
844
|
+
error: 'no active caller-key deployment found for this app environment',
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
return sendJson(res, 201, {
|
|
848
|
+
ok: true,
|
|
849
|
+
key: {
|
|
850
|
+
org: rotated.deployment.orgSlug,
|
|
851
|
+
app: rotated.deployment.appSlug,
|
|
852
|
+
env: rotated.deployment.environment,
|
|
853
|
+
deploymentId: rotated.deployment.deploymentId,
|
|
854
|
+
active: rotated.deployment.active,
|
|
855
|
+
createdAt: new Date().toISOString(),
|
|
856
|
+
},
|
|
857
|
+
callerKey: rotated.key,
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
return sendJson(res, 404, { error: 'not found' });
|
|
861
|
+
}
|
|
862
|
+
async function authorizeControlPlane(req, res, gate, options) {
|
|
863
|
+
const auth = await gate.authorize(req);
|
|
864
|
+
if (!auth.ok) {
|
|
865
|
+
if (auth.status === 401)
|
|
866
|
+
sendUnauthorized(res, auth.message);
|
|
867
|
+
else
|
|
868
|
+
sendForbidden(res, auth.message);
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
if (options.requireIdentity && !auth.identity) {
|
|
872
|
+
sendUnauthorized(res, 'identity required');
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
return auth.identity;
|
|
876
|
+
}
|
|
877
|
+
function parseTenantDeployPath(pathname) {
|
|
878
|
+
const match = /^\/v1\/orgs\/([^/]+)\/apps\/([^/]+)\/envs\/([^/]+)\/deploy$/.exec(pathname);
|
|
879
|
+
if (!match)
|
|
880
|
+
return undefined;
|
|
881
|
+
try {
|
|
882
|
+
return validateTenantRef({
|
|
883
|
+
org: decodeURIComponent(match[1]),
|
|
884
|
+
app: decodeURIComponent(match[2]),
|
|
885
|
+
env: decodeURIComponent(match[3]),
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
return undefined;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function parseMembersPath(pathname) {
|
|
893
|
+
const collection = /^\/v1\/orgs\/([^/]+)\/members$/.exec(pathname);
|
|
894
|
+
if (collection) {
|
|
895
|
+
try {
|
|
896
|
+
return { org: validateSlug('org', decodeURIComponent(collection[1])) };
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
return undefined;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const item = /^\/v1\/orgs\/([^/]+)\/members\/([^/]+)$/.exec(pathname);
|
|
903
|
+
if (!item)
|
|
904
|
+
return undefined;
|
|
905
|
+
try {
|
|
906
|
+
return {
|
|
907
|
+
org: validateSlug('org', decodeURIComponent(item[1])),
|
|
908
|
+
subject: decodeURIComponent(item[2]),
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
return undefined;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function parseDeploymentsPath(pathname) {
|
|
916
|
+
const match = /^\/v1\/orgs\/([^/]+)\/deployments$/.exec(pathname);
|
|
917
|
+
if (!match)
|
|
918
|
+
return undefined;
|
|
919
|
+
try {
|
|
920
|
+
return { org: validateSlug('org', decodeURIComponent(match[1])) };
|
|
921
|
+
}
|
|
922
|
+
catch {
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function parseKeysPath(pathname) {
|
|
927
|
+
const match = /^\/v1\/orgs\/([^/]+)\/apps\/([^/]+)\/envs\/([^/]+)\/keys(?:\/(rotate))?$/.exec(pathname);
|
|
928
|
+
if (!match)
|
|
929
|
+
return undefined;
|
|
930
|
+
try {
|
|
931
|
+
const ref = validateTenantRef({
|
|
932
|
+
org: decodeURIComponent(match[1]),
|
|
933
|
+
app: decodeURIComponent(match[2]),
|
|
934
|
+
env: decodeURIComponent(match[3]),
|
|
935
|
+
});
|
|
936
|
+
return { ...ref, ...(match[4] === 'rotate' ? { action: 'rotate' } : {}) };
|
|
937
|
+
}
|
|
938
|
+
catch {
|
|
939
|
+
return undefined;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function createAdmissionGate(_logger) {
|
|
943
|
+
return async () => ({ allow: true });
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Validate the deploy request's `secrets` field into a `name -> value` map. Rejects a non-object or a
|
|
947
|
+
* non-string value, naming only the offending key (never a value) so secrets never reach an error body.
|
|
948
|
+
*/
|
|
949
|
+
function parseSecrets(raw) {
|
|
950
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
951
|
+
throw new Error('"secrets" must be an object of string values');
|
|
952
|
+
}
|
|
953
|
+
const out = {};
|
|
954
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
955
|
+
if (typeof value !== 'string')
|
|
956
|
+
throw new Error(`secret "${name}" must be a string`);
|
|
957
|
+
out[name] = value;
|
|
958
|
+
}
|
|
959
|
+
return out;
|
|
960
|
+
}
|
|
961
|
+
function baseFromRequest(req, tls) {
|
|
962
|
+
const host = req.headers.host ?? 'localhost';
|
|
963
|
+
return `${effectiveProto(req, tls.trustProxy ?? false)}://${host}`;
|
|
964
|
+
}
|
|
965
|
+
/** Start the deploy service on its own `node:http` server (used by the `noodle-service` bin). */
|
|
966
|
+
export async function serveService(options = {}) {
|
|
967
|
+
const host = options.host ?? '127.0.0.1';
|
|
968
|
+
const gate = options.deployGate ??
|
|
969
|
+
(options.googleClientId !== undefined
|
|
970
|
+
? new GoogleControlPlaneGate({
|
|
971
|
+
audience: options.googleClientId,
|
|
972
|
+
admins: options.controlPlaneAdmins ?? [],
|
|
973
|
+
...(options.controlPlaneAllowedEmailDomain !== undefined
|
|
974
|
+
? { allowedEmailDomain: options.controlPlaneAllowedEmailDomain }
|
|
975
|
+
: {}),
|
|
976
|
+
...(options.googleVerifier ? { verifier: options.googleVerifier } : {}),
|
|
977
|
+
})
|
|
978
|
+
: undefined);
|
|
979
|
+
// Fail closed: never expose an unauthenticated deploy endpoint on a non-loopback bind.
|
|
980
|
+
if (gate === undefined && !isLoopbackHost(host)) {
|
|
981
|
+
throw new Error(`refusing to bind a non-loopback host (${host}) without deploy authentication; ` +
|
|
982
|
+
'set NOODLE_GOOGLE_CLIENT_ID (or pass a deployGate)');
|
|
983
|
+
}
|
|
984
|
+
// Select exactly one durable backend: Postgres (cloud, ADR 0035) > file (local) > in-memory. The
|
|
985
|
+
// Postgres modules are loaded lazily so `pg` / the Cloud SQL connector are never pulled in on the
|
|
986
|
+
// file/in-memory paths (keeping the default startup light and dependency-free).
|
|
987
|
+
let store;
|
|
988
|
+
let controlPlaneStore = options.controlPlaneStore;
|
|
989
|
+
let pgPool;
|
|
990
|
+
if (options.databaseUrl !== undefined || options.cloudSql !== undefined) {
|
|
991
|
+
const [{ createPostgresPool }, { PostgresArtifactStore }] = await Promise.all([
|
|
992
|
+
import('./store/cloudsql-pool.js'),
|
|
993
|
+
import('./store/postgres.js'),
|
|
994
|
+
]);
|
|
995
|
+
pgPool = await createPostgresPool({
|
|
996
|
+
...(options.databaseUrl !== undefined ? { databaseUrl: options.databaseUrl } : {}),
|
|
997
|
+
...(options.cloudSql !== undefined ? { cloudSql: options.cloudSql } : {}),
|
|
998
|
+
});
|
|
999
|
+
const postgres = new PostgresArtifactStore(pgPool.pool);
|
|
1000
|
+
await postgres.ensureSchema();
|
|
1001
|
+
store = postgres;
|
|
1002
|
+
controlPlaneStore = controlPlaneStore ?? postgres;
|
|
1003
|
+
}
|
|
1004
|
+
else if (options.dataDir !== undefined) {
|
|
1005
|
+
store = new JsonFileArtifactStore(options.dataDir);
|
|
1006
|
+
}
|
|
1007
|
+
controlPlaneStore = controlPlaneStore ?? new InMemoryControlPlaneStore();
|
|
1008
|
+
// Fail closed: any durable store must encrypt secrets at rest, so it requires a master-key custodian. A
|
|
1009
|
+
// KMS key (the KEK never enters the process — ADR 0037) takes precedence over a static key; an
|
|
1010
|
+
// absent/invalid key throws here, before any traffic is served. The KMS adapter + its `@google-cloud/kms`
|
|
1011
|
+
// SDK load lazily, so a static/file deployment never pulls them in.
|
|
1012
|
+
let secretBox;
|
|
1013
|
+
if (store) {
|
|
1014
|
+
if (options.kmsKeyName !== undefined) {
|
|
1015
|
+
const { gcpKmsMasterKeyProvider } = await import('./secret/kms-master-key.js');
|
|
1016
|
+
secretBox = new SecretBox(gcpKmsMasterKeyProvider({ keyName: options.kmsKeyName }));
|
|
1017
|
+
}
|
|
1018
|
+
else if (options.secretMasterKey !== undefined) {
|
|
1019
|
+
secretBox = new SecretBox(staticMasterKeyProvider(options.secretMasterKey));
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
throw new Error('persistence requires a secret master-key custodian; set NOODLE_KMS_KEY or NOODLE_SECRET_MASTER_KEY');
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// Self-hosted authorization server (OA-2): build the AS Express app + its state store (the shared Postgres
|
|
1026
|
+
// store when persistence is on, else in-memory). Express + the SDK auth handlers load lazily, only here, so
|
|
1027
|
+
// an AS-less deployment never pulls them in. Owner-only token verification is wired to the same signing key
|
|
1028
|
+
// in-process (no JWKS round-trip), which is correct because every instance shares the static key via env.
|
|
1029
|
+
let authServerApp;
|
|
1030
|
+
let resolvedVerifyOwnerToken = options.verifyOwnerToken;
|
|
1031
|
+
let resolvedAuthServerIssuer = options.authServerIssuer;
|
|
1032
|
+
if (options.oauth) {
|
|
1033
|
+
let oauthStore;
|
|
1034
|
+
if (pgPool) {
|
|
1035
|
+
const { PostgresOAuthStore } = await import('./oauth/store-postgres.js');
|
|
1036
|
+
const postgresOAuth = new PostgresOAuthStore(pgPool.pool);
|
|
1037
|
+
await postgresOAuth.ensureSchema();
|
|
1038
|
+
oauthStore = postgresOAuth;
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
const { InMemoryOAuthStore } = await import('./oauth/store.js');
|
|
1042
|
+
oauthStore = new InMemoryOAuthStore();
|
|
1043
|
+
}
|
|
1044
|
+
const [{ NoodleOAuthProvider }, { createOAuthApp }] = await Promise.all([
|
|
1045
|
+
import('./oauth/provider.js'),
|
|
1046
|
+
import('./oauth/app.js'),
|
|
1047
|
+
]);
|
|
1048
|
+
// Normalize once so the token `iss`, the verifier's expected issuer, and the PRM `authorization_servers`
|
|
1049
|
+
// entry are byte-identical (a trailing-slash `PUBLIC_BASE_URL` would otherwise desync them).
|
|
1050
|
+
const oauthIssuer = options.oauth.issuer.replace(/\/+$/, '');
|
|
1051
|
+
const provider = new NoodleOAuthProvider({
|
|
1052
|
+
issuer: oauthIssuer,
|
|
1053
|
+
store: oauthStore,
|
|
1054
|
+
signer: options.oauth.signer,
|
|
1055
|
+
google: options.oauth.google,
|
|
1056
|
+
...(options.oauth.allowedEmailDomain !== undefined
|
|
1057
|
+
? { allowedEmailDomain: options.oauth.allowedEmailDomain }
|
|
1058
|
+
: {}),
|
|
1059
|
+
});
|
|
1060
|
+
authServerApp = createOAuthApp(provider);
|
|
1061
|
+
resolvedAuthServerIssuer = resolvedAuthServerIssuer ?? oauthIssuer;
|
|
1062
|
+
// Resource-server verification uses the AS signing key directly (co-hosted, same process), so an
|
|
1063
|
+
// owner-only endpoint validates tokens without an HTTP JWKS fetch. Multi-instance-safe because the key is
|
|
1064
|
+
// shared via env.
|
|
1065
|
+
resolvedVerifyOwnerToken =
|
|
1066
|
+
resolvedVerifyOwnerToken ??
|
|
1067
|
+
createJwtVerifier({
|
|
1068
|
+
issuer: oauthIssuer,
|
|
1069
|
+
keyResolver: await options.oauth.signer.verifierKey(),
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
const registry = new ServerRegistry(store, secretBox);
|
|
1073
|
+
// Recovery is lazy by default (ADR 0036): each server recompiles on its first request, which is correct
|
|
1074
|
+
// on a multi-instance platform (any instance serves any deploy) and a cheaper cold start. `warmAll` opts
|
|
1075
|
+
// into eager recompile-all-on-boot (boot-time validation of every server, for a pinned/on-prem instance).
|
|
1076
|
+
const recovered = store && options.warmAll ? await registry.recover() : undefined;
|
|
1077
|
+
// Readiness reflects durable-store reachability. Built here so the handler never imports `pg`; a
|
|
1078
|
+
// file/in-memory deployment is always ready.
|
|
1079
|
+
const readinessProbe = pgPool
|
|
1080
|
+
? async () => {
|
|
1081
|
+
try {
|
|
1082
|
+
await pgPool.pool.query('SELECT 1');
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
: async () => true;
|
|
1090
|
+
const handlerOptions = {
|
|
1091
|
+
...options,
|
|
1092
|
+
readinessProbe,
|
|
1093
|
+
controlPlaneStore,
|
|
1094
|
+
...(options.googleClientId !== undefined
|
|
1095
|
+
? { controlPlaneGoogleClientId: options.googleClientId }
|
|
1096
|
+
: {}),
|
|
1097
|
+
...(gate ? { deployGate: gate } : {}),
|
|
1098
|
+
...(resolvedVerifyOwnerToken ? { verifyOwnerToken: resolvedVerifyOwnerToken } : {}),
|
|
1099
|
+
...(resolvedAuthServerIssuer ? { authServerIssuer: resolvedAuthServerIssuer } : {}),
|
|
1100
|
+
...(authServerApp ? { authServerApp } : {}),
|
|
1101
|
+
};
|
|
1102
|
+
const http = createServer(createServiceHandler(registry, handlerOptions));
|
|
1103
|
+
return new Promise((resolve) => {
|
|
1104
|
+
http.listen(options.port ?? 8787, host, () => {
|
|
1105
|
+
const { port } = http.address();
|
|
1106
|
+
resolve({
|
|
1107
|
+
http,
|
|
1108
|
+
url: `http://${host}:${port}`,
|
|
1109
|
+
port,
|
|
1110
|
+
registry,
|
|
1111
|
+
...(recovered ? { recovered } : {}),
|
|
1112
|
+
close: async () => {
|
|
1113
|
+
await new Promise((res, rej) => http.close((e) => (e ? rej(e) : res())));
|
|
1114
|
+
if (pgPool)
|
|
1115
|
+
await pgPool.close();
|
|
1116
|
+
},
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
async function readBody(req, max) {
|
|
1122
|
+
const chunks = [];
|
|
1123
|
+
let size = 0;
|
|
1124
|
+
for await (const chunk of req) {
|
|
1125
|
+
const buf = chunk;
|
|
1126
|
+
size += buf.length;
|
|
1127
|
+
if (size > max)
|
|
1128
|
+
return { ok: false };
|
|
1129
|
+
chunks.push(buf);
|
|
1130
|
+
}
|
|
1131
|
+
return { ok: true, text: Buffer.concat(chunks).toString('utf8') };
|
|
1132
|
+
}
|
|
1133
|
+
async function readJsonBody(req, max) {
|
|
1134
|
+
const body = await readBody(req, max);
|
|
1135
|
+
if (!body.ok)
|
|
1136
|
+
return { ok: false, status: 413, error: 'request body too large' };
|
|
1137
|
+
try {
|
|
1138
|
+
return { ok: true, value: JSON.parse(body.text) };
|
|
1139
|
+
}
|
|
1140
|
+
catch (error) {
|
|
1141
|
+
return { ok: false, status: 400, error: `invalid JSON: ${error.message}` };
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function sendJson(res, status, body) {
|
|
1145
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
1146
|
+
res.end(JSON.stringify(body));
|
|
1147
|
+
}
|
|
1148
|
+
function sendUnauthorized(res, message = 'unauthorized') {
|
|
1149
|
+
res.writeHead(401, {
|
|
1150
|
+
'content-type': 'application/json; charset=utf-8',
|
|
1151
|
+
'www-authenticate': 'Bearer realm="noodle-deploy"',
|
|
1152
|
+
});
|
|
1153
|
+
res.end(JSON.stringify({ error: message }));
|
|
1154
|
+
}
|
|
1155
|
+
function sendForbidden(res, message) {
|
|
1156
|
+
res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' });
|
|
1157
|
+
res.end(JSON.stringify({ error: message }));
|
|
1158
|
+
}
|
|
1159
|
+
/** Loopback hosts may run the control plane without auth; any other bind must configure auth. */
|
|
1160
|
+
function isLoopbackHost(host) {
|
|
1161
|
+
return host === '127.0.0.1' || host === '::1' || host === 'localhost';
|
|
1162
|
+
}
|
|
1163
|
+
//# sourceMappingURL=service.js.map
|