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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/executors.json +10 -0
  2. package/package.json +4 -4
  3. package/src/cli/output.js +1 -1
  4. package/src/developer-agent/model.js +4 -4
  5. package/src/developer-agent/model.js.map +1 -1
  6. package/src/developer-agent/operation-registry.js +67 -10
  7. package/src/developer-agent/operation-registry.js.map +1 -1
  8. package/src/developer-agent/planning.js +7 -9
  9. package/src/developer-agent/planning.js.map +1 -1
  10. package/src/developer-agent/workspace-inspection.js +12 -0
  11. package/src/developer-agent/workspace-inspection.js.map +1 -1
  12. package/src/executors/blueprint-trust/blueprint-trust.js +6 -0
  13. package/src/executors/blueprint-trust/blueprint-trust.js.map +1 -1
  14. package/src/executors/deployment-plan/deployment-plan.js +5 -0
  15. package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
  16. package/src/executors/foundation-preflight/foundation-preflight.d.ts +2 -0
  17. package/src/executors/foundation-preflight/foundation-preflight.js +6 -2
  18. package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -1
  19. package/src/executors/foundation-preflight/schema.json +10 -0
  20. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.d.ts +2 -0
  21. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js +2 -1
  22. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js.map +1 -1
  23. package/src/executors/foundation-profile-preflight/schema.json +19 -3
  24. package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
  25. package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
  26. package/src/executors/infrastructure-plan/infrastructure-plan.d.ts +229 -5
  27. package/src/executors/infrastructure-plan/infrastructure-plan.js +2298 -175
  28. package/src/executors/infrastructure-plan/infrastructure-plan.js.map +1 -1
  29. package/src/executors/infrastructure-plan/schema.json +16 -0
  30. package/src/executors/local-deploy/fixtures/local-deploy-state-machine.mjs +53 -0
  31. package/src/executors/local-deploy/local-approval.d.ts +23 -0
  32. package/src/executors/local-deploy/local-approval.js +43 -0
  33. package/src/executors/local-deploy/local-approval.js.map +1 -0
  34. package/src/executors/local-deploy/local-deploy.d.ts +69 -0
  35. package/src/executors/local-deploy/local-deploy.js +911 -0
  36. package/src/executors/local-deploy/local-deploy.js.map +1 -0
  37. package/src/executors/local-deploy/schema.json +42 -0
  38. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.d.ts +3 -0
  39. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js +94 -0
  40. package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js.map +1 -0
  41. package/src/executors/router-waf-rehearsal/schema.json +9 -0
  42. package/src/executors/secret-metadata/schema.json +5 -0
  43. package/src/executors/secret-metadata/secret-metadata.d.ts +12 -1
  44. package/src/executors/secret-metadata/secret-metadata.js +68 -16
  45. package/src/executors/secret-metadata/secret-metadata.js.map +1 -1
  46. package/src/executors/sst-lifecycle/logging-preflight.d.ts +5 -0
  47. package/src/executors/sst-lifecycle/logging-preflight.js +23 -13
  48. package/src/executors/sst-lifecycle/logging-preflight.js.map +1 -1
  49. package/src/executors/sst-lifecycle/run-sst-command.d.ts +8 -1
  50. package/src/executors/sst-lifecycle/run-sst-command.js +144 -12
  51. package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
  52. package/src/executors/sst-lifecycle/schema.json +13 -1
  53. package/src/executors/sst-lifecycle/sst-environment-source.d.ts +25 -0
  54. package/src/executors/sst-lifecycle/sst-environment-source.js +213 -0
  55. package/src/executors/sst-lifecycle/sst-environment-source.js.map +1 -0
  56. package/src/foundation/index.d.ts +2 -0
  57. package/src/foundation/index.js +2 -0
  58. package/src/foundation/index.js.map +1 -1
  59. package/src/foundation/logging-preflight.js +1 -1
  60. package/src/foundation/logging-preflight.js.map +1 -1
  61. package/src/foundation/logging.d.ts +3 -1
  62. package/src/foundation/logging.js +1 -1
  63. package/src/foundation/logging.js.map +1 -1
  64. package/src/foundation/partial-state.d.ts +10 -0
  65. package/src/foundation/partial-state.js +21 -0
  66. package/src/foundation/partial-state.js.map +1 -0
  67. package/src/foundation/router-waf.d.ts +98 -0
  68. package/src/foundation/router-waf.js +401 -0
  69. package/src/foundation/router-waf.js.map +1 -0
  70. package/src/generators/function/function.js +2 -1
  71. package/src/generators/function/function.js.map +1 -1
  72. package/src/generators/post-generation-install.d.ts +1 -0
  73. package/src/generators/post-generation-install.js +134 -67
  74. package/src/generators/post-generation-install.js.map +1 -1
  75. package/src/generators/preset/generator.js +23 -3
  76. package/src/generators/preset/generator.js.map +1 -1
  77. package/src/generators/preset/schema.d.ts +7 -0
  78. package/src/generators/preset/schema.json +47 -0
  79. package/src/generators/preset/tool-versions.d.ts +5 -5
  80. package/src/generators/preset/tool-versions.js +4 -4
  81. package/src/generators/preset/workspace-files-ci-acceptance.d.ts +1 -0
  82. package/src/generators/preset/workspace-files-ci-acceptance.js +791 -0
  83. package/src/generators/preset/workspace-files-ci-acceptance.js.map +1 -0
  84. package/src/generators/preset/workspace-files-developer-agent.d.ts +2 -0
  85. package/src/generators/preset/workspace-files-developer-agent.js +25 -11
  86. package/src/generators/preset/workspace-files-developer-agent.js.map +1 -1
  87. package/src/generators/preset/workspace-files-foundation-workload-roles.js +5 -2
  88. package/src/generators/preset/workspace-files-foundation-workload-roles.js.map +1 -1
  89. package/src/generators/preset/workspace-files-performance-load.js +2 -1
  90. package/src/generators/preset/workspace-files-performance-load.js.map +1 -1
  91. package/src/generators/preset/workspace-files-workload-role-baseline.js +254 -42
  92. package/src/generators/preset/workspace-files-workload-role-baseline.js.map +1 -1
  93. package/src/generators/preset/workspace-files.d.ts +8 -4
  94. package/src/generators/preset/workspace-files.js +1039 -68
  95. package/src/generators/preset/workspace-files.js.map +1 -1
  96. package/src/generators/repository-policy/repository-policy.js +65 -3
  97. package/src/generators/repository-policy/repository-policy.js.map +1 -1
  98. package/src/generators/repository-policy/schema.d.ts +6 -0
  99. package/src/generators/repository-policy/schema.json +46 -0
  100. package/src/generators/slice/built-in-plan-api-files.js +2 -1
  101. package/src/generators/slice/built-in-plan-api-files.js.map +1 -1
  102. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +19 -1
  103. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js.map +1 -1
  104. package/src/generators/slice/built-in-plan-data-files.js +16 -5
  105. package/src/generators/slice/built-in-plan-data-files.js.map +1 -1
  106. package/src/generators/slice/built-in-plan-data-retention-job-files.js +10 -6
  107. package/src/generators/slice/built-in-plan-data-retention-job-files.js.map +1 -1
  108. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +25 -10
  109. package/src/generators/slice/built-in-plan-database-lifecycle-files.js.map +1 -1
  110. package/src/generators/slice/built-in-plan-foundation-files.js +2 -3
  111. package/src/generators/slice/built-in-plan-foundation-files.js.map +1 -1
  112. package/src/generators/slice/built-in-plan-infrastructure-files.js +364 -29
  113. package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -1
  114. package/src/generators/slice/built-in-plan-infrastructure-safety-files.js +67 -11
  115. package/src/generators/slice/built-in-plan-infrastructure-safety-files.js.map +1 -1
  116. package/src/generators/slice/built-in-plan-metadata-files.js +1 -0
  117. package/src/generators/slice/built-in-plan-metadata-files.js.map +1 -1
  118. package/src/generators/slice/built-in-plan-operations-files.js +442 -5
  119. package/src/generators/slice/built-in-plan-operations-files.js.map +1 -1
  120. package/src/generators/slice/built-in-plan-performance-files.d.ts +2 -1
  121. package/src/generators/slice/built-in-plan-performance-files.js +41 -47
  122. package/src/generators/slice/built-in-plan-performance-files.js.map +1 -1
  123. package/src/generators/slice/built-in-plan-project-files.js +14 -1
  124. package/src/generators/slice/built-in-plan-project-files.js.map +1 -1
  125. package/src/generators/slice/built-in-plan-release-files.js +235 -56
  126. package/src/generators/slice/built-in-plan-release-files.js.map +1 -1
  127. package/src/generators/slice/built-in-plan-testing-files.js +92 -5
  128. package/src/generators/slice/built-in-plan-testing-files.js.map +1 -1
  129. package/src/generators/slice/built-in-plan-web-files.js +13 -0
  130. package/src/generators/slice/built-in-plan-web-files.js.map +1 -1
  131. package/src/generators/slice/built-in-plan-workload-role-files.js +5 -2
  132. package/src/generators/slice/built-in-plan-workload-role-files.js.map +1 -1
  133. package/src/generators/slice/built-in-plan.d.ts +2 -1
  134. package/src/generators/slice/built-in-plan.js +8 -5
  135. package/src/generators/slice/built-in-plan.js.map +1 -1
  136. package/src/generators/slice/slice.js +11 -3
  137. package/src/generators/slice/slice.js.map +1 -1
  138. package/src/generators/upgrade/upgrade.js +35 -0
  139. package/src/generators/upgrade/upgrade.js.map +1 -1
  140. package/src/generators/workload/frozen-container.d.ts +3 -2
  141. package/src/generators/workload/frozen-container.js +6 -4
  142. package/src/generators/workload/frozen-container.js.map +1 -1
  143. package/src/generators/workload/workload.js +2 -2
  144. package/src/generators/workload/workload.js.map +1 -1
  145. package/src/git-policy/github-bootstrap.d.ts +3 -1
  146. package/src/git-policy/github-bootstrap.js +22 -5
  147. package/src/git-policy/github-bootstrap.js.map +1 -1
  148. package/src/git-policy/routing.d.ts +2 -0
  149. package/src/git-policy/routing.js +12 -1
  150. package/src/git-policy/routing.js.map +1 -1
  151. package/src/infrastructure-change/local-capability.d.ts +73 -0
  152. package/src/infrastructure-change/local-capability.js +114 -0
  153. package/src/infrastructure-change/local-capability.js.map +1 -0
  154. package/src/infrastructure-change/model.d.ts +25 -4
  155. package/src/infrastructure-change/model.js +1 -0
  156. package/src/infrastructure-change/model.js.map +1 -1
  157. package/src/infrastructure-change/normalize.d.ts +4 -0
  158. package/src/infrastructure-change/normalize.js +47 -7
  159. package/src/infrastructure-change/normalize.js.map +1 -1
  160. package/src/infrastructure-change/plan.d.ts +30 -3
  161. package/src/infrastructure-change/plan.js +323 -13
  162. package/src/infrastructure-change/plan.js.map +1 -1
  163. package/src/infrastructure-change/risk.d.ts +3 -1
  164. package/src/infrastructure-change/risk.js +96 -16
  165. package/src/infrastructure-change/risk.js.map +1 -1
  166. package/src/local-dev/environment-inspector.d.ts +1 -0
  167. package/src/local-dev/environment-inspector.js +21 -9
  168. package/src/local-dev/environment-inspector.js.map +1 -1
  169. package/src/local-dev/private-state.d.ts +2 -0
  170. package/src/local-dev/private-state.js +3 -1
  171. package/src/local-dev/private-state.js.map +1 -1
  172. package/src/performance/cost-controls.d.ts +5 -3
  173. package/src/performance/cost-controls.js +30 -24
  174. package/src/performance/cost-controls.js.map +1 -1
  175. package/src/performance/model.d.ts +1 -1
  176. package/src/performance/model.js +0 -2
  177. package/src/performance/model.js.map +1 -1
  178. package/src/performance/test-fixtures.js +0 -2
  179. package/src/performance/test-fixtures.js.map +1 -1
  180. package/src/release/source-authority.d.ts +1 -0
  181. package/src/release/source-authority.js +136 -9
  182. package/src/release/source-authority.js.map +1 -1
  183. package/src/repository-policy/deployment-plan.d.ts +4 -1
  184. package/src/repository-policy/deployment-plan.js +8 -0
  185. package/src/repository-policy/deployment-plan.js.map +1 -1
  186. package/src/repository-policy/foundation-profile.js +1 -2
  187. package/src/repository-policy/foundation-profile.js.map +1 -1
  188. package/src/repository-policy/policy.d.ts +25 -1
  189. package/src/repository-policy/policy.js +145 -4
  190. package/src/repository-policy/policy.js.map +1 -1
  191. package/src/schemas/performance-policy-v1.json +0 -2
  192. package/src/schemas/repository-policy-v1.json +41 -0
  193. package/src/secrets/operator.js +22 -4
  194. package/src/secrets/operator.js.map +1 -1
  195. package/src/secrets/verification.js +9 -1
  196. package/src/secrets/verification.js.map +1 -1
  197. package/src/sst-environment-source-format.d.ts +3 -0
  198. package/src/sst-environment-source-format.js +179 -0
  199. package/src/sst-environment-source-format.js.map +1 -0
  200. package/src/supply-chain/blueprint-trust-git.js +72 -21
  201. package/src/supply-chain/blueprint-trust-git.js.map +1 -1
  202. package/src/supply-chain/blueprint-verification-authority.js +55 -3
  203. package/src/supply-chain/blueprint-verification-authority.js.map +1 -1
  204. package/src/supply-chain/blueprint-verification-execution.js +17 -2
  205. package/src/supply-chain/blueprint-verification-execution.js.map +1 -1
  206. package/src/supply-chain/build-output.d.ts +7 -1
  207. package/src/supply-chain/build-output.js +160 -20
  208. package/src/supply-chain/build-output.js.map +1 -1
  209. package/src/supply-chain/generated-command.js +131 -43
  210. package/src/supply-chain/generated-command.js.map +1 -1
  211. package/src/supply-chain/generated-container-release-command.js +187 -38
  212. package/src/supply-chain/generated-container-release-command.js.map +1 -1
  213. package/src/supply-chain/generated-ecr-collector-command.js +12 -2
  214. package/src/supply-chain/generated-ecr-collector-command.js.map +1 -1
  215. package/src/supply-chain/generated-scanner-command.js +114 -54
  216. package/src/supply-chain/generated-scanner-command.js.map +1 -1
  217. package/src/supply-chain/inventory.d.ts +17 -0
  218. package/src/supply-chain/inventory.js +114 -3
  219. package/src/supply-chain/inventory.js.map +1 -1
  220. package/src/supply-chain/pnpm-toolchain-authority.d.ts +4 -3
  221. package/src/supply-chain/pnpm-toolchain-authority.js +20 -34
  222. package/src/supply-chain/pnpm-toolchain-authority.js.map +1 -1
  223. package/src/supply-chain/policy.js +4 -1
  224. package/src/supply-chain/policy.js.map +1 -1
  225. package/src/supply-chain/sbom.d.ts +1 -1
  226. package/src/supply-chain/sbom.js +30 -5
  227. package/src/supply-chain/sbom.js.map +1 -1
  228. package/src/supply-chain/scanner.js +10 -1
  229. package/src/supply-chain/scanner.js.map +1 -1
  230. package/src/workload-roles/account-baseline.d.ts +11 -0
  231. package/src/workload-roles/account-baseline.js +253 -16
  232. package/src/workload-roles/account-baseline.js.map +1 -1
  233. package/src/workload-roles/authority.d.ts +3 -1
  234. package/src/workload-roles/authority.js +48 -2
  235. package/src/workload-roles/authority.js.map +1 -1
  236. package/src/workload-roles/aws-s3-state.js +27 -0
  237. package/src/workload-roles/aws-s3-state.js.map +1 -1
  238. package/src/workload-roles/boundary.d.ts +2 -1
  239. package/src/workload-roles/boundary.js +9 -2
  240. package/src/workload-roles/boundary.js.map +1 -1
  241. package/src/workload-roles/handler.js +52 -24
  242. package/src/workload-roles/handler.js.map +1 -1
  243. package/src/workload-roles/injection.js +1 -1
  244. package/src/workload-roles/injection.js.map +1 -1
  245. package/src/workload-roles/provisioner.js +12 -4
  246. package/src/workload-roles/provisioner.js.map +1 -1
  247. package/src/workload-roles/registration.d.ts +1 -0
  248. package/src/workload-roles/registration.js +5 -0
  249. package/src/workload-roles/registration.js.map +1 -1
  250. package/src/workload-roles/stage-boundary.js +1 -1
  251. package/src/workload-roles/stage-boundary.js.map +1 -1
  252. package/src/workload-roles/standard-slice.js +1 -0
  253. package/src/workload-roles/standard-slice.js.map +1 -1
@@ -1,13 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_SST_DIFF_JSON_BYTES = void 0;
4
+ exports.collectInfrastructureBuildArtifacts = collectInfrastructureBuildArtifacts;
5
+ exports.infrastructureConfigurationMetadataDigest = infrastructureConfigurationMetadataDigest;
6
+ exports.matchesExpectedAssumedRoleArn = matchesExpectedAssumedRoleArn;
7
+ exports.localProductionConfirmation = localProductionConfirmation;
8
+ exports.revalidateLocalUnlockCallerAuthority = revalidateLocalUnlockCallerAuthority;
9
+ exports.parseRefreshPreviewJson = parseRefreshPreviewJson;
3
10
  exports.extractPrivateImportIdentifierDigests = extractPrivateImportIdentifierDigests;
4
11
  exports.approvalCandidateSourceCommit = approvalCandidateSourceCommit;
5
12
  exports.createCandidateBoundSstChildEnvironment = createCandidateBoundSstChildEnvironment;
13
+ exports.normalizeDesiredSstDiffV4_17_1 = normalizeDesiredSstDiffV4_17_1;
6
14
  exports.assessPulumiRefreshPreview = assessPulumiRefreshPreview;
15
+ exports.requiresIamMutationAnalysis = requiresIamMutationAnalysis;
7
16
  exports.standardRemoveProbeForResource = standardRemoveProbeForResource;
8
17
  exports.probeStandardRemoveResource = probeStandardRemoveResource;
9
18
  exports.observeInfrastructureState = observeInfrastructureState;
19
+ exports.actionRisk = actionRisk;
10
20
  exports.validateGovernanceEvidence = validateGovernanceEvidence;
21
+ exports.createLocalPlanApprovalRecord = createLocalPlanApprovalRecord;
22
+ exports.hasUnreviewableLocalPlanBlock = hasUnreviewableLocalPlanBlock;
23
+ exports.createLocalRefreshGovernanceReview = createLocalRefreshGovernanceReview;
24
+ exports.createLocalDeployGovernanceReview = createLocalDeployGovernanceReview;
25
+ exports.authorizeRetainedLocalInfrastructurePlan = authorizeRetainedLocalInfrastructurePlan;
26
+ exports.requiresGovernanceValidation = requiresGovernanceValidation;
27
+ exports.isLocalPlanReviewPending = isLocalPlanReviewPending;
28
+ exports.observeLocalMutationStateIdentity = observeLocalMutationStateIdentity;
29
+ exports.retainLocalRefreshStateBackup = retainLocalRefreshStateBackup;
11
30
  exports.runInfrastructurePlanExecutor = runInfrastructurePlanExecutor;
12
31
  exports.revalidateInfrastructureMutationAuthorization = revalidateInfrastructureMutationAuthorization;
13
32
  exports.recordInfrastructureMutationConvergence = recordInfrastructureMutationConvergence;
@@ -21,8 +40,16 @@ const node_os_1 = require("node:os");
21
40
  const devkit_1 = require("@nx/devkit");
22
41
  const canonical_json_1 = require("@empire-builder-kit/runtime/canonical-json");
23
42
  const index_js_1 = require("../../infrastructure-change/index.js");
43
+ const local_capability_js_1 = require("../../infrastructure-change/local-capability.js");
44
+ const private_state_js_1 = require("../../local-dev/private-state.js");
24
45
  const index_js_2 = require("../../repository-policy/index.js");
46
+ const source_authority_js_1 = require("../../release/source-authority.js");
25
47
  const run_sst_command_js_1 = require("../sst-lifecycle/run-sst-command.js");
48
+ function isCanonicalDeploymentStage(value) {
49
+ return (value === 'development' || value === 'staging' || value === 'production');
50
+ }
51
+ const refreshPreviewDiagnosticsByObservation = new WeakMap();
52
+ const refreshMutationPlanMaterialByObservation = new WeakMap();
26
53
  function repositoryInfrastructureApprovalMode(context, stage) {
27
54
  return stage === 'production'
28
55
  ? (0, index_js_2.readRepositoryOperatingPolicy)(context.root).productionApproval.mode
@@ -47,6 +74,99 @@ function projectEvidence(context, projects) {
47
74
  const SHA256 = /^[a-f0-9]{64}$/;
48
75
  const SOURCE_COMMIT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
49
76
  const MAX_JSON_BYTES = 5 * 1024 * 1024;
77
+ exports.MAX_SST_DIFF_JSON_BYTES = 64 * 1024 * 1024;
78
+ const MAX_BUILD_ARTIFACTS = 512;
79
+ const MAX_BUILD_ARTIFACT_BYTES = 256 * 1024 * 1024;
80
+ const MAX_BUILD_ARTIFACT_TOTAL_BYTES = 4 * 1024 * 1024 * 1024;
81
+ function sameBuildArtifactIdentity(left, right) {
82
+ return (left.dev === right.dev &&
83
+ left.ino === right.ino &&
84
+ left.mode === right.mode &&
85
+ left.nlink === right.nlink &&
86
+ left.size === right.size &&
87
+ left.mtimeMs === right.mtimeMs &&
88
+ left.ctimeMs === right.ctimeMs);
89
+ }
90
+ /** @internal */
91
+ function collectInfrastructureBuildArtifacts(projectRoot) {
92
+ const artifactsRoot = (0, node_path_1.join)(projectRoot, '.sst', 'artifacts');
93
+ const entries = [];
94
+ let totalBytes = 0;
95
+ if ((0, node_fs_1.existsSync)(artifactsRoot)) {
96
+ const rootIdentity = (0, node_fs_1.lstatSync)(artifactsRoot);
97
+ if (!rootIdentity.isDirectory() || rootIdentity.isSymbolicLink()) {
98
+ throw new Error('The SST artifact root must be one real directory.');
99
+ }
100
+ const children = (0, node_fs_1.readdirSync)(artifactsRoot, { withFileTypes: true }).sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.name, right.name));
101
+ if (children.length > 4_096) {
102
+ throw new Error('The SST artifact root exceeds its directory bound.');
103
+ }
104
+ for (const child of children) {
105
+ if (!child.isDirectory())
106
+ continue;
107
+ const artifactPath = (0, node_path_1.join)(artifactsRoot, child.name, 'code.zip');
108
+ if (!(0, node_fs_1.existsSync)(artifactPath))
109
+ continue;
110
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u.test(child.name)) {
111
+ throw new Error('An SST artifact directory name is invalid.');
112
+ }
113
+ if (entries.length === MAX_BUILD_ARTIFACTS) {
114
+ throw new Error('The SST build artifact count exceeds its bound.');
115
+ }
116
+ const lexical = (0, node_fs_1.lstatSync)(artifactPath);
117
+ if (!lexical.isFile() ||
118
+ lexical.isSymbolicLink() ||
119
+ lexical.nlink !== 1) {
120
+ throw new Error(`SST build artifact ${child.name} has an invalid identity.`);
121
+ }
122
+ const descriptor = (0, node_fs_1.openSync)(artifactPath, 'r');
123
+ try {
124
+ const before = (0, node_fs_1.fstatSync)(descriptor);
125
+ if (!before.isFile() ||
126
+ before.nlink !== 1 ||
127
+ lexical.dev !== before.dev ||
128
+ lexical.ino !== before.ino ||
129
+ lexical.mode !== before.mode ||
130
+ lexical.size !== before.size ||
131
+ before.size < 1 ||
132
+ before.size > MAX_BUILD_ARTIFACT_BYTES) {
133
+ throw new Error(`SST build artifact ${child.name} has an invalid identity.`);
134
+ }
135
+ const bytes = (0, node_fs_1.readFileSync)(descriptor);
136
+ const after = (0, node_fs_1.fstatSync)(descriptor);
137
+ if (bytes.byteLength !== before.size ||
138
+ !sameBuildArtifactIdentity(before, after)) {
139
+ throw new Error(`SST build artifact ${child.name} changed while reading.`);
140
+ }
141
+ totalBytes += bytes.byteLength;
142
+ if (totalBytes > MAX_BUILD_ARTIFACT_TOTAL_BYTES) {
143
+ throw new Error('The SST build artifact bytes exceed their aggregate bound.');
144
+ }
145
+ entries.push({
146
+ digest: (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex'),
147
+ path: `.sst/artifacts/${child.name}/code.zip`,
148
+ size: bytes.byteLength,
149
+ });
150
+ }
151
+ finally {
152
+ (0, node_fs_1.closeSync)(descriptor);
153
+ }
154
+ }
155
+ }
156
+ const unsigned = {
157
+ entries,
158
+ kind: 'ebk-infrastructure-build-artifacts',
159
+ schemaVersion: 1,
160
+ };
161
+ return { ...unsigned, digest: digest(canonicalJson(unsigned)) };
162
+ }
163
+ /** @internal */
164
+ function infrastructureConfigurationMetadataDigest(input) {
165
+ if (!SHA256.test(input.buildArtifactDigest)) {
166
+ throw new Error('The infrastructure build artifact digest is invalid.');
167
+ }
168
+ return digest(canonicalJson(input));
169
+ }
50
170
  const STANDARD_REMOVE_CLOUD_CONTROL_TYPES = {
51
171
  'aws:acm/certificate:Certificate': 'AWS::CertificateManager::Certificate',
52
172
  'aws:apigatewayv2/api:Api': 'AWS::ApiGatewayV2::Api',
@@ -106,6 +226,9 @@ const STANDARD_REMOVE_CLOUD_CONTROL_TYPES = {
106
226
  'aws:sfn/stateMachine:StateMachine': 'AWS::StepFunctions::StateMachine',
107
227
  'aws:sns/topic:Topic': 'AWS::SNS::Topic',
108
228
  'aws:sqs/queue:Queue': 'AWS::SQS::Queue',
229
+ 'aws:wafv2/ipSet:IpSet': 'AWS::WAFv2::IPSet',
230
+ 'aws:wafv2/webAcl:WebAcl': 'AWS::WAFv2::WebACL',
231
+ 'aws:wafv2/webAclLoggingConfiguration:WebAclLoggingConfiguration': 'AWS::WAFv2::LoggingConfiguration',
109
232
  };
110
233
  function digest(value) {
111
234
  return (0, node_crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
@@ -117,6 +240,66 @@ function requiredEnvironment(environment, name, pattern) {
117
240
  }
118
241
  return value;
119
242
  }
243
+ function expectedDeploymentAccount(environment, invocation) {
244
+ const retained = requiredEnvironment(invocation.environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/);
245
+ const ambient = environment.AWS_EXPECTED_ACCOUNT_ID?.trim();
246
+ if (ambient !== undefined && ambient !== '' && ambient !== retained) {
247
+ throw new Error('Ambient AWS_EXPECTED_ACCOUNT_ID does not match committed deployment topology.');
248
+ }
249
+ return retained;
250
+ }
251
+ function matchesExpectedAssumedRoleArn(account, callerArn, expectedRoleArn) {
252
+ const expected = /^arn:([^:]+):iam::(\d{12}):role\/(.+)$/.exec(expectedRoleArn);
253
+ const observed = /^arn:([^:]+):sts::(\d{12}):assumed-role\/(.+)\/([^/]+)$/.exec(callerArn);
254
+ const expectedRoleName = expected?.[3]?.split('/').at(-1);
255
+ return Boolean(expected &&
256
+ observed &&
257
+ expectedRoleName &&
258
+ expected[1] === observed[1] &&
259
+ expected[2] === account &&
260
+ observed[2] === account &&
261
+ expectedRoleName === observed[3]);
262
+ }
263
+ function localProductionConfirmation(input) {
264
+ if (!['deploy', 'refresh', 'remove'].includes(input.action) ||
265
+ !/^\d{12}$/.test(input.accountId) ||
266
+ !/^[a-z]{2}(?:-gov)?-[a-z]+(?:-[a-z]+)*-\d$/.test(input.region) ||
267
+ !SOURCE_COMMIT.test(input.sourceCommit)) {
268
+ throw new Error('Local production confirmation requires one exact action, account, Region, and source commit.');
269
+ }
270
+ return [
271
+ input.action,
272
+ 'production',
273
+ input.accountId,
274
+ input.region,
275
+ input.sourceCommit,
276
+ ].join(':');
277
+ }
278
+ function revalidateLocalUnlockCallerAuthority(input) {
279
+ const environment = input.environment ?? process.env;
280
+ const expectedAccount = expectedDeploymentAccount(environment, input.invocation);
281
+ const expectedRoleArn = input.invocation.environment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN?.trim();
282
+ if (!expectedRoleArn) {
283
+ throw new Error('Local unlock requires one exact expected temporary assumed role.');
284
+ }
285
+ const caller = record(runJsonCommand(input.spawn ??
286
+ node_child_process_1.spawnSync, 'aws', [
287
+ 'sts',
288
+ 'get-caller-identity',
289
+ '--output',
290
+ 'json',
291
+ ...(input.profile ? ['--profile', input.profile] : []),
292
+ ], {
293
+ cwd: input.invocation.cwd,
294
+ env: (0, run_sst_command_js_1.createSstChildEnvironment)(environment, input.invocation.environment),
295
+ label: 'Local unlock AWS caller identity',
296
+ }), 'Local unlock AWS caller identity');
297
+ const callerArn = typeof caller['Arn'] === 'string' ? caller['Arn'] : '';
298
+ if (caller['Account'] !== expectedAccount ||
299
+ !matchesExpectedAssumedRoleArn(expectedAccount, callerArn, expectedRoleArn)) {
300
+ throw new Error('Local unlock AWS caller does not match the committed account and exact temporary assumed role.');
301
+ }
302
+ }
120
303
  function parseJson(value, label) {
121
304
  if (Buffer.byteLength(value, 'utf8') > MAX_JSON_BYTES) {
122
305
  throw new Error(`${label} exceeds the 5 MiB metadata limit.`);
@@ -128,6 +311,17 @@ function parseJson(value, label) {
128
311
  throw new Error(`${label} is not valid JSON.`);
129
312
  }
130
313
  }
314
+ function parseRefreshPreviewJson(value) {
315
+ if (Buffer.byteLength(value, 'utf8') > exports.MAX_SST_DIFF_JSON_BYTES) {
316
+ throw new Error('SST refresh preview exceeds the 64 MiB metadata limit.');
317
+ }
318
+ try {
319
+ return JSON.parse(value);
320
+ }
321
+ catch {
322
+ throw new Error('SST refresh preview is not valid JSON.');
323
+ }
324
+ }
131
325
  function record(value, label) {
132
326
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
133
327
  throw new Error(`${label} must be one JSON object.`);
@@ -184,6 +378,56 @@ function stringArray(value, label) {
184
378
  }
185
379
  return [...new Set(value)].sort();
186
380
  }
381
+ function adoptedImports(value) {
382
+ if (!Array.isArray(value) || value.length > 64) {
383
+ throw new Error('policy.adoptedImports must be one bounded array.');
384
+ }
385
+ const imports = value.map((entry, index) => {
386
+ const candidate = record(entry, `policy.adoptedImports[${String(index)}]`);
387
+ requireExactKeys(candidate, [
388
+ 'authority',
389
+ 'logicalId',
390
+ 'privateIdentifierTemplate',
391
+ 'recoveryRunbookRef',
392
+ 'removalBehavior',
393
+ 'resourceType',
394
+ ], `policy.adoptedImports[${String(index)}]`);
395
+ const logicalId = candidate['logicalId'];
396
+ const privateIdentifierTemplate = candidate['privateIdentifierTemplate'];
397
+ const recoveryRunbookRef = candidate['recoveryRunbookRef'];
398
+ const resourceType = candidate['resourceType'];
399
+ if (candidate['authority'] !== 'workload-role-account-baseline' ||
400
+ candidate['removalBehavior'] !== 'retain' ||
401
+ typeof logicalId !== 'string' ||
402
+ !/^[A-Za-z][A-Za-z0-9._-]{0,255}$/.test(logicalId) ||
403
+ typeof resourceType !== 'string' ||
404
+ !/^[A-Za-z0-9][A-Za-z0-9:._/-]{0,255}$/.test(resourceType) ||
405
+ typeof privateIdentifierTemplate !== 'string' ||
406
+ privateIdentifierTemplate.length > 512 ||
407
+ privateIdentifierTemplate.split('{stage}').length !== 2 ||
408
+ /[{}]/.test(privateIdentifierTemplate.replace('{stage}', '')) ||
409
+ typeof recoveryRunbookRef !== 'string' ||
410
+ recoveryRunbookRef.length > 256 ||
411
+ recoveryRunbookRef.startsWith('/') ||
412
+ recoveryRunbookRef.split('/').some((part) => !part || part === '..')) {
413
+ throw new Error(`policy.adoptedImports[${String(index)}] must bind one exact retained workload-role import and recovery runbook.`);
414
+ }
415
+ return {
416
+ authority: 'workload-role-account-baseline',
417
+ logicalId,
418
+ privateIdentifierTemplate,
419
+ recoveryRunbookRef,
420
+ removalBehavior: 'retain',
421
+ resourceType,
422
+ };
423
+ });
424
+ const keys = imports.map(({ logicalId, resourceType }) => `${logicalId}\u0000${resourceType}`);
425
+ if (new Set(keys).size !== keys.length) {
426
+ throw new Error('policy.adoptedImports contains a duplicate resource.');
427
+ }
428
+ return imports.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
429
+ (0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
430
+ }
187
431
  function readJsonFile(path, label) {
188
432
  return parseJson((0, node_fs_1.readFileSync)(path, 'utf8'), label);
189
433
  }
@@ -224,7 +468,9 @@ function readDescriptor(path, project) {
224
468
  }
225
469
  return {
226
470
  policy: {
471
+ adoptedImports: adoptedImports(policy.adoptedImports),
227
472
  additiveLowRiskTypes: stringArray(policy.additiveLowRiskTypes, 'policy.additiveLowRiskTypes'),
473
+ additiveReviewedTypes: stringArray(policy.additiveReviewedTypes, 'policy.additiveReviewedTypes'),
228
474
  costOrQuotaTypes: stringArray(policy.costOrQuotaTypes, 'policy.costOrQuotaTypes'),
229
475
  statefulTypes: stringArray(policy.statefulTypes, 'policy.statefulTypes'),
230
476
  },
@@ -242,7 +488,7 @@ function readOptionalArray(environment, name) {
242
488
  }
243
489
  return value;
244
490
  }
245
- function planAuthorityPolicyDigest(environment) {
491
+ function planAuthorityPolicyDigest(environment, invocation) {
246
492
  const parseArray = (name) => {
247
493
  const raw = requiredEnvironment(environment, name);
248
494
  if (Buffer.byteLength(raw, 'utf8') > 128 * 1024) {
@@ -253,6 +499,38 @@ function planAuthorityPolicyDigest(environment) {
253
499
  return value;
254
500
  throw new Error(`${name} must contain one non-empty exact JSON array.`);
255
501
  };
502
+ if (environment.EBK_LOCAL_DEPLOYMENT_MODE !== undefined) {
503
+ if (environment.EBK_LOCAL_DEPLOYMENT_MODE !== 'federated' ||
504
+ environment.GITHUB_ACTIONS === 'true') {
505
+ throw new Error('Local infrastructure plan authority requires an admitted federated profile outside GitHub Actions.');
506
+ }
507
+ for (const name of [
508
+ 'AWS_PLAN_GENERATED_OBJECT_ARNS_JSON',
509
+ 'AWS_PLAN_GENERATED_PARAMETER_ARNS_JSON',
510
+ 'AWS_PLAN_GENERATED_SECRET_ARNS_JSON',
511
+ 'AWS_PLAN_PROVIDER_REFRESH_POLICY_JSON',
512
+ 'AWS_PLAN_STATE_KMS_KEY_ARN',
513
+ ]) {
514
+ if (environment[name] !== undefined) {
515
+ throw new Error(`Local infrastructure plan authority rejects protected-only input ${name}.`);
516
+ }
517
+ }
518
+ const region = requiredEnvironment(environment, 'AWS_REGION', /^[a-z]{2}(?:-gov)?-[a-z]+-\d$/);
519
+ const roleArn = requiredEnvironment(environment, 'AWS_PLAN_ROLE_ARN', /^arn:aws:iam::\d{12}:role\/[A-Za-z0-9+=,.@_/-]{1,512}$/);
520
+ if (invocation.environment.AWS_REGION !== region ||
521
+ invocation.environment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN !== roleArn) {
522
+ throw new Error('Local plan authority differs from the exact lifecycle Region or assumed role.');
523
+ }
524
+ return digest(canonicalJson({
525
+ account: requiredEnvironment(environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/),
526
+ applications: parseArray('AWS_PLAN_SST_APPS_JSON'),
527
+ mode: 'local-federated',
528
+ region,
529
+ roleArn,
530
+ sourceTree: requiredEnvironment(environment, 'EBK_LOCAL_SOURCE_TREE', /^[a-f0-9]{40}$/),
531
+ stateBucketArn: requiredEnvironment(environment, 'AWS_PLAN_STATE_BUCKET_ARN', /^arn:aws:s3:::[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/),
532
+ }));
533
+ }
256
534
  const providerPolicy = parseArray('AWS_PLAN_PROVIDER_REFRESH_POLICY_JSON');
257
535
  if (providerPolicy.some((statement) => typeof statement !== 'object' ||
258
536
  statement === null ||
@@ -297,24 +575,70 @@ function releaseCandidateBinding(input) {
297
575
  }
298
576
  return { digest: digest(raw), path };
299
577
  }
578
+ function localSourceCandidateBinding(input) {
579
+ const policy = (0, index_js_2.readRepositoryOperatingPolicy)(input.context.root);
580
+ const stage = requiredEnvironment(input.environment, 'EBK_STAGE');
581
+ if (input.environment.EBK_LOCAL_DEPLOYMENT_MODE !== 'federated' ||
582
+ input.environment.GITHUB_ACTIONS === 'true' ||
583
+ !['development', 'staging', 'production'].includes(stage) ||
584
+ (0, index_js_2.requiresGithubDeployment)(policy.deploymentProfile, stage)) {
585
+ throw new Error('Local source candidates require a stage admitted by the committed deployment profile.');
586
+ }
587
+ const expectedTree = requiredEnvironment(input.environment, 'EBK_LOCAL_SOURCE_TREE', /^[a-f0-9]{40}$/);
588
+ const status = input.spawn('git', ['status', '--porcelain=v1', '--untracked-files=all'], {
589
+ cwd: input.context.root,
590
+ encoding: 'utf8',
591
+ env: input.environment,
592
+ maxBuffer: MAX_JSON_BYTES,
593
+ shell: false,
594
+ });
595
+ const tree = input.spawn('git', ['rev-parse', 'HEAD^{tree}'], {
596
+ cwd: input.context.root,
597
+ encoding: 'utf8',
598
+ env: input.environment,
599
+ maxBuffer: MAX_JSON_BYTES,
600
+ shell: false,
601
+ });
602
+ if (status.status !== 0 ||
603
+ status.stdout !== '' ||
604
+ status.stderr !== '' ||
605
+ tree.status !== 0 ||
606
+ tree.stdout.trim() !== expectedTree) {
607
+ throw new Error('Local source candidate differs from the exact clean Git tree.');
608
+ }
609
+ const materialSourceTree = input.materialSourceTree ?? expectedTree;
610
+ if (!SOURCE_COMMIT.test(materialSourceTree)) {
611
+ throw new Error('Local source candidate material tree must be one exact Git object ID.');
612
+ }
613
+ return {
614
+ digest: digest(canonicalJson({
615
+ kind: 'ebk-local-source-candidate',
616
+ project: input.project,
617
+ schemaVersion: 1,
618
+ sourceCommit: input.sourceCommit,
619
+ sourceTree: materialSourceTree,
620
+ })),
621
+ };
622
+ }
300
623
  /** @internal */
301
624
  function approvalCandidateSourceCommit(context, options, environment) {
302
625
  const project = context.projectName;
303
- const selectedSource = environment['EBK_RELEASE_SOURCE_COMMIT']?.trim();
304
- if (selectedSource !== undefined && !SOURCE_COMMIT.test(selectedSource)) {
626
+ const workflowSelectedSource = environment['EBK_RELEASE_SOURCE_COMMIT']?.trim();
627
+ if (workflowSelectedSource !== undefined &&
628
+ !SOURCE_COMMIT.test(workflowSelectedSource)) {
305
629
  throw new Error('EBK_RELEASE_SOURCE_COMMIT must be one exact Git object ID when set.');
306
630
  }
307
631
  if (!project)
308
- return selectedSource;
632
+ return workflowSelectedSource;
309
633
  const approvalPath = relativeFile(context.root, options.approvalPath ?? 'operations/infrastructure-approval.json');
310
634
  if (!(0, node_fs_1.existsSync)(approvalPath))
311
- return selectedSource;
635
+ return workflowSelectedSource;
312
636
  let batch;
313
637
  try {
314
638
  batch = record(readJsonFile(approvalPath, 'Infrastructure approval candidate binding'), 'Infrastructure approval candidate binding');
315
639
  }
316
640
  catch {
317
- return selectedSource;
641
+ return workflowSelectedSource;
318
642
  }
319
643
  const action = options.action ?? 'deploy';
320
644
  const stage = options.stage?.trim();
@@ -323,7 +647,7 @@ function approvalCandidateSourceCommit(context, options, environment) {
323
647
  batch['action'] !== action ||
324
648
  stage === undefined ||
325
649
  batch['stage'] !== stage) {
326
- return selectedSource;
650
+ return workflowSelectedSource;
327
651
  }
328
652
  let approvedSource;
329
653
  try {
@@ -333,16 +657,40 @@ function approvalCandidateSourceCommit(context, options, environment) {
333
657
  approvedSource = planned['sourceCommit'];
334
658
  }
335
659
  catch {
336
- return selectedSource;
660
+ return workflowSelectedSource;
337
661
  }
338
662
  if (typeof approvedSource !== 'string' ||
339
663
  !SOURCE_COMMIT.test(approvedSource)) {
340
- return selectedSource;
664
+ return workflowSelectedSource;
341
665
  }
342
- if (selectedSource === undefined) {
666
+ const localAuthority = environment['EBK_LOCAL_DEPLOYMENT_MODE'] === 'federated';
667
+ const localSelectedSource = localAuthority
668
+ ? environment['EBK_SOURCE_COMMIT']?.trim()
669
+ : undefined;
670
+ if (localSelectedSource !== undefined &&
671
+ !SOURCE_COMMIT.test(localSelectedSource)) {
672
+ throw new Error('EBK_SOURCE_COMMIT must be one exact Git object ID for local approval resume.');
673
+ }
674
+ if (localAuthority && localSelectedSource === undefined) {
675
+ throw new Error('Local approval-resume infrastructure planning requires the exact clean local source.');
676
+ }
677
+ if (!localAuthority && workflowSelectedSource === undefined) {
343
678
  throw new Error('Approval-resume infrastructure planning requires a workflow-selected release source.');
344
679
  }
345
- if (selectedSource !== undefined && selectedSource !== approvedSource) {
680
+ if (localAuthority) {
681
+ try {
682
+ (0, source_authority_js_1.validateReviewedReleaseCheckout)({
683
+ deploymentSourceCommit: localSelectedSource,
684
+ releaseSourceCommit: approvedSource,
685
+ repositoryRoot: context.root,
686
+ reviewedSourceCommit: localSelectedSource,
687
+ });
688
+ }
689
+ catch {
690
+ throw new Error('Infrastructure approval candidate is not the exact clean local approval-only descendant.');
691
+ }
692
+ }
693
+ else if (workflowSelectedSource !== approvedSource) {
346
694
  throw new Error('Infrastructure approval candidate differs from the workflow-selected release source.');
347
695
  }
348
696
  return approvedSource;
@@ -352,10 +700,23 @@ function createCandidateBoundSstChildEnvironment(environment, invocationEnvironm
352
700
  if (!SOURCE_COMMIT.test(candidateSourceCommit)) {
353
701
  throw new Error('Infrastructure lifecycle candidate must be one exact Git object ID.');
354
702
  }
355
- return {
703
+ const childEnvironment = {
356
704
  ...(0, run_sst_command_js_1.createSstChildEnvironment)(environment, invocationEnvironment),
357
705
  EBK_RELEASE_SOURCE_COMMIT: candidateSourceCommit,
358
706
  };
707
+ // These values authorize the retained plan after SST has produced it. They
708
+ // are not desired infrastructure inputs and must never enter SST's program
709
+ // environment, where components such as Nextjs can persist the ambient
710
+ // process environment in a command resource. Keeping them outside the SST
711
+ // child makes the unapproved and approved previews materially identical.
712
+ for (const name of [
713
+ 'EBK_LOCAL_PLAN_APPROVER_ARN',
714
+ 'EBK_LOCAL_PLAN_MATERIAL_DIGEST',
715
+ 'EBK_LOCAL_PLAN_SUMMARY_DIGEST',
716
+ ]) {
717
+ delete childEnvironment[name];
718
+ }
719
+ return childEnvironment;
359
720
  }
360
721
  function frameworkAndBlueprintVersions(workspaceRoot, projectRoot) {
361
722
  const workspacePackage = record(readJsonFile((0, node_path_1.join)(workspaceRoot, 'package.json'), 'Workspace package'), 'Workspace package');
@@ -461,15 +822,26 @@ function defaultSpawn(executable, args, options) {
461
822
  function canonicalJson(value) {
462
823
  return (0, canonical_json_1.canonicalJson)(value, { omitUndefinedObjectValues: true });
463
824
  }
464
- function stateResources(value) {
825
+ function stateExport(value) {
465
826
  const root = record(value, 'SST state export');
827
+ const sstCheckpoint = typeof root['latest'] === 'object' && root['latest'] !== null;
466
828
  const deployment = typeof root['deployment'] === 'object' && root['deployment'] !== null
467
829
  ? record(root['deployment'], 'SST state deployment')
468
- : root;
830
+ : sstCheckpoint
831
+ ? record(root['latest'], 'SST state checkpoint')
832
+ : root;
469
833
  if (!Array.isArray(deployment['resources'])) {
470
834
  throw new Error('SST state export does not contain a Pulumi deployment resource list.');
471
835
  }
472
- return deployment['resources'].map((resource, index) => record(resource, `SST state resource ${index}`));
836
+ if (sstCheckpoint &&
837
+ (typeof root['stack'] !== 'string' ||
838
+ !/^[^/]+\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\/(development|staging|production)$/.test(root['stack']))) {
839
+ throw new Error('SST state checkpoint stack identity is malformed.');
840
+ }
841
+ return {
842
+ resources: deployment['resources'].map((resource, index) => record(resource, `SST state resource ${index}`)),
843
+ ...(sstCheckpoint ? { sstStack: root['stack'] } : {}),
844
+ };
473
845
  }
474
846
  function stringRecord(value) {
475
847
  if (typeof value !== 'object' ||
@@ -619,7 +991,21 @@ function removePhysicalSelector(resource) {
619
991
  return undefined;
620
992
  }
621
993
  function expectedTags(resource) {
622
- for (const material of [resource.inputs, resource.outputs]) {
994
+ // The AWS provider records the complete effective tag authority in tagsAll.
995
+ // Resource-local tags omit provider default tags, so comparing those bytes to
996
+ // AWS's complete observed tag set would permanently classify every
997
+ // default-tagged resource as drifted.
998
+ const materialAuthority = resource.external === true
999
+ ? [resource.outputs, resource.inputs]
1000
+ : [resource.inputs, resource.outputs];
1001
+ for (const material of materialAuthority) {
1002
+ if (typeof material !== 'object' || material === null)
1003
+ continue;
1004
+ const tags = stringRecord(material['tagsAll']);
1005
+ if (tags)
1006
+ return tags;
1007
+ }
1008
+ for (const material of materialAuthority) {
623
1009
  if (typeof material !== 'object' || material === null)
624
1010
  continue;
625
1011
  const tags = stringRecord(material['tags']);
@@ -647,8 +1033,473 @@ const PULUMI_REFRESH_PREVIEW_VERSION = '3.215.0';
647
1033
  const REFRESH_PREVIEW_PROPERTY_OVERLAYS = {
648
1034
  'aws:secretsmanager/secretVersion:SecretVersion': ['inputs.secretString'],
649
1035
  };
1036
+ function observeSecretsManagerVersion(input) {
1037
+ const urn = typeof input.resource.urn === 'string' ? input.resource.urn : 'unknown';
1038
+ const inputs = record(input.resource.inputs, `${urn} inputs`);
1039
+ const outputs = record(input.resource.outputs, `${urn} outputs`);
1040
+ const secretId = outputs['secretId'];
1041
+ const versionId = outputs['versionId'];
1042
+ const expectedStages = outputs['versionStages'];
1043
+ const resourceId = input.resource.id;
1044
+ const expectedArn = new RegExp(`^arn:(?:aws|aws-us-gov|aws-cn):secretsmanager:${input.region}:${input.account}:secret:[^\\s]+$`);
1045
+ if (typeof secretId !== 'string' ||
1046
+ secretId.length > 2_048 ||
1047
+ hasControlCharacters(secretId) ||
1048
+ !expectedArn.test(secretId) ||
1049
+ typeof versionId !== 'string' ||
1050
+ versionId.length > 256 ||
1051
+ hasControlCharacters(versionId) ||
1052
+ inputs['secretId'] !== secretId ||
1053
+ resourceId !== `${secretId}|${versionId}` ||
1054
+ !Array.isArray(expectedStages) ||
1055
+ expectedStages.length === 0 ||
1056
+ expectedStages.length > 20 ||
1057
+ expectedStages.some((stage) => typeof stage !== 'string' ||
1058
+ stage.length === 0 ||
1059
+ stage.length > 256 ||
1060
+ hasControlCharacters(stage)) ||
1061
+ new Set(expectedStages).size !== expectedStages.length) {
1062
+ throw new Error('Secrets Manager version state does not expose one exact account-, Region-, and stage-bound identity.');
1063
+ }
1064
+ const normalizedExpectedStages = [...expectedStages].sort(deterministic_order_js_1.compareUtf16);
1065
+ const listed = record(runJsonCommand(input.spawn, 'aws', [
1066
+ 'secretsmanager',
1067
+ 'list-secret-version-ids',
1068
+ '--secret-id',
1069
+ secretId,
1070
+ '--include-deprecated',
1071
+ '--max-results',
1072
+ '100',
1073
+ '--output',
1074
+ 'json',
1075
+ ], {
1076
+ cwd: input.invocation.cwd,
1077
+ env: input.childEnvironment,
1078
+ label: 'AWS Secrets Manager version-stage observation',
1079
+ }), 'AWS Secrets Manager version-stage observation');
1080
+ if (listed['NextToken'] !== undefined) {
1081
+ throw new Error('AWS Secrets Manager version-stage observation exceeds its bounded inventory.');
1082
+ }
1083
+ const versions = listed['Versions'];
1084
+ if (!Array.isArray(versions) || versions.length > 100) {
1085
+ throw new Error('AWS Secrets Manager version-stage observation is malformed or unbounded.');
1086
+ }
1087
+ const observed = new Map();
1088
+ for (const [index, value] of versions.entries()) {
1089
+ const version = record(value, `AWS Secrets Manager version-stage observation Versions[${String(index)}]`);
1090
+ const observedVersionId = version['VersionId'];
1091
+ const observedStages = version['VersionStages'] ?? [];
1092
+ if (typeof observedVersionId !== 'string' ||
1093
+ observedVersionId.length === 0 ||
1094
+ observedVersionId.length > 256 ||
1095
+ hasControlCharacters(observedVersionId) ||
1096
+ !Array.isArray(observedStages) ||
1097
+ observedStages.length > 20 ||
1098
+ observedStages.some((stage) => typeof stage !== 'string' ||
1099
+ stage.length === 0 ||
1100
+ stage.length > 256 ||
1101
+ hasControlCharacters(stage)) ||
1102
+ new Set(observedStages).size !== observedStages.length ||
1103
+ observed.has(observedVersionId)) {
1104
+ throw new Error('AWS Secrets Manager version-stage observation contains an ambiguous version identity.');
1105
+ }
1106
+ observed.set(observedVersionId, [...observedStages].sort(deterministic_order_js_1.compareUtf16));
1107
+ }
1108
+ const observedStages = observed.get(versionId);
1109
+ return {
1110
+ arnDigest: digest(secretId),
1111
+ drifted: observedStages === undefined ||
1112
+ canonicalJson(observedStages) !== canonicalJson(normalizedExpectedStages),
1113
+ observedIdentityDigest: observedStages === undefined
1114
+ ? null
1115
+ : digest(canonicalJson({
1116
+ secretId,
1117
+ versionId,
1118
+ versionStages: observedStages,
1119
+ })),
1120
+ urn,
1121
+ };
1122
+ }
650
1123
  function normalizedDiffMaterial(diff) {
651
- return diff.resources.map((resource) => Object.fromEntries(Object.entries(resource).filter(([key]) => key !== 'sourceIndex')));
1124
+ return (0, index_js_1.materialNormalizedSstDiffResources)(diff);
1125
+ }
1126
+ const REFRESH_STATE_PROVIDER_OPERATIONS = new Set([
1127
+ 'discard',
1128
+ 'discard-replaced',
1129
+ 'read',
1130
+ 'read-replacement',
1131
+ ]);
1132
+ function withoutRefreshStateResources(diff) {
1133
+ return {
1134
+ ...diff,
1135
+ resources: diff.resources.filter(({ providerOperation }) => !REFRESH_STATE_PROVIDER_OPERATIONS.has(providerOperation)),
1136
+ };
1137
+ }
1138
+ const MAX_REFRESH_DIAGNOSTIC_RESOURCES = 512;
1139
+ const MAX_REFRESH_DIAGNOSTIC_PATHS_PER_RESOURCE = 128;
1140
+ const MAX_REFRESH_DIAGNOSTIC_DEPTH = 32;
1141
+ function isExactInertSameEntry(recordEntry) {
1142
+ if (recordEntry['op'] !== 'same')
1143
+ return false;
1144
+ const urn = recordEntry['urn'];
1145
+ const resourceType = recordEntry['type'];
1146
+ const detailedDiff = recordEntry['detailedDiff'];
1147
+ const diffs = recordEntry['diffs'];
1148
+ const keys = recordEntry['keys'];
1149
+ const hasOld = Object.prototype.hasOwnProperty.call(recordEntry, 'old');
1150
+ const hasNew = Object.prototype.hasOwnProperty.call(recordEntry, 'new');
1151
+ const isEmptyRecord = typeof detailedDiff === 'object' &&
1152
+ detailedDiff !== null &&
1153
+ !Array.isArray(detailedDiff) &&
1154
+ Object.keys(detailedDiff).length === 0;
1155
+ return (typeof urn === 'string' &&
1156
+ urn.length > 0 &&
1157
+ typeof resourceType === 'string' &&
1158
+ resourceType.length > 0 &&
1159
+ (detailedDiff === undefined || detailedDiff === null || isEmptyRecord) &&
1160
+ (diffs === undefined || (Array.isArray(diffs) && diffs.length === 0)) &&
1161
+ (keys === undefined || (Array.isArray(keys) && keys.length === 0)) &&
1162
+ hasOld === hasNew &&
1163
+ (!hasOld ||
1164
+ canonicalJson(recordEntry['old']) === canonicalJson(recordEntry['new'])));
1165
+ }
1166
+ /** @internal */
1167
+ function normalizeDesiredSstDiffV4_17_1(input) {
1168
+ let raw = input.stdout;
1169
+ if (typeof raw === 'string') {
1170
+ try {
1171
+ raw = JSON.parse(raw);
1172
+ }
1173
+ catch {
1174
+ return (0, index_js_1.normalizeSstDiffV4_17_1)(input);
1175
+ }
1176
+ }
1177
+ const desired = Array.isArray(raw)
1178
+ ? raw.filter((entry) => !(typeof entry === 'object' &&
1179
+ entry !== null &&
1180
+ !Array.isArray(entry) &&
1181
+ isExactInertSameEntry(entry)))
1182
+ : raw;
1183
+ return (0, index_js_1.normalizeSstDiffV4_17_1)({ ...input, stdout: desired });
1184
+ }
1185
+ function jsonPointerSegment(value) {
1186
+ return value.replace(/~/g, '~0').replace(/\//g, '~1');
1187
+ }
1188
+ function changedJsonPointerPaths(left, right) {
1189
+ const paths = [];
1190
+ const visit = (previous, current, path, depth) => {
1191
+ if (paths.length >= MAX_REFRESH_DIAGNOSTIC_PATHS_PER_RESOURCE)
1192
+ return;
1193
+ if (previous === undefined || current === undefined) {
1194
+ if (previous !== current)
1195
+ paths.push(path || '/');
1196
+ return;
1197
+ }
1198
+ if (canonicalJson(previous) === canonicalJson(current)) {
1199
+ return;
1200
+ }
1201
+ const previousRecord = typeof previous === 'object' &&
1202
+ previous !== null &&
1203
+ !Array.isArray(previous)
1204
+ ? previous
1205
+ : undefined;
1206
+ const currentRecord = typeof current === 'object' && current !== null && !Array.isArray(current)
1207
+ ? current
1208
+ : undefined;
1209
+ if (depth >= MAX_REFRESH_DIAGNOSTIC_DEPTH ||
1210
+ previousRecord === undefined ||
1211
+ currentRecord === undefined) {
1212
+ paths.push(path || '/');
1213
+ return;
1214
+ }
1215
+ for (const key of [
1216
+ ...new Set([
1217
+ ...Object.keys(previousRecord),
1218
+ ...Object.keys(currentRecord),
1219
+ ]),
1220
+ ].sort(deterministic_order_js_1.compareUtf16)) {
1221
+ if (paths.length >= MAX_REFRESH_DIAGNOSTIC_PATHS_PER_RESOURCE)
1222
+ break;
1223
+ visit(previousRecord[key], currentRecord[key], `${path}/${jsonPointerSegment(key)}`, depth + 1);
1224
+ }
1225
+ };
1226
+ visit(left, right, '', 0);
1227
+ return paths;
1228
+ }
1229
+ const EXTERNAL_LOOKUP_NAME_TYPES = new Set([
1230
+ 'aws:cloudwatch/eventBus:EventBus',
1231
+ 'aws:ecs/cluster:Cluster',
1232
+ 'aws:iam/role:Role',
1233
+ ]);
1234
+ const EXTERNAL_LOOKUP_TAG_NAME_TYPES = new Set([
1235
+ 'aws:ec2/eip:Eip',
1236
+ 'aws:ec2/instance:Instance',
1237
+ 'aws:ec2/internetGateway:InternetGateway',
1238
+ 'aws:ec2/natGateway:NatGateway',
1239
+ 'aws:ec2/routeTable:RouteTable',
1240
+ 'aws:ec2/securityGroup:SecurityGroup',
1241
+ 'aws:ec2/subnet:Subnet',
1242
+ 'aws:ec2/vpc:Vpc',
1243
+ ]);
1244
+ function exactRecord(value) {
1245
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
1246
+ ? value
1247
+ : undefined;
1248
+ }
1249
+ function exactStringAt(value, ...path) {
1250
+ let current = value;
1251
+ for (const segment of path) {
1252
+ const currentRecord = exactRecord(current);
1253
+ if (!currentRecord)
1254
+ return undefined;
1255
+ current = currentRecord[segment];
1256
+ }
1257
+ return typeof current === 'string' && current.length > 0
1258
+ ? current
1259
+ : undefined;
1260
+ }
1261
+ function escapedRegularExpression(value) {
1262
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1263
+ }
1264
+ const SST_PRETTY_NAME_SUFFIX = /^[abcdefhkmnorstuvwxz]{8}$/u;
1265
+ function exactSstPhysicalName(maxLength, project, stack, logicalId, candidate) {
1266
+ // Pinned SST 4.17.1's physicalName(max, name) reserves nine characters for
1267
+ // `-<pretty suffix>`, strips non-alphanumeric logical-name characters, and
1268
+ // selects one of name, stage+name, or app+stage+name before truncation. Long
1269
+ // external IAM role reads therefore do not use the simple full prefix below.
1270
+ const mainLength = maxLength - 9;
1271
+ const name = logicalId.replace(/[^a-zA-Z0-9]/gu, '');
1272
+ if (mainLength <= 0 || name.length === 0 || candidate.length > maxLength) {
1273
+ return false;
1274
+ }
1275
+ let expectedMain;
1276
+ if (name.length + 1 >= mainLength) {
1277
+ expectedMain = name.substring(0, mainLength);
1278
+ }
1279
+ else if (name.length + stack.length + 2 >= mainLength) {
1280
+ expectedMain = `${stack.substring(0, mainLength - name.length - 1)}-${name}`;
1281
+ }
1282
+ else {
1283
+ expectedMain = `${project.substring(0, mainLength - stack.length - name.length - 2)}-${stack}-${name}`;
1284
+ }
1285
+ return (candidate.length === expectedMain.length + 9 &&
1286
+ candidate.startsWith(`${expectedMain}-`) &&
1287
+ SST_PRETTY_NAME_SUFFIX.test(candidate.slice(-8)));
1288
+ }
1289
+ function exactOwnPropertyAbsent(value, ...path) {
1290
+ const property = path.at(-1);
1291
+ if (!property)
1292
+ return false;
1293
+ let current = value;
1294
+ for (const segment of path.slice(0, -1)) {
1295
+ const currentRecord = exactRecord(current);
1296
+ if (!currentRecord)
1297
+ return false;
1298
+ current = currentRecord[segment];
1299
+ }
1300
+ const parent = exactRecord(current);
1301
+ return Boolean(parent && !Object.prototype.hasOwnProperty.call(parent, property));
1302
+ }
1303
+ function exactExternalLookupNormalization(input) {
1304
+ // Pulumi `Resource.get` records stack-local auto-names in input state even
1305
+ // though the provider read is bound to a different exact physical ID. The
1306
+ // provider hydrates that one input from the observed resource on every
1307
+ // refresh, so treating it as owned-resource drift permanently wedges a
1308
+ // consumer of pre-provisioned roles or Foundation discovery resources. This
1309
+ // exception is intentionally limited to external resources, exact Pulumi
1310
+ // auto-name shapes, one known input path, and an unchanged output/identity
1311
+ // whose observed value corroborates the hydrated input.
1312
+ const urn = input.entry['urn'];
1313
+ const resourceType = input.entry['type'];
1314
+ if (typeof urn !== 'string' ||
1315
+ typeof resourceType !== 'string' ||
1316
+ input.resource?.external !== true ||
1317
+ input.resource.urn !== urn ||
1318
+ input.resource.type !== resourceType) {
1319
+ return undefined;
1320
+ }
1321
+ const urnParts = urn.split('::');
1322
+ const stack = urnParts[0]?.replace(/^urn:pulumi:/u, '');
1323
+ const project = urnParts[1];
1324
+ const logicalId = urnParts[3];
1325
+ if (urnParts.length !== 4 ||
1326
+ !stack ||
1327
+ !project ||
1328
+ !logicalId ||
1329
+ !/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/u.test(stack) ||
1330
+ !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(project) ||
1331
+ !/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u.test(logicalId)) {
1332
+ return undefined;
1333
+ }
1334
+ const oldValue = input.entry['old'];
1335
+ const newValue = input.entry['new'];
1336
+ const oldRecord = exactRecord(oldValue);
1337
+ const newRecord = exactRecord(newValue);
1338
+ const oldInputs = exactRecord(oldRecord?.['inputs']);
1339
+ const oldOutputs = exactRecord(oldRecord?.['outputs']);
1340
+ const retainedInputs = exactRecord(input.resource.inputs);
1341
+ const retainedOutputs = exactRecord(input.resource.outputs);
1342
+ if (!oldRecord ||
1343
+ !newRecord ||
1344
+ !oldInputs ||
1345
+ !oldOutputs ||
1346
+ !retainedInputs ||
1347
+ !retainedOutputs ||
1348
+ canonicalJson(oldInputs) !== canonicalJson(retainedInputs) ||
1349
+ canonicalJson(oldOutputs) !== canonicalJson(retainedOutputs) ||
1350
+ (oldRecord['id'] !== undefined && oldRecord['id'] !== input.resource.id) ||
1351
+ (newRecord['id'] !== undefined &&
1352
+ oldRecord['id'] !== undefined &&
1353
+ newRecord['id'] !== oldRecord['id'])) {
1354
+ return undefined;
1355
+ }
1356
+ const onlyPath = input.changedPropertyPaths.length === 1
1357
+ ? input.changedPropertyPaths[0]
1358
+ : undefined;
1359
+ const expectedPrefix = `${project}-${stack}-${logicalId}`;
1360
+ if (onlyPath === '/inputs/name' &&
1361
+ EXTERNAL_LOOKUP_NAME_TYPES.has(resourceType)) {
1362
+ const oldName = exactStringAt(oldRecord, 'inputs', 'name');
1363
+ const newName = exactStringAt(newRecord, 'inputs', 'name');
1364
+ const observedName = exactStringAt(newRecord, 'outputs', 'name');
1365
+ const retainedObservedName = exactStringAt(input.resource.outputs, 'name');
1366
+ if (oldName &&
1367
+ newName &&
1368
+ observedName &&
1369
+ retainedObservedName === observedName &&
1370
+ newName === observedName &&
1371
+ oldName !== newName &&
1372
+ (resourceType === 'aws:iam/role:Role'
1373
+ ? exactSstPhysicalName(64, project, stack, logicalId, oldName)
1374
+ : new RegExp(`^${escapedRegularExpression(expectedPrefix)}-[abcdefhkmnorstuvwxz]{8}$`, 'u').test(oldName))) {
1375
+ return { changedPropertyPath: onlyPath, logicalId, resourceType };
1376
+ }
1377
+ }
1378
+ if (onlyPath === '/inputs/tags/Name' &&
1379
+ EXTERNAL_LOOKUP_TAG_NAME_TYPES.has(resourceType)) {
1380
+ const oldName = exactStringAt(oldRecord, 'inputs', 'tags', 'Name');
1381
+ const newName = exactStringAt(newRecord, 'inputs', 'tags', 'Name');
1382
+ const observedName = exactStringAt(newRecord, 'outputs', 'tagsAll', 'Name') ??
1383
+ exactStringAt(newRecord, 'outputs', 'tags', 'Name');
1384
+ const retainedObservedName = exactStringAt(input.resource.outputs, 'tagsAll', 'Name') ??
1385
+ exactStringAt(input.resource.outputs, 'tags', 'Name');
1386
+ if (oldName === expectedPrefix &&
1387
+ newName &&
1388
+ observedName &&
1389
+ retainedObservedName === observedName &&
1390
+ newName === observedName &&
1391
+ oldName !== newName) {
1392
+ return { changedPropertyPath: onlyPath, logicalId, resourceType };
1393
+ }
1394
+ if (resourceType === 'aws:ec2/securityGroup:SecurityGroup' &&
1395
+ oldName === expectedPrefix &&
1396
+ exactOwnPropertyAbsent(newRecord, 'inputs', 'tags', 'Name') &&
1397
+ exactOwnPropertyAbsent(newRecord, 'outputs', 'tags', 'Name') &&
1398
+ exactOwnPropertyAbsent(newRecord, 'outputs', 'tagsAll', 'Name') &&
1399
+ exactOwnPropertyAbsent(input.resource.outputs, 'tags', 'Name') &&
1400
+ exactOwnPropertyAbsent(input.resource.outputs, 'tagsAll', 'Name')) {
1401
+ return { changedPropertyPath: onlyPath, logicalId, resourceType };
1402
+ }
1403
+ }
1404
+ return undefined;
1405
+ }
1406
+ const EXTERNAL_FRAMEWORK_TAG_OBSERVATION_PATHS = new Set([
1407
+ '/inputs/name',
1408
+ '/inputs/tags/Name',
1409
+ '/outputs/tags/ebk:framework-version',
1410
+ '/outputs/tagsAll/ebk:framework-version',
1411
+ ]);
1412
+ function externalProviderReadLogicalId(resourceType, physicalId) {
1413
+ // This is the secret-free identity projection used by
1414
+ // infrastructure-change/normalize.ts for Pulumi provider reads. Keeping the
1415
+ // correlation on that projection lets the refresh observer prove that an
1416
+ // external-state observation is also present in the ordinary desired plan
1417
+ // without retaining its physical ID.
1418
+ return `external-${(0, node_crypto_1.createHash)('sha256')
1419
+ .update(resourceType)
1420
+ .update('\0')
1421
+ .update(physicalId)
1422
+ .digest('hex')
1423
+ .slice(0, 24)}`;
1424
+ }
1425
+ function exactExternalFrameworkTagObservation(input) {
1426
+ // A downstream stack records Resource.get state for Foundation resources.
1427
+ // After Foundation advances its framework-version tag, Pulumi reports that
1428
+ // external observation during Billing's refresh phase even though Billing's
1429
+ // ordinary desired phase already classifies the same physical identity as a
1430
+ // non-mutating provider read. Exclude only that exact metadata transition:
1431
+ // retained external identity, one matching ordinary read, both framework
1432
+ // tag projections changing together, and at most SST's already-bounded
1433
+ // name hydration. Any other provider observation remains drift.
1434
+ const urn = input.entry['urn'];
1435
+ const resourceType = input.entry['type'];
1436
+ if (typeof urn !== 'string' ||
1437
+ typeof resourceType !== 'string' ||
1438
+ input.resource?.external !== true ||
1439
+ input.resource.urn !== urn ||
1440
+ input.resource.type !== resourceType ||
1441
+ input.changedPropertyPaths.length === 0 ||
1442
+ input.changedPropertyPaths.length >=
1443
+ MAX_REFRESH_DIAGNOSTIC_PATHS_PER_RESOURCE ||
1444
+ input.changedPropertyPaths.some((path) => !EXTERNAL_FRAMEWORK_TAG_OBSERVATION_PATHS.has(path)) ||
1445
+ !input.changedPropertyPaths.includes('/outputs/tags/ebk:framework-version') ||
1446
+ !input.changedPropertyPaths.includes('/outputs/tagsAll/ebk:framework-version')) {
1447
+ return undefined;
1448
+ }
1449
+ const inputPaths = input.changedPropertyPaths.filter((path) => path.startsWith('/inputs/'));
1450
+ if (inputPaths.length > 1)
1451
+ return undefined;
1452
+ const oldRecord = exactRecord(input.entry['old']);
1453
+ const newRecord = exactRecord(input.entry['new']);
1454
+ const oldInputs = exactRecord(oldRecord?.['inputs']);
1455
+ const oldOutputs = exactRecord(oldRecord?.['outputs']);
1456
+ const retainedInputs = exactRecord(input.resource.inputs);
1457
+ const retainedOutputs = exactRecord(input.resource.outputs);
1458
+ const physicalId = input.resource.id;
1459
+ if (!oldRecord ||
1460
+ !newRecord ||
1461
+ !oldInputs ||
1462
+ !oldOutputs ||
1463
+ !retainedInputs ||
1464
+ !retainedOutputs ||
1465
+ typeof physicalId !== 'string' ||
1466
+ physicalId.length === 0 ||
1467
+ canonicalJson(oldInputs) !== canonicalJson(retainedInputs) ||
1468
+ canonicalJson(oldOutputs) !== canonicalJson(retainedOutputs) ||
1469
+ (oldRecord['id'] !== undefined && oldRecord['id'] !== physicalId) ||
1470
+ (newRecord['id'] !== undefined && newRecord['id'] !== physicalId)) {
1471
+ return undefined;
1472
+ }
1473
+ const oldFrameworkVersion = exactStringAt(oldRecord, 'outputs', 'tagsAll', 'ebk:framework-version');
1474
+ const oldFrameworkVersionTag = exactStringAt(oldRecord, 'outputs', 'tags', 'ebk:framework-version');
1475
+ const newFrameworkVersion = exactStringAt(newRecord, 'outputs', 'tagsAll', 'ebk:framework-version');
1476
+ const newFrameworkVersionTag = exactStringAt(newRecord, 'outputs', 'tags', 'ebk:framework-version');
1477
+ if (!oldFrameworkVersion ||
1478
+ oldFrameworkVersionTag !== oldFrameworkVersion ||
1479
+ !newFrameworkVersion ||
1480
+ newFrameworkVersionTag !== newFrameworkVersion ||
1481
+ oldFrameworkVersion === newFrameworkVersion) {
1482
+ return undefined;
1483
+ }
1484
+ if (inputPaths.length === 1 &&
1485
+ !exactExternalLookupNormalization({
1486
+ changedPropertyPaths: inputPaths,
1487
+ entry: input.entry,
1488
+ resource: input.resource,
1489
+ })) {
1490
+ return undefined;
1491
+ }
1492
+ const providerReadLogicalId = externalProviderReadLogicalId(resourceType, physicalId);
1493
+ const providerReadKey = `${resourceType}\0${providerReadLogicalId}`;
1494
+ if ((input.ordinaryProviderReadCounts.get(providerReadKey) ?? 0) < 1) {
1495
+ return undefined;
1496
+ }
1497
+ return {
1498
+ changedPropertyPaths: [...input.changedPropertyPaths],
1499
+ logicalId: urn.split('::').at(-1) ?? '',
1500
+ providerReadLogicalId,
1501
+ resourceType,
1502
+ };
652
1503
  }
653
1504
  function assessPulumiRefreshPreview(input) {
654
1505
  const sstPath = input.invocation.args[0];
@@ -660,14 +1511,14 @@ function assessPulumiRefreshPreview(input) {
660
1511
  env: {
661
1512
  ...input.childEnvironment,
662
1513
  PULUMI_OPTION_REFRESH: 'true',
663
- PULUMI_OPTION_RUN_PROGRAM: 'true',
1514
+ PULUMI_OPTION_RUN_PROGRAM: 'false',
664
1515
  },
665
- maxBuffer: 20 * 1024 * 1024,
1516
+ maxBuffer: exports.MAX_SST_DIFF_JSON_BYTES,
666
1517
  shell: false,
667
1518
  });
668
1519
  let raw;
669
1520
  try {
670
- raw = parseJson(result.stdout, 'SST refresh preview');
1521
+ raw = parseRefreshPreviewJson(result.stdout);
671
1522
  }
672
1523
  catch {
673
1524
  raw = null;
@@ -675,7 +1526,26 @@ function assessPulumiRefreshPreview(input) {
675
1526
  const entries = Array.isArray(raw) ? raw : [];
676
1527
  const refreshEntries = [];
677
1528
  const desiredEntries = [];
678
- let shapeComplete = Array.isArray(raw);
1529
+ const diagnosticResources = [];
1530
+ const externalFrameworkTagObservations = [];
1531
+ const externalLookupNormalizations = [];
1532
+ const ordinaryProviderReadCounts = new Map();
1533
+ for (const resource of input.ordinaryDiff.resources) {
1534
+ if (resource.operation !== 'read' ||
1535
+ resource.providerOperation !== 'read' ||
1536
+ resource.replacement ||
1537
+ resource.changedProperties.length > 0) {
1538
+ continue;
1539
+ }
1540
+ const key = `${resource.resourceType}\0${resource.logicalId}`;
1541
+ ordinaryProviderReadCounts.set(key, (ordinaryProviderReadCounts.get(key) ?? 0) + 1);
1542
+ }
1543
+ const externalResources = (input.stateResources ?? []).filter((resource) => resource.external === true && typeof resource.urn === 'string');
1544
+ const externalResourcesByUrn = new Map(externalResources.map((resource) => [resource.urn, resource]));
1545
+ let shapeComplete = Array.isArray(raw) &&
1546
+ externalResourcesByUrn.size === externalResources.length;
1547
+ let inertSameCount = 0;
1548
+ let rejectedSameCount = 0;
679
1549
  const observedRefreshUrns = new Set();
680
1550
  for (const entry of entries) {
681
1551
  if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
@@ -700,72 +1570,334 @@ function assessPulumiRefreshPreview(input) {
700
1570
  !Array.isArray(detailedDiff) &&
701
1571
  Object.keys(detailedDiff).length > 0;
702
1572
  const hasDiffs = Array.isArray(diffs) && diffs.length > 0;
1573
+ const changedPropertyPaths = [
1574
+ ...(hasOld && hasNew
1575
+ ? changedJsonPointerPaths(recordEntry['old'], recordEntry['new'])
1576
+ : []),
1577
+ ...(hasDetailedDiff
1578
+ ? Object.keys(detailedDiff).map((path) => `/providerDetailedDiff/${jsonPointerSegment(path)}`)
1579
+ : []),
1580
+ ...(hasDiffs
1581
+ ? diffs
1582
+ .filter((path) => typeof path === 'string')
1583
+ .map((path) => `/providerDiffs/${jsonPointerSegment(path)}`)
1584
+ : []),
1585
+ ];
1586
+ const boundedChangedPropertyPaths = [...new Set(changedPropertyPaths)]
1587
+ .sort(deterministic_order_js_1.compareUtf16)
1588
+ .slice(0, MAX_REFRESH_DIAGNOSTIC_PATHS_PER_RESOURCE);
1589
+ const externalLookupNormalization = !hasDetailedDiff && !hasDiffs
1590
+ ? exactExternalLookupNormalization({
1591
+ changedPropertyPaths: [...new Set(changedPropertyPaths)].sort(deterministic_order_js_1.compareUtf16),
1592
+ entry: recordEntry,
1593
+ resource: typeof urn === 'string'
1594
+ ? externalResourcesByUrn.get(urn)
1595
+ : undefined,
1596
+ })
1597
+ : undefined;
1598
+ const externalFrameworkTagObservation = input.action === 'deploy' && !hasDetailedDiff && !hasDiffs
1599
+ ? exactExternalFrameworkTagObservation({
1600
+ changedPropertyPaths: [...new Set(changedPropertyPaths)].sort(deterministic_order_js_1.compareUtf16),
1601
+ entry: recordEntry,
1602
+ ordinaryProviderReadCounts,
1603
+ resource: typeof urn === 'string'
1604
+ ? externalResourcesByUrn.get(urn)
1605
+ : undefined,
1606
+ })
1607
+ : undefined;
703
1608
  if (hasNew && recordEntry['new'] === null) {
704
1609
  refreshEntries.push({ ...recordEntry, op: 'delete' });
705
1610
  }
1611
+ else if (hasOld &&
1612
+ hasNew &&
1613
+ canonicalJson(recordEntry['old']) === canonicalJson(recordEntry['new'])) {
1614
+ // The refresh old/new pair is Pulumi's recorded-versus-observed cloud
1615
+ // authority. A refresh-preview event can also inherit detailedDiff and
1616
+ // diffs metadata from the ordinary desired-source phase (for example,
1617
+ // a candidate tag update). Identical canonical old/new material proves
1618
+ // the provider read itself observed no change; desired changes remain
1619
+ // independently bound by the preceding ordinary preview.
1620
+ continue;
1621
+ }
1622
+ else if (externalLookupNormalization) {
1623
+ if (externalLookupNormalizations.length >=
1624
+ MAX_REFRESH_DIAGNOSTIC_RESOURCES) {
1625
+ shapeComplete = false;
1626
+ }
1627
+ else {
1628
+ externalLookupNormalizations.push(externalLookupNormalization);
1629
+ }
1630
+ continue;
1631
+ }
1632
+ else if (externalFrameworkTagObservation) {
1633
+ if (externalFrameworkTagObservations.length >=
1634
+ MAX_REFRESH_DIAGNOSTIC_RESOURCES) {
1635
+ shapeComplete = false;
1636
+ }
1637
+ else {
1638
+ externalFrameworkTagObservations.push(externalFrameworkTagObservation);
1639
+ const key = `${externalFrameworkTagObservation.resourceType}\0${externalFrameworkTagObservation.providerReadLogicalId}`;
1640
+ ordinaryProviderReadCounts.set(key, (ordinaryProviderReadCounts.get(key) ?? 1) - 1);
1641
+ }
1642
+ continue;
1643
+ }
706
1644
  else if (hasDetailedDiff ||
707
1645
  hasDiffs ||
708
1646
  (hasOld &&
709
1647
  hasNew &&
710
1648
  canonicalJson(recordEntry['old']) !==
711
1649
  canonicalJson(recordEntry['new']))) {
712
- refreshEntries.push({ ...recordEntry, op: 'update' });
713
- }
714
- else if (hasOld &&
715
- hasNew &&
716
- canonicalJson(recordEntry['old']) === canonicalJson(recordEntry['new'])) {
717
- // Refresh events with identical old/new material are provider reads,
718
- // not drift. SST filters Pulumi "same" steps, so this is the explicit
719
- // no-change refresh shape.
720
- continue;
1650
+ refreshEntries.push({
1651
+ ...recordEntry,
1652
+ detailedDiff: Object.fromEntries(boundedChangedPropertyPaths.map((path) => [
1653
+ path,
1654
+ {
1655
+ diffKind: 'update',
1656
+ inputDiff: path.startsWith('/inputs/'),
1657
+ },
1658
+ ])),
1659
+ op: 'update',
1660
+ });
721
1661
  }
722
1662
  else {
723
1663
  shapeComplete = false;
724
1664
  }
1665
+ if (refreshEntries.length > diagnosticResources.length &&
1666
+ diagnosticResources.length < MAX_REFRESH_DIAGNOSTIC_RESOURCES) {
1667
+ diagnosticResources.push({
1668
+ changeBasis: [
1669
+ ...(hasDetailedDiff ? ['detailedDiff'] : []),
1670
+ ...(hasDiffs ? ['diffs'] : []),
1671
+ ...(hasOld &&
1672
+ hasNew &&
1673
+ canonicalJson(recordEntry['old']) !==
1674
+ canonicalJson(recordEntry['new'])
1675
+ ? ['old-new']
1676
+ : []),
1677
+ ],
1678
+ changedPropertyPaths: boundedChangedPropertyPaths,
1679
+ logicalId: typeof urn === 'string' ? (urn.split('::').at(-1) ?? '') : '',
1680
+ resourceType: typeof recordEntry['type'] === 'string'
1681
+ ? recordEntry['type']
1682
+ : 'unknown',
1683
+ });
1684
+ }
725
1685
  }
726
- else if (operation !== 'same') {
727
- desiredEntries.push(entry);
1686
+ else if (operation === 'same') {
1687
+ const isExactNoChangeMarker = isExactInertSameEntry(recordEntry);
1688
+ if (isExactNoChangeMarker)
1689
+ inertSameCount += 1;
1690
+ else {
1691
+ rejectedSameCount += 1;
1692
+ shapeComplete = false;
1693
+ }
1694
+ // Pinned Pulumi can emit an inert `same` step when the provider refresh
1695
+ // observed no drift and the following desired preview has no changes.
1696
+ // It carries no desired authority. Accept only the exact no-change shape
1697
+ // above and omit it from both the drift and desired projections.
728
1698
  }
729
1699
  else {
730
- shapeComplete = false;
1700
+ desiredEntries.push(entry);
731
1701
  }
732
1702
  }
733
- const drift = (0, index_js_1.normalizeSstDiffV4_17_1)({
734
- exitCode: result.status ?? 1,
1703
+ const candidateDrift = (0, index_js_1.normalizeSstDiffV4_17_1)({
1704
+ exitCode: 0,
735
1705
  stdout: refreshEntries,
736
1706
  toolVersion: input.toolVersion,
737
1707
  });
738
- const desired = (0, index_js_1.normalizeSstDiffV4_17_1)({
739
- exitCode: result.status ?? 1,
1708
+ const candidatePostRefreshDesired = normalizeDesiredSstDiffV4_17_1({
1709
+ exitCode: 0,
740
1710
  stdout: desiredEntries,
741
1711
  toolVersion: input.toolVersion,
742
1712
  });
1713
+ const knownDiagnosticOperations = new Set([
1714
+ 'create',
1715
+ 'create-replacement',
1716
+ 'delete',
1717
+ 'delete-replaced',
1718
+ 'discard',
1719
+ 'discard-replaced',
1720
+ 'import',
1721
+ 'import-replacement',
1722
+ 'read',
1723
+ 'read-replacement',
1724
+ 'remove-pending-replace',
1725
+ 'replace',
1726
+ 'update',
1727
+ ]);
1728
+ const postRefreshDesiredOperationCounts = {};
1729
+ for (const entry of desiredEntries) {
1730
+ const operation = typeof entry === 'object' &&
1731
+ entry !== null &&
1732
+ !Array.isArray(entry) &&
1733
+ typeof entry['op'] === 'string' &&
1734
+ knownDiagnosticOperations.has(entry['op'])
1735
+ ? entry['op']
1736
+ : 'unsupported';
1737
+ postRefreshDesiredOperationCounts[operation] =
1738
+ (postRefreshDesiredOperationCounts[operation] ?? 0) + 1;
1739
+ }
1740
+ const spawnErrorCode = result.error?.code;
1741
+ const errorCode = typeof spawnErrorCode === 'string' &&
1742
+ /^E[A-Z0-9_]{1,31}$/.test(spawnErrorCode)
1743
+ ? spawnErrorCode
1744
+ : null;
1745
+ const command = {
1746
+ errorCode,
1747
+ signal: typeof result.signal === 'string' &&
1748
+ /^SIG[A-Z0-9]{1,15}$/.test(result.signal)
1749
+ ? result.signal
1750
+ : null,
1751
+ status: Number.isInteger(result.status) ? result.status : null,
1752
+ stderrBytes: Buffer.byteLength(result.stderr ?? '', 'utf8'),
1753
+ stdoutBytes: Buffer.byteLength(result.stdout ?? '', 'utf8'),
1754
+ };
1755
+ // SST/Pulumi emits no machine-readable completion marker for an exit-one
1756
+ // preview. A well-formed prefix is therefore not mutation authority.
743
1757
  const commandSucceeded = result.status === 0;
744
- const desiredMatchesOrdinary = desired.complete &&
745
- input.ordinaryDiff.complete &&
746
- canonicalJson(normalizedDiffMaterial(desired)) ===
747
- canonicalJson(normalizedDiffMaterial(input.ordinaryDiff));
1758
+ const drift = commandSucceeded
1759
+ ? candidateDrift
1760
+ : (0, index_js_1.normalizeSstDiffV4_17_1)({
1761
+ exitCode: result.status ?? -1,
1762
+ stdout: refreshEntries,
1763
+ toolVersion: input.toolVersion,
1764
+ });
1765
+ const postRefreshDesired = commandSucceeded
1766
+ ? candidatePostRefreshDesired
1767
+ : normalizeDesiredSstDiffV4_17_1({
1768
+ exitCode: result.status ?? -1,
1769
+ stdout: desiredEntries,
1770
+ toolVersion: input.toolVersion,
1771
+ });
1772
+ const postRefreshDesiredFailureCounts = {};
1773
+ for (const failure of postRefreshDesired.failures) {
1774
+ postRefreshDesiredFailureCounts[failure.code] =
1775
+ (postRefreshDesiredFailureCounts[failure.code] ?? 0) + 1;
1776
+ }
1777
+ const ordinaryDesired = withoutRefreshStateResources(input.ordinaryDiff);
1778
+ const ordinaryDesiredMaterial = normalizedDiffMaterial(ordinaryDesired);
1779
+ const refreshReconciliation = input.action === 'refresh';
1780
+ const providerRefreshSeparated = (ordinaryDesired.complete || refreshReconciliation) &&
1781
+ postRefreshDesired.complete;
748
1782
  const complete = commandSucceeded &&
749
1783
  shapeComplete &&
750
1784
  drift.complete &&
751
- desired.complete &&
1785
+ (ordinaryDesired.complete || refreshReconciliation) &&
1786
+ postRefreshDesired.complete &&
752
1787
  input.toolVersion === index_js_1.SST_DIFF_TOOL_VERSION &&
753
1788
  PULUMI_REFRESH_PREVIEW_VERSION === '3.215.0';
754
- return {
755
- commandSucceeded,
756
- complete,
757
- desiredMatchesOrdinary,
758
- digest: digest(canonicalJson({
759
- desired: normalizedDiffMaterial(desired),
760
- desiredMatchesOrdinary,
1789
+ const entryCounts = {
1790
+ desired: desiredEntries.length,
1791
+ externalFrameworkTagObserved: externalFrameworkTagObservations.length,
1792
+ externalLookupNormalized: externalLookupNormalizations.length,
1793
+ inertSame: inertSameCount,
1794
+ refresh: observedRefreshUrns.size,
1795
+ rejectedSame: rejectedSameCount,
1796
+ };
1797
+ const ordinaryDesiredDiagnostics = {
1798
+ complete: ordinaryDesired.complete,
1799
+ failureCounts: Object.fromEntries(ordinaryDesired.failures
1800
+ .map(({ code }) => code)
1801
+ .sort(deterministic_order_js_1.compareUtf16)
1802
+ .reduce((counts, code) => {
1803
+ counts.set(code, (counts.get(code) ?? 0) + 1);
1804
+ return counts;
1805
+ }, new Map())),
1806
+ resourceCount: ordinaryDesired.resources.length,
1807
+ };
1808
+ const postRefreshDesiredDiagnostics = {
1809
+ complete: postRefreshDesired.complete,
1810
+ failureCounts: Object.fromEntries(Object.entries(postRefreshDesiredFailureCounts).sort(([left], [right]) => (0, deterministic_order_js_1.compareUtf16)(left, right))),
1811
+ operationCounts: Object.fromEntries(Object.entries(postRefreshDesiredOperationCounts).sort(([left], [right]) => (0, deterministic_order_js_1.compareUtf16)(left, right))),
1812
+ resourceCount: postRefreshDesired.resources.length,
1813
+ };
1814
+ const sortedDiagnosticResources = diagnosticResources.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
1815
+ (0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
1816
+ const sortedExternalLookupNormalizations = externalLookupNormalizations.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
1817
+ (0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
1818
+ const sortedExternalFrameworkTagObservations = externalFrameworkTagObservations.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
1819
+ (0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
1820
+ const observedRefreshInventory = [...observedRefreshUrns].sort();
1821
+ const previewDigest = digest(canonicalJson(
1822
+ // Refresh applies only the observed provider-state reconciliation. The
1823
+ // following desired preview is a completeness/operation-shape guard and
1824
+ // must be planned again before any deploy; binding its ephemeral build
1825
+ // identifiers here would make an otherwise identical refresh unreviewable.
1826
+ refreshReconciliation
1827
+ ? {
1828
+ action: input.action,
1829
+ commandSucceeded,
1830
+ complete,
1831
+ desiredAuthority: 'refresh-reconciliation-plus-post-refresh-desired-validation',
761
1832
  drift: normalizedDiffMaterial(drift),
1833
+ entryCounts,
1834
+ externalFrameworkTagObservations: sortedExternalFrameworkTagObservations,
1835
+ externalLookupNormalizations: sortedExternalLookupNormalizations,
1836
+ observedRefreshInventory,
1837
+ ordinaryDesired: ordinaryDesiredDiagnostics,
1838
+ postRefreshDesired: postRefreshDesiredDiagnostics,
1839
+ providerRefreshSeparated,
762
1840
  pulumiVersion: PULUMI_REFRESH_PREVIEW_VERSION,
1841
+ resources: sortedDiagnosticResources,
763
1842
  sstVersion: input.toolVersion,
764
- })),
1843
+ }
1844
+ : {
1845
+ action: input.action,
1846
+ desired: ordinaryDesiredMaterial,
1847
+ desiredAuthority: 'ordinary-preview-plus-separated-refresh-preview',
1848
+ ordinaryDesiredComplete: ordinaryDesired.complete,
1849
+ ordinaryDesiredFailures: ordinaryDesired.failures,
1850
+ drift: normalizedDiffMaterial(drift),
1851
+ externalFrameworkTagObservations: sortedExternalFrameworkTagObservations,
1852
+ externalLookupNormalizations: sortedExternalLookupNormalizations,
1853
+ // The ordinary preview above is the exact desired-source
1854
+ // authority retained by the material infrastructure plan. The
1855
+ // second program execution exists only to prove the provider
1856
+ // refresh stayed separated and that the desired program still
1857
+ // completed. Binding its full resource projection makes harmless
1858
+ // build/linker scheduling differences create a new drift digest
1859
+ // even when the ordinary material plan and provider observation
1860
+ // are exact. Retain its bounded completeness and operation shape;
1861
+ // never substitute it for the ordinary desired authority.
1862
+ postRefreshDesired: postRefreshDesiredDiagnostics,
1863
+ providerRefreshSeparated,
1864
+ pulumiVersion: PULUMI_REFRESH_PREVIEW_VERSION,
1865
+ sstVersion: input.toolVersion,
1866
+ }));
1867
+ const diagnostics = {
1868
+ authorityMode: refreshReconciliation
1869
+ ? 'refresh-reconciliation'
1870
+ : 'ordinary-and-refresh-separated',
1871
+ command,
1872
+ commandSucceeded,
1873
+ complete,
1874
+ entryCounts,
1875
+ providerRefreshSeparated,
1876
+ ordinaryDesired: ordinaryDesiredDiagnostics,
1877
+ postRefreshDesired: postRefreshDesiredDiagnostics,
1878
+ digest: previewDigest,
1879
+ kind: 'ebk-infrastructure-refresh-preview-diagnostics',
1880
+ externalFrameworkTagObservations: sortedExternalFrameworkTagObservations,
1881
+ externalLookupNormalizations: sortedExternalLookupNormalizations,
1882
+ resources: sortedDiagnosticResources,
1883
+ schemaVersion: 7,
1884
+ };
1885
+ return {
1886
+ commandSucceeded,
1887
+ complete,
1888
+ diagnostics,
1889
+ providerRefreshSeparated,
1890
+ digest: previewDigest,
765
1891
  drift,
766
- observedRefreshUrns: [...observedRefreshUrns].sort(),
1892
+ observedRefreshUrns: observedRefreshInventory,
1893
+ rawDrift: JSON.stringify(refreshEntries),
1894
+ stderr: result.stderr,
767
1895
  };
768
1896
  }
1897
+ /** @internal */
1898
+ function requiresIamMutationAnalysis(_action, resource) {
1899
+ return (0, index_js_1.requiresIamPolicyAnalysis)(resource);
1900
+ }
769
1901
  function costQuotaEvidence(input) {
770
1902
  const quotaByType = {
771
1903
  'aws:lambda/function:Function': {
@@ -869,8 +2001,10 @@ function iamPolicyDocuments(input) {
869
2001
  : undefined;
870
2002
  if (!policyProperty)
871
2003
  continue;
872
- if (resource.operation === 'delete')
2004
+ if (resource.operation === 'delete' ||
2005
+ REFRESH_STATE_PROVIDER_OPERATIONS.has(resource.providerOperation)) {
873
2006
  continue;
2007
+ }
874
2008
  const rawEntry = entries[resource.sourceIndex];
875
2009
  if (typeof rawEntry !== 'object' ||
876
2010
  rawEntry === null ||
@@ -1753,16 +2887,19 @@ function probeStandardRemoveResource(input) {
1753
2887
  function observeInfrastructureState(input) {
1754
2888
  const stage = requiredEnvironment(input.childEnvironment, 'EBK_CONSUMER_STAGE');
1755
2889
  const region = requiredEnvironment(input.childEnvironment, 'AWS_REGION');
1756
- const expectedAccount = requiredEnvironment(input.environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/);
2890
+ const expectedAccount = expectedDeploymentAccount(input.environment, input.invocation);
1757
2891
  const caller = record(runJsonCommand(input.spawn, 'aws', ['sts', 'get-caller-identity', '--output', 'json'], {
1758
2892
  cwd: input.invocation.cwd,
1759
2893
  env: input.childEnvironment,
1760
2894
  label: 'AWS caller identity',
1761
2895
  }), 'AWS caller identity');
2896
+ const callerArn = typeof caller['Arn'] === 'string' ? caller['Arn'] : '';
2897
+ const expectedRoleArn = input.childEnvironment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN?.trim();
1762
2898
  if (caller['Account'] !== expectedAccount ||
1763
- typeof caller['Arn'] !== 'string' ||
1764
- !caller['Arn']) {
1765
- throw new Error('Read-only infrastructure observer does not match the expected AWS account.');
2899
+ !callerArn ||
2900
+ (expectedRoleArn &&
2901
+ !matchesExpectedAssumedRoleArn(expectedAccount, callerArn, expectedRoleArn))) {
2902
+ throw new Error('Read-only infrastructure observer does not match the expected AWS account and temporary assumed role.');
1766
2903
  }
1767
2904
  const stateLocation = exactSstStateLocation(input.environment);
1768
2905
  const stateKey = `app/${input.project}/${stage}.json`;
@@ -1794,7 +2931,10 @@ function observeInfrastructureState(input) {
1794
2931
  maxBuffer: 32 * 1024 * 1024,
1795
2932
  })
1796
2933
  : null;
1797
- const resources = stateExists ? stateResources(exported) : [];
2934
+ const exportedState = stateExists
2935
+ ? stateExport(exported)
2936
+ : { resources: [] };
2937
+ const resources = exportedState.resources;
1798
2938
  const stackIdentities = new Set();
1799
2939
  for (const resource of resources) {
1800
2940
  const urn = typeof resource.urn === 'string' ? resource.urn : '';
@@ -1808,6 +2948,10 @@ function observeInfrastructureState(input) {
1808
2948
  const stackIdentity = stateExists
1809
2949
  ? [...stackIdentities][0]
1810
2950
  : `${input.project}/${stage}`;
2951
+ if (exportedState.sstStack &&
2952
+ exportedState.sstStack.split('/').slice(1).join('/') !== stackIdentity) {
2953
+ throw new Error('SST state checkpoint identity does not match its Pulumi resource inventory.');
2954
+ }
1811
2955
  const [application, observedStage] = stackIdentity.split('/');
1812
2956
  if (!application || observedStage !== stage) {
1813
2957
  throw new Error('SST state export stack identity does not match the requested stage.');
@@ -1833,14 +2977,14 @@ function observeInfrastructureState(input) {
1833
2977
  urn: typeof resource.urn === 'string' ? resource.urn : 'unknown',
1834
2978
  }));
1835
2979
  const refreshPreview = assessPulumiRefreshPreview({
2980
+ action: input.action,
1836
2981
  childEnvironment: input.childEnvironment,
1837
2982
  invocation: input.invocation,
1838
2983
  ordinaryDiff: input.ordinaryDiff,
1839
2984
  spawn: input.spawn,
2985
+ stateResources: deployableResources,
1840
2986
  toolVersion: input.toolVersion,
1841
2987
  });
1842
- const refreshInventoryComplete = deployableResources.every((resource) => typeof resource.urn === 'string' &&
1843
- refreshPreview.observedRefreshUrns.includes(resource.urn));
1844
2988
  const structuralResources = deployableResources
1845
2989
  .map((resource) => ({
1846
2990
  dependencies: Array.isArray(resource.dependencies)
@@ -1862,9 +3006,15 @@ function observeInfrastructureState(input) {
1862
3006
  urn: typeof resource.urn === 'string' ? resource.urn : 'unknown',
1863
3007
  }))
1864
3008
  .filter((value) => value.arn !== undefined && value.tags !== undefined);
3009
+ // Multiple Pulumi resources can deliberately project the same physical AWS
3010
+ // resource (for example, a workload role and its registration component).
3011
+ // The Resource Groups Tagging API rejects duplicate ResourceARNList values,
3012
+ // while the evidence model must still compare the one observed tag set with
3013
+ // every distinct Pulumi URN below.
3014
+ const taggedArns = [...new Set(tagged.map(({ arn }) => arn))].sort(deterministic_order_js_1.compareUtf16);
1865
3015
  const observedTags = new Map();
1866
- for (let index = 0; index < tagged.length; index += 100) {
1867
- const arns = tagged.slice(index, index + 100).map(({ arn }) => arn);
3016
+ for (let index = 0; index < taggedArns.length; index += 100) {
3017
+ const arns = taggedArns.slice(index, index + 100);
1868
3018
  const result = record(runJsonCommand(input.spawn, 'aws', [
1869
3019
  'resourcegroupstaggingapi',
1870
3020
  'get-resources',
@@ -1907,6 +3057,27 @@ function observeInfrastructureState(input) {
1907
3057
  typeof resource.urn === 'string' ? resource.urn : 'unknown',
1908
3058
  resource,
1909
3059
  ]));
3060
+ const secretVersionObservations = new Map(deployableResources
3061
+ .filter((resource) => resource.type === 'aws:secretsmanager/secretVersion:SecretVersion')
3062
+ .map((resource) => {
3063
+ const observation = observeSecretsManagerVersion({
3064
+ account: expectedAccount,
3065
+ childEnvironment: input.childEnvironment,
3066
+ invocation: input.invocation,
3067
+ region,
3068
+ resource,
3069
+ spawn: input.spawn,
3070
+ });
3071
+ return [observation.urn, observation];
3072
+ }));
3073
+ // A successful pinned Pulumi refresh preview reads the complete stack but
3074
+ // emits refresh events only for recorded-versus-observed changes. Unchanged
3075
+ // resources are intentionally absent from the JSON event stream; requiring
3076
+ // one event per state resource would turn a clean read into partial coverage.
3077
+ // Every emitted refresh event is still parsed fail-closed above. Pulumi's
3078
+ // later desired-preview events are independently normalized but never used
3079
+ // as drift authority; the preceding ordinary preview remains the sole
3080
+ // desired-update authority.
1910
3081
  const coverage = structuralResources.map(({ type, urn }) => {
1911
3082
  const taggedResource = taggedByUrn.get(urn);
1912
3083
  const observed = taggedResource !== undefined && observedTags.has(taggedResource.arn);
@@ -1919,27 +3090,33 @@ function observeInfrastructureState(input) {
1919
3090
  : []),
1920
3091
  ].filter((path) => materialProperties.includes(path));
1921
3092
  const refreshCoversResource = refreshPreview.complete &&
1922
- refreshPreview.desiredMatchesOrdinary &&
1923
- incompleteInventory.length === 0 &&
1924
- refreshPreview.observedRefreshUrns.includes(urn);
3093
+ refreshPreview.providerRefreshSeparated &&
3094
+ incompleteInventory.length === 0;
3095
+ const secretVersionObservation = secretVersionObservations.get(urn);
1925
3096
  return {
1926
3097
  adapter: refreshCoversResource
1927
- ? 'sst-pulumi-refresh-preview'
3098
+ ? secretVersionObservation
3099
+ ? 'sst-pulumi-refresh-preview+aws-secrets-manager-version-stages'
3100
+ : 'sst-pulumi-refresh-preview'
1928
3101
  : taggedResource === undefined
1929
3102
  ? 'none'
1930
3103
  : 'aws-resource-groups-tagging-api',
1931
- arnDigest: taggedResource === undefined ? null : digest(taggedResource.arn),
3104
+ arnDigest: secretVersionObservation?.arnDigest ??
3105
+ (taggedResource === undefined ? null : digest(taggedResource.arn)),
1932
3106
  coveredProperties: refreshCoversResource
1933
3107
  ? [
1934
3108
  'existence',
1935
- ...materialProperties.filter((path) => !overlayProperties.includes(path)),
3109
+ ...materialProperties.filter((path) => !overlayProperties.includes(path) ||
3110
+ secretVersionObservation !== undefined),
1936
3111
  ]
1937
3112
  : observed
1938
3113
  ? ['existence', 'tags']
1939
3114
  : [],
1940
3115
  type,
1941
3116
  unobservedMaterialProperties: refreshCoversResource
1942
- ? overlayProperties
3117
+ ? secretVersionObservation
3118
+ ? []
3119
+ : overlayProperties
1943
3120
  : materialProperties.length > 0
1944
3121
  ? materialProperties
1945
3122
  : ['service-native-configuration'],
@@ -1947,7 +3124,8 @@ function observeInfrastructureState(input) {
1947
3124
  };
1948
3125
  });
1949
3126
  const unsupported = coverage
1950
- .filter(({ coveredProperties, unobservedMaterialProperties }) => coveredProperties.length < 2 || unobservedMaterialProperties.length > 0)
3127
+ .filter(({ coveredProperties, unobservedMaterialProperties }) => coveredProperties.length === 0 ||
3128
+ unobservedMaterialProperties.length > 0)
1951
3129
  .map(({ adapter, coveredProperties, type, unobservedMaterialProperties, urn, }) => ({
1952
3130
  adapter,
1953
3131
  coveredProperties,
@@ -1956,8 +3134,7 @@ function observeInfrastructureState(input) {
1956
3134
  urn,
1957
3135
  }));
1958
3136
  if (!refreshPreview.complete ||
1959
- !refreshPreview.desiredMatchesOrdinary ||
1960
- !refreshInventoryComplete ||
3137
+ !refreshPreview.providerRefreshSeparated ||
1961
3138
  incompleteInventory.length > 0) {
1962
3139
  unsupported.push({
1963
3140
  adapter: 'sst-pulumi-refresh-preview',
@@ -1965,10 +3142,9 @@ function observeInfrastructureState(input) {
1965
3142
  type: 'full-stack-refresh-preview',
1966
3143
  unobservedMaterialProperties: [
1967
3144
  ...(!refreshPreview.complete ? ['preview.complete'] : []),
1968
- ...(!refreshPreview.desiredMatchesOrdinary
1969
- ? ['preview.desired-plan-equivalence']
3145
+ ...(!refreshPreview.providerRefreshSeparated
3146
+ ? ['preview.provider-refresh-separated']
1970
3147
  : []),
1971
- ...(!refreshInventoryComplete ? ['preview.refresh-inventory'] : []),
1972
3148
  ...(incompleteInventory.length > 0 ? ['state.custom-inventory'] : []),
1973
3149
  ],
1974
3150
  urn: 'full-stack',
@@ -1989,12 +3165,18 @@ function observeInfrastructureState(input) {
1989
3165
  stateKey: `app/${application}/${stage}.json`,
1990
3166
  })),
1991
3167
  };
3168
+ const allDrifted = [
3169
+ ...drifted,
3170
+ ...[...secretVersionObservations.values()]
3171
+ .filter((observation) => observation.drifted)
3172
+ .map(({ arnDigest, urn }) => ({ arnDigest, urn })),
3173
+ ].sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.urn, right.urn));
1992
3174
  const driftStatus = !refreshPreview.commandSucceeded
1993
3175
  ? 'failed'
1994
- : refreshPreview.drift.resources.length > 0 || drifted.length > 0
1995
- ? 'drift-detected'
1996
- : unsupported.length > 0
1997
- ? 'partial-coverage'
3176
+ : unsupported.length > 0
3177
+ ? 'partial-coverage'
3178
+ : refreshPreview.drift.resources.length > 0 || allDrifted.length > 0
3179
+ ? 'drift-detected'
1998
3180
  : 'no-drift';
1999
3181
  // The full export is canonicalized and hashed in memory. Only the digest is
2000
3182
  // retained, so changes to IDs, inputs, outputs, protection, or provider
@@ -2038,11 +3220,14 @@ function observeInfrastructureState(input) {
2038
3220
  })
2039
3221
  .sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
2040
3222
  (0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
2041
- return {
3223
+ const observation = {
2042
3224
  callInventory: [
2043
3225
  'resourcegroupstaggingapi:GetResources',
2044
3226
  's3:GetObject',
2045
3227
  's3:ListBucket',
3228
+ ...(secretVersionObservations.size > 0
3229
+ ? ['secretsmanager:ListSecretVersionIds']
3230
+ : []),
2046
3231
  'sst:DiffRefreshPreview',
2047
3232
  'ssm:GetParameter',
2048
3233
  'sts:GetCallerIdentity',
@@ -2051,19 +3236,28 @@ function observeInfrastructureState(input) {
2051
3236
  coverage,
2052
3237
  digest: digest(canonicalJson({
2053
3238
  coverage,
2054
- drifted,
3239
+ drifted: allDrifted,
2055
3240
  incompleteInventory,
2056
3241
  observerBinding,
2057
3242
  refreshPreviewDigest: refreshPreview.digest,
2058
3243
  refreshPreviewDrift: normalizedDiffMaterial(refreshPreview.drift),
2059
3244
  refreshPreviewInventory: refreshPreview.observedRefreshUrns,
3245
+ secretsManagerVersionIdentities: [
3246
+ ...secretVersionObservations.values(),
3247
+ ]
3248
+ .map(({ arnDigest, observedIdentityDigest, urn }) => ({
3249
+ arnDigest,
3250
+ observedIdentityDigest,
3251
+ urn,
3252
+ }))
3253
+ .sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.urn, right.urn)),
2060
3254
  unsupported,
2061
3255
  })),
2062
3256
  expiresAt,
2063
3257
  generatedAt,
2064
3258
  status: driftStatus,
2065
3259
  },
2066
- observerArnDigest: digest(caller['Arn']),
3260
+ observerArnDigest: digest(callerArn),
2067
3261
  resources: resourceInventory,
2068
3262
  state: {
2069
3263
  backend: stateExists
@@ -2077,6 +3271,13 @@ function observeInfrastructureState(input) {
2077
3271
  version: digest(canonicalJson({ lock: stateLock.digest, stateDigest })),
2078
3272
  },
2079
3273
  };
3274
+ refreshPreviewDiagnosticsByObservation.set(observation, refreshPreview.diagnostics);
3275
+ refreshMutationPlanMaterialByObservation.set(observation, {
3276
+ diff: refreshPreview.drift,
3277
+ stderr: refreshPreview.stderr,
3278
+ stdout: refreshPreview.rawDrift,
3279
+ });
3280
+ return observation;
2080
3281
  }
2081
3282
  function writeGithubOutputs(environment, values) {
2082
3283
  const path = environment.GITHUB_OUTPUT?.trim();
@@ -2090,7 +3291,8 @@ function writePrivateEvidence(path, contents) {
2090
3291
  (0, node_fs_1.writeFileSync)(path, contents, { encoding: 'utf8', mode: 0o600 });
2091
3292
  (0, node_fs_1.chmodSync)(path, 0o600);
2092
3293
  }
2093
- function actionRisk(risk, action) {
3294
+ /** @internal */
3295
+ function actionRisk(risk, action, options = {}) {
2094
3296
  if (action === 'deploy')
2095
3297
  return risk;
2096
3298
  const code = action === 'refresh'
@@ -2107,17 +3309,27 @@ function actionRisk(risk, action) {
2107
3309
  else {
2108
3310
  requiredApprovals.add('data-owner-and-recovery');
2109
3311
  }
3312
+ const findings = action === 'refresh'
3313
+ ? risk.findings.map((finding) => finding.disposition === 'block'
3314
+ ? {
3315
+ ...finding,
3316
+ disposition: 'review',
3317
+ message: 'Provider refresh records observed cloud state without mutating the cloud; the exact drift decision must review this finding and a later desired-state plan must still converge. ' +
3318
+ finding.message,
3319
+ }
3320
+ : finding)
3321
+ : risk.findings;
2110
3322
  return {
2111
- aggregate: [
2112
- 'unknown',
2113
- 'protection-reduction',
2114
- 'security-expansion',
2115
- ].includes(risk.aggregate)
2116
- ? risk.aggregate
2117
- : 'stateful-or-destructive',
3323
+ aggregate: options.diffComplete === false
3324
+ ? 'unknown'
3325
+ : action === 'refresh'
3326
+ ? 'stateful-or-destructive'
3327
+ : ['unknown', 'protection-reduction', 'security-expansion'].includes(risk.aggregate)
3328
+ ? risk.aggregate
3329
+ : 'stateful-or-destructive',
2118
3330
  applyAllowed: false,
2119
3331
  findings: [
2120
- ...risk.findings,
3332
+ ...findings,
2121
3333
  {
2122
3334
  class: 'stateful-or-destructive',
2123
3335
  code,
@@ -2128,17 +3340,62 @@ function actionRisk(risk, action) {
2128
3340
  requiredApprovals: [...requiredApprovals].sort(),
2129
3341
  };
2130
3342
  }
2131
- function validateGovernanceEvidence(reviewed, plan, observedResources = [], importIdentifierDigests = {}) {
3343
+ function validateGovernanceEvidence(reviewed, plan, observedResources = [], importIdentifierDigests = {}, adoptedImportPolicy = []) {
2132
3344
  const imports = plan.diff.resources.filter(({ operation }) => operation === 'import');
2133
- const replacements = plan.diff.resources.filter(({ operation, replacement }) => operation === 'replace' || replacement);
2134
- const requiresGovernance = plan.action !== 'deploy' || imports.length > 0 || replacements.length > 0;
2135
- if (!requiresGovernance)
2136
- return {};
3345
+ const adoptedByResource = new Map(adoptedImportPolicy.map((entry) => [
3346
+ `${entry.logicalId}\u0000${entry.resourceType}`,
3347
+ entry,
3348
+ ]));
3349
+ const governedImports = imports.filter((resource) => !adoptedByResource.has(infrastructureResourceKey(resource)));
3350
+ const replacements = governedReplacementResources(plan);
3351
+ const digests = {};
3352
+ if (imports.length > 0) {
3353
+ if (Object.keys(importIdentifierDigests).length !== imports.length) {
3354
+ throw new Error('Infrastructure import private identity is ambiguous.');
3355
+ }
3356
+ const adoptedResources = imports.flatMap((resource) => {
3357
+ const key = infrastructureResourceKey(resource);
3358
+ const adopted = adoptedByResource.get(key);
3359
+ if (!adopted)
3360
+ return [];
3361
+ const expectedIdentifierDigest = digest(adopted.privateIdentifierTemplate.replace('{stage}', plan.target.stage));
3362
+ if (importIdentifierDigests[key] !== expectedIdentifierDigest) {
3363
+ throw new Error(`Adopted infrastructure import ${resource.logicalId} does not match its exact stage-bound private identity.`);
3364
+ }
3365
+ return [
3366
+ {
3367
+ authority: adopted.authority,
3368
+ logicalId: adopted.logicalId,
3369
+ privateIdentifierDigest: expectedIdentifierDigest,
3370
+ recoveryRunbookRef: adopted.recoveryRunbookRef,
3371
+ removalBehavior: adopted.removalBehavior,
3372
+ resourceType: adopted.resourceType,
3373
+ },
3374
+ ];
3375
+ });
3376
+ if (adoptedResources.length > 0) {
3377
+ digests['adopted-import'] = digest(canonicalJson({
3378
+ kind: 'ebk-adopted-infrastructure-import-authority',
3379
+ materialDigest: plan.materialDigest,
3380
+ project: plan.projects[0],
3381
+ resources: adoptedResources,
3382
+ schemaVersion: 1,
3383
+ sourceCommit: plan.sourceCommit,
3384
+ stage: plan.target.stage,
3385
+ stateDigest: plan.state.digest,
3386
+ }));
3387
+ }
3388
+ }
3389
+ const requiresGovernance = plan.action !== 'deploy' ||
3390
+ governedImports.length > 0 ||
3391
+ replacements.length > 0;
3392
+ if (!requiresGovernance) {
3393
+ return Object.fromEntries(Object.entries(digests).sort(([left], [right]) => (0, deterministic_order_js_1.compareUtf16)(left, right)));
3394
+ }
2137
3395
  if (!reviewed) {
2138
3396
  throw new Error('Governed infrastructure mutation evidence is absent from the exact approval record.');
2139
3397
  }
2140
3398
  const governance = record(reviewed.governance, 'Infrastructure approval governance evidence');
2141
- const digests = {};
2142
3399
  const common = (value, kind, label, extraKeys) => {
2143
3400
  requireExactKeys(value, [
2144
3401
  'kind',
@@ -2246,11 +3503,12 @@ function validateGovernanceEvidence(reviewed, plan, observedResources = [], impo
2246
3503
  }
2247
3504
  digests.removal = digest(canonicalJson(removal));
2248
3505
  }
2249
- if (imports.length > 0) {
3506
+ if (governedImports.length > 0) {
2250
3507
  const importEvidence = record(governance['import'], 'Infrastructure import governance');
2251
3508
  common(importEvidence, 'ebk-infrastructure-import-governance', 'Infrastructure import governance', ['resources']);
2252
3509
  const resources = importEvidence['resources'];
2253
- if (!Array.isArray(resources) || resources.length !== imports.length) {
3510
+ if (!Array.isArray(resources) ||
3511
+ resources.length !== governedImports.length) {
2254
3512
  throw new Error('Infrastructure import governance must cover every exact imported resource.');
2255
3513
  }
2256
3514
  const byResource = new Map(resources.map((entry, index) => {
@@ -2269,7 +3527,7 @@ function validateGovernanceEvidence(reviewed, plan, observedResources = [], impo
2269
3527
  value,
2270
3528
  ];
2271
3529
  }));
2272
- for (const resource of imports) {
3530
+ for (const resource of governedImports) {
2273
3531
  const key = infrastructureResourceKey(resource);
2274
3532
  const evidence = byResource.get(key);
2275
3533
  if (!evidence ||
@@ -2282,20 +3540,20 @@ function validateGovernanceEvidence(reviewed, plan, observedResources = [], impo
2282
3540
  throw new Error(`Infrastructure import ${resource.logicalId} lacks exact source-bound private identity, ownership, controller, and tested removal evidence.`);
2283
3541
  }
2284
3542
  }
2285
- if (byResource.size !== imports.length ||
2286
- Object.keys(importIdentifierDigests).length !== imports.length) {
3543
+ if (byResource.size !== governedImports.length) {
2287
3544
  throw new Error('Infrastructure import governance is ambiguous.');
2288
3545
  }
2289
3546
  digests.import = digest(canonicalJson(importEvidence));
2290
3547
  }
2291
3548
  if (replacements.length > 0) {
3549
+ const activeOperatorAcknowledgement = reviewed?.approval?.mode === 'operator-acknowledgement';
2292
3550
  const replacementEvidence = record(governance['replacement'], 'Infrastructure replacement governance');
2293
3551
  common(replacementEvidence, 'ebk-infrastructure-replacement-governance', 'Infrastructure replacement governance', ['resources']);
2294
3552
  const resources = replacementEvidence['resources'];
2295
3553
  if (!Array.isArray(resources) || resources.length !== replacements.length) {
2296
3554
  throw new Error('Infrastructure replacement governance must cover every exact replacement.');
2297
3555
  }
2298
- const byLogicalId = new Map(resources.map((entry, index) => {
3556
+ const byResource = new Map(resources.map((entry, index) => {
2299
3557
  const value = record(entry, `Infrastructure replacement resource ${index}`);
2300
3558
  requireExactKeys(value, [
2301
3559
  'dataCopyEvidenceRef',
@@ -2307,14 +3565,20 @@ function validateGovernanceEvidence(reviewed, plan, observedResources = [], impo
2307
3565
  'rollbackLimit',
2308
3566
  'strategy',
2309
3567
  ], `Infrastructure replacement resource ${index}`);
2310
- return [value['logicalId'], value];
3568
+ return [
3569
+ `${value['logicalId']}\u0000${value['resourceType']}`,
3570
+ value,
3571
+ ];
2311
3572
  }));
2312
3573
  for (const resource of replacements) {
2313
- const evidence = byLogicalId.get(resource.logicalId);
3574
+ const evidence = byResource.get(`${resource.logicalId}\u0000${resource.resourceType}`);
2314
3575
  if (!evidence ||
2315
3576
  evidence['resourceType'] !== resource.resourceType ||
2316
3577
  evidence['strategy'] !== resource.replacementStrategy ||
2317
- evidence['nameCollisionCheck'] !== 'passed' ||
3578
+ evidence['nameCollisionCheck'] !==
3579
+ (activeOperatorAcknowledgement
3580
+ ? 'not-independently-verified'
3581
+ : 'passed') ||
2318
3582
  ![
2319
3583
  'dataCopyEvidenceRef',
2320
3584
  'dependencyCutoverRef',
@@ -2322,16 +3586,32 @@ function validateGovernanceEvidence(reviewed, plan, observedResources = [], impo
2322
3586
  'rollbackLimit',
2323
3587
  ].every((key) => typeof evidence[key] === 'string' &&
2324
3588
  evidence[key].trim().length > 0)) {
2325
- throw new Error(`Infrastructure replacement ${resource.logicalId} lacks exact cutover, data, rollback, collision, or recovery evidence.`);
3589
+ throw new Error(`Infrastructure replacement ${resource.logicalId} lacks exact cutover, data, rollback, honestly classified collision review, or recovery evidence.`);
2326
3590
  }
2327
3591
  }
2328
- if (byLogicalId.size !== replacements.length) {
3592
+ if (byResource.size !== replacements.length) {
2329
3593
  throw new Error('Infrastructure replacement governance is ambiguous.');
2330
3594
  }
2331
3595
  digests.replacement = digest(canonicalJson(replacementEvidence));
2332
3596
  }
2333
3597
  return Object.fromEntries(Object.entries(digests).sort(([left], [right]) => (0, deterministic_order_js_1.compareUtf16)(left, right)));
2334
3598
  }
3599
+ function governedReplacementResources(plan) {
3600
+ const replacements = new Map();
3601
+ for (const resource of plan.diff.resources) {
3602
+ if (resource.operation !== 'replace' && !resource.replacement)
3603
+ continue;
3604
+ const key = `${resource.logicalId}\u0000${resource.resourceType}`;
3605
+ const retained = replacements.get(key);
3606
+ if (retained &&
3607
+ retained.replacementStrategy !== resource.replacementStrategy) {
3608
+ throw new Error(`Infrastructure replacement ${resource.logicalId} has ambiguous provider strategies.`);
3609
+ }
3610
+ replacements.set(key, retained ?? resource);
3611
+ }
3612
+ return [...replacements.values()].sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
3613
+ (0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
3614
+ }
2335
3615
  function computeCurrentInfrastructurePlan(input) {
2336
3616
  const { action, context, dependencies, environment, invocation, options } = input;
2337
3617
  const project = context.projectName;
@@ -2357,17 +3637,66 @@ function computeCurrentInfrastructurePlan(input) {
2357
3637
  }));
2358
3638
  const spawn = dependencies.spawn ?? defaultSpawn;
2359
3639
  const sourceCommit = assertCheckedOutSourceCommit(dependencies.spawn, context.root, environment);
2360
- const repository = requiredEnvironment(environment, 'GITHUB_REPOSITORY');
2361
3640
  const candidateSourceCommit = input.candidateSourceCommit ?? sourceCommit;
2362
- const candidate = releaseCandidateBinding({
2363
- project,
2364
- repository,
2365
- sourceCommit: candidateSourceCommit,
2366
- workspaceRoot: context.root,
2367
- });
3641
+ let materialEnvironment = environment;
3642
+ let materialSourceTree;
3643
+ if (environment.EBK_LOCAL_DEPLOYMENT_MODE === 'federated' &&
3644
+ candidateSourceCommit !== sourceCommit) {
3645
+ try {
3646
+ (0, source_authority_js_1.validateReviewedReleaseCheckout)({
3647
+ deploymentSourceCommit: sourceCommit,
3648
+ releaseSourceCommit: candidateSourceCommit,
3649
+ repositoryRoot: context.root,
3650
+ reviewedSourceCommit: sourceCommit,
3651
+ });
3652
+ }
3653
+ catch {
3654
+ throw new Error('Local deployment cannot substitute a different release source commit.');
3655
+ }
3656
+ const releaseTree = spawn('git', ['rev-parse', `${candidateSourceCommit}^{tree}`], {
3657
+ cwd: context.root,
3658
+ encoding: 'utf8',
3659
+ env: environment,
3660
+ maxBuffer: MAX_JSON_BYTES,
3661
+ shell: false,
3662
+ });
3663
+ materialSourceTree = releaseTree.stdout.trim();
3664
+ if (releaseTree.status !== 0 ||
3665
+ releaseTree.stderr !== '' ||
3666
+ !SOURCE_COMMIT.test(materialSourceTree)) {
3667
+ throw new Error('Local deployment cannot resolve the exact reviewed release source tree.');
3668
+ }
3669
+ materialEnvironment = {
3670
+ ...environment,
3671
+ EBK_LOCAL_SOURCE_TREE: materialSourceTree,
3672
+ };
3673
+ }
3674
+ const repository = environment.EBK_LOCAL_DEPLOYMENT_MODE === 'federated'
3675
+ ? `local-source/${requiredEnvironment(materialEnvironment, 'EBK_LOCAL_SOURCE_TREE', /^[a-f0-9]{40}$/)}`
3676
+ : requiredEnvironment(environment, 'GITHUB_REPOSITORY');
3677
+ const candidate = environment.EBK_LOCAL_DEPLOYMENT_MODE === 'federated'
3678
+ ? localSourceCandidateBinding({
3679
+ context,
3680
+ environment,
3681
+ ...(materialSourceTree === undefined ? {} : { materialSourceTree }),
3682
+ project,
3683
+ sourceCommit: candidateSourceCommit,
3684
+ spawn,
3685
+ })
3686
+ : releaseCandidateBinding({
3687
+ project,
3688
+ repository,
3689
+ sourceCommit: candidateSourceCommit,
3690
+ workspaceRoot: context.root,
3691
+ });
2368
3692
  const childEnvironment = {
2369
3693
  ...createCandidateBoundSstChildEnvironment(environment, invocation.environment, candidateSourceCommit),
2370
- EBK_CANDIDATE_IDENTITY: candidate.digest,
3694
+ // Workload-role publication is one stage-wide boundary spanning the
3695
+ // Foundation and every Slice. The reviewed source commit is the only
3696
+ // candidate identity common to that complete project set; the
3697
+ // project-specific sealed candidate digest remains independently bound in
3698
+ // the material infrastructure plan below.
3699
+ EBK_CANDIDATE_IDENTITY: candidateSourceCommit,
2371
3700
  };
2372
3701
  let backendBootstrapDigest = digest('dependency-supplied-observer');
2373
3702
  if (dependencies.observe === undefined) {
@@ -2377,7 +3706,7 @@ function computeCurrentInfrastructurePlan(input) {
2377
3706
  }
2378
3707
  const stage = requiredEnvironment(childEnvironment, 'EBK_CONSUMER_STAGE');
2379
3708
  backendBootstrapDigest = preflightSstBootstrap({
2380
- accountId: requiredEnvironment(environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/),
3709
+ accountId: expectedDeploymentAccount(environment, invocation),
2381
3710
  childEnvironment,
2382
3711
  invocation,
2383
3712
  region: requiredEnvironment(childEnvironment, 'AWS_REGION'),
@@ -2415,29 +3744,53 @@ function computeCurrentInfrastructurePlan(input) {
2415
3744
  cwd: invocation.cwd,
2416
3745
  encoding: 'utf8',
2417
3746
  env: childEnvironment,
2418
- maxBuffer: 20 * 1024 * 1024,
3747
+ maxBuffer: exports.MAX_SST_DIFF_JSON_BYTES,
2419
3748
  shell: false,
2420
3749
  });
2421
- const diff = (0, index_js_1.normalizeSstDiffV4_17_1)({
2422
- exitCode: diffResult.status ?? 1,
3750
+ const diff = normalizeDesiredSstDiffV4_17_1({
3751
+ errorCode: typeof diffResult.error?.code === 'string' ? diffResult.error.code : null,
3752
+ exitCode: diffResult.status ?? -1,
3753
+ signal: typeof diffResult.signal === 'string' ? diffResult.signal : null,
3754
+ spawnError: diffResult.error != null,
3755
+ stderr: diffResult.stderr,
2423
3756
  stdout: diffResult.stdout,
2424
3757
  toolVersion: version,
2425
3758
  });
2426
- const importIdentifierDigests = extractPrivateImportIdentifierDigests(diffResult.stdout, diff);
3759
+ const now = (dependencies.now ?? (() => new Date()))();
3760
+ const observation = (dependencies.observe ?? observeInfrastructureState)({
3761
+ action,
3762
+ backendBootstrapDigest,
3763
+ childEnvironment,
3764
+ environment,
3765
+ invocation,
3766
+ now,
3767
+ ordinaryDiff: diff,
3768
+ project,
3769
+ sourceCommit: candidateSourceCommit,
3770
+ spawn,
3771
+ toolVersion: version,
3772
+ });
3773
+ const refreshMutationPlanMaterial = action === 'refresh'
3774
+ ? refreshMutationPlanMaterialByObservation.get(observation)
3775
+ : undefined;
3776
+ const plannedDiff = refreshMutationPlanMaterial?.diff ?? diff;
3777
+ const plannedDiffStderr = refreshMutationPlanMaterial?.stderr ?? diffResult.stderr;
3778
+ const plannedDiffStdout = refreshMutationPlanMaterial?.stdout ?? diffResult.stdout;
3779
+ const importIdentifierDigests = extractPrivateImportIdentifierDigests(plannedDiffStdout, plannedDiff);
2427
3780
  const iamValidations = iamPolicyDocuments({
2428
- accountId: requiredEnvironment(environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/),
3781
+ accountId: expectedDeploymentAccount(environment, invocation),
2429
3782
  childEnvironment,
2430
- diff,
2431
- diffStdout: diffResult.stdout,
3783
+ diff: plannedDiff,
3784
+ diffStdout: plannedDiffStdout,
2432
3785
  invocation,
2433
3786
  region: requiredEnvironment(childEnvironment, 'AWS_REGION'),
2434
3787
  sourceCommit: candidateSourceCommit,
2435
3788
  spawn,
2436
3789
  });
2437
3790
  const boundedCostQuotaEvidence = costQuotaEvidence({
2438
- accountId: requiredEnvironment(environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/),
3791
+ accountId: expectedDeploymentAccount(environment, invocation),
2439
3792
  childEnvironment,
2440
- diff,
3793
+ diff: plannedDiff,
2441
3794
  invocation,
2442
3795
  policy: descriptor.policy,
2443
3796
  project,
@@ -2446,12 +3799,7 @@ function computeCurrentInfrastructurePlan(input) {
2446
3799
  spawn,
2447
3800
  });
2448
3801
  const policySignals = readOptionalArray(environment, 'EBK_INFRA_POLICY_SIGNALS_PATH');
2449
- const policyBearingIamResources = diff.resources.filter(({ operation, resourceType }) => operation !== 'delete' &&
2450
- [
2451
- 'aws:iam/policy:Policy',
2452
- 'aws:iam/role:Role',
2453
- 'aws:iam/rolePolicy:RolePolicy',
2454
- ].includes(resourceType));
3802
+ const policyBearingIamResources = plannedDiff.resources.filter((resource) => requiresIamMutationAnalysis(action, resource));
2455
3803
  if (policyBearingIamResources.some(({ logicalId }) => !iamValidations.some((validation) => validation.logicalId === logicalId &&
2456
3804
  validation.analyzer === 'aws-iam-access-analyzer-validate-policy' &&
2457
3805
  validation.status === 'complete'))) {
@@ -2462,45 +3810,35 @@ function computeCurrentInfrastructurePlan(input) {
2462
3810
  });
2463
3811
  }
2464
3812
  const risk = actionRisk((0, index_js_1.classifyInfrastructureRisk)({
2465
- diff,
3813
+ diff: plannedDiff,
2466
3814
  iamValidations,
2467
3815
  policy: descriptor.policy,
2468
3816
  policySignals,
2469
- }), action);
2470
- const now = (dependencies.now ?? (() => new Date()))();
2471
- const observation = (dependencies.observe ?? observeInfrastructureState)({
2472
- backendBootstrapDigest,
2473
- childEnvironment,
2474
- environment,
2475
- invocation,
2476
- now,
2477
- ordinaryDiff: diff,
2478
- project,
2479
- sourceCommit,
2480
- spawn,
2481
- toolVersion: version,
2482
- });
3817
+ }), action, { diffComplete: plannedDiff.complete });
2483
3818
  const drift = observation.drift;
2484
3819
  const state = observation.state;
2485
3820
  const expiresAt = new Date(Math.min(now.getTime() + 60 * 60_000, Date.parse(drift.expiresAt), Date.parse(state.expiresAt)));
2486
- const account = requiredEnvironment(environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/);
3821
+ const account = expectedDeploymentAccount(environment, invocation);
2487
3822
  const framework = frameworkVersion(context.root);
2488
- const planAuthorityDigest = planAuthorityPolicyDigest(environment);
3823
+ const planAuthorityDigest = planAuthorityPolicyDigest(materialEnvironment, invocation);
3824
+ const buildArtifacts = collectInfrastructureBuildArtifacts(root);
2489
3825
  return {
3826
+ buildArtifacts,
2490
3827
  costQuotaEvidence: boundedCostQuotaEvidence,
2491
3828
  descriptor,
2492
- diffStderr: diffResult.stderr,
2493
- diffStdout: diffResult.stdout,
3829
+ diffStderr: plannedDiffStderr,
3830
+ diffStdout: plannedDiffStdout,
2494
3831
  iamValidations,
2495
3832
  importIdentifierDigests,
2496
3833
  observation,
2497
3834
  plan: (0, index_js_1.createInfrastructurePlan)({
2498
3835
  action,
2499
- configurationMetadataDigest: digest(canonicalJson({
3836
+ configurationMetadataDigest: infrastructureConfigurationMetadataDigest({
3837
+ buildArtifactDigest: buildArtifacts.digest,
2500
3838
  configurationInputs,
2501
3839
  releaseCandidateDigest: candidate.digest,
2502
- })),
2503
- diff,
3840
+ }),
3841
+ diff: plannedDiff,
2504
3842
  driftEvidenceDigest: drift.digest,
2505
3843
  driftEvidenceExpiresAt: drift.expiresAt,
2506
3844
  driftEvidenceGeneratedAt: drift.generatedAt,
@@ -2546,6 +3884,591 @@ function computeCurrentInfrastructurePlan(input) {
2546
3884
  toolVersion: version,
2547
3885
  };
2548
3886
  }
3887
+ /** @internal */
3888
+ function createLocalPlanApprovalRecord(input) {
3889
+ const expectedMaterialDigest = input.environment['EBK_LOCAL_PLAN_MATERIAL_DIGEST']?.trim();
3890
+ if (!expectedMaterialDigest)
3891
+ return undefined;
3892
+ const summaryDigest = input.environment['EBK_LOCAL_PLAN_SUMMARY_DIGEST']?.trim() ?? '';
3893
+ const approverArn = input.environment['EBK_LOCAL_PLAN_APPROVER_ARN']?.trim() ?? '';
3894
+ const sourceCommit = input.environment['EBK_SOURCE_COMMIT']?.trim() ?? '';
3895
+ const policy = (0, index_js_2.readRepositoryOperatingPolicy)(input.context.root);
3896
+ if (input.environment['GITHUB_ACTIONS'] === 'true' ||
3897
+ input.environment['EBK_LOCAL_DEPLOYMENT_MODE'] !== 'federated' ||
3898
+ (input.plan.action !== 'deploy' && input.plan.action !== 'refresh') ||
3899
+ !isCanonicalDeploymentStage(input.plan.target.stage) ||
3900
+ (0, index_js_2.requiresGithubDeployment)(policy.deploymentProfile, input.plan.target.stage) ||
3901
+ expectedMaterialDigest !== input.plan.materialDigest ||
3902
+ !SHA256.test(summaryDigest) ||
3903
+ sourceCommit !== input.plan.sourceCommit ||
3904
+ !/^arn:(aws|aws-us-gov|aws-cn):sts::\d{12}:assumed-role\/.+\/.+$/.test(approverArn) ||
3905
+ input.plan.requiredApprovals.length === 0 ||
3906
+ hasUnreviewableLocalPlanBlock(input.plan)) {
3907
+ throw new Error('Local infrastructure plan approval does not match the exact federated actor, source, stage, and reviewed plan.');
3908
+ }
3909
+ const evidenceRef = `local-plan-summary:${summaryDigest}`;
3910
+ return {
3911
+ ...(input.plan.action === 'refresh' &&
3912
+ input.plan.requiredApprovals.includes('point-of-no-return')
3913
+ ? { acknowledgement: `refresh-state-adoption:${summaryDigest}` }
3914
+ : {}),
3915
+ approvedAt: input.now.toISOString(),
3916
+ approver: `local-federated:${digest(approverArn)}`,
3917
+ author: 'ebk-infrastructure-plan',
3918
+ codeownerReviewed: false,
3919
+ evidenceRefs: Object.fromEntries(input.plan.requiredApprovals.map((requirement) => [
3920
+ requirement,
3921
+ evidenceRef,
3922
+ ])),
3923
+ expiresAt: input.plan.expiresAt,
3924
+ materialDigest: input.plan.materialDigest,
3925
+ mode: 'operator-acknowledgement',
3926
+ stage: input.plan.target.stage,
3927
+ };
3928
+ }
3929
+ /** @internal */
3930
+ function hasUnreviewableLocalPlanBlock(plan) {
3931
+ const blocks = plan.resourceFindings.filter(({ disposition }) => disposition === 'block');
3932
+ if (blocks.length === 0)
3933
+ return false;
3934
+ return !(plan.action === 'refresh' &&
3935
+ blocks.length === 1 &&
3936
+ blocks[0]?.class === 'stateful-or-destructive' &&
3937
+ blocks[0]?.code === 'protected-state-refresh' &&
3938
+ blocks[0]?.resourceId === undefined);
3939
+ }
3940
+ /** @internal */
3941
+ function createLocalRefreshGovernanceReview(input) {
3942
+ if (input.plan.action !== 'refresh')
3943
+ return undefined;
3944
+ const summaryDigest = input.environment['EBK_LOCAL_PLAN_SUMMARY_DIGEST']?.trim() ?? '';
3945
+ const evidenceRef = `local-plan-summary:${summaryDigest}`;
3946
+ const project = input.plan.projects[0];
3947
+ if (!SHA256.test(summaryDigest) ||
3948
+ input.plan.projects.length !== 1 ||
3949
+ !project ||
3950
+ input.approval.materialDigest !== input.plan.materialDigest ||
3951
+ input.approval.mode !== 'operator-acknowledgement' ||
3952
+ input.approval.evidenceRefs['deployment-review'] !== evidenceRef ||
3953
+ !SHA256.test(input.plan.driftEvidenceDigest) ||
3954
+ !SHA256.test(input.plan.state.digest)) {
3955
+ throw new Error('Local refresh governance does not match the exact reviewed plan summary.');
3956
+ }
3957
+ return {
3958
+ approval: input.approval,
3959
+ governance: {
3960
+ driftDecision: {
3961
+ actor: input.approval.approver,
3962
+ approvalRef: evidenceRef,
3963
+ decision: 'adopt',
3964
+ driftEvidenceDigest: input.plan.driftEvidenceDigest,
3965
+ kind: 'ebk-infrastructure-drift-decision',
3966
+ materialDigest: input.plan.materialDigest,
3967
+ project,
3968
+ reviewedSourceChangeRef: input.plan.sourceCommit,
3969
+ runbookRef: 'docs/repository-operating-policy.md',
3970
+ schemaVersion: 1,
3971
+ sourceCommit: input.plan.sourceCommit,
3972
+ stage: input.plan.target.stage,
3973
+ stateDigest: input.plan.state.digest,
3974
+ },
3975
+ },
3976
+ planned: input.plan,
3977
+ };
3978
+ }
3979
+ /** @internal */
3980
+ function createLocalDeployGovernanceReview(input) {
3981
+ if (input.plan.action !== 'deploy')
3982
+ return undefined;
3983
+ const replacements = governedReplacementResources(input.plan);
3984
+ if (replacements.length === 0)
3985
+ return undefined;
3986
+ const summaryDigest = input.environment['EBK_LOCAL_PLAN_SUMMARY_DIGEST']?.trim() ?? '';
3987
+ const evidenceRef = `local-plan-summary:${summaryDigest}`;
3988
+ const project = input.plan.projects[0];
3989
+ if (!SHA256.test(summaryDigest) ||
3990
+ input.plan.projects.length !== 1 ||
3991
+ !project ||
3992
+ input.approval.materialDigest !== input.plan.materialDigest ||
3993
+ input.approval.mode !== 'operator-acknowledgement' ||
3994
+ input.approval.evidenceRefs['deployment-review'] !== evidenceRef ||
3995
+ Object.values(input.approval.evidenceRefs).some((reference) => reference !== evidenceRef) ||
3996
+ replacements.some(({ replacementStrategy }) => replacementStrategy !== 'create-before-delete') ||
3997
+ !SHA256.test(input.plan.state.digest)) {
3998
+ throw new Error('Local deploy governance does not match the exact reviewed plan summary and create-before-delete inventory.');
3999
+ }
4000
+ return {
4001
+ approval: input.approval,
4002
+ governance: {
4003
+ replacement: {
4004
+ kind: 'ebk-infrastructure-replacement-governance',
4005
+ materialDigest: input.plan.materialDigest,
4006
+ project,
4007
+ resources: replacements.map((resource) => ({
4008
+ dataCopyEvidenceRef: evidenceRef,
4009
+ dependencyCutoverRef: evidenceRef,
4010
+ logicalId: resource.logicalId,
4011
+ nameCollisionCheck: 'not-independently-verified',
4012
+ recoveryRunbookRef: 'docs/repository-operating-policy.md',
4013
+ resourceType: resource.resourceType,
4014
+ rollbackLimit: 'before-dependent-cutover',
4015
+ strategy: resource.replacementStrategy,
4016
+ })),
4017
+ schemaVersion: 1,
4018
+ sourceCommit: input.plan.sourceCommit,
4019
+ stage: input.plan.target.stage,
4020
+ stateDigest: input.plan.state.digest,
4021
+ },
4022
+ },
4023
+ planned: input.plan,
4024
+ };
4025
+ }
4026
+ /**
4027
+ * Retains exact local approval for a plan that the stage-wide local executor
4028
+ * has just recomputed and reviewed. This deliberately does not run SST again:
4029
+ * the project mutation executor consumes one invocation-scoped capability and
4030
+ * rechecks source, artifacts, credentials, policy, state identity, and lock
4031
+ * immediately before spawn without claiming provider-preview replay.
4032
+ *
4033
+ * @internal
4034
+ */
4035
+ function authorizeRetainedLocalInfrastructurePlan(input) {
4036
+ if (input.plan.diff.resources.some(({ operation }) => operation === 'import')) {
4037
+ throw new Error('Local retained-plan authorization cannot reconstruct private import identity evidence.');
4038
+ }
4039
+ const approval = createLocalPlanApprovalRecord(input) ??
4040
+ (input.plan.requiredApprovals.length === 0
4041
+ ? {
4042
+ approvedAt: input.now.toISOString(),
4043
+ approver: 'committed-standing-policy',
4044
+ author: 'committed-standing-policy',
4045
+ codeownerReviewed: true,
4046
+ evidenceRefs: {},
4047
+ expiresAt: input.plan.expiresAt,
4048
+ materialDigest: input.plan.materialDigest,
4049
+ mode: 'standing-policy',
4050
+ stage: input.plan.target.stage,
4051
+ }
4052
+ : undefined);
4053
+ if (!approval) {
4054
+ throw new Error('Local retained-plan authorization requires current standing policy or an exact operator acknowledgement.');
4055
+ }
4056
+ const reviewedApproval = createLocalDeployGovernanceReview({
4057
+ approval,
4058
+ environment: input.environment,
4059
+ plan: input.plan,
4060
+ }) ??
4061
+ createLocalRefreshGovernanceReview({
4062
+ approval,
4063
+ environment: input.environment,
4064
+ plan: input.plan,
4065
+ });
4066
+ const validation = (0, index_js_1.validateInfrastructurePlanForApply)({
4067
+ approval,
4068
+ now: input.now.toISOString(),
4069
+ planned: input.plan,
4070
+ recomputed: input.plan,
4071
+ sourceChangesSincePlan: [],
4072
+ });
4073
+ if (!validation.allowed) {
4074
+ throw new Error('Local retained-plan authorization did not admit the exact current plan.');
4075
+ }
4076
+ const governanceEvidenceDigests = validateGovernanceEvidence(reviewedApproval, input.plan);
4077
+ const authorization = {
4078
+ action: input.plan.action,
4079
+ approval,
4080
+ ...(Object.keys(governanceEvidenceDigests).length > 0
4081
+ ? { governanceEvidenceDigests }
4082
+ : {}),
4083
+ kind: 'ebk-infrastructure-authorization',
4084
+ ...(reviewedApproval?.governance !== undefined
4085
+ ? { localGovernance: reviewedApproval.governance }
4086
+ : {}),
4087
+ plannedMaterialDigest: input.plan.materialDigest,
4088
+ recomputedMaterialDigest: input.plan.materialDigest,
4089
+ schemaVersion: 1,
4090
+ sourceChangesSincePlan: [],
4091
+ validation,
4092
+ };
4093
+ const root = projectRoot(input.context, {});
4094
+ const evidenceDirectory = (0, node_path_1.join)(root, '.ebk', 'evidence');
4095
+ (0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
4096
+ (0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
4097
+ writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan-authorization.json'), `${JSON.stringify(authorization, null, 2)}\n`);
4098
+ return authorization;
4099
+ }
4100
+ /** @internal */
4101
+ function requiresGovernanceValidation(input) {
4102
+ return (input.hasApproval ||
4103
+ (input.adoptedImportCount > 0 && input.action !== 'refresh'));
4104
+ }
4105
+ /** @internal */
4106
+ function isLocalPlanReviewPending(input) {
4107
+ const findingCodes = input.validation.findings.map(({ code }) => code).sort();
4108
+ const hasExactPendingReviewFindings = findingCodes.length === 1 && findingCodes[0] === 'approval-missing';
4109
+ if (input.environment['GITHUB_ACTIONS'] === 'true' ||
4110
+ input.environment['EBK_LOCAL_DEPLOYMENT_MODE'] !== 'federated' ||
4111
+ (input.plan.action !== 'deploy' && input.plan.action !== 'refresh') ||
4112
+ input.plan.requiredApprovals.length === 0 ||
4113
+ hasUnreviewableLocalPlanBlock(input.plan) ||
4114
+ input.validation.allowed ||
4115
+ !hasExactPendingReviewFindings) {
4116
+ return false;
4117
+ }
4118
+ const policy = (0, index_js_2.readRepositoryOperatingPolicy)(input.context.root);
4119
+ if (!isCanonicalDeploymentStage(input.plan.target.stage))
4120
+ return false;
4121
+ return !(0, index_js_2.requiresGithubDeployment)(policy.deploymentProfile, input.plan.target.stage);
4122
+ }
4123
+ function runLocalGitAuthorityCommand(input) {
4124
+ const result = input.spawn('git', input.args, {
4125
+ cwd: input.context.root,
4126
+ encoding: 'utf8',
4127
+ env: input.environment,
4128
+ maxBuffer: MAX_JSON_BYTES,
4129
+ shell: false,
4130
+ });
4131
+ if (result.status !== 0 || result.stderr !== '') {
4132
+ throw new Error('Local mutation source identity could not be revalidated.');
4133
+ }
4134
+ return result.stdout.trim();
4135
+ }
4136
+ /** @internal */
4137
+ function observeLocalMutationStateIdentity(input) {
4138
+ const stage = requiredEnvironment(input.childEnvironment, 'EBK_CONSUMER_STAGE');
4139
+ const region = requiredEnvironment(input.childEnvironment, 'AWS_REGION');
4140
+ const expectedAccount = expectedDeploymentAccount(input.environment, input.invocation);
4141
+ const caller = record(runJsonCommand(input.spawn, 'aws', ['sts', 'get-caller-identity', '--output', 'json'], {
4142
+ cwd: input.invocation.cwd,
4143
+ env: input.childEnvironment,
4144
+ label: 'Local mutation AWS caller identity',
4145
+ }), 'Local mutation AWS caller identity');
4146
+ const callerArn = typeof caller['Arn'] === 'string' ? caller['Arn'] : '';
4147
+ const expectedRoleArn = input.childEnvironment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN?.trim();
4148
+ if (caller['Account'] !== expectedAccount ||
4149
+ !callerArn ||
4150
+ !expectedRoleArn ||
4151
+ !matchesExpectedAssumedRoleArn(expectedAccount, callerArn, expectedRoleArn)) {
4152
+ throw new Error('Local mutation caller no longer matches the expected account and temporary assumed role.');
4153
+ }
4154
+ const stateLocation = exactSstStateLocation(input.environment);
4155
+ const stateKey = `app/${input.project}/${stage}.json`;
4156
+ const stateExists = exactS3KeyExists({
4157
+ childEnvironment: input.childEnvironment,
4158
+ invocation: input.invocation,
4159
+ key: stateKey,
4160
+ label: `Local mutation SST state identity ${stateKey}`,
4161
+ spawn: input.spawn,
4162
+ stateBucketName: stateLocation.bucketName,
4163
+ });
4164
+ const sstPath = input.invocation.args[0];
4165
+ if (!sstPath)
4166
+ throw new Error('Workspace SST path is missing.');
4167
+ const exported = stateExists
4168
+ ? runJsonCommand(input.spawn, input.invocation.executable, [
4169
+ sstPath,
4170
+ 'state',
4171
+ 'export',
4172
+ '--stage',
4173
+ stage,
4174
+ ...(input.invocation.args.includes('--profile')
4175
+ ? input.invocation.args.slice(input.invocation.args.indexOf('--profile'), input.invocation.args.indexOf('--profile') + 2)
4176
+ : []),
4177
+ ], {
4178
+ cwd: input.invocation.cwd,
4179
+ env: input.childEnvironment,
4180
+ label: 'Local mutation SST state export',
4181
+ maxBuffer: 32 * 1024 * 1024,
4182
+ })
4183
+ : null;
4184
+ const exportedState = stateExists
4185
+ ? stateExport(exported)
4186
+ : { resources: [] };
4187
+ const stackIdentities = new Set();
4188
+ for (const resource of exportedState.resources) {
4189
+ const urn = typeof resource.urn === 'string' ? resource.urn : '';
4190
+ const match = /^urn:pulumi:([^:]+)::([^:]+)::/.exec(urn);
4191
+ if (match)
4192
+ stackIdentities.add(`${match[2]}/${match[1]}`);
4193
+ }
4194
+ if (stateExists && stackIdentities.size !== 1) {
4195
+ throw new Error('Local mutation SST state does not identify exactly one application and stack.');
4196
+ }
4197
+ const stack = stateExists
4198
+ ? [...stackIdentities][0]
4199
+ : `${input.project}/${stage}`;
4200
+ if (exportedState.sstStack &&
4201
+ exportedState.sstStack.split('/').slice(1).join('/') !== stack) {
4202
+ throw new Error('Local mutation SST checkpoint identity does not match its resource inventory.');
4203
+ }
4204
+ const [application, observedStage] = stack.split('/');
4205
+ if (application !== input.project ||
4206
+ observedStage !== stage ||
4207
+ !stateLocation.applications.includes(application)) {
4208
+ throw new Error('Local mutation SST state is outside the exact project and stage authority.');
4209
+ }
4210
+ const stateLock = observeSstStateLock({
4211
+ app: application,
4212
+ childEnvironment: input.childEnvironment,
4213
+ invocation: input.invocation,
4214
+ spawn: input.spawn,
4215
+ stage,
4216
+ stateBucketName: stateLocation.bucketName,
4217
+ });
4218
+ const observerBinding = {
4219
+ accountFingerprint: digest(expectedAccount),
4220
+ backendBootstrapDigest: input.backendBootstrapDigest,
4221
+ project: input.project,
4222
+ region,
4223
+ sourceCommit: input.sourceCommit,
4224
+ stage,
4225
+ stateLocationDigest: digest(canonicalJson({
4226
+ bucketArn: stateLocation.bucketArn,
4227
+ lockKey: stateLock.key,
4228
+ stateKey,
4229
+ })),
4230
+ };
4231
+ const stateDigest = digest(canonicalJson(stateExists
4232
+ ? { exportedState: exported, observerBinding }
4233
+ : { observerBinding, stateKey, status: 'absent' }));
4234
+ const generatedAt = input.now.toISOString();
4235
+ const expiresAt = new Date(input.now.getTime() + 60 * 60_000).toISOString();
4236
+ return {
4237
+ actorArn: callerArn,
4238
+ exportedState: exported,
4239
+ identity: {
4240
+ backend: stateExists
4241
+ ? 'sst-pulumi-state-export'
4242
+ : 'sst-pulumi-state-absent',
4243
+ digest: digest(canonicalJson({ lock: stateLock.digest, stateDigest })),
4244
+ expiresAt,
4245
+ locked: stateLock.locked,
4246
+ observedAt: generatedAt,
4247
+ stack,
4248
+ version: digest(canonicalJson({ lock: stateLock.digest, stateDigest })),
4249
+ },
4250
+ resourceCount: exportedState.resources.length,
4251
+ };
4252
+ }
4253
+ /** @internal */
4254
+ function retainLocalRefreshStateBackup(input) {
4255
+ if (input.plan.action !== 'refresh' ||
4256
+ input.observation.identity.backend !== 'sst-pulumi-state-export' ||
4257
+ input.observation.exportedState === null) {
4258
+ throw new Error('Local refresh requires an exportable current state recovery point.');
4259
+ }
4260
+ const evidenceDirectory = (0, node_path_1.join)(projectRoot(input.context, {}), '.ebk', 'evidence');
4261
+ (0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
4262
+ (0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
4263
+ const backupContents = `${JSON.stringify(input.observation.exportedState, null, 2)}\n`;
4264
+ const backupPath = (0, node_path_1.join)(evidenceDirectory, 'infrastructure-state-backup.private.json');
4265
+ writePrivateEvidence(backupPath, backupContents);
4266
+ (0, private_state_js_1.hardenPrivatePath)(backupPath, 'file');
4267
+ (0, private_state_js_1.assertPrivatePath)(backupPath, 'file');
4268
+ const retainedBackup = (0, node_fs_1.readFileSync)(backupPath, 'utf8');
4269
+ const retainedBackupStat = (0, node_fs_1.lstatSync)(backupPath, { bigint: true });
4270
+ if (!retainedBackupStat.isFile() ||
4271
+ retainedBackupStat.nlink !== 1n ||
4272
+ retainedBackup !== backupContents) {
4273
+ throw new Error('Local refresh state recovery point could not be verified after write.');
4274
+ }
4275
+ writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-state-backup.json'), `${JSON.stringify({
4276
+ backupDigest: digest(backupContents),
4277
+ capturedAt: input.now.toISOString(),
4278
+ kind: 'ebk-infrastructure-state-backup',
4279
+ project: input.project,
4280
+ resourceCount: input.observation.resourceCount,
4281
+ schemaVersion: 1,
4282
+ sourceCommit: input.plan.sourceCommit,
4283
+ stage: input.plan.target.stage,
4284
+ stateDigest: input.observation.identity.digest,
4285
+ stateVersion: input.observation.identity.version,
4286
+ }, null, 2)}\n`);
4287
+ }
4288
+ function consumeLocalMutationCapability(input) {
4289
+ const capabilityPath = input.environment.EBK_LOCAL_MUTATION_CAPABILITY_PATH?.trim() ?? '';
4290
+ const nonce = input.environment.EBK_LOCAL_MUTATION_CAPABILITY_NONCE?.trim() ?? '';
4291
+ if (!(0, node_path_1.isAbsolute)(capabilityPath) || !nonce) {
4292
+ throw new Error('Local mutation requires one invocation-scoped capability path and nonce.');
4293
+ }
4294
+ const parent = (0, node_fs_1.realpathSync)((0, node_path_1.dirname)(capabilityPath));
4295
+ const temporaryRoot = (0, node_fs_1.realpathSync)((0, node_os_1.tmpdir)());
4296
+ const parentRelative = (0, node_path_1.relative)(temporaryRoot, parent);
4297
+ const parentStat = (0, node_fs_1.lstatSync)(parent, { bigint: true });
4298
+ let parentIsPrivate = true;
4299
+ try {
4300
+ (0, private_state_js_1.assertPrivatePath)(parent, 'directory');
4301
+ }
4302
+ catch {
4303
+ parentIsPrivate = false;
4304
+ }
4305
+ if (parentRelative === '..' ||
4306
+ parentRelative.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) ||
4307
+ (0, node_path_1.isAbsolute)(parentRelative) ||
4308
+ !(0, node_path_1.basename)(parent).startsWith('ebk-local-mutation-') ||
4309
+ !parentStat.isDirectory() ||
4310
+ !parentIsPrivate) {
4311
+ throw new Error('Local mutation capability directory is outside the private temporary authority boundary.');
4312
+ }
4313
+ let before;
4314
+ try {
4315
+ before = (0, node_fs_1.lstatSync)(capabilityPath, { bigint: true });
4316
+ }
4317
+ catch {
4318
+ throw new Error('Local mutation capability is malformed, expired, replayed, or absent.');
4319
+ }
4320
+ const expectedUid = process.getuid?.();
4321
+ let capabilityIsPrivate = true;
4322
+ try {
4323
+ (0, private_state_js_1.assertPrivatePath)(capabilityPath, 'file');
4324
+ }
4325
+ catch {
4326
+ capabilityIsPrivate = false;
4327
+ }
4328
+ if (!before.isFile() ||
4329
+ before.nlink !== 1n ||
4330
+ before.size > 64n * 1024n ||
4331
+ !capabilityIsPrivate ||
4332
+ (expectedUid !== undefined && before.uid !== BigInt(expectedUid))) {
4333
+ throw new Error('Local mutation capability is not one bounded private regular file.');
4334
+ }
4335
+ const contents = (0, node_fs_1.readFileSync)(capabilityPath, 'utf8');
4336
+ const after = (0, node_fs_1.lstatSync)(capabilityPath, { bigint: true });
4337
+ if (before.dev !== after.dev ||
4338
+ before.ino !== after.ino ||
4339
+ before.mode !== after.mode ||
4340
+ before.nlink !== after.nlink ||
4341
+ before.size !== after.size ||
4342
+ before.mtimeNs !== after.mtimeNs ||
4343
+ before.ctimeNs !== after.ctimeNs) {
4344
+ throw new Error('Local mutation capability changed while it was read.');
4345
+ }
4346
+ const capability = record(parseJson(contents, 'Local mutation capability'), 'Local mutation capability');
4347
+ (0, local_capability_js_1.validateLocalMutationCapability)({
4348
+ authorizationContents: input.authorizationContents,
4349
+ capability,
4350
+ expectedAuthority: capability.authority,
4351
+ nonce,
4352
+ now: input.now,
4353
+ planContents: input.planContents,
4354
+ });
4355
+ const spawn = input.dependencies.spawn ?? defaultSpawn;
4356
+ const deploymentSourceCommit = assertCheckedOutSourceCommit(input.dependencies.spawn, input.context.root, input.environment);
4357
+ const sourceStatus = runLocalGitAuthorityCommand({
4358
+ args: ['status', '--porcelain=v1', '--untracked-files=all'],
4359
+ context: input.context,
4360
+ environment: input.environment,
4361
+ spawn,
4362
+ });
4363
+ const deploymentSourceTree = runLocalGitAuthorityCommand({
4364
+ args: ['rev-parse', 'HEAD^{tree}'],
4365
+ context: input.context,
4366
+ environment: input.environment,
4367
+ spawn,
4368
+ });
4369
+ const releaseSourceTree = runLocalGitAuthorityCommand({
4370
+ args: ['rev-parse', `${input.plan.sourceCommit}^{tree}`],
4371
+ context: input.context,
4372
+ environment: input.environment,
4373
+ spawn,
4374
+ });
4375
+ if (sourceStatus !== '') {
4376
+ throw new Error('Local mutation source tree changed after its current plan was acknowledged.');
4377
+ }
4378
+ if (deploymentSourceCommit !== input.plan.sourceCommit) {
4379
+ (0, source_authority_js_1.validateReviewedReleaseCheckout)({
4380
+ deploymentSourceCommit,
4381
+ releaseSourceCommit: input.plan.sourceCommit,
4382
+ repositoryRoot: input.context.root,
4383
+ reviewedSourceCommit: deploymentSourceCommit,
4384
+ });
4385
+ }
4386
+ const root = projectRoot(input.context, input.options);
4387
+ const descriptorPath = relativeFile(root, input.options.descriptorPath ?? 'operations/infrastructure-change.json');
4388
+ const descriptor = readDescriptor(descriptorPath, input.context.projectName ?? '');
4389
+ const buildArtifacts = collectInfrastructureBuildArtifacts(root);
4390
+ const retainedBuildArtifacts = record(readJsonFile((0, node_path_1.join)(root, '.ebk', 'evidence', 'infrastructure-build-artifacts.json'), 'Retained infrastructure build artifacts'), 'Retained infrastructure build artifacts');
4391
+ if (retainedBuildArtifacts['digest'] !== buildArtifacts.digest) {
4392
+ throw new Error('Local mutation build artifacts changed after the current plan was acknowledged.');
4393
+ }
4394
+ const policy = (0, index_js_2.readRepositoryOperatingPolicy)(input.context.root);
4395
+ const expectedRoleArn = input.invocation.environment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN?.trim() ?? '';
4396
+ if (digest(descriptor.raw) !== capability.authority.policyProfileDigest ||
4397
+ frameworkVersion(input.context.root) !==
4398
+ capability.authority.frameworkVersion ||
4399
+ planAuthorityPolicyDigest(input.environment, input.invocation) !==
4400
+ capability.authority.planAuthorityPolicyDigest) {
4401
+ throw new Error('Local mutation source, policy, role, artifact, or environment authority changed after acknowledgement.');
4402
+ }
4403
+ const childEnvironment = createCandidateBoundSstChildEnvironment({
4404
+ ...input.environment,
4405
+ EBK_LOCAL_SOURCE_TREE: releaseSourceTree,
4406
+ }, input.invocation.environment, input.plan.sourceCommit);
4407
+ const stateLocation = exactSstStateLocation(input.environment);
4408
+ const backendBootstrapDigest = preflightSstBootstrap({
4409
+ accountId: expectedDeploymentAccount(input.environment, input.invocation),
4410
+ childEnvironment,
4411
+ invocation: input.invocation,
4412
+ region: requiredEnvironment(childEnvironment, 'AWS_REGION'),
4413
+ spawn,
4414
+ stateBucketName: stateLocation.bucketName,
4415
+ });
4416
+ preflightSstBackendParameter({
4417
+ childEnvironment,
4418
+ expectedType: 'SecureString',
4419
+ invocation: input.invocation,
4420
+ name: `/sst/passphrase/${input.context.projectName}/${input.plan.target.stage}`,
4421
+ spawn,
4422
+ });
4423
+ preflightProtectedSstSecrets({
4424
+ application: input.context.projectName ?? '',
4425
+ childEnvironment,
4426
+ invocation: input.invocation,
4427
+ spawn,
4428
+ stage: input.plan.target.stage,
4429
+ stateBucketName: stateLocation.bucketName,
4430
+ });
4431
+ const stateObservation = observeLocalMutationStateIdentity({
4432
+ backendBootstrapDigest,
4433
+ childEnvironment,
4434
+ environment: input.environment,
4435
+ invocation: input.invocation,
4436
+ now: input.now,
4437
+ project: input.context.projectName ?? '',
4438
+ sourceCommit: input.plan.sourceCommit,
4439
+ spawn,
4440
+ });
4441
+ const state = stateObservation.identity;
4442
+ const expectedAuthority = (0, local_capability_js_1.localMutationAuthorityEnvelope)({
4443
+ actorArn: stateObservation.actorArn,
4444
+ deploymentProfile: policy.deploymentProfile,
4445
+ deploymentSourceCommit,
4446
+ deploymentSourceTree,
4447
+ expectedRoleArn,
4448
+ plan: input.plan,
4449
+ project: input.context.projectName ?? '',
4450
+ releaseSourceTree,
4451
+ });
4452
+ if ((0, local_capability_js_1.computeLocalMutationAuthorityDigest)(expectedAuthority) !==
4453
+ capability.authorityDigest ||
4454
+ state.locked !== false ||
4455
+ state.backend !== capability.authority.state.backend ||
4456
+ state.digest !== capability.authority.state.digest ||
4457
+ state.stack !== capability.authority.state.stack ||
4458
+ state.version !== capability.authority.state.version) {
4459
+ throw new Error('Local mutation state identity or lock changed after the current plan was acknowledged.');
4460
+ }
4461
+ if (input.action === 'refresh') {
4462
+ retainLocalRefreshStateBackup({
4463
+ context: input.context,
4464
+ now: input.now,
4465
+ observation: stateObservation,
4466
+ plan: input.plan,
4467
+ project: input.context.projectName ?? '',
4468
+ });
4469
+ }
4470
+ (0, node_fs_1.rmSync)(capabilityPath);
4471
+ }
2549
4472
  async function runInfrastructurePlanExecutor(options, context, environment = process.env, dependencies = {}) {
2550
4473
  const action = options.action ?? 'deploy';
2551
4474
  const root = projectRoot(context, options);
@@ -2572,7 +4495,27 @@ async function runInfrastructurePlanExecutor(options, context, environment = pro
2572
4495
  let approval;
2573
4496
  let reviewedApproval;
2574
4497
  let changes = [];
2575
- if (descriptor.standingPolicyRiskClasses.includes(plan.aggregateRisk) &&
4498
+ const localApproval = createLocalPlanApprovalRecord({
4499
+ context,
4500
+ environment,
4501
+ now,
4502
+ plan,
4503
+ });
4504
+ if (localApproval) {
4505
+ approval = localApproval;
4506
+ reviewedApproval =
4507
+ createLocalDeployGovernanceReview({
4508
+ approval: localApproval,
4509
+ environment,
4510
+ plan,
4511
+ }) ??
4512
+ createLocalRefreshGovernanceReview({
4513
+ approval: localApproval,
4514
+ environment,
4515
+ plan,
4516
+ });
4517
+ }
4518
+ else if (descriptor.standingPolicyRiskClasses.includes(plan.aggregateRisk) &&
2576
4519
  plan.requiredApprovals.length === 0) {
2577
4520
  approval = {
2578
4521
  approvedAt: now.toISOString(),
@@ -2628,8 +4571,12 @@ async function runInfrastructurePlanExecutor(options, context, environment = pro
2628
4571
  recomputed: plan,
2629
4572
  sourceChangesSincePlan: changes,
2630
4573
  });
2631
- const governanceEvidenceDigests = approval
2632
- ? validateGovernanceEvidence(reviewedApproval, planned, current.observation.resources, current.importIdentifierDigests)
4574
+ const governanceEvidenceDigests = requiresGovernanceValidation({
4575
+ action: planned.action,
4576
+ adoptedImportCount: descriptor.policy.adoptedImports.length,
4577
+ hasApproval: approval !== undefined,
4578
+ })
4579
+ ? validateGovernanceEvidence(reviewedApproval, planned, current.observation.resources, current.importIdentifierDigests, descriptor.policy.adoptedImports)
2633
4580
  : {};
2634
4581
  const authorization = {
2635
4582
  action,
@@ -2638,6 +4585,9 @@ async function runInfrastructurePlanExecutor(options, context, environment = pro
2638
4585
  ? { governanceEvidenceDigests }
2639
4586
  : {}),
2640
4587
  kind: 'ebk-infrastructure-authorization',
4588
+ ...(localApproval && reviewedApproval?.governance !== undefined
4589
+ ? { localGovernance: reviewedApproval.governance }
4590
+ : {}),
2641
4591
  plannedMaterialDigest: planned.materialDigest,
2642
4592
  recomputedMaterialDigest: plan.materialDigest,
2643
4593
  schemaVersion: 1,
@@ -2647,9 +4597,14 @@ async function runInfrastructurePlanExecutor(options, context, environment = pro
2647
4597
  const evidenceDirectory = (0, node_path_1.join)(root, '.ebk', 'evidence');
2648
4598
  (0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
2649
4599
  (0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
4600
+ const refreshPreviewDiagnostics = refreshPreviewDiagnosticsByObservation.get(current.observation);
4601
+ if (refreshPreviewDiagnostics) {
4602
+ writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-drift-diagnostics.json'), `${JSON.stringify(refreshPreviewDiagnostics, null, 2)}\n`);
4603
+ }
2650
4604
  writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.raw.json'), current.diffStdout);
2651
4605
  writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.private-stderr.txt'), current.diffStderr);
2652
4606
  writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.json'), `${JSON.stringify(plan, null, 2)}\n`);
4607
+ writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-build-artifacts.json'), `${JSON.stringify(current.buildArtifacts, null, 2)}\n`);
2653
4608
  writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan-authorization.json'), `${JSON.stringify(authorization, null, 2)}\n`);
2654
4609
  writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-observer-authority.json'), `${JSON.stringify({
2655
4610
  kind: 'ebk-infrastructure-observer-authority',
@@ -2686,15 +4641,32 @@ async function runInfrastructurePlanExecutor(options, context, environment = pro
2686
4641
  const waitsForEnvironmentReviewer = approvalMode === 'environment-reviewer' &&
2687
4642
  plan.target.stage === 'production' &&
2688
4643
  (0, index_js_1.isEnvironmentReviewerPendingAuthorization)(authorization);
2689
- return { success: validation.allowed || waitsForEnvironmentReviewer };
4644
+ const waitsForLocalPlanReview = isLocalPlanReviewPending({
4645
+ context,
4646
+ environment,
4647
+ plan,
4648
+ validation,
4649
+ });
4650
+ return {
4651
+ success: validation.allowed ||
4652
+ waitsForEnvironmentReviewer ||
4653
+ waitsForLocalPlanReview,
4654
+ };
2690
4655
  }
2691
4656
  async function revalidateInfrastructureMutationAuthorization(action, options, context, invocation, environment = process.env, dependencies = {}) {
2692
4657
  const root = projectRoot(context, options);
2693
4658
  const evidenceDirectory = (0, node_path_1.join)(root, '.ebk', 'evidence');
2694
- const retainedPlan = record(readJsonFile((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.json'), 'Retained infrastructure plan'), 'Retained infrastructure plan');
4659
+ const retainedPlanContents = (0, node_fs_1.readFileSync)((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.json'), 'utf8');
4660
+ const retainedPlan = record(parseJson(retainedPlanContents, 'Retained infrastructure plan'), 'Retained infrastructure plan');
2695
4661
  const authorizationRaw = (0, node_fs_1.readFileSync)((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan-authorization.json'), 'utf8');
2696
4662
  const retainedAuthorization = record(parseJson(authorizationRaw, 'Retained infrastructure authorization'), 'Retained infrastructure authorization');
2697
4663
  const stage = invocation.environment.EBK_CONSUMER_STAGE;
4664
+ const operatingPolicy = (0, index_js_2.readRepositoryOperatingPolicy)(context.root);
4665
+ const localGovernedAuthority = (stage === 'development' ||
4666
+ stage === 'staging' ||
4667
+ stage === 'production') &&
4668
+ !(0, index_js_2.requiresGithubDeployment)(operatingPolicy.deploymentProfile, stage) &&
4669
+ environment.GITHUB_ACTIONS !== 'true';
2698
4670
  const approvalMode = repositoryInfrastructureApprovalMode(context, stage);
2699
4671
  const reviewedSummaryDigest = environment.EBK_ENVIRONMENT_REVIEWED_PLAN_DIGEST?.trim();
2700
4672
  if (reviewedSummaryDigest && approvalMode !== 'environment-reviewer') {
@@ -2740,10 +4712,77 @@ async function revalidateInfrastructureMutationAuthorization(action, options, co
2740
4712
  !retainedPlan.projects.includes(context.projectName ?? '')) {
2741
4713
  throw new Error(`Protected ${action} requires the exact retained plan and plan-clean authorization for the current project and stage.`);
2742
4714
  }
4715
+ if (localGovernedAuthority && stage === 'production') {
4716
+ const expectedAccount = expectedDeploymentAccount(environment, invocation);
4717
+ const expectedConfirmation = localProductionConfirmation({
4718
+ action,
4719
+ accountId: expectedAccount,
4720
+ region: retainedPlan.target.region,
4721
+ sourceCommit: retainedPlan.sourceCommit,
4722
+ });
4723
+ if (options.localProductionConfirmation !== expectedConfirmation) {
4724
+ throw new Error(`Local production mutation requires --localProductionConfirmation=${expectedConfirmation}.`);
4725
+ }
4726
+ }
2743
4727
  const planOptions = {
2744
4728
  ...options,
2745
4729
  action,
2746
4730
  };
4731
+ if (localGovernedAuthority && (action === 'deploy' || action === 'refresh')) {
4732
+ const now = (dependencies.now ?? (() => new Date()))();
4733
+ (dependencies.localCapability ?? consumeLocalMutationCapability)({
4734
+ action,
4735
+ authorizationContents: authorizationRaw,
4736
+ context,
4737
+ dependencies,
4738
+ environment,
4739
+ invocation,
4740
+ now,
4741
+ options: planOptions,
4742
+ plan: retainedPlan,
4743
+ planContents: retainedPlanContents,
4744
+ });
4745
+ const reviewedApproval = retainedAuthorization.approval &&
4746
+ retainedAuthorization.localGovernance !== undefined
4747
+ ? {
4748
+ approval: retainedAuthorization.approval,
4749
+ governance: retainedAuthorization.localGovernance,
4750
+ planned: retainedPlan,
4751
+ }
4752
+ : undefined;
4753
+ const governanceEvidenceDigests = validateGovernanceEvidence(reviewedApproval, retainedPlan);
4754
+ if (canonicalJson(governanceEvidenceDigests) !==
4755
+ canonicalJson(retainedAuthorization.governanceEvidenceDigests ?? {})) {
4756
+ throw new Error('Local governed mutation evidence changed after operator acknowledgement.');
4757
+ }
4758
+ const verifiedAt = now.toISOString();
4759
+ const binding = {
4760
+ action,
4761
+ kind: 'ebk-infrastructure-mutation-binding',
4762
+ localAuthority: {
4763
+ confirmationDigest: stage === 'production'
4764
+ ? digest(options.localProductionConfirmation ?? '')
4765
+ : null,
4766
+ expectedRoleArn: invocation.environment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN,
4767
+ mode: 'local-invocation-capability',
4768
+ },
4769
+ recomputedMaterialDigest: retainedPlan.materialDigest,
4770
+ retainedAuthorizationDigest: digest(authorizationRaw),
4771
+ retainedMaterialDigest: retainedPlan.materialDigest,
4772
+ schemaVersion: 1,
4773
+ sourceCommit: retainedPlan.sourceCommit,
4774
+ stage: retainedPlan.target.stage,
4775
+ ...(Object.keys(governanceEvidenceDigests).length > 0
4776
+ ? { governanceEvidenceDigests }
4777
+ : {}),
4778
+ validation: retainedAuthorization.validation,
4779
+ verifiedAt,
4780
+ };
4781
+ (0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
4782
+ (0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
4783
+ writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-mutation-binding.json'), `${JSON.stringify(binding, null, 2)}\n`);
4784
+ return;
4785
+ }
2747
4786
  const candidateSourceCommit = approvalCandidateSourceCommit(context, planOptions, environment) ??
2748
4787
  retainedPlan.sourceCommit;
2749
4788
  const recomputedCurrent = computeCurrentInfrastructurePlan({
@@ -2758,18 +4797,34 @@ async function revalidateInfrastructureMutationAuthorization(action, options, co
2758
4797
  const recomputed = recomputedCurrent.plan;
2759
4798
  let reviewedApproval;
2760
4799
  const approvalRecordPath = retainedAuthorization.approval?.recordPath;
4800
+ if (retainedAuthorization.localGovernance !== undefined &&
4801
+ (!localGovernedAuthority ||
4802
+ (action !== 'deploy' && action !== 'refresh') ||
4803
+ approvalRecordPath !== undefined)) {
4804
+ throw new Error('Local infrastructure governance is invalid outside one exact local refresh authorization.');
4805
+ }
2761
4806
  if (approvalRecordPath) {
2762
4807
  const batch = record(readJsonFile(relativeFile(context.root, approvalRecordPath), 'Retained infrastructure approval governance'), 'Retained infrastructure approval governance');
2763
4808
  reviewedApproval = batch.projects[context.projectName ?? ''];
2764
4809
  }
2765
- const governanceEvidenceDigests = validateGovernanceEvidence(reviewedApproval, reviewedApproval?.planned ?? retainedPlan, recomputedCurrent.observation.resources, recomputedCurrent.importIdentifierDigests);
4810
+ else if (localGovernedAuthority &&
4811
+ retainedAuthorization.approval &&
4812
+ retainedAuthorization.localGovernance !== undefined) {
4813
+ reviewedApproval = {
4814
+ approval: retainedAuthorization.approval,
4815
+ governance: retainedAuthorization.localGovernance,
4816
+ planned: retainedPlan,
4817
+ };
4818
+ }
4819
+ const governanceEvidenceDigests = validateGovernanceEvidence(reviewedApproval, reviewedApproval?.planned ?? retainedPlan, recomputedCurrent.observation.resources, recomputedCurrent.importIdentifierDigests, recomputedCurrent.descriptor.policy.adoptedImports);
2766
4820
  if (canonicalJson(governanceEvidenceDigests) !==
2767
4821
  canonicalJson(retainedAuthorization.governanceEvidenceDigests ?? {})) {
2768
4822
  throw new Error('Governed infrastructure mutation evidence changed after plan authorization.');
2769
4823
  }
2770
4824
  const changes = sourceChanges(dependencies.spawn, context.root, environment, retainedPlan.sourceCommit);
2771
4825
  const now = (dependencies.now ?? (() => new Date()))().toISOString();
2772
- const validationPlan = retainedAuthorization.approval?.mode === 'human-record'
4826
+ const validationPlan = retainedAuthorization.approval?.mode === 'human-record' ||
4827
+ retainedAuthorization.approval?.mode === 'operator-acknowledgement'
2773
4828
  ? {
2774
4829
  ...retainedPlan,
2775
4830
  // The retained authorization already proved the reviewer acted after
@@ -2778,15 +4833,28 @@ async function revalidateInfrastructureMutationAuthorization(action, options, co
2778
4833
  generatedAt: retainedAuthorization.approval.approvedAt,
2779
4834
  }
2780
4835
  : retainedPlan;
2781
- const validation = (0, index_js_1.validateInfrastructurePlanForApply)({
2782
- ...(retainedAuthorization.approval
2783
- ? { approval: retainedAuthorization.approval }
2784
- : {}),
2785
- now,
2786
- planned: validationPlan,
2787
- recomputed,
2788
- sourceChangesSincePlan: changes,
2789
- });
4836
+ const protectedSourceAuthority = environment.GITHUB_ACTIONS === 'true' &&
4837
+ (action === 'deploy' || action === 'refresh');
4838
+ const validation = protectedSourceAuthority
4839
+ ? (0, index_js_1.validateInfrastructurePlanForProtectedApply)({
4840
+ ...(retainedAuthorization.approval
4841
+ ? { approval: retainedAuthorization.approval }
4842
+ : {}),
4843
+ ...(environmentAdmission ? { environmentReviewerApproved: true } : {}),
4844
+ now,
4845
+ planned: validationPlan,
4846
+ recomputed,
4847
+ sourceChangesSincePlan: changes,
4848
+ })
4849
+ : (0, index_js_1.validateInfrastructurePlanForApply)({
4850
+ ...(retainedAuthorization.approval
4851
+ ? { approval: retainedAuthorization.approval }
4852
+ : {}),
4853
+ now,
4854
+ planned: validationPlan,
4855
+ recomputed,
4856
+ sourceChangesSincePlan: changes,
4857
+ });
2790
4858
  const environmentReviewerAllowed = environmentAdmission !== undefined &&
2791
4859
  (0, index_js_1.isEnvironmentReviewerPendingAuthorization)({
2792
4860
  ...retainedAuthorization,
@@ -2797,6 +4865,17 @@ async function revalidateInfrastructureMutationAuthorization(action, options, co
2797
4865
  const binding = {
2798
4866
  action,
2799
4867
  kind: 'ebk-infrastructure-mutation-binding',
4868
+ ...(localGovernedAuthority
4869
+ ? {
4870
+ localAuthority: {
4871
+ confirmationDigest: stage === 'production'
4872
+ ? digest(options.localProductionConfirmation ?? '')
4873
+ : null,
4874
+ expectedRoleArn: invocation.environment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN,
4875
+ mode: 'local-federated',
4876
+ },
4877
+ }
4878
+ : {}),
2800
4879
  recomputedMaterialDigest: recomputed.materialDigest,
2801
4880
  retainedAuthorizationDigest: digest(authorizationRaw),
2802
4881
  retainedMaterialDigest: retainedPlan.materialDigest,
@@ -2904,6 +4983,7 @@ async function recordInfrastructureMutationConvergence(action, options, context,
2904
4983
  };
2905
4984
  let result;
2906
4985
  let converged = false;
4986
+ let refreshStateChangedAfterNonzeroExit = false;
2907
4987
  try {
2908
4988
  const planOptions = {
2909
4989
  ...options,
@@ -3063,6 +5143,25 @@ async function recordInfrastructureMutationConvergence(action, options, context,
3063
5143
  }
3064
5144
  else {
3065
5145
  const postPlanEmpty = current.plan.diff.complete && current.plan.diff.resources.length === 0;
5146
+ refreshStateChangedAfterNonzeroExit =
5147
+ action === 'refresh' &&
5148
+ !applySucceeded &&
5149
+ retainedPlan.state.backend === current.plan.state.backend &&
5150
+ retainedPlan.state.stack === current.plan.state.stack &&
5151
+ retainedPlan.state.digest !== current.plan.state.digest &&
5152
+ retainedPlan.state.version !== current.plan.state.version &&
5153
+ current.plan.state.locked === false &&
5154
+ current.plan.diff.complete &&
5155
+ current.plan.diff.failures.length === 0 &&
5156
+ (current.plan.driftStatus === 'no-drift' ||
5157
+ current.plan.driftStatus === 'drift-detected');
5158
+ const mutationCompleted = applySucceeded;
5159
+ const refreshHandoffRetained = action === 'refresh' &&
5160
+ mutationCompleted &&
5161
+ current.plan.diff.complete &&
5162
+ current.plan.diff.failures.length === 0 &&
5163
+ (current.plan.driftStatus === 'no-drift' ||
5164
+ current.plan.driftStatus === 'drift-detected');
3066
5165
  const requiredCheckIds = action === 'deploy'
3067
5166
  ? ['deployed-smoke', 'alarm-health', 'data-integrity']
3068
5167
  : [];
@@ -3072,7 +5171,7 @@ async function recordInfrastructureMutationConvergence(action, options, context,
3072
5171
  // resource in the authorized plan. The empty recomputed post-plan
3073
5172
  // cannot prove that no provider mutation occurred.
3074
5173
  changedResourceCount: retainedPlan.diff.resources.length,
3075
- status: applySucceeded ? 'succeeded' : 'failed',
5174
+ status: mutationCompleted ? 'succeeded' : 'failed',
3076
5175
  },
3077
5176
  checks: [],
3078
5177
  cleanupDebt: [],
@@ -3089,6 +5188,14 @@ async function recordInfrastructureMutationConvergence(action, options, context,
3089
5188
  action,
3090
5189
  ...evidenceBinding,
3091
5190
  ...convergence,
5191
+ ...(refreshHandoffRetained && !convergence.converged
5192
+ ? {
5193
+ findings: [
5194
+ 'Refresh completed with an exact remaining desired-state plan; run a new governed deployment plan before applying it.',
5195
+ ],
5196
+ outcome: 'refresh-complete-replan-required',
5197
+ }
5198
+ : {}),
3092
5199
  kind: 'ebk-infrastructure-convergence',
3093
5200
  postPlan: current.plan.diff,
3094
5201
  postApplyDrift: {
@@ -3103,6 +5210,19 @@ async function recordInfrastructureMutationConvergence(action, options, context,
3103
5210
  driftBasis: postPlanEmpty && current.plan.driftStatus === 'no-drift'
3104
5211
  ? 'live-observer-no-drift-and-empty-source-state-plan'
3105
5212
  : 'live-observer-not-converged',
5213
+ processOutcome: refreshStateChangedAfterNonzeroExit
5214
+ ? {
5215
+ childExit: 'nonzero',
5216
+ disposition: 'state-changed-recovery-required',
5217
+ postStateDigest: current.plan.state.digest,
5218
+ retainedStateDigest: retainedPlan.state.digest,
5219
+ }
5220
+ : {
5221
+ childExit: applySucceeded ? 'zero' : 'nonzero',
5222
+ disposition: applySucceeded
5223
+ ? 'completed'
5224
+ : 'not-reconciled',
5225
+ },
3106
5226
  }
3107
5227
  : {}),
3108
5228
  schemaVersion: 1,
@@ -3123,14 +5243,17 @@ async function recordInfrastructureMutationConvergence(action, options, context,
3123
5243
  (0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
3124
5244
  (0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
3125
5245
  writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-convergence.json'), `${JSON.stringify(result, null, 2)}\n`);
3126
- // A successful deploy intentionally hands off nonterminal
3127
- // apply-complete-unverified evidence to deployed smoke. The lifecycle
3128
- // command succeeded, but no caller may treat this return value as terminal
3129
- // convergence; only finalizeInfrastructureMutationConvergence can write it.
5246
+ // Successful deploy and refresh commands may intentionally retain
5247
+ // nonterminal handoffs. Deploy waits for deployed smoke; refresh may leave
5248
+ // an exact desired-state plan that must be separately reviewed and applied.
5249
+ // Neither handoff is terminal convergence.
3130
5250
  return (converged ||
3131
5251
  (action === 'deploy' &&
3132
5252
  applySucceeded &&
3133
- result['outcome'] === 'apply-complete-unverified'));
5253
+ result['outcome'] === 'apply-complete-unverified') ||
5254
+ (action === 'refresh' &&
5255
+ applySucceeded &&
5256
+ result['outcome'] === 'refresh-complete-replan-required'));
3134
5257
  }
3135
5258
  async function finalizeInfrastructureMutationConvergence(options, context, environment = process.env, dependencies = {}) {
3136
5259
  const root = projectRoot(context, options);