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

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 (824) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +113 -3
  3. package/executors.json +30 -0
  4. package/package.json +84 -6
  5. package/src/api-contract/baseline-update.d.ts +82 -0
  6. package/src/api-contract/baseline-update.js +225 -0
  7. package/src/api-contract/baseline-update.js.map +1 -0
  8. package/src/api-contract/compatibility.d.ts +30 -0
  9. package/src/api-contract/compatibility.js +643 -0
  10. package/src/api-contract/compatibility.js.map +1 -0
  11. package/src/api-contract/contract-graph.d.ts +35 -0
  12. package/src/api-contract/contract-graph.js +141 -0
  13. package/src/api-contract/contract-graph.js.map +1 -0
  14. package/src/api-contract/evidence.d.ts +23 -0
  15. package/src/api-contract/evidence.js +87 -0
  16. package/src/api-contract/evidence.js.map +1 -0
  17. package/src/api-contract/index.d.ts +6 -0
  18. package/src/api-contract/index.js +10 -0
  19. package/src/api-contract/index.js.map +1 -0
  20. package/src/api-contract/model.d.ts +144 -0
  21. package/src/api-contract/model.fixtures.d.ts +2 -0
  22. package/src/api-contract/model.fixtures.js +81 -0
  23. package/src/api-contract/model.fixtures.js.map +1 -0
  24. package/src/api-contract/model.js +993 -0
  25. package/src/api-contract/model.js.map +1 -0
  26. package/src/api-contract/rollout.d.ts +32 -0
  27. package/src/api-contract/rollout.js +88 -0
  28. package/src/api-contract/rollout.js.map +1 -0
  29. package/src/authorization/compatibility.d.ts +62 -0
  30. package/src/authorization/compatibility.js +175 -0
  31. package/src/authorization/compatibility.js.map +1 -0
  32. package/src/authorization/index.d.ts +1 -0
  33. package/src/authorization/index.js +5 -0
  34. package/src/authorization/index.js.map +1 -0
  35. package/src/blueprints/execution.d.ts +25 -1
  36. package/src/blueprints/execution.js +246 -83
  37. package/src/blueprints/execution.js.map +1 -1
  38. package/src/blueprints/external-sandbox.d.ts +53 -0
  39. package/src/blueprints/external-sandbox.js +1182 -0
  40. package/src/blueprints/external-sandbox.js.map +1 -0
  41. package/src/blueprints/manifest.d.ts +42 -2
  42. package/src/blueprints/manifest.js +82 -27
  43. package/src/blueprints/manifest.js.map +1 -1
  44. package/src/blueprints/resolver.d.ts +14 -5
  45. package/src/blueprints/resolver.js +228 -68
  46. package/src/blueprints/resolver.js.map +1 -1
  47. package/src/cli/agent-command.d.ts +30 -0
  48. package/src/cli/agent-command.js +204 -0
  49. package/src/cli/agent-command.js.map +1 -0
  50. package/src/cli/agent-runtime.d.ts +25 -0
  51. package/src/cli/agent-runtime.js +488 -0
  52. package/src/cli/agent-runtime.js.map +1 -0
  53. package/src/cli/blueprint-authority.d.ts +9 -0
  54. package/src/cli/blueprint-authority.js +43 -0
  55. package/src/cli/blueprint-authority.js.map +1 -0
  56. package/src/cli/blueprint-command.d.ts +24 -0
  57. package/src/cli/blueprint-command.js +63 -0
  58. package/src/cli/blueprint-command.js.map +1 -0
  59. package/src/cli/bootstrap.d.ts +31 -0
  60. package/src/cli/bootstrap.js +165 -0
  61. package/src/cli/bootstrap.js.map +1 -0
  62. package/src/cli/dev-command.d.ts +92 -0
  63. package/src/cli/dev-command.js +561 -0
  64. package/src/cli/dev-command.js.map +1 -0
  65. package/src/cli/main.d.ts +131 -0
  66. package/src/cli/main.js +1199 -0
  67. package/src/cli/main.js.map +1 -0
  68. package/src/cli/output.d.ts +19 -0
  69. package/src/cli/output.js +51 -0
  70. package/src/cli/output.js.map +1 -0
  71. package/src/cli/personal-secret-runtime.d.ts +81 -0
  72. package/src/cli/personal-secret-runtime.js +567 -0
  73. package/src/cli/personal-secret-runtime.js.map +1 -0
  74. package/src/cli/secret-terminal.d.ts +12 -0
  75. package/src/cli/secret-terminal.js +128 -0
  76. package/src/cli/secret-terminal.js.map +1 -0
  77. package/src/container-images.d.ts +1 -0
  78. package/src/container-images.js +5 -0
  79. package/src/container-images.js.map +1 -0
  80. package/src/data-lifecycle/index.d.ts +3 -0
  81. package/src/data-lifecycle/index.js +7 -0
  82. package/src/data-lifecycle/index.js.map +1 -0
  83. package/src/data-lifecycle/model.d.ts +172 -0
  84. package/src/data-lifecycle/model.fixtures.d.ts +2 -0
  85. package/src/data-lifecycle/model.fixtures.js +244 -0
  86. package/src/data-lifecycle/model.fixtures.js.map +1 -0
  87. package/src/data-lifecycle/model.js +95 -0
  88. package/src/data-lifecycle/model.js.map +1 -0
  89. package/src/data-lifecycle/sensitive-artifacts.d.ts +28 -0
  90. package/src/data-lifecycle/sensitive-artifacts.js +146 -0
  91. package/src/data-lifecycle/sensitive-artifacts.js.map +1 -0
  92. package/src/data-lifecycle/validation.d.ts +32 -0
  93. package/src/data-lifecycle/validation.js +837 -0
  94. package/src/data-lifecycle/validation.js.map +1 -0
  95. package/src/database-retirement/contract.d.ts +68 -0
  96. package/src/database-retirement/contract.js +166 -0
  97. package/src/database-retirement/contract.js.map +1 -0
  98. package/src/database-retirement/index.d.ts +1 -0
  99. package/src/database-retirement/index.js +5 -0
  100. package/src/database-retirement/index.js.map +1 -0
  101. package/src/deterministic-order.d.ts +2 -0
  102. package/src/deterministic-order.js +7 -0
  103. package/src/deterministic-order.js.map +1 -0
  104. package/src/developer-agent/context.d.ts +27 -0
  105. package/src/developer-agent/context.js +497 -0
  106. package/src/developer-agent/context.js.map +1 -0
  107. package/src/developer-agent/generator-planning.d.ts +34 -0
  108. package/src/developer-agent/generator-planning.js +645 -0
  109. package/src/developer-agent/generator-planning.js.map +1 -0
  110. package/src/developer-agent/generator-preview.d.ts +8 -0
  111. package/src/developer-agent/generator-preview.js +650 -0
  112. package/src/developer-agent/generator-preview.js.map +1 -0
  113. package/src/developer-agent/git-inspection.d.ts +108 -0
  114. package/src/developer-agent/git-inspection.js +815 -0
  115. package/src/developer-agent/git-inspection.js.map +1 -0
  116. package/src/developer-agent/handoff.d.ts +43 -0
  117. package/src/developer-agent/handoff.js +698 -0
  118. package/src/developer-agent/handoff.js.map +1 -0
  119. package/src/developer-agent/index.d.ts +10 -0
  120. package/src/developer-agent/index.js +14 -0
  121. package/src/developer-agent/index.js.map +1 -0
  122. package/src/developer-agent/model.d.ts +481 -0
  123. package/src/developer-agent/model.js +143 -0
  124. package/src/developer-agent/model.js.map +1 -0
  125. package/src/developer-agent/operation-registry.d.ts +45 -0
  126. package/src/developer-agent/operation-registry.js +1255 -0
  127. package/src/developer-agent/operation-registry.js.map +1 -0
  128. package/src/developer-agent/ordering.d.ts +9 -0
  129. package/src/developer-agent/ordering.js +15 -0
  130. package/src/developer-agent/ordering.js.map +1 -0
  131. package/src/developer-agent/planning.d.ts +18 -0
  132. package/src/developer-agent/planning.js +447 -0
  133. package/src/developer-agent/planning.js.map +1 -0
  134. package/src/developer-agent/preview-changes.d.ts +26 -0
  135. package/src/developer-agent/preview-changes.js +203 -0
  136. package/src/developer-agent/preview-changes.js.map +1 -0
  137. package/src/developer-agent/preview-source.d.ts +2 -0
  138. package/src/developer-agent/preview-source.js +16 -0
  139. package/src/developer-agent/preview-source.js.map +1 -0
  140. package/src/developer-agent/validation.d.ts +18 -0
  141. package/src/developer-agent/validation.js +2617 -0
  142. package/src/developer-agent/validation.js.map +1 -0
  143. package/src/developer-agent/workspace-inspection.d.ts +20 -0
  144. package/src/developer-agent/workspace-inspection.js +1134 -0
  145. package/src/developer-agent/workspace-inspection.js.map +1 -0
  146. package/src/developer-agent/workspace-path.d.ts +6 -0
  147. package/src/developer-agent/workspace-path.js +22 -0
  148. package/src/developer-agent/workspace-path.js.map +1 -0
  149. package/src/executors/blueprint-trust/blueprint-trust.d.ts +29 -0
  150. package/src/executors/blueprint-trust/blueprint-trust.js +115 -0
  151. package/src/executors/blueprint-trust/blueprint-trust.js.map +1 -0
  152. package/src/executors/blueprint-trust/schema.json +27 -0
  153. package/src/executors/blueprint-verification/blueprint-verification.d.ts +13 -0
  154. package/src/executors/blueprint-verification/blueprint-verification.js +110 -0
  155. package/src/executors/blueprint-verification/blueprint-verification.js.map +1 -0
  156. package/src/executors/blueprint-verification/schema.json +44 -0
  157. package/src/executors/deployment-plan/deployment-plan.js +1 -0
  158. package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
  159. package/src/executors/dev-doctor/dev-doctor.d.ts +19 -5
  160. package/src/executors/dev-doctor/dev-doctor.js +186 -32
  161. package/src/executors/dev-doctor/dev-doctor.js.map +1 -1
  162. package/src/executors/dev-doctor/schema.json +1 -6
  163. package/src/executors/dev-setup/dev-setup.d.ts +7 -1
  164. package/src/executors/dev-setup/dev-setup.js +14 -3
  165. package/src/executors/dev-setup/dev-setup.js.map +1 -1
  166. package/src/executors/developer-context/developer-context.d.ts +38 -0
  167. package/src/executors/developer-context/developer-context.js +78 -0
  168. package/src/executors/developer-context/developer-context.js.map +1 -0
  169. package/src/executors/developer-context/schema.json +87 -0
  170. package/src/executors/fleet/fleet.d.ts +12 -43
  171. package/src/executors/fleet/fleet.js +120 -500
  172. package/src/executors/fleet/fleet.js.map +1 -1
  173. package/src/executors/fleet/schema.json +64 -14
  174. package/src/executors/github-bootstrap/github-bootstrap.d.ts +1 -0
  175. package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
  176. package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
  177. package/src/executors/github-bootstrap/schema.json +5 -0
  178. package/src/executors/infrastructure-plan/infrastructure-plan.d.ts +144 -0
  179. package/src/executors/infrastructure-plan/infrastructure-plan.js +3320 -0
  180. package/src/executors/infrastructure-plan/infrastructure-plan.js.map +1 -0
  181. package/src/executors/infrastructure-plan/schema.json +78 -0
  182. package/src/executors/secret-guardrails/schema.json +6 -0
  183. package/src/executors/secret-guardrails/secret-guardrails.d.ts +3 -0
  184. package/src/executors/secret-guardrails/secret-guardrails.js +44 -0
  185. package/src/executors/secret-guardrails/secret-guardrails.js.map +1 -0
  186. package/src/executors/secret-metadata/schema.json +31 -0
  187. package/src/executors/secret-metadata/secret-metadata.d.ts +32 -0
  188. package/src/executors/secret-metadata/secret-metadata.js +131 -0
  189. package/src/executors/secret-metadata/secret-metadata.js.map +1 -0
  190. package/src/executors/sst-lifecycle/logging-preflight.d.ts +17 -0
  191. package/src/executors/sst-lifecycle/logging-preflight.js +196 -0
  192. package/src/executors/sst-lifecycle/logging-preflight.js.map +1 -0
  193. package/src/executors/sst-lifecycle/run-sst-command.d.ts +23 -3
  194. package/src/executors/sst-lifecycle/run-sst-command.js +113 -10
  195. package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
  196. package/src/executors/sst-lifecycle/schema.json +9 -5
  197. package/src/executors/sst-lifecycle/secret-preflight.d.ts +25 -0
  198. package/src/executors/sst-lifecycle/secret-preflight.js +119 -0
  199. package/src/executors/sst-lifecycle/secret-preflight.js.map +1 -0
  200. package/src/foundation/discovery.d.ts +17 -1
  201. package/src/foundation/discovery.js +16 -1
  202. package/src/foundation/discovery.js.map +1 -1
  203. package/src/foundation/index.d.ts +3 -0
  204. package/src/foundation/index.js +3 -0
  205. package/src/foundation/index.js.map +1 -1
  206. package/src/foundation/local-database-contract.d.ts +1 -0
  207. package/src/foundation/local-database-contract.js +3 -0
  208. package/src/foundation/local-database-contract.js.map +1 -1
  209. package/src/foundation/logging-preflight.d.ts +30 -0
  210. package/src/foundation/logging-preflight.js +138 -0
  211. package/src/foundation/logging-preflight.js.map +1 -0
  212. package/src/foundation/logging.d.ts +70 -0
  213. package/src/foundation/logging.js +133 -0
  214. package/src/foundation/logging.js.map +1 -0
  215. package/src/foundation/preflight.d.ts +9 -0
  216. package/src/foundation/preflight.js +58 -2
  217. package/src/foundation/preflight.js.map +1 -1
  218. package/src/foundation/provision-database-bindings.d.ts +9 -1
  219. package/src/foundation/provision-database-bindings.js +25 -5
  220. package/src/foundation/provision-database-bindings.js.map +1 -1
  221. package/src/foundation/provision-logical-database.d.ts +10 -2
  222. package/src/foundation/provision-logical-database.js +68 -113
  223. package/src/foundation/provision-logical-database.js.map +1 -1
  224. package/src/foundation/router-origin-proof.d.ts +2 -0
  225. package/src/foundation/router-origin-proof.js +25 -0
  226. package/src/foundation/router-origin-proof.js.map +1 -0
  227. package/src/foundation/stages.d.ts +1 -0
  228. package/src/foundation/stages.js +9 -1
  229. package/src/foundation/stages.js.map +1 -1
  230. package/src/frontend-security/index.d.ts +1 -0
  231. package/src/frontend-security/index.js +5 -0
  232. package/src/frontend-security/index.js.map +1 -0
  233. package/src/frontend-security/security-headers.d.ts +50 -0
  234. package/src/frontend-security/security-headers.js +418 -0
  235. package/src/frontend-security/security-headers.js.map +1 -0
  236. package/src/generators/feature-registry.d.ts +13 -0
  237. package/src/generators/feature-registry.js +73 -0
  238. package/src/generators/feature-registry.js.map +1 -0
  239. package/src/generators/function/function.d.ts +1 -1
  240. package/src/generators/function/function.js +51 -6
  241. package/src/generators/function/function.js.map +1 -1
  242. package/src/generators/function/schema.d.ts +1 -0
  243. package/src/generators/function/schema.json +6 -1
  244. package/src/generators/logging-registry.d.ts +13 -0
  245. package/src/generators/logging-registry.js +62 -0
  246. package/src/generators/logging-registry.js.map +1 -0
  247. package/src/generators/post-generation-install.d.ts +67 -0
  248. package/src/generators/post-generation-install.js +3308 -0
  249. package/src/generators/post-generation-install.js.map +1 -0
  250. package/src/generators/preset/generator.js +3 -0
  251. package/src/generators/preset/generator.js.map +1 -1
  252. package/src/generators/preset/tool-versions.d.ts +18 -0
  253. package/src/generators/preset/tool-versions.js +22 -0
  254. package/src/generators/preset/tool-versions.js.map +1 -0
  255. package/src/generators/preset/workspace-files-developer-agent.d.ts +14 -0
  256. package/src/generators/preset/workspace-files-developer-agent.js +511 -0
  257. package/src/generators/preset/workspace-files-developer-agent.js.map +1 -0
  258. package/src/generators/preset/workspace-files-foundation-workload-roles.d.ts +6 -0
  259. package/src/generators/preset/workspace-files-foundation-workload-roles.js +406 -0
  260. package/src/generators/preset/workspace-files-foundation-workload-roles.js.map +1 -0
  261. package/src/generators/preset/workspace-files-performance-cost.d.ts +66 -0
  262. package/src/generators/preset/workspace-files-performance-cost.js +866 -0
  263. package/src/generators/preset/workspace-files-performance-cost.js.map +1 -0
  264. package/src/generators/preset/workspace-files-performance-load.d.ts +73 -0
  265. package/src/generators/preset/workspace-files-performance-load.js +801 -0
  266. package/src/generators/preset/workspace-files-performance-load.js.map +1 -0
  267. package/src/generators/preset/workspace-files-workload-role-baseline.d.ts +9 -0
  268. package/src/generators/preset/workspace-files-workload-role-baseline.js +1102 -0
  269. package/src/generators/preset/workspace-files-workload-role-baseline.js.map +1 -0
  270. package/src/generators/preset/workspace-files.d.ts +14 -1
  271. package/src/generators/preset/workspace-files.js +8189 -245
  272. package/src/generators/preset/workspace-files.js.map +1 -1
  273. package/src/generators/repository-policy/repository-policy.js +3 -47
  274. package/src/generators/repository-policy/repository-policy.js.map +1 -1
  275. package/src/generators/slice/built-in-plan-api-contract-files.d.ts +2 -0
  276. package/src/generators/slice/built-in-plan-api-contract-files.js +2656 -0
  277. package/src/generators/slice/built-in-plan-api-contract-files.js.map +1 -0
  278. package/src/generators/slice/built-in-plan-api-files.d.ts +2 -0
  279. package/src/generators/slice/built-in-plan-api-files.js +1186 -0
  280. package/src/generators/slice/built-in-plan-api-files.js.map +1 -0
  281. package/src/generators/slice/built-in-plan-async-work-files.d.ts +2 -0
  282. package/src/generators/slice/built-in-plan-async-work-files.js +7903 -0
  283. package/src/generators/slice/built-in-plan-async-work-files.js.map +1 -0
  284. package/src/generators/slice/built-in-plan-authorization-files.d.ts +2 -0
  285. package/src/generators/slice/built-in-plan-authorization-files.js +2314 -0
  286. package/src/generators/slice/built-in-plan-authorization-files.js.map +1 -0
  287. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.d.ts +2 -0
  288. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +559 -0
  289. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js.map +1 -0
  290. package/src/generators/slice/built-in-plan-data-deletion-files.d.ts +2 -0
  291. package/src/generators/slice/built-in-plan-data-deletion-files.js +1101 -0
  292. package/src/generators/slice/built-in-plan-data-deletion-files.js.map +1 -0
  293. package/src/generators/slice/built-in-plan-data-export-infrastructure-files.d.ts +2 -0
  294. package/src/generators/slice/built-in-plan-data-export-infrastructure-files.js +249 -0
  295. package/src/generators/slice/built-in-plan-data-export-infrastructure-files.js.map +1 -0
  296. package/src/generators/slice/built-in-plan-data-files.d.ts +2 -0
  297. package/src/generators/slice/built-in-plan-data-files.js +1530 -0
  298. package/src/generators/slice/built-in-plan-data-files.js.map +1 -0
  299. package/src/generators/slice/built-in-plan-data-lifecycle-files.d.ts +2 -0
  300. package/src/generators/slice/built-in-plan-data-lifecycle-files.js +750 -0
  301. package/src/generators/slice/built-in-plan-data-lifecycle-files.js.map +1 -0
  302. package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.d.ts +2 -0
  303. package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.js +828 -0
  304. package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.js.map +1 -0
  305. package/src/generators/slice/built-in-plan-data-retention-job-files.d.ts +2 -0
  306. package/src/generators/slice/built-in-plan-data-retention-job-files.js +2604 -0
  307. package/src/generators/slice/built-in-plan-data-retention-job-files.js.map +1 -0
  308. package/src/generators/slice/built-in-plan-data-retention-launcher-files.d.ts +2 -0
  309. package/src/generators/slice/built-in-plan-data-retention-launcher-files.js +144 -0
  310. package/src/generators/slice/built-in-plan-data-retention-launcher-files.js.map +1 -0
  311. package/src/generators/slice/built-in-plan-data-rights-files.d.ts +2 -0
  312. package/src/generators/slice/built-in-plan-data-rights-files.js +4628 -0
  313. package/src/generators/slice/built-in-plan-data-rights-files.js.map +1 -0
  314. package/src/generators/slice/built-in-plan-database-lifecycle-files.d.ts +2 -0
  315. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1901 -0
  316. package/src/generators/slice/built-in-plan-database-lifecycle-files.js.map +1 -0
  317. package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.d.ts +2 -0
  318. package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js +485 -0
  319. package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js.map +1 -0
  320. package/src/generators/slice/built-in-plan-foundation-files.d.ts +2 -0
  321. package/src/generators/slice/built-in-plan-foundation-files.js +184 -0
  322. package/src/generators/slice/built-in-plan-foundation-files.js.map +1 -0
  323. package/src/generators/slice/built-in-plan-frontend-files.d.ts +2 -0
  324. package/src/generators/slice/built-in-plan-frontend-files.js +2233 -0
  325. package/src/generators/slice/built-in-plan-frontend-files.js.map +1 -0
  326. package/src/generators/slice/built-in-plan-infrastructure-files.d.ts +2 -0
  327. package/src/generators/slice/built-in-plan-infrastructure-files.js +3038 -0
  328. package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -0
  329. package/src/generators/slice/built-in-plan-infrastructure-safety-files.d.ts +2 -0
  330. package/src/generators/slice/built-in-plan-infrastructure-safety-files.js +312 -0
  331. package/src/generators/slice/built-in-plan-infrastructure-safety-files.js.map +1 -0
  332. package/src/generators/slice/built-in-plan-metadata-files.d.ts +3 -0
  333. package/src/generators/slice/built-in-plan-metadata-files.js +219 -0
  334. package/src/generators/slice/built-in-plan-metadata-files.js.map +1 -0
  335. package/src/generators/slice/built-in-plan-operations-files.d.ts +2 -0
  336. package/src/generators/slice/built-in-plan-operations-files.js +630 -0
  337. package/src/generators/slice/built-in-plan-operations-files.js.map +1 -0
  338. package/src/generators/slice/built-in-plan-performance-files.d.ts +3 -0
  339. package/src/generators/slice/built-in-plan-performance-files.js +3293 -0
  340. package/src/generators/slice/built-in-plan-performance-files.js.map +1 -0
  341. package/src/generators/slice/built-in-plan-project-files.d.ts +3 -0
  342. package/src/generators/slice/built-in-plan-project-files.js +979 -0
  343. package/src/generators/slice/built-in-plan-project-files.js.map +1 -0
  344. package/src/generators/slice/built-in-plan-release-files.d.ts +2 -0
  345. package/src/generators/slice/built-in-plan-release-files.js +6387 -0
  346. package/src/generators/slice/built-in-plan-release-files.js.map +1 -0
  347. package/src/generators/slice/built-in-plan-reliability-files.d.ts +2 -0
  348. package/src/generators/slice/built-in-plan-reliability-files.js +4499 -0
  349. package/src/generators/slice/built-in-plan-reliability-files.js.map +1 -0
  350. package/src/generators/slice/built-in-plan-secret-runtime-files.d.ts +2 -0
  351. package/src/generators/slice/built-in-plan-secret-runtime-files.js +1004 -0
  352. package/src/generators/slice/built-in-plan-secret-runtime-files.js.map +1 -0
  353. package/src/generators/slice/built-in-plan-shared.d.ts +53 -0
  354. package/src/generators/slice/built-in-plan-shared.js +113 -0
  355. package/src/generators/slice/built-in-plan-shared.js.map +1 -0
  356. package/src/generators/slice/built-in-plan-testing-files.d.ts +2 -0
  357. package/src/generators/slice/built-in-plan-testing-files.js +2356 -0
  358. package/src/generators/slice/built-in-plan-testing-files.js.map +1 -0
  359. package/src/generators/slice/built-in-plan-verification-files.d.ts +2 -0
  360. package/src/generators/slice/built-in-plan-verification-files.js +358 -0
  361. package/src/generators/slice/built-in-plan-verification-files.js.map +1 -0
  362. package/src/generators/slice/built-in-plan-web-files.d.ts +2 -0
  363. package/src/generators/slice/built-in-plan-web-files.js +430 -0
  364. package/src/generators/slice/built-in-plan-web-files.js.map +1 -0
  365. package/src/generators/slice/built-in-plan-workload-role-files.d.ts +2 -0
  366. package/src/generators/slice/built-in-plan-workload-role-files.js +2356 -0
  367. package/src/generators/slice/built-in-plan-workload-role-files.js.map +1 -0
  368. package/src/generators/slice/built-in-plan.d.ts +8 -0
  369. package/src/generators/slice/built-in-plan.js +264 -0
  370. package/src/generators/slice/built-in-plan.js.map +1 -0
  371. package/src/generators/slice/schema.json +1 -1
  372. package/src/generators/slice/slice.d.ts +1 -22
  373. package/src/generators/slice/slice.js +185 -1510
  374. package/src/generators/slice/slice.js.map +1 -1
  375. package/src/generators/upgrade/schema.d.ts +2 -0
  376. package/src/generators/upgrade/schema.json +11 -1
  377. package/src/generators/upgrade/upgrade.d.ts +51 -1
  378. package/src/generators/upgrade/upgrade.js +820 -82
  379. package/src/generators/upgrade/upgrade.js.map +1 -1
  380. package/src/generators/workload/frozen-container.d.ts +8 -0
  381. package/src/generators/workload/frozen-container.js +36 -0
  382. package/src/generators/workload/frozen-container.js.map +1 -0
  383. package/src/generators/workload/performance-registration.d.ts +32 -0
  384. package/src/generators/workload/performance-registration.js +226 -0
  385. package/src/generators/workload/performance-registration.js.map +1 -0
  386. package/src/generators/workload/reliability-registration.d.ts +16 -0
  387. package/src/generators/workload/reliability-registration.js +150 -0
  388. package/src/generators/workload/reliability-registration.js.map +1 -0
  389. package/src/generators/workload/schema.d.ts +1 -0
  390. package/src/generators/workload/schema.json +6 -1
  391. package/src/generators/workload/service-performance-files.d.ts +4 -0
  392. package/src/generators/workload/service-performance-files.js +1846 -0
  393. package/src/generators/workload/service-performance-files.js.map +1 -0
  394. package/src/generators/workload/workload.d.ts +3 -2
  395. package/src/generators/workload/workload.js +1083 -18
  396. package/src/generators/workload/workload.js.map +1 -1
  397. package/src/git-policy/action-pins.d.ts +10 -0
  398. package/src/git-policy/action-pins.js +10 -0
  399. package/src/git-policy/action-pins.js.map +1 -1
  400. package/src/git-policy/github-bootstrap.d.ts +14 -1
  401. package/src/git-policy/github-bootstrap.js +274 -46
  402. package/src/git-policy/github-bootstrap.js.map +1 -1
  403. package/src/index.d.ts +2 -0
  404. package/src/index.js +2 -0
  405. package/src/index.js.map +1 -1
  406. package/src/infrastructure-change/convergence.d.ts +13 -0
  407. package/src/infrastructure-change/convergence.js +85 -0
  408. package/src/infrastructure-change/convergence.js.map +1 -0
  409. package/src/infrastructure-change/drift.d.ts +13 -0
  410. package/src/infrastructure-change/drift.js +112 -0
  411. package/src/infrastructure-change/drift.js.map +1 -0
  412. package/src/infrastructure-change/index.d.ts +7 -0
  413. package/src/infrastructure-change/index.js +11 -0
  414. package/src/infrastructure-change/index.js.map +1 -0
  415. package/src/infrastructure-change/model.d.ts +253 -0
  416. package/src/infrastructure-change/model.js +28 -0
  417. package/src/infrastructure-change/model.js.map +1 -0
  418. package/src/infrastructure-change/normalize.d.ts +12 -0
  419. package/src/infrastructure-change/normalize.js +245 -0
  420. package/src/infrastructure-change/normalize.js.map +1 -0
  421. package/src/infrastructure-change/plan.d.ts +40 -0
  422. package/src/infrastructure-change/plan.js +361 -0
  423. package/src/infrastructure-change/plan.js.map +1 -0
  424. package/src/infrastructure-change/risk.d.ts +7 -0
  425. package/src/infrastructure-change/risk.js +269 -0
  426. package/src/infrastructure-change/risk.js.map +1 -0
  427. package/src/infrastructure-change/summary.d.ts +20 -0
  428. package/src/infrastructure-change/summary.js +130 -0
  429. package/src/infrastructure-change/summary.js.map +1 -0
  430. package/src/local-dev/context-discovery.d.ts +2 -0
  431. package/src/local-dev/context-discovery.js +59 -0
  432. package/src/local-dev/context-discovery.js.map +1 -0
  433. package/src/local-dev/contract-validation.d.ts +15 -0
  434. package/src/local-dev/contract-validation.js +84 -0
  435. package/src/local-dev/contract-validation.js.map +1 -0
  436. package/src/local-dev/contracts.d.ts +10 -1
  437. package/src/local-dev/contracts.js +57 -5
  438. package/src/local-dev/contracts.js.map +1 -1
  439. package/src/local-dev/control-contract.d.ts +67 -0
  440. package/src/local-dev/control-contract.js +180 -0
  441. package/src/local-dev/control-contract.js.map +1 -0
  442. package/src/local-dev/dev-command-core.d.ts +60 -0
  443. package/src/local-dev/dev-command-core.js +89 -0
  444. package/src/local-dev/dev-command-core.js.map +1 -0
  445. package/src/local-dev/effects.d.ts +16 -0
  446. package/src/local-dev/effects.js +63 -0
  447. package/src/local-dev/effects.js.map +1 -0
  448. package/src/local-dev/environment-inspector.d.ts +125 -0
  449. package/src/local-dev/environment-inspector.js +687 -0
  450. package/src/local-dev/environment-inspector.js.map +1 -0
  451. package/src/local-dev/environment-state.d.ts +113 -0
  452. package/src/local-dev/environment-state.js +463 -0
  453. package/src/local-dev/environment-state.js.map +1 -0
  454. package/src/local-dev/errors.d.ts +6 -0
  455. package/src/local-dev/errors.js +41 -0
  456. package/src/local-dev/errors.js.map +1 -0
  457. package/src/local-dev/fleet-client.d.ts +33 -0
  458. package/src/local-dev/fleet-client.js +156 -0
  459. package/src/local-dev/fleet-client.js.map +1 -0
  460. package/src/local-dev/fleet-configuration.d.ts +13 -0
  461. package/src/local-dev/fleet-configuration.js +71 -0
  462. package/src/local-dev/fleet-configuration.js.map +1 -0
  463. package/src/local-dev/fleet-inventory.d.ts +56 -0
  464. package/src/local-dev/fleet-inventory.js +469 -0
  465. package/src/local-dev/fleet-inventory.js.map +1 -0
  466. package/src/local-dev/fleet-ipc.d.ts +28 -0
  467. package/src/local-dev/fleet-ipc.js +267 -0
  468. package/src/local-dev/fleet-ipc.js.map +1 -0
  469. package/src/local-dev/fleet-launcher.d.ts +53 -0
  470. package/src/local-dev/fleet-launcher.js +379 -0
  471. package/src/local-dev/fleet-launcher.js.map +1 -0
  472. package/src/local-dev/fleet-lock.d.ts +41 -0
  473. package/src/local-dev/fleet-lock.js +135 -0
  474. package/src/local-dev/fleet-lock.js.map +1 -0
  475. package/src/local-dev/fleet-logs.d.ts +66 -0
  476. package/src/local-dev/fleet-logs.js +542 -0
  477. package/src/local-dev/fleet-logs.js.map +1 -0
  478. package/src/local-dev/fleet-process.d.ts +50 -0
  479. package/src/local-dev/fleet-process.js +289 -0
  480. package/src/local-dev/fleet-process.js.map +1 -0
  481. package/src/local-dev/fleet-runtime.d.ts +22 -0
  482. package/src/local-dev/fleet-runtime.js +368 -0
  483. package/src/local-dev/fleet-runtime.js.map +1 -0
  484. package/src/local-dev/fleet-session.d.ts +70 -0
  485. package/src/local-dev/fleet-session.js +301 -0
  486. package/src/local-dev/fleet-session.js.map +1 -0
  487. package/src/local-dev/fleet-slice-runner-daemon.d.ts +1 -0
  488. package/src/local-dev/fleet-slice-runner-daemon.js +21 -0
  489. package/src/local-dev/fleet-slice-runner-daemon.js.map +1 -0
  490. package/src/local-dev/fleet-startup.d.ts +22 -0
  491. package/src/local-dev/fleet-startup.js +114 -0
  492. package/src/local-dev/fleet-startup.js.map +1 -0
  493. package/src/local-dev/fleet-state.d.ts +62 -0
  494. package/src/local-dev/fleet-state.js +297 -0
  495. package/src/local-dev/fleet-state.js.map +1 -0
  496. package/src/local-dev/fleet-supervisor-daemon.d.ts +1 -0
  497. package/src/local-dev/fleet-supervisor-daemon.js +41 -0
  498. package/src/local-dev/fleet-supervisor-daemon.js.map +1 -0
  499. package/src/local-dev/fleet-supervisor.d.ts +52 -0
  500. package/src/local-dev/fleet-supervisor.js +384 -0
  501. package/src/local-dev/fleet-supervisor.js.map +1 -0
  502. package/src/local-dev/gateway.js +1 -1
  503. package/src/local-dev/gateway.js.map +1 -1
  504. package/src/local-dev/identity.js +4 -6
  505. package/src/local-dev/identity.js.map +1 -1
  506. package/src/local-dev/index.d.ts +1 -0
  507. package/src/local-dev/index.js +1 -0
  508. package/src/local-dev/index.js.map +1 -1
  509. package/src/local-dev/local-context.d.ts +62 -0
  510. package/src/local-dev/local-context.js +299 -0
  511. package/src/local-dev/local-context.js.map +1 -0
  512. package/src/local-dev/nx-workspace.d.ts +45 -0
  513. package/src/local-dev/nx-workspace.js +228 -0
  514. package/src/local-dev/nx-workspace.js.map +1 -0
  515. package/src/local-dev/personal-secret-ledger.d.ts +75 -0
  516. package/src/local-dev/personal-secret-ledger.js +391 -0
  517. package/src/local-dev/personal-secret-ledger.js.map +1 -0
  518. package/src/local-dev/personal-stage.d.ts +18 -0
  519. package/src/local-dev/personal-stage.js +129 -0
  520. package/src/local-dev/personal-stage.js.map +1 -0
  521. package/src/local-dev/private-state.d.ts +17 -0
  522. package/src/local-dev/private-state.js +254 -0
  523. package/src/local-dev/private-state.js.map +1 -0
  524. package/src/local-dev/process-identity.d.ts +9 -0
  525. package/src/local-dev/process-identity.js +79 -0
  526. package/src/local-dev/process-identity.js.map +1 -0
  527. package/src/local-dev/runtime-paths.d.ts +17 -0
  528. package/src/local-dev/runtime-paths.js +33 -0
  529. package/src/local-dev/runtime-paths.js.map +1 -0
  530. package/src/local-dev/secret-slice-claim.d.ts +17 -0
  531. package/src/local-dev/secret-slice-claim.js +75 -0
  532. package/src/local-dev/secret-slice-claim.js.map +1 -0
  533. package/src/local-dev/slice-claim.d.ts +36 -0
  534. package/src/local-dev/slice-claim.js +199 -0
  535. package/src/local-dev/slice-claim.js.map +1 -0
  536. package/src/local-dev/slice-runner.d.ts +121 -0
  537. package/src/local-dev/slice-runner.js +715 -0
  538. package/src/local-dev/slice-runner.js.map +1 -0
  539. package/src/local-dev/tls.js +51 -17
  540. package/src/local-dev/tls.js.map +1 -1
  541. package/src/ownership/composition.d.ts +5 -2
  542. package/src/ownership/composition.js +26 -3
  543. package/src/ownership/composition.js.map +1 -1
  544. package/src/ownership/hash.d.ts +1 -1
  545. package/src/ownership/hash.js +3 -1
  546. package/src/ownership/hash.js.map +1 -1
  547. package/src/ownership/record.d.ts +32 -1
  548. package/src/ownership/record.js +139 -2
  549. package/src/ownership/record.js.map +1 -1
  550. package/src/performance/cost-collection.d.ts +225 -0
  551. package/src/performance/cost-collection.js +825 -0
  552. package/src/performance/cost-collection.js.map +1 -0
  553. package/src/performance/cost-controls.d.ts +131 -0
  554. package/src/performance/cost-controls.js +336 -0
  555. package/src/performance/cost-controls.js.map +1 -0
  556. package/src/performance/evidence.d.ts +140 -0
  557. package/src/performance/evidence.js +599 -0
  558. package/src/performance/evidence.js.map +1 -0
  559. package/src/performance/index.d.ts +10 -0
  560. package/src/performance/index.js +14 -0
  561. package/src/performance/index.js.map +1 -0
  562. package/src/performance/load-safety.d.ts +84 -0
  563. package/src/performance/load-safety.js +346 -0
  564. package/src/performance/load-safety.js.map +1 -0
  565. package/src/performance/model.d.ts +129 -0
  566. package/src/performance/model.js +452 -0
  567. package/src/performance/model.js.map +1 -0
  568. package/src/performance/pr-artifact-budget.d.ts +42 -0
  569. package/src/performance/pr-artifact-budget.js +151 -0
  570. package/src/performance/pr-artifact-budget.js.map +1 -0
  571. package/src/performance/runner.d.ts +75 -0
  572. package/src/performance/runner.js +227 -0
  573. package/src/performance/runner.js.map +1 -0
  574. package/src/performance/scenario-endpoint.d.ts +1 -0
  575. package/src/performance/scenario-endpoint.js +5 -0
  576. package/src/performance/scenario-endpoint.js.map +1 -0
  577. package/src/performance/scenario.d.ts +193 -0
  578. package/src/performance/scenario.js +736 -0
  579. package/src/performance/scenario.js.map +1 -0
  580. package/src/performance/test-fixtures.d.ts +7 -0
  581. package/src/performance/test-fixtures.js +169 -0
  582. package/src/performance/test-fixtures.js.map +1 -0
  583. package/src/performance/unit-cost.d.ts +105 -0
  584. package/src/performance/unit-cost.js +364 -0
  585. package/src/performance/unit-cost.js.map +1 -0
  586. package/src/release/index.d.ts +4 -0
  587. package/src/release/index.js +8 -0
  588. package/src/release/index.js.map +1 -0
  589. package/src/release/lifecycle.d.ts +132 -0
  590. package/src/release/lifecycle.js +411 -0
  591. package/src/release/lifecycle.js.map +1 -0
  592. package/src/release/manifest.d.ts +148 -0
  593. package/src/release/manifest.js +503 -0
  594. package/src/release/manifest.js.map +1 -0
  595. package/src/release/rollout.d.ts +170 -0
  596. package/src/release/rollout.js +578 -0
  597. package/src/release/rollout.js.map +1 -0
  598. package/src/release/source-authority.d.ts +19 -0
  599. package/src/release/source-authority.js +195 -0
  600. package/src/release/source-authority.js.map +1 -0
  601. package/src/repository-policy/deployment-plan.d.ts +2 -1
  602. package/src/repository-policy/deployment-plan.js +2 -0
  603. package/src/repository-policy/deployment-plan.js.map +1 -1
  604. package/src/repository-policy/policy.d.ts +1 -1
  605. package/src/repository-policy/policy.js +29 -25
  606. package/src/repository-policy/policy.js.map +1 -1
  607. package/src/schemas/aggregate-count-evidence-v1.json +135 -0
  608. package/src/schemas/api-contract-baseline-v1.json +674 -0
  609. package/src/schemas/api-contract-compatibility-report-v1.json +126 -0
  610. package/src/schemas/aws-billing-actual-evidence-v1.json +407 -0
  611. package/src/schemas/blueprint-v1.json +18 -1
  612. package/src/schemas/data-lifecycle-policy-v1.json +517 -0
  613. package/src/schemas/generated-ownership-v2.json +22 -0
  614. package/src/schemas/performance-policy-v1.json +335 -0
  615. package/src/schemas/repository-policy-v1.json +1 -0
  616. package/src/schemas/testing-evidence-quarantine-v1.json +168 -0
  617. package/src/schemas/testing-evidence-result-v1.json +319 -0
  618. package/src/secrets/adapters.d.ts +49 -0
  619. package/src/secrets/adapters.js +184 -0
  620. package/src/secrets/adapters.js.map +1 -0
  621. package/src/secrets/aws-control-plane.d.ts +20 -0
  622. package/src/secrets/aws-control-plane.js +255 -0
  623. package/src/secrets/aws-control-plane.js.map +1 -0
  624. package/src/secrets/declaration-verification.d.ts +11 -0
  625. package/src/secrets/declaration-verification.js +68 -0
  626. package/src/secrets/declaration-verification.js.map +1 -0
  627. package/src/secrets/guardrails.d.ts +26 -0
  628. package/src/secrets/guardrails.js +328 -0
  629. package/src/secrets/guardrails.js.map +1 -0
  630. package/src/secrets/index.d.ts +12 -0
  631. package/src/secrets/index.js +16 -0
  632. package/src/secrets/index.js.map +1 -0
  633. package/src/secrets/metadata.d.ts +48 -0
  634. package/src/secrets/metadata.js +171 -0
  635. package/src/secrets/metadata.js.map +1 -0
  636. package/src/secrets/model.d.ts +49 -0
  637. package/src/secrets/model.js +125 -0
  638. package/src/secrets/model.js.map +1 -0
  639. package/src/secrets/operator.d.ts +101 -0
  640. package/src/secrets/operator.js +229 -0
  641. package/src/secrets/operator.js.map +1 -0
  642. package/src/secrets/permissions.d.ts +34 -0
  643. package/src/secrets/permissions.js +86 -0
  644. package/src/secrets/permissions.js.map +1 -0
  645. package/src/secrets/personal-command.d.ts +83 -0
  646. package/src/secrets/personal-command.js +197 -0
  647. package/src/secrets/personal-command.js.map +1 -0
  648. package/src/secrets/personal-ledger.d.ts +21 -0
  649. package/src/secrets/personal-ledger.js +135 -0
  650. package/src/secrets/personal-ledger.js.map +1 -0
  651. package/src/secrets/registry.d.ts +9 -0
  652. package/src/secrets/registry.js +86 -0
  653. package/src/secrets/registry.js.map +1 -0
  654. package/src/secrets/verification.d.ts +18 -0
  655. package/src/secrets/verification.js +139 -0
  656. package/src/secrets/verification.js.map +1 -0
  657. package/src/secrets/workload-access.d.ts +46 -0
  658. package/src/secrets/workload-access.js +163 -0
  659. package/src/secrets/workload-access.js.map +1 -0
  660. package/src/supply-chain/action-pins.d.ts +26 -0
  661. package/src/supply-chain/action-pins.js +208 -0
  662. package/src/supply-chain/action-pins.js.map +1 -0
  663. package/src/supply-chain/blueprint-trust-git.d.ts +130 -0
  664. package/src/supply-chain/blueprint-trust-git.js +1581 -0
  665. package/src/supply-chain/blueprint-trust-git.js.map +1 -0
  666. package/src/supply-chain/blueprint-trust-process.d.ts +4 -0
  667. package/src/supply-chain/blueprint-trust-process.js +88 -0
  668. package/src/supply-chain/blueprint-trust-process.js.map +1 -0
  669. package/src/supply-chain/blueprint-trust.d.ts +354 -0
  670. package/src/supply-chain/blueprint-trust.js +1558 -0
  671. package/src/supply-chain/blueprint-trust.js.map +1 -0
  672. package/src/supply-chain/blueprint-verification-authority.d.ts +62 -0
  673. package/src/supply-chain/blueprint-verification-authority.js +588 -0
  674. package/src/supply-chain/blueprint-verification-authority.js.map +1 -0
  675. package/src/supply-chain/blueprint-verification-execution.d.ts +68 -0
  676. package/src/supply-chain/blueprint-verification-execution.js +624 -0
  677. package/src/supply-chain/blueprint-verification-execution.js.map +1 -0
  678. package/src/supply-chain/build-output.d.ts +35 -0
  679. package/src/supply-chain/build-output.js +398 -0
  680. package/src/supply-chain/build-output.js.map +1 -0
  681. package/src/supply-chain/common.d.ts +21 -0
  682. package/src/supply-chain/common.js +107 -0
  683. package/src/supply-chain/common.js.map +1 -0
  684. package/src/supply-chain/container-artifact-contract.d.ts +23 -0
  685. package/src/supply-chain/container-artifact-contract.js +208 -0
  686. package/src/supply-chain/container-artifact-contract.js.map +1 -0
  687. package/src/supply-chain/container-release.d.ts +119 -0
  688. package/src/supply-chain/container-release.js +496 -0
  689. package/src/supply-chain/container-release.js.map +1 -0
  690. package/src/supply-chain/ecr-applicability.d.ts +19 -0
  691. package/src/supply-chain/ecr-applicability.js +103 -0
  692. package/src/supply-chain/ecr-applicability.js.map +1 -0
  693. package/src/supply-chain/ecr-scanning.d.ts +83 -0
  694. package/src/supply-chain/ecr-scanning.js +321 -0
  695. package/src/supply-chain/ecr-scanning.js.map +1 -0
  696. package/src/supply-chain/ecs-deployed-image.d.ts +85 -0
  697. package/src/supply-chain/ecs-deployed-image.js +300 -0
  698. package/src/supply-chain/ecs-deployed-image.js.map +1 -0
  699. package/src/supply-chain/errors.d.ts +6 -0
  700. package/src/supply-chain/errors.js +15 -0
  701. package/src/supply-chain/errors.js.map +1 -0
  702. package/src/supply-chain/generated-blueprint-trust-command.d.ts +4 -0
  703. package/src/supply-chain/generated-blueprint-trust-command.js +1362 -0
  704. package/src/supply-chain/generated-blueprint-trust-command.js.map +1 -0
  705. package/src/supply-chain/generated-command.d.ts +1 -0
  706. package/src/supply-chain/generated-command.js +687 -0
  707. package/src/supply-chain/generated-command.js.map +1 -0
  708. package/src/supply-chain/generated-container-release-command.d.ts +16 -0
  709. package/src/supply-chain/generated-container-release-command.js +2087 -0
  710. package/src/supply-chain/generated-container-release-command.js.map +1 -0
  711. package/src/supply-chain/generated-ecr-collector-command.d.ts +21 -0
  712. package/src/supply-chain/generated-ecr-collector-command.js +1338 -0
  713. package/src/supply-chain/generated-ecr-collector-command.js.map +1 -0
  714. package/src/supply-chain/generated-files.d.ts +13 -0
  715. package/src/supply-chain/generated-files.js +401 -0
  716. package/src/supply-chain/generated-files.js.map +1 -0
  717. package/src/supply-chain/generated-scanner-command.d.ts +1 -0
  718. package/src/supply-chain/generated-scanner-command.js +718 -0
  719. package/src/supply-chain/generated-scanner-command.js.map +1 -0
  720. package/src/supply-chain/index.d.ts +28 -0
  721. package/src/supply-chain/index.js +35 -0
  722. package/src/supply-chain/index.js.map +1 -0
  723. package/src/supply-chain/inventory.d.ts +79 -0
  724. package/src/supply-chain/inventory.js +717 -0
  725. package/src/supply-chain/inventory.js.map +1 -0
  726. package/src/supply-chain/license-expression.d.ts +7 -0
  727. package/src/supply-chain/license-expression.js +128 -0
  728. package/src/supply-chain/license-expression.js.map +1 -0
  729. package/src/supply-chain/pnpm-toolchain-authority.d.ts +31 -0
  730. package/src/supply-chain/pnpm-toolchain-authority.js +316 -0
  731. package/src/supply-chain/pnpm-toolchain-authority.js.map +1 -0
  732. package/src/supply-chain/policy.d.ts +105 -0
  733. package/src/supply-chain/policy.js +301 -0
  734. package/src/supply-chain/policy.js.map +1 -0
  735. package/src/supply-chain/publication.d.ts +48 -0
  736. package/src/supply-chain/publication.js +85 -0
  737. package/src/supply-chain/publication.js.map +1 -0
  738. package/src/supply-chain/sbom.d.ts +228 -0
  739. package/src/supply-chain/sbom.js +826 -0
  740. package/src/supply-chain/sbom.js.map +1 -0
  741. package/src/supply-chain/scanner.d.ts +77 -0
  742. package/src/supply-chain/scanner.js +502 -0
  743. package/src/supply-chain/scanner.js.map +1 -0
  744. package/src/supply-chain/vendored-source.d.ts +95 -0
  745. package/src/supply-chain/vendored-source.js +315 -0
  746. package/src/supply-chain/vendored-source.js.map +1 -0
  747. package/src/testing-evidence/fixture-scope.d.ts +52 -0
  748. package/src/testing-evidence/fixture-scope.js +140 -0
  749. package/src/testing-evidence/fixture-scope.js.map +1 -0
  750. package/src/testing-evidence/index.d.ts +5 -0
  751. package/src/testing-evidence/index.js +9 -0
  752. package/src/testing-evidence/index.js.map +1 -0
  753. package/src/testing-evidence/quarantine.d.ts +48 -0
  754. package/src/testing-evidence/quarantine.js +253 -0
  755. package/src/testing-evidence/quarantine.js.map +1 -0
  756. package/src/testing-evidence/result.d.ts +108 -0
  757. package/src/testing-evidence/result.js +571 -0
  758. package/src/testing-evidence/result.js.map +1 -0
  759. package/src/testing-evidence/runner-adapter.d.ts +51 -0
  760. package/src/testing-evidence/runner-adapter.js +288 -0
  761. package/src/testing-evidence/runner-adapter.js.map +1 -0
  762. package/src/testing-evidence/sensitive-artifacts.d.ts +13 -0
  763. package/src/testing-evidence/sensitive-artifacts.js +93 -0
  764. package/src/testing-evidence/sensitive-artifacts.js.map +1 -0
  765. package/src/work-markers/work-markers.js +4 -3
  766. package/src/work-markers/work-markers.js.map +1 -1
  767. package/src/workload-roles/account-baseline.d.ts +132 -0
  768. package/src/workload-roles/account-baseline.js +743 -0
  769. package/src/workload-roles/account-baseline.js.map +1 -0
  770. package/src/workload-roles/authority.d.ts +38 -0
  771. package/src/workload-roles/authority.js +156 -0
  772. package/src/workload-roles/authority.js.map +1 -0
  773. package/src/workload-roles/aws-adapter.d.ts +99 -0
  774. package/src/workload-roles/aws-adapter.js +416 -0
  775. package/src/workload-roles/aws-adapter.js.map +1 -0
  776. package/src/workload-roles/aws-internal-handler.d.ts +27 -0
  777. package/src/workload-roles/aws-internal-handler.js +58 -0
  778. package/src/workload-roles/aws-internal-handler.js.map +1 -0
  779. package/src/workload-roles/aws-s3-state.d.ts +52 -0
  780. package/src/workload-roles/aws-s3-state.js +164 -0
  781. package/src/workload-roles/aws-s3-state.js.map +1 -0
  782. package/src/workload-roles/aws-sdk-control-plane.d.ts +12 -0
  783. package/src/workload-roles/aws-sdk-control-plane.js +291 -0
  784. package/src/workload-roles/aws-sdk-control-plane.js.map +1 -0
  785. package/src/workload-roles/boundary.d.ts +33 -0
  786. package/src/workload-roles/boundary.js +130 -0
  787. package/src/workload-roles/boundary.js.map +1 -0
  788. package/src/workload-roles/convergence.d.ts +66 -0
  789. package/src/workload-roles/convergence.js +186 -0
  790. package/src/workload-roles/convergence.js.map +1 -0
  791. package/src/workload-roles/effective-policy.d.ts +51 -0
  792. package/src/workload-roles/effective-policy.js +183 -0
  793. package/src/workload-roles/effective-policy.js.map +1 -0
  794. package/src/workload-roles/handler.d.ts +43 -0
  795. package/src/workload-roles/handler.js +130 -0
  796. package/src/workload-roles/handler.js.map +1 -0
  797. package/src/workload-roles/history.d.ts +47 -0
  798. package/src/workload-roles/history.js +324 -0
  799. package/src/workload-roles/history.js.map +1 -0
  800. package/src/workload-roles/index.d.ts +17 -0
  801. package/src/workload-roles/index.js +21 -0
  802. package/src/workload-roles/index.js.map +1 -0
  803. package/src/workload-roles/injection.d.ts +119 -0
  804. package/src/workload-roles/injection.js +495 -0
  805. package/src/workload-roles/injection.js.map +1 -0
  806. package/src/workload-roles/manifest.d.ts +54 -0
  807. package/src/workload-roles/manifest.js +343 -0
  808. package/src/workload-roles/manifest.js.map +1 -0
  809. package/src/workload-roles/provisioner.d.ts +70 -0
  810. package/src/workload-roles/provisioner.js +231 -0
  811. package/src/workload-roles/provisioner.js.map +1 -0
  812. package/src/workload-roles/registration.d.ts +101 -0
  813. package/src/workload-roles/registration.js +457 -0
  814. package/src/workload-roles/registration.js.map +1 -0
  815. package/src/workload-roles/stage-boundary.d.ts +31 -0
  816. package/src/workload-roles/stage-boundary.js +208 -0
  817. package/src/workload-roles/stage-boundary.js.map +1 -0
  818. package/src/workload-roles/standard-slice.d.ts +54 -0
  819. package/src/workload-roles/standard-slice.js +252 -0
  820. package/src/workload-roles/standard-slice.js.map +1 -0
  821. package/migrations/standard-pre-v1-to-v1.md +0 -11
  822. package/src/foundation/aurora-logical-database.d.ts +0 -76
  823. package/src/foundation/aurora-logical-database.js +0 -419
  824. package/src/foundation/aurora-logical-database.js.map +0 -1
@@ -0,0 +1,2604 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderProductDataRetentionJobFiles = renderProductDataRetentionJobFiles;
4
+ const built_in_plan_shared_js_1 = require("./built-in-plan-shared.js");
5
+ const frozen_container_js_1 = require("../workload/frozen-container.js");
6
+ /** @internal */
7
+ function renderProductDataRetentionJobFiles(name, runtimeVersion) {
8
+ const pascalName = name
9
+ .split('-')
10
+ .map((segment) => segment[0]?.toUpperCase() + segment.slice(1))
11
+ .join('');
12
+ const identifierPrefix = pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
13
+ return {
14
+ 'data/retention-control.ts': `import {
15
+ createApprovalRequest,
16
+ decideApprovalRequest,
17
+ digestApprovalRequest,
18
+ type ApprovalRequest,
19
+ type AuthorizationPrincipal,
20
+ } from '@empire-builder-kit/runtime/authorization';
21
+ import {
22
+ createDataLifecyclePseudonymousReference,
23
+ } from '@empire-builder-kit/runtime/data-lifecycle';
24
+ import type { Client as PgClient } from 'pg';
25
+ import {
26
+ ${identifierPrefix}AuthorizationPolicy,
27
+ } from '../authorization/policy.js';
28
+ import { requireDirectAdmin } from './data-rights-shared.js';
29
+ import { approvalReference } from './pseudonymous-reference.js';
30
+ import {
31
+ ${pascalName}DataLifecycleRuntimePolicy,
32
+ } from './runtime-policy.cjs';
33
+ import { withPrimaryDatabase } from '../database/runtime-client.js';
34
+
35
+ const ACTION = '${name}:retention-apply';
36
+ const POLICY_VERSION =
37
+ ${pascalName}DataLifecycleRuntimePolicy.policyVersion;
38
+ const RETENTION_DAYS =
39
+ ${pascalName}DataLifecycleRuntimePolicy.retentionDays;
40
+ const AUTHORIZATION_POLICY_VERSION =
41
+ ${identifierPrefix}AuthorizationPolicy.version;
42
+
43
+ export interface ${pascalName}RetentionPlanV1 {
44
+ asOf: string;
45
+ candidateDigest: string;
46
+ categoryId: 'product-record';
47
+ due: number;
48
+ locationId: 'primary-database';
49
+ maxItems: 10000;
50
+ pageSize: 100;
51
+ policyVersion: string;
52
+ retentionDays: number;
53
+ runId: string;
54
+ }
55
+
56
+ interface RetentionReportRow {
57
+ approvalRequestId: string | null;
58
+ plan: ${pascalName}RetentionPlanV1;
59
+ revision: number;
60
+ status: string;
61
+ }
62
+
63
+ function plusDays(value: string, days: number): string {
64
+ const result = new Date(value);
65
+ result.setUTCDate(result.getUTCDate() + days);
66
+ return result.toISOString();
67
+ }
68
+
69
+ function runReference(runId: string): string {
70
+ return createDataLifecyclePseudonymousReference({
71
+ categoryId: 'lifecycle-control-metadata',
72
+ slice: '${name}',
73
+ value: runId,
74
+ });
75
+ }
76
+
77
+ function requirePlan(
78
+ value: ${pascalName}RetentionPlanV1,
79
+ runId: string,
80
+ ): ${pascalName}RetentionPlanV1 {
81
+ if (
82
+ value?.runId !== runId ||
83
+ value.policyVersion !== POLICY_VERSION ||
84
+ value.categoryId !== 'product-record' ||
85
+ value.locationId !== 'primary-database' ||
86
+ value.maxItems !== 10000 ||
87
+ value.pageSize !== 100 ||
88
+ value.retentionDays !== RETENTION_DAYS ||
89
+ !Number.isSafeInteger(value.due) ||
90
+ value.due < 0 ||
91
+ !/^sha256:[a-f0-9]{64}$/.test(value.candidateDigest)
92
+ ) {
93
+ throw new Error('Retention report plan is invalid.');
94
+ }
95
+ return value;
96
+ }
97
+
98
+ async function loadReport(
99
+ client: PgClient,
100
+ runId: string,
101
+ ): Promise<RetentionReportRow> {
102
+ const result = await client.query<{
103
+ approvalRequestId: string | null;
104
+ plan: ${pascalName}RetentionPlanV1;
105
+ revision: number;
106
+ status: string;
107
+ }>(
108
+ \`SELECT approval_request_id::text AS "approvalRequestId",
109
+ plan_json AS plan, revision, status
110
+ FROM data_lifecycle_retention_runs
111
+ WHERE id = $1::uuid
112
+ FOR UPDATE\`,
113
+ [runId],
114
+ );
115
+ const row = result.rows[0];
116
+ if (!row) throw new Error('Retention report was not found.');
117
+ return { ...row, plan: requirePlan(row.plan, runId) };
118
+ }
119
+
120
+ async function writeAudit(
121
+ client: PgClient,
122
+ input: {
123
+ action: string;
124
+ approvalRequestId: string;
125
+ correlationId: string;
126
+ principal: AuthorizationPrincipal;
127
+ reasonCategory: string;
128
+ runId: string;
129
+ },
130
+ ) {
131
+ await client.query(
132
+ \`INSERT INTO authorization_audit
133
+ (action, approval_reference, authority_mode, correlation_id, decision,
134
+ delegation_reference, policy_version, principal_kind,
135
+ pseudonymous_subject_id, reason_category, resource_reference,
136
+ resource_type, slice, stage, subject_type)
137
+ VALUES ($1, $2, 'direct', $3, 'allow', NULL, $4, $5, $6, $7, $8,
138
+ 'retention-run', $9, $10, $11)\`,
139
+ [
140
+ input.action,
141
+ approvalReference(input.approvalRequestId),
142
+ input.correlationId,
143
+ AUTHORIZATION_POLICY_VERSION,
144
+ input.principal.kind,
145
+ createDataLifecyclePseudonymousReference({
146
+ categoryId: 'authorization-subject',
147
+ slice: '${name}',
148
+ value: input.principal.actor.id,
149
+ }),
150
+ input.reasonCategory,
151
+ runReference(input.runId),
152
+ '${name}',
153
+ process.env.EBK_STAGE ?? 'development',
154
+ input.principal.subject.type,
155
+ ],
156
+ );
157
+ }
158
+
159
+ export async function request${pascalName}RetentionApproval(input: {
160
+ correlationId: string;
161
+ now: string;
162
+ principal: AuthorizationPrincipal;
163
+ runId: string;
164
+ }) {
165
+ requireDirectAdmin(input.principal);
166
+ return withPrimaryDatabase(
167
+ { correlationId: input.correlationId },
168
+ 'api-database-records',
169
+ async (client) => {
170
+ await client.query('BEGIN');
171
+ try {
172
+ const report = await loadReport(client, input.runId);
173
+ if (
174
+ report.status !== 'reported' ||
175
+ report.approvalRequestId !== null
176
+ ) {
177
+ throw new Error(
178
+ 'Retention report already has an approval request or is no longer reviewable.',
179
+ );
180
+ }
181
+ const approval = createApprovalRequest({
182
+ action: ACTION,
183
+ author: input.principal,
184
+ command: report.plan,
185
+ expiresAt: plusDays(input.now, 1),
186
+ now: input.now,
187
+ policyVersion: AUTHORIZATION_POLICY_VERSION,
188
+ resource: { id: input.runId, type: 'retention-run' },
189
+ slice: '${name}',
190
+ });
191
+ await client.query(
192
+ \`INSERT INTO approval_requests
193
+ (id, author_reference, expires_at, policy_version,
194
+ request_digest, request_json, revision, status)
195
+ VALUES ($1::uuid, $2, $3, $4, $5, $6::jsonb, $7, $8)\`,
196
+ [
197
+ approval.id,
198
+ approval.author.effectiveHumanId ?? approval.author.actorId,
199
+ approval.expiresAt,
200
+ approval.policyVersion,
201
+ approval.requestDigest,
202
+ JSON.stringify(approval),
203
+ approval.revision,
204
+ approval.status,
205
+ ],
206
+ );
207
+ const updated = await client.query(
208
+ \`UPDATE data_lifecycle_retention_runs
209
+ SET approval_request_id = $3::uuid, revision = revision + 1,
210
+ status = 'approval-pending', updated_at = $4
211
+ WHERE id = $1::uuid AND revision = $2
212
+ AND approval_request_id IS NULL AND status = 'reported'\`,
213
+ [input.runId, report.revision, approval.id, input.now],
214
+ );
215
+ if (updated.rowCount !== 1) {
216
+ throw new Error('Retention approval request lost compare-and-set.');
217
+ }
218
+ await writeAudit(client, {
219
+ action: '${name}:retention-approval-request',
220
+ approvalRequestId: approval.id,
221
+ correlationId: input.correlationId,
222
+ principal: input.principal,
223
+ reasonCategory: 'approval-pending',
224
+ runId: input.runId,
225
+ });
226
+ await client.query('COMMIT');
227
+ return {
228
+ approvalRequestId: approval.id,
229
+ expiresAt: approval.expiresAt,
230
+ requestDigest: approval.requestDigest,
231
+ status: approval.status,
232
+ };
233
+ } catch (error) {
234
+ await client.query('ROLLBACK').catch(() => undefined);
235
+ throw error;
236
+ }
237
+ },
238
+ );
239
+ }
240
+
241
+ export async function decide${pascalName}RetentionApproval(input: {
242
+ approvalRequestId: string;
243
+ correlationId: string;
244
+ expectedRequestDigest: string;
245
+ now: string;
246
+ outcome: 'approved' | 'rejected';
247
+ principal: AuthorizationPrincipal;
248
+ reasonReference: string;
249
+ runId: string;
250
+ }) {
251
+ requireDirectAdmin(input.principal);
252
+ return withPrimaryDatabase(
253
+ { correlationId: input.correlationId },
254
+ 'api-database-records',
255
+ async (client) => {
256
+ await client.query('BEGIN');
257
+ try {
258
+ const report = await loadReport(client, input.runId);
259
+ if (
260
+ report.status !== 'approval-pending' ||
261
+ report.approvalRequestId !== input.approvalRequestId
262
+ ) {
263
+ throw new Error('Retention approval does not match the report.');
264
+ }
265
+ const stored = await client.query<{
266
+ request: ApprovalRequest<string>;
267
+ }>(
268
+ \`SELECT request_json AS request
269
+ FROM approval_requests
270
+ WHERE id = $1::uuid
271
+ FOR UPDATE\`,
272
+ [input.approvalRequestId],
273
+ );
274
+ const approval = stored.rows[0]?.request;
275
+ if (
276
+ !approval ||
277
+ approval.id !== input.approvalRequestId ||
278
+ approval.action !== ACTION ||
279
+ approval.policyVersion !== AUTHORIZATION_POLICY_VERSION ||
280
+ approval.resource.id !== input.runId ||
281
+ approval.resource.type !== 'retention-run' ||
282
+ approval.schemaVersion !== 1 ||
283
+ approval.slice !== '${name}' ||
284
+ digestApprovalRequest({
285
+ action: ACTION,
286
+ command: report.plan,
287
+ resource: { id: input.runId, type: 'retention-run' },
288
+ }) !== approval.requestDigest
289
+ ) {
290
+ throw new Error('Retention approval is not bound to the report.');
291
+ }
292
+ const transition = decideApprovalRequest(approval, {
293
+ approver: input.principal,
294
+ expectedRequestDigest: input.expectedRequestDigest,
295
+ now: input.now,
296
+ outcome: input.outcome,
297
+ reasonReference: input.reasonReference,
298
+ requiredPolicyVersion: AUTHORIZATION_POLICY_VERSION,
299
+ });
300
+ if (transition.decision.decision !== 'allow') {
301
+ throw new Error(
302
+ 'Retention approval decision was denied: ' +
303
+ transition.decision.category,
304
+ );
305
+ }
306
+ const approvalUpdated = await client.query(
307
+ \`UPDATE approval_requests
308
+ SET request_json = $3::jsonb, revision = $4, status = $5
309
+ WHERE id = $1::uuid AND revision = $2\`,
310
+ [
311
+ approval.id,
312
+ approval.revision,
313
+ JSON.stringify(transition.request),
314
+ transition.request.revision,
315
+ transition.request.status,
316
+ ],
317
+ );
318
+ const reportUpdated = await client.query(
319
+ \`UPDATE data_lifecycle_retention_runs
320
+ SET revision = revision + 1, status = $3, updated_at = $4
321
+ WHERE id = $1::uuid AND revision = $2
322
+ AND status = 'approval-pending'\`,
323
+ [
324
+ input.runId,
325
+ report.revision,
326
+ transition.request.status === 'approved'
327
+ ? 'approval-approved'
328
+ : 'approval-rejected',
329
+ input.now,
330
+ ],
331
+ );
332
+ if (
333
+ approvalUpdated.rowCount !== 1 ||
334
+ reportUpdated.rowCount !== 1
335
+ ) {
336
+ throw new Error('Retention approval decision lost compare-and-set.');
337
+ }
338
+ await writeAudit(client, {
339
+ action: '${name}:retention-approval-decision',
340
+ approvalRequestId: approval.id,
341
+ correlationId: input.correlationId,
342
+ principal: input.principal,
343
+ reasonCategory:
344
+ transition.request.status === 'approved'
345
+ ? 'approval-approved'
346
+ : 'approval-rejected',
347
+ runId: input.runId,
348
+ });
349
+ await client.query('COMMIT');
350
+ return {
351
+ approvalRequestId: approval.id,
352
+ status: transition.request.status as 'approved' | 'rejected',
353
+ };
354
+ } catch (error) {
355
+ await client.query('ROLLBACK').catch(() => undefined);
356
+ throw error;
357
+ }
358
+ },
359
+ );
360
+ }
361
+ `,
362
+ 'jobs/data-retention/src/approval-compatibility.spec.ts': `import {
363
+ beginApprovalExecution,
364
+ createApprovalRequest,
365
+ decideApprovalRequest,
366
+ type AuthorizationPrincipal,
367
+ } from '@empire-builder-kit/runtime/authorization';
368
+ import { describe, expect, it } from 'vitest';
369
+
370
+ function human(id: string, kind: 'admin' | 'customer') {
371
+ return {
372
+ actor: { id, type: 'user' as const },
373
+ authority: { mode: 'direct' as const },
374
+ kind,
375
+ roles: kind === 'admin' ? ['${name}-admin'] : [],
376
+ scopes: [],
377
+ subject: { id, type: 'user' as const },
378
+ } satisfies AuthorizationPrincipal;
379
+ }
380
+
381
+ describe('${pascalName} retention approval compatibility', () => {
382
+ it('denies stale apply and stale exact-retry policy versions', () => {
383
+ const runId = '00000000-0000-4000-8000-000000000001';
384
+ const plan = {
385
+ asOf: '2026-07-27T12:00:00.000Z',
386
+ candidateDigest: 'sha256:' + 'a'.repeat(64),
387
+ categoryId: 'product-record',
388
+ due: 1,
389
+ locationId: 'primary-database',
390
+ maxItems: 10000,
391
+ pageSize: 100,
392
+ policyVersion: '1.0.0',
393
+ retentionDays: 365,
394
+ runId,
395
+ };
396
+ const pending = createApprovalRequest({
397
+ action: '${name}:retention-apply',
398
+ author: human('report-author', 'admin'),
399
+ command: plan,
400
+ expiresAt: '2026-07-27T13:00:00.000Z',
401
+ now: '2026-07-27T12:00:00.000Z',
402
+ policyVersion: '1.0.0',
403
+ resource: { id: runId, type: 'retention-run' },
404
+ slice: '${name}',
405
+ });
406
+ expect(
407
+ decideApprovalRequest(pending, {
408
+ approver: human('privacy-reviewer', 'admin'),
409
+ expectedRequestDigest: pending.requestDigest,
410
+ now: '2026-07-27T12:05:00.000Z',
411
+ outcome: 'approved',
412
+ reasonReference: 'privacy-review-stale',
413
+ requiredPolicyVersion: '2.0.0',
414
+ }),
415
+ ).toMatchObject({
416
+ decision: {
417
+ category: 'policy-stale',
418
+ decision: 'deny',
419
+ policyVersion: '2.0.0',
420
+ },
421
+ request: pending,
422
+ });
423
+ const approved = decideApprovalRequest(pending, {
424
+ approver: human('privacy-reviewer', 'admin'),
425
+ expectedRequestDigest: pending.requestDigest,
426
+ now: '2026-07-27T12:05:00.000Z',
427
+ outcome: 'approved',
428
+ reasonReference: 'privacy-review-1',
429
+ requiredPolicyVersion: '1.0.0',
430
+ }).request;
431
+ const input = {
432
+ action: '${name}:retention-apply',
433
+ command: plan,
434
+ executionId: runId,
435
+ now: '2026-07-27T12:06:00.000Z',
436
+ resource: { id: runId, type: 'retention-run' },
437
+ };
438
+
439
+ expect(
440
+ beginApprovalExecution(approved, {
441
+ ...input,
442
+ requiredPolicyVersion: '2.0.0',
443
+ }),
444
+ ).toMatchObject({
445
+ decision: {
446
+ category: 'policy-stale',
447
+ decision: 'deny',
448
+ policyVersion: '2.0.0',
449
+ },
450
+ request: { status: 'approved' },
451
+ });
452
+ const begun = beginApprovalExecution(approved, {
453
+ ...input,
454
+ requiredPolicyVersion: '1.0.0',
455
+ });
456
+ expect(begun.decision).toMatchObject({ decision: 'allow' });
457
+ expect(
458
+ beginApprovalExecution(begun.request, {
459
+ ...input,
460
+ now: '2026-07-27T12:07:00.000Z',
461
+ requiredPolicyVersion: '2.0.0',
462
+ }),
463
+ ).toMatchObject({
464
+ decision: {
465
+ category: 'policy-stale',
466
+ decision: 'deny',
467
+ policyVersion: '2.0.0',
468
+ },
469
+ request: begun.request,
470
+ });
471
+ });
472
+ });
473
+ `,
474
+ 'jobs/data-retention/package.json': (0, built_in_plan_shared_js_1.json)({
475
+ name: `@${name}/job-data-retention`,
476
+ private: true,
477
+ type: 'module',
478
+ version: '0.0.0',
479
+ scripts: {
480
+ build: 'tsc -p tsconfig.build.json',
481
+ 'docker-build': `docker build -f packages/${name}/jobs/data-retention/Dockerfile -t ${name}-data-retention:local ../../../..`,
482
+ lint: 'eslint .',
483
+ test: 'vitest run',
484
+ typecheck: 'tsc --noEmit -p tsconfig.json',
485
+ verify: 'pnpm run lint && pnpm run typecheck && pnpm test && pnpm run build',
486
+ },
487
+ dependencies: {
488
+ '@aws-sdk/client-eventbridge': built_in_plan_shared_js_1.PRODUCT_VERSIONS.awsEventBridge,
489
+ '@aws-sdk/client-sfn': built_in_plan_shared_js_1.PRODUCT_VERSIONS.awsSfn,
490
+ '@empire-builder-kit/runtime': runtimeVersion,
491
+ pg: built_in_plan_shared_js_1.PRODUCT_VERSIONS.pg,
492
+ },
493
+ devDependencies: {
494
+ '@types/node': built_in_plan_shared_js_1.PRODUCT_VERSIONS.typesNode,
495
+ '@types/pg': built_in_plan_shared_js_1.PRODUCT_VERSIONS.typesPg,
496
+ typescript: '~5.9.2',
497
+ vitest: built_in_plan_shared_js_1.PRODUCT_VERSIONS.vitest,
498
+ },
499
+ }),
500
+ 'jobs/data-retention/tsconfig.json': (0, built_in_plan_shared_js_1.json)({
501
+ compilerOptions: {
502
+ forceConsistentCasingInFileNames: true,
503
+ module: 'nodenext',
504
+ moduleResolution: 'nodenext',
505
+ noEmitOnError: true,
506
+ outDir: 'dist',
507
+ resolveJsonModule: true,
508
+ rootDir: '../..',
509
+ strict: true,
510
+ target: 'es2022',
511
+ types: ['node'],
512
+ },
513
+ include: [
514
+ 'src/**/*.ts',
515
+ '../../authorization/policy.ts',
516
+ '../../contracts/events.ts',
517
+ '../../data/lifecycle-event.ts',
518
+ '../../data/policy.json',
519
+ '../../data/pseudonymous-reference.ts',
520
+ '../../data/runtime-policy.cts',
521
+ '../../secrets/declarations.ts',
522
+ ],
523
+ }),
524
+ 'jobs/data-retention/tsconfig.build.json': (0, built_in_plan_shared_js_1.json)({
525
+ extends: './tsconfig.json',
526
+ exclude: ['src/**/*.spec.ts'],
527
+ }),
528
+ 'jobs/data-retention/vitest.config.ts': `import { configDefaults, defineConfig } from 'vitest/config';
529
+
530
+ export default defineConfig({
531
+ test: {
532
+ environment: 'node',
533
+ exclude: [...configDefaults.exclude, '**/dist/**'],
534
+ },
535
+ });
536
+ `,
537
+ 'jobs/data-retention/Dockerfile': `FROM ${frozen_container_js_1.WORKLOAD_CONTAINER_NODE_IMAGE} AS build
538
+ ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org
539
+ ENV COREPACK_NPM_REGISTRY=$NPM_CONFIG_REGISTRY
540
+ ENV NPM_CONFIG_REGISTRY=$NPM_CONFIG_REGISTRY
541
+ ENV PNPM_HOME=/pnpm
542
+ ENV PATH=/pnpm:$PATH
543
+ WORKDIR /workspace
544
+ RUN corepack enable && corepack prepare ${frozen_container_js_1.WORKLOAD_CONTAINER_PNPM_DESCRIPTOR} --activate
545
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
546
+ RUN pnpm fetch --frozen-lockfile
547
+ COPY packages/${name}/jobs/data-retention/package.json packages/${name}/jobs/data-retention/package.json
548
+ RUN pnpm --filter @${name}/job-data-retention install --offline --frozen-lockfile
549
+ COPY packages/${name}/jobs/data-retention/tsconfig.json packages/${name}/jobs/data-retention/tsconfig.build.json packages/${name}/jobs/data-retention/
550
+ COPY packages/${name}/jobs/data-retention/src packages/${name}/jobs/data-retention/src
551
+ COPY packages/${name}/authorization/policy.ts packages/${name}/authorization/policy.ts
552
+ COPY packages/${name}/contracts/events.ts packages/${name}/contracts/events.ts
553
+ COPY packages/${name}/data/lifecycle-event.ts packages/${name}/data/lifecycle-event.ts
554
+ COPY packages/${name}/data/policy.json packages/${name}/data/policy.json
555
+ COPY packages/${name}/data/pseudonymous-reference.ts packages/${name}/data/pseudonymous-reference.ts
556
+ COPY packages/${name}/data/runtime-policy.cts packages/${name}/data/runtime-policy.cts
557
+ COPY packages/${name}/secrets/declarations.ts packages/${name}/secrets/declarations.ts
558
+ RUN pnpm --filter @${name}/job-data-retention run build
559
+ RUN --network=none pnpm --config.inject-workspace-packages=true --config.store-dir=/workspace/.ebk/cache/pnpm-store --filter @${name}/job-data-retention deploy --prod --offline --frozen-lockfile /opt/data-retention
560
+
561
+ FROM ${frozen_container_js_1.WORKLOAD_CONTAINER_NODE_IMAGE}
562
+ WORKDIR /app
563
+ ADD --checksum=sha256:e5bb2084ccf45087bda1c9bffdea0eb15ee67f0b91646106e466714f9de3c7e3 https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem ./rds-global-bundle.pem
564
+ COPY --from=build --chown=node:node /opt/data-retention ./
565
+ USER node
566
+ CMD ["node", "dist/jobs/data-retention/src/run.js"]
567
+ `,
568
+ 'jobs/data-retention/README.md': `# Data retention Job
569
+
570
+ This is the one generated Standard data-retention Job. The Slice target starts
571
+ its fixed AWS-internal Step Functions workflow; operators never run this image
572
+ or connect to the database directly.
573
+
574
+ Report scans at most 100 pages of 100 records, freezes the exact candidate
575
+ set, and returns its safe run reference without mutating product data or
576
+ creating an approval. The authenticated API creates the exact durable request;
577
+ apply requires approval by a different direct human. The Job claims approval
578
+ with compare-and-set, persists its checkpoint after every page, uses stable
579
+ per-record idempotency, and resumes only the same execution identity.
580
+
581
+ If any candidate changes after the approved scan, apply fails closed. Preserve
582
+ the frozen candidate set, create a new report, and obtain a new non-self
583
+ approval. Never delete or rewrite the changed row to make the old approval
584
+ appear current.
585
+
586
+ Control-row expiry is not part of an approved retention plan. Infrastructure
587
+ must invoke \`dist/jobs/data-retention/src/maintenance.js\` on an independent
588
+ bounded schedule with \`EBK_MAINTENANCE_CORRELATION_ID\`; that entrypoint uses
589
+ the exact \`${name}:retention-maintenance\` action and fails closed on active
590
+ control-metadata holds.
591
+
592
+ The seeded adapter owns the Product Slice's record selection. Keep it aligned
593
+ with \`data/policy.json\`; never log or return raw identifiers or record
594
+ content.
595
+ `,
596
+ 'jobs/data-retention/src/adapter.ts': `import { createHash } from 'node:crypto';
597
+ import {
598
+ createDataLifecycleLedgerEntry,
599
+ evaluateDataLifecycleHolds,
600
+ type DataLifecycleRetentionAdapter,
601
+ type DataLifecycleRuntimeHoldV1,
602
+ } from '@empire-builder-kit/runtime/data-lifecycle';
603
+ import { buildEventEntry } from '@empire-builder-kit/runtime/events';
604
+ import type { Client } from 'pg';
605
+ import {
606
+ ${pascalName}DataLifecycleChanged,
607
+ } from '../../../contracts/events.js';
608
+ import {
609
+ create${pascalName}DataLifecycleChanged,
610
+ } from '../../../data/lifecycle-event.js';
611
+ import {
612
+ approvalReference,
613
+ productRecordReference,
614
+ } from '../../../data/pseudonymous-reference.js';
615
+ import {
616
+ ${pascalName}DataLifecycleRuntimePolicy,
617
+ } from '../../../data/runtime-policy.cjs';
618
+
619
+ const OUTBOX_COPY_ID = 'event-contract';
620
+ const POLICY_VERSION =
621
+ ${pascalName}DataLifecycleRuntimePolicy.policyVersion;
622
+ const RETENTION_DAYS =
623
+ ${pascalName}DataLifecycleRuntimePolicy.retentionDays;
624
+
625
+ interface RetentionLifecycleEventEntry {
626
+ Detail: string;
627
+ DetailType: string;
628
+ EventBusName: string;
629
+ Source: string;
630
+ }
631
+
632
+ function ledgerExpiry(completedAt: string): string {
633
+ const expiresAt = new Date(completedAt);
634
+ expiresAt.setUTCDate(expiresAt.getUTCDate() + 400);
635
+ return expiresAt.toISOString();
636
+ }
637
+
638
+ export interface ${pascalName}RetentionAdapterOptions {
639
+ approvalRequestId?: string;
640
+ asOf: string;
641
+ client: Client;
642
+ completedAt: string;
643
+ correlationId: string;
644
+ eventBusName: string;
645
+ mode: 'apply' | 'report';
646
+ publishLifecycleEvent(
647
+ entry: RetentionLifecycleEventEntry,
648
+ ): Promise<void>;
649
+ runId: string;
650
+ stage: string;
651
+ }
652
+
653
+ export interface ${pascalName}RetentionCandidateRecord {
654
+ candidateOrder: string;
655
+ dueAt: string;
656
+ recordId: string;
657
+ recordRef: string;
658
+ }
659
+
660
+ /**
661
+ * Product-specific retention selection is seeded. Raw identifiers remain
662
+ * inside this AWS-internal Job and never enter its checkpoint or evidence.
663
+ */
664
+ export function create${pascalName}RetentionAdapter(
665
+ options: ${pascalName}RetentionAdapterOptions,
666
+ ): DataLifecycleRetentionAdapter & {
667
+ candidateDigest(): string;
668
+ candidateRecords(): readonly ${pascalName}RetentionCandidateRecord[];
669
+ } {
670
+ const recordIds = new Map<string, string>();
671
+ const candidateRecords = new Map<
672
+ string,
673
+ ${pascalName}RetentionCandidateRecord
674
+ >();
675
+ const digest = createHash('sha256');
676
+ return {
677
+ candidateDigest() {
678
+ return 'sha256:' + digest.copy().digest('hex');
679
+ },
680
+ candidateRecords() {
681
+ return [...candidateRecords.values()];
682
+ },
683
+ async scan({ asOf, cursor, limit }) {
684
+ const result = await options.client.query<{
685
+ cursorKey: string;
686
+ dueAt: Date;
687
+ id: string;
688
+ }>(
689
+ options.mode === 'report'
690
+ ? \`SELECT md5('${name}\\0' || id::text) AS "cursorKey",
691
+ last_activity_at + ($4::int * INTERVAL '1 day') AS "dueAt",
692
+ id::text
693
+ FROM records
694
+ WHERE last_activity_at + ($4::int * INTERVAL '1 day')
695
+ <= $1::timestamptz
696
+ AND test_expires_at IS NULL
697
+ AND ($2::text IS NULL OR md5('${name}\\0' || id::text) > $2)
698
+ ORDER BY md5('${name}\\0' || id::text)
699
+ LIMIT $3\`
700
+ : \`SELECT candidate_order AS "cursorKey", due_at AS "dueAt",
701
+ record_id::text AS id
702
+ FROM data_lifecycle_retention_candidates
703
+ WHERE run_id = $1::uuid
704
+ AND ($2::text IS NULL OR candidate_order > $2)
705
+ ORDER BY candidate_order
706
+ LIMIT $3\`,
707
+ options.mode === 'report'
708
+ ? [asOf, cursor ?? null, limit + 1, RETENTION_DAYS]
709
+ : [options.runId, cursor ?? null, limit + 1],
710
+ );
711
+ const pageRows = result.rows.slice(0, limit);
712
+ const items = pageRows.map((row) => {
713
+ const recordRef = productRecordReference(row.id);
714
+ recordIds.set(recordRef, row.id);
715
+ const dueAt = row.dueAt.toISOString();
716
+ candidateRecords.set(recordRef, {
717
+ candidateOrder: row.cursorKey,
718
+ dueAt,
719
+ recordId: row.id,
720
+ recordRef,
721
+ });
722
+ const candidate = {
723
+ categoryId: 'product-record',
724
+ dueAt,
725
+ locationId: 'primary-database',
726
+ recordRef,
727
+ };
728
+ digest.update(
729
+ [
730
+ candidate.categoryId,
731
+ candidate.dueAt,
732
+ candidate.locationId,
733
+ candidate.recordRef,
734
+ ].join('\\0') + '\\n',
735
+ 'utf8',
736
+ );
737
+ return candidate;
738
+ });
739
+ return {
740
+ items,
741
+ ...(result.rows.length > limit
742
+ ? { nextCursor: pageRows.at(-1)?.cursorKey }
743
+ : {}),
744
+ };
745
+ },
746
+ async apply({ candidate, idempotencyKey }) {
747
+ const recordId = recordIds.get(candidate.recordRef);
748
+ if (!recordId) {
749
+ throw new Error(
750
+ 'Retention apply must use a candidate from this bounded adapter scan.',
751
+ );
752
+ }
753
+ const ledger = createDataLifecycleLedgerEntry({
754
+ action: 'deletion',
755
+ categoryId: candidate.categoryId,
756
+ effectiveAt: options.completedAt,
757
+ expiresAt: ledgerExpiry(options.completedAt),
758
+ policyVersion: POLICY_VERSION,
759
+ pseudonymousKey: candidate.recordRef,
760
+ });
761
+ const requestRef = approvalReference(
762
+ options.approvalRequestId ?? options.runId + ':' + idempotencyKey,
763
+ );
764
+ const lifecycleEvent = create${pascalName}DataLifecycleChanged({
765
+ action: 'deletion',
766
+ occurredAt: options.completedAt,
767
+ policyVersion: POLICY_VERSION,
768
+ recordId,
769
+ requestRef,
770
+ });
771
+ const built = buildEventEntry(
772
+ ${pascalName}DataLifecycleChanged,
773
+ lifecycleEvent,
774
+ {
775
+ busName: options.eventBusName,
776
+ context: { correlationId: options.correlationId },
777
+ eventId: idempotencyKey,
778
+ occurredAt: options.completedAt,
779
+ stage: options.stage,
780
+ },
781
+ );
782
+ let event: RetentionLifecycleEventEntry = {
783
+ Detail: built.entry.detail,
784
+ DetailType: built.entry.detailType,
785
+ EventBusName: built.entry.eventBusName,
786
+ Source: built.entry.source,
787
+ };
788
+ let deletedNow = false;
789
+ let publishedAt: Date | null = null;
790
+ let copyStatus: string | undefined;
791
+ await options.client.query('BEGIN');
792
+ try {
793
+ const record = await options.client.query<{
794
+ due: boolean;
795
+ }>(
796
+ \`SELECT
797
+ last_activity_at + ($3::int * INTERVAL '1 day')
798
+ <= $2::timestamptz
799
+ AS due
800
+ FROM records
801
+ WHERE id = $1::uuid
802
+ FOR UPDATE\`,
803
+ [recordId, options.asOf, RETENTION_DAYS],
804
+ );
805
+ const existing = await options.client.query<{
806
+ eventJson: RetentionLifecycleEventEntry;
807
+ publishedAt: Date | null;
808
+ }>(
809
+ \`SELECT event_json AS "eventJson", published_at AS "publishedAt"
810
+ FROM data_lifecycle_outbox
811
+ WHERE idempotency_key = $1
812
+ FOR UPDATE\`,
813
+ [idempotencyKey],
814
+ );
815
+ const copy = await options.client.query<{ status: string }>(
816
+ \`SELECT status
817
+ FROM data_lifecycle_downstream_copies
818
+ WHERE source_slice = $1
819
+ AND pseudonymous_key = $2
820
+ AND copy_id = $3
821
+ FOR UPDATE\`,
822
+ ['${name}', candidate.recordRef, OUTBOX_COPY_ID],
823
+ );
824
+ publishedAt = existing.rows[0]?.publishedAt ?? null;
825
+ copyStatus = copy.rows[0]?.status;
826
+ if (record.rows[0]) {
827
+ if (!record.rows[0].due) {
828
+ throw new Error(
829
+ 'Retention candidate changed after its approved scan.',
830
+ );
831
+ }
832
+ if (existing.rowCount !== 0 || copy.rowCount !== 0) {
833
+ throw new Error(
834
+ 'Retention propagation state exists for an undeleted candidate.',
835
+ );
836
+ }
837
+ const holdRows = await options.client.query<{
838
+ holdJson: DataLifecycleRuntimeHoldV1;
839
+ }>(
840
+ \`SELECT hold_json AS "holdJson"
841
+ FROM data_lifecycle_holds
842
+ WHERE category_id = 'product-record'
843
+ AND scope_ref = $1
844
+ AND status = 'active'
845
+ ORDER BY hold_id
846
+ FOR SHARE\`,
847
+ [candidate.recordRef],
848
+ );
849
+ const hold = evaluateDataLifecycleHolds({
850
+ categoryId: candidate.categoryId,
851
+ holds: [
852
+ ...${pascalName}DataLifecycleRuntimePolicy.declaredHolds,
853
+ ...holdRows.rows.map(({ holdJson }) => holdJson),
854
+ ],
855
+ now: options.completedAt,
856
+ operation: 'deletion',
857
+ scopeRef: candidate.recordRef,
858
+ });
859
+ if (hold.blocked) {
860
+ throw new Error(
861
+ 'Retention candidate is protected by an active hold.',
862
+ );
863
+ }
864
+ } else {
865
+ if (existing.rowCount !== 1 || copy.rowCount !== 1) {
866
+ throw new Error(
867
+ 'Retention candidate disappeared without durable propagation state.',
868
+ );
869
+ }
870
+ event = existing.rows[0].eventJson;
871
+ }
872
+ const recorded = await options.client.query(
873
+ \`INSERT INTO data_lifecycle_ledger
874
+ (entry_id, action, category_id, effective_at, expires_at,
875
+ idempotency_key, policy_version, pseudonymous_key)
876
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
877
+ ON CONFLICT (idempotency_key) DO NOTHING
878
+ RETURNING entry_id\`,
879
+ [
880
+ ledger.entryId,
881
+ ledger.action,
882
+ ledger.categoryId,
883
+ ledger.effectiveAt,
884
+ ledger.expiresAt,
885
+ idempotencyKey,
886
+ ledger.policyVersion,
887
+ ledger.pseudonymousKey,
888
+ ],
889
+ );
890
+ if (record.rows[0]) {
891
+ if (recorded.rowCount !== 1) {
892
+ throw new Error(
893
+ 'Retention ledger already exists for an undeleted candidate.',
894
+ );
895
+ }
896
+ } else if (recorded.rowCount !== 0) {
897
+ throw new Error(
898
+ 'Retention candidate disappeared without its durable ledger entry.',
899
+ );
900
+ }
901
+ if (record.rows[0]) {
902
+ const deleted = await options.client.query(
903
+ \`DELETE FROM records
904
+ WHERE id = $1::uuid
905
+ AND last_activity_at + ($3::int * INTERVAL '1 day')
906
+ <= $2::timestamptz
907
+ RETURNING id\`,
908
+ [recordId, options.asOf, RETENTION_DAYS],
909
+ );
910
+ if (deleted.rowCount !== 1) {
911
+ throw new Error(
912
+ 'Retention candidate changed after its approved scan.',
913
+ );
914
+ }
915
+ await options.client.query(
916
+ \`INSERT INTO data_lifecycle_outbox
917
+ (idempotency_key, event_json, expires_at)
918
+ VALUES ($1, $2::jsonb, $3)\`,
919
+ [idempotencyKey, JSON.stringify(event), ledgerExpiry(options.completedAt)],
920
+ );
921
+ await options.client.query(
922
+ \`INSERT INTO data_lifecycle_downstream_copies
923
+ (source_slice, pseudonymous_key, copy_id, recipient, purpose,
924
+ expires_at, action, hold_refs, status, updated_at)
925
+ VALUES ($1, $2, $3, $4, 'lifecycle-reconciliation', $5,
926
+ 'deletion', '[]'::jsonb, 'propagation-pending', $6)\`,
927
+ [
928
+ '${name}',
929
+ candidate.recordRef,
930
+ OUTBOX_COPY_ID,
931
+ 'foundation-event-bus',
932
+ ledgerExpiry(options.completedAt),
933
+ options.completedAt,
934
+ ],
935
+ );
936
+ deletedNow = true;
937
+ copyStatus = 'propagation-pending';
938
+ }
939
+ await options.client.query('COMMIT');
940
+ } catch (error) {
941
+ await options.client.query('ROLLBACK').catch(() => undefined);
942
+ throw error;
943
+ }
944
+ if (publishedAt && copyStatus === 'reconciled') {
945
+ return 'already-applied';
946
+ }
947
+ if (publishedAt || copyStatus !== 'propagation-pending') {
948
+ throw new Error('Retention propagation state is inconsistent.');
949
+ }
950
+ try {
951
+ await options.publishLifecycleEvent(event);
952
+ } catch (error) {
953
+ await options.client
954
+ .query(
955
+ \`UPDATE data_lifecycle_outbox
956
+ SET attempts = attempts + 1,
957
+ last_error_code = 'event-publication-failed'
958
+ WHERE idempotency_key = $1\`,
959
+ [idempotencyKey],
960
+ )
961
+ .catch(() => undefined);
962
+ throw error;
963
+ }
964
+ await options.client.query('BEGIN');
965
+ try {
966
+ const published = await options.client.query(
967
+ \`UPDATE data_lifecycle_outbox
968
+ SET attempts = attempts + 1,
969
+ last_error_code = NULL,
970
+ published_at = $2
971
+ WHERE idempotency_key = $1
972
+ AND published_at IS NULL\`,
973
+ [idempotencyKey, options.completedAt],
974
+ );
975
+ const reconciled = await options.client.query(
976
+ \`UPDATE data_lifecycle_downstream_copies
977
+ SET status = 'reconciled', updated_at = $4
978
+ WHERE source_slice = $1
979
+ AND pseudonymous_key = $2
980
+ AND copy_id = $3
981
+ AND status = 'propagation-pending'\`,
982
+ [
983
+ '${name}',
984
+ candidate.recordRef,
985
+ OUTBOX_COPY_ID,
986
+ options.completedAt,
987
+ ],
988
+ );
989
+ if (published.rowCount !== 1 || reconciled.rowCount !== 1) {
990
+ throw new Error(
991
+ 'Retention propagation acknowledgement lost compare-and-set.',
992
+ );
993
+ }
994
+ await options.client.query('COMMIT');
995
+ } catch (error) {
996
+ await options.client.query('ROLLBACK').catch(() => undefined);
997
+ throw error;
998
+ }
999
+ return deletedNow ? 'applied' : 'already-applied';
1000
+ },
1001
+ };
1002
+ }
1003
+ `,
1004
+ 'jobs/data-retention/src/maintenance.ts': `import { readFile } from 'node:fs/promises';
1005
+ import {
1006
+ createAuthorizationEvaluator,
1007
+ defineCoarseAdmission,
1008
+ type AuthorizationPrincipal,
1009
+ } from '@empire-builder-kit/runtime/authorization';
1010
+ import {
1011
+ createDataLifecyclePseudonymousReference,
1012
+ evaluateDataLifecycleHolds,
1013
+ type DataLifecycleRuntimeHoldV1,
1014
+ } from '@empire-builder-kit/runtime/data-lifecycle';
1015
+ import { createLogger } from '@empire-builder-kit/runtime/logging';
1016
+ import {
1017
+ bindServerSecretRuntime,
1018
+ createAwsSecretsManagerSource,
1019
+ createServerSecretResolver,
1020
+ } from '@empire-builder-kit/runtime/secrets';
1021
+ import pg from 'pg';
1022
+ import {
1023
+ ${identifierPrefix}AuthorizationActions,
1024
+ ${identifierPrefix}AuthorizationPolicy,
1025
+ } from '../../../authorization/policy.js';
1026
+ import { Secrets } from '../../../secrets/declarations.js';
1027
+
1028
+ const { Client } = pg;
1029
+ const POLICY_VERSION = ${identifierPrefix}AuthorizationPolicy.version;
1030
+ const WORKLOAD_IDENTITY = '${name}:data-retention-job';
1031
+ const BATCH_LIMIT = 1000;
1032
+ let credentialResolver:
1033
+ | ReturnType<typeof createServerSecretResolver>
1034
+ | undefined;
1035
+
1036
+ function required(name: string): string {
1037
+ const value = process.env[name]?.trim();
1038
+ if (!value) throw new Error(name + ' is required.');
1039
+ return value;
1040
+ }
1041
+
1042
+ function databaseCredentialResolver() {
1043
+ credentialResolver ??= createServerSecretResolver({
1044
+ binding: bindServerSecretRuntime({
1045
+ stage: required('EBK_STAGE'),
1046
+ workload: WORKLOAD_IDENTITY,
1047
+ }),
1048
+ cache: { freshTtlMs: 0, staleTtlMs: 0 },
1049
+ source: createAwsSecretsManagerSource(),
1050
+ });
1051
+ return credentialResolver;
1052
+ }
1053
+
1054
+ async function readCredential(secretArn: string, correlationId: string) {
1055
+ const declaration = Secrets.${pascalName}PrimaryDatabaseCredential;
1056
+ const credential = await databaseCredentialResolver().get(
1057
+ declaration,
1058
+ {
1059
+ logicalName: declaration.logicalName,
1060
+ provider: 'aws-secrets-manager',
1061
+ secretArn,
1062
+ },
1063
+ { correlationId },
1064
+ );
1065
+ if (
1066
+ credential.database !== required('EBK_DATABASE_NAME') ||
1067
+ credential.host !== required('EBK_DATABASE_HOST') ||
1068
+ credential.port !== Number(required('EBK_DATABASE_PORT')) ||
1069
+ credential.username !== required('EBK_DATABASE_USERNAME') ||
1070
+ !credential.password
1071
+ ) {
1072
+ throw new Error(
1073
+ 'Database credential does not match the exact maintenance binding.',
1074
+ );
1075
+ }
1076
+ return credential;
1077
+ }
1078
+
1079
+ export async function authorizeRetentionMaintenance(
1080
+ now: string,
1081
+ ): Promise<AuthorizationPrincipal> {
1082
+ if (required('EBK_WORKLOAD_IDENTITY') !== WORKLOAD_IDENTITY) {
1083
+ throw new Error('Retention maintenance workload identity is invalid.');
1084
+ }
1085
+ const principal: AuthorizationPrincipal = {
1086
+ actor: { id: WORKLOAD_IDENTITY, type: 'service' },
1087
+ audience: WORKLOAD_IDENTITY,
1088
+ authority: {
1089
+ machineIdentityId: WORKLOAD_IDENTITY,
1090
+ mode: 'machine',
1091
+ },
1092
+ kind: 'agent',
1093
+ roles: [],
1094
+ scopes: [],
1095
+ subject: { id: WORKLOAD_IDENTITY, type: 'service' },
1096
+ };
1097
+ const evaluator = createAuthorizationEvaluator({
1098
+ admission: defineCoarseAdmission({
1099
+ anyOf: [{ kinds: ['agent'], subjectTypes: ['service'] }],
1100
+ }),
1101
+ policy: ${identifierPrefix}AuthorizationPolicy,
1102
+ requiredPolicyVersion: POLICY_VERSION,
1103
+ });
1104
+ await evaluator.authorize({
1105
+ action: ${identifierPrefix}AuthorizationActions.retentionMaintenance,
1106
+ context: {
1107
+ facts: { operationId: 'bounded-control-maintenance' },
1108
+ now,
1109
+ stage: required('EBK_STAGE'),
1110
+ },
1111
+ principal,
1112
+ resource: { attributes: { collection: true }, type: 'slice' },
1113
+ });
1114
+ return principal;
1115
+ }
1116
+
1117
+ function assertNoActiveControlHold(
1118
+ holds: readonly DataLifecycleRuntimeHoldV1[],
1119
+ now: string,
1120
+ ) {
1121
+ for (const hold of holds) {
1122
+ const evaluated = evaluateDataLifecycleHolds({
1123
+ categoryId: 'lifecycle-control-metadata',
1124
+ holds: [hold],
1125
+ now,
1126
+ operation: 'deletion',
1127
+ scopeRef: hold.scopeRef,
1128
+ });
1129
+ if (evaluated.blocked) {
1130
+ throw new Error(
1131
+ 'Retention maintenance is blocked by an active control-metadata hold.',
1132
+ );
1133
+ }
1134
+ }
1135
+ }
1136
+
1137
+ const reconciliationStatements = [
1138
+ \`WITH candidates AS (
1139
+ SELECT approval.ctid
1140
+ FROM approval_requests approval
1141
+ WHERE approval.expires_at <= $1::timestamptz
1142
+ AND approval.status IN ('pending', 'approved')
1143
+ AND (
1144
+ EXISTS (
1145
+ SELECT 1 FROM data_lifecycle_requests request
1146
+ WHERE request.approval_request_id = approval.id
1147
+ AND request.status IN ('requested', 'verified', 'held')
1148
+ )
1149
+ OR EXISTS (
1150
+ SELECT 1 FROM data_lifecycle_retention_runs run
1151
+ WHERE run.approval_request_id = approval.id
1152
+ AND run.status IN (
1153
+ 'approval-pending',
1154
+ 'approval-approved'
1155
+ )
1156
+ )
1157
+ )
1158
+ ORDER BY approval.expires_at, approval.id
1159
+ LIMIT $2
1160
+ FOR UPDATE OF approval SKIP LOCKED
1161
+ )
1162
+ UPDATE approval_requests approval
1163
+ SET request_json = jsonb_set(
1164
+ jsonb_set(
1165
+ approval.request_json,
1166
+ '{status}',
1167
+ to_jsonb('expired'::text),
1168
+ true
1169
+ ),
1170
+ '{revision}',
1171
+ to_jsonb(approval.revision + 1),
1172
+ true
1173
+ ),
1174
+ revision = approval.revision + 1,
1175
+ status = 'expired'
1176
+ WHERE approval.ctid IN (SELECT ctid FROM candidates)\`,
1177
+ \`WITH candidates AS (
1178
+ SELECT request.ctid,
1179
+ CASE approval.status
1180
+ WHEN 'expired' THEN 'approval-expired'
1181
+ ELSE 'approval-rejected'
1182
+ END::text AS outcome_code
1183
+ FROM data_lifecycle_requests request
1184
+ JOIN approval_requests approval
1185
+ ON approval.id = request.approval_request_id
1186
+ WHERE approval.status IN ('expired', 'rejected')
1187
+ AND request.status IN ('requested', 'verified', 'held')
1188
+ AND request.request_json->>'kind' IN (
1189
+ 'data-deletion',
1190
+ 'data-export'
1191
+ )
1192
+ ORDER BY request.updated_at, request.id
1193
+ LIMIT $2
1194
+ FOR UPDATE OF request SKIP LOCKED
1195
+ )
1196
+ UPDATE data_lifecycle_requests request
1197
+ SET request_json = jsonb_set(
1198
+ jsonb_set(
1199
+ jsonb_set(
1200
+ request.request_json,
1201
+ '{status}',
1202
+ to_jsonb('rejected'::text),
1203
+ true
1204
+ ),
1205
+ '{outcomeCode}',
1206
+ to_jsonb(candidates.outcome_code),
1207
+ true
1208
+ ),
1209
+ '{updatedAt}',
1210
+ to_jsonb($1::text),
1211
+ true
1212
+ ),
1213
+ revision = request.revision + 1,
1214
+ status = 'rejected',
1215
+ updated_at = $1::timestamptz
1216
+ FROM candidates
1217
+ WHERE request.ctid = candidates.ctid\`,
1218
+ \`WITH candidates AS (
1219
+ SELECT artifact.ctid
1220
+ FROM data_lifecycle_export_artifacts artifact
1221
+ JOIN data_lifecycle_requests request
1222
+ ON request.approval_request_id = artifact.approval_request_id
1223
+ WHERE request.status = 'rejected'
1224
+ AND request.request_json->>'kind' = 'data-export'
1225
+ AND artifact.cleanup_state = 'requested'
1226
+ AND artifact.upload_started_at IS NULL
1227
+ AND artifact.current_version_id IS NULL
1228
+ AND NOT EXISTS (
1229
+ SELECT 1 FROM data_lifecycle_export_versions version
1230
+ WHERE version.approval_request_id =
1231
+ artifact.approval_request_id
1232
+ )
1233
+ ORDER BY artifact.updated_at, artifact.approval_request_id
1234
+ LIMIT $2
1235
+ FOR UPDATE OF artifact SKIP LOCKED
1236
+ )
1237
+ UPDATE data_lifecycle_export_artifacts artifact
1238
+ SET cleanup_state = 'completed',
1239
+ last_error_code = NULL,
1240
+ updated_at = $1::timestamptz
1241
+ WHERE artifact.ctid IN (SELECT ctid FROM candidates)\`,
1242
+ \`WITH candidates AS (
1243
+ SELECT run.ctid
1244
+ FROM data_lifecycle_retention_runs run
1245
+ WHERE run.expires_at <= $1::timestamptz
1246
+ AND run.status = 'reported'
1247
+ ORDER BY run.expires_at, run.id
1248
+ LIMIT $2
1249
+ FOR UPDATE OF run SKIP LOCKED
1250
+ )
1251
+ UPDATE data_lifecycle_retention_runs run
1252
+ SET revision = run.revision + 1,
1253
+ status = 'expired',
1254
+ updated_at = $1::timestamptz
1255
+ WHERE run.ctid IN (SELECT ctid FROM candidates)\`,
1256
+ \`WITH candidates AS (
1257
+ SELECT run.ctid,
1258
+ CASE approval.status
1259
+ WHEN 'expired' THEN 'approval-expired'
1260
+ ELSE 'approval-rejected'
1261
+ END::text AS terminal_status
1262
+ FROM data_lifecycle_retention_runs run
1263
+ JOIN approval_requests approval
1264
+ ON approval.id = run.approval_request_id
1265
+ WHERE run.status IN ('approval-pending', 'approval-approved')
1266
+ AND approval.status IN ('expired', 'rejected')
1267
+ ORDER BY run.updated_at, run.id
1268
+ LIMIT $2
1269
+ FOR UPDATE OF run SKIP LOCKED
1270
+ )
1271
+ UPDATE data_lifecycle_retention_runs run
1272
+ SET revision = run.revision + 1,
1273
+ status = candidates.terminal_status,
1274
+ updated_at = $1::timestamptz
1275
+ FROM candidates
1276
+ WHERE run.ctid = candidates.ctid\`,
1277
+ ] as const;
1278
+
1279
+ const maintenanceStatements = [
1280
+ \`WITH expired AS (
1281
+ SELECT ctid FROM data_lifecycle_outbox
1282
+ WHERE expires_at <= $1::timestamptz
1283
+ AND published_at IS NOT NULL
1284
+ ORDER BY expires_at, idempotency_key
1285
+ LIMIT $2
1286
+ FOR UPDATE SKIP LOCKED
1287
+ )
1288
+ DELETE FROM data_lifecycle_outbox
1289
+ WHERE ctid IN (SELECT ctid FROM expired)\`,
1290
+ \`WITH expired AS (
1291
+ SELECT ctid FROM data_lifecycle_ledger
1292
+ WHERE expires_at <= $1::timestamptz
1293
+ ORDER BY expires_at, entry_id
1294
+ LIMIT $2
1295
+ FOR UPDATE SKIP LOCKED
1296
+ )
1297
+ DELETE FROM data_lifecycle_ledger
1298
+ WHERE ctid IN (SELECT ctid FROM expired)\`,
1299
+ \`SELECT ebk_prune_data_lifecycle_audit(
1300
+ $1::timestamptz,
1301
+ $2::integer
1302
+ )\`,
1303
+ \`WITH expired AS (
1304
+ SELECT ctid FROM data_lifecycle_export_artifacts
1305
+ WHERE cleanup_state = 'completed'
1306
+ AND updated_at <= $1::timestamptz - INTERVAL '400 days'
1307
+ ORDER BY updated_at, approval_request_id
1308
+ LIMIT $2
1309
+ FOR UPDATE SKIP LOCKED
1310
+ )
1311
+ DELETE FROM data_lifecycle_export_artifacts
1312
+ WHERE ctid IN (SELECT ctid FROM expired)\`,
1313
+ \`WITH expired AS (
1314
+ SELECT ctid FROM data_lifecycle_receipts
1315
+ WHERE received_at <= $1::timestamptz - INTERVAL '400 days'
1316
+ ORDER BY received_at, source_slice, pseudonymous_key
1317
+ LIMIT $2
1318
+ FOR UPDATE SKIP LOCKED
1319
+ )
1320
+ DELETE FROM data_lifecycle_receipts
1321
+ WHERE ctid IN (SELECT ctid FROM expired)\`,
1322
+ \`WITH expired AS (
1323
+ SELECT ctid FROM data_lifecycle_downstream_copies
1324
+ WHERE expires_at <= $1::timestamptz
1325
+ AND status IN ('deleted', 'expired', 'reconciled')
1326
+ ORDER BY expires_at, source_slice, pseudonymous_key, copy_id
1327
+ LIMIT $2
1328
+ FOR UPDATE SKIP LOCKED
1329
+ )
1330
+ DELETE FROM data_lifecycle_downstream_copies
1331
+ WHERE ctid IN (SELECT ctid FROM expired)\`,
1332
+ \`SELECT ebk_prune_authorization_audit(
1333
+ $1::timestamptz,
1334
+ $2::integer
1335
+ )\`,
1336
+ \`WITH expired AS (
1337
+ SELECT ctid FROM delegation_grants
1338
+ WHERE expires_at <= $1::timestamptz
1339
+ AND created_at <= $1::timestamptz - INTERVAL '400 days'
1340
+ ORDER BY expires_at, id
1341
+ LIMIT $2
1342
+ FOR UPDATE SKIP LOCKED
1343
+ )
1344
+ DELETE FROM delegation_grants
1345
+ WHERE ctid IN (SELECT ctid FROM expired)\`,
1346
+ \`WITH expired AS (
1347
+ SELECT ctid FROM data_lifecycle_holds
1348
+ WHERE status = 'released'
1349
+ AND expires_at <= $1::timestamptz - INTERVAL '400 days'
1350
+ ORDER BY expires_at, hold_id
1351
+ LIMIT $2
1352
+ FOR UPDATE SKIP LOCKED
1353
+ )
1354
+ DELETE FROM data_lifecycle_holds
1355
+ WHERE ctid IN (SELECT ctid FROM expired)\`,
1356
+ \`WITH expired AS (
1357
+ SELECT ctid FROM data_lifecycle_requests
1358
+ WHERE updated_at <= $1::timestamptz - INTERVAL '400 days'
1359
+ AND status IN ('completed', 'failed', 'rejected')
1360
+ ORDER BY updated_at, id
1361
+ LIMIT $2
1362
+ FOR UPDATE SKIP LOCKED
1363
+ )
1364
+ DELETE FROM data_lifecycle_requests
1365
+ WHERE ctid IN (SELECT ctid FROM expired)\`,
1366
+ \`WITH expired AS (
1367
+ SELECT candidate.ctid
1368
+ FROM data_lifecycle_retention_candidates candidate
1369
+ JOIN data_lifecycle_retention_runs run ON run.id = candidate.run_id
1370
+ WHERE run.expires_at <= $1::timestamptz
1371
+ AND run.status IN (
1372
+ 'completed',
1373
+ 'approval-expired',
1374
+ 'approval-rejected',
1375
+ 'expired'
1376
+ )
1377
+ ORDER BY run.expires_at, candidate.run_id, candidate.candidate_order
1378
+ LIMIT $2
1379
+ FOR UPDATE OF candidate SKIP LOCKED
1380
+ )
1381
+ DELETE FROM data_lifecycle_retention_candidates
1382
+ WHERE ctid IN (SELECT ctid FROM expired)\`,
1383
+ \`WITH expired AS (
1384
+ SELECT ctid FROM data_lifecycle_retention_runs
1385
+ WHERE expires_at <= $1::timestamptz
1386
+ AND status IN (
1387
+ 'completed',
1388
+ 'approval-expired',
1389
+ 'approval-rejected',
1390
+ 'expired'
1391
+ )
1392
+ AND NOT EXISTS (
1393
+ SELECT 1 FROM data_lifecycle_retention_candidates
1394
+ WHERE run_id = data_lifecycle_retention_runs.id
1395
+ )
1396
+ ORDER BY expires_at, id
1397
+ LIMIT $2
1398
+ FOR UPDATE SKIP LOCKED
1399
+ )
1400
+ DELETE FROM data_lifecycle_retention_runs
1401
+ WHERE ctid IN (SELECT ctid FROM expired)\`,
1402
+ \`WITH expired AS (
1403
+ SELECT ctid FROM approval_requests
1404
+ WHERE expires_at <= $1::timestamptz - INTERVAL '400 days'
1405
+ AND NOT EXISTS (
1406
+ SELECT 1 FROM data_lifecycle_requests
1407
+ WHERE approval_request_id = approval_requests.id
1408
+ )
1409
+ AND NOT EXISTS (
1410
+ SELECT 1 FROM data_lifecycle_retention_runs
1411
+ WHERE approval_request_id = approval_requests.id
1412
+ )
1413
+ ORDER BY expires_at, id
1414
+ LIMIT $2
1415
+ FOR UPDATE SKIP LOCKED
1416
+ )
1417
+ DELETE FROM approval_requests
1418
+ WHERE ctid IN (SELECT ctid FROM expired)\`,
1419
+ ] as const;
1420
+
1421
+ export async function runRetentionMaintenance() {
1422
+ const now = new Date().toISOString();
1423
+ const correlationId = required('EBK_MAINTENANCE_CORRELATION_ID');
1424
+ const principal = await authorizeRetentionMaintenance(now);
1425
+ const logger = createLogger({
1426
+ component: 'job-data-retention-maintenance',
1427
+ feature: 'data-lifecycle',
1428
+ operation: 'retention-maintenance',
1429
+ slice: '${name}',
1430
+ stage: required('EBK_STAGE'),
1431
+ workspace: required('EBK_WORKSPACE'),
1432
+ });
1433
+ const credential = await readCredential(
1434
+ required('EBK_READWRITE_SECRET_ARN'),
1435
+ correlationId,
1436
+ );
1437
+ const ca = await readFile(required('EBK_RDS_CA_BUNDLE'), 'utf8');
1438
+ const client = new Client({
1439
+ ...credential,
1440
+ connectionTimeoutMillis: 10_000,
1441
+ ssl: { ca, rejectUnauthorized: true },
1442
+ });
1443
+ try {
1444
+ await client.connect();
1445
+ await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
1446
+ try {
1447
+ await client.query(
1448
+ \`INSERT INTO authorization_audit
1449
+ (action, approval_reference, authority_mode, correlation_id,
1450
+ decision, delegation_reference, policy_version, principal_kind,
1451
+ pseudonymous_subject_id, reason_category, resource_reference,
1452
+ resource_type, slice, stage, subject_type)
1453
+ VALUES ($1, NULL, 'machine', $2, 'allow', NULL, $3, 'agent', $4,
1454
+ 'policy-allow', $5, 'slice', $6, $7, 'service')\`,
1455
+ [
1456
+ ${identifierPrefix}AuthorizationActions.retentionMaintenance,
1457
+ correlationId,
1458
+ POLICY_VERSION,
1459
+ createDataLifecyclePseudonymousReference({
1460
+ categoryId: 'authorization-subject',
1461
+ slice: '${name}',
1462
+ value: principal.actor.id,
1463
+ }),
1464
+ createDataLifecyclePseudonymousReference({
1465
+ categoryId: 'lifecycle-control-metadata',
1466
+ slice: '${name}',
1467
+ value: 'bounded-control-maintenance',
1468
+ }),
1469
+ '${name}',
1470
+ required('EBK_STAGE'),
1471
+ ],
1472
+ );
1473
+ const holdRows = await client.query<{
1474
+ holdJson: DataLifecycleRuntimeHoldV1;
1475
+ }>(
1476
+ \`SELECT hold_json AS "holdJson"
1477
+ FROM data_lifecycle_holds
1478
+ WHERE category_id = 'lifecycle-control-metadata'
1479
+ AND status = 'active'
1480
+ ORDER BY hold_id
1481
+ FOR SHARE\`,
1482
+ );
1483
+ assertNoActiveControlHold(
1484
+ holdRows.rows.map(({ holdJson }) => holdJson),
1485
+ now,
1486
+ );
1487
+ let reconciled = 0;
1488
+ for (const statement of reconciliationStatements) {
1489
+ const result = await client.query(statement, [now, BATCH_LIMIT]);
1490
+ reconciled += result.rowCount ?? 0;
1491
+ }
1492
+ let deleted = 0;
1493
+ for (const statement of maintenanceStatements) {
1494
+ const result = await client.query(statement, [now, BATCH_LIMIT]);
1495
+ deleted += result.rowCount ?? 0;
1496
+ }
1497
+ await client.query('COMMIT');
1498
+ logger.info('Retention control maintenance completed', {
1499
+ attributes: {
1500
+ deleted,
1501
+ reconciled,
1502
+ statementCount:
1503
+ reconciliationStatements.length +
1504
+ maintenanceStatements.length,
1505
+ },
1506
+ outcome: 'success',
1507
+ });
1508
+ return {
1509
+ deleted,
1510
+ reconciled,
1511
+ statementCount:
1512
+ reconciliationStatements.length + maintenanceStatements.length,
1513
+ };
1514
+ } catch (error) {
1515
+ await client.query('ROLLBACK').catch(() => undefined);
1516
+ throw error;
1517
+ }
1518
+ } catch (error) {
1519
+ logger.error('Retention control maintenance failed', {
1520
+ attributes: {
1521
+ errorCode:
1522
+ error instanceof Error
1523
+ ? error.name.replace(/[^A-Za-z0-9_.-]/g, '').slice(0, 80)
1524
+ : 'RetentionMaintenanceError',
1525
+ },
1526
+ outcome: 'failure',
1527
+ });
1528
+ throw error;
1529
+ } finally {
1530
+ await client.end().catch(() => undefined);
1531
+ }
1532
+ }
1533
+
1534
+ if (import.meta.url === new URL(process.argv[1] ?? '', 'file:').href) {
1535
+ await runRetentionMaintenance();
1536
+ }
1537
+ `,
1538
+ 'jobs/data-retention/src/retention-contract.spec.ts': `import { readFile } from 'node:fs/promises';
1539
+ import { afterEach, describe, expect, it, vi } from 'vitest';
1540
+ import { runDataLifecycleRetention } from '@empire-builder-kit/runtime/data-lifecycle';
1541
+ import type { Client } from 'pg';
1542
+ import {
1543
+ ${identifierPrefix}AuthorizationActions,
1544
+ } from '../../../authorization/policy.js';
1545
+ import { productRecordReference } from '../../../data/pseudonymous-reference.js';
1546
+ import { create${pascalName}RetentionAdapter } from './adapter.js';
1547
+ import { authorizeRetentionMaintenance } from './maintenance.js';
1548
+ import { authorizeRetentionOperation } from './run.js';
1549
+
1550
+ const recordId = '11111111-1111-4111-8111-111111111111';
1551
+ const runId = '22222222-2222-4222-8222-222222222222';
1552
+ const asOf = '2026-07-28T12:00:00.000Z';
1553
+
1554
+ interface QueryScenario {
1555
+ hold: boolean;
1556
+ queries: string[];
1557
+ }
1558
+
1559
+ function clientFor(scenario: QueryScenario): Client {
1560
+ return {
1561
+ async query(statement: string) {
1562
+ const normalized = statement.replace(/\\s+/g, ' ').trim();
1563
+ scenario.queries.push(normalized);
1564
+ if (
1565
+ normalized.includes(
1566
+ 'FROM data_lifecycle_retention_candidates',
1567
+ )
1568
+ ) {
1569
+ return {
1570
+ rowCount: 1,
1571
+ rows: [
1572
+ {
1573
+ cursorKey: 'candidate-001',
1574
+ dueAt: new Date('2026-07-27T12:00:00.000Z'),
1575
+ id: recordId,
1576
+ },
1577
+ ],
1578
+ };
1579
+ }
1580
+ if (
1581
+ normalized.includes('FROM records') &&
1582
+ normalized.includes('FOR UPDATE')
1583
+ ) {
1584
+ return { rowCount: 1, rows: [{ due: true }] };
1585
+ }
1586
+ if (normalized.includes('FROM data_lifecycle_outbox')) {
1587
+ return { rowCount: 0, rows: [] };
1588
+ }
1589
+ if (normalized.includes('FROM data_lifecycle_downstream_copies')) {
1590
+ return { rowCount: 0, rows: [] };
1591
+ }
1592
+ if (normalized.includes('FROM data_lifecycle_holds')) {
1593
+ return {
1594
+ rowCount: scenario.hold ? 1 : 0,
1595
+ rows: scenario.hold
1596
+ ? [
1597
+ {
1598
+ holdJson: {
1599
+ categoryId: 'product-record',
1600
+ effects: ['block-deletion'],
1601
+ expiresAt: '2099-07-28T12:00:00.000Z',
1602
+ holdId: 'retention-hold',
1603
+ issuerCode: 'legal',
1604
+ purpose: 'legal',
1605
+ reviewAt: '2098-07-28T12:00:00.000Z',
1606
+ scopeRef: productRecordReference(recordId),
1607
+ startsAt: '2026-07-27T12:00:00.000Z',
1608
+ status: 'active',
1609
+ },
1610
+ },
1611
+ ]
1612
+ : [],
1613
+ };
1614
+ }
1615
+ return { rowCount: 1, rows: [] };
1616
+ },
1617
+ } as unknown as Client;
1618
+ }
1619
+
1620
+ function adapterFor(
1621
+ scenario: QueryScenario,
1622
+ publishLifecycleEvent: () => Promise<void>,
1623
+ ) {
1624
+ return create${pascalName}RetentionAdapter({
1625
+ approvalRequestId: '33333333-3333-4333-8333-333333333333',
1626
+ asOf,
1627
+ client: clientFor(scenario),
1628
+ completedAt: asOf,
1629
+ correlationId: '44444444-4444-4444-8444-444444444444',
1630
+ eventBusName: 'sample-product-development',
1631
+ mode: 'apply',
1632
+ publishLifecycleEvent,
1633
+ runId,
1634
+ stage: 'development',
1635
+ });
1636
+ }
1637
+
1638
+ afterEach(() => {
1639
+ vi.unstubAllEnvs();
1640
+ });
1641
+
1642
+ describe('generated data-retention Job contract', () => {
1643
+ it('keeps bounded maintenance outside the approved candidate command', async () => {
1644
+ const runSource = await readFile(new URL('./run.ts', import.meta.url), 'utf8');
1645
+ const maintenanceSource = await readFile(
1646
+ new URL('./maintenance.ts', import.meta.url),
1647
+ 'utf8',
1648
+ );
1649
+
1650
+ expect(runSource).not.toContain('cleanupControlRows');
1651
+ expect(runSource).not.toContain('retentionMaintenance');
1652
+ expect(maintenanceSource).not.toContain('beginApprovalExecution');
1653
+ expect(maintenanceSource).toContain(
1654
+ '${identifierPrefix}AuthorizationActions.retentionMaintenance',
1655
+ );
1656
+ expect(maintenanceSource).toContain(
1657
+ 'BEGIN ISOLATION LEVEL SERIALIZABLE',
1658
+ );
1659
+ expect(maintenanceSource).toContain('LIMIT $2');
1660
+ });
1661
+
1662
+ it('requires exact report/apply/maintenance actions and workload identity', async () => {
1663
+ vi.stubEnv('EBK_STAGE', 'development');
1664
+ vi.stubEnv('EBK_WORKLOAD_IDENTITY', '${name}:data-retention-job');
1665
+ await expect(
1666
+ authorizeRetentionOperation(
1667
+ ${identifierPrefix}AuthorizationActions.retentionReport,
1668
+ asOf,
1669
+ runId,
1670
+ ),
1671
+ ).resolves.toMatchObject({
1672
+ actor: { id: '${name}:data-retention-job', type: 'service' },
1673
+ });
1674
+ await expect(
1675
+ authorizeRetentionOperation(
1676
+ ${identifierPrefix}AuthorizationActions.retentionApply,
1677
+ asOf,
1678
+ runId,
1679
+ ),
1680
+ ).resolves.toMatchObject({
1681
+ actor: { id: '${name}:data-retention-job', type: 'service' },
1682
+ });
1683
+ await expect(authorizeRetentionMaintenance(asOf)).resolves.toMatchObject({
1684
+ actor: { id: '${name}:data-retention-job', type: 'service' },
1685
+ });
1686
+
1687
+ vi.stubEnv('EBK_WORKLOAD_IDENTITY', '${name}:event-consumer');
1688
+ await expect(
1689
+ authorizeRetentionOperation(
1690
+ ${identifierPrefix}AuthorizationActions.retentionReport,
1691
+ asOf,
1692
+ runId,
1693
+ ),
1694
+ ).rejects.toThrow('Retention Job workload identity is invalid.');
1695
+ await expect(authorizeRetentionMaintenance(asOf)).rejects.toThrow(
1696
+ 'Retention maintenance workload identity is invalid.',
1697
+ );
1698
+
1699
+ vi.stubEnv('EBK_WORKLOAD_IDENTITY', '');
1700
+ await expect(
1701
+ authorizeRetentionOperation(
1702
+ ${identifierPrefix}AuthorizationActions.retentionApply,
1703
+ asOf,
1704
+ runId,
1705
+ ),
1706
+ ).rejects.toThrow('EBK_WORKLOAD_IDENTITY is required.');
1707
+ });
1708
+
1709
+ it('locks the candidate before holds and leaves the checkpoint incomplete when held', async () => {
1710
+ const scenario: QueryScenario = { hold: true, queries: [] };
1711
+ const result = await runDataLifecycleRetention({
1712
+ adapter: adapterFor(scenario, async () => undefined),
1713
+ asOf,
1714
+ limit: 100,
1715
+ mode: 'apply',
1716
+ policyVersion: '1.0.0',
1717
+ });
1718
+ const recordLock = scenario.queries.findIndex(
1719
+ (statement) =>
1720
+ statement.includes('FROM records') &&
1721
+ statement.includes('FOR UPDATE'),
1722
+ );
1723
+ const holdRead = scenario.queries.findIndex((statement) =>
1724
+ statement.includes('FROM data_lifecycle_holds'),
1725
+ );
1726
+
1727
+ expect(result.status).toBe('retry-required');
1728
+ expect(result.checkpoint.complete).toBe(false);
1729
+ expect(recordLock).toBeGreaterThanOrEqual(0);
1730
+ expect(holdRead).toBeGreaterThan(recordLock);
1731
+ expect(
1732
+ scenario.queries.some((statement) =>
1733
+ statement.startsWith('DELETE FROM records'),
1734
+ ),
1735
+ ).toBe(false);
1736
+ });
1737
+
1738
+ it('keeps the checkpoint incomplete until durable propagation is acknowledged', async () => {
1739
+ const failed: QueryScenario = { hold: false, queries: [] };
1740
+ const failedResult = await runDataLifecycleRetention({
1741
+ adapter: adapterFor(failed, async () => {
1742
+ throw new Error('event publication failed');
1743
+ }),
1744
+ asOf,
1745
+ limit: 100,
1746
+ mode: 'apply',
1747
+ policyVersion: '1.0.0',
1748
+ });
1749
+
1750
+ expect(failedResult.status).toBe('retry-required');
1751
+ expect(failedResult.checkpoint.complete).toBe(false);
1752
+ expect(
1753
+ failed.queries.some((statement) =>
1754
+ statement.includes('INSERT INTO data_lifecycle_outbox'),
1755
+ ),
1756
+ ).toBe(true);
1757
+ expect(
1758
+ failed.queries.some((statement) =>
1759
+ statement.includes("status = 'reconciled'"),
1760
+ ),
1761
+ ).toBe(false);
1762
+
1763
+ const succeeded: QueryScenario = { hold: false, queries: [] };
1764
+ const succeededResult = await runDataLifecycleRetention({
1765
+ adapter: adapterFor(succeeded, async () => undefined),
1766
+ asOf,
1767
+ limit: 100,
1768
+ mode: 'apply',
1769
+ policyVersion: '1.0.0',
1770
+ });
1771
+
1772
+ expect(succeededResult.status).toBe('complete');
1773
+ expect(succeededResult.checkpoint.complete).toBe(true);
1774
+ const published = succeeded.queries.findIndex((statement) =>
1775
+ statement.includes('published_at = $2'),
1776
+ );
1777
+ const reconciled = succeeded.queries.findIndex((statement) =>
1778
+ statement.includes("status = 'reconciled'"),
1779
+ );
1780
+ expect(published).toBeGreaterThanOrEqual(0);
1781
+ expect(reconciled).toBeGreaterThanOrEqual(0);
1782
+ });
1783
+ });
1784
+ `,
1785
+ 'jobs/data-retention/src/run.ts': `import { readFile } from 'node:fs/promises';
1786
+ import {
1787
+ EventBridgeClient,
1788
+ PutEventsCommand,
1789
+ type PutEventsRequestEntry,
1790
+ } from '@aws-sdk/client-eventbridge';
1791
+ import {
1792
+ SendTaskFailureCommand,
1793
+ SendTaskSuccessCommand,
1794
+ SFNClient,
1795
+ } from '@aws-sdk/client-sfn';
1796
+ import {
1797
+ beginApprovalExecution,
1798
+ completeApprovalExecution,
1799
+ createAuthorizationEvaluator,
1800
+ defineCoarseAdmission,
1801
+ digestApprovalRequest,
1802
+ type ApprovalRequest,
1803
+ type AuthorizationPrincipal,
1804
+ } from '@empire-builder-kit/runtime/authorization';
1805
+ import {
1806
+ createDataLifecycleEvidence,
1807
+ createDataLifecyclePseudonymousReference,
1808
+ createDataLifecycleRetentionCheckpoint,
1809
+ runDataLifecycleRetention,
1810
+ type DataLifecycleRetentionCheckpointV1,
1811
+ } from '@empire-builder-kit/runtime/data-lifecycle';
1812
+ import { createLogger } from '@empire-builder-kit/runtime/logging';
1813
+ import {
1814
+ bindServerSecretRuntime,
1815
+ createAwsSecretsManagerSource,
1816
+ createServerSecretResolver,
1817
+ } from '@empire-builder-kit/runtime/secrets';
1818
+ import pg from 'pg';
1819
+ import {
1820
+ ${identifierPrefix}AuthorizationActions,
1821
+ ${identifierPrefix}AuthorizationPolicy,
1822
+ } from '../../../authorization/policy.js';
1823
+ import { approvalReference } from '../../../data/pseudonymous-reference.js';
1824
+ import {
1825
+ ${pascalName}DataLifecycleRuntimePolicy,
1826
+ } from '../../../data/runtime-policy.cjs';
1827
+ import { Secrets } from '../../../secrets/declarations.js';
1828
+ import { create${pascalName}RetentionAdapter } from './adapter.js';
1829
+
1830
+ const { Client } = pg;
1831
+ const eventBridge = new EventBridgeClient({});
1832
+ const POLICY_VERSION =
1833
+ ${pascalName}DataLifecycleRuntimePolicy.policyVersion;
1834
+ const RETENTION_DAYS =
1835
+ ${pascalName}DataLifecycleRuntimePolicy.retentionDays;
1836
+ const AUTHORIZATION_POLICY_VERSION =
1837
+ ${identifierPrefix}AuthorizationPolicy.version;
1838
+ const ACTION = ${identifierPrefix}AuthorizationActions.retentionApply;
1839
+ const WORKLOAD_IDENTITY = '${name}:data-retention-job';
1840
+ const MAX_PAGES = 100;
1841
+ const PAGE_SIZE = 100;
1842
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
1843
+ const SHA256 = /^sha256:[a-f0-9]{64}$/;
1844
+ let credentialResolver:
1845
+ | ReturnType<typeof createServerSecretResolver>
1846
+ | undefined;
1847
+
1848
+ async function publishLifecycleEvent(entry: PutEventsRequestEntry) {
1849
+ const result = await eventBridge.send(
1850
+ new PutEventsCommand({ Entries: [entry] }),
1851
+ );
1852
+ if (result.FailedEntryCount !== 0 || result.Entries?.[0]?.ErrorCode) {
1853
+ throw new Error('EventBridge rejected the retention lifecycle event.');
1854
+ }
1855
+ }
1856
+
1857
+ interface RetentionPlan {
1858
+ asOf: string;
1859
+ candidateDigest: string;
1860
+ categoryId: 'product-record';
1861
+ due: number;
1862
+ locationId: 'primary-database';
1863
+ maxItems: 10000;
1864
+ pageSize: 100;
1865
+ policyVersion: string;
1866
+ retentionDays: number;
1867
+ runId: string;
1868
+ }
1869
+
1870
+ interface RetentionCounts {
1871
+ alreadyApplied: number;
1872
+ applied: number;
1873
+ applyPages: number;
1874
+ due: number;
1875
+ reportPages: number;
1876
+ }
1877
+
1878
+ interface RetentionRunRow {
1879
+ approvalRequestId: string;
1880
+ checkpointJson: DataLifecycleRetentionCheckpointV1;
1881
+ countsJson: RetentionCounts;
1882
+ planJson: RetentionPlan;
1883
+ revision: number;
1884
+ status: string;
1885
+ }
1886
+
1887
+ function required(name: string): string {
1888
+ const value = process.env[name]?.trim();
1889
+ if (!value) throw new Error(name + ' is required.');
1890
+ return value;
1891
+ }
1892
+
1893
+ function requiredUuid(name: string): string {
1894
+ const value = required(name);
1895
+ if (!UUID.test(value)) throw new Error(name + ' must be a UUID.');
1896
+ return value;
1897
+ }
1898
+
1899
+ function requiredInstant(name: string): string {
1900
+ const value = required(name);
1901
+ const parsed = Date.parse(value);
1902
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
1903
+ throw new Error(name + ' must be a canonical timestamp.');
1904
+ }
1905
+ return value;
1906
+ }
1907
+
1908
+ function optionalUuid(name: string): string | undefined {
1909
+ const value = process.env[name]?.trim();
1910
+ if (!value) return undefined;
1911
+ if (!UUID.test(value)) throw new Error(name + ' must be a UUID.');
1912
+ return value;
1913
+ }
1914
+
1915
+ function safeErrorCode(error: unknown): string {
1916
+ const value =
1917
+ error && typeof error === 'object' && 'name' in error
1918
+ ? String(error.name)
1919
+ : 'RetentionJobError';
1920
+ return value.replace(/[^A-Za-z0-9_.-]/g, '').slice(0, 80) || 'Error';
1921
+ }
1922
+
1923
+ function addDays(value: string, days: number): string {
1924
+ const result = new Date(value);
1925
+ result.setUTCDate(result.getUTCDate() + days);
1926
+ return result.toISOString();
1927
+ }
1928
+
1929
+ function databaseCredentialResolver() {
1930
+ credentialResolver ??= createServerSecretResolver({
1931
+ binding: bindServerSecretRuntime({
1932
+ stage: required('EBK_STAGE'),
1933
+ workload: WORKLOAD_IDENTITY,
1934
+ }),
1935
+ cache: { freshTtlMs: 0, staleTtlMs: 0 },
1936
+ source: createAwsSecretsManagerSource(),
1937
+ });
1938
+ return credentialResolver;
1939
+ }
1940
+
1941
+ async function readCredential(secretArn: string, correlationId: string) {
1942
+ const declaration = Secrets.${pascalName}PrimaryDatabaseCredential;
1943
+ const credential = await databaseCredentialResolver().get(
1944
+ declaration,
1945
+ {
1946
+ logicalName: declaration.logicalName,
1947
+ provider: 'aws-secrets-manager',
1948
+ secretArn,
1949
+ },
1950
+ { correlationId },
1951
+ );
1952
+ if (
1953
+ credential.database !== required('EBK_DATABASE_NAME') ||
1954
+ credential.host !== required('EBK_DATABASE_HOST') ||
1955
+ credential.port !== Number(required('EBK_DATABASE_PORT')) ||
1956
+ credential.username !== required('EBK_DATABASE_USERNAME') ||
1957
+ !credential.password
1958
+ ) {
1959
+ throw new Error(
1960
+ 'Database credential does not match the exact retention Job binding.',
1961
+ );
1962
+ }
1963
+ return credential;
1964
+ }
1965
+
1966
+ type RetentionAuthorizationAction =
1967
+ | typeof ${identifierPrefix}AuthorizationActions.retentionApply
1968
+ | typeof ${identifierPrefix}AuthorizationActions.retentionReport;
1969
+
1970
+ export async function authorizeRetentionOperation(
1971
+ action: RetentionAuthorizationAction,
1972
+ now: string,
1973
+ runId: string,
1974
+ ): Promise<AuthorizationPrincipal> {
1975
+ if (required('EBK_WORKLOAD_IDENTITY') !== WORKLOAD_IDENTITY) {
1976
+ throw new Error('Retention Job workload identity is invalid.');
1977
+ }
1978
+ const principal: AuthorizationPrincipal = {
1979
+ actor: { id: WORKLOAD_IDENTITY, type: 'service' },
1980
+ audience: WORKLOAD_IDENTITY,
1981
+ authority: {
1982
+ machineIdentityId: WORKLOAD_IDENTITY,
1983
+ mode: 'machine',
1984
+ },
1985
+ kind: 'agent',
1986
+ roles: [],
1987
+ scopes: [],
1988
+ subject: { id: WORKLOAD_IDENTITY, type: 'service' },
1989
+ };
1990
+ const evaluator = createAuthorizationEvaluator({
1991
+ admission: defineCoarseAdmission({
1992
+ anyOf: [{ kinds: ['agent'], subjectTypes: ['service'] }],
1993
+ }),
1994
+ policy: ${identifierPrefix}AuthorizationPolicy,
1995
+ requiredPolicyVersion: AUTHORIZATION_POLICY_VERSION,
1996
+ });
1997
+ await evaluator.authorize({
1998
+ action,
1999
+ context: {
2000
+ facts: { operationId: runId },
2001
+ now,
2002
+ stage: required('EBK_STAGE'),
2003
+ },
2004
+ principal,
2005
+ resource: { attributes: { collection: true }, type: 'slice' },
2006
+ });
2007
+ return principal;
2008
+ }
2009
+
2010
+ async function writeRetentionAuthorizationAudit(
2011
+ client: InstanceType<typeof Client>,
2012
+ action: RetentionAuthorizationAction,
2013
+ correlationId: string,
2014
+ now: string,
2015
+ principal: AuthorizationPrincipal,
2016
+ runId: string,
2017
+ ) {
2018
+ await client.query(
2019
+ \`INSERT INTO authorization_audit
2020
+ (action, approval_reference, authority_mode, correlation_id, decision,
2021
+ delegation_reference, policy_version, principal_kind,
2022
+ pseudonymous_subject_id, reason_category, resource_reference,
2023
+ resource_type, slice, stage, subject_type)
2024
+ VALUES ($1, NULL, 'machine', $2, 'allow', NULL, $3, 'agent', $4,
2025
+ 'policy-allow', $5, 'retention-run', $6, $7, 'service')\`,
2026
+ [
2027
+ action,
2028
+ correlationId,
2029
+ AUTHORIZATION_POLICY_VERSION,
2030
+ createDataLifecyclePseudonymousReference({
2031
+ categoryId: 'authorization-subject',
2032
+ slice: '${name}',
2033
+ value: principal.actor.id,
2034
+ }),
2035
+ createDataLifecyclePseudonymousReference({
2036
+ categoryId: 'lifecycle-control-metadata',
2037
+ slice: '${name}',
2038
+ value: runId,
2039
+ }),
2040
+ '${name}',
2041
+ required('EBK_STAGE'),
2042
+ ],
2043
+ );
2044
+ }
2045
+
2046
+ async function report(
2047
+ client: InstanceType<typeof Client>,
2048
+ asOf: string,
2049
+ correlationId: string,
2050
+ runId: string,
2051
+ ) {
2052
+ const action = ${identifierPrefix}AuthorizationActions.retentionReport;
2053
+ const principal = await authorizeRetentionOperation(action, asOf, runId);
2054
+ await writeRetentionAuthorizationAudit(
2055
+ client,
2056
+ action,
2057
+ correlationId,
2058
+ asOf,
2059
+ principal,
2060
+ runId,
2061
+ );
2062
+ const adapter = create${pascalName}RetentionAdapter({
2063
+ asOf,
2064
+ client,
2065
+ completedAt: asOf,
2066
+ correlationId,
2067
+ eventBusName: required('EVENT_BUS_NAME'),
2068
+ mode: 'report',
2069
+ publishLifecycleEvent,
2070
+ runId,
2071
+ stage: required('EBK_STAGE'),
2072
+ });
2073
+ let checkpoint: DataLifecycleRetentionCheckpointV1 | undefined;
2074
+ let due = 0;
2075
+ let pages = 0;
2076
+ let status: 'complete' | 'more' | 'retry-required' = 'more';
2077
+ while (status === 'more' && pages < MAX_PAGES) {
2078
+ const result = await runDataLifecycleRetention({
2079
+ adapter,
2080
+ asOf,
2081
+ checkpoint,
2082
+ limit: PAGE_SIZE,
2083
+ mode: 'report',
2084
+ policyVersion: POLICY_VERSION,
2085
+ });
2086
+ checkpoint = result.checkpoint;
2087
+ due += result.due;
2088
+ status = result.status;
2089
+ pages += 1;
2090
+ }
2091
+ if (status !== 'complete' || !checkpoint?.complete) {
2092
+ throw new Error('Retention report exceeded its bounded execution.');
2093
+ }
2094
+ const plan: RetentionPlan = {
2095
+ asOf,
2096
+ candidateDigest: adapter.candidateDigest(),
2097
+ categoryId: 'product-record',
2098
+ due,
2099
+ locationId: 'primary-database',
2100
+ maxItems: 10000,
2101
+ pageSize: 100,
2102
+ policyVersion: POLICY_VERSION,
2103
+ retentionDays: RETENTION_DAYS,
2104
+ runId,
2105
+ };
2106
+ if (!SHA256.test(plan.candidateDigest)) {
2107
+ throw new Error('Retention report candidate digest is invalid.');
2108
+ }
2109
+ const counts: RetentionCounts = {
2110
+ alreadyApplied: 0,
2111
+ applied: 0,
2112
+ applyPages: 0,
2113
+ due,
2114
+ reportPages: pages,
2115
+ };
2116
+ await client.query('BEGIN');
2117
+ try {
2118
+ await client.query(
2119
+ \`INSERT INTO data_lifecycle_retention_runs
2120
+ (id, as_of, checkpoint_json, counts_json, expires_at, plan_json,
2121
+ policy_version, revision, status)
2122
+ VALUES ($1::uuid, $2, $3::jsonb, $4::jsonb, $5, $6::jsonb,
2123
+ $7, 0, 'reported')\`,
2124
+ [
2125
+ runId,
2126
+ asOf,
2127
+ JSON.stringify(createDataLifecycleRetentionCheckpoint(POLICY_VERSION)),
2128
+ JSON.stringify(counts),
2129
+ addDays(asOf, 400),
2130
+ JSON.stringify(plan),
2131
+ POLICY_VERSION,
2132
+ ],
2133
+ );
2134
+ const candidates = adapter.candidateRecords();
2135
+ if (candidates.length !== due) {
2136
+ throw new Error('Retention report candidate inventory is incomplete.');
2137
+ }
2138
+ for (const candidate of candidates) {
2139
+ await client.query(
2140
+ \`INSERT INTO data_lifecycle_retention_candidates
2141
+ (run_id, candidate_order, record_id, record_ref, due_at)
2142
+ VALUES ($1::uuid, $2, $3::uuid, $4, $5)\`,
2143
+ [
2144
+ runId,
2145
+ candidate.candidateOrder,
2146
+ candidate.recordId,
2147
+ candidate.recordRef,
2148
+ candidate.dueAt,
2149
+ ],
2150
+ );
2151
+ }
2152
+ await client.query('COMMIT');
2153
+ } catch (error) {
2154
+ await client.query('ROLLBACK').catch(() => undefined);
2155
+ throw error;
2156
+ }
2157
+ return {
2158
+ counts,
2159
+ planDigest:
2160
+ 'sha256:' +
2161
+ digestApprovalRequest({
2162
+ action: ACTION,
2163
+ command: plan,
2164
+ resource: { id: runId, type: 'retention-run' },
2165
+ }),
2166
+ runRef: runId,
2167
+ };
2168
+ }
2169
+
2170
+ async function loadRun(
2171
+ client: InstanceType<typeof Client>,
2172
+ approvalRequestId: string,
2173
+ runId: string,
2174
+ ): Promise<{
2175
+ approval: ApprovalRequest<string>;
2176
+ run: RetentionRunRow;
2177
+ }> {
2178
+ const result = await client.query<{
2179
+ approvalJson: ApprovalRequest<string>;
2180
+ approvalRequestId: string;
2181
+ checkpointJson: DataLifecycleRetentionCheckpointV1;
2182
+ countsJson: RetentionCounts;
2183
+ planJson: RetentionPlan;
2184
+ revision: number;
2185
+ status: string;
2186
+ }>(
2187
+ \`SELECT a.request_json AS "approvalJson",
2188
+ r.approval_request_id::text AS "approvalRequestId",
2189
+ r.checkpoint_json AS "checkpointJson",
2190
+ r.counts_json AS "countsJson",
2191
+ r.plan_json AS "planJson",
2192
+ r.revision,
2193
+ r.status
2194
+ FROM data_lifecycle_retention_runs r
2195
+ JOIN approval_requests a ON a.id = r.approval_request_id
2196
+ WHERE r.id = $1::uuid AND a.id = $2::uuid\`,
2197
+ [runId, approvalRequestId],
2198
+ );
2199
+ const row = result.rows[0];
2200
+ if (!row) throw new Error('Retention run and approval do not match.');
2201
+ return {
2202
+ approval: row.approvalJson,
2203
+ run: {
2204
+ approvalRequestId: row.approvalRequestId,
2205
+ checkpointJson: row.checkpointJson,
2206
+ countsJson: row.countsJson,
2207
+ planJson: row.planJson,
2208
+ revision: row.revision,
2209
+ status: row.status,
2210
+ },
2211
+ };
2212
+ }
2213
+
2214
+ async function validateApprovedPlan(
2215
+ client: InstanceType<typeof Client>,
2216
+ correlationId: string,
2217
+ plan: RetentionPlan,
2218
+ ) {
2219
+ const adapter = create${pascalName}RetentionAdapter({
2220
+ asOf: plan.asOf,
2221
+ client,
2222
+ completedAt: plan.asOf,
2223
+ correlationId,
2224
+ eventBusName: required('EVENT_BUS_NAME'),
2225
+ mode: 'apply',
2226
+ publishLifecycleEvent,
2227
+ runId: plan.runId,
2228
+ stage: required('EBK_STAGE'),
2229
+ });
2230
+ let checkpoint: DataLifecycleRetentionCheckpointV1 | undefined;
2231
+ let due = 0;
2232
+ let pages = 0;
2233
+ let status: 'complete' | 'more' | 'retry-required' = 'more';
2234
+ while (status === 'more' && pages < MAX_PAGES) {
2235
+ const result = await runDataLifecycleRetention({
2236
+ adapter,
2237
+ asOf: plan.asOf,
2238
+ checkpoint,
2239
+ limit: PAGE_SIZE,
2240
+ mode: 'report',
2241
+ policyVersion: POLICY_VERSION,
2242
+ });
2243
+ checkpoint = result.checkpoint;
2244
+ due += result.due;
2245
+ pages += 1;
2246
+ status = result.status;
2247
+ }
2248
+ if (
2249
+ status !== 'complete' ||
2250
+ !checkpoint?.complete ||
2251
+ due !== plan.due ||
2252
+ adapter.candidateDigest() !== plan.candidateDigest
2253
+ ) {
2254
+ throw new Error(
2255
+ 'Retention candidates changed after review; create and approve a new report.',
2256
+ );
2257
+ }
2258
+ }
2259
+
2260
+ async function apply(
2261
+ client: InstanceType<typeof Client>,
2262
+ approvalRequestId: string,
2263
+ correlationId: string,
2264
+ now: string,
2265
+ runId: string,
2266
+ ) {
2267
+ const lock = await client.query<{ locked: boolean }>(
2268
+ \`SELECT pg_try_advisory_lock(hashtext($1)) AS locked\`,
2269
+ ['data-retention:' + runId],
2270
+ );
2271
+ if (!lock.rows[0]?.locked) {
2272
+ throw new Error('The retention run is already active.');
2273
+ }
2274
+ try {
2275
+ let { approval, run } = await loadRun(
2276
+ client,
2277
+ approvalRequestId,
2278
+ runId,
2279
+ );
2280
+ if (
2281
+ run.planJson.runId !== runId ||
2282
+ run.planJson.policyVersion !== POLICY_VERSION ||
2283
+ run.planJson.maxItems !== 10000 ||
2284
+ run.planJson.pageSize !== 100 ||
2285
+ !SHA256.test(run.planJson.candidateDigest) ||
2286
+ approval.id !== approvalRequestId ||
2287
+ approval.policyVersion !== AUTHORIZATION_POLICY_VERSION ||
2288
+ approval.schemaVersion !== 1 ||
2289
+ approval.slice !== '${name}' ||
2290
+ digestApprovalRequest({
2291
+ action: ACTION,
2292
+ command: run.planJson,
2293
+ resource: { id: runId, type: 'retention-run' },
2294
+ }) !== approval.requestDigest
2295
+ ) {
2296
+ throw new Error('Retention approval is not bound to the exact plan.');
2297
+ }
2298
+ if (
2299
+ run.status !== 'approval-approved' &&
2300
+ run.status !== 'retry-required' &&
2301
+ run.status !== 'applying'
2302
+ ) {
2303
+ throw new Error('Retention run is not approved for apply.');
2304
+ }
2305
+ const claimed = beginApprovalExecution(approval, {
2306
+ action: ACTION,
2307
+ command: run.planJson,
2308
+ executionId: runId,
2309
+ now,
2310
+ requiredPolicyVersion: AUTHORIZATION_POLICY_VERSION,
2311
+ resource: { id: runId, type: 'retention-run' },
2312
+ });
2313
+ if (claimed.decision.decision !== 'allow') {
2314
+ throw new Error(
2315
+ 'Retention apply approval was denied: ' +
2316
+ claimed.decision.category,
2317
+ );
2318
+ }
2319
+ const principal = await authorizeRetentionOperation(
2320
+ ${identifierPrefix}AuthorizationActions.retentionApply,
2321
+ now,
2322
+ runId,
2323
+ );
2324
+ if (run.status === 'approval-approved') {
2325
+ await validateApprovedPlan(client, correlationId, run.planJson);
2326
+ }
2327
+ await client.query('BEGIN');
2328
+ try {
2329
+ if (claimed.request.revision !== approval.revision) {
2330
+ const updated = await client.query(
2331
+ \`UPDATE approval_requests
2332
+ SET request_json = $3::jsonb, revision = $4, status = $5
2333
+ WHERE id = $1::uuid AND revision = $2\`,
2334
+ [
2335
+ approval.id,
2336
+ approval.revision,
2337
+ JSON.stringify(claimed.request),
2338
+ claimed.request.revision,
2339
+ claimed.request.status,
2340
+ ],
2341
+ );
2342
+ if (updated.rowCount !== 1) {
2343
+ throw new Error('Retention approval claim lost compare-and-set.');
2344
+ }
2345
+ }
2346
+ const started = await client.query(
2347
+ \`UPDATE data_lifecycle_retention_runs
2348
+ SET revision = revision + 1, status = 'applying', updated_at = $3
2349
+ WHERE id = $1::uuid AND revision = $2
2350
+ AND status IN ('approval-approved', 'retry-required', 'applying')\`,
2351
+ [runId, run.revision, now],
2352
+ );
2353
+ if (started.rowCount !== 1) {
2354
+ throw new Error('Retention run claim lost compare-and-set.');
2355
+ }
2356
+ await writeRetentionAuthorizationAudit(
2357
+ client,
2358
+ ${identifierPrefix}AuthorizationActions.retentionApply,
2359
+ correlationId,
2360
+ now,
2361
+ principal,
2362
+ runId,
2363
+ );
2364
+ await client.query('COMMIT');
2365
+ } catch (error) {
2366
+ await client.query('ROLLBACK').catch(() => undefined);
2367
+ throw error;
2368
+ }
2369
+ approval = claimed.request;
2370
+ run = (await loadRun(client, approvalRequestId, runId)).run;
2371
+ const adapter = create${pascalName}RetentionAdapter({
2372
+ approvalRequestId,
2373
+ asOf: run.planJson.asOf,
2374
+ client,
2375
+ completedAt: now,
2376
+ correlationId,
2377
+ eventBusName: required('EVENT_BUS_NAME'),
2378
+ mode: 'apply',
2379
+ publishLifecycleEvent,
2380
+ runId,
2381
+ stage: required('EBK_STAGE'),
2382
+ });
2383
+ let checkpoint = run.checkpointJson;
2384
+ const counts = { ...run.countsJson };
2385
+ let invocationPages = 0;
2386
+ let status: 'complete' | 'more' | 'retry-required' = 'more';
2387
+ while (status === 'more' && invocationPages < MAX_PAGES) {
2388
+ const result = await runDataLifecycleRetention({
2389
+ adapter,
2390
+ asOf: run.planJson.asOf,
2391
+ checkpoint,
2392
+ limit: PAGE_SIZE,
2393
+ mode: 'apply',
2394
+ policyVersion: POLICY_VERSION,
2395
+ });
2396
+ checkpoint = result.checkpoint;
2397
+ counts.alreadyApplied += result.alreadyApplied;
2398
+ counts.applied += result.applied;
2399
+ counts.applyPages += 1;
2400
+ invocationPages += 1;
2401
+ status = result.status;
2402
+ const persisted = await client.query(
2403
+ \`UPDATE data_lifecycle_retention_runs
2404
+ SET checkpoint_json = $2::jsonb, counts_json = $3::jsonb,
2405
+ revision = revision + 1, status = $4, updated_at = $5
2406
+ WHERE id = $1::uuid\`,
2407
+ [
2408
+ runId,
2409
+ JSON.stringify(checkpoint),
2410
+ JSON.stringify(counts),
2411
+ status === 'retry-required' ? 'retry-required' : 'applying',
2412
+ now,
2413
+ ],
2414
+ );
2415
+ if (persisted.rowCount !== 1) {
2416
+ throw new Error('Retention checkpoint could not be persisted.');
2417
+ }
2418
+ }
2419
+ if (status !== 'complete' || !checkpoint.complete) {
2420
+ throw new Error(
2421
+ status === 'retry-required'
2422
+ ? 'Retention adapter requires a safe checkpoint retry.'
2423
+ : 'Retention apply exceeded its bounded execution.',
2424
+ );
2425
+ }
2426
+ const completed = completeApprovalExecution(approval, {
2427
+ executionId: runId,
2428
+ now,
2429
+ requiredPolicyVersion: AUTHORIZATION_POLICY_VERSION,
2430
+ });
2431
+ if (completed.decision.decision !== 'allow') {
2432
+ throw new Error('Retention approval completion was denied.');
2433
+ }
2434
+ await client.query('BEGIN');
2435
+ try {
2436
+ const approvalUpdated = await client.query(
2437
+ \`UPDATE approval_requests
2438
+ SET request_json = $3::jsonb, revision = $4, status = $5
2439
+ WHERE id = $1::uuid AND revision = $2\`,
2440
+ [
2441
+ approval.id,
2442
+ approval.revision,
2443
+ JSON.stringify(completed.request),
2444
+ completed.request.revision,
2445
+ completed.request.status,
2446
+ ],
2447
+ );
2448
+ const runUpdated = await client.query(
2449
+ \`UPDATE data_lifecycle_retention_runs
2450
+ SET checkpoint_json = $2::jsonb, completed_at = $3,
2451
+ counts_json = $4::jsonb, revision = revision + 1,
2452
+ status = 'completed', updated_at = $3
2453
+ WHERE id = $1::uuid AND status = 'applying'\`,
2454
+ [runId, JSON.stringify(checkpoint), now, JSON.stringify(counts)],
2455
+ );
2456
+ const candidatesDeleted = await client.query(
2457
+ \`DELETE FROM data_lifecycle_retention_candidates
2458
+ WHERE run_id = $1::uuid\`,
2459
+ [runId],
2460
+ );
2461
+ if (approvalUpdated.rowCount !== 1 || runUpdated.rowCount !== 1) {
2462
+ throw new Error('Retention completion lost compare-and-set.');
2463
+ }
2464
+ if (candidatesDeleted.rowCount !== run.planJson.due) {
2465
+ throw new Error('Retention candidate cleanup was incomplete.');
2466
+ }
2467
+ await client.query('COMMIT');
2468
+ } catch (error) {
2469
+ await client.query('ROLLBACK').catch(() => undefined);
2470
+ throw error;
2471
+ }
2472
+ return {
2473
+ approvalRequestId: approval.id,
2474
+ checkpointComplete: true,
2475
+ checkpointDigest:
2476
+ 'sha256:' +
2477
+ digestApprovalRequest({
2478
+ action: 'checkpoint',
2479
+ command: checkpoint,
2480
+ resource: { id: runId, type: 'retention-run' },
2481
+ }),
2482
+ counts,
2483
+ runRef: runId,
2484
+ };
2485
+ } finally {
2486
+ await client
2487
+ .query(\`SELECT pg_advisory_unlock(hashtext($1))\`, [
2488
+ 'data-retention:' + runId,
2489
+ ])
2490
+ .catch(() => undefined);
2491
+ }
2492
+ }
2493
+
2494
+ export async function runDataRetentionJob() {
2495
+ const mode = required('EBK_RETENTION_MODE');
2496
+ if (mode !== 'report' && mode !== 'apply') {
2497
+ throw new Error('EBK_RETENTION_MODE must be report or apply.');
2498
+ }
2499
+ const asOf = requiredInstant('EBK_RETENTION_AS_OF');
2500
+ const now = new Date().toISOString();
2501
+ const runId = requiredUuid('EBK_RETENTION_RUN_ID');
2502
+ const approvalRequestId = optionalUuid('EBK_RETENTION_APPROVAL_ID');
2503
+ if (mode === 'apply' && !approvalRequestId) {
2504
+ throw new Error('Retention apply requires its durable approval request.');
2505
+ }
2506
+ if (mode === 'report' && approvalRequestId) {
2507
+ throw new Error('Retention report cannot select an approval request.');
2508
+ }
2509
+ const token = required('EBK_STEP_FUNCTIONS_TASK_TOKEN');
2510
+ const correlationId = requiredUuid('EBK_RETENTION_CORRELATION_ID');
2511
+ const logger = createLogger({
2512
+ component: 'job-data-retention',
2513
+ feature: 'data-lifecycle',
2514
+ operation: 'retention-' + mode,
2515
+ slice: '${name}',
2516
+ stage: required('EBK_STAGE'),
2517
+ workspace: required('EBK_WORKSPACE'),
2518
+ });
2519
+ const secretArn = required('EBK_READWRITE_SECRET_ARN');
2520
+ const credential = await readCredential(secretArn, correlationId);
2521
+ const ca = await readFile(required('EBK_RDS_CA_BUNDLE'), 'utf8');
2522
+ const client = new Client({
2523
+ ...credential,
2524
+ connectionTimeoutMillis: 10_000,
2525
+ ssl: { ca, rejectUnauthorized: true },
2526
+ });
2527
+ const sfn = new SFNClient({});
2528
+ try {
2529
+ await client.connect();
2530
+ const result =
2531
+ mode === 'report'
2532
+ ? await report(client, asOf, correlationId, runId)
2533
+ : await apply(
2534
+ client,
2535
+ approvalRequestId as string,
2536
+ correlationId,
2537
+ now,
2538
+ runId,
2539
+ );
2540
+ const completedAt = new Date().toISOString();
2541
+ const evidence = createDataLifecycleEvidence({
2542
+ categoryId: 'product-record',
2543
+ completedAt,
2544
+ counts: {
2545
+ ...result.counts,
2546
+ },
2547
+ operation: mode === 'apply' ? 'retention-apply' : 'retention-report',
2548
+ outcome: 'succeeded',
2549
+ policyVersion: POLICY_VERSION,
2550
+ ...('approvalRequestId' in result
2551
+ ? { requestRef: approvalReference(result.approvalRequestId) }
2552
+ : {}),
2553
+ startedAt: mode === 'report' ? asOf : now,
2554
+ summary:
2555
+ mode === 'apply'
2556
+ ? 'AWS-internal bounded retention apply completed under durable non-author approval.'
2557
+ : 'AWS-internal bounded retention report completed without product-data mutation; request and non-author approval remain required.',
2558
+ systemIds: ['primary-database', 'lifecycle-control'],
2559
+ });
2560
+ const output = JSON.stringify({ evidence, ...result });
2561
+ await sfn.send(
2562
+ new SendTaskSuccessCommand({ output, taskToken: token }),
2563
+ );
2564
+ logger.info('Data retention Job completed', {
2565
+ attributes: {
2566
+ applied: result.counts.applied,
2567
+ due: result.counts.due,
2568
+ mode,
2569
+ applyPages: result.counts.applyPages,
2570
+ reportPages: result.counts.reportPages,
2571
+ },
2572
+ outcome: 'success',
2573
+ });
2574
+ return { evidence, ...result };
2575
+ } catch (error) {
2576
+ logger.error('Data retention Job failed', {
2577
+ attributes: { errorCode: safeErrorCode(error), mode },
2578
+ outcome: 'failure',
2579
+ });
2580
+ await sfn
2581
+ .send(
2582
+ new SendTaskFailureCommand({
2583
+ cause: JSON.stringify({
2584
+ code: safeErrorCode(error),
2585
+ runRef: runId,
2586
+ }),
2587
+ error: 'DataRetentionJobFailed',
2588
+ taskToken: token,
2589
+ }),
2590
+ )
2591
+ .catch(() => undefined);
2592
+ throw error;
2593
+ } finally {
2594
+ await client.end().catch(() => undefined);
2595
+ }
2596
+ }
2597
+
2598
+ if (import.meta.url === new URL(process.argv[1] ?? '', 'file:').href) {
2599
+ await runDataRetentionJob();
2600
+ }
2601
+ `,
2602
+ };
2603
+ }
2604
+ //# sourceMappingURL=built-in-plan-data-retention-job-files.js.map