@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,2604 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderProductDataRetentionJobFiles = renderProductDataRetentionJobFiles;
|
|
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
|
+
/** @internal */
|
|
7
|
+
function renderProductDataRetentionJobFiles(name, runtimeVersion) {
|
|
8
|
+
const pascalName = name
|
|
9
|
+
.split('-')
|
|
10
|
+
.map((segment) => segment[0]?.toUpperCase() + segment.slice(1))
|
|
11
|
+
.join('');
|
|
12
|
+
const identifierPrefix = pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
|
|
13
|
+
return {
|
|
14
|
+
'data/retention-control.ts': `import {
|
|
15
|
+
createApprovalRequest,
|
|
16
|
+
decideApprovalRequest,
|
|
17
|
+
digestApprovalRequest,
|
|
18
|
+
type ApprovalRequest,
|
|
19
|
+
type AuthorizationPrincipal,
|
|
20
|
+
} from '@empire-builder-kit/runtime/authorization';
|
|
21
|
+
import {
|
|
22
|
+
createDataLifecyclePseudonymousReference,
|
|
23
|
+
} from '@empire-builder-kit/runtime/data-lifecycle';
|
|
24
|
+
import type { Client as PgClient } from 'pg';
|
|
25
|
+
import {
|
|
26
|
+
${identifierPrefix}AuthorizationPolicy,
|
|
27
|
+
} from '../authorization/policy.js';
|
|
28
|
+
import { requireDirectAdmin } from './data-rights-shared.js';
|
|
29
|
+
import { approvalReference } from './pseudonymous-reference.js';
|
|
30
|
+
import {
|
|
31
|
+
${pascalName}DataLifecycleRuntimePolicy,
|
|
32
|
+
} from './runtime-policy.cjs';
|
|
33
|
+
import { withPrimaryDatabase } from '../database/runtime-client.js';
|
|
34
|
+
|
|
35
|
+
const ACTION = '${name}:retention-apply';
|
|
36
|
+
const POLICY_VERSION =
|
|
37
|
+
${pascalName}DataLifecycleRuntimePolicy.policyVersion;
|
|
38
|
+
const RETENTION_DAYS =
|
|
39
|
+
${pascalName}DataLifecycleRuntimePolicy.retentionDays;
|
|
40
|
+
const AUTHORIZATION_POLICY_VERSION =
|
|
41
|
+
${identifierPrefix}AuthorizationPolicy.version;
|
|
42
|
+
|
|
43
|
+
export interface ${pascalName}RetentionPlanV1 {
|
|
44
|
+
asOf: string;
|
|
45
|
+
candidateDigest: string;
|
|
46
|
+
categoryId: 'product-record';
|
|
47
|
+
due: number;
|
|
48
|
+
locationId: 'primary-database';
|
|
49
|
+
maxItems: 10000;
|
|
50
|
+
pageSize: 100;
|
|
51
|
+
policyVersion: string;
|
|
52
|
+
retentionDays: number;
|
|
53
|
+
runId: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface RetentionReportRow {
|
|
57
|
+
approvalRequestId: string | null;
|
|
58
|
+
plan: ${pascalName}RetentionPlanV1;
|
|
59
|
+
revision: number;
|
|
60
|
+
status: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function plusDays(value: string, days: number): string {
|
|
64
|
+
const result = new Date(value);
|
|
65
|
+
result.setUTCDate(result.getUTCDate() + days);
|
|
66
|
+
return result.toISOString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function runReference(runId: string): string {
|
|
70
|
+
return createDataLifecyclePseudonymousReference({
|
|
71
|
+
categoryId: 'lifecycle-control-metadata',
|
|
72
|
+
slice: '${name}',
|
|
73
|
+
value: runId,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function requirePlan(
|
|
78
|
+
value: ${pascalName}RetentionPlanV1,
|
|
79
|
+
runId: string,
|
|
80
|
+
): ${pascalName}RetentionPlanV1 {
|
|
81
|
+
if (
|
|
82
|
+
value?.runId !== runId ||
|
|
83
|
+
value.policyVersion !== POLICY_VERSION ||
|
|
84
|
+
value.categoryId !== 'product-record' ||
|
|
85
|
+
value.locationId !== 'primary-database' ||
|
|
86
|
+
value.maxItems !== 10000 ||
|
|
87
|
+
value.pageSize !== 100 ||
|
|
88
|
+
value.retentionDays !== RETENTION_DAYS ||
|
|
89
|
+
!Number.isSafeInteger(value.due) ||
|
|
90
|
+
value.due < 0 ||
|
|
91
|
+
!/^sha256:[a-f0-9]{64}$/.test(value.candidateDigest)
|
|
92
|
+
) {
|
|
93
|
+
throw new Error('Retention report plan is invalid.');
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function loadReport(
|
|
99
|
+
client: PgClient,
|
|
100
|
+
runId: string,
|
|
101
|
+
): Promise<RetentionReportRow> {
|
|
102
|
+
const result = await client.query<{
|
|
103
|
+
approvalRequestId: string | null;
|
|
104
|
+
plan: ${pascalName}RetentionPlanV1;
|
|
105
|
+
revision: number;
|
|
106
|
+
status: string;
|
|
107
|
+
}>(
|
|
108
|
+
\`SELECT approval_request_id::text AS "approvalRequestId",
|
|
109
|
+
plan_json AS plan, revision, status
|
|
110
|
+
FROM data_lifecycle_retention_runs
|
|
111
|
+
WHERE id = $1::uuid
|
|
112
|
+
FOR UPDATE\`,
|
|
113
|
+
[runId],
|
|
114
|
+
);
|
|
115
|
+
const row = result.rows[0];
|
|
116
|
+
if (!row) throw new Error('Retention report was not found.');
|
|
117
|
+
return { ...row, plan: requirePlan(row.plan, runId) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function writeAudit(
|
|
121
|
+
client: PgClient,
|
|
122
|
+
input: {
|
|
123
|
+
action: string;
|
|
124
|
+
approvalRequestId: string;
|
|
125
|
+
correlationId: string;
|
|
126
|
+
principal: AuthorizationPrincipal;
|
|
127
|
+
reasonCategory: string;
|
|
128
|
+
runId: string;
|
|
129
|
+
},
|
|
130
|
+
) {
|
|
131
|
+
await client.query(
|
|
132
|
+
\`INSERT INTO authorization_audit
|
|
133
|
+
(action, approval_reference, authority_mode, correlation_id, decision,
|
|
134
|
+
delegation_reference, policy_version, principal_kind,
|
|
135
|
+
pseudonymous_subject_id, reason_category, resource_reference,
|
|
136
|
+
resource_type, slice, stage, subject_type)
|
|
137
|
+
VALUES ($1, $2, 'direct', $3, 'allow', NULL, $4, $5, $6, $7, $8,
|
|
138
|
+
'retention-run', $9, $10, $11)\`,
|
|
139
|
+
[
|
|
140
|
+
input.action,
|
|
141
|
+
approvalReference(input.approvalRequestId),
|
|
142
|
+
input.correlationId,
|
|
143
|
+
AUTHORIZATION_POLICY_VERSION,
|
|
144
|
+
input.principal.kind,
|
|
145
|
+
createDataLifecyclePseudonymousReference({
|
|
146
|
+
categoryId: 'authorization-subject',
|
|
147
|
+
slice: '${name}',
|
|
148
|
+
value: input.principal.actor.id,
|
|
149
|
+
}),
|
|
150
|
+
input.reasonCategory,
|
|
151
|
+
runReference(input.runId),
|
|
152
|
+
'${name}',
|
|
153
|
+
process.env.EBK_STAGE ?? 'development',
|
|
154
|
+
input.principal.subject.type,
|
|
155
|
+
],
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function request${pascalName}RetentionApproval(input: {
|
|
160
|
+
correlationId: string;
|
|
161
|
+
now: string;
|
|
162
|
+
principal: AuthorizationPrincipal;
|
|
163
|
+
runId: string;
|
|
164
|
+
}) {
|
|
165
|
+
requireDirectAdmin(input.principal);
|
|
166
|
+
return withPrimaryDatabase(
|
|
167
|
+
{ correlationId: input.correlationId },
|
|
168
|
+
'api-database-records',
|
|
169
|
+
async (client) => {
|
|
170
|
+
await client.query('BEGIN');
|
|
171
|
+
try {
|
|
172
|
+
const report = await loadReport(client, input.runId);
|
|
173
|
+
if (
|
|
174
|
+
report.status !== 'reported' ||
|
|
175
|
+
report.approvalRequestId !== null
|
|
176
|
+
) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
'Retention report already has an approval request or is no longer reviewable.',
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const approval = createApprovalRequest({
|
|
182
|
+
action: ACTION,
|
|
183
|
+
author: input.principal,
|
|
184
|
+
command: report.plan,
|
|
185
|
+
expiresAt: plusDays(input.now, 1),
|
|
186
|
+
now: input.now,
|
|
187
|
+
policyVersion: AUTHORIZATION_POLICY_VERSION,
|
|
188
|
+
resource: { id: input.runId, type: 'retention-run' },
|
|
189
|
+
slice: '${name}',
|
|
190
|
+
});
|
|
191
|
+
await client.query(
|
|
192
|
+
\`INSERT INTO approval_requests
|
|
193
|
+
(id, author_reference, expires_at, policy_version,
|
|
194
|
+
request_digest, request_json, revision, status)
|
|
195
|
+
VALUES ($1::uuid, $2, $3, $4, $5, $6::jsonb, $7, $8)\`,
|
|
196
|
+
[
|
|
197
|
+
approval.id,
|
|
198
|
+
approval.author.effectiveHumanId ?? approval.author.actorId,
|
|
199
|
+
approval.expiresAt,
|
|
200
|
+
approval.policyVersion,
|
|
201
|
+
approval.requestDigest,
|
|
202
|
+
JSON.stringify(approval),
|
|
203
|
+
approval.revision,
|
|
204
|
+
approval.status,
|
|
205
|
+
],
|
|
206
|
+
);
|
|
207
|
+
const updated = await client.query(
|
|
208
|
+
\`UPDATE data_lifecycle_retention_runs
|
|
209
|
+
SET approval_request_id = $3::uuid, revision = revision + 1,
|
|
210
|
+
status = 'approval-pending', updated_at = $4
|
|
211
|
+
WHERE id = $1::uuid AND revision = $2
|
|
212
|
+
AND approval_request_id IS NULL AND status = 'reported'\`,
|
|
213
|
+
[input.runId, report.revision, approval.id, input.now],
|
|
214
|
+
);
|
|
215
|
+
if (updated.rowCount !== 1) {
|
|
216
|
+
throw new Error('Retention approval request lost compare-and-set.');
|
|
217
|
+
}
|
|
218
|
+
await writeAudit(client, {
|
|
219
|
+
action: '${name}:retention-approval-request',
|
|
220
|
+
approvalRequestId: approval.id,
|
|
221
|
+
correlationId: input.correlationId,
|
|
222
|
+
principal: input.principal,
|
|
223
|
+
reasonCategory: 'approval-pending',
|
|
224
|
+
runId: input.runId,
|
|
225
|
+
});
|
|
226
|
+
await client.query('COMMIT');
|
|
227
|
+
return {
|
|
228
|
+
approvalRequestId: approval.id,
|
|
229
|
+
expiresAt: approval.expiresAt,
|
|
230
|
+
requestDigest: approval.requestDigest,
|
|
231
|
+
status: approval.status,
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function decide${pascalName}RetentionApproval(input: {
|
|
242
|
+
approvalRequestId: string;
|
|
243
|
+
correlationId: string;
|
|
244
|
+
expectedRequestDigest: string;
|
|
245
|
+
now: string;
|
|
246
|
+
outcome: 'approved' | 'rejected';
|
|
247
|
+
principal: AuthorizationPrincipal;
|
|
248
|
+
reasonReference: string;
|
|
249
|
+
runId: string;
|
|
250
|
+
}) {
|
|
251
|
+
requireDirectAdmin(input.principal);
|
|
252
|
+
return withPrimaryDatabase(
|
|
253
|
+
{ correlationId: input.correlationId },
|
|
254
|
+
'api-database-records',
|
|
255
|
+
async (client) => {
|
|
256
|
+
await client.query('BEGIN');
|
|
257
|
+
try {
|
|
258
|
+
const report = await loadReport(client, input.runId);
|
|
259
|
+
if (
|
|
260
|
+
report.status !== 'approval-pending' ||
|
|
261
|
+
report.approvalRequestId !== input.approvalRequestId
|
|
262
|
+
) {
|
|
263
|
+
throw new Error('Retention approval does not match the report.');
|
|
264
|
+
}
|
|
265
|
+
const stored = await client.query<{
|
|
266
|
+
request: ApprovalRequest<string>;
|
|
267
|
+
}>(
|
|
268
|
+
\`SELECT request_json AS request
|
|
269
|
+
FROM approval_requests
|
|
270
|
+
WHERE id = $1::uuid
|
|
271
|
+
FOR UPDATE\`,
|
|
272
|
+
[input.approvalRequestId],
|
|
273
|
+
);
|
|
274
|
+
const approval = stored.rows[0]?.request;
|
|
275
|
+
if (
|
|
276
|
+
!approval ||
|
|
277
|
+
approval.id !== input.approvalRequestId ||
|
|
278
|
+
approval.action !== ACTION ||
|
|
279
|
+
approval.policyVersion !== AUTHORIZATION_POLICY_VERSION ||
|
|
280
|
+
approval.resource.id !== input.runId ||
|
|
281
|
+
approval.resource.type !== 'retention-run' ||
|
|
282
|
+
approval.schemaVersion !== 1 ||
|
|
283
|
+
approval.slice !== '${name}' ||
|
|
284
|
+
digestApprovalRequest({
|
|
285
|
+
action: ACTION,
|
|
286
|
+
command: report.plan,
|
|
287
|
+
resource: { id: input.runId, type: 'retention-run' },
|
|
288
|
+
}) !== approval.requestDigest
|
|
289
|
+
) {
|
|
290
|
+
throw new Error('Retention approval is not bound to the report.');
|
|
291
|
+
}
|
|
292
|
+
const transition = decideApprovalRequest(approval, {
|
|
293
|
+
approver: input.principal,
|
|
294
|
+
expectedRequestDigest: input.expectedRequestDigest,
|
|
295
|
+
now: input.now,
|
|
296
|
+
outcome: input.outcome,
|
|
297
|
+
reasonReference: input.reasonReference,
|
|
298
|
+
requiredPolicyVersion: AUTHORIZATION_POLICY_VERSION,
|
|
299
|
+
});
|
|
300
|
+
if (transition.decision.decision !== 'allow') {
|
|
301
|
+
throw new Error(
|
|
302
|
+
'Retention approval decision was denied: ' +
|
|
303
|
+
transition.decision.category,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const approvalUpdated = await client.query(
|
|
307
|
+
\`UPDATE approval_requests
|
|
308
|
+
SET request_json = $3::jsonb, revision = $4, status = $5
|
|
309
|
+
WHERE id = $1::uuid AND revision = $2\`,
|
|
310
|
+
[
|
|
311
|
+
approval.id,
|
|
312
|
+
approval.revision,
|
|
313
|
+
JSON.stringify(transition.request),
|
|
314
|
+
transition.request.revision,
|
|
315
|
+
transition.request.status,
|
|
316
|
+
],
|
|
317
|
+
);
|
|
318
|
+
const reportUpdated = await client.query(
|
|
319
|
+
\`UPDATE data_lifecycle_retention_runs
|
|
320
|
+
SET revision = revision + 1, status = $3, updated_at = $4
|
|
321
|
+
WHERE id = $1::uuid AND revision = $2
|
|
322
|
+
AND status = 'approval-pending'\`,
|
|
323
|
+
[
|
|
324
|
+
input.runId,
|
|
325
|
+
report.revision,
|
|
326
|
+
transition.request.status === 'approved'
|
|
327
|
+
? 'approval-approved'
|
|
328
|
+
: 'approval-rejected',
|
|
329
|
+
input.now,
|
|
330
|
+
],
|
|
331
|
+
);
|
|
332
|
+
if (
|
|
333
|
+
approvalUpdated.rowCount !== 1 ||
|
|
334
|
+
reportUpdated.rowCount !== 1
|
|
335
|
+
) {
|
|
336
|
+
throw new Error('Retention approval decision lost compare-and-set.');
|
|
337
|
+
}
|
|
338
|
+
await writeAudit(client, {
|
|
339
|
+
action: '${name}:retention-approval-decision',
|
|
340
|
+
approvalRequestId: approval.id,
|
|
341
|
+
correlationId: input.correlationId,
|
|
342
|
+
principal: input.principal,
|
|
343
|
+
reasonCategory:
|
|
344
|
+
transition.request.status === 'approved'
|
|
345
|
+
? 'approval-approved'
|
|
346
|
+
: 'approval-rejected',
|
|
347
|
+
runId: input.runId,
|
|
348
|
+
});
|
|
349
|
+
await client.query('COMMIT');
|
|
350
|
+
return {
|
|
351
|
+
approvalRequestId: approval.id,
|
|
352
|
+
status: transition.request.status as 'approved' | 'rejected',
|
|
353
|
+
};
|
|
354
|
+
} catch (error) {
|
|
355
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
`,
|
|
362
|
+
'jobs/data-retention/src/approval-compatibility.spec.ts': `import {
|
|
363
|
+
beginApprovalExecution,
|
|
364
|
+
createApprovalRequest,
|
|
365
|
+
decideApprovalRequest,
|
|
366
|
+
type AuthorizationPrincipal,
|
|
367
|
+
} from '@empire-builder-kit/runtime/authorization';
|
|
368
|
+
import { describe, expect, it } from 'vitest';
|
|
369
|
+
|
|
370
|
+
function human(id: string, kind: 'admin' | 'customer') {
|
|
371
|
+
return {
|
|
372
|
+
actor: { id, type: 'user' as const },
|
|
373
|
+
authority: { mode: 'direct' as const },
|
|
374
|
+
kind,
|
|
375
|
+
roles: kind === 'admin' ? ['${name}-admin'] : [],
|
|
376
|
+
scopes: [],
|
|
377
|
+
subject: { id, type: 'user' as const },
|
|
378
|
+
} satisfies AuthorizationPrincipal;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
describe('${pascalName} retention approval compatibility', () => {
|
|
382
|
+
it('denies stale apply and stale exact-retry policy versions', () => {
|
|
383
|
+
const runId = '00000000-0000-4000-8000-000000000001';
|
|
384
|
+
const plan = {
|
|
385
|
+
asOf: '2026-07-27T12:00:00.000Z',
|
|
386
|
+
candidateDigest: 'sha256:' + 'a'.repeat(64),
|
|
387
|
+
categoryId: 'product-record',
|
|
388
|
+
due: 1,
|
|
389
|
+
locationId: 'primary-database',
|
|
390
|
+
maxItems: 10000,
|
|
391
|
+
pageSize: 100,
|
|
392
|
+
policyVersion: '1.0.0',
|
|
393
|
+
retentionDays: 365,
|
|
394
|
+
runId,
|
|
395
|
+
};
|
|
396
|
+
const pending = createApprovalRequest({
|
|
397
|
+
action: '${name}:retention-apply',
|
|
398
|
+
author: human('report-author', 'admin'),
|
|
399
|
+
command: plan,
|
|
400
|
+
expiresAt: '2026-07-27T13:00:00.000Z',
|
|
401
|
+
now: '2026-07-27T12:00:00.000Z',
|
|
402
|
+
policyVersion: '1.0.0',
|
|
403
|
+
resource: { id: runId, type: 'retention-run' },
|
|
404
|
+
slice: '${name}',
|
|
405
|
+
});
|
|
406
|
+
expect(
|
|
407
|
+
decideApprovalRequest(pending, {
|
|
408
|
+
approver: human('privacy-reviewer', 'admin'),
|
|
409
|
+
expectedRequestDigest: pending.requestDigest,
|
|
410
|
+
now: '2026-07-27T12:05:00.000Z',
|
|
411
|
+
outcome: 'approved',
|
|
412
|
+
reasonReference: 'privacy-review-stale',
|
|
413
|
+
requiredPolicyVersion: '2.0.0',
|
|
414
|
+
}),
|
|
415
|
+
).toMatchObject({
|
|
416
|
+
decision: {
|
|
417
|
+
category: 'policy-stale',
|
|
418
|
+
decision: 'deny',
|
|
419
|
+
policyVersion: '2.0.0',
|
|
420
|
+
},
|
|
421
|
+
request: pending,
|
|
422
|
+
});
|
|
423
|
+
const approved = decideApprovalRequest(pending, {
|
|
424
|
+
approver: human('privacy-reviewer', 'admin'),
|
|
425
|
+
expectedRequestDigest: pending.requestDigest,
|
|
426
|
+
now: '2026-07-27T12:05:00.000Z',
|
|
427
|
+
outcome: 'approved',
|
|
428
|
+
reasonReference: 'privacy-review-1',
|
|
429
|
+
requiredPolicyVersion: '1.0.0',
|
|
430
|
+
}).request;
|
|
431
|
+
const input = {
|
|
432
|
+
action: '${name}:retention-apply',
|
|
433
|
+
command: plan,
|
|
434
|
+
executionId: runId,
|
|
435
|
+
now: '2026-07-27T12:06:00.000Z',
|
|
436
|
+
resource: { id: runId, type: 'retention-run' },
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
expect(
|
|
440
|
+
beginApprovalExecution(approved, {
|
|
441
|
+
...input,
|
|
442
|
+
requiredPolicyVersion: '2.0.0',
|
|
443
|
+
}),
|
|
444
|
+
).toMatchObject({
|
|
445
|
+
decision: {
|
|
446
|
+
category: 'policy-stale',
|
|
447
|
+
decision: 'deny',
|
|
448
|
+
policyVersion: '2.0.0',
|
|
449
|
+
},
|
|
450
|
+
request: { status: 'approved' },
|
|
451
|
+
});
|
|
452
|
+
const begun = beginApprovalExecution(approved, {
|
|
453
|
+
...input,
|
|
454
|
+
requiredPolicyVersion: '1.0.0',
|
|
455
|
+
});
|
|
456
|
+
expect(begun.decision).toMatchObject({ decision: 'allow' });
|
|
457
|
+
expect(
|
|
458
|
+
beginApprovalExecution(begun.request, {
|
|
459
|
+
...input,
|
|
460
|
+
now: '2026-07-27T12:07:00.000Z',
|
|
461
|
+
requiredPolicyVersion: '2.0.0',
|
|
462
|
+
}),
|
|
463
|
+
).toMatchObject({
|
|
464
|
+
decision: {
|
|
465
|
+
category: 'policy-stale',
|
|
466
|
+
decision: 'deny',
|
|
467
|
+
policyVersion: '2.0.0',
|
|
468
|
+
},
|
|
469
|
+
request: begun.request,
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
`,
|
|
474
|
+
'jobs/data-retention/package.json': (0, built_in_plan_shared_js_1.json)({
|
|
475
|
+
name: `@${name}/job-data-retention`,
|
|
476
|
+
private: true,
|
|
477
|
+
type: 'module',
|
|
478
|
+
version: '0.0.0',
|
|
479
|
+
scripts: {
|
|
480
|
+
build: 'tsc -p tsconfig.build.json',
|
|
481
|
+
'docker-build': `docker build -f packages/${name}/jobs/data-retention/Dockerfile -t ${name}-data-retention:local ../../../..`,
|
|
482
|
+
lint: 'eslint .',
|
|
483
|
+
test: 'vitest run',
|
|
484
|
+
typecheck: 'tsc --noEmit -p tsconfig.json',
|
|
485
|
+
verify: 'pnpm run lint && pnpm run typecheck && pnpm test && pnpm run build',
|
|
486
|
+
},
|
|
487
|
+
dependencies: {
|
|
488
|
+
'@aws-sdk/client-eventbridge': built_in_plan_shared_js_1.PRODUCT_VERSIONS.awsEventBridge,
|
|
489
|
+
'@aws-sdk/client-sfn': built_in_plan_shared_js_1.PRODUCT_VERSIONS.awsSfn,
|
|
490
|
+
'@empire-builder-kit/runtime': runtimeVersion,
|
|
491
|
+
pg: built_in_plan_shared_js_1.PRODUCT_VERSIONS.pg,
|
|
492
|
+
},
|
|
493
|
+
devDependencies: {
|
|
494
|
+
'@types/node': built_in_plan_shared_js_1.PRODUCT_VERSIONS.typesNode,
|
|
495
|
+
'@types/pg': built_in_plan_shared_js_1.PRODUCT_VERSIONS.typesPg,
|
|
496
|
+
typescript: '~5.9.2',
|
|
497
|
+
vitest: built_in_plan_shared_js_1.PRODUCT_VERSIONS.vitest,
|
|
498
|
+
},
|
|
499
|
+
}),
|
|
500
|
+
'jobs/data-retention/tsconfig.json': (0, built_in_plan_shared_js_1.json)({
|
|
501
|
+
compilerOptions: {
|
|
502
|
+
forceConsistentCasingInFileNames: true,
|
|
503
|
+
module: 'nodenext',
|
|
504
|
+
moduleResolution: 'nodenext',
|
|
505
|
+
noEmitOnError: true,
|
|
506
|
+
outDir: 'dist',
|
|
507
|
+
resolveJsonModule: true,
|
|
508
|
+
rootDir: '../..',
|
|
509
|
+
strict: true,
|
|
510
|
+
target: 'es2022',
|
|
511
|
+
types: ['node'],
|
|
512
|
+
},
|
|
513
|
+
include: [
|
|
514
|
+
'src/**/*.ts',
|
|
515
|
+
'../../authorization/policy.ts',
|
|
516
|
+
'../../contracts/events.ts',
|
|
517
|
+
'../../data/lifecycle-event.ts',
|
|
518
|
+
'../../data/policy.json',
|
|
519
|
+
'../../data/pseudonymous-reference.ts',
|
|
520
|
+
'../../data/runtime-policy.cts',
|
|
521
|
+
'../../secrets/declarations.ts',
|
|
522
|
+
],
|
|
523
|
+
}),
|
|
524
|
+
'jobs/data-retention/tsconfig.build.json': (0, built_in_plan_shared_js_1.json)({
|
|
525
|
+
extends: './tsconfig.json',
|
|
526
|
+
exclude: ['src/**/*.spec.ts'],
|
|
527
|
+
}),
|
|
528
|
+
'jobs/data-retention/vitest.config.ts': `import { configDefaults, defineConfig } from 'vitest/config';
|
|
529
|
+
|
|
530
|
+
export default defineConfig({
|
|
531
|
+
test: {
|
|
532
|
+
environment: 'node',
|
|
533
|
+
exclude: [...configDefaults.exclude, '**/dist/**'],
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
`,
|
|
537
|
+
'jobs/data-retention/Dockerfile': `FROM ${frozen_container_js_1.WORKLOAD_CONTAINER_NODE_IMAGE} AS build
|
|
538
|
+
ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org
|
|
539
|
+
ENV COREPACK_NPM_REGISTRY=$NPM_CONFIG_REGISTRY
|
|
540
|
+
ENV NPM_CONFIG_REGISTRY=$NPM_CONFIG_REGISTRY
|
|
541
|
+
ENV PNPM_HOME=/pnpm
|
|
542
|
+
ENV PATH=/pnpm:$PATH
|
|
543
|
+
WORKDIR /workspace
|
|
544
|
+
RUN corepack enable && corepack prepare ${frozen_container_js_1.WORKLOAD_CONTAINER_PNPM_DESCRIPTOR} --activate
|
|
545
|
+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
546
|
+
RUN pnpm fetch --frozen-lockfile
|
|
547
|
+
COPY packages/${name}/jobs/data-retention/package.json packages/${name}/jobs/data-retention/package.json
|
|
548
|
+
RUN pnpm --filter @${name}/job-data-retention install --offline --frozen-lockfile
|
|
549
|
+
COPY packages/${name}/jobs/data-retention/tsconfig.json packages/${name}/jobs/data-retention/tsconfig.build.json packages/${name}/jobs/data-retention/
|
|
550
|
+
COPY packages/${name}/jobs/data-retention/src packages/${name}/jobs/data-retention/src
|
|
551
|
+
COPY packages/${name}/authorization/policy.ts packages/${name}/authorization/policy.ts
|
|
552
|
+
COPY packages/${name}/contracts/events.ts packages/${name}/contracts/events.ts
|
|
553
|
+
COPY packages/${name}/data/lifecycle-event.ts packages/${name}/data/lifecycle-event.ts
|
|
554
|
+
COPY packages/${name}/data/policy.json packages/${name}/data/policy.json
|
|
555
|
+
COPY packages/${name}/data/pseudonymous-reference.ts packages/${name}/data/pseudonymous-reference.ts
|
|
556
|
+
COPY packages/${name}/data/runtime-policy.cts packages/${name}/data/runtime-policy.cts
|
|
557
|
+
COPY packages/${name}/secrets/declarations.ts packages/${name}/secrets/declarations.ts
|
|
558
|
+
RUN pnpm --filter @${name}/job-data-retention run build
|
|
559
|
+
RUN --network=none pnpm --config.inject-workspace-packages=true --config.store-dir=/workspace/.ebk/cache/pnpm-store --filter @${name}/job-data-retention deploy --prod --offline --frozen-lockfile /opt/data-retention
|
|
560
|
+
|
|
561
|
+
FROM ${frozen_container_js_1.WORKLOAD_CONTAINER_NODE_IMAGE}
|
|
562
|
+
WORKDIR /app
|
|
563
|
+
ADD --checksum=sha256:e5bb2084ccf45087bda1c9bffdea0eb15ee67f0b91646106e466714f9de3c7e3 https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem ./rds-global-bundle.pem
|
|
564
|
+
COPY --from=build --chown=node:node /opt/data-retention ./
|
|
565
|
+
USER node
|
|
566
|
+
CMD ["node", "dist/jobs/data-retention/src/run.js"]
|
|
567
|
+
`,
|
|
568
|
+
'jobs/data-retention/README.md': `# Data retention Job
|
|
569
|
+
|
|
570
|
+
This is the one generated Standard data-retention Job. The Slice target starts
|
|
571
|
+
its fixed AWS-internal Step Functions workflow; operators never run this image
|
|
572
|
+
or connect to the database directly.
|
|
573
|
+
|
|
574
|
+
Report scans at most 100 pages of 100 records, freezes the exact candidate
|
|
575
|
+
set, and returns its safe run reference without mutating product data or
|
|
576
|
+
creating an approval. The authenticated API creates the exact durable request;
|
|
577
|
+
apply requires approval by a different direct human. The Job claims approval
|
|
578
|
+
with compare-and-set, persists its checkpoint after every page, uses stable
|
|
579
|
+
per-record idempotency, and resumes only the same execution identity.
|
|
580
|
+
|
|
581
|
+
If any candidate changes after the approved scan, apply fails closed. Preserve
|
|
582
|
+
the frozen candidate set, create a new report, and obtain a new non-self
|
|
583
|
+
approval. Never delete or rewrite the changed row to make the old approval
|
|
584
|
+
appear current.
|
|
585
|
+
|
|
586
|
+
Control-row expiry is not part of an approved retention plan. Infrastructure
|
|
587
|
+
must invoke \`dist/jobs/data-retention/src/maintenance.js\` on an independent
|
|
588
|
+
bounded schedule with \`EBK_MAINTENANCE_CORRELATION_ID\`; that entrypoint uses
|
|
589
|
+
the exact \`${name}:retention-maintenance\` action and fails closed on active
|
|
590
|
+
control-metadata holds.
|
|
591
|
+
|
|
592
|
+
The seeded adapter owns the Product Slice's record selection. Keep it aligned
|
|
593
|
+
with \`data/policy.json\`; never log or return raw identifiers or record
|
|
594
|
+
content.
|
|
595
|
+
`,
|
|
596
|
+
'jobs/data-retention/src/adapter.ts': `import { createHash } from 'node:crypto';
|
|
597
|
+
import {
|
|
598
|
+
createDataLifecycleLedgerEntry,
|
|
599
|
+
evaluateDataLifecycleHolds,
|
|
600
|
+
type DataLifecycleRetentionAdapter,
|
|
601
|
+
type DataLifecycleRuntimeHoldV1,
|
|
602
|
+
} from '@empire-builder-kit/runtime/data-lifecycle';
|
|
603
|
+
import { buildEventEntry } from '@empire-builder-kit/runtime/events';
|
|
604
|
+
import type { Client } from 'pg';
|
|
605
|
+
import {
|
|
606
|
+
${pascalName}DataLifecycleChanged,
|
|
607
|
+
} from '../../../contracts/events.js';
|
|
608
|
+
import {
|
|
609
|
+
create${pascalName}DataLifecycleChanged,
|
|
610
|
+
} from '../../../data/lifecycle-event.js';
|
|
611
|
+
import {
|
|
612
|
+
approvalReference,
|
|
613
|
+
productRecordReference,
|
|
614
|
+
} from '../../../data/pseudonymous-reference.js';
|
|
615
|
+
import {
|
|
616
|
+
${pascalName}DataLifecycleRuntimePolicy,
|
|
617
|
+
} from '../../../data/runtime-policy.cjs';
|
|
618
|
+
|
|
619
|
+
const OUTBOX_COPY_ID = 'event-contract';
|
|
620
|
+
const POLICY_VERSION =
|
|
621
|
+
${pascalName}DataLifecycleRuntimePolicy.policyVersion;
|
|
622
|
+
const RETENTION_DAYS =
|
|
623
|
+
${pascalName}DataLifecycleRuntimePolicy.retentionDays;
|
|
624
|
+
|
|
625
|
+
interface RetentionLifecycleEventEntry {
|
|
626
|
+
Detail: string;
|
|
627
|
+
DetailType: string;
|
|
628
|
+
EventBusName: string;
|
|
629
|
+
Source: string;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function ledgerExpiry(completedAt: string): string {
|
|
633
|
+
const expiresAt = new Date(completedAt);
|
|
634
|
+
expiresAt.setUTCDate(expiresAt.getUTCDate() + 400);
|
|
635
|
+
return expiresAt.toISOString();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export interface ${pascalName}RetentionAdapterOptions {
|
|
639
|
+
approvalRequestId?: string;
|
|
640
|
+
asOf: string;
|
|
641
|
+
client: Client;
|
|
642
|
+
completedAt: string;
|
|
643
|
+
correlationId: string;
|
|
644
|
+
eventBusName: string;
|
|
645
|
+
mode: 'apply' | 'report';
|
|
646
|
+
publishLifecycleEvent(
|
|
647
|
+
entry: RetentionLifecycleEventEntry,
|
|
648
|
+
): Promise<void>;
|
|
649
|
+
runId: string;
|
|
650
|
+
stage: string;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export interface ${pascalName}RetentionCandidateRecord {
|
|
654
|
+
candidateOrder: string;
|
|
655
|
+
dueAt: string;
|
|
656
|
+
recordId: string;
|
|
657
|
+
recordRef: string;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Product-specific retention selection is seeded. Raw identifiers remain
|
|
662
|
+
* inside this AWS-internal Job and never enter its checkpoint or evidence.
|
|
663
|
+
*/
|
|
664
|
+
export function create${pascalName}RetentionAdapter(
|
|
665
|
+
options: ${pascalName}RetentionAdapterOptions,
|
|
666
|
+
): DataLifecycleRetentionAdapter & {
|
|
667
|
+
candidateDigest(): string;
|
|
668
|
+
candidateRecords(): readonly ${pascalName}RetentionCandidateRecord[];
|
|
669
|
+
} {
|
|
670
|
+
const recordIds = new Map<string, string>();
|
|
671
|
+
const candidateRecords = new Map<
|
|
672
|
+
string,
|
|
673
|
+
${pascalName}RetentionCandidateRecord
|
|
674
|
+
>();
|
|
675
|
+
const digest = createHash('sha256');
|
|
676
|
+
return {
|
|
677
|
+
candidateDigest() {
|
|
678
|
+
return 'sha256:' + digest.copy().digest('hex');
|
|
679
|
+
},
|
|
680
|
+
candidateRecords() {
|
|
681
|
+
return [...candidateRecords.values()];
|
|
682
|
+
},
|
|
683
|
+
async scan({ asOf, cursor, limit }) {
|
|
684
|
+
const result = await options.client.query<{
|
|
685
|
+
cursorKey: string;
|
|
686
|
+
dueAt: Date;
|
|
687
|
+
id: string;
|
|
688
|
+
}>(
|
|
689
|
+
options.mode === 'report'
|
|
690
|
+
? \`SELECT md5('${name}\\0' || id::text) AS "cursorKey",
|
|
691
|
+
last_activity_at + ($4::int * INTERVAL '1 day') AS "dueAt",
|
|
692
|
+
id::text
|
|
693
|
+
FROM records
|
|
694
|
+
WHERE last_activity_at + ($4::int * INTERVAL '1 day')
|
|
695
|
+
<= $1::timestamptz
|
|
696
|
+
AND test_expires_at IS NULL
|
|
697
|
+
AND ($2::text IS NULL OR md5('${name}\\0' || id::text) > $2)
|
|
698
|
+
ORDER BY md5('${name}\\0' || id::text)
|
|
699
|
+
LIMIT $3\`
|
|
700
|
+
: \`SELECT candidate_order AS "cursorKey", due_at AS "dueAt",
|
|
701
|
+
record_id::text AS id
|
|
702
|
+
FROM data_lifecycle_retention_candidates
|
|
703
|
+
WHERE run_id = $1::uuid
|
|
704
|
+
AND ($2::text IS NULL OR candidate_order > $2)
|
|
705
|
+
ORDER BY candidate_order
|
|
706
|
+
LIMIT $3\`,
|
|
707
|
+
options.mode === 'report'
|
|
708
|
+
? [asOf, cursor ?? null, limit + 1, RETENTION_DAYS]
|
|
709
|
+
: [options.runId, cursor ?? null, limit + 1],
|
|
710
|
+
);
|
|
711
|
+
const pageRows = result.rows.slice(0, limit);
|
|
712
|
+
const items = pageRows.map((row) => {
|
|
713
|
+
const recordRef = productRecordReference(row.id);
|
|
714
|
+
recordIds.set(recordRef, row.id);
|
|
715
|
+
const dueAt = row.dueAt.toISOString();
|
|
716
|
+
candidateRecords.set(recordRef, {
|
|
717
|
+
candidateOrder: row.cursorKey,
|
|
718
|
+
dueAt,
|
|
719
|
+
recordId: row.id,
|
|
720
|
+
recordRef,
|
|
721
|
+
});
|
|
722
|
+
const candidate = {
|
|
723
|
+
categoryId: 'product-record',
|
|
724
|
+
dueAt,
|
|
725
|
+
locationId: 'primary-database',
|
|
726
|
+
recordRef,
|
|
727
|
+
};
|
|
728
|
+
digest.update(
|
|
729
|
+
[
|
|
730
|
+
candidate.categoryId,
|
|
731
|
+
candidate.dueAt,
|
|
732
|
+
candidate.locationId,
|
|
733
|
+
candidate.recordRef,
|
|
734
|
+
].join('\\0') + '\\n',
|
|
735
|
+
'utf8',
|
|
736
|
+
);
|
|
737
|
+
return candidate;
|
|
738
|
+
});
|
|
739
|
+
return {
|
|
740
|
+
items,
|
|
741
|
+
...(result.rows.length > limit
|
|
742
|
+
? { nextCursor: pageRows.at(-1)?.cursorKey }
|
|
743
|
+
: {}),
|
|
744
|
+
};
|
|
745
|
+
},
|
|
746
|
+
async apply({ candidate, idempotencyKey }) {
|
|
747
|
+
const recordId = recordIds.get(candidate.recordRef);
|
|
748
|
+
if (!recordId) {
|
|
749
|
+
throw new Error(
|
|
750
|
+
'Retention apply must use a candidate from this bounded adapter scan.',
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
const ledger = createDataLifecycleLedgerEntry({
|
|
754
|
+
action: 'deletion',
|
|
755
|
+
categoryId: candidate.categoryId,
|
|
756
|
+
effectiveAt: options.completedAt,
|
|
757
|
+
expiresAt: ledgerExpiry(options.completedAt),
|
|
758
|
+
policyVersion: POLICY_VERSION,
|
|
759
|
+
pseudonymousKey: candidate.recordRef,
|
|
760
|
+
});
|
|
761
|
+
const requestRef = approvalReference(
|
|
762
|
+
options.approvalRequestId ?? options.runId + ':' + idempotencyKey,
|
|
763
|
+
);
|
|
764
|
+
const lifecycleEvent = create${pascalName}DataLifecycleChanged({
|
|
765
|
+
action: 'deletion',
|
|
766
|
+
occurredAt: options.completedAt,
|
|
767
|
+
policyVersion: POLICY_VERSION,
|
|
768
|
+
recordId,
|
|
769
|
+
requestRef,
|
|
770
|
+
});
|
|
771
|
+
const built = buildEventEntry(
|
|
772
|
+
${pascalName}DataLifecycleChanged,
|
|
773
|
+
lifecycleEvent,
|
|
774
|
+
{
|
|
775
|
+
busName: options.eventBusName,
|
|
776
|
+
context: { correlationId: options.correlationId },
|
|
777
|
+
eventId: idempotencyKey,
|
|
778
|
+
occurredAt: options.completedAt,
|
|
779
|
+
stage: options.stage,
|
|
780
|
+
},
|
|
781
|
+
);
|
|
782
|
+
let event: RetentionLifecycleEventEntry = {
|
|
783
|
+
Detail: built.entry.detail,
|
|
784
|
+
DetailType: built.entry.detailType,
|
|
785
|
+
EventBusName: built.entry.eventBusName,
|
|
786
|
+
Source: built.entry.source,
|
|
787
|
+
};
|
|
788
|
+
let deletedNow = false;
|
|
789
|
+
let publishedAt: Date | null = null;
|
|
790
|
+
let copyStatus: string | undefined;
|
|
791
|
+
await options.client.query('BEGIN');
|
|
792
|
+
try {
|
|
793
|
+
const record = await options.client.query<{
|
|
794
|
+
due: boolean;
|
|
795
|
+
}>(
|
|
796
|
+
\`SELECT
|
|
797
|
+
last_activity_at + ($3::int * INTERVAL '1 day')
|
|
798
|
+
<= $2::timestamptz
|
|
799
|
+
AS due
|
|
800
|
+
FROM records
|
|
801
|
+
WHERE id = $1::uuid
|
|
802
|
+
FOR UPDATE\`,
|
|
803
|
+
[recordId, options.asOf, RETENTION_DAYS],
|
|
804
|
+
);
|
|
805
|
+
const existing = await options.client.query<{
|
|
806
|
+
eventJson: RetentionLifecycleEventEntry;
|
|
807
|
+
publishedAt: Date | null;
|
|
808
|
+
}>(
|
|
809
|
+
\`SELECT event_json AS "eventJson", published_at AS "publishedAt"
|
|
810
|
+
FROM data_lifecycle_outbox
|
|
811
|
+
WHERE idempotency_key = $1
|
|
812
|
+
FOR UPDATE\`,
|
|
813
|
+
[idempotencyKey],
|
|
814
|
+
);
|
|
815
|
+
const copy = await options.client.query<{ status: string }>(
|
|
816
|
+
\`SELECT status
|
|
817
|
+
FROM data_lifecycle_downstream_copies
|
|
818
|
+
WHERE source_slice = $1
|
|
819
|
+
AND pseudonymous_key = $2
|
|
820
|
+
AND copy_id = $3
|
|
821
|
+
FOR UPDATE\`,
|
|
822
|
+
['${name}', candidate.recordRef, OUTBOX_COPY_ID],
|
|
823
|
+
);
|
|
824
|
+
publishedAt = existing.rows[0]?.publishedAt ?? null;
|
|
825
|
+
copyStatus = copy.rows[0]?.status;
|
|
826
|
+
if (record.rows[0]) {
|
|
827
|
+
if (!record.rows[0].due) {
|
|
828
|
+
throw new Error(
|
|
829
|
+
'Retention candidate changed after its approved scan.',
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
if (existing.rowCount !== 0 || copy.rowCount !== 0) {
|
|
833
|
+
throw new Error(
|
|
834
|
+
'Retention propagation state exists for an undeleted candidate.',
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
const holdRows = await options.client.query<{
|
|
838
|
+
holdJson: DataLifecycleRuntimeHoldV1;
|
|
839
|
+
}>(
|
|
840
|
+
\`SELECT hold_json AS "holdJson"
|
|
841
|
+
FROM data_lifecycle_holds
|
|
842
|
+
WHERE category_id = 'product-record'
|
|
843
|
+
AND scope_ref = $1
|
|
844
|
+
AND status = 'active'
|
|
845
|
+
ORDER BY hold_id
|
|
846
|
+
FOR SHARE\`,
|
|
847
|
+
[candidate.recordRef],
|
|
848
|
+
);
|
|
849
|
+
const hold = evaluateDataLifecycleHolds({
|
|
850
|
+
categoryId: candidate.categoryId,
|
|
851
|
+
holds: [
|
|
852
|
+
...${pascalName}DataLifecycleRuntimePolicy.declaredHolds,
|
|
853
|
+
...holdRows.rows.map(({ holdJson }) => holdJson),
|
|
854
|
+
],
|
|
855
|
+
now: options.completedAt,
|
|
856
|
+
operation: 'deletion',
|
|
857
|
+
scopeRef: candidate.recordRef,
|
|
858
|
+
});
|
|
859
|
+
if (hold.blocked) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
'Retention candidate is protected by an active hold.',
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
if (existing.rowCount !== 1 || copy.rowCount !== 1) {
|
|
866
|
+
throw new Error(
|
|
867
|
+
'Retention candidate disappeared without durable propagation state.',
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
event = existing.rows[0].eventJson;
|
|
871
|
+
}
|
|
872
|
+
const recorded = await options.client.query(
|
|
873
|
+
\`INSERT INTO data_lifecycle_ledger
|
|
874
|
+
(entry_id, action, category_id, effective_at, expires_at,
|
|
875
|
+
idempotency_key, policy_version, pseudonymous_key)
|
|
876
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
877
|
+
ON CONFLICT (idempotency_key) DO NOTHING
|
|
878
|
+
RETURNING entry_id\`,
|
|
879
|
+
[
|
|
880
|
+
ledger.entryId,
|
|
881
|
+
ledger.action,
|
|
882
|
+
ledger.categoryId,
|
|
883
|
+
ledger.effectiveAt,
|
|
884
|
+
ledger.expiresAt,
|
|
885
|
+
idempotencyKey,
|
|
886
|
+
ledger.policyVersion,
|
|
887
|
+
ledger.pseudonymousKey,
|
|
888
|
+
],
|
|
889
|
+
);
|
|
890
|
+
if (record.rows[0]) {
|
|
891
|
+
if (recorded.rowCount !== 1) {
|
|
892
|
+
throw new Error(
|
|
893
|
+
'Retention ledger already exists for an undeleted candidate.',
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
} else if (recorded.rowCount !== 0) {
|
|
897
|
+
throw new Error(
|
|
898
|
+
'Retention candidate disappeared without its durable ledger entry.',
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
if (record.rows[0]) {
|
|
902
|
+
const deleted = await options.client.query(
|
|
903
|
+
\`DELETE FROM records
|
|
904
|
+
WHERE id = $1::uuid
|
|
905
|
+
AND last_activity_at + ($3::int * INTERVAL '1 day')
|
|
906
|
+
<= $2::timestamptz
|
|
907
|
+
RETURNING id\`,
|
|
908
|
+
[recordId, options.asOf, RETENTION_DAYS],
|
|
909
|
+
);
|
|
910
|
+
if (deleted.rowCount !== 1) {
|
|
911
|
+
throw new Error(
|
|
912
|
+
'Retention candidate changed after its approved scan.',
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
await options.client.query(
|
|
916
|
+
\`INSERT INTO data_lifecycle_outbox
|
|
917
|
+
(idempotency_key, event_json, expires_at)
|
|
918
|
+
VALUES ($1, $2::jsonb, $3)\`,
|
|
919
|
+
[idempotencyKey, JSON.stringify(event), ledgerExpiry(options.completedAt)],
|
|
920
|
+
);
|
|
921
|
+
await options.client.query(
|
|
922
|
+
\`INSERT INTO data_lifecycle_downstream_copies
|
|
923
|
+
(source_slice, pseudonymous_key, copy_id, recipient, purpose,
|
|
924
|
+
expires_at, action, hold_refs, status, updated_at)
|
|
925
|
+
VALUES ($1, $2, $3, $4, 'lifecycle-reconciliation', $5,
|
|
926
|
+
'deletion', '[]'::jsonb, 'propagation-pending', $6)\`,
|
|
927
|
+
[
|
|
928
|
+
'${name}',
|
|
929
|
+
candidate.recordRef,
|
|
930
|
+
OUTBOX_COPY_ID,
|
|
931
|
+
'foundation-event-bus',
|
|
932
|
+
ledgerExpiry(options.completedAt),
|
|
933
|
+
options.completedAt,
|
|
934
|
+
],
|
|
935
|
+
);
|
|
936
|
+
deletedNow = true;
|
|
937
|
+
copyStatus = 'propagation-pending';
|
|
938
|
+
}
|
|
939
|
+
await options.client.query('COMMIT');
|
|
940
|
+
} catch (error) {
|
|
941
|
+
await options.client.query('ROLLBACK').catch(() => undefined);
|
|
942
|
+
throw error;
|
|
943
|
+
}
|
|
944
|
+
if (publishedAt && copyStatus === 'reconciled') {
|
|
945
|
+
return 'already-applied';
|
|
946
|
+
}
|
|
947
|
+
if (publishedAt || copyStatus !== 'propagation-pending') {
|
|
948
|
+
throw new Error('Retention propagation state is inconsistent.');
|
|
949
|
+
}
|
|
950
|
+
try {
|
|
951
|
+
await options.publishLifecycleEvent(event);
|
|
952
|
+
} catch (error) {
|
|
953
|
+
await options.client
|
|
954
|
+
.query(
|
|
955
|
+
\`UPDATE data_lifecycle_outbox
|
|
956
|
+
SET attempts = attempts + 1,
|
|
957
|
+
last_error_code = 'event-publication-failed'
|
|
958
|
+
WHERE idempotency_key = $1\`,
|
|
959
|
+
[idempotencyKey],
|
|
960
|
+
)
|
|
961
|
+
.catch(() => undefined);
|
|
962
|
+
throw error;
|
|
963
|
+
}
|
|
964
|
+
await options.client.query('BEGIN');
|
|
965
|
+
try {
|
|
966
|
+
const published = await options.client.query(
|
|
967
|
+
\`UPDATE data_lifecycle_outbox
|
|
968
|
+
SET attempts = attempts + 1,
|
|
969
|
+
last_error_code = NULL,
|
|
970
|
+
published_at = $2
|
|
971
|
+
WHERE idempotency_key = $1
|
|
972
|
+
AND published_at IS NULL\`,
|
|
973
|
+
[idempotencyKey, options.completedAt],
|
|
974
|
+
);
|
|
975
|
+
const reconciled = await options.client.query(
|
|
976
|
+
\`UPDATE data_lifecycle_downstream_copies
|
|
977
|
+
SET status = 'reconciled', updated_at = $4
|
|
978
|
+
WHERE source_slice = $1
|
|
979
|
+
AND pseudonymous_key = $2
|
|
980
|
+
AND copy_id = $3
|
|
981
|
+
AND status = 'propagation-pending'\`,
|
|
982
|
+
[
|
|
983
|
+
'${name}',
|
|
984
|
+
candidate.recordRef,
|
|
985
|
+
OUTBOX_COPY_ID,
|
|
986
|
+
options.completedAt,
|
|
987
|
+
],
|
|
988
|
+
);
|
|
989
|
+
if (published.rowCount !== 1 || reconciled.rowCount !== 1) {
|
|
990
|
+
throw new Error(
|
|
991
|
+
'Retention propagation acknowledgement lost compare-and-set.',
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
await options.client.query('COMMIT');
|
|
995
|
+
} catch (error) {
|
|
996
|
+
await options.client.query('ROLLBACK').catch(() => undefined);
|
|
997
|
+
throw error;
|
|
998
|
+
}
|
|
999
|
+
return deletedNow ? 'applied' : 'already-applied';
|
|
1000
|
+
},
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
`,
|
|
1004
|
+
'jobs/data-retention/src/maintenance.ts': `import { readFile } from 'node:fs/promises';
|
|
1005
|
+
import {
|
|
1006
|
+
createAuthorizationEvaluator,
|
|
1007
|
+
defineCoarseAdmission,
|
|
1008
|
+
type AuthorizationPrincipal,
|
|
1009
|
+
} from '@empire-builder-kit/runtime/authorization';
|
|
1010
|
+
import {
|
|
1011
|
+
createDataLifecyclePseudonymousReference,
|
|
1012
|
+
evaluateDataLifecycleHolds,
|
|
1013
|
+
type DataLifecycleRuntimeHoldV1,
|
|
1014
|
+
} from '@empire-builder-kit/runtime/data-lifecycle';
|
|
1015
|
+
import { createLogger } from '@empire-builder-kit/runtime/logging';
|
|
1016
|
+
import {
|
|
1017
|
+
bindServerSecretRuntime,
|
|
1018
|
+
createAwsSecretsManagerSource,
|
|
1019
|
+
createServerSecretResolver,
|
|
1020
|
+
} from '@empire-builder-kit/runtime/secrets';
|
|
1021
|
+
import pg from 'pg';
|
|
1022
|
+
import {
|
|
1023
|
+
${identifierPrefix}AuthorizationActions,
|
|
1024
|
+
${identifierPrefix}AuthorizationPolicy,
|
|
1025
|
+
} from '../../../authorization/policy.js';
|
|
1026
|
+
import { Secrets } from '../../../secrets/declarations.js';
|
|
1027
|
+
|
|
1028
|
+
const { Client } = pg;
|
|
1029
|
+
const POLICY_VERSION = ${identifierPrefix}AuthorizationPolicy.version;
|
|
1030
|
+
const WORKLOAD_IDENTITY = '${name}:data-retention-job';
|
|
1031
|
+
const BATCH_LIMIT = 1000;
|
|
1032
|
+
let credentialResolver:
|
|
1033
|
+
| ReturnType<typeof createServerSecretResolver>
|
|
1034
|
+
| undefined;
|
|
1035
|
+
|
|
1036
|
+
function required(name: string): string {
|
|
1037
|
+
const value = process.env[name]?.trim();
|
|
1038
|
+
if (!value) throw new Error(name + ' is required.');
|
|
1039
|
+
return value;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function databaseCredentialResolver() {
|
|
1043
|
+
credentialResolver ??= createServerSecretResolver({
|
|
1044
|
+
binding: bindServerSecretRuntime({
|
|
1045
|
+
stage: required('EBK_STAGE'),
|
|
1046
|
+
workload: WORKLOAD_IDENTITY,
|
|
1047
|
+
}),
|
|
1048
|
+
cache: { freshTtlMs: 0, staleTtlMs: 0 },
|
|
1049
|
+
source: createAwsSecretsManagerSource(),
|
|
1050
|
+
});
|
|
1051
|
+
return credentialResolver;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async function readCredential(secretArn: string, correlationId: string) {
|
|
1055
|
+
const declaration = Secrets.${pascalName}PrimaryDatabaseCredential;
|
|
1056
|
+
const credential = await databaseCredentialResolver().get(
|
|
1057
|
+
declaration,
|
|
1058
|
+
{
|
|
1059
|
+
logicalName: declaration.logicalName,
|
|
1060
|
+
provider: 'aws-secrets-manager',
|
|
1061
|
+
secretArn,
|
|
1062
|
+
},
|
|
1063
|
+
{ correlationId },
|
|
1064
|
+
);
|
|
1065
|
+
if (
|
|
1066
|
+
credential.database !== required('EBK_DATABASE_NAME') ||
|
|
1067
|
+
credential.host !== required('EBK_DATABASE_HOST') ||
|
|
1068
|
+
credential.port !== Number(required('EBK_DATABASE_PORT')) ||
|
|
1069
|
+
credential.username !== required('EBK_DATABASE_USERNAME') ||
|
|
1070
|
+
!credential.password
|
|
1071
|
+
) {
|
|
1072
|
+
throw new Error(
|
|
1073
|
+
'Database credential does not match the exact maintenance binding.',
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
return credential;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export async function authorizeRetentionMaintenance(
|
|
1080
|
+
now: string,
|
|
1081
|
+
): Promise<AuthorizationPrincipal> {
|
|
1082
|
+
if (required('EBK_WORKLOAD_IDENTITY') !== WORKLOAD_IDENTITY) {
|
|
1083
|
+
throw new Error('Retention maintenance workload identity is invalid.');
|
|
1084
|
+
}
|
|
1085
|
+
const principal: AuthorizationPrincipal = {
|
|
1086
|
+
actor: { id: WORKLOAD_IDENTITY, type: 'service' },
|
|
1087
|
+
audience: WORKLOAD_IDENTITY,
|
|
1088
|
+
authority: {
|
|
1089
|
+
machineIdentityId: WORKLOAD_IDENTITY,
|
|
1090
|
+
mode: 'machine',
|
|
1091
|
+
},
|
|
1092
|
+
kind: 'agent',
|
|
1093
|
+
roles: [],
|
|
1094
|
+
scopes: [],
|
|
1095
|
+
subject: { id: WORKLOAD_IDENTITY, type: 'service' },
|
|
1096
|
+
};
|
|
1097
|
+
const evaluator = createAuthorizationEvaluator({
|
|
1098
|
+
admission: defineCoarseAdmission({
|
|
1099
|
+
anyOf: [{ kinds: ['agent'], subjectTypes: ['service'] }],
|
|
1100
|
+
}),
|
|
1101
|
+
policy: ${identifierPrefix}AuthorizationPolicy,
|
|
1102
|
+
requiredPolicyVersion: POLICY_VERSION,
|
|
1103
|
+
});
|
|
1104
|
+
await evaluator.authorize({
|
|
1105
|
+
action: ${identifierPrefix}AuthorizationActions.retentionMaintenance,
|
|
1106
|
+
context: {
|
|
1107
|
+
facts: { operationId: 'bounded-control-maintenance' },
|
|
1108
|
+
now,
|
|
1109
|
+
stage: required('EBK_STAGE'),
|
|
1110
|
+
},
|
|
1111
|
+
principal,
|
|
1112
|
+
resource: { attributes: { collection: true }, type: 'slice' },
|
|
1113
|
+
});
|
|
1114
|
+
return principal;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function assertNoActiveControlHold(
|
|
1118
|
+
holds: readonly DataLifecycleRuntimeHoldV1[],
|
|
1119
|
+
now: string,
|
|
1120
|
+
) {
|
|
1121
|
+
for (const hold of holds) {
|
|
1122
|
+
const evaluated = evaluateDataLifecycleHolds({
|
|
1123
|
+
categoryId: 'lifecycle-control-metadata',
|
|
1124
|
+
holds: [hold],
|
|
1125
|
+
now,
|
|
1126
|
+
operation: 'deletion',
|
|
1127
|
+
scopeRef: hold.scopeRef,
|
|
1128
|
+
});
|
|
1129
|
+
if (evaluated.blocked) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
'Retention maintenance is blocked by an active control-metadata hold.',
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const reconciliationStatements = [
|
|
1138
|
+
\`WITH candidates AS (
|
|
1139
|
+
SELECT approval.ctid
|
|
1140
|
+
FROM approval_requests approval
|
|
1141
|
+
WHERE approval.expires_at <= $1::timestamptz
|
|
1142
|
+
AND approval.status IN ('pending', 'approved')
|
|
1143
|
+
AND (
|
|
1144
|
+
EXISTS (
|
|
1145
|
+
SELECT 1 FROM data_lifecycle_requests request
|
|
1146
|
+
WHERE request.approval_request_id = approval.id
|
|
1147
|
+
AND request.status IN ('requested', 'verified', 'held')
|
|
1148
|
+
)
|
|
1149
|
+
OR EXISTS (
|
|
1150
|
+
SELECT 1 FROM data_lifecycle_retention_runs run
|
|
1151
|
+
WHERE run.approval_request_id = approval.id
|
|
1152
|
+
AND run.status IN (
|
|
1153
|
+
'approval-pending',
|
|
1154
|
+
'approval-approved'
|
|
1155
|
+
)
|
|
1156
|
+
)
|
|
1157
|
+
)
|
|
1158
|
+
ORDER BY approval.expires_at, approval.id
|
|
1159
|
+
LIMIT $2
|
|
1160
|
+
FOR UPDATE OF approval SKIP LOCKED
|
|
1161
|
+
)
|
|
1162
|
+
UPDATE approval_requests approval
|
|
1163
|
+
SET request_json = jsonb_set(
|
|
1164
|
+
jsonb_set(
|
|
1165
|
+
approval.request_json,
|
|
1166
|
+
'{status}',
|
|
1167
|
+
to_jsonb('expired'::text),
|
|
1168
|
+
true
|
|
1169
|
+
),
|
|
1170
|
+
'{revision}',
|
|
1171
|
+
to_jsonb(approval.revision + 1),
|
|
1172
|
+
true
|
|
1173
|
+
),
|
|
1174
|
+
revision = approval.revision + 1,
|
|
1175
|
+
status = 'expired'
|
|
1176
|
+
WHERE approval.ctid IN (SELECT ctid FROM candidates)\`,
|
|
1177
|
+
\`WITH candidates AS (
|
|
1178
|
+
SELECT request.ctid,
|
|
1179
|
+
CASE approval.status
|
|
1180
|
+
WHEN 'expired' THEN 'approval-expired'
|
|
1181
|
+
ELSE 'approval-rejected'
|
|
1182
|
+
END::text AS outcome_code
|
|
1183
|
+
FROM data_lifecycle_requests request
|
|
1184
|
+
JOIN approval_requests approval
|
|
1185
|
+
ON approval.id = request.approval_request_id
|
|
1186
|
+
WHERE approval.status IN ('expired', 'rejected')
|
|
1187
|
+
AND request.status IN ('requested', 'verified', 'held')
|
|
1188
|
+
AND request.request_json->>'kind' IN (
|
|
1189
|
+
'data-deletion',
|
|
1190
|
+
'data-export'
|
|
1191
|
+
)
|
|
1192
|
+
ORDER BY request.updated_at, request.id
|
|
1193
|
+
LIMIT $2
|
|
1194
|
+
FOR UPDATE OF request SKIP LOCKED
|
|
1195
|
+
)
|
|
1196
|
+
UPDATE data_lifecycle_requests request
|
|
1197
|
+
SET request_json = jsonb_set(
|
|
1198
|
+
jsonb_set(
|
|
1199
|
+
jsonb_set(
|
|
1200
|
+
request.request_json,
|
|
1201
|
+
'{status}',
|
|
1202
|
+
to_jsonb('rejected'::text),
|
|
1203
|
+
true
|
|
1204
|
+
),
|
|
1205
|
+
'{outcomeCode}',
|
|
1206
|
+
to_jsonb(candidates.outcome_code),
|
|
1207
|
+
true
|
|
1208
|
+
),
|
|
1209
|
+
'{updatedAt}',
|
|
1210
|
+
to_jsonb($1::text),
|
|
1211
|
+
true
|
|
1212
|
+
),
|
|
1213
|
+
revision = request.revision + 1,
|
|
1214
|
+
status = 'rejected',
|
|
1215
|
+
updated_at = $1::timestamptz
|
|
1216
|
+
FROM candidates
|
|
1217
|
+
WHERE request.ctid = candidates.ctid\`,
|
|
1218
|
+
\`WITH candidates AS (
|
|
1219
|
+
SELECT artifact.ctid
|
|
1220
|
+
FROM data_lifecycle_export_artifacts artifact
|
|
1221
|
+
JOIN data_lifecycle_requests request
|
|
1222
|
+
ON request.approval_request_id = artifact.approval_request_id
|
|
1223
|
+
WHERE request.status = 'rejected'
|
|
1224
|
+
AND request.request_json->>'kind' = 'data-export'
|
|
1225
|
+
AND artifact.cleanup_state = 'requested'
|
|
1226
|
+
AND artifact.upload_started_at IS NULL
|
|
1227
|
+
AND artifact.current_version_id IS NULL
|
|
1228
|
+
AND NOT EXISTS (
|
|
1229
|
+
SELECT 1 FROM data_lifecycle_export_versions version
|
|
1230
|
+
WHERE version.approval_request_id =
|
|
1231
|
+
artifact.approval_request_id
|
|
1232
|
+
)
|
|
1233
|
+
ORDER BY artifact.updated_at, artifact.approval_request_id
|
|
1234
|
+
LIMIT $2
|
|
1235
|
+
FOR UPDATE OF artifact SKIP LOCKED
|
|
1236
|
+
)
|
|
1237
|
+
UPDATE data_lifecycle_export_artifacts artifact
|
|
1238
|
+
SET cleanup_state = 'completed',
|
|
1239
|
+
last_error_code = NULL,
|
|
1240
|
+
updated_at = $1::timestamptz
|
|
1241
|
+
WHERE artifact.ctid IN (SELECT ctid FROM candidates)\`,
|
|
1242
|
+
\`WITH candidates AS (
|
|
1243
|
+
SELECT run.ctid
|
|
1244
|
+
FROM data_lifecycle_retention_runs run
|
|
1245
|
+
WHERE run.expires_at <= $1::timestamptz
|
|
1246
|
+
AND run.status = 'reported'
|
|
1247
|
+
ORDER BY run.expires_at, run.id
|
|
1248
|
+
LIMIT $2
|
|
1249
|
+
FOR UPDATE OF run SKIP LOCKED
|
|
1250
|
+
)
|
|
1251
|
+
UPDATE data_lifecycle_retention_runs run
|
|
1252
|
+
SET revision = run.revision + 1,
|
|
1253
|
+
status = 'expired',
|
|
1254
|
+
updated_at = $1::timestamptz
|
|
1255
|
+
WHERE run.ctid IN (SELECT ctid FROM candidates)\`,
|
|
1256
|
+
\`WITH candidates AS (
|
|
1257
|
+
SELECT run.ctid,
|
|
1258
|
+
CASE approval.status
|
|
1259
|
+
WHEN 'expired' THEN 'approval-expired'
|
|
1260
|
+
ELSE 'approval-rejected'
|
|
1261
|
+
END::text AS terminal_status
|
|
1262
|
+
FROM data_lifecycle_retention_runs run
|
|
1263
|
+
JOIN approval_requests approval
|
|
1264
|
+
ON approval.id = run.approval_request_id
|
|
1265
|
+
WHERE run.status IN ('approval-pending', 'approval-approved')
|
|
1266
|
+
AND approval.status IN ('expired', 'rejected')
|
|
1267
|
+
ORDER BY run.updated_at, run.id
|
|
1268
|
+
LIMIT $2
|
|
1269
|
+
FOR UPDATE OF run SKIP LOCKED
|
|
1270
|
+
)
|
|
1271
|
+
UPDATE data_lifecycle_retention_runs run
|
|
1272
|
+
SET revision = run.revision + 1,
|
|
1273
|
+
status = candidates.terminal_status,
|
|
1274
|
+
updated_at = $1::timestamptz
|
|
1275
|
+
FROM candidates
|
|
1276
|
+
WHERE run.ctid = candidates.ctid\`,
|
|
1277
|
+
] as const;
|
|
1278
|
+
|
|
1279
|
+
const maintenanceStatements = [
|
|
1280
|
+
\`WITH expired AS (
|
|
1281
|
+
SELECT ctid FROM data_lifecycle_outbox
|
|
1282
|
+
WHERE expires_at <= $1::timestamptz
|
|
1283
|
+
AND published_at IS NOT NULL
|
|
1284
|
+
ORDER BY expires_at, idempotency_key
|
|
1285
|
+
LIMIT $2
|
|
1286
|
+
FOR UPDATE SKIP LOCKED
|
|
1287
|
+
)
|
|
1288
|
+
DELETE FROM data_lifecycle_outbox
|
|
1289
|
+
WHERE ctid IN (SELECT ctid FROM expired)\`,
|
|
1290
|
+
\`WITH expired AS (
|
|
1291
|
+
SELECT ctid FROM data_lifecycle_ledger
|
|
1292
|
+
WHERE expires_at <= $1::timestamptz
|
|
1293
|
+
ORDER BY expires_at, entry_id
|
|
1294
|
+
LIMIT $2
|
|
1295
|
+
FOR UPDATE SKIP LOCKED
|
|
1296
|
+
)
|
|
1297
|
+
DELETE FROM data_lifecycle_ledger
|
|
1298
|
+
WHERE ctid IN (SELECT ctid FROM expired)\`,
|
|
1299
|
+
\`SELECT ebk_prune_data_lifecycle_audit(
|
|
1300
|
+
$1::timestamptz,
|
|
1301
|
+
$2::integer
|
|
1302
|
+
)\`,
|
|
1303
|
+
\`WITH expired AS (
|
|
1304
|
+
SELECT ctid FROM data_lifecycle_export_artifacts
|
|
1305
|
+
WHERE cleanup_state = 'completed'
|
|
1306
|
+
AND updated_at <= $1::timestamptz - INTERVAL '400 days'
|
|
1307
|
+
ORDER BY updated_at, approval_request_id
|
|
1308
|
+
LIMIT $2
|
|
1309
|
+
FOR UPDATE SKIP LOCKED
|
|
1310
|
+
)
|
|
1311
|
+
DELETE FROM data_lifecycle_export_artifacts
|
|
1312
|
+
WHERE ctid IN (SELECT ctid FROM expired)\`,
|
|
1313
|
+
\`WITH expired AS (
|
|
1314
|
+
SELECT ctid FROM data_lifecycle_receipts
|
|
1315
|
+
WHERE received_at <= $1::timestamptz - INTERVAL '400 days'
|
|
1316
|
+
ORDER BY received_at, source_slice, pseudonymous_key
|
|
1317
|
+
LIMIT $2
|
|
1318
|
+
FOR UPDATE SKIP LOCKED
|
|
1319
|
+
)
|
|
1320
|
+
DELETE FROM data_lifecycle_receipts
|
|
1321
|
+
WHERE ctid IN (SELECT ctid FROM expired)\`,
|
|
1322
|
+
\`WITH expired AS (
|
|
1323
|
+
SELECT ctid FROM data_lifecycle_downstream_copies
|
|
1324
|
+
WHERE expires_at <= $1::timestamptz
|
|
1325
|
+
AND status IN ('deleted', 'expired', 'reconciled')
|
|
1326
|
+
ORDER BY expires_at, source_slice, pseudonymous_key, copy_id
|
|
1327
|
+
LIMIT $2
|
|
1328
|
+
FOR UPDATE SKIP LOCKED
|
|
1329
|
+
)
|
|
1330
|
+
DELETE FROM data_lifecycle_downstream_copies
|
|
1331
|
+
WHERE ctid IN (SELECT ctid FROM expired)\`,
|
|
1332
|
+
\`SELECT ebk_prune_authorization_audit(
|
|
1333
|
+
$1::timestamptz,
|
|
1334
|
+
$2::integer
|
|
1335
|
+
)\`,
|
|
1336
|
+
\`WITH expired AS (
|
|
1337
|
+
SELECT ctid FROM delegation_grants
|
|
1338
|
+
WHERE expires_at <= $1::timestamptz
|
|
1339
|
+
AND created_at <= $1::timestamptz - INTERVAL '400 days'
|
|
1340
|
+
ORDER BY expires_at, id
|
|
1341
|
+
LIMIT $2
|
|
1342
|
+
FOR UPDATE SKIP LOCKED
|
|
1343
|
+
)
|
|
1344
|
+
DELETE FROM delegation_grants
|
|
1345
|
+
WHERE ctid IN (SELECT ctid FROM expired)\`,
|
|
1346
|
+
\`WITH expired AS (
|
|
1347
|
+
SELECT ctid FROM data_lifecycle_holds
|
|
1348
|
+
WHERE status = 'released'
|
|
1349
|
+
AND expires_at <= $1::timestamptz - INTERVAL '400 days'
|
|
1350
|
+
ORDER BY expires_at, hold_id
|
|
1351
|
+
LIMIT $2
|
|
1352
|
+
FOR UPDATE SKIP LOCKED
|
|
1353
|
+
)
|
|
1354
|
+
DELETE FROM data_lifecycle_holds
|
|
1355
|
+
WHERE ctid IN (SELECT ctid FROM expired)\`,
|
|
1356
|
+
\`WITH expired AS (
|
|
1357
|
+
SELECT ctid FROM data_lifecycle_requests
|
|
1358
|
+
WHERE updated_at <= $1::timestamptz - INTERVAL '400 days'
|
|
1359
|
+
AND status IN ('completed', 'failed', 'rejected')
|
|
1360
|
+
ORDER BY updated_at, id
|
|
1361
|
+
LIMIT $2
|
|
1362
|
+
FOR UPDATE SKIP LOCKED
|
|
1363
|
+
)
|
|
1364
|
+
DELETE FROM data_lifecycle_requests
|
|
1365
|
+
WHERE ctid IN (SELECT ctid FROM expired)\`,
|
|
1366
|
+
\`WITH expired AS (
|
|
1367
|
+
SELECT candidate.ctid
|
|
1368
|
+
FROM data_lifecycle_retention_candidates candidate
|
|
1369
|
+
JOIN data_lifecycle_retention_runs run ON run.id = candidate.run_id
|
|
1370
|
+
WHERE run.expires_at <= $1::timestamptz
|
|
1371
|
+
AND run.status IN (
|
|
1372
|
+
'completed',
|
|
1373
|
+
'approval-expired',
|
|
1374
|
+
'approval-rejected',
|
|
1375
|
+
'expired'
|
|
1376
|
+
)
|
|
1377
|
+
ORDER BY run.expires_at, candidate.run_id, candidate.candidate_order
|
|
1378
|
+
LIMIT $2
|
|
1379
|
+
FOR UPDATE OF candidate SKIP LOCKED
|
|
1380
|
+
)
|
|
1381
|
+
DELETE FROM data_lifecycle_retention_candidates
|
|
1382
|
+
WHERE ctid IN (SELECT ctid FROM expired)\`,
|
|
1383
|
+
\`WITH expired AS (
|
|
1384
|
+
SELECT ctid FROM data_lifecycle_retention_runs
|
|
1385
|
+
WHERE expires_at <= $1::timestamptz
|
|
1386
|
+
AND status IN (
|
|
1387
|
+
'completed',
|
|
1388
|
+
'approval-expired',
|
|
1389
|
+
'approval-rejected',
|
|
1390
|
+
'expired'
|
|
1391
|
+
)
|
|
1392
|
+
AND NOT EXISTS (
|
|
1393
|
+
SELECT 1 FROM data_lifecycle_retention_candidates
|
|
1394
|
+
WHERE run_id = data_lifecycle_retention_runs.id
|
|
1395
|
+
)
|
|
1396
|
+
ORDER BY expires_at, id
|
|
1397
|
+
LIMIT $2
|
|
1398
|
+
FOR UPDATE SKIP LOCKED
|
|
1399
|
+
)
|
|
1400
|
+
DELETE FROM data_lifecycle_retention_runs
|
|
1401
|
+
WHERE ctid IN (SELECT ctid FROM expired)\`,
|
|
1402
|
+
\`WITH expired AS (
|
|
1403
|
+
SELECT ctid FROM approval_requests
|
|
1404
|
+
WHERE expires_at <= $1::timestamptz - INTERVAL '400 days'
|
|
1405
|
+
AND NOT EXISTS (
|
|
1406
|
+
SELECT 1 FROM data_lifecycle_requests
|
|
1407
|
+
WHERE approval_request_id = approval_requests.id
|
|
1408
|
+
)
|
|
1409
|
+
AND NOT EXISTS (
|
|
1410
|
+
SELECT 1 FROM data_lifecycle_retention_runs
|
|
1411
|
+
WHERE approval_request_id = approval_requests.id
|
|
1412
|
+
)
|
|
1413
|
+
ORDER BY expires_at, id
|
|
1414
|
+
LIMIT $2
|
|
1415
|
+
FOR UPDATE SKIP LOCKED
|
|
1416
|
+
)
|
|
1417
|
+
DELETE FROM approval_requests
|
|
1418
|
+
WHERE ctid IN (SELECT ctid FROM expired)\`,
|
|
1419
|
+
] as const;
|
|
1420
|
+
|
|
1421
|
+
export async function runRetentionMaintenance() {
|
|
1422
|
+
const now = new Date().toISOString();
|
|
1423
|
+
const correlationId = required('EBK_MAINTENANCE_CORRELATION_ID');
|
|
1424
|
+
const principal = await authorizeRetentionMaintenance(now);
|
|
1425
|
+
const logger = createLogger({
|
|
1426
|
+
component: 'job-data-retention-maintenance',
|
|
1427
|
+
feature: 'data-lifecycle',
|
|
1428
|
+
operation: 'retention-maintenance',
|
|
1429
|
+
slice: '${name}',
|
|
1430
|
+
stage: required('EBK_STAGE'),
|
|
1431
|
+
workspace: required('EBK_WORKSPACE'),
|
|
1432
|
+
});
|
|
1433
|
+
const credential = await readCredential(
|
|
1434
|
+
required('EBK_READWRITE_SECRET_ARN'),
|
|
1435
|
+
correlationId,
|
|
1436
|
+
);
|
|
1437
|
+
const ca = await readFile(required('EBK_RDS_CA_BUNDLE'), 'utf8');
|
|
1438
|
+
const client = new Client({
|
|
1439
|
+
...credential,
|
|
1440
|
+
connectionTimeoutMillis: 10_000,
|
|
1441
|
+
ssl: { ca, rejectUnauthorized: true },
|
|
1442
|
+
});
|
|
1443
|
+
try {
|
|
1444
|
+
await client.connect();
|
|
1445
|
+
await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
|
|
1446
|
+
try {
|
|
1447
|
+
await client.query(
|
|
1448
|
+
\`INSERT INTO authorization_audit
|
|
1449
|
+
(action, approval_reference, authority_mode, correlation_id,
|
|
1450
|
+
decision, delegation_reference, policy_version, principal_kind,
|
|
1451
|
+
pseudonymous_subject_id, reason_category, resource_reference,
|
|
1452
|
+
resource_type, slice, stage, subject_type)
|
|
1453
|
+
VALUES ($1, NULL, 'machine', $2, 'allow', NULL, $3, 'agent', $4,
|
|
1454
|
+
'policy-allow', $5, 'slice', $6, $7, 'service')\`,
|
|
1455
|
+
[
|
|
1456
|
+
${identifierPrefix}AuthorizationActions.retentionMaintenance,
|
|
1457
|
+
correlationId,
|
|
1458
|
+
POLICY_VERSION,
|
|
1459
|
+
createDataLifecyclePseudonymousReference({
|
|
1460
|
+
categoryId: 'authorization-subject',
|
|
1461
|
+
slice: '${name}',
|
|
1462
|
+
value: principal.actor.id,
|
|
1463
|
+
}),
|
|
1464
|
+
createDataLifecyclePseudonymousReference({
|
|
1465
|
+
categoryId: 'lifecycle-control-metadata',
|
|
1466
|
+
slice: '${name}',
|
|
1467
|
+
value: 'bounded-control-maintenance',
|
|
1468
|
+
}),
|
|
1469
|
+
'${name}',
|
|
1470
|
+
required('EBK_STAGE'),
|
|
1471
|
+
],
|
|
1472
|
+
);
|
|
1473
|
+
const holdRows = await client.query<{
|
|
1474
|
+
holdJson: DataLifecycleRuntimeHoldV1;
|
|
1475
|
+
}>(
|
|
1476
|
+
\`SELECT hold_json AS "holdJson"
|
|
1477
|
+
FROM data_lifecycle_holds
|
|
1478
|
+
WHERE category_id = 'lifecycle-control-metadata'
|
|
1479
|
+
AND status = 'active'
|
|
1480
|
+
ORDER BY hold_id
|
|
1481
|
+
FOR SHARE\`,
|
|
1482
|
+
);
|
|
1483
|
+
assertNoActiveControlHold(
|
|
1484
|
+
holdRows.rows.map(({ holdJson }) => holdJson),
|
|
1485
|
+
now,
|
|
1486
|
+
);
|
|
1487
|
+
let reconciled = 0;
|
|
1488
|
+
for (const statement of reconciliationStatements) {
|
|
1489
|
+
const result = await client.query(statement, [now, BATCH_LIMIT]);
|
|
1490
|
+
reconciled += result.rowCount ?? 0;
|
|
1491
|
+
}
|
|
1492
|
+
let deleted = 0;
|
|
1493
|
+
for (const statement of maintenanceStatements) {
|
|
1494
|
+
const result = await client.query(statement, [now, BATCH_LIMIT]);
|
|
1495
|
+
deleted += result.rowCount ?? 0;
|
|
1496
|
+
}
|
|
1497
|
+
await client.query('COMMIT');
|
|
1498
|
+
logger.info('Retention control maintenance completed', {
|
|
1499
|
+
attributes: {
|
|
1500
|
+
deleted,
|
|
1501
|
+
reconciled,
|
|
1502
|
+
statementCount:
|
|
1503
|
+
reconciliationStatements.length +
|
|
1504
|
+
maintenanceStatements.length,
|
|
1505
|
+
},
|
|
1506
|
+
outcome: 'success',
|
|
1507
|
+
});
|
|
1508
|
+
return {
|
|
1509
|
+
deleted,
|
|
1510
|
+
reconciled,
|
|
1511
|
+
statementCount:
|
|
1512
|
+
reconciliationStatements.length + maintenanceStatements.length,
|
|
1513
|
+
};
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
1516
|
+
throw error;
|
|
1517
|
+
}
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
logger.error('Retention control maintenance failed', {
|
|
1520
|
+
attributes: {
|
|
1521
|
+
errorCode:
|
|
1522
|
+
error instanceof Error
|
|
1523
|
+
? error.name.replace(/[^A-Za-z0-9_.-]/g, '').slice(0, 80)
|
|
1524
|
+
: 'RetentionMaintenanceError',
|
|
1525
|
+
},
|
|
1526
|
+
outcome: 'failure',
|
|
1527
|
+
});
|
|
1528
|
+
throw error;
|
|
1529
|
+
} finally {
|
|
1530
|
+
await client.end().catch(() => undefined);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
if (import.meta.url === new URL(process.argv[1] ?? '', 'file:').href) {
|
|
1535
|
+
await runRetentionMaintenance();
|
|
1536
|
+
}
|
|
1537
|
+
`,
|
|
1538
|
+
'jobs/data-retention/src/retention-contract.spec.ts': `import { readFile } from 'node:fs/promises';
|
|
1539
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
1540
|
+
import { runDataLifecycleRetention } from '@empire-builder-kit/runtime/data-lifecycle';
|
|
1541
|
+
import type { Client } from 'pg';
|
|
1542
|
+
import {
|
|
1543
|
+
${identifierPrefix}AuthorizationActions,
|
|
1544
|
+
} from '../../../authorization/policy.js';
|
|
1545
|
+
import { productRecordReference } from '../../../data/pseudonymous-reference.js';
|
|
1546
|
+
import { create${pascalName}RetentionAdapter } from './adapter.js';
|
|
1547
|
+
import { authorizeRetentionMaintenance } from './maintenance.js';
|
|
1548
|
+
import { authorizeRetentionOperation } from './run.js';
|
|
1549
|
+
|
|
1550
|
+
const recordId = '11111111-1111-4111-8111-111111111111';
|
|
1551
|
+
const runId = '22222222-2222-4222-8222-222222222222';
|
|
1552
|
+
const asOf = '2026-07-28T12:00:00.000Z';
|
|
1553
|
+
|
|
1554
|
+
interface QueryScenario {
|
|
1555
|
+
hold: boolean;
|
|
1556
|
+
queries: string[];
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function clientFor(scenario: QueryScenario): Client {
|
|
1560
|
+
return {
|
|
1561
|
+
async query(statement: string) {
|
|
1562
|
+
const normalized = statement.replace(/\\s+/g, ' ').trim();
|
|
1563
|
+
scenario.queries.push(normalized);
|
|
1564
|
+
if (
|
|
1565
|
+
normalized.includes(
|
|
1566
|
+
'FROM data_lifecycle_retention_candidates',
|
|
1567
|
+
)
|
|
1568
|
+
) {
|
|
1569
|
+
return {
|
|
1570
|
+
rowCount: 1,
|
|
1571
|
+
rows: [
|
|
1572
|
+
{
|
|
1573
|
+
cursorKey: 'candidate-001',
|
|
1574
|
+
dueAt: new Date('2026-07-27T12:00:00.000Z'),
|
|
1575
|
+
id: recordId,
|
|
1576
|
+
},
|
|
1577
|
+
],
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
if (
|
|
1581
|
+
normalized.includes('FROM records') &&
|
|
1582
|
+
normalized.includes('FOR UPDATE')
|
|
1583
|
+
) {
|
|
1584
|
+
return { rowCount: 1, rows: [{ due: true }] };
|
|
1585
|
+
}
|
|
1586
|
+
if (normalized.includes('FROM data_lifecycle_outbox')) {
|
|
1587
|
+
return { rowCount: 0, rows: [] };
|
|
1588
|
+
}
|
|
1589
|
+
if (normalized.includes('FROM data_lifecycle_downstream_copies')) {
|
|
1590
|
+
return { rowCount: 0, rows: [] };
|
|
1591
|
+
}
|
|
1592
|
+
if (normalized.includes('FROM data_lifecycle_holds')) {
|
|
1593
|
+
return {
|
|
1594
|
+
rowCount: scenario.hold ? 1 : 0,
|
|
1595
|
+
rows: scenario.hold
|
|
1596
|
+
? [
|
|
1597
|
+
{
|
|
1598
|
+
holdJson: {
|
|
1599
|
+
categoryId: 'product-record',
|
|
1600
|
+
effects: ['block-deletion'],
|
|
1601
|
+
expiresAt: '2099-07-28T12:00:00.000Z',
|
|
1602
|
+
holdId: 'retention-hold',
|
|
1603
|
+
issuerCode: 'legal',
|
|
1604
|
+
purpose: 'legal',
|
|
1605
|
+
reviewAt: '2098-07-28T12:00:00.000Z',
|
|
1606
|
+
scopeRef: productRecordReference(recordId),
|
|
1607
|
+
startsAt: '2026-07-27T12:00:00.000Z',
|
|
1608
|
+
status: 'active',
|
|
1609
|
+
},
|
|
1610
|
+
},
|
|
1611
|
+
]
|
|
1612
|
+
: [],
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
return { rowCount: 1, rows: [] };
|
|
1616
|
+
},
|
|
1617
|
+
} as unknown as Client;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function adapterFor(
|
|
1621
|
+
scenario: QueryScenario,
|
|
1622
|
+
publishLifecycleEvent: () => Promise<void>,
|
|
1623
|
+
) {
|
|
1624
|
+
return create${pascalName}RetentionAdapter({
|
|
1625
|
+
approvalRequestId: '33333333-3333-4333-8333-333333333333',
|
|
1626
|
+
asOf,
|
|
1627
|
+
client: clientFor(scenario),
|
|
1628
|
+
completedAt: asOf,
|
|
1629
|
+
correlationId: '44444444-4444-4444-8444-444444444444',
|
|
1630
|
+
eventBusName: 'sample-product-development',
|
|
1631
|
+
mode: 'apply',
|
|
1632
|
+
publishLifecycleEvent,
|
|
1633
|
+
runId,
|
|
1634
|
+
stage: 'development',
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
afterEach(() => {
|
|
1639
|
+
vi.unstubAllEnvs();
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
describe('generated data-retention Job contract', () => {
|
|
1643
|
+
it('keeps bounded maintenance outside the approved candidate command', async () => {
|
|
1644
|
+
const runSource = await readFile(new URL('./run.ts', import.meta.url), 'utf8');
|
|
1645
|
+
const maintenanceSource = await readFile(
|
|
1646
|
+
new URL('./maintenance.ts', import.meta.url),
|
|
1647
|
+
'utf8',
|
|
1648
|
+
);
|
|
1649
|
+
|
|
1650
|
+
expect(runSource).not.toContain('cleanupControlRows');
|
|
1651
|
+
expect(runSource).not.toContain('retentionMaintenance');
|
|
1652
|
+
expect(maintenanceSource).not.toContain('beginApprovalExecution');
|
|
1653
|
+
expect(maintenanceSource).toContain(
|
|
1654
|
+
'${identifierPrefix}AuthorizationActions.retentionMaintenance',
|
|
1655
|
+
);
|
|
1656
|
+
expect(maintenanceSource).toContain(
|
|
1657
|
+
'BEGIN ISOLATION LEVEL SERIALIZABLE',
|
|
1658
|
+
);
|
|
1659
|
+
expect(maintenanceSource).toContain('LIMIT $2');
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
it('requires exact report/apply/maintenance actions and workload identity', async () => {
|
|
1663
|
+
vi.stubEnv('EBK_STAGE', 'development');
|
|
1664
|
+
vi.stubEnv('EBK_WORKLOAD_IDENTITY', '${name}:data-retention-job');
|
|
1665
|
+
await expect(
|
|
1666
|
+
authorizeRetentionOperation(
|
|
1667
|
+
${identifierPrefix}AuthorizationActions.retentionReport,
|
|
1668
|
+
asOf,
|
|
1669
|
+
runId,
|
|
1670
|
+
),
|
|
1671
|
+
).resolves.toMatchObject({
|
|
1672
|
+
actor: { id: '${name}:data-retention-job', type: 'service' },
|
|
1673
|
+
});
|
|
1674
|
+
await expect(
|
|
1675
|
+
authorizeRetentionOperation(
|
|
1676
|
+
${identifierPrefix}AuthorizationActions.retentionApply,
|
|
1677
|
+
asOf,
|
|
1678
|
+
runId,
|
|
1679
|
+
),
|
|
1680
|
+
).resolves.toMatchObject({
|
|
1681
|
+
actor: { id: '${name}:data-retention-job', type: 'service' },
|
|
1682
|
+
});
|
|
1683
|
+
await expect(authorizeRetentionMaintenance(asOf)).resolves.toMatchObject({
|
|
1684
|
+
actor: { id: '${name}:data-retention-job', type: 'service' },
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
vi.stubEnv('EBK_WORKLOAD_IDENTITY', '${name}:event-consumer');
|
|
1688
|
+
await expect(
|
|
1689
|
+
authorizeRetentionOperation(
|
|
1690
|
+
${identifierPrefix}AuthorizationActions.retentionReport,
|
|
1691
|
+
asOf,
|
|
1692
|
+
runId,
|
|
1693
|
+
),
|
|
1694
|
+
).rejects.toThrow('Retention Job workload identity is invalid.');
|
|
1695
|
+
await expect(authorizeRetentionMaintenance(asOf)).rejects.toThrow(
|
|
1696
|
+
'Retention maintenance workload identity is invalid.',
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
vi.stubEnv('EBK_WORKLOAD_IDENTITY', '');
|
|
1700
|
+
await expect(
|
|
1701
|
+
authorizeRetentionOperation(
|
|
1702
|
+
${identifierPrefix}AuthorizationActions.retentionApply,
|
|
1703
|
+
asOf,
|
|
1704
|
+
runId,
|
|
1705
|
+
),
|
|
1706
|
+
).rejects.toThrow('EBK_WORKLOAD_IDENTITY is required.');
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
it('locks the candidate before holds and leaves the checkpoint incomplete when held', async () => {
|
|
1710
|
+
const scenario: QueryScenario = { hold: true, queries: [] };
|
|
1711
|
+
const result = await runDataLifecycleRetention({
|
|
1712
|
+
adapter: adapterFor(scenario, async () => undefined),
|
|
1713
|
+
asOf,
|
|
1714
|
+
limit: 100,
|
|
1715
|
+
mode: 'apply',
|
|
1716
|
+
policyVersion: '1.0.0',
|
|
1717
|
+
});
|
|
1718
|
+
const recordLock = scenario.queries.findIndex(
|
|
1719
|
+
(statement) =>
|
|
1720
|
+
statement.includes('FROM records') &&
|
|
1721
|
+
statement.includes('FOR UPDATE'),
|
|
1722
|
+
);
|
|
1723
|
+
const holdRead = scenario.queries.findIndex((statement) =>
|
|
1724
|
+
statement.includes('FROM data_lifecycle_holds'),
|
|
1725
|
+
);
|
|
1726
|
+
|
|
1727
|
+
expect(result.status).toBe('retry-required');
|
|
1728
|
+
expect(result.checkpoint.complete).toBe(false);
|
|
1729
|
+
expect(recordLock).toBeGreaterThanOrEqual(0);
|
|
1730
|
+
expect(holdRead).toBeGreaterThan(recordLock);
|
|
1731
|
+
expect(
|
|
1732
|
+
scenario.queries.some((statement) =>
|
|
1733
|
+
statement.startsWith('DELETE FROM records'),
|
|
1734
|
+
),
|
|
1735
|
+
).toBe(false);
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
it('keeps the checkpoint incomplete until durable propagation is acknowledged', async () => {
|
|
1739
|
+
const failed: QueryScenario = { hold: false, queries: [] };
|
|
1740
|
+
const failedResult = await runDataLifecycleRetention({
|
|
1741
|
+
adapter: adapterFor(failed, async () => {
|
|
1742
|
+
throw new Error('event publication failed');
|
|
1743
|
+
}),
|
|
1744
|
+
asOf,
|
|
1745
|
+
limit: 100,
|
|
1746
|
+
mode: 'apply',
|
|
1747
|
+
policyVersion: '1.0.0',
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
expect(failedResult.status).toBe('retry-required');
|
|
1751
|
+
expect(failedResult.checkpoint.complete).toBe(false);
|
|
1752
|
+
expect(
|
|
1753
|
+
failed.queries.some((statement) =>
|
|
1754
|
+
statement.includes('INSERT INTO data_lifecycle_outbox'),
|
|
1755
|
+
),
|
|
1756
|
+
).toBe(true);
|
|
1757
|
+
expect(
|
|
1758
|
+
failed.queries.some((statement) =>
|
|
1759
|
+
statement.includes("status = 'reconciled'"),
|
|
1760
|
+
),
|
|
1761
|
+
).toBe(false);
|
|
1762
|
+
|
|
1763
|
+
const succeeded: QueryScenario = { hold: false, queries: [] };
|
|
1764
|
+
const succeededResult = await runDataLifecycleRetention({
|
|
1765
|
+
adapter: adapterFor(succeeded, async () => undefined),
|
|
1766
|
+
asOf,
|
|
1767
|
+
limit: 100,
|
|
1768
|
+
mode: 'apply',
|
|
1769
|
+
policyVersion: '1.0.0',
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
expect(succeededResult.status).toBe('complete');
|
|
1773
|
+
expect(succeededResult.checkpoint.complete).toBe(true);
|
|
1774
|
+
const published = succeeded.queries.findIndex((statement) =>
|
|
1775
|
+
statement.includes('published_at = $2'),
|
|
1776
|
+
);
|
|
1777
|
+
const reconciled = succeeded.queries.findIndex((statement) =>
|
|
1778
|
+
statement.includes("status = 'reconciled'"),
|
|
1779
|
+
);
|
|
1780
|
+
expect(published).toBeGreaterThanOrEqual(0);
|
|
1781
|
+
expect(reconciled).toBeGreaterThanOrEqual(0);
|
|
1782
|
+
});
|
|
1783
|
+
});
|
|
1784
|
+
`,
|
|
1785
|
+
'jobs/data-retention/src/run.ts': `import { readFile } from 'node:fs/promises';
|
|
1786
|
+
import {
|
|
1787
|
+
EventBridgeClient,
|
|
1788
|
+
PutEventsCommand,
|
|
1789
|
+
type PutEventsRequestEntry,
|
|
1790
|
+
} from '@aws-sdk/client-eventbridge';
|
|
1791
|
+
import {
|
|
1792
|
+
SendTaskFailureCommand,
|
|
1793
|
+
SendTaskSuccessCommand,
|
|
1794
|
+
SFNClient,
|
|
1795
|
+
} from '@aws-sdk/client-sfn';
|
|
1796
|
+
import {
|
|
1797
|
+
beginApprovalExecution,
|
|
1798
|
+
completeApprovalExecution,
|
|
1799
|
+
createAuthorizationEvaluator,
|
|
1800
|
+
defineCoarseAdmission,
|
|
1801
|
+
digestApprovalRequest,
|
|
1802
|
+
type ApprovalRequest,
|
|
1803
|
+
type AuthorizationPrincipal,
|
|
1804
|
+
} from '@empire-builder-kit/runtime/authorization';
|
|
1805
|
+
import {
|
|
1806
|
+
createDataLifecycleEvidence,
|
|
1807
|
+
createDataLifecyclePseudonymousReference,
|
|
1808
|
+
createDataLifecycleRetentionCheckpoint,
|
|
1809
|
+
runDataLifecycleRetention,
|
|
1810
|
+
type DataLifecycleRetentionCheckpointV1,
|
|
1811
|
+
} from '@empire-builder-kit/runtime/data-lifecycle';
|
|
1812
|
+
import { createLogger } from '@empire-builder-kit/runtime/logging';
|
|
1813
|
+
import {
|
|
1814
|
+
bindServerSecretRuntime,
|
|
1815
|
+
createAwsSecretsManagerSource,
|
|
1816
|
+
createServerSecretResolver,
|
|
1817
|
+
} from '@empire-builder-kit/runtime/secrets';
|
|
1818
|
+
import pg from 'pg';
|
|
1819
|
+
import {
|
|
1820
|
+
${identifierPrefix}AuthorizationActions,
|
|
1821
|
+
${identifierPrefix}AuthorizationPolicy,
|
|
1822
|
+
} from '../../../authorization/policy.js';
|
|
1823
|
+
import { approvalReference } from '../../../data/pseudonymous-reference.js';
|
|
1824
|
+
import {
|
|
1825
|
+
${pascalName}DataLifecycleRuntimePolicy,
|
|
1826
|
+
} from '../../../data/runtime-policy.cjs';
|
|
1827
|
+
import { Secrets } from '../../../secrets/declarations.js';
|
|
1828
|
+
import { create${pascalName}RetentionAdapter } from './adapter.js';
|
|
1829
|
+
|
|
1830
|
+
const { Client } = pg;
|
|
1831
|
+
const eventBridge = new EventBridgeClient({});
|
|
1832
|
+
const POLICY_VERSION =
|
|
1833
|
+
${pascalName}DataLifecycleRuntimePolicy.policyVersion;
|
|
1834
|
+
const RETENTION_DAYS =
|
|
1835
|
+
${pascalName}DataLifecycleRuntimePolicy.retentionDays;
|
|
1836
|
+
const AUTHORIZATION_POLICY_VERSION =
|
|
1837
|
+
${identifierPrefix}AuthorizationPolicy.version;
|
|
1838
|
+
const ACTION = ${identifierPrefix}AuthorizationActions.retentionApply;
|
|
1839
|
+
const WORKLOAD_IDENTITY = '${name}:data-retention-job';
|
|
1840
|
+
const MAX_PAGES = 100;
|
|
1841
|
+
const PAGE_SIZE = 100;
|
|
1842
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
1843
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/;
|
|
1844
|
+
let credentialResolver:
|
|
1845
|
+
| ReturnType<typeof createServerSecretResolver>
|
|
1846
|
+
| undefined;
|
|
1847
|
+
|
|
1848
|
+
async function publishLifecycleEvent(entry: PutEventsRequestEntry) {
|
|
1849
|
+
const result = await eventBridge.send(
|
|
1850
|
+
new PutEventsCommand({ Entries: [entry] }),
|
|
1851
|
+
);
|
|
1852
|
+
if (result.FailedEntryCount !== 0 || result.Entries?.[0]?.ErrorCode) {
|
|
1853
|
+
throw new Error('EventBridge rejected the retention lifecycle event.');
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
interface RetentionPlan {
|
|
1858
|
+
asOf: string;
|
|
1859
|
+
candidateDigest: string;
|
|
1860
|
+
categoryId: 'product-record';
|
|
1861
|
+
due: number;
|
|
1862
|
+
locationId: 'primary-database';
|
|
1863
|
+
maxItems: 10000;
|
|
1864
|
+
pageSize: 100;
|
|
1865
|
+
policyVersion: string;
|
|
1866
|
+
retentionDays: number;
|
|
1867
|
+
runId: string;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
interface RetentionCounts {
|
|
1871
|
+
alreadyApplied: number;
|
|
1872
|
+
applied: number;
|
|
1873
|
+
applyPages: number;
|
|
1874
|
+
due: number;
|
|
1875
|
+
reportPages: number;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
interface RetentionRunRow {
|
|
1879
|
+
approvalRequestId: string;
|
|
1880
|
+
checkpointJson: DataLifecycleRetentionCheckpointV1;
|
|
1881
|
+
countsJson: RetentionCounts;
|
|
1882
|
+
planJson: RetentionPlan;
|
|
1883
|
+
revision: number;
|
|
1884
|
+
status: string;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
function required(name: string): string {
|
|
1888
|
+
const value = process.env[name]?.trim();
|
|
1889
|
+
if (!value) throw new Error(name + ' is required.');
|
|
1890
|
+
return value;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
function requiredUuid(name: string): string {
|
|
1894
|
+
const value = required(name);
|
|
1895
|
+
if (!UUID.test(value)) throw new Error(name + ' must be a UUID.');
|
|
1896
|
+
return value;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function requiredInstant(name: string): string {
|
|
1900
|
+
const value = required(name);
|
|
1901
|
+
const parsed = Date.parse(value);
|
|
1902
|
+
if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
|
|
1903
|
+
throw new Error(name + ' must be a canonical timestamp.');
|
|
1904
|
+
}
|
|
1905
|
+
return value;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function optionalUuid(name: string): string | undefined {
|
|
1909
|
+
const value = process.env[name]?.trim();
|
|
1910
|
+
if (!value) return undefined;
|
|
1911
|
+
if (!UUID.test(value)) throw new Error(name + ' must be a UUID.');
|
|
1912
|
+
return value;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
function safeErrorCode(error: unknown): string {
|
|
1916
|
+
const value =
|
|
1917
|
+
error && typeof error === 'object' && 'name' in error
|
|
1918
|
+
? String(error.name)
|
|
1919
|
+
: 'RetentionJobError';
|
|
1920
|
+
return value.replace(/[^A-Za-z0-9_.-]/g, '').slice(0, 80) || 'Error';
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function addDays(value: string, days: number): string {
|
|
1924
|
+
const result = new Date(value);
|
|
1925
|
+
result.setUTCDate(result.getUTCDate() + days);
|
|
1926
|
+
return result.toISOString();
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function databaseCredentialResolver() {
|
|
1930
|
+
credentialResolver ??= createServerSecretResolver({
|
|
1931
|
+
binding: bindServerSecretRuntime({
|
|
1932
|
+
stage: required('EBK_STAGE'),
|
|
1933
|
+
workload: WORKLOAD_IDENTITY,
|
|
1934
|
+
}),
|
|
1935
|
+
cache: { freshTtlMs: 0, staleTtlMs: 0 },
|
|
1936
|
+
source: createAwsSecretsManagerSource(),
|
|
1937
|
+
});
|
|
1938
|
+
return credentialResolver;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
async function readCredential(secretArn: string, correlationId: string) {
|
|
1942
|
+
const declaration = Secrets.${pascalName}PrimaryDatabaseCredential;
|
|
1943
|
+
const credential = await databaseCredentialResolver().get(
|
|
1944
|
+
declaration,
|
|
1945
|
+
{
|
|
1946
|
+
logicalName: declaration.logicalName,
|
|
1947
|
+
provider: 'aws-secrets-manager',
|
|
1948
|
+
secretArn,
|
|
1949
|
+
},
|
|
1950
|
+
{ correlationId },
|
|
1951
|
+
);
|
|
1952
|
+
if (
|
|
1953
|
+
credential.database !== required('EBK_DATABASE_NAME') ||
|
|
1954
|
+
credential.host !== required('EBK_DATABASE_HOST') ||
|
|
1955
|
+
credential.port !== Number(required('EBK_DATABASE_PORT')) ||
|
|
1956
|
+
credential.username !== required('EBK_DATABASE_USERNAME') ||
|
|
1957
|
+
!credential.password
|
|
1958
|
+
) {
|
|
1959
|
+
throw new Error(
|
|
1960
|
+
'Database credential does not match the exact retention Job binding.',
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
return credential;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
type RetentionAuthorizationAction =
|
|
1967
|
+
| typeof ${identifierPrefix}AuthorizationActions.retentionApply
|
|
1968
|
+
| typeof ${identifierPrefix}AuthorizationActions.retentionReport;
|
|
1969
|
+
|
|
1970
|
+
export async function authorizeRetentionOperation(
|
|
1971
|
+
action: RetentionAuthorizationAction,
|
|
1972
|
+
now: string,
|
|
1973
|
+
runId: string,
|
|
1974
|
+
): Promise<AuthorizationPrincipal> {
|
|
1975
|
+
if (required('EBK_WORKLOAD_IDENTITY') !== WORKLOAD_IDENTITY) {
|
|
1976
|
+
throw new Error('Retention Job workload identity is invalid.');
|
|
1977
|
+
}
|
|
1978
|
+
const principal: AuthorizationPrincipal = {
|
|
1979
|
+
actor: { id: WORKLOAD_IDENTITY, type: 'service' },
|
|
1980
|
+
audience: WORKLOAD_IDENTITY,
|
|
1981
|
+
authority: {
|
|
1982
|
+
machineIdentityId: WORKLOAD_IDENTITY,
|
|
1983
|
+
mode: 'machine',
|
|
1984
|
+
},
|
|
1985
|
+
kind: 'agent',
|
|
1986
|
+
roles: [],
|
|
1987
|
+
scopes: [],
|
|
1988
|
+
subject: { id: WORKLOAD_IDENTITY, type: 'service' },
|
|
1989
|
+
};
|
|
1990
|
+
const evaluator = createAuthorizationEvaluator({
|
|
1991
|
+
admission: defineCoarseAdmission({
|
|
1992
|
+
anyOf: [{ kinds: ['agent'], subjectTypes: ['service'] }],
|
|
1993
|
+
}),
|
|
1994
|
+
policy: ${identifierPrefix}AuthorizationPolicy,
|
|
1995
|
+
requiredPolicyVersion: AUTHORIZATION_POLICY_VERSION,
|
|
1996
|
+
});
|
|
1997
|
+
await evaluator.authorize({
|
|
1998
|
+
action,
|
|
1999
|
+
context: {
|
|
2000
|
+
facts: { operationId: runId },
|
|
2001
|
+
now,
|
|
2002
|
+
stage: required('EBK_STAGE'),
|
|
2003
|
+
},
|
|
2004
|
+
principal,
|
|
2005
|
+
resource: { attributes: { collection: true }, type: 'slice' },
|
|
2006
|
+
});
|
|
2007
|
+
return principal;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
async function writeRetentionAuthorizationAudit(
|
|
2011
|
+
client: InstanceType<typeof Client>,
|
|
2012
|
+
action: RetentionAuthorizationAction,
|
|
2013
|
+
correlationId: string,
|
|
2014
|
+
now: string,
|
|
2015
|
+
principal: AuthorizationPrincipal,
|
|
2016
|
+
runId: string,
|
|
2017
|
+
) {
|
|
2018
|
+
await client.query(
|
|
2019
|
+
\`INSERT INTO authorization_audit
|
|
2020
|
+
(action, approval_reference, authority_mode, correlation_id, decision,
|
|
2021
|
+
delegation_reference, policy_version, principal_kind,
|
|
2022
|
+
pseudonymous_subject_id, reason_category, resource_reference,
|
|
2023
|
+
resource_type, slice, stage, subject_type)
|
|
2024
|
+
VALUES ($1, NULL, 'machine', $2, 'allow', NULL, $3, 'agent', $4,
|
|
2025
|
+
'policy-allow', $5, 'retention-run', $6, $7, 'service')\`,
|
|
2026
|
+
[
|
|
2027
|
+
action,
|
|
2028
|
+
correlationId,
|
|
2029
|
+
AUTHORIZATION_POLICY_VERSION,
|
|
2030
|
+
createDataLifecyclePseudonymousReference({
|
|
2031
|
+
categoryId: 'authorization-subject',
|
|
2032
|
+
slice: '${name}',
|
|
2033
|
+
value: principal.actor.id,
|
|
2034
|
+
}),
|
|
2035
|
+
createDataLifecyclePseudonymousReference({
|
|
2036
|
+
categoryId: 'lifecycle-control-metadata',
|
|
2037
|
+
slice: '${name}',
|
|
2038
|
+
value: runId,
|
|
2039
|
+
}),
|
|
2040
|
+
'${name}',
|
|
2041
|
+
required('EBK_STAGE'),
|
|
2042
|
+
],
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
async function report(
|
|
2047
|
+
client: InstanceType<typeof Client>,
|
|
2048
|
+
asOf: string,
|
|
2049
|
+
correlationId: string,
|
|
2050
|
+
runId: string,
|
|
2051
|
+
) {
|
|
2052
|
+
const action = ${identifierPrefix}AuthorizationActions.retentionReport;
|
|
2053
|
+
const principal = await authorizeRetentionOperation(action, asOf, runId);
|
|
2054
|
+
await writeRetentionAuthorizationAudit(
|
|
2055
|
+
client,
|
|
2056
|
+
action,
|
|
2057
|
+
correlationId,
|
|
2058
|
+
asOf,
|
|
2059
|
+
principal,
|
|
2060
|
+
runId,
|
|
2061
|
+
);
|
|
2062
|
+
const adapter = create${pascalName}RetentionAdapter({
|
|
2063
|
+
asOf,
|
|
2064
|
+
client,
|
|
2065
|
+
completedAt: asOf,
|
|
2066
|
+
correlationId,
|
|
2067
|
+
eventBusName: required('EVENT_BUS_NAME'),
|
|
2068
|
+
mode: 'report',
|
|
2069
|
+
publishLifecycleEvent,
|
|
2070
|
+
runId,
|
|
2071
|
+
stage: required('EBK_STAGE'),
|
|
2072
|
+
});
|
|
2073
|
+
let checkpoint: DataLifecycleRetentionCheckpointV1 | undefined;
|
|
2074
|
+
let due = 0;
|
|
2075
|
+
let pages = 0;
|
|
2076
|
+
let status: 'complete' | 'more' | 'retry-required' = 'more';
|
|
2077
|
+
while (status === 'more' && pages < MAX_PAGES) {
|
|
2078
|
+
const result = await runDataLifecycleRetention({
|
|
2079
|
+
adapter,
|
|
2080
|
+
asOf,
|
|
2081
|
+
checkpoint,
|
|
2082
|
+
limit: PAGE_SIZE,
|
|
2083
|
+
mode: 'report',
|
|
2084
|
+
policyVersion: POLICY_VERSION,
|
|
2085
|
+
});
|
|
2086
|
+
checkpoint = result.checkpoint;
|
|
2087
|
+
due += result.due;
|
|
2088
|
+
status = result.status;
|
|
2089
|
+
pages += 1;
|
|
2090
|
+
}
|
|
2091
|
+
if (status !== 'complete' || !checkpoint?.complete) {
|
|
2092
|
+
throw new Error('Retention report exceeded its bounded execution.');
|
|
2093
|
+
}
|
|
2094
|
+
const plan: RetentionPlan = {
|
|
2095
|
+
asOf,
|
|
2096
|
+
candidateDigest: adapter.candidateDigest(),
|
|
2097
|
+
categoryId: 'product-record',
|
|
2098
|
+
due,
|
|
2099
|
+
locationId: 'primary-database',
|
|
2100
|
+
maxItems: 10000,
|
|
2101
|
+
pageSize: 100,
|
|
2102
|
+
policyVersion: POLICY_VERSION,
|
|
2103
|
+
retentionDays: RETENTION_DAYS,
|
|
2104
|
+
runId,
|
|
2105
|
+
};
|
|
2106
|
+
if (!SHA256.test(plan.candidateDigest)) {
|
|
2107
|
+
throw new Error('Retention report candidate digest is invalid.');
|
|
2108
|
+
}
|
|
2109
|
+
const counts: RetentionCounts = {
|
|
2110
|
+
alreadyApplied: 0,
|
|
2111
|
+
applied: 0,
|
|
2112
|
+
applyPages: 0,
|
|
2113
|
+
due,
|
|
2114
|
+
reportPages: pages,
|
|
2115
|
+
};
|
|
2116
|
+
await client.query('BEGIN');
|
|
2117
|
+
try {
|
|
2118
|
+
await client.query(
|
|
2119
|
+
\`INSERT INTO data_lifecycle_retention_runs
|
|
2120
|
+
(id, as_of, checkpoint_json, counts_json, expires_at, plan_json,
|
|
2121
|
+
policy_version, revision, status)
|
|
2122
|
+
VALUES ($1::uuid, $2, $3::jsonb, $4::jsonb, $5, $6::jsonb,
|
|
2123
|
+
$7, 0, 'reported')\`,
|
|
2124
|
+
[
|
|
2125
|
+
runId,
|
|
2126
|
+
asOf,
|
|
2127
|
+
JSON.stringify(createDataLifecycleRetentionCheckpoint(POLICY_VERSION)),
|
|
2128
|
+
JSON.stringify(counts),
|
|
2129
|
+
addDays(asOf, 400),
|
|
2130
|
+
JSON.stringify(plan),
|
|
2131
|
+
POLICY_VERSION,
|
|
2132
|
+
],
|
|
2133
|
+
);
|
|
2134
|
+
const candidates = adapter.candidateRecords();
|
|
2135
|
+
if (candidates.length !== due) {
|
|
2136
|
+
throw new Error('Retention report candidate inventory is incomplete.');
|
|
2137
|
+
}
|
|
2138
|
+
for (const candidate of candidates) {
|
|
2139
|
+
await client.query(
|
|
2140
|
+
\`INSERT INTO data_lifecycle_retention_candidates
|
|
2141
|
+
(run_id, candidate_order, record_id, record_ref, due_at)
|
|
2142
|
+
VALUES ($1::uuid, $2, $3::uuid, $4, $5)\`,
|
|
2143
|
+
[
|
|
2144
|
+
runId,
|
|
2145
|
+
candidate.candidateOrder,
|
|
2146
|
+
candidate.recordId,
|
|
2147
|
+
candidate.recordRef,
|
|
2148
|
+
candidate.dueAt,
|
|
2149
|
+
],
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
await client.query('COMMIT');
|
|
2153
|
+
} catch (error) {
|
|
2154
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
2155
|
+
throw error;
|
|
2156
|
+
}
|
|
2157
|
+
return {
|
|
2158
|
+
counts,
|
|
2159
|
+
planDigest:
|
|
2160
|
+
'sha256:' +
|
|
2161
|
+
digestApprovalRequest({
|
|
2162
|
+
action: ACTION,
|
|
2163
|
+
command: plan,
|
|
2164
|
+
resource: { id: runId, type: 'retention-run' },
|
|
2165
|
+
}),
|
|
2166
|
+
runRef: runId,
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
async function loadRun(
|
|
2171
|
+
client: InstanceType<typeof Client>,
|
|
2172
|
+
approvalRequestId: string,
|
|
2173
|
+
runId: string,
|
|
2174
|
+
): Promise<{
|
|
2175
|
+
approval: ApprovalRequest<string>;
|
|
2176
|
+
run: RetentionRunRow;
|
|
2177
|
+
}> {
|
|
2178
|
+
const result = await client.query<{
|
|
2179
|
+
approvalJson: ApprovalRequest<string>;
|
|
2180
|
+
approvalRequestId: string;
|
|
2181
|
+
checkpointJson: DataLifecycleRetentionCheckpointV1;
|
|
2182
|
+
countsJson: RetentionCounts;
|
|
2183
|
+
planJson: RetentionPlan;
|
|
2184
|
+
revision: number;
|
|
2185
|
+
status: string;
|
|
2186
|
+
}>(
|
|
2187
|
+
\`SELECT a.request_json AS "approvalJson",
|
|
2188
|
+
r.approval_request_id::text AS "approvalRequestId",
|
|
2189
|
+
r.checkpoint_json AS "checkpointJson",
|
|
2190
|
+
r.counts_json AS "countsJson",
|
|
2191
|
+
r.plan_json AS "planJson",
|
|
2192
|
+
r.revision,
|
|
2193
|
+
r.status
|
|
2194
|
+
FROM data_lifecycle_retention_runs r
|
|
2195
|
+
JOIN approval_requests a ON a.id = r.approval_request_id
|
|
2196
|
+
WHERE r.id = $1::uuid AND a.id = $2::uuid\`,
|
|
2197
|
+
[runId, approvalRequestId],
|
|
2198
|
+
);
|
|
2199
|
+
const row = result.rows[0];
|
|
2200
|
+
if (!row) throw new Error('Retention run and approval do not match.');
|
|
2201
|
+
return {
|
|
2202
|
+
approval: row.approvalJson,
|
|
2203
|
+
run: {
|
|
2204
|
+
approvalRequestId: row.approvalRequestId,
|
|
2205
|
+
checkpointJson: row.checkpointJson,
|
|
2206
|
+
countsJson: row.countsJson,
|
|
2207
|
+
planJson: row.planJson,
|
|
2208
|
+
revision: row.revision,
|
|
2209
|
+
status: row.status,
|
|
2210
|
+
},
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
async function validateApprovedPlan(
|
|
2215
|
+
client: InstanceType<typeof Client>,
|
|
2216
|
+
correlationId: string,
|
|
2217
|
+
plan: RetentionPlan,
|
|
2218
|
+
) {
|
|
2219
|
+
const adapter = create${pascalName}RetentionAdapter({
|
|
2220
|
+
asOf: plan.asOf,
|
|
2221
|
+
client,
|
|
2222
|
+
completedAt: plan.asOf,
|
|
2223
|
+
correlationId,
|
|
2224
|
+
eventBusName: required('EVENT_BUS_NAME'),
|
|
2225
|
+
mode: 'apply',
|
|
2226
|
+
publishLifecycleEvent,
|
|
2227
|
+
runId: plan.runId,
|
|
2228
|
+
stage: required('EBK_STAGE'),
|
|
2229
|
+
});
|
|
2230
|
+
let checkpoint: DataLifecycleRetentionCheckpointV1 | undefined;
|
|
2231
|
+
let due = 0;
|
|
2232
|
+
let pages = 0;
|
|
2233
|
+
let status: 'complete' | 'more' | 'retry-required' = 'more';
|
|
2234
|
+
while (status === 'more' && pages < MAX_PAGES) {
|
|
2235
|
+
const result = await runDataLifecycleRetention({
|
|
2236
|
+
adapter,
|
|
2237
|
+
asOf: plan.asOf,
|
|
2238
|
+
checkpoint,
|
|
2239
|
+
limit: PAGE_SIZE,
|
|
2240
|
+
mode: 'report',
|
|
2241
|
+
policyVersion: POLICY_VERSION,
|
|
2242
|
+
});
|
|
2243
|
+
checkpoint = result.checkpoint;
|
|
2244
|
+
due += result.due;
|
|
2245
|
+
pages += 1;
|
|
2246
|
+
status = result.status;
|
|
2247
|
+
}
|
|
2248
|
+
if (
|
|
2249
|
+
status !== 'complete' ||
|
|
2250
|
+
!checkpoint?.complete ||
|
|
2251
|
+
due !== plan.due ||
|
|
2252
|
+
adapter.candidateDigest() !== plan.candidateDigest
|
|
2253
|
+
) {
|
|
2254
|
+
throw new Error(
|
|
2255
|
+
'Retention candidates changed after review; create and approve a new report.',
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
async function apply(
|
|
2261
|
+
client: InstanceType<typeof Client>,
|
|
2262
|
+
approvalRequestId: string,
|
|
2263
|
+
correlationId: string,
|
|
2264
|
+
now: string,
|
|
2265
|
+
runId: string,
|
|
2266
|
+
) {
|
|
2267
|
+
const lock = await client.query<{ locked: boolean }>(
|
|
2268
|
+
\`SELECT pg_try_advisory_lock(hashtext($1)) AS locked\`,
|
|
2269
|
+
['data-retention:' + runId],
|
|
2270
|
+
);
|
|
2271
|
+
if (!lock.rows[0]?.locked) {
|
|
2272
|
+
throw new Error('The retention run is already active.');
|
|
2273
|
+
}
|
|
2274
|
+
try {
|
|
2275
|
+
let { approval, run } = await loadRun(
|
|
2276
|
+
client,
|
|
2277
|
+
approvalRequestId,
|
|
2278
|
+
runId,
|
|
2279
|
+
);
|
|
2280
|
+
if (
|
|
2281
|
+
run.planJson.runId !== runId ||
|
|
2282
|
+
run.planJson.policyVersion !== POLICY_VERSION ||
|
|
2283
|
+
run.planJson.maxItems !== 10000 ||
|
|
2284
|
+
run.planJson.pageSize !== 100 ||
|
|
2285
|
+
!SHA256.test(run.planJson.candidateDigest) ||
|
|
2286
|
+
approval.id !== approvalRequestId ||
|
|
2287
|
+
approval.policyVersion !== AUTHORIZATION_POLICY_VERSION ||
|
|
2288
|
+
approval.schemaVersion !== 1 ||
|
|
2289
|
+
approval.slice !== '${name}' ||
|
|
2290
|
+
digestApprovalRequest({
|
|
2291
|
+
action: ACTION,
|
|
2292
|
+
command: run.planJson,
|
|
2293
|
+
resource: { id: runId, type: 'retention-run' },
|
|
2294
|
+
}) !== approval.requestDigest
|
|
2295
|
+
) {
|
|
2296
|
+
throw new Error('Retention approval is not bound to the exact plan.');
|
|
2297
|
+
}
|
|
2298
|
+
if (
|
|
2299
|
+
run.status !== 'approval-approved' &&
|
|
2300
|
+
run.status !== 'retry-required' &&
|
|
2301
|
+
run.status !== 'applying'
|
|
2302
|
+
) {
|
|
2303
|
+
throw new Error('Retention run is not approved for apply.');
|
|
2304
|
+
}
|
|
2305
|
+
const claimed = beginApprovalExecution(approval, {
|
|
2306
|
+
action: ACTION,
|
|
2307
|
+
command: run.planJson,
|
|
2308
|
+
executionId: runId,
|
|
2309
|
+
now,
|
|
2310
|
+
requiredPolicyVersion: AUTHORIZATION_POLICY_VERSION,
|
|
2311
|
+
resource: { id: runId, type: 'retention-run' },
|
|
2312
|
+
});
|
|
2313
|
+
if (claimed.decision.decision !== 'allow') {
|
|
2314
|
+
throw new Error(
|
|
2315
|
+
'Retention apply approval was denied: ' +
|
|
2316
|
+
claimed.decision.category,
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
const principal = await authorizeRetentionOperation(
|
|
2320
|
+
${identifierPrefix}AuthorizationActions.retentionApply,
|
|
2321
|
+
now,
|
|
2322
|
+
runId,
|
|
2323
|
+
);
|
|
2324
|
+
if (run.status === 'approval-approved') {
|
|
2325
|
+
await validateApprovedPlan(client, correlationId, run.planJson);
|
|
2326
|
+
}
|
|
2327
|
+
await client.query('BEGIN');
|
|
2328
|
+
try {
|
|
2329
|
+
if (claimed.request.revision !== approval.revision) {
|
|
2330
|
+
const updated = await client.query(
|
|
2331
|
+
\`UPDATE approval_requests
|
|
2332
|
+
SET request_json = $3::jsonb, revision = $4, status = $5
|
|
2333
|
+
WHERE id = $1::uuid AND revision = $2\`,
|
|
2334
|
+
[
|
|
2335
|
+
approval.id,
|
|
2336
|
+
approval.revision,
|
|
2337
|
+
JSON.stringify(claimed.request),
|
|
2338
|
+
claimed.request.revision,
|
|
2339
|
+
claimed.request.status,
|
|
2340
|
+
],
|
|
2341
|
+
);
|
|
2342
|
+
if (updated.rowCount !== 1) {
|
|
2343
|
+
throw new Error('Retention approval claim lost compare-and-set.');
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
const started = await client.query(
|
|
2347
|
+
\`UPDATE data_lifecycle_retention_runs
|
|
2348
|
+
SET revision = revision + 1, status = 'applying', updated_at = $3
|
|
2349
|
+
WHERE id = $1::uuid AND revision = $2
|
|
2350
|
+
AND status IN ('approval-approved', 'retry-required', 'applying')\`,
|
|
2351
|
+
[runId, run.revision, now],
|
|
2352
|
+
);
|
|
2353
|
+
if (started.rowCount !== 1) {
|
|
2354
|
+
throw new Error('Retention run claim lost compare-and-set.');
|
|
2355
|
+
}
|
|
2356
|
+
await writeRetentionAuthorizationAudit(
|
|
2357
|
+
client,
|
|
2358
|
+
${identifierPrefix}AuthorizationActions.retentionApply,
|
|
2359
|
+
correlationId,
|
|
2360
|
+
now,
|
|
2361
|
+
principal,
|
|
2362
|
+
runId,
|
|
2363
|
+
);
|
|
2364
|
+
await client.query('COMMIT');
|
|
2365
|
+
} catch (error) {
|
|
2366
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
2367
|
+
throw error;
|
|
2368
|
+
}
|
|
2369
|
+
approval = claimed.request;
|
|
2370
|
+
run = (await loadRun(client, approvalRequestId, runId)).run;
|
|
2371
|
+
const adapter = create${pascalName}RetentionAdapter({
|
|
2372
|
+
approvalRequestId,
|
|
2373
|
+
asOf: run.planJson.asOf,
|
|
2374
|
+
client,
|
|
2375
|
+
completedAt: now,
|
|
2376
|
+
correlationId,
|
|
2377
|
+
eventBusName: required('EVENT_BUS_NAME'),
|
|
2378
|
+
mode: 'apply',
|
|
2379
|
+
publishLifecycleEvent,
|
|
2380
|
+
runId,
|
|
2381
|
+
stage: required('EBK_STAGE'),
|
|
2382
|
+
});
|
|
2383
|
+
let checkpoint = run.checkpointJson;
|
|
2384
|
+
const counts = { ...run.countsJson };
|
|
2385
|
+
let invocationPages = 0;
|
|
2386
|
+
let status: 'complete' | 'more' | 'retry-required' = 'more';
|
|
2387
|
+
while (status === 'more' && invocationPages < MAX_PAGES) {
|
|
2388
|
+
const result = await runDataLifecycleRetention({
|
|
2389
|
+
adapter,
|
|
2390
|
+
asOf: run.planJson.asOf,
|
|
2391
|
+
checkpoint,
|
|
2392
|
+
limit: PAGE_SIZE,
|
|
2393
|
+
mode: 'apply',
|
|
2394
|
+
policyVersion: POLICY_VERSION,
|
|
2395
|
+
});
|
|
2396
|
+
checkpoint = result.checkpoint;
|
|
2397
|
+
counts.alreadyApplied += result.alreadyApplied;
|
|
2398
|
+
counts.applied += result.applied;
|
|
2399
|
+
counts.applyPages += 1;
|
|
2400
|
+
invocationPages += 1;
|
|
2401
|
+
status = result.status;
|
|
2402
|
+
const persisted = await client.query(
|
|
2403
|
+
\`UPDATE data_lifecycle_retention_runs
|
|
2404
|
+
SET checkpoint_json = $2::jsonb, counts_json = $3::jsonb,
|
|
2405
|
+
revision = revision + 1, status = $4, updated_at = $5
|
|
2406
|
+
WHERE id = $1::uuid\`,
|
|
2407
|
+
[
|
|
2408
|
+
runId,
|
|
2409
|
+
JSON.stringify(checkpoint),
|
|
2410
|
+
JSON.stringify(counts),
|
|
2411
|
+
status === 'retry-required' ? 'retry-required' : 'applying',
|
|
2412
|
+
now,
|
|
2413
|
+
],
|
|
2414
|
+
);
|
|
2415
|
+
if (persisted.rowCount !== 1) {
|
|
2416
|
+
throw new Error('Retention checkpoint could not be persisted.');
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
if (status !== 'complete' || !checkpoint.complete) {
|
|
2420
|
+
throw new Error(
|
|
2421
|
+
status === 'retry-required'
|
|
2422
|
+
? 'Retention adapter requires a safe checkpoint retry.'
|
|
2423
|
+
: 'Retention apply exceeded its bounded execution.',
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
const completed = completeApprovalExecution(approval, {
|
|
2427
|
+
executionId: runId,
|
|
2428
|
+
now,
|
|
2429
|
+
requiredPolicyVersion: AUTHORIZATION_POLICY_VERSION,
|
|
2430
|
+
});
|
|
2431
|
+
if (completed.decision.decision !== 'allow') {
|
|
2432
|
+
throw new Error('Retention approval completion was denied.');
|
|
2433
|
+
}
|
|
2434
|
+
await client.query('BEGIN');
|
|
2435
|
+
try {
|
|
2436
|
+
const approvalUpdated = await client.query(
|
|
2437
|
+
\`UPDATE approval_requests
|
|
2438
|
+
SET request_json = $3::jsonb, revision = $4, status = $5
|
|
2439
|
+
WHERE id = $1::uuid AND revision = $2\`,
|
|
2440
|
+
[
|
|
2441
|
+
approval.id,
|
|
2442
|
+
approval.revision,
|
|
2443
|
+
JSON.stringify(completed.request),
|
|
2444
|
+
completed.request.revision,
|
|
2445
|
+
completed.request.status,
|
|
2446
|
+
],
|
|
2447
|
+
);
|
|
2448
|
+
const runUpdated = await client.query(
|
|
2449
|
+
\`UPDATE data_lifecycle_retention_runs
|
|
2450
|
+
SET checkpoint_json = $2::jsonb, completed_at = $3,
|
|
2451
|
+
counts_json = $4::jsonb, revision = revision + 1,
|
|
2452
|
+
status = 'completed', updated_at = $3
|
|
2453
|
+
WHERE id = $1::uuid AND status = 'applying'\`,
|
|
2454
|
+
[runId, JSON.stringify(checkpoint), now, JSON.stringify(counts)],
|
|
2455
|
+
);
|
|
2456
|
+
const candidatesDeleted = await client.query(
|
|
2457
|
+
\`DELETE FROM data_lifecycle_retention_candidates
|
|
2458
|
+
WHERE run_id = $1::uuid\`,
|
|
2459
|
+
[runId],
|
|
2460
|
+
);
|
|
2461
|
+
if (approvalUpdated.rowCount !== 1 || runUpdated.rowCount !== 1) {
|
|
2462
|
+
throw new Error('Retention completion lost compare-and-set.');
|
|
2463
|
+
}
|
|
2464
|
+
if (candidatesDeleted.rowCount !== run.planJson.due) {
|
|
2465
|
+
throw new Error('Retention candidate cleanup was incomplete.');
|
|
2466
|
+
}
|
|
2467
|
+
await client.query('COMMIT');
|
|
2468
|
+
} catch (error) {
|
|
2469
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
2470
|
+
throw error;
|
|
2471
|
+
}
|
|
2472
|
+
return {
|
|
2473
|
+
approvalRequestId: approval.id,
|
|
2474
|
+
checkpointComplete: true,
|
|
2475
|
+
checkpointDigest:
|
|
2476
|
+
'sha256:' +
|
|
2477
|
+
digestApprovalRequest({
|
|
2478
|
+
action: 'checkpoint',
|
|
2479
|
+
command: checkpoint,
|
|
2480
|
+
resource: { id: runId, type: 'retention-run' },
|
|
2481
|
+
}),
|
|
2482
|
+
counts,
|
|
2483
|
+
runRef: runId,
|
|
2484
|
+
};
|
|
2485
|
+
} finally {
|
|
2486
|
+
await client
|
|
2487
|
+
.query(\`SELECT pg_advisory_unlock(hashtext($1))\`, [
|
|
2488
|
+
'data-retention:' + runId,
|
|
2489
|
+
])
|
|
2490
|
+
.catch(() => undefined);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
export async function runDataRetentionJob() {
|
|
2495
|
+
const mode = required('EBK_RETENTION_MODE');
|
|
2496
|
+
if (mode !== 'report' && mode !== 'apply') {
|
|
2497
|
+
throw new Error('EBK_RETENTION_MODE must be report or apply.');
|
|
2498
|
+
}
|
|
2499
|
+
const asOf = requiredInstant('EBK_RETENTION_AS_OF');
|
|
2500
|
+
const now = new Date().toISOString();
|
|
2501
|
+
const runId = requiredUuid('EBK_RETENTION_RUN_ID');
|
|
2502
|
+
const approvalRequestId = optionalUuid('EBK_RETENTION_APPROVAL_ID');
|
|
2503
|
+
if (mode === 'apply' && !approvalRequestId) {
|
|
2504
|
+
throw new Error('Retention apply requires its durable approval request.');
|
|
2505
|
+
}
|
|
2506
|
+
if (mode === 'report' && approvalRequestId) {
|
|
2507
|
+
throw new Error('Retention report cannot select an approval request.');
|
|
2508
|
+
}
|
|
2509
|
+
const token = required('EBK_STEP_FUNCTIONS_TASK_TOKEN');
|
|
2510
|
+
const correlationId = requiredUuid('EBK_RETENTION_CORRELATION_ID');
|
|
2511
|
+
const logger = createLogger({
|
|
2512
|
+
component: 'job-data-retention',
|
|
2513
|
+
feature: 'data-lifecycle',
|
|
2514
|
+
operation: 'retention-' + mode,
|
|
2515
|
+
slice: '${name}',
|
|
2516
|
+
stage: required('EBK_STAGE'),
|
|
2517
|
+
workspace: required('EBK_WORKSPACE'),
|
|
2518
|
+
});
|
|
2519
|
+
const secretArn = required('EBK_READWRITE_SECRET_ARN');
|
|
2520
|
+
const credential = await readCredential(secretArn, correlationId);
|
|
2521
|
+
const ca = await readFile(required('EBK_RDS_CA_BUNDLE'), 'utf8');
|
|
2522
|
+
const client = new Client({
|
|
2523
|
+
...credential,
|
|
2524
|
+
connectionTimeoutMillis: 10_000,
|
|
2525
|
+
ssl: { ca, rejectUnauthorized: true },
|
|
2526
|
+
});
|
|
2527
|
+
const sfn = new SFNClient({});
|
|
2528
|
+
try {
|
|
2529
|
+
await client.connect();
|
|
2530
|
+
const result =
|
|
2531
|
+
mode === 'report'
|
|
2532
|
+
? await report(client, asOf, correlationId, runId)
|
|
2533
|
+
: await apply(
|
|
2534
|
+
client,
|
|
2535
|
+
approvalRequestId as string,
|
|
2536
|
+
correlationId,
|
|
2537
|
+
now,
|
|
2538
|
+
runId,
|
|
2539
|
+
);
|
|
2540
|
+
const completedAt = new Date().toISOString();
|
|
2541
|
+
const evidence = createDataLifecycleEvidence({
|
|
2542
|
+
categoryId: 'product-record',
|
|
2543
|
+
completedAt,
|
|
2544
|
+
counts: {
|
|
2545
|
+
...result.counts,
|
|
2546
|
+
},
|
|
2547
|
+
operation: mode === 'apply' ? 'retention-apply' : 'retention-report',
|
|
2548
|
+
outcome: 'succeeded',
|
|
2549
|
+
policyVersion: POLICY_VERSION,
|
|
2550
|
+
...('approvalRequestId' in result
|
|
2551
|
+
? { requestRef: approvalReference(result.approvalRequestId) }
|
|
2552
|
+
: {}),
|
|
2553
|
+
startedAt: mode === 'report' ? asOf : now,
|
|
2554
|
+
summary:
|
|
2555
|
+
mode === 'apply'
|
|
2556
|
+
? 'AWS-internal bounded retention apply completed under durable non-author approval.'
|
|
2557
|
+
: 'AWS-internal bounded retention report completed without product-data mutation; request and non-author approval remain required.',
|
|
2558
|
+
systemIds: ['primary-database', 'lifecycle-control'],
|
|
2559
|
+
});
|
|
2560
|
+
const output = JSON.stringify({ evidence, ...result });
|
|
2561
|
+
await sfn.send(
|
|
2562
|
+
new SendTaskSuccessCommand({ output, taskToken: token }),
|
|
2563
|
+
);
|
|
2564
|
+
logger.info('Data retention Job completed', {
|
|
2565
|
+
attributes: {
|
|
2566
|
+
applied: result.counts.applied,
|
|
2567
|
+
due: result.counts.due,
|
|
2568
|
+
mode,
|
|
2569
|
+
applyPages: result.counts.applyPages,
|
|
2570
|
+
reportPages: result.counts.reportPages,
|
|
2571
|
+
},
|
|
2572
|
+
outcome: 'success',
|
|
2573
|
+
});
|
|
2574
|
+
return { evidence, ...result };
|
|
2575
|
+
} catch (error) {
|
|
2576
|
+
logger.error('Data retention Job failed', {
|
|
2577
|
+
attributes: { errorCode: safeErrorCode(error), mode },
|
|
2578
|
+
outcome: 'failure',
|
|
2579
|
+
});
|
|
2580
|
+
await sfn
|
|
2581
|
+
.send(
|
|
2582
|
+
new SendTaskFailureCommand({
|
|
2583
|
+
cause: JSON.stringify({
|
|
2584
|
+
code: safeErrorCode(error),
|
|
2585
|
+
runRef: runId,
|
|
2586
|
+
}),
|
|
2587
|
+
error: 'DataRetentionJobFailed',
|
|
2588
|
+
taskToken: token,
|
|
2589
|
+
}),
|
|
2590
|
+
)
|
|
2591
|
+
.catch(() => undefined);
|
|
2592
|
+
throw error;
|
|
2593
|
+
} finally {
|
|
2594
|
+
await client.end().catch(() => undefined);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
if (import.meta.url === new URL(process.argv[1] ?? '', 'file:').href) {
|
|
2599
|
+
await runDataRetentionJob();
|
|
2600
|
+
}
|
|
2601
|
+
`,
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
//# sourceMappingURL=built-in-plan-data-retention-job-files.js.map
|