@empire-builder-kit/nx 1.0.0-beta.9 → 1.0.0-rc.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/NOTICE +6 -0
- package/README.md +113 -3
- package/executors.json +30 -0
- package/package.json +84 -6
- package/src/api-contract/baseline-update.d.ts +82 -0
- package/src/api-contract/baseline-update.js +225 -0
- package/src/api-contract/baseline-update.js.map +1 -0
- package/src/api-contract/compatibility.d.ts +30 -0
- package/src/api-contract/compatibility.js +643 -0
- package/src/api-contract/compatibility.js.map +1 -0
- package/src/api-contract/contract-graph.d.ts +35 -0
- package/src/api-contract/contract-graph.js +141 -0
- package/src/api-contract/contract-graph.js.map +1 -0
- package/src/api-contract/evidence.d.ts +23 -0
- package/src/api-contract/evidence.js +87 -0
- package/src/api-contract/evidence.js.map +1 -0
- package/src/api-contract/index.d.ts +6 -0
- package/src/api-contract/index.js +10 -0
- package/src/api-contract/index.js.map +1 -0
- package/src/api-contract/model.d.ts +144 -0
- package/src/api-contract/model.fixtures.d.ts +2 -0
- package/src/api-contract/model.fixtures.js +81 -0
- package/src/api-contract/model.fixtures.js.map +1 -0
- package/src/api-contract/model.js +993 -0
- package/src/api-contract/model.js.map +1 -0
- package/src/api-contract/rollout.d.ts +32 -0
- package/src/api-contract/rollout.js +88 -0
- package/src/api-contract/rollout.js.map +1 -0
- package/src/authorization/compatibility.d.ts +62 -0
- package/src/authorization/compatibility.js +175 -0
- package/src/authorization/compatibility.js.map +1 -0
- package/src/authorization/index.d.ts +1 -0
- package/src/authorization/index.js +5 -0
- package/src/authorization/index.js.map +1 -0
- package/src/blueprints/execution.d.ts +25 -1
- package/src/blueprints/execution.js +246 -83
- package/src/blueprints/execution.js.map +1 -1
- package/src/blueprints/external-sandbox.d.ts +53 -0
- package/src/blueprints/external-sandbox.js +1182 -0
- package/src/blueprints/external-sandbox.js.map +1 -0
- package/src/blueprints/manifest.d.ts +42 -2
- package/src/blueprints/manifest.js +82 -27
- package/src/blueprints/manifest.js.map +1 -1
- package/src/blueprints/resolver.d.ts +14 -5
- package/src/blueprints/resolver.js +228 -68
- package/src/blueprints/resolver.js.map +1 -1
- package/src/cli/agent-command.d.ts +30 -0
- package/src/cli/agent-command.js +204 -0
- package/src/cli/agent-command.js.map +1 -0
- package/src/cli/agent-runtime.d.ts +25 -0
- package/src/cli/agent-runtime.js +488 -0
- package/src/cli/agent-runtime.js.map +1 -0
- package/src/cli/blueprint-authority.d.ts +9 -0
- package/src/cli/blueprint-authority.js +43 -0
- package/src/cli/blueprint-authority.js.map +1 -0
- package/src/cli/blueprint-command.d.ts +24 -0
- package/src/cli/blueprint-command.js +63 -0
- package/src/cli/blueprint-command.js.map +1 -0
- package/src/cli/bootstrap.d.ts +31 -0
- package/src/cli/bootstrap.js +165 -0
- package/src/cli/bootstrap.js.map +1 -0
- package/src/cli/dev-command.d.ts +92 -0
- package/src/cli/dev-command.js +561 -0
- package/src/cli/dev-command.js.map +1 -0
- package/src/cli/main.d.ts +131 -0
- package/src/cli/main.js +1199 -0
- package/src/cli/main.js.map +1 -0
- package/src/cli/output.d.ts +19 -0
- package/src/cli/output.js +51 -0
- package/src/cli/output.js.map +1 -0
- package/src/cli/personal-secret-runtime.d.ts +81 -0
- package/src/cli/personal-secret-runtime.js +567 -0
- package/src/cli/personal-secret-runtime.js.map +1 -0
- package/src/cli/secret-terminal.d.ts +12 -0
- package/src/cli/secret-terminal.js +128 -0
- package/src/cli/secret-terminal.js.map +1 -0
- package/src/container-images.d.ts +1 -0
- package/src/container-images.js +5 -0
- package/src/container-images.js.map +1 -0
- package/src/data-lifecycle/index.d.ts +3 -0
- package/src/data-lifecycle/index.js +7 -0
- package/src/data-lifecycle/index.js.map +1 -0
- package/src/data-lifecycle/model.d.ts +172 -0
- package/src/data-lifecycle/model.fixtures.d.ts +2 -0
- package/src/data-lifecycle/model.fixtures.js +244 -0
- package/src/data-lifecycle/model.fixtures.js.map +1 -0
- package/src/data-lifecycle/model.js +95 -0
- package/src/data-lifecycle/model.js.map +1 -0
- package/src/data-lifecycle/sensitive-artifacts.d.ts +28 -0
- package/src/data-lifecycle/sensitive-artifacts.js +146 -0
- package/src/data-lifecycle/sensitive-artifacts.js.map +1 -0
- package/src/data-lifecycle/validation.d.ts +32 -0
- package/src/data-lifecycle/validation.js +837 -0
- package/src/data-lifecycle/validation.js.map +1 -0
- package/src/database-retirement/contract.d.ts +68 -0
- package/src/database-retirement/contract.js +166 -0
- package/src/database-retirement/contract.js.map +1 -0
- package/src/database-retirement/index.d.ts +1 -0
- package/src/database-retirement/index.js +5 -0
- package/src/database-retirement/index.js.map +1 -0
- package/src/deterministic-order.d.ts +2 -0
- package/src/deterministic-order.js +7 -0
- package/src/deterministic-order.js.map +1 -0
- package/src/developer-agent/context.d.ts +27 -0
- package/src/developer-agent/context.js +497 -0
- package/src/developer-agent/context.js.map +1 -0
- package/src/developer-agent/generator-planning.d.ts +34 -0
- package/src/developer-agent/generator-planning.js +645 -0
- package/src/developer-agent/generator-planning.js.map +1 -0
- package/src/developer-agent/generator-preview.d.ts +8 -0
- package/src/developer-agent/generator-preview.js +650 -0
- package/src/developer-agent/generator-preview.js.map +1 -0
- package/src/developer-agent/git-inspection.d.ts +108 -0
- package/src/developer-agent/git-inspection.js +815 -0
- package/src/developer-agent/git-inspection.js.map +1 -0
- package/src/developer-agent/handoff.d.ts +43 -0
- package/src/developer-agent/handoff.js +698 -0
- package/src/developer-agent/handoff.js.map +1 -0
- package/src/developer-agent/index.d.ts +10 -0
- package/src/developer-agent/index.js +14 -0
- package/src/developer-agent/index.js.map +1 -0
- package/src/developer-agent/model.d.ts +481 -0
- package/src/developer-agent/model.js +143 -0
- package/src/developer-agent/model.js.map +1 -0
- package/src/developer-agent/operation-registry.d.ts +45 -0
- package/src/developer-agent/operation-registry.js +1255 -0
- package/src/developer-agent/operation-registry.js.map +1 -0
- package/src/developer-agent/ordering.d.ts +9 -0
- package/src/developer-agent/ordering.js +15 -0
- package/src/developer-agent/ordering.js.map +1 -0
- package/src/developer-agent/planning.d.ts +18 -0
- package/src/developer-agent/planning.js +447 -0
- package/src/developer-agent/planning.js.map +1 -0
- package/src/developer-agent/preview-changes.d.ts +26 -0
- package/src/developer-agent/preview-changes.js +203 -0
- package/src/developer-agent/preview-changes.js.map +1 -0
- package/src/developer-agent/preview-source.d.ts +2 -0
- package/src/developer-agent/preview-source.js +16 -0
- package/src/developer-agent/preview-source.js.map +1 -0
- package/src/developer-agent/validation.d.ts +18 -0
- package/src/developer-agent/validation.js +2617 -0
- package/src/developer-agent/validation.js.map +1 -0
- package/src/developer-agent/workspace-inspection.d.ts +20 -0
- package/src/developer-agent/workspace-inspection.js +1134 -0
- package/src/developer-agent/workspace-inspection.js.map +1 -0
- package/src/developer-agent/workspace-path.d.ts +6 -0
- package/src/developer-agent/workspace-path.js +22 -0
- package/src/developer-agent/workspace-path.js.map +1 -0
- package/src/executors/blueprint-trust/blueprint-trust.d.ts +29 -0
- package/src/executors/blueprint-trust/blueprint-trust.js +115 -0
- package/src/executors/blueprint-trust/blueprint-trust.js.map +1 -0
- package/src/executors/blueprint-trust/schema.json +27 -0
- package/src/executors/blueprint-verification/blueprint-verification.d.ts +13 -0
- package/src/executors/blueprint-verification/blueprint-verification.js +110 -0
- package/src/executors/blueprint-verification/blueprint-verification.js.map +1 -0
- package/src/executors/blueprint-verification/schema.json +44 -0
- package/src/executors/deployment-plan/deployment-plan.js +1 -0
- package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
- package/src/executors/dev-doctor/dev-doctor.d.ts +19 -5
- package/src/executors/dev-doctor/dev-doctor.js +186 -32
- package/src/executors/dev-doctor/dev-doctor.js.map +1 -1
- package/src/executors/dev-doctor/schema.json +1 -6
- package/src/executors/dev-setup/dev-setup.d.ts +7 -1
- package/src/executors/dev-setup/dev-setup.js +14 -3
- package/src/executors/dev-setup/dev-setup.js.map +1 -1
- package/src/executors/developer-context/developer-context.d.ts +38 -0
- package/src/executors/developer-context/developer-context.js +78 -0
- package/src/executors/developer-context/developer-context.js.map +1 -0
- package/src/executors/developer-context/schema.json +87 -0
- package/src/executors/fleet/fleet.d.ts +12 -43
- package/src/executors/fleet/fleet.js +120 -500
- package/src/executors/fleet/fleet.js.map +1 -1
- package/src/executors/fleet/schema.json +64 -14
- package/src/executors/github-bootstrap/github-bootstrap.d.ts +1 -0
- package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
- package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
- package/src/executors/github-bootstrap/schema.json +5 -0
- package/src/executors/infrastructure-plan/infrastructure-plan.d.ts +144 -0
- package/src/executors/infrastructure-plan/infrastructure-plan.js +3320 -0
- package/src/executors/infrastructure-plan/infrastructure-plan.js.map +1 -0
- package/src/executors/infrastructure-plan/schema.json +78 -0
- package/src/executors/secret-guardrails/schema.json +6 -0
- package/src/executors/secret-guardrails/secret-guardrails.d.ts +3 -0
- package/src/executors/secret-guardrails/secret-guardrails.js +44 -0
- package/src/executors/secret-guardrails/secret-guardrails.js.map +1 -0
- package/src/executors/secret-metadata/schema.json +31 -0
- package/src/executors/secret-metadata/secret-metadata.d.ts +32 -0
- package/src/executors/secret-metadata/secret-metadata.js +131 -0
- package/src/executors/secret-metadata/secret-metadata.js.map +1 -0
- package/src/executors/sst-lifecycle/logging-preflight.d.ts +17 -0
- package/src/executors/sst-lifecycle/logging-preflight.js +196 -0
- package/src/executors/sst-lifecycle/logging-preflight.js.map +1 -0
- package/src/executors/sst-lifecycle/run-sst-command.d.ts +23 -3
- package/src/executors/sst-lifecycle/run-sst-command.js +113 -10
- package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
- package/src/executors/sst-lifecycle/schema.json +9 -5
- package/src/executors/sst-lifecycle/secret-preflight.d.ts +25 -0
- package/src/executors/sst-lifecycle/secret-preflight.js +119 -0
- package/src/executors/sst-lifecycle/secret-preflight.js.map +1 -0
- package/src/foundation/discovery.d.ts +17 -1
- package/src/foundation/discovery.js +16 -1
- package/src/foundation/discovery.js.map +1 -1
- package/src/foundation/index.d.ts +3 -0
- package/src/foundation/index.js +3 -0
- package/src/foundation/index.js.map +1 -1
- package/src/foundation/local-database-contract.d.ts +1 -0
- package/src/foundation/local-database-contract.js +3 -0
- package/src/foundation/local-database-contract.js.map +1 -1
- package/src/foundation/logging-preflight.d.ts +30 -0
- package/src/foundation/logging-preflight.js +138 -0
- package/src/foundation/logging-preflight.js.map +1 -0
- package/src/foundation/logging.d.ts +70 -0
- package/src/foundation/logging.js +133 -0
- package/src/foundation/logging.js.map +1 -0
- package/src/foundation/preflight.d.ts +9 -0
- package/src/foundation/preflight.js +58 -2
- package/src/foundation/preflight.js.map +1 -1
- package/src/foundation/provision-database-bindings.d.ts +9 -1
- package/src/foundation/provision-database-bindings.js +25 -5
- package/src/foundation/provision-database-bindings.js.map +1 -1
- package/src/foundation/provision-logical-database.d.ts +10 -2
- package/src/foundation/provision-logical-database.js +68 -113
- package/src/foundation/provision-logical-database.js.map +1 -1
- package/src/foundation/router-origin-proof.d.ts +2 -0
- package/src/foundation/router-origin-proof.js +25 -0
- package/src/foundation/router-origin-proof.js.map +1 -0
- package/src/foundation/stages.d.ts +1 -0
- package/src/foundation/stages.js +9 -1
- package/src/foundation/stages.js.map +1 -1
- package/src/frontend-security/index.d.ts +1 -0
- package/src/frontend-security/index.js +5 -0
- package/src/frontend-security/index.js.map +1 -0
- package/src/frontend-security/security-headers.d.ts +50 -0
- package/src/frontend-security/security-headers.js +418 -0
- package/src/frontend-security/security-headers.js.map +1 -0
- package/src/generators/feature-registry.d.ts +13 -0
- package/src/generators/feature-registry.js +73 -0
- package/src/generators/feature-registry.js.map +1 -0
- package/src/generators/function/function.d.ts +1 -1
- package/src/generators/function/function.js +51 -6
- package/src/generators/function/function.js.map +1 -1
- package/src/generators/function/schema.d.ts +1 -0
- package/src/generators/function/schema.json +6 -1
- package/src/generators/logging-registry.d.ts +13 -0
- package/src/generators/logging-registry.js +62 -0
- package/src/generators/logging-registry.js.map +1 -0
- package/src/generators/post-generation-install.d.ts +67 -0
- package/src/generators/post-generation-install.js +3308 -0
- package/src/generators/post-generation-install.js.map +1 -0
- package/src/generators/preset/generator.js +3 -0
- package/src/generators/preset/generator.js.map +1 -1
- package/src/generators/preset/tool-versions.d.ts +18 -0
- package/src/generators/preset/tool-versions.js +22 -0
- package/src/generators/preset/tool-versions.js.map +1 -0
- package/src/generators/preset/workspace-files-developer-agent.d.ts +14 -0
- package/src/generators/preset/workspace-files-developer-agent.js +511 -0
- package/src/generators/preset/workspace-files-developer-agent.js.map +1 -0
- package/src/generators/preset/workspace-files-foundation-workload-roles.d.ts +6 -0
- package/src/generators/preset/workspace-files-foundation-workload-roles.js +406 -0
- package/src/generators/preset/workspace-files-foundation-workload-roles.js.map +1 -0
- package/src/generators/preset/workspace-files-performance-cost.d.ts +66 -0
- package/src/generators/preset/workspace-files-performance-cost.js +866 -0
- package/src/generators/preset/workspace-files-performance-cost.js.map +1 -0
- package/src/generators/preset/workspace-files-performance-load.d.ts +73 -0
- package/src/generators/preset/workspace-files-performance-load.js +801 -0
- package/src/generators/preset/workspace-files-performance-load.js.map +1 -0
- package/src/generators/preset/workspace-files-workload-role-baseline.d.ts +9 -0
- package/src/generators/preset/workspace-files-workload-role-baseline.js +1102 -0
- package/src/generators/preset/workspace-files-workload-role-baseline.js.map +1 -0
- package/src/generators/preset/workspace-files.d.ts +14 -1
- package/src/generators/preset/workspace-files.js +8189 -245
- package/src/generators/preset/workspace-files.js.map +1 -1
- package/src/generators/repository-policy/repository-policy.js +3 -47
- package/src/generators/repository-policy/repository-policy.js.map +1 -1
- package/src/generators/slice/built-in-plan-api-contract-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-api-contract-files.js +2656 -0
- package/src/generators/slice/built-in-plan-api-contract-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-api-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-api-files.js +1186 -0
- package/src/generators/slice/built-in-plan-api-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-async-work-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-async-work-files.js +7903 -0
- package/src/generators/slice/built-in-plan-async-work-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-authorization-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-authorization-files.js +2314 -0
- package/src/generators/slice/built-in-plan-authorization-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-authorization-infrastructure-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +559 -0
- package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-deletion-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-deletion-files.js +1101 -0
- package/src/generators/slice/built-in-plan-data-deletion-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-export-infrastructure-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-export-infrastructure-files.js +249 -0
- package/src/generators/slice/built-in-plan-data-export-infrastructure-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-files.js +1530 -0
- package/src/generators/slice/built-in-plan-data-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-files.js +750 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.js +828 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-retention-job-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-retention-job-files.js +2604 -0
- package/src/generators/slice/built-in-plan-data-retention-job-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-retention-launcher-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-retention-launcher-files.js +144 -0
- package/src/generators/slice/built-in-plan-data-retention-launcher-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-rights-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-rights-files.js +4628 -0
- package/src/generators/slice/built-in-plan-data-rights-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-database-lifecycle-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1901 -0
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js +485 -0
- package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-foundation-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-foundation-files.js +184 -0
- package/src/generators/slice/built-in-plan-foundation-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-frontend-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-frontend-files.js +2233 -0
- package/src/generators/slice/built-in-plan-frontend-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.js +3038 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-infrastructure-safety-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-infrastructure-safety-files.js +312 -0
- package/src/generators/slice/built-in-plan-infrastructure-safety-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-metadata-files.d.ts +3 -0
- package/src/generators/slice/built-in-plan-metadata-files.js +219 -0
- package/src/generators/slice/built-in-plan-metadata-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-operations-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-operations-files.js +630 -0
- package/src/generators/slice/built-in-plan-operations-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-performance-files.d.ts +3 -0
- package/src/generators/slice/built-in-plan-performance-files.js +3293 -0
- package/src/generators/slice/built-in-plan-performance-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-project-files.d.ts +3 -0
- package/src/generators/slice/built-in-plan-project-files.js +979 -0
- package/src/generators/slice/built-in-plan-project-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-release-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-release-files.js +6387 -0
- package/src/generators/slice/built-in-plan-release-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-reliability-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-reliability-files.js +4499 -0
- package/src/generators/slice/built-in-plan-reliability-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-secret-runtime-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-secret-runtime-files.js +1004 -0
- package/src/generators/slice/built-in-plan-secret-runtime-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-shared.d.ts +53 -0
- package/src/generators/slice/built-in-plan-shared.js +113 -0
- package/src/generators/slice/built-in-plan-shared.js.map +1 -0
- package/src/generators/slice/built-in-plan-testing-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-testing-files.js +2356 -0
- package/src/generators/slice/built-in-plan-testing-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-verification-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-verification-files.js +358 -0
- package/src/generators/slice/built-in-plan-verification-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-web-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-web-files.js +430 -0
- package/src/generators/slice/built-in-plan-web-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-workload-role-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-workload-role-files.js +2356 -0
- package/src/generators/slice/built-in-plan-workload-role-files.js.map +1 -0
- package/src/generators/slice/built-in-plan.d.ts +8 -0
- package/src/generators/slice/built-in-plan.js +264 -0
- package/src/generators/slice/built-in-plan.js.map +1 -0
- package/src/generators/slice/schema.json +1 -1
- package/src/generators/slice/slice.d.ts +1 -22
- package/src/generators/slice/slice.js +185 -1510
- package/src/generators/slice/slice.js.map +1 -1
- package/src/generators/upgrade/schema.d.ts +2 -0
- package/src/generators/upgrade/schema.json +11 -1
- package/src/generators/upgrade/upgrade.d.ts +51 -1
- package/src/generators/upgrade/upgrade.js +820 -82
- package/src/generators/upgrade/upgrade.js.map +1 -1
- package/src/generators/workload/frozen-container.d.ts +8 -0
- package/src/generators/workload/frozen-container.js +36 -0
- package/src/generators/workload/frozen-container.js.map +1 -0
- package/src/generators/workload/performance-registration.d.ts +32 -0
- package/src/generators/workload/performance-registration.js +226 -0
- package/src/generators/workload/performance-registration.js.map +1 -0
- package/src/generators/workload/reliability-registration.d.ts +16 -0
- package/src/generators/workload/reliability-registration.js +150 -0
- package/src/generators/workload/reliability-registration.js.map +1 -0
- package/src/generators/workload/schema.d.ts +1 -0
- package/src/generators/workload/schema.json +6 -1
- package/src/generators/workload/service-performance-files.d.ts +4 -0
- package/src/generators/workload/service-performance-files.js +1846 -0
- package/src/generators/workload/service-performance-files.js.map +1 -0
- package/src/generators/workload/workload.d.ts +3 -2
- package/src/generators/workload/workload.js +1083 -18
- package/src/generators/workload/workload.js.map +1 -1
- package/src/git-policy/action-pins.d.ts +10 -0
- package/src/git-policy/action-pins.js +10 -0
- package/src/git-policy/action-pins.js.map +1 -1
- package/src/git-policy/github-bootstrap.d.ts +14 -1
- package/src/git-policy/github-bootstrap.js +274 -46
- package/src/git-policy/github-bootstrap.js.map +1 -1
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/index.js.map +1 -1
- package/src/infrastructure-change/convergence.d.ts +13 -0
- package/src/infrastructure-change/convergence.js +85 -0
- package/src/infrastructure-change/convergence.js.map +1 -0
- package/src/infrastructure-change/drift.d.ts +13 -0
- package/src/infrastructure-change/drift.js +112 -0
- package/src/infrastructure-change/drift.js.map +1 -0
- package/src/infrastructure-change/index.d.ts +7 -0
- package/src/infrastructure-change/index.js +11 -0
- package/src/infrastructure-change/index.js.map +1 -0
- package/src/infrastructure-change/model.d.ts +253 -0
- package/src/infrastructure-change/model.js +28 -0
- package/src/infrastructure-change/model.js.map +1 -0
- package/src/infrastructure-change/normalize.d.ts +12 -0
- package/src/infrastructure-change/normalize.js +245 -0
- package/src/infrastructure-change/normalize.js.map +1 -0
- package/src/infrastructure-change/plan.d.ts +40 -0
- package/src/infrastructure-change/plan.js +361 -0
- package/src/infrastructure-change/plan.js.map +1 -0
- package/src/infrastructure-change/risk.d.ts +7 -0
- package/src/infrastructure-change/risk.js +269 -0
- package/src/infrastructure-change/risk.js.map +1 -0
- package/src/infrastructure-change/summary.d.ts +20 -0
- package/src/infrastructure-change/summary.js +130 -0
- package/src/infrastructure-change/summary.js.map +1 -0
- package/src/local-dev/context-discovery.d.ts +2 -0
- package/src/local-dev/context-discovery.js +59 -0
- package/src/local-dev/context-discovery.js.map +1 -0
- package/src/local-dev/contract-validation.d.ts +15 -0
- package/src/local-dev/contract-validation.js +84 -0
- package/src/local-dev/contract-validation.js.map +1 -0
- package/src/local-dev/contracts.d.ts +10 -1
- package/src/local-dev/contracts.js +57 -5
- package/src/local-dev/contracts.js.map +1 -1
- package/src/local-dev/control-contract.d.ts +67 -0
- package/src/local-dev/control-contract.js +180 -0
- package/src/local-dev/control-contract.js.map +1 -0
- package/src/local-dev/dev-command-core.d.ts +60 -0
- package/src/local-dev/dev-command-core.js +89 -0
- package/src/local-dev/dev-command-core.js.map +1 -0
- package/src/local-dev/effects.d.ts +16 -0
- package/src/local-dev/effects.js +63 -0
- package/src/local-dev/effects.js.map +1 -0
- package/src/local-dev/environment-inspector.d.ts +125 -0
- package/src/local-dev/environment-inspector.js +687 -0
- package/src/local-dev/environment-inspector.js.map +1 -0
- package/src/local-dev/environment-state.d.ts +113 -0
- package/src/local-dev/environment-state.js +463 -0
- package/src/local-dev/environment-state.js.map +1 -0
- package/src/local-dev/errors.d.ts +6 -0
- package/src/local-dev/errors.js +41 -0
- package/src/local-dev/errors.js.map +1 -0
- package/src/local-dev/fleet-client.d.ts +33 -0
- package/src/local-dev/fleet-client.js +156 -0
- package/src/local-dev/fleet-client.js.map +1 -0
- package/src/local-dev/fleet-configuration.d.ts +13 -0
- package/src/local-dev/fleet-configuration.js +71 -0
- package/src/local-dev/fleet-configuration.js.map +1 -0
- package/src/local-dev/fleet-inventory.d.ts +56 -0
- package/src/local-dev/fleet-inventory.js +469 -0
- package/src/local-dev/fleet-inventory.js.map +1 -0
- package/src/local-dev/fleet-ipc.d.ts +28 -0
- package/src/local-dev/fleet-ipc.js +267 -0
- package/src/local-dev/fleet-ipc.js.map +1 -0
- package/src/local-dev/fleet-launcher.d.ts +53 -0
- package/src/local-dev/fleet-launcher.js +379 -0
- package/src/local-dev/fleet-launcher.js.map +1 -0
- package/src/local-dev/fleet-lock.d.ts +41 -0
- package/src/local-dev/fleet-lock.js +135 -0
- package/src/local-dev/fleet-lock.js.map +1 -0
- package/src/local-dev/fleet-logs.d.ts +66 -0
- package/src/local-dev/fleet-logs.js +542 -0
- package/src/local-dev/fleet-logs.js.map +1 -0
- package/src/local-dev/fleet-process.d.ts +50 -0
- package/src/local-dev/fleet-process.js +289 -0
- package/src/local-dev/fleet-process.js.map +1 -0
- package/src/local-dev/fleet-runtime.d.ts +22 -0
- package/src/local-dev/fleet-runtime.js +368 -0
- package/src/local-dev/fleet-runtime.js.map +1 -0
- package/src/local-dev/fleet-session.d.ts +70 -0
- package/src/local-dev/fleet-session.js +301 -0
- package/src/local-dev/fleet-session.js.map +1 -0
- package/src/local-dev/fleet-slice-runner-daemon.d.ts +1 -0
- package/src/local-dev/fleet-slice-runner-daemon.js +21 -0
- package/src/local-dev/fleet-slice-runner-daemon.js.map +1 -0
- package/src/local-dev/fleet-startup.d.ts +22 -0
- package/src/local-dev/fleet-startup.js +114 -0
- package/src/local-dev/fleet-startup.js.map +1 -0
- package/src/local-dev/fleet-state.d.ts +62 -0
- package/src/local-dev/fleet-state.js +297 -0
- package/src/local-dev/fleet-state.js.map +1 -0
- package/src/local-dev/fleet-supervisor-daemon.d.ts +1 -0
- package/src/local-dev/fleet-supervisor-daemon.js +41 -0
- package/src/local-dev/fleet-supervisor-daemon.js.map +1 -0
- package/src/local-dev/fleet-supervisor.d.ts +52 -0
- package/src/local-dev/fleet-supervisor.js +384 -0
- package/src/local-dev/fleet-supervisor.js.map +1 -0
- package/src/local-dev/gateway.js +1 -1
- package/src/local-dev/gateway.js.map +1 -1
- package/src/local-dev/identity.js +4 -6
- package/src/local-dev/identity.js.map +1 -1
- package/src/local-dev/index.d.ts +1 -0
- package/src/local-dev/index.js +1 -0
- package/src/local-dev/index.js.map +1 -1
- package/src/local-dev/local-context.d.ts +62 -0
- package/src/local-dev/local-context.js +299 -0
- package/src/local-dev/local-context.js.map +1 -0
- package/src/local-dev/nx-workspace.d.ts +45 -0
- package/src/local-dev/nx-workspace.js +228 -0
- package/src/local-dev/nx-workspace.js.map +1 -0
- package/src/local-dev/personal-secret-ledger.d.ts +75 -0
- package/src/local-dev/personal-secret-ledger.js +391 -0
- package/src/local-dev/personal-secret-ledger.js.map +1 -0
- package/src/local-dev/personal-stage.d.ts +18 -0
- package/src/local-dev/personal-stage.js +129 -0
- package/src/local-dev/personal-stage.js.map +1 -0
- package/src/local-dev/private-state.d.ts +17 -0
- package/src/local-dev/private-state.js +254 -0
- package/src/local-dev/private-state.js.map +1 -0
- package/src/local-dev/process-identity.d.ts +9 -0
- package/src/local-dev/process-identity.js +79 -0
- package/src/local-dev/process-identity.js.map +1 -0
- package/src/local-dev/runtime-paths.d.ts +17 -0
- package/src/local-dev/runtime-paths.js +33 -0
- package/src/local-dev/runtime-paths.js.map +1 -0
- package/src/local-dev/secret-slice-claim.d.ts +17 -0
- package/src/local-dev/secret-slice-claim.js +75 -0
- package/src/local-dev/secret-slice-claim.js.map +1 -0
- package/src/local-dev/slice-claim.d.ts +36 -0
- package/src/local-dev/slice-claim.js +199 -0
- package/src/local-dev/slice-claim.js.map +1 -0
- package/src/local-dev/slice-runner.d.ts +121 -0
- package/src/local-dev/slice-runner.js +715 -0
- package/src/local-dev/slice-runner.js.map +1 -0
- package/src/local-dev/tls.js +51 -17
- package/src/local-dev/tls.js.map +1 -1
- package/src/ownership/composition.d.ts +5 -2
- package/src/ownership/composition.js +26 -3
- package/src/ownership/composition.js.map +1 -1
- package/src/ownership/hash.d.ts +1 -1
- package/src/ownership/hash.js +3 -1
- package/src/ownership/hash.js.map +1 -1
- package/src/ownership/record.d.ts +32 -1
- package/src/ownership/record.js +139 -2
- package/src/ownership/record.js.map +1 -1
- package/src/performance/cost-collection.d.ts +225 -0
- package/src/performance/cost-collection.js +825 -0
- package/src/performance/cost-collection.js.map +1 -0
- package/src/performance/cost-controls.d.ts +131 -0
- package/src/performance/cost-controls.js +336 -0
- package/src/performance/cost-controls.js.map +1 -0
- package/src/performance/evidence.d.ts +140 -0
- package/src/performance/evidence.js +599 -0
- package/src/performance/evidence.js.map +1 -0
- package/src/performance/index.d.ts +10 -0
- package/src/performance/index.js +14 -0
- package/src/performance/index.js.map +1 -0
- package/src/performance/load-safety.d.ts +84 -0
- package/src/performance/load-safety.js +346 -0
- package/src/performance/load-safety.js.map +1 -0
- package/src/performance/model.d.ts +129 -0
- package/src/performance/model.js +452 -0
- package/src/performance/model.js.map +1 -0
- package/src/performance/pr-artifact-budget.d.ts +42 -0
- package/src/performance/pr-artifact-budget.js +151 -0
- package/src/performance/pr-artifact-budget.js.map +1 -0
- package/src/performance/runner.d.ts +75 -0
- package/src/performance/runner.js +227 -0
- package/src/performance/runner.js.map +1 -0
- package/src/performance/scenario-endpoint.d.ts +1 -0
- package/src/performance/scenario-endpoint.js +5 -0
- package/src/performance/scenario-endpoint.js.map +1 -0
- package/src/performance/scenario.d.ts +193 -0
- package/src/performance/scenario.js +736 -0
- package/src/performance/scenario.js.map +1 -0
- package/src/performance/test-fixtures.d.ts +7 -0
- package/src/performance/test-fixtures.js +169 -0
- package/src/performance/test-fixtures.js.map +1 -0
- package/src/performance/unit-cost.d.ts +105 -0
- package/src/performance/unit-cost.js +364 -0
- package/src/performance/unit-cost.js.map +1 -0
- package/src/release/index.d.ts +4 -0
- package/src/release/index.js +8 -0
- package/src/release/index.js.map +1 -0
- package/src/release/lifecycle.d.ts +132 -0
- package/src/release/lifecycle.js +411 -0
- package/src/release/lifecycle.js.map +1 -0
- package/src/release/manifest.d.ts +148 -0
- package/src/release/manifest.js +503 -0
- package/src/release/manifest.js.map +1 -0
- package/src/release/rollout.d.ts +170 -0
- package/src/release/rollout.js +578 -0
- package/src/release/rollout.js.map +1 -0
- package/src/release/source-authority.d.ts +19 -0
- package/src/release/source-authority.js +195 -0
- package/src/release/source-authority.js.map +1 -0
- package/src/repository-policy/deployment-plan.d.ts +2 -1
- package/src/repository-policy/deployment-plan.js +2 -0
- package/src/repository-policy/deployment-plan.js.map +1 -1
- package/src/repository-policy/policy.d.ts +1 -1
- package/src/repository-policy/policy.js +29 -25
- package/src/repository-policy/policy.js.map +1 -1
- package/src/schemas/aggregate-count-evidence-v1.json +135 -0
- package/src/schemas/api-contract-baseline-v1.json +674 -0
- package/src/schemas/api-contract-compatibility-report-v1.json +126 -0
- package/src/schemas/aws-billing-actual-evidence-v1.json +407 -0
- package/src/schemas/blueprint-v1.json +18 -1
- package/src/schemas/data-lifecycle-policy-v1.json +517 -0
- package/src/schemas/generated-ownership-v2.json +22 -0
- package/src/schemas/performance-policy-v1.json +335 -0
- package/src/schemas/repository-policy-v1.json +1 -0
- package/src/schemas/testing-evidence-quarantine-v1.json +168 -0
- package/src/schemas/testing-evidence-result-v1.json +319 -0
- package/src/secrets/adapters.d.ts +49 -0
- package/src/secrets/adapters.js +184 -0
- package/src/secrets/adapters.js.map +1 -0
- package/src/secrets/aws-control-plane.d.ts +20 -0
- package/src/secrets/aws-control-plane.js +255 -0
- package/src/secrets/aws-control-plane.js.map +1 -0
- package/src/secrets/declaration-verification.d.ts +11 -0
- package/src/secrets/declaration-verification.js +68 -0
- package/src/secrets/declaration-verification.js.map +1 -0
- package/src/secrets/guardrails.d.ts +26 -0
- package/src/secrets/guardrails.js +328 -0
- package/src/secrets/guardrails.js.map +1 -0
- package/src/secrets/index.d.ts +12 -0
- package/src/secrets/index.js +16 -0
- package/src/secrets/index.js.map +1 -0
- package/src/secrets/metadata.d.ts +48 -0
- package/src/secrets/metadata.js +171 -0
- package/src/secrets/metadata.js.map +1 -0
- package/src/secrets/model.d.ts +49 -0
- package/src/secrets/model.js +125 -0
- package/src/secrets/model.js.map +1 -0
- package/src/secrets/operator.d.ts +101 -0
- package/src/secrets/operator.js +229 -0
- package/src/secrets/operator.js.map +1 -0
- package/src/secrets/permissions.d.ts +34 -0
- package/src/secrets/permissions.js +86 -0
- package/src/secrets/permissions.js.map +1 -0
- package/src/secrets/personal-command.d.ts +83 -0
- package/src/secrets/personal-command.js +197 -0
- package/src/secrets/personal-command.js.map +1 -0
- package/src/secrets/personal-ledger.d.ts +21 -0
- package/src/secrets/personal-ledger.js +135 -0
- package/src/secrets/personal-ledger.js.map +1 -0
- package/src/secrets/registry.d.ts +9 -0
- package/src/secrets/registry.js +86 -0
- package/src/secrets/registry.js.map +1 -0
- package/src/secrets/verification.d.ts +18 -0
- package/src/secrets/verification.js +139 -0
- package/src/secrets/verification.js.map +1 -0
- package/src/secrets/workload-access.d.ts +46 -0
- package/src/secrets/workload-access.js +163 -0
- package/src/secrets/workload-access.js.map +1 -0
- package/src/supply-chain/action-pins.d.ts +26 -0
- package/src/supply-chain/action-pins.js +208 -0
- package/src/supply-chain/action-pins.js.map +1 -0
- package/src/supply-chain/blueprint-trust-git.d.ts +130 -0
- package/src/supply-chain/blueprint-trust-git.js +1581 -0
- package/src/supply-chain/blueprint-trust-git.js.map +1 -0
- package/src/supply-chain/blueprint-trust-process.d.ts +4 -0
- package/src/supply-chain/blueprint-trust-process.js +88 -0
- package/src/supply-chain/blueprint-trust-process.js.map +1 -0
- package/src/supply-chain/blueprint-trust.d.ts +354 -0
- package/src/supply-chain/blueprint-trust.js +1558 -0
- package/src/supply-chain/blueprint-trust.js.map +1 -0
- package/src/supply-chain/blueprint-verification-authority.d.ts +62 -0
- package/src/supply-chain/blueprint-verification-authority.js +588 -0
- package/src/supply-chain/blueprint-verification-authority.js.map +1 -0
- package/src/supply-chain/blueprint-verification-execution.d.ts +68 -0
- package/src/supply-chain/blueprint-verification-execution.js +624 -0
- package/src/supply-chain/blueprint-verification-execution.js.map +1 -0
- package/src/supply-chain/build-output.d.ts +35 -0
- package/src/supply-chain/build-output.js +398 -0
- package/src/supply-chain/build-output.js.map +1 -0
- package/src/supply-chain/common.d.ts +21 -0
- package/src/supply-chain/common.js +107 -0
- package/src/supply-chain/common.js.map +1 -0
- package/src/supply-chain/container-artifact-contract.d.ts +23 -0
- package/src/supply-chain/container-artifact-contract.js +208 -0
- package/src/supply-chain/container-artifact-contract.js.map +1 -0
- package/src/supply-chain/container-release.d.ts +119 -0
- package/src/supply-chain/container-release.js +496 -0
- package/src/supply-chain/container-release.js.map +1 -0
- package/src/supply-chain/ecr-applicability.d.ts +19 -0
- package/src/supply-chain/ecr-applicability.js +103 -0
- package/src/supply-chain/ecr-applicability.js.map +1 -0
- package/src/supply-chain/ecr-scanning.d.ts +83 -0
- package/src/supply-chain/ecr-scanning.js +321 -0
- package/src/supply-chain/ecr-scanning.js.map +1 -0
- package/src/supply-chain/ecs-deployed-image.d.ts +85 -0
- package/src/supply-chain/ecs-deployed-image.js +300 -0
- package/src/supply-chain/ecs-deployed-image.js.map +1 -0
- package/src/supply-chain/errors.d.ts +6 -0
- package/src/supply-chain/errors.js +15 -0
- package/src/supply-chain/errors.js.map +1 -0
- package/src/supply-chain/generated-blueprint-trust-command.d.ts +4 -0
- package/src/supply-chain/generated-blueprint-trust-command.js +1362 -0
- package/src/supply-chain/generated-blueprint-trust-command.js.map +1 -0
- package/src/supply-chain/generated-command.d.ts +1 -0
- package/src/supply-chain/generated-command.js +687 -0
- package/src/supply-chain/generated-command.js.map +1 -0
- package/src/supply-chain/generated-container-release-command.d.ts +16 -0
- package/src/supply-chain/generated-container-release-command.js +2087 -0
- package/src/supply-chain/generated-container-release-command.js.map +1 -0
- package/src/supply-chain/generated-ecr-collector-command.d.ts +21 -0
- package/src/supply-chain/generated-ecr-collector-command.js +1338 -0
- package/src/supply-chain/generated-ecr-collector-command.js.map +1 -0
- package/src/supply-chain/generated-files.d.ts +13 -0
- package/src/supply-chain/generated-files.js +401 -0
- package/src/supply-chain/generated-files.js.map +1 -0
- package/src/supply-chain/generated-scanner-command.d.ts +1 -0
- package/src/supply-chain/generated-scanner-command.js +718 -0
- package/src/supply-chain/generated-scanner-command.js.map +1 -0
- package/src/supply-chain/index.d.ts +28 -0
- package/src/supply-chain/index.js +35 -0
- package/src/supply-chain/index.js.map +1 -0
- package/src/supply-chain/inventory.d.ts +79 -0
- package/src/supply-chain/inventory.js +717 -0
- package/src/supply-chain/inventory.js.map +1 -0
- package/src/supply-chain/license-expression.d.ts +7 -0
- package/src/supply-chain/license-expression.js +128 -0
- package/src/supply-chain/license-expression.js.map +1 -0
- package/src/supply-chain/pnpm-toolchain-authority.d.ts +31 -0
- package/src/supply-chain/pnpm-toolchain-authority.js +316 -0
- package/src/supply-chain/pnpm-toolchain-authority.js.map +1 -0
- package/src/supply-chain/policy.d.ts +105 -0
- package/src/supply-chain/policy.js +301 -0
- package/src/supply-chain/policy.js.map +1 -0
- package/src/supply-chain/publication.d.ts +48 -0
- package/src/supply-chain/publication.js +85 -0
- package/src/supply-chain/publication.js.map +1 -0
- package/src/supply-chain/sbom.d.ts +228 -0
- package/src/supply-chain/sbom.js +826 -0
- package/src/supply-chain/sbom.js.map +1 -0
- package/src/supply-chain/scanner.d.ts +77 -0
- package/src/supply-chain/scanner.js +502 -0
- package/src/supply-chain/scanner.js.map +1 -0
- package/src/supply-chain/vendored-source.d.ts +95 -0
- package/src/supply-chain/vendored-source.js +315 -0
- package/src/supply-chain/vendored-source.js.map +1 -0
- package/src/testing-evidence/fixture-scope.d.ts +52 -0
- package/src/testing-evidence/fixture-scope.js +140 -0
- package/src/testing-evidence/fixture-scope.js.map +1 -0
- package/src/testing-evidence/index.d.ts +5 -0
- package/src/testing-evidence/index.js +9 -0
- package/src/testing-evidence/index.js.map +1 -0
- package/src/testing-evidence/quarantine.d.ts +48 -0
- package/src/testing-evidence/quarantine.js +253 -0
- package/src/testing-evidence/quarantine.js.map +1 -0
- package/src/testing-evidence/result.d.ts +108 -0
- package/src/testing-evidence/result.js +571 -0
- package/src/testing-evidence/result.js.map +1 -0
- package/src/testing-evidence/runner-adapter.d.ts +51 -0
- package/src/testing-evidence/runner-adapter.js +288 -0
- package/src/testing-evidence/runner-adapter.js.map +1 -0
- package/src/testing-evidence/sensitive-artifacts.d.ts +13 -0
- package/src/testing-evidence/sensitive-artifacts.js +93 -0
- package/src/testing-evidence/sensitive-artifacts.js.map +1 -0
- package/src/work-markers/work-markers.js +4 -3
- package/src/work-markers/work-markers.js.map +1 -1
- package/src/workload-roles/account-baseline.d.ts +132 -0
- package/src/workload-roles/account-baseline.js +743 -0
- package/src/workload-roles/account-baseline.js.map +1 -0
- package/src/workload-roles/authority.d.ts +38 -0
- package/src/workload-roles/authority.js +156 -0
- package/src/workload-roles/authority.js.map +1 -0
- package/src/workload-roles/aws-adapter.d.ts +99 -0
- package/src/workload-roles/aws-adapter.js +416 -0
- package/src/workload-roles/aws-adapter.js.map +1 -0
- package/src/workload-roles/aws-internal-handler.d.ts +27 -0
- package/src/workload-roles/aws-internal-handler.js +58 -0
- package/src/workload-roles/aws-internal-handler.js.map +1 -0
- package/src/workload-roles/aws-s3-state.d.ts +52 -0
- package/src/workload-roles/aws-s3-state.js +164 -0
- package/src/workload-roles/aws-s3-state.js.map +1 -0
- package/src/workload-roles/aws-sdk-control-plane.d.ts +12 -0
- package/src/workload-roles/aws-sdk-control-plane.js +291 -0
- package/src/workload-roles/aws-sdk-control-plane.js.map +1 -0
- package/src/workload-roles/boundary.d.ts +33 -0
- package/src/workload-roles/boundary.js +130 -0
- package/src/workload-roles/boundary.js.map +1 -0
- package/src/workload-roles/convergence.d.ts +66 -0
- package/src/workload-roles/convergence.js +186 -0
- package/src/workload-roles/convergence.js.map +1 -0
- package/src/workload-roles/effective-policy.d.ts +51 -0
- package/src/workload-roles/effective-policy.js +183 -0
- package/src/workload-roles/effective-policy.js.map +1 -0
- package/src/workload-roles/handler.d.ts +43 -0
- package/src/workload-roles/handler.js +130 -0
- package/src/workload-roles/handler.js.map +1 -0
- package/src/workload-roles/history.d.ts +47 -0
- package/src/workload-roles/history.js +324 -0
- package/src/workload-roles/history.js.map +1 -0
- package/src/workload-roles/index.d.ts +17 -0
- package/src/workload-roles/index.js +21 -0
- package/src/workload-roles/index.js.map +1 -0
- package/src/workload-roles/injection.d.ts +119 -0
- package/src/workload-roles/injection.js +495 -0
- package/src/workload-roles/injection.js.map +1 -0
- package/src/workload-roles/manifest.d.ts +54 -0
- package/src/workload-roles/manifest.js +343 -0
- package/src/workload-roles/manifest.js.map +1 -0
- package/src/workload-roles/provisioner.d.ts +70 -0
- package/src/workload-roles/provisioner.js +231 -0
- package/src/workload-roles/provisioner.js.map +1 -0
- package/src/workload-roles/registration.d.ts +101 -0
- package/src/workload-roles/registration.js +457 -0
- package/src/workload-roles/registration.js.map +1 -0
- package/src/workload-roles/stage-boundary.d.ts +31 -0
- package/src/workload-roles/stage-boundary.js +208 -0
- package/src/workload-roles/stage-boundary.js.map +1 -0
- package/src/workload-roles/standard-slice.d.ts +54 -0
- package/src/workload-roles/standard-slice.js +252 -0
- package/src/workload-roles/standard-slice.js.map +1 -0
- package/migrations/standard-pre-v1-to-v1.md +0 -11
- package/src/foundation/aurora-logical-database.d.ts +0 -76
- package/src/foundation/aurora-logical-database.js +0 -419
- package/src/foundation/aurora-logical-database.js.map +0 -1
|
@@ -0,0 +1,1901 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderDatabaseLifecycleFiles = renderDatabaseLifecycleFiles;
|
|
4
|
+
const built_in_plan_shared_js_1 = require("./built-in-plan-shared.js");
|
|
5
|
+
const frozen_container_js_1 = require("../workload/frozen-container.js");
|
|
6
|
+
function renderDatabaseLifecycleLoggingHelper() {
|
|
7
|
+
return `export const DATABASE_LIFECYCLE_EVENT_CONTRACT = Object.freeze({
|
|
8
|
+
database_bootstrap_client_error: Object.freeze({
|
|
9
|
+
level: 'ERROR',
|
|
10
|
+
message: 'Database bootstrap client error',
|
|
11
|
+
}),
|
|
12
|
+
database_bootstrap_connection_retry: Object.freeze({
|
|
13
|
+
level: 'WARN',
|
|
14
|
+
message: 'Retrying database bootstrap connection',
|
|
15
|
+
}),
|
|
16
|
+
database_bootstrap_succeeded: Object.freeze({
|
|
17
|
+
level: 'INFO',
|
|
18
|
+
message: 'Database bootstrap succeeded',
|
|
19
|
+
outcome: 'success',
|
|
20
|
+
}),
|
|
21
|
+
database_bootstrap_failed: Object.freeze({
|
|
22
|
+
level: 'ERROR',
|
|
23
|
+
message: 'Database bootstrap failed',
|
|
24
|
+
outcome: 'failure',
|
|
25
|
+
}),
|
|
26
|
+
database_retirement_client_error: Object.freeze({
|
|
27
|
+
level: 'ERROR',
|
|
28
|
+
message: 'Database retirement client error',
|
|
29
|
+
}),
|
|
30
|
+
database_retirement_succeeded: Object.freeze({
|
|
31
|
+
level: 'INFO',
|
|
32
|
+
message: 'Database retirement succeeded',
|
|
33
|
+
outcome: 'success',
|
|
34
|
+
}),
|
|
35
|
+
database_retirement_failed: Object.freeze({
|
|
36
|
+
level: 'ERROR',
|
|
37
|
+
message: 'Database retirement failed',
|
|
38
|
+
outcome: 'failure',
|
|
39
|
+
}),
|
|
40
|
+
database_connection_retry: Object.freeze({
|
|
41
|
+
level: 'WARN',
|
|
42
|
+
message: 'Retrying database migration connection',
|
|
43
|
+
}),
|
|
44
|
+
database_client_error: Object.freeze({
|
|
45
|
+
level: 'ERROR',
|
|
46
|
+
message: 'Database migration client error',
|
|
47
|
+
}),
|
|
48
|
+
migration_skipped: Object.freeze({
|
|
49
|
+
level: 'INFO',
|
|
50
|
+
message: 'Database migration skipped',
|
|
51
|
+
}),
|
|
52
|
+
migration_applied: Object.freeze({
|
|
53
|
+
level: 'INFO',
|
|
54
|
+
message: 'Database migration applied',
|
|
55
|
+
}),
|
|
56
|
+
migration_rollback_failed: Object.freeze({
|
|
57
|
+
level: 'ERROR',
|
|
58
|
+
message: 'Database migration rollback failed',
|
|
59
|
+
outcome: 'failure',
|
|
60
|
+
}),
|
|
61
|
+
migration_completed: Object.freeze({
|
|
62
|
+
level: 'INFO',
|
|
63
|
+
message: 'Database migration completed',
|
|
64
|
+
outcome: 'success',
|
|
65
|
+
}),
|
|
66
|
+
migration_failed: Object.freeze({
|
|
67
|
+
level: 'ERROR',
|
|
68
|
+
message: 'Database migration failed',
|
|
69
|
+
outcome: 'failure',
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const LOG_RECORD_LIMIT_BYTES = 16 * 1024;
|
|
74
|
+
const ATTRIBUTE_STRING_LIMIT = 256;
|
|
75
|
+
const SAFE_ATTRIBUTE_KEYS = new Set([
|
|
76
|
+
'attempt',
|
|
77
|
+
'checksum',
|
|
78
|
+
'count',
|
|
79
|
+
'database',
|
|
80
|
+
'delayMs',
|
|
81
|
+
'elapsedMs',
|
|
82
|
+
'errorCode',
|
|
83
|
+
'errorName',
|
|
84
|
+
'name',
|
|
85
|
+
'phase',
|
|
86
|
+
'roleKind',
|
|
87
|
+
]);
|
|
88
|
+
const OPAQUE_ID_PATTERN = /^[A-Za-z0-9._:/-]{1,128}$/;
|
|
89
|
+
|
|
90
|
+
const requiredIdentity = (value, field) => {
|
|
91
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
92
|
+
if (
|
|
93
|
+
normalized.length === 0 ||
|
|
94
|
+
normalized.length > 128 ||
|
|
95
|
+
[...normalized].some((character) => {
|
|
96
|
+
const code = character.charCodeAt(0);
|
|
97
|
+
return code <= 31 || code === 127;
|
|
98
|
+
})
|
|
99
|
+
) {
|
|
100
|
+
throw new Error('Database lifecycle logger ' + field + ' is invalid.');
|
|
101
|
+
}
|
|
102
|
+
return normalized;
|
|
103
|
+
};
|
|
104
|
+
const requiredOpaqueId = (value, field) => {
|
|
105
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
106
|
+
if (!OPAQUE_ID_PATTERN.test(normalized)) {
|
|
107
|
+
throw new Error('Database lifecycle logger ' + field + ' is invalid.');
|
|
108
|
+
}
|
|
109
|
+
return normalized;
|
|
110
|
+
};
|
|
111
|
+
const boundedString = (value, truncation) => {
|
|
112
|
+
if (value.length <= ATTRIBUTE_STRING_LIMIT) return value;
|
|
113
|
+
truncation.truncatedStrings += 1;
|
|
114
|
+
const suffix = '…[truncated:' + (value.length - ATTRIBUTE_STRING_LIMIT) + ']';
|
|
115
|
+
return value.slice(0, ATTRIBUTE_STRING_LIMIT - suffix.length) + suffix;
|
|
116
|
+
};
|
|
117
|
+
const safeAttributes = (fields) => {
|
|
118
|
+
const attributes = {};
|
|
119
|
+
const truncation = { droppedKeys: 0, truncatedStrings: 0 };
|
|
120
|
+
let keys;
|
|
121
|
+
try {
|
|
122
|
+
keys = Object.keys(fields).sort();
|
|
123
|
+
} catch {
|
|
124
|
+
keys = [];
|
|
125
|
+
truncation.droppedKeys += 1;
|
|
126
|
+
}
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
if (!SAFE_ATTRIBUTE_KEYS.has(key)) {
|
|
129
|
+
truncation.droppedKeys += 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
let value;
|
|
133
|
+
try {
|
|
134
|
+
value = fields[key];
|
|
135
|
+
} catch {
|
|
136
|
+
truncation.droppedKeys += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (typeof value === 'string') {
|
|
140
|
+
attributes[key] = boundedString(value, truncation);
|
|
141
|
+
} else if (typeof value === 'number' && Number.isFinite(value)) {
|
|
142
|
+
attributes[key] = value;
|
|
143
|
+
} else {
|
|
144
|
+
truncation.droppedKeys += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (truncation.droppedKeys || truncation.truncatedStrings) {
|
|
148
|
+
attributes.ebkTruncation = {
|
|
149
|
+
...(truncation.droppedKeys
|
|
150
|
+
? { droppedKeys: truncation.droppedKeys }
|
|
151
|
+
: {}),
|
|
152
|
+
...(truncation.truncatedStrings
|
|
153
|
+
? { truncatedStrings: truncation.truncatedStrings }
|
|
154
|
+
: {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return attributes;
|
|
158
|
+
};
|
|
159
|
+
const timestamp = (now) => {
|
|
160
|
+
try {
|
|
161
|
+
const value = now();
|
|
162
|
+
if (!Number.isNaN(value.getTime())) return value.toISOString();
|
|
163
|
+
} catch {
|
|
164
|
+
// Logging never replaces the database lifecycle result.
|
|
165
|
+
}
|
|
166
|
+
return new Date().toISOString();
|
|
167
|
+
};
|
|
168
|
+
const encodedBytes = (record) =>
|
|
169
|
+
Buffer.byteLength(JSON.stringify(record), 'utf8');
|
|
170
|
+
const fitRecord = (record) => {
|
|
171
|
+
const attributes = { ...record.attributes };
|
|
172
|
+
let result = { ...record, attributes };
|
|
173
|
+
while (encodedBytes(result) > LOG_RECORD_LIMIT_BYTES) {
|
|
174
|
+
const removable = Object.keys(attributes)
|
|
175
|
+
.filter(
|
|
176
|
+
(key) =>
|
|
177
|
+
key !== 'databaseLifecycleId' &&
|
|
178
|
+
key !== 'ebkTruncation' &&
|
|
179
|
+
key !== 'lifecycleEvent',
|
|
180
|
+
)
|
|
181
|
+
.sort()
|
|
182
|
+
.at(-1);
|
|
183
|
+
if (!removable) break;
|
|
184
|
+
delete attributes[removable];
|
|
185
|
+
const truncation = attributes.ebkTruncation ?? {};
|
|
186
|
+
attributes.ebkTruncation = {
|
|
187
|
+
...truncation,
|
|
188
|
+
recordKeysDropped: (truncation.recordKeysDropped ?? 0) + 1,
|
|
189
|
+
};
|
|
190
|
+
result = { ...record, attributes };
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const createDatabaseLifecycleLogger = ({
|
|
196
|
+
component,
|
|
197
|
+
correlationId,
|
|
198
|
+
databaseLifecycleId,
|
|
199
|
+
now = () => new Date(),
|
|
200
|
+
operation,
|
|
201
|
+
slice,
|
|
202
|
+
stage,
|
|
203
|
+
streams = { stderr: process.stderr, stdout: process.stdout },
|
|
204
|
+
workspace,
|
|
205
|
+
}) => {
|
|
206
|
+
const identity = {
|
|
207
|
+
component: requiredIdentity(component, 'component'),
|
|
208
|
+
correlationId: requiredOpaqueId(correlationId, 'correlationId'),
|
|
209
|
+
databaseLifecycleId: requiredOpaqueId(
|
|
210
|
+
databaseLifecycleId,
|
|
211
|
+
'databaseLifecycleId',
|
|
212
|
+
),
|
|
213
|
+
operation: requiredIdentity(operation, 'operation'),
|
|
214
|
+
slice: requiredIdentity(slice, 'slice'),
|
|
215
|
+
stage: requiredIdentity(stage, 'stage'),
|
|
216
|
+
workspace: requiredIdentity(workspace, 'workspace'),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
emit(lifecycleEvent, fields = {}) {
|
|
221
|
+
if (
|
|
222
|
+
!Object.prototype.hasOwnProperty.call(
|
|
223
|
+
DATABASE_LIFECYCLE_EVENT_CONTRACT,
|
|
224
|
+
lifecycleEvent,
|
|
225
|
+
)
|
|
226
|
+
) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
'Unsupported database lifecycle event: ' + String(lifecycleEvent),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const definition = DATABASE_LIFECYCLE_EVENT_CONTRACT[lifecycleEvent];
|
|
232
|
+
const record = fitRecord({
|
|
233
|
+
schemaVersion: 1,
|
|
234
|
+
timestamp: timestamp(now),
|
|
235
|
+
level: definition.level,
|
|
236
|
+
message: definition.message,
|
|
237
|
+
workspace: identity.workspace,
|
|
238
|
+
stage: identity.stage,
|
|
239
|
+
slice: identity.slice,
|
|
240
|
+
feature: 'database-lifecycle',
|
|
241
|
+
component: identity.component,
|
|
242
|
+
operation: identity.operation,
|
|
243
|
+
...(definition.outcome ? { outcome: definition.outcome } : {}),
|
|
244
|
+
correlationId: identity.correlationId,
|
|
245
|
+
attributes: {
|
|
246
|
+
databaseLifecycleId: identity.databaseLifecycleId,
|
|
247
|
+
lifecycleEvent,
|
|
248
|
+
...safeAttributes(fields),
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
const stream =
|
|
252
|
+
definition.level === 'WARN' ||
|
|
253
|
+
definition.level === 'ERROR' ||
|
|
254
|
+
definition.level === 'FATAL'
|
|
255
|
+
? streams.stderr
|
|
256
|
+
: streams.stdout;
|
|
257
|
+
try {
|
|
258
|
+
stream.write(JSON.stringify(record) + '\\n');
|
|
259
|
+
} catch {
|
|
260
|
+
// Logging never replaces the database lifecycle result.
|
|
261
|
+
}
|
|
262
|
+
return record;
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
`;
|
|
267
|
+
}
|
|
268
|
+
/** @internal */
|
|
269
|
+
function renderDatabaseLifecycleFiles(name) {
|
|
270
|
+
return {
|
|
271
|
+
'.dockerignore': `.sst
|
|
272
|
+
.env
|
|
273
|
+
.env.*
|
|
274
|
+
!.env.example
|
|
275
|
+
dist
|
|
276
|
+
node_modules
|
|
277
|
+
web/.next
|
|
278
|
+
web/.open-next
|
|
279
|
+
`,
|
|
280
|
+
'database/bindings.json': (0, built_in_plan_shared_js_1.json)({
|
|
281
|
+
primary: {
|
|
282
|
+
active: 'primary',
|
|
283
|
+
provisioned: ['primary'],
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
'migration/Dockerfile': `FROM ${frozen_container_js_1.WORKLOAD_CONTAINER_NODE_IMAGE} AS build
|
|
287
|
+
ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org
|
|
288
|
+
ENV COREPACK_NPM_REGISTRY=$NPM_CONFIG_REGISTRY
|
|
289
|
+
ENV NPM_CONFIG_REGISTRY=$NPM_CONFIG_REGISTRY
|
|
290
|
+
ENV PNPM_HOME=/pnpm
|
|
291
|
+
ENV PATH=/pnpm:$PATH
|
|
292
|
+
WORKDIR /workspace
|
|
293
|
+
RUN corepack enable && corepack prepare ${frozen_container_js_1.WORKLOAD_CONTAINER_PNPM_DESCRIPTOR} --activate
|
|
294
|
+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
295
|
+
RUN pnpm fetch --frozen-lockfile
|
|
296
|
+
COPY packages/${name}/migration/package.json packages/${name}/migration/package.json
|
|
297
|
+
RUN pnpm --filter @ebk-migration/${name} install --offline --frozen-lockfile
|
|
298
|
+
RUN --network=none pnpm --config.inject-workspace-packages=true --config.store-dir=/workspace/.ebk/cache/pnpm-store --filter @ebk-migration/${name} deploy --prod --offline --frozen-lockfile /opt/migration
|
|
299
|
+
|
|
300
|
+
FROM ${frozen_container_js_1.WORKLOAD_CONTAINER_NODE_IMAGE}
|
|
301
|
+
WORKDIR /app
|
|
302
|
+
ADD --checksum=sha256:e5bb2084ccf45087bda1c9bffdea0eb15ee67f0b91646106e466714f9de3c7e3 https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem ./rds-global-bundle.pem
|
|
303
|
+
COPY --from=build --chown=node:node /opt/migration ./
|
|
304
|
+
COPY --chown=node:node packages/${name}/jobs/database-lifecycle-logging.mjs ./jobs/database-lifecycle-logging.mjs
|
|
305
|
+
COPY --chown=node:node packages/${name}/jobs/bootstrap-database.mjs ./jobs/bootstrap-database.mjs
|
|
306
|
+
COPY --chown=node:node packages/${name}/jobs/migrate-database.mjs ./jobs/migrate-database.mjs
|
|
307
|
+
COPY --chown=node:node packages/${name}/jobs/retire-database.mjs ./jobs/retire-database.mjs
|
|
308
|
+
COPY --chown=node:node packages/${name}/database/migrations ./database/migrations
|
|
309
|
+
USER node
|
|
310
|
+
CMD ["node", "jobs/migrate-database.mjs"]
|
|
311
|
+
`,
|
|
312
|
+
'migration/package.json': (0, built_in_plan_shared_js_1.json)({
|
|
313
|
+
name: `@ebk-migration/${name}`,
|
|
314
|
+
version: '0.0.0',
|
|
315
|
+
private: true,
|
|
316
|
+
type: 'module',
|
|
317
|
+
dependencies: {
|
|
318
|
+
'@aws-sdk/client-secrets-manager': '3.1090.0',
|
|
319
|
+
'@aws-sdk/client-sfn': '3.1090.0',
|
|
320
|
+
'@aws-sdk/rds-signer': '3.1090.0',
|
|
321
|
+
pg: '8.22.0',
|
|
322
|
+
},
|
|
323
|
+
}),
|
|
324
|
+
'jobs/database-lifecycle-logging.mjs': renderDatabaseLifecycleLoggingHelper(),
|
|
325
|
+
'jobs/bootstrap-database.mjs': `import { randomBytes } from 'node:crypto';
|
|
326
|
+
import { readFile } from 'node:fs/promises';
|
|
327
|
+
import {
|
|
328
|
+
GetSecretValueCommand,
|
|
329
|
+
PutSecretValueCommand,
|
|
330
|
+
SecretsManagerClient,
|
|
331
|
+
} from '@aws-sdk/client-secrets-manager';
|
|
332
|
+
import { Signer } from '@aws-sdk/rds-signer';
|
|
333
|
+
import pg from 'pg';
|
|
334
|
+
import { createDatabaseLifecycleLogger } from './database-lifecycle-logging.mjs';
|
|
335
|
+
|
|
336
|
+
const { Client } = pg;
|
|
337
|
+
const required = (name) => {
|
|
338
|
+
const value = process.env[name]?.trim();
|
|
339
|
+
if (!value) throw new Error(name + ' is required.');
|
|
340
|
+
return value;
|
|
341
|
+
};
|
|
342
|
+
const requiredIdentifier = (name) => {
|
|
343
|
+
const value = required(name);
|
|
344
|
+
if (!/^[a-z][a-z0-9_]{0,62}$/.test(value)) {
|
|
345
|
+
throw new Error(name + ' must be a normalized PostgreSQL identifier.');
|
|
346
|
+
}
|
|
347
|
+
return value;
|
|
348
|
+
};
|
|
349
|
+
const requiredPort = () => {
|
|
350
|
+
const value = required('EBK_DATABASE_PORT');
|
|
351
|
+
if (!/^\\d+$/.test(value)) {
|
|
352
|
+
throw new Error('EBK_DATABASE_PORT must be an integer.');
|
|
353
|
+
}
|
|
354
|
+
const port = Number(value);
|
|
355
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
|
|
356
|
+
throw new Error('EBK_DATABASE_PORT must be from 1 through 65535.');
|
|
357
|
+
}
|
|
358
|
+
return port;
|
|
359
|
+
};
|
|
360
|
+
let bootstrapId = 'unresolved';
|
|
361
|
+
let bootstrapPhase = 'configuration';
|
|
362
|
+
let bootstrapLockClient;
|
|
363
|
+
let bootstrapLockKey;
|
|
364
|
+
let lifecycleLogger;
|
|
365
|
+
|
|
366
|
+
const log = (event, fields = {}) => lifecycleLogger?.emit(event, fields);
|
|
367
|
+
const safeErrorFields = (error) => {
|
|
368
|
+
const name = error instanceof Error ? error.name : 'UnknownError';
|
|
369
|
+
const code =
|
|
370
|
+
error &&
|
|
371
|
+
typeof error === 'object' &&
|
|
372
|
+
typeof error.code === 'string' &&
|
|
373
|
+
/^[A-Z0-9_]{2,32}$/.test(error.code)
|
|
374
|
+
? error.code
|
|
375
|
+
: undefined;
|
|
376
|
+
return {
|
|
377
|
+
errorName: name.replace(/[^A-Za-z0-9_.-]/g, '').slice(0, 80) || 'Error',
|
|
378
|
+
...(code ? { errorCode: code } : {}),
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
const retryableConnection = (error) => {
|
|
382
|
+
const code = error && typeof error === 'object' ? error.code : undefined;
|
|
383
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
384
|
+
return new Set([
|
|
385
|
+
'08000',
|
|
386
|
+
'08001',
|
|
387
|
+
'08003',
|
|
388
|
+
'08006',
|
|
389
|
+
'57P03',
|
|
390
|
+
'ECONNREFUSED',
|
|
391
|
+
'ECONNRESET',
|
|
392
|
+
'ETIMEDOUT',
|
|
393
|
+
]).has(code) ||
|
|
394
|
+
/resum|starting up|temporarily unavailable|timeout|connection terminated unexpectedly|server closed the connection unexpectedly|terminating connection/i.test(message);
|
|
395
|
+
};
|
|
396
|
+
const sleep = (milliseconds) =>
|
|
397
|
+
new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
398
|
+
const quoteIdentifier = (value) => {
|
|
399
|
+
if (!/^[a-z][a-z0-9_]{0,62}$/.test(value)) {
|
|
400
|
+
throw new Error('Unsafe PostgreSQL identifier.');
|
|
401
|
+
}
|
|
402
|
+
return '"' + value + '"';
|
|
403
|
+
};
|
|
404
|
+
const quoteLiteral = (value) => "'" + value.replaceAll("'", "''") + "'";
|
|
405
|
+
const generatePassword = () =>
|
|
406
|
+
randomBytes(24)
|
|
407
|
+
.toString('base64')
|
|
408
|
+
.replace(/[^A-Za-z0-9]/g, '')
|
|
409
|
+
.slice(0, 32)
|
|
410
|
+
.padEnd(32, 'a');
|
|
411
|
+
|
|
412
|
+
const secrets = new SecretsManagerClient({});
|
|
413
|
+
const readJsonSecret = async (secretArn, missingAllowed = false) => {
|
|
414
|
+
try {
|
|
415
|
+
const result = await secrets.send(
|
|
416
|
+
new GetSecretValueCommand({ SecretId: secretArn }),
|
|
417
|
+
);
|
|
418
|
+
if (!result.SecretString) {
|
|
419
|
+
throw new Error('Secret must contain a JSON SecretString.');
|
|
420
|
+
}
|
|
421
|
+
const value = JSON.parse(result.SecretString);
|
|
422
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
423
|
+
throw new Error('Secret JSON must be an object.');
|
|
424
|
+
}
|
|
425
|
+
return value;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (
|
|
428
|
+
missingAllowed &&
|
|
429
|
+
error &&
|
|
430
|
+
typeof error === 'object' &&
|
|
431
|
+
error.name === 'ResourceNotFoundException'
|
|
432
|
+
) {
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const runtimeCredential = async ({
|
|
439
|
+
database,
|
|
440
|
+
host,
|
|
441
|
+
port,
|
|
442
|
+
rotationToken,
|
|
443
|
+
secretArn,
|
|
444
|
+
username,
|
|
445
|
+
}) => {
|
|
446
|
+
const current = await readJsonSecret(secretArn, true);
|
|
447
|
+
const identityMatches =
|
|
448
|
+
current &&
|
|
449
|
+
current.database === database &&
|
|
450
|
+
current.username === username &&
|
|
451
|
+
typeof current.password === 'string' &&
|
|
452
|
+
/^[A-Za-z0-9]{32}$/.test(current.password);
|
|
453
|
+
if (current && !identityMatches) {
|
|
454
|
+
throw new Error('Existing generated database credential has incompatible metadata.');
|
|
455
|
+
}
|
|
456
|
+
const rotate =
|
|
457
|
+
rotationToken !== '' && current?.rotationToken !== rotationToken;
|
|
458
|
+
const connectionMetadataChanged =
|
|
459
|
+
current && (current.host !== host || current.port !== port);
|
|
460
|
+
return {
|
|
461
|
+
needsWrite: !current || rotate || connectionMetadataChanged,
|
|
462
|
+
password: !current || rotate ? generatePassword() : current.password,
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
const writeRuntimeCredential = async ({
|
|
466
|
+
database,
|
|
467
|
+
host,
|
|
468
|
+
password,
|
|
469
|
+
port,
|
|
470
|
+
rotationToken,
|
|
471
|
+
secretArn,
|
|
472
|
+
username,
|
|
473
|
+
}) => {
|
|
474
|
+
await secrets.send(
|
|
475
|
+
new PutSecretValueCommand({
|
|
476
|
+
SecretId: secretArn,
|
|
477
|
+
SecretString: JSON.stringify({
|
|
478
|
+
database,
|
|
479
|
+
host,
|
|
480
|
+
password,
|
|
481
|
+
port,
|
|
482
|
+
...(rotationToken ? { rotationToken } : {}),
|
|
483
|
+
username,
|
|
484
|
+
}),
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
487
|
+
};
|
|
488
|
+
const publishRuntimeCredentials = async ({
|
|
489
|
+
database,
|
|
490
|
+
host,
|
|
491
|
+
port,
|
|
492
|
+
readonly,
|
|
493
|
+
readonlySecretArn,
|
|
494
|
+
readonlyRole,
|
|
495
|
+
readwrite,
|
|
496
|
+
readwriteSecretArn,
|
|
497
|
+
readwriteRole,
|
|
498
|
+
rotationToken,
|
|
499
|
+
}) => {
|
|
500
|
+
if (readwrite.needsWrite) {
|
|
501
|
+
await writeRuntimeCredential({
|
|
502
|
+
database,
|
|
503
|
+
host,
|
|
504
|
+
password: readwrite.password,
|
|
505
|
+
port,
|
|
506
|
+
rotationToken,
|
|
507
|
+
secretArn: readwriteSecretArn,
|
|
508
|
+
username: readwriteRole,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (readonly.needsWrite) {
|
|
512
|
+
await writeRuntimeCredential({
|
|
513
|
+
database,
|
|
514
|
+
host,
|
|
515
|
+
password: readonly.password,
|
|
516
|
+
port,
|
|
517
|
+
rotationToken,
|
|
518
|
+
secretArn: readonlySecretArn,
|
|
519
|
+
username: readonlyRole,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
const connectWithRetry = async ({
|
|
524
|
+
database,
|
|
525
|
+
host,
|
|
526
|
+
password,
|
|
527
|
+
port,
|
|
528
|
+
roleKind,
|
|
529
|
+
username,
|
|
530
|
+
ssl,
|
|
531
|
+
}) => {
|
|
532
|
+
const startedAt = Date.now();
|
|
533
|
+
let lastError;
|
|
534
|
+
for (let attempt = 1; attempt <= 10; attempt += 1) {
|
|
535
|
+
let candidate;
|
|
536
|
+
try {
|
|
537
|
+
candidate = new Client({
|
|
538
|
+
connectionTimeoutMillis: 10_000,
|
|
539
|
+
database,
|
|
540
|
+
host,
|
|
541
|
+
password:
|
|
542
|
+
typeof password === 'function' ? await password() : password,
|
|
543
|
+
port,
|
|
544
|
+
ssl: { ca: ssl, rejectUnauthorized: true },
|
|
545
|
+
statement_timeout: 10 * 60 * 1000,
|
|
546
|
+
user: username,
|
|
547
|
+
});
|
|
548
|
+
candidate.on('error', (error) => {
|
|
549
|
+
log('database_bootstrap_client_error', {
|
|
550
|
+
roleKind,
|
|
551
|
+
...safeErrorFields(error),
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
await candidate.connect();
|
|
555
|
+
return candidate;
|
|
556
|
+
} catch (error) {
|
|
557
|
+
lastError = error;
|
|
558
|
+
await candidate?.end().catch(() => undefined);
|
|
559
|
+
if (!retryableConnection(error) || attempt === 10) throw error;
|
|
560
|
+
log('database_bootstrap_connection_retry', {
|
|
561
|
+
attempt,
|
|
562
|
+
elapsedMs: Date.now() - startedAt,
|
|
563
|
+
roleKind,
|
|
564
|
+
...safeErrorFields(error),
|
|
565
|
+
});
|
|
566
|
+
await sleep(Math.min(attempt * 5_000, 30_000));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
throw lastError;
|
|
570
|
+
};
|
|
571
|
+
const roleExists = async (client, role) => {
|
|
572
|
+
const result = await client.query(
|
|
573
|
+
'SELECT 1 FROM pg_roles WHERE rolname = ' + quoteLiteral(role),
|
|
574
|
+
);
|
|
575
|
+
return result.rowCount > 0;
|
|
576
|
+
};
|
|
577
|
+
const executeStatements = async (client, statements) => {
|
|
578
|
+
for (const statement of statements) await client.query(statement);
|
|
579
|
+
};
|
|
580
|
+
const releaseBootstrapLock = async () => {
|
|
581
|
+
const client = bootstrapLockClient;
|
|
582
|
+
const key = bootstrapLockKey;
|
|
583
|
+
bootstrapLockClient = undefined;
|
|
584
|
+
bootstrapLockKey = undefined;
|
|
585
|
+
if (!client) return;
|
|
586
|
+
if (key) {
|
|
587
|
+
await client
|
|
588
|
+
.query('SELECT pg_advisory_unlock(hashtextextended($1, 0))', [key])
|
|
589
|
+
.catch(() => undefined);
|
|
590
|
+
}
|
|
591
|
+
await client.end().catch(() => undefined);
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
async function main() {
|
|
595
|
+
bootstrapId = required('EBK_BOOTSTRAP_ID');
|
|
596
|
+
lifecycleLogger = createDatabaseLifecycleLogger({
|
|
597
|
+
component: 'database-bootstrap',
|
|
598
|
+
correlationId: required('EBK_DATABASE_LIFECYCLE_CORRELATION_ID'),
|
|
599
|
+
databaseLifecycleId: bootstrapId,
|
|
600
|
+
operation: 'database.bootstrap',
|
|
601
|
+
slice: '${name}',
|
|
602
|
+
stage: required('EBK_STAGE'),
|
|
603
|
+
workspace: required('EBK_WORKSPACE'),
|
|
604
|
+
});
|
|
605
|
+
const database = requiredIdentifier('EBK_DATABASE_NAME');
|
|
606
|
+
const host = required('EBK_DATABASE_HOST');
|
|
607
|
+
const masterSecretArn = required('EBK_MASTER_SECRET_ARN');
|
|
608
|
+
const migratorRole = requiredIdentifier('EBK_DATABASE_MIGRATOR_ROLE');
|
|
609
|
+
const port = requiredPort();
|
|
610
|
+
const readonlyRole = requiredIdentifier('EBK_DATABASE_READONLY_ROLE');
|
|
611
|
+
const readonlySecretArn = required('EBK_READONLY_SECRET_ARN');
|
|
612
|
+
const readwriteRole = requiredIdentifier('EBK_DATABASE_READWRITE_ROLE');
|
|
613
|
+
const readwriteSecretArn = required('EBK_READWRITE_SECRET_ARN');
|
|
614
|
+
const region = required('AWS_REGION');
|
|
615
|
+
const rotationToken = process.env.EBK_DATABASE_ROTATION_TOKEN?.trim() ?? '';
|
|
616
|
+
const runtimeHost = required('EBK_RUNTIME_DATABASE_HOST');
|
|
617
|
+
const ca = await readFile(required('EBK_RDS_CA_BUNDLE'), 'utf8');
|
|
618
|
+
|
|
619
|
+
bootstrapPhase = 'credential-resolution';
|
|
620
|
+
const master = await readJsonSecret(masterSecretArn);
|
|
621
|
+
if (
|
|
622
|
+
typeof master.username !== 'string' ||
|
|
623
|
+
typeof master.password !== 'string' ||
|
|
624
|
+
master.password.length === 0
|
|
625
|
+
) {
|
|
626
|
+
throw new Error('Foundation master secret lacks username or password.');
|
|
627
|
+
}
|
|
628
|
+
if (!/^[a-z][a-z0-9_]{0,62}$/.test(master.username)) {
|
|
629
|
+
throw new Error('Foundation master secret username is invalid.');
|
|
630
|
+
}
|
|
631
|
+
const masterUsername = master.username;
|
|
632
|
+
|
|
633
|
+
bootstrapPhase = 'database-convergence';
|
|
634
|
+
const postgres = await connectWithRetry({
|
|
635
|
+
database: 'postgres',
|
|
636
|
+
host,
|
|
637
|
+
password: master.password,
|
|
638
|
+
port,
|
|
639
|
+
roleKind: 'master',
|
|
640
|
+
ssl: ca,
|
|
641
|
+
username: masterUsername,
|
|
642
|
+
});
|
|
643
|
+
bootstrapLockClient = postgres;
|
|
644
|
+
bootstrapLockKey = 'ebk-bootstrap:' + database;
|
|
645
|
+
await postgres.query(
|
|
646
|
+
'SELECT pg_advisory_lock(hashtextextended($1, 0))',
|
|
647
|
+
[bootstrapLockKey],
|
|
648
|
+
);
|
|
649
|
+
bootstrapPhase = 'credential-resolution';
|
|
650
|
+
const readwrite = await runtimeCredential({
|
|
651
|
+
database,
|
|
652
|
+
host: runtimeHost,
|
|
653
|
+
port,
|
|
654
|
+
rotationToken,
|
|
655
|
+
secretArn: readwriteSecretArn,
|
|
656
|
+
username: readwriteRole,
|
|
657
|
+
});
|
|
658
|
+
const readonly = await runtimeCredential({
|
|
659
|
+
database,
|
|
660
|
+
host: runtimeHost,
|
|
661
|
+
port,
|
|
662
|
+
rotationToken,
|
|
663
|
+
secretArn: readonlySecretArn,
|
|
664
|
+
username: readonlyRole,
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
bootstrapPhase = 'database-convergence';
|
|
668
|
+
const existing = await postgres.query(
|
|
669
|
+
'SELECT 1 FROM pg_database WHERE datname = ' + quoteLiteral(database),
|
|
670
|
+
);
|
|
671
|
+
if (existing.rowCount === 0) {
|
|
672
|
+
await postgres.query('CREATE DATABASE ' + quoteIdentifier(database));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const masterDatabase = await connectWithRetry({
|
|
676
|
+
database,
|
|
677
|
+
host,
|
|
678
|
+
password: master.password,
|
|
679
|
+
port,
|
|
680
|
+
roleKind: 'master',
|
|
681
|
+
ssl: ca,
|
|
682
|
+
username: masterUsername,
|
|
683
|
+
});
|
|
684
|
+
try {
|
|
685
|
+
for (const [role, password] of [
|
|
686
|
+
[readwriteRole, readwrite.password],
|
|
687
|
+
[readonlyRole, readonly.password],
|
|
688
|
+
]) {
|
|
689
|
+
if (await roleExists(masterDatabase, role)) {
|
|
690
|
+
await masterDatabase.query(
|
|
691
|
+
'ALTER ROLE ' + quoteIdentifier(role) + ' WITH LOGIN PASSWORD ' + quoteLiteral(password),
|
|
692
|
+
);
|
|
693
|
+
} else {
|
|
694
|
+
await masterDatabase.query(
|
|
695
|
+
'CREATE ROLE ' + quoteIdentifier(role) + ' WITH LOGIN PASSWORD ' + quoteLiteral(password),
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (await roleExists(masterDatabase, migratorRole)) {
|
|
700
|
+
await masterDatabase.query(
|
|
701
|
+
'ALTER ROLE ' + quoteIdentifier(migratorRole) + ' WITH LOGIN PASSWORD NULL',
|
|
702
|
+
);
|
|
703
|
+
} else {
|
|
704
|
+
await masterDatabase.query(
|
|
705
|
+
'CREATE ROLE ' + quoteIdentifier(migratorRole) + ' WITH LOGIN',
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// PostgreSQL 16+ can record creator ADMIN memberships when a CREATEROLE
|
|
710
|
+
// principal creates a role. Keep that non-inheriting administration path
|
|
711
|
+
// available for the membership grants below, but explicitly remove SET
|
|
712
|
+
// and INHERIT options before the migrator receives rds_iam. Default
|
|
713
|
+
// privileges are then converged through direct role-owned sessions.
|
|
714
|
+
await executeStatements(masterDatabase, [
|
|
715
|
+
'REVOKE INHERIT OPTION FOR ' + quoteIdentifier(migratorRole) +
|
|
716
|
+
' FROM CURRENT_USER',
|
|
717
|
+
'REVOKE SET OPTION FOR ' + quoteIdentifier(migratorRole) +
|
|
718
|
+
' FROM CURRENT_USER',
|
|
719
|
+
'REVOKE INHERIT OPTION FOR ' + quoteIdentifier(readwriteRole) +
|
|
720
|
+
' FROM CURRENT_USER',
|
|
721
|
+
'REVOKE SET OPTION FOR ' + quoteIdentifier(readwriteRole) +
|
|
722
|
+
' FROM CURRENT_USER',
|
|
723
|
+
'REVOKE ALL ON DATABASE ' + quoteIdentifier(database) + ' FROM PUBLIC',
|
|
724
|
+
'REVOKE CREATE ON DATABASE ' + quoteIdentifier(database) + ' FROM ' +
|
|
725
|
+
quoteIdentifier(migratorRole) + ', ' + quoteIdentifier(readwriteRole),
|
|
726
|
+
'GRANT CONNECT ON DATABASE ' + quoteIdentifier(database) + ' TO ' +
|
|
727
|
+
quoteIdentifier(migratorRole) + ', ' + quoteIdentifier(readwriteRole) +
|
|
728
|
+
', ' + quoteIdentifier(readonlyRole),
|
|
729
|
+
'GRANT ALL ON SCHEMA public TO ' + quoteIdentifier(migratorRole) +
|
|
730
|
+
', ' + quoteIdentifier(readwriteRole),
|
|
731
|
+
'GRANT USAGE ON SCHEMA public TO ' + quoteIdentifier(readonlyRole),
|
|
732
|
+
'GRANT rds_iam TO ' + quoteIdentifier(migratorRole),
|
|
733
|
+
'GRANT ' + quoteIdentifier(readwriteRole) + ' TO ' +
|
|
734
|
+
quoteIdentifier(migratorRole),
|
|
735
|
+
]);
|
|
736
|
+
} finally {
|
|
737
|
+
await masterDatabase.end().catch(() => undefined);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Publish immediately after ALTER ROLE convergence. A database password and
|
|
741
|
+
// Secrets Manager AWSCURRENT cannot change atomically, so keep this
|
|
742
|
+
// serialized stale window ahead of slower default-privilege work. Runtime
|
|
743
|
+
// authentication failure invalidates the bounded cache and retries once.
|
|
744
|
+
bootstrapPhase = 'runtime-secret-publication';
|
|
745
|
+
await publishRuntimeCredentials({
|
|
746
|
+
database,
|
|
747
|
+
host: runtimeHost,
|
|
748
|
+
port,
|
|
749
|
+
readonly,
|
|
750
|
+
readonlySecretArn,
|
|
751
|
+
readonlyRole,
|
|
752
|
+
readwrite,
|
|
753
|
+
readwriteSecretArn,
|
|
754
|
+
readwriteRole,
|
|
755
|
+
rotationToken,
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
bootstrapPhase = 'default-privilege-convergence';
|
|
759
|
+
const readwriteDatabase = await connectWithRetry({
|
|
760
|
+
database,
|
|
761
|
+
host,
|
|
762
|
+
password: readwrite.password,
|
|
763
|
+
port,
|
|
764
|
+
roleKind: 'readwrite',
|
|
765
|
+
ssl: ca,
|
|
766
|
+
username: readwriteRole,
|
|
767
|
+
});
|
|
768
|
+
try {
|
|
769
|
+
await executeStatements(readwriteDatabase, [
|
|
770
|
+
'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ' +
|
|
771
|
+
quoteIdentifier(readwriteRole),
|
|
772
|
+
'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ' +
|
|
773
|
+
quoteIdentifier(readonlyRole),
|
|
774
|
+
'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ' +
|
|
775
|
+
quoteIdentifier(readonlyRole),
|
|
776
|
+
]);
|
|
777
|
+
} finally {
|
|
778
|
+
await readwriteDatabase.end().catch(() => undefined);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const signer = new Signer({
|
|
782
|
+
hostname: host,
|
|
783
|
+
port,
|
|
784
|
+
region,
|
|
785
|
+
username: migratorRole,
|
|
786
|
+
});
|
|
787
|
+
const migratorDatabase = await connectWithRetry({
|
|
788
|
+
database,
|
|
789
|
+
host,
|
|
790
|
+
password: () => signer.getAuthToken(),
|
|
791
|
+
port,
|
|
792
|
+
roleKind: 'migrator',
|
|
793
|
+
ssl: ca,
|
|
794
|
+
username: migratorRole,
|
|
795
|
+
});
|
|
796
|
+
try {
|
|
797
|
+
await executeStatements(migratorDatabase, [
|
|
798
|
+
'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ' +
|
|
799
|
+
quoteIdentifier(readwriteRole),
|
|
800
|
+
'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ' +
|
|
801
|
+
quoteIdentifier(readonlyRole),
|
|
802
|
+
'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ' +
|
|
803
|
+
quoteIdentifier(readwriteRole),
|
|
804
|
+
'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ' +
|
|
805
|
+
quoteIdentifier(readonlyRole),
|
|
806
|
+
]);
|
|
807
|
+
} finally {
|
|
808
|
+
await migratorDatabase.end().catch(() => undefined);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
await releaseBootstrapLock();
|
|
812
|
+
log('database_bootstrap_succeeded', { database });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
main().catch(async (error) => {
|
|
816
|
+
await releaseBootstrapLock();
|
|
817
|
+
log('database_bootstrap_failed', {
|
|
818
|
+
phase: bootstrapPhase,
|
|
819
|
+
...safeErrorFields(error),
|
|
820
|
+
});
|
|
821
|
+
process.exitCode = 1;
|
|
822
|
+
});
|
|
823
|
+
`,
|
|
824
|
+
'jobs/retire-database.mjs': `import { createHash } from 'node:crypto';
|
|
825
|
+
import { readFile } from 'node:fs/promises';
|
|
826
|
+
import { pathToFileURL } from 'node:url';
|
|
827
|
+
import {
|
|
828
|
+
DeleteSecretCommand,
|
|
829
|
+
DescribeSecretCommand,
|
|
830
|
+
GetSecretValueCommand,
|
|
831
|
+
SecretsManagerClient,
|
|
832
|
+
} from '@aws-sdk/client-secrets-manager';
|
|
833
|
+
import {
|
|
834
|
+
SendTaskFailureCommand,
|
|
835
|
+
SendTaskSuccessCommand,
|
|
836
|
+
SFNClient,
|
|
837
|
+
} from '@aws-sdk/client-sfn';
|
|
838
|
+
import pg from 'pg';
|
|
839
|
+
import { createDatabaseLifecycleLogger } from './database-lifecycle-logging.mjs';
|
|
840
|
+
|
|
841
|
+
const { Client } = pg;
|
|
842
|
+
const FIXED_DATABASE_IDENTIFIER = 'retirement_rehearsal';
|
|
843
|
+
const FIXED_STAGE = 'development';
|
|
844
|
+
const MAX_CALLBACK_BYTES = 64 * 1024;
|
|
845
|
+
const MAX_RETAINED_SIBLINGS = 32;
|
|
846
|
+
const SECRET_ABSENCE_POLL_INTERVAL_MS = 2_000;
|
|
847
|
+
const SECRET_ABSENCE_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
848
|
+
const IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]{0,62}$/;
|
|
849
|
+
const DATABASE_IDENTIFIER_PATTERN =
|
|
850
|
+
/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
851
|
+
const CANDIDATE_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
852
|
+
const COMMIT_PATTERN = /^[a-f0-9]{40}$/;
|
|
853
|
+
const DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
854
|
+
const SECRET_ARN_PATTERN =
|
|
855
|
+
/^arn:(aws|aws-us-gov|aws-cn):secretsmanager:[a-z0-9-]+:\\d{12}:secret:[A-Za-z0-9/_+=.@-]+$/;
|
|
856
|
+
const SAFE_FAILURE_CODE_PATTERN = /^[A-Z0-9_]{2,32}$/;
|
|
857
|
+
const SAFE_ERROR_NAME_PATTERN = /^[A-Za-z0-9_.-]{1,80}$/;
|
|
858
|
+
|
|
859
|
+
const required = (environment, name) => {
|
|
860
|
+
const value = environment[name]?.trim();
|
|
861
|
+
if (!value) throw new Error(name + ' is required.');
|
|
862
|
+
return value;
|
|
863
|
+
};
|
|
864
|
+
const requiredExact = (environment, name, expected) => {
|
|
865
|
+
const value = required(environment, name);
|
|
866
|
+
if (value !== expected) {
|
|
867
|
+
throw new Error(name + ' must equal the fixed generated value.');
|
|
868
|
+
}
|
|
869
|
+
return value;
|
|
870
|
+
};
|
|
871
|
+
const requiredIdentifier = (environment, name) => {
|
|
872
|
+
const value = required(environment, name);
|
|
873
|
+
if (!IDENTIFIER_PATTERN.test(value)) {
|
|
874
|
+
throw new Error(name + ' must be a normalized PostgreSQL identifier.');
|
|
875
|
+
}
|
|
876
|
+
return value;
|
|
877
|
+
};
|
|
878
|
+
const requiredPort = (environment) => {
|
|
879
|
+
const value = required(environment, 'EBK_DATABASE_PORT');
|
|
880
|
+
if (!/^\\d+$/.test(value)) {
|
|
881
|
+
throw new Error('EBK_DATABASE_PORT must be an integer.');
|
|
882
|
+
}
|
|
883
|
+
const port = Number(value);
|
|
884
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
|
|
885
|
+
throw new Error('EBK_DATABASE_PORT must be from 1 through 65535.');
|
|
886
|
+
}
|
|
887
|
+
return port;
|
|
888
|
+
};
|
|
889
|
+
const safeErrorFields = (error) => {
|
|
890
|
+
const name = error instanceof Error ? error.name : 'UnknownError';
|
|
891
|
+
const code =
|
|
892
|
+
error &&
|
|
893
|
+
typeof error === 'object' &&
|
|
894
|
+
typeof error.code === 'string' &&
|
|
895
|
+
/^[A-Z0-9_]{2,32}$/.test(error.code)
|
|
896
|
+
? error.code
|
|
897
|
+
: undefined;
|
|
898
|
+
return {
|
|
899
|
+
errorName: name.replace(/[^A-Za-z0-9_.-]/g, '').slice(0, 80) || 'Error',
|
|
900
|
+
...(code ? { errorCode: code } : {}),
|
|
901
|
+
};
|
|
902
|
+
};
|
|
903
|
+
const callbackFailure = (phase, error) => {
|
|
904
|
+
const fields = safeErrorFields(error);
|
|
905
|
+
return {
|
|
906
|
+
cause: JSON.stringify({
|
|
907
|
+
errorCode:
|
|
908
|
+
typeof fields.errorCode === 'string' &&
|
|
909
|
+
SAFE_FAILURE_CODE_PATTERN.test(fields.errorCode)
|
|
910
|
+
? fields.errorCode
|
|
911
|
+
: undefined,
|
|
912
|
+
errorName: fields.errorName,
|
|
913
|
+
phase,
|
|
914
|
+
}).slice(0, 32 * 1024),
|
|
915
|
+
error: SAFE_ERROR_NAME_PATTERN.test(fields.errorName)
|
|
916
|
+
? fields.errorName
|
|
917
|
+
: 'DatabaseRetirementError',
|
|
918
|
+
};
|
|
919
|
+
};
|
|
920
|
+
const quoteIdentifier = (value) => {
|
|
921
|
+
if (!IDENTIFIER_PATTERN.test(value)) {
|
|
922
|
+
throw new Error('Unsafe PostgreSQL identifier.');
|
|
923
|
+
}
|
|
924
|
+
return '"' + value + '"';
|
|
925
|
+
};
|
|
926
|
+
const requiredPattern = (environment, name, pattern) => {
|
|
927
|
+
const value = required(environment, name);
|
|
928
|
+
if (!pattern.test(value)) {
|
|
929
|
+
throw new Error(name + ' is invalid.');
|
|
930
|
+
}
|
|
931
|
+
return value;
|
|
932
|
+
};
|
|
933
|
+
const requiredSecretArn = (environment, name) =>
|
|
934
|
+
requiredPattern(environment, name, SECRET_ARN_PATTERN);
|
|
935
|
+
const requiredTaskToken = (environment) => {
|
|
936
|
+
const value = required(environment, 'EBK_STEP_FUNCTIONS_TASK_TOKEN');
|
|
937
|
+
if (Buffer.byteLength(value, 'utf8') > 1024) {
|
|
938
|
+
throw new Error(
|
|
939
|
+
'EBK_STEP_FUNCTIONS_TASK_TOKEN exceeds the Step Functions limit.',
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
return value;
|
|
943
|
+
};
|
|
944
|
+
const expectedDatabaseName = (slice, databaseIdentifier) =>
|
|
945
|
+
[FIXED_STAGE, slice, databaseIdentifier]
|
|
946
|
+
.join('_')
|
|
947
|
+
.replaceAll('-', '_');
|
|
948
|
+
const expectedSecretPath = (slice, databaseIdentifier, kind) =>
|
|
949
|
+
':secret:ebk/' +
|
|
950
|
+
FIXED_STAGE +
|
|
951
|
+
'/' +
|
|
952
|
+
slice +
|
|
953
|
+
'/database/' +
|
|
954
|
+
databaseIdentifier +
|
|
955
|
+
'/' +
|
|
956
|
+
kind +
|
|
957
|
+
'-';
|
|
958
|
+
const requireExactDatabaseResources = ({
|
|
959
|
+
databaseIdentifier,
|
|
960
|
+
databaseName,
|
|
961
|
+
migratorRole,
|
|
962
|
+
readonlyRole,
|
|
963
|
+
readonlySecretArn,
|
|
964
|
+
readwriteRole,
|
|
965
|
+
readwriteSecretArn,
|
|
966
|
+
slice,
|
|
967
|
+
}) => {
|
|
968
|
+
const expectedDatabase = expectedDatabaseName(slice, databaseIdentifier);
|
|
969
|
+
if (
|
|
970
|
+
databaseName !== expectedDatabase ||
|
|
971
|
+
migratorRole !== expectedDatabase + '_migrator' ||
|
|
972
|
+
readonlyRole !== expectedDatabase + '_readonly' ||
|
|
973
|
+
readwriteRole !== expectedDatabase + '_readwrite' ||
|
|
974
|
+
!readonlySecretArn.includes(
|
|
975
|
+
expectedSecretPath(slice, databaseIdentifier, 'readonly'),
|
|
976
|
+
) ||
|
|
977
|
+
!readwriteSecretArn.includes(
|
|
978
|
+
expectedSecretPath(slice, databaseIdentifier, 'readwrite'),
|
|
979
|
+
)
|
|
980
|
+
) {
|
|
981
|
+
throw new Error(
|
|
982
|
+
'Database retirement resources differ from the exact generated identity.',
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
const readJsonSecret = async (secrets, secretArn) => {
|
|
987
|
+
const result = await secrets.send(
|
|
988
|
+
new GetSecretValueCommand({ SecretId: secretArn }),
|
|
989
|
+
);
|
|
990
|
+
if (!result.SecretString) {
|
|
991
|
+
throw new Error('Secret must contain a JSON SecretString.');
|
|
992
|
+
}
|
|
993
|
+
const value = JSON.parse(result.SecretString);
|
|
994
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
995
|
+
throw new Error('Secret JSON must be an object.');
|
|
996
|
+
}
|
|
997
|
+
return value;
|
|
998
|
+
};
|
|
999
|
+
const secretDoesNotExist = (error) =>
|
|
1000
|
+
error &&
|
|
1001
|
+
typeof error === 'object' &&
|
|
1002
|
+
error.name === 'ResourceNotFoundException';
|
|
1003
|
+
const normalizedDate = (value) =>
|
|
1004
|
+
value instanceof Date
|
|
1005
|
+
? value.toISOString()
|
|
1006
|
+
: typeof value === 'string'
|
|
1007
|
+
? value
|
|
1008
|
+
: undefined;
|
|
1009
|
+
const describeSecretMetadata = async (secrets, secretArn) => {
|
|
1010
|
+
try {
|
|
1011
|
+
const result = await secrets.send(
|
|
1012
|
+
new DescribeSecretCommand({ SecretId: secretArn }),
|
|
1013
|
+
);
|
|
1014
|
+
return {
|
|
1015
|
+
arn: result.ARN ?? secretArn,
|
|
1016
|
+
createdDate: normalizedDate(result.CreatedDate),
|
|
1017
|
+
deletedDate: normalizedDate(result.DeletedDate),
|
|
1018
|
+
exists: true,
|
|
1019
|
+
lastChangedDate: normalizedDate(result.LastChangedDate),
|
|
1020
|
+
name: result.Name ?? '',
|
|
1021
|
+
rotationEnabled: result.RotationEnabled === true,
|
|
1022
|
+
versions: Object.entries(result.VersionIdsToStages ?? {})
|
|
1023
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
1024
|
+
.map(([version, stages]) => [
|
|
1025
|
+
version,
|
|
1026
|
+
[...(stages ?? [])].sort(),
|
|
1027
|
+
]),
|
|
1028
|
+
};
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
if (secretDoesNotExist(error)) {
|
|
1031
|
+
return { arn: secretArn, exists: false };
|
|
1032
|
+
}
|
|
1033
|
+
throw error;
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
const stableJson = (value) => JSON.stringify(value);
|
|
1037
|
+
const fingerprint = (value) =>
|
|
1038
|
+
createHash('sha256').update(stableJson(value), 'utf8').digest('hex');
|
|
1039
|
+
const queryDatabaseMetadata = async (postgres, databaseName) => {
|
|
1040
|
+
const result = await postgres.query(
|
|
1041
|
+
'SELECT oid::text, datname, encoding, datcollate, datctype, datistemplate, ' +
|
|
1042
|
+
'datallowconn, datconnlimit FROM pg_database WHERE datname = $1',
|
|
1043
|
+
[databaseName],
|
|
1044
|
+
);
|
|
1045
|
+
return result.rows[0] ?? null;
|
|
1046
|
+
};
|
|
1047
|
+
const queryRoleMetadata = async (postgres, roles) => {
|
|
1048
|
+
const result = await postgres.query(
|
|
1049
|
+
'SELECT oid::text, rolname, rolcanlogin, rolinherit, rolcreatedb, ' +
|
|
1050
|
+
'rolcreaterole, rolreplication, rolbypassrls, rolconnlimit, ' +
|
|
1051
|
+
'rolvaliduntil::text FROM pg_roles WHERE rolname = ANY($1::text[]) ' +
|
|
1052
|
+
'ORDER BY rolname',
|
|
1053
|
+
[roles],
|
|
1054
|
+
);
|
|
1055
|
+
return result.rows;
|
|
1056
|
+
};
|
|
1057
|
+
const siblingFingerprint = async (postgres, secrets, sibling) =>
|
|
1058
|
+
fingerprint({
|
|
1059
|
+
database: await queryDatabaseMetadata(postgres, sibling.databaseName),
|
|
1060
|
+
databaseIdentifier: sibling.databaseIdentifier,
|
|
1061
|
+
roles: await queryRoleMetadata(postgres, [
|
|
1062
|
+
sibling.migratorRole,
|
|
1063
|
+
sibling.readonlyRole,
|
|
1064
|
+
sibling.readwriteRole,
|
|
1065
|
+
]),
|
|
1066
|
+
secrets: {
|
|
1067
|
+
readonly: await describeSecretMetadata(
|
|
1068
|
+
secrets,
|
|
1069
|
+
sibling.readonlySecretArn,
|
|
1070
|
+
),
|
|
1071
|
+
readwrite: await describeSecretMetadata(
|
|
1072
|
+
secrets,
|
|
1073
|
+
sibling.readwriteSecretArn,
|
|
1074
|
+
),
|
|
1075
|
+
},
|
|
1076
|
+
});
|
|
1077
|
+
const fingerprintSiblings = async (postgres, secrets, siblings) =>
|
|
1078
|
+
Object.fromEntries(
|
|
1079
|
+
await Promise.all(
|
|
1080
|
+
siblings.map(async (sibling) => [
|
|
1081
|
+
sibling.databaseIdentifier,
|
|
1082
|
+
await siblingFingerprint(postgres, secrets, sibling),
|
|
1083
|
+
]),
|
|
1084
|
+
),
|
|
1085
|
+
);
|
|
1086
|
+
const requireRetainedSiblings = (environment, slice) => {
|
|
1087
|
+
let parsed;
|
|
1088
|
+
try {
|
|
1089
|
+
parsed = JSON.parse(required(environment, 'EBK_RETAINED_SIBLINGS'));
|
|
1090
|
+
} catch {
|
|
1091
|
+
throw new Error('EBK_RETAINED_SIBLINGS must be valid JSON.');
|
|
1092
|
+
}
|
|
1093
|
+
if (
|
|
1094
|
+
!Array.isArray(parsed) ||
|
|
1095
|
+
parsed.length < 1 ||
|
|
1096
|
+
parsed.length > MAX_RETAINED_SIBLINGS
|
|
1097
|
+
) {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
'EBK_RETAINED_SIBLINGS must contain from 1 through ' +
|
|
1100
|
+
MAX_RETAINED_SIBLINGS +
|
|
1101
|
+
' exact siblings.',
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
const expectedKeys = [
|
|
1105
|
+
'databaseIdentifier',
|
|
1106
|
+
'databaseName',
|
|
1107
|
+
'migratorRole',
|
|
1108
|
+
'readonlyRole',
|
|
1109
|
+
'readonlySecretArn',
|
|
1110
|
+
'readwriteRole',
|
|
1111
|
+
'readwriteSecretArn',
|
|
1112
|
+
];
|
|
1113
|
+
const siblings = parsed.map((value) => {
|
|
1114
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1115
|
+
throw new Error('Every retained sibling must be an object.');
|
|
1116
|
+
}
|
|
1117
|
+
const keys = Object.keys(value).sort();
|
|
1118
|
+
if (
|
|
1119
|
+
keys.length !== expectedKeys.length ||
|
|
1120
|
+
keys.some((key, index) => key !== [...expectedKeys].sort()[index])
|
|
1121
|
+
) {
|
|
1122
|
+
throw new Error(
|
|
1123
|
+
'Every retained sibling must contain only exact generated resources.',
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
const databaseIdentifier =
|
|
1127
|
+
typeof value.databaseIdentifier === 'string'
|
|
1128
|
+
? value.databaseIdentifier
|
|
1129
|
+
: '';
|
|
1130
|
+
if (
|
|
1131
|
+
!DATABASE_IDENTIFIER_PATTERN.test(databaseIdentifier) ||
|
|
1132
|
+
databaseIdentifier === FIXED_DATABASE_IDENTIFIER
|
|
1133
|
+
) {
|
|
1134
|
+
throw new Error('Retained sibling database identifier is invalid.');
|
|
1135
|
+
}
|
|
1136
|
+
const sibling = {
|
|
1137
|
+
databaseIdentifier,
|
|
1138
|
+
databaseName:
|
|
1139
|
+
typeof value.databaseName === 'string' ? value.databaseName : '',
|
|
1140
|
+
migratorRole:
|
|
1141
|
+
typeof value.migratorRole === 'string' ? value.migratorRole : '',
|
|
1142
|
+
readonlyRole:
|
|
1143
|
+
typeof value.readonlyRole === 'string' ? value.readonlyRole : '',
|
|
1144
|
+
readonlySecretArn:
|
|
1145
|
+
typeof value.readonlySecretArn === 'string'
|
|
1146
|
+
? value.readonlySecretArn
|
|
1147
|
+
: '',
|
|
1148
|
+
readwriteRole:
|
|
1149
|
+
typeof value.readwriteRole === 'string' ? value.readwriteRole : '',
|
|
1150
|
+
readwriteSecretArn:
|
|
1151
|
+
typeof value.readwriteSecretArn === 'string'
|
|
1152
|
+
? value.readwriteSecretArn
|
|
1153
|
+
: '',
|
|
1154
|
+
};
|
|
1155
|
+
for (const field of [
|
|
1156
|
+
'databaseName',
|
|
1157
|
+
'migratorRole',
|
|
1158
|
+
'readonlyRole',
|
|
1159
|
+
'readwriteRole',
|
|
1160
|
+
]) {
|
|
1161
|
+
if (!IDENTIFIER_PATTERN.test(sibling[field])) {
|
|
1162
|
+
throw new Error('Retained sibling ' + field + ' is invalid.');
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
for (const field of ['readonlySecretArn', 'readwriteSecretArn']) {
|
|
1166
|
+
if (!SECRET_ARN_PATTERN.test(sibling[field])) {
|
|
1167
|
+
throw new Error('Retained sibling ' + field + ' is invalid.');
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
requireExactDatabaseResources({ ...sibling, slice });
|
|
1171
|
+
return sibling;
|
|
1172
|
+
});
|
|
1173
|
+
const identifiers = siblings.map(({ databaseIdentifier }) =>
|
|
1174
|
+
databaseIdentifier,
|
|
1175
|
+
);
|
|
1176
|
+
if (new Set(identifiers).size !== identifiers.length) {
|
|
1177
|
+
throw new Error('Retained sibling identities must be unique.');
|
|
1178
|
+
}
|
|
1179
|
+
return siblings.sort((left, right) =>
|
|
1180
|
+
left.databaseIdentifier < right.databaseIdentifier
|
|
1181
|
+
? -1
|
|
1182
|
+
: left.databaseIdentifier > right.databaseIdentifier
|
|
1183
|
+
? 1
|
|
1184
|
+
: 0,
|
|
1185
|
+
);
|
|
1186
|
+
};
|
|
1187
|
+
const resourceExistence = async ({
|
|
1188
|
+
databaseName,
|
|
1189
|
+
migratorRole,
|
|
1190
|
+
postgres,
|
|
1191
|
+
readonlyRole,
|
|
1192
|
+
readonlySecretArn,
|
|
1193
|
+
readwriteRole,
|
|
1194
|
+
readwriteSecretArn,
|
|
1195
|
+
secrets,
|
|
1196
|
+
}) => {
|
|
1197
|
+
const database = await queryDatabaseMetadata(postgres, databaseName);
|
|
1198
|
+
const roles = new Set(
|
|
1199
|
+
(
|
|
1200
|
+
await queryRoleMetadata(postgres, [
|
|
1201
|
+
migratorRole,
|
|
1202
|
+
readonlyRole,
|
|
1203
|
+
readwriteRole,
|
|
1204
|
+
])
|
|
1205
|
+
).map(({ rolname }) => rolname),
|
|
1206
|
+
);
|
|
1207
|
+
return {
|
|
1208
|
+
databaseExists: database !== null,
|
|
1209
|
+
migratorRoleExists: roles.has(migratorRole),
|
|
1210
|
+
readonlyRoleExists: roles.has(readonlyRole),
|
|
1211
|
+
readonlySecretExists: (
|
|
1212
|
+
await describeSecretMetadata(secrets, readonlySecretArn)
|
|
1213
|
+
).exists,
|
|
1214
|
+
readwriteRoleExists: roles.has(readwriteRole),
|
|
1215
|
+
readwriteSecretExists: (
|
|
1216
|
+
await describeSecretMetadata(secrets, readwriteSecretArn)
|
|
1217
|
+
).exists,
|
|
1218
|
+
};
|
|
1219
|
+
};
|
|
1220
|
+
const deleteExactSecret = async (secrets, secretArn) => {
|
|
1221
|
+
try {
|
|
1222
|
+
await secrets.send(
|
|
1223
|
+
new DeleteSecretCommand({
|
|
1224
|
+
ForceDeleteWithoutRecovery: true,
|
|
1225
|
+
SecretId: secretArn,
|
|
1226
|
+
}),
|
|
1227
|
+
);
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
if (!secretDoesNotExist(error)) throw error;
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
const waitForSecretAbsence = async ({
|
|
1233
|
+
now,
|
|
1234
|
+
secretArns,
|
|
1235
|
+
secrets,
|
|
1236
|
+
sleep,
|
|
1237
|
+
}) => {
|
|
1238
|
+
const deadline = now() + SECRET_ABSENCE_TIMEOUT_MS;
|
|
1239
|
+
while (true) {
|
|
1240
|
+
const descriptions = await Promise.all(
|
|
1241
|
+
secretArns.map((secretArn) =>
|
|
1242
|
+
describeSecretMetadata(secrets, secretArn),
|
|
1243
|
+
),
|
|
1244
|
+
);
|
|
1245
|
+
if (descriptions.every(({ exists }) => !exists)) return;
|
|
1246
|
+
if (now() >= deadline) {
|
|
1247
|
+
throw new Error(
|
|
1248
|
+
'Timed out proving exact generated credential-container absence.',
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
await sleep(SECRET_ABSENCE_POLL_INTERVAL_MS);
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
const requiredConfiguration = (environment) => {
|
|
1255
|
+
const slice = requiredExact(environment, 'EBK_SLICE', '${name}');
|
|
1256
|
+
const databaseIdentifier = requiredExact(
|
|
1257
|
+
environment,
|
|
1258
|
+
'EBK_DATABASE_IDENTIFIER',
|
|
1259
|
+
FIXED_DATABASE_IDENTIFIER,
|
|
1260
|
+
);
|
|
1261
|
+
requiredExact(
|
|
1262
|
+
environment,
|
|
1263
|
+
'EBK_EXECUTION_DATABASE_IDENTIFIER',
|
|
1264
|
+
databaseIdentifier,
|
|
1265
|
+
);
|
|
1266
|
+
requiredExact(environment, 'EBK_EXECUTION_SLICE', slice);
|
|
1267
|
+
requiredExact(environment, 'EBK_EXECUTION_STAGE', FIXED_STAGE);
|
|
1268
|
+
const databaseName = requiredIdentifier(
|
|
1269
|
+
environment,
|
|
1270
|
+
'EBK_DATABASE_NAME',
|
|
1271
|
+
);
|
|
1272
|
+
const migratorRole = requiredIdentifier(
|
|
1273
|
+
environment,
|
|
1274
|
+
'EBK_DATABASE_MIGRATOR_ROLE',
|
|
1275
|
+
);
|
|
1276
|
+
const readonlyRole = requiredIdentifier(
|
|
1277
|
+
environment,
|
|
1278
|
+
'EBK_DATABASE_READONLY_ROLE',
|
|
1279
|
+
);
|
|
1280
|
+
const readwriteRole = requiredIdentifier(
|
|
1281
|
+
environment,
|
|
1282
|
+
'EBK_DATABASE_READWRITE_ROLE',
|
|
1283
|
+
);
|
|
1284
|
+
const readonlySecretArn = requiredSecretArn(
|
|
1285
|
+
environment,
|
|
1286
|
+
'EBK_READONLY_SECRET_ARN',
|
|
1287
|
+
);
|
|
1288
|
+
const readwriteSecretArn = requiredSecretArn(
|
|
1289
|
+
environment,
|
|
1290
|
+
'EBK_READWRITE_SECRET_ARN',
|
|
1291
|
+
);
|
|
1292
|
+
requireExactDatabaseResources({
|
|
1293
|
+
databaseIdentifier,
|
|
1294
|
+
databaseName,
|
|
1295
|
+
migratorRole,
|
|
1296
|
+
readonlyRole,
|
|
1297
|
+
readonlySecretArn,
|
|
1298
|
+
readwriteRole,
|
|
1299
|
+
readwriteSecretArn,
|
|
1300
|
+
slice,
|
|
1301
|
+
});
|
|
1302
|
+
return {
|
|
1303
|
+
candidateIdentity: requiredPattern(
|
|
1304
|
+
environment,
|
|
1305
|
+
'EBK_CANDIDATE_IDENTITY',
|
|
1306
|
+
CANDIDATE_PATTERN,
|
|
1307
|
+
),
|
|
1308
|
+
correlationId: requiredPattern(
|
|
1309
|
+
environment,
|
|
1310
|
+
'EBK_DATABASE_LIFECYCLE_CORRELATION_ID',
|
|
1311
|
+
/^[A-Za-z0-9._:/-]{1,128}$/,
|
|
1312
|
+
),
|
|
1313
|
+
databaseIdentifier,
|
|
1314
|
+
databaseName,
|
|
1315
|
+
host: required(environment, 'EBK_DATABASE_HOST'),
|
|
1316
|
+
masterSecretArn: requiredSecretArn(
|
|
1317
|
+
environment,
|
|
1318
|
+
'EBK_MASTER_SECRET_ARN',
|
|
1319
|
+
),
|
|
1320
|
+
migratorRole,
|
|
1321
|
+
port: requiredPort(environment),
|
|
1322
|
+
readonlyRole,
|
|
1323
|
+
readonlySecretArn,
|
|
1324
|
+
registrationDigest: requiredPattern(
|
|
1325
|
+
environment,
|
|
1326
|
+
'EBK_REGISTRATION_DIGEST',
|
|
1327
|
+
DIGEST_PATTERN,
|
|
1328
|
+
),
|
|
1329
|
+
retirementId: requiredPattern(
|
|
1330
|
+
environment,
|
|
1331
|
+
'EBK_RETIREMENT_ID',
|
|
1332
|
+
/^[A-Za-z0-9._:/-]{1,128}$/,
|
|
1333
|
+
),
|
|
1334
|
+
retainedSiblings: requireRetainedSiblings(environment, slice),
|
|
1335
|
+
readwriteRole,
|
|
1336
|
+
readwriteSecretArn,
|
|
1337
|
+
rdsCaBundle: required(environment, 'EBK_RDS_CA_BUNDLE'),
|
|
1338
|
+
slice,
|
|
1339
|
+
sourceCommit: requiredPattern(
|
|
1340
|
+
environment,
|
|
1341
|
+
'EBK_SOURCE_COMMIT',
|
|
1342
|
+
COMMIT_PATTERN,
|
|
1343
|
+
),
|
|
1344
|
+
stage: requiredExact(environment, 'EBK_STAGE', FIXED_STAGE),
|
|
1345
|
+
taskToken: requiredTaskToken(environment),
|
|
1346
|
+
workspace: required(environment, 'EBK_WORKSPACE'),
|
|
1347
|
+
};
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
export async function runDatabaseRetirementRehearsal({
|
|
1351
|
+
environment = process.env,
|
|
1352
|
+
now = () => Date.now(),
|
|
1353
|
+
postgresClient = (configuration) => new Client(configuration),
|
|
1354
|
+
readCaBundle = readFile,
|
|
1355
|
+
secrets = new SecretsManagerClient({}),
|
|
1356
|
+
sfn = new SFNClient({}),
|
|
1357
|
+
sleep = (duration) =>
|
|
1358
|
+
new Promise((resolve) => setTimeout(resolve, duration)),
|
|
1359
|
+
} = {}) {
|
|
1360
|
+
let configuration;
|
|
1361
|
+
let lifecycleLogger;
|
|
1362
|
+
let postgres;
|
|
1363
|
+
let lockKey;
|
|
1364
|
+
let retirementPhase = 'configuration';
|
|
1365
|
+
let taskToken =
|
|
1366
|
+
typeof environment.EBK_STEP_FUNCTIONS_TASK_TOKEN === 'string'
|
|
1367
|
+
? environment.EBK_STEP_FUNCTIONS_TASK_TOKEN.trim()
|
|
1368
|
+
: '';
|
|
1369
|
+
const log = (event, fields = {}) => lifecycleLogger?.emit(event, fields);
|
|
1370
|
+
try {
|
|
1371
|
+
configuration = requiredConfiguration(environment);
|
|
1372
|
+
taskToken = configuration.taskToken;
|
|
1373
|
+
lifecycleLogger = createDatabaseLifecycleLogger({
|
|
1374
|
+
component: 'database-retirement',
|
|
1375
|
+
correlationId: configuration.correlationId,
|
|
1376
|
+
databaseLifecycleId: configuration.retirementId,
|
|
1377
|
+
operation: 'database.retire',
|
|
1378
|
+
slice: '${name}',
|
|
1379
|
+
stage: FIXED_STAGE,
|
|
1380
|
+
workspace: configuration.workspace,
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
retirementPhase = 'credential-resolution';
|
|
1384
|
+
const [ca, master] = await Promise.all([
|
|
1385
|
+
readCaBundle(configuration.rdsCaBundle, 'utf8'),
|
|
1386
|
+
readJsonSecret(secrets, configuration.masterSecretArn),
|
|
1387
|
+
]);
|
|
1388
|
+
if (
|
|
1389
|
+
typeof master.username !== 'string' ||
|
|
1390
|
+
typeof master.password !== 'string' ||
|
|
1391
|
+
master.password.length === 0 ||
|
|
1392
|
+
!IDENTIFIER_PATTERN.test(master.username)
|
|
1393
|
+
) {
|
|
1394
|
+
throw new Error('Foundation master secret is invalid.');
|
|
1395
|
+
}
|
|
1396
|
+
postgres = postgresClient({
|
|
1397
|
+
connectionTimeoutMillis: 10_000,
|
|
1398
|
+
database: 'postgres',
|
|
1399
|
+
host: configuration.host,
|
|
1400
|
+
password: master.password,
|
|
1401
|
+
port: configuration.port,
|
|
1402
|
+
ssl: { ca, rejectUnauthorized: true },
|
|
1403
|
+
statement_timeout: 10 * 60 * 1000,
|
|
1404
|
+
user: master.username,
|
|
1405
|
+
});
|
|
1406
|
+
postgres.on('error', (error) => {
|
|
1407
|
+
log('database_retirement_client_error', safeErrorFields(error));
|
|
1408
|
+
});
|
|
1409
|
+
await postgres.connect();
|
|
1410
|
+
lockKey = 'ebk-bootstrap:' + configuration.databaseName;
|
|
1411
|
+
retirementPhase = 'serialized-retirement';
|
|
1412
|
+
await postgres.query(
|
|
1413
|
+
'SELECT pg_advisory_lock(hashtextextended($1, 0))',
|
|
1414
|
+
[lockKey],
|
|
1415
|
+
);
|
|
1416
|
+
const beforeSiblingFingerprints = await fingerprintSiblings(
|
|
1417
|
+
postgres,
|
|
1418
|
+
secrets,
|
|
1419
|
+
configuration.retainedSiblings,
|
|
1420
|
+
);
|
|
1421
|
+
|
|
1422
|
+
const existing = await queryDatabaseMetadata(
|
|
1423
|
+
postgres,
|
|
1424
|
+
configuration.databaseName,
|
|
1425
|
+
);
|
|
1426
|
+
const roles = new Set(
|
|
1427
|
+
(
|
|
1428
|
+
await queryRoleMetadata(postgres, [
|
|
1429
|
+
configuration.migratorRole,
|
|
1430
|
+
configuration.readwriteRole,
|
|
1431
|
+
configuration.readonlyRole,
|
|
1432
|
+
])
|
|
1433
|
+
).map(({ rolname }) => rolname),
|
|
1434
|
+
);
|
|
1435
|
+
if (existing !== null) {
|
|
1436
|
+
const grantees = [
|
|
1437
|
+
configuration.migratorRole,
|
|
1438
|
+
configuration.readwriteRole,
|
|
1439
|
+
configuration.readonlyRole,
|
|
1440
|
+
]
|
|
1441
|
+
.filter((role) => roles.has(role))
|
|
1442
|
+
.map(quoteIdentifier)
|
|
1443
|
+
.join(', ');
|
|
1444
|
+
if (grantees) {
|
|
1445
|
+
await postgres.query(
|
|
1446
|
+
'REVOKE ALL ON DATABASE ' +
|
|
1447
|
+
quoteIdentifier(configuration.databaseName) +
|
|
1448
|
+
' FROM ' +
|
|
1449
|
+
grantees,
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
await postgres.query(
|
|
1453
|
+
'DROP DATABASE ' +
|
|
1454
|
+
quoteIdentifier(configuration.databaseName) +
|
|
1455
|
+
' WITH (FORCE)',
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
if (
|
|
1459
|
+
roles.has(configuration.readwriteRole) &&
|
|
1460
|
+
roles.has(configuration.migratorRole)
|
|
1461
|
+
) {
|
|
1462
|
+
await postgres.query(
|
|
1463
|
+
'REVOKE ' +
|
|
1464
|
+
quoteIdentifier(configuration.readwriteRole) +
|
|
1465
|
+
' FROM ' +
|
|
1466
|
+
quoteIdentifier(configuration.migratorRole),
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
for (const role of [
|
|
1470
|
+
configuration.migratorRole,
|
|
1471
|
+
configuration.readwriteRole,
|
|
1472
|
+
configuration.readonlyRole,
|
|
1473
|
+
]) {
|
|
1474
|
+
await postgres.query('DROP ROLE IF EXISTS ' + quoteIdentifier(role));
|
|
1475
|
+
}
|
|
1476
|
+
retirementPhase = 'credential-container-retirement';
|
|
1477
|
+
await Promise.all([
|
|
1478
|
+
deleteExactSecret(secrets, configuration.readonlySecretArn),
|
|
1479
|
+
deleteExactSecret(secrets, configuration.readwriteSecretArn),
|
|
1480
|
+
]);
|
|
1481
|
+
await waitForSecretAbsence({
|
|
1482
|
+
now,
|
|
1483
|
+
secretArns: [
|
|
1484
|
+
configuration.readonlySecretArn,
|
|
1485
|
+
configuration.readwriteSecretArn,
|
|
1486
|
+
],
|
|
1487
|
+
secrets,
|
|
1488
|
+
sleep,
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
retirementPhase = 'absence-proof';
|
|
1492
|
+
const after = {
|
|
1493
|
+
...(await resourceExistence({
|
|
1494
|
+
...configuration,
|
|
1495
|
+
postgres,
|
|
1496
|
+
secrets,
|
|
1497
|
+
})),
|
|
1498
|
+
siblingFingerprints: await fingerprintSiblings(
|
|
1499
|
+
postgres,
|
|
1500
|
+
secrets,
|
|
1501
|
+
configuration.retainedSiblings,
|
|
1502
|
+
),
|
|
1503
|
+
};
|
|
1504
|
+
if (
|
|
1505
|
+
Object.entries(after)
|
|
1506
|
+
.filter(([key]) => key !== 'siblingFingerprints')
|
|
1507
|
+
.some(([, exists]) => exists !== false)
|
|
1508
|
+
) {
|
|
1509
|
+
throw new Error(
|
|
1510
|
+
'Exact database retirement left one or more target resources present.',
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
for (const [sibling, before] of Object.entries(
|
|
1514
|
+
beforeSiblingFingerprints,
|
|
1515
|
+
)) {
|
|
1516
|
+
if (after.siblingFingerprints[sibling] !== before) {
|
|
1517
|
+
throw new Error(
|
|
1518
|
+
'Retained sibling changed during exact database retirement.',
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const evidence = {
|
|
1524
|
+
after,
|
|
1525
|
+
beforeSiblingFingerprints,
|
|
1526
|
+
candidateIdentity: configuration.candidateIdentity,
|
|
1527
|
+
database: FIXED_DATABASE_IDENTIFIER,
|
|
1528
|
+
databaseLifecycleCorrelationId: configuration.correlationId,
|
|
1529
|
+
evidenceVersion: 1,
|
|
1530
|
+
registrationDigest: configuration.registrationDigest,
|
|
1531
|
+
sourceCommit: configuration.sourceCommit,
|
|
1532
|
+
stage: FIXED_STAGE,
|
|
1533
|
+
slice: configuration.slice,
|
|
1534
|
+
};
|
|
1535
|
+
const output = JSON.stringify(evidence);
|
|
1536
|
+
if (Buffer.byteLength(output, 'utf8') > MAX_CALLBACK_BYTES) {
|
|
1537
|
+
throw new Error(
|
|
1538
|
+
'Database retirement evidence exceeds the bounded callback size.',
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
retirementPhase = 'callback';
|
|
1543
|
+
await sfn.send(
|
|
1544
|
+
new SendTaskSuccessCommand({
|
|
1545
|
+
output,
|
|
1546
|
+
taskToken: configuration.taskToken,
|
|
1547
|
+
}),
|
|
1548
|
+
);
|
|
1549
|
+
log('database_retirement_succeeded', {
|
|
1550
|
+
database: configuration.databaseName,
|
|
1551
|
+
});
|
|
1552
|
+
return evidence;
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
log('database_retirement_failed', {
|
|
1555
|
+
phase: retirementPhase,
|
|
1556
|
+
...safeErrorFields(error),
|
|
1557
|
+
});
|
|
1558
|
+
if (taskToken) {
|
|
1559
|
+
const failure = callbackFailure(retirementPhase, error);
|
|
1560
|
+
await sfn
|
|
1561
|
+
.send(
|
|
1562
|
+
new SendTaskFailureCommand({
|
|
1563
|
+
cause: failure.cause,
|
|
1564
|
+
error: failure.error,
|
|
1565
|
+
taskToken,
|
|
1566
|
+
}),
|
|
1567
|
+
)
|
|
1568
|
+
.catch(() => undefined);
|
|
1569
|
+
}
|
|
1570
|
+
throw error;
|
|
1571
|
+
} finally {
|
|
1572
|
+
if (postgres && lockKey) {
|
|
1573
|
+
await postgres
|
|
1574
|
+
.query('SELECT pg_advisory_unlock(hashtextextended($1, 0))', [
|
|
1575
|
+
lockKey,
|
|
1576
|
+
])
|
|
1577
|
+
.catch(() => undefined);
|
|
1578
|
+
}
|
|
1579
|
+
await postgres?.end().catch(() => undefined);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
if (
|
|
1584
|
+
process.argv[1] &&
|
|
1585
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
1586
|
+
) {
|
|
1587
|
+
runDatabaseRetirementRehearsal().catch(() => {
|
|
1588
|
+
process.exitCode = 1;
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
`,
|
|
1592
|
+
'jobs/migrate-database.mjs': `import { createHash } from 'node:crypto';
|
|
1593
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
1594
|
+
import { Signer } from '@aws-sdk/rds-signer';
|
|
1595
|
+
import pg from 'pg';
|
|
1596
|
+
import { createDatabaseLifecycleLogger } from './database-lifecycle-logging.mjs';
|
|
1597
|
+
|
|
1598
|
+
const { Client } = pg;
|
|
1599
|
+
const required = (name) => {
|
|
1600
|
+
const value = process.env[name]?.trim();
|
|
1601
|
+
if (!value) throw new Error(name + ' is required.');
|
|
1602
|
+
return value;
|
|
1603
|
+
};
|
|
1604
|
+
const requiredIdentifier = (name) => {
|
|
1605
|
+
const value = required(name);
|
|
1606
|
+
if (!/^[a-z][a-z0-9_]{0,62}$/.test(value)) {
|
|
1607
|
+
throw new Error(name + ' is not a valid database identifier.');
|
|
1608
|
+
}
|
|
1609
|
+
return value;
|
|
1610
|
+
};
|
|
1611
|
+
const quoteIdentifier = (value) => '"' + value.replaceAll('"', '""') + '"';
|
|
1612
|
+
let migrationId = 'unresolved';
|
|
1613
|
+
let migrationPhase = 'configuration';
|
|
1614
|
+
let lifecycleLogger;
|
|
1615
|
+
|
|
1616
|
+
const log = (event, fields = {}) => lifecycleLogger?.emit(event, fields);
|
|
1617
|
+
const safeErrorFields = (error) => {
|
|
1618
|
+
const name = error instanceof Error ? error.name : 'UnknownError';
|
|
1619
|
+
const code =
|
|
1620
|
+
error &&
|
|
1621
|
+
typeof error === 'object' &&
|
|
1622
|
+
typeof error.code === 'string' &&
|
|
1623
|
+
/^[A-Z0-9_]{2,32}$/.test(error.code)
|
|
1624
|
+
? error.code
|
|
1625
|
+
: undefined;
|
|
1626
|
+
return {
|
|
1627
|
+
errorName: name.replace(/[^A-Za-z0-9_.-]/g, '').slice(0, 80) || 'Error',
|
|
1628
|
+
...(code ? { errorCode: code } : {}),
|
|
1629
|
+
};
|
|
1630
|
+
};
|
|
1631
|
+
const retryableConnection = (error) => {
|
|
1632
|
+
const code = error && typeof error === 'object' ? error.code : undefined;
|
|
1633
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1634
|
+
return new Set([
|
|
1635
|
+
'08000',
|
|
1636
|
+
'08001',
|
|
1637
|
+
'08003',
|
|
1638
|
+
'08006',
|
|
1639
|
+
'57P03',
|
|
1640
|
+
'ECONNREFUSED',
|
|
1641
|
+
'ECONNRESET',
|
|
1642
|
+
'ETIMEDOUT',
|
|
1643
|
+
]).has(code) ||
|
|
1644
|
+
/resum|starting up|temporarily unavailable|timeout|connection terminated unexpectedly|server closed the connection unexpectedly|terminating connection/i.test(message);
|
|
1645
|
+
};
|
|
1646
|
+
const sleep = (milliseconds) =>
|
|
1647
|
+
new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1648
|
+
|
|
1649
|
+
const sqlCodeOnly = (sql) => {
|
|
1650
|
+
let index = 0;
|
|
1651
|
+
let result = '';
|
|
1652
|
+
const masked = (value) => value.replace(/[^\\r\\n]/g, ' ');
|
|
1653
|
+
while (index < sql.length) {
|
|
1654
|
+
if (sql.startsWith('--', index)) {
|
|
1655
|
+
const end = sql.indexOf('\\n', index);
|
|
1656
|
+
const next = end === -1 ? sql.length : end;
|
|
1657
|
+
result += masked(sql.slice(index, next));
|
|
1658
|
+
index = next;
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
if (sql.startsWith('/*', index)) {
|
|
1662
|
+
const start = index;
|
|
1663
|
+
let depth = 1;
|
|
1664
|
+
index += 2;
|
|
1665
|
+
while (index < sql.length && depth > 0) {
|
|
1666
|
+
if (sql.startsWith('/*', index)) {
|
|
1667
|
+
depth += 1;
|
|
1668
|
+
index += 2;
|
|
1669
|
+
} else if (sql.startsWith('*/', index)) {
|
|
1670
|
+
depth -= 1;
|
|
1671
|
+
index += 2;
|
|
1672
|
+
} else {
|
|
1673
|
+
index += 1;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
result += masked(sql.slice(start, index));
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
const quote = sql[index];
|
|
1680
|
+
if (quote === "'" || quote === '"') {
|
|
1681
|
+
const start = index;
|
|
1682
|
+
index += 1;
|
|
1683
|
+
while (index < sql.length) {
|
|
1684
|
+
if (sql[index] === quote) {
|
|
1685
|
+
if (sql[index + 1] === quote) {
|
|
1686
|
+
index += 2;
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
index += 1;
|
|
1690
|
+
break;
|
|
1691
|
+
}
|
|
1692
|
+
if (quote === "'" && sql[index] === '\\\\' && index + 1 < sql.length) {
|
|
1693
|
+
index += 2;
|
|
1694
|
+
} else {
|
|
1695
|
+
index += 1;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
result += masked(sql.slice(start, index));
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
if (sql[index] === '$') {
|
|
1702
|
+
const tag = sql
|
|
1703
|
+
.slice(index)
|
|
1704
|
+
.match(/^\\$(?:[A-Za-z_][A-Za-z0-9_]*)?\\$/)?.[0];
|
|
1705
|
+
if (tag) {
|
|
1706
|
+
const start = index;
|
|
1707
|
+
const close = sql.indexOf(tag, index + tag.length);
|
|
1708
|
+
index = close === -1 ? sql.length : close + tag.length;
|
|
1709
|
+
result += masked(sql.slice(start, index));
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
result += sql[index];
|
|
1714
|
+
index += 1;
|
|
1715
|
+
}
|
|
1716
|
+
return result;
|
|
1717
|
+
};
|
|
1718
|
+
const hasExecutableSql = (sql) => sqlCodeOnly(sql).trim().length > 0;
|
|
1719
|
+
const unsupportedNonTransactionalSql =
|
|
1720
|
+
/\\b(?:CREATE|DROP)\\s+INDEX\\s+CONCURRENTLY\\b|\\bVACUUM\\b/i;
|
|
1721
|
+
|
|
1722
|
+
async function main() {
|
|
1723
|
+
migrationId = required('EBK_MIGRATION_ID');
|
|
1724
|
+
lifecycleLogger = createDatabaseLifecycleLogger({
|
|
1725
|
+
component: 'database-migration',
|
|
1726
|
+
correlationId: required('EBK_DATABASE_LIFECYCLE_CORRELATION_ID'),
|
|
1727
|
+
databaseLifecycleId: migrationId,
|
|
1728
|
+
operation: 'database.migrate',
|
|
1729
|
+
slice: '${name}',
|
|
1730
|
+
stage: required('EBK_STAGE'),
|
|
1731
|
+
workspace: required('EBK_WORKSPACE'),
|
|
1732
|
+
});
|
|
1733
|
+
const host = required('EBK_DATABASE_HOST');
|
|
1734
|
+
const database = required('EBK_DATABASE_NAME');
|
|
1735
|
+
const username = required('EBK_DATABASE_USERNAME');
|
|
1736
|
+
const readwriteRole = requiredIdentifier('EBK_DATABASE_READWRITE_ROLE');
|
|
1737
|
+
const region = required('AWS_REGION');
|
|
1738
|
+
const caBundlePath = required('EBK_RDS_CA_BUNDLE');
|
|
1739
|
+
const port = Number(required('EBK_DATABASE_PORT'));
|
|
1740
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
|
|
1741
|
+
throw new Error('EBK_DATABASE_PORT must be a valid TCP port.');
|
|
1742
|
+
}
|
|
1743
|
+
migrationPhase = 'load_ca_bundle';
|
|
1744
|
+
const ca = await readFile(caBundlePath, 'utf8');
|
|
1745
|
+
const connectDeadline = Date.now() + 8 * 60 * 1000;
|
|
1746
|
+
let attempt = 0;
|
|
1747
|
+
let client;
|
|
1748
|
+
while (!client) {
|
|
1749
|
+
attempt += 1;
|
|
1750
|
+
let candidate;
|
|
1751
|
+
try {
|
|
1752
|
+
migrationPhase = 'connect';
|
|
1753
|
+
const password = await new Signer({
|
|
1754
|
+
hostname: host,
|
|
1755
|
+
port,
|
|
1756
|
+
region,
|
|
1757
|
+
username,
|
|
1758
|
+
}).getAuthToken();
|
|
1759
|
+
candidate = new Client({
|
|
1760
|
+
application_name: migrationId,
|
|
1761
|
+
connectionTimeoutMillis: 15_000,
|
|
1762
|
+
database,
|
|
1763
|
+
host,
|
|
1764
|
+
password,
|
|
1765
|
+
port,
|
|
1766
|
+
ssl: { ca, rejectUnauthorized: true },
|
|
1767
|
+
user: username,
|
|
1768
|
+
});
|
|
1769
|
+
await candidate.connect();
|
|
1770
|
+
client = candidate;
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
await candidate?.end().catch(() => undefined);
|
|
1773
|
+
if (!retryableConnection(error) || Date.now() >= connectDeadline) {
|
|
1774
|
+
throw error;
|
|
1775
|
+
}
|
|
1776
|
+
const delayMs = Math.min(attempt * 5_000, 30_000);
|
|
1777
|
+
log('database_connection_retry', {
|
|
1778
|
+
attempt,
|
|
1779
|
+
delayMs,
|
|
1780
|
+
...safeErrorFields(error),
|
|
1781
|
+
});
|
|
1782
|
+
await sleep(delayMs);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
let clientFailure;
|
|
1787
|
+
client.on('error', (error) => {
|
|
1788
|
+
clientFailure ??= error;
|
|
1789
|
+
log('database_client_error', {
|
|
1790
|
+
phase: migrationPhase,
|
|
1791
|
+
...safeErrorFields(error),
|
|
1792
|
+
});
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
let locked = false;
|
|
1796
|
+
try {
|
|
1797
|
+
migrationPhase = 'lock';
|
|
1798
|
+
await client.query("SET lock_timeout = '5min'");
|
|
1799
|
+
await client.query(
|
|
1800
|
+
'SELECT pg_advisory_lock(hashtextextended($1, 0))',
|
|
1801
|
+
[migrationId],
|
|
1802
|
+
);
|
|
1803
|
+
locked = true;
|
|
1804
|
+
migrationPhase = 'ledger';
|
|
1805
|
+
await client.query(
|
|
1806
|
+
'CREATE TABLE IF NOT EXISTS _ebk_migrations (' +
|
|
1807
|
+
'name text PRIMARY KEY, ' +
|
|
1808
|
+
'checksum text NOT NULL, ' +
|
|
1809
|
+
'applied_at timestamptz NOT NULL DEFAULT now())',
|
|
1810
|
+
);
|
|
1811
|
+
const directory = new URL('../database/migrations/', import.meta.url);
|
|
1812
|
+
const files = (await readdir(directory))
|
|
1813
|
+
.filter((file) => file.endsWith('.sql'))
|
|
1814
|
+
.sort();
|
|
1815
|
+
for (const name of files) {
|
|
1816
|
+
migrationPhase = 'migration:' + name;
|
|
1817
|
+
const sql = await readFile(new URL(name, directory), 'utf8');
|
|
1818
|
+
if (!hasExecutableSql(sql)) {
|
|
1819
|
+
throw new Error('Migration contains no executable SQL.');
|
|
1820
|
+
}
|
|
1821
|
+
if (unsupportedNonTransactionalSql.test(sqlCodeOnly(sql))) {
|
|
1822
|
+
throw new Error(
|
|
1823
|
+
'Migration requires an unsupported non-transactional operation.',
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
const checksum = createHash('sha256').update(sql).digest('hex');
|
|
1827
|
+
const existing = await client.query(
|
|
1828
|
+
'SELECT checksum FROM _ebk_migrations WHERE name = $1',
|
|
1829
|
+
[name],
|
|
1830
|
+
);
|
|
1831
|
+
const appliedChecksum = existing.rows[0]?.checksum;
|
|
1832
|
+
if (appliedChecksum === checksum) {
|
|
1833
|
+
log('migration_skipped', { checksum, name });
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
if (appliedChecksum) {
|
|
1837
|
+
throw new Error('An applied migration checksum changed.');
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
await client.query('BEGIN');
|
|
1841
|
+
try {
|
|
1842
|
+
const statements = sql
|
|
1843
|
+
.split(/^--> statement-breakpoint\\s*$/m)
|
|
1844
|
+
.map((value) => value.trim())
|
|
1845
|
+
.filter(Boolean);
|
|
1846
|
+
for (const statement of statements) {
|
|
1847
|
+
await client.query(statement);
|
|
1848
|
+
}
|
|
1849
|
+
await client.query(
|
|
1850
|
+
'INSERT INTO _ebk_migrations (name, checksum) VALUES ($1, $2)',
|
|
1851
|
+
[name, checksum],
|
|
1852
|
+
);
|
|
1853
|
+
await client.query('COMMIT');
|
|
1854
|
+
log('migration_applied', { checksum, name });
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
await client.query('ROLLBACK').catch((rollbackError) => {
|
|
1857
|
+
log('migration_rollback_failed', {
|
|
1858
|
+
phase: migrationPhase,
|
|
1859
|
+
...safeErrorFields(rollbackError),
|
|
1860
|
+
});
|
|
1861
|
+
});
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
migrationPhase = 'audit-capability';
|
|
1866
|
+
await client.query(
|
|
1867
|
+
'REVOKE UPDATE, DELETE, TRUNCATE ON TABLE ' +
|
|
1868
|
+
'authorization_audit, data_lifecycle_audit FROM ' +
|
|
1869
|
+
quoteIdentifier(readwriteRole),
|
|
1870
|
+
);
|
|
1871
|
+
await client.query(
|
|
1872
|
+
'GRANT EXECUTE ON FUNCTION ' +
|
|
1873
|
+
'ebk_prune_authorization_audit(timestamptz, integer), ' +
|
|
1874
|
+
'ebk_prune_data_lifecycle_audit(timestamptz, integer) TO ' +
|
|
1875
|
+
quoteIdentifier(readwriteRole),
|
|
1876
|
+
);
|
|
1877
|
+
if (clientFailure) throw clientFailure;
|
|
1878
|
+
log('migration_completed', { count: files.length });
|
|
1879
|
+
} finally {
|
|
1880
|
+
migrationPhase = 'cleanup';
|
|
1881
|
+
if (locked) {
|
|
1882
|
+
await client.query(
|
|
1883
|
+
'SELECT pg_advisory_unlock(hashtextextended($1, 0))',
|
|
1884
|
+
[migrationId],
|
|
1885
|
+
).catch(() => undefined);
|
|
1886
|
+
}
|
|
1887
|
+
await client.end().catch(() => undefined);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
await main().catch((error) => {
|
|
1892
|
+
log('migration_failed', {
|
|
1893
|
+
phase: migrationPhase,
|
|
1894
|
+
...safeErrorFields(error),
|
|
1895
|
+
});
|
|
1896
|
+
process.exitCode = 1;
|
|
1897
|
+
});
|
|
1898
|
+
`,
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
//# sourceMappingURL=built-in-plan-database-lifecycle-files.js.map
|