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

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