@empire-builder-kit/nx 1.0.0-rc.13 → 1.0.0-rc.17

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 (215) hide show
  1. package/executors.json +10 -0
  2. package/package.json +3 -3
  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 +53 -8
  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/deployment-plan/deployment-plan.js +5 -0
  13. package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
  14. package/src/executors/foundation-preflight/foundation-preflight.d.ts +2 -0
  15. package/src/executors/foundation-preflight/foundation-preflight.js +6 -2
  16. package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -1
  17. package/src/executors/foundation-preflight/schema.json +10 -0
  18. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.d.ts +2 -0
  19. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js +2 -1
  20. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js.map +1 -1
  21. package/src/executors/foundation-profile-preflight/schema.json +19 -3
  22. package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
  23. package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
  24. package/src/executors/infrastructure-plan/infrastructure-plan.d.ts +229 -5
  25. package/src/executors/infrastructure-plan/infrastructure-plan.js +2298 -175
  26. package/src/executors/infrastructure-plan/infrastructure-plan.js.map +1 -1
  27. package/src/executors/infrastructure-plan/schema.json +16 -0
  28. package/src/executors/local-deploy/fixtures/local-deploy-state-machine.mjs +53 -0
  29. package/src/executors/local-deploy/local-approval.d.ts +23 -0
  30. package/src/executors/local-deploy/local-approval.js +43 -0
  31. package/src/executors/local-deploy/local-approval.js.map +1 -0
  32. package/src/executors/local-deploy/local-deploy.d.ts +69 -0
  33. package/src/executors/local-deploy/local-deploy.js +911 -0
  34. package/src/executors/local-deploy/local-deploy.js.map +1 -0
  35. package/src/executors/local-deploy/schema.json +42 -0
  36. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.d.ts +3 -0
  37. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js +94 -0
  38. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js.map +1 -0
  39. package/src/executors/router-waf-rehearsal/schema.json +9 -0
  40. package/src/executors/secret-metadata/schema.json +5 -0
  41. package/src/executors/secret-metadata/secret-metadata.d.ts +12 -1
  42. package/src/executors/secret-metadata/secret-metadata.js +68 -16
  43. package/src/executors/secret-metadata/secret-metadata.js.map +1 -1
  44. package/src/executors/sst-lifecycle/logging-preflight.d.ts +5 -0
  45. package/src/executors/sst-lifecycle/logging-preflight.js +23 -13
  46. package/src/executors/sst-lifecycle/logging-preflight.js.map +1 -1
  47. package/src/executors/sst-lifecycle/run-sst-command.d.ts +8 -1
  48. package/src/executors/sst-lifecycle/run-sst-command.js +144 -12
  49. package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
  50. package/src/executors/sst-lifecycle/schema.json +13 -1
  51. package/src/executors/sst-lifecycle/sst-environment-source.d.ts +25 -0
  52. package/src/executors/sst-lifecycle/sst-environment-source.js +213 -0
  53. package/src/executors/sst-lifecycle/sst-environment-source.js.map +1 -0
  54. package/src/foundation/index.d.ts +2 -0
  55. package/src/foundation/index.js +2 -0
  56. package/src/foundation/index.js.map +1 -1
  57. package/src/foundation/logging-preflight.js +1 -1
  58. package/src/foundation/logging-preflight.js.map +1 -1
  59. package/src/foundation/logging.d.ts +3 -1
  60. package/src/foundation/logging.js +1 -1
  61. package/src/foundation/logging.js.map +1 -1
  62. package/src/foundation/partial-state.d.ts +10 -0
  63. package/src/foundation/partial-state.js +21 -0
  64. package/src/foundation/partial-state.js.map +1 -0
  65. package/src/foundation/router-waf.d.ts +98 -0
  66. package/src/foundation/router-waf.js +401 -0
  67. package/src/foundation/router-waf.js.map +1 -0
  68. package/src/generators/function/function.js +2 -1
  69. package/src/generators/function/function.js.map +1 -1
  70. package/src/generators/preset/generator.js +23 -3
  71. package/src/generators/preset/generator.js.map +1 -1
  72. package/src/generators/preset/schema.d.ts +7 -0
  73. package/src/generators/preset/schema.json +47 -0
  74. package/src/generators/preset/workspace-files-ci-acceptance.js +176 -36
  75. package/src/generators/preset/workspace-files-ci-acceptance.js.map +1 -1
  76. package/src/generators/preset/workspace-files-developer-agent.d.ts +2 -0
  77. package/src/generators/preset/workspace-files-developer-agent.js +25 -11
  78. package/src/generators/preset/workspace-files-developer-agent.js.map +1 -1
  79. package/src/generators/preset/workspace-files-foundation-workload-roles.js +5 -2
  80. package/src/generators/preset/workspace-files-foundation-workload-roles.js.map +1 -1
  81. package/src/generators/preset/workspace-files-workload-role-baseline.js +254 -42
  82. package/src/generators/preset/workspace-files-workload-role-baseline.js.map +1 -1
  83. package/src/generators/preset/workspace-files.d.ts +5 -1
  84. package/src/generators/preset/workspace-files.js +788 -49
  85. package/src/generators/preset/workspace-files.js.map +1 -1
  86. package/src/generators/repository-policy/repository-policy.js +65 -3
  87. package/src/generators/repository-policy/repository-policy.js.map +1 -1
  88. package/src/generators/repository-policy/schema.d.ts +6 -0
  89. package/src/generators/repository-policy/schema.json +46 -0
  90. package/src/generators/slice/built-in-plan-api-files.js +2 -1
  91. package/src/generators/slice/built-in-plan-api-files.js.map +1 -1
  92. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +18 -0
  93. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js.map +1 -1
  94. package/src/generators/slice/built-in-plan-data-files.js +16 -5
  95. package/src/generators/slice/built-in-plan-data-files.js.map +1 -1
  96. package/src/generators/slice/built-in-plan-data-retention-job-files.js +6 -3
  97. package/src/generators/slice/built-in-plan-data-retention-job-files.js.map +1 -1
  98. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +21 -7
  99. package/src/generators/slice/built-in-plan-database-lifecycle-files.js.map +1 -1
  100. package/src/generators/slice/built-in-plan-foundation-files.js +2 -3
  101. package/src/generators/slice/built-in-plan-foundation-files.js.map +1 -1
  102. package/src/generators/slice/built-in-plan-infrastructure-files.js +359 -27
  103. package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -1
  104. package/src/generators/slice/built-in-plan-infrastructure-safety-files.js +67 -11
  105. package/src/generators/slice/built-in-plan-infrastructure-safety-files.js.map +1 -1
  106. package/src/generators/slice/built-in-plan-metadata-files.js +1 -0
  107. package/src/generators/slice/built-in-plan-metadata-files.js.map +1 -1
  108. package/src/generators/slice/built-in-plan-operations-files.js +442 -5
  109. package/src/generators/slice/built-in-plan-operations-files.js.map +1 -1
  110. package/src/generators/slice/built-in-plan-performance-files.d.ts +2 -1
  111. package/src/generators/slice/built-in-plan-performance-files.js +34 -47
  112. package/src/generators/slice/built-in-plan-performance-files.js.map +1 -1
  113. package/src/generators/slice/built-in-plan-project-files.js +14 -2
  114. package/src/generators/slice/built-in-plan-project-files.js.map +1 -1
  115. package/src/generators/slice/built-in-plan-release-files.js +119 -30
  116. package/src/generators/slice/built-in-plan-release-files.js.map +1 -1
  117. package/src/generators/slice/built-in-plan-web-files.js +13 -0
  118. package/src/generators/slice/built-in-plan-web-files.js.map +1 -1
  119. package/src/generators/slice/built-in-plan-workload-role-files.js +5 -2
  120. package/src/generators/slice/built-in-plan-workload-role-files.js.map +1 -1
  121. package/src/generators/slice/built-in-plan.d.ts +2 -1
  122. package/src/generators/slice/built-in-plan.js +8 -5
  123. package/src/generators/slice/built-in-plan.js.map +1 -1
  124. package/src/generators/slice/slice.js +11 -3
  125. package/src/generators/slice/slice.js.map +1 -1
  126. package/src/generators/upgrade/upgrade.js +35 -0
  127. package/src/generators/upgrade/upgrade.js.map +1 -1
  128. package/src/generators/workload/workload.js +2 -2
  129. package/src/generators/workload/workload.js.map +1 -1
  130. package/src/git-policy/github-bootstrap.d.ts +3 -1
  131. package/src/git-policy/github-bootstrap.js +22 -5
  132. package/src/git-policy/github-bootstrap.js.map +1 -1
  133. package/src/infrastructure-change/local-capability.d.ts +73 -0
  134. package/src/infrastructure-change/local-capability.js +114 -0
  135. package/src/infrastructure-change/local-capability.js.map +1 -0
  136. package/src/infrastructure-change/model.d.ts +25 -4
  137. package/src/infrastructure-change/model.js +1 -0
  138. package/src/infrastructure-change/model.js.map +1 -1
  139. package/src/infrastructure-change/normalize.d.ts +4 -0
  140. package/src/infrastructure-change/normalize.js +47 -7
  141. package/src/infrastructure-change/normalize.js.map +1 -1
  142. package/src/infrastructure-change/plan.d.ts +30 -3
  143. package/src/infrastructure-change/plan.js +323 -13
  144. package/src/infrastructure-change/plan.js.map +1 -1
  145. package/src/infrastructure-change/risk.d.ts +3 -1
  146. package/src/infrastructure-change/risk.js +96 -16
  147. package/src/infrastructure-change/risk.js.map +1 -1
  148. package/src/local-dev/private-state.d.ts +2 -0
  149. package/src/local-dev/private-state.js +3 -1
  150. package/src/local-dev/private-state.js.map +1 -1
  151. package/src/performance/cost-controls.d.ts +5 -3
  152. package/src/performance/cost-controls.js +30 -24
  153. package/src/performance/cost-controls.js.map +1 -1
  154. package/src/performance/model.d.ts +1 -1
  155. package/src/performance/model.js +0 -2
  156. package/src/performance/model.js.map +1 -1
  157. package/src/performance/test-fixtures.js +0 -2
  158. package/src/performance/test-fixtures.js.map +1 -1
  159. package/src/release/source-authority.d.ts +1 -0
  160. package/src/release/source-authority.js +136 -9
  161. package/src/release/source-authority.js.map +1 -1
  162. package/src/repository-policy/deployment-plan.d.ts +4 -1
  163. package/src/repository-policy/deployment-plan.js +8 -0
  164. package/src/repository-policy/deployment-plan.js.map +1 -1
  165. package/src/repository-policy/foundation-profile.js +1 -2
  166. package/src/repository-policy/foundation-profile.js.map +1 -1
  167. package/src/repository-policy/policy.d.ts +25 -1
  168. package/src/repository-policy/policy.js +145 -4
  169. package/src/repository-policy/policy.js.map +1 -1
  170. package/src/schemas/performance-policy-v1.json +0 -2
  171. package/src/schemas/repository-policy-v1.json +41 -0
  172. package/src/secrets/operator.js +22 -4
  173. package/src/secrets/operator.js.map +1 -1
  174. package/src/secrets/verification.js +9 -1
  175. package/src/secrets/verification.js.map +1 -1
  176. package/src/sst-environment-source-format.d.ts +3 -0
  177. package/src/sst-environment-source-format.js +179 -0
  178. package/src/sst-environment-source-format.js.map +1 -0
  179. package/src/supply-chain/blueprint-verification-authority.js +55 -3
  180. package/src/supply-chain/blueprint-verification-authority.js.map +1 -1
  181. package/src/supply-chain/generated-command.js +1 -0
  182. package/src/supply-chain/generated-command.js.map +1 -1
  183. package/src/supply-chain/generated-container-release-command.js +187 -38
  184. package/src/supply-chain/generated-container-release-command.js.map +1 -1
  185. package/src/supply-chain/generated-ecr-collector-command.js +12 -2
  186. package/src/supply-chain/generated-ecr-collector-command.js.map +1 -1
  187. package/src/supply-chain/generated-scanner-command.js +1 -0
  188. package/src/supply-chain/generated-scanner-command.js.map +1 -1
  189. package/src/supply-chain/sbom.d.ts +1 -1
  190. package/src/supply-chain/sbom.js +30 -5
  191. package/src/supply-chain/sbom.js.map +1 -1
  192. package/src/workload-roles/account-baseline.d.ts +11 -0
  193. package/src/workload-roles/account-baseline.js +253 -16
  194. package/src/workload-roles/account-baseline.js.map +1 -1
  195. package/src/workload-roles/authority.d.ts +3 -1
  196. package/src/workload-roles/authority.js +48 -2
  197. package/src/workload-roles/authority.js.map +1 -1
  198. package/src/workload-roles/aws-s3-state.js +27 -0
  199. package/src/workload-roles/aws-s3-state.js.map +1 -1
  200. package/src/workload-roles/boundary.d.ts +2 -1
  201. package/src/workload-roles/boundary.js +9 -2
  202. package/src/workload-roles/boundary.js.map +1 -1
  203. package/src/workload-roles/handler.js +52 -24
  204. package/src/workload-roles/handler.js.map +1 -1
  205. package/src/workload-roles/injection.js +1 -1
  206. package/src/workload-roles/injection.js.map +1 -1
  207. package/src/workload-roles/provisioner.js +12 -4
  208. package/src/workload-roles/provisioner.js.map +1 -1
  209. package/src/workload-roles/registration.d.ts +1 -0
  210. package/src/workload-roles/registration.js +5 -0
  211. package/src/workload-roles/registration.js.map +1 -1
  212. package/src/workload-roles/stage-boundary.js +1 -1
  213. package/src/workload-roles/stage-boundary.js.map +1 -1
  214. package/src/workload-roles/standard-slice.js +1 -0
  215. package/src/workload-roles/standard-slice.js.map +1 -1
@@ -709,6 +709,19 @@ function planRoleAuditStep(branch, mode = 'produce') {
709
709
  ],
710
710
  Resource: '*',
711
711
  },
712
+ {
713
+ Sid: 'ObserveStageSecretVersionIdentity',
714
+ Effect: 'Allow',
715
+ Action: 'secretsmanager:ListSecretVersionIds',
716
+ Resource:
717
+ 'arn:aws:secretsmanager:' + process.env.AWS_REGION + ':' +
718
+ account + ':secret:*',
719
+ Condition: {
720
+ StringEquals: {
721
+ 'aws:ResourceTag/ebk:stage': stateStage,
722
+ },
723
+ },
724
+ },
712
725
  {
713
726
  Sid: 'ValidatePlannedIamPolicies',
714
727
  Effect: 'Allow',
@@ -969,6 +982,7 @@ exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
969
982
  '/targets/blueprint-trust-check-protected',
970
983
  '/targets/developer-context',
971
984
  '/targets/deployment-plan',
985
+ '/targets/deploy-local',
972
986
  '/targets/dev',
973
987
  '/targets/dev-doctor',
974
988
  '/targets/dev-setup',
@@ -989,6 +1003,10 @@ const FOUNDATION_SEEDED_PATHS = new Set([
989
1003
  'docs/costs.md',
990
1004
  'docs/runbooks.md',
991
1005
  'docs/security.md',
1006
+ 'performance/account-cost-policy.json',
1007
+ 'security/router-waf-blocking-rehearsal.json',
1008
+ 'security/router-waf-live-evidence.json',
1009
+ 'security/router-waf.json',
992
1010
  ]);
993
1011
  function createFoundationOwnership(files, frameworkVersion) {
994
1012
  const paths = Object.keys(files)
@@ -1051,7 +1069,16 @@ function createWorkspaceOwnership(files, frameworkVersion) {
1051
1069
  }
1052
1070
  function renderWorkspaceFiles(options) {
1053
1071
  const { frameworkVersion, name } = options;
1072
+ const deploymentProfile = options.deploymentProfile ?? 'starter';
1073
+ const initialRepositoryPolicy = (0, index_js_4.createIncubationRepositoryPolicy)(deploymentProfile);
1074
+ initialRepositoryPolicy.deploymentAccounts = options.deploymentAccounts ?? {
1075
+ development: null,
1076
+ production: null,
1077
+ staging: null,
1078
+ };
1079
+ initialRepositoryPolicy.deploymentRegion = options.deploymentRegion ?? null;
1054
1080
  const developerAgentFilePlan = (0, workspace_files_developer_agent_js_1.renderDeveloperAgentWorkspaceFilePlan)({
1081
+ deploymentProfile,
1055
1082
  frameworkVersion,
1056
1083
  name,
1057
1084
  });
@@ -1064,6 +1091,9 @@ function renderWorkspaceFiles(options) {
1064
1091
  const uploadArtifactAction = (0, index_js_3.pinnedAction)(index_js_3.GITHUB_ACTION_PINS.uploadArtifact);
1065
1092
  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'";
1066
1093
  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'";
1094
+ const protectedPush = "github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')";
1095
+ const protectedDeploymentRouteJobName = 'protected deployment route';
1096
+ const stagingApplyJobPrefix = 'apply staging plan ';
1067
1097
  const files = {
1068
1098
  ...(0, workspace_files_ci_acceptance_js_1.renderWorkspaceCiAcceptanceFiles)(),
1069
1099
  '.node-version': `${tool_versions_js_1.WORKSPACE_NODE_VERSION}\n`,
@@ -1095,24 +1125,26 @@ env:
1095
1125
  jobs:
1096
1126
  acceptance:
1097
1127
  name: accepted merge evidence
1098
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
1128
+ if: ${protectedPush}
1099
1129
  runs-on: ubuntu-latest
1100
1130
  outputs:
1131
+ history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1101
1132
  reused: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1133
+ route: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' && steps.resolve.outputs.route || '' }}
1102
1134
  steps:
1103
1135
  - name: Check out immutable merged candidate
1104
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
1136
+ if: ${protectedPush}
1105
1137
  uses: ${checkoutAction}
1106
1138
  with:
1107
1139
  fetch-depth: 0
1108
1140
  ref: \${{ github.sha }}
1109
- - name: Resolve exact protected-promotion acceptance
1110
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
1141
+ - name: Resolve exact protected-route acceptance
1142
+ if: ${protectedPush}
1111
1143
  id: resolve
1112
1144
  env:
1113
1145
  GH_TOKEN: \${{ github.token }}
1114
1146
  run: node tools/ci/workspace-acceptance.mjs resolve
1115
- - name: Restore exact protected-promotion acceptance
1147
+ - name: Restore exact protected-route acceptance
1116
1148
  if: steps.resolve.outputs.reused == 'true'
1117
1149
  id: restore
1118
1150
  continue-on-error: true
@@ -1132,13 +1164,17 @@ jobs:
1132
1164
  id: verify
1133
1165
  continue-on-error: true
1134
1166
  env:
1167
+ EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
1135
1168
  EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
1169
+ EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
1136
1170
  EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
1137
1171
  EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
1138
1172
  run: >-
1139
1173
  node tools/ci/workspace-acceptance.mjs verify
1140
1174
  .ebk/evidence/reused-acceptance/manifest.json
1141
1175
  "$EBK_ACCEPTANCE_HEAD"
1176
+ "$EBK_ACCEPTANCE_BASE"
1177
+ "$EBK_ACCEPTANCE_ROUTE"
1142
1178
  "$EBK_ACCEPTANCE_RUN_ID"
1143
1179
  "$EBK_ACCEPTANCE_RUN_ATTEMPT"
1144
1180
 
@@ -1198,12 +1234,14 @@ jobs:
1198
1234
  run: pnpm nx run-many -t verify --parallel=3
1199
1235
 
1200
1236
  seal-acceptance:
1201
- name: seal protected promotion acceptance
1237
+ name: seal protected route acceptance
1202
1238
  needs: [policy, verify]
1203
1239
  if: >-
1204
1240
  always() &&
1241
+ needs.policy.result == 'success' &&
1242
+ needs.verify.result == 'success' &&
1205
1243
  github.event_name == 'pull_request' &&
1206
- ${protectedDevelopPromotion}
1244
+ (${protectedDevelopPromotion} || ${protectedMainSync})
1207
1245
  runs-on: ubuntu-latest
1208
1246
  steps:
1209
1247
  - name: Check out immutable accepted candidate
@@ -1211,13 +1249,16 @@ jobs:
1211
1249
  with:
1212
1250
  fetch-depth: 0
1213
1251
  ref: \${{ env.EBK_CI_CANDIDATE_SHA }}
1214
- - name: Seal exact protected-promotion acceptance
1252
+ - name: Seal exact protected-route acceptance
1253
+ id: seal
1215
1254
  env:
1255
+ EBK_BASE_COMMIT: \${{ github.event.pull_request.base.sha }}
1216
1256
  EBK_HEAD_REPOSITORY: \${{ github.event.pull_request.head.repo.full_name }}
1217
1257
  run: >-
1218
1258
  node tools/ci/workspace-acceptance.mjs seal
1219
1259
  .ebk/evidence/ci/workspace-acceptance/manifest.json
1220
- - name: Retain exact protected-promotion acceptance
1260
+ - name: Retain exact protected-route acceptance
1261
+ if: steps.seal.outputs.sealed == 'true'
1221
1262
  uses: ${uploadArtifactAction}
1222
1263
  with:
1223
1264
  name: workspace-acceptance-\${{ env.EBK_CI_CANDIDATE_SHA }}-\${{ github.run_id }}-\${{ github.run_attempt }}
@@ -1232,6 +1273,7 @@ jobs:
1232
1273
  if: >-
1233
1274
  always() &&
1234
1275
  github.event_name == 'push' &&
1276
+ needs.acceptance.outputs.history_only != 'true' &&
1235
1277
  (needs.acceptance.outputs.reused == 'true' ||
1236
1278
  (needs.policy.result == 'success' && needs.verify.result == 'success'))
1237
1279
  runs-on: ubuntu-latest
@@ -1298,7 +1340,97 @@ permissions:
1298
1340
  contents: read
1299
1341
 
1300
1342
  jobs:
1343
+ history-classification:
1344
+ name: classify protected history-only sync
1345
+ if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
1346
+ runs-on: ubuntu-latest
1347
+ permissions:
1348
+ actions: read
1349
+ contents: read
1350
+ pull-requests: read
1351
+ outputs:
1352
+ history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1353
+ steps:
1354
+ - name: Check out immutable protected candidate
1355
+ uses: ${checkoutAction}
1356
+ with:
1357
+ fetch-depth: 0
1358
+ ref: \${{ github.sha }}
1359
+ - uses: ${setupNodeAction}
1360
+ with:
1361
+ node-version: ${tool_versions_js_1.WORKSPACE_NODE_VERSION}
1362
+ - name: Resolve exact protected-route acceptance
1363
+ id: resolve
1364
+ env:
1365
+ GH_TOKEN: \${{ github.token }}
1366
+ run: node tools/ci/workspace-acceptance.mjs resolve
1367
+ - name: Restore exact protected-route acceptance
1368
+ if: steps.resolve.outputs.reused == 'true'
1369
+ id: restore
1370
+ continue-on-error: true
1371
+ env:
1372
+ EBK_ACCEPTANCE_ARTIFACT_DIGEST: \${{ steps.resolve.outputs.artifact_digest }}
1373
+ EBK_ACCEPTANCE_ARTIFACT_ID: \${{ steps.resolve.outputs.artifact_id }}
1374
+ EBK_ACCEPTANCE_ARTIFACT_SIZE: \${{ steps.resolve.outputs.artifact_size }}
1375
+ GH_TOKEN: \${{ github.token }}
1376
+ run: >-
1377
+ node tools/ci/workspace-acceptance.mjs download
1378
+ .ebk/evidence/reused-history-acceptance
1379
+ "$EBK_ACCEPTANCE_ARTIFACT_ID"
1380
+ "$EBK_ACCEPTANCE_ARTIFACT_DIGEST"
1381
+ "$EBK_ACCEPTANCE_ARTIFACT_SIZE"
1382
+ - name: Verify exact history-only acceptance
1383
+ if: steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success'
1384
+ id: verify
1385
+ continue-on-error: true
1386
+ env:
1387
+ EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
1388
+ EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
1389
+ EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
1390
+ EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
1391
+ EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
1392
+ run: >-
1393
+ node tools/ci/workspace-acceptance.mjs verify
1394
+ .ebk/evidence/reused-history-acceptance/manifest.json
1395
+ "$EBK_ACCEPTANCE_HEAD"
1396
+ "$EBK_ACCEPTANCE_BASE"
1397
+ "$EBK_ACCEPTANCE_ROUTE"
1398
+ "$EBK_ACCEPTANCE_RUN_ID"
1399
+ "$EBK_ACCEPTANCE_RUN_ATTEMPT"
1400
+
1401
+ deployment-mode:
1402
+ name: classify deployment profile
1403
+ runs-on: ubuntu-latest
1404
+ outputs:
1405
+ profile: \${{ steps.profile.outputs.profile }}
1406
+ steps:
1407
+ - name: Check out immutable deployment policy
1408
+ uses: ${checkoutAction}
1409
+ with:
1410
+ ref: \${{ github.sha }}
1411
+ - id: profile
1412
+ name: Read exact committed deployment profile
1413
+ run: |
1414
+ node - <<'NODE'
1415
+ const { readFileSync } = require('node:fs');
1416
+ const policy = JSON.parse(readFileSync('ebk.repository-policy.json', 'utf8'));
1417
+ const profiles = new Set(['starter', 'production-isolated', 'fully-isolated']);
1418
+ if (!profiles.has(policy.deploymentProfile)) {
1419
+ throw new Error('Committed deployment profile is absent or invalid.');
1420
+ }
1421
+ require('node:fs').appendFileSync(
1422
+ process.env.GITHUB_OUTPUT,
1423
+ 'profile=' + policy.deploymentProfile + '\\n',
1424
+ );
1425
+ NODE
1426
+
1301
1427
  route:
1428
+ name: ${protectedDeploymentRouteJobName}
1429
+ needs: [history-classification, deployment-mode]
1430
+ if: >-
1431
+ always() &&
1432
+ needs.history-classification.outputs.history_only != 'true' &&
1433
+ needs.deployment-mode.result == 'success'
1302
1434
  runs-on: ubuntu-latest
1303
1435
  steps:
1304
1436
  - name: Validate protected ref
@@ -1311,14 +1443,22 @@ jobs:
1311
1443
  esac
1312
1444
 
1313
1445
  plan-staging:
1314
- needs: route
1315
- if: github.ref == 'refs/heads/develop'
1446
+ needs: [route, deployment-mode]
1447
+ if: >-
1448
+ github.ref == 'refs/heads/develop' &&
1449
+ (github.event_name == 'workflow_dispatch' ||
1450
+ needs.deployment-mode.outputs.profile != 'starter')
1316
1451
  runs-on: ubuntu-latest
1317
1452
  outputs:
1453
+ deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
1454
+ deployment_region: \${{ steps.plan.outputs.deployment_region }}
1455
+ development_account_id: \${{ steps.plan.outputs.development_account_id }}
1318
1456
  foundation_project: \${{ steps.plan.outputs.foundation_project }}
1319
1457
  has_slices: \${{ steps.plan.outputs.has_slices }}
1320
1458
  ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
1459
+ production_account_id: \${{ steps.plan.outputs.production_account_id }}
1321
1460
  slice_projects: \${{ steps.plan.outputs.slice_projects }}
1461
+ staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
1322
1462
  steps:
1323
1463
  - uses: ${checkoutAction}
1324
1464
  with:
@@ -1529,16 +1669,21 @@ jobs:
1529
1669
  path: .ebk/evidence/staging-release-source/**
1530
1670
 
1531
1671
  plan-production:
1532
- needs: route
1672
+ needs: [route, deployment-mode]
1533
1673
  if: github.ref == 'refs/heads/main'
1534
1674
  runs-on: ubuntu-latest
1535
1675
  outputs:
1676
+ deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
1677
+ deployment_region: \${{ steps.plan.outputs.deployment_region }}
1678
+ development_account_id: \${{ steps.plan.outputs.development_account_id }}
1536
1679
  foundation_project: \${{ steps.plan.outputs.foundation_project }}
1537
1680
  foundation_profile: \${{ steps.plan.outputs.foundation_profile }}
1538
1681
  has_slices: \${{ steps.plan.outputs.has_slices }}
1539
1682
  ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
1540
1683
  production_approval_mode: \${{ steps.plan.outputs.production_approval_mode }}
1684
+ production_account_id: \${{ steps.plan.outputs.production_account_id }}
1541
1685
  slice_projects: \${{ steps.plan.outputs.slice_projects }}
1686
+ staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
1542
1687
  steps:
1543
1688
  - uses: ${checkoutAction}
1544
1689
  with:
@@ -1859,6 +2004,8 @@ jobs:
1859
2004
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1860
2005
  EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1861
2006
  EBK_STAGE: staging
2007
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2008
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
1862
2009
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
1863
2010
  steps:
1864
2011
  - uses: ${checkoutAction}
@@ -1874,6 +2021,10 @@ jobs:
1874
2021
  - run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1875
2022
  - name: Verify managed workspace integrity before AWS authority
1876
2023
  run: pnpm nx run ${name}:workspace-health
2024
+ - name: Bind staging account to committed deployment topology
2025
+ run: |
2026
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
2027
+ test "$AWS_REGION" = "$EBK_POLICY_REGION"
1877
2028
  ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${configureAwsAction}
1878
2029
  with:
1879
2030
  role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
@@ -1913,6 +2064,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
1913
2064
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1914
2065
  EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1915
2066
  EBK_STAGE: staging
2067
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2068
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
1916
2069
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
1917
2070
  steps:
1918
2071
  - uses: ${checkoutAction}
@@ -1928,6 +2081,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
1928
2081
  - run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1929
2082
  - name: Verify managed workspace integrity before AWS authority
1930
2083
  run: pnpm nx run ${name}:workspace-health
2084
+ - name: Bind staging account to committed deployment topology
2085
+ run: |
2086
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
2087
+ test "$AWS_REGION" = "$EBK_POLICY_REGION"
1931
2088
  ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Restore both native platform records
1932
2089
  uses: ${downloadArtifactAction}
1933
2090
  with:
@@ -1969,6 +2126,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
1969
2126
  AWS_DEPLOY_ROLE_ARN: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
1970
2127
  AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
1971
2128
  AWS_REGION: \${{ vars.AWS_REGION }}
2129
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
2130
+ EBK_POLICY_PRODUCTION_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
2131
+ EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
2132
+ EBK_POLICY_STAGING_ACCOUNT_ID: \${{ needs.plan-production.outputs.staging_account_id }}
1972
2133
  EBK_CANDIDATE_RUN_ATTEMPT: \${{ needs.release-source-production.outputs.candidate_run_attempt }}
1973
2134
  EBK_CANDIDATE_RUN_ID: \${{ needs.release-source-production.outputs.candidate_run_id }}
1974
2135
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-production.outputs.source_commit }}
@@ -2069,11 +2230,13 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
2069
2230
  cp "$source_sidecar" "$target"
2070
2231
  done
2071
2232
  - uses: ${configureAwsAction}
2233
+ if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
2072
2234
  with:
2073
2235
  role-to-assume: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
2074
2236
  role-session-name: ebk-staging-container-pull-\${{ github.run_id }}-\${{ github.run_attempt }}
2075
2237
  aws-region: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
2076
2238
  - name: Authenticate exact staging source registry
2239
+ if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
2077
2240
  env:
2078
2241
  EBK_STAGING_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
2079
2242
  EBK_STAGING_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
@@ -2092,10 +2255,27 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
2092
2255
  role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
2093
2256
  role-session-name: ebk-production-container-push-\${{ github.run_id }}-\${{ github.run_attempt }}
2094
2257
  aws-region: \${{ vars.AWS_REGION }}
2258
+ - name: Bind production authority to committed deployment topology
2259
+ run: |
2260
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
2261
+ { echo 'Production environment account differs from committed deployment topology'; exit 1; }
2262
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
2263
+ { echo 'Production environment Region differs from committed deployment topology'; exit 1; }
2264
+ - name: Authenticate shared-account staging source registry
2265
+ if: env.EBK_DEPLOYMENT_PROFILE == 'starter'
2266
+ run: |
2267
+ test "$EBK_POLICY_STAGING_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
2268
+ { echo 'Starter promotion requires one shared AWS account'; exit 1; }
2269
+ registry_suffix=.com
2270
+ case "$EBK_POLICY_REGION" in cn-*) registry_suffix=.com.cn ;; esac
2271
+ aws ecr get-login-password --region "$EBK_POLICY_REGION" |
2272
+ docker login --username AWS --password-stdin \
2273
+ "$EBK_POLICY_STAGING_ACCOUNT_ID.dkr.ecr.$EBK_POLICY_REGION.amazonaws$registry_suffix"
2095
2274
  - name: Promote exact staging indexes without rebuilding
2096
2275
  env:
2097
- EBK_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
2098
- EBK_STAGING_PROMOTION_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
2276
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
2277
+ 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 }}
2278
+ EBK_STAGING_PROMOTION_REGION: \${{ needs.plan-production.outputs.deployment_profile == 'starter' && needs.plan-production.outputs.deployment_region || vars.AWS_STAGING_PROMOTION_REGION }}
2099
2279
  run: |
2100
2280
  test "$(aws sts get-caller-identity --query Account --output text)" = "$AWS_EXPECTED_ACCOUNT_ID" ||
2101
2281
  { echo 'Production promotion role account differs from policy'; exit 1; }
@@ -2314,6 +2494,8 @@ ${planRoleAuditStep('main', 'verify')}
2314
2494
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
2315
2495
  EBK_SOURCE_COMMIT: \${{ github.sha }}
2316
2496
  EBK_STAGE: staging
2497
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2498
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
2317
2499
  FOUNDATION_PROJECT: \${{ needs.plan-staging.outputs.foundation_project }}
2318
2500
  ORDERED_PROJECTS: \${{ needs.plan-staging.outputs.ordered_projects }}
2319
2501
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
@@ -2421,7 +2603,7 @@ ${planRoleAuditStep('develop', 'verify')}
2421
2603
  packages/*/.ebk/evidence/infrastructure-plan-command.json
2422
2604
 
2423
2605
  deploy-staging:
2424
- name: apply staging plan \${{ needs.infrastructure-plan-staging.outputs.plan_digest || 'recovery-without-resource-plan' }}
2606
+ name: ${stagingApplyJobPrefix}\${{ needs.infrastructure-plan-staging.outputs.plan_digest || 'recovery-without-resource-plan' }}
2425
2607
  needs: [plan-staging, release-source-staging, container-assemble-staging, infrastructure-plan-staging]
2426
2608
  if: |
2427
2609
  always() &&
@@ -2500,6 +2682,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
2500
2682
  test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'staging AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
2501
2683
  test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'staging AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
2502
2684
  test -n "$AWS_REGION" || { echo 'staging AWS_REGION is required'; exit 1; }
2685
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
2686
+ { echo 'staging environment account differs from committed deployment topology'; exit 1; }
2687
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
2688
+ { echo 'staging environment Region differs from committed deployment topology'; exit 1; }
2503
2689
  test -n "$EBK_FLEET_ORIGIN" ||
2504
2690
  { echo 'staging environment EBK_FLEET_ORIGIN is required'; exit 1; }
2505
2691
  test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
@@ -3136,6 +3322,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
3136
3322
  EBK_APPROVED_MAIN_COMMIT: \${{ github.sha }}
3137
3323
  EBK_SOURCE_COMMIT: \${{ github.sha }}
3138
3324
  EBK_STAGE: production
3325
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
3326
+ EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
3139
3327
  EBK_VERIFIED_STAGING_RUN_ID: \${{ needs.release-source-production.outputs.verified_staging_run_id }}
3140
3328
  FOUNDATION_PROJECT: \${{ needs.plan-production.outputs.foundation_project }}
3141
3329
  ORDERED_PROJECTS: \${{ needs.plan-production.outputs.ordered_projects }}
@@ -3232,6 +3420,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
3232
3420
  test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'production AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
3233
3421
  test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'production AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
3234
3422
  test -n "$AWS_REGION" || { echo 'production AWS_REGION is required'; exit 1; }
3423
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
3424
+ { echo 'production environment account differs from committed deployment topology'; exit 1; }
3425
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
3426
+ { echo 'production environment Region differs from committed deployment topology'; exit 1; }
3235
3427
  test -n "$EBK_FLEET_ORIGIN" ||
3236
3428
  { echo 'production environment EBK_FLEET_ORIGIN is required'; exit 1; }
3237
3429
  test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
@@ -4898,13 +5090,51 @@ permissions:
4898
5090
  contents: read
4899
5091
 
4900
5092
  jobs:
5093
+ deployment-classification:
5094
+ name: classify exact staging deployment
5095
+ if: >-
5096
+ github.event_name == 'workflow_run' &&
5097
+ github.event.workflow_run.event == 'push' &&
5098
+ github.event.workflow_run.head_branch == 'develop' &&
5099
+ github.event.workflow_run.conclusion == 'success'
5100
+ permissions:
5101
+ actions: read
5102
+ contents: read
5103
+ outputs:
5104
+ deployed: \${{ steps.classify.outputs.deployed }}
5105
+ runs-on: ubuntu-latest
5106
+ steps:
5107
+ - id: classify
5108
+ name: Require one successful staging deployment job
5109
+ env:
5110
+ EBK_DEPLOYMENT_RUN_ID: \${{ github.event.workflow_run.id }}
5111
+ GH_TOKEN: \${{ github.token }}
5112
+ GH_REPO: \${{ github.repository }}
5113
+ run: |
5114
+ jobs="$(gh api \
5115
+ "/repos/$GH_REPO/actions/runs/$EBK_DEPLOYMENT_RUN_ID/jobs?filter=latest&per_page=100")"
5116
+ apply_count="$(printf '%s' "$jobs" | jq \
5117
+ '[.jobs[] | select(.name | startswith("${stagingApplyJobPrefix}")) | select(.conclusion == "success")] | length')"
5118
+ route_success_count="$(printf '%s' "$jobs" | jq \
5119
+ '[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "success")] | length')"
5120
+ route_skipped_count="$(printf '%s' "$jobs" | jq \
5121
+ '[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "skipped")] | length')"
5122
+ case "$apply_count:$route_success_count:$route_skipped_count" in
5123
+ 1:1:0) echo 'deployed=true' >> "$GITHUB_OUTPUT" ;;
5124
+ 0:0:1) echo 'deployed=false' >> "$GITHUB_OUTPUT" ;;
5125
+ *) echo 'Deploy route and staging application evidence disagree'; exit 1 ;;
5126
+ esac
5127
+
4901
5128
  plan:
5129
+ needs: deployment-classification
4902
5130
  if: >-
4903
- (github.event_name == 'workflow_run' &&
5131
+ always() &&
5132
+ ((github.event_name == 'workflow_run' &&
4904
5133
  github.event.workflow_run.conclusion == 'success' &&
4905
5134
  github.event.workflow_run.event == 'push' &&
4906
- github.event.workflow_run.head_branch == 'develop') ||
4907
- github.event_name == 'workflow_dispatch'
5135
+ github.event.workflow_run.head_branch == 'develop' &&
5136
+ needs.deployment-classification.outputs.deployed == 'true') ||
5137
+ github.event_name == 'workflow_dispatch')
4908
5138
  outputs:
4909
5139
  deployment_run_attempt: \${{ steps.identity.outputs.deployment_run_attempt }}
4910
5140
  deployment_run_id: \${{ steps.identity.outputs.deployment_run_id }}
@@ -5397,14 +5627,14 @@ jobs:
5397
5627
  test "$(node --version | cut -d. -f1)" = v22
5398
5628
  - name: Run exact deployed smoke
5399
5629
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5400
- run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
5630
+ run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
5401
5631
  - name: Verify reviewed frontend visual baseline
5402
5632
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5403
- run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:frontend-visual"
5633
+ run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:frontend-visual"
5404
5634
  - name: Verify deployed frontend security headers
5405
5635
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5406
5636
  run: |
5407
- node node_modules/nx/bin/nx.js run \
5637
+ node node_modules/nx/dist/bin/nx.js run \
5408
5638
  "$EBK_PROJECT:frontend-security-headers" -- \
5409
5639
  --url="$EBK_SMOKE_URL"
5410
5640
  - name: Bind exact alarm and data-integrity evidence
@@ -6851,11 +7081,11 @@ jobs:
6851
7081
  if [ "$EBK_ACTION" = apply ]; then args+=("--apply"); fi
6852
7082
  pnpm nx run "$EBK_PROJECT:secret-metadata" -- "\${args[@]}"
6853
7083
  `,
6854
- '.github/workflows/sync-back.yml': `name: Sync production history
7084
+ '.github/workflows/sync-back.yml': `name: Sync protected main history
6855
7085
 
6856
7086
  on:
6857
7087
  workflow_run:
6858
- workflows: [Deploy]
7088
+ workflows: [Verify]
6859
7089
  types: [completed]
6860
7090
 
6861
7091
  permissions:
@@ -6866,6 +7096,7 @@ jobs:
6866
7096
  sync-back:
6867
7097
  if: >-
6868
7098
  github.event.workflow_run.conclusion == 'success' &&
7099
+ github.event.workflow_run.event == 'push' &&
6869
7100
  github.event.workflow_run.head_branch == 'main'
6870
7101
  runs-on: ubuntu-latest
6871
7102
  steps:
@@ -6879,8 +7110,8 @@ jobs:
6879
7110
  gh pr create \
6880
7111
  --head main \
6881
7112
  --base develop \
6882
- --title 'chore: sync production history to develop' \
6883
- --body 'Managed EBK sync-back. Review and merge; this workflow never pushes or auto-merges.'
7113
+ --title 'chore(git): sync protected main to develop' \
7114
+ --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.'
6884
7115
  number=$(gh pr list --state open --head main --base develop --json number --jq '.[0].number')
6885
7116
  fi
6886
7117
  echo "Sync-back pull request #$number is ready for review."
@@ -7234,6 +7465,16 @@ use the direct installed \`.bin/ebk blueprint verification-review\` entry docume
7234
7465
  \`docs/blueprints.md\`.
7235
7466
 
7236
7467
  Workspace creation only writes source files. It does not create or change cloud resources.
7468
+
7469
+ New workspaces default to the one-account \`starter\` deployment profile. Configure the exact
7470
+ shared account and Region in \`ebk.repository-policy.json\`, authenticate through IAM Identity
7471
+ Center, add exact development and staging CIDRs to
7472
+ \`packages/foundation/security/router-waf.json\`, and use
7473
+ \`pnpm nx run ${name}:deploy-local -- --stage=development
7474
+ --profile=<profile> --expectedRoleArn=<role-arn>\`. Never invoke SST directly.
7475
+ For a brand-new stage with generated Product Slices, complete the documented workload-role
7476
+ Foundation publication and pass \`--phase=foundation-bootstrap\` once before publishing and
7477
+ deploying the complete stage.
7237
7478
  `,
7238
7479
  'docs/architecture.md': `# Workspace architecture
7239
7480
 
@@ -7250,6 +7491,11 @@ Foundation implementation files.
7250
7491
  Canonical consumer stages use the matching \`development\`, \`staging\`, or \`production\`
7251
7492
  Foundation. Framework-derived personal stages may use only the \`development\`
7252
7493
  Foundation; generic preview stages are unsupported.
7494
+
7495
+ The committed deployment profile changes account and local/GitHub authority only. Every
7496
+ canonical stage always owns a separate Router/domain, Aurora cluster and migration history,
7497
+ Cognito user pool, event bus, secret namespace, IAM boundary, logs, queues, ECR namespace, and
7498
+ stage-qualified SSM discovery contract. Discovery rejects a record for another canonical stage.
7253
7499
  `,
7254
7500
  'docs/git-workflow.md': `# Protected Git workflow
7255
7501
 
@@ -7270,6 +7516,9 @@ GitHub rulesets, environments, and AWS IAM are the authorization boundaries; loc
7270
7516
  only defense in depth.
7271
7517
 
7272
7518
  The \`staging\` environment accepts only \`develop\`; \`production\` accepts only \`main\`.
7519
+ These workflows exist for every deployment profile. The staging workflow is optional for
7520
+ \`starter\`, whose governed local Nx target may deploy development and staging. Production is
7521
+ GitHub-only for every profile; \`fully-isolated\` also requires GitHub for staging.
7273
7522
  The committed \`productionApproval.mode\` selects one of two production authorization
7274
7523
  contracts:
7275
7524
 
@@ -7292,7 +7541,9 @@ contracts:
7292
7541
  Both modes retain required checks, a non-author CODEOWNER approval, no ruleset bypass, the
7293
7542
  literal production environment, and its branch-scoped credentials. The base \`staging\` and
7294
7543
  \`production\` environments own deployment, smoke, and ECR evidence roles plus their
7295
- variables; production also owns the staging-registry promotion pull role. Dedicated
7544
+ variables; isolated profiles also keep the staging-registry promotion pull role in production.
7545
+ Starter promotion authenticates once to the shared account and copies the exact staging digest
7546
+ into a distinct production ECR repository. Dedicated
7296
7547
  \`infrastructure-plan-<stage>\` environments gate read-only role use. Separate
7297
7548
  \`async-recovery-<stage>\`, \`performance-aggregate-<stage>\`,
7298
7549
  \`performance-cost-<stage>\`, \`performance-load-<stage>\`, and
@@ -7441,18 +7692,35 @@ materializes, close and reopen the pull request as a human and then approve it.
7441
7692
  `,
7442
7693
  'docs/repository-operating-policy.md': `# Repository Operating Policy
7443
7694
 
7444
- \`ebk.repository-policy.json\` is the reviewed source of truth for production access,
7445
- production approval, per-Slice readiness, and the one shared production Foundation profile.
7695
+ \`ebk.repository-policy.json\` is the reviewed source of truth for deployment profile, exact
7696
+ stage account IDs, deployment Region, production access, production approval, per-Slice
7697
+ readiness, and the one shared production Foundation profile.
7446
7698
  New workspaces start with production disabled and explicit Team-compatible
7447
7699
  \`productionApproval.mode: "promotion-pr"\`. A missing approval mode or Slice entry fails
7448
7700
  closed; no GitHub capability is allowed to choose a mode implicitly.
7449
7701
 
7702
+ Deployment profiles are proportional to operator maturity:
7703
+
7704
+ - \`starter\` (default) requires one shared account, permits local development and staging
7705
+ through the governed root Nx target, and reserves production for protected GitHub deployment;
7706
+ - \`production-isolated\` requires one development/staging account and a distinct production
7707
+ account; only production is GitHub-only; and
7708
+ - \`fully-isolated\` requires three distinct accounts; staging and production are GitHub-only.
7709
+
7710
+ Equal account IDs never imply shared stage resources. Router, Aurora, Cognito, events, secrets,
7711
+ IAM, logs, queues, resource names, and SSM discovery remain stage-specific. Existing prerelease
7712
+ repositories migrate explicitly to \`fully-isolated\`; upgrades never infer or weaken topology.
7713
+ That profile-only migration retains the former authority model but stays undeployable until exact
7714
+ development, staging, and production account IDs plus \`deploymentRegion\` are committed.
7715
+
7450
7716
  The named repository modes are source-writing presets, not runtime state. Report a change
7451
7717
  first and inspect the complete current/next policy:
7452
7718
 
7453
7719
  \`\`\`bash
7454
7720
  pnpm nx g @empire-builder-kit/nx:repository-policy \\
7455
- --preset=controlled-production --slices=billing
7721
+ --deploymentProfile=starter --deploymentRegion=us-east-1 \\
7722
+ --developmentAccountId=111122223333 --stagingAccountId=111122223333 \\
7723
+ --productionAccountId=111122223333
7456
7724
  \`\`\`
7457
7725
 
7458
7726
  Apply the reviewed source change separately with \`--apply\`. This command never contacts AWS
@@ -7462,6 +7730,66 @@ Use \`--productionApprovalMode=promotion-pr|environment-reviewer\` for an explic
7462
7730
  transition. This changes committed policy only; a separate bootstrap report/apply reconciles
7463
7731
  remote GitHub settings.
7464
7732
 
7733
+ After applying exact topology, authenticate with temporary federated credentials for development
7734
+ or staging and invoke
7735
+ \`pnpm nx run ${name}:deploy-local -- --stage=<stage> --profile=<profile>
7736
+ --expectedRoleArn=<role-arn>\`. Production runs only through the protected main-branch GitHub
7737
+ workflow. Each admitted local invocation plans the complete current stage, displays the stage
7738
+ summary and first material project, and requires a real TTY response of
7739
+ \`approve:<action>:<stage>:<project>:<current-plan-digest>\`. A human or authorized agent may type
7740
+ that acknowledgement; it is active operator control, not a claim of independent human review.
7741
+ The target creates one private ten-minute capability for that project, rechecks the exact source,
7742
+ artifacts, account, Region, role, stage, policy, state, and lock without a second provider preview,
7743
+ consumes the capability once, and removes it on every exit path. Unattended scripts cannot approve.
7744
+ Every material deploy or refresh mutates at most the first material project and ends with a
7745
+ controlled non-success \`replan-required\` handoff, including the last project in the ordered stage.
7746
+ Rerun the complete stage, review the new current summary, and repeat until one invocation proves
7747
+ every project is semantically converged. Only that convergence invocation succeeds. A preview is
7748
+ recent review and audit evidence; it is not represented as a cryptographically replayable Pulumi
7749
+ transaction. A complete, unlocked deploy plan containing only provider reads with no changed
7750
+ properties, replacement, failure, or unmatched risk finding is retained as observation evidence
7751
+ and counts as semantically converged. The standalone project planner may still return its
7752
+ conservative non-success for that drift observation; the root local transaction accepts only a
7753
+ freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
7754
+ only when their logical ID and resource type match exactly. Any stale or ambiguous
7755
+ observation fails closed; a plan with desired changes remains material and requires review. One
7756
+ narrower case stays reviewable instead of becoming
7757
+ drift: when an upstream Foundation apply changes only both external framework-version tag
7758
+ projections (plus SST's already-bounded name hydration), the retained
7759
+ external identity must match one exact non-replacing provider read in the same ordinary plan.
7760
+ Missing read correlation, owned resources, other tag or property changes, incomplete tag pairs,
7761
+ and provider failures remain drift and fail closed. The local target requires the exact SST v5 account bootstrap normally
7762
+ created by the pinned \`ebk dev up\` path, then creates and attests only missing exact
7763
+ per-project/canonical-stage state passphrases before its formal read-only plans. This passphrase
7764
+ bootstrap is the only persistent pre-acknowledgement write; it creates no deployment authority
7765
+ and is reported in bounded local evidence. Clean immutable source, immediate per-project plans,
7766
+ migration and retention preflights, and secret-free result
7767
+ evidence remain mandatory. Direct SST is forbidden.
7768
+
7769
+ After an interrupted apply, use the same target with \`--action=refresh\` and the exact
7770
+ phase, stage, profile, and role. It retains the same plan-summary review boundary, admits only
7771
+ bounded drift-detected refresh evidence, and never turns refresh into an implicit deploy. When
7772
+ the interrupted write leaves a recorded provider output unusable by the ordinary desired preview,
7773
+ the plan stops without mutation. The non-writing refresh preview separates provider refresh
7774
+ records from its later desired preview and cannot replace failed ordinary desired authority;
7775
+ repair the recorded dependency or use the documented
7776
+ operator recovery path before planning again.
7777
+
7778
+ A stage-wide refresh still plans and authorizes every selected project. When one exact retained
7779
+ project plan proves an absent SST state that is unlocked, complete, empty, and drift-free, the
7780
+ root target records that project as a refresh no-op instead of invoking SST against a nonexistent
7781
+ stage. It also retains the pinned provider's exact SSM \`ebk:framework-version\` \`tagsAll\`
7782
+ normalization as a semantically inert observation after the same bounded shape
7783
+ repeats following refresh. Value changes, other tags, security findings, deletes, replacements,
7784
+ sensitive properties, and every other resource type remain material. Locked, incomplete, failed,
7785
+ or otherwise drifted plans are never skipped.
7786
+
7787
+ For a stage with no workload-role account baseline, publish the fixed Foundation-only candidate
7788
+ and invoke this root target with \`--phase=foundation-bootstrap\`. After Foundation outputs exist,
7789
+ publish the complete \`full-stage\` candidate and invoke the target without \`--phase\` (or with
7790
+ \`--phase=full-stage\`). The bootstrap phase selects exactly Foundation; it is not an arbitrary
7791
+ project filter.
7792
+
7465
7793
  Validation requires both an explicit production approval mode and an explicit Foundation
7466
7794
  profile. It does not infer omitted values. Use \`promotion-pr\` for a private GitHub Team
7467
7795
  repository. Use \`environment-reviewer\` only when the repository plan can enforce a separate
@@ -7475,10 +7803,16 @@ environment reviewer.
7475
7803
 
7476
7804
  Protected production deploy and refresh validate the complete Foundation/Slice set before AWS
7477
7805
  access. The Foundation is always ordered first. The set is derived from committed policy and
7478
- cannot be narrowed with an ad hoc project argument.
7479
-
7480
- \`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` is a prerelease candidate at
7481
- 0–4 ACU with a five-minute auto-pause and potential cold-resume latency. Both keep managed NAT,
7806
+ cannot be narrowed with an ad hoc project argument. The retained stage preview remains review
7807
+ evidence. Immediately before each project mutation the workflow computes one fresh plan against
7808
+ current state and applies it without a second redundant preview. Source, artifacts, configuration,
7809
+ policy, environment, and protected identities remain exact. Provider-only read/discard observations
7810
+ may appear or disappear, but any new delete, replacement, import, stateful or security change, IAM
7811
+ expansion, protection reduction, or unclassified resource stops for renewed approval. After an
7812
+ upstream mutation, every downstream project therefore receives a fresh independent risk decision.
7813
+
7814
+ \`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` keeps a smaller
7815
+ 0.5–4 ACU warm floor for starter production traffic. Both keep managed NAT,
7482
7816
  30-day backups, deletion protection, retained data, and the protected GitHub/OIDC/AWS path.
7483
7817
  Changing policy affects only later lifecycle requests; it never deploys or removes resources.
7484
7818
  Changing profile requires a separate reviewed Foundation deployment and retained operational
@@ -7549,7 +7883,7 @@ deploy or refresh before AWS access. Protected remove and unlock remain usable f
7549
7883
  There is no local or silent bypass in this prerelease contract; any future waiver mechanism
7550
7884
  must be committed, reviewable, and auditable.
7551
7885
  `,
7552
- 'ebk.repository-policy.json': json((0, index_js_4.createIncubationRepositoryPolicy)()),
7886
+ 'ebk.repository-policy.json': json(initialRepositoryPolicy),
7553
7887
  'nx.json': json({
7554
7888
  $schema: './node_modules/nx/schemas/nx-schema.json',
7555
7889
  defaultBase: 'develop',
@@ -7589,6 +7923,7 @@ must be committed, reviewable, and auditable.
7589
7923
  inputs: ['default', '^production'],
7590
7924
  },
7591
7925
  },
7926
+ analytics: false,
7592
7927
  neverConnectToCloud: true,
7593
7928
  useDaemonProcess: false,
7594
7929
  }),
@@ -7617,10 +7952,12 @@ must be committed, reviewable, and auditable.
7617
7952
  },
7618
7953
  devDependencies: {
7619
7954
  '@aws-sdk/client-iam': TOOL_VERSIONS.awsSdk,
7955
+ '@aws-sdk/client-rds': TOOL_VERSIONS.awsSdk,
7620
7956
  '@aws-sdk/client-s3': TOOL_VERSIONS.awsSdk,
7621
7957
  '@aws-sdk/client-secrets-manager': TOOL_VERSIONS.awsSdk,
7622
7958
  '@aws-sdk/client-ssm': TOOL_VERSIONS.awsSdk,
7623
7959
  '@aws-sdk/client-sts': TOOL_VERSIONS.awsSdk,
7960
+ '@aws-sdk/credential-providers': TOOL_VERSIONS.awsSdk,
7624
7961
  '@eslint/js': TOOL_VERSIONS.eslintJs,
7625
7962
  '@empire-builder-kit/nx': frameworkVersion,
7626
7963
  '@types/node': TOOL_VERSIONS.nodeTypes,
@@ -7704,6 +8041,13 @@ must be committed, reviewable, and auditable.
7704
8041
  executor: 'nx:run-commands',
7705
8042
  options: { command: 'node tools/install-ebk-hooks.mjs' },
7706
8043
  },
8044
+ 'deploy-local': {
8045
+ cache: false,
8046
+ executor: '@empire-builder-kit/nx:local-deploy',
8047
+ options: {
8048
+ stage: 'development',
8049
+ },
8050
+ },
7707
8051
  'supply-chain-check': {
7708
8052
  executor: 'nx:run-commands',
7709
8053
  options: {
@@ -7772,6 +8116,10 @@ Workspace creation does not deploy this project. Review \`docs/costs.md\`,
7772
8116
 
7773
8117
  Set \`EBK_ROOT_DOMAIN\` before deploying when Route 53 in the target account owns a public
7774
8118
  domain. Without it, the shared Router uses its functional provider URL.
8119
+
8120
+ Before deploying development or staging, add at least one exact operator/probe CIDR to
8121
+ \`security/router-waf.json\`. Production is public by default behind the same managed and
8122
+ rate-limiting baseline; select its allowlist mode explicitly when required.
7775
8123
  `,
7776
8124
  'packages/foundation/contracts/foundation.json': json({
7777
8125
  contractVersion: '1.0.0',
@@ -7831,8 +8179,8 @@ seeded decision record aligned with organization-specific budgets and alerts.
7831
8179
  - Development uses EC2 NAT, a development bastion, and Aurora scale-to-zero.
7832
8180
  - Staging uses EC2 NAT and Aurora scale-to-zero by default.
7833
8181
  - Production always uses managed NAT and retains protected data. Its explicit Repository
7834
- Operating Policy profile selects either the standard always-warm Aurora floor or the
7835
- prerelease cost-optimized auto-pause candidate.
8182
+ Operating Policy profile selects either the standard 2-ACU Aurora floor or the
8183
+ cost-optimized 0.5-ACU warm floor.
7836
8184
  - ECS, EventBridge, Cognito, SNS, SSM, and CloudWatch are primarily usage-priced; logs,
7837
8185
  metrics, alarms, and retained data still require active cost review.
7838
8186
 
@@ -7850,8 +8198,69 @@ Use Nx for every supported lifecycle action:
7850
8198
  - \`pnpm nx run foundation:unlock --stage=development\`
7851
8199
  - \`pnpm nx run foundation:remove --stage=development\`
7852
8200
 
7853
- Do not invoke SST lifecycle commands directly. Production deployment belongs in the trusted
7854
- CI path.
8201
+ Do not invoke SST lifecycle commands directly. The root \`deploy-local\` target is the governed
8202
+ local path for stages admitted by the committed deployment profile. Its fixed
8203
+ \`foundation-bootstrap\` phase is the only local partial-stage path and exists solely for the
8204
+ documented first workload-role publication; isolated protected stages remain on the trusted
8205
+ GitHub path.
8206
+
8207
+ Every local invocation plans the complete selected stage but mutates at most one material
8208
+ project. Type the exact current \`approve:<action>:<stage>:<project>:<plan-digest>\` challenge in a
8209
+ real TTY after reviewing the displayed summary. A human or authorized agent may operate that TTY;
8210
+ unattended input is refused. The acknowledgement creates one ten-minute, one-use, private
8211
+ capability bound to source, artifacts, policy, environment, credentials, and current state. A
8212
+ completed deploy or refresh returns the controlled non-success \`replan-required\`; rerun the
8213
+ complete stage, review the new current summary, and continue one project at a time. Only a
8214
+ semantically converged complete stage reports success. No upstream mutation can authorize a
8215
+ downstream project. A complete, unlocked deploy plan containing only provider reads with no
8216
+ changed properties, replacement, failure, or unmatched risk finding is retained as observation
8217
+ evidence and counts as semantically converged. The standalone project planner may still return
8218
+ its conservative non-success for that drift observation; the root local transaction accepts only
8219
+ a freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
8220
+ only when their logical ID and resource type match exactly. Any stale or ambiguous
8221
+ observation fails closed; a plan with desired changes remains material and requires review. A
8222
+ material downstream plan may also retain an upstream Foundation framework-version tag advance as
8223
+ observation only when both external tag
8224
+ projections, the retained identity, optional bounded SST name hydration, and one non-replacing
8225
+ provider read correlate exactly. Every other external property or uncorroborated read remains
8226
+ drift and blocks before the TTY challenge.
8227
+
8228
+ For an interrupted local apply, run the same root target with \`--action=refresh\`, review its
8229
+ current provider-refresh evidence, and type the displayed TTY challenge. Before mutation the
8230
+ executor writes and verifies a private state recovery point plus a secret-free digest/version
8231
+ manifest. The refresh preview validates its provider and later desired-event partitions; any
8232
+ ordinary preview failure or malformed post-refresh desired event remains blocked. Provider-only
8233
+ observation noise is retained without claiming a replayable preview. Because refresh changes
8234
+ recorded state rather than cloud resources, provider-output security and protection findings
8235
+ remain explicit review requirements under the drift decision. Refresh never continues into deploy
8236
+ or claims desired-state convergence: run the governed deploy target afterward and resolve every
8237
+ remaining source-versus-state change. Exclude the \`.private.json\` recovery point from archives.
8238
+
8239
+ The root refresh target includes every selected project in its exact reviewed plan set. It skips
8240
+ the SST child only when that project's retained plan proves an absent, unlocked, complete, empty,
8241
+ and drift-free state, or when the only retained difference is the pinned provider's exact repeated
8242
+ SSM \`ebk:framework-version\` \`tagsAll\` normalization. It records the project as an explicit refresh no-op
8243
+ in either case while retaining the observation. Value changes, other tags, security findings,
8244
+ deletes, replacements, sensitive
8245
+ properties, and every other resource type remain material; every other plan must execute or fail
8246
+ closed.
8247
+
8248
+ ## Router WAF
8249
+
8250
+ Each canonical stage owns one global-scope Web ACL attached to that stage's Foundation Router.
8251
+ The committed \`security/router-waf.json\` policy requires AWS Common Rule Set, Known Bad
8252
+ Inputs, Amazon IP Reputation, an IP rate limit, CloudWatch metrics, and sampled requests.
8253
+ Development and staging fail synthesis until their allowlists contain explicit CIDRs.
8254
+ Production is public by default and may be changed to an explicit allowlist.
8255
+
8256
+ For tuning, set a nonproduction stage to \`count\`, deploy and inspect sampled requests, then
8257
+ return staging to \`block\` and run the live allow/deny, managed-rule, rate-limit, metric, and
8258
+ sampled-request probes. Write their bounded, secret-free result to
8259
+ \`security/router-waf-live-evidence.json\`, then run
8260
+ \`pnpm nx run foundation:router-waf-rehearsal\`. The executor seals the exact policy digest
8261
+ into \`router-waf-blocking-rehearsal.json\`. Production synthesis rejects a missing, stale, or
8262
+ count-mode rehearsal. Full request logging is opt-in, blocked-only, retention-bounded, and
8263
+ redacts query strings plus credential-bearing headers.
7855
8264
 
7856
8265
  Foundation stages are canonical. \`ebk dev\` derives a Product Slice personal
7857
8266
  stage from the operating-system account and maps that consumer to the
@@ -7886,6 +8295,12 @@ connects as the Slice's no-password IAM-authenticated migrator user.
7886
8295
  stage-scoped SSM/secret path and least-privilege IAM.
7887
8296
  - Public domains are configured explicitly through \`EBK_ROOT_DOMAIN\`; generated source never
7888
8297
  contains credentials or customer domain assumptions.
8298
+ - Every stage's CloudFront Router has its own global Web ACL. The baseline always includes AWS
8299
+ Common Rule Set, Known Bad Inputs, Amazon IP Reputation, rate blocking, CloudWatch metrics,
8300
+ and sampled requests. Nonproduction is allowlisted by default. Production is public by
8301
+ default, but cannot deploy until the exact policy has passed a staging blocking rehearsal.
8302
+ - Full WAF request logging is disabled by default. Enabling it keeps blocked requests only,
8303
+ uses bounded retention, and redacts query strings, cookies, authorization, and API keys.
7889
8304
 
7890
8305
  The managed \`contracts/frontend-security-headers.json\` file owns the transport
7891
8306
  minimum: HTTPS in deployed environments, one-year HSTS there, and the common
@@ -7918,7 +8333,12 @@ declare const $app: {
7918
8333
  import {
7919
8334
  FOUNDATION_SSM_SCHEMA_VERSION,
7920
8335
  STANDARD_FOUNDATION_CAPABILITIES,
8336
+ createRouterWafRules,
8337
+ createRouterWafStagePlan,
8338
+ foundationDatabaseEndpointHost,
7921
8339
  foundationSsmPath,
8340
+ normalizeRouterWafPolicy,
8341
+ reconcileRouterWafBlockingRehearsal,
7922
8342
  type FoundationSsmKey,
7923
8343
  } from '@empire-builder-kit/nx/foundation';
7924
8344
  import {
@@ -7928,6 +8348,9 @@ import {
7928
8348
  } from '@empire-builder-kit/nx/repository-policy';
7929
8349
  import { createFoundationAccountCostControlPlan } from '@empire-builder-kit/nx/performance';
7930
8350
  import accountCostPolicy from '../performance/account-cost-policy.json';
8351
+ import routerWafBlockingRehearsal from '../security/router-waf-blocking-rehearsal.json';
8352
+ import routerWafLiveEvidence from '../security/router-waf-live-evidence.json';
8353
+ import routerWafPolicy from '../security/router-waf.json';
7931
8354
  import type { FoundationWorkloadRolePreflight } from './workload-roles/preflight.js';
7932
8355
 
7933
8356
  function identityDomainPrefix(stage: string, accountId: string): string {
@@ -7979,6 +8402,18 @@ export function foundation(
7979
8402
  )
7980
8403
  : undefined;
7981
8404
  const natMode = productionConfiguration?.contract.nat ?? 'ec2';
8405
+ const normalizedRouterWafPolicy = normalizeRouterWafPolicy(routerWafPolicy);
8406
+ const wafPlan = createRouterWafStagePlan({
8407
+ policy: normalizedRouterWafPolicy,
8408
+ rehearsal: isProduction
8409
+ ? reconcileRouterWafBlockingRehearsal({
8410
+ evidence: routerWafLiveEvidence,
8411
+ policy: normalizedRouterWafPolicy,
8412
+ receipt: routerWafBlockingRehearsal,
8413
+ })
8414
+ : null,
8415
+ stage: $app.stage,
8416
+ });
7982
8417
  const natBinding = workloadRoles?.bindings.nat;
7983
8418
  if (natMode === 'ec2' && natBinding) {
7984
8419
  $transform(
@@ -7988,6 +8423,9 @@ export function foundation(
7988
8423
  args.name = natBinding.instanceProfileName;
7989
8424
  args.role = natBinding.roleName;
7990
8425
  opts.import = natBinding.instanceProfileName;
8426
+ opts.ignoreChanges = [
8427
+ ...new Set([...(opts.ignoreChanges ?? []), 'tags', 'tagsAll']),
8428
+ ];
7991
8429
  },
7992
8430
  );
7993
8431
  }
@@ -8009,8 +8447,83 @@ export function foundation(
8009
8447
  aliases: ['*.' + rootDomain],
8010
8448
  dns: sst.aws.dns(),
8011
8449
  },
8012
- }
8450
+ }
8013
8451
  : {}),
8452
+ waf: {
8453
+ logging: wafPlan.logging,
8454
+ managedRules: {
8455
+ coreRuleSet: true,
8456
+ knownBadInputs: true,
8457
+ sqlInjection: false,
8458
+ },
8459
+ rateLimitPerIp: wafPlan.rateLimitPerIp,
8460
+ },
8461
+ transform: {
8462
+ cdn(args: any) {
8463
+ if (!args.webAclArn) {
8464
+ throw new Error(
8465
+ 'Foundation Router must attach its exact global Web ACL.',
8466
+ );
8467
+ }
8468
+ },
8469
+ waf(args: any, opts: any) {
8470
+ if (args.scope !== 'CLOUDFRONT') {
8471
+ throw new Error(
8472
+ 'Foundation Router Web ACL must use global CloudFront scope.',
8473
+ );
8474
+ }
8475
+ const ipSets: { ipv4Arn?: any; ipv6Arn?: any } = {};
8476
+ if (wafPlan.ipv4Cidrs.length > 0) {
8477
+ ipSets.ipv4Arn = new aws.wafv2.IpSet(
8478
+ 'FoundationRouterIpv4Allowlist',
8479
+ {
8480
+ addresses: wafPlan.ipv4Cidrs,
8481
+ ipAddressVersion: 'IPV4',
8482
+ name: 'foundation-' + wafPlan.stage + '-router-ipv4-allowlist',
8483
+ scope: 'CLOUDFRONT',
8484
+ tags: {
8485
+ 'ebk:policy-digest': wafPlan.policyDigest,
8486
+ 'ebk:stage': wafPlan.stage,
8487
+ },
8488
+ },
8489
+ { provider: opts.provider },
8490
+ ).arn;
8491
+ }
8492
+ if (wafPlan.ipv6Cidrs.length > 0) {
8493
+ ipSets.ipv6Arn = new aws.wafv2.IpSet(
8494
+ 'FoundationRouterIpv6Allowlist',
8495
+ {
8496
+ addresses: wafPlan.ipv6Cidrs,
8497
+ ipAddressVersion: 'IPV6',
8498
+ name: 'foundation-' + wafPlan.stage + '-router-ipv6-allowlist',
8499
+ scope: 'CLOUDFRONT',
8500
+ tags: {
8501
+ 'ebk:policy-digest': wafPlan.policyDigest,
8502
+ 'ebk:stage': wafPlan.stage,
8503
+ },
8504
+ },
8505
+ { provider: opts.provider },
8506
+ ).arn;
8507
+ }
8508
+ args.defaultAction = { allow: {} };
8509
+ args.rules = createRouterWafRules(wafPlan, ipSets);
8510
+ args.scope = 'CLOUDFRONT';
8511
+ args.tags = {
8512
+ ...(args.tags ?? {}),
8513
+ 'ebk:policy-digest': wafPlan.policyDigest,
8514
+ 'ebk:stage': wafPlan.stage,
8515
+ };
8516
+ args.visibilityConfig = {
8517
+ cloudwatchMetricsEnabled: true,
8518
+ metricName:
8519
+ 'Ebk' +
8520
+ wafPlan.stage[0].toUpperCase() +
8521
+ wafPlan.stage.slice(1) +
8522
+ 'FoundationRouterWaf',
8523
+ sampledRequestsEnabled: true,
8524
+ };
8525
+ },
8526
+ },
8014
8527
  });
8015
8528
  const database = new sst.aws.Aurora('FoundationDatabase', {
8016
8529
  // SQL bootstrap and migrations run inside the VPC. Keep the public HTTPS
@@ -8034,6 +8547,46 @@ export function foundation(
8034
8547
  },
8035
8548
  vpc,
8036
8549
  });
8550
+ const databaseClusterEndpoint = database.nodes.cluster.endpoint.apply(
8551
+ foundationDatabaseEndpointHost,
8552
+ );
8553
+ const databaseReaderEndpoint = database.nodes.cluster.readerEndpoint.apply(
8554
+ foundationDatabaseEndpointHost,
8555
+ );
8556
+ // SST v4.17.1 creates a top-level LinkRef on the next tick and its Aurora
8557
+ // implementation unconditionally splits cluster endpoints. During a
8558
+ // governed refresh after an interrupted first deploy, those provider
8559
+ // outputs are legitimately absent. Preserve the normal link contract while
8560
+ // keeping that read-only recovery program total; a real deploy resolves the
8561
+ // exact endpoints before any discovery resource is created.
8562
+ database.getSSTLink = () => ({
8563
+ include: [
8564
+ sst.aws.permission({
8565
+ actions: ['secretsmanager:GetSecretValue'],
8566
+ resources: [database.secretArn],
8567
+ }),
8568
+ sst.aws.permission({
8569
+ actions: [
8570
+ 'rds-data:BatchExecuteStatement',
8571
+ 'rds-data:BeginTransaction',
8572
+ 'rds-data:CommitTransaction',
8573
+ 'rds-data:ExecuteStatement',
8574
+ 'rds-data:RollbackTransaction',
8575
+ ],
8576
+ resources: [database.clusterArn],
8577
+ }),
8578
+ ],
8579
+ properties: {
8580
+ clusterArn: database.clusterArn,
8581
+ database: database.database,
8582
+ host: databaseClusterEndpoint,
8583
+ password: database.password,
8584
+ port: database.port,
8585
+ reader: databaseReaderEndpoint,
8586
+ secretArn: database.secretArn,
8587
+ username: database.username,
8588
+ },
8589
+ });
8037
8590
  const foundationAccountId = aws.getCallerIdentityOutput({}).accountId;
8038
8591
  const foundationPartition = aws.getPartitionOutput({});
8039
8592
  const foundationRegion = aws.getRegionOutput({});
@@ -8096,6 +8649,7 @@ export function foundation(
8096
8649
  },
8097
8650
  ],
8098
8651
  resources: [alertTopic.arn],
8652
+ sid: 'AllowBudgetAlerts',
8099
8653
  },
8100
8654
  {
8101
8655
  actions: ['sns:Publish'],
@@ -8109,6 +8663,7 @@ export function foundation(
8109
8663
  type: 'Service',
8110
8664
  }],
8111
8665
  resources: [alertTopic.arn],
8666
+ sid: 'AllowCostAnomalyAlerts',
8112
8667
  },
8113
8668
  {
8114
8669
  actions: ['sns:Publish'],
@@ -8131,6 +8686,7 @@ export function foundation(
8131
8686
  type: 'Service',
8132
8687
  }],
8133
8688
  resources: [alertTopic.arn],
8689
+ sid: 'AllowRdsEventAlerts',
8134
8690
  },
8135
8691
  {
8136
8692
  actions: ['sns:Publish'],
@@ -8155,6 +8711,7 @@ export function foundation(
8155
8711
  },
8156
8712
  ],
8157
8713
  resources: [alertTopic.arn],
8714
+ sid: 'AllowCloudWatchAlarmAlerts',
8158
8715
  },
8159
8716
  ],
8160
8717
  }).json,
@@ -8183,6 +8740,41 @@ export function foundation(
8183
8740
  { dependsOn: [alertTopicPolicy] },
8184
8741
  )
8185
8742
  : undefined;
8743
+ const accountCostAnomalyMonitor = isProduction
8744
+ ? new aws.costexplorer.AnomalyMonitor(
8745
+ 'FoundationAccountCostAnomalyMonitor',
8746
+ {
8747
+ monitorDimension: 'SERVICE',
8748
+ monitorType: 'DIMENSIONAL',
8749
+ name: '${name}-account-service-cost-anomalies',
8750
+ tags: {
8751
+ 'ebk:cost-policy-digest': accountCostControl.policyDigest,
8752
+ 'ebk:cost-scope': '${name}/account/production',
8753
+ 'ebk:owner': accountCostControl.owner,
8754
+ },
8755
+ },
8756
+ )
8757
+ : undefined;
8758
+ const accountCostAnomalySubscription =
8759
+ accountCostControl.anomaly && accountCostAnomalyMonitor
8760
+ ? new aws.costexplorer.AnomalySubscription(
8761
+ 'FoundationAccountCostAnomalySubscription',
8762
+ {
8763
+ frequency: accountCostControl.anomaly.subscription.frequency,
8764
+ monitorArnLists: [accountCostAnomalyMonitor.arn],
8765
+ name: accountCostControl.anomaly.subscription.name,
8766
+ subscribers: [{ address: alertTopic.arn, type: 'SNS' }],
8767
+ tags: {
8768
+ 'ebk:cost-policy-digest': accountCostControl.policyDigest,
8769
+ 'ebk:cost-scope': '${name}/account/production',
8770
+ 'ebk:owner': accountCostControl.owner,
8771
+ },
8772
+ thresholdExpression:
8773
+ accountCostControl.anomaly.subscription.thresholdExpression,
8774
+ },
8775
+ { dependsOn: [accountCostAnomalyMonitor, alertTopicPolicy] },
8776
+ )
8777
+ : undefined;
8186
8778
  const databaseEvents = isProduction
8187
8779
  ? new aws.rds.EventSubscription(
8188
8780
  'FoundationDatabaseEvents',
@@ -8285,12 +8877,10 @@ export function foundation(
8285
8877
  clusterId: cluster.id,
8286
8878
  contractVersion: '1.0.0',
8287
8879
  databaseClusterArn: database.clusterArn,
8288
- databaseClusterEndpoint: database.nodes.cluster.endpoint.apply(
8289
- (endpoint: string) => endpoint.split(':')[0],
8290
- ),
8880
+ databaseClusterEndpoint,
8291
8881
  databaseClusterId: database.id,
8292
8882
  databaseClusterResourceId: database.nodes.cluster.clusterResourceId,
8293
- databaseHost: database.host,
8883
+ databaseHost: databaseClusterEndpoint,
8294
8884
  databaseMasterSecretArn: database.secretArn,
8295
8885
  databasePort: database.port.apply(String),
8296
8886
  eventBusArn: eventBus.arn,
@@ -8312,6 +8902,7 @@ export function foundation(
8312
8902
  capabilities: STANDARD_FOUNDATION_CAPABILITIES,
8313
8903
  clusterId: cluster.id,
8314
8904
  contractVersion: '1.0.0',
8905
+ costAnomalySubscriptionArn: accountCostAnomalySubscription?.arn,
8315
8906
  databaseCapacityAlarmName: databaseCapacityAlarm.name,
8316
8907
  dashboardName: dashboard.dashboardName,
8317
8908
  databaseClusterId: database.id,
@@ -8343,13 +8934,49 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8343
8934
  actualNotificationThresholdPercent: 80,
8344
8935
  forecastNotificationThresholdPercent: 100,
8345
8936
  kind: 'ebk-foundation-account-cost-policy',
8346
- monthlyMaximumUsd: 1_000,
8937
+ monthlyMaximumUsd: deploymentProfile === 'starter' ? 250 : 1_000,
8347
8938
  owner: 'replace-with-owner',
8348
8939
  reviewAt: null,
8349
8940
  schemaVersion: 1,
8350
8941
  status: 'example',
8351
8942
  workspace: name,
8352
8943
  }),
8944
+ 'packages/foundation/security/router-waf.json': json({
8945
+ kind: 'ebk-router-waf-policy',
8946
+ logging: {
8947
+ enabled: false,
8948
+ retention: '1 week',
8949
+ },
8950
+ rateLimitPerIp: 2_000,
8951
+ schemaVersion: 1,
8952
+ stages: {
8953
+ development: {
8954
+ access: 'allowlist',
8955
+ allowedCidrs: [],
8956
+ mode: 'block',
8957
+ },
8958
+ production: {
8959
+ access: 'public',
8960
+ allowedCidrs: [],
8961
+ mode: 'block',
8962
+ },
8963
+ staging: {
8964
+ access: 'allowlist',
8965
+ allowedCidrs: [],
8966
+ mode: 'block',
8967
+ },
8968
+ },
8969
+ }),
8970
+ 'packages/foundation/security/router-waf-blocking-rehearsal.json': json({
8971
+ kind: 'ebk-router-waf-blocking-rehearsal',
8972
+ schemaVersion: 1,
8973
+ status: 'pending',
8974
+ }),
8975
+ 'packages/foundation/security/router-waf-live-evidence.json': json({
8976
+ kind: 'ebk-router-waf-live-evidence',
8977
+ schemaVersion: 1,
8978
+ status: 'pending',
8979
+ }),
8353
8980
  'packages/foundation/configuration/declarations.json': json({
8354
8981
  project: 'foundation',
8355
8982
  schemaVersion: 1,
@@ -8382,29 +9009,96 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8382
9009
  version: '4.17.1',
8383
9010
  },
8384
9011
  policy: {
9012
+ adoptedImports: [
9013
+ {
9014
+ authority: 'workload-role-account-baseline',
9015
+ logicalId: 'FoundationVpcNatInstanceProfile',
9016
+ privateIdentifierTemplate: 'ebk-{stage}-foundation-nat-profile',
9017
+ recoveryRunbookRef: 'docs/workload-role-account-baseline.md',
9018
+ removalBehavior: 'retain',
9019
+ resourceType: 'aws:iam/instanceProfile:InstanceProfile',
9020
+ },
9021
+ ],
8385
9022
  additiveLowRiskTypes: [
8386
9023
  'aws:cloudwatch/dashboard:Dashboard',
8387
9024
  'aws:cloudwatch/logGroup:LogGroup',
8388
9025
  'aws:cloudwatch/metricAlarm:MetricAlarm',
8389
9026
  ],
9027
+ additiveReviewedTypes: [
9028
+ 'aws:cloudwatch/eventArchive:EventArchive',
9029
+ 'aws:cloudwatch/eventBus:EventBus',
9030
+ 'aws:cognito/userPoolDomain:UserPoolDomain',
9031
+ 'aws:ec2/defaultSecurityGroup:DefaultSecurityGroup',
9032
+ 'aws:ec2/internetGateway:InternetGateway',
9033
+ 'aws:ec2/routeTable:RouteTable',
9034
+ 'aws:ec2/routeTableAssociation:RouteTableAssociation',
9035
+ 'aws:ec2/securityGroup:SecurityGroup',
9036
+ 'aws:ec2/subnet:Subnet',
9037
+ 'aws:ec2/vpc:Vpc',
9038
+ 'aws:ecs/cluster:Cluster',
9039
+ 'aws:ecs/clusterCapacityProviders:ClusterCapacityProviders',
9040
+ 'aws:rds/clusterParameterGroup:ClusterParameterGroup',
9041
+ 'aws:rds/eventSubscription:EventSubscription',
9042
+ 'aws:rds/parameterGroup:ParameterGroup',
9043
+ 'aws:rds/subnetGroup:SubnetGroup',
9044
+ 'aws:servicediscovery/privateDnsNamespace:PrivateDnsNamespace',
9045
+ 'aws:sns/topic:Topic',
9046
+ 'aws:sns/topicPolicy:TopicPolicy',
9047
+ 'aws:wafv2/webAclLoggingConfiguration:WebAclLoggingConfiguration',
9048
+ 'pulumi-nodejs:dynamic:Resource',
9049
+ 'pulumi:providers:aws',
9050
+ 'pulumi:providers:pulumi-nodejs',
9051
+ 'pulumi:providers:random',
9052
+ 'pulumi:pulumi:Stack',
9053
+ 'sst:aws:Aurora',
9054
+ 'sst:aws:Bus',
9055
+ 'sst:aws:CDN',
9056
+ 'sst:aws:Cluster',
9057
+ 'sst:aws:CognitoUserPool',
9058
+ 'sst:aws:Router',
9059
+ 'sst:aws:SnsTopic',
9060
+ 'sst:aws:Vpc',
9061
+ 'sst:sst:LinkRef',
9062
+ 'sst:sst:Version',
9063
+ ],
8390
9064
  costOrQuotaTypes: [
8391
9065
  'aws:apigatewayv2/api:Api',
8392
9066
  'aws:budgets/budget:Budget',
9067
+ 'aws:cloudfront/cachePolicy:CachePolicy',
8393
9068
  'aws:cloudfront/distribution:Distribution',
9069
+ 'aws:cloudfront/function:Function',
9070
+ 'aws:cloudfront/keyValueStore:KeyValueStore',
9071
+ 'aws:costexplorer/anomalyMonitor:AnomalyMonitor',
9072
+ 'aws:costexplorer/anomalySubscription:AnomalySubscription',
8394
9073
  'aws:cognito/userPool:UserPool',
9074
+ 'aws:ec2/eip:Eip',
9075
+ 'aws:ec2/eipAssociation:EipAssociation',
9076
+ 'aws:ec2/instance:Instance',
8395
9077
  'aws:ec2/natGateway:NatGateway',
8396
9078
  'aws:rds/cluster:Cluster',
9079
+ 'aws:rds/clusterInstance:ClusterInstance',
9080
+ 'aws:wafv2/ipSet:IpSet',
9081
+ 'aws:wafv2/webAcl:WebAcl',
8397
9082
  ],
8398
9083
  failClosedOnUnknown: true,
8399
9084
  protectedPlanAuthority: 'deploy-role-residual',
8400
9085
  standingPolicyRiskClasses: ['no-change', 'additive-low-risk'],
8401
9086
  statefulTypes: [
9087
+ 'aws:cloudwatch/eventArchive:EventArchive',
9088
+ 'aws:cloudwatch/eventBus:EventBus',
8402
9089
  'aws:cognito/userPool:UserPool',
8403
9090
  'aws:dynamodb/table:Table',
8404
9091
  'aws:events/eventBus:EventBus',
8405
9092
  'aws:kms/key:Key',
8406
9093
  'aws:rds/cluster:Cluster',
8407
9094
  'aws:s3/bucket:Bucket',
9095
+ 'aws:secretsmanager/secret:Secret',
9096
+ 'aws:secretsmanager/secretVersion:SecretVersion',
9097
+ 'aws:sns/topic:Topic',
9098
+ 'aws:ssm/parameter:Parameter',
9099
+ 'aws:wafv2/ipSet:IpSet',
9100
+ 'aws:wafv2/webAcl:WebAcl',
9101
+ 'random:index/randomPassword:RandomPassword',
8408
9102
  ],
8409
9103
  },
8410
9104
  project: 'foundation',
@@ -8502,6 +9196,13 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8502
9196
  executor: '@empire-builder-kit/nx:foundation-profile-preflight',
8503
9197
  options: { stage: 'development' },
8504
9198
  },
9199
+ 'router-waf-rehearsal': {
9200
+ cache: false,
9201
+ executor: '@empire-builder-kit/nx:router-waf-rehearsal',
9202
+ outputs: [
9203
+ `{projectRoot}/security/router-waf-blocking-rehearsal.json`,
9204
+ ],
9205
+ },
8505
9206
  typecheck: {
8506
9207
  executor: 'nx:run-commands',
8507
9208
  options: {
@@ -8529,6 +9230,11 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8529
9230
  }),
8530
9231
  'packages/foundation/scripts/verify-foundation.mjs': `import { readFileSync } from 'node:fs';
8531
9232
 
9233
+ import {
9234
+ normalizeRouterWafPolicy,
9235
+ reconcileRouterWafBlockingRehearsal,
9236
+ } from '@empire-builder-kit/nx/foundation';
9237
+
8532
9238
  const descriptor = JSON.parse(
8533
9239
  readFileSync(new URL('../contracts/foundation.json', import.meta.url), 'utf8'),
8534
9240
  );
@@ -8544,6 +9250,33 @@ const frontendSecurityHeaders = JSON.parse(
8544
9250
  'utf8',
8545
9251
  ),
8546
9252
  );
9253
+ const routerWafPolicy = normalizeRouterWafPolicy(
9254
+ JSON.parse(
9255
+ readFileSync(
9256
+ new URL('../security/router-waf.json', import.meta.url),
9257
+ 'utf8',
9258
+ ),
9259
+ ),
9260
+ );
9261
+ reconcileRouterWafBlockingRehearsal({
9262
+ evidence: JSON.parse(
9263
+ readFileSync(
9264
+ new URL('../security/router-waf-live-evidence.json', import.meta.url),
9265
+ 'utf8',
9266
+ ),
9267
+ ),
9268
+ policy: routerWafPolicy,
9269
+ receipt:
9270
+ JSON.parse(
9271
+ readFileSync(
9272
+ new URL(
9273
+ '../security/router-waf-blocking-rehearsal.json',
9274
+ import.meta.url,
9275
+ ),
9276
+ 'utf8',
9277
+ ),
9278
+ ),
9279
+ });
8547
9280
  const expectedCapabilities = [
8548
9281
  'network',
8549
9282
  'routing',
@@ -8559,6 +9292,13 @@ const expectedCapabilities = [
8559
9292
  if (descriptor.contractVersion !== '1.0.0') {
8560
9293
  throw new Error('Foundation contractVersion must be 1.0.0.');
8561
9294
  }
9295
+ if (
9296
+ routerWafPolicy.stages.development.access !== 'allowlist' ||
9297
+ routerWafPolicy.stages.staging.access !== 'allowlist' ||
9298
+ routerWafPolicy.stages.production.mode !== 'block'
9299
+ ) {
9300
+ throw new Error('Foundation Router WAF stage defaults are invalid.');
9301
+ }
8562
9302
  if (JSON.stringify(descriptor.capabilities) !== JSON.stringify(expectedCapabilities)) {
8563
9303
  throw new Error('Foundation capability descriptor does not match the v1 standard.');
8564
9304
  }
@@ -8567,7 +9307,7 @@ if (
8567
9307
  costs.aurora?.engineVersion !== '${index_js_4.FOUNDATION_AURORA_ENGINE_VERSION}' ||
8568
9308
  costs.productionProfileSource !== 'ebk.repository-policy.json' ||
8569
9309
  costs.productionProfiles?.standard?.aurora?.min !== '2 ACU' ||
8570
- costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0 ACU' ||
9310
+ costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0.5 ACU' ||
8571
9311
  !costs.stages?.development ||
8572
9312
  !costs.stages?.staging ||
8573
9313
  costs.stages?.production?.dataRetention !== 'retained-and-protected'
@@ -8623,16 +9363,15 @@ export default $config({
8623
9363
  providers: {
8624
9364
  aws: {
8625
9365
  defaultTags: {
9366
+ // SST contributes sst:app and sst:stage. Keep the provider baseline
9367
+ // at eight tags so generated S3 asset objects remain within AWS's
9368
+ // hard ten-tag object limit. Richer performance tags stay explicit.
8626
9369
  tags: {
8627
9370
  'ebk:cost-allocation': 'foundation',
8628
9371
  'ebk:cost-scope': '${name}/foundation/' + stage,
8629
9372
  'ebk:framework-version': '${frameworkVersion}',
8630
9373
  'ebk:managed-by': 'empire-builder-kit',
8631
9374
  'ebk:owner': 'foundation',
8632
- 'ebk:production-profile':
8633
- process.env.EBK_PRODUCTION_FOUNDATION_PROFILE ?? 'standard',
8634
- 'ebk:repository':
8635
- process.env.GITHUB_REPOSITORY ?? '${name}',
8636
9375
  'ebk:slice': 'foundation',
8637
9376
  'ebk:stage': stage,
8638
9377
  'ebk:workspace': '${name}',