@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,3293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PRODUCT_PERFORMANCE_SEEDED_PATHS = void 0;
|
|
4
|
+
exports.renderProductPerformanceFiles = renderProductPerformanceFiles;
|
|
5
|
+
const built_in_plan_shared_js_1 = require("./built-in-plan-shared.js");
|
|
6
|
+
const index_js_1 = require("../../performance/index.js");
|
|
7
|
+
exports.PRODUCT_PERFORMANCE_SEEDED_PATHS = [
|
|
8
|
+
'docs/runbooks/performance-capacity-cost.md',
|
|
9
|
+
'performance/aggregate-count-provider.ts',
|
|
10
|
+
'performance/cost-allocation.json',
|
|
11
|
+
'performance/cost-collection.json',
|
|
12
|
+
'performance/load-scenarios.json',
|
|
13
|
+
'performance/policy.json',
|
|
14
|
+
];
|
|
15
|
+
/** @internal */
|
|
16
|
+
function renderProductPerformanceFiles(name) {
|
|
17
|
+
const pascalName = name
|
|
18
|
+
.split('-')
|
|
19
|
+
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
20
|
+
.join('');
|
|
21
|
+
return {
|
|
22
|
+
'infra/performance-cost-controls.ts': `import {
|
|
23
|
+
createFoundationAccountCostControlPlan,
|
|
24
|
+
createPerformanceCostControlPlan,
|
|
25
|
+
requireExactFoundationAccountCostAuthority,
|
|
26
|
+
} from '@empire-builder-kit/nx/performance';
|
|
27
|
+
import accountCostPolicy from '../../foundation/performance/account-cost-policy.json';
|
|
28
|
+
import performancePolicy from '../performance/policy.json';
|
|
29
|
+
|
|
30
|
+
function budgetNotifications(
|
|
31
|
+
notifications: readonly {
|
|
32
|
+
comparisonOperator: 'GREATER_THAN';
|
|
33
|
+
notificationType: 'ACTUAL' | 'FORECASTED';
|
|
34
|
+
threshold: number;
|
|
35
|
+
thresholdType: 'PERCENTAGE';
|
|
36
|
+
}[],
|
|
37
|
+
alertTopicArn: any,
|
|
38
|
+
) {
|
|
39
|
+
return notifications.map((notification) => ({
|
|
40
|
+
...notification,
|
|
41
|
+
subscriberSnsTopicArns: [alertTopicArn],
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function create${pascalName}PerformanceCostControls(input: {
|
|
46
|
+
accountCostPolicyDigest: any;
|
|
47
|
+
accountMonthlyMaximumUsd: any;
|
|
48
|
+
alertTopicArn: any;
|
|
49
|
+
evaluatedAt?: string;
|
|
50
|
+
workspace: string;
|
|
51
|
+
}) {
|
|
52
|
+
const evaluatedAt = input.evaluatedAt ?? new Date().toISOString();
|
|
53
|
+
const foundationAccountCostControl = createFoundationAccountCostControlPlan({
|
|
54
|
+
evaluatedAt,
|
|
55
|
+
policy: accountCostPolicy,
|
|
56
|
+
region: process.env.AWS_REGION,
|
|
57
|
+
stage: $app.stage,
|
|
58
|
+
workspace: input.workspace,
|
|
59
|
+
});
|
|
60
|
+
const plan = createPerformanceCostControlPlan({
|
|
61
|
+
accountBudgetAuthority: foundationAccountCostControl.authority,
|
|
62
|
+
evaluatedAt,
|
|
63
|
+
policy: performancePolicy,
|
|
64
|
+
region: process.env.AWS_REGION,
|
|
65
|
+
stage: $app.stage,
|
|
66
|
+
workspace: input.workspace,
|
|
67
|
+
});
|
|
68
|
+
if (plan.status === 'disabled-non-protected-stage') {
|
|
69
|
+
return {
|
|
70
|
+
accountBudgetAuthority: plan.accountBudgetAuthority,
|
|
71
|
+
anomalyMonitor: undefined,
|
|
72
|
+
anomalySubscription: undefined,
|
|
73
|
+
materialDigest: plan.materialDigest,
|
|
74
|
+
pilotBudget: undefined,
|
|
75
|
+
policyDigest: plan.policyDigest,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const deployedAccountBudgetAuthority = input.accountCostPolicyDigest.apply(
|
|
79
|
+
(policyDigest: string) =>
|
|
80
|
+
input.accountMonthlyMaximumUsd.apply((monthlyMaximumUsd: number) =>
|
|
81
|
+
requireExactFoundationAccountCostAuthority({
|
|
82
|
+
actual: {
|
|
83
|
+
kind: 'ebk-foundation-account-cost-authority',
|
|
84
|
+
monthlyMaximumUsd,
|
|
85
|
+
policyDigest,
|
|
86
|
+
stage: $app.stage,
|
|
87
|
+
workspace: input.workspace,
|
|
88
|
+
},
|
|
89
|
+
expected: plan.accountBudgetAuthority,
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
const tags = {
|
|
94
|
+
'ebk:account-cost-policy-digest': deployedAccountBudgetAuthority.apply(
|
|
95
|
+
(authority: { policyDigest: string }) => authority.policyDigest,
|
|
96
|
+
),
|
|
97
|
+
'ebk:cost-control-material-digest': deployedAccountBudgetAuthority.apply(
|
|
98
|
+
() => plan.materialDigest,
|
|
99
|
+
),
|
|
100
|
+
'ebk:cost-scope':
|
|
101
|
+
plan.workspace + '/' + plan.slice + '/' + plan.stage,
|
|
102
|
+
'ebk:cost-policy-digest': plan.policyDigest,
|
|
103
|
+
'ebk:cost-route': plan.route.semanticRef,
|
|
104
|
+
'ebk:owner': plan.owner,
|
|
105
|
+
};
|
|
106
|
+
const pilotBudget = plan.pilotBudget
|
|
107
|
+
? new aws.budgets.Budget('${pascalName}PilotCostBudget', {
|
|
108
|
+
budgetType: plan.pilotBudget.budgetType,
|
|
109
|
+
filterExpression: plan.pilotBudget.filterExpression,
|
|
110
|
+
limitAmount: plan.pilotBudget.limitAmount,
|
|
111
|
+
limitUnit: plan.pilotBudget.limitUnit,
|
|
112
|
+
metrics: 'UnblendedCost',
|
|
113
|
+
name: plan.pilotBudget.name,
|
|
114
|
+
notifications: budgetNotifications(
|
|
115
|
+
plan.pilotBudget.notifications,
|
|
116
|
+
input.alertTopicArn,
|
|
117
|
+
),
|
|
118
|
+
tags,
|
|
119
|
+
timeUnit: plan.pilotBudget.timeUnit,
|
|
120
|
+
})
|
|
121
|
+
: undefined;
|
|
122
|
+
const anomalyMonitor = plan.anomaly
|
|
123
|
+
? new aws.costexplorer.AnomalyMonitor(
|
|
124
|
+
'${pascalName}CostAnomalyMonitor',
|
|
125
|
+
{
|
|
126
|
+
monitorSpecification: plan.anomaly.monitor.monitorSpecification,
|
|
127
|
+
monitorType: plan.anomaly.monitor.monitorType,
|
|
128
|
+
name: plan.anomaly.monitor.name,
|
|
129
|
+
tags,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
: undefined;
|
|
133
|
+
const anomalySubscription =
|
|
134
|
+
plan.anomaly && anomalyMonitor
|
|
135
|
+
? new aws.costexplorer.AnomalySubscription(
|
|
136
|
+
'${pascalName}CostAnomalySubscription',
|
|
137
|
+
{
|
|
138
|
+
frequency: plan.anomaly.subscription.frequency,
|
|
139
|
+
monitorArnLists: [anomalyMonitor.arn],
|
|
140
|
+
name: plan.anomaly.subscription.name,
|
|
141
|
+
subscribers: [
|
|
142
|
+
{ address: input.alertTopicArn, type: 'SNS' },
|
|
143
|
+
],
|
|
144
|
+
tags,
|
|
145
|
+
thresholdExpression:
|
|
146
|
+
plan.anomaly.subscription.thresholdExpression,
|
|
147
|
+
},
|
|
148
|
+
{ dependsOn: [anomalyMonitor] },
|
|
149
|
+
)
|
|
150
|
+
: undefined;
|
|
151
|
+
return {
|
|
152
|
+
accountBudgetAuthority: deployedAccountBudgetAuthority,
|
|
153
|
+
anomalyMonitor,
|
|
154
|
+
anomalySubscription,
|
|
155
|
+
materialDigest: plan.materialDigest,
|
|
156
|
+
pilotBudget,
|
|
157
|
+
policyDigest: plan.policyDigest,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
`,
|
|
161
|
+
'performance/policy.json': (0, built_in_plan_shared_js_1.json)({
|
|
162
|
+
$schema: '../../../node_modules/@empire-builder-kit/nx/schemas/performance-policy-v1.json',
|
|
163
|
+
authority: {
|
|
164
|
+
journeyAuthorityPath: 'docs/reliability/slo.json',
|
|
165
|
+
operationsAuthorityPath: 'operations/reliability.json',
|
|
166
|
+
releaseManifestPath: 'operations/release.json',
|
|
167
|
+
repositoryPolicyPath: '../../ebk.repository-policy.json',
|
|
168
|
+
},
|
|
169
|
+
budgets: [
|
|
170
|
+
{
|
|
171
|
+
comparator: 'maximum',
|
|
172
|
+
evidenceProvenance: ['test', 'actual'],
|
|
173
|
+
id: 'api-warm-p95',
|
|
174
|
+
journeyRef: 'journey:replace-with-api-journey',
|
|
175
|
+
metric: 'latency-milliseconds',
|
|
176
|
+
percentile: 95,
|
|
177
|
+
population: 'warm',
|
|
178
|
+
scenarioRef: 'scenario:api-peak',
|
|
179
|
+
status: 'example',
|
|
180
|
+
threshold: 750,
|
|
181
|
+
unit: 'milliseconds',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
comparator: 'maximum',
|
|
185
|
+
evidenceProvenance: ['test', 'actual'],
|
|
186
|
+
id: 'async-backlog-p99',
|
|
187
|
+
journeyRef: 'journey:replace-with-async-journey',
|
|
188
|
+
metric: 'backlog-age-seconds',
|
|
189
|
+
percentile: 99,
|
|
190
|
+
population: 'all',
|
|
191
|
+
scenarioRef: 'scenario:async-burst',
|
|
192
|
+
status: 'example',
|
|
193
|
+
threshold: 300,
|
|
194
|
+
unit: 'seconds',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
comparator: 'maximum',
|
|
198
|
+
evidenceProvenance: ['test', 'actual'],
|
|
199
|
+
id: 'database-cold-resume-p95',
|
|
200
|
+
journeyRef: 'journey:replace-with-database-journey',
|
|
201
|
+
metric: 'cold-resume-milliseconds',
|
|
202
|
+
percentile: 95,
|
|
203
|
+
population: 'cold',
|
|
204
|
+
scenarioRef: 'scenario:database-cold-resume',
|
|
205
|
+
status: 'example',
|
|
206
|
+
threshold: 15_000,
|
|
207
|
+
unit: 'milliseconds',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
comparator: 'maximum',
|
|
211
|
+
evidenceProvenance: ['estimate', 'actual'],
|
|
212
|
+
id: 'monthly-cost',
|
|
213
|
+
journeyRef: 'journey:replace-with-business-outcome',
|
|
214
|
+
metric: 'monthly-cost-usd',
|
|
215
|
+
population: 'all',
|
|
216
|
+
scenarioRef: 'scenario:normal-month',
|
|
217
|
+
status: 'example',
|
|
218
|
+
threshold: 1_000,
|
|
219
|
+
unit: 'usd',
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
capacity: [
|
|
223
|
+
{
|
|
224
|
+
capacityUnit: 'requests-per-second',
|
|
225
|
+
configuredMaximum: null,
|
|
226
|
+
downstreamMaximum: null,
|
|
227
|
+
failureMode: 'api-throttles-or-overloads-database',
|
|
228
|
+
id: 'api-lambda',
|
|
229
|
+
kind: 'api-lambda',
|
|
230
|
+
protectionRef: 'control:api-lambda-concurrency',
|
|
231
|
+
quotaMaximum: null,
|
|
232
|
+
saturationSignalRef: 'runtime:nodejs-heap-utilization-percent',
|
|
233
|
+
status: 'requires-adopter-value',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
capacityUnit: 'messages-per-second',
|
|
237
|
+
configuredMaximum: null,
|
|
238
|
+
downstreamMaximum: null,
|
|
239
|
+
failureMode: 'backlog-age-grows-or-provider-throttles',
|
|
240
|
+
id: 'async-worker',
|
|
241
|
+
kind: 'async-worker',
|
|
242
|
+
protectionRef: 'control:async-worker-concurrency',
|
|
243
|
+
quotaMaximum: null,
|
|
244
|
+
saturationSignalRef: 'database:protected-async-backlog-utilization-percent',
|
|
245
|
+
status: 'requires-adopter-value',
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
capacityUnit: 'database-connections',
|
|
249
|
+
configuredMaximum: null,
|
|
250
|
+
downstreamMaximum: null,
|
|
251
|
+
failureMode: 'connection-or-capacity-exhaustion',
|
|
252
|
+
id: 'database',
|
|
253
|
+
kind: 'database',
|
|
254
|
+
protectionRef: 'control:database-connections',
|
|
255
|
+
quotaMaximum: null,
|
|
256
|
+
saturationSignalRef: 'database:connection-utilization-percent',
|
|
257
|
+
status: 'requires-adopter-value',
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
capacityUnit: 'concurrent-requests',
|
|
261
|
+
configuredMaximum: null,
|
|
262
|
+
downstreamMaximum: null,
|
|
263
|
+
failureMode: 'service-saturates-or-overloads-dependency',
|
|
264
|
+
id: 'service-extension',
|
|
265
|
+
kind: 'service',
|
|
266
|
+
protectionRef: 'control:service-extension-concurrency',
|
|
267
|
+
quotaMaximum: null,
|
|
268
|
+
saturationSignalRef: 'adopter:service-saturation-percent',
|
|
269
|
+
status: 'requires-adopter-value',
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
cost: {
|
|
273
|
+
accountMonthlyMaximumUsd: 1_000,
|
|
274
|
+
actualNotificationThresholdPercent: 80,
|
|
275
|
+
anomalyRouteRef: 'alarm:cost-anomaly',
|
|
276
|
+
anomalyThresholdUsd: 50,
|
|
277
|
+
forecastNotificationThresholdPercent: 100,
|
|
278
|
+
notificationRoute: 'foundation-alert-topic',
|
|
279
|
+
pilotMonthlyMaximumUsd: 250,
|
|
280
|
+
sharedCostAllocationRef: 'performance/cost-allocation.json',
|
|
281
|
+
status: 'example',
|
|
282
|
+
testRunMaximumUsd: 25,
|
|
283
|
+
unitCostRefs: ['completed-business-journey', 'delivered-async-work'],
|
|
284
|
+
},
|
|
285
|
+
evidence: {
|
|
286
|
+
maximumAgeDays: { actual: 45, estimate: 30, test: 14 },
|
|
287
|
+
protectedEvidenceRequiresExactRelease: true,
|
|
288
|
+
},
|
|
289
|
+
kind: 'ebk-performance-policy',
|
|
290
|
+
load: {
|
|
291
|
+
allowedProtectedStages: ['staging'],
|
|
292
|
+
maximumConcurrency: 100,
|
|
293
|
+
maximumDurationSeconds: 1_800,
|
|
294
|
+
maximumEstimatedCostUsd: 25,
|
|
295
|
+
maximumRatePerSecond: 200,
|
|
296
|
+
maximumRequests: 100_000,
|
|
297
|
+
productionExceptionalOnly: true,
|
|
298
|
+
},
|
|
299
|
+
owner: 'REPLACE_WITH_OWNER',
|
|
300
|
+
reviewAt: null,
|
|
301
|
+
schemaVersion: 1,
|
|
302
|
+
slice: name,
|
|
303
|
+
tags: {
|
|
304
|
+
forbiddenKeys: ['ebk:feature'],
|
|
305
|
+
requiredKeys: [
|
|
306
|
+
'ebk:cost-allocation',
|
|
307
|
+
'ebk:cost-scope',
|
|
308
|
+
'ebk:framework-version',
|
|
309
|
+
'ebk:managed-by',
|
|
310
|
+
'ebk:owner',
|
|
311
|
+
'ebk:production-profile',
|
|
312
|
+
'ebk:repository',
|
|
313
|
+
'ebk:slice',
|
|
314
|
+
'ebk:stage',
|
|
315
|
+
'ebk:workspace',
|
|
316
|
+
],
|
|
317
|
+
unsupportedResourcePolicy: 'report-with-reason-and-allocation',
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
'performance/load-scenarios.json': (0, built_in_plan_shared_js_1.json)({
|
|
321
|
+
kind: 'ebk-protected-load-scenario-catalog',
|
|
322
|
+
scenarios: [
|
|
323
|
+
{
|
|
324
|
+
adapterStatus: 'managed',
|
|
325
|
+
adapterId: 'ebk-api-lambda-v1',
|
|
326
|
+
demandShape: 'normal-to-peak-ramp',
|
|
327
|
+
disabledUntilAdopted: true,
|
|
328
|
+
estimatedCostUsdPerRequest: 0.00001,
|
|
329
|
+
id: 'api-peak',
|
|
330
|
+
saturationSignalRef: 'runtime:nodejs-heap-utilization-percent',
|
|
331
|
+
workload: 'api-lambda',
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
adapterStatus: 'managed',
|
|
335
|
+
adapterId: 'ebk-async-worker-v1',
|
|
336
|
+
demandShape: 'bounded-flash-burst-and-recovery',
|
|
337
|
+
disabledUntilAdopted: true,
|
|
338
|
+
estimatedCostUsdPerRequest: 0.00002,
|
|
339
|
+
id: 'async-burst',
|
|
340
|
+
saturationSignalRef: 'database:protected-async-backlog-utilization-percent',
|
|
341
|
+
workload: 'async-worker',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
adapterStatus: 'managed',
|
|
345
|
+
adapterId: 'ebk-database-v1',
|
|
346
|
+
demandShape: 'idle-window-then-first-request',
|
|
347
|
+
disabledUntilAdopted: true,
|
|
348
|
+
estimatedCostUsdPerRequest: 0.0001,
|
|
349
|
+
id: 'database-cold-resume',
|
|
350
|
+
saturationSignalRef: 'database:connection-utilization-percent',
|
|
351
|
+
workload: 'database',
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
adapterStatus: 'requires-adopter-service-endpoint',
|
|
355
|
+
adapterId: 'ebk-service-v1',
|
|
356
|
+
demandShape: 'bounded-steady-and-saturation-ramp',
|
|
357
|
+
disabledUntilAdopted: true,
|
|
358
|
+
estimatedCostUsdPerRequest: 0.00002,
|
|
359
|
+
id: 'service-extension',
|
|
360
|
+
saturationSignalRef: 'adopter:service-saturation-percent',
|
|
361
|
+
workload: 'service',
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
schemaVersion: 1,
|
|
365
|
+
slice: name,
|
|
366
|
+
}),
|
|
367
|
+
'performance/cost-allocation.json': (0, built_in_plan_shared_js_1.json)({
|
|
368
|
+
allocations: [
|
|
369
|
+
{
|
|
370
|
+
capabilityId: 'completed-business-journey',
|
|
371
|
+
driver: 'replace-with-reviewed-resource-allocation',
|
|
372
|
+
id: 'completed-business-journey',
|
|
373
|
+
status: 'requires-adopter-value',
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
capabilityId: 'delivered-async-work',
|
|
377
|
+
driver: 'replace-with-reviewed-resource-allocation',
|
|
378
|
+
id: 'delivered-async-work',
|
|
379
|
+
status: 'requires-adopter-value',
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
actualCostProvider: 'aws-billing-actual',
|
|
383
|
+
forecastProviders: ['infrastructure-plan', 'aws-price-estimate'],
|
|
384
|
+
kind: 'ebk-cost-allocation-contract',
|
|
385
|
+
provenanceLabels: ['estimate', 'test', 'actual'],
|
|
386
|
+
resourceTagKey: 'ebk:cost-allocation',
|
|
387
|
+
schemaVersion: 1,
|
|
388
|
+
sharedCostsRemainExplicit: true,
|
|
389
|
+
slice: name,
|
|
390
|
+
unitCosts: [
|
|
391
|
+
{
|
|
392
|
+
aggregateCountRef: 'operations:completed-business-journey',
|
|
393
|
+
anomalyIncreasePercent: 20,
|
|
394
|
+
id: 'completed-business-journey',
|
|
395
|
+
ownerRouteRef: 'alarm:cost-anomaly',
|
|
396
|
+
status: 'requires-adopter-value',
|
|
397
|
+
unit: 'completed-business-journey',
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
aggregateCountRef: 'operations:delivered-async-work',
|
|
401
|
+
anomalyIncreasePercent: 20,
|
|
402
|
+
id: 'delivered-async-work',
|
|
403
|
+
ownerRouteRef: 'alarm:cost-anomaly',
|
|
404
|
+
status: 'requires-adopter-value',
|
|
405
|
+
unit: 'delivered-async-work',
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
}),
|
|
409
|
+
'performance/cost-collection.json': (0, built_in_plan_shared_js_1.json)((0, index_js_1.createExternalCostCollectionContract)({
|
|
410
|
+
outputPath: '.ebk/evidence/performance-cost-actual.json',
|
|
411
|
+
owner: `${name}-product`,
|
|
412
|
+
slice: name,
|
|
413
|
+
})),
|
|
414
|
+
'operations/performance.json': (0, built_in_plan_shared_js_1.json)({
|
|
415
|
+
authorityHooks: {
|
|
416
|
+
alarmAndRouting: 'operations/reliability.json',
|
|
417
|
+
journeyAndSlo: 'docs/reliability/slo.json',
|
|
418
|
+
release: 'operations/release.json',
|
|
419
|
+
repositoryProfile: '../../ebk.repository-policy.json',
|
|
420
|
+
},
|
|
421
|
+
costCollection: {
|
|
422
|
+
contract: 'performance/cost-collection.json',
|
|
423
|
+
liveCollectorOwnerRequired: true,
|
|
424
|
+
operationsConsoleRequired: false,
|
|
425
|
+
},
|
|
426
|
+
costControls: {
|
|
427
|
+
accountBudgetAuthority: 'packages/foundation/performance/account-cost-policy.json',
|
|
428
|
+
accountBudgetBinding: 'foundation-discovery-schema-v6-policy-digest-and-amount',
|
|
429
|
+
accountBudgetInfrastructure: 'packages/foundation/infra/foundation.ts',
|
|
430
|
+
accountBudgetOwner: 'foundation',
|
|
431
|
+
accountBudgetStages: ['staging', 'production'],
|
|
432
|
+
anomalyMonitorScope: 'workspace-slice-stage-composite-tag',
|
|
433
|
+
anomalySubscriptionFrequency: 'IMMEDIATE',
|
|
434
|
+
automaticSpendActions: 'forbidden',
|
|
435
|
+
infrastructure: 'infra/performance-cost-controls.ts',
|
|
436
|
+
owner: 'performance/policy.json#owner',
|
|
437
|
+
pilotBudgetOwner: 'product-slice',
|
|
438
|
+
pilotBudgetStage: 'staging',
|
|
439
|
+
policy: 'performance/policy.json#cost',
|
|
440
|
+
route: 'foundation-alert-topic',
|
|
441
|
+
status: 'blocked-until-cost-policy-adopted',
|
|
442
|
+
},
|
|
443
|
+
unitCost: {
|
|
444
|
+
capabilities: ['completed-business-journey', 'delivered-async-work'],
|
|
445
|
+
evaluator: 'scripts/performance-unit-cost.mjs',
|
|
446
|
+
liveBillingAndAggregateEvidenceRequired: true,
|
|
447
|
+
operationsConsoleRequired: false,
|
|
448
|
+
status: 'incomplete-until-live-evidence',
|
|
449
|
+
},
|
|
450
|
+
evidence: {
|
|
451
|
+
exactReleaseRequiredForProtectedResults: true,
|
|
452
|
+
provenanceLabels: ['estimate', 'test', 'actual'],
|
|
453
|
+
stagingIsNotProduction: true,
|
|
454
|
+
},
|
|
455
|
+
liveEvidence: {
|
|
456
|
+
accountBudgetAndAnomalyRoute: 'required-pre-v1',
|
|
457
|
+
actualBillingAndTwoUnitCosts: 'required-pre-v1',
|
|
458
|
+
coldAndWarmLambdaNextPaths: 'required-pre-v1',
|
|
459
|
+
databaseColdResumeAndIdleCost: 'required-pre-v1',
|
|
460
|
+
profileTransitionRehearsal: 'required-pre-v1',
|
|
461
|
+
protectedApiAsyncDatabaseLoad: 'required-pre-v1',
|
|
462
|
+
serviceRuntimeLoad: 'required-pre-v1-adopter-integration',
|
|
463
|
+
status: 'incomplete',
|
|
464
|
+
},
|
|
465
|
+
load: {
|
|
466
|
+
allowlistedScenarioAdapters: [
|
|
467
|
+
'ebk-api-lambda-v1',
|
|
468
|
+
'ebk-async-worker-v1',
|
|
469
|
+
'ebk-database-v1',
|
|
470
|
+
],
|
|
471
|
+
localProtectedMutationForbidden: true,
|
|
472
|
+
productionExceptionalOnly: true,
|
|
473
|
+
protectedGuard: 'scripts/performance-load-guard.mjs',
|
|
474
|
+
protectedRunner: '.github/workflows/performance-load.yml',
|
|
475
|
+
runnerStatus: 'managed-fixed-scenario-v1',
|
|
476
|
+
serviceAdapterOwner: null,
|
|
477
|
+
serviceAdapterStatus: 'requires-adopter-service-runtime-endpoint',
|
|
478
|
+
},
|
|
479
|
+
metricDimensions: [
|
|
480
|
+
'Stage',
|
|
481
|
+
'Slice',
|
|
482
|
+
'Feature',
|
|
483
|
+
'Component',
|
|
484
|
+
'Operation',
|
|
485
|
+
'Outcome',
|
|
486
|
+
],
|
|
487
|
+
prohibitedMetricDimensions: [
|
|
488
|
+
'release',
|
|
489
|
+
'user',
|
|
490
|
+
'tenant',
|
|
491
|
+
'request',
|
|
492
|
+
'work-item',
|
|
493
|
+
'payload',
|
|
494
|
+
'raw-error',
|
|
495
|
+
],
|
|
496
|
+
schemaVersion: 1,
|
|
497
|
+
slice: name,
|
|
498
|
+
workloads: [
|
|
499
|
+
'api-lambda',
|
|
500
|
+
'async-worker',
|
|
501
|
+
'browser',
|
|
502
|
+
'database',
|
|
503
|
+
'job',
|
|
504
|
+
'service',
|
|
505
|
+
],
|
|
506
|
+
}),
|
|
507
|
+
'scripts/performance-policy-check.mjs': `import { existsSync, readFileSync } from 'node:fs';
|
|
508
|
+
import {
|
|
509
|
+
assessPerformancePolicyReadiness,
|
|
510
|
+
evaluateCapacitySafety,
|
|
511
|
+
unitCostLiveEvidenceStatus,
|
|
512
|
+
validatePerformancePolicy,
|
|
513
|
+
verifyPerformanceOperationalHooks,
|
|
514
|
+
} from '@empire-builder-kit/nx/performance';
|
|
515
|
+
|
|
516
|
+
const policy = validatePerformancePolicy(
|
|
517
|
+
JSON.parse(
|
|
518
|
+
readFileSync(
|
|
519
|
+
new URL('../performance/policy.json', import.meta.url),
|
|
520
|
+
'utf8',
|
|
521
|
+
),
|
|
522
|
+
),
|
|
523
|
+
);
|
|
524
|
+
const requireReady = process.argv.includes('--require-ready');
|
|
525
|
+
const readAuthority = (authorityPath) => {
|
|
526
|
+
const [path] = authorityPath.split('#', 1);
|
|
527
|
+
const url = new URL('../' + path, import.meta.url);
|
|
528
|
+
return existsSync(url) ? JSON.parse(readFileSync(url, 'utf8')) : undefined;
|
|
529
|
+
};
|
|
530
|
+
for (const path of Object.values(policy.authority)) {
|
|
531
|
+
if (!readAuthority(path)) {
|
|
532
|
+
throw new Error('Performance authority is missing: ' + path);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (requireReady) {
|
|
536
|
+
await verifyPerformanceOperationalHooks(
|
|
537
|
+
policy,
|
|
538
|
+
(kind, authorityPath, semanticRef) => {
|
|
539
|
+
const authority = readAuthority(authorityPath);
|
|
540
|
+
if (!authority) return false;
|
|
541
|
+
if (kind === 'journey') {
|
|
542
|
+
return (
|
|
543
|
+
typeof semanticRef === 'string' &&
|
|
544
|
+
Array.isArray(authority.objectives) &&
|
|
545
|
+
authority.objectives.some(
|
|
546
|
+
(objective) => 'journey:' + objective?.journey === semanticRef,
|
|
547
|
+
)
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if (kind === 'alarm') {
|
|
551
|
+
return (
|
|
552
|
+
typeof semanticRef === 'string' &&
|
|
553
|
+
authority.performanceReferences?.alarms?.includes(semanticRef)
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
if (kind === 'capacity-control') {
|
|
557
|
+
return (
|
|
558
|
+
typeof semanticRef === 'string' &&
|
|
559
|
+
authority.performanceReferences?.capacityControls?.includes(
|
|
560
|
+
semanticRef,
|
|
561
|
+
)
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
if (kind === 'capacity-signal') {
|
|
565
|
+
return (
|
|
566
|
+
typeof semanticRef === 'string' &&
|
|
567
|
+
authority.performanceReferences?.capacitySignals?.includes(
|
|
568
|
+
semanticRef,
|
|
569
|
+
)
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return true;
|
|
573
|
+
},
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
const readiness = assessPerformancePolicyReadiness(policy);
|
|
577
|
+
const capacity = evaluateCapacitySafety(policy.capacity);
|
|
578
|
+
const result = {
|
|
579
|
+
capacity,
|
|
580
|
+
readiness,
|
|
581
|
+
schemaVersion: 1,
|
|
582
|
+
slice: policy.slice,
|
|
583
|
+
unitCost: unitCostLiveEvidenceStatus(policy),
|
|
584
|
+
};
|
|
585
|
+
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
586
|
+
if (
|
|
587
|
+
requireReady &&
|
|
588
|
+
(!readiness.ready || capacity.status !== 'safe')
|
|
589
|
+
) process.exitCode = 2;
|
|
590
|
+
`,
|
|
591
|
+
'scripts/performance-budget-check.mjs': `import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
592
|
+
import { fileURLToPath } from 'node:url';
|
|
593
|
+
import {
|
|
594
|
+
collectManagedPrArtifactBudget,
|
|
595
|
+
} from '@empire-builder-kit/nx/performance';
|
|
596
|
+
|
|
597
|
+
const result = collectManagedPrArtifactBudget({
|
|
598
|
+
slice: '${name}',
|
|
599
|
+
sliceRoot: fileURLToPath(new URL('../', import.meta.url)),
|
|
600
|
+
});
|
|
601
|
+
const evidenceDirectory = new URL('../.ebk/evidence/', import.meta.url);
|
|
602
|
+
mkdirSync(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
603
|
+
chmodSync(evidenceDirectory, 0o700);
|
|
604
|
+
const evidencePath = new URL(
|
|
605
|
+
'performance-budget.json',
|
|
606
|
+
evidenceDirectory,
|
|
607
|
+
);
|
|
608
|
+
writeFileSync(evidencePath, JSON.stringify(result, null, 2) + '\\n', {
|
|
609
|
+
encoding: 'utf8',
|
|
610
|
+
mode: 0o600,
|
|
611
|
+
});
|
|
612
|
+
chmodSync(evidencePath, 0o600);
|
|
613
|
+
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
614
|
+
if (result.outcome !== 'passed') process.exitCode = 2;
|
|
615
|
+
`,
|
|
616
|
+
'scripts/performance-evidence-evaluate.mjs': `import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
617
|
+
import {
|
|
618
|
+
evaluatePerformanceBudgets,
|
|
619
|
+
validatePerformancePolicy,
|
|
620
|
+
} from '@empire-builder-kit/nx/performance';
|
|
621
|
+
|
|
622
|
+
const observationsPath = process.env.EBK_PERFORMANCE_OBSERVATIONS_PATH?.trim();
|
|
623
|
+
const releasePath = process.env.EBK_RELEASE_BINDING_PATH?.trim();
|
|
624
|
+
const now = process.env.EBK_EVIDENCE_NOW?.trim();
|
|
625
|
+
if (!observationsPath || !releasePath || !now) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
'Adopted performance evidence requires exact observation, release-binding, and evidence-time paths.',
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
const policy = validatePerformancePolicy(
|
|
631
|
+
JSON.parse(
|
|
632
|
+
readFileSync(
|
|
633
|
+
new URL('../performance/policy.json', import.meta.url),
|
|
634
|
+
'utf8',
|
|
635
|
+
),
|
|
636
|
+
),
|
|
637
|
+
);
|
|
638
|
+
const result = evaluatePerformanceBudgets({
|
|
639
|
+
expectedRelease: JSON.parse(readFileSync(releasePath, 'utf8')),
|
|
640
|
+
now,
|
|
641
|
+
observations: JSON.parse(readFileSync(observationsPath, 'utf8')),
|
|
642
|
+
policy,
|
|
643
|
+
});
|
|
644
|
+
const evidenceDirectory = new URL('../.ebk/evidence/', import.meta.url);
|
|
645
|
+
mkdirSync(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
646
|
+
chmodSync(evidenceDirectory, 0o700);
|
|
647
|
+
const evidencePath = new URL(
|
|
648
|
+
'performance-adopted-budget.json',
|
|
649
|
+
evidenceDirectory,
|
|
650
|
+
);
|
|
651
|
+
writeFileSync(evidencePath, JSON.stringify(result, null, 2) + '\\n', {
|
|
652
|
+
encoding: 'utf8',
|
|
653
|
+
mode: 0o600,
|
|
654
|
+
});
|
|
655
|
+
chmodSync(evidencePath, 0o600);
|
|
656
|
+
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
657
|
+
if (result.outcome !== 'passed') process.exitCode = 2;
|
|
658
|
+
`,
|
|
659
|
+
'scripts/performance-unit-cost.mjs': `import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
660
|
+
import {
|
|
661
|
+
evaluateUnitCostTrends,
|
|
662
|
+
validateAwsBillingActualEvidence,
|
|
663
|
+
validateCostAllocationContract,
|
|
664
|
+
validatePerformancePolicy,
|
|
665
|
+
} from '@empire-builder-kit/nx/performance';
|
|
666
|
+
|
|
667
|
+
const baselinePath = process.env.EBK_UNIT_COST_BASELINE_PATH?.trim();
|
|
668
|
+
const baselineBillingPath =
|
|
669
|
+
process.env.EBK_BASELINE_BILLING_EVIDENCE_PATH?.trim();
|
|
670
|
+
const expectedBaselineBillingDigest =
|
|
671
|
+
process.env.EBK_EXPECTED_BASELINE_BILLING_EVIDENCE_DIGEST?.trim();
|
|
672
|
+
const baselineReleasePath =
|
|
673
|
+
process.env.EBK_BASELINE_RELEASE_BINDING_PATH?.trim();
|
|
674
|
+
const billingPath = process.env.EBK_BILLING_OBSERVATION_PATH?.trim();
|
|
675
|
+
const expectedBillingDigest =
|
|
676
|
+
process.env.EBK_EXPECTED_BILLING_EVIDENCE_DIGEST?.trim();
|
|
677
|
+
const currentPath = process.env.EBK_UNIT_COST_CURRENT_PATH?.trim();
|
|
678
|
+
const releasePath = process.env.EBK_RELEASE_BINDING_PATH?.trim();
|
|
679
|
+
const evaluatedAt = process.env.EBK_EVIDENCE_NOW?.trim();
|
|
680
|
+
if (
|
|
681
|
+
!baselinePath ||
|
|
682
|
+
!baselineBillingPath ||
|
|
683
|
+
!expectedBaselineBillingDigest ||
|
|
684
|
+
!baselineReleasePath ||
|
|
685
|
+
!billingPath ||
|
|
686
|
+
!expectedBillingDigest ||
|
|
687
|
+
!currentPath ||
|
|
688
|
+
!releasePath ||
|
|
689
|
+
!evaluatedAt
|
|
690
|
+
) {
|
|
691
|
+
throw new Error(
|
|
692
|
+
'Unit-cost evaluation requires independently expected baseline/current billing digests, exact observations, release bindings, and evidence time.',
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
const allocation = validateCostAllocationContract(
|
|
696
|
+
JSON.parse(
|
|
697
|
+
readFileSync(
|
|
698
|
+
new URL('../performance/cost-allocation.json', import.meta.url),
|
|
699
|
+
'utf8',
|
|
700
|
+
),
|
|
701
|
+
),
|
|
702
|
+
{ requireAdopted: true },
|
|
703
|
+
);
|
|
704
|
+
const capabilities = allocation.unitCosts.map(
|
|
705
|
+
({ anomalyIncreasePercent, id, ownerRouteRef, unit }) => ({
|
|
706
|
+
anomalyIncreasePercent,
|
|
707
|
+
id,
|
|
708
|
+
ownerRouteRef,
|
|
709
|
+
unit,
|
|
710
|
+
}),
|
|
711
|
+
);
|
|
712
|
+
const baselineBilling = validateAwsBillingActualEvidence(
|
|
713
|
+
JSON.parse(readFileSync(baselineBillingPath, 'utf8')),
|
|
714
|
+
);
|
|
715
|
+
const billing = validateAwsBillingActualEvidence(
|
|
716
|
+
JSON.parse(readFileSync(billingPath, 'utf8')),
|
|
717
|
+
);
|
|
718
|
+
const policy = validatePerformancePolicy(
|
|
719
|
+
JSON.parse(
|
|
720
|
+
readFileSync(
|
|
721
|
+
new URL('../performance/policy.json', import.meta.url),
|
|
722
|
+
'utf8',
|
|
723
|
+
),
|
|
724
|
+
),
|
|
725
|
+
);
|
|
726
|
+
const result = evaluateUnitCostTrends({
|
|
727
|
+
baseline: JSON.parse(readFileSync(baselinePath, 'utf8')),
|
|
728
|
+
baselineBillingEvidence: baselineBilling,
|
|
729
|
+
capabilities,
|
|
730
|
+
current: JSON.parse(readFileSync(currentPath, 'utf8')),
|
|
731
|
+
currentBillingEvidence: billing,
|
|
732
|
+
evaluatedAt,
|
|
733
|
+
expectedBaselineBillingEvidenceDigest: expectedBaselineBillingDigest,
|
|
734
|
+
expectedBaselineRelease: JSON.parse(
|
|
735
|
+
readFileSync(baselineReleasePath, 'utf8'),
|
|
736
|
+
),
|
|
737
|
+
expectedCurrentBillingEvidenceDigest: expectedBillingDigest,
|
|
738
|
+
expectedCurrentRelease: JSON.parse(readFileSync(releasePath, 'utf8')),
|
|
739
|
+
policy,
|
|
740
|
+
});
|
|
741
|
+
const evidenceDirectory = new URL('../.ebk/evidence/', import.meta.url);
|
|
742
|
+
mkdirSync(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
743
|
+
chmodSync(evidenceDirectory, 0o700);
|
|
744
|
+
const evidencePath = new URL('performance-unit-cost.json', evidenceDirectory);
|
|
745
|
+
writeFileSync(evidencePath, JSON.stringify(result, null, 2) + '\\n', {
|
|
746
|
+
encoding: 'utf8',
|
|
747
|
+
mode: 0o600,
|
|
748
|
+
});
|
|
749
|
+
chmodSync(evidencePath, 0o600);
|
|
750
|
+
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
751
|
+
`,
|
|
752
|
+
'performance/aggregate-count-provider.ts': `export interface ProtectedAggregateCountRequest {
|
|
753
|
+
authority: Readonly<{
|
|
754
|
+
mode: 'dedicated-oidc-read-role';
|
|
755
|
+
permissionContract: 'adopter-owned-least-privilege-read-v1';
|
|
756
|
+
}>;
|
|
757
|
+
capabilities: readonly string[];
|
|
758
|
+
environment: Readonly<{
|
|
759
|
+
accountFingerprint: string;
|
|
760
|
+
region: string;
|
|
761
|
+
slice: string;
|
|
762
|
+
stage: 'production' | 'staging';
|
|
763
|
+
workspace: string;
|
|
764
|
+
}>;
|
|
765
|
+
period: Readonly<{ end: string; start: string }>;
|
|
766
|
+
release: Readonly<{
|
|
767
|
+
candidateDigest: string;
|
|
768
|
+
releaseId: string;
|
|
769
|
+
releaseMaterialDigest: string;
|
|
770
|
+
sourceCommit: string;
|
|
771
|
+
}>;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export interface ProtectedAggregateCountResult {
|
|
775
|
+
counts: readonly Readonly<{
|
|
776
|
+
aggregateId: string;
|
|
777
|
+
capabilityId: string;
|
|
778
|
+
count: number;
|
|
779
|
+
sourceRef: string;
|
|
780
|
+
}>[];
|
|
781
|
+
requestId: string;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export async function collectAggregateCounts(
|
|
785
|
+
request: ProtectedAggregateCountRequest,
|
|
786
|
+
): Promise<ProtectedAggregateCountResult> {
|
|
787
|
+
void request;
|
|
788
|
+
throw new Error(
|
|
789
|
+
'Adopt performance/aggregate-count-provider.ts before protected aggregate collection.',
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
`,
|
|
793
|
+
'scripts/performance-aggregate-counts.mjs': `import {
|
|
794
|
+
chmodSync,
|
|
795
|
+
existsSync,
|
|
796
|
+
lstatSync,
|
|
797
|
+
mkdirSync,
|
|
798
|
+
readFileSync,
|
|
799
|
+
readdirSync,
|
|
800
|
+
realpathSync,
|
|
801
|
+
writeFileSync,
|
|
802
|
+
} from 'node:fs';
|
|
803
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
804
|
+
import { fileURLToPath } from 'node:url';
|
|
805
|
+
import {
|
|
806
|
+
awsAccountFingerprint,
|
|
807
|
+
createAggregateCountEvidence,
|
|
808
|
+
validateAggregateCountEvidence,
|
|
809
|
+
validateCostAllocationContract,
|
|
810
|
+
} from '@empire-builder-kit/nx/performance';
|
|
811
|
+
import { isBoundedSafeReference } from '@empire-builder-kit/runtime/safe-reference';
|
|
812
|
+
import { collectAggregateCounts } from '../performance/aggregate-count-provider.ts';
|
|
813
|
+
|
|
814
|
+
const sliceRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
815
|
+
const workspaceRoot = resolve(sliceRoot, '../..');
|
|
816
|
+
const required = (name) => {
|
|
817
|
+
const value = process.env[name]?.trim();
|
|
818
|
+
if (!value) throw new Error(name + ' is required.');
|
|
819
|
+
return value;
|
|
820
|
+
};
|
|
821
|
+
const authorityOnly =
|
|
822
|
+
process.argv.length === 3 && process.argv[2] === '--validate-authority';
|
|
823
|
+
if (process.argv.length > (authorityOnly ? 3 : 2)) {
|
|
824
|
+
throw new Error('Aggregate-count wrapper received unsupported arguments.');
|
|
825
|
+
}
|
|
826
|
+
const exactKeys = (value, keys, label) => {
|
|
827
|
+
if (
|
|
828
|
+
!value ||
|
|
829
|
+
typeof value !== 'object' ||
|
|
830
|
+
Array.isArray(value) ||
|
|
831
|
+
Object.keys(value).sort().join(',') !== [...keys].sort().join(',')
|
|
832
|
+
) {
|
|
833
|
+
throw new Error(label + ' contains unsupported fields.');
|
|
834
|
+
}
|
|
835
|
+
return value;
|
|
836
|
+
};
|
|
837
|
+
const readBoundedJson = (path, label) => {
|
|
838
|
+
const metadata = lstatSync(path);
|
|
839
|
+
if (
|
|
840
|
+
!metadata.isFile() ||
|
|
841
|
+
metadata.isSymbolicLink() ||
|
|
842
|
+
metadata.size < 2 ||
|
|
843
|
+
metadata.size > 1024 * 1024
|
|
844
|
+
) {
|
|
845
|
+
throw new Error(label + ' must be one bounded regular file.');
|
|
846
|
+
}
|
|
847
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
848
|
+
};
|
|
849
|
+
const releaseArtifactRoot = resolve(
|
|
850
|
+
sliceRoot,
|
|
851
|
+
'.ebk/performance-aggregate-input/release-artifact',
|
|
852
|
+
);
|
|
853
|
+
const artifactRootMetadata = lstatSync(releaseArtifactRoot);
|
|
854
|
+
const artifactRootRelative = relative(
|
|
855
|
+
sliceRoot,
|
|
856
|
+
realpathSync(releaseArtifactRoot),
|
|
857
|
+
);
|
|
858
|
+
if (
|
|
859
|
+
!artifactRootMetadata.isDirectory() ||
|
|
860
|
+
artifactRootMetadata.isSymbolicLink() ||
|
|
861
|
+
artifactRootRelative === '..' ||
|
|
862
|
+
artifactRootRelative.startsWith('..' + sep) ||
|
|
863
|
+
isAbsolute(artifactRootRelative)
|
|
864
|
+
) {
|
|
865
|
+
throw new Error('Release artifact root is unsafe.');
|
|
866
|
+
}
|
|
867
|
+
const releaseFiles = [];
|
|
868
|
+
let releaseBytes = 0;
|
|
869
|
+
const visit = (directory) => {
|
|
870
|
+
if (releaseFiles.length > 4096 || releaseBytes > 64 * 1024 * 1024) {
|
|
871
|
+
throw new Error('Release artifact exceeds its traversal bound.');
|
|
872
|
+
}
|
|
873
|
+
for (const name of readdirSync(directory).sort()) {
|
|
874
|
+
const path = resolve(directory, name);
|
|
875
|
+
const metadata = lstatSync(path);
|
|
876
|
+
if (metadata.isSymbolicLink()) {
|
|
877
|
+
throw new Error('Release artifact contains a symbolic link.');
|
|
878
|
+
}
|
|
879
|
+
if (metadata.isDirectory()) visit(path);
|
|
880
|
+
else if (metadata.isFile()) {
|
|
881
|
+
releaseBytes += metadata.size;
|
|
882
|
+
releaseFiles.push(path);
|
|
883
|
+
if (
|
|
884
|
+
releaseFiles.length > 4096 ||
|
|
885
|
+
releaseBytes > 64 * 1024 * 1024
|
|
886
|
+
) {
|
|
887
|
+
throw new Error('Release artifact exceeds its traversal bound.');
|
|
888
|
+
}
|
|
889
|
+
} else {
|
|
890
|
+
throw new Error('Release artifact contains a special file.');
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
visit(releaseArtifactRoot);
|
|
895
|
+
const bindingSuffix =
|
|
896
|
+
'/packages/${name}/.ebk/evidence/active-protected-release.json';
|
|
897
|
+
const bindingMatches = releaseFiles.filter((path) =>
|
|
898
|
+
('/' + relative(releaseArtifactRoot, path).replaceAll('\\\\', '/')).endsWith(
|
|
899
|
+
bindingSuffix,
|
|
900
|
+
),
|
|
901
|
+
);
|
|
902
|
+
if (bindingMatches.length !== 1) {
|
|
903
|
+
throw new Error(
|
|
904
|
+
'Release artifact lacks one exact ${name} active protected release binding.',
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
const source = exactKeys(
|
|
908
|
+
readBoundedJson(bindingMatches[0], 'Active protected release binding'),
|
|
909
|
+
[
|
|
910
|
+
'candidateDigest',
|
|
911
|
+
'deploymentCommit',
|
|
912
|
+
'kind',
|
|
913
|
+
'manifestDigest',
|
|
914
|
+
'releaseId',
|
|
915
|
+
'releaseMaterialDigest',
|
|
916
|
+
'releaseSourceCommit',
|
|
917
|
+
'runAttempt',
|
|
918
|
+
'runId',
|
|
919
|
+
'schemaVersion',
|
|
920
|
+
'stage',
|
|
921
|
+
'workflow',
|
|
922
|
+
],
|
|
923
|
+
'Active protected release binding',
|
|
924
|
+
);
|
|
925
|
+
const digestPattern = /^[a-f0-9]{64}$/;
|
|
926
|
+
const commitPattern = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
927
|
+
const stage = required('EBK_STAGE');
|
|
928
|
+
const sourceCommit = required('EBK_SOURCE_COMMIT');
|
|
929
|
+
if (
|
|
930
|
+
source.kind !== 'ebk-active-protected-release-binding' ||
|
|
931
|
+
source.schemaVersion !== 1 ||
|
|
932
|
+
source.stage !== stage ||
|
|
933
|
+
source.workflow !== 'Deploy' ||
|
|
934
|
+
String(source.runId) !== required('EBK_RELEASE_ARTIFACT_RUN_ID') ||
|
|
935
|
+
String(source.runAttempt) !== required('EBK_RELEASE_RUN_ATTEMPT') ||
|
|
936
|
+
source.deploymentCommit !== sourceCommit ||
|
|
937
|
+
!commitPattern.test(source.deploymentCommit ?? '') ||
|
|
938
|
+
!digestPattern.test(source.candidateDigest ?? '') ||
|
|
939
|
+
!digestPattern.test(source.manifestDigest ?? '') ||
|
|
940
|
+
!digestPattern.test(source.releaseMaterialDigest ?? '') ||
|
|
941
|
+
!commitPattern.test(source.releaseSourceCommit ?? '') ||
|
|
942
|
+
!isBoundedSafeReference(source.releaseId, 'performance-evidence')
|
|
943
|
+
) {
|
|
944
|
+
throw new Error(
|
|
945
|
+
'Active protected release binding does not match its exact Deploy authority.',
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
const release = {
|
|
949
|
+
candidateDigest: source.candidateDigest,
|
|
950
|
+
releaseId: source.releaseId,
|
|
951
|
+
releaseMaterialDigest: source.releaseMaterialDigest,
|
|
952
|
+
sourceCommit: source.deploymentCommit,
|
|
953
|
+
};
|
|
954
|
+
const workspaceManifest = readBoundedJson(
|
|
955
|
+
resolve(workspaceRoot, 'package.json'),
|
|
956
|
+
'Workspace manifest',
|
|
957
|
+
);
|
|
958
|
+
if (
|
|
959
|
+
!isBoundedSafeReference(
|
|
960
|
+
workspaceManifest.name,
|
|
961
|
+
'performance-evidence',
|
|
962
|
+
)
|
|
963
|
+
) {
|
|
964
|
+
throw new Error('Workspace manifest lacks one bounded name.');
|
|
965
|
+
}
|
|
966
|
+
const environment = {
|
|
967
|
+
accountFingerprint: awsAccountFingerprint(
|
|
968
|
+
required('AWS_EXPECTED_ACCOUNT_ID'),
|
|
969
|
+
),
|
|
970
|
+
region: required('AWS_REGION'),
|
|
971
|
+
slice: '${name}',
|
|
972
|
+
stage,
|
|
973
|
+
workspace: workspaceManifest.name,
|
|
974
|
+
};
|
|
975
|
+
const period = {
|
|
976
|
+
end: required('EBK_AGGREGATE_PERIOD_END'),
|
|
977
|
+
start: required('EBK_AGGREGATE_PERIOD_START'),
|
|
978
|
+
};
|
|
979
|
+
const collectionStartedAt = new Date();
|
|
980
|
+
const instantPattern = /^\\d{4}-\\d{2}-\\d{2}T00:00:00\\.000Z$/;
|
|
981
|
+
const durationDays =
|
|
982
|
+
(Date.parse(period.end) - Date.parse(period.start)) / 86_400_000;
|
|
983
|
+
if (
|
|
984
|
+
!instantPattern.test(period.start) ||
|
|
985
|
+
!instantPattern.test(period.end) ||
|
|
986
|
+
!Number.isSafeInteger(durationDays) ||
|
|
987
|
+
durationDays < 1 ||
|
|
988
|
+
durationDays > 370 ||
|
|
989
|
+
Date.parse(period.end) > collectionStartedAt.getTime() ||
|
|
990
|
+
!/^(?:af|ap|ca|cn|eu|il|me|mx|sa|us)(?:-gov|-iso|-isob)?-[a-z]+-\\d$/.test(
|
|
991
|
+
environment.region,
|
|
992
|
+
) ||
|
|
993
|
+
(stage !== 'staging' && stage !== 'production')
|
|
994
|
+
) {
|
|
995
|
+
throw new Error(
|
|
996
|
+
'Aggregate environment and closed billing period are invalid.',
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
const allocation = validateCostAllocationContract(
|
|
1000
|
+
readBoundedJson(
|
|
1001
|
+
resolve(sliceRoot, 'performance/cost-allocation.json'),
|
|
1002
|
+
'Cost allocation authority',
|
|
1003
|
+
),
|
|
1004
|
+
{ requireAdopted: true },
|
|
1005
|
+
);
|
|
1006
|
+
if (allocation.slice !== '${name}') {
|
|
1007
|
+
throw new Error('Cost allocation authority belongs to another Slice.');
|
|
1008
|
+
}
|
|
1009
|
+
const capabilities = allocation.allocations
|
|
1010
|
+
.map(({ capabilityId }) => capabilityId)
|
|
1011
|
+
.sort();
|
|
1012
|
+
if (authorityOnly) {
|
|
1013
|
+
process.stdout.write(
|
|
1014
|
+
JSON.stringify({
|
|
1015
|
+
kind: 'ebk-protected-aggregate-authority-result',
|
|
1016
|
+
outcome: 'passed',
|
|
1017
|
+
release,
|
|
1018
|
+
schemaVersion: 1,
|
|
1019
|
+
}) + '\\n',
|
|
1020
|
+
);
|
|
1021
|
+
process.exit(0);
|
|
1022
|
+
}
|
|
1023
|
+
if (
|
|
1024
|
+
required('EBK_AGGREGATE_ACTUAL_ACCOUNT_ID') !==
|
|
1025
|
+
required('AWS_EXPECTED_ACCOUNT_ID')
|
|
1026
|
+
) {
|
|
1027
|
+
throw new Error(
|
|
1028
|
+
'Aggregate-count provider lacks a verified protected account binding.',
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
const provider = exactKeys(
|
|
1032
|
+
await collectAggregateCounts({
|
|
1033
|
+
authority: {
|
|
1034
|
+
mode: 'dedicated-oidc-read-role',
|
|
1035
|
+
permissionContract: 'adopter-owned-least-privilege-read-v1',
|
|
1036
|
+
},
|
|
1037
|
+
capabilities,
|
|
1038
|
+
environment,
|
|
1039
|
+
period,
|
|
1040
|
+
release,
|
|
1041
|
+
}),
|
|
1042
|
+
['counts', 'requestId'],
|
|
1043
|
+
'Aggregate-count provider result',
|
|
1044
|
+
);
|
|
1045
|
+
if (!Array.isArray(provider.counts)) {
|
|
1046
|
+
throw new Error('Aggregate-count provider counts must be an array.');
|
|
1047
|
+
}
|
|
1048
|
+
const providerCapabilities = provider.counts
|
|
1049
|
+
.map(({ capabilityId }) => capabilityId)
|
|
1050
|
+
.sort();
|
|
1051
|
+
if (JSON.stringify(providerCapabilities) !== JSON.stringify(capabilities)) {
|
|
1052
|
+
throw new Error(
|
|
1053
|
+
'Aggregate-count provider must return the exact adopted capability set.',
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
const evidence = createAggregateCountEvidence({
|
|
1057
|
+
collectedAt: new Date().toISOString(),
|
|
1058
|
+
counts: provider.counts,
|
|
1059
|
+
environment,
|
|
1060
|
+
kind: 'ebk-aggregate-count-evidence',
|
|
1061
|
+
period,
|
|
1062
|
+
producer: {
|
|
1063
|
+
access: {
|
|
1064
|
+
mode: 'dedicated-oidc-read-role',
|
|
1065
|
+
permissionContract: 'adopter-owned-least-privilege-read-v1',
|
|
1066
|
+
},
|
|
1067
|
+
authority: 'adopter-protected-aggregate-v1',
|
|
1068
|
+
requestId: provider.requestId,
|
|
1069
|
+
},
|
|
1070
|
+
release,
|
|
1071
|
+
schemaVersion: 1,
|
|
1072
|
+
});
|
|
1073
|
+
const evidenceRoot = resolve(sliceRoot, '.ebk/evidence');
|
|
1074
|
+
for (const directory of [resolve(sliceRoot, '.ebk'), evidenceRoot]) {
|
|
1075
|
+
if (!existsSync(directory)) mkdirSync(directory, { mode: 0o700 });
|
|
1076
|
+
const metadata = lstatSync(directory);
|
|
1077
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
1078
|
+
throw new Error('Aggregate-count evidence directory is unsafe.');
|
|
1079
|
+
}
|
|
1080
|
+
chmodSync(directory, 0o700);
|
|
1081
|
+
}
|
|
1082
|
+
const evidenceRootRelative = relative(sliceRoot, realpathSync(evidenceRoot));
|
|
1083
|
+
if (
|
|
1084
|
+
evidenceRootRelative === '..' ||
|
|
1085
|
+
evidenceRootRelative.startsWith('..' + sep) ||
|
|
1086
|
+
isAbsolute(evidenceRootRelative)
|
|
1087
|
+
) {
|
|
1088
|
+
throw new Error('Aggregate-count evidence directory escaped its Slice.');
|
|
1089
|
+
}
|
|
1090
|
+
const evidencePath = resolve(evidenceRoot, 'aggregate-count-evidence.json');
|
|
1091
|
+
const evidenceBytes = Buffer.from(
|
|
1092
|
+
JSON.stringify(evidence, null, 2) + '\\n',
|
|
1093
|
+
'utf8',
|
|
1094
|
+
);
|
|
1095
|
+
if (evidenceBytes.length > 1024 * 1024) {
|
|
1096
|
+
throw new Error('Aggregate-count evidence exceeds its byte bound.');
|
|
1097
|
+
}
|
|
1098
|
+
writeFileSync(evidencePath, evidenceBytes, {
|
|
1099
|
+
flag: 'wx',
|
|
1100
|
+
mode: 0o600,
|
|
1101
|
+
});
|
|
1102
|
+
chmodSync(evidencePath, 0o600);
|
|
1103
|
+
const outputMetadata = lstatSync(evidencePath);
|
|
1104
|
+
if (
|
|
1105
|
+
!outputMetadata.isFile() ||
|
|
1106
|
+
outputMetadata.isSymbolicLink() ||
|
|
1107
|
+
outputMetadata.size !== evidenceBytes.length ||
|
|
1108
|
+
validateAggregateCountEvidence(
|
|
1109
|
+
JSON.parse(readFileSync(evidencePath, 'utf8')),
|
|
1110
|
+
).materialDigest !== evidence.materialDigest
|
|
1111
|
+
) {
|
|
1112
|
+
throw new Error('Aggregate-count evidence output did not remain canonical.');
|
|
1113
|
+
}
|
|
1114
|
+
process.stdout.write(
|
|
1115
|
+
JSON.stringify({
|
|
1116
|
+
kind: 'ebk-protected-aggregate-count-result',
|
|
1117
|
+
materialDigest: evidence.materialDigest,
|
|
1118
|
+
outcome: 'passed',
|
|
1119
|
+
schemaVersion: 1,
|
|
1120
|
+
}) + '\\n',
|
|
1121
|
+
);
|
|
1122
|
+
`,
|
|
1123
|
+
'scripts/performance-cost-collect.mjs': `import {
|
|
1124
|
+
CostExplorerClient,
|
|
1125
|
+
GetCostAndUsageCommand,
|
|
1126
|
+
ListCostAllocationTagsCommand,
|
|
1127
|
+
} from '@aws-sdk/client-cost-explorer';
|
|
1128
|
+
import {
|
|
1129
|
+
GetCallerIdentityCommand,
|
|
1130
|
+
STSClient,
|
|
1131
|
+
} from '@aws-sdk/client-sts';
|
|
1132
|
+
import { createHash } from 'node:crypto';
|
|
1133
|
+
import {
|
|
1134
|
+
chmodSync,
|
|
1135
|
+
lstatSync,
|
|
1136
|
+
mkdirSync,
|
|
1137
|
+
readFileSync,
|
|
1138
|
+
writeFileSync,
|
|
1139
|
+
} from 'node:fs';
|
|
1140
|
+
import {
|
|
1141
|
+
collectAwsBillingActualEvidence,
|
|
1142
|
+
validateAggregateCountEvidence,
|
|
1143
|
+
validateCostAllocationContract,
|
|
1144
|
+
} from '@empire-builder-kit/nx/performance';
|
|
1145
|
+
|
|
1146
|
+
const required = (name) => {
|
|
1147
|
+
const value = process.env[name]?.trim();
|
|
1148
|
+
if (!value) throw new Error(name + ' is required.');
|
|
1149
|
+
return value;
|
|
1150
|
+
};
|
|
1151
|
+
const digest = (bytes) =>
|
|
1152
|
+
createHash('sha256').update(bytes).digest('hex');
|
|
1153
|
+
const readBoundedJson = (path, expectedDigest, label) => {
|
|
1154
|
+
const metadata = lstatSync(path);
|
|
1155
|
+
if (
|
|
1156
|
+
!metadata.isFile() ||
|
|
1157
|
+
metadata.isSymbolicLink() ||
|
|
1158
|
+
metadata.size <= 0 ||
|
|
1159
|
+
metadata.size > 1024 * 1024
|
|
1160
|
+
) {
|
|
1161
|
+
throw new Error(label + ' must be one bounded regular file.');
|
|
1162
|
+
}
|
|
1163
|
+
const bytes = readFileSync(path);
|
|
1164
|
+
if (!/^[a-f0-9]{64}$/.test(expectedDigest) || digest(bytes) !== expectedDigest) {
|
|
1165
|
+
throw new Error(label + ' does not match its independently expected digest.');
|
|
1166
|
+
}
|
|
1167
|
+
return JSON.parse(bytes.toString('utf8'));
|
|
1168
|
+
};
|
|
1169
|
+
const aggregate = validateAggregateCountEvidence(
|
|
1170
|
+
readBoundedJson(
|
|
1171
|
+
required('EBK_AGGREGATE_COUNT_EVIDENCE_PATH'),
|
|
1172
|
+
required('EBK_EXPECTED_AGGREGATE_COUNT_EVIDENCE_DIGEST'),
|
|
1173
|
+
'Aggregate-count evidence',
|
|
1174
|
+
),
|
|
1175
|
+
);
|
|
1176
|
+
const release = readBoundedJson(
|
|
1177
|
+
required('EBK_RELEASE_BINDING_PATH'),
|
|
1178
|
+
required('EBK_EXPECTED_RELEASE_BINDING_DIGEST'),
|
|
1179
|
+
'Release binding',
|
|
1180
|
+
);
|
|
1181
|
+
if (JSON.stringify(release) !== JSON.stringify(aggregate.release)) {
|
|
1182
|
+
throw new Error('Aggregate counts and reviewed release binding differ.');
|
|
1183
|
+
}
|
|
1184
|
+
const environment = aggregate.environment;
|
|
1185
|
+
if (
|
|
1186
|
+
environment.slice !== '${name}' ||
|
|
1187
|
+
environment.stage !== required('EBK_STAGE') ||
|
|
1188
|
+
environment.region !== required('AWS_REGION') ||
|
|
1189
|
+
environment.workspace !== required('EBK_WORKSPACE') ||
|
|
1190
|
+
environment.accountFingerprint !== required('EBK_ACCOUNT_FINGERPRINT') ||
|
|
1191
|
+
release.sourceCommit !== required('EBK_SOURCE_COMMIT') ||
|
|
1192
|
+
aggregate.period.start !==
|
|
1193
|
+
required('EBK_EXPECTED_BILLING_PERIOD_START') ||
|
|
1194
|
+
aggregate.period.end !== required('EBK_EXPECTED_BILLING_PERIOD_END')
|
|
1195
|
+
) {
|
|
1196
|
+
throw new Error('Cost collection escaped its exact reviewed cohort.');
|
|
1197
|
+
}
|
|
1198
|
+
const allocation = validateCostAllocationContract(
|
|
1199
|
+
JSON.parse(
|
|
1200
|
+
readFileSync(
|
|
1201
|
+
new URL('../performance/cost-allocation.json', import.meta.url),
|
|
1202
|
+
'utf8',
|
|
1203
|
+
),
|
|
1204
|
+
),
|
|
1205
|
+
{ requireAdopted: true },
|
|
1206
|
+
);
|
|
1207
|
+
if (allocation.slice !== '${name}') {
|
|
1208
|
+
throw new Error('Cost allocation authority belongs to another Slice.');
|
|
1209
|
+
}
|
|
1210
|
+
const allocations = allocation.allocations.map(
|
|
1211
|
+
({ capabilityId, id }) => ({ allocationId: id, capabilityId }),
|
|
1212
|
+
);
|
|
1213
|
+
const sts = new STSClient({ region: environment.region });
|
|
1214
|
+
const costExplorer = new CostExplorerClient({ region: 'us-east-1' });
|
|
1215
|
+
const requiredCostAllocationTags = [
|
|
1216
|
+
'ebk:cost-allocation',
|
|
1217
|
+
'ebk:cost-scope',
|
|
1218
|
+
'ebk:slice',
|
|
1219
|
+
'ebk:stage',
|
|
1220
|
+
'ebk:workspace',
|
|
1221
|
+
];
|
|
1222
|
+
const costAllocationTagResponse = await costExplorer.send(
|
|
1223
|
+
new ListCostAllocationTagsCommand({
|
|
1224
|
+
MaxResults: 100,
|
|
1225
|
+
Status: 'Active',
|
|
1226
|
+
TagKeys: requiredCostAllocationTags,
|
|
1227
|
+
Type: 'UserDefined',
|
|
1228
|
+
}),
|
|
1229
|
+
);
|
|
1230
|
+
const activeCostAllocationTags = (costAllocationTagResponse.CostAllocationTags ?? [])
|
|
1231
|
+
.map(({ Status, TagKey, Type }) => ({ Status, TagKey, Type }))
|
|
1232
|
+
.sort((left, right) =>
|
|
1233
|
+
(left.TagKey ?? '') < (right.TagKey ?? '')
|
|
1234
|
+
? -1
|
|
1235
|
+
: (left.TagKey ?? '') > (right.TagKey ?? '')
|
|
1236
|
+
? 1
|
|
1237
|
+
: 0,
|
|
1238
|
+
);
|
|
1239
|
+
if (
|
|
1240
|
+
costAllocationTagResponse.NextToken ||
|
|
1241
|
+
!costAllocationTagResponse.$metadata.requestId ||
|
|
1242
|
+
JSON.stringify(activeCostAllocationTags) !==
|
|
1243
|
+
JSON.stringify(
|
|
1244
|
+
requiredCostAllocationTags.map((TagKey) => ({
|
|
1245
|
+
Status: 'Active',
|
|
1246
|
+
TagKey,
|
|
1247
|
+
Type: 'UserDefined',
|
|
1248
|
+
})),
|
|
1249
|
+
)
|
|
1250
|
+
) {
|
|
1251
|
+
throw new Error(
|
|
1252
|
+
'Required Product cost-allocation tags are not all ACTIVE in AWS.',
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
const client = {
|
|
1256
|
+
async getCallerIdentity() {
|
|
1257
|
+
const response = await sts.send(new GetCallerIdentityCommand({}));
|
|
1258
|
+
if (!response.Account || !response.$metadata.requestId) {
|
|
1259
|
+
throw new Error('AWS STS identity response is incomplete.');
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
accountId: response.Account,
|
|
1263
|
+
requestId: response.$metadata.requestId,
|
|
1264
|
+
};
|
|
1265
|
+
},
|
|
1266
|
+
async getCostAndUsage(request) {
|
|
1267
|
+
const filter = {
|
|
1268
|
+
And: request.filter.tags.map(({ key, value }) => ({
|
|
1269
|
+
Tags: {
|
|
1270
|
+
Key: key,
|
|
1271
|
+
MatchOptions: ['EQUALS'],
|
|
1272
|
+
Values: [value],
|
|
1273
|
+
},
|
|
1274
|
+
})),
|
|
1275
|
+
};
|
|
1276
|
+
const response = await costExplorer.send(
|
|
1277
|
+
new GetCostAndUsageCommand({
|
|
1278
|
+
Filter: filter,
|
|
1279
|
+
Granularity: request.granularity,
|
|
1280
|
+
GroupBy: request.groupBy
|
|
1281
|
+
? [{ Key: request.groupBy.key, Type: request.groupBy.type }]
|
|
1282
|
+
: undefined,
|
|
1283
|
+
Metrics: [request.metric],
|
|
1284
|
+
TimePeriod: {
|
|
1285
|
+
End: request.timePeriod.end,
|
|
1286
|
+
Start: request.timePeriod.start,
|
|
1287
|
+
},
|
|
1288
|
+
}),
|
|
1289
|
+
);
|
|
1290
|
+
if (response.NextPageToken || !response.$metadata.requestId) {
|
|
1291
|
+
throw new Error('AWS Cost Explorer result exceeded its exact page bound.');
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
requestId: response.$metadata.requestId,
|
|
1295
|
+
results: (response.ResultsByTime ?? []).map((result) => ({
|
|
1296
|
+
end: result.TimePeriod?.End ?? '',
|
|
1297
|
+
estimated: result.Estimated ?? true,
|
|
1298
|
+
groups: (result.Groups ?? []).map((group) => {
|
|
1299
|
+
const key = group.Keys?.[0] ?? '';
|
|
1300
|
+
const prefix = 'ebk:cost-allocation$';
|
|
1301
|
+
const metric = group.Metrics?.UnblendedCost;
|
|
1302
|
+
if (
|
|
1303
|
+
group.Keys?.length !== 1 ||
|
|
1304
|
+
!key.startsWith(prefix) ||
|
|
1305
|
+
!metric?.Amount
|
|
1306
|
+
) {
|
|
1307
|
+
throw new Error('AWS allocation row is incomplete or unallocated.');
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
allocationId: key.slice(prefix.length),
|
|
1311
|
+
amountUsd: metric.Amount,
|
|
1312
|
+
currency: metric.Unit === 'USD' ? 'USD' : metric.Unit,
|
|
1313
|
+
};
|
|
1314
|
+
}),
|
|
1315
|
+
start: result.TimePeriod?.Start ?? '',
|
|
1316
|
+
total: request.groupBy
|
|
1317
|
+
? null
|
|
1318
|
+
: {
|
|
1319
|
+
amountUsd: result.Total?.UnblendedCost?.Amount ?? '',
|
|
1320
|
+
currency:
|
|
1321
|
+
result.Total?.UnblendedCost?.Unit === 'USD'
|
|
1322
|
+
? 'USD'
|
|
1323
|
+
: result.Total?.UnblendedCost?.Unit,
|
|
1324
|
+
},
|
|
1325
|
+
})),
|
|
1326
|
+
};
|
|
1327
|
+
},
|
|
1328
|
+
};
|
|
1329
|
+
const report = await collectAwsBillingActualEvidence({
|
|
1330
|
+
aggregateEvidence: aggregate,
|
|
1331
|
+
allocations,
|
|
1332
|
+
client,
|
|
1333
|
+
collectedAt: new Date().toISOString(),
|
|
1334
|
+
collection: {
|
|
1335
|
+
aggregateArtifact: {
|
|
1336
|
+
name: required('EBK_AGGREGATE_ARTIFACT_NAME'),
|
|
1337
|
+
runId: required('EBK_AGGREGATE_ARTIFACT_RUN_ID'),
|
|
1338
|
+
sha256: required('EBK_EXPECTED_AGGREGATE_COUNT_EVIDENCE_DIGEST'),
|
|
1339
|
+
},
|
|
1340
|
+
releaseArtifact: {
|
|
1341
|
+
name: required('EBK_RELEASE_ARTIFACT_NAME'),
|
|
1342
|
+
runId: required('EBK_RELEASE_ARTIFACT_RUN_ID'),
|
|
1343
|
+
sha256: required('EBK_EXPECTED_RELEASE_BINDING_DIGEST'),
|
|
1344
|
+
},
|
|
1345
|
+
repository: required('GITHUB_REPOSITORY'),
|
|
1346
|
+
runAttempt: required('GITHUB_RUN_ATTEMPT'),
|
|
1347
|
+
runId: required('GITHUB_RUN_ID'),
|
|
1348
|
+
sourceCommit: release.sourceCommit,
|
|
1349
|
+
workflow: '.github/workflows/performance-cost-actual.yml',
|
|
1350
|
+
},
|
|
1351
|
+
environment,
|
|
1352
|
+
period: aggregate.period,
|
|
1353
|
+
release,
|
|
1354
|
+
});
|
|
1355
|
+
const evidenceDirectory = new URL('../.ebk/evidence/', import.meta.url);
|
|
1356
|
+
mkdirSync(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
1357
|
+
chmodSync(evidenceDirectory, 0o700);
|
|
1358
|
+
const evidencePath = new URL(
|
|
1359
|
+
'performance-cost-actual.json',
|
|
1360
|
+
evidenceDirectory,
|
|
1361
|
+
);
|
|
1362
|
+
writeFileSync(evidencePath, JSON.stringify(report, null, 2) + '\\n', {
|
|
1363
|
+
encoding: 'utf8',
|
|
1364
|
+
flag: 'wx',
|
|
1365
|
+
mode: 0o600,
|
|
1366
|
+
});
|
|
1367
|
+
chmodSync(evidencePath, 0o600);
|
|
1368
|
+
process.stdout.write(JSON.stringify(report) + '\\n');
|
|
1369
|
+
`,
|
|
1370
|
+
'scripts/performance-load-guard.mjs': `import {
|
|
1371
|
+
lstatSync,
|
|
1372
|
+
readFileSync,
|
|
1373
|
+
realpathSync,
|
|
1374
|
+
} from 'node:fs';
|
|
1375
|
+
import { fileURLToPath } from 'node:url';
|
|
1376
|
+
import { relative, resolve, sep } from 'node:path';
|
|
1377
|
+
import {
|
|
1378
|
+
validateProtectedLoadAuthorization,
|
|
1379
|
+
validatePerformancePolicy,
|
|
1380
|
+
} from '@empire-builder-kit/nx/performance';
|
|
1381
|
+
|
|
1382
|
+
const requestedPlanPath =
|
|
1383
|
+
process.env.EBK_PROTECTED_LOAD_PLAN_PATH?.trim();
|
|
1384
|
+
const requestedApprovalPath =
|
|
1385
|
+
process.env.EBK_PROTECTED_LOAD_APPROVAL_PATH?.trim();
|
|
1386
|
+
const currentSourceCommit = process.env.EBK_SOURCE_COMMIT?.trim();
|
|
1387
|
+
const expectedAccountFingerprint =
|
|
1388
|
+
process.env.EBK_ACCOUNT_FINGERPRINT?.trim();
|
|
1389
|
+
const expectedEndpoint = process.env.EBK_LOAD_ENDPOINT?.trim();
|
|
1390
|
+
const expectedRegion = process.env.AWS_REGION?.trim();
|
|
1391
|
+
const now = process.env.EBK_EVIDENCE_NOW?.trim();
|
|
1392
|
+
if (
|
|
1393
|
+
!requestedPlanPath ||
|
|
1394
|
+
!requestedApprovalPath ||
|
|
1395
|
+
!currentSourceCommit ||
|
|
1396
|
+
!expectedAccountFingerprint ||
|
|
1397
|
+
!expectedEndpoint ||
|
|
1398
|
+
!expectedRegion ||
|
|
1399
|
+
!now
|
|
1400
|
+
) {
|
|
1401
|
+
throw new Error(
|
|
1402
|
+
'Protected load guard requires exact plan, approval, source, target, Region, and time inputs.',
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const sliceRoot = realpathSync(
|
|
1407
|
+
fileURLToPath(new URL('../', import.meta.url)),
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
function managedInput(requestedPath, directory, label) {
|
|
1411
|
+
const root = resolve(sliceRoot, '.ebk/performance-load', directory);
|
|
1412
|
+
const rootChild = relative(sliceRoot, root);
|
|
1413
|
+
let cursor = sliceRoot;
|
|
1414
|
+
for (const component of rootChild.split(sep).filter(Boolean)) {
|
|
1415
|
+
cursor = resolve(cursor, component);
|
|
1416
|
+
const metadata = lstatSync(cursor);
|
|
1417
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
1418
|
+
throw new Error(label + ' managed directory contains a symbolic link.');
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const candidate = resolve(process.cwd(), requestedPath);
|
|
1422
|
+
const child = relative(root, candidate);
|
|
1423
|
+
if (
|
|
1424
|
+
!/^[a-z0-9][a-z0-9._-]*\\.json$/.test(child) ||
|
|
1425
|
+
child.includes(sep) ||
|
|
1426
|
+
child === '..' ||
|
|
1427
|
+
child.startsWith('..' + sep)
|
|
1428
|
+
) {
|
|
1429
|
+
throw new Error(label + ' must be one exact managed JSON file.');
|
|
1430
|
+
}
|
|
1431
|
+
const metadata = lstatSync(candidate);
|
|
1432
|
+
if (
|
|
1433
|
+
!metadata.isFile() ||
|
|
1434
|
+
metadata.isSymbolicLink() ||
|
|
1435
|
+
(process.platform !== 'win32' && (metadata.mode & 0o077) !== 0) ||
|
|
1436
|
+
(typeof process.getuid === 'function' &&
|
|
1437
|
+
metadata.uid !== process.getuid()) ||
|
|
1438
|
+
metadata.size <= 0 ||
|
|
1439
|
+
metadata.size > 1024 * 1024
|
|
1440
|
+
) {
|
|
1441
|
+
throw new Error(label + ' must be a regular bounded non-symlink file.');
|
|
1442
|
+
}
|
|
1443
|
+
const realRoot = realpathSync(root);
|
|
1444
|
+
const realCandidate = realpathSync(candidate);
|
|
1445
|
+
if (relative(realRoot, realCandidate) !== child) {
|
|
1446
|
+
throw new Error(label + ' resolves outside its managed directory.');
|
|
1447
|
+
}
|
|
1448
|
+
return realCandidate;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
const planPath = managedInput(requestedPlanPath, 'plans', 'Protected plan');
|
|
1452
|
+
const approvalPath = managedInput(
|
|
1453
|
+
requestedApprovalPath,
|
|
1454
|
+
'approvals',
|
|
1455
|
+
'Protected approval',
|
|
1456
|
+
);
|
|
1457
|
+
const policy = validatePerformancePolicy(
|
|
1458
|
+
JSON.parse(
|
|
1459
|
+
readFileSync(
|
|
1460
|
+
new URL('../performance/policy.json', import.meta.url),
|
|
1461
|
+
'utf8',
|
|
1462
|
+
),
|
|
1463
|
+
),
|
|
1464
|
+
);
|
|
1465
|
+
const result = validateProtectedLoadAuthorization({
|
|
1466
|
+
approval: JSON.parse(readFileSync(approvalPath, 'utf8')),
|
|
1467
|
+
currentSourceCommit,
|
|
1468
|
+
expectedAccountFingerprint,
|
|
1469
|
+
expectedEndpoint,
|
|
1470
|
+
expectedRegion,
|
|
1471
|
+
now,
|
|
1472
|
+
plan: JSON.parse(readFileSync(planPath, 'utf8')),
|
|
1473
|
+
policy,
|
|
1474
|
+
});
|
|
1475
|
+
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
1476
|
+
`,
|
|
1477
|
+
'scripts/performance-load-runner.mjs': `import {
|
|
1478
|
+
appendFileSync,
|
|
1479
|
+
chmodSync,
|
|
1480
|
+
closeSync,
|
|
1481
|
+
constants,
|
|
1482
|
+
existsSync,
|
|
1483
|
+
lstatSync,
|
|
1484
|
+
mkdirSync,
|
|
1485
|
+
openSync,
|
|
1486
|
+
readFileSync,
|
|
1487
|
+
writeFileSync,
|
|
1488
|
+
} from 'node:fs';
|
|
1489
|
+
import {
|
|
1490
|
+
createProtectedLoadPlan,
|
|
1491
|
+
createProtectedLoadScenarioDriver,
|
|
1492
|
+
runBoundedProtectedLoad,
|
|
1493
|
+
validatePerformancePolicy,
|
|
1494
|
+
} from '@empire-builder-kit/nx/performance';
|
|
1495
|
+
|
|
1496
|
+
const planPath = process.env.EBK_PROTECTED_LOAD_PLAN_PATH?.trim();
|
|
1497
|
+
const authorizationPath =
|
|
1498
|
+
process.env.EBK_PROTECTED_LOAD_AUTHORIZATION_PATH?.trim();
|
|
1499
|
+
const hmacKeyPath =
|
|
1500
|
+
process.env.EBK_PERFORMANCE_LOAD_HMAC_KEY_PATH?.trim();
|
|
1501
|
+
const emergencyStopPath = process.env.EBK_EMERGENCY_STOP_PATH?.trim();
|
|
1502
|
+
if (!planPath || !authorizationPath || !hmacKeyPath || !emergencyStopPath) {
|
|
1503
|
+
throw new Error(
|
|
1504
|
+
'Protected load runner requires exact plan, authorization, job-scoped HMAC-key, and emergency-stop paths.',
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
const readHmacKey = (path) => {
|
|
1508
|
+
const metadata = lstatSync(path);
|
|
1509
|
+
if (
|
|
1510
|
+
!metadata.isFile() ||
|
|
1511
|
+
metadata.isSymbolicLink() ||
|
|
1512
|
+
(process.platform !== 'win32' && (metadata.mode & 0o077) !== 0) ||
|
|
1513
|
+
(typeof process.getuid === 'function' &&
|
|
1514
|
+
metadata.uid !== process.getuid()) ||
|
|
1515
|
+
metadata.size <= 0 ||
|
|
1516
|
+
metadata.size > 2_048
|
|
1517
|
+
) {
|
|
1518
|
+
throw new Error('Protected load HMAC key must be one bounded regular file.');
|
|
1519
|
+
}
|
|
1520
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
1521
|
+
const secret =
|
|
1522
|
+
value && typeof value.secret === 'string' ? value.secret : '';
|
|
1523
|
+
const decoded = /^[A-Za-z0-9_-]{43}$/.test(secret)
|
|
1524
|
+
? Buffer.from(secret, 'base64url')
|
|
1525
|
+
: Buffer.alloc(0);
|
|
1526
|
+
if (
|
|
1527
|
+
!value ||
|
|
1528
|
+
typeof value !== 'object' ||
|
|
1529
|
+
Array.isArray(value) ||
|
|
1530
|
+
Object.keys(value).sort().join() !== 'id,secret' ||
|
|
1531
|
+
typeof value.id !== 'string' ||
|
|
1532
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value.id) ||
|
|
1533
|
+
decoded.length !== 32 ||
|
|
1534
|
+
decoded.toString('base64url') !== secret
|
|
1535
|
+
) {
|
|
1536
|
+
throw new Error('Protected load HMAC key material is invalid.');
|
|
1537
|
+
}
|
|
1538
|
+
return Object.freeze({ id: value.id, secret: value.secret });
|
|
1539
|
+
};
|
|
1540
|
+
const hmacKey = readHmacKey(hmacKeyPath);
|
|
1541
|
+
const policy = validatePerformancePolicy(
|
|
1542
|
+
JSON.parse(
|
|
1543
|
+
readFileSync(
|
|
1544
|
+
new URL('../performance/policy.json', import.meta.url),
|
|
1545
|
+
'utf8',
|
|
1546
|
+
),
|
|
1547
|
+
),
|
|
1548
|
+
);
|
|
1549
|
+
if (
|
|
1550
|
+
policy.authority.operationsAuthorityPath !==
|
|
1551
|
+
'operations/reliability.json'
|
|
1552
|
+
) {
|
|
1553
|
+
throw new Error(
|
|
1554
|
+
'Protected load requires the exact generated reliability operations authority.',
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
const operationsAuthority = JSON.parse(
|
|
1558
|
+
readFileSync(
|
|
1559
|
+
new URL('../operations/reliability.json', import.meta.url),
|
|
1560
|
+
'utf8',
|
|
1561
|
+
),
|
|
1562
|
+
);
|
|
1563
|
+
const providedPlan = JSON.parse(readFileSync(planPath, 'utf8'));
|
|
1564
|
+
const plan = createProtectedLoadPlan(policy, providedPlan);
|
|
1565
|
+
if (plan.materialDigest !== providedPlan.materialDigest) {
|
|
1566
|
+
throw new Error('Protected load plan material changed after authorization.');
|
|
1567
|
+
}
|
|
1568
|
+
if (plan.evidencePath !== '.ebk/evidence/performance-load-report.json') {
|
|
1569
|
+
throw new Error('Protected load evidence path is not the managed path.');
|
|
1570
|
+
}
|
|
1571
|
+
const catalog = JSON.parse(
|
|
1572
|
+
readFileSync(
|
|
1573
|
+
new URL('../performance/load-scenarios.json', import.meta.url),
|
|
1574
|
+
'utf8',
|
|
1575
|
+
),
|
|
1576
|
+
);
|
|
1577
|
+
const authorization = JSON.parse(readFileSync(authorizationPath, 'utf8'));
|
|
1578
|
+
const evidenceDirectory = new URL('../.ebk/evidence/', import.meta.url);
|
|
1579
|
+
mkdirSync(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
1580
|
+
chmodSync(evidenceDirectory, 0o700);
|
|
1581
|
+
const effectsPath = new URL('performance-load-effects.jsonl', evidenceDirectory);
|
|
1582
|
+
const maximumEffectLineBytes = 2_048;
|
|
1583
|
+
const maximumEffectLedgerBytes = Math.min(
|
|
1584
|
+
64 * 1024 * 1024,
|
|
1585
|
+
plan.maximumRequests * maximumEffectLineBytes,
|
|
1586
|
+
);
|
|
1587
|
+
const effects = new Map();
|
|
1588
|
+
let effectLedgerBytes = 0;
|
|
1589
|
+
if (existsSync(effectsPath)) {
|
|
1590
|
+
const metadata = lstatSync(effectsPath);
|
|
1591
|
+
if (
|
|
1592
|
+
!metadata.isFile() ||
|
|
1593
|
+
metadata.isSymbolicLink() ||
|
|
1594
|
+
metadata.size > maximumEffectLedgerBytes
|
|
1595
|
+
) {
|
|
1596
|
+
throw new Error('Protected load effect ledger must be a bounded regular file.');
|
|
1597
|
+
}
|
|
1598
|
+
const bytes = readFileSync(effectsPath, 'utf8');
|
|
1599
|
+
effectLedgerBytes = Buffer.byteLength(bytes);
|
|
1600
|
+
if (bytes && !bytes.endsWith('\\n')) {
|
|
1601
|
+
throw new Error('Protected load effect ledger has an incomplete record.');
|
|
1602
|
+
}
|
|
1603
|
+
const lines = bytes ? bytes.slice(0, -1).split('\\n') : [];
|
|
1604
|
+
if (lines.length > plan.maximumRequests) {
|
|
1605
|
+
throw new Error('Protected load effect ledger exceeds the request budget.');
|
|
1606
|
+
}
|
|
1607
|
+
for (const line of lines) {
|
|
1608
|
+
if (Buffer.byteLength(line) > maximumEffectLineBytes) {
|
|
1609
|
+
throw new Error('Protected load effect ledger record is oversized.');
|
|
1610
|
+
}
|
|
1611
|
+
const effect = JSON.parse(line);
|
|
1612
|
+
if (
|
|
1613
|
+
!effect ||
|
|
1614
|
+
typeof effect !== 'object' ||
|
|
1615
|
+
typeof effect.effectId !== 'string' ||
|
|
1616
|
+
effects.has(effect.effectId)
|
|
1617
|
+
) {
|
|
1618
|
+
throw new Error('Protected load effect ledger identity is invalid.');
|
|
1619
|
+
}
|
|
1620
|
+
effects.set(effect.effectId, effect);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const effectLedger = openSync(
|
|
1624
|
+
effectsPath,
|
|
1625
|
+
constants.O_APPEND |
|
|
1626
|
+
constants.O_CREAT |
|
|
1627
|
+
constants.O_WRONLY |
|
|
1628
|
+
constants.O_NOFOLLOW,
|
|
1629
|
+
0o600,
|
|
1630
|
+
);
|
|
1631
|
+
chmodSync(effectsPath, 0o600);
|
|
1632
|
+
const recordEffect = async (effect) => {
|
|
1633
|
+
const prior = effects.get(effect.effectId);
|
|
1634
|
+
if (prior) {
|
|
1635
|
+
if (JSON.stringify(prior) !== JSON.stringify(effect)) {
|
|
1636
|
+
throw new Error('Protected load effect identity contains other material.');
|
|
1637
|
+
}
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
const line = JSON.stringify(effect) + '\\n';
|
|
1641
|
+
const lineBytes = Buffer.byteLength(line);
|
|
1642
|
+
if (
|
|
1643
|
+
lineBytes > maximumEffectLineBytes ||
|
|
1644
|
+
effects.size >= plan.maximumRequests ||
|
|
1645
|
+
effectLedgerBytes + lineBytes > maximumEffectLedgerBytes
|
|
1646
|
+
) {
|
|
1647
|
+
throw new Error('Protected load effect ledger exceeded its hard bounds.');
|
|
1648
|
+
}
|
|
1649
|
+
appendFileSync(effectLedger, line, 'utf8');
|
|
1650
|
+
effectLedgerBytes += lineBytes;
|
|
1651
|
+
effects.set(effect.effectId, effect);
|
|
1652
|
+
};
|
|
1653
|
+
const { allowedDriver, driver } = createProtectedLoadScenarioDriver({
|
|
1654
|
+
catalog,
|
|
1655
|
+
hmacKey,
|
|
1656
|
+
operationsAuthority,
|
|
1657
|
+
plan,
|
|
1658
|
+
policy,
|
|
1659
|
+
recordEffect,
|
|
1660
|
+
});
|
|
1661
|
+
const report = await runBoundedProtectedLoad({
|
|
1662
|
+
abortRequested: () => existsSync(emergencyStopPath),
|
|
1663
|
+
allowedDrivers: [allowedDriver],
|
|
1664
|
+
authorization,
|
|
1665
|
+
driver,
|
|
1666
|
+
plan,
|
|
1667
|
+
});
|
|
1668
|
+
closeSync(effectLedger);
|
|
1669
|
+
const output = new URL('performance-load-report.json', evidenceDirectory);
|
|
1670
|
+
const bytes = JSON.stringify(report, null, 2) + '\\n';
|
|
1671
|
+
try {
|
|
1672
|
+
writeFileSync(output, bytes, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
1673
|
+
} catch {
|
|
1674
|
+
if (readFileSync(output, 'utf8') !== bytes) {
|
|
1675
|
+
throw new Error('Protected load evidence path already contains other bytes.');
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
chmodSync(output, 0o600);
|
|
1679
|
+
process.stdout.write(JSON.stringify(report) + '\\n');
|
|
1680
|
+
`,
|
|
1681
|
+
'scripts/performance-load-cleanup.mjs': `import {
|
|
1682
|
+
chmodSync,
|
|
1683
|
+
existsSync,
|
|
1684
|
+
lstatSync,
|
|
1685
|
+
mkdirSync,
|
|
1686
|
+
readFileSync,
|
|
1687
|
+
writeFileSync,
|
|
1688
|
+
} from 'node:fs';
|
|
1689
|
+
import {
|
|
1690
|
+
cleanupProtectedLoadScenarioEffects,
|
|
1691
|
+
createProtectedLoadPlan,
|
|
1692
|
+
validatePerformancePolicy,
|
|
1693
|
+
} from '@empire-builder-kit/nx/performance';
|
|
1694
|
+
|
|
1695
|
+
const planPath = process.env.EBK_PROTECTED_LOAD_PLAN_PATH?.trim();
|
|
1696
|
+
const hmacKeyPath =
|
|
1697
|
+
process.env.EBK_PERFORMANCE_LOAD_HMAC_KEY_PATH?.trim();
|
|
1698
|
+
if (!planPath || !hmacKeyPath) {
|
|
1699
|
+
throw new Error(
|
|
1700
|
+
'Protected load cleanup requires one exact plan path and job-scoped HMAC key.',
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
const metadata = lstatSync(hmacKeyPath);
|
|
1704
|
+
if (
|
|
1705
|
+
!metadata.isFile() ||
|
|
1706
|
+
metadata.isSymbolicLink() ||
|
|
1707
|
+
(process.platform !== 'win32' && (metadata.mode & 0o077) !== 0) ||
|
|
1708
|
+
(typeof process.getuid === 'function' &&
|
|
1709
|
+
metadata.uid !== process.getuid()) ||
|
|
1710
|
+
metadata.size <= 0 ||
|
|
1711
|
+
metadata.size > 2_048
|
|
1712
|
+
) {
|
|
1713
|
+
throw new Error('Protected load HMAC key must be one bounded regular file.');
|
|
1714
|
+
}
|
|
1715
|
+
const hmacKey = JSON.parse(readFileSync(hmacKeyPath, 'utf8'));
|
|
1716
|
+
const hmacSecret =
|
|
1717
|
+
hmacKey && typeof hmacKey.secret === 'string' ? hmacKey.secret : '';
|
|
1718
|
+
const decodedHmacSecret = /^[A-Za-z0-9_-]{43}$/.test(hmacSecret)
|
|
1719
|
+
? Buffer.from(hmacSecret, 'base64url')
|
|
1720
|
+
: Buffer.alloc(0);
|
|
1721
|
+
if (
|
|
1722
|
+
!hmacKey ||
|
|
1723
|
+
typeof hmacKey !== 'object' ||
|
|
1724
|
+
Array.isArray(hmacKey) ||
|
|
1725
|
+
Object.keys(hmacKey).sort().join() !== 'id,secret' ||
|
|
1726
|
+
typeof hmacKey.id !== 'string' ||
|
|
1727
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(hmacKey.id) ||
|
|
1728
|
+
decodedHmacSecret.length !== 32 ||
|
|
1729
|
+
decodedHmacSecret.toString('base64url') !== hmacSecret
|
|
1730
|
+
) {
|
|
1731
|
+
throw new Error('Protected load HMAC key material is invalid.');
|
|
1732
|
+
}
|
|
1733
|
+
const policy = validatePerformancePolicy(
|
|
1734
|
+
JSON.parse(
|
|
1735
|
+
readFileSync(
|
|
1736
|
+
new URL('../performance/policy.json', import.meta.url),
|
|
1737
|
+
'utf8',
|
|
1738
|
+
),
|
|
1739
|
+
),
|
|
1740
|
+
);
|
|
1741
|
+
if (
|
|
1742
|
+
policy.authority.operationsAuthorityPath !==
|
|
1743
|
+
'operations/reliability.json'
|
|
1744
|
+
) {
|
|
1745
|
+
throw new Error(
|
|
1746
|
+
'Protected load cleanup requires the exact generated reliability operations authority.',
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
const operationsAuthority = JSON.parse(
|
|
1750
|
+
readFileSync(
|
|
1751
|
+
new URL('../operations/reliability.json', import.meta.url),
|
|
1752
|
+
'utf8',
|
|
1753
|
+
),
|
|
1754
|
+
);
|
|
1755
|
+
const providedPlan = JSON.parse(readFileSync(planPath, 'utf8'));
|
|
1756
|
+
const plan = createProtectedLoadPlan(policy, providedPlan);
|
|
1757
|
+
if (plan.materialDigest !== providedPlan.materialDigest) {
|
|
1758
|
+
throw new Error('Protected load cleanup plan material is inconsistent.');
|
|
1759
|
+
}
|
|
1760
|
+
const catalog = JSON.parse(
|
|
1761
|
+
readFileSync(
|
|
1762
|
+
new URL('../performance/load-scenarios.json', import.meta.url),
|
|
1763
|
+
'utf8',
|
|
1764
|
+
),
|
|
1765
|
+
);
|
|
1766
|
+
const evidenceDirectory = new URL('../.ebk/evidence/', import.meta.url);
|
|
1767
|
+
mkdirSync(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
1768
|
+
chmodSync(evidenceDirectory, 0o700);
|
|
1769
|
+
const effectsPath = new URL('performance-load-effects.jsonl', evidenceDirectory);
|
|
1770
|
+
const maximumEffectLineBytes = 2_048;
|
|
1771
|
+
const maximumEffectLedgerBytes = Math.min(
|
|
1772
|
+
64 * 1024 * 1024,
|
|
1773
|
+
plan.maximumRequests * maximumEffectLineBytes,
|
|
1774
|
+
);
|
|
1775
|
+
let effects = [];
|
|
1776
|
+
if (existsSync(effectsPath)) {
|
|
1777
|
+
const metadata = lstatSync(effectsPath);
|
|
1778
|
+
if (
|
|
1779
|
+
!metadata.isFile() ||
|
|
1780
|
+
metadata.isSymbolicLink() ||
|
|
1781
|
+
metadata.size > maximumEffectLedgerBytes
|
|
1782
|
+
) {
|
|
1783
|
+
throw new Error('Protected load effect ledger must be a bounded regular file.');
|
|
1784
|
+
}
|
|
1785
|
+
const bytes = readFileSync(effectsPath, 'utf8');
|
|
1786
|
+
if (bytes && !bytes.endsWith('\\n')) {
|
|
1787
|
+
throw new Error('Protected load effect ledger has an incomplete record.');
|
|
1788
|
+
}
|
|
1789
|
+
const lines = bytes ? bytes.slice(0, -1).split('\\n') : [];
|
|
1790
|
+
if (
|
|
1791
|
+
lines.length > plan.maximumRequests ||
|
|
1792
|
+
lines.some((line) => Buffer.byteLength(line) > maximumEffectLineBytes)
|
|
1793
|
+
) {
|
|
1794
|
+
throw new Error('Protected load effect ledger exceeds its hard bounds.');
|
|
1795
|
+
}
|
|
1796
|
+
effects = lines.map((line) => JSON.parse(line));
|
|
1797
|
+
}
|
|
1798
|
+
const report = await cleanupProtectedLoadScenarioEffects({
|
|
1799
|
+
catalog,
|
|
1800
|
+
effects,
|
|
1801
|
+
hmacKey,
|
|
1802
|
+
operationsAuthority,
|
|
1803
|
+
plan,
|
|
1804
|
+
policy,
|
|
1805
|
+
});
|
|
1806
|
+
const output = new URL('performance-load-cleanup.json', evidenceDirectory);
|
|
1807
|
+
const bytes = JSON.stringify(report, null, 2) + '\\n';
|
|
1808
|
+
try {
|
|
1809
|
+
writeFileSync(output, bytes, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
1810
|
+
} catch {
|
|
1811
|
+
if (readFileSync(output, 'utf8') !== bytes) {
|
|
1812
|
+
throw new Error('Protected cleanup evidence path contains other bytes.');
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
chmodSync(output, 0o600);
|
|
1816
|
+
process.stdout.write(JSON.stringify(report) + '\\n');
|
|
1817
|
+
`,
|
|
1818
|
+
'scripts/performance-tag-coverage.mjs': `import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1819
|
+
import {
|
|
1820
|
+
deriveExpectedResourceTagInventory,
|
|
1821
|
+
evaluateResourceTagCoverage,
|
|
1822
|
+
} from '@empire-builder-kit/nx/performance';
|
|
1823
|
+
|
|
1824
|
+
const deploymentPath =
|
|
1825
|
+
process.env.EBK_DEPLOYMENT_RESOURCE_TAG_INVENTORY_PATH?.trim();
|
|
1826
|
+
const observedPath =
|
|
1827
|
+
process.env.EBK_OBSERVED_RESOURCE_TAG_INVENTORY_PATH?.trim();
|
|
1828
|
+
if (!deploymentPath || !observedPath) {
|
|
1829
|
+
throw new Error(
|
|
1830
|
+
'Exact deployment resource inventory and observed tag inventory paths are required.',
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
const expected = deriveExpectedResourceTagInventory(
|
|
1834
|
+
JSON.parse(readFileSync(deploymentPath, 'utf8')),
|
|
1835
|
+
);
|
|
1836
|
+
const result = evaluateResourceTagCoverage({
|
|
1837
|
+
expected,
|
|
1838
|
+
observed: JSON.parse(readFileSync(observedPath, 'utf8')),
|
|
1839
|
+
});
|
|
1840
|
+
const evidenceDirectory = new URL('../.ebk/evidence/', import.meta.url);
|
|
1841
|
+
mkdirSync(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
1842
|
+
chmodSync(evidenceDirectory, 0o700);
|
|
1843
|
+
const evidencePath = new URL(
|
|
1844
|
+
'performance-tag-coverage.json',
|
|
1845
|
+
evidenceDirectory,
|
|
1846
|
+
);
|
|
1847
|
+
writeFileSync(evidencePath, JSON.stringify(result, null, 2) + '\\n', {
|
|
1848
|
+
encoding: 'utf8',
|
|
1849
|
+
mode: 0o600,
|
|
1850
|
+
});
|
|
1851
|
+
chmodSync(evidencePath, 0o600);
|
|
1852
|
+
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
1853
|
+
if (result.status !== 'complete') process.exitCode = 2;
|
|
1854
|
+
`,
|
|
1855
|
+
'web/src/app/%5F%5Febk/performance/handler.ts': `import 'server-only';
|
|
1856
|
+
import {
|
|
1857
|
+
handleProtectedLoadCleanupEndpoint,
|
|
1858
|
+
handleProtectedLoadScenarioEndpoint,
|
|
1859
|
+
type ProtectedLoadEndpointBindingV1,
|
|
1860
|
+
type ProtectedLoadEndpointAdapterId,
|
|
1861
|
+
type ProtectedLoadRequestAuthorizationV1,
|
|
1862
|
+
} from '@empire-builder-kit/runtime/performance';
|
|
1863
|
+
import { randomUUID } from 'node:crypto';
|
|
1864
|
+
import { withPrimaryDatabase } from '../../../../../database/runtime-client';
|
|
1865
|
+
import { Secrets } from '../../../../../secrets/declarations';
|
|
1866
|
+
import { createStandardSliceSecretRuntime } from '../../../../../secrets/runtime';
|
|
1867
|
+
|
|
1868
|
+
type ManagedAdapterId = Exclude<
|
|
1869
|
+
ProtectedLoadEndpointAdapterId,
|
|
1870
|
+
'ebk-service-v1'
|
|
1871
|
+
>;
|
|
1872
|
+
|
|
1873
|
+
function required(name: string): string {
|
|
1874
|
+
const value = process.env[name]?.trim();
|
|
1875
|
+
if (!value) throw new Error('protected-performance-endpoint-disabled');
|
|
1876
|
+
return value;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
type EndpointSecretRuntime = ReturnType<
|
|
1880
|
+
typeof createStandardSliceSecretRuntime
|
|
1881
|
+
>;
|
|
1882
|
+
let endpointSecretRuntime: EndpointSecretRuntime | undefined;
|
|
1883
|
+
let endpointSecretRuntimeStage: 'production' | 'staging' | undefined;
|
|
1884
|
+
|
|
1885
|
+
function performanceSecretRuntime(
|
|
1886
|
+
stage: 'production' | 'staging',
|
|
1887
|
+
): EndpointSecretRuntime {
|
|
1888
|
+
if (
|
|
1889
|
+
endpointSecretRuntimeStage !== undefined &&
|
|
1890
|
+
endpointSecretRuntimeStage !== stage
|
|
1891
|
+
) {
|
|
1892
|
+
throw new Error('protected-performance-endpoint-disabled');
|
|
1893
|
+
}
|
|
1894
|
+
endpointSecretRuntimeStage = stage;
|
|
1895
|
+
endpointSecretRuntime ??= createStandardSliceSecretRuntime({
|
|
1896
|
+
stage,
|
|
1897
|
+
workload: '${name}.web-server',
|
|
1898
|
+
});
|
|
1899
|
+
return endpointSecretRuntime;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
async function withEndpointBinding<Result>(
|
|
1903
|
+
adapterId: ManagedAdapterId,
|
|
1904
|
+
operation: (binding: ProtectedLoadEndpointBindingV1) => Promise<Result>,
|
|
1905
|
+
): Promise<Result> {
|
|
1906
|
+
const stage = process.env.EBK_STAGE;
|
|
1907
|
+
if (
|
|
1908
|
+
process.env.EBK_PERFORMANCE_LOAD_ENABLED !== 'true' ||
|
|
1909
|
+
(stage !== 'staging' && stage !== 'production')
|
|
1910
|
+
) {
|
|
1911
|
+
throw new Error('protected-performance-endpoint-disabled');
|
|
1912
|
+
}
|
|
1913
|
+
return performanceSecretRuntime(stage).runWithSecret({
|
|
1914
|
+
declaration: Secrets.${pascalName}PerformanceLoadHmacKeys,
|
|
1915
|
+
isAuthenticationFailure: () => false,
|
|
1916
|
+
operation: (hmacKeys) =>
|
|
1917
|
+
operation({
|
|
1918
|
+
adapterId,
|
|
1919
|
+
deployedCandidateDigest: required(
|
|
1920
|
+
'EBK_PERFORMANCE_CANDIDATE_DIGEST',
|
|
1921
|
+
),
|
|
1922
|
+
deployedReleaseId: required('EBK_PERFORMANCE_RELEASE_ID'),
|
|
1923
|
+
deployedReleaseMaterialDigest: required(
|
|
1924
|
+
'EBK_PERFORMANCE_RELEASE_MATERIAL_DIGEST',
|
|
1925
|
+
),
|
|
1926
|
+
deployedSourceCommit: required('EBK_SOURCE_COMMIT'),
|
|
1927
|
+
hmacKeys,
|
|
1928
|
+
stage,
|
|
1929
|
+
target: {
|
|
1930
|
+
accountFingerprint: required(
|
|
1931
|
+
'EBK_PERFORMANCE_ACCOUNT_FINGERPRINT',
|
|
1932
|
+
),
|
|
1933
|
+
origin: required('EBK_PERFORMANCE_ORIGIN'),
|
|
1934
|
+
region: required('EBK_PERFORMANCE_REGION'),
|
|
1935
|
+
slice: required('EBK_PERFORMANCE_SLICE'),
|
|
1936
|
+
},
|
|
1937
|
+
}),
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
function requestMaterial(request: Request, body?: unknown) {
|
|
1942
|
+
return {
|
|
1943
|
+
...(body === undefined ? {} : { body }),
|
|
1944
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
1945
|
+
method: request.method as 'GET' | 'POST',
|
|
1946
|
+
url: request.url,
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
async function boundedRequestBody(request: Request): Promise<unknown> {
|
|
1951
|
+
const maximumBytes = 32 * 1024;
|
|
1952
|
+
const declaredLength = request.headers.get('content-length');
|
|
1953
|
+
if (
|
|
1954
|
+
declaredLength !== null &&
|
|
1955
|
+
(!/^\\d+$/.test(declaredLength) ||
|
|
1956
|
+
Number(declaredLength) <= 0 ||
|
|
1957
|
+
Number(declaredLength) > maximumBytes)
|
|
1958
|
+
) {
|
|
1959
|
+
throw new Error('protected-performance-request-body-invalid');
|
|
1960
|
+
}
|
|
1961
|
+
if (!request.body) {
|
|
1962
|
+
throw new Error('protected-performance-request-body-invalid');
|
|
1963
|
+
}
|
|
1964
|
+
const reader = request.body.getReader();
|
|
1965
|
+
const chunks: Uint8Array[] = [];
|
|
1966
|
+
let size = 0;
|
|
1967
|
+
try {
|
|
1968
|
+
for (;;) {
|
|
1969
|
+
const { done, value } = await reader.read();
|
|
1970
|
+
if (done) break;
|
|
1971
|
+
size += value.byteLength;
|
|
1972
|
+
if (size > maximumBytes) {
|
|
1973
|
+
await reader.cancel();
|
|
1974
|
+
throw new Error('protected-performance-request-body-invalid');
|
|
1975
|
+
}
|
|
1976
|
+
chunks.push(value);
|
|
1977
|
+
}
|
|
1978
|
+
} finally {
|
|
1979
|
+
reader.releaseLock();
|
|
1980
|
+
}
|
|
1981
|
+
if (size === 0) {
|
|
1982
|
+
throw new Error('protected-performance-request-body-invalid');
|
|
1983
|
+
}
|
|
1984
|
+
const bytes = new Uint8Array(size);
|
|
1985
|
+
let offset = 0;
|
|
1986
|
+
for (const chunk of chunks) {
|
|
1987
|
+
bytes.set(chunk, offset);
|
|
1988
|
+
offset += chunk.byteLength;
|
|
1989
|
+
}
|
|
1990
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
function memorySaturation(): number {
|
|
1994
|
+
const memory = process.memoryUsage();
|
|
1995
|
+
if (memory.heapTotal <= 0) return 0;
|
|
1996
|
+
return Math.min(100, (memory.heapUsed / memory.heapTotal) * 100);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function effectUuid(effectId: string): string {
|
|
2000
|
+
return [
|
|
2001
|
+
effectId.slice(0, 8),
|
|
2002
|
+
effectId.slice(8, 12),
|
|
2003
|
+
'4' + effectId.slice(13, 16),
|
|
2004
|
+
'8' + effectId.slice(17, 20),
|
|
2005
|
+
effectId.slice(20, 32),
|
|
2006
|
+
].join('-');
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
interface ProtectedPerformanceRunRow {
|
|
2010
|
+
adapterId: string;
|
|
2011
|
+
cleanupStartedAt: Date | null;
|
|
2012
|
+
createdAt: Date;
|
|
2013
|
+
expiresAt: Date;
|
|
2014
|
+
maximumRequests: number;
|
|
2015
|
+
releaseMaterialDigest: string;
|
|
2016
|
+
runId: string;
|
|
2017
|
+
scenarioId: string;
|
|
2018
|
+
sourceCommit: string;
|
|
2019
|
+
stage: string;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
function matchesProtectedPerformanceRun(
|
|
2023
|
+
actual: ProtectedPerformanceRunRow | undefined,
|
|
2024
|
+
authorization: ProtectedLoadRequestAuthorizationV1,
|
|
2025
|
+
): actual is ProtectedPerformanceRunRow {
|
|
2026
|
+
return (
|
|
2027
|
+
actual !== undefined &&
|
|
2028
|
+
actual.adapterId === authorization.adapterId &&
|
|
2029
|
+
actual.createdAt.toISOString() === authorization.createdAt &&
|
|
2030
|
+
actual.expiresAt.toISOString() === authorization.expiresAt &&
|
|
2031
|
+
actual.maximumRequests === authorization.maximumRequests &&
|
|
2032
|
+
actual.releaseMaterialDigest ===
|
|
2033
|
+
authorization.release.releaseMaterialDigest &&
|
|
2034
|
+
actual.runId === authorization.runId &&
|
|
2035
|
+
actual.scenarioId === authorization.scenarioId &&
|
|
2036
|
+
actual.sourceCommit === authorization.release.sourceCommit &&
|
|
2037
|
+
actual.stage === authorization.stage
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
export async function reserveRequest(
|
|
2042
|
+
authorization: ProtectedLoadRequestAuthorizationV1,
|
|
2043
|
+
) {
|
|
2044
|
+
return withPrimaryDatabase(
|
|
2045
|
+
{ correlationId: randomUUID() },
|
|
2046
|
+
'web-server',
|
|
2047
|
+
async (client) => {
|
|
2048
|
+
await client.query('BEGIN');
|
|
2049
|
+
try {
|
|
2050
|
+
await client.query(
|
|
2051
|
+
\`INSERT INTO performance_load_runs
|
|
2052
|
+
(plan_digest, run_id, adapter_id, scenario_id, stage,
|
|
2053
|
+
source_commit, release_material_digest, maximum_requests,
|
|
2054
|
+
created_at, expires_at)
|
|
2055
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::timestamptz,
|
|
2056
|
+
$10::timestamptz)
|
|
2057
|
+
ON CONFLICT (plan_digest) DO NOTHING\`,
|
|
2058
|
+
[
|
|
2059
|
+
authorization.planMaterialDigest,
|
|
2060
|
+
authorization.runId,
|
|
2061
|
+
authorization.adapterId,
|
|
2062
|
+
authorization.scenarioId,
|
|
2063
|
+
authorization.stage,
|
|
2064
|
+
authorization.release.sourceCommit,
|
|
2065
|
+
authorization.release.releaseMaterialDigest,
|
|
2066
|
+
authorization.maximumRequests,
|
|
2067
|
+
authorization.createdAt,
|
|
2068
|
+
authorization.expiresAt,
|
|
2069
|
+
],
|
|
2070
|
+
);
|
|
2071
|
+
const run = await client.query<ProtectedPerformanceRunRow>(
|
|
2072
|
+
\`SELECT
|
|
2073
|
+
adapter_id AS "adapterId",
|
|
2074
|
+
cleanup_started_at AS "cleanupStartedAt",
|
|
2075
|
+
created_at AS "createdAt",
|
|
2076
|
+
expires_at AS "expiresAt",
|
|
2077
|
+
maximum_requests AS "maximumRequests",
|
|
2078
|
+
release_material_digest AS "releaseMaterialDigest",
|
|
2079
|
+
run_id AS "runId",
|
|
2080
|
+
scenario_id AS "scenarioId",
|
|
2081
|
+
source_commit AS "sourceCommit",
|
|
2082
|
+
stage
|
|
2083
|
+
FROM performance_load_runs
|
|
2084
|
+
WHERE plan_digest = $1
|
|
2085
|
+
FOR UPDATE\`,
|
|
2086
|
+
[authorization.planMaterialDigest],
|
|
2087
|
+
);
|
|
2088
|
+
const actual = run.rows[0];
|
|
2089
|
+
if (
|
|
2090
|
+
!matchesProtectedPerformanceRun(actual, authorization) ||
|
|
2091
|
+
actual.cleanupStartedAt !== null
|
|
2092
|
+
) {
|
|
2093
|
+
throw new Error('protected-performance-run-collision');
|
|
2094
|
+
}
|
|
2095
|
+
const claimed = await client.query(
|
|
2096
|
+
\`INSERT INTO performance_load_requests
|
|
2097
|
+
(plan_digest, request_ordinal, effect_id)
|
|
2098
|
+
VALUES ($1, $2, $3)
|
|
2099
|
+
ON CONFLICT (plan_digest, request_ordinal) DO NOTHING\`,
|
|
2100
|
+
[
|
|
2101
|
+
authorization.planMaterialDigest,
|
|
2102
|
+
authorization.requestOrdinal,
|
|
2103
|
+
authorization.effectId,
|
|
2104
|
+
],
|
|
2105
|
+
);
|
|
2106
|
+
if (claimed.rowCount !== 1) {
|
|
2107
|
+
throw new Error('protected-performance-request-replayed');
|
|
2108
|
+
}
|
|
2109
|
+
await client.query('COMMIT');
|
|
2110
|
+
return 'claimed' as const;
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
2113
|
+
throw error;
|
|
2114
|
+
}
|
|
2115
|
+
},
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
export async function databaseExecution(
|
|
2120
|
+
adapterId: 'ebk-async-worker-v1' | 'ebk-database-v1',
|
|
2121
|
+
effectId: string,
|
|
2122
|
+
authorization: ProtectedLoadRequestAuthorizationV1,
|
|
2123
|
+
) {
|
|
2124
|
+
return withPrimaryDatabase(
|
|
2125
|
+
{ correlationId: randomUUID() },
|
|
2126
|
+
'web-server',
|
|
2127
|
+
async (client) => {
|
|
2128
|
+
await client.query('BEGIN');
|
|
2129
|
+
try {
|
|
2130
|
+
const run = await client.query<ProtectedPerformanceRunRow>(
|
|
2131
|
+
\`SELECT
|
|
2132
|
+
adapter_id AS "adapterId",
|
|
2133
|
+
cleanup_started_at AS "cleanupStartedAt",
|
|
2134
|
+
created_at AS "createdAt",
|
|
2135
|
+
expires_at AS "expiresAt",
|
|
2136
|
+
maximum_requests AS "maximumRequests",
|
|
2137
|
+
release_material_digest AS "releaseMaterialDigest",
|
|
2138
|
+
run_id AS "runId",
|
|
2139
|
+
scenario_id AS "scenarioId",
|
|
2140
|
+
source_commit AS "sourceCommit",
|
|
2141
|
+
stage
|
|
2142
|
+
FROM performance_load_runs
|
|
2143
|
+
WHERE plan_digest = $1
|
|
2144
|
+
FOR UPDATE\`,
|
|
2145
|
+
[authorization.planMaterialDigest],
|
|
2146
|
+
);
|
|
2147
|
+
const activeRun = run.rows[0];
|
|
2148
|
+
if (
|
|
2149
|
+
!matchesProtectedPerformanceRun(activeRun, authorization) ||
|
|
2150
|
+
activeRun.cleanupStartedAt !== null
|
|
2151
|
+
) {
|
|
2152
|
+
throw new Error('protected-performance-run-cleaning');
|
|
2153
|
+
}
|
|
2154
|
+
if (adapterId === 'ebk-async-worker-v1') {
|
|
2155
|
+
const inserted = await client.query(
|
|
2156
|
+
\`INSERT INTO performance_load_async_effects
|
|
2157
|
+
(effect_id, plan_digest, run_id, expires_at)
|
|
2158
|
+
VALUES ($1, $2, $3, now() + INTERVAL '24 hours')
|
|
2159
|
+
ON CONFLICT (effect_id) DO NOTHING\`,
|
|
2160
|
+
[
|
|
2161
|
+
effectId,
|
|
2162
|
+
authorization.planMaterialDigest,
|
|
2163
|
+
authorization.runId,
|
|
2164
|
+
],
|
|
2165
|
+
);
|
|
2166
|
+
const identity = await client.query<{
|
|
2167
|
+
planDigest: string;
|
|
2168
|
+
runId: string;
|
|
2169
|
+
}>(
|
|
2170
|
+
\`SELECT
|
|
2171
|
+
plan_digest AS "planDigest",
|
|
2172
|
+
run_id AS "runId"
|
|
2173
|
+
FROM performance_load_async_effects
|
|
2174
|
+
WHERE effect_id = $1\`,
|
|
2175
|
+
[effectId],
|
|
2176
|
+
);
|
|
2177
|
+
if (
|
|
2178
|
+
identity.rows[0]?.planDigest !==
|
|
2179
|
+
authorization.planMaterialDigest ||
|
|
2180
|
+
identity.rows[0]?.runId !== authorization.runId
|
|
2181
|
+
) {
|
|
2182
|
+
throw new Error('protected-performance-effect-collision');
|
|
2183
|
+
}
|
|
2184
|
+
const pending = await client.query<{ count: string }>(
|
|
2185
|
+
\`SELECT count(*)::text AS count
|
|
2186
|
+
FROM performance_load_async_effects
|
|
2187
|
+
WHERE processed_at IS NULL AND expires_at > now()\`,
|
|
2188
|
+
);
|
|
2189
|
+
const maximum = authorization.configuredMaximum;
|
|
2190
|
+
if (!Number.isFinite(maximum) || maximum <= 0) {
|
|
2191
|
+
throw new Error('protected-performance-async-maximum-invalid');
|
|
2192
|
+
}
|
|
2193
|
+
const result = {
|
|
2194
|
+
effectState:
|
|
2195
|
+
inserted.rowCount === 1
|
|
2196
|
+
? ('created' as const)
|
|
2197
|
+
: ('already-existed' as const),
|
|
2198
|
+
saturationPercent: Math.min(
|
|
2199
|
+
100,
|
|
2200
|
+
(Number(pending.rows[0]?.count ?? 0) / maximum) * 100,
|
|
2201
|
+
),
|
|
2202
|
+
};
|
|
2203
|
+
await client.query('COMMIT');
|
|
2204
|
+
return result;
|
|
2205
|
+
}
|
|
2206
|
+
const id = effectUuid(effectId);
|
|
2207
|
+
const inserted = await client.query(
|
|
2208
|
+
\`INSERT INTO records
|
|
2209
|
+
(id, name, owner_id, run_scope, test_expires_at)
|
|
2210
|
+
VALUES (
|
|
2211
|
+
$1::uuid,
|
|
2212
|
+
'ebk-protected-performance-load',
|
|
2213
|
+
'ebk-performance',
|
|
2214
|
+
$2,
|
|
2215
|
+
now() + INTERVAL '24 hours'
|
|
2216
|
+
)
|
|
2217
|
+
ON CONFLICT (id) DO NOTHING\`,
|
|
2218
|
+
[id, authorization.planMaterialDigest],
|
|
2219
|
+
);
|
|
2220
|
+
const identity = await client.query<{
|
|
2221
|
+
name: string;
|
|
2222
|
+
ownerId: string;
|
|
2223
|
+
runScope: string;
|
|
2224
|
+
}>(
|
|
2225
|
+
\`SELECT
|
|
2226
|
+
name,
|
|
2227
|
+
owner_id AS "ownerId",
|
|
2228
|
+
run_scope AS "runScope"
|
|
2229
|
+
FROM records
|
|
2230
|
+
WHERE id = $1::uuid\`,
|
|
2231
|
+
[id],
|
|
2232
|
+
);
|
|
2233
|
+
if (
|
|
2234
|
+
identity.rows[0]?.name !== 'ebk-protected-performance-load' ||
|
|
2235
|
+
identity.rows[0]?.ownerId !== 'ebk-performance' ||
|
|
2236
|
+
identity.rows[0]?.runScope !== authorization.planMaterialDigest
|
|
2237
|
+
) {
|
|
2238
|
+
throw new Error('protected-performance-effect-collision');
|
|
2239
|
+
}
|
|
2240
|
+
const activity = await client.query<{
|
|
2241
|
+
active: string;
|
|
2242
|
+
maximum: string;
|
|
2243
|
+
}>(
|
|
2244
|
+
\`SELECT
|
|
2245
|
+
count(*)::text AS active,
|
|
2246
|
+
current_setting('max_connections') AS maximum
|
|
2247
|
+
FROM pg_stat_activity
|
|
2248
|
+
WHERE datname = current_database()\`,
|
|
2249
|
+
);
|
|
2250
|
+
const active = Number(activity.rows[0]?.active ?? 0);
|
|
2251
|
+
const maximum = Number(activity.rows[0]?.maximum ?? 0);
|
|
2252
|
+
if (!Number.isFinite(maximum) || maximum <= 0) {
|
|
2253
|
+
throw new Error('protected-performance-database-maximum-invalid');
|
|
2254
|
+
}
|
|
2255
|
+
const result = {
|
|
2256
|
+
effectState:
|
|
2257
|
+
inserted.rowCount === 1
|
|
2258
|
+
? ('created' as const)
|
|
2259
|
+
: ('already-existed' as const),
|
|
2260
|
+
saturationPercent: Math.min(100, (active / maximum) * 100),
|
|
2261
|
+
};
|
|
2262
|
+
await client.query('COMMIT');
|
|
2263
|
+
return result;
|
|
2264
|
+
} catch (error) {
|
|
2265
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
2266
|
+
throw error;
|
|
2267
|
+
}
|
|
2268
|
+
},
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
export async function cleanupRun(
|
|
2273
|
+
adapterId: 'ebk-async-worker-v1' | 'ebk-database-v1',
|
|
2274
|
+
authorization: ProtectedLoadRequestAuthorizationV1,
|
|
2275
|
+
) {
|
|
2276
|
+
return withPrimaryDatabase(
|
|
2277
|
+
{ correlationId: randomUUID() },
|
|
2278
|
+
'web-server',
|
|
2279
|
+
async (client) => {
|
|
2280
|
+
await client.query('BEGIN');
|
|
2281
|
+
try {
|
|
2282
|
+
await client.query(
|
|
2283
|
+
\`INSERT INTO performance_load_runs
|
|
2284
|
+
(plan_digest, run_id, adapter_id, scenario_id, stage,
|
|
2285
|
+
source_commit, release_material_digest, maximum_requests,
|
|
2286
|
+
created_at, expires_at, cleanup_started_at)
|
|
2287
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::timestamptz,
|
|
2288
|
+
$10::timestamptz, now())
|
|
2289
|
+
ON CONFLICT (plan_digest) DO NOTHING\`,
|
|
2290
|
+
[
|
|
2291
|
+
authorization.planMaterialDigest,
|
|
2292
|
+
authorization.runId,
|
|
2293
|
+
authorization.adapterId,
|
|
2294
|
+
authorization.scenarioId,
|
|
2295
|
+
authorization.stage,
|
|
2296
|
+
authorization.release.sourceCommit,
|
|
2297
|
+
authorization.release.releaseMaterialDigest,
|
|
2298
|
+
authorization.maximumRequests,
|
|
2299
|
+
authorization.createdAt,
|
|
2300
|
+
authorization.expiresAt,
|
|
2301
|
+
],
|
|
2302
|
+
);
|
|
2303
|
+
const run = await client.query<ProtectedPerformanceRunRow>(
|
|
2304
|
+
\`SELECT
|
|
2305
|
+
adapter_id AS "adapterId",
|
|
2306
|
+
cleanup_started_at AS "cleanupStartedAt",
|
|
2307
|
+
created_at AS "createdAt",
|
|
2308
|
+
expires_at AS "expiresAt",
|
|
2309
|
+
maximum_requests AS "maximumRequests",
|
|
2310
|
+
release_material_digest AS "releaseMaterialDigest",
|
|
2311
|
+
run_id AS "runId",
|
|
2312
|
+
scenario_id AS "scenarioId",
|
|
2313
|
+
source_commit AS "sourceCommit",
|
|
2314
|
+
stage
|
|
2315
|
+
FROM performance_load_runs
|
|
2316
|
+
WHERE plan_digest = $1
|
|
2317
|
+
FOR UPDATE\`,
|
|
2318
|
+
[authorization.planMaterialDigest],
|
|
2319
|
+
);
|
|
2320
|
+
if (!matchesProtectedPerformanceRun(run.rows[0], authorization)) {
|
|
2321
|
+
throw new Error('protected-performance-run-collision');
|
|
2322
|
+
}
|
|
2323
|
+
await client.query(
|
|
2324
|
+
\`UPDATE performance_load_runs
|
|
2325
|
+
SET cleanup_started_at = COALESCE(cleanup_started_at, now())
|
|
2326
|
+
WHERE plan_digest = $1\`,
|
|
2327
|
+
[authorization.planMaterialDigest],
|
|
2328
|
+
);
|
|
2329
|
+
await client.query('COMMIT');
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
2332
|
+
throw error;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
await client.query('BEGIN');
|
|
2336
|
+
try {
|
|
2337
|
+
const removed =
|
|
2338
|
+
adapterId === 'ebk-async-worker-v1'
|
|
2339
|
+
? await client.query(
|
|
2340
|
+
\`DELETE FROM performance_load_async_effects
|
|
2341
|
+
WHERE plan_digest = $1
|
|
2342
|
+
AND run_id = $2\`,
|
|
2343
|
+
[authorization.planMaterialDigest, authorization.runId],
|
|
2344
|
+
)
|
|
2345
|
+
: await client.query(
|
|
2346
|
+
\`DELETE FROM records
|
|
2347
|
+
WHERE owner_id = 'ebk-performance'
|
|
2348
|
+
AND name = 'ebk-protected-performance-load'
|
|
2349
|
+
AND run_scope = $1\`,
|
|
2350
|
+
[authorization.planMaterialDigest],
|
|
2351
|
+
);
|
|
2352
|
+
const remaining =
|
|
2353
|
+
adapterId === 'ebk-async-worker-v1'
|
|
2354
|
+
? await client.query<{ count: string }>(
|
|
2355
|
+
\`SELECT count(*)::text AS count
|
|
2356
|
+
FROM performance_load_async_effects
|
|
2357
|
+
WHERE plan_digest = $1
|
|
2358
|
+
AND run_id = $2\`,
|
|
2359
|
+
[authorization.planMaterialDigest, authorization.runId],
|
|
2360
|
+
)
|
|
2361
|
+
: await client.query<{ count: string }>(
|
|
2362
|
+
\`SELECT count(*)::text AS count
|
|
2363
|
+
FROM records
|
|
2364
|
+
WHERE owner_id = 'ebk-performance'
|
|
2365
|
+
AND name = 'ebk-protected-performance-load'
|
|
2366
|
+
AND run_scope = $1\`,
|
|
2367
|
+
[authorization.planMaterialDigest],
|
|
2368
|
+
);
|
|
2369
|
+
const remainingEffectCount = Number(remaining.rows[0]?.count ?? 0);
|
|
2370
|
+
if (
|
|
2371
|
+
!Number.isSafeInteger(remainingEffectCount) ||
|
|
2372
|
+
remainingEffectCount !== 0
|
|
2373
|
+
) {
|
|
2374
|
+
throw new Error('protected-performance-cleanup-incomplete');
|
|
2375
|
+
}
|
|
2376
|
+
await client.query('COMMIT');
|
|
2377
|
+
return {
|
|
2378
|
+
remainingEffectCount,
|
|
2379
|
+
removedEffectCount: removed.rowCount ?? 0,
|
|
2380
|
+
};
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
2383
|
+
throw error;
|
|
2384
|
+
}
|
|
2385
|
+
},
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function refused() {
|
|
2390
|
+
return Response.json(
|
|
2391
|
+
{
|
|
2392
|
+
kind: 'ebk-protected-load-endpoint-refusal',
|
|
2393
|
+
status: 'not-found',
|
|
2394
|
+
},
|
|
2395
|
+
{ status: 404 },
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
export async function runPerformanceScenarioEndpoint(
|
|
2400
|
+
adapterId: ManagedAdapterId,
|
|
2401
|
+
request: Request,
|
|
2402
|
+
) {
|
|
2403
|
+
try {
|
|
2404
|
+
const body =
|
|
2405
|
+
request.method === 'POST'
|
|
2406
|
+
? await boundedRequestBody(request)
|
|
2407
|
+
: undefined;
|
|
2408
|
+
const result = await withEndpointBinding(adapterId, (binding) =>
|
|
2409
|
+
handleProtectedLoadScenarioEndpoint({
|
|
2410
|
+
binding,
|
|
2411
|
+
execute: async ({ authorization, effectId }) => {
|
|
2412
|
+
if (
|
|
2413
|
+
adapterId === 'ebk-async-worker-v1' ||
|
|
2414
|
+
adapterId === 'ebk-database-v1'
|
|
2415
|
+
) {
|
|
2416
|
+
if (!effectId) throw new Error('protected-effect-required');
|
|
2417
|
+
return databaseExecution(adapterId, effectId, authorization);
|
|
2418
|
+
}
|
|
2419
|
+
return { saturationPercent: memorySaturation() };
|
|
2420
|
+
},
|
|
2421
|
+
request: requestMaterial(request, body),
|
|
2422
|
+
reserve: reserveRequest,
|
|
2423
|
+
}),
|
|
2424
|
+
);
|
|
2425
|
+
return Response.json(result.body, { status: result.status });
|
|
2426
|
+
} catch {
|
|
2427
|
+
return refused();
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
export async function runPerformanceCleanupEndpoint(
|
|
2432
|
+
adapterId: 'ebk-async-worker-v1' | 'ebk-database-v1',
|
|
2433
|
+
request: Request,
|
|
2434
|
+
) {
|
|
2435
|
+
try {
|
|
2436
|
+
const body = await boundedRequestBody(request);
|
|
2437
|
+
const result = await withEndpointBinding(adapterId, (binding) =>
|
|
2438
|
+
handleProtectedLoadCleanupEndpoint({
|
|
2439
|
+
binding,
|
|
2440
|
+
cleanup: (authorization) => cleanupRun(adapterId, authorization),
|
|
2441
|
+
request: requestMaterial(request, body),
|
|
2442
|
+
}),
|
|
2443
|
+
);
|
|
2444
|
+
return Response.json(result.body, { status: result.status });
|
|
2445
|
+
} catch {
|
|
2446
|
+
return refused();
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
`,
|
|
2450
|
+
'web/src/app/%5F%5Febk/performance/handler.spec.ts': `import {
|
|
2451
|
+
beforeAll,
|
|
2452
|
+
describe,
|
|
2453
|
+
expect,
|
|
2454
|
+
it,
|
|
2455
|
+
vi,
|
|
2456
|
+
} from 'vitest';
|
|
2457
|
+
|
|
2458
|
+
const mocks = vi.hoisted(() => {
|
|
2459
|
+
const runWithSecret = vi.fn(async ({ operation }) =>
|
|
2460
|
+
operation({
|
|
2461
|
+
current: {
|
|
2462
|
+
id: 'performance-current-v1',
|
|
2463
|
+
secret: 'current-performance-secret-material-0001',
|
|
2464
|
+
},
|
|
2465
|
+
}),
|
|
2466
|
+
);
|
|
2467
|
+
return {
|
|
2468
|
+
createSecretRuntime: vi.fn(() => ({ runWithSecret })),
|
|
2469
|
+
runWithSecret,
|
|
2470
|
+
};
|
|
2471
|
+
});
|
|
2472
|
+
|
|
2473
|
+
vi.mock('server-only', () => ({}));
|
|
2474
|
+
vi.mock('@empire-builder-kit/runtime/performance', () => ({
|
|
2475
|
+
handleProtectedLoadCleanupEndpoint: vi.fn(),
|
|
2476
|
+
handleProtectedLoadScenarioEndpoint: vi.fn(async ({ binding }) => ({
|
|
2477
|
+
body: {
|
|
2478
|
+
candidateDigest: binding.deployedCandidateDigest,
|
|
2479
|
+
kind: 'ebk-protected-load-endpoint-telemetry',
|
|
2480
|
+
status: 'accepted',
|
|
2481
|
+
},
|
|
2482
|
+
status: 200,
|
|
2483
|
+
})),
|
|
2484
|
+
}));
|
|
2485
|
+
vi.mock('../../../../../database/runtime-client', () => ({
|
|
2486
|
+
withPrimaryDatabase: vi.fn(),
|
|
2487
|
+
}));
|
|
2488
|
+
vi.mock('../../../../../secrets/declarations', () => ({
|
|
2489
|
+
Secrets: { ${pascalName}PerformanceLoadHmacKeys: {} },
|
|
2490
|
+
}));
|
|
2491
|
+
vi.mock('../../../../../secrets/runtime', () => ({
|
|
2492
|
+
createStandardSliceSecretRuntime: mocks.createSecretRuntime,
|
|
2493
|
+
}));
|
|
2494
|
+
|
|
2495
|
+
import { runPerformanceScenarioEndpoint } from './handler';
|
|
2496
|
+
|
|
2497
|
+
describe('${name} protected performance secret cache lifetime', () => {
|
|
2498
|
+
beforeAll(() => {
|
|
2499
|
+
process.env.EBK_PERFORMANCE_ACCOUNT_FINGERPRINT =
|
|
2500
|
+
'sha256:' + 'a'.repeat(64);
|
|
2501
|
+
process.env.EBK_PERFORMANCE_CANDIDATE_DIGEST = 'b'.repeat(64);
|
|
2502
|
+
process.env.EBK_PERFORMANCE_LOAD_ENABLED = 'true';
|
|
2503
|
+
process.env.EBK_PERFORMANCE_ORIGIN =
|
|
2504
|
+
'https://performance.example.test';
|
|
2505
|
+
process.env.EBK_PERFORMANCE_REGION = 'us-east-1';
|
|
2506
|
+
process.env.EBK_PERFORMANCE_RELEASE_ID = 'release-handler-test';
|
|
2507
|
+
process.env.EBK_PERFORMANCE_RELEASE_MATERIAL_DIGEST = 'd'.repeat(64);
|
|
2508
|
+
process.env.EBK_PERFORMANCE_SLICE = '${name}';
|
|
2509
|
+
process.env.EBK_SOURCE_COMMIT = 'c'.repeat(40);
|
|
2510
|
+
process.env.EBK_STAGE = 'staging';
|
|
2511
|
+
});
|
|
2512
|
+
|
|
2513
|
+
it('reuses one resolver across requests so declaration TTL is effective', async () => {
|
|
2514
|
+
const endpoint =
|
|
2515
|
+
'https://performance.example.test/${name}/__ebk/performance/api-lambda';
|
|
2516
|
+
await expect(
|
|
2517
|
+
runPerformanceScenarioEndpoint(
|
|
2518
|
+
'ebk-api-lambda-v1',
|
|
2519
|
+
new Request(endpoint),
|
|
2520
|
+
),
|
|
2521
|
+
).resolves.toMatchObject({ status: 200 });
|
|
2522
|
+
await expect(
|
|
2523
|
+
runPerformanceScenarioEndpoint(
|
|
2524
|
+
'ebk-api-lambda-v1',
|
|
2525
|
+
new Request(endpoint),
|
|
2526
|
+
),
|
|
2527
|
+
).resolves.toMatchObject({ status: 200 });
|
|
2528
|
+
expect(mocks.createSecretRuntime).toHaveBeenCalledTimes(1);
|
|
2529
|
+
expect(mocks.runWithSecret).toHaveBeenCalledTimes(2);
|
|
2530
|
+
});
|
|
2531
|
+
});
|
|
2532
|
+
`,
|
|
2533
|
+
'integration/performance-load-cleanup.spec.ts': `import { Client } from 'pg';
|
|
2534
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2535
|
+
import type { ProtectedLoadRequestAuthorizationV1 } from '@empire-builder-kit/runtime/performance';
|
|
2536
|
+
import { createInboxCleanupHandler } from '../async/inbox-cleanup.js';
|
|
2537
|
+
import {
|
|
2538
|
+
cleanupRun,
|
|
2539
|
+
databaseExecution,
|
|
2540
|
+
reserveRequest,
|
|
2541
|
+
} from '../web/src/app/%5F%5Febk/performance/handler.js';
|
|
2542
|
+
|
|
2543
|
+
vi.mock('server-only', () => ({}));
|
|
2544
|
+
vi.mock('../database/runtime-client.js', () => ({
|
|
2545
|
+
withPrimaryDatabase: async (
|
|
2546
|
+
_context: unknown,
|
|
2547
|
+
_workload: unknown,
|
|
2548
|
+
operation: (client: Client) => Promise<unknown>,
|
|
2549
|
+
) => {
|
|
2550
|
+
const { Client: RuntimeClient } = await import('pg');
|
|
2551
|
+
const client = new RuntimeClient({
|
|
2552
|
+
connectionString: process.env.DATABASE_URL,
|
|
2553
|
+
});
|
|
2554
|
+
await client.connect();
|
|
2555
|
+
try {
|
|
2556
|
+
return await operation(client);
|
|
2557
|
+
} finally {
|
|
2558
|
+
await client.end();
|
|
2559
|
+
}
|
|
2560
|
+
},
|
|
2561
|
+
}));
|
|
2562
|
+
|
|
2563
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
2564
|
+
if (!databaseUrl) {
|
|
2565
|
+
throw new Error(
|
|
2566
|
+
'DATABASE_URL is required for ${name} performance cleanup integration proof.',
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
const touchedPlanDigests = new Set<string>();
|
|
2571
|
+
|
|
2572
|
+
function authorization(input: {
|
|
2573
|
+
effectId: string | null;
|
|
2574
|
+
operation: 'cleanup' | 'execute';
|
|
2575
|
+
planMaterialDigest: string;
|
|
2576
|
+
requestOrdinal?: number;
|
|
2577
|
+
}): ProtectedLoadRequestAuthorizationV1 {
|
|
2578
|
+
return {
|
|
2579
|
+
adapterId: 'ebk-async-worker-v1',
|
|
2580
|
+
configuredMaximum: 100,
|
|
2581
|
+
createdAt: '2026-07-29T12:00:00.000Z',
|
|
2582
|
+
durationSeconds: 60,
|
|
2583
|
+
effectId: input.effectId,
|
|
2584
|
+
expiresAt: '2026-07-29T13:00:00.000Z',
|
|
2585
|
+
hmacKeyId: 'integration-v1',
|
|
2586
|
+
maximumRequests: 10,
|
|
2587
|
+
operation: input.operation,
|
|
2588
|
+
planMaterialDigest: input.planMaterialDigest,
|
|
2589
|
+
release: {
|
|
2590
|
+
candidateDigest: 'c'.repeat(64),
|
|
2591
|
+
releaseId: 'integration-release',
|
|
2592
|
+
releaseMaterialDigest: 'd'.repeat(64),
|
|
2593
|
+
sourceCommit: 'b'.repeat(40),
|
|
2594
|
+
},
|
|
2595
|
+
requestOrdinal: input.requestOrdinal ?? 1,
|
|
2596
|
+
runId: 'integration-run-' + input.planMaterialDigest.slice(0, 8),
|
|
2597
|
+
saturationSignalRef:
|
|
2598
|
+
'database:protected-async-backlog-utilization-percent',
|
|
2599
|
+
scenarioId: 'async-burst',
|
|
2600
|
+
stage: 'staging',
|
|
2601
|
+
target: {
|
|
2602
|
+
accountFingerprint: 'sha256:' + 'e'.repeat(64),
|
|
2603
|
+
endpoint:
|
|
2604
|
+
'https://performance.example.test/${name}/__ebk/performance/async-worker',
|
|
2605
|
+
region: 'us-east-1',
|
|
2606
|
+
slice: '${name}',
|
|
2607
|
+
},
|
|
2608
|
+
workload: 'async-worker',
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
async function withClient<Result>(
|
|
2613
|
+
operation: (client: Client) => Promise<Result>,
|
|
2614
|
+
): Promise<Result> {
|
|
2615
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
2616
|
+
await client.connect();
|
|
2617
|
+
try {
|
|
2618
|
+
return await operation(client);
|
|
2619
|
+
} finally {
|
|
2620
|
+
await client.end();
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// A loaded-host budget: hosted runners share four cores between Vitest,
|
|
2625
|
+
// the proof registry, and the dockerized database.
|
|
2626
|
+
async function waitForActiveStatement(
|
|
2627
|
+
client: Client,
|
|
2628
|
+
fragment: string,
|
|
2629
|
+
): Promise<void> {
|
|
2630
|
+
const deadline = Date.now() + 15_000;
|
|
2631
|
+
while (Date.now() < deadline) {
|
|
2632
|
+
const active = await client.query<{ count: string }>(
|
|
2633
|
+
\`SELECT count(*)::text AS count
|
|
2634
|
+
FROM pg_stat_activity
|
|
2635
|
+
WHERE datname = current_database()
|
|
2636
|
+
AND pid <> pg_backend_pid()
|
|
2637
|
+
AND state = 'active'
|
|
2638
|
+
AND query LIKE $1\`,
|
|
2639
|
+
['%' + fragment + '%'],
|
|
2640
|
+
);
|
|
2641
|
+
if (Number(active.rows[0]?.count ?? 0) > 0) return;
|
|
2642
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2643
|
+
}
|
|
2644
|
+
throw new Error('Timed out waiting for the adversarial database statement.');
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
afterEach(async () => {
|
|
2648
|
+
await withClient(async (client) => {
|
|
2649
|
+
await client
|
|
2650
|
+
.query(
|
|
2651
|
+
'DROP TRIGGER IF EXISTS ebk_performance_delay_effect ON performance_load_async_effects',
|
|
2652
|
+
)
|
|
2653
|
+
.catch(() => undefined);
|
|
2654
|
+
await client
|
|
2655
|
+
.query('DROP FUNCTION IF EXISTS ebk_performance_delay_effect()')
|
|
2656
|
+
.catch(() => undefined);
|
|
2657
|
+
for (const planDigest of touchedPlanDigests) {
|
|
2658
|
+
await client.query('DELETE FROM records WHERE run_scope = $1', [
|
|
2659
|
+
planDigest,
|
|
2660
|
+
]);
|
|
2661
|
+
await client.query(
|
|
2662
|
+
'DELETE FROM performance_load_async_effects WHERE plan_digest = $1',
|
|
2663
|
+
[planDigest],
|
|
2664
|
+
);
|
|
2665
|
+
await client.query(
|
|
2666
|
+
'DELETE FROM performance_load_requests WHERE plan_digest = $1',
|
|
2667
|
+
[planDigest],
|
|
2668
|
+
);
|
|
2669
|
+
await client.query(
|
|
2670
|
+
'DELETE FROM performance_load_runs WHERE plan_digest = $1',
|
|
2671
|
+
[planDigest],
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
await client.query(
|
|
2675
|
+
"DELETE FROM cleanup_deadline_reports WHERE feature = 'performance-load'",
|
|
2676
|
+
);
|
|
2677
|
+
});
|
|
2678
|
+
touchedPlanDigests.clear();
|
|
2679
|
+
});
|
|
2680
|
+
|
|
2681
|
+
describe('${name} protected performance cleanup integration', () => {
|
|
2682
|
+
it('serializes cleanup behind an in-flight effect and rejects every late effect', async () => {
|
|
2683
|
+
const planMaterialDigest = '1'.repeat(64);
|
|
2684
|
+
const effectId = '2'.repeat(64);
|
|
2685
|
+
touchedPlanDigests.add(planMaterialDigest);
|
|
2686
|
+
const executeAuthorization = authorization({
|
|
2687
|
+
effectId,
|
|
2688
|
+
operation: 'execute',
|
|
2689
|
+
planMaterialDigest,
|
|
2690
|
+
});
|
|
2691
|
+
const cleanupAuthorization = authorization({
|
|
2692
|
+
effectId: null,
|
|
2693
|
+
operation: 'cleanup',
|
|
2694
|
+
planMaterialDigest,
|
|
2695
|
+
});
|
|
2696
|
+
|
|
2697
|
+
await reserveRequest(executeAuthorization);
|
|
2698
|
+
await withClient(async (client) => {
|
|
2699
|
+
await client.query(
|
|
2700
|
+
\`CREATE OR REPLACE FUNCTION ebk_performance_delay_effect()
|
|
2701
|
+
RETURNS trigger
|
|
2702
|
+
LANGUAGE plpgsql
|
|
2703
|
+
AS $$
|
|
2704
|
+
BEGIN
|
|
2705
|
+
PERFORM pg_sleep(1);
|
|
2706
|
+
RETURN NEW;
|
|
2707
|
+
END
|
|
2708
|
+
$$\`,
|
|
2709
|
+
);
|
|
2710
|
+
await client.query(
|
|
2711
|
+
\`CREATE TRIGGER ebk_performance_delay_effect
|
|
2712
|
+
BEFORE INSERT ON performance_load_async_effects
|
|
2713
|
+
FOR EACH ROW EXECUTE FUNCTION ebk_performance_delay_effect()\`,
|
|
2714
|
+
);
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
const observer = new Client({ connectionString: databaseUrl });
|
|
2718
|
+
await observer.connect();
|
|
2719
|
+
try {
|
|
2720
|
+
const effect = databaseExecution(
|
|
2721
|
+
'ebk-async-worker-v1',
|
|
2722
|
+
effectId,
|
|
2723
|
+
executeAuthorization,
|
|
2724
|
+
);
|
|
2725
|
+
await waitForActiveStatement(
|
|
2726
|
+
observer,
|
|
2727
|
+
'INSERT INTO performance_load_async_effects',
|
|
2728
|
+
);
|
|
2729
|
+
const cleanup = cleanupRun(
|
|
2730
|
+
'ebk-async-worker-v1',
|
|
2731
|
+
cleanupAuthorization,
|
|
2732
|
+
);
|
|
2733
|
+
await expect(effect).resolves.toMatchObject({ effectState: 'created' });
|
|
2734
|
+
await expect(cleanup).resolves.toEqual({
|
|
2735
|
+
remainingEffectCount: 0,
|
|
2736
|
+
removedEffectCount: 1,
|
|
2737
|
+
});
|
|
2738
|
+
} finally {
|
|
2739
|
+
await observer.end();
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
await expect(
|
|
2743
|
+
databaseExecution(
|
|
2744
|
+
'ebk-async-worker-v1',
|
|
2745
|
+
'3'.repeat(64),
|
|
2746
|
+
{
|
|
2747
|
+
...executeAuthorization,
|
|
2748
|
+
effectId: '3'.repeat(64),
|
|
2749
|
+
requestOrdinal: 2,
|
|
2750
|
+
},
|
|
2751
|
+
),
|
|
2752
|
+
).rejects.toThrow('protected-performance-run-cleaning');
|
|
2753
|
+
await expect(
|
|
2754
|
+
reserveRequest({
|
|
2755
|
+
...executeAuthorization,
|
|
2756
|
+
effectId: '3'.repeat(64),
|
|
2757
|
+
requestOrdinal: 2,
|
|
2758
|
+
}),
|
|
2759
|
+
).rejects.toThrow('protected-performance-run-collision');
|
|
2760
|
+
|
|
2761
|
+
await withClient(async (client) => {
|
|
2762
|
+
const state = await client.query<{
|
|
2763
|
+
cleanupStarted: boolean;
|
|
2764
|
+
effects: string;
|
|
2765
|
+
}>(
|
|
2766
|
+
\`SELECT
|
|
2767
|
+
run.cleanup_started_at IS NOT NULL AS "cleanupStarted",
|
|
2768
|
+
(
|
|
2769
|
+
SELECT count(*)::text
|
|
2770
|
+
FROM performance_load_async_effects AS effect
|
|
2771
|
+
WHERE effect.plan_digest = run.plan_digest
|
|
2772
|
+
) AS effects
|
|
2773
|
+
FROM performance_load_runs AS run
|
|
2774
|
+
WHERE run.plan_digest = $1\`,
|
|
2775
|
+
[planMaterialDigest],
|
|
2776
|
+
);
|
|
2777
|
+
expect(state.rows).toEqual([
|
|
2778
|
+
{ cleanupStarted: true, effects: '0' },
|
|
2779
|
+
]);
|
|
2780
|
+
});
|
|
2781
|
+
});
|
|
2782
|
+
|
|
2783
|
+
it('tombstones and proves an effect-free run absent without a local ledger', async () => {
|
|
2784
|
+
const planMaterialDigest = '4'.repeat(64);
|
|
2785
|
+
touchedPlanDigests.add(planMaterialDigest);
|
|
2786
|
+
const cleanupAuthorization = authorization({
|
|
2787
|
+
effectId: null,
|
|
2788
|
+
operation: 'cleanup',
|
|
2789
|
+
planMaterialDigest,
|
|
2790
|
+
});
|
|
2791
|
+
|
|
2792
|
+
await expect(
|
|
2793
|
+
cleanupRun('ebk-async-worker-v1', cleanupAuthorization),
|
|
2794
|
+
).resolves.toEqual({
|
|
2795
|
+
remainingEffectCount: 0,
|
|
2796
|
+
removedEffectCount: 0,
|
|
2797
|
+
});
|
|
2798
|
+
await expect(
|
|
2799
|
+
cleanupRun('ebk-async-worker-v1', cleanupAuthorization),
|
|
2800
|
+
).resolves.toEqual({
|
|
2801
|
+
remainingEffectCount: 0,
|
|
2802
|
+
removedEffectCount: 0,
|
|
2803
|
+
});
|
|
2804
|
+
await expect(
|
|
2805
|
+
reserveRequest({
|
|
2806
|
+
...cleanupAuthorization,
|
|
2807
|
+
effectId: '5'.repeat(64),
|
|
2808
|
+
operation: 'execute',
|
|
2809
|
+
}),
|
|
2810
|
+
).rejects.toThrow('protected-performance-run-collision');
|
|
2811
|
+
});
|
|
2812
|
+
|
|
2813
|
+
it('expires only framework-owned due rows and retains fresh audit state', async () => {
|
|
2814
|
+
const oldPlan = '6'.repeat(64);
|
|
2815
|
+
const freshPlan = '7'.repeat(64);
|
|
2816
|
+
const expiredEffectPlan = '8'.repeat(64);
|
|
2817
|
+
const freshEffectPlan = '9'.repeat(64);
|
|
2818
|
+
for (const digest of [
|
|
2819
|
+
oldPlan,
|
|
2820
|
+
freshPlan,
|
|
2821
|
+
expiredEffectPlan,
|
|
2822
|
+
freshEffectPlan,
|
|
2823
|
+
]) {
|
|
2824
|
+
touchedPlanDigests.add(digest);
|
|
2825
|
+
}
|
|
2826
|
+
const expiredRecordId = '10000000-0000-4000-8000-000000000001';
|
|
2827
|
+
const freshRecordId = '10000000-0000-4000-8000-000000000002';
|
|
2828
|
+
const adopterRecordId = '10000000-0000-4000-8000-000000000003';
|
|
2829
|
+
|
|
2830
|
+
await withClient(async (client) => {
|
|
2831
|
+
await client.query(
|
|
2832
|
+
\`INSERT INTO performance_load_runs
|
|
2833
|
+
(plan_digest, run_id, adapter_id, scenario_id, stage,
|
|
2834
|
+
source_commit, release_material_digest, maximum_requests,
|
|
2835
|
+
created_at, expires_at)
|
|
2836
|
+
VALUES
|
|
2837
|
+
($1, 'old-run', 'ebk-async-worker-v1', 'async-burst', 'staging',
|
|
2838
|
+
$3, $4, 10, now() - INTERVAL '32 days',
|
|
2839
|
+
now() - INTERVAL '31 days'),
|
|
2840
|
+
($2, 'fresh-run', 'ebk-async-worker-v1', 'async-burst', 'staging',
|
|
2841
|
+
$3, $4, 10, now() - INTERVAL '30 days',
|
|
2842
|
+
now() - INTERVAL '29 days')\`,
|
|
2843
|
+
[oldPlan, freshPlan, 'b'.repeat(40), 'd'.repeat(64)],
|
|
2844
|
+
);
|
|
2845
|
+
await client.query(
|
|
2846
|
+
\`INSERT INTO performance_load_requests
|
|
2847
|
+
(plan_digest, request_ordinal, effect_id)
|
|
2848
|
+
VALUES ($1, 1, NULL), ($2, 1, NULL)\`,
|
|
2849
|
+
[oldPlan, freshPlan],
|
|
2850
|
+
);
|
|
2851
|
+
await client.query(
|
|
2852
|
+
\`INSERT INTO performance_load_async_effects
|
|
2853
|
+
(effect_id, plan_digest, run_id, expires_at)
|
|
2854
|
+
VALUES
|
|
2855
|
+
($1, $2, 'expired', now() - INTERVAL '1 minute'),
|
|
2856
|
+
($3, $4, 'fresh', now() + INTERVAL '1 day')\`,
|
|
2857
|
+
['a'.repeat(64), expiredEffectPlan, 'f'.repeat(64), freshEffectPlan],
|
|
2858
|
+
);
|
|
2859
|
+
await client.query(
|
|
2860
|
+
\`INSERT INTO records
|
|
2861
|
+
(id, name, owner_id, run_scope, test_expires_at)
|
|
2862
|
+
VALUES
|
|
2863
|
+
($1::uuid, 'ebk-protected-performance-load', 'ebk-performance',
|
|
2864
|
+
$4, now() - INTERVAL '1 minute'),
|
|
2865
|
+
($2::uuid, 'ebk-protected-performance-load', 'ebk-performance',
|
|
2866
|
+
$5, now() + INTERVAL '1 day'),
|
|
2867
|
+
($3::uuid, 'adopter-performance-data', 'adopter-owner',
|
|
2868
|
+
$4, now() - INTERVAL '1 minute')\`,
|
|
2869
|
+
[
|
|
2870
|
+
expiredRecordId,
|
|
2871
|
+
freshRecordId,
|
|
2872
|
+
adopterRecordId,
|
|
2873
|
+
expiredEffectPlan,
|
|
2874
|
+
freshEffectPlan,
|
|
2875
|
+
],
|
|
2876
|
+
);
|
|
2877
|
+
});
|
|
2878
|
+
|
|
2879
|
+
const emitted: Array<{ metricName?: string; value?: number }> = [];
|
|
2880
|
+
const handler = createInboxCleanupHandler({
|
|
2881
|
+
logger: {
|
|
2882
|
+
error: vi.fn(),
|
|
2883
|
+
info: vi.fn(),
|
|
2884
|
+
} as never,
|
|
2885
|
+
metrics: {
|
|
2886
|
+
emit: (metric: { metricName?: string; value?: number }) => {
|
|
2887
|
+
emitted.push(metric);
|
|
2888
|
+
return {} as never;
|
|
2889
|
+
},
|
|
2890
|
+
} as never,
|
|
2891
|
+
now: Date.now,
|
|
2892
|
+
withDatabase: (async (
|
|
2893
|
+
_context: unknown,
|
|
2894
|
+
_workload: unknown,
|
|
2895
|
+
operation: (client: Client) => Promise<unknown>,
|
|
2896
|
+
) => withClient(operation)) as never,
|
|
2897
|
+
});
|
|
2898
|
+
await expect(handler({ id: 'performance-expiry-proof' })).resolves.toMatchObject({
|
|
2899
|
+
performanceAuditRetentionDays: 30,
|
|
2900
|
+
performanceCleanupDeadlineHours: 24,
|
|
2901
|
+
reportedPerformanceDeadlineBreaches: 0,
|
|
2902
|
+
remainingPerformanceExpired: 0,
|
|
2903
|
+
removedPerformance: 4,
|
|
2904
|
+
});
|
|
2905
|
+
expect(emitted).toContainEqual(
|
|
2906
|
+
expect.objectContaining({
|
|
2907
|
+
metricName: 'PerformanceLoadCleanupBacklog',
|
|
2908
|
+
value: 0,
|
|
2909
|
+
}),
|
|
2910
|
+
);
|
|
2911
|
+
expect(emitted).toContainEqual(
|
|
2912
|
+
expect.objectContaining({
|
|
2913
|
+
metricName: 'PerformanceLoadCleanupDeadlineBreaches',
|
|
2914
|
+
value: 0,
|
|
2915
|
+
}),
|
|
2916
|
+
);
|
|
2917
|
+
|
|
2918
|
+
await withClient(async (client) => {
|
|
2919
|
+
const effects = await client.query<{ planDigest: string }>(
|
|
2920
|
+
\`SELECT plan_digest AS "planDigest"
|
|
2921
|
+
FROM performance_load_async_effects
|
|
2922
|
+
WHERE plan_digest = ANY($1::text[])
|
|
2923
|
+
ORDER BY plan_digest\`,
|
|
2924
|
+
[[expiredEffectPlan, freshEffectPlan]],
|
|
2925
|
+
);
|
|
2926
|
+
expect(effects.rows).toEqual([{ planDigest: freshEffectPlan }]);
|
|
2927
|
+
const records = await client.query<{ id: string }>(
|
|
2928
|
+
\`SELECT id::text AS id
|
|
2929
|
+
FROM records
|
|
2930
|
+
WHERE id = ANY($1::uuid[])
|
|
2931
|
+
ORDER BY id\`,
|
|
2932
|
+
[[expiredRecordId, freshRecordId, adopterRecordId]],
|
|
2933
|
+
);
|
|
2934
|
+
expect(records.rows).toEqual([
|
|
2935
|
+
{ id: freshRecordId },
|
|
2936
|
+
{ id: adopterRecordId },
|
|
2937
|
+
]);
|
|
2938
|
+
const audits = await client.query<{
|
|
2939
|
+
planDigest: string;
|
|
2940
|
+
requests: string;
|
|
2941
|
+
}>(
|
|
2942
|
+
\`SELECT
|
|
2943
|
+
run.plan_digest AS "planDigest",
|
|
2944
|
+
count(request.request_ordinal)::text AS requests
|
|
2945
|
+
FROM performance_load_runs AS run
|
|
2946
|
+
LEFT JOIN performance_load_requests AS request
|
|
2947
|
+
ON request.plan_digest = run.plan_digest
|
|
2948
|
+
WHERE run.plan_digest = ANY($1::text[])
|
|
2949
|
+
GROUP BY run.plan_digest
|
|
2950
|
+
ORDER BY run.plan_digest\`,
|
|
2951
|
+
[[oldPlan, freshPlan]],
|
|
2952
|
+
);
|
|
2953
|
+
expect(audits.rows).toEqual([
|
|
2954
|
+
{ planDigest: freshPlan, requests: '1' },
|
|
2955
|
+
]);
|
|
2956
|
+
});
|
|
2957
|
+
});
|
|
2958
|
+
|
|
2959
|
+
it('durably retries deadline telemetry after deleting stale protected residue', async () => {
|
|
2960
|
+
const stalePlan = 'a'.repeat(64);
|
|
2961
|
+
const staleEffectId = 'b'.repeat(64);
|
|
2962
|
+
const atDeadlinePlan = 'c'.repeat(64);
|
|
2963
|
+
const atDeadlineEffectId = 'd'.repeat(64);
|
|
2964
|
+
const beforeDeadlinePlan = 'e'.repeat(64);
|
|
2965
|
+
const beforeDeadlineEffectId = 'f'.repeat(64);
|
|
2966
|
+
const staleRecordId = '10000000-0000-4000-8000-000000000004';
|
|
2967
|
+
for (const planDigest of [
|
|
2968
|
+
stalePlan,
|
|
2969
|
+
atDeadlinePlan,
|
|
2970
|
+
beforeDeadlinePlan,
|
|
2971
|
+
]) {
|
|
2972
|
+
touchedPlanDigests.add(planDigest);
|
|
2973
|
+
}
|
|
2974
|
+
await withClient(async (client) => {
|
|
2975
|
+
await client.query(
|
|
2976
|
+
\`INSERT INTO performance_load_async_effects
|
|
2977
|
+
(effect_id, plan_digest, run_id, expires_at)
|
|
2978
|
+
VALUES
|
|
2979
|
+
($1, $2, 'stale-deadline',
|
|
2980
|
+
clock_timestamp() - INTERVAL '25 hours'),
|
|
2981
|
+
($3, $4, 'exact-deadline',
|
|
2982
|
+
clock_timestamp() - INTERVAL '24 hours'),
|
|
2983
|
+
($5, $6, 'before-deadline',
|
|
2984
|
+
clock_timestamp() - INTERVAL '23 hours 59 minutes')\`,
|
|
2985
|
+
[
|
|
2986
|
+
staleEffectId,
|
|
2987
|
+
stalePlan,
|
|
2988
|
+
atDeadlineEffectId,
|
|
2989
|
+
atDeadlinePlan,
|
|
2990
|
+
beforeDeadlineEffectId,
|
|
2991
|
+
beforeDeadlinePlan,
|
|
2992
|
+
],
|
|
2993
|
+
);
|
|
2994
|
+
await client.query(
|
|
2995
|
+
\`INSERT INTO records
|
|
2996
|
+
(id, name, owner_id, run_scope, test_expires_at)
|
|
2997
|
+
VALUES ($1::uuid, 'ebk-protected-performance-load',
|
|
2998
|
+
'ebk-performance', $2, now() - INTERVAL '25 hours')\`,
|
|
2999
|
+
[staleRecordId, stalePlan],
|
|
3000
|
+
);
|
|
3001
|
+
});
|
|
3002
|
+
|
|
3003
|
+
const failedEmissions: Array<{ metricName?: string; value?: number }> = [];
|
|
3004
|
+
const failedHandler = createInboxCleanupHandler({
|
|
3005
|
+
logger: { error: vi.fn(), info: vi.fn() } as never,
|
|
3006
|
+
metrics: {
|
|
3007
|
+
emit: (metric: { metricName?: string; value?: number }) => {
|
|
3008
|
+
failedEmissions.push(metric);
|
|
3009
|
+
return metric.metricName ===
|
|
3010
|
+
'PerformanceLoadCleanupDeadlineBreaches'
|
|
3011
|
+
? undefined
|
|
3012
|
+
: ({} as never);
|
|
3013
|
+
},
|
|
3014
|
+
} as never,
|
|
3015
|
+
now: Date.now,
|
|
3016
|
+
withDatabase: (async (
|
|
3017
|
+
_context: unknown,
|
|
3018
|
+
_workload: unknown,
|
|
3019
|
+
operation: (client: Client) => Promise<unknown>,
|
|
3020
|
+
) => withClient(operation)) as never,
|
|
3021
|
+
});
|
|
3022
|
+
await expect(
|
|
3023
|
+
failedHandler({ id: 'performance-deadline-report-failure' }),
|
|
3024
|
+
).rejects.toThrow('Performance cleanup deadline metric delivery failed.');
|
|
3025
|
+
|
|
3026
|
+
await withClient(async (client) => {
|
|
3027
|
+
const state = await client.query<{
|
|
3028
|
+
effects: string;
|
|
3029
|
+
records: string;
|
|
3030
|
+
reports: string;
|
|
3031
|
+
resourceRefs: string[];
|
|
3032
|
+
}>(
|
|
3033
|
+
\`SELECT
|
|
3034
|
+
(SELECT count(*)::text
|
|
3035
|
+
FROM performance_load_async_effects
|
|
3036
|
+
WHERE effect_id = ANY($1::text[])) AS effects,
|
|
3037
|
+
(SELECT count(*)::text
|
|
3038
|
+
FROM records
|
|
3039
|
+
WHERE id = $2::uuid) AS records,
|
|
3040
|
+
(SELECT count(*)::text
|
|
3041
|
+
FROM cleanup_deadline_reports
|
|
3042
|
+
WHERE feature = 'performance-load') AS reports,
|
|
3043
|
+
(SELECT array_agg(resource_ref ORDER BY resource_ref)
|
|
3044
|
+
FROM cleanup_deadline_reports
|
|
3045
|
+
WHERE feature = 'performance-load') AS "resourceRefs"\`,
|
|
3046
|
+
[
|
|
3047
|
+
[staleEffectId, atDeadlineEffectId, beforeDeadlineEffectId],
|
|
3048
|
+
staleRecordId,
|
|
3049
|
+
],
|
|
3050
|
+
);
|
|
3051
|
+
expect(state.rows).toEqual([
|
|
3052
|
+
{
|
|
3053
|
+
effects: '0',
|
|
3054
|
+
records: '0',
|
|
3055
|
+
reports: '3',
|
|
3056
|
+
resourceRefs: [
|
|
3057
|
+
'async-effect:' + staleEffectId,
|
|
3058
|
+
'async-effect:' + atDeadlineEffectId,
|
|
3059
|
+
'record:' + staleRecordId,
|
|
3060
|
+
].sort(),
|
|
3061
|
+
},
|
|
3062
|
+
]);
|
|
3063
|
+
});
|
|
3064
|
+
expect(JSON.stringify(failedEmissions)).not.toContain(staleEffectId);
|
|
3065
|
+
expect(JSON.stringify(failedEmissions)).not.toContain(staleRecordId);
|
|
3066
|
+
expect(JSON.stringify(failedEmissions)).not.toContain(atDeadlineEffectId);
|
|
3067
|
+
expect(JSON.stringify(failedEmissions)).not.toContain(
|
|
3068
|
+
beforeDeadlineEffectId,
|
|
3069
|
+
);
|
|
3070
|
+
|
|
3071
|
+
const retryEmissions: Array<{ metricName?: string; value?: number }> = [];
|
|
3072
|
+
const retryHandler = createInboxCleanupHandler({
|
|
3073
|
+
logger: { error: vi.fn(), info: vi.fn() } as never,
|
|
3074
|
+
metrics: {
|
|
3075
|
+
emit: (metric: { metricName?: string; value?: number }) => {
|
|
3076
|
+
retryEmissions.push(metric);
|
|
3077
|
+
return {} as never;
|
|
3078
|
+
},
|
|
3079
|
+
} as never,
|
|
3080
|
+
now: Date.now,
|
|
3081
|
+
withDatabase: (async (
|
|
3082
|
+
_context: unknown,
|
|
3083
|
+
_workload: unknown,
|
|
3084
|
+
operation: (client: Client) => Promise<unknown>,
|
|
3085
|
+
) => withClient(operation)) as never,
|
|
3086
|
+
});
|
|
3087
|
+
await expect(
|
|
3088
|
+
retryHandler({ id: 'performance-deadline-report-retry' }),
|
|
3089
|
+
).rejects.toThrow('performance-load-cleanup-deadline-breached');
|
|
3090
|
+
expect(retryEmissions).toContainEqual(
|
|
3091
|
+
expect.objectContaining({
|
|
3092
|
+
metricName: 'PerformanceLoadCleanupDeadlineBreaches',
|
|
3093
|
+
value: 3,
|
|
3094
|
+
}),
|
|
3095
|
+
);
|
|
3096
|
+
expect(JSON.stringify(retryEmissions)).not.toContain(staleEffectId);
|
|
3097
|
+
expect(JSON.stringify(retryEmissions)).not.toContain(staleRecordId);
|
|
3098
|
+
expect(JSON.stringify(retryEmissions)).not.toContain(atDeadlineEffectId);
|
|
3099
|
+
expect(JSON.stringify(retryEmissions)).not.toContain(
|
|
3100
|
+
beforeDeadlineEffectId,
|
|
3101
|
+
);
|
|
3102
|
+
|
|
3103
|
+
await withClient(async (client) => {
|
|
3104
|
+
const reports = await client.query<{ count: string }>(
|
|
3105
|
+
\`SELECT count(*)::text AS count
|
|
3106
|
+
FROM cleanup_deadline_reports
|
|
3107
|
+
WHERE feature = 'performance-load'\`,
|
|
3108
|
+
);
|
|
3109
|
+
expect(reports.rows).toEqual([{ count: '0' }]);
|
|
3110
|
+
});
|
|
3111
|
+
await expect(
|
|
3112
|
+
retryHandler({ id: 'performance-deadline-after-ack' }),
|
|
3113
|
+
).resolves.toMatchObject({ reportedPerformanceDeadlineBreaches: 0 });
|
|
3114
|
+
});
|
|
3115
|
+
});
|
|
3116
|
+
`,
|
|
3117
|
+
'web/src/app/%5F%5Febk/performance/api-lambda/route.ts': `import 'server-only';
|
|
3118
|
+
import { runPerformanceScenarioEndpoint } from '../handler';
|
|
3119
|
+
|
|
3120
|
+
export function GET(request: Request) {
|
|
3121
|
+
return runPerformanceScenarioEndpoint('ebk-api-lambda-v1', request);
|
|
3122
|
+
}
|
|
3123
|
+
`,
|
|
3124
|
+
'web/src/app/%5F%5Febk/performance/async-worker/route.ts': `import 'server-only';
|
|
3125
|
+
import { runPerformanceScenarioEndpoint } from '../handler';
|
|
3126
|
+
|
|
3127
|
+
export function POST(request: Request) {
|
|
3128
|
+
return runPerformanceScenarioEndpoint('ebk-async-worker-v1', request);
|
|
3129
|
+
}
|
|
3130
|
+
`,
|
|
3131
|
+
'web/src/app/%5F%5Febk/performance/async-worker/cleanup/route.ts': `import 'server-only';
|
|
3132
|
+
import { runPerformanceCleanupEndpoint } from '../../handler';
|
|
3133
|
+
|
|
3134
|
+
export function POST(request: Request) {
|
|
3135
|
+
return runPerformanceCleanupEndpoint('ebk-async-worker-v1', request);
|
|
3136
|
+
}
|
|
3137
|
+
`,
|
|
3138
|
+
'web/src/app/%5F%5Febk/performance/database/route.ts': `import 'server-only';
|
|
3139
|
+
import { runPerformanceScenarioEndpoint } from '../handler';
|
|
3140
|
+
|
|
3141
|
+
export function POST(request: Request) {
|
|
3142
|
+
return runPerformanceScenarioEndpoint('ebk-database-v1', request);
|
|
3143
|
+
}
|
|
3144
|
+
`,
|
|
3145
|
+
'web/src/app/%5F%5Febk/performance/database/cleanup/route.ts': `import 'server-only';
|
|
3146
|
+
import { runPerformanceCleanupEndpoint } from '../../handler';
|
|
3147
|
+
|
|
3148
|
+
export function POST(request: Request) {
|
|
3149
|
+
return runPerformanceCleanupEndpoint('ebk-database-v1', request);
|
|
3150
|
+
}
|
|
3151
|
+
`,
|
|
3152
|
+
'docs/runbooks/performance-capacity-cost.md': `# Performance, capacity, and cost
|
|
3153
|
+
|
|
3154
|
+
The generated policy is deliberately an unadopted seed. Replace example budgets,
|
|
3155
|
+
unknown capacity values, owner, review date, and allocation drivers from actual
|
|
3156
|
+
product requirements before protected promotion. Reliability remains authority
|
|
3157
|
+
for journey/SLO, alarm, and routing semantics; release and repository policy
|
|
3158
|
+
remain authority for candidate and production-profile identity. The performance
|
|
3159
|
+
contract consumes those files through explicit hooks and does not duplicate
|
|
3160
|
+
their schemas.
|
|
3161
|
+
|
|
3162
|
+
Keep warm/cold and cache hit/miss populations separate. Record p50, p95, and p99
|
|
3163
|
+
where meaningful, achieved throughput, the first exhausted dependency, quota
|
|
3164
|
+
headroom, and the scenario/tool/network limits. Estimates, test measurements,
|
|
3165
|
+
and delayed actual billing are different provenance classes. Never label a
|
|
3166
|
+
forecast or staging measurement as production actual.
|
|
3167
|
+
|
|
3168
|
+
Local commands can validate policy, budgets, capacity, and tag coverage. They
|
|
3169
|
+
cannot run protected load. The protected workflow must call
|
|
3170
|
+
\`performance-load-guard.mjs\` immediately before an allowlisted runner and
|
|
3171
|
+
must bind the exact candidate, target account, Region, endpoint, maximum
|
|
3172
|
+
concurrency/rate/requests/duration/cost, stop conditions, cleanup, and emergency
|
|
3173
|
+
termination. Production load is exceptional and requires explicit human
|
|
3174
|
+
evidence; prefer staging, synthetics, canaries, and production observation.
|
|
3175
|
+
The three base-Slice managed adapters use fixed API/Lambda, async-worker, and database
|
|
3176
|
+
request/cleanup paths. They remain disabled unless the deployed staging
|
|
3177
|
+
environment binds the exact plan, run, release, source commit, scenario, signal,
|
|
3178
|
+
adapter, exact Slice URL, and deployment-bound HMAC key ID. The workflow obtains
|
|
3179
|
+
only the current per-Slice signer from the exact AWSCURRENT Secrets Manager
|
|
3180
|
+
generation after OIDC target verification. Each response must return an
|
|
3181
|
+
exact-release JSON telemetry envelope for its reviewed saturation signal.
|
|
3182
|
+
Adding a generated Service promotes its reserved scenario to the managed
|
|
3183
|
+
Service runtime endpoint, exact per-task admission signal, and cleanup ledger;
|
|
3184
|
+
before that generator runs, Service load remains explicitly blocked.
|
|
3185
|
+
Next/Lambda heap data is never relabeled as Service evidence. The runner
|
|
3186
|
+
computes estimated test cost from the reviewed per-request
|
|
3187
|
+
scenario value; target headers are never cost authority. Async and database
|
|
3188
|
+
adapters persist a deterministic cleanup identity before each request and
|
|
3189
|
+
cleanup must prove every identity absent, including after a runner crash.
|
|
3190
|
+
The plan target is the exact fixed adapter request URL—not merely an origin or
|
|
3191
|
+
base path—so target approval and the URL actually exercised remain identical.
|
|
3192
|
+
The scenario signal must resolve exactly one capacity resource in the
|
|
3193
|
+
plan-bound policy; the report binds the adapter and complete scenario-contract
|
|
3194
|
+
digest so changed cost, signal, or adoption material cannot reuse evidence.
|
|
3195
|
+
|
|
3196
|
+
## Performance load HMAC rotation
|
|
3197
|
+
|
|
3198
|
+
The HMAC secret is scoped to one Slice and contains exact \`current\` and
|
|
3199
|
+
optional \`previous\` records. The endpoint keeps one resolver for the deployed
|
|
3200
|
+
web workload, caches a successful AWSCURRENT read for at most one second, and
|
|
3201
|
+
has no stale fallback. Execution accepts only \`current\`; cleanup accepts
|
|
3202
|
+
\`current\` or the immediately previous generation so an interrupted old run
|
|
3203
|
+
can remove its effects. This control-plane read rate and the corresponding
|
|
3204
|
+
Secrets Manager request cost belong in the load cost model.
|
|
3205
|
+
|
|
3206
|
+
Each \`secret\` is the canonical unpadded base64url encoding of exactly 32
|
|
3207
|
+
cryptographically random bytes (43 characters). Generate a new value with a
|
|
3208
|
+
reviewed cryptographic random-byte source; a long passphrase, repeated text, or
|
|
3209
|
+
arbitrary 32-character string is rejected.
|
|
3210
|
+
|
|
3211
|
+
For routine rotation, disable new performance runs, publish
|
|
3212
|
+
\`{ current: new, previous: old }\`, deploy, and prove every endpoint observes
|
|
3213
|
+
the new key within the one-second cache bound. Then switch the protected signer
|
|
3214
|
+
to the new current key and re-enable new runs. Retain previous until all jobs
|
|
3215
|
+
signed by it are terminal, cleanup and expiry backlogs are zero, and both the
|
|
3216
|
+
24-hour authorization window and subsequent 24-hour cleanup window have
|
|
3217
|
+
elapsed. Do not start another rotation or retire previous during that overlap.
|
|
3218
|
+
The hourly worker measures residue at least 24 hours past its
|
|
3219
|
+
eligible expiry, emits \`PerformanceLoadCleanupDeadlineBreaches\`, and fails the
|
|
3220
|
+
invocation so the dedicated alarm cannot silently clear. This is an executable
|
|
3221
|
+
deadline-violation signal, not a claim that deletion succeeds through an AWS or
|
|
3222
|
+
database outage. Previous-key retirement remains a protected operator action;
|
|
3223
|
+
operators must not retire it while either cleanup metric is nonzero.
|
|
3224
|
+
For suspected compromise, disable the workflow and endpoint first, rotate,
|
|
3225
|
+
redeploy, and investigate before restoring access.
|
|
3226
|
+
|
|
3227
|
+
Evidence retains only the bounded HMAC key ID. Never log, hash, upload, or place
|
|
3228
|
+
the secret in GitHub environment output. Live rotation evidence must show
|
|
3229
|
+
current execution succeeds, previous execution fails, previous cleanup
|
|
3230
|
+
succeeds, retired cleanup fails, cross-Slice substitution fails, and no secret
|
|
3231
|
+
sentinel appears in logs or retained artifacts.
|
|
3232
|
+
|
|
3233
|
+
## AWS cost controls
|
|
3234
|
+
|
|
3235
|
+
Protected deployments fail until the seeded cost policy names a real owner, a
|
|
3236
|
+
future review instant, and declares the account, pilot, anomaly, notification,
|
|
3237
|
+
and per-test values adopted. Stable v1 provisions these controls only in the
|
|
3238
|
+
commercial \`aws\` partition, where the workload account is also the billing
|
|
3239
|
+
authority. GovCloud, China, and isolated partitions fail before resource
|
|
3240
|
+
construction; a separately modeled commercial billing-account provider is
|
|
3241
|
+
required before those partitions can adopt this path. The protected Foundation
|
|
3242
|
+
stack is the sole owner of the account monthly actual/forecast Budget. It
|
|
3243
|
+
publishes the exact policy digest and monthly maximum through Foundation
|
|
3244
|
+
discovery. Every protected Product plan must bind both and adopt the same
|
|
3245
|
+
maximum before creating a cost resource. The production Product stack creates
|
|
3246
|
+
only the Slice-tag-filtered custom Cost Anomaly monitor plus its immediate SNS
|
|
3247
|
+
subscription. The custom monitor avoids the
|
|
3248
|
+
one-per-account dimensional-monitor collision when several Slices coexist. The
|
|
3249
|
+
staging Product stack creates the lower pilot monthly Budget filtered to the exact
|
|
3250
|
+
\`ebk:slice\` and \`ebk:stage=staging\` allocation tags. Personal and
|
|
3251
|
+
development stages create none of these global billing resources.
|
|
3252
|
+
|
|
3253
|
+
Both Budgets and the anomaly subscription route only to the Foundation alert
|
|
3254
|
+
topic and retain the policy digest, semantic route, and named owner in resource
|
|
3255
|
+
tags. The Foundation topic policy must allow only \`sns:Publish\` from
|
|
3256
|
+
\`budgets.amazonaws.com\`, \`costalerts.amazonaws.com\`, and its existing RDS
|
|
3257
|
+
events publisher. These controls notify; generated infrastructure contains no
|
|
3258
|
+
BudgetAction, auto-adjustment, stop, throttle, IAM-policy change, or other
|
|
3259
|
+
automatic spend-changing response. Investigate through the named owner and
|
|
3260
|
+
runbook, then use a separately approved owning tool for any intervention.
|
|
3261
|
+
|
|
3262
|
+
Every supported resource must carry the bounded workspace/Slice/stage/owner/
|
|
3263
|
+
repository/profile/managed-by/framework tags where AWS supports them.
|
|
3264
|
+
Unsupported resources remain visible with a reason and shared-cost allocation
|
|
3265
|
+
reference. Never use feature, user, tenant, request, work-item, payload, or raw
|
|
3266
|
+
error identifiers as resource tags or metric dimensions.
|
|
3267
|
+
Tag coverage compares observations with a nonempty exact resource inventory
|
|
3268
|
+
derived from deployment plan/state and bound to the same release; an empty,
|
|
3269
|
+
omitted, extra, or cross-release inventory fails. The external cost collection
|
|
3270
|
+
contract names its adopter-owned read-only collector and requires canonical
|
|
3271
|
+
closed-window AWS Cost Explorer allocation and total queries. Evidence binds
|
|
3272
|
+
the STS-derived account fingerprint, Region, stage, workspace, Slice, period,
|
|
3273
|
+
release/source, request identities, response digests, reconciled allocation
|
|
3274
|
+
rows, and independently sourced aggregate-count evidence. Generation does not
|
|
3275
|
+
manufacture live cost values.
|
|
3276
|
+
The managed PR budget derives its observation directly from the completed
|
|
3277
|
+
Next.js client artifact and does not consume caller-authored measurements.
|
|
3278
|
+
Adopter performance budgets remain a separate readiness and exact-release
|
|
3279
|
+
evaluation. Unit-cost evaluation validates baseline and current billing
|
|
3280
|
+
evidence before it accepts observations. Each of the two capability
|
|
3281
|
+
observations must name the exact allocation and aggregate identifiers, amounts,
|
|
3282
|
+
counts, evidence digests, account, Region, stage, workspace, Slice, period, and
|
|
3283
|
+
release from that validated evidence; clean generation reports the missing live
|
|
3284
|
+
evidence as incomplete.
|
|
3285
|
+
|
|
3286
|
+
The generated scenarios cover API/Lambda, async worker, database cold resume,
|
|
3287
|
+
and a Service extension that becomes managed when its Service is generated,
|
|
3288
|
+
but are disabled until deliberately adopted. A clean generated Slice therefore reports incomplete rather than
|
|
3289
|
+
claiming capacity, cost, or live load evidence.
|
|
3290
|
+
`,
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
//# sourceMappingURL=built-in-plan-performance-files.js.map
|