@empire-builder-kit/nx 1.0.0-rc.2 → 1.0.0-rc.20

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.
Files changed (253) hide show
  1. package/executors.json +10 -0
  2. package/package.json +4 -4
  3. package/src/cli/output.js +1 -1
  4. package/src/developer-agent/model.js +4 -4
  5. package/src/developer-agent/model.js.map +1 -1
  6. package/src/developer-agent/operation-registry.js +66 -9
  7. package/src/developer-agent/operation-registry.js.map +1 -1
  8. package/src/developer-agent/planning.js +7 -9
  9. package/src/developer-agent/planning.js.map +1 -1
  10. package/src/developer-agent/workspace-inspection.js +12 -0
  11. package/src/developer-agent/workspace-inspection.js.map +1 -1
  12. package/src/executors/blueprint-trust/blueprint-trust.js +6 -0
  13. package/src/executors/blueprint-trust/blueprint-trust.js.map +1 -1
  14. package/src/executors/deployment-plan/deployment-plan.js +5 -0
  15. package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
  16. package/src/executors/foundation-preflight/foundation-preflight.d.ts +2 -0
  17. package/src/executors/foundation-preflight/foundation-preflight.js +6 -2
  18. package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -1
  19. package/src/executors/foundation-preflight/schema.json +10 -0
  20. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.d.ts +2 -0
  21. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js +2 -1
  22. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js.map +1 -1
  23. package/src/executors/foundation-profile-preflight/schema.json +19 -3
  24. package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
  25. package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
  26. package/src/executors/infrastructure-plan/infrastructure-plan.d.ts +229 -5
  27. package/src/executors/infrastructure-plan/infrastructure-plan.js +2298 -175
  28. package/src/executors/infrastructure-plan/infrastructure-plan.js.map +1 -1
  29. package/src/executors/infrastructure-plan/schema.json +16 -0
  30. package/src/executors/local-deploy/fixtures/local-deploy-state-machine.mjs +53 -0
  31. package/src/executors/local-deploy/local-approval.d.ts +23 -0
  32. package/src/executors/local-deploy/local-approval.js +43 -0
  33. package/src/executors/local-deploy/local-approval.js.map +1 -0
  34. package/src/executors/local-deploy/local-deploy.d.ts +69 -0
  35. package/src/executors/local-deploy/local-deploy.js +911 -0
  36. package/src/executors/local-deploy/local-deploy.js.map +1 -0
  37. package/src/executors/local-deploy/schema.json +42 -0
  38. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.d.ts +3 -0
  39. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js +94 -0
  40. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js.map +1 -0
  41. package/src/executors/router-waf-rehearsal/schema.json +9 -0
  42. package/src/executors/secret-metadata/schema.json +5 -0
  43. package/src/executors/secret-metadata/secret-metadata.d.ts +12 -1
  44. package/src/executors/secret-metadata/secret-metadata.js +68 -16
  45. package/src/executors/secret-metadata/secret-metadata.js.map +1 -1
  46. package/src/executors/sst-lifecycle/logging-preflight.d.ts +5 -0
  47. package/src/executors/sst-lifecycle/logging-preflight.js +23 -13
  48. package/src/executors/sst-lifecycle/logging-preflight.js.map +1 -1
  49. package/src/executors/sst-lifecycle/run-sst-command.d.ts +8 -1
  50. package/src/executors/sst-lifecycle/run-sst-command.js +144 -12
  51. package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
  52. package/src/executors/sst-lifecycle/schema.json +13 -1
  53. package/src/executors/sst-lifecycle/sst-environment-source.d.ts +25 -0
  54. package/src/executors/sst-lifecycle/sst-environment-source.js +213 -0
  55. package/src/executors/sst-lifecycle/sst-environment-source.js.map +1 -0
  56. package/src/foundation/index.d.ts +2 -0
  57. package/src/foundation/index.js +2 -0
  58. package/src/foundation/index.js.map +1 -1
  59. package/src/foundation/logging-preflight.js +1 -1
  60. package/src/foundation/logging-preflight.js.map +1 -1
  61. package/src/foundation/logging.d.ts +3 -1
  62. package/src/foundation/logging.js +1 -1
  63. package/src/foundation/logging.js.map +1 -1
  64. package/src/foundation/partial-state.d.ts +10 -0
  65. package/src/foundation/partial-state.js +21 -0
  66. package/src/foundation/partial-state.js.map +1 -0
  67. package/src/foundation/router-waf.d.ts +98 -0
  68. package/src/foundation/router-waf.js +401 -0
  69. package/src/foundation/router-waf.js.map +1 -0
  70. package/src/generators/function/function.js +2 -1
  71. package/src/generators/function/function.js.map +1 -1
  72. package/src/generators/post-generation-install.d.ts +1 -0
  73. package/src/generators/post-generation-install.js +134 -67
  74. package/src/generators/post-generation-install.js.map +1 -1
  75. package/src/generators/preset/generator.js +23 -3
  76. package/src/generators/preset/generator.js.map +1 -1
  77. package/src/generators/preset/schema.d.ts +7 -0
  78. package/src/generators/preset/schema.json +47 -0
  79. package/src/generators/preset/tool-versions.d.ts +5 -5
  80. package/src/generators/preset/tool-versions.js +4 -4
  81. package/src/generators/preset/workspace-files-ci-acceptance.d.ts +1 -0
  82. package/src/generators/preset/workspace-files-ci-acceptance.js +791 -0
  83. package/src/generators/preset/workspace-files-ci-acceptance.js.map +1 -0
  84. package/src/generators/preset/workspace-files-developer-agent.d.ts +2 -0
  85. package/src/generators/preset/workspace-files-developer-agent.js +25 -11
  86. package/src/generators/preset/workspace-files-developer-agent.js.map +1 -1
  87. package/src/generators/preset/workspace-files-foundation-workload-roles.js +5 -2
  88. package/src/generators/preset/workspace-files-foundation-workload-roles.js.map +1 -1
  89. package/src/generators/preset/workspace-files-performance-load.js +2 -1
  90. package/src/generators/preset/workspace-files-performance-load.js.map +1 -1
  91. package/src/generators/preset/workspace-files-workload-role-baseline.js +254 -42
  92. package/src/generators/preset/workspace-files-workload-role-baseline.js.map +1 -1
  93. package/src/generators/preset/workspace-files.d.ts +8 -4
  94. package/src/generators/preset/workspace-files.js +1020 -62
  95. package/src/generators/preset/workspace-files.js.map +1 -1
  96. package/src/generators/repository-policy/repository-policy.js +65 -3
  97. package/src/generators/repository-policy/repository-policy.js.map +1 -1
  98. package/src/generators/repository-policy/schema.d.ts +6 -0
  99. package/src/generators/repository-policy/schema.json +46 -0
  100. package/src/generators/slice/built-in-plan-api-files.js +2 -1
  101. package/src/generators/slice/built-in-plan-api-files.js.map +1 -1
  102. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +19 -1
  103. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js.map +1 -1
  104. package/src/generators/slice/built-in-plan-data-files.js +16 -5
  105. package/src/generators/slice/built-in-plan-data-files.js.map +1 -1
  106. package/src/generators/slice/built-in-plan-data-retention-job-files.js +10 -6
  107. package/src/generators/slice/built-in-plan-data-retention-job-files.js.map +1 -1
  108. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +25 -10
  109. package/src/generators/slice/built-in-plan-database-lifecycle-files.js.map +1 -1
  110. package/src/generators/slice/built-in-plan-foundation-files.js +2 -3
  111. package/src/generators/slice/built-in-plan-foundation-files.js.map +1 -1
  112. package/src/generators/slice/built-in-plan-infrastructure-files.js +359 -27
  113. package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -1
  114. package/src/generators/slice/built-in-plan-infrastructure-safety-files.js +67 -11
  115. package/src/generators/slice/built-in-plan-infrastructure-safety-files.js.map +1 -1
  116. package/src/generators/slice/built-in-plan-metadata-files.js +1 -0
  117. package/src/generators/slice/built-in-plan-metadata-files.js.map +1 -1
  118. package/src/generators/slice/built-in-plan-operations-files.js +442 -5
  119. package/src/generators/slice/built-in-plan-operations-files.js.map +1 -1
  120. package/src/generators/slice/built-in-plan-performance-files.d.ts +2 -1
  121. package/src/generators/slice/built-in-plan-performance-files.js +34 -47
  122. package/src/generators/slice/built-in-plan-performance-files.js.map +1 -1
  123. package/src/generators/slice/built-in-plan-project-files.js +14 -1
  124. package/src/generators/slice/built-in-plan-project-files.js.map +1 -1
  125. package/src/generators/slice/built-in-plan-release-files.js +235 -56
  126. package/src/generators/slice/built-in-plan-release-files.js.map +1 -1
  127. package/src/generators/slice/built-in-plan-testing-files.js +92 -5
  128. package/src/generators/slice/built-in-plan-testing-files.js.map +1 -1
  129. package/src/generators/slice/built-in-plan-web-files.js +13 -0
  130. package/src/generators/slice/built-in-plan-web-files.js.map +1 -1
  131. package/src/generators/slice/built-in-plan-workload-role-files.js +5 -2
  132. package/src/generators/slice/built-in-plan-workload-role-files.js.map +1 -1
  133. package/src/generators/slice/built-in-plan.d.ts +2 -1
  134. package/src/generators/slice/built-in-plan.js +8 -5
  135. package/src/generators/slice/built-in-plan.js.map +1 -1
  136. package/src/generators/slice/slice.js +11 -3
  137. package/src/generators/slice/slice.js.map +1 -1
  138. package/src/generators/upgrade/upgrade.js +35 -0
  139. package/src/generators/upgrade/upgrade.js.map +1 -1
  140. package/src/generators/workload/frozen-container.d.ts +3 -2
  141. package/src/generators/workload/frozen-container.js +6 -4
  142. package/src/generators/workload/frozen-container.js.map +1 -1
  143. package/src/generators/workload/workload.js +2 -2
  144. package/src/generators/workload/workload.js.map +1 -1
  145. package/src/git-policy/github-bootstrap.d.ts +3 -1
  146. package/src/git-policy/github-bootstrap.js +22 -5
  147. package/src/git-policy/github-bootstrap.js.map +1 -1
  148. package/src/git-policy/routing.d.ts +2 -0
  149. package/src/git-policy/routing.js +12 -1
  150. package/src/git-policy/routing.js.map +1 -1
  151. package/src/infrastructure-change/local-capability.d.ts +73 -0
  152. package/src/infrastructure-change/local-capability.js +114 -0
  153. package/src/infrastructure-change/local-capability.js.map +1 -0
  154. package/src/infrastructure-change/model.d.ts +25 -4
  155. package/src/infrastructure-change/model.js +1 -0
  156. package/src/infrastructure-change/model.js.map +1 -1
  157. package/src/infrastructure-change/normalize.d.ts +4 -0
  158. package/src/infrastructure-change/normalize.js +47 -7
  159. package/src/infrastructure-change/normalize.js.map +1 -1
  160. package/src/infrastructure-change/plan.d.ts +30 -3
  161. package/src/infrastructure-change/plan.js +323 -13
  162. package/src/infrastructure-change/plan.js.map +1 -1
  163. package/src/infrastructure-change/risk.d.ts +3 -1
  164. package/src/infrastructure-change/risk.js +96 -16
  165. package/src/infrastructure-change/risk.js.map +1 -1
  166. package/src/local-dev/environment-inspector.d.ts +1 -0
  167. package/src/local-dev/environment-inspector.js +21 -9
  168. package/src/local-dev/environment-inspector.js.map +1 -1
  169. package/src/local-dev/private-state.d.ts +2 -0
  170. package/src/local-dev/private-state.js +3 -1
  171. package/src/local-dev/private-state.js.map +1 -1
  172. package/src/performance/cost-controls.d.ts +5 -3
  173. package/src/performance/cost-controls.js +30 -24
  174. package/src/performance/cost-controls.js.map +1 -1
  175. package/src/performance/model.d.ts +1 -1
  176. package/src/performance/model.js +0 -2
  177. package/src/performance/model.js.map +1 -1
  178. package/src/performance/test-fixtures.js +0 -2
  179. package/src/performance/test-fixtures.js.map +1 -1
  180. package/src/release/source-authority.d.ts +1 -0
  181. package/src/release/source-authority.js +136 -9
  182. package/src/release/source-authority.js.map +1 -1
  183. package/src/repository-policy/deployment-plan.d.ts +4 -1
  184. package/src/repository-policy/deployment-plan.js +8 -0
  185. package/src/repository-policy/deployment-plan.js.map +1 -1
  186. package/src/repository-policy/foundation-profile.js +1 -2
  187. package/src/repository-policy/foundation-profile.js.map +1 -1
  188. package/src/repository-policy/policy.d.ts +25 -1
  189. package/src/repository-policy/policy.js +145 -4
  190. package/src/repository-policy/policy.js.map +1 -1
  191. package/src/schemas/performance-policy-v1.json +0 -2
  192. package/src/schemas/repository-policy-v1.json +41 -0
  193. package/src/secrets/operator.js +22 -4
  194. package/src/secrets/operator.js.map +1 -1
  195. package/src/secrets/verification.js +9 -1
  196. package/src/secrets/verification.js.map +1 -1
  197. package/src/sst-environment-source-format.d.ts +3 -0
  198. package/src/sst-environment-source-format.js +179 -0
  199. package/src/sst-environment-source-format.js.map +1 -0
  200. package/src/supply-chain/blueprint-trust-git.js +72 -21
  201. package/src/supply-chain/blueprint-trust-git.js.map +1 -1
  202. package/src/supply-chain/blueprint-verification-authority.js +55 -3
  203. package/src/supply-chain/blueprint-verification-authority.js.map +1 -1
  204. package/src/supply-chain/blueprint-verification-execution.js +17 -2
  205. package/src/supply-chain/blueprint-verification-execution.js.map +1 -1
  206. package/src/supply-chain/build-output.d.ts +7 -1
  207. package/src/supply-chain/build-output.js +160 -20
  208. package/src/supply-chain/build-output.js.map +1 -1
  209. package/src/supply-chain/generated-command.js +131 -43
  210. package/src/supply-chain/generated-command.js.map +1 -1
  211. package/src/supply-chain/generated-container-release-command.js +187 -38
  212. package/src/supply-chain/generated-container-release-command.js.map +1 -1
  213. package/src/supply-chain/generated-ecr-collector-command.js +12 -2
  214. package/src/supply-chain/generated-ecr-collector-command.js.map +1 -1
  215. package/src/supply-chain/generated-scanner-command.js +114 -54
  216. package/src/supply-chain/generated-scanner-command.js.map +1 -1
  217. package/src/supply-chain/inventory.d.ts +17 -0
  218. package/src/supply-chain/inventory.js +114 -3
  219. package/src/supply-chain/inventory.js.map +1 -1
  220. package/src/supply-chain/pnpm-toolchain-authority.d.ts +4 -3
  221. package/src/supply-chain/pnpm-toolchain-authority.js +20 -34
  222. package/src/supply-chain/pnpm-toolchain-authority.js.map +1 -1
  223. package/src/supply-chain/policy.js +4 -1
  224. package/src/supply-chain/policy.js.map +1 -1
  225. package/src/supply-chain/sbom.d.ts +1 -1
  226. package/src/supply-chain/sbom.js +30 -5
  227. package/src/supply-chain/sbom.js.map +1 -1
  228. package/src/supply-chain/scanner.js +10 -1
  229. package/src/supply-chain/scanner.js.map +1 -1
  230. package/src/workload-roles/account-baseline.d.ts +11 -0
  231. package/src/workload-roles/account-baseline.js +253 -16
  232. package/src/workload-roles/account-baseline.js.map +1 -1
  233. package/src/workload-roles/authority.d.ts +3 -1
  234. package/src/workload-roles/authority.js +48 -2
  235. package/src/workload-roles/authority.js.map +1 -1
  236. package/src/workload-roles/aws-s3-state.js +27 -0
  237. package/src/workload-roles/aws-s3-state.js.map +1 -1
  238. package/src/workload-roles/boundary.d.ts +2 -1
  239. package/src/workload-roles/boundary.js +9 -2
  240. package/src/workload-roles/boundary.js.map +1 -1
  241. package/src/workload-roles/handler.js +52 -24
  242. package/src/workload-roles/handler.js.map +1 -1
  243. package/src/workload-roles/injection.js +1 -1
  244. package/src/workload-roles/injection.js.map +1 -1
  245. package/src/workload-roles/provisioner.js +12 -4
  246. package/src/workload-roles/provisioner.js.map +1 -1
  247. package/src/workload-roles/registration.d.ts +1 -0
  248. package/src/workload-roles/registration.js +5 -0
  249. package/src/workload-roles/registration.js.map +1 -1
  250. package/src/workload-roles/stage-boundary.js +1 -1
  251. package/src/workload-roles/stage-boundary.js.map +1 -1
  252. package/src/workload-roles/standard-slice.js +1 -0
  253. package/src/workload-roles/standard-slice.js.map +1 -1
@@ -16,6 +16,7 @@ const index_js_6 = require("../../supply-chain/index.js");
16
16
  const deterministic_order_js_1 = require("../../deterministic-order.js");
17
17
  const tool_versions_js_1 = require("./tool-versions.js");
18
18
  const workspace_files_developer_agent_js_1 = require("./workspace-files-developer-agent.js");
19
+ const workspace_files_ci_acceptance_js_1 = require("./workspace-files-ci-acceptance.js");
19
20
  const workspace_files_foundation_workload_roles_js_1 = require("./workspace-files-foundation-workload-roles.js");
20
21
  const workspace_files_performance_cost_js_1 = require("./workspace-files-performance-cost.js");
21
22
  const workspace_files_performance_load_js_1 = require("./workspace-files-performance-load.js");
@@ -60,11 +61,12 @@ exports.PLAN_PROVIDER_REFRESH_GLOBAL_READ_ACTIONS = [
60
61
  'rds:DescribeDBProxies',
61
62
  'rds:DescribeDBSubnetGroups',
62
63
  ];
63
- function validatePlanProviderRefreshPolicy(value, region, account) {
64
+ function validatePlanProviderRefreshPolicy(value, region, account, stage) {
64
65
  if (!Array.isArray(value) ||
65
66
  value.length === 0 ||
66
67
  !/^[a-z]{2}(?:-gov)?-[a-z]+-\d$/.test(region) ||
67
- !/^\d{12}$/.test(account)) {
68
+ !/^\d{12}$/.test(account) ||
69
+ !['production', 'staging'].includes(stage)) {
68
70
  throw new Error('Provider refresh policy must be a non-empty exact statement array for one account and region.');
69
71
  }
70
72
  const allowed = new Set(exports.PLAN_PROVIDER_REFRESH_READ_ACTIONS);
@@ -117,6 +119,14 @@ function validatePlanProviderRefreshPolicy(value, region, account) {
117
119
  if (resources.some((resource) => {
118
120
  const arn = resource.split(':');
119
121
  const target = arn.slice(5).join(':');
122
+ const boundedPathWildcard = target.endsWith('/*') && !/[*?]/.test(target.slice(0, -2));
123
+ const boundedLambdaStagePrefix = arn[2] === 'lambda' &&
124
+ actions.every((action) => action.startsWith('lambda:') && !global.has(action)) &&
125
+ new RegExp('^function:(?:' +
126
+ stage +
127
+ '-|[A-Za-z0-9][A-Za-z0-9_-]*-' +
128
+ stage +
129
+ '-)[A-Za-z0-9_-]*\\*$').test(target);
120
130
  return (arn[0] !== 'arn' ||
121
131
  arn[1] !== 'aws' ||
122
132
  !/^[a-z0-9-]+$/.test(arn[2] ?? '') ||
@@ -124,7 +134,8 @@ function validatePlanProviderRefreshPolicy(value, region, account) {
124
134
  !['', account].includes(arn[4] ?? '') ||
125
135
  !target ||
126
136
  (/[*?]/.test(target) &&
127
- (!target.endsWith('/*') || /[*?]/.test(target.slice(0, -2)))));
137
+ !boundedPathWildcard &&
138
+ !boundedLambdaStagePrefix));
128
139
  })) {
129
140
  throw new Error(`Provider refresh statement ${index} is not bounded to exact targets in the selected account and region.`);
130
141
  }
@@ -474,6 +485,8 @@ function planRoleAuditStep(branch, mode = 'produce') {
474
485
  EBK_PLAN_ENVIRONMENT: infrastructure-plan-${stage}
475
486
  EBK_PLAN_ROLE_ARN: \${{ env.AWS_PLAN_ROLE_ARN }}
476
487
  EBK_PLAN_REPOSITORY: \${{ github.repository }}
488
+ EBK_PLAN_REPOSITORY_ID: \${{ github.repository_id }}
489
+ EBK_PLAN_REPOSITORY_OWNER_ID: \${{ github.repository_owner_id }}
477
490
  run: |
478
491
  role_name="\${EBK_PLAN_ROLE_ARN##*/}"
479
492
  test "$(aws sts get-caller-identity --query Account --output text)" = "$AWS_EXPECTED_ACCOUNT_ID" ||
@@ -506,8 +519,23 @@ function planRoleAuditStep(branch, mode = 'produce') {
506
519
  const role = JSON.parse(readFileSync(process.argv[2], 'utf8')).Role;
507
520
  const policy = JSON.parse(readFileSync(process.argv[3], 'utf8')).PolicyDocument;
508
521
  const account = process.env.AWS_EXPECTED_ACCOUNT_ID;
522
+ const repository = process.env.EBK_PLAN_REPOSITORY ?? '';
523
+ const repositoryId = process.env.EBK_PLAN_REPOSITORY_ID ?? '';
524
+ const repositoryOwnerId = process.env.EBK_PLAN_REPOSITORY_OWNER_ID ?? '';
509
525
  const stateBucketArn = process.env.AWS_PLAN_STATE_BUCKET_ARN;
510
526
  const stateKmsKeyArn = process.env.AWS_PLAN_STATE_KMS_KEY_ARN;
527
+ if (
528
+ !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\\/[A-Za-z0-9_.-]{1,100}$/.test(
529
+ repository,
530
+ ) ||
531
+ !/^[1-9][0-9]*$/.test(repositoryOwnerId) ||
532
+ !/^[1-9][0-9]*$/.test(repositoryId)
533
+ ) throw new Error('Exact immutable GitHub repository identity is required.');
534
+ const [repositoryOwner, repositoryName] = repository.split('/');
535
+ const oidcSubject =
536
+ 'repo:' + repositoryOwner + '@' + repositoryOwnerId + '/' +
537
+ repositoryName + '@' + repositoryId + ':environment:' +
538
+ process.env.EBK_PLAN_ENVIRONMENT;
511
539
  const parseExactArnArray = (name, pattern) => {
512
540
  let value;
513
541
  try {
@@ -551,6 +579,7 @@ function planRoleAuditStep(branch, mode = 'produce') {
551
579
  'AWS_PLAN_GENERATED_SECRET_ARNS_JSON',
552
580
  new RegExp('^arn:aws:secretsmanager:' + process.env.AWS_REGION + ':' + account + ':secret:ebk/'),
553
581
  );
582
+ const stateStage = process.env.EBK_PLAN_BRANCH === 'main' ? 'production' : 'staging';
554
583
  let providerRefreshPolicy;
555
584
  try {
556
585
  providerRefreshPolicy = JSON.parse(
@@ -614,6 +643,23 @@ function planRoleAuditStep(branch, mode = 'produce') {
614
643
  for (const resource of resources) {
615
644
  const arn = resource.split(':');
616
645
  const target = arn.slice(5).join(':');
646
+ const boundedPathWildcard =
647
+ target.endsWith('/*') &&
648
+ !/[*?]/.test(target.slice(0, -2));
649
+ const boundedLambdaStagePrefix =
650
+ arn[2] === 'lambda' &&
651
+ actions.every(
652
+ (action) =>
653
+ action.startsWith('lambda:') &&
654
+ !globalProviderReadActions.has(action),
655
+ ) &&
656
+ new RegExp(
657
+ '^function:(?:' +
658
+ stateStage +
659
+ '-|[A-Za-z0-9][A-Za-z0-9_-]*-' +
660
+ stateStage +
661
+ '-)[A-Za-z0-9_-]*\\\\*$',
662
+ ).test(target);
617
663
  if (
618
664
  arn[0] !== 'arn' ||
619
665
  arn[1] !== 'aws' ||
@@ -622,7 +668,8 @@ function planRoleAuditStep(branch, mode = 'produce') {
622
668
  !['', account].includes(arn[4] ?? '') ||
623
669
  !target ||
624
670
  (/[*?]/.test(target) &&
625
- (!target.endsWith('/*') || /[*?]/.test(target.slice(0, -2))))
671
+ !boundedPathWildcard &&
672
+ !boundedLambdaStagePrefix)
626
673
  ) throw new Error('Provider refresh statement ' + index + ' has a target outside the selected account and region.');
627
674
  }
628
675
  }
@@ -637,7 +684,6 @@ function planRoleAuditStep(branch, mode = 'produce') {
637
684
  ).sort((left, right) =>
638
685
  left.Sid < right.Sid ? -1 : left.Sid > right.Sid ? 1 : 0,
639
686
  );
640
- const stateStage = process.env.EBK_PLAN_BRANCH === 'main' ? 'production' : 'staging';
641
687
  if (
642
688
  stateKmsKeyArn &&
643
689
  stateKmsKeyArn !==
@@ -681,7 +727,7 @@ function planRoleAuditStep(branch, mode = 'produce') {
681
727
  Action: 'sts:AssumeRoleWithWebIdentity',
682
728
  Condition: { StringEquals: {
683
729
  'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
684
- 'token.actions.githubusercontent.com:sub': 'repo:' + process.env.EBK_PLAN_REPOSITORY + ':environment:' + process.env.EBK_PLAN_ENVIRONMENT,
730
+ 'token.actions.githubusercontent.com:sub': oidcSubject,
685
731
  }},
686
732
  }],
687
733
  };
@@ -708,6 +754,19 @@ function planRoleAuditStep(branch, mode = 'produce') {
708
754
  ],
709
755
  Resource: '*',
710
756
  },
757
+ {
758
+ Sid: 'ObserveStageSecretVersionIdentity',
759
+ Effect: 'Allow',
760
+ Action: 'secretsmanager:ListSecretVersionIds',
761
+ Resource:
762
+ 'arn:aws:secretsmanager:' + process.env.AWS_REGION + ':' +
763
+ account + ':secret:*',
764
+ Condition: {
765
+ StringEquals: {
766
+ 'aws:ResourceTag/ebk:stage': stateStage,
767
+ },
768
+ },
769
+ },
711
770
  {
712
771
  Sid: 'ValidatePlannedIamPolicies',
713
772
  Effect: 'Allow',
@@ -782,7 +841,10 @@ function planRoleAuditStep(branch, mode = 'produce') {
782
841
  generatedSecretArns,
783
842
  providerRefreshStatements,
784
843
  region: process.env.AWS_REGION,
785
- repository: process.env.EBK_PLAN_REPOSITORY,
844
+ repository,
845
+ repositoryId,
846
+ repositoryOwnerId,
847
+ oidcSubject,
786
848
  role: {
787
849
  arn: role.Arn,
788
850
  policy: canonical(policy),
@@ -953,6 +1015,9 @@ const WORKSPACE_SEEDED_PATHS = new Set([
953
1015
  exports.WORKSPACE_PACKAGE_MANAGED_JSON_POINTERS = [
954
1016
  '/devDependencies/eslint-plugin-jsx-a11y',
955
1017
  '/devDependencies/vitest',
1018
+ '/engines/node',
1019
+ '/engines/pnpm',
1020
+ '/packageManager',
956
1021
  ];
957
1022
  exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
958
1023
  '/name',
@@ -961,9 +1026,11 @@ exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
961
1026
  '/root',
962
1027
  '/tags',
963
1028
  '/targets/blueprint-trust-check',
1029
+ '/targets/blueprint-trust-check-main',
964
1030
  '/targets/blueprint-trust-check-protected',
965
1031
  '/targets/developer-context',
966
1032
  '/targets/deployment-plan',
1033
+ '/targets/deploy-local',
967
1034
  '/targets/dev',
968
1035
  '/targets/dev-doctor',
969
1036
  '/targets/dev-setup',
@@ -973,6 +1040,7 @@ exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
973
1040
  '/targets/supply-chain-check-protected',
974
1041
  '/targets/supply-chain-scan',
975
1042
  '/targets/verify',
1043
+ '/targets/verify-proposed',
976
1044
  '/targets/work-markers',
977
1045
  '/targets/workload-role-candidate-report',
978
1046
  '/targets/workload-role-provisioner-build',
@@ -983,6 +1051,10 @@ const FOUNDATION_SEEDED_PATHS = new Set([
983
1051
  'docs/costs.md',
984
1052
  'docs/runbooks.md',
985
1053
  'docs/security.md',
1054
+ 'performance/account-cost-policy.json',
1055
+ 'security/router-waf-blocking-rehearsal.json',
1056
+ 'security/router-waf-live-evidence.json',
1057
+ 'security/router-waf.json',
986
1058
  ]);
987
1059
  function createFoundationOwnership(files, frameworkVersion) {
988
1060
  const paths = Object.keys(files)
@@ -1045,7 +1117,16 @@ function createWorkspaceOwnership(files, frameworkVersion) {
1045
1117
  }
1046
1118
  function renderWorkspaceFiles(options) {
1047
1119
  const { frameworkVersion, name } = options;
1120
+ const deploymentProfile = options.deploymentProfile ?? 'starter';
1121
+ const initialRepositoryPolicy = (0, index_js_4.createIncubationRepositoryPolicy)(deploymentProfile);
1122
+ initialRepositoryPolicy.deploymentAccounts = options.deploymentAccounts ?? {
1123
+ development: null,
1124
+ production: null,
1125
+ staging: null,
1126
+ };
1127
+ initialRepositoryPolicy.deploymentRegion = options.deploymentRegion ?? null;
1048
1128
  const developerAgentFilePlan = (0, workspace_files_developer_agent_js_1.renderDeveloperAgentWorkspaceFilePlan)({
1129
+ deploymentProfile,
1049
1130
  frameworkVersion,
1050
1131
  name,
1051
1132
  });
@@ -1056,7 +1137,13 @@ function renderWorkspaceFiles(options) {
1056
1137
  const pnpmAction = (0, index_js_3.pinnedAction)(index_js_3.GITHUB_ACTION_PINS.pnpmSetup);
1057
1138
  const setupNodeAction = (0, index_js_3.pinnedAction)(index_js_3.GITHUB_ACTION_PINS.setupNode);
1058
1139
  const uploadArtifactAction = (0, index_js_3.pinnedAction)(index_js_3.GITHUB_ACTION_PINS.uploadArtifact);
1140
+ const protectedDevelopPromotion = "github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref == 'develop' && github.event.pull_request.base.ref == 'main'";
1141
+ const protectedMainSync = "github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref == 'main' && github.event.pull_request.base.ref == 'develop'";
1142
+ const protectedPush = "github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')";
1143
+ const protectedDeploymentRouteJobName = 'protected deployment route';
1144
+ const stagingApplyJobPrefix = 'apply staging plan ';
1059
1145
  const files = {
1146
+ ...(0, workspace_files_ci_acceptance_js_1.renderWorkspaceCiAcceptanceFiles)(),
1060
1147
  '.node-version': `${tool_versions_js_1.WORKSPACE_NODE_VERSION}\n`,
1061
1148
  '.github/CODEOWNERS': `# Replace this placeholder with the GitHub user or team that owns protected source.
1062
1149
  # GitHub bootstrap remains blocked until a non-placeholder catch-all owner is committed.
@@ -1072,15 +1159,77 @@ on:
1072
1159
  workflow_dispatch:
1073
1160
 
1074
1161
  concurrency:
1075
- group: verify-\${{ github.ref }}
1076
- cancel-in-progress: true
1162
+ group: verify-\${{ github.event_name == 'push' && github.sha || github.ref }}
1163
+ cancel-in-progress: \${{ github.event_name != 'push' }}
1077
1164
 
1078
1165
  permissions:
1166
+ actions: read
1079
1167
  contents: read
1168
+ pull-requests: read
1169
+
1170
+ env:
1171
+ EBK_CI_CANDIDATE_SHA: \${{ github.event.pull_request.head.sha || github.sha }}
1080
1172
 
1081
1173
  jobs:
1174
+ acceptance:
1175
+ name: accepted merge evidence
1176
+ if: ${protectedPush}
1177
+ runs-on: ubuntu-latest
1178
+ outputs:
1179
+ history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1180
+ reused: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1181
+ route: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' && steps.resolve.outputs.route || '' }}
1182
+ steps:
1183
+ - name: Check out immutable merged candidate
1184
+ if: ${protectedPush}
1185
+ uses: ${checkoutAction}
1186
+ with:
1187
+ fetch-depth: 0
1188
+ ref: \${{ github.sha }}
1189
+ - name: Resolve exact protected-route acceptance
1190
+ if: ${protectedPush}
1191
+ id: resolve
1192
+ env:
1193
+ GH_TOKEN: \${{ github.token }}
1194
+ run: node tools/ci/workspace-acceptance.mjs resolve
1195
+ - name: Restore exact protected-route acceptance
1196
+ if: steps.resolve.outputs.reused == 'true'
1197
+ id: restore
1198
+ continue-on-error: true
1199
+ env:
1200
+ EBK_ACCEPTANCE_ARTIFACT_DIGEST: \${{ steps.resolve.outputs.artifact_digest }}
1201
+ EBK_ACCEPTANCE_ARTIFACT_ID: \${{ steps.resolve.outputs.artifact_id }}
1202
+ EBK_ACCEPTANCE_ARTIFACT_SIZE: \${{ steps.resolve.outputs.artifact_size }}
1203
+ GH_TOKEN: \${{ github.token }}
1204
+ run: >-
1205
+ node tools/ci/workspace-acceptance.mjs download
1206
+ .ebk/evidence/reused-acceptance
1207
+ "$EBK_ACCEPTANCE_ARTIFACT_ID"
1208
+ "$EBK_ACCEPTANCE_ARTIFACT_DIGEST"
1209
+ "$EBK_ACCEPTANCE_ARTIFACT_SIZE"
1210
+ - name: Verify exact merged-tree acceptance
1211
+ if: steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success'
1212
+ id: verify
1213
+ continue-on-error: true
1214
+ env:
1215
+ EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
1216
+ EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
1217
+ EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
1218
+ EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
1219
+ EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
1220
+ run: >-
1221
+ node tools/ci/workspace-acceptance.mjs verify
1222
+ .ebk/evidence/reused-acceptance/manifest.json
1223
+ "$EBK_ACCEPTANCE_HEAD"
1224
+ "$EBK_ACCEPTANCE_BASE"
1225
+ "$EBK_ACCEPTANCE_ROUTE"
1226
+ "$EBK_ACCEPTANCE_RUN_ID"
1227
+ "$EBK_ACCEPTANCE_RUN_ATTEMPT"
1228
+
1082
1229
  policy:
1083
1230
  name: policy
1231
+ needs: acceptance
1232
+ if: always() && needs.acceptance.outputs.reused != 'true'
1084
1233
  runs-on: ubuntu-latest
1085
1234
  steps:
1086
1235
  - uses: ${checkoutAction}
@@ -1094,12 +1243,19 @@ jobs:
1094
1243
  cache: pnpm
1095
1244
  - run: pnpm install --frozen-lockfile
1096
1245
  - run: node tools/ebk-git-policy.mjs
1246
+ env:
1247
+ EBK_HEAD_REPOSITORY: \${{ github.event.pull_request.head.repo.full_name }}
1097
1248
 
1098
1249
  verify:
1099
1250
  name: verify
1251
+ needs: acceptance
1252
+ if: always() && needs.acceptance.outputs.reused != 'true'
1100
1253
  runs-on: ubuntu-latest
1101
1254
  steps:
1102
1255
  - uses: ${checkoutAction}
1256
+ with:
1257
+ fetch-depth: 0
1258
+ ref: \${{ github.event.pull_request.head.sha || github.sha }}
1103
1259
  - uses: ${pnpmAction}
1104
1260
  - uses: ${setupNodeAction}
1105
1261
  with:
@@ -1107,9 +1263,80 @@ jobs:
1107
1263
  cache: pnpm
1108
1264
  - run: pnpm install --frozen-lockfile
1109
1265
  - run: pnpm nx format:check
1110
- - run: pnpm nx run-many -t verify --parallel=3
1266
+ - name: Verify proposed workspace
1267
+ if: github.event_name == 'pull_request' && !(${protectedDevelopPromotion} || ${protectedMainSync})
1268
+ run: |
1269
+ pnpm nx run ${name}:verify-proposed
1270
+ pnpm nx run-many -t verify --exclude=${name} --parallel=3
1271
+ - name: Verify protected develop promotion
1272
+ if: github.event_name == 'pull_request' && ${protectedDevelopPromotion}
1273
+ run: pnpm nx run-many -t verify --parallel=3
1274
+ - name: Verify protected main sync
1275
+ if: github.event_name == 'pull_request' && ${protectedMainSync}
1276
+ run: |
1277
+ pnpm nx run ${name}:blueprint-trust-check-main
1278
+ pnpm nx run ${name}:verify-proposed
1279
+ pnpm nx run-many -t verify --exclude=${name} --parallel=3
1280
+ - name: Verify adopted workspace
1281
+ if: github.event_name != 'pull_request'
1282
+ run: pnpm nx run-many -t verify --parallel=3
1283
+
1284
+ seal-acceptance:
1285
+ name: seal protected route acceptance
1286
+ needs: [policy, verify]
1287
+ if: >-
1288
+ always() &&
1289
+ needs.policy.result == 'success' &&
1290
+ needs.verify.result == 'success' &&
1291
+ github.event_name == 'pull_request' &&
1292
+ (${protectedDevelopPromotion} || ${protectedMainSync})
1293
+ runs-on: ubuntu-latest
1294
+ steps:
1295
+ - name: Check out immutable accepted candidate
1296
+ uses: ${checkoutAction}
1297
+ with:
1298
+ fetch-depth: 0
1299
+ ref: \${{ env.EBK_CI_CANDIDATE_SHA }}
1300
+ - name: Seal exact protected-route acceptance
1301
+ id: seal
1302
+ env:
1303
+ EBK_BASE_COMMIT: \${{ github.event.pull_request.base.sha }}
1304
+ EBK_HEAD_REPOSITORY: \${{ github.event.pull_request.head.repo.full_name }}
1305
+ run: >-
1306
+ node tools/ci/workspace-acceptance.mjs seal
1307
+ .ebk/evidence/ci/workspace-acceptance/manifest.json
1308
+ - name: Retain exact protected-route acceptance
1309
+ if: steps.seal.outputs.sealed == 'true'
1310
+ uses: ${uploadArtifactAction}
1311
+ with:
1312
+ name: workspace-acceptance-\${{ env.EBK_CI_CANDIDATE_SHA }}-\${{ github.run_id }}-\${{ github.run_attempt }}
1313
+ path: .ebk/evidence/ci/workspace-acceptance
1314
+ if-no-files-found: error
1315
+ include-hidden-files: true
1316
+ retention-days: 30
1317
+
1318
+ release-candidate:
1319
+ name: build immutable release candidates
1320
+ needs: [acceptance, policy, verify]
1321
+ if: >-
1322
+ always() &&
1323
+ github.event_name == 'push' &&
1324
+ needs.acceptance.outputs.history_only != 'true' &&
1325
+ (needs.acceptance.outputs.reused == 'true' ||
1326
+ (needs.policy.result == 'success' && needs.verify.result == 'success'))
1327
+ runs-on: ubuntu-latest
1328
+ steps:
1329
+ - uses: ${checkoutAction}
1330
+ with:
1331
+ fetch-depth: 0
1332
+ ref: \${{ github.sha }}
1333
+ - uses: ${pnpmAction}
1334
+ - uses: ${setupNodeAction}
1335
+ with:
1336
+ node-version: ${tool_versions_js_1.WORKSPACE_NODE_VERSION}
1337
+ cache: pnpm
1338
+ - run: pnpm install --frozen-lockfile
1111
1339
  - name: Build immutable release candidates
1112
- if: github.event_name == 'push'
1113
1340
  env:
1114
1341
  EBK_SOURCE_COMMIT: \${{ github.sha }}
1115
1342
  run: |
@@ -1127,7 +1354,6 @@ jobs:
1127
1354
  ".ebk/evidence/release-candidate/packages/$project/"
1128
1355
  done
1129
1356
  - name: Retain exact immutable release candidate
1130
- if: github.event_name == 'push'
1131
1357
  uses: ${uploadArtifactAction}
1132
1358
  with:
1133
1359
  name: release-candidate-\${{ github.sha }}
@@ -1162,7 +1388,97 @@ permissions:
1162
1388
  contents: read
1163
1389
 
1164
1390
  jobs:
1391
+ history-classification:
1392
+ name: classify protected history-only sync
1393
+ if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
1394
+ runs-on: ubuntu-latest
1395
+ permissions:
1396
+ actions: read
1397
+ contents: read
1398
+ pull-requests: read
1399
+ outputs:
1400
+ history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1401
+ steps:
1402
+ - name: Check out immutable protected candidate
1403
+ uses: ${checkoutAction}
1404
+ with:
1405
+ fetch-depth: 0
1406
+ ref: \${{ github.sha }}
1407
+ - uses: ${setupNodeAction}
1408
+ with:
1409
+ node-version: ${tool_versions_js_1.WORKSPACE_NODE_VERSION}
1410
+ - name: Resolve exact protected-route acceptance
1411
+ id: resolve
1412
+ env:
1413
+ GH_TOKEN: \${{ github.token }}
1414
+ run: node tools/ci/workspace-acceptance.mjs resolve
1415
+ - name: Restore exact protected-route acceptance
1416
+ if: steps.resolve.outputs.reused == 'true'
1417
+ id: restore
1418
+ continue-on-error: true
1419
+ env:
1420
+ EBK_ACCEPTANCE_ARTIFACT_DIGEST: \${{ steps.resolve.outputs.artifact_digest }}
1421
+ EBK_ACCEPTANCE_ARTIFACT_ID: \${{ steps.resolve.outputs.artifact_id }}
1422
+ EBK_ACCEPTANCE_ARTIFACT_SIZE: \${{ steps.resolve.outputs.artifact_size }}
1423
+ GH_TOKEN: \${{ github.token }}
1424
+ run: >-
1425
+ node tools/ci/workspace-acceptance.mjs download
1426
+ .ebk/evidence/reused-history-acceptance
1427
+ "$EBK_ACCEPTANCE_ARTIFACT_ID"
1428
+ "$EBK_ACCEPTANCE_ARTIFACT_DIGEST"
1429
+ "$EBK_ACCEPTANCE_ARTIFACT_SIZE"
1430
+ - name: Verify exact history-only acceptance
1431
+ if: steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success'
1432
+ id: verify
1433
+ continue-on-error: true
1434
+ env:
1435
+ EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
1436
+ EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
1437
+ EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
1438
+ EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
1439
+ EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
1440
+ run: >-
1441
+ node tools/ci/workspace-acceptance.mjs verify
1442
+ .ebk/evidence/reused-history-acceptance/manifest.json
1443
+ "$EBK_ACCEPTANCE_HEAD"
1444
+ "$EBK_ACCEPTANCE_BASE"
1445
+ "$EBK_ACCEPTANCE_ROUTE"
1446
+ "$EBK_ACCEPTANCE_RUN_ID"
1447
+ "$EBK_ACCEPTANCE_RUN_ATTEMPT"
1448
+
1449
+ deployment-mode:
1450
+ name: classify deployment profile
1451
+ runs-on: ubuntu-latest
1452
+ outputs:
1453
+ profile: \${{ steps.profile.outputs.profile }}
1454
+ steps:
1455
+ - name: Check out immutable deployment policy
1456
+ uses: ${checkoutAction}
1457
+ with:
1458
+ ref: \${{ github.sha }}
1459
+ - id: profile
1460
+ name: Read exact committed deployment profile
1461
+ run: |
1462
+ node - <<'NODE'
1463
+ const { readFileSync } = require('node:fs');
1464
+ const policy = JSON.parse(readFileSync('ebk.repository-policy.json', 'utf8'));
1465
+ const profiles = new Set(['starter', 'production-isolated', 'fully-isolated']);
1466
+ if (!profiles.has(policy.deploymentProfile)) {
1467
+ throw new Error('Committed deployment profile is absent or invalid.');
1468
+ }
1469
+ require('node:fs').appendFileSync(
1470
+ process.env.GITHUB_OUTPUT,
1471
+ 'profile=' + policy.deploymentProfile + '\\n',
1472
+ );
1473
+ NODE
1474
+
1165
1475
  route:
1476
+ name: ${protectedDeploymentRouteJobName}
1477
+ needs: [history-classification, deployment-mode]
1478
+ if: >-
1479
+ always() &&
1480
+ needs.history-classification.outputs.history_only != 'true' &&
1481
+ needs.deployment-mode.result == 'success'
1166
1482
  runs-on: ubuntu-latest
1167
1483
  steps:
1168
1484
  - name: Validate protected ref
@@ -1175,14 +1491,22 @@ jobs:
1175
1491
  esac
1176
1492
 
1177
1493
  plan-staging:
1178
- needs: route
1179
- if: github.ref == 'refs/heads/develop'
1494
+ needs: [route, deployment-mode]
1495
+ if: >-
1496
+ github.ref == 'refs/heads/develop' &&
1497
+ (github.event_name == 'workflow_dispatch' ||
1498
+ needs.deployment-mode.outputs.profile != 'starter')
1180
1499
  runs-on: ubuntu-latest
1181
1500
  outputs:
1501
+ deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
1502
+ deployment_region: \${{ steps.plan.outputs.deployment_region }}
1503
+ development_account_id: \${{ steps.plan.outputs.development_account_id }}
1182
1504
  foundation_project: \${{ steps.plan.outputs.foundation_project }}
1183
1505
  has_slices: \${{ steps.plan.outputs.has_slices }}
1184
1506
  ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
1507
+ production_account_id: \${{ steps.plan.outputs.production_account_id }}
1185
1508
  slice_projects: \${{ steps.plan.outputs.slice_projects }}
1509
+ staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
1186
1510
  steps:
1187
1511
  - uses: ${checkoutAction}
1188
1512
  with:
@@ -1393,16 +1717,21 @@ jobs:
1393
1717
  path: .ebk/evidence/staging-release-source/**
1394
1718
 
1395
1719
  plan-production:
1396
- needs: route
1720
+ needs: [route, deployment-mode]
1397
1721
  if: github.ref == 'refs/heads/main'
1398
1722
  runs-on: ubuntu-latest
1399
1723
  outputs:
1724
+ deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
1725
+ deployment_region: \${{ steps.plan.outputs.deployment_region }}
1726
+ development_account_id: \${{ steps.plan.outputs.development_account_id }}
1400
1727
  foundation_project: \${{ steps.plan.outputs.foundation_project }}
1401
1728
  foundation_profile: \${{ steps.plan.outputs.foundation_profile }}
1402
1729
  has_slices: \${{ steps.plan.outputs.has_slices }}
1403
1730
  ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
1404
1731
  production_approval_mode: \${{ steps.plan.outputs.production_approval_mode }}
1732
+ production_account_id: \${{ steps.plan.outputs.production_account_id }}
1405
1733
  slice_projects: \${{ steps.plan.outputs.slice_projects }}
1734
+ staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
1406
1735
  steps:
1407
1736
  - uses: ${checkoutAction}
1408
1737
  with:
@@ -1723,6 +2052,8 @@ jobs:
1723
2052
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1724
2053
  EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1725
2054
  EBK_STAGE: staging
2055
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2056
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
1726
2057
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
1727
2058
  steps:
1728
2059
  - uses: ${checkoutAction}
@@ -1738,6 +2069,10 @@ jobs:
1738
2069
  - run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1739
2070
  - name: Verify managed workspace integrity before AWS authority
1740
2071
  run: pnpm nx run ${name}:workspace-health
2072
+ - name: Bind staging account to committed deployment topology
2073
+ run: |
2074
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
2075
+ test "$AWS_REGION" = "$EBK_POLICY_REGION"
1741
2076
  ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${configureAwsAction}
1742
2077
  with:
1743
2078
  role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
@@ -1777,6 +2112,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
1777
2112
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1778
2113
  EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1779
2114
  EBK_STAGE: staging
2115
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2116
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
1780
2117
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
1781
2118
  steps:
1782
2119
  - uses: ${checkoutAction}
@@ -1792,6 +2129,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
1792
2129
  - run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1793
2130
  - name: Verify managed workspace integrity before AWS authority
1794
2131
  run: pnpm nx run ${name}:workspace-health
2132
+ - name: Bind staging account to committed deployment topology
2133
+ run: |
2134
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
2135
+ test "$AWS_REGION" = "$EBK_POLICY_REGION"
1795
2136
  ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Restore both native platform records
1796
2137
  uses: ${downloadArtifactAction}
1797
2138
  with:
@@ -1833,6 +2174,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
1833
2174
  AWS_DEPLOY_ROLE_ARN: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
1834
2175
  AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
1835
2176
  AWS_REGION: \${{ vars.AWS_REGION }}
2177
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
2178
+ EBK_POLICY_PRODUCTION_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
2179
+ EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
2180
+ EBK_POLICY_STAGING_ACCOUNT_ID: \${{ needs.plan-production.outputs.staging_account_id }}
1836
2181
  EBK_CANDIDATE_RUN_ATTEMPT: \${{ needs.release-source-production.outputs.candidate_run_attempt }}
1837
2182
  EBK_CANDIDATE_RUN_ID: \${{ needs.release-source-production.outputs.candidate_run_id }}
1838
2183
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-production.outputs.source_commit }}
@@ -1933,11 +2278,13 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
1933
2278
  cp "$source_sidecar" "$target"
1934
2279
  done
1935
2280
  - uses: ${configureAwsAction}
2281
+ if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
1936
2282
  with:
1937
2283
  role-to-assume: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
1938
2284
  role-session-name: ebk-staging-container-pull-\${{ github.run_id }}-\${{ github.run_attempt }}
1939
2285
  aws-region: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
1940
2286
  - name: Authenticate exact staging source registry
2287
+ if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
1941
2288
  env:
1942
2289
  EBK_STAGING_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
1943
2290
  EBK_STAGING_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
@@ -1956,10 +2303,27 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
1956
2303
  role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
1957
2304
  role-session-name: ebk-production-container-push-\${{ github.run_id }}-\${{ github.run_attempt }}
1958
2305
  aws-region: \${{ vars.AWS_REGION }}
2306
+ - name: Bind production authority to committed deployment topology
2307
+ run: |
2308
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
2309
+ { echo 'Production environment account differs from committed deployment topology'; exit 1; }
2310
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
2311
+ { echo 'Production environment Region differs from committed deployment topology'; exit 1; }
2312
+ - name: Authenticate shared-account staging source registry
2313
+ if: env.EBK_DEPLOYMENT_PROFILE == 'starter'
2314
+ run: |
2315
+ test "$EBK_POLICY_STAGING_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
2316
+ { echo 'Starter promotion requires one shared AWS account'; exit 1; }
2317
+ registry_suffix=.com
2318
+ case "$EBK_POLICY_REGION" in cn-*) registry_suffix=.com.cn ;; esac
2319
+ aws ecr get-login-password --region "$EBK_POLICY_REGION" |
2320
+ docker login --username AWS --password-stdin \
2321
+ "$EBK_POLICY_STAGING_ACCOUNT_ID.dkr.ecr.$EBK_POLICY_REGION.amazonaws$registry_suffix"
1959
2322
  - name: Promote exact staging indexes without rebuilding
1960
2323
  env:
1961
- EBK_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
1962
- EBK_STAGING_PROMOTION_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
2324
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
2325
+ EBK_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID: \${{ needs.plan-production.outputs.deployment_profile == 'starter' && needs.plan-production.outputs.staging_account_id || vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
2326
+ EBK_STAGING_PROMOTION_REGION: \${{ needs.plan-production.outputs.deployment_profile == 'starter' && needs.plan-production.outputs.deployment_region || vars.AWS_STAGING_PROMOTION_REGION }}
1963
2327
  run: |
1964
2328
  test "$(aws sts get-caller-identity --query Account --output text)" = "$AWS_EXPECTED_ACCOUNT_ID" ||
1965
2329
  { echo 'Production promotion role account differs from policy'; exit 1; }
@@ -2178,6 +2542,8 @@ ${planRoleAuditStep('main', 'verify')}
2178
2542
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
2179
2543
  EBK_SOURCE_COMMIT: \${{ github.sha }}
2180
2544
  EBK_STAGE: staging
2545
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2546
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
2181
2547
  FOUNDATION_PROJECT: \${{ needs.plan-staging.outputs.foundation_project }}
2182
2548
  ORDERED_PROJECTS: \${{ needs.plan-staging.outputs.ordered_projects }}
2183
2549
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
@@ -2285,7 +2651,7 @@ ${planRoleAuditStep('develop', 'verify')}
2285
2651
  packages/*/.ebk/evidence/infrastructure-plan-command.json
2286
2652
 
2287
2653
  deploy-staging:
2288
- name: apply staging plan \${{ needs.infrastructure-plan-staging.outputs.plan_digest || 'recovery-without-resource-plan' }}
2654
+ name: ${stagingApplyJobPrefix}\${{ needs.infrastructure-plan-staging.outputs.plan_digest || 'recovery-without-resource-plan' }}
2289
2655
  needs: [plan-staging, release-source-staging, container-assemble-staging, infrastructure-plan-staging]
2290
2656
  if: |
2291
2657
  always() &&
@@ -2364,6 +2730,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
2364
2730
  test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'staging AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
2365
2731
  test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'staging AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
2366
2732
  test -n "$AWS_REGION" || { echo 'staging AWS_REGION is required'; exit 1; }
2733
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
2734
+ { echo 'staging environment account differs from committed deployment topology'; exit 1; }
2735
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
2736
+ { echo 'staging environment Region differs from committed deployment topology'; exit 1; }
2367
2737
  test -n "$EBK_FLEET_ORIGIN" ||
2368
2738
  { echo 'staging environment EBK_FLEET_ORIGIN is required'; exit 1; }
2369
2739
  test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
@@ -2375,8 +2745,7 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
2375
2745
  test -n "$AWS_PLAN_GENERATED_SECRET_ARNS_JSON" &&
2376
2746
  test -n "$AWS_PLAN_PROVIDER_REFRESH_POLICY_JSON" ||
2377
2747
  { echo 'staging exact plan-discovery inputs are required'; exit 1; }
2378
- test -n "$AWS_PLAN_ROLE_ARN" &&
2379
- test -n "$AWS_PLAN_STATE_KMS_KEY_ARN" ||
2748
+ test -n "$AWS_PLAN_ROLE_ARN" ||
2380
2749
  { echo 'staging exact plan-authority inputs are required'; exit 1; }
2381
2750
  case "$AWS_DEPLOY_ROLE_ARN" in
2382
2751
  arn:aws:iam::$AWS_EXPECTED_ACCOUNT_ID:role/*) ;;
@@ -3000,6 +3369,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
3000
3369
  EBK_APPROVED_MAIN_COMMIT: \${{ github.sha }}
3001
3370
  EBK_SOURCE_COMMIT: \${{ github.sha }}
3002
3371
  EBK_STAGE: production
3372
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
3373
+ EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
3003
3374
  EBK_VERIFIED_STAGING_RUN_ID: \${{ needs.release-source-production.outputs.verified_staging_run_id }}
3004
3375
  FOUNDATION_PROJECT: \${{ needs.plan-production.outputs.foundation_project }}
3005
3376
  ORDERED_PROJECTS: \${{ needs.plan-production.outputs.ordered_projects }}
@@ -3096,6 +3467,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
3096
3467
  test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'production AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
3097
3468
  test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'production AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
3098
3469
  test -n "$AWS_REGION" || { echo 'production AWS_REGION is required'; exit 1; }
3470
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
3471
+ { echo 'production environment account differs from committed deployment topology'; exit 1; }
3472
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
3473
+ { echo 'production environment Region differs from committed deployment topology'; exit 1; }
3099
3474
  test -n "$EBK_FLEET_ORIGIN" ||
3100
3475
  { echo 'production environment EBK_FLEET_ORIGIN is required'; exit 1; }
3101
3476
  test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
@@ -3107,8 +3482,7 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
3107
3482
  test -n "$AWS_PLAN_GENERATED_SECRET_ARNS_JSON" &&
3108
3483
  test -n "$AWS_PLAN_PROVIDER_REFRESH_POLICY_JSON" ||
3109
3484
  { echo 'production exact plan-discovery inputs are required'; exit 1; }
3110
- test -n "$AWS_PLAN_ROLE_ARN" &&
3111
- test -n "$AWS_PLAN_STATE_KMS_KEY_ARN" ||
3485
+ test -n "$AWS_PLAN_ROLE_ARN" ||
3112
3486
  { echo 'production exact plan-authority inputs are required'; exit 1; }
3113
3487
  case "$AWS_DEPLOY_ROLE_ARN" in
3114
3488
  arn:aws:iam::$AWS_EXPECTED_ACCOUNT_ID:role/*) ;;
@@ -4762,13 +5136,51 @@ permissions:
4762
5136
  contents: read
4763
5137
 
4764
5138
  jobs:
5139
+ deployment-classification:
5140
+ name: classify exact staging deployment
5141
+ if: >-
5142
+ github.event_name == 'workflow_run' &&
5143
+ github.event.workflow_run.event == 'push' &&
5144
+ github.event.workflow_run.head_branch == 'develop' &&
5145
+ github.event.workflow_run.conclusion == 'success'
5146
+ permissions:
5147
+ actions: read
5148
+ contents: read
5149
+ outputs:
5150
+ deployed: \${{ steps.classify.outputs.deployed }}
5151
+ runs-on: ubuntu-latest
5152
+ steps:
5153
+ - id: classify
5154
+ name: Require one successful staging deployment job
5155
+ env:
5156
+ EBK_DEPLOYMENT_RUN_ID: \${{ github.event.workflow_run.id }}
5157
+ GH_TOKEN: \${{ github.token }}
5158
+ GH_REPO: \${{ github.repository }}
5159
+ run: |
5160
+ jobs="$(gh api \
5161
+ "/repos/$GH_REPO/actions/runs/$EBK_DEPLOYMENT_RUN_ID/jobs?filter=latest&per_page=100")"
5162
+ apply_count="$(printf '%s' "$jobs" | jq \
5163
+ '[.jobs[] | select(.name | startswith("${stagingApplyJobPrefix}")) | select(.conclusion == "success")] | length')"
5164
+ route_success_count="$(printf '%s' "$jobs" | jq \
5165
+ '[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "success")] | length')"
5166
+ route_skipped_count="$(printf '%s' "$jobs" | jq \
5167
+ '[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "skipped")] | length')"
5168
+ case "$apply_count:$route_success_count:$route_skipped_count" in
5169
+ 1:1:0) echo 'deployed=true' >> "$GITHUB_OUTPUT" ;;
5170
+ 0:0:1) echo 'deployed=false' >> "$GITHUB_OUTPUT" ;;
5171
+ *) echo 'Deploy route and staging application evidence disagree'; exit 1 ;;
5172
+ esac
5173
+
4765
5174
  plan:
5175
+ needs: deployment-classification
4766
5176
  if: >-
4767
- (github.event_name == 'workflow_run' &&
5177
+ always() &&
5178
+ ((github.event_name == 'workflow_run' &&
4768
5179
  github.event.workflow_run.conclusion == 'success' &&
4769
5180
  github.event.workflow_run.event == 'push' &&
4770
- github.event.workflow_run.head_branch == 'develop') ||
4771
- github.event_name == 'workflow_dispatch'
5181
+ github.event.workflow_run.head_branch == 'develop' &&
5182
+ needs.deployment-classification.outputs.deployed == 'true') ||
5183
+ github.event_name == 'workflow_dispatch')
4772
5184
  outputs:
4773
5185
  deployment_run_attempt: \${{ steps.identity.outputs.deployment_run_attempt }}
4774
5186
  deployment_run_id: \${{ steps.identity.outputs.deployment_run_id }}
@@ -4875,9 +5287,10 @@ jobs:
4875
5287
  EBK_SMOKE_AUTHORITY_VERSION: \${{ vars.EBK_SMOKE_AUTHORITY_VERSION }}
4876
5288
  EBK_SMOKE_TARGETS: \${{ vars.EBK_SMOKE_TARGETS }}
4877
5289
  EBK_SOURCE_COMMIT: \${{ needs.plan.outputs.source_commit }}
4878
- PLAYWRIGHT_BROWSERS_PATH: \${{ runner.temp }}/ebk-playwright-browsers
4879
5290
  runs-on: ubuntu-latest
4880
5291
  steps:
5292
+ - name: Initialize private browser cache path
5293
+ run: echo "PLAYWRIGHT_BROWSERS_PATH=$RUNNER_TEMP/ebk-playwright-browsers" >> "$GITHUB_ENV"
4881
5294
  - name: Validate credential-free preparation
4882
5295
  run: |
4883
5296
  test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
@@ -5261,14 +5674,14 @@ jobs:
5261
5674
  test "$(node --version | cut -d. -f1)" = v22
5262
5675
  - name: Run exact deployed smoke
5263
5676
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5264
- run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
5677
+ run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
5265
5678
  - name: Verify reviewed frontend visual baseline
5266
5679
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5267
- run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:frontend-visual"
5680
+ run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:frontend-visual"
5268
5681
  - name: Verify deployed frontend security headers
5269
5682
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5270
5683
  run: |
5271
- node node_modules/nx/bin/nx.js run \
5684
+ node node_modules/nx/dist/bin/nx.js run \
5272
5685
  "$EBK_PROJECT:frontend-security-headers" -- \
5273
5686
  --url="$EBK_SMOKE_URL"
5274
5687
  - name: Bind exact alarm and data-integrity evidence
@@ -6715,11 +7128,11 @@ jobs:
6715
7128
  if [ "$EBK_ACTION" = apply ]; then args+=("--apply"); fi
6716
7129
  pnpm nx run "$EBK_PROJECT:secret-metadata" -- "\${args[@]}"
6717
7130
  `,
6718
- '.github/workflows/sync-back.yml': `name: Sync production history
7131
+ '.github/workflows/sync-back.yml': `name: Sync protected main history
6719
7132
 
6720
7133
  on:
6721
7134
  workflow_run:
6722
- workflows: [Deploy]
7135
+ workflows: [Verify]
6723
7136
  types: [completed]
6724
7137
 
6725
7138
  permissions:
@@ -6730,6 +7143,7 @@ jobs:
6730
7143
  sync-back:
6731
7144
  if: >-
6732
7145
  github.event.workflow_run.conclusion == 'success' &&
7146
+ github.event.workflow_run.event == 'push' &&
6733
7147
  github.event.workflow_run.head_branch == 'main'
6734
7148
  runs-on: ubuntu-latest
6735
7149
  steps:
@@ -6743,8 +7157,8 @@ jobs:
6743
7157
  gh pr create \
6744
7158
  --head main \
6745
7159
  --base develop \
6746
- --title 'chore: sync production history to develop' \
6747
- --body 'Managed EBK sync-back. Review and merge; this workflow never pushes or auto-merges.'
7160
+ --title 'chore(git): sync protected main to develop' \
7161
+ --body 'Managed EBK source-history sync. Review and merge; this does not claim or waive independent staging or production deployment evidence, and this workflow never pushes or auto-merges.'
6748
7162
  number=$(gh pr list --state open --head main --base develop --json number --jq '.[0].number')
6749
7163
  fi
6750
7164
  echo "Sync-back pull request #$number is ready for review."
@@ -6989,6 +7403,8 @@ dist
6989
7403
  .open-next
6990
7404
  .sst
6991
7405
  **/next-env.d.ts
7406
+ **/api/generated/*.json
7407
+ **/database/migrations/**
6992
7408
  pnpm-lock.yaml
6993
7409
  tools/blueprint-trust-check.mjs
6994
7410
  `,
@@ -7096,6 +7512,16 @@ use the direct installed \`.bin/ebk blueprint verification-review\` entry docume
7096
7512
  \`docs/blueprints.md\`.
7097
7513
 
7098
7514
  Workspace creation only writes source files. It does not create or change cloud resources.
7515
+
7516
+ New workspaces default to the one-account \`starter\` deployment profile. Configure the exact
7517
+ shared account and Region in \`ebk.repository-policy.json\`, authenticate through IAM Identity
7518
+ Center, add exact development and staging CIDRs to
7519
+ \`packages/foundation/security/router-waf.json\`, and use
7520
+ \`pnpm nx run ${name}:deploy-local -- --stage=development
7521
+ --profile=<profile> --expectedRoleArn=<role-arn>\`. Never invoke SST directly.
7522
+ For a brand-new stage with generated Product Slices, complete the documented workload-role
7523
+ Foundation publication and pass \`--phase=foundation-bootstrap\` once before publishing and
7524
+ deploying the complete stage.
7099
7525
  `,
7100
7526
  'docs/architecture.md': `# Workspace architecture
7101
7527
 
@@ -7112,6 +7538,11 @@ Foundation implementation files.
7112
7538
  Canonical consumer stages use the matching \`development\`, \`staging\`, or \`production\`
7113
7539
  Foundation. Framework-derived personal stages may use only the \`development\`
7114
7540
  Foundation; generic preview stages are unsupported.
7541
+
7542
+ The committed deployment profile changes account and local/GitHub authority only. Every
7543
+ canonical stage always owns a separate Router/domain, Aurora cluster and migration history,
7544
+ Cognito user pool, event bus, secret namespace, IAM boundary, logs, queues, ECR namespace, and
7545
+ stage-qualified SSM discovery contract. Discovery rejects a record for another canonical stage.
7115
7546
  `,
7116
7547
  'docs/git-workflow.md': `# Protected Git workflow
7117
7548
 
@@ -7132,6 +7563,9 @@ GitHub rulesets, environments, and AWS IAM are the authorization boundaries; loc
7132
7563
  only defense in depth.
7133
7564
 
7134
7565
  The \`staging\` environment accepts only \`develop\`; \`production\` accepts only \`main\`.
7566
+ These workflows exist for every deployment profile. The staging workflow is optional for
7567
+ \`starter\`, whose governed local Nx target may deploy development and staging. Production is
7568
+ GitHub-only for every profile; \`fully-isolated\` also requires GitHub for staging.
7135
7569
  The committed \`productionApproval.mode\` selects one of two production authorization
7136
7570
  contracts:
7137
7571
 
@@ -7154,14 +7588,22 @@ contracts:
7154
7588
  Both modes retain required checks, a non-author CODEOWNER approval, no ruleset bypass, the
7155
7589
  literal production environment, and its branch-scoped credentials. The base \`staging\` and
7156
7590
  \`production\` environments own deployment, smoke, and ECR evidence roles plus their
7157
- variables; production also owns the staging-registry promotion pull role. Dedicated
7591
+ variables; isolated profiles also keep the staging-registry promotion pull role in production.
7592
+ Starter promotion authenticates once to the shared account and copies the exact staging digest
7593
+ into a distinct production ECR repository. Dedicated
7158
7594
  \`infrastructure-plan-<stage>\` environments gate read-only role use. Separate
7159
7595
  \`async-recovery-<stage>\`, \`performance-aggregate-<stage>\`,
7160
7596
  \`performance-cost-<stage>\`, \`performance-load-<stage>\`, and
7161
7597
  \`reliability-read-<stage>\` environments own only the role and variables required by that
7162
7598
  capability. Infrastructure-plan jobs use a repository-variable read-only role/input inventory;
7163
7599
  an independent clean audit job assumes that exact environment-subject role and verifies its
7164
- trust and complete inline policy before any repository-backed plan job may assume it. Every
7600
+ trust and complete inline policy before any repository-backed plan job may assume it. Plan-role
7601
+ trust uses GitHub's immutable repository identity subject,
7602
+ \`repo:<owner>@<owner-id>/<repository>@<repository-id>:environment:<environment>\`.
7603
+ The audit derives it from the workflow's repository name, owner ID, repository ID, and literal
7604
+ environment and refuses missing IDs or the older mutable name-only subject. Update every
7605
+ adopter-owned AWS OIDC role trust to the immutable identity before protected use; never retain
7606
+ both forms. Every
7165
7607
  \`*-staging\` environment accepts only \`develop\`; every
7166
7608
  \`*-production\` environment accepts only \`main\`; reviewer, self-review, and wait protection
7167
7609
  matches the corresponding base stage. The credential-free \`npm-prerelease\` environment
@@ -7303,18 +7745,35 @@ materializes, close and reopen the pull request as a human and then approve it.
7303
7745
  `,
7304
7746
  'docs/repository-operating-policy.md': `# Repository Operating Policy
7305
7747
 
7306
- \`ebk.repository-policy.json\` is the reviewed source of truth for production access,
7307
- production approval, per-Slice readiness, and the one shared production Foundation profile.
7748
+ \`ebk.repository-policy.json\` is the reviewed source of truth for deployment profile, exact
7749
+ stage account IDs, deployment Region, production access, production approval, per-Slice
7750
+ readiness, and the one shared production Foundation profile.
7308
7751
  New workspaces start with production disabled and explicit Team-compatible
7309
7752
  \`productionApproval.mode: "promotion-pr"\`. A missing approval mode or Slice entry fails
7310
7753
  closed; no GitHub capability is allowed to choose a mode implicitly.
7311
7754
 
7755
+ Deployment profiles are proportional to operator maturity:
7756
+
7757
+ - \`starter\` (default) requires one shared account, permits local development and staging
7758
+ through the governed root Nx target, and reserves production for protected GitHub deployment;
7759
+ - \`production-isolated\` requires one development/staging account and a distinct production
7760
+ account; only production is GitHub-only; and
7761
+ - \`fully-isolated\` requires three distinct accounts; staging and production are GitHub-only.
7762
+
7763
+ Equal account IDs never imply shared stage resources. Router, Aurora, Cognito, events, secrets,
7764
+ IAM, logs, queues, resource names, and SSM discovery remain stage-specific. Existing prerelease
7765
+ repositories migrate explicitly to \`fully-isolated\`; upgrades never infer or weaken topology.
7766
+ That profile-only migration retains the former authority model but stays undeployable until exact
7767
+ development, staging, and production account IDs plus \`deploymentRegion\` are committed.
7768
+
7312
7769
  The named repository modes are source-writing presets, not runtime state. Report a change
7313
7770
  first and inspect the complete current/next policy:
7314
7771
 
7315
7772
  \`\`\`bash
7316
7773
  pnpm nx g @empire-builder-kit/nx:repository-policy \\
7317
- --preset=controlled-production --slices=billing
7774
+ --deploymentProfile=starter --deploymentRegion=us-east-1 \\
7775
+ --developmentAccountId=111122223333 --stagingAccountId=111122223333 \\
7776
+ --productionAccountId=111122223333
7318
7777
  \`\`\`
7319
7778
 
7320
7779
  Apply the reviewed source change separately with \`--apply\`. This command never contacts AWS
@@ -7324,6 +7783,66 @@ Use \`--productionApprovalMode=promotion-pr|environment-reviewer\` for an explic
7324
7783
  transition. This changes committed policy only; a separate bootstrap report/apply reconciles
7325
7784
  remote GitHub settings.
7326
7785
 
7786
+ After applying exact topology, authenticate with temporary federated credentials for development
7787
+ or staging and invoke
7788
+ \`pnpm nx run ${name}:deploy-local -- --stage=<stage> --profile=<profile>
7789
+ --expectedRoleArn=<role-arn>\`. Production runs only through the protected main-branch GitHub
7790
+ workflow. Each admitted local invocation plans the complete current stage, displays the stage
7791
+ summary and first material project, and requires a real TTY response of
7792
+ \`approve:<action>:<stage>:<project>:<current-plan-digest>\`. A human or authorized agent may type
7793
+ that acknowledgement; it is active operator control, not a claim of independent human review.
7794
+ The target creates one private ten-minute capability for that project, rechecks the exact source,
7795
+ artifacts, account, Region, role, stage, policy, state, and lock without a second provider preview,
7796
+ consumes the capability once, and removes it on every exit path. Unattended scripts cannot approve.
7797
+ Every material deploy or refresh mutates at most the first material project and ends with a
7798
+ controlled non-success \`replan-required\` handoff, including the last project in the ordered stage.
7799
+ Rerun the complete stage, review the new current summary, and repeat until one invocation proves
7800
+ every project is semantically converged. Only that convergence invocation succeeds. A preview is
7801
+ recent review and audit evidence; it is not represented as a cryptographically replayable Pulumi
7802
+ transaction. A complete, unlocked deploy plan containing only provider reads with no changed
7803
+ properties, replacement, failure, or unmatched risk finding is retained as observation evidence
7804
+ and counts as semantically converged. The standalone project planner may still return its
7805
+ conservative non-success for that drift observation; the root local transaction accepts only a
7806
+ freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
7807
+ only when their logical ID and resource type match exactly. Any stale or ambiguous
7808
+ observation fails closed; a plan with desired changes remains material and requires review. One
7809
+ narrower case stays reviewable instead of becoming
7810
+ drift: when an upstream Foundation apply changes only both external framework-version tag
7811
+ projections (plus SST's already-bounded name hydration), the retained
7812
+ external identity must match one exact non-replacing provider read in the same ordinary plan.
7813
+ Missing read correlation, owned resources, other tag or property changes, incomplete tag pairs,
7814
+ and provider failures remain drift and fail closed. The local target requires the exact SST v5 account bootstrap normally
7815
+ created by the pinned \`ebk dev up\` path, then creates and attests only missing exact
7816
+ per-project/canonical-stage state passphrases before its formal read-only plans. This passphrase
7817
+ bootstrap is the only persistent pre-acknowledgement write; it creates no deployment authority
7818
+ and is reported in bounded local evidence. Clean immutable source, immediate per-project plans,
7819
+ migration and retention preflights, and secret-free result
7820
+ evidence remain mandatory. Direct SST is forbidden.
7821
+
7822
+ After an interrupted apply, use the same target with \`--action=refresh\` and the exact
7823
+ phase, stage, profile, and role. It retains the same plan-summary review boundary, admits only
7824
+ bounded drift-detected refresh evidence, and never turns refresh into an implicit deploy. When
7825
+ the interrupted write leaves a recorded provider output unusable by the ordinary desired preview,
7826
+ the plan stops without mutation. The non-writing refresh preview separates provider refresh
7827
+ records from its later desired preview and cannot replace failed ordinary desired authority;
7828
+ repair the recorded dependency or use the documented
7829
+ operator recovery path before planning again.
7830
+
7831
+ A stage-wide refresh still plans and authorizes every selected project. When one exact retained
7832
+ project plan proves an absent SST state that is unlocked, complete, empty, and drift-free, the
7833
+ root target records that project as a refresh no-op instead of invoking SST against a nonexistent
7834
+ stage. It also retains the pinned provider's exact SSM \`ebk:framework-version\` \`tagsAll\`
7835
+ normalization as a semantically inert observation after the same bounded shape
7836
+ repeats following refresh. Value changes, other tags, security findings, deletes, replacements,
7837
+ sensitive properties, and every other resource type remain material. Locked, incomplete, failed,
7838
+ or otherwise drifted plans are never skipped.
7839
+
7840
+ For a stage with no workload-role account baseline, publish the fixed Foundation-only candidate
7841
+ and invoke this root target with \`--phase=foundation-bootstrap\`. After Foundation outputs exist,
7842
+ publish the complete \`full-stage\` candidate and invoke the target without \`--phase\` (or with
7843
+ \`--phase=full-stage\`). The bootstrap phase selects exactly Foundation; it is not an arbitrary
7844
+ project filter.
7845
+
7327
7846
  Validation requires both an explicit production approval mode and an explicit Foundation
7328
7847
  profile. It does not infer omitted values. Use \`promotion-pr\` for a private GitHub Team
7329
7848
  repository. Use \`environment-reviewer\` only when the repository plan can enforce a separate
@@ -7337,10 +7856,16 @@ environment reviewer.
7337
7856
 
7338
7857
  Protected production deploy and refresh validate the complete Foundation/Slice set before AWS
7339
7858
  access. The Foundation is always ordered first. The set is derived from committed policy and
7340
- cannot be narrowed with an ad hoc project argument.
7341
-
7342
- \`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` is a prerelease candidate at
7343
- 0–4 ACU with a five-minute auto-pause and potential cold-resume latency. Both keep managed NAT,
7859
+ cannot be narrowed with an ad hoc project argument. The retained stage preview remains review
7860
+ evidence. Immediately before each project mutation the workflow computes one fresh plan against
7861
+ current state and applies it without a second redundant preview. Source, artifacts, configuration,
7862
+ policy, environment, and protected identities remain exact. Provider-only read/discard observations
7863
+ may appear or disappear, but any new delete, replacement, import, stateful or security change, IAM
7864
+ expansion, protection reduction, or unclassified resource stops for renewed approval. After an
7865
+ upstream mutation, every downstream project therefore receives a fresh independent risk decision.
7866
+
7867
+ \`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` keeps a smaller
7868
+ 0.5–4 ACU warm floor for starter production traffic. Both keep managed NAT,
7344
7869
  30-day backups, deletion protection, retained data, and the protected GitHub/OIDC/AWS path.
7345
7870
  Changing policy affects only later lifecycle requests; it never deploys or removes resources.
7346
7871
  Changing profile requires a separate reviewed Foundation deployment and retained operational
@@ -7411,7 +7936,7 @@ deploy or refresh before AWS access. Protected remove and unlock remain usable f
7411
7936
  There is no local or silent bypass in this prerelease contract; any future waiver mechanism
7412
7937
  must be committed, reviewable, and auditable.
7413
7938
  `,
7414
- 'ebk.repository-policy.json': json((0, index_js_4.createIncubationRepositoryPolicy)()),
7939
+ 'ebk.repository-policy.json': json(initialRepositoryPolicy),
7415
7940
  'nx.json': json({
7416
7941
  $schema: './node_modules/nx/schemas/nx-schema.json',
7417
7942
  defaultBase: 'develop',
@@ -7451,6 +7976,7 @@ must be committed, reviewable, and auditable.
7451
7976
  inputs: ['default', '^production'],
7452
7977
  },
7453
7978
  },
7979
+ analytics: false,
7454
7980
  neverConnectToCloud: true,
7455
7981
  useDaemonProcess: false,
7456
7982
  }),
@@ -7460,8 +7986,8 @@ must be committed, reviewable, and auditable.
7460
7986
  private: true,
7461
7987
  packageManager: tool_versions_js_1.WORKSPACE_PNPM_DESCRIPTOR,
7462
7988
  engines: {
7463
- node: '>=22.12.0',
7464
- pnpm: '>=10.0.0',
7989
+ node: '>=22.13.0',
7990
+ pnpm: '>=11.20.0',
7465
7991
  },
7466
7992
  scripts: {
7467
7993
  build: `nx run-many -t build --exclude=${name}`,
@@ -7479,10 +8005,12 @@ must be committed, reviewable, and auditable.
7479
8005
  },
7480
8006
  devDependencies: {
7481
8007
  '@aws-sdk/client-iam': TOOL_VERSIONS.awsSdk,
8008
+ '@aws-sdk/client-rds': TOOL_VERSIONS.awsSdk,
7482
8009
  '@aws-sdk/client-s3': TOOL_VERSIONS.awsSdk,
7483
8010
  '@aws-sdk/client-secrets-manager': TOOL_VERSIONS.awsSdk,
7484
8011
  '@aws-sdk/client-ssm': TOOL_VERSIONS.awsSdk,
7485
8012
  '@aws-sdk/client-sts': TOOL_VERSIONS.awsSdk,
8013
+ '@aws-sdk/credential-providers': TOOL_VERSIONS.awsSdk,
7486
8014
  '@eslint/js': TOOL_VERSIONS.eslintJs,
7487
8015
  '@empire-builder-kit/nx': frameworkVersion,
7488
8016
  '@types/node': TOOL_VERSIONS.nodeTypes,
@@ -7515,6 +8043,15 @@ must be committed, reviewable, and auditable.
7515
8043
  protectedRef: 'refs/remotes/origin/develop',
7516
8044
  },
7517
8045
  },
8046
+ 'blueprint-trust-check-main': {
8047
+ executor: '@empire-builder-kit/nx:blueprint-trust',
8048
+ options: {
8049
+ operation: 'adopt',
8050
+ output: '.ebk/evidence/supply-chain/blueprint-trust.json',
8051
+ protectedBranch: 'main',
8052
+ protectedRef: 'refs/remotes/origin/main',
8053
+ },
8054
+ },
7518
8055
  'blueprint-trust-check-protected': {
7519
8056
  executor: '@empire-builder-kit/nx:blueprint-trust',
7520
8057
  options: {
@@ -7557,6 +8094,13 @@ must be committed, reviewable, and auditable.
7557
8094
  executor: 'nx:run-commands',
7558
8095
  options: { command: 'node tools/install-ebk-hooks.mjs' },
7559
8096
  },
8097
+ 'deploy-local': {
8098
+ cache: false,
8099
+ executor: '@empire-builder-kit/nx:local-deploy',
8100
+ options: {
8101
+ stage: 'development',
8102
+ },
8103
+ },
7560
8104
  'supply-chain-check': {
7561
8105
  executor: 'nx:run-commands',
7562
8106
  options: {
@@ -7598,6 +8142,11 @@ must be committed, reviewable, and auditable.
7598
8142
  executor: 'nx:run-commands',
7599
8143
  options: { command: 'node tools/workspace-health.mjs' },
7600
8144
  },
8145
+ 'verify-proposed': {
8146
+ dependsOn: ['supply-chain-check'],
8147
+ executor: 'nx:run-commands',
8148
+ options: { command: 'node tools/workspace-health.mjs' },
8149
+ },
7601
8150
  'workspace-health': {
7602
8151
  executor: 'nx:run-commands',
7603
8152
  options: { command: 'node tools/workspace-health.mjs' },
@@ -7620,6 +8169,10 @@ Workspace creation does not deploy this project. Review \`docs/costs.md\`,
7620
8169
 
7621
8170
  Set \`EBK_ROOT_DOMAIN\` before deploying when Route 53 in the target account owns a public
7622
8171
  domain. Without it, the shared Router uses its functional provider URL.
8172
+
8173
+ Before deploying development or staging, add at least one exact operator/probe CIDR to
8174
+ \`security/router-waf.json\`. Production is public by default behind the same managed and
8175
+ rate-limiting baseline; select its allowlist mode explicitly when required.
7623
8176
  `,
7624
8177
  'packages/foundation/contracts/foundation.json': json({
7625
8178
  contractVersion: '1.0.0',
@@ -7679,8 +8232,8 @@ seeded decision record aligned with organization-specific budgets and alerts.
7679
8232
  - Development uses EC2 NAT, a development bastion, and Aurora scale-to-zero.
7680
8233
  - Staging uses EC2 NAT and Aurora scale-to-zero by default.
7681
8234
  - Production always uses managed NAT and retains protected data. Its explicit Repository
7682
- Operating Policy profile selects either the standard always-warm Aurora floor or the
7683
- prerelease cost-optimized auto-pause candidate.
8235
+ Operating Policy profile selects either the standard 2-ACU Aurora floor or the
8236
+ cost-optimized 0.5-ACU warm floor.
7684
8237
  - ECS, EventBridge, Cognito, SNS, SSM, and CloudWatch are primarily usage-priced; logs,
7685
8238
  metrics, alarms, and retained data still require active cost review.
7686
8239
 
@@ -7698,8 +8251,69 @@ Use Nx for every supported lifecycle action:
7698
8251
  - \`pnpm nx run foundation:unlock --stage=development\`
7699
8252
  - \`pnpm nx run foundation:remove --stage=development\`
7700
8253
 
7701
- Do not invoke SST lifecycle commands directly. Production deployment belongs in the trusted
7702
- CI path.
8254
+ Do not invoke SST lifecycle commands directly. The root \`deploy-local\` target is the governed
8255
+ local path for stages admitted by the committed deployment profile. Its fixed
8256
+ \`foundation-bootstrap\` phase is the only local partial-stage path and exists solely for the
8257
+ documented first workload-role publication; isolated protected stages remain on the trusted
8258
+ GitHub path.
8259
+
8260
+ Every local invocation plans the complete selected stage but mutates at most one material
8261
+ project. Type the exact current \`approve:<action>:<stage>:<project>:<plan-digest>\` challenge in a
8262
+ real TTY after reviewing the displayed summary. A human or authorized agent may operate that TTY;
8263
+ unattended input is refused. The acknowledgement creates one ten-minute, one-use, private
8264
+ capability bound to source, artifacts, policy, environment, credentials, and current state. A
8265
+ completed deploy or refresh returns the controlled non-success \`replan-required\`; rerun the
8266
+ complete stage, review the new current summary, and continue one project at a time. Only a
8267
+ semantically converged complete stage reports success. No upstream mutation can authorize a
8268
+ downstream project. A complete, unlocked deploy plan containing only provider reads with no
8269
+ changed properties, replacement, failure, or unmatched risk finding is retained as observation
8270
+ evidence and counts as semantically converged. The standalone project planner may still return
8271
+ its conservative non-success for that drift observation; the root local transaction accepts only
8272
+ a freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
8273
+ only when their logical ID and resource type match exactly. Any stale or ambiguous
8274
+ observation fails closed; a plan with desired changes remains material and requires review. A
8275
+ material downstream plan may also retain an upstream Foundation framework-version tag advance as
8276
+ observation only when both external tag
8277
+ projections, the retained identity, optional bounded SST name hydration, and one non-replacing
8278
+ provider read correlate exactly. Every other external property or uncorroborated read remains
8279
+ drift and blocks before the TTY challenge.
8280
+
8281
+ For an interrupted local apply, run the same root target with \`--action=refresh\`, review its
8282
+ current provider-refresh evidence, and type the displayed TTY challenge. Before mutation the
8283
+ executor writes and verifies a private state recovery point plus a secret-free digest/version
8284
+ manifest. The refresh preview validates its provider and later desired-event partitions; any
8285
+ ordinary preview failure or malformed post-refresh desired event remains blocked. Provider-only
8286
+ observation noise is retained without claiming a replayable preview. Because refresh changes
8287
+ recorded state rather than cloud resources, provider-output security and protection findings
8288
+ remain explicit review requirements under the drift decision. Refresh never continues into deploy
8289
+ or claims desired-state convergence: run the governed deploy target afterward and resolve every
8290
+ remaining source-versus-state change. Exclude the \`.private.json\` recovery point from archives.
8291
+
8292
+ The root refresh target includes every selected project in its exact reviewed plan set. It skips
8293
+ the SST child only when that project's retained plan proves an absent, unlocked, complete, empty,
8294
+ and drift-free state, or when the only retained difference is the pinned provider's exact repeated
8295
+ SSM \`ebk:framework-version\` \`tagsAll\` normalization. It records the project as an explicit refresh no-op
8296
+ in either case while retaining the observation. Value changes, other tags, security findings,
8297
+ deletes, replacements, sensitive
8298
+ properties, and every other resource type remain material; every other plan must execute or fail
8299
+ closed.
8300
+
8301
+ ## Router WAF
8302
+
8303
+ Each canonical stage owns one global-scope Web ACL attached to that stage's Foundation Router.
8304
+ The committed \`security/router-waf.json\` policy requires AWS Common Rule Set, Known Bad
8305
+ Inputs, Amazon IP Reputation, an IP rate limit, CloudWatch metrics, and sampled requests.
8306
+ Development and staging fail synthesis until their allowlists contain explicit CIDRs.
8307
+ Production is public by default and may be changed to an explicit allowlist.
8308
+
8309
+ For tuning, set a nonproduction stage to \`count\`, deploy and inspect sampled requests, then
8310
+ return staging to \`block\` and run the live allow/deny, managed-rule, rate-limit, metric, and
8311
+ sampled-request probes. Write their bounded, secret-free result to
8312
+ \`security/router-waf-live-evidence.json\`, then run
8313
+ \`pnpm nx run foundation:router-waf-rehearsal\`. The executor seals the exact policy digest
8314
+ into \`router-waf-blocking-rehearsal.json\`. Production synthesis rejects a missing, stale, or
8315
+ count-mode rehearsal. Full request logging is opt-in, blocked-only, retention-bounded, and
8316
+ redacts query strings plus credential-bearing headers.
7703
8317
 
7704
8318
  Foundation stages are canonical. \`ebk dev\` derives a Product Slice personal
7705
8319
  stage from the operating-system account and maps that consumer to the
@@ -7734,6 +8348,12 @@ connects as the Slice's no-password IAM-authenticated migrator user.
7734
8348
  stage-scoped SSM/secret path and least-privilege IAM.
7735
8349
  - Public domains are configured explicitly through \`EBK_ROOT_DOMAIN\`; generated source never
7736
8350
  contains credentials or customer domain assumptions.
8351
+ - Every stage's CloudFront Router has its own global Web ACL. The baseline always includes AWS
8352
+ Common Rule Set, Known Bad Inputs, Amazon IP Reputation, rate blocking, CloudWatch metrics,
8353
+ and sampled requests. Nonproduction is allowlisted by default. Production is public by
8354
+ default, but cannot deploy until the exact policy has passed a staging blocking rehearsal.
8355
+ - Full WAF request logging is disabled by default. Enabling it keeps blocked requests only,
8356
+ uses bounded retention, and redacts query strings, cookies, authorization, and API keys.
7737
8357
 
7738
8358
  The managed \`contracts/frontend-security-headers.json\` file owns the transport
7739
8359
  minimum: HTTPS in deployed environments, one-year HSTS there, and the common
@@ -7766,7 +8386,12 @@ declare const $app: {
7766
8386
  import {
7767
8387
  FOUNDATION_SSM_SCHEMA_VERSION,
7768
8388
  STANDARD_FOUNDATION_CAPABILITIES,
8389
+ createRouterWafRules,
8390
+ createRouterWafStagePlan,
8391
+ foundationDatabaseEndpointHost,
7769
8392
  foundationSsmPath,
8393
+ normalizeRouterWafPolicy,
8394
+ reconcileRouterWafBlockingRehearsal,
7770
8395
  type FoundationSsmKey,
7771
8396
  } from '@empire-builder-kit/nx/foundation';
7772
8397
  import {
@@ -7776,6 +8401,9 @@ import {
7776
8401
  } from '@empire-builder-kit/nx/repository-policy';
7777
8402
  import { createFoundationAccountCostControlPlan } from '@empire-builder-kit/nx/performance';
7778
8403
  import accountCostPolicy from '../performance/account-cost-policy.json';
8404
+ import routerWafBlockingRehearsal from '../security/router-waf-blocking-rehearsal.json';
8405
+ import routerWafLiveEvidence from '../security/router-waf-live-evidence.json';
8406
+ import routerWafPolicy from '../security/router-waf.json';
7779
8407
  import type { FoundationWorkloadRolePreflight } from './workload-roles/preflight.js';
7780
8408
 
7781
8409
  function identityDomainPrefix(stage: string, accountId: string): string {
@@ -7827,6 +8455,18 @@ export function foundation(
7827
8455
  )
7828
8456
  : undefined;
7829
8457
  const natMode = productionConfiguration?.contract.nat ?? 'ec2';
8458
+ const normalizedRouterWafPolicy = normalizeRouterWafPolicy(routerWafPolicy);
8459
+ const wafPlan = createRouterWafStagePlan({
8460
+ policy: normalizedRouterWafPolicy,
8461
+ rehearsal: isProduction
8462
+ ? reconcileRouterWafBlockingRehearsal({
8463
+ evidence: routerWafLiveEvidence,
8464
+ policy: normalizedRouterWafPolicy,
8465
+ receipt: routerWafBlockingRehearsal,
8466
+ })
8467
+ : null,
8468
+ stage: $app.stage,
8469
+ });
7830
8470
  const natBinding = workloadRoles?.bindings.nat;
7831
8471
  if (natMode === 'ec2' && natBinding) {
7832
8472
  $transform(
@@ -7836,6 +8476,9 @@ export function foundation(
7836
8476
  args.name = natBinding.instanceProfileName;
7837
8477
  args.role = natBinding.roleName;
7838
8478
  opts.import = natBinding.instanceProfileName;
8479
+ opts.ignoreChanges = [
8480
+ ...new Set([...(opts.ignoreChanges ?? []), 'tags', 'tagsAll']),
8481
+ ];
7839
8482
  },
7840
8483
  );
7841
8484
  }
@@ -7857,8 +8500,83 @@ export function foundation(
7857
8500
  aliases: ['*.' + rootDomain],
7858
8501
  dns: sst.aws.dns(),
7859
8502
  },
7860
- }
8503
+ }
7861
8504
  : {}),
8505
+ waf: {
8506
+ logging: wafPlan.logging,
8507
+ managedRules: {
8508
+ coreRuleSet: true,
8509
+ knownBadInputs: true,
8510
+ sqlInjection: false,
8511
+ },
8512
+ rateLimitPerIp: wafPlan.rateLimitPerIp,
8513
+ },
8514
+ transform: {
8515
+ cdn(args: any) {
8516
+ if (!args.webAclArn) {
8517
+ throw new Error(
8518
+ 'Foundation Router must attach its exact global Web ACL.',
8519
+ );
8520
+ }
8521
+ },
8522
+ waf(args: any, opts: any) {
8523
+ if (args.scope !== 'CLOUDFRONT') {
8524
+ throw new Error(
8525
+ 'Foundation Router Web ACL must use global CloudFront scope.',
8526
+ );
8527
+ }
8528
+ const ipSets: { ipv4Arn?: any; ipv6Arn?: any } = {};
8529
+ if (wafPlan.ipv4Cidrs.length > 0) {
8530
+ ipSets.ipv4Arn = new aws.wafv2.IpSet(
8531
+ 'FoundationRouterIpv4Allowlist',
8532
+ {
8533
+ addresses: wafPlan.ipv4Cidrs,
8534
+ ipAddressVersion: 'IPV4',
8535
+ name: 'foundation-' + wafPlan.stage + '-router-ipv4-allowlist',
8536
+ scope: 'CLOUDFRONT',
8537
+ tags: {
8538
+ 'ebk:policy-digest': wafPlan.policyDigest,
8539
+ 'ebk:stage': wafPlan.stage,
8540
+ },
8541
+ },
8542
+ { provider: opts.provider },
8543
+ ).arn;
8544
+ }
8545
+ if (wafPlan.ipv6Cidrs.length > 0) {
8546
+ ipSets.ipv6Arn = new aws.wafv2.IpSet(
8547
+ 'FoundationRouterIpv6Allowlist',
8548
+ {
8549
+ addresses: wafPlan.ipv6Cidrs,
8550
+ ipAddressVersion: 'IPV6',
8551
+ name: 'foundation-' + wafPlan.stage + '-router-ipv6-allowlist',
8552
+ scope: 'CLOUDFRONT',
8553
+ tags: {
8554
+ 'ebk:policy-digest': wafPlan.policyDigest,
8555
+ 'ebk:stage': wafPlan.stage,
8556
+ },
8557
+ },
8558
+ { provider: opts.provider },
8559
+ ).arn;
8560
+ }
8561
+ args.defaultAction = { allow: {} };
8562
+ args.rules = createRouterWafRules(wafPlan, ipSets);
8563
+ args.scope = 'CLOUDFRONT';
8564
+ args.tags = {
8565
+ ...(args.tags ?? {}),
8566
+ 'ebk:policy-digest': wafPlan.policyDigest,
8567
+ 'ebk:stage': wafPlan.stage,
8568
+ };
8569
+ args.visibilityConfig = {
8570
+ cloudwatchMetricsEnabled: true,
8571
+ metricName:
8572
+ 'Ebk' +
8573
+ wafPlan.stage[0].toUpperCase() +
8574
+ wafPlan.stage.slice(1) +
8575
+ 'FoundationRouterWaf',
8576
+ sampledRequestsEnabled: true,
8577
+ };
8578
+ },
8579
+ },
7862
8580
  });
7863
8581
  const database = new sst.aws.Aurora('FoundationDatabase', {
7864
8582
  // SQL bootstrap and migrations run inside the VPC. Keep the public HTTPS
@@ -7882,6 +8600,46 @@ export function foundation(
7882
8600
  },
7883
8601
  vpc,
7884
8602
  });
8603
+ const databaseClusterEndpoint = database.nodes.cluster.endpoint.apply(
8604
+ foundationDatabaseEndpointHost,
8605
+ );
8606
+ const databaseReaderEndpoint = database.nodes.cluster.readerEndpoint.apply(
8607
+ foundationDatabaseEndpointHost,
8608
+ );
8609
+ // SST v4.17.1 creates a top-level LinkRef on the next tick and its Aurora
8610
+ // implementation unconditionally splits cluster endpoints. During a
8611
+ // governed refresh after an interrupted first deploy, those provider
8612
+ // outputs are legitimately absent. Preserve the normal link contract while
8613
+ // keeping that read-only recovery program total; a real deploy resolves the
8614
+ // exact endpoints before any discovery resource is created.
8615
+ database.getSSTLink = () => ({
8616
+ include: [
8617
+ sst.aws.permission({
8618
+ actions: ['secretsmanager:GetSecretValue'],
8619
+ resources: [database.secretArn],
8620
+ }),
8621
+ sst.aws.permission({
8622
+ actions: [
8623
+ 'rds-data:BatchExecuteStatement',
8624
+ 'rds-data:BeginTransaction',
8625
+ 'rds-data:CommitTransaction',
8626
+ 'rds-data:ExecuteStatement',
8627
+ 'rds-data:RollbackTransaction',
8628
+ ],
8629
+ resources: [database.clusterArn],
8630
+ }),
8631
+ ],
8632
+ properties: {
8633
+ clusterArn: database.clusterArn,
8634
+ database: database.database,
8635
+ host: databaseClusterEndpoint,
8636
+ password: database.password,
8637
+ port: database.port,
8638
+ reader: databaseReaderEndpoint,
8639
+ secretArn: database.secretArn,
8640
+ username: database.username,
8641
+ },
8642
+ });
7885
8643
  const foundationAccountId = aws.getCallerIdentityOutput({}).accountId;
7886
8644
  const foundationPartition = aws.getPartitionOutput({});
7887
8645
  const foundationRegion = aws.getRegionOutput({});
@@ -7944,6 +8702,7 @@ export function foundation(
7944
8702
  },
7945
8703
  ],
7946
8704
  resources: [alertTopic.arn],
8705
+ sid: 'AllowBudgetAlerts',
7947
8706
  },
7948
8707
  {
7949
8708
  actions: ['sns:Publish'],
@@ -7957,6 +8716,7 @@ export function foundation(
7957
8716
  type: 'Service',
7958
8717
  }],
7959
8718
  resources: [alertTopic.arn],
8719
+ sid: 'AllowCostAnomalyAlerts',
7960
8720
  },
7961
8721
  {
7962
8722
  actions: ['sns:Publish'],
@@ -7979,6 +8739,7 @@ export function foundation(
7979
8739
  type: 'Service',
7980
8740
  }],
7981
8741
  resources: [alertTopic.arn],
8742
+ sid: 'AllowRdsEventAlerts',
7982
8743
  },
7983
8744
  {
7984
8745
  actions: ['sns:Publish'],
@@ -8003,6 +8764,7 @@ export function foundation(
8003
8764
  },
8004
8765
  ],
8005
8766
  resources: [alertTopic.arn],
8767
+ sid: 'AllowCloudWatchAlarmAlerts',
8006
8768
  },
8007
8769
  ],
8008
8770
  }).json,
@@ -8031,6 +8793,41 @@ export function foundation(
8031
8793
  { dependsOn: [alertTopicPolicy] },
8032
8794
  )
8033
8795
  : undefined;
8796
+ const accountCostAnomalyMonitor = isProduction
8797
+ ? new aws.costexplorer.AnomalyMonitor(
8798
+ 'FoundationAccountCostAnomalyMonitor',
8799
+ {
8800
+ monitorDimension: 'SERVICE',
8801
+ monitorType: 'DIMENSIONAL',
8802
+ name: '${name}-account-service-cost-anomalies',
8803
+ tags: {
8804
+ 'ebk:cost-policy-digest': accountCostControl.policyDigest,
8805
+ 'ebk:cost-scope': '${name}/account/production',
8806
+ 'ebk:owner': accountCostControl.owner,
8807
+ },
8808
+ },
8809
+ )
8810
+ : undefined;
8811
+ const accountCostAnomalySubscription =
8812
+ accountCostControl.anomaly && accountCostAnomalyMonitor
8813
+ ? new aws.costexplorer.AnomalySubscription(
8814
+ 'FoundationAccountCostAnomalySubscription',
8815
+ {
8816
+ frequency: accountCostControl.anomaly.subscription.frequency,
8817
+ monitorArnLists: [accountCostAnomalyMonitor.arn],
8818
+ name: accountCostControl.anomaly.subscription.name,
8819
+ subscribers: [{ address: alertTopic.arn, type: 'SNS' }],
8820
+ tags: {
8821
+ 'ebk:cost-policy-digest': accountCostControl.policyDigest,
8822
+ 'ebk:cost-scope': '${name}/account/production',
8823
+ 'ebk:owner': accountCostControl.owner,
8824
+ },
8825
+ thresholdExpression:
8826
+ accountCostControl.anomaly.subscription.thresholdExpression,
8827
+ },
8828
+ { dependsOn: [accountCostAnomalyMonitor, alertTopicPolicy] },
8829
+ )
8830
+ : undefined;
8034
8831
  const databaseEvents = isProduction
8035
8832
  ? new aws.rds.EventSubscription(
8036
8833
  'FoundationDatabaseEvents',
@@ -8133,12 +8930,10 @@ export function foundation(
8133
8930
  clusterId: cluster.id,
8134
8931
  contractVersion: '1.0.0',
8135
8932
  databaseClusterArn: database.clusterArn,
8136
- databaseClusterEndpoint: database.nodes.cluster.endpoint.apply(
8137
- (endpoint: string) => endpoint.split(':')[0],
8138
- ),
8933
+ databaseClusterEndpoint,
8139
8934
  databaseClusterId: database.id,
8140
8935
  databaseClusterResourceId: database.nodes.cluster.clusterResourceId,
8141
- databaseHost: database.host,
8936
+ databaseHost: databaseClusterEndpoint,
8142
8937
  databaseMasterSecretArn: database.secretArn,
8143
8938
  databasePort: database.port.apply(String),
8144
8939
  eventBusArn: eventBus.arn,
@@ -8160,6 +8955,7 @@ export function foundation(
8160
8955
  capabilities: STANDARD_FOUNDATION_CAPABILITIES,
8161
8956
  clusterId: cluster.id,
8162
8957
  contractVersion: '1.0.0',
8958
+ costAnomalySubscriptionArn: accountCostAnomalySubscription?.arn,
8163
8959
  databaseCapacityAlarmName: databaseCapacityAlarm.name,
8164
8960
  dashboardName: dashboard.dashboardName,
8165
8961
  databaseClusterId: database.id,
@@ -8191,13 +8987,49 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8191
8987
  actualNotificationThresholdPercent: 80,
8192
8988
  forecastNotificationThresholdPercent: 100,
8193
8989
  kind: 'ebk-foundation-account-cost-policy',
8194
- monthlyMaximumUsd: 1_000,
8990
+ monthlyMaximumUsd: deploymentProfile === 'starter' ? 250 : 1_000,
8195
8991
  owner: 'replace-with-owner',
8196
8992
  reviewAt: null,
8197
8993
  schemaVersion: 1,
8198
8994
  status: 'example',
8199
8995
  workspace: name,
8200
8996
  }),
8997
+ 'packages/foundation/security/router-waf.json': json({
8998
+ kind: 'ebk-router-waf-policy',
8999
+ logging: {
9000
+ enabled: false,
9001
+ retention: '1 week',
9002
+ },
9003
+ rateLimitPerIp: 2_000,
9004
+ schemaVersion: 1,
9005
+ stages: {
9006
+ development: {
9007
+ access: 'allowlist',
9008
+ allowedCidrs: [],
9009
+ mode: 'block',
9010
+ },
9011
+ production: {
9012
+ access: 'public',
9013
+ allowedCidrs: [],
9014
+ mode: 'block',
9015
+ },
9016
+ staging: {
9017
+ access: 'allowlist',
9018
+ allowedCidrs: [],
9019
+ mode: 'block',
9020
+ },
9021
+ },
9022
+ }),
9023
+ 'packages/foundation/security/router-waf-blocking-rehearsal.json': json({
9024
+ kind: 'ebk-router-waf-blocking-rehearsal',
9025
+ schemaVersion: 1,
9026
+ status: 'pending',
9027
+ }),
9028
+ 'packages/foundation/security/router-waf-live-evidence.json': json({
9029
+ kind: 'ebk-router-waf-live-evidence',
9030
+ schemaVersion: 1,
9031
+ status: 'pending',
9032
+ }),
8201
9033
  'packages/foundation/configuration/declarations.json': json({
8202
9034
  project: 'foundation',
8203
9035
  schemaVersion: 1,
@@ -8230,29 +9062,96 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8230
9062
  version: '4.17.1',
8231
9063
  },
8232
9064
  policy: {
9065
+ adoptedImports: [
9066
+ {
9067
+ authority: 'workload-role-account-baseline',
9068
+ logicalId: 'FoundationVpcNatInstanceProfile',
9069
+ privateIdentifierTemplate: 'ebk-{stage}-foundation-nat-profile',
9070
+ recoveryRunbookRef: 'docs/workload-role-account-baseline.md',
9071
+ removalBehavior: 'retain',
9072
+ resourceType: 'aws:iam/instanceProfile:InstanceProfile',
9073
+ },
9074
+ ],
8233
9075
  additiveLowRiskTypes: [
8234
9076
  'aws:cloudwatch/dashboard:Dashboard',
8235
9077
  'aws:cloudwatch/logGroup:LogGroup',
8236
9078
  'aws:cloudwatch/metricAlarm:MetricAlarm',
8237
9079
  ],
9080
+ additiveReviewedTypes: [
9081
+ 'aws:cloudwatch/eventArchive:EventArchive',
9082
+ 'aws:cloudwatch/eventBus:EventBus',
9083
+ 'aws:cognito/userPoolDomain:UserPoolDomain',
9084
+ 'aws:ec2/defaultSecurityGroup:DefaultSecurityGroup',
9085
+ 'aws:ec2/internetGateway:InternetGateway',
9086
+ 'aws:ec2/routeTable:RouteTable',
9087
+ 'aws:ec2/routeTableAssociation:RouteTableAssociation',
9088
+ 'aws:ec2/securityGroup:SecurityGroup',
9089
+ 'aws:ec2/subnet:Subnet',
9090
+ 'aws:ec2/vpc:Vpc',
9091
+ 'aws:ecs/cluster:Cluster',
9092
+ 'aws:ecs/clusterCapacityProviders:ClusterCapacityProviders',
9093
+ 'aws:rds/clusterParameterGroup:ClusterParameterGroup',
9094
+ 'aws:rds/eventSubscription:EventSubscription',
9095
+ 'aws:rds/parameterGroup:ParameterGroup',
9096
+ 'aws:rds/subnetGroup:SubnetGroup',
9097
+ 'aws:servicediscovery/privateDnsNamespace:PrivateDnsNamespace',
9098
+ 'aws:sns/topic:Topic',
9099
+ 'aws:sns/topicPolicy:TopicPolicy',
9100
+ 'aws:wafv2/webAclLoggingConfiguration:WebAclLoggingConfiguration',
9101
+ 'pulumi-nodejs:dynamic:Resource',
9102
+ 'pulumi:providers:aws',
9103
+ 'pulumi:providers:pulumi-nodejs',
9104
+ 'pulumi:providers:random',
9105
+ 'pulumi:pulumi:Stack',
9106
+ 'sst:aws:Aurora',
9107
+ 'sst:aws:Bus',
9108
+ 'sst:aws:CDN',
9109
+ 'sst:aws:Cluster',
9110
+ 'sst:aws:CognitoUserPool',
9111
+ 'sst:aws:Router',
9112
+ 'sst:aws:SnsTopic',
9113
+ 'sst:aws:Vpc',
9114
+ 'sst:sst:LinkRef',
9115
+ 'sst:sst:Version',
9116
+ ],
8238
9117
  costOrQuotaTypes: [
8239
9118
  'aws:apigatewayv2/api:Api',
8240
9119
  'aws:budgets/budget:Budget',
9120
+ 'aws:cloudfront/cachePolicy:CachePolicy',
8241
9121
  'aws:cloudfront/distribution:Distribution',
9122
+ 'aws:cloudfront/function:Function',
9123
+ 'aws:cloudfront/keyValueStore:KeyValueStore',
9124
+ 'aws:costexplorer/anomalyMonitor:AnomalyMonitor',
9125
+ 'aws:costexplorer/anomalySubscription:AnomalySubscription',
8242
9126
  'aws:cognito/userPool:UserPool',
9127
+ 'aws:ec2/eip:Eip',
9128
+ 'aws:ec2/eipAssociation:EipAssociation',
9129
+ 'aws:ec2/instance:Instance',
8243
9130
  'aws:ec2/natGateway:NatGateway',
8244
9131
  'aws:rds/cluster:Cluster',
9132
+ 'aws:rds/clusterInstance:ClusterInstance',
9133
+ 'aws:wafv2/ipSet:IpSet',
9134
+ 'aws:wafv2/webAcl:WebAcl',
8245
9135
  ],
8246
9136
  failClosedOnUnknown: true,
8247
9137
  protectedPlanAuthority: 'deploy-role-residual',
8248
9138
  standingPolicyRiskClasses: ['no-change', 'additive-low-risk'],
8249
9139
  statefulTypes: [
9140
+ 'aws:cloudwatch/eventArchive:EventArchive',
9141
+ 'aws:cloudwatch/eventBus:EventBus',
8250
9142
  'aws:cognito/userPool:UserPool',
8251
9143
  'aws:dynamodb/table:Table',
8252
9144
  'aws:events/eventBus:EventBus',
8253
9145
  'aws:kms/key:Key',
8254
9146
  'aws:rds/cluster:Cluster',
8255
9147
  'aws:s3/bucket:Bucket',
9148
+ 'aws:secretsmanager/secret:Secret',
9149
+ 'aws:secretsmanager/secretVersion:SecretVersion',
9150
+ 'aws:sns/topic:Topic',
9151
+ 'aws:ssm/parameter:Parameter',
9152
+ 'aws:wafv2/ipSet:IpSet',
9153
+ 'aws:wafv2/webAcl:WebAcl',
9154
+ 'random:index/randomPassword:RandomPassword',
8256
9155
  ],
8257
9156
  },
8258
9157
  project: 'foundation',
@@ -8350,6 +9249,13 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8350
9249
  executor: '@empire-builder-kit/nx:foundation-profile-preflight',
8351
9250
  options: { stage: 'development' },
8352
9251
  },
9252
+ 'router-waf-rehearsal': {
9253
+ cache: false,
9254
+ executor: '@empire-builder-kit/nx:router-waf-rehearsal',
9255
+ outputs: [
9256
+ `{projectRoot}/security/router-waf-blocking-rehearsal.json`,
9257
+ ],
9258
+ },
8353
9259
  typecheck: {
8354
9260
  executor: 'nx:run-commands',
8355
9261
  options: {
@@ -8377,6 +9283,11 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8377
9283
  }),
8378
9284
  'packages/foundation/scripts/verify-foundation.mjs': `import { readFileSync } from 'node:fs';
8379
9285
 
9286
+ import {
9287
+ normalizeRouterWafPolicy,
9288
+ reconcileRouterWafBlockingRehearsal,
9289
+ } from '@empire-builder-kit/nx/foundation';
9290
+
8380
9291
  const descriptor = JSON.parse(
8381
9292
  readFileSync(new URL('../contracts/foundation.json', import.meta.url), 'utf8'),
8382
9293
  );
@@ -8392,6 +9303,33 @@ const frontendSecurityHeaders = JSON.parse(
8392
9303
  'utf8',
8393
9304
  ),
8394
9305
  );
9306
+ const routerWafPolicy = normalizeRouterWafPolicy(
9307
+ JSON.parse(
9308
+ readFileSync(
9309
+ new URL('../security/router-waf.json', import.meta.url),
9310
+ 'utf8',
9311
+ ),
9312
+ ),
9313
+ );
9314
+ reconcileRouterWafBlockingRehearsal({
9315
+ evidence: JSON.parse(
9316
+ readFileSync(
9317
+ new URL('../security/router-waf-live-evidence.json', import.meta.url),
9318
+ 'utf8',
9319
+ ),
9320
+ ),
9321
+ policy: routerWafPolicy,
9322
+ receipt:
9323
+ JSON.parse(
9324
+ readFileSync(
9325
+ new URL(
9326
+ '../security/router-waf-blocking-rehearsal.json',
9327
+ import.meta.url,
9328
+ ),
9329
+ 'utf8',
9330
+ ),
9331
+ ),
9332
+ });
8395
9333
  const expectedCapabilities = [
8396
9334
  'network',
8397
9335
  'routing',
@@ -8407,6 +9345,13 @@ const expectedCapabilities = [
8407
9345
  if (descriptor.contractVersion !== '1.0.0') {
8408
9346
  throw new Error('Foundation contractVersion must be 1.0.0.');
8409
9347
  }
9348
+ if (
9349
+ routerWafPolicy.stages.development.access !== 'allowlist' ||
9350
+ routerWafPolicy.stages.staging.access !== 'allowlist' ||
9351
+ routerWafPolicy.stages.production.mode !== 'block'
9352
+ ) {
9353
+ throw new Error('Foundation Router WAF stage defaults are invalid.');
9354
+ }
8410
9355
  if (JSON.stringify(descriptor.capabilities) !== JSON.stringify(expectedCapabilities)) {
8411
9356
  throw new Error('Foundation capability descriptor does not match the v1 standard.');
8412
9357
  }
@@ -8415,7 +9360,7 @@ if (
8415
9360
  costs.aurora?.engineVersion !== '${index_js_4.FOUNDATION_AURORA_ENGINE_VERSION}' ||
8416
9361
  costs.productionProfileSource !== 'ebk.repository-policy.json' ||
8417
9362
  costs.productionProfiles?.standard?.aurora?.min !== '2 ACU' ||
8418
- costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0 ACU' ||
9363
+ costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0.5 ACU' ||
8419
9364
  !costs.stages?.development ||
8420
9365
  !costs.stages?.staging ||
8421
9366
  costs.stages?.production?.dataRetention !== 'retained-and-protected'
@@ -8471,16 +9416,15 @@ export default $config({
8471
9416
  providers: {
8472
9417
  aws: {
8473
9418
  defaultTags: {
9419
+ // SST contributes sst:app and sst:stage. Keep the provider baseline
9420
+ // at eight tags so generated S3 asset objects remain within AWS's
9421
+ // hard ten-tag object limit. Richer performance tags stay explicit.
8474
9422
  tags: {
8475
9423
  'ebk:cost-allocation': 'foundation',
8476
9424
  'ebk:cost-scope': '${name}/foundation/' + stage,
8477
9425
  'ebk:framework-version': '${frameworkVersion}',
8478
9426
  'ebk:managed-by': 'empire-builder-kit',
8479
9427
  'ebk:owner': 'foundation',
8480
- 'ebk:production-profile':
8481
- process.env.EBK_PRODUCTION_FOUNDATION_PROFILE ?? 'standard',
8482
- 'ebk:repository':
8483
- process.env.GITHUB_REPOSITORY ?? '${name}',
8484
9428
  'ebk:slice': 'foundation',
8485
9429
  'ebk:stage': stage,
8486
9430
  'ebk:workspace': '${name}',
@@ -8524,6 +9468,16 @@ export default $config({
8524
9468
  - 'packages/*/services/*'
8525
9469
 
8526
9470
  autoInstallPeers: true
9471
+ blockExoticSubdeps: true
9472
+ enableGlobalVirtualStore: false
9473
+ ignorePnpmfile: true
9474
+ pnpmfile: []
9475
+ minimumReleaseAge: 1440
9476
+ minimumReleaseAgeExclude:
9477
+ - '@empire-builder-kit/*'
9478
+ minimumReleaseAgeIgnoreMissingTime: false
9479
+ minimumReleaseAgeStrict: true
9480
+ verifyDepsBeforeRun: error
8527
9481
  storeDir: .ebk/cache/pnpm-store
8528
9482
  supportedArchitectures:
8529
9483
  os:
@@ -9393,18 +10347,22 @@ if (eventName === 'pull_request' || eventName === 'push') {
9393
10347
  }
9394
10348
  if (eventName === 'pull_request') {
9395
10349
  const baseRef = process.env.GITHUB_BASE_REF;
10350
+ const headRepository = process.env.EBK_HEAD_REPOSITORY;
9396
10351
  const headRef = process.env.GITHUB_HEAD_REF;
9397
- if (!baseRef || !headRef) {
9398
- throw new Error('Pull-request branch metadata is missing.');
10352
+ const repository = process.env.GITHUB_REPOSITORY;
10353
+ if (!baseRef || !headRepository || !headRef || !repository) {
10354
+ throw new Error('Pull-request branch and repository metadata is missing.');
9399
10355
  }
9400
10356
  Object.assign(request, {
9401
10357
  baseIsAncestorOfHead: isAncestor('refs/remotes/origin/' + baseRef, 'HEAD'),
9402
10358
  baseRef,
10359
+ headRepository,
9403
10360
  headRef,
9404
10361
  mainIsAncestorOfDevelop: isAncestor(
9405
10362
  'refs/remotes/origin/main',
9406
10363
  'refs/remotes/origin/develop',
9407
10364
  ),
10365
+ repository,
9408
10366
  });
9409
10367
  } else if (eventName === 'push') {
9410
10368
  Object.assign(request, {