@empire-builder-kit/nx 1.0.0-rc.15 → 1.0.0-rc.18

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 +3 -1
  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 +6 -2
  84. package/src/generators/preset/workspace-files.js +821 -54
  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 +1 -0
  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
@@ -61,11 +61,12 @@ exports.PLAN_PROVIDER_REFRESH_GLOBAL_READ_ACTIONS = [
61
61
  'rds:DescribeDBProxies',
62
62
  'rds:DescribeDBSubnetGroups',
63
63
  ];
64
- function validatePlanProviderRefreshPolicy(value, region, account) {
64
+ function validatePlanProviderRefreshPolicy(value, region, account, stage) {
65
65
  if (!Array.isArray(value) ||
66
66
  value.length === 0 ||
67
67
  !/^[a-z]{2}(?:-gov)?-[a-z]+-\d$/.test(region) ||
68
- !/^\d{12}$/.test(account)) {
68
+ !/^\d{12}$/.test(account) ||
69
+ !['production', 'staging'].includes(stage)) {
69
70
  throw new Error('Provider refresh policy must be a non-empty exact statement array for one account and region.');
70
71
  }
71
72
  const allowed = new Set(exports.PLAN_PROVIDER_REFRESH_READ_ACTIONS);
@@ -118,6 +119,14 @@ function validatePlanProviderRefreshPolicy(value, region, account) {
118
119
  if (resources.some((resource) => {
119
120
  const arn = resource.split(':');
120
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);
121
130
  return (arn[0] !== 'arn' ||
122
131
  arn[1] !== 'aws' ||
123
132
  !/^[a-z0-9-]+$/.test(arn[2] ?? '') ||
@@ -125,7 +134,8 @@ function validatePlanProviderRefreshPolicy(value, region, account) {
125
134
  !['', account].includes(arn[4] ?? '') ||
126
135
  !target ||
127
136
  (/[*?]/.test(target) &&
128
- (!target.endsWith('/*') || /[*?]/.test(target.slice(0, -2)))));
137
+ !boundedPathWildcard &&
138
+ !boundedLambdaStagePrefix));
129
139
  })) {
130
140
  throw new Error(`Provider refresh statement ${index} is not bounded to exact targets in the selected account and region.`);
131
141
  }
@@ -552,6 +562,7 @@ function planRoleAuditStep(branch, mode = 'produce') {
552
562
  'AWS_PLAN_GENERATED_SECRET_ARNS_JSON',
553
563
  new RegExp('^arn:aws:secretsmanager:' + process.env.AWS_REGION + ':' + account + ':secret:ebk/'),
554
564
  );
565
+ const stateStage = process.env.EBK_PLAN_BRANCH === 'main' ? 'production' : 'staging';
555
566
  let providerRefreshPolicy;
556
567
  try {
557
568
  providerRefreshPolicy = JSON.parse(
@@ -615,6 +626,23 @@ function planRoleAuditStep(branch, mode = 'produce') {
615
626
  for (const resource of resources) {
616
627
  const arn = resource.split(':');
617
628
  const target = arn.slice(5).join(':');
629
+ const boundedPathWildcard =
630
+ target.endsWith('/*') &&
631
+ !/[*?]/.test(target.slice(0, -2));
632
+ const boundedLambdaStagePrefix =
633
+ arn[2] === 'lambda' &&
634
+ actions.every(
635
+ (action) =>
636
+ action.startsWith('lambda:') &&
637
+ !globalProviderReadActions.has(action),
638
+ ) &&
639
+ new RegExp(
640
+ '^function:(?:' +
641
+ stateStage +
642
+ '-|[A-Za-z0-9][A-Za-z0-9_-]*-' +
643
+ stateStage +
644
+ '-)[A-Za-z0-9_-]*\\\\*$',
645
+ ).test(target);
618
646
  if (
619
647
  arn[0] !== 'arn' ||
620
648
  arn[1] !== 'aws' ||
@@ -623,7 +651,8 @@ function planRoleAuditStep(branch, mode = 'produce') {
623
651
  !['', account].includes(arn[4] ?? '') ||
624
652
  !target ||
625
653
  (/[*?]/.test(target) &&
626
- (!target.endsWith('/*') || /[*?]/.test(target.slice(0, -2))))
654
+ !boundedPathWildcard &&
655
+ !boundedLambdaStagePrefix)
627
656
  ) throw new Error('Provider refresh statement ' + index + ' has a target outside the selected account and region.');
628
657
  }
629
658
  }
@@ -638,7 +667,6 @@ function planRoleAuditStep(branch, mode = 'produce') {
638
667
  ).sort((left, right) =>
639
668
  left.Sid < right.Sid ? -1 : left.Sid > right.Sid ? 1 : 0,
640
669
  );
641
- const stateStage = process.env.EBK_PLAN_BRANCH === 'main' ? 'production' : 'staging';
642
670
  if (
643
671
  stateKmsKeyArn &&
644
672
  stateKmsKeyArn !==
@@ -709,6 +737,19 @@ function planRoleAuditStep(branch, mode = 'produce') {
709
737
  ],
710
738
  Resource: '*',
711
739
  },
740
+ {
741
+ Sid: 'ObserveStageSecretVersionIdentity',
742
+ Effect: 'Allow',
743
+ Action: 'secretsmanager:ListSecretVersionIds',
744
+ Resource:
745
+ 'arn:aws:secretsmanager:' + process.env.AWS_REGION + ':' +
746
+ account + ':secret:*',
747
+ Condition: {
748
+ StringEquals: {
749
+ 'aws:ResourceTag/ebk:stage': stateStage,
750
+ },
751
+ },
752
+ },
712
753
  {
713
754
  Sid: 'ValidatePlannedIamPolicies',
714
755
  Effect: 'Allow',
@@ -969,6 +1010,7 @@ exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
969
1010
  '/targets/blueprint-trust-check-protected',
970
1011
  '/targets/developer-context',
971
1012
  '/targets/deployment-plan',
1013
+ '/targets/deploy-local',
972
1014
  '/targets/dev',
973
1015
  '/targets/dev-doctor',
974
1016
  '/targets/dev-setup',
@@ -989,6 +1031,10 @@ const FOUNDATION_SEEDED_PATHS = new Set([
989
1031
  'docs/costs.md',
990
1032
  'docs/runbooks.md',
991
1033
  'docs/security.md',
1034
+ 'performance/account-cost-policy.json',
1035
+ 'security/router-waf-blocking-rehearsal.json',
1036
+ 'security/router-waf-live-evidence.json',
1037
+ 'security/router-waf.json',
992
1038
  ]);
993
1039
  function createFoundationOwnership(files, frameworkVersion) {
994
1040
  const paths = Object.keys(files)
@@ -1051,7 +1097,16 @@ function createWorkspaceOwnership(files, frameworkVersion) {
1051
1097
  }
1052
1098
  function renderWorkspaceFiles(options) {
1053
1099
  const { frameworkVersion, name } = options;
1100
+ const deploymentProfile = options.deploymentProfile ?? 'starter';
1101
+ const initialRepositoryPolicy = (0, index_js_4.createIncubationRepositoryPolicy)(deploymentProfile);
1102
+ initialRepositoryPolicy.deploymentAccounts = options.deploymentAccounts ?? {
1103
+ development: null,
1104
+ production: null,
1105
+ staging: null,
1106
+ };
1107
+ initialRepositoryPolicy.deploymentRegion = options.deploymentRegion ?? null;
1054
1108
  const developerAgentFilePlan = (0, workspace_files_developer_agent_js_1.renderDeveloperAgentWorkspaceFilePlan)({
1109
+ deploymentProfile,
1055
1110
  frameworkVersion,
1056
1111
  name,
1057
1112
  });
@@ -1064,6 +1119,9 @@ function renderWorkspaceFiles(options) {
1064
1119
  const uploadArtifactAction = (0, index_js_3.pinnedAction)(index_js_3.GITHUB_ACTION_PINS.uploadArtifact);
1065
1120
  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
1121
  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'";
1122
+ const protectedPush = "github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')";
1123
+ const protectedDeploymentRouteJobName = 'protected deployment route';
1124
+ const stagingApplyJobPrefix = 'apply staging plan ';
1067
1125
  const files = {
1068
1126
  ...(0, workspace_files_ci_acceptance_js_1.renderWorkspaceCiAcceptanceFiles)(),
1069
1127
  '.node-version': `${tool_versions_js_1.WORKSPACE_NODE_VERSION}\n`,
@@ -1095,24 +1153,26 @@ env:
1095
1153
  jobs:
1096
1154
  acceptance:
1097
1155
  name: accepted merge evidence
1098
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
1156
+ if: ${protectedPush}
1099
1157
  runs-on: ubuntu-latest
1100
1158
  outputs:
1159
+ history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1101
1160
  reused: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1161
+ route: \${{ steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' && steps.resolve.outputs.route || '' }}
1102
1162
  steps:
1103
1163
  - name: Check out immutable merged candidate
1104
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
1164
+ if: ${protectedPush}
1105
1165
  uses: ${checkoutAction}
1106
1166
  with:
1107
1167
  fetch-depth: 0
1108
1168
  ref: \${{ github.sha }}
1109
- - name: Resolve exact protected-promotion acceptance
1110
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
1169
+ - name: Resolve exact protected-route acceptance
1170
+ if: ${protectedPush}
1111
1171
  id: resolve
1112
1172
  env:
1113
1173
  GH_TOKEN: \${{ github.token }}
1114
1174
  run: node tools/ci/workspace-acceptance.mjs resolve
1115
- - name: Restore exact protected-promotion acceptance
1175
+ - name: Restore exact protected-route acceptance
1116
1176
  if: steps.resolve.outputs.reused == 'true'
1117
1177
  id: restore
1118
1178
  continue-on-error: true
@@ -1132,13 +1192,17 @@ jobs:
1132
1192
  id: verify
1133
1193
  continue-on-error: true
1134
1194
  env:
1195
+ EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
1135
1196
  EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
1197
+ EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
1136
1198
  EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
1137
1199
  EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
1138
1200
  run: >-
1139
1201
  node tools/ci/workspace-acceptance.mjs verify
1140
1202
  .ebk/evidence/reused-acceptance/manifest.json
1141
1203
  "$EBK_ACCEPTANCE_HEAD"
1204
+ "$EBK_ACCEPTANCE_BASE"
1205
+ "$EBK_ACCEPTANCE_ROUTE"
1142
1206
  "$EBK_ACCEPTANCE_RUN_ID"
1143
1207
  "$EBK_ACCEPTANCE_RUN_ATTEMPT"
1144
1208
 
@@ -1198,12 +1262,14 @@ jobs:
1198
1262
  run: pnpm nx run-many -t verify --parallel=3
1199
1263
 
1200
1264
  seal-acceptance:
1201
- name: seal protected promotion acceptance
1265
+ name: seal protected route acceptance
1202
1266
  needs: [policy, verify]
1203
1267
  if: >-
1204
1268
  always() &&
1269
+ needs.policy.result == 'success' &&
1270
+ needs.verify.result == 'success' &&
1205
1271
  github.event_name == 'pull_request' &&
1206
- ${protectedDevelopPromotion}
1272
+ (${protectedDevelopPromotion} || ${protectedMainSync})
1207
1273
  runs-on: ubuntu-latest
1208
1274
  steps:
1209
1275
  - name: Check out immutable accepted candidate
@@ -1211,13 +1277,16 @@ jobs:
1211
1277
  with:
1212
1278
  fetch-depth: 0
1213
1279
  ref: \${{ env.EBK_CI_CANDIDATE_SHA }}
1214
- - name: Seal exact protected-promotion acceptance
1280
+ - name: Seal exact protected-route acceptance
1281
+ id: seal
1215
1282
  env:
1283
+ EBK_BASE_COMMIT: \${{ github.event.pull_request.base.sha }}
1216
1284
  EBK_HEAD_REPOSITORY: \${{ github.event.pull_request.head.repo.full_name }}
1217
1285
  run: >-
1218
1286
  node tools/ci/workspace-acceptance.mjs seal
1219
1287
  .ebk/evidence/ci/workspace-acceptance/manifest.json
1220
- - name: Retain exact protected-promotion acceptance
1288
+ - name: Retain exact protected-route acceptance
1289
+ if: steps.seal.outputs.sealed == 'true'
1221
1290
  uses: ${uploadArtifactAction}
1222
1291
  with:
1223
1292
  name: workspace-acceptance-\${{ env.EBK_CI_CANDIDATE_SHA }}-\${{ github.run_id }}-\${{ github.run_attempt }}
@@ -1232,6 +1301,7 @@ jobs:
1232
1301
  if: >-
1233
1302
  always() &&
1234
1303
  github.event_name == 'push' &&
1304
+ needs.acceptance.outputs.history_only != 'true' &&
1235
1305
  (needs.acceptance.outputs.reused == 'true' ||
1236
1306
  (needs.policy.result == 'success' && needs.verify.result == 'success'))
1237
1307
  runs-on: ubuntu-latest
@@ -1298,7 +1368,97 @@ permissions:
1298
1368
  contents: read
1299
1369
 
1300
1370
  jobs:
1371
+ history-classification:
1372
+ name: classify protected history-only sync
1373
+ if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
1374
+ runs-on: ubuntu-latest
1375
+ permissions:
1376
+ actions: read
1377
+ contents: read
1378
+ pull-requests: read
1379
+ outputs:
1380
+ history_only: \${{ steps.resolve.outputs.history_only == 'true' && steps.restore.outcome == 'success' && steps.verify.outcome == 'success' }}
1381
+ steps:
1382
+ - name: Check out immutable protected candidate
1383
+ uses: ${checkoutAction}
1384
+ with:
1385
+ fetch-depth: 0
1386
+ ref: \${{ github.sha }}
1387
+ - uses: ${setupNodeAction}
1388
+ with:
1389
+ node-version: ${tool_versions_js_1.WORKSPACE_NODE_VERSION}
1390
+ - name: Resolve exact protected-route acceptance
1391
+ id: resolve
1392
+ env:
1393
+ GH_TOKEN: \${{ github.token }}
1394
+ run: node tools/ci/workspace-acceptance.mjs resolve
1395
+ - name: Restore exact protected-route acceptance
1396
+ if: steps.resolve.outputs.reused == 'true'
1397
+ id: restore
1398
+ continue-on-error: true
1399
+ env:
1400
+ EBK_ACCEPTANCE_ARTIFACT_DIGEST: \${{ steps.resolve.outputs.artifact_digest }}
1401
+ EBK_ACCEPTANCE_ARTIFACT_ID: \${{ steps.resolve.outputs.artifact_id }}
1402
+ EBK_ACCEPTANCE_ARTIFACT_SIZE: \${{ steps.resolve.outputs.artifact_size }}
1403
+ GH_TOKEN: \${{ github.token }}
1404
+ run: >-
1405
+ node tools/ci/workspace-acceptance.mjs download
1406
+ .ebk/evidence/reused-history-acceptance
1407
+ "$EBK_ACCEPTANCE_ARTIFACT_ID"
1408
+ "$EBK_ACCEPTANCE_ARTIFACT_DIGEST"
1409
+ "$EBK_ACCEPTANCE_ARTIFACT_SIZE"
1410
+ - name: Verify exact history-only acceptance
1411
+ if: steps.resolve.outputs.reused == 'true' && steps.restore.outcome == 'success'
1412
+ id: verify
1413
+ continue-on-error: true
1414
+ env:
1415
+ EBK_ACCEPTANCE_BASE: \${{ steps.resolve.outputs.base_commit }}
1416
+ EBK_ACCEPTANCE_HEAD: \${{ steps.resolve.outputs.head_commit }}
1417
+ EBK_ACCEPTANCE_ROUTE: \${{ steps.resolve.outputs.route }}
1418
+ EBK_ACCEPTANCE_RUN_ATTEMPT: \${{ steps.resolve.outputs.run_attempt }}
1419
+ EBK_ACCEPTANCE_RUN_ID: \${{ steps.resolve.outputs.run_id }}
1420
+ run: >-
1421
+ node tools/ci/workspace-acceptance.mjs verify
1422
+ .ebk/evidence/reused-history-acceptance/manifest.json
1423
+ "$EBK_ACCEPTANCE_HEAD"
1424
+ "$EBK_ACCEPTANCE_BASE"
1425
+ "$EBK_ACCEPTANCE_ROUTE"
1426
+ "$EBK_ACCEPTANCE_RUN_ID"
1427
+ "$EBK_ACCEPTANCE_RUN_ATTEMPT"
1428
+
1429
+ deployment-mode:
1430
+ name: classify deployment profile
1431
+ runs-on: ubuntu-latest
1432
+ outputs:
1433
+ profile: \${{ steps.profile.outputs.profile }}
1434
+ steps:
1435
+ - name: Check out immutable deployment policy
1436
+ uses: ${checkoutAction}
1437
+ with:
1438
+ ref: \${{ github.sha }}
1439
+ - id: profile
1440
+ name: Read exact committed deployment profile
1441
+ run: |
1442
+ node - <<'NODE'
1443
+ const { readFileSync } = require('node:fs');
1444
+ const policy = JSON.parse(readFileSync('ebk.repository-policy.json', 'utf8'));
1445
+ const profiles = new Set(['starter', 'production-isolated', 'fully-isolated']);
1446
+ if (!profiles.has(policy.deploymentProfile)) {
1447
+ throw new Error('Committed deployment profile is absent or invalid.');
1448
+ }
1449
+ require('node:fs').appendFileSync(
1450
+ process.env.GITHUB_OUTPUT,
1451
+ 'profile=' + policy.deploymentProfile + '\\n',
1452
+ );
1453
+ NODE
1454
+
1301
1455
  route:
1456
+ name: ${protectedDeploymentRouteJobName}
1457
+ needs: [history-classification, deployment-mode]
1458
+ if: >-
1459
+ always() &&
1460
+ needs.history-classification.outputs.history_only != 'true' &&
1461
+ needs.deployment-mode.result == 'success'
1302
1462
  runs-on: ubuntu-latest
1303
1463
  steps:
1304
1464
  - name: Validate protected ref
@@ -1311,14 +1471,22 @@ jobs:
1311
1471
  esac
1312
1472
 
1313
1473
  plan-staging:
1314
- needs: route
1315
- if: github.ref == 'refs/heads/develop'
1474
+ needs: [route, deployment-mode]
1475
+ if: >-
1476
+ github.ref == 'refs/heads/develop' &&
1477
+ (github.event_name == 'workflow_dispatch' ||
1478
+ needs.deployment-mode.outputs.profile != 'starter')
1316
1479
  runs-on: ubuntu-latest
1317
1480
  outputs:
1481
+ deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
1482
+ deployment_region: \${{ steps.plan.outputs.deployment_region }}
1483
+ development_account_id: \${{ steps.plan.outputs.development_account_id }}
1318
1484
  foundation_project: \${{ steps.plan.outputs.foundation_project }}
1319
1485
  has_slices: \${{ steps.plan.outputs.has_slices }}
1320
1486
  ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
1487
+ production_account_id: \${{ steps.plan.outputs.production_account_id }}
1321
1488
  slice_projects: \${{ steps.plan.outputs.slice_projects }}
1489
+ staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
1322
1490
  steps:
1323
1491
  - uses: ${checkoutAction}
1324
1492
  with:
@@ -1529,16 +1697,21 @@ jobs:
1529
1697
  path: .ebk/evidence/staging-release-source/**
1530
1698
 
1531
1699
  plan-production:
1532
- needs: route
1700
+ needs: [route, deployment-mode]
1533
1701
  if: github.ref == 'refs/heads/main'
1534
1702
  runs-on: ubuntu-latest
1535
1703
  outputs:
1704
+ deployment_profile: \${{ steps.plan.outputs.deployment_profile }}
1705
+ deployment_region: \${{ steps.plan.outputs.deployment_region }}
1706
+ development_account_id: \${{ steps.plan.outputs.development_account_id }}
1536
1707
  foundation_project: \${{ steps.plan.outputs.foundation_project }}
1537
1708
  foundation_profile: \${{ steps.plan.outputs.foundation_profile }}
1538
1709
  has_slices: \${{ steps.plan.outputs.has_slices }}
1539
1710
  ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
1540
1711
  production_approval_mode: \${{ steps.plan.outputs.production_approval_mode }}
1712
+ production_account_id: \${{ steps.plan.outputs.production_account_id }}
1541
1713
  slice_projects: \${{ steps.plan.outputs.slice_projects }}
1714
+ staging_account_id: \${{ steps.plan.outputs.staging_account_id }}
1542
1715
  steps:
1543
1716
  - uses: ${checkoutAction}
1544
1717
  with:
@@ -1859,6 +2032,8 @@ jobs:
1859
2032
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1860
2033
  EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1861
2034
  EBK_STAGE: staging
2035
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2036
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
1862
2037
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
1863
2038
  steps:
1864
2039
  - uses: ${checkoutAction}
@@ -1874,6 +2049,10 @@ jobs:
1874
2049
  - run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1875
2050
  - name: Verify managed workspace integrity before AWS authority
1876
2051
  run: pnpm nx run ${name}:workspace-health
2052
+ - name: Bind staging account to committed deployment topology
2053
+ run: |
2054
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
2055
+ test "$AWS_REGION" = "$EBK_POLICY_REGION"
1877
2056
  ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${configureAwsAction}
1878
2057
  with:
1879
2058
  role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
@@ -1913,6 +2092,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
1913
2092
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1914
2093
  EBK_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
1915
2094
  EBK_STAGE: staging
2095
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2096
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
1916
2097
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
1917
2098
  steps:
1918
2099
  - uses: ${checkoutAction}
@@ -1928,6 +2109,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - uses: ${c
1928
2109
  - run: pnpm install --frozen-lockfile --ignore-scripts --ignore-pnpmfile
1929
2110
  - name: Verify managed workspace integrity before AWS authority
1930
2111
  run: pnpm nx run ${name}:workspace-health
2112
+ - name: Bind staging account to committed deployment topology
2113
+ run: |
2114
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID"
2115
+ test "$AWS_REGION" = "$EBK_POLICY_REGION"
1931
2116
  ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Restore both native platform records
1932
2117
  uses: ${downloadArtifactAction}
1933
2118
  with:
@@ -1969,6 +2154,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
1969
2154
  AWS_DEPLOY_ROLE_ARN: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
1970
2155
  AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
1971
2156
  AWS_REGION: \${{ vars.AWS_REGION }}
2157
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
2158
+ EBK_POLICY_PRODUCTION_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
2159
+ EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
2160
+ EBK_POLICY_STAGING_ACCOUNT_ID: \${{ needs.plan-production.outputs.staging_account_id }}
1972
2161
  EBK_CANDIDATE_RUN_ATTEMPT: \${{ needs.release-source-production.outputs.candidate_run_attempt }}
1973
2162
  EBK_CANDIDATE_RUN_ID: \${{ needs.release-source-production.outputs.candidate_run_id }}
1974
2163
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-production.outputs.source_commit }}
@@ -2069,11 +2258,13 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
2069
2258
  cp "$source_sidecar" "$target"
2070
2259
  done
2071
2260
  - uses: ${configureAwsAction}
2261
+ if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
2072
2262
  with:
2073
2263
  role-to-assume: \${{ secrets.AWS_STAGING_PROMOTION_PULL_ROLE_ARN }}
2074
2264
  role-session-name: ebk-staging-container-pull-\${{ github.run_id }}-\${{ github.run_attempt }}
2075
2265
  aws-region: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
2076
2266
  - name: Authenticate exact staging source registry
2267
+ if: env.EBK_DEPLOYMENT_PROFILE != 'starter'
2077
2268
  env:
2078
2269
  EBK_STAGING_ACCOUNT_ID: \${{ vars.AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID }}
2079
2270
  EBK_STAGING_REGION: \${{ vars.AWS_STAGING_PROMOTION_REGION }}
@@ -2092,10 +2283,27 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
2092
2283
  role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
2093
2284
  role-session-name: ebk-production-container-push-\${{ github.run_id }}-\${{ github.run_attempt }}
2094
2285
  aws-region: \${{ vars.AWS_REGION }}
2286
+ - name: Bind production authority to committed deployment topology
2287
+ run: |
2288
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
2289
+ { echo 'Production environment account differs from committed deployment topology'; exit 1; }
2290
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
2291
+ { echo 'Production environment Region differs from committed deployment topology'; exit 1; }
2292
+ - name: Authenticate shared-account staging source registry
2293
+ if: env.EBK_DEPLOYMENT_PROFILE == 'starter'
2294
+ run: |
2295
+ test "$EBK_POLICY_STAGING_ACCOUNT_ID" = "$EBK_POLICY_PRODUCTION_ACCOUNT_ID" ||
2296
+ { echo 'Starter promotion requires one shared AWS account'; exit 1; }
2297
+ registry_suffix=.com
2298
+ case "$EBK_POLICY_REGION" in cn-*) registry_suffix=.com.cn ;; esac
2299
+ aws ecr get-login-password --region "$EBK_POLICY_REGION" |
2300
+ docker login --username AWS --password-stdin \
2301
+ "$EBK_POLICY_STAGING_ACCOUNT_ID.dkr.ecr.$EBK_POLICY_REGION.amazonaws$registry_suffix"
2095
2302
  - name: Promote exact staging indexes without rebuilding
2096
2303
  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 }}
2304
+ EBK_DEPLOYMENT_PROFILE: \${{ needs.plan-production.outputs.deployment_profile }}
2305
+ 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 }}
2306
+ EBK_STAGING_PROMOTION_REGION: \${{ needs.plan-production.outputs.deployment_profile == 'starter' && needs.plan-production.outputs.deployment_region || vars.AWS_STAGING_PROMOTION_REGION }}
2099
2307
  run: |
2100
2308
  test "$(aws sts get-caller-identity --query Account --output text)" = "$AWS_EXPECTED_ACCOUNT_ID" ||
2101
2309
  { echo 'Production promotion role account differs from policy'; exit 1; }
@@ -2314,6 +2522,8 @@ ${planRoleAuditStep('main', 'verify')}
2314
2522
  EBK_RELEASE_SOURCE_COMMIT: \${{ needs.release-source-staging.outputs.source_commit }}
2315
2523
  EBK_SOURCE_COMMIT: \${{ github.sha }}
2316
2524
  EBK_STAGE: staging
2525
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-staging.outputs.staging_account_id }}
2526
+ EBK_POLICY_REGION: \${{ needs.plan-staging.outputs.deployment_region }}
2317
2527
  FOUNDATION_PROJECT: \${{ needs.plan-staging.outputs.foundation_project }}
2318
2528
  ORDERED_PROJECTS: \${{ needs.plan-staging.outputs.ordered_projects }}
2319
2529
  SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
@@ -2421,7 +2631,7 @@ ${planRoleAuditStep('develop', 'verify')}
2421
2631
  packages/*/.ebk/evidence/infrastructure-plan-command.json
2422
2632
 
2423
2633
  deploy-staging:
2424
- name: apply staging plan \${{ needs.infrastructure-plan-staging.outputs.plan_digest || 'recovery-without-resource-plan' }}
2634
+ name: ${stagingApplyJobPrefix}\${{ needs.infrastructure-plan-staging.outputs.plan_digest || 'recovery-without-resource-plan' }}
2425
2635
  needs: [plan-staging, release-source-staging, container-assemble-staging, infrastructure-plan-staging]
2426
2636
  if: |
2427
2637
  always() &&
@@ -2500,6 +2710,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
2500
2710
  test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'staging AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
2501
2711
  test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'staging AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
2502
2712
  test -n "$AWS_REGION" || { echo 'staging AWS_REGION is required'; exit 1; }
2713
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
2714
+ { echo 'staging environment account differs from committed deployment topology'; exit 1; }
2715
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
2716
+ { echo 'staging environment Region differs from committed deployment topology'; exit 1; }
2503
2717
  test -n "$EBK_FLEET_ORIGIN" ||
2504
2718
  { echo 'staging environment EBK_FLEET_ORIGIN is required'; exit 1; }
2505
2719
  test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
@@ -3136,6 +3350,8 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
3136
3350
  EBK_APPROVED_MAIN_COMMIT: \${{ github.sha }}
3137
3351
  EBK_SOURCE_COMMIT: \${{ github.sha }}
3138
3352
  EBK_STAGE: production
3353
+ EBK_POLICY_ACCOUNT_ID: \${{ needs.plan-production.outputs.production_account_id }}
3354
+ EBK_POLICY_REGION: \${{ needs.plan-production.outputs.deployment_region }}
3139
3355
  EBK_VERIFIED_STAGING_RUN_ID: \${{ needs.release-source-production.outputs.verified_staging_run_id }}
3140
3356
  FOUNDATION_PROJECT: \${{ needs.plan-production.outputs.foundation_project }}
3141
3357
  ORDERED_PROJECTS: \${{ needs.plan-production.outputs.ordered_projects }}
@@ -3232,6 +3448,10 @@ ${stagingReleaseCandidateDownloadSteps(downloadArtifactAction)} - name: Res
3232
3448
  test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'production AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
3233
3449
  test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'production AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
3234
3450
  test -n "$AWS_REGION" || { echo 'production AWS_REGION is required'; exit 1; }
3451
+ test "$AWS_EXPECTED_ACCOUNT_ID" = "$EBK_POLICY_ACCOUNT_ID" ||
3452
+ { echo 'production environment account differs from committed deployment topology'; exit 1; }
3453
+ test "$AWS_REGION" = "$EBK_POLICY_REGION" ||
3454
+ { echo 'production environment Region differs from committed deployment topology'; exit 1; }
3235
3455
  test -n "$EBK_FLEET_ORIGIN" ||
3236
3456
  { echo 'production environment EBK_FLEET_ORIGIN is required'; exit 1; }
3237
3457
  test "$EBK_FLEET_ORIGIN" = "$EBK_PLANNED_FLEET_ORIGIN" ||
@@ -4898,13 +5118,51 @@ permissions:
4898
5118
  contents: read
4899
5119
 
4900
5120
  jobs:
5121
+ deployment-classification:
5122
+ name: classify exact staging deployment
5123
+ if: >-
5124
+ github.event_name == 'workflow_run' &&
5125
+ github.event.workflow_run.event == 'push' &&
5126
+ github.event.workflow_run.head_branch == 'develop' &&
5127
+ github.event.workflow_run.conclusion == 'success'
5128
+ permissions:
5129
+ actions: read
5130
+ contents: read
5131
+ outputs:
5132
+ deployed: \${{ steps.classify.outputs.deployed }}
5133
+ runs-on: ubuntu-latest
5134
+ steps:
5135
+ - id: classify
5136
+ name: Require one successful staging deployment job
5137
+ env:
5138
+ EBK_DEPLOYMENT_RUN_ID: \${{ github.event.workflow_run.id }}
5139
+ GH_TOKEN: \${{ github.token }}
5140
+ GH_REPO: \${{ github.repository }}
5141
+ run: |
5142
+ jobs="$(gh api \
5143
+ "/repos/$GH_REPO/actions/runs/$EBK_DEPLOYMENT_RUN_ID/jobs?filter=latest&per_page=100")"
5144
+ apply_count="$(printf '%s' "$jobs" | jq \
5145
+ '[.jobs[] | select(.name | startswith("${stagingApplyJobPrefix}")) | select(.conclusion == "success")] | length')"
5146
+ route_success_count="$(printf '%s' "$jobs" | jq \
5147
+ '[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "success")] | length')"
5148
+ route_skipped_count="$(printf '%s' "$jobs" | jq \
5149
+ '[.jobs[] | select(.name == "${protectedDeploymentRouteJobName}" and .conclusion == "skipped")] | length')"
5150
+ case "$apply_count:$route_success_count:$route_skipped_count" in
5151
+ 1:1:0) echo 'deployed=true' >> "$GITHUB_OUTPUT" ;;
5152
+ 0:0:1) echo 'deployed=false' >> "$GITHUB_OUTPUT" ;;
5153
+ *) echo 'Deploy route and staging application evidence disagree'; exit 1 ;;
5154
+ esac
5155
+
4901
5156
  plan:
5157
+ needs: deployment-classification
4902
5158
  if: >-
4903
- (github.event_name == 'workflow_run' &&
5159
+ always() &&
5160
+ ((github.event_name == 'workflow_run' &&
4904
5161
  github.event.workflow_run.conclusion == 'success' &&
4905
5162
  github.event.workflow_run.event == 'push' &&
4906
- github.event.workflow_run.head_branch == 'develop') ||
4907
- github.event_name == 'workflow_dispatch'
5163
+ github.event.workflow_run.head_branch == 'develop' &&
5164
+ needs.deployment-classification.outputs.deployed == 'true') ||
5165
+ github.event_name == 'workflow_dispatch')
4908
5166
  outputs:
4909
5167
  deployment_run_attempt: \${{ steps.identity.outputs.deployment_run_attempt }}
4910
5168
  deployment_run_id: \${{ steps.identity.outputs.deployment_run_id }}
@@ -5397,14 +5655,14 @@ jobs:
5397
5655
  test "$(node --version | cut -d. -f1)" = v22
5398
5656
  - name: Run exact deployed smoke
5399
5657
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5400
- run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
5658
+ run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:smoke-deployed"
5401
5659
  - name: Verify reviewed frontend visual baseline
5402
5660
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5403
- run: node node_modules/nx/bin/nx.js run "$EBK_PROJECT:frontend-visual"
5661
+ run: node node_modules/nx/dist/bin/nx.js run "$EBK_PROJECT:frontend-visual"
5404
5662
  - name: Verify deployed frontend security headers
5405
5663
  working-directory: \${{ runner.temp }}/ebk-prepared-workspace
5406
5664
  run: |
5407
- node node_modules/nx/bin/nx.js run \
5665
+ node node_modules/nx/dist/bin/nx.js run \
5408
5666
  "$EBK_PROJECT:frontend-security-headers" -- \
5409
5667
  --url="$EBK_SMOKE_URL"
5410
5668
  - name: Bind exact alarm and data-integrity evidence
@@ -6851,11 +7109,11 @@ jobs:
6851
7109
  if [ "$EBK_ACTION" = apply ]; then args+=("--apply"); fi
6852
7110
  pnpm nx run "$EBK_PROJECT:secret-metadata" -- "\${args[@]}"
6853
7111
  `,
6854
- '.github/workflows/sync-back.yml': `name: Sync production history
7112
+ '.github/workflows/sync-back.yml': `name: Sync protected main history
6855
7113
 
6856
7114
  on:
6857
7115
  workflow_run:
6858
- workflows: [Deploy]
7116
+ workflows: [Verify]
6859
7117
  types: [completed]
6860
7118
 
6861
7119
  permissions:
@@ -6866,6 +7124,7 @@ jobs:
6866
7124
  sync-back:
6867
7125
  if: >-
6868
7126
  github.event.workflow_run.conclusion == 'success' &&
7127
+ github.event.workflow_run.event == 'push' &&
6869
7128
  github.event.workflow_run.head_branch == 'main'
6870
7129
  runs-on: ubuntu-latest
6871
7130
  steps:
@@ -6879,8 +7138,8 @@ jobs:
6879
7138
  gh pr create \
6880
7139
  --head main \
6881
7140
  --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.'
7141
+ --title 'chore(git): sync protected main to develop' \
7142
+ --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
7143
  number=$(gh pr list --state open --head main --base develop --json number --jq '.[0].number')
6885
7144
  fi
6886
7145
  echo "Sync-back pull request #$number is ready for review."
@@ -7234,6 +7493,16 @@ use the direct installed \`.bin/ebk blueprint verification-review\` entry docume
7234
7493
  \`docs/blueprints.md\`.
7235
7494
 
7236
7495
  Workspace creation only writes source files. It does not create or change cloud resources.
7496
+
7497
+ New workspaces default to the one-account \`starter\` deployment profile. Configure the exact
7498
+ shared account and Region in \`ebk.repository-policy.json\`, authenticate through IAM Identity
7499
+ Center, add exact development and staging CIDRs to
7500
+ \`packages/foundation/security/router-waf.json\`, and use
7501
+ \`pnpm nx run ${name}:deploy-local -- --stage=development
7502
+ --profile=<profile> --expectedRoleArn=<role-arn>\`. Never invoke SST directly.
7503
+ For a brand-new stage with generated Product Slices, complete the documented workload-role
7504
+ Foundation publication and pass \`--phase=foundation-bootstrap\` once before publishing and
7505
+ deploying the complete stage.
7237
7506
  `,
7238
7507
  'docs/architecture.md': `# Workspace architecture
7239
7508
 
@@ -7250,6 +7519,11 @@ Foundation implementation files.
7250
7519
  Canonical consumer stages use the matching \`development\`, \`staging\`, or \`production\`
7251
7520
  Foundation. Framework-derived personal stages may use only the \`development\`
7252
7521
  Foundation; generic preview stages are unsupported.
7522
+
7523
+ The committed deployment profile changes account and local/GitHub authority only. Every
7524
+ canonical stage always owns a separate Router/domain, Aurora cluster and migration history,
7525
+ Cognito user pool, event bus, secret namespace, IAM boundary, logs, queues, ECR namespace, and
7526
+ stage-qualified SSM discovery contract. Discovery rejects a record for another canonical stage.
7253
7527
  `,
7254
7528
  'docs/git-workflow.md': `# Protected Git workflow
7255
7529
 
@@ -7270,6 +7544,9 @@ GitHub rulesets, environments, and AWS IAM are the authorization boundaries; loc
7270
7544
  only defense in depth.
7271
7545
 
7272
7546
  The \`staging\` environment accepts only \`develop\`; \`production\` accepts only \`main\`.
7547
+ These workflows exist for every deployment profile. The staging workflow is optional for
7548
+ \`starter\`, whose governed local Nx target may deploy development and staging. Production is
7549
+ GitHub-only for every profile; \`fully-isolated\` also requires GitHub for staging.
7273
7550
  The committed \`productionApproval.mode\` selects one of two production authorization
7274
7551
  contracts:
7275
7552
 
@@ -7292,7 +7569,9 @@ contracts:
7292
7569
  Both modes retain required checks, a non-author CODEOWNER approval, no ruleset bypass, the
7293
7570
  literal production environment, and its branch-scoped credentials. The base \`staging\` and
7294
7571
  \`production\` environments own deployment, smoke, and ECR evidence roles plus their
7295
- variables; production also owns the staging-registry promotion pull role. Dedicated
7572
+ variables; isolated profiles also keep the staging-registry promotion pull role in production.
7573
+ Starter promotion authenticates once to the shared account and copies the exact staging digest
7574
+ into a distinct production ECR repository. Dedicated
7296
7575
  \`infrastructure-plan-<stage>\` environments gate read-only role use. Separate
7297
7576
  \`async-recovery-<stage>\`, \`performance-aggregate-<stage>\`,
7298
7577
  \`performance-cost-<stage>\`, \`performance-load-<stage>\`, and
@@ -7441,18 +7720,35 @@ materializes, close and reopen the pull request as a human and then approve it.
7441
7720
  `,
7442
7721
  'docs/repository-operating-policy.md': `# Repository Operating Policy
7443
7722
 
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.
7723
+ \`ebk.repository-policy.json\` is the reviewed source of truth for deployment profile, exact
7724
+ stage account IDs, deployment Region, production access, production approval, per-Slice
7725
+ readiness, and the one shared production Foundation profile.
7446
7726
  New workspaces start with production disabled and explicit Team-compatible
7447
7727
  \`productionApproval.mode: "promotion-pr"\`. A missing approval mode or Slice entry fails
7448
7728
  closed; no GitHub capability is allowed to choose a mode implicitly.
7449
7729
 
7730
+ Deployment profiles are proportional to operator maturity:
7731
+
7732
+ - \`starter\` (default) requires one shared account, permits local development and staging
7733
+ through the governed root Nx target, and reserves production for protected GitHub deployment;
7734
+ - \`production-isolated\` requires one development/staging account and a distinct production
7735
+ account; only production is GitHub-only; and
7736
+ - \`fully-isolated\` requires three distinct accounts; staging and production are GitHub-only.
7737
+
7738
+ Equal account IDs never imply shared stage resources. Router, Aurora, Cognito, events, secrets,
7739
+ IAM, logs, queues, resource names, and SSM discovery remain stage-specific. Existing prerelease
7740
+ repositories migrate explicitly to \`fully-isolated\`; upgrades never infer or weaken topology.
7741
+ That profile-only migration retains the former authority model but stays undeployable until exact
7742
+ development, staging, and production account IDs plus \`deploymentRegion\` are committed.
7743
+
7450
7744
  The named repository modes are source-writing presets, not runtime state. Report a change
7451
7745
  first and inspect the complete current/next policy:
7452
7746
 
7453
7747
  \`\`\`bash
7454
7748
  pnpm nx g @empire-builder-kit/nx:repository-policy \\
7455
- --preset=controlled-production --slices=billing
7749
+ --deploymentProfile=starter --deploymentRegion=us-east-1 \\
7750
+ --developmentAccountId=111122223333 --stagingAccountId=111122223333 \\
7751
+ --productionAccountId=111122223333
7456
7752
  \`\`\`
7457
7753
 
7458
7754
  Apply the reviewed source change separately with \`--apply\`. This command never contacts AWS
@@ -7462,6 +7758,66 @@ Use \`--productionApprovalMode=promotion-pr|environment-reviewer\` for an explic
7462
7758
  transition. This changes committed policy only; a separate bootstrap report/apply reconciles
7463
7759
  remote GitHub settings.
7464
7760
 
7761
+ After applying exact topology, authenticate with temporary federated credentials for development
7762
+ or staging and invoke
7763
+ \`pnpm nx run ${name}:deploy-local -- --stage=<stage> --profile=<profile>
7764
+ --expectedRoleArn=<role-arn>\`. Production runs only through the protected main-branch GitHub
7765
+ workflow. Each admitted local invocation plans the complete current stage, displays the stage
7766
+ summary and first material project, and requires a real TTY response of
7767
+ \`approve:<action>:<stage>:<project>:<current-plan-digest>\`. A human or authorized agent may type
7768
+ that acknowledgement; it is active operator control, not a claim of independent human review.
7769
+ The target creates one private ten-minute capability for that project, rechecks the exact source,
7770
+ artifacts, account, Region, role, stage, policy, state, and lock without a second provider preview,
7771
+ consumes the capability once, and removes it on every exit path. Unattended scripts cannot approve.
7772
+ Every material deploy or refresh mutates at most the first material project and ends with a
7773
+ controlled non-success \`replan-required\` handoff, including the last project in the ordered stage.
7774
+ Rerun the complete stage, review the new current summary, and repeat until one invocation proves
7775
+ every project is semantically converged. Only that convergence invocation succeeds. A preview is
7776
+ recent review and audit evidence; it is not represented as a cryptographically replayable Pulumi
7777
+ transaction. A complete, unlocked deploy plan containing only provider reads with no changed
7778
+ properties, replacement, failure, or unmatched risk finding is retained as observation evidence
7779
+ and counts as semantically converged. The standalone project planner may still return its
7780
+ conservative non-success for that drift observation; the root local transaction accepts only a
7781
+ freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
7782
+ only when their logical ID and resource type match exactly. Any stale or ambiguous
7783
+ observation fails closed; a plan with desired changes remains material and requires review. One
7784
+ narrower case stays reviewable instead of becoming
7785
+ drift: when an upstream Foundation apply changes only both external framework-version tag
7786
+ projections (plus SST's already-bounded name hydration), the retained
7787
+ external identity must match one exact non-replacing provider read in the same ordinary plan.
7788
+ Missing read correlation, owned resources, other tag or property changes, incomplete tag pairs,
7789
+ and provider failures remain drift and fail closed. The local target requires the exact SST v5 account bootstrap normally
7790
+ created by the pinned \`ebk dev up\` path, then creates and attests only missing exact
7791
+ per-project/canonical-stage state passphrases before its formal read-only plans. This passphrase
7792
+ bootstrap is the only persistent pre-acknowledgement write; it creates no deployment authority
7793
+ and is reported in bounded local evidence. Clean immutable source, immediate per-project plans,
7794
+ migration and retention preflights, and secret-free result
7795
+ evidence remain mandatory. Direct SST is forbidden.
7796
+
7797
+ After an interrupted apply, use the same target with \`--action=refresh\` and the exact
7798
+ phase, stage, profile, and role. It retains the same plan-summary review boundary, admits only
7799
+ bounded drift-detected refresh evidence, and never turns refresh into an implicit deploy. When
7800
+ the interrupted write leaves a recorded provider output unusable by the ordinary desired preview,
7801
+ the plan stops without mutation. The non-writing refresh preview separates provider refresh
7802
+ records from its later desired preview and cannot replace failed ordinary desired authority;
7803
+ repair the recorded dependency or use the documented
7804
+ operator recovery path before planning again.
7805
+
7806
+ A stage-wide refresh still plans and authorizes every selected project. When one exact retained
7807
+ project plan proves an absent SST state that is unlocked, complete, empty, and drift-free, the
7808
+ root target records that project as a refresh no-op instead of invoking SST against a nonexistent
7809
+ stage. It also retains the pinned provider's exact SSM \`ebk:framework-version\` \`tagsAll\`
7810
+ normalization as a semantically inert observation after the same bounded shape
7811
+ repeats following refresh. Value changes, other tags, security findings, deletes, replacements,
7812
+ sensitive properties, and every other resource type remain material. Locked, incomplete, failed,
7813
+ or otherwise drifted plans are never skipped.
7814
+
7815
+ For a stage with no workload-role account baseline, publish the fixed Foundation-only candidate
7816
+ and invoke this root target with \`--phase=foundation-bootstrap\`. After Foundation outputs exist,
7817
+ publish the complete \`full-stage\` candidate and invoke the target without \`--phase\` (or with
7818
+ \`--phase=full-stage\`). The bootstrap phase selects exactly Foundation; it is not an arbitrary
7819
+ project filter.
7820
+
7465
7821
  Validation requires both an explicit production approval mode and an explicit Foundation
7466
7822
  profile. It does not infer omitted values. Use \`promotion-pr\` for a private GitHub Team
7467
7823
  repository. Use \`environment-reviewer\` only when the repository plan can enforce a separate
@@ -7475,10 +7831,16 @@ environment reviewer.
7475
7831
 
7476
7832
  Protected production deploy and refresh validate the complete Foundation/Slice set before AWS
7477
7833
  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,
7834
+ cannot be narrowed with an ad hoc project argument. The retained stage preview remains review
7835
+ evidence. Immediately before each project mutation the workflow computes one fresh plan against
7836
+ current state and applies it without a second redundant preview. Source, artifacts, configuration,
7837
+ policy, environment, and protected identities remain exact. Provider-only read/discard observations
7838
+ may appear or disappear, but any new delete, replacement, import, stateful or security change, IAM
7839
+ expansion, protection reduction, or unclassified resource stops for renewed approval. After an
7840
+ upstream mutation, every downstream project therefore receives a fresh independent risk decision.
7841
+
7842
+ \`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` keeps a smaller
7843
+ 0.5–4 ACU warm floor for starter production traffic. Both keep managed NAT,
7482
7844
  30-day backups, deletion protection, retained data, and the protected GitHub/OIDC/AWS path.
7483
7845
  Changing policy affects only later lifecycle requests; it never deploys or removes resources.
7484
7846
  Changing profile requires a separate reviewed Foundation deployment and retained operational
@@ -7549,7 +7911,7 @@ deploy or refresh before AWS access. Protected remove and unlock remain usable f
7549
7911
  There is no local or silent bypass in this prerelease contract; any future waiver mechanism
7550
7912
  must be committed, reviewable, and auditable.
7551
7913
  `,
7552
- 'ebk.repository-policy.json': json((0, index_js_4.createIncubationRepositoryPolicy)()),
7914
+ 'ebk.repository-policy.json': json(initialRepositoryPolicy),
7553
7915
  'nx.json': json({
7554
7916
  $schema: './node_modules/nx/schemas/nx-schema.json',
7555
7917
  defaultBase: 'develop',
@@ -7589,6 +7951,7 @@ must be committed, reviewable, and auditable.
7589
7951
  inputs: ['default', '^production'],
7590
7952
  },
7591
7953
  },
7954
+ analytics: false,
7592
7955
  neverConnectToCloud: true,
7593
7956
  useDaemonProcess: false,
7594
7957
  }),
@@ -7617,10 +7980,12 @@ must be committed, reviewable, and auditable.
7617
7980
  },
7618
7981
  devDependencies: {
7619
7982
  '@aws-sdk/client-iam': TOOL_VERSIONS.awsSdk,
7983
+ '@aws-sdk/client-rds': TOOL_VERSIONS.awsSdk,
7620
7984
  '@aws-sdk/client-s3': TOOL_VERSIONS.awsSdk,
7621
7985
  '@aws-sdk/client-secrets-manager': TOOL_VERSIONS.awsSdk,
7622
7986
  '@aws-sdk/client-ssm': TOOL_VERSIONS.awsSdk,
7623
7987
  '@aws-sdk/client-sts': TOOL_VERSIONS.awsSdk,
7988
+ '@aws-sdk/credential-providers': TOOL_VERSIONS.awsSdk,
7624
7989
  '@eslint/js': TOOL_VERSIONS.eslintJs,
7625
7990
  '@empire-builder-kit/nx': frameworkVersion,
7626
7991
  '@types/node': TOOL_VERSIONS.nodeTypes,
@@ -7704,6 +8069,13 @@ must be committed, reviewable, and auditable.
7704
8069
  executor: 'nx:run-commands',
7705
8070
  options: { command: 'node tools/install-ebk-hooks.mjs' },
7706
8071
  },
8072
+ 'deploy-local': {
8073
+ cache: false,
8074
+ executor: '@empire-builder-kit/nx:local-deploy',
8075
+ options: {
8076
+ stage: 'development',
8077
+ },
8078
+ },
7707
8079
  'supply-chain-check': {
7708
8080
  executor: 'nx:run-commands',
7709
8081
  options: {
@@ -7772,6 +8144,10 @@ Workspace creation does not deploy this project. Review \`docs/costs.md\`,
7772
8144
 
7773
8145
  Set \`EBK_ROOT_DOMAIN\` before deploying when Route 53 in the target account owns a public
7774
8146
  domain. Without it, the shared Router uses its functional provider URL.
8147
+
8148
+ Before deploying development or staging, add at least one exact operator/probe CIDR to
8149
+ \`security/router-waf.json\`. Production is public by default behind the same managed and
8150
+ rate-limiting baseline; select its allowlist mode explicitly when required.
7775
8151
  `,
7776
8152
  'packages/foundation/contracts/foundation.json': json({
7777
8153
  contractVersion: '1.0.0',
@@ -7831,8 +8207,8 @@ seeded decision record aligned with organization-specific budgets and alerts.
7831
8207
  - Development uses EC2 NAT, a development bastion, and Aurora scale-to-zero.
7832
8208
  - Staging uses EC2 NAT and Aurora scale-to-zero by default.
7833
8209
  - 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.
8210
+ Operating Policy profile selects either the standard 2-ACU Aurora floor or the
8211
+ cost-optimized 0.5-ACU warm floor.
7836
8212
  - ECS, EventBridge, Cognito, SNS, SSM, and CloudWatch are primarily usage-priced; logs,
7837
8213
  metrics, alarms, and retained data still require active cost review.
7838
8214
 
@@ -7850,8 +8226,69 @@ Use Nx for every supported lifecycle action:
7850
8226
  - \`pnpm nx run foundation:unlock --stage=development\`
7851
8227
  - \`pnpm nx run foundation:remove --stage=development\`
7852
8228
 
7853
- Do not invoke SST lifecycle commands directly. Production deployment belongs in the trusted
7854
- CI path.
8229
+ Do not invoke SST lifecycle commands directly. The root \`deploy-local\` target is the governed
8230
+ local path for stages admitted by the committed deployment profile. Its fixed
8231
+ \`foundation-bootstrap\` phase is the only local partial-stage path and exists solely for the
8232
+ documented first workload-role publication; isolated protected stages remain on the trusted
8233
+ GitHub path.
8234
+
8235
+ Every local invocation plans the complete selected stage but mutates at most one material
8236
+ project. Type the exact current \`approve:<action>:<stage>:<project>:<plan-digest>\` challenge in a
8237
+ real TTY after reviewing the displayed summary. A human or authorized agent may operate that TTY;
8238
+ unattended input is refused. The acknowledgement creates one ten-minute, one-use, private
8239
+ capability bound to source, artifacts, policy, environment, credentials, and current state. A
8240
+ completed deploy or refresh returns the controlled non-success \`replan-required\`; rerun the
8241
+ complete stage, review the new current summary, and continue one project at a time. Only a
8242
+ semantically converged complete stage reports success. No upstream mutation can authorize a
8243
+ downstream project. A complete, unlocked deploy plan containing only provider reads with no
8244
+ changed properties, replacement, failure, or unmatched risk finding is retained as observation
8245
+ evidence and counts as semantically converged. The standalone project planner may still return
8246
+ its conservative non-success for that drift observation; the root local transaction accepts only
8247
+ a freshly emitted exact provider-read-only record from that child. Repeated reads coalesce
8248
+ only when their logical ID and resource type match exactly. Any stale or ambiguous
8249
+ observation fails closed; a plan with desired changes remains material and requires review. A
8250
+ material downstream plan may also retain an upstream Foundation framework-version tag advance as
8251
+ observation only when both external tag
8252
+ projections, the retained identity, optional bounded SST name hydration, and one non-replacing
8253
+ provider read correlate exactly. Every other external property or uncorroborated read remains
8254
+ drift and blocks before the TTY challenge.
8255
+
8256
+ For an interrupted local apply, run the same root target with \`--action=refresh\`, review its
8257
+ current provider-refresh evidence, and type the displayed TTY challenge. Before mutation the
8258
+ executor writes and verifies a private state recovery point plus a secret-free digest/version
8259
+ manifest. The refresh preview validates its provider and later desired-event partitions; any
8260
+ ordinary preview failure or malformed post-refresh desired event remains blocked. Provider-only
8261
+ observation noise is retained without claiming a replayable preview. Because refresh changes
8262
+ recorded state rather than cloud resources, provider-output security and protection findings
8263
+ remain explicit review requirements under the drift decision. Refresh never continues into deploy
8264
+ or claims desired-state convergence: run the governed deploy target afterward and resolve every
8265
+ remaining source-versus-state change. Exclude the \`.private.json\` recovery point from archives.
8266
+
8267
+ The root refresh target includes every selected project in its exact reviewed plan set. It skips
8268
+ the SST child only when that project's retained plan proves an absent, unlocked, complete, empty,
8269
+ and drift-free state, or when the only retained difference is the pinned provider's exact repeated
8270
+ SSM \`ebk:framework-version\` \`tagsAll\` normalization. It records the project as an explicit refresh no-op
8271
+ in either case while retaining the observation. Value changes, other tags, security findings,
8272
+ deletes, replacements, sensitive
8273
+ properties, and every other resource type remain material; every other plan must execute or fail
8274
+ closed.
8275
+
8276
+ ## Router WAF
8277
+
8278
+ Each canonical stage owns one global-scope Web ACL attached to that stage's Foundation Router.
8279
+ The committed \`security/router-waf.json\` policy requires AWS Common Rule Set, Known Bad
8280
+ Inputs, Amazon IP Reputation, an IP rate limit, CloudWatch metrics, and sampled requests.
8281
+ Development and staging fail synthesis until their allowlists contain explicit CIDRs.
8282
+ Production is public by default and may be changed to an explicit allowlist.
8283
+
8284
+ For tuning, set a nonproduction stage to \`count\`, deploy and inspect sampled requests, then
8285
+ return staging to \`block\` and run the live allow/deny, managed-rule, rate-limit, metric, and
8286
+ sampled-request probes. Write their bounded, secret-free result to
8287
+ \`security/router-waf-live-evidence.json\`, then run
8288
+ \`pnpm nx run foundation:router-waf-rehearsal\`. The executor seals the exact policy digest
8289
+ into \`router-waf-blocking-rehearsal.json\`. Production synthesis rejects a missing, stale, or
8290
+ count-mode rehearsal. Full request logging is opt-in, blocked-only, retention-bounded, and
8291
+ redacts query strings plus credential-bearing headers.
7855
8292
 
7856
8293
  Foundation stages are canonical. \`ebk dev\` derives a Product Slice personal
7857
8294
  stage from the operating-system account and maps that consumer to the
@@ -7886,6 +8323,12 @@ connects as the Slice's no-password IAM-authenticated migrator user.
7886
8323
  stage-scoped SSM/secret path and least-privilege IAM.
7887
8324
  - Public domains are configured explicitly through \`EBK_ROOT_DOMAIN\`; generated source never
7888
8325
  contains credentials or customer domain assumptions.
8326
+ - Every stage's CloudFront Router has its own global Web ACL. The baseline always includes AWS
8327
+ Common Rule Set, Known Bad Inputs, Amazon IP Reputation, rate blocking, CloudWatch metrics,
8328
+ and sampled requests. Nonproduction is allowlisted by default. Production is public by
8329
+ default, but cannot deploy until the exact policy has passed a staging blocking rehearsal.
8330
+ - Full WAF request logging is disabled by default. Enabling it keeps blocked requests only,
8331
+ uses bounded retention, and redacts query strings, cookies, authorization, and API keys.
7889
8332
 
7890
8333
  The managed \`contracts/frontend-security-headers.json\` file owns the transport
7891
8334
  minimum: HTTPS in deployed environments, one-year HSTS there, and the common
@@ -7918,7 +8361,12 @@ declare const $app: {
7918
8361
  import {
7919
8362
  FOUNDATION_SSM_SCHEMA_VERSION,
7920
8363
  STANDARD_FOUNDATION_CAPABILITIES,
8364
+ createRouterWafRules,
8365
+ createRouterWafStagePlan,
8366
+ foundationDatabaseEndpointHost,
7921
8367
  foundationSsmPath,
8368
+ normalizeRouterWafPolicy,
8369
+ reconcileRouterWafBlockingRehearsal,
7922
8370
  type FoundationSsmKey,
7923
8371
  } from '@empire-builder-kit/nx/foundation';
7924
8372
  import {
@@ -7928,6 +8376,9 @@ import {
7928
8376
  } from '@empire-builder-kit/nx/repository-policy';
7929
8377
  import { createFoundationAccountCostControlPlan } from '@empire-builder-kit/nx/performance';
7930
8378
  import accountCostPolicy from '../performance/account-cost-policy.json';
8379
+ import routerWafBlockingRehearsal from '../security/router-waf-blocking-rehearsal.json';
8380
+ import routerWafLiveEvidence from '../security/router-waf-live-evidence.json';
8381
+ import routerWafPolicy from '../security/router-waf.json';
7931
8382
  import type { FoundationWorkloadRolePreflight } from './workload-roles/preflight.js';
7932
8383
 
7933
8384
  function identityDomainPrefix(stage: string, accountId: string): string {
@@ -7979,6 +8430,18 @@ export function foundation(
7979
8430
  )
7980
8431
  : undefined;
7981
8432
  const natMode = productionConfiguration?.contract.nat ?? 'ec2';
8433
+ const normalizedRouterWafPolicy = normalizeRouterWafPolicy(routerWafPolicy);
8434
+ const wafPlan = createRouterWafStagePlan({
8435
+ policy: normalizedRouterWafPolicy,
8436
+ rehearsal: isProduction
8437
+ ? reconcileRouterWafBlockingRehearsal({
8438
+ evidence: routerWafLiveEvidence,
8439
+ policy: normalizedRouterWafPolicy,
8440
+ receipt: routerWafBlockingRehearsal,
8441
+ })
8442
+ : null,
8443
+ stage: $app.stage,
8444
+ });
7982
8445
  const natBinding = workloadRoles?.bindings.nat;
7983
8446
  if (natMode === 'ec2' && natBinding) {
7984
8447
  $transform(
@@ -7988,6 +8451,9 @@ export function foundation(
7988
8451
  args.name = natBinding.instanceProfileName;
7989
8452
  args.role = natBinding.roleName;
7990
8453
  opts.import = natBinding.instanceProfileName;
8454
+ opts.ignoreChanges = [
8455
+ ...new Set([...(opts.ignoreChanges ?? []), 'tags', 'tagsAll']),
8456
+ ];
7991
8457
  },
7992
8458
  );
7993
8459
  }
@@ -8009,8 +8475,83 @@ export function foundation(
8009
8475
  aliases: ['*.' + rootDomain],
8010
8476
  dns: sst.aws.dns(),
8011
8477
  },
8012
- }
8478
+ }
8013
8479
  : {}),
8480
+ waf: {
8481
+ logging: wafPlan.logging,
8482
+ managedRules: {
8483
+ coreRuleSet: true,
8484
+ knownBadInputs: true,
8485
+ sqlInjection: false,
8486
+ },
8487
+ rateLimitPerIp: wafPlan.rateLimitPerIp,
8488
+ },
8489
+ transform: {
8490
+ cdn(args: any) {
8491
+ if (!args.webAclArn) {
8492
+ throw new Error(
8493
+ 'Foundation Router must attach its exact global Web ACL.',
8494
+ );
8495
+ }
8496
+ },
8497
+ waf(args: any, opts: any) {
8498
+ if (args.scope !== 'CLOUDFRONT') {
8499
+ throw new Error(
8500
+ 'Foundation Router Web ACL must use global CloudFront scope.',
8501
+ );
8502
+ }
8503
+ const ipSets: { ipv4Arn?: any; ipv6Arn?: any } = {};
8504
+ if (wafPlan.ipv4Cidrs.length > 0) {
8505
+ ipSets.ipv4Arn = new aws.wafv2.IpSet(
8506
+ 'FoundationRouterIpv4Allowlist',
8507
+ {
8508
+ addresses: wafPlan.ipv4Cidrs,
8509
+ ipAddressVersion: 'IPV4',
8510
+ name: 'foundation-' + wafPlan.stage + '-router-ipv4-allowlist',
8511
+ scope: 'CLOUDFRONT',
8512
+ tags: {
8513
+ 'ebk:policy-digest': wafPlan.policyDigest,
8514
+ 'ebk:stage': wafPlan.stage,
8515
+ },
8516
+ },
8517
+ { provider: opts.provider },
8518
+ ).arn;
8519
+ }
8520
+ if (wafPlan.ipv6Cidrs.length > 0) {
8521
+ ipSets.ipv6Arn = new aws.wafv2.IpSet(
8522
+ 'FoundationRouterIpv6Allowlist',
8523
+ {
8524
+ addresses: wafPlan.ipv6Cidrs,
8525
+ ipAddressVersion: 'IPV6',
8526
+ name: 'foundation-' + wafPlan.stage + '-router-ipv6-allowlist',
8527
+ scope: 'CLOUDFRONT',
8528
+ tags: {
8529
+ 'ebk:policy-digest': wafPlan.policyDigest,
8530
+ 'ebk:stage': wafPlan.stage,
8531
+ },
8532
+ },
8533
+ { provider: opts.provider },
8534
+ ).arn;
8535
+ }
8536
+ args.defaultAction = { allow: {} };
8537
+ args.rules = createRouterWafRules(wafPlan, ipSets);
8538
+ args.scope = 'CLOUDFRONT';
8539
+ args.tags = {
8540
+ ...(args.tags ?? {}),
8541
+ 'ebk:policy-digest': wafPlan.policyDigest,
8542
+ 'ebk:stage': wafPlan.stage,
8543
+ };
8544
+ args.visibilityConfig = {
8545
+ cloudwatchMetricsEnabled: true,
8546
+ metricName:
8547
+ 'Ebk' +
8548
+ wafPlan.stage[0].toUpperCase() +
8549
+ wafPlan.stage.slice(1) +
8550
+ 'FoundationRouterWaf',
8551
+ sampledRequestsEnabled: true,
8552
+ };
8553
+ },
8554
+ },
8014
8555
  });
8015
8556
  const database = new sst.aws.Aurora('FoundationDatabase', {
8016
8557
  // SQL bootstrap and migrations run inside the VPC. Keep the public HTTPS
@@ -8034,6 +8575,46 @@ export function foundation(
8034
8575
  },
8035
8576
  vpc,
8036
8577
  });
8578
+ const databaseClusterEndpoint = database.nodes.cluster.endpoint.apply(
8579
+ foundationDatabaseEndpointHost,
8580
+ );
8581
+ const databaseReaderEndpoint = database.nodes.cluster.readerEndpoint.apply(
8582
+ foundationDatabaseEndpointHost,
8583
+ );
8584
+ // SST v4.17.1 creates a top-level LinkRef on the next tick and its Aurora
8585
+ // implementation unconditionally splits cluster endpoints. During a
8586
+ // governed refresh after an interrupted first deploy, those provider
8587
+ // outputs are legitimately absent. Preserve the normal link contract while
8588
+ // keeping that read-only recovery program total; a real deploy resolves the
8589
+ // exact endpoints before any discovery resource is created.
8590
+ database.getSSTLink = () => ({
8591
+ include: [
8592
+ sst.aws.permission({
8593
+ actions: ['secretsmanager:GetSecretValue'],
8594
+ resources: [database.secretArn],
8595
+ }),
8596
+ sst.aws.permission({
8597
+ actions: [
8598
+ 'rds-data:BatchExecuteStatement',
8599
+ 'rds-data:BeginTransaction',
8600
+ 'rds-data:CommitTransaction',
8601
+ 'rds-data:ExecuteStatement',
8602
+ 'rds-data:RollbackTransaction',
8603
+ ],
8604
+ resources: [database.clusterArn],
8605
+ }),
8606
+ ],
8607
+ properties: {
8608
+ clusterArn: database.clusterArn,
8609
+ database: database.database,
8610
+ host: databaseClusterEndpoint,
8611
+ password: database.password,
8612
+ port: database.port,
8613
+ reader: databaseReaderEndpoint,
8614
+ secretArn: database.secretArn,
8615
+ username: database.username,
8616
+ },
8617
+ });
8037
8618
  const foundationAccountId = aws.getCallerIdentityOutput({}).accountId;
8038
8619
  const foundationPartition = aws.getPartitionOutput({});
8039
8620
  const foundationRegion = aws.getRegionOutput({});
@@ -8096,6 +8677,7 @@ export function foundation(
8096
8677
  },
8097
8678
  ],
8098
8679
  resources: [alertTopic.arn],
8680
+ sid: 'AllowBudgetAlerts',
8099
8681
  },
8100
8682
  {
8101
8683
  actions: ['sns:Publish'],
@@ -8109,6 +8691,7 @@ export function foundation(
8109
8691
  type: 'Service',
8110
8692
  }],
8111
8693
  resources: [alertTopic.arn],
8694
+ sid: 'AllowCostAnomalyAlerts',
8112
8695
  },
8113
8696
  {
8114
8697
  actions: ['sns:Publish'],
@@ -8131,6 +8714,7 @@ export function foundation(
8131
8714
  type: 'Service',
8132
8715
  }],
8133
8716
  resources: [alertTopic.arn],
8717
+ sid: 'AllowRdsEventAlerts',
8134
8718
  },
8135
8719
  {
8136
8720
  actions: ['sns:Publish'],
@@ -8155,6 +8739,7 @@ export function foundation(
8155
8739
  },
8156
8740
  ],
8157
8741
  resources: [alertTopic.arn],
8742
+ sid: 'AllowCloudWatchAlarmAlerts',
8158
8743
  },
8159
8744
  ],
8160
8745
  }).json,
@@ -8183,6 +8768,41 @@ export function foundation(
8183
8768
  { dependsOn: [alertTopicPolicy] },
8184
8769
  )
8185
8770
  : undefined;
8771
+ const accountCostAnomalyMonitor = isProduction
8772
+ ? new aws.costexplorer.AnomalyMonitor(
8773
+ 'FoundationAccountCostAnomalyMonitor',
8774
+ {
8775
+ monitorDimension: 'SERVICE',
8776
+ monitorType: 'DIMENSIONAL',
8777
+ name: '${name}-account-service-cost-anomalies',
8778
+ tags: {
8779
+ 'ebk:cost-policy-digest': accountCostControl.policyDigest,
8780
+ 'ebk:cost-scope': '${name}/account/production',
8781
+ 'ebk:owner': accountCostControl.owner,
8782
+ },
8783
+ },
8784
+ )
8785
+ : undefined;
8786
+ const accountCostAnomalySubscription =
8787
+ accountCostControl.anomaly && accountCostAnomalyMonitor
8788
+ ? new aws.costexplorer.AnomalySubscription(
8789
+ 'FoundationAccountCostAnomalySubscription',
8790
+ {
8791
+ frequency: accountCostControl.anomaly.subscription.frequency,
8792
+ monitorArnLists: [accountCostAnomalyMonitor.arn],
8793
+ name: accountCostControl.anomaly.subscription.name,
8794
+ subscribers: [{ address: alertTopic.arn, type: 'SNS' }],
8795
+ tags: {
8796
+ 'ebk:cost-policy-digest': accountCostControl.policyDigest,
8797
+ 'ebk:cost-scope': '${name}/account/production',
8798
+ 'ebk:owner': accountCostControl.owner,
8799
+ },
8800
+ thresholdExpression:
8801
+ accountCostControl.anomaly.subscription.thresholdExpression,
8802
+ },
8803
+ { dependsOn: [accountCostAnomalyMonitor, alertTopicPolicy] },
8804
+ )
8805
+ : undefined;
8186
8806
  const databaseEvents = isProduction
8187
8807
  ? new aws.rds.EventSubscription(
8188
8808
  'FoundationDatabaseEvents',
@@ -8285,12 +8905,10 @@ export function foundation(
8285
8905
  clusterId: cluster.id,
8286
8906
  contractVersion: '1.0.0',
8287
8907
  databaseClusterArn: database.clusterArn,
8288
- databaseClusterEndpoint: database.nodes.cluster.endpoint.apply(
8289
- (endpoint: string) => endpoint.split(':')[0],
8290
- ),
8908
+ databaseClusterEndpoint,
8291
8909
  databaseClusterId: database.id,
8292
8910
  databaseClusterResourceId: database.nodes.cluster.clusterResourceId,
8293
- databaseHost: database.host,
8911
+ databaseHost: databaseClusterEndpoint,
8294
8912
  databaseMasterSecretArn: database.secretArn,
8295
8913
  databasePort: database.port.apply(String),
8296
8914
  eventBusArn: eventBus.arn,
@@ -8312,6 +8930,7 @@ export function foundation(
8312
8930
  capabilities: STANDARD_FOUNDATION_CAPABILITIES,
8313
8931
  clusterId: cluster.id,
8314
8932
  contractVersion: '1.0.0',
8933
+ costAnomalySubscriptionArn: accountCostAnomalySubscription?.arn,
8315
8934
  databaseCapacityAlarmName: databaseCapacityAlarm.name,
8316
8935
  dashboardName: dashboard.dashboardName,
8317
8936
  databaseClusterId: database.id,
@@ -8343,13 +8962,49 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8343
8962
  actualNotificationThresholdPercent: 80,
8344
8963
  forecastNotificationThresholdPercent: 100,
8345
8964
  kind: 'ebk-foundation-account-cost-policy',
8346
- monthlyMaximumUsd: 1_000,
8965
+ monthlyMaximumUsd: deploymentProfile === 'starter' ? 250 : 1_000,
8347
8966
  owner: 'replace-with-owner',
8348
8967
  reviewAt: null,
8349
8968
  schemaVersion: 1,
8350
8969
  status: 'example',
8351
8970
  workspace: name,
8352
8971
  }),
8972
+ 'packages/foundation/security/router-waf.json': json({
8973
+ kind: 'ebk-router-waf-policy',
8974
+ logging: {
8975
+ enabled: false,
8976
+ retention: '1 week',
8977
+ },
8978
+ rateLimitPerIp: 2_000,
8979
+ schemaVersion: 1,
8980
+ stages: {
8981
+ development: {
8982
+ access: 'allowlist',
8983
+ allowedCidrs: [],
8984
+ mode: 'block',
8985
+ },
8986
+ production: {
8987
+ access: 'public',
8988
+ allowedCidrs: [],
8989
+ mode: 'block',
8990
+ },
8991
+ staging: {
8992
+ access: 'allowlist',
8993
+ allowedCidrs: [],
8994
+ mode: 'block',
8995
+ },
8996
+ },
8997
+ }),
8998
+ 'packages/foundation/security/router-waf-blocking-rehearsal.json': json({
8999
+ kind: 'ebk-router-waf-blocking-rehearsal',
9000
+ schemaVersion: 1,
9001
+ status: 'pending',
9002
+ }),
9003
+ 'packages/foundation/security/router-waf-live-evidence.json': json({
9004
+ kind: 'ebk-router-waf-live-evidence',
9005
+ schemaVersion: 1,
9006
+ status: 'pending',
9007
+ }),
8353
9008
  'packages/foundation/configuration/declarations.json': json({
8354
9009
  project: 'foundation',
8355
9010
  schemaVersion: 1,
@@ -8382,29 +9037,96 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8382
9037
  version: '4.17.1',
8383
9038
  },
8384
9039
  policy: {
9040
+ adoptedImports: [
9041
+ {
9042
+ authority: 'workload-role-account-baseline',
9043
+ logicalId: 'FoundationVpcNatInstanceProfile',
9044
+ privateIdentifierTemplate: 'ebk-{stage}-foundation-nat-profile',
9045
+ recoveryRunbookRef: 'docs/workload-role-account-baseline.md',
9046
+ removalBehavior: 'retain',
9047
+ resourceType: 'aws:iam/instanceProfile:InstanceProfile',
9048
+ },
9049
+ ],
8385
9050
  additiveLowRiskTypes: [
8386
9051
  'aws:cloudwatch/dashboard:Dashboard',
8387
9052
  'aws:cloudwatch/logGroup:LogGroup',
8388
9053
  'aws:cloudwatch/metricAlarm:MetricAlarm',
8389
9054
  ],
9055
+ additiveReviewedTypes: [
9056
+ 'aws:cloudwatch/eventArchive:EventArchive',
9057
+ 'aws:cloudwatch/eventBus:EventBus',
9058
+ 'aws:cognito/userPoolDomain:UserPoolDomain',
9059
+ 'aws:ec2/defaultSecurityGroup:DefaultSecurityGroup',
9060
+ 'aws:ec2/internetGateway:InternetGateway',
9061
+ 'aws:ec2/routeTable:RouteTable',
9062
+ 'aws:ec2/routeTableAssociation:RouteTableAssociation',
9063
+ 'aws:ec2/securityGroup:SecurityGroup',
9064
+ 'aws:ec2/subnet:Subnet',
9065
+ 'aws:ec2/vpc:Vpc',
9066
+ 'aws:ecs/cluster:Cluster',
9067
+ 'aws:ecs/clusterCapacityProviders:ClusterCapacityProviders',
9068
+ 'aws:rds/clusterParameterGroup:ClusterParameterGroup',
9069
+ 'aws:rds/eventSubscription:EventSubscription',
9070
+ 'aws:rds/parameterGroup:ParameterGroup',
9071
+ 'aws:rds/subnetGroup:SubnetGroup',
9072
+ 'aws:servicediscovery/privateDnsNamespace:PrivateDnsNamespace',
9073
+ 'aws:sns/topic:Topic',
9074
+ 'aws:sns/topicPolicy:TopicPolicy',
9075
+ 'aws:wafv2/webAclLoggingConfiguration:WebAclLoggingConfiguration',
9076
+ 'pulumi-nodejs:dynamic:Resource',
9077
+ 'pulumi:providers:aws',
9078
+ 'pulumi:providers:pulumi-nodejs',
9079
+ 'pulumi:providers:random',
9080
+ 'pulumi:pulumi:Stack',
9081
+ 'sst:aws:Aurora',
9082
+ 'sst:aws:Bus',
9083
+ 'sst:aws:CDN',
9084
+ 'sst:aws:Cluster',
9085
+ 'sst:aws:CognitoUserPool',
9086
+ 'sst:aws:Router',
9087
+ 'sst:aws:SnsTopic',
9088
+ 'sst:aws:Vpc',
9089
+ 'sst:sst:LinkRef',
9090
+ 'sst:sst:Version',
9091
+ ],
8390
9092
  costOrQuotaTypes: [
8391
9093
  'aws:apigatewayv2/api:Api',
8392
9094
  'aws:budgets/budget:Budget',
9095
+ 'aws:cloudfront/cachePolicy:CachePolicy',
8393
9096
  'aws:cloudfront/distribution:Distribution',
9097
+ 'aws:cloudfront/function:Function',
9098
+ 'aws:cloudfront/keyValueStore:KeyValueStore',
9099
+ 'aws:costexplorer/anomalyMonitor:AnomalyMonitor',
9100
+ 'aws:costexplorer/anomalySubscription:AnomalySubscription',
8394
9101
  'aws:cognito/userPool:UserPool',
9102
+ 'aws:ec2/eip:Eip',
9103
+ 'aws:ec2/eipAssociation:EipAssociation',
9104
+ 'aws:ec2/instance:Instance',
8395
9105
  'aws:ec2/natGateway:NatGateway',
8396
9106
  'aws:rds/cluster:Cluster',
9107
+ 'aws:rds/clusterInstance:ClusterInstance',
9108
+ 'aws:wafv2/ipSet:IpSet',
9109
+ 'aws:wafv2/webAcl:WebAcl',
8397
9110
  ],
8398
9111
  failClosedOnUnknown: true,
8399
9112
  protectedPlanAuthority: 'deploy-role-residual',
8400
9113
  standingPolicyRiskClasses: ['no-change', 'additive-low-risk'],
8401
9114
  statefulTypes: [
9115
+ 'aws:cloudwatch/eventArchive:EventArchive',
9116
+ 'aws:cloudwatch/eventBus:EventBus',
8402
9117
  'aws:cognito/userPool:UserPool',
8403
9118
  'aws:dynamodb/table:Table',
8404
9119
  'aws:events/eventBus:EventBus',
8405
9120
  'aws:kms/key:Key',
8406
9121
  'aws:rds/cluster:Cluster',
8407
9122
  'aws:s3/bucket:Bucket',
9123
+ 'aws:secretsmanager/secret:Secret',
9124
+ 'aws:secretsmanager/secretVersion:SecretVersion',
9125
+ 'aws:sns/topic:Topic',
9126
+ 'aws:ssm/parameter:Parameter',
9127
+ 'aws:wafv2/ipSet:IpSet',
9128
+ 'aws:wafv2/webAcl:WebAcl',
9129
+ 'random:index/randomPassword:RandomPassword',
8408
9130
  ],
8409
9131
  },
8410
9132
  project: 'foundation',
@@ -8502,6 +9224,13 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8502
9224
  executor: '@empire-builder-kit/nx:foundation-profile-preflight',
8503
9225
  options: { stage: 'development' },
8504
9226
  },
9227
+ 'router-waf-rehearsal': {
9228
+ cache: false,
9229
+ executor: '@empire-builder-kit/nx:router-waf-rehearsal',
9230
+ outputs: [
9231
+ `{projectRoot}/security/router-waf-blocking-rehearsal.json`,
9232
+ ],
9233
+ },
8505
9234
  typecheck: {
8506
9235
  executor: 'nx:run-commands',
8507
9236
  options: {
@@ -8529,6 +9258,11 @@ export function app(workloadRoles?: FoundationWorkloadRolePreflight) {
8529
9258
  }),
8530
9259
  'packages/foundation/scripts/verify-foundation.mjs': `import { readFileSync } from 'node:fs';
8531
9260
 
9261
+ import {
9262
+ normalizeRouterWafPolicy,
9263
+ reconcileRouterWafBlockingRehearsal,
9264
+ } from '@empire-builder-kit/nx/foundation';
9265
+
8532
9266
  const descriptor = JSON.parse(
8533
9267
  readFileSync(new URL('../contracts/foundation.json', import.meta.url), 'utf8'),
8534
9268
  );
@@ -8544,6 +9278,33 @@ const frontendSecurityHeaders = JSON.parse(
8544
9278
  'utf8',
8545
9279
  ),
8546
9280
  );
9281
+ const routerWafPolicy = normalizeRouterWafPolicy(
9282
+ JSON.parse(
9283
+ readFileSync(
9284
+ new URL('../security/router-waf.json', import.meta.url),
9285
+ 'utf8',
9286
+ ),
9287
+ ),
9288
+ );
9289
+ reconcileRouterWafBlockingRehearsal({
9290
+ evidence: JSON.parse(
9291
+ readFileSync(
9292
+ new URL('../security/router-waf-live-evidence.json', import.meta.url),
9293
+ 'utf8',
9294
+ ),
9295
+ ),
9296
+ policy: routerWafPolicy,
9297
+ receipt:
9298
+ JSON.parse(
9299
+ readFileSync(
9300
+ new URL(
9301
+ '../security/router-waf-blocking-rehearsal.json',
9302
+ import.meta.url,
9303
+ ),
9304
+ 'utf8',
9305
+ ),
9306
+ ),
9307
+ });
8547
9308
  const expectedCapabilities = [
8548
9309
  'network',
8549
9310
  'routing',
@@ -8559,6 +9320,13 @@ const expectedCapabilities = [
8559
9320
  if (descriptor.contractVersion !== '1.0.0') {
8560
9321
  throw new Error('Foundation contractVersion must be 1.0.0.');
8561
9322
  }
9323
+ if (
9324
+ routerWafPolicy.stages.development.access !== 'allowlist' ||
9325
+ routerWafPolicy.stages.staging.access !== 'allowlist' ||
9326
+ routerWafPolicy.stages.production.mode !== 'block'
9327
+ ) {
9328
+ throw new Error('Foundation Router WAF stage defaults are invalid.');
9329
+ }
8562
9330
  if (JSON.stringify(descriptor.capabilities) !== JSON.stringify(expectedCapabilities)) {
8563
9331
  throw new Error('Foundation capability descriptor does not match the v1 standard.');
8564
9332
  }
@@ -8567,7 +9335,7 @@ if (
8567
9335
  costs.aurora?.engineVersion !== '${index_js_4.FOUNDATION_AURORA_ENGINE_VERSION}' ||
8568
9336
  costs.productionProfileSource !== 'ebk.repository-policy.json' ||
8569
9337
  costs.productionProfiles?.standard?.aurora?.min !== '2 ACU' ||
8570
- costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0 ACU' ||
9338
+ costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0.5 ACU' ||
8571
9339
  !costs.stages?.development ||
8572
9340
  !costs.stages?.staging ||
8573
9341
  costs.stages?.production?.dataRetention !== 'retained-and-protected'
@@ -8623,16 +9391,15 @@ export default $config({
8623
9391
  providers: {
8624
9392
  aws: {
8625
9393
  defaultTags: {
9394
+ // SST contributes sst:app and sst:stage. Keep the provider baseline
9395
+ // at eight tags so generated S3 asset objects remain within AWS's
9396
+ // hard ten-tag object limit. Richer performance tags stay explicit.
8626
9397
  tags: {
8627
9398
  'ebk:cost-allocation': 'foundation',
8628
9399
  'ebk:cost-scope': '${name}/foundation/' + stage,
8629
9400
  'ebk:framework-version': '${frameworkVersion}',
8630
9401
  'ebk:managed-by': 'empire-builder-kit',
8631
9402
  '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
9403
  'ebk:slice': 'foundation',
8637
9404
  'ebk:stage': stage,
8638
9405
  'ebk:workspace': '${name}',