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

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 +67 -10
  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 +1039 -68
  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 +364 -29
  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 +41 -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),
@@ -826,11 +888,17 @@ function planRoleAuditJob(stage, configureAwsAction) {
826
888
  const production = stage === 'production';
827
889
  const branch = production ? 'main' : 'develop';
828
890
  const prefix = production ? 'AWS_PRODUCTION_PLAN' : 'AWS_STAGING_PLAN';
891
+ const profileAdmission = production
892
+ ? ''
893
+ : ` (github.event_name == 'workflow_dispatch' ||\n needs.deployment-mode.outputs.profile != 'starter') &&\n`;
829
894
  return ` infrastructure-plan-role-audit-${stage}:
830
895
  name: audit ${stage} infrastructure plan role
831
- needs: route
896
+ needs: [route, deployment-mode]
832
897
  if: |
833
- github.ref == 'refs/heads/${branch}' &&
898
+ always() &&
899
+ needs.route.result == 'success' &&
900
+ needs.deployment-mode.result == 'success' &&
901
+ ${profileAdmission} github.ref == 'refs/heads/${branch}' &&
834
902
  (github.event_name == 'push' && 'deploy' || inputs.action) != 'unlock'
835
903
  runs-on: ubuntu-latest
836
904
  environment: infrastructure-plan-${stage}
@@ -953,6 +1021,9 @@ const WORKSPACE_SEEDED_PATHS = new Set([
953
1021
  exports.WORKSPACE_PACKAGE_MANAGED_JSON_POINTERS = [
954
1022
  '/devDependencies/eslint-plugin-jsx-a11y',
955
1023
  '/devDependencies/vitest',
1024
+ '/engines/node',
1025
+ '/engines/pnpm',
1026
+ '/packageManager',
956
1027
  ];
957
1028
  exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
958
1029
  '/name',
@@ -961,9 +1032,11 @@ exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
961
1032
  '/root',
962
1033
  '/tags',
963
1034
  '/targets/blueprint-trust-check',
1035
+ '/targets/blueprint-trust-check-main',
964
1036
  '/targets/blueprint-trust-check-protected',
965
1037
  '/targets/developer-context',
966
1038
  '/targets/deployment-plan',
1039
+ '/targets/deploy-local',
967
1040
  '/targets/dev',
968
1041
  '/targets/dev-doctor',
969
1042
  '/targets/dev-setup',
@@ -973,6 +1046,7 @@ exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
973
1046
  '/targets/supply-chain-check-protected',
974
1047
  '/targets/supply-chain-scan',
975
1048
  '/targets/verify',
1049
+ '/targets/verify-proposed',
976
1050
  '/targets/work-markers',
977
1051
  '/targets/workload-role-candidate-report',
978
1052
  '/targets/workload-role-provisioner-build',
@@ -983,6 +1057,10 @@ const FOUNDATION_SEEDED_PATHS = new Set([
983
1057
  'docs/costs.md',
984
1058
  'docs/runbooks.md',
985
1059
  'docs/security.md',
1060
+ 'performance/account-cost-policy.json',
1061
+ 'security/router-waf-blocking-rehearsal.json',
1062
+ 'security/router-waf-live-evidence.json',
1063
+ 'security/router-waf.json',
986
1064
  ]);
987
1065
  function createFoundationOwnership(files, frameworkVersion) {
988
1066
  const paths = Object.keys(files)
@@ -1045,7 +1123,16 @@ function createWorkspaceOwnership(files, frameworkVersion) {
1045
1123
  }
1046
1124
  function renderWorkspaceFiles(options) {
1047
1125
  const { frameworkVersion, name } = options;
1126
+ const deploymentProfile = options.deploymentProfile ?? 'starter';
1127
+ const initialRepositoryPolicy = (0, index_js_4.createIncubationRepositoryPolicy)(deploymentProfile);
1128
+ initialRepositoryPolicy.deploymentAccounts = options.deploymentAccounts ?? {
1129
+ development: null,
1130
+ production: null,
1131
+ staging: null,
1132
+ };
1133
+ initialRepositoryPolicy.deploymentRegion = options.deploymentRegion ?? null;
1048
1134
  const developerAgentFilePlan = (0, workspace_files_developer_agent_js_1.renderDeveloperAgentWorkspaceFilePlan)({
1135
+ deploymentProfile,
1049
1136
  frameworkVersion,
1050
1137
  name,
1051
1138
  });
@@ -1056,7 +1143,13 @@ function renderWorkspaceFiles(options) {
1056
1143
  const pnpmAction = (0, index_js_3.pinnedAction)(index_js_3.GITHUB_ACTION_PINS.pnpmSetup);
1057
1144
  const setupNodeAction = (0, index_js_3.pinnedAction)(index_js_3.GITHUB_ACTION_PINS.setupNode);
1058
1145
  const uploadArtifactAction = (0, index_js_3.pinnedAction)(index_js_3.GITHUB_ACTION_PINS.uploadArtifact);
1146
+ 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'";
1147
+ 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'";
1148
+ const protectedPush = "github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')";
1149
+ const protectedDeploymentRouteJobName = 'protected deployment route';
1150
+ const stagingApplyJobPrefix = 'apply staging plan ';
1059
1151
  const files = {
1152
+ ...(0, workspace_files_ci_acceptance_js_1.renderWorkspaceCiAcceptanceFiles)(),
1060
1153
  '.node-version': `${tool_versions_js_1.WORKSPACE_NODE_VERSION}\n`,
1061
1154
  '.github/CODEOWNERS': `# Replace this placeholder with the GitHub user or team that owns protected source.
1062
1155
  # GitHub bootstrap remains blocked until a non-placeholder catch-all owner is committed.
@@ -1072,15 +1165,77 @@ on:
1072
1165
  workflow_dispatch:
1073
1166
 
1074
1167
  concurrency:
1075
- group: verify-\${{ github.ref }}
1076
- cancel-in-progress: true
1168
+ group: verify-\${{ github.event_name == 'push' && github.sha || github.ref }}
1169
+ cancel-in-progress: \${{ github.event_name != 'push' }}
1077
1170
 
1078
1171
  permissions:
1172
+ actions: read
1079
1173
  contents: read
1174
+ pull-requests: read
1175
+
1176
+ env:
1177
+ EBK_CI_CANDIDATE_SHA: \${{ github.event.pull_request.head.sha || github.sha }}
1080
1178
 
1081
1179
  jobs:
1180
+ acceptance:
1181
+ name: accepted merge evidence
1182
+ if: ${protectedPush}
1183
+ runs-on: ubuntu-latest
1184
+ outputs:
1185
+ history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1186
+ reused: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1187
+ route: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' && steps.resolve.outputs.route || '' }}
1188
+ steps:
1189
+ - name: Check out immutable merged candidate
1190
+ if: ${protectedPush}
1191
+ uses: ${checkoutAction}
1192
+ with:
1193
+ fetch-depth: 0
1194
+ ref: \${{ github.sha }}
1195
+ - name: Resolve exact protected-route acceptance
1196
+ if: ${protectedPush}
1197
+ id: resolve
1198
+ env:
1199
+ GH_TOKEN: \${{ github.token }}
1200
+ run: node tools/ci/workspace-acceptance.mjs resolve
1201
+ - name: Restore exact protected-route acceptance
1202
+ if: steps.resolve.outputs.reused == 'true'
1203
+ id: restore
1204
+ continue-on-error: true
1205
+ env:
1206
+ EBK_ACCEPTANCE_ARTIFACT_DIGEST: \${{ steps.resolve.outputs.artifact_digest }}
1207
+ EBK_ACCEPTANCE_ARTIFACT_ID: \${{ steps.resolve.outputs.artifact_id }}
1208
+ EBK_ACCEPTANCE_ARTIFACT_SIZE: \${{ steps.resolve.outputs.artifact_size }}
1209
+ GH_TOKEN: \${{ github.token }}
1210
+ run: >-
1211
+ node tools/ci/workspace-acceptance.mjs download
1212
+ .ebk/evidence/reused-acceptance
1213
+ "$EBK_ACCEPTANCE_ARTIFACT_ID"
1214
+ "$EBK_ACCEPTANCE_ARTIFACT_DIGEST"
1215
+ "$EBK_ACCEPTANCE_ARTIFACT_SIZE"
1216
+ - name: Verify exact merged-tree acceptance
1217
+ if: steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success'
1218
+ id: verify
1219
+ continue-on-error: true
1220
+ env:
1221
+ EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
1222
+ EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
1223
+ EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
1224
+ EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
1225
+ EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
1226
+ run: >-
1227
+ node tools/ci/workspace-acceptance.mjs verify
1228
+ .ebk/evidence/reused-acceptance/manifest.json
1229
+ "$EBK_ACCEPTANCE_HEAD"
1230
+ "$EBK_ACCEPTANCE_BASE"
1231
+ "$EBK_ACCEPTANCE_ROUTE"
1232
+ "$EBK_ACCEPTANCE_RUN_ID"
1233
+ "$EBK_ACCEPTANCE_RUN_ATTEMPT"
1234
+
1082
1235
  policy:
1083
1236
  name: policy
1237
+ needs: acceptance
1238
+ if: always() && needs.acceptance.outputs.reused != 'true'
1084
1239
  runs-on: ubuntu-latest
1085
1240
  steps:
1086
1241
  - uses: ${checkoutAction}
@@ -1094,22 +1249,100 @@ jobs:
1094
1249
  cache: pnpm
1095
1250
  - run: pnpm install --frozen-lockfile
1096
1251
  - run: node tools/ebk-git-policy.mjs
1252
+ env:
1253
+ EBK_HEAD_REPOSITORY: \${{ github.event.pull_request.head.repo.full_name }}
1097
1254
 
1098
1255
  verify:
1099
1256
  name: verify
1257
+ needs: acceptance
1258
+ if: always() && needs.acceptance.outputs.reused != 'true'
1259
+ runs-on: ubuntu-latest
1260
+ steps:
1261
+ - uses: ${checkoutAction}
1262
+ with:
1263
+ fetch-depth: 0
1264
+ ref: \${{ github.event.pull_request.head.sha || github.sha }}
1265
+ - uses: ${pnpmAction}
1266
+ - uses: ${setupNodeAction}
1267
+ with:
1268
+ node-version: ${tool_versions_js_1.WORKSPACE_NODE_VERSION}
1269
+ cache: pnpm
1270
+ - run: pnpm install --frozen-lockfile
1271
+ - run: pnpm format:check
1272
+ - name: Verify proposed workspace
1273
+ if: github.event_name == 'pull_request' && !(${protectedDevelopPromotion} || ${protectedMainSync})
1274
+ run: |
1275
+ pnpm nx run ${name}:verify-proposed
1276
+ pnpm nx run-many -t verify --exclude=${name} --parallel=3
1277
+ - name: Verify protected develop promotion
1278
+ if: github.event_name == 'pull_request' && ${protectedDevelopPromotion}
1279
+ run: pnpm nx run-many -t verify --parallel=3
1280
+ - name: Verify protected main sync
1281
+ if: github.event_name == 'pull_request' && ${protectedMainSync}
1282
+ run: |
1283
+ pnpm nx run ${name}:blueprint-trust-check-main
1284
+ pnpm nx run ${name}:verify-proposed
1285
+ pnpm nx run-many -t verify --exclude=${name} --parallel=3
1286
+ - name: Verify adopted workspace
1287
+ if: github.event_name != 'pull_request'
1288
+ run: pnpm nx run-many -t verify --parallel=3
1289
+
1290
+ seal-acceptance:
1291
+ name: seal protected route acceptance
1292
+ needs: [policy, verify]
1293
+ if: >-
1294
+ always() &&
1295
+ needs.policy.result == 'success' &&
1296
+ needs.verify.result == 'success' &&
1297
+ github.event_name == 'pull_request' &&
1298
+ (${protectedDevelopPromotion} || ${protectedMainSync})
1299
+ runs-on: ubuntu-latest
1300
+ steps:
1301
+ - name: Check out immutable accepted candidate
1302
+ uses: ${checkoutAction}
1303
+ with:
1304
+ fetch-depth: 0
1305
+ ref: \${{ env.EBK_CI_CANDIDATE_SHA }}
1306
+ - name: Seal exact protected-route acceptance
1307
+ id: seal
1308
+ env:
1309
+ EBK_BASE_COMMIT: \${{ github.event.pull_request.base.sha }}
1310
+ EBK_HEAD_REPOSITORY: \${{ github.event.pull_request.head.repo.full_name }}
1311
+ run: >-
1312
+ node tools/ci/workspace-acceptance.mjs seal
1313
+ .ebk/evidence/ci/workspace-acceptance/manifest.json
1314
+ - name: Retain exact protected-route acceptance
1315
+ if: steps.seal.outputs.sealed == 'true'
1316
+ uses: ${uploadArtifactAction}
1317
+ with:
1318
+ name: workspace-acceptance-\${{ env.EBK_CI_CANDIDATE_SHA }}-\${{ github.run_id }}-\${{ github.run_attempt }}
1319
+ path: .ebk/evidence/ci/workspace-acceptance
1320
+ if-no-files-found: error
1321
+ include-hidden-files: true
1322
+ retention-days: 30
1323
+
1324
+ release-candidate:
1325
+ name: build immutable release candidates
1326
+ needs: [acceptance, policy, verify]
1327
+ if: >-
1328
+ always() &&
1329
+ github.event_name == 'push' &&
1330
+ needs.acceptance.outputs.history_only != 'true' &&
1331
+ (needs.acceptance.outputs.reused == 'true' ||
1332
+ (needs.policy.result == 'success' && needs.verify.result == 'success'))
1100
1333
  runs-on: ubuntu-latest
1101
1334
  steps:
1102
1335
  - uses: ${checkoutAction}
1336
+ with:
1337
+ fetch-depth: 0
1338
+ ref: \${{ github.sha }}
1103
1339
  - uses: ${pnpmAction}
1104
1340
  - uses: ${setupNodeAction}
1105
1341
  with:
1106
1342
  node-version: ${tool_versions_js_1.WORKSPACE_NODE_VERSION}
1107
1343
  cache: pnpm
1108
1344
  - run: pnpm install --frozen-lockfile
1109
- - run: pnpm nx format:check
1110
- - run: pnpm nx run-many -t verify --parallel=3
1111
1345
  - name: Build immutable release candidates
1112
- if: github.event_name == 'push'
1113
1346
  env:
1114
1347
  EBK_SOURCE_COMMIT: \${{ github.sha }}
1115
1348
  run: |
@@ -1127,7 +1360,6 @@ jobs:
1127
1360
  ".ebk/evidence/release-candidate/packages/$project/"
1128
1361
  done
1129
1362
  - name: Retain exact immutable release candidate
1130
- if: github.event_name == 'push'
1131
1363
  uses: ${uploadArtifactAction}
1132
1364
  with:
1133
1365
  name: release-candidate-\${{ github.sha }}
@@ -1162,7 +1394,97 @@ permissions:
1162
1394
  contents: read
1163
1395
 
1164
1396
  jobs:
1397
+ history-classification:
1398
+ name: classify protected history-only sync
1399
+ if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
1400
+ runs-on: ubuntu-latest
1401
+ permissions:
1402
+ actions: read
1403
+ contents: read
1404
+ pull-requests: read
1405
+ outputs:
1406
+ history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1407
+ steps:
1408
+ - name: Check out immutable protected candidate
1409
+ uses: ${checkoutAction}
1410
+ with:
1411
+ fetch-depth: 0
1412
+ ref: \${{ github.sha }}
1413
+ - uses: ${setupNodeAction}
1414
+ with:
1415
+ node-version: ${tool_versions_js_1.WORKSPACE_NODE_VERSION}
1416
+ - name: Resolve exact protected-route acceptance
1417
+ id: resolve
1418
+ env:
1419
+ GH_TOKEN: \${{ github.token }}
1420
+ run: node tools/ci/workspace-acceptance.mjs resolve
1421
+ - name: Restore exact protected-route acceptance
1422
+ if: steps.resolve.outputs.reused == 'true'
1423
+ id: restore
1424
+ continue-on-error: true
1425
+ env:
1426
+ EBK_ACCEPTANCE_ARTIFACT_DIGEST: \${{ steps.resolve.outputs.artifact_digest }}
1427
+ EBK_ACCEPTANCE_ARTIFACT_ID: \${{ steps.resolve.outputs.artifact_id }}
1428
+ EBK_ACCEPTANCE_ARTIFACT_SIZE: \${{ steps.resolve.outputs.artifact_size }}
1429
+ GH_TOKEN: \${{ github.token }}
1430
+ run: >-
1431
+ node tools/ci/workspace-acceptance.mjs download
1432
+ .ebk/evidence/reused-history-acceptance
1433
+ "$EBK_ACCEPTANCE_ARTIFACT_ID"
1434
+ "$EBK_ACCEPTANCE_ARTIFACT_DIGEST"
1435
+ "$EBK_ACCEPTANCE_ARTIFACT_SIZE"
1436
+ - name: Verify exact history-only acceptance
1437
+ if: steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success'
1438
+ id: verify
1439
+ continue-on-error: true
1440
+ env:
1441
+ EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
1442
+ EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
1443
+ EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
1444
+ EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
1445
+ EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
1446
+ run: >-
1447
+ node tools/ci/workspace-acceptance.mjs verify
1448
+ .ebk/evidence/reused-history-acceptance/manifest.json
1449
+ "$EBK_ACCEPTANCE_HEAD"
1450
+ "$EBK_ACCEPTANCE_BASE"
1451
+ "$EBK_ACCEPTANCE_ROUTE"
1452
+ "$EBK_ACCEPTANCE_RUN_ID"
1453
+ "$EBK_ACCEPTANCE_RUN_ATTEMPT"
1454
+
1455
+ deployment-mode:
1456
+ name: classify deployment profile
1457
+ runs-on: ubuntu-latest
1458
+ outputs:
1459
+ profile: \${{ steps.profile.outputs.profile }}
1460
+ steps:
1461
+ - name: Check out immutable deployment policy
1462
+ uses: ${checkoutAction}
1463
+ with:
1464
+ ref: \${{ github.sha }}
1465
+ - id: profile
1466
+ name: Read exact committed deployment profile
1467
+ run: |
1468
+ node - <<'NODE'
1469
+ const { readFileSync } = require('node:fs');
1470
+ const policy = JSON.parse(readFileSync('ebk.repository-policy.json', 'utf8'));
1471
+ const profiles = new Set(['starter', 'production-isolated', 'fully-isolated']);
1472
+ if (!profiles.has(policy.deploymentProfile)) {
1473
+ throw new Error('Committed deployment profile is absent or invalid.');
1474
+ }
1475
+ require('node:fs').appendFileSync(
1476
+ process.env.GITHUB_OUTPUT,
1477
+ 'profile=' + policy.deploymentProfile + '\\n',
1478
+ );
1479
+ NODE
1480
+
1165
1481
  route:
1482
+ name: ${protectedDeploymentRouteJobName}
1483
+ needs: [history-classification, deployment-mode]
1484
+ if: >-
1485
+ always() &&
1486
+ needs.history-classification.outputs.history_only != 'true' &&
1487
+ needs.deployment-mode.result == 'success'
1166
1488
  runs-on: ubuntu-latest
1167
1489
  steps:
1168
1490
  - name: Validate protected ref
@@ -1175,14 +1497,25 @@ jobs:
1175
1497
  esac
1176
1498
 
1177
1499
  plan-staging:
1178
- needs: route
1179
- if: github.ref == 'refs/heads/develop'
1500
+ needs: [route, deployment-mode]
1501
+ if: >-
1502
+ always() &&
1503
+ needs.route.result == 'success' &&
1504
+ needs.deployment-mode.result == 'success' &&
1505
+ github.ref == 'refs/heads/develop' &&
1506
+ (github.event_name == 'workflow_dispatch' ||
1507
+ needs.deployment-mode.outputs.profile != 'starter')
1180
1508
  runs-on: ubuntu-latest
1181
1509
  outputs:
1510
+ deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
1511
+ deployment_region: \${{ steps.plan.outputs.deployment_region }}
1512
+ development_account_id: \${{ steps.plan.outputs.development_account_id }}
1182
1513
  foundation_project: \${{ steps.plan.outputs.foundation_project }}
1183
1514
  has_slices: \${{ steps.plan.outputs.has_slices }}
1184
1515
  ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
1516
+ production_account_id: \${{ steps.plan.outputs.production_account_id }}
1185
1517
  slice_projects: \${{ steps.plan.outputs.slice_projects }}
1518
+ staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
1186
1519
  steps:
1187
1520
  - uses: ${checkoutAction}
1188
1521
  with:
@@ -1393,16 +1726,25 @@ jobs:
1393
1726
  path: .ebk/evidence/staging-release-source/**
1394
1727
 
1395
1728
  plan-production:
1396
- needs: route
1397
- if: github.ref == 'refs/heads/main'
1729
+ needs: [route, deployment-mode]
1730
+ if: >-
1731
+ always() &&
1732
+ needs.route.result == 'success' &&
1733
+ needs.deployment-mode.result == 'success' &&
1734
+ github.ref == 'refs/heads/main'
1398
1735
  runs-on: ubuntu-latest
1399
1736
  outputs:
1737
+ deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
1738
+ deployment_region: \${{ steps.plan.outputs.deployment_region }}
1739
+ development_account_id: \${{ steps.plan.outputs.development_account_id }}
1400
1740
  foundation_project: \${{ steps.plan.outputs.foundation_project }}
1401
1741
  foundation_profile: \${{ steps.plan.outputs.foundation_profile }}
1402
1742
  has_slices: \${{ steps.plan.outputs.has_slices }}
1403
1743
  ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
1404
1744
  production_approval_mode: \${{ steps.plan.outputs.production_approval_mode }}
1745
+ production_account_id: \${{ steps.plan.outputs.production_account_id }}
1405
1746
  slice_projects: \${{ steps.plan.outputs.slice_projects }}
1747
+ staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
1406
1748
  steps:
1407
1749
  - uses: ${checkoutAction}
1408
1750
  with:
@@ -1723,6 +2065,8 @@ jobs:
1723
2065
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1724
2066
  EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1725
2067
  EBK_STAGE: staging
2068
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2069
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
1726
2070
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
1727
2071
  steps:
1728
2072
  - uses: ${checkoutAction}
@@ -1738,6 +2082,10 @@ jobs:
1738
2082
  - run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1739
2083
  - name: Verify managed workspace integrity before AWS authority
1740
2084
  run: pnpm nx run ${name}:workspace-health
2085
+ - name: Bind staging account to committed deployment topology
2086
+ run: |
2087
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
2088
+ test "$AWS_REGION" = "$EBK_POLICY_REGION"
1741
2089
  ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${configureAwsAction}
1742
2090
  with:
1743
2091
  role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
@@ -1777,6 +2125,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
1777
2125
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1778
2126
  EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1779
2127
  EBK_STAGE: staging
2128
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2129
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
1780
2130
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
1781
2131
  steps:
1782
2132
  - uses: ${checkoutAction}
@@ -1792,6 +2142,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
1792
2142
  - run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1793
2143
  - name: Verify managed workspace integrity before AWS authority
1794
2144
  run: pnpm nx run ${name}:workspace-health
2145
+ - name: Bind staging account to committed deployment topology
2146
+ run: |
2147
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
2148
+ test "$AWS_REGION" = "$EBK_POLICY_REGION"
1795
2149
  ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Restore both native platform records
1796
2150
  uses: ${downloadArtifactAction}
1797
2151
  with:
@@ -1833,6 +2187,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
1833
2187
  AWS_DEPLOY_ROLE_ARN: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
1834
2188
  AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
1835
2189
  AWS_REGION: \${{ vars.AWS_REGION }}
2190
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
2191
+ EBK_POLICY_PRODUCTION_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
2192
+ EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
2193
+ EBK_POLICY_STAGING_ACCOUNT_ID: \${{ needs.plan-production.outputs.staging_account_id }}
1836
2194
  EBK_CANDIDATE_RUN_ATTEMPT: \${{ needs.release-source-production.outputs.candidate_run_attempt }}
1837
2195
  EBK_CANDIDATE_RUN_ID: \${{ needs.release-source-production.outputs.candidate_run_id }}
1838
2196
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-production.outputs.source_commit }}
@@ -1933,11 +2291,13 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
1933
2291
  cp "$source_sidecar" "$target"
1934
2292
  done
1935
2293
  - uses: ${configureAwsAction}
2294
+ if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
1936
2295
  with:
1937
2296
  role-to-assume: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
1938
2297
  role-session-name: ebk-staging-container-pull-\${{ github.run_id }}-\${{ github.run_attempt }}
1939
2298
  aws-region: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
1940
2299
  - name: Authenticate exact staging source registry
2300
+ if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
1941
2301
  env:
1942
2302
  EBK_STAGING_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
1943
2303
  EBK_STAGING_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
@@ -1956,10 +2316,27 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
1956
2316
  role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
1957
2317
  role-session-name: ebk-production-container-push-\${{ github.run_id }}-\${{ github.run_attempt }}
1958
2318
  aws-region: \${{ vars.AWS_REGION }}
2319
+ - name: Bind production authority to committed deployment topology
2320
+ run: |
2321
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
2322
+ { echo 'Production environment account differs from committed deployment topology'; exit 1; }
2323
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
2324
+ { echo 'Production environment Region differs from committed deployment topology'; exit 1; }
2325
+ - name: Authenticate shared-account staging source registry
2326
+ if: env.EBK_DEPLOYMENT_PROFILE == 'starter'
2327
+ run: |
2328
+ test "$EBK_POLICY_STAGING_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
2329
+ { echo 'Starter promotion requires one shared AWS account'; exit 1; }
2330
+ registry_suffix=.com
2331
+ case "$EBK_POLICY_REGION" in cn-*) registry_suffix=.com.cn ;; esac
2332
+ aws ecr get-login-password --region "$EBK_POLICY_REGION" |
2333
+ docker login --username AWS --password-stdin \
2334
+ "$EBK_POLICY_STAGING_ACCOUNT_ID.dkr.ecr.$EBK_POLICY_REGION.amazonaws$registry_suffix"
1959
2335
  - name: Promote exact staging indexes without rebuilding
1960
2336
  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 }}
2337
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
2338
+ 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 }}
2339
+ EBK_STAGING_PROMOTION_REGION: \${{ needs.plan-production.outputs.deployment_profile == 'starter' && needs.plan-production.outputs.deployment_region || vars.AWS_STAGING_PROMOTION_REGION }}
1963
2340
  run: |
1964
2341
  test "$(aws sts get-caller-identity --query Account --output text)" = "$AWS_EXPECTED_ACCOUNT_ID" ||
1965
2342
  { echo 'Production promotion role account differs from policy'; exit 1; }
@@ -2178,6 +2555,8 @@ ${planRoleAuditStep('main', 'verify')}
2178
2555
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
2179
2556
  EBK_SOURCE_COMMIT: \${{ github.sha }}
2180
2557
  EBK_STAGE: staging
2558
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2559
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
2181
2560
  FOUNDATION_PROJECT: \${{ needs.plan-staging.outputs.foundation_project }}
2182
2561
  ORDERED_PROJECTS: \${{ needs.plan-staging.outputs.ordered_projects }}
2183
2562
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
@@ -2285,7 +2664,7 @@ ${planRoleAuditStep('develop', 'verify')}
2285
2664
  packages/*/.ebk/evidence/infrastructure-plan-command.json
2286
2665
 
2287
2666
  deploy-staging:
2288
- name: apply staging plan \${{ needs.infrastructure-plan-staging.outputs.plan_digest || 'recovery-without-resource-plan' }}
2667
+ name: ${stagingApplyJobPrefix}\${{ needs.infrastructure-plan-staging.outputs.plan_digest || 'recovery-without-resource-plan' }}
2289
2668
  needs: [plan-staging, release-source-staging, container-assemble-staging, infrastructure-plan-staging]
2290
2669
  if: |
2291
2670
  always() &&
@@ -2364,6 +2743,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
2364
2743
  test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'staging AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
2365
2744
  test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'staging AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
2366
2745
  test -n "$AWS_REGION" || { echo 'staging AWS_REGION is required'; exit 1; }
2746
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
2747
+ { echo 'staging environment account differs from committed deployment topology'; exit 1; }
2748
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
2749
+ { echo 'staging environment Region differs from committed deployment topology'; exit 1; }
2367
2750
  test -n "$EBK_FLEET_ORIGIN" ||
2368
2751
  { echo 'staging environment EBK_FLEET_ORIGIN is required'; exit 1; }
2369
2752
  test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
@@ -2375,8 +2758,7 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
2375
2758
  test -n "$AWS_PLAN_GENERATED_SECRET_ARNS_JSON" &&
2376
2759
  test -n "$AWS_PLAN_PROVIDER_REFRESH_POLICY_JSON" ||
2377
2760
  { 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" ||
2761
+ test -n "$AWS_PLAN_ROLE_ARN" ||
2380
2762
  { echo 'staging exact plan-authority inputs are required'; exit 1; }
2381
2763
  case "$AWS_DEPLOY_ROLE_ARN" in
2382
2764
  arn:aws:iam::$AWS_EXPECTED_ACCOUNT_ID:role/*) ;;
@@ -3000,6 +3382,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
3000
3382
  EBK_APPROVED_MAIN_COMMIT: \${{ github.sha }}
3001
3383
  EBK_SOURCE_COMMIT: \${{ github.sha }}
3002
3384
  EBK_STAGE: production
3385
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
3386
+ EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
3003
3387
  EBK_VERIFIED_STAGING_RUN_ID: \${{ needs.release-source-production.outputs.verified_staging_run_id }}
3004
3388
  FOUNDATION_PROJECT: \${{ needs.plan-production.outputs.foundation_project }}
3005
3389
  ORDERED_PROJECTS: \${{ needs.plan-production.outputs.ordered_projects }}
@@ -3096,6 +3480,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
3096
3480
  test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'production AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
3097
3481
  test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'production AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
3098
3482
  test -n "$AWS_REGION" || { echo 'production AWS_REGION is required'; exit 1; }
3483
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
3484
+ { echo 'production environment account differs from committed deployment topology'; exit 1; }
3485
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
3486
+ { echo 'production environment Region differs from committed deployment topology'; exit 1; }
3099
3487
  test -n "$EBK_FLEET_ORIGIN" ||
3100
3488
  { echo 'production environment EBK_FLEET_ORIGIN is required'; exit 1; }
3101
3489
  test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
@@ -3107,8 +3495,7 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
3107
3495
  test -n "$AWS_PLAN_GENERATED_SECRET_ARNS_JSON" &&
3108
3496
  test -n "$AWS_PLAN_PROVIDER_REFRESH_POLICY_JSON" ||
3109
3497
  { 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" ||
3498
+ test -n "$AWS_PLAN_ROLE_ARN" ||
3112
3499
  { echo 'production exact plan-authority inputs are required'; exit 1; }
3113
3500
  case "$AWS_DEPLOY_ROLE_ARN" in
3114
3501
  arn:aws:iam::$AWS_EXPECTED_ACCOUNT_ID:role/*) ;;
@@ -4762,13 +5149,51 @@ permissions:
4762
5149
  contents: read
4763
5150
 
4764
5151
  jobs:
5152
+ deployment-classification:
5153
+ name: classify exact staging deployment
5154
+ if: >-
5155
+ github.event_name == 'workflow_run' &&
5156
+ github.event.workflow_run.event == 'push' &&
5157
+ github.event.workflow_run.head_branch == 'develop' &&
5158
+ github.event.workflow_run.conclusion == 'success'
5159
+ permissions:
5160
+ actions: read
5161
+ contents: read
5162
+ outputs:
5163
+ deployed: \${{ steps.classify.outputs.deployed }}
5164
+ runs-on: ubuntu-latest
5165
+ steps:
5166
+ - id: classify
5167
+ name: Require one successful staging deployment job
5168
+ env:
5169
+ EBK_DEPLOYMENT_RUN_ID: \${{ github.event.workflow_run.id }}
5170
+ GH_TOKEN: \${{ github.token }}
5171
+ GH_REPO: \${{ github.repository }}
5172
+ run: |
5173
+ jobs="$(gh api \
5174
+ "/repos/$GH_REPO/actions/runs/$EBK_DEPLOYMENT_RUN_ID/jobs?filter=latest&per_page=100")"
5175
+ apply_count="$(printf '%s' "$jobs" | jq \
5176
+ '[.jobs[] | select(.name | startswith("${stagingApplyJobPrefix}")) | select(.conclusion == "success")] | length')"
5177
+ route_success_count="$(printf '%s' "$jobs" | jq \
5178
+ '[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "success")] | length')"
5179
+ route_skipped_count="$(printf '%s' "$jobs" | jq \
5180
+ '[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "skipped")] | length')"
5181
+ case "$apply_count:$route_success_count:$route_skipped_count" in
5182
+ 1:1:0) echo 'deployed=true' >> "$GITHUB_OUTPUT" ;;
5183
+ 0:0:1) echo 'deployed=false' >> "$GITHUB_OUTPUT" ;;
5184
+ *) echo 'Deploy route and staging application evidence disagree'; exit 1 ;;
5185
+ esac
5186
+
4765
5187
  plan:
5188
+ needs: deployment-classification
4766
5189
  if: >-
4767
- (github.event_name == 'workflow_run' &&
5190
+ always() &&
5191
+ ((github.event_name == 'workflow_run' &&
4768
5192
  github.event.workflow_run.conclusion == 'success' &&
4769
5193
  github.event.workflow_run.event == 'push' &&
4770
- github.event.workflow_run.head_branch == 'develop') ||
4771
- github.event_name == 'workflow_dispatch'
5194
+ github.event.workflow_run.head_branch == 'develop' &&
5195
+ needs.deployment-classification.outputs.deployed == 'true') ||
5196
+ github.event_name == 'workflow_dispatch')
4772
5197
  outputs:
4773
5198
  deployment_run_attempt: \${{ steps.identity.outputs.deployment_run_attempt }}
4774
5199
  deployment_run_id: \${{ steps.identity.outputs.deployment_run_id }}
@@ -4875,9 +5300,10 @@ jobs:
4875
5300
  EBK_SMOKE_AUTHORITY_VERSION: \${{ vars.EBK_SMOKE_AUTHORITY_VERSION }}
4876
5301
  EBK_SMOKE_TARGETS: \${{ vars.EBK_SMOKE_TARGETS }}
4877
5302
  EBK_SOURCE_COMMIT: \${{ needs.plan.outputs.source_commit }}
4878
- PLAYWRIGHT_BROWSERS_PATH: \${{ runner.temp }}/ebk-playwright-browsers
4879
5303
  runs-on: ubuntu-latest
4880
5304
  steps:
5305
+ - name: Initialize private browser cache path
5306
+ run: echo "PLAYWRIGHT_BROWSERS_PATH=$RUNNER_TEMP/ebk-playwright-browsers" >> "$GITHUB_ENV"
4881
5307
  - name: Validate credential-free preparation
4882
5308
  run: |
4883
5309
  test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
@@ -5261,14 +5687,14 @@ jobs:
5261
5687
  test "$(node --version | cut -d. -f1)" = v22
5262
5688
  - name: Run exact deployed smoke
5263
5689
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5264
- run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
5690
+ run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
5265
5691
  - name: Verify reviewed frontend visual baseline
5266
5692
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5267
- run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:frontend-visual"
5693
+ run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:frontend-visual"
5268
5694
  - name: Verify deployed frontend security headers
5269
5695
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5270
5696
  run: |
5271
- node node_modules/nx/bin/nx.js run \
5697
+ node node_modules/nx/dist/bin/nx.js run \
5272
5698
  "$EBK_PROJECT:frontend-security-headers" -- \
5273
5699
  --url="$EBK_SMOKE_URL"
5274
5700
  - name: Bind exact alarm and data-integrity evidence
@@ -6715,11 +7141,11 @@ jobs:
6715
7141
  if [ "$EBK_ACTION" = apply ]; then args+=("--apply"); fi
6716
7142
  pnpm nx run "$EBK_PROJECT:secret-metadata" -- "\${args[@]}"
6717
7143
  `,
6718
- '.github/workflows/sync-back.yml': `name: Sync production history
7144
+ '.github/workflows/sync-back.yml': `name: Sync protected main history
6719
7145
 
6720
7146
  on:
6721
7147
  workflow_run:
6722
- workflows: [Deploy]
7148
+ workflows: [Verify]
6723
7149
  types: [completed]
6724
7150
 
6725
7151
  permissions:
@@ -6730,6 +7156,7 @@ jobs:
6730
7156
  sync-back:
6731
7157
  if: >-
6732
7158
  github.event.workflow_run.conclusion == 'success' &&
7159
+ github.event.workflow_run.event == 'push' &&
6733
7160
  github.event.workflow_run.head_branch == 'main'
6734
7161
  runs-on: ubuntu-latest
6735
7162
  steps:
@@ -6743,8 +7170,8 @@ jobs:
6743
7170
  gh pr create \
6744
7171
  --head main \
6745
7172
  --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.'
7173
+ --title 'chore(git): sync protected main to develop' \
7174
+ --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
7175
  number=$(gh pr list --state open --head main --base develop --json number --jq '.[0].number')
6749
7176
  fi
6750
7177
  echo "Sync-back pull request #$number is ready for review."
@@ -6989,6 +7416,8 @@ dist
6989
7416
  .open-next
6990
7417
  .sst
6991
7418
  **/next-env.d.ts
7419
+ **/api/generated/*.json
7420
+ **/database/migrations/**
6992
7421
  pnpm-lock.yaml
6993
7422
  tools/blueprint-trust-check.mjs
6994
7423
  `,
@@ -7096,6 +7525,16 @@ use the direct installed \`.bin/ebk blueprint verification-review\` entry docume
7096
7525
  \`docs/blueprints.md\`.
7097
7526
 
7098
7527
  Workspace creation only writes source files. It does not create or change cloud resources.
7528
+
7529
+ New workspaces default to the one-account \`starter\` deployment profile. Configure the exact
7530
+ shared account and Region in \`ebk.repository-policy.json\`, authenticate through IAM Identity
7531
+ Center, add exact development and staging CIDRs to
7532
+ \`packages/foundation/security/router-waf.json\`, and use
7533
+ \`pnpm nx run ${name}:deploy-local -- --stage=development
7534
+ --profile=<profile> --expectedRoleArn=<role-arn>\`. Never invoke SST directly.
7535
+ For a brand-new stage with generated Product Slices, complete the documented workload-role
7536
+ Foundation publication and pass \`--phase=foundation-bootstrap\` once before publishing and
7537
+ deploying the complete stage.
7099
7538
  `,
7100
7539
  'docs/architecture.md': `# Workspace architecture
7101
7540
 
@@ -7112,6 +7551,11 @@ Foundation implementation files.
7112
7551
  Canonical consumer stages use the matching \`development\`, \`staging\`, or \`production\`
7113
7552
  Foundation. Framework-derived personal stages may use only the \`development\`
7114
7553
  Foundation; generic preview stages are unsupported.
7554
+
7555
+ The committed deployment profile changes account and local/GitHub authority only. Every
7556
+ canonical stage always owns a separate Router/domain, Aurora cluster and migration history,
7557
+ Cognito user pool, event bus, secret namespace, IAM boundary, logs, queues, ECR namespace, and
7558
+ stage-qualified SSM discovery contract. Discovery rejects a record for another canonical stage.
7115
7559
  `,
7116
7560
  'docs/git-workflow.md': `# Protected Git workflow
7117
7561
 
@@ -7132,6 +7576,9 @@ GitHub rulesets, environments, and AWS IAM are the authorization boundaries; loc
7132
7576
  only defense in depth.
7133
7577
 
7134
7578
  The \`staging\` environment accepts only \`develop\`; \`production\` accepts only \`main\`.
7579
+ These workflows exist for every deployment profile. The staging workflow is optional for
7580
+ \`starter\`, whose governed local Nx target may deploy development and staging. Production is
7581
+ GitHub-only for every profile; \`fully-isolated\` also requires GitHub for staging.
7135
7582
  The committed \`productionApproval.mode\` selects one of two production authorization
7136
7583
  contracts:
7137
7584
 
@@ -7154,14 +7601,22 @@ contracts:
7154
7601
  Both modes retain required checks, a non-author CODEOWNER approval, no ruleset bypass, the
7155
7602
  literal production environment, and its branch-scoped credentials. The base \`staging\` and
7156
7603
  \`production\` environments own deployment, smoke, and ECR evidence roles plus their
7157
- variables; production also owns the staging-registry promotion pull role. Dedicated
7604
+ variables; isolated profiles also keep the staging-registry promotion pull role in production.
7605
+ Starter promotion authenticates once to the shared account and copies the exact staging digest
7606
+ into a distinct production ECR repository. Dedicated
7158
7607
  \`infrastructure-plan-<stage>\` environments gate read-only role use. Separate
7159
7608
  \`async-recovery-<stage>\`, \`performance-aggregate-<stage>\`,
7160
7609
  \`performance-cost-<stage>\`, \`performance-load-<stage>\`, and
7161
7610
  \`reliability-read-<stage>\` environments own only the role and variables required by that
7162
7611
  capability. Infrastructure-plan jobs use a repository-variable read-only role/input inventory;
7163
7612
  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
7613
+ trust and complete inline policy before any repository-backed plan job may assume it. Plan-role
7614
+ trust uses GitHub's immutable repository identity subject,
7615
+ \`repo:<owner>@<owner-id>/<repository>@<repository-id>:environment:<environment>\`.
7616
+ The audit derives it from the workflow's repository name, owner ID, repository ID, and literal
7617
+ environment and refuses missing IDs or the older mutable name-only subject. Update every
7618
+ adopter-owned AWS OIDC role trust to the immutable identity before protected use; never retain
7619
+ both forms. Every
7165
7620
  \`*-staging\` environment accepts only \`develop\`; every
7166
7621
  \`*-production\` environment accepts only \`main\`; reviewer, self-review, and wait protection
7167
7622
  matches the corresponding base stage. The credential-free \`npm-prerelease\` environment
@@ -7303,18 +7758,35 @@ materializes, close and reopen the pull request as a human and then approve it.
7303
7758
  `,
7304
7759
  'docs/repository-operating-policy.md': `# Repository Operating Policy
7305
7760
 
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.
7761
+ \`ebk.repository-policy.json\` is the reviewed source of truth for deployment profile, exact
7762
+ stage account IDs, deployment Region, production access, production approval, per-Slice
7763
+ readiness, and the one shared production Foundation profile.
7308
7764
  New workspaces start with production disabled and explicit Team-compatible
7309
7765
  \`productionApproval.mode: "promotion-pr"\`. A missing approval mode or Slice entry fails
7310
7766
  closed; no GitHub capability is allowed to choose a mode implicitly.
7311
7767
 
7768
+ Deployment profiles are proportional to operator maturity:
7769
+
7770
+ - \`starter\` (default) requires one shared account, permits local development and staging
7771
+ through the governed root Nx target, and reserves production for protected GitHub deployment;
7772
+ - \`production-isolated\` requires one development/staging account and a distinct production
7773
+ account; only production is GitHub-only; and
7774
+ - \`fully-isolated\` requires three distinct accounts; staging and production are GitHub-only.
7775
+
7776
+ Equal account IDs never imply shared stage resources. Router, Aurora, Cognito, events, secrets,
7777
+ IAM, logs, queues, resource names, and SSM discovery remain stage-specific. Existing prerelease
7778
+ repositories migrate explicitly to \`fully-isolated\`; upgrades never infer or weaken topology.
7779
+ That profile-only migration retains the former authority model but stays undeployable until exact
7780
+ development, staging, and production account IDs plus \`deploymentRegion\` are committed.
7781
+
7312
7782
  The named repository modes are source-writing presets, not runtime state. Report a change
7313
7783
  first and inspect the complete current/next policy:
7314
7784
 
7315
7785
  \`\`\`bash
7316
7786
  pnpm nx g @empire-builder-kit/nx:repository-policy \\
7317
- --preset=controlled-production --slices=billing
7787
+ --deploymentProfile=starter --deploymentRegion=us-east-1 \\
7788
+ --developmentAccountId=111122223333 --stagingAccountId=111122223333 \\
7789
+ --productionAccountId=111122223333
7318
7790
  \`\`\`
7319
7791
 
7320
7792
  Apply the reviewed source change separately with \`--apply\`. This command never contacts AWS
@@ -7324,6 +7796,66 @@ Use \`--productionApprovalMode=promotion-pr|environment-reviewer\` for an explic
7324
7796
  transition. This changes committed policy only; a separate bootstrap report/apply reconciles
7325
7797
  remote GitHub settings.
7326
7798
 
7799
+ After applying exact topology, authenticate with temporary federated credentials for development
7800
+ or staging and invoke
7801
+ \`pnpm nx run ${name}:deploy-local -- --stage=<stage> --profile=<profile>
7802
+ --expectedRoleArn=<role-arn>\`. Production runs only through the protected main-branch GitHub
7803
+ workflow. Each admitted local invocation plans the complete current stage, displays the stage
7804
+ summary and first material project, and requires a real TTY response of
7805
+ \`approve:<action>:<stage>:<project>:<current-plan-digest>\`. A human or authorized agent may type
7806
+ that acknowledgement; it is active operator control, not a claim of independent human review.
7807
+ The target creates one private ten-minute capability for that project, rechecks the exact source,
7808
+ artifacts, account, Region, role, stage, policy, state, and lock without a second provider preview,
7809
+ consumes the capability once, and removes it on every exit path. Unattended scripts cannot approve.
7810
+ Every material deploy or refresh mutates at most the first material project and ends with a
7811
+ controlled non-success \`replan-required\` handoff, including the last project in the ordered stage.
7812
+ Rerun the complete stage, review the new current summary, and repeat until one invocation proves
7813
+ every project is semantically converged. Only that convergence invocation succeeds. A preview is
7814
+ recent review and audit evidence; it is not represented as a cryptographically replayable Pulumi
7815
+ transaction. A complete, unlocked deploy plan containing only provider reads with no changed
7816
+ properties, replacement, failure, or unmatched risk finding is retained as observation evidence
7817
+ and counts as semantically converged. The standalone project planner may still return its
7818
+ conservative non-success for that drift observation; the root local transaction accepts only a
7819
+ freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
7820
+ only when their logical ID and resource type match exactly. Any stale or ambiguous
7821
+ observation fails closed; a plan with desired changes remains material and requires review. One
7822
+ narrower case stays reviewable instead of becoming
7823
+ drift: when an upstream Foundation apply changes only both external framework-version tag
7824
+ projections (plus SST's already-bounded name hydration), the retained
7825
+ external identity must match one exact non-replacing provider read in the same ordinary plan.
7826
+ Missing read correlation, owned resources, other tag or property changes, incomplete tag pairs,
7827
+ and provider failures remain drift and fail closed. The local target requires the exact SST v5 account bootstrap normally
7828
+ created by the pinned \`ebk dev up\` path, then creates and attests only missing exact
7829
+ per-project/canonical-stage state passphrases before its formal read-only plans. This passphrase
7830
+ bootstrap is the only persistent pre-acknowledgement write; it creates no deployment authority
7831
+ and is reported in bounded local evidence. Clean immutable source, immediate per-project plans,
7832
+ migration and retention preflights, and secret-free result
7833
+ evidence remain mandatory. Direct SST is forbidden.
7834
+
7835
+ After an interrupted apply, use the same target with \`--action=refresh\` and the exact
7836
+ phase, stage, profile, and role. It retains the same plan-summary review boundary, admits only
7837
+ bounded drift-detected refresh evidence, and never turns refresh into an implicit deploy. When
7838
+ the interrupted write leaves a recorded provider output unusable by the ordinary desired preview,
7839
+ the plan stops without mutation. The non-writing refresh preview separates provider refresh
7840
+ records from its later desired preview and cannot replace failed ordinary desired authority;
7841
+ repair the recorded dependency or use the documented
7842
+ operator recovery path before planning again.
7843
+
7844
+ A stage-wide refresh still plans and authorizes every selected project. When one exact retained
7845
+ project plan proves an absent SST state that is unlocked, complete, empty, and drift-free, the
7846
+ root target records that project as a refresh no-op instead of invoking SST against a nonexistent
7847
+ stage. It also retains the pinned provider's exact SSM \`ebk:framework-version\` \`tagsAll\`
7848
+ normalization as a semantically inert observation after the same bounded shape
7849
+ repeats following refresh. Value changes, other tags, security findings, deletes, replacements,
7850
+ sensitive properties, and every other resource type remain material. Locked, incomplete, failed,
7851
+ or otherwise drifted plans are never skipped.
7852
+
7853
+ For a stage with no workload-role account baseline, publish the fixed Foundation-only candidate
7854
+ and invoke this root target with \`--phase=foundation-bootstrap\`. After Foundation outputs exist,
7855
+ publish the complete \`full-stage\` candidate and invoke the target without \`--phase\` (or with
7856
+ \`--phase=full-stage\`). The bootstrap phase selects exactly Foundation; it is not an arbitrary
7857
+ project filter.
7858
+
7327
7859
  Validation requires both an explicit production approval mode and an explicit Foundation
7328
7860
  profile. It does not infer omitted values. Use \`promotion-pr\` for a private GitHub Team
7329
7861
  repository. Use \`environment-reviewer\` only when the repository plan can enforce a separate
@@ -7337,10 +7869,16 @@ environment reviewer.
7337
7869
 
7338
7870
  Protected production deploy and refresh validate the complete Foundation/Slice set before AWS
7339
7871
  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,
7872
+ cannot be narrowed with an ad hoc project argument. The retained stage preview remains review
7873
+ evidence. Immediately before each project mutation the workflow computes one fresh plan against
7874
+ current state and applies it without a second redundant preview. Source, artifacts, configuration,
7875
+ policy, environment, and protected identities remain exact. Provider-only read/discard observations
7876
+ may appear or disappear, but any new delete, replacement, import, stateful or security change, IAM
7877
+ expansion, protection reduction, or unclassified resource stops for renewed approval. After an
7878
+ upstream mutation, every downstream project therefore receives a fresh independent risk decision.
7879
+
7880
+ \`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` keeps a smaller
7881
+ 0.5–4 ACU warm floor for starter production traffic. Both keep managed NAT,
7344
7882
  30-day backups, deletion protection, retained data, and the protected GitHub/OIDC/AWS path.
7345
7883
  Changing policy affects only later lifecycle requests; it never deploys or removes resources.
7346
7884
  Changing profile requires a separate reviewed Foundation deployment and retained operational
@@ -7411,7 +7949,7 @@ deploy or refresh before AWS access. Protected remove and unlock remain usable f
7411
7949
  There is no local or silent bypass in this prerelease contract; any future waiver mechanism
7412
7950
  must be committed, reviewable, and auditable.
7413
7951
  `,
7414
- 'ebk.repository-policy.json': json((0, index_js_4.createIncubationRepositoryPolicy)()),
7952
+ 'ebk.repository-policy.json': json(initialRepositoryPolicy),
7415
7953
  'nx.json': json({
7416
7954
  $schema: './node_modules/nx/schemas/nx-schema.json',
7417
7955
  defaultBase: 'develop',
@@ -7451,6 +7989,7 @@ must be committed, reviewable, and auditable.
7451
7989
  inputs: ['default', '^production'],
7452
7990
  },
7453
7991
  },
7992
+ analytics: false,
7454
7993
  neverConnectToCloud: true,
7455
7994
  useDaemonProcess: false,
7456
7995
  }),
@@ -7460,29 +7999,31 @@ must be committed, reviewable, and auditable.
7460
7999
  private: true,
7461
8000
  packageManager: tool_versions_js_1.WORKSPACE_PNPM_DESCRIPTOR,
7462
8001
  engines: {
7463
- node: '>=22.12.0',
7464
- pnpm: '>=10.0.0',
8002
+ node: '>=22.13.0',
8003
+ pnpm: '>=11.20.0',
7465
8004
  },
7466
8005
  scripts: {
7467
8006
  build: `nx run-many -t build --exclude=${name}`,
7468
8007
  format: 'nx format:write',
7469
- 'format:check': 'nx format:check',
8008
+ 'format:check': 'nx format:check --all',
7470
8009
  graph: 'nx graph',
7471
8010
  health: `nx run ${name}:workspace-health`,
7472
8011
  lint: `nx run-many -t lint --exclude=${name}`,
7473
8012
  test: `nx run-many -t test --exclude=${name}`,
7474
8013
  typecheck: `nx run-many -t typecheck --exclude=${name}`,
7475
- verify: `nx run ${name}:verify && nx run-many -t verify --exclude=${name}`,
8014
+ verify: `nx format:check --all && nx run ${name}:verify && nx run-many -t verify --exclude=${name}`,
7476
8015
  },
7477
8016
  dependencies: {
7478
8017
  '@empire-builder-kit/runtime': frameworkVersion,
7479
8018
  },
7480
8019
  devDependencies: {
7481
8020
  '@aws-sdk/client-iam': TOOL_VERSIONS.awsSdk,
8021
+ '@aws-sdk/client-rds': TOOL_VERSIONS.awsSdk,
7482
8022
  '@aws-sdk/client-s3': TOOL_VERSIONS.awsSdk,
7483
8023
  '@aws-sdk/client-secrets-manager': TOOL_VERSIONS.awsSdk,
7484
8024
  '@aws-sdk/client-ssm': TOOL_VERSIONS.awsSdk,
7485
8025
  '@aws-sdk/client-sts': TOOL_VERSIONS.awsSdk,
8026
+ '@aws-sdk/credential-providers': TOOL_VERSIONS.awsSdk,
7486
8027
  '@eslint/js': TOOL_VERSIONS.eslintJs,
7487
8028
  '@empire-builder-kit/nx': frameworkVersion,
7488
8029
  '@types/node': TOOL_VERSIONS.nodeTypes,
@@ -7515,6 +8056,15 @@ must be committed, reviewable, and auditable.
7515
8056
  protectedRef: 'refs/remotes/origin/develop',
7516
8057
  },
7517
8058
  },
8059
+ 'blueprint-trust-check-main': {
8060
+ executor: '@empire-builder-kit/nx:blueprint-trust',
8061
+ options: {
8062
+ operation: 'adopt',
8063
+ output: '.ebk/evidence/supply-chain/blueprint-trust.json',
8064
+ protectedBranch: 'main',
8065
+ protectedRef: 'refs/remotes/origin/main',
8066
+ },
8067
+ },
7518
8068
  'blueprint-trust-check-protected': {
7519
8069
  executor: '@empire-builder-kit/nx:blueprint-trust',
7520
8070
  options: {
@@ -7557,6 +8107,13 @@ must be committed, reviewable, and auditable.
7557
8107
  executor: 'nx:run-commands',
7558
8108
  options: { command: 'node tools/install-ebk-hooks.mjs' },
7559
8109
  },
8110
+ 'deploy-local': {
8111
+ cache: false,
8112
+ executor: '@empire-builder-kit/nx:local-deploy',
8113
+ options: {
8114
+ stage: 'development',
8115
+ },
8116
+ },
7560
8117
  'supply-chain-check': {
7561
8118
  executor: 'nx:run-commands',
7562
8119
  options: {
@@ -7598,6 +8155,11 @@ must be committed, reviewable, and auditable.
7598
8155
  executor: 'nx:run-commands',
7599
8156
  options: { command: 'node tools/workspace-health.mjs' },
7600
8157
  },
8158
+ 'verify-proposed': {
8159
+ dependsOn: ['supply-chain-check'],
8160
+ executor: 'nx:run-commands',
8161
+ options: { command: 'node tools/workspace-health.mjs' },
8162
+ },
7601
8163
  'workspace-health': {
7602
8164
  executor: 'nx:run-commands',
7603
8165
  options: { command: 'node tools/workspace-health.mjs' },
@@ -7620,6 +8182,10 @@ Workspace creation does not deploy this project. Review \`docs/costs.md\`,
7620
8182
 
7621
8183
  Set \`EBK_ROOT_DOMAIN\` before deploying when Route 53 in the target account owns a public
7622
8184
  domain. Without it, the shared Router uses its functional provider URL.
8185
+
8186
+ Before deploying development or staging, add at least one exact operator/probe CIDR to
8187
+ \`security/router-waf.json\`. Production is public by default behind the same managed and
8188
+ rate-limiting baseline; select its allowlist mode explicitly when required.
7623
8189
  `,
7624
8190
  'packages/foundation/contracts/foundation.json': json({
7625
8191
  contractVersion: '1.0.0',
@@ -7679,8 +8245,8 @@ seeded decision record aligned with organization-specific budgets and alerts.
7679
8245
  - Development uses EC2 NAT, a development bastion, and Aurora scale-to-zero.
7680
8246
  - Staging uses EC2 NAT and Aurora scale-to-zero by default.
7681
8247
  - 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.
8248
+ Operating Policy profile selects either the standard 2-ACU Aurora floor or the
8249
+ cost-optimized 0.5-ACU warm floor.
7684
8250
  - ECS, EventBridge, Cognito, SNS, SSM, and CloudWatch are primarily usage-priced; logs,
7685
8251
  metrics, alarms, and retained data still require active cost review.
7686
8252
 
@@ -7698,8 +8264,69 @@ Use Nx for every supported lifecycle action:
7698
8264
  - \`pnpm nx run foundation:unlock --stage=development\`
7699
8265
  - \`pnpm nx run foundation:remove --stage=development\`
7700
8266
 
7701
- Do not invoke SST lifecycle commands directly. Production deployment belongs in the trusted
7702
- CI path.
8267
+ Do not invoke SST lifecycle commands directly. The root \`deploy-local\` target is the governed
8268
+ local path for stages admitted by the committed deployment profile. Its fixed
8269
+ \`foundation-bootstrap\` phase is the only local partial-stage path and exists solely for the
8270
+ documented first workload-role publication; isolated protected stages remain on the trusted
8271
+ GitHub path.
8272
+
8273
+ Every local invocation plans the complete selected stage but mutates at most one material
8274
+ project. Type the exact current \`approve:<action>:<stage>:<project>:<plan-digest>\` challenge in a
8275
+ real TTY after reviewing the displayed summary. A human or authorized agent may operate that TTY;
8276
+ unattended input is refused. The acknowledgement creates one ten-minute, one-use, private
8277
+ capability bound to source, artifacts, policy, environment, credentials, and current state. A
8278
+ completed deploy or refresh returns the controlled non-success \`replan-required\`; rerun the
8279
+ complete stage, review the new current summary, and continue one project at a time. Only a
8280
+ semantically converged complete stage reports success. No upstream mutation can authorize a
8281
+ downstream project. A complete, unlocked deploy plan containing only provider reads with no
8282
+ changed properties, replacement, failure, or unmatched risk finding is retained as observation
8283
+ evidence and counts as semantically converged. The standalone project planner may still return
8284
+ its conservative non-success for that drift observation; the root local transaction accepts only
8285
+ a freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
8286
+ only when their logical ID and resource type match exactly. Any stale or ambiguous
8287
+ observation fails closed; a plan with desired changes remains material and requires review. A
8288
+ material downstream plan may also retain an upstream Foundation framework-version tag advance as
8289
+ observation only when both external tag
8290
+ projections, the retained identity, optional bounded SST name hydration, and one non-replacing
8291
+ provider read correlate exactly. Every other external property or uncorroborated read remains
8292
+ drift and blocks before the TTY challenge.
8293
+
8294
+ For an interrupted local apply, run the same root target with \`--action=refresh\`, review its
8295
+ current provider-refresh evidence, and type the displayed TTY challenge. Before mutation the
8296
+ executor writes and verifies a private state recovery point plus a secret-free digest/version
8297
+ manifest. The refresh preview validates its provider and later desired-event partitions; any
8298
+ ordinary preview failure or malformed post-refresh desired event remains blocked. Provider-only
8299
+ observation noise is retained without claiming a replayable preview. Because refresh changes
8300
+ recorded state rather than cloud resources, provider-output security and protection findings
8301
+ remain explicit review requirements under the drift decision. Refresh never continues into deploy
8302
+ or claims desired-state convergence: run the governed deploy target afterward and resolve every
8303
+ remaining source-versus-state change. Exclude the \`.private.json\` recovery point from archives.
8304
+
8305
+ The root refresh target includes every selected project in its exact reviewed plan set. It skips
8306
+ the SST child only when that project's retained plan proves an absent, unlocked, complete, empty,
8307
+ and drift-free state, or when the only retained difference is the pinned provider's exact repeated
8308
+ SSM \`ebk:framework-version\` \`tagsAll\` normalization. It records the project as an explicit refresh no-op
8309
+ in either case while retaining the observation. Value changes, other tags, security findings,
8310
+ deletes, replacements, sensitive
8311
+ properties, and every other resource type remain material; every other plan must execute or fail
8312
+ closed.
8313
+
8314
+ ## Router WAF
8315
+
8316
+ Each canonical stage owns one global-scope Web ACL attached to that stage's Foundation Router.
8317
+ The committed \`security/router-waf.json\` policy requires AWS Common Rule Set, Known Bad
8318
+ Inputs, Amazon IP Reputation, an IP rate limit, CloudWatch metrics, and sampled requests.
8319
+ Development and staging fail synthesis until their allowlists contain explicit CIDRs.
8320
+ Production is public by default and may be changed to an explicit allowlist.
8321
+
8322
+ For tuning, set a nonproduction stage to \`count\`, deploy and inspect sampled requests, then
8323
+ return staging to \`block\` and run the live allow/deny, managed-rule, rate-limit, metric, and
8324
+ sampled-request probes. Write their bounded, secret-free result to
8325
+ \`security/router-waf-live-evidence.json\`, then run
8326
+ \`pnpm nx run foundation:router-waf-rehearsal\`. The executor seals the exact policy digest
8327
+ into \`router-waf-blocking-rehearsal.json\`. Production synthesis rejects a missing, stale, or
8328
+ count-mode rehearsal. Full request logging is opt-in, blocked-only, retention-bounded, and
8329
+ redacts query strings plus credential-bearing headers.
7703
8330
 
7704
8331
  Foundation stages are canonical. \`ebk dev\` derives a Product Slice personal
7705
8332
  stage from the operating-system account and maps that consumer to the
@@ -7734,6 +8361,12 @@ connects as the Slice's no-password IAM-authenticated migrator user.
7734
8361
  stage-scoped SSM/secret path and least-privilege IAM.
7735
8362
  - Public domains are configured explicitly through \`EBK_ROOT_DOMAIN\`; generated source never
7736
8363
  contains credentials or customer domain assumptions.
8364
+ - Every stage's CloudFront Router has its own global Web ACL. The baseline always includes AWS
8365
+ Common Rule Set, Known Bad Inputs, Amazon IP Reputation, rate blocking, CloudWatch metrics,
8366
+ and sampled requests. Nonproduction is allowlisted by default. Production is public by
8367
+ default, but cannot deploy until the exact policy has passed a staging blocking rehearsal.
8368
+ - Full WAF request logging is disabled by default. Enabling it keeps blocked requests only,
8369
+ uses bounded retention, and redacts query strings, cookies, authorization, and API keys.
7737
8370
 
7738
8371
  The managed \`contracts/frontend-security-headers.json\` file owns the transport
7739
8372
  minimum: HTTPS in deployed environments, one-year HSTS there, and the common
@@ -7766,7 +8399,12 @@ declare const $app: {
7766
8399
  import {
7767
8400
  FOUNDATION_SSM_SCHEMA_VERSION,
7768
8401
  STANDARD_FOUNDATION_CAPABILITIES,
8402
+ createRouterWafRules,
8403
+ createRouterWafStagePlan,
8404
+ foundationDatabaseEndpointHost,
7769
8405
  foundationSsmPath,
8406
+ normalizeRouterWafPolicy,
8407
+ reconcileRouterWafBlockingRehearsal,
7770
8408
  type FoundationSsmKey,
7771
8409
  } from '@empire-builder-kit/nx/foundation';
7772
8410
  import {
@@ -7776,6 +8414,9 @@ import {
7776
8414
  } from '@empire-builder-kit/nx/repository-policy';
7777
8415
  import { createFoundationAccountCostControlPlan } from '@empire-builder-kit/nx/performance';
7778
8416
  import accountCostPolicy from '../performance/account-cost-policy.json';
8417
+ import routerWafBlockingRehearsal from '../security/router-waf-blocking-rehearsal.json';
8418
+ import routerWafLiveEvidence from '../security/router-waf-live-evidence.json';
8419
+ import routerWafPolicy from '../security/router-waf.json';
7779
8420
  import type { FoundationWorkloadRolePreflight } from './workload-roles/preflight.js';
7780
8421
 
7781
8422
  function identityDomainPrefix(stage: string, accountId: string): string {
@@ -7827,6 +8468,18 @@ export function foundation(
7827
8468
  )
7828
8469
  : undefined;
7829
8470
  const natMode = productionConfiguration?.contract.nat ?? 'ec2';
8471
+ const normalizedRouterWafPolicy = normalizeRouterWafPolicy(routerWafPolicy);
8472
+ const wafPlan = createRouterWafStagePlan({
8473
+ policy: normalizedRouterWafPolicy,
8474
+ rehearsal: isProduction
8475
+ ? reconcileRouterWafBlockingRehearsal({
8476
+ evidence: routerWafLiveEvidence,
8477
+ policy: normalizedRouterWafPolicy,
8478
+ receipt: routerWafBlockingRehearsal,
8479
+ })
8480
+ : null,
8481
+ stage: $app.stage,
8482
+ });
7830
8483
  const natBinding = workloadRoles?.bindings.nat;
7831
8484
  if (natMode === 'ec2' && natBinding) {
7832
8485
  $transform(
@@ -7836,6 +8489,9 @@ export function foundation(
7836
8489
  args.name = natBinding.instanceProfileName;
7837
8490
  args.role = natBinding.roleName;
7838
8491
  opts.import = natBinding.instanceProfileName;
8492
+ opts.ignoreChanges = [
8493
+ ...new Set([...(opts.ignoreChanges ?? []), 'tags', 'tagsAll']),
8494
+ ];
7839
8495
  },
7840
8496
  );
7841
8497
  }
@@ -7857,8 +8513,83 @@ export function foundation(
7857
8513
  aliases: ['*.' + rootDomain],
7858
8514
  dns: sst.aws.dns(),
7859
8515
  },
7860
- }
8516
+ }
7861
8517
  : {}),
8518
+ waf: {
8519
+ logging: wafPlan.logging,
8520
+ managedRules: {
8521
+ coreRuleSet: true,
8522
+ knownBadInputs: true,
8523
+ sqlInjection: false,
8524
+ },
8525
+ rateLimitPerIp: wafPlan.rateLimitPerIp,
8526
+ },
8527
+ transform: {
8528
+ cdn(args: any) {
8529
+ if (!args.webAclArn) {
8530
+ throw new Error(
8531
+ 'Foundation Router must attach its exact global Web ACL.',
8532
+ );
8533
+ }
8534
+ },
8535
+ waf(args: any, opts: any) {
8536
+ if (args.scope !== 'CLOUDFRONT') {
8537
+ throw new Error(
8538
+ 'Foundation Router Web ACL must use global CloudFront scope.',
8539
+ );
8540
+ }
8541
+ const ipSets: { ipv4Arn?: any; ipv6Arn?: any } = {};
8542
+ if (wafPlan.ipv4Cidrs.length > 0) {
8543
+ ipSets.ipv4Arn = new aws.wafv2.IpSet(
8544
+ 'FoundationRouterIpv4Allowlist',
8545
+ {
8546
+ addresses: wafPlan.ipv4Cidrs,
8547
+ ipAddressVersion: 'IPV4',
8548
+ name: 'foundation-' + wafPlan.stage + '-router-ipv4-allowlist',
8549
+ scope: 'CLOUDFRONT',
8550
+ tags: {
8551
+ 'ebk:policy-digest': wafPlan.policyDigest,
8552
+ 'ebk:stage': wafPlan.stage,
8553
+ },
8554
+ },
8555
+ { provider: opts.provider },
8556
+ ).arn;
8557
+ }
8558
+ if (wafPlan.ipv6Cidrs.length > 0) {
8559
+ ipSets.ipv6Arn = new aws.wafv2.IpSet(
8560
+ 'FoundationRouterIpv6Allowlist',
8561
+ {
8562
+ addresses: wafPlan.ipv6Cidrs,
8563
+ ipAddressVersion: 'IPV6',
8564
+ name: 'foundation-' + wafPlan.stage + '-router-ipv6-allowlist',
8565
+ scope: 'CLOUDFRONT',
8566
+ tags: {
8567
+ 'ebk:policy-digest': wafPlan.policyDigest,
8568
+ 'ebk:stage': wafPlan.stage,
8569
+ },
8570
+ },
8571
+ { provider: opts.provider },
8572
+ ).arn;
8573
+ }
8574
+ args.defaultAction = { allow: {} };
8575
+ args.rules = createRouterWafRules(wafPlan, ipSets);
8576
+ args.scope = 'CLOUDFRONT';
8577
+ args.tags = {
8578
+ ...(args.tags ?? {}),
8579
+ 'ebk:policy-digest': wafPlan.policyDigest,
8580
+ 'ebk:stage': wafPlan.stage,
8581
+ };
8582
+ args.visibilityConfig = {
8583
+ cloudwatchMetricsEnabled: true,
8584
+ metricName:
8585
+ 'Ebk' +
8586
+ wafPlan.stage[0].toUpperCase() +
8587
+ wafPlan.stage.slice(1) +
8588
+ 'FoundationRouterWaf',
8589
+ sampledRequestsEnabled: true,
8590
+ };
8591
+ },
8592
+ },
7862
8593
  });
7863
8594
  const database = new sst.aws.Aurora('FoundationDatabase', {
7864
8595
  // SQL bootstrap and migrations run inside the VPC. Keep the public HTTPS
@@ -7882,6 +8613,46 @@ export function foundation(
7882
8613
  },
7883
8614
  vpc,
7884
8615
  });
8616
+ const databaseClusterEndpoint = database.nodes.cluster.endpoint.apply(
8617
+ foundationDatabaseEndpointHost,
8618
+ );
8619
+ const databaseReaderEndpoint = database.nodes.cluster.readerEndpoint.apply(
8620
+ foundationDatabaseEndpointHost,
8621
+ );
8622
+ // SST v4.17.1 creates a top-level LinkRef on the next tick and its Aurora
8623
+ // implementation unconditionally splits cluster endpoints. During a
8624
+ // governed refresh after an interrupted first deploy, those provider
8625
+ // outputs are legitimately absent. Preserve the normal link contract while
8626
+ // keeping that read-only recovery program total; a real deploy resolves the
8627
+ // exact endpoints before any discovery resource is created.
8628
+ database.getSSTLink = () => ({
8629
+ include: [
8630
+ sst.aws.permission({
8631
+ actions: ['secretsmanager:GetSecretValue'],
8632
+ resources: [database.secretArn],
8633
+ }),
8634
+ sst.aws.permission({
8635
+ actions: [
8636
+ 'rds-data:BatchExecuteStatement',
8637
+ 'rds-data:BeginTransaction',
8638
+ 'rds-data:CommitTransaction',
8639
+ 'rds-data:ExecuteStatement',
8640
+ 'rds-data:RollbackTransaction',
8641
+ ],
8642
+ resources: [database.clusterArn],
8643
+ }),
8644
+ ],
8645
+ properties: {
8646
+ clusterArn: database.clusterArn,
8647
+ database: database.database,
8648
+ host: databaseClusterEndpoint,
8649
+ password: database.password,
8650
+ port: database.port,
8651
+ reader: databaseReaderEndpoint,
8652
+ secretArn: database.secretArn,
8653
+ username: database.username,
8654
+ },
8655
+ });
7885
8656
  const foundationAccountId = aws.getCallerIdentityOutput({}).accountId;
7886
8657
  const foundationPartition = aws.getPartitionOutput({});
7887
8658
  const foundationRegion = aws.getRegionOutput({});
@@ -7944,6 +8715,7 @@ export function foundation(
7944
8715
  },
7945
8716
  ],
7946
8717
  resources: [alertTopic.arn],
8718
+ sid: 'AllowBudgetAlerts',
7947
8719
  },
7948
8720
  {
7949
8721
  actions: ['sns:Publish'],
@@ -7957,6 +8729,7 @@ export function foundation(
7957
8729
  type: 'Service',
7958
8730
  }],
7959
8731
  resources: [alertTopic.arn],
8732
+ sid: 'AllowCostAnomalyAlerts',
7960
8733
  },
7961
8734
  {
7962
8735
  actions: ['sns:Publish'],
@@ -7979,6 +8752,7 @@ export function foundation(
7979
8752
  type: 'Service',
7980
8753
  }],
7981
8754
  resources: [alertTopic.arn],
8755
+ sid: 'AllowRdsEventAlerts',
7982
8756
  },
7983
8757
  {
7984
8758
  actions: ['sns:Publish'],
@@ -8003,6 +8777,7 @@ export function foundation(
8003
8777
  },
8004
8778
  ],
8005
8779
  resources: [alertTopic.arn],
8780
+ sid: 'AllowCloudWatchAlarmAlerts',
8006
8781
  },
8007
8782
  ],
8008
8783
  }).json,
@@ -8031,6 +8806,41 @@ export function foundation(
8031
8806
  { dependsOn: [alertTopicPolicy] },
8032
8807
  )
8033
8808
  : undefined;
8809
+ const accountCostAnomalyMonitor = isProduction
8810
+ ? new aws.costexplorer.AnomalyMonitor(
8811
+ 'FoundationAccountCostAnomalyMonitor',
8812
+ {
8813
+ monitorDimension: 'SERVICE',
8814
+ monitorType: 'DIMENSIONAL',
8815
+ name: '${name}-account-service-cost-anomalies',
8816
+ tags: {
8817
+ 'ebk:cost-policy-digest': accountCostControl.policyDigest,
8818
+ 'ebk:cost-scope': '${name}/account/production',
8819
+ 'ebk:owner': accountCostControl.owner,
8820
+ },
8821
+ },
8822
+ )
8823
+ : undefined;
8824
+ const accountCostAnomalySubscription =
8825
+ accountCostControl.anomaly && accountCostAnomalyMonitor
8826
+ ? new aws.costexplorer.AnomalySubscription(
8827
+ 'FoundationAccountCostAnomalySubscription',
8828
+ {
8829
+ frequency: accountCostControl.anomaly.subscription.frequency,
8830
+ monitorArnLists: [accountCostAnomalyMonitor.arn],
8831
+ name: accountCostControl.anomaly.subscription.name,
8832
+ subscribers: [{ address: alertTopic.arn, type: 'SNS' }],
8833
+ tags: {
8834
+ 'ebk:cost-policy-digest': accountCostControl.policyDigest,
8835
+ 'ebk:cost-scope': '${name}/account/production',
8836
+ 'ebk:owner': accountCostControl.owner,
8837
+ },
8838
+ thresholdExpression:
8839
+ accountCostControl.anomaly.subscription.thresholdExpression,
8840
+ },
8841
+ { dependsOn: [accountCostAnomalyMonitor, alertTopicPolicy] },
8842
+ )
8843
+ : undefined;
8034
8844
  const databaseEvents = isProduction
8035
8845
  ? new aws.rds.EventSubscription(
8036
8846
  'FoundationDatabaseEvents',
@@ -8133,12 +8943,10 @@ export function foundation(
8133
8943
  clusterId: cluster.id,
8134
8944
  contractVersion: '1.0.0',
8135
8945
  databaseClusterArn: database.clusterArn,
8136
- databaseClusterEndpoint: database.nodes.cluster.endpoint.apply(
8137
- (endpoint: string) => endpoint.split(':')[0],
8138
- ),
8946
+ databaseClusterEndpoint,
8139
8947
  databaseClusterId: database.id,
8140
8948
  databaseClusterResourceId: database.nodes.cluster.clusterResourceId,
8141
- databaseHost: database.host,
8949
+ databaseHost: databaseClusterEndpoint,
8142
8950
  databaseMasterSecretArn: database.secretArn,
8143
8951
  databasePort: database.port.apply(String),
8144
8952
  eventBusArn: eventBus.arn,
@@ -8160,6 +8968,7 @@ export function foundation(
8160
8968
  capabilities: STANDARD_FOUNDATION_CAPABILITIES,
8161
8969
  clusterId: cluster.id,
8162
8970
  contractVersion: '1.0.0',
8971
+ costAnomalySubscriptionArn: accountCostAnomalySubscription?.arn,
8163
8972
  databaseCapacityAlarmName: databaseCapacityAlarm.name,
8164
8973
  dashboardName: dashboard.dashboardName,
8165
8974
  databaseClusterId: database.id,
@@ -8191,13 +9000,49 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8191
9000
  actualNotificationThresholdPercent: 80,
8192
9001
  forecastNotificationThresholdPercent: 100,
8193
9002
  kind: 'ebk-foundation-account-cost-policy',
8194
- monthlyMaximumUsd: 1_000,
9003
+ monthlyMaximumUsd: deploymentProfile === 'starter' ? 250 : 1_000,
8195
9004
  owner: 'replace-with-owner',
8196
9005
  reviewAt: null,
8197
9006
  schemaVersion: 1,
8198
9007
  status: 'example',
8199
9008
  workspace: name,
8200
9009
  }),
9010
+ 'packages/foundation/security/router-waf.json': json({
9011
+ kind: 'ebk-router-waf-policy',
9012
+ logging: {
9013
+ enabled: false,
9014
+ retention: '1 week',
9015
+ },
9016
+ rateLimitPerIp: 2_000,
9017
+ schemaVersion: 1,
9018
+ stages: {
9019
+ development: {
9020
+ access: 'allowlist',
9021
+ allowedCidrs: [],
9022
+ mode: 'block',
9023
+ },
9024
+ production: {
9025
+ access: 'public',
9026
+ allowedCidrs: [],
9027
+ mode: 'block',
9028
+ },
9029
+ staging: {
9030
+ access: 'allowlist',
9031
+ allowedCidrs: [],
9032
+ mode: 'block',
9033
+ },
9034
+ },
9035
+ }),
9036
+ 'packages/foundation/security/router-waf-blocking-rehearsal.json': json({
9037
+ kind: 'ebk-router-waf-blocking-rehearsal',
9038
+ schemaVersion: 1,
9039
+ status: 'pending',
9040
+ }),
9041
+ 'packages/foundation/security/router-waf-live-evidence.json': json({
9042
+ kind: 'ebk-router-waf-live-evidence',
9043
+ schemaVersion: 1,
9044
+ status: 'pending',
9045
+ }),
8201
9046
  'packages/foundation/configuration/declarations.json': json({
8202
9047
  project: 'foundation',
8203
9048
  schemaVersion: 1,
@@ -8230,29 +9075,96 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8230
9075
  version: '4.17.1',
8231
9076
  },
8232
9077
  policy: {
9078
+ adoptedImports: [
9079
+ {
9080
+ authority: 'workload-role-account-baseline',
9081
+ logicalId: 'FoundationVpcNatInstanceProfile',
9082
+ privateIdentifierTemplate: 'ebk-{stage}-foundation-nat-profile',
9083
+ recoveryRunbookRef: 'docs/workload-role-account-baseline.md',
9084
+ removalBehavior: 'retain',
9085
+ resourceType: 'aws:iam/instanceProfile:InstanceProfile',
9086
+ },
9087
+ ],
8233
9088
  additiveLowRiskTypes: [
8234
9089
  'aws:cloudwatch/dashboard:Dashboard',
8235
9090
  'aws:cloudwatch/logGroup:LogGroup',
8236
9091
  'aws:cloudwatch/metricAlarm:MetricAlarm',
8237
9092
  ],
9093
+ additiveReviewedTypes: [
9094
+ 'aws:cloudwatch/eventArchive:EventArchive',
9095
+ 'aws:cloudwatch/eventBus:EventBus',
9096
+ 'aws:cognito/userPoolDomain:UserPoolDomain',
9097
+ 'aws:ec2/defaultSecurityGroup:DefaultSecurityGroup',
9098
+ 'aws:ec2/internetGateway:InternetGateway',
9099
+ 'aws:ec2/routeTable:RouteTable',
9100
+ 'aws:ec2/routeTableAssociation:RouteTableAssociation',
9101
+ 'aws:ec2/securityGroup:SecurityGroup',
9102
+ 'aws:ec2/subnet:Subnet',
9103
+ 'aws:ec2/vpc:Vpc',
9104
+ 'aws:ecs/cluster:Cluster',
9105
+ 'aws:ecs/clusterCapacityProviders:ClusterCapacityProviders',
9106
+ 'aws:rds/clusterParameterGroup:ClusterParameterGroup',
9107
+ 'aws:rds/eventSubscription:EventSubscription',
9108
+ 'aws:rds/parameterGroup:ParameterGroup',
9109
+ 'aws:rds/subnetGroup:SubnetGroup',
9110
+ 'aws:servicediscovery/privateDnsNamespace:PrivateDnsNamespace',
9111
+ 'aws:sns/topic:Topic',
9112
+ 'aws:sns/topicPolicy:TopicPolicy',
9113
+ 'aws:wafv2/webAclLoggingConfiguration:WebAclLoggingConfiguration',
9114
+ 'pulumi-nodejs:dynamic:Resource',
9115
+ 'pulumi:providers:aws',
9116
+ 'pulumi:providers:pulumi-nodejs',
9117
+ 'pulumi:providers:random',
9118
+ 'pulumi:pulumi:Stack',
9119
+ 'sst:aws:Aurora',
9120
+ 'sst:aws:Bus',
9121
+ 'sst:aws:CDN',
9122
+ 'sst:aws:Cluster',
9123
+ 'sst:aws:CognitoUserPool',
9124
+ 'sst:aws:Router',
9125
+ 'sst:aws:SnsTopic',
9126
+ 'sst:aws:Vpc',
9127
+ 'sst:sst:LinkRef',
9128
+ 'sst:sst:Version',
9129
+ ],
8238
9130
  costOrQuotaTypes: [
8239
9131
  'aws:apigatewayv2/api:Api',
8240
9132
  'aws:budgets/budget:Budget',
9133
+ 'aws:cloudfront/cachePolicy:CachePolicy',
8241
9134
  'aws:cloudfront/distribution:Distribution',
9135
+ 'aws:cloudfront/function:Function',
9136
+ 'aws:cloudfront/keyValueStore:KeyValueStore',
9137
+ 'aws:costexplorer/anomalyMonitor:AnomalyMonitor',
9138
+ 'aws:costexplorer/anomalySubscription:AnomalySubscription',
8242
9139
  'aws:cognito/userPool:UserPool',
9140
+ 'aws:ec2/eip:Eip',
9141
+ 'aws:ec2/eipAssociation:EipAssociation',
9142
+ 'aws:ec2/instance:Instance',
8243
9143
  'aws:ec2/natGateway:NatGateway',
8244
9144
  'aws:rds/cluster:Cluster',
9145
+ 'aws:rds/clusterInstance:ClusterInstance',
9146
+ 'aws:wafv2/ipSet:IpSet',
9147
+ 'aws:wafv2/webAcl:WebAcl',
8245
9148
  ],
8246
9149
  failClosedOnUnknown: true,
8247
9150
  protectedPlanAuthority: 'deploy-role-residual',
8248
9151
  standingPolicyRiskClasses: ['no-change', 'additive-low-risk'],
8249
9152
  statefulTypes: [
9153
+ 'aws:cloudwatch/eventArchive:EventArchive',
9154
+ 'aws:cloudwatch/eventBus:EventBus',
8250
9155
  'aws:cognito/userPool:UserPool',
8251
9156
  'aws:dynamodb/table:Table',
8252
9157
  'aws:events/eventBus:EventBus',
8253
9158
  'aws:kms/key:Key',
8254
9159
  'aws:rds/cluster:Cluster',
8255
9160
  'aws:s3/bucket:Bucket',
9161
+ 'aws:secretsmanager/secret:Secret',
9162
+ 'aws:secretsmanager/secretVersion:SecretVersion',
9163
+ 'aws:sns/topic:Topic',
9164
+ 'aws:ssm/parameter:Parameter',
9165
+ 'aws:wafv2/ipSet:IpSet',
9166
+ 'aws:wafv2/webAcl:WebAcl',
9167
+ 'random:index/randomPassword:RandomPassword',
8256
9168
  ],
8257
9169
  },
8258
9170
  project: 'foundation',
@@ -8350,6 +9262,13 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8350
9262
  executor: '@empire-builder-kit/nx:foundation-profile-preflight',
8351
9263
  options: { stage: 'development' },
8352
9264
  },
9265
+ 'router-waf-rehearsal': {
9266
+ cache: false,
9267
+ executor: '@empire-builder-kit/nx:router-waf-rehearsal',
9268
+ outputs: [
9269
+ `{projectRoot}/security/router-waf-blocking-rehearsal.json`,
9270
+ ],
9271
+ },
8353
9272
  typecheck: {
8354
9273
  executor: 'nx:run-commands',
8355
9274
  options: {
@@ -8377,6 +9296,11 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8377
9296
  }),
8378
9297
  'packages/foundation/scripts/verify-foundation.mjs': `import { readFileSync } from 'node:fs';
8379
9298
 
9299
+ import {
9300
+ normalizeRouterWafPolicy,
9301
+ reconcileRouterWafBlockingRehearsal,
9302
+ } from '@empire-builder-kit/nx/foundation';
9303
+
8380
9304
  const descriptor = JSON.parse(
8381
9305
  readFileSync(new URL('../contracts/foundation.json', import.meta.url), 'utf8'),
8382
9306
  );
@@ -8392,6 +9316,33 @@ const frontendSecurityHeaders = JSON.parse(
8392
9316
  'utf8',
8393
9317
  ),
8394
9318
  );
9319
+ const routerWafPolicy = normalizeRouterWafPolicy(
9320
+ JSON.parse(
9321
+ readFileSync(
9322
+ new URL('../security/router-waf.json', import.meta.url),
9323
+ 'utf8',
9324
+ ),
9325
+ ),
9326
+ );
9327
+ reconcileRouterWafBlockingRehearsal({
9328
+ evidence: JSON.parse(
9329
+ readFileSync(
9330
+ new URL('../security/router-waf-live-evidence.json', import.meta.url),
9331
+ 'utf8',
9332
+ ),
9333
+ ),
9334
+ policy: routerWafPolicy,
9335
+ receipt:
9336
+ JSON.parse(
9337
+ readFileSync(
9338
+ new URL(
9339
+ '../security/router-waf-blocking-rehearsal.json',
9340
+ import.meta.url,
9341
+ ),
9342
+ 'utf8',
9343
+ ),
9344
+ ),
9345
+ });
8395
9346
  const expectedCapabilities = [
8396
9347
  'network',
8397
9348
  'routing',
@@ -8407,6 +9358,13 @@ const expectedCapabilities = [
8407
9358
  if (descriptor.contractVersion !== '1.0.0') {
8408
9359
  throw new Error('Foundation contractVersion must be 1.0.0.');
8409
9360
  }
9361
+ if (
9362
+ routerWafPolicy.stages.development.access !== 'allowlist' ||
9363
+ routerWafPolicy.stages.staging.access !== 'allowlist' ||
9364
+ routerWafPolicy.stages.production.mode !== 'block'
9365
+ ) {
9366
+ throw new Error('Foundation Router WAF stage defaults are invalid.');
9367
+ }
8410
9368
  if (JSON.stringify(descriptor.capabilities) !== JSON.stringify(expectedCapabilities)) {
8411
9369
  throw new Error('Foundation capability descriptor does not match the v1 standard.');
8412
9370
  }
@@ -8415,7 +9373,7 @@ if (
8415
9373
  costs.aurora?.engineVersion !== '${index_js_4.FOUNDATION_AURORA_ENGINE_VERSION}' ||
8416
9374
  costs.productionProfileSource !== 'ebk.repository-policy.json' ||
8417
9375
  costs.productionProfiles?.standard?.aurora?.min !== '2 ACU' ||
8418
- costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0 ACU' ||
9376
+ costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0.5 ACU' ||
8419
9377
  !costs.stages?.development ||
8420
9378
  !costs.stages?.staging ||
8421
9379
  costs.stages?.production?.dataRetention !== 'retained-and-protected'
@@ -8471,16 +9429,15 @@ export default $config({
8471
9429
  providers: {
8472
9430
  aws: {
8473
9431
  defaultTags: {
9432
+ // SST contributes sst:app and sst:stage. Keep the provider baseline
9433
+ // at eight tags so generated S3 asset objects remain within AWS's
9434
+ // hard ten-tag object limit. Richer performance tags stay explicit.
8474
9435
  tags: {
8475
9436
  'ebk:cost-allocation': 'foundation',
8476
9437
  'ebk:cost-scope': '${name}/foundation/' + stage,
8477
9438
  'ebk:framework-version': '${frameworkVersion}',
8478
9439
  'ebk:managed-by': 'empire-builder-kit',
8479
9440
  '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
9441
  'ebk:slice': 'foundation',
8485
9442
  'ebk:stage': stage,
8486
9443
  'ebk:workspace': '${name}',
@@ -8524,6 +9481,16 @@ export default $config({
8524
9481
  - 'packages/*/services/*'
8525
9482
 
8526
9483
  autoInstallPeers: true
9484
+ blockExoticSubdeps: true
9485
+ enableGlobalVirtualStore: false
9486
+ ignorePnpmfile: true
9487
+ pnpmfile: []
9488
+ minimumReleaseAge: 1440
9489
+ minimumReleaseAgeExclude:
9490
+ - '@empire-builder-kit/*'
9491
+ minimumReleaseAgeIgnoreMissingTime: false
9492
+ minimumReleaseAgeStrict: true
9493
+ verifyDepsBeforeRun: error
8527
9494
  storeDir: .ebk/cache/pnpm-store
8528
9495
  supportedArchitectures:
8529
9496
  os:
@@ -9393,18 +10360,22 @@ if (eventName === 'pull_request' || eventName === 'push') {
9393
10360
  }
9394
10361
  if (eventName === 'pull_request') {
9395
10362
  const baseRef = process.env.GITHUB_BASE_REF;
10363
+ const headRepository = process.env.EBK_HEAD_REPOSITORY;
9396
10364
  const headRef = process.env.GITHUB_HEAD_REF;
9397
- if (!baseRef || !headRef) {
9398
- throw new Error('Pull-request branch metadata is missing.');
10365
+ const repository = process.env.GITHUB_REPOSITORY;
10366
+ if (!baseRef || !headRepository || !headRef || !repository) {
10367
+ throw new Error('Pull-request branch and repository metadata is missing.');
9399
10368
  }
9400
10369
  Object.assign(request, {
9401
10370
  baseIsAncestorOfHead: isAncestor('refs/remotes/origin/' + baseRef, 'HEAD'),
9402
10371
  baseRef,
10372
+ headRepository,
9403
10373
  headRef,
9404
10374
  mainIsAncestorOfDevelop: isAncestor(
9405
10375
  'refs/remotes/origin/main',
9406
10376
  'refs/remotes/origin/develop',
9407
10377
  ),
10378
+ repository,
9408
10379
  });
9409
10380
  } else if (eventName === 'push') {
9410
10381
  Object.assign(request, {