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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (824) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +113 -3
  3. package/executors.json +30 -0
  4. package/package.json +84 -6
  5. package/src/api-contract/baseline-update.d.ts +82 -0
  6. package/src/api-contract/baseline-update.js +225 -0
  7. package/src/api-contract/baseline-update.js.map +1 -0
  8. package/src/api-contract/compatibility.d.ts +30 -0
  9. package/src/api-contract/compatibility.js +643 -0
  10. package/src/api-contract/compatibility.js.map +1 -0
  11. package/src/api-contract/contract-graph.d.ts +35 -0
  12. package/src/api-contract/contract-graph.js +141 -0
  13. package/src/api-contract/contract-graph.js.map +1 -0
  14. package/src/api-contract/evidence.d.ts +23 -0
  15. package/src/api-contract/evidence.js +87 -0
  16. package/src/api-contract/evidence.js.map +1 -0
  17. package/src/api-contract/index.d.ts +6 -0
  18. package/src/api-contract/index.js +10 -0
  19. package/src/api-contract/index.js.map +1 -0
  20. package/src/api-contract/model.d.ts +144 -0
  21. package/src/api-contract/model.fixtures.d.ts +2 -0
  22. package/src/api-contract/model.fixtures.js +81 -0
  23. package/src/api-contract/model.fixtures.js.map +1 -0
  24. package/src/api-contract/model.js +993 -0
  25. package/src/api-contract/model.js.map +1 -0
  26. package/src/api-contract/rollout.d.ts +32 -0
  27. package/src/api-contract/rollout.js +88 -0
  28. package/src/api-contract/rollout.js.map +1 -0
  29. package/src/authorization/compatibility.d.ts +62 -0
  30. package/src/authorization/compatibility.js +175 -0
  31. package/src/authorization/compatibility.js.map +1 -0
  32. package/src/authorization/index.d.ts +1 -0
  33. package/src/authorization/index.js +5 -0
  34. package/src/authorization/index.js.map +1 -0
  35. package/src/blueprints/execution.d.ts +25 -1
  36. package/src/blueprints/execution.js +246 -83
  37. package/src/blueprints/execution.js.map +1 -1
  38. package/src/blueprints/external-sandbox.d.ts +53 -0
  39. package/src/blueprints/external-sandbox.js +1182 -0
  40. package/src/blueprints/external-sandbox.js.map +1 -0
  41. package/src/blueprints/manifest.d.ts +42 -2
  42. package/src/blueprints/manifest.js +82 -27
  43. package/src/blueprints/manifest.js.map +1 -1
  44. package/src/blueprints/resolver.d.ts +14 -5
  45. package/src/blueprints/resolver.js +228 -68
  46. package/src/blueprints/resolver.js.map +1 -1
  47. package/src/cli/agent-command.d.ts +30 -0
  48. package/src/cli/agent-command.js +204 -0
  49. package/src/cli/agent-command.js.map +1 -0
  50. package/src/cli/agent-runtime.d.ts +25 -0
  51. package/src/cli/agent-runtime.js +488 -0
  52. package/src/cli/agent-runtime.js.map +1 -0
  53. package/src/cli/blueprint-authority.d.ts +9 -0
  54. package/src/cli/blueprint-authority.js +43 -0
  55. package/src/cli/blueprint-authority.js.map +1 -0
  56. package/src/cli/blueprint-command.d.ts +24 -0
  57. package/src/cli/blueprint-command.js +63 -0
  58. package/src/cli/blueprint-command.js.map +1 -0
  59. package/src/cli/bootstrap.d.ts +31 -0
  60. package/src/cli/bootstrap.js +165 -0
  61. package/src/cli/bootstrap.js.map +1 -0
  62. package/src/cli/dev-command.d.ts +92 -0
  63. package/src/cli/dev-command.js +561 -0
  64. package/src/cli/dev-command.js.map +1 -0
  65. package/src/cli/main.d.ts +131 -0
  66. package/src/cli/main.js +1199 -0
  67. package/src/cli/main.js.map +1 -0
  68. package/src/cli/output.d.ts +19 -0
  69. package/src/cli/output.js +51 -0
  70. package/src/cli/output.js.map +1 -0
  71. package/src/cli/personal-secret-runtime.d.ts +81 -0
  72. package/src/cli/personal-secret-runtime.js +567 -0
  73. package/src/cli/personal-secret-runtime.js.map +1 -0
  74. package/src/cli/secret-terminal.d.ts +12 -0
  75. package/src/cli/secret-terminal.js +128 -0
  76. package/src/cli/secret-terminal.js.map +1 -0
  77. package/src/container-images.d.ts +1 -0
  78. package/src/container-images.js +5 -0
  79. package/src/container-images.js.map +1 -0
  80. package/src/data-lifecycle/index.d.ts +3 -0
  81. package/src/data-lifecycle/index.js +7 -0
  82. package/src/data-lifecycle/index.js.map +1 -0
  83. package/src/data-lifecycle/model.d.ts +172 -0
  84. package/src/data-lifecycle/model.fixtures.d.ts +2 -0
  85. package/src/data-lifecycle/model.fixtures.js +244 -0
  86. package/src/data-lifecycle/model.fixtures.js.map +1 -0
  87. package/src/data-lifecycle/model.js +95 -0
  88. package/src/data-lifecycle/model.js.map +1 -0
  89. package/src/data-lifecycle/sensitive-artifacts.d.ts +28 -0
  90. package/src/data-lifecycle/sensitive-artifacts.js +146 -0
  91. package/src/data-lifecycle/sensitive-artifacts.js.map +1 -0
  92. package/src/data-lifecycle/validation.d.ts +32 -0
  93. package/src/data-lifecycle/validation.js +837 -0
  94. package/src/data-lifecycle/validation.js.map +1 -0
  95. package/src/database-retirement/contract.d.ts +68 -0
  96. package/src/database-retirement/contract.js +166 -0
  97. package/src/database-retirement/contract.js.map +1 -0
  98. package/src/database-retirement/index.d.ts +1 -0
  99. package/src/database-retirement/index.js +5 -0
  100. package/src/database-retirement/index.js.map +1 -0
  101. package/src/deterministic-order.d.ts +2 -0
  102. package/src/deterministic-order.js +7 -0
  103. package/src/deterministic-order.js.map +1 -0
  104. package/src/developer-agent/context.d.ts +27 -0
  105. package/src/developer-agent/context.js +497 -0
  106. package/src/developer-agent/context.js.map +1 -0
  107. package/src/developer-agent/generator-planning.d.ts +34 -0
  108. package/src/developer-agent/generator-planning.js +645 -0
  109. package/src/developer-agent/generator-planning.js.map +1 -0
  110. package/src/developer-agent/generator-preview.d.ts +8 -0
  111. package/src/developer-agent/generator-preview.js +650 -0
  112. package/src/developer-agent/generator-preview.js.map +1 -0
  113. package/src/developer-agent/git-inspection.d.ts +108 -0
  114. package/src/developer-agent/git-inspection.js +815 -0
  115. package/src/developer-agent/git-inspection.js.map +1 -0
  116. package/src/developer-agent/handoff.d.ts +43 -0
  117. package/src/developer-agent/handoff.js +698 -0
  118. package/src/developer-agent/handoff.js.map +1 -0
  119. package/src/developer-agent/index.d.ts +10 -0
  120. package/src/developer-agent/index.js +14 -0
  121. package/src/developer-agent/index.js.map +1 -0
  122. package/src/developer-agent/model.d.ts +481 -0
  123. package/src/developer-agent/model.js +143 -0
  124. package/src/developer-agent/model.js.map +1 -0
  125. package/src/developer-agent/operation-registry.d.ts +45 -0
  126. package/src/developer-agent/operation-registry.js +1255 -0
  127. package/src/developer-agent/operation-registry.js.map +1 -0
  128. package/src/developer-agent/ordering.d.ts +9 -0
  129. package/src/developer-agent/ordering.js +15 -0
  130. package/src/developer-agent/ordering.js.map +1 -0
  131. package/src/developer-agent/planning.d.ts +18 -0
  132. package/src/developer-agent/planning.js +447 -0
  133. package/src/developer-agent/planning.js.map +1 -0
  134. package/src/developer-agent/preview-changes.d.ts +26 -0
  135. package/src/developer-agent/preview-changes.js +203 -0
  136. package/src/developer-agent/preview-changes.js.map +1 -0
  137. package/src/developer-agent/preview-source.d.ts +2 -0
  138. package/src/developer-agent/preview-source.js +16 -0
  139. package/src/developer-agent/preview-source.js.map +1 -0
  140. package/src/developer-agent/validation.d.ts +18 -0
  141. package/src/developer-agent/validation.js +2617 -0
  142. package/src/developer-agent/validation.js.map +1 -0
  143. package/src/developer-agent/workspace-inspection.d.ts +20 -0
  144. package/src/developer-agent/workspace-inspection.js +1134 -0
  145. package/src/developer-agent/workspace-inspection.js.map +1 -0
  146. package/src/developer-agent/workspace-path.d.ts +6 -0
  147. package/src/developer-agent/workspace-path.js +22 -0
  148. package/src/developer-agent/workspace-path.js.map +1 -0
  149. package/src/executors/blueprint-trust/blueprint-trust.d.ts +29 -0
  150. package/src/executors/blueprint-trust/blueprint-trust.js +115 -0
  151. package/src/executors/blueprint-trust/blueprint-trust.js.map +1 -0
  152. package/src/executors/blueprint-trust/schema.json +27 -0
  153. package/src/executors/blueprint-verification/blueprint-verification.d.ts +13 -0
  154. package/src/executors/blueprint-verification/blueprint-verification.js +110 -0
  155. package/src/executors/blueprint-verification/blueprint-verification.js.map +1 -0
  156. package/src/executors/blueprint-verification/schema.json +44 -0
  157. package/src/executors/deployment-plan/deployment-plan.js +1 -0
  158. package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
  159. package/src/executors/dev-doctor/dev-doctor.d.ts +19 -5
  160. package/src/executors/dev-doctor/dev-doctor.js +186 -32
  161. package/src/executors/dev-doctor/dev-doctor.js.map +1 -1
  162. package/src/executors/dev-doctor/schema.json +1 -6
  163. package/src/executors/dev-setup/dev-setup.d.ts +7 -1
  164. package/src/executors/dev-setup/dev-setup.js +14 -3
  165. package/src/executors/dev-setup/dev-setup.js.map +1 -1
  166. package/src/executors/developer-context/developer-context.d.ts +38 -0
  167. package/src/executors/developer-context/developer-context.js +78 -0
  168. package/src/executors/developer-context/developer-context.js.map +1 -0
  169. package/src/executors/developer-context/schema.json +87 -0
  170. package/src/executors/fleet/fleet.d.ts +12 -43
  171. package/src/executors/fleet/fleet.js +120 -500
  172. package/src/executors/fleet/fleet.js.map +1 -1
  173. package/src/executors/fleet/schema.json +64 -14
  174. package/src/executors/github-bootstrap/github-bootstrap.d.ts +1 -0
  175. package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
  176. package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
  177. package/src/executors/github-bootstrap/schema.json +5 -0
  178. package/src/executors/infrastructure-plan/infrastructure-plan.d.ts +144 -0
  179. package/src/executors/infrastructure-plan/infrastructure-plan.js +3320 -0
  180. package/src/executors/infrastructure-plan/infrastructure-plan.js.map +1 -0
  181. package/src/executors/infrastructure-plan/schema.json +78 -0
  182. package/src/executors/secret-guardrails/schema.json +6 -0
  183. package/src/executors/secret-guardrails/secret-guardrails.d.ts +3 -0
  184. package/src/executors/secret-guardrails/secret-guardrails.js +44 -0
  185. package/src/executors/secret-guardrails/secret-guardrails.js.map +1 -0
  186. package/src/executors/secret-metadata/schema.json +31 -0
  187. package/src/executors/secret-metadata/secret-metadata.d.ts +32 -0
  188. package/src/executors/secret-metadata/secret-metadata.js +131 -0
  189. package/src/executors/secret-metadata/secret-metadata.js.map +1 -0
  190. package/src/executors/sst-lifecycle/logging-preflight.d.ts +17 -0
  191. package/src/executors/sst-lifecycle/logging-preflight.js +196 -0
  192. package/src/executors/sst-lifecycle/logging-preflight.js.map +1 -0
  193. package/src/executors/sst-lifecycle/run-sst-command.d.ts +23 -3
  194. package/src/executors/sst-lifecycle/run-sst-command.js +113 -10
  195. package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
  196. package/src/executors/sst-lifecycle/schema.json +9 -5
  197. package/src/executors/sst-lifecycle/secret-preflight.d.ts +25 -0
  198. package/src/executors/sst-lifecycle/secret-preflight.js +119 -0
  199. package/src/executors/sst-lifecycle/secret-preflight.js.map +1 -0
  200. package/src/foundation/discovery.d.ts +17 -1
  201. package/src/foundation/discovery.js +16 -1
  202. package/src/foundation/discovery.js.map +1 -1
  203. package/src/foundation/index.d.ts +3 -0
  204. package/src/foundation/index.js +3 -0
  205. package/src/foundation/index.js.map +1 -1
  206. package/src/foundation/local-database-contract.d.ts +1 -0
  207. package/src/foundation/local-database-contract.js +3 -0
  208. package/src/foundation/local-database-contract.js.map +1 -1
  209. package/src/foundation/logging-preflight.d.ts +30 -0
  210. package/src/foundation/logging-preflight.js +138 -0
  211. package/src/foundation/logging-preflight.js.map +1 -0
  212. package/src/foundation/logging.d.ts +70 -0
  213. package/src/foundation/logging.js +133 -0
  214. package/src/foundation/logging.js.map +1 -0
  215. package/src/foundation/preflight.d.ts +9 -0
  216. package/src/foundation/preflight.js +58 -2
  217. package/src/foundation/preflight.js.map +1 -1
  218. package/src/foundation/provision-database-bindings.d.ts +9 -1
  219. package/src/foundation/provision-database-bindings.js +25 -5
  220. package/src/foundation/provision-database-bindings.js.map +1 -1
  221. package/src/foundation/provision-logical-database.d.ts +10 -2
  222. package/src/foundation/provision-logical-database.js +68 -113
  223. package/src/foundation/provision-logical-database.js.map +1 -1
  224. package/src/foundation/router-origin-proof.d.ts +2 -0
  225. package/src/foundation/router-origin-proof.js +25 -0
  226. package/src/foundation/router-origin-proof.js.map +1 -0
  227. package/src/foundation/stages.d.ts +1 -0
  228. package/src/foundation/stages.js +9 -1
  229. package/src/foundation/stages.js.map +1 -1
  230. package/src/frontend-security/index.d.ts +1 -0
  231. package/src/frontend-security/index.js +5 -0
  232. package/src/frontend-security/index.js.map +1 -0
  233. package/src/frontend-security/security-headers.d.ts +50 -0
  234. package/src/frontend-security/security-headers.js +418 -0
  235. package/src/frontend-security/security-headers.js.map +1 -0
  236. package/src/generators/feature-registry.d.ts +13 -0
  237. package/src/generators/feature-registry.js +73 -0
  238. package/src/generators/feature-registry.js.map +1 -0
  239. package/src/generators/function/function.d.ts +1 -1
  240. package/src/generators/function/function.js +51 -6
  241. package/src/generators/function/function.js.map +1 -1
  242. package/src/generators/function/schema.d.ts +1 -0
  243. package/src/generators/function/schema.json +6 -1
  244. package/src/generators/logging-registry.d.ts +13 -0
  245. package/src/generators/logging-registry.js +62 -0
  246. package/src/generators/logging-registry.js.map +1 -0
  247. package/src/generators/post-generation-install.d.ts +67 -0
  248. package/src/generators/post-generation-install.js +3308 -0
  249. package/src/generators/post-generation-install.js.map +1 -0
  250. package/src/generators/preset/generator.js +3 -0
  251. package/src/generators/preset/generator.js.map +1 -1
  252. package/src/generators/preset/tool-versions.d.ts +18 -0
  253. package/src/generators/preset/tool-versions.js +22 -0
  254. package/src/generators/preset/tool-versions.js.map +1 -0
  255. package/src/generators/preset/workspace-files-developer-agent.d.ts +14 -0
  256. package/src/generators/preset/workspace-files-developer-agent.js +511 -0
  257. package/src/generators/preset/workspace-files-developer-agent.js.map +1 -0
  258. package/src/generators/preset/workspace-files-foundation-workload-roles.d.ts +6 -0
  259. package/src/generators/preset/workspace-files-foundation-workload-roles.js +406 -0
  260. package/src/generators/preset/workspace-files-foundation-workload-roles.js.map +1 -0
  261. package/src/generators/preset/workspace-files-performance-cost.d.ts +66 -0
  262. package/src/generators/preset/workspace-files-performance-cost.js +866 -0
  263. package/src/generators/preset/workspace-files-performance-cost.js.map +1 -0
  264. package/src/generators/preset/workspace-files-performance-load.d.ts +73 -0
  265. package/src/generators/preset/workspace-files-performance-load.js +801 -0
  266. package/src/generators/preset/workspace-files-performance-load.js.map +1 -0
  267. package/src/generators/preset/workspace-files-workload-role-baseline.d.ts +9 -0
  268. package/src/generators/preset/workspace-files-workload-role-baseline.js +1102 -0
  269. package/src/generators/preset/workspace-files-workload-role-baseline.js.map +1 -0
  270. package/src/generators/preset/workspace-files.d.ts +14 -1
  271. package/src/generators/preset/workspace-files.js +8189 -245
  272. package/src/generators/preset/workspace-files.js.map +1 -1
  273. package/src/generators/repository-policy/repository-policy.js +3 -47
  274. package/src/generators/repository-policy/repository-policy.js.map +1 -1
  275. package/src/generators/slice/built-in-plan-api-contract-files.d.ts +2 -0
  276. package/src/generators/slice/built-in-plan-api-contract-files.js +2656 -0
  277. package/src/generators/slice/built-in-plan-api-contract-files.js.map +1 -0
  278. package/src/generators/slice/built-in-plan-api-files.d.ts +2 -0
  279. package/src/generators/slice/built-in-plan-api-files.js +1186 -0
  280. package/src/generators/slice/built-in-plan-api-files.js.map +1 -0
  281. package/src/generators/slice/built-in-plan-async-work-files.d.ts +2 -0
  282. package/src/generators/slice/built-in-plan-async-work-files.js +7903 -0
  283. package/src/generators/slice/built-in-plan-async-work-files.js.map +1 -0
  284. package/src/generators/slice/built-in-plan-authorization-files.d.ts +2 -0
  285. package/src/generators/slice/built-in-plan-authorization-files.js +2314 -0
  286. package/src/generators/slice/built-in-plan-authorization-files.js.map +1 -0
  287. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.d.ts +2 -0
  288. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +559 -0
  289. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js.map +1 -0
  290. package/src/generators/slice/built-in-plan-data-deletion-files.d.ts +2 -0
  291. package/src/generators/slice/built-in-plan-data-deletion-files.js +1101 -0
  292. package/src/generators/slice/built-in-plan-data-deletion-files.js.map +1 -0
  293. package/src/generators/slice/built-in-plan-data-export-infrastructure-files.d.ts +2 -0
  294. package/src/generators/slice/built-in-plan-data-export-infrastructure-files.js +249 -0
  295. package/src/generators/slice/built-in-plan-data-export-infrastructure-files.js.map +1 -0
  296. package/src/generators/slice/built-in-plan-data-files.d.ts +2 -0
  297. package/src/generators/slice/built-in-plan-data-files.js +1530 -0
  298. package/src/generators/slice/built-in-plan-data-files.js.map +1 -0
  299. package/src/generators/slice/built-in-plan-data-lifecycle-files.d.ts +2 -0
  300. package/src/generators/slice/built-in-plan-data-lifecycle-files.js +750 -0
  301. package/src/generators/slice/built-in-plan-data-lifecycle-files.js.map +1 -0
  302. package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.d.ts +2 -0
  303. package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.js +828 -0
  304. package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.js.map +1 -0
  305. package/src/generators/slice/built-in-plan-data-retention-job-files.d.ts +2 -0
  306. package/src/generators/slice/built-in-plan-data-retention-job-files.js +2604 -0
  307. package/src/generators/slice/built-in-plan-data-retention-job-files.js.map +1 -0
  308. package/src/generators/slice/built-in-plan-data-retention-launcher-files.d.ts +2 -0
  309. package/src/generators/slice/built-in-plan-data-retention-launcher-files.js +144 -0
  310. package/src/generators/slice/built-in-plan-data-retention-launcher-files.js.map +1 -0
  311. package/src/generators/slice/built-in-plan-data-rights-files.d.ts +2 -0
  312. package/src/generators/slice/built-in-plan-data-rights-files.js +4628 -0
  313. package/src/generators/slice/built-in-plan-data-rights-files.js.map +1 -0
  314. package/src/generators/slice/built-in-plan-database-lifecycle-files.d.ts +2 -0
  315. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1901 -0
  316. package/src/generators/slice/built-in-plan-database-lifecycle-files.js.map +1 -0
  317. package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.d.ts +2 -0
  318. package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js +485 -0
  319. package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js.map +1 -0
  320. package/src/generators/slice/built-in-plan-foundation-files.d.ts +2 -0
  321. package/src/generators/slice/built-in-plan-foundation-files.js +184 -0
  322. package/src/generators/slice/built-in-plan-foundation-files.js.map +1 -0
  323. package/src/generators/slice/built-in-plan-frontend-files.d.ts +2 -0
  324. package/src/generators/slice/built-in-plan-frontend-files.js +2233 -0
  325. package/src/generators/slice/built-in-plan-frontend-files.js.map +1 -0
  326. package/src/generators/slice/built-in-plan-infrastructure-files.d.ts +2 -0
  327. package/src/generators/slice/built-in-plan-infrastructure-files.js +3038 -0
  328. package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -0
  329. package/src/generators/slice/built-in-plan-infrastructure-safety-files.d.ts +2 -0
  330. package/src/generators/slice/built-in-plan-infrastructure-safety-files.js +312 -0
  331. package/src/generators/slice/built-in-plan-infrastructure-safety-files.js.map +1 -0
  332. package/src/generators/slice/built-in-plan-metadata-files.d.ts +3 -0
  333. package/src/generators/slice/built-in-plan-metadata-files.js +219 -0
  334. package/src/generators/slice/built-in-plan-metadata-files.js.map +1 -0
  335. package/src/generators/slice/built-in-plan-operations-files.d.ts +2 -0
  336. package/src/generators/slice/built-in-plan-operations-files.js +630 -0
  337. package/src/generators/slice/built-in-plan-operations-files.js.map +1 -0
  338. package/src/generators/slice/built-in-plan-performance-files.d.ts +3 -0
  339. package/src/generators/slice/built-in-plan-performance-files.js +3293 -0
  340. package/src/generators/slice/built-in-plan-performance-files.js.map +1 -0
  341. package/src/generators/slice/built-in-plan-project-files.d.ts +3 -0
  342. package/src/generators/slice/built-in-plan-project-files.js +979 -0
  343. package/src/generators/slice/built-in-plan-project-files.js.map +1 -0
  344. package/src/generators/slice/built-in-plan-release-files.d.ts +2 -0
  345. package/src/generators/slice/built-in-plan-release-files.js +6387 -0
  346. package/src/generators/slice/built-in-plan-release-files.js.map +1 -0
  347. package/src/generators/slice/built-in-plan-reliability-files.d.ts +2 -0
  348. package/src/generators/slice/built-in-plan-reliability-files.js +4499 -0
  349. package/src/generators/slice/built-in-plan-reliability-files.js.map +1 -0
  350. package/src/generators/slice/built-in-plan-secret-runtime-files.d.ts +2 -0
  351. package/src/generators/slice/built-in-plan-secret-runtime-files.js +1004 -0
  352. package/src/generators/slice/built-in-plan-secret-runtime-files.js.map +1 -0
  353. package/src/generators/slice/built-in-plan-shared.d.ts +53 -0
  354. package/src/generators/slice/built-in-plan-shared.js +113 -0
  355. package/src/generators/slice/built-in-plan-shared.js.map +1 -0
  356. package/src/generators/slice/built-in-plan-testing-files.d.ts +2 -0
  357. package/src/generators/slice/built-in-plan-testing-files.js +2356 -0
  358. package/src/generators/slice/built-in-plan-testing-files.js.map +1 -0
  359. package/src/generators/slice/built-in-plan-verification-files.d.ts +2 -0
  360. package/src/generators/slice/built-in-plan-verification-files.js +358 -0
  361. package/src/generators/slice/built-in-plan-verification-files.js.map +1 -0
  362. package/src/generators/slice/built-in-plan-web-files.d.ts +2 -0
  363. package/src/generators/slice/built-in-plan-web-files.js +430 -0
  364. package/src/generators/slice/built-in-plan-web-files.js.map +1 -0
  365. package/src/generators/slice/built-in-plan-workload-role-files.d.ts +2 -0
  366. package/src/generators/slice/built-in-plan-workload-role-files.js +2356 -0
  367. package/src/generators/slice/built-in-plan-workload-role-files.js.map +1 -0
  368. package/src/generators/slice/built-in-plan.d.ts +8 -0
  369. package/src/generators/slice/built-in-plan.js +264 -0
  370. package/src/generators/slice/built-in-plan.js.map +1 -0
  371. package/src/generators/slice/schema.json +1 -1
  372. package/src/generators/slice/slice.d.ts +1 -22
  373. package/src/generators/slice/slice.js +185 -1510
  374. package/src/generators/slice/slice.js.map +1 -1
  375. package/src/generators/upgrade/schema.d.ts +2 -0
  376. package/src/generators/upgrade/schema.json +11 -1
  377. package/src/generators/upgrade/upgrade.d.ts +51 -1
  378. package/src/generators/upgrade/upgrade.js +820 -82
  379. package/src/generators/upgrade/upgrade.js.map +1 -1
  380. package/src/generators/workload/frozen-container.d.ts +8 -0
  381. package/src/generators/workload/frozen-container.js +36 -0
  382. package/src/generators/workload/frozen-container.js.map +1 -0
  383. package/src/generators/workload/performance-registration.d.ts +32 -0
  384. package/src/generators/workload/performance-registration.js +226 -0
  385. package/src/generators/workload/performance-registration.js.map +1 -0
  386. package/src/generators/workload/reliability-registration.d.ts +16 -0
  387. package/src/generators/workload/reliability-registration.js +150 -0
  388. package/src/generators/workload/reliability-registration.js.map +1 -0
  389. package/src/generators/workload/schema.d.ts +1 -0
  390. package/src/generators/workload/schema.json +6 -1
  391. package/src/generators/workload/service-performance-files.d.ts +4 -0
  392. package/src/generators/workload/service-performance-files.js +1846 -0
  393. package/src/generators/workload/service-performance-files.js.map +1 -0
  394. package/src/generators/workload/workload.d.ts +3 -2
  395. package/src/generators/workload/workload.js +1083 -18
  396. package/src/generators/workload/workload.js.map +1 -1
  397. package/src/git-policy/action-pins.d.ts +10 -0
  398. package/src/git-policy/action-pins.js +10 -0
  399. package/src/git-policy/action-pins.js.map +1 -1
  400. package/src/git-policy/github-bootstrap.d.ts +14 -1
  401. package/src/git-policy/github-bootstrap.js +274 -46
  402. package/src/git-policy/github-bootstrap.js.map +1 -1
  403. package/src/index.d.ts +2 -0
  404. package/src/index.js +2 -0
  405. package/src/index.js.map +1 -1
  406. package/src/infrastructure-change/convergence.d.ts +13 -0
  407. package/src/infrastructure-change/convergence.js +85 -0
  408. package/src/infrastructure-change/convergence.js.map +1 -0
  409. package/src/infrastructure-change/drift.d.ts +13 -0
  410. package/src/infrastructure-change/drift.js +112 -0
  411. package/src/infrastructure-change/drift.js.map +1 -0
  412. package/src/infrastructure-change/index.d.ts +7 -0
  413. package/src/infrastructure-change/index.js +11 -0
  414. package/src/infrastructure-change/index.js.map +1 -0
  415. package/src/infrastructure-change/model.d.ts +253 -0
  416. package/src/infrastructure-change/model.js +28 -0
  417. package/src/infrastructure-change/model.js.map +1 -0
  418. package/src/infrastructure-change/normalize.d.ts +12 -0
  419. package/src/infrastructure-change/normalize.js +245 -0
  420. package/src/infrastructure-change/normalize.js.map +1 -0
  421. package/src/infrastructure-change/plan.d.ts +40 -0
  422. package/src/infrastructure-change/plan.js +361 -0
  423. package/src/infrastructure-change/plan.js.map +1 -0
  424. package/src/infrastructure-change/risk.d.ts +7 -0
  425. package/src/infrastructure-change/risk.js +269 -0
  426. package/src/infrastructure-change/risk.js.map +1 -0
  427. package/src/infrastructure-change/summary.d.ts +20 -0
  428. package/src/infrastructure-change/summary.js +130 -0
  429. package/src/infrastructure-change/summary.js.map +1 -0
  430. package/src/local-dev/context-discovery.d.ts +2 -0
  431. package/src/local-dev/context-discovery.js +59 -0
  432. package/src/local-dev/context-discovery.js.map +1 -0
  433. package/src/local-dev/contract-validation.d.ts +15 -0
  434. package/src/local-dev/contract-validation.js +84 -0
  435. package/src/local-dev/contract-validation.js.map +1 -0
  436. package/src/local-dev/contracts.d.ts +10 -1
  437. package/src/local-dev/contracts.js +57 -5
  438. package/src/local-dev/contracts.js.map +1 -1
  439. package/src/local-dev/control-contract.d.ts +67 -0
  440. package/src/local-dev/control-contract.js +180 -0
  441. package/src/local-dev/control-contract.js.map +1 -0
  442. package/src/local-dev/dev-command-core.d.ts +60 -0
  443. package/src/local-dev/dev-command-core.js +89 -0
  444. package/src/local-dev/dev-command-core.js.map +1 -0
  445. package/src/local-dev/effects.d.ts +16 -0
  446. package/src/local-dev/effects.js +63 -0
  447. package/src/local-dev/effects.js.map +1 -0
  448. package/src/local-dev/environment-inspector.d.ts +125 -0
  449. package/src/local-dev/environment-inspector.js +687 -0
  450. package/src/local-dev/environment-inspector.js.map +1 -0
  451. package/src/local-dev/environment-state.d.ts +113 -0
  452. package/src/local-dev/environment-state.js +463 -0
  453. package/src/local-dev/environment-state.js.map +1 -0
  454. package/src/local-dev/errors.d.ts +6 -0
  455. package/src/local-dev/errors.js +41 -0
  456. package/src/local-dev/errors.js.map +1 -0
  457. package/src/local-dev/fleet-client.d.ts +33 -0
  458. package/src/local-dev/fleet-client.js +156 -0
  459. package/src/local-dev/fleet-client.js.map +1 -0
  460. package/src/local-dev/fleet-configuration.d.ts +13 -0
  461. package/src/local-dev/fleet-configuration.js +71 -0
  462. package/src/local-dev/fleet-configuration.js.map +1 -0
  463. package/src/local-dev/fleet-inventory.d.ts +56 -0
  464. package/src/local-dev/fleet-inventory.js +469 -0
  465. package/src/local-dev/fleet-inventory.js.map +1 -0
  466. package/src/local-dev/fleet-ipc.d.ts +28 -0
  467. package/src/local-dev/fleet-ipc.js +267 -0
  468. package/src/local-dev/fleet-ipc.js.map +1 -0
  469. package/src/local-dev/fleet-launcher.d.ts +53 -0
  470. package/src/local-dev/fleet-launcher.js +379 -0
  471. package/src/local-dev/fleet-launcher.js.map +1 -0
  472. package/src/local-dev/fleet-lock.d.ts +41 -0
  473. package/src/local-dev/fleet-lock.js +135 -0
  474. package/src/local-dev/fleet-lock.js.map +1 -0
  475. package/src/local-dev/fleet-logs.d.ts +66 -0
  476. package/src/local-dev/fleet-logs.js +542 -0
  477. package/src/local-dev/fleet-logs.js.map +1 -0
  478. package/src/local-dev/fleet-process.d.ts +50 -0
  479. package/src/local-dev/fleet-process.js +289 -0
  480. package/src/local-dev/fleet-process.js.map +1 -0
  481. package/src/local-dev/fleet-runtime.d.ts +22 -0
  482. package/src/local-dev/fleet-runtime.js +368 -0
  483. package/src/local-dev/fleet-runtime.js.map +1 -0
  484. package/src/local-dev/fleet-session.d.ts +70 -0
  485. package/src/local-dev/fleet-session.js +301 -0
  486. package/src/local-dev/fleet-session.js.map +1 -0
  487. package/src/local-dev/fleet-slice-runner-daemon.d.ts +1 -0
  488. package/src/local-dev/fleet-slice-runner-daemon.js +21 -0
  489. package/src/local-dev/fleet-slice-runner-daemon.js.map +1 -0
  490. package/src/local-dev/fleet-startup.d.ts +22 -0
  491. package/src/local-dev/fleet-startup.js +114 -0
  492. package/src/local-dev/fleet-startup.js.map +1 -0
  493. package/src/local-dev/fleet-state.d.ts +62 -0
  494. package/src/local-dev/fleet-state.js +297 -0
  495. package/src/local-dev/fleet-state.js.map +1 -0
  496. package/src/local-dev/fleet-supervisor-daemon.d.ts +1 -0
  497. package/src/local-dev/fleet-supervisor-daemon.js +41 -0
  498. package/src/local-dev/fleet-supervisor-daemon.js.map +1 -0
  499. package/src/local-dev/fleet-supervisor.d.ts +52 -0
  500. package/src/local-dev/fleet-supervisor.js +384 -0
  501. package/src/local-dev/fleet-supervisor.js.map +1 -0
  502. package/src/local-dev/gateway.js +1 -1
  503. package/src/local-dev/gateway.js.map +1 -1
  504. package/src/local-dev/identity.js +4 -6
  505. package/src/local-dev/identity.js.map +1 -1
  506. package/src/local-dev/index.d.ts +1 -0
  507. package/src/local-dev/index.js +1 -0
  508. package/src/local-dev/index.js.map +1 -1
  509. package/src/local-dev/local-context.d.ts +62 -0
  510. package/src/local-dev/local-context.js +299 -0
  511. package/src/local-dev/local-context.js.map +1 -0
  512. package/src/local-dev/nx-workspace.d.ts +45 -0
  513. package/src/local-dev/nx-workspace.js +228 -0
  514. package/src/local-dev/nx-workspace.js.map +1 -0
  515. package/src/local-dev/personal-secret-ledger.d.ts +75 -0
  516. package/src/local-dev/personal-secret-ledger.js +391 -0
  517. package/src/local-dev/personal-secret-ledger.js.map +1 -0
  518. package/src/local-dev/personal-stage.d.ts +18 -0
  519. package/src/local-dev/personal-stage.js +129 -0
  520. package/src/local-dev/personal-stage.js.map +1 -0
  521. package/src/local-dev/private-state.d.ts +17 -0
  522. package/src/local-dev/private-state.js +254 -0
  523. package/src/local-dev/private-state.js.map +1 -0
  524. package/src/local-dev/process-identity.d.ts +9 -0
  525. package/src/local-dev/process-identity.js +79 -0
  526. package/src/local-dev/process-identity.js.map +1 -0
  527. package/src/local-dev/runtime-paths.d.ts +17 -0
  528. package/src/local-dev/runtime-paths.js +33 -0
  529. package/src/local-dev/runtime-paths.js.map +1 -0
  530. package/src/local-dev/secret-slice-claim.d.ts +17 -0
  531. package/src/local-dev/secret-slice-claim.js +75 -0
  532. package/src/local-dev/secret-slice-claim.js.map +1 -0
  533. package/src/local-dev/slice-claim.d.ts +36 -0
  534. package/src/local-dev/slice-claim.js +199 -0
  535. package/src/local-dev/slice-claim.js.map +1 -0
  536. package/src/local-dev/slice-runner.d.ts +121 -0
  537. package/src/local-dev/slice-runner.js +715 -0
  538. package/src/local-dev/slice-runner.js.map +1 -0
  539. package/src/local-dev/tls.js +51 -17
  540. package/src/local-dev/tls.js.map +1 -1
  541. package/src/ownership/composition.d.ts +5 -2
  542. package/src/ownership/composition.js +26 -3
  543. package/src/ownership/composition.js.map +1 -1
  544. package/src/ownership/hash.d.ts +1 -1
  545. package/src/ownership/hash.js +3 -1
  546. package/src/ownership/hash.js.map +1 -1
  547. package/src/ownership/record.d.ts +32 -1
  548. package/src/ownership/record.js +139 -2
  549. package/src/ownership/record.js.map +1 -1
  550. package/src/performance/cost-collection.d.ts +225 -0
  551. package/src/performance/cost-collection.js +825 -0
  552. package/src/performance/cost-collection.js.map +1 -0
  553. package/src/performance/cost-controls.d.ts +131 -0
  554. package/src/performance/cost-controls.js +336 -0
  555. package/src/performance/cost-controls.js.map +1 -0
  556. package/src/performance/evidence.d.ts +140 -0
  557. package/src/performance/evidence.js +599 -0
  558. package/src/performance/evidence.js.map +1 -0
  559. package/src/performance/index.d.ts +10 -0
  560. package/src/performance/index.js +14 -0
  561. package/src/performance/index.js.map +1 -0
  562. package/src/performance/load-safety.d.ts +84 -0
  563. package/src/performance/load-safety.js +346 -0
  564. package/src/performance/load-safety.js.map +1 -0
  565. package/src/performance/model.d.ts +129 -0
  566. package/src/performance/model.js +452 -0
  567. package/src/performance/model.js.map +1 -0
  568. package/src/performance/pr-artifact-budget.d.ts +42 -0
  569. package/src/performance/pr-artifact-budget.js +151 -0
  570. package/src/performance/pr-artifact-budget.js.map +1 -0
  571. package/src/performance/runner.d.ts +75 -0
  572. package/src/performance/runner.js +227 -0
  573. package/src/performance/runner.js.map +1 -0
  574. package/src/performance/scenario-endpoint.d.ts +1 -0
  575. package/src/performance/scenario-endpoint.js +5 -0
  576. package/src/performance/scenario-endpoint.js.map +1 -0
  577. package/src/performance/scenario.d.ts +193 -0
  578. package/src/performance/scenario.js +736 -0
  579. package/src/performance/scenario.js.map +1 -0
  580. package/src/performance/test-fixtures.d.ts +7 -0
  581. package/src/performance/test-fixtures.js +169 -0
  582. package/src/performance/test-fixtures.js.map +1 -0
  583. package/src/performance/unit-cost.d.ts +105 -0
  584. package/src/performance/unit-cost.js +364 -0
  585. package/src/performance/unit-cost.js.map +1 -0
  586. package/src/release/index.d.ts +4 -0
  587. package/src/release/index.js +8 -0
  588. package/src/release/index.js.map +1 -0
  589. package/src/release/lifecycle.d.ts +132 -0
  590. package/src/release/lifecycle.js +411 -0
  591. package/src/release/lifecycle.js.map +1 -0
  592. package/src/release/manifest.d.ts +148 -0
  593. package/src/release/manifest.js +503 -0
  594. package/src/release/manifest.js.map +1 -0
  595. package/src/release/rollout.d.ts +170 -0
  596. package/src/release/rollout.js +578 -0
  597. package/src/release/rollout.js.map +1 -0
  598. package/src/release/source-authority.d.ts +19 -0
  599. package/src/release/source-authority.js +195 -0
  600. package/src/release/source-authority.js.map +1 -0
  601. package/src/repository-policy/deployment-plan.d.ts +2 -1
  602. package/src/repository-policy/deployment-plan.js +2 -0
  603. package/src/repository-policy/deployment-plan.js.map +1 -1
  604. package/src/repository-policy/policy.d.ts +1 -1
  605. package/src/repository-policy/policy.js +29 -25
  606. package/src/repository-policy/policy.js.map +1 -1
  607. package/src/schemas/aggregate-count-evidence-v1.json +135 -0
  608. package/src/schemas/api-contract-baseline-v1.json +674 -0
  609. package/src/schemas/api-contract-compatibility-report-v1.json +126 -0
  610. package/src/schemas/aws-billing-actual-evidence-v1.json +407 -0
  611. package/src/schemas/blueprint-v1.json +18 -1
  612. package/src/schemas/data-lifecycle-policy-v1.json +517 -0
  613. package/src/schemas/generated-ownership-v2.json +22 -0
  614. package/src/schemas/performance-policy-v1.json +335 -0
  615. package/src/schemas/repository-policy-v1.json +1 -0
  616. package/src/schemas/testing-evidence-quarantine-v1.json +168 -0
  617. package/src/schemas/testing-evidence-result-v1.json +319 -0
  618. package/src/secrets/adapters.d.ts +49 -0
  619. package/src/secrets/adapters.js +184 -0
  620. package/src/secrets/adapters.js.map +1 -0
  621. package/src/secrets/aws-control-plane.d.ts +20 -0
  622. package/src/secrets/aws-control-plane.js +255 -0
  623. package/src/secrets/aws-control-plane.js.map +1 -0
  624. package/src/secrets/declaration-verification.d.ts +11 -0
  625. package/src/secrets/declaration-verification.js +68 -0
  626. package/src/secrets/declaration-verification.js.map +1 -0
  627. package/src/secrets/guardrails.d.ts +26 -0
  628. package/src/secrets/guardrails.js +328 -0
  629. package/src/secrets/guardrails.js.map +1 -0
  630. package/src/secrets/index.d.ts +12 -0
  631. package/src/secrets/index.js +16 -0
  632. package/src/secrets/index.js.map +1 -0
  633. package/src/secrets/metadata.d.ts +48 -0
  634. package/src/secrets/metadata.js +171 -0
  635. package/src/secrets/metadata.js.map +1 -0
  636. package/src/secrets/model.d.ts +49 -0
  637. package/src/secrets/model.js +125 -0
  638. package/src/secrets/model.js.map +1 -0
  639. package/src/secrets/operator.d.ts +101 -0
  640. package/src/secrets/operator.js +229 -0
  641. package/src/secrets/operator.js.map +1 -0
  642. package/src/secrets/permissions.d.ts +34 -0
  643. package/src/secrets/permissions.js +86 -0
  644. package/src/secrets/permissions.js.map +1 -0
  645. package/src/secrets/personal-command.d.ts +83 -0
  646. package/src/secrets/personal-command.js +197 -0
  647. package/src/secrets/personal-command.js.map +1 -0
  648. package/src/secrets/personal-ledger.d.ts +21 -0
  649. package/src/secrets/personal-ledger.js +135 -0
  650. package/src/secrets/personal-ledger.js.map +1 -0
  651. package/src/secrets/registry.d.ts +9 -0
  652. package/src/secrets/registry.js +86 -0
  653. package/src/secrets/registry.js.map +1 -0
  654. package/src/secrets/verification.d.ts +18 -0
  655. package/src/secrets/verification.js +139 -0
  656. package/src/secrets/verification.js.map +1 -0
  657. package/src/secrets/workload-access.d.ts +46 -0
  658. package/src/secrets/workload-access.js +163 -0
  659. package/src/secrets/workload-access.js.map +1 -0
  660. package/src/supply-chain/action-pins.d.ts +26 -0
  661. package/src/supply-chain/action-pins.js +208 -0
  662. package/src/supply-chain/action-pins.js.map +1 -0
  663. package/src/supply-chain/blueprint-trust-git.d.ts +130 -0
  664. package/src/supply-chain/blueprint-trust-git.js +1581 -0
  665. package/src/supply-chain/blueprint-trust-git.js.map +1 -0
  666. package/src/supply-chain/blueprint-trust-process.d.ts +4 -0
  667. package/src/supply-chain/blueprint-trust-process.js +88 -0
  668. package/src/supply-chain/blueprint-trust-process.js.map +1 -0
  669. package/src/supply-chain/blueprint-trust.d.ts +354 -0
  670. package/src/supply-chain/blueprint-trust.js +1558 -0
  671. package/src/supply-chain/blueprint-trust.js.map +1 -0
  672. package/src/supply-chain/blueprint-verification-authority.d.ts +62 -0
  673. package/src/supply-chain/blueprint-verification-authority.js +588 -0
  674. package/src/supply-chain/blueprint-verification-authority.js.map +1 -0
  675. package/src/supply-chain/blueprint-verification-execution.d.ts +68 -0
  676. package/src/supply-chain/blueprint-verification-execution.js +624 -0
  677. package/src/supply-chain/blueprint-verification-execution.js.map +1 -0
  678. package/src/supply-chain/build-output.d.ts +35 -0
  679. package/src/supply-chain/build-output.js +398 -0
  680. package/src/supply-chain/build-output.js.map +1 -0
  681. package/src/supply-chain/common.d.ts +21 -0
  682. package/src/supply-chain/common.js +107 -0
  683. package/src/supply-chain/common.js.map +1 -0
  684. package/src/supply-chain/container-artifact-contract.d.ts +23 -0
  685. package/src/supply-chain/container-artifact-contract.js +208 -0
  686. package/src/supply-chain/container-artifact-contract.js.map +1 -0
  687. package/src/supply-chain/container-release.d.ts +119 -0
  688. package/src/supply-chain/container-release.js +496 -0
  689. package/src/supply-chain/container-release.js.map +1 -0
  690. package/src/supply-chain/ecr-applicability.d.ts +19 -0
  691. package/src/supply-chain/ecr-applicability.js +103 -0
  692. package/src/supply-chain/ecr-applicability.js.map +1 -0
  693. package/src/supply-chain/ecr-scanning.d.ts +83 -0
  694. package/src/supply-chain/ecr-scanning.js +321 -0
  695. package/src/supply-chain/ecr-scanning.js.map +1 -0
  696. package/src/supply-chain/ecs-deployed-image.d.ts +85 -0
  697. package/src/supply-chain/ecs-deployed-image.js +300 -0
  698. package/src/supply-chain/ecs-deployed-image.js.map +1 -0
  699. package/src/supply-chain/errors.d.ts +6 -0
  700. package/src/supply-chain/errors.js +15 -0
  701. package/src/supply-chain/errors.js.map +1 -0
  702. package/src/supply-chain/generated-blueprint-trust-command.d.ts +4 -0
  703. package/src/supply-chain/generated-blueprint-trust-command.js +1362 -0
  704. package/src/supply-chain/generated-blueprint-trust-command.js.map +1 -0
  705. package/src/supply-chain/generated-command.d.ts +1 -0
  706. package/src/supply-chain/generated-command.js +687 -0
  707. package/src/supply-chain/generated-command.js.map +1 -0
  708. package/src/supply-chain/generated-container-release-command.d.ts +16 -0
  709. package/src/supply-chain/generated-container-release-command.js +2087 -0
  710. package/src/supply-chain/generated-container-release-command.js.map +1 -0
  711. package/src/supply-chain/generated-ecr-collector-command.d.ts +21 -0
  712. package/src/supply-chain/generated-ecr-collector-command.js +1338 -0
  713. package/src/supply-chain/generated-ecr-collector-command.js.map +1 -0
  714. package/src/supply-chain/generated-files.d.ts +13 -0
  715. package/src/supply-chain/generated-files.js +401 -0
  716. package/src/supply-chain/generated-files.js.map +1 -0
  717. package/src/supply-chain/generated-scanner-command.d.ts +1 -0
  718. package/src/supply-chain/generated-scanner-command.js +718 -0
  719. package/src/supply-chain/generated-scanner-command.js.map +1 -0
  720. package/src/supply-chain/index.d.ts +28 -0
  721. package/src/supply-chain/index.js +35 -0
  722. package/src/supply-chain/index.js.map +1 -0
  723. package/src/supply-chain/inventory.d.ts +79 -0
  724. package/src/supply-chain/inventory.js +717 -0
  725. package/src/supply-chain/inventory.js.map +1 -0
  726. package/src/supply-chain/license-expression.d.ts +7 -0
  727. package/src/supply-chain/license-expression.js +128 -0
  728. package/src/supply-chain/license-expression.js.map +1 -0
  729. package/src/supply-chain/pnpm-toolchain-authority.d.ts +31 -0
  730. package/src/supply-chain/pnpm-toolchain-authority.js +316 -0
  731. package/src/supply-chain/pnpm-toolchain-authority.js.map +1 -0
  732. package/src/supply-chain/policy.d.ts +105 -0
  733. package/src/supply-chain/policy.js +301 -0
  734. package/src/supply-chain/policy.js.map +1 -0
  735. package/src/supply-chain/publication.d.ts +48 -0
  736. package/src/supply-chain/publication.js +85 -0
  737. package/src/supply-chain/publication.js.map +1 -0
  738. package/src/supply-chain/sbom.d.ts +228 -0
  739. package/src/supply-chain/sbom.js +826 -0
  740. package/src/supply-chain/sbom.js.map +1 -0
  741. package/src/supply-chain/scanner.d.ts +77 -0
  742. package/src/supply-chain/scanner.js +502 -0
  743. package/src/supply-chain/scanner.js.map +1 -0
  744. package/src/supply-chain/vendored-source.d.ts +95 -0
  745. package/src/supply-chain/vendored-source.js +315 -0
  746. package/src/supply-chain/vendored-source.js.map +1 -0
  747. package/src/testing-evidence/fixture-scope.d.ts +52 -0
  748. package/src/testing-evidence/fixture-scope.js +140 -0
  749. package/src/testing-evidence/fixture-scope.js.map +1 -0
  750. package/src/testing-evidence/index.d.ts +5 -0
  751. package/src/testing-evidence/index.js +9 -0
  752. package/src/testing-evidence/index.js.map +1 -0
  753. package/src/testing-evidence/quarantine.d.ts +48 -0
  754. package/src/testing-evidence/quarantine.js +253 -0
  755. package/src/testing-evidence/quarantine.js.map +1 -0
  756. package/src/testing-evidence/result.d.ts +108 -0
  757. package/src/testing-evidence/result.js +571 -0
  758. package/src/testing-evidence/result.js.map +1 -0
  759. package/src/testing-evidence/runner-adapter.d.ts +51 -0
  760. package/src/testing-evidence/runner-adapter.js +288 -0
  761. package/src/testing-evidence/runner-adapter.js.map +1 -0
  762. package/src/testing-evidence/sensitive-artifacts.d.ts +13 -0
  763. package/src/testing-evidence/sensitive-artifacts.js +93 -0
  764. package/src/testing-evidence/sensitive-artifacts.js.map +1 -0
  765. package/src/work-markers/work-markers.js +4 -3
  766. package/src/work-markers/work-markers.js.map +1 -1
  767. package/src/workload-roles/account-baseline.d.ts +132 -0
  768. package/src/workload-roles/account-baseline.js +743 -0
  769. package/src/workload-roles/account-baseline.js.map +1 -0
  770. package/src/workload-roles/authority.d.ts +38 -0
  771. package/src/workload-roles/authority.js +156 -0
  772. package/src/workload-roles/authority.js.map +1 -0
  773. package/src/workload-roles/aws-adapter.d.ts +99 -0
  774. package/src/workload-roles/aws-adapter.js +416 -0
  775. package/src/workload-roles/aws-adapter.js.map +1 -0
  776. package/src/workload-roles/aws-internal-handler.d.ts +27 -0
  777. package/src/workload-roles/aws-internal-handler.js +58 -0
  778. package/src/workload-roles/aws-internal-handler.js.map +1 -0
  779. package/src/workload-roles/aws-s3-state.d.ts +52 -0
  780. package/src/workload-roles/aws-s3-state.js +164 -0
  781. package/src/workload-roles/aws-s3-state.js.map +1 -0
  782. package/src/workload-roles/aws-sdk-control-plane.d.ts +12 -0
  783. package/src/workload-roles/aws-sdk-control-plane.js +291 -0
  784. package/src/workload-roles/aws-sdk-control-plane.js.map +1 -0
  785. package/src/workload-roles/boundary.d.ts +33 -0
  786. package/src/workload-roles/boundary.js +130 -0
  787. package/src/workload-roles/boundary.js.map +1 -0
  788. package/src/workload-roles/convergence.d.ts +66 -0
  789. package/src/workload-roles/convergence.js +186 -0
  790. package/src/workload-roles/convergence.js.map +1 -0
  791. package/src/workload-roles/effective-policy.d.ts +51 -0
  792. package/src/workload-roles/effective-policy.js +183 -0
  793. package/src/workload-roles/effective-policy.js.map +1 -0
  794. package/src/workload-roles/handler.d.ts +43 -0
  795. package/src/workload-roles/handler.js +130 -0
  796. package/src/workload-roles/handler.js.map +1 -0
  797. package/src/workload-roles/history.d.ts +47 -0
  798. package/src/workload-roles/history.js +324 -0
  799. package/src/workload-roles/history.js.map +1 -0
  800. package/src/workload-roles/index.d.ts +17 -0
  801. package/src/workload-roles/index.js +21 -0
  802. package/src/workload-roles/index.js.map +1 -0
  803. package/src/workload-roles/injection.d.ts +119 -0
  804. package/src/workload-roles/injection.js +495 -0
  805. package/src/workload-roles/injection.js.map +1 -0
  806. package/src/workload-roles/manifest.d.ts +54 -0
  807. package/src/workload-roles/manifest.js +343 -0
  808. package/src/workload-roles/manifest.js.map +1 -0
  809. package/src/workload-roles/provisioner.d.ts +70 -0
  810. package/src/workload-roles/provisioner.js +231 -0
  811. package/src/workload-roles/provisioner.js.map +1 -0
  812. package/src/workload-roles/registration.d.ts +101 -0
  813. package/src/workload-roles/registration.js +457 -0
  814. package/src/workload-roles/registration.js.map +1 -0
  815. package/src/workload-roles/stage-boundary.d.ts +31 -0
  816. package/src/workload-roles/stage-boundary.js +208 -0
  817. package/src/workload-roles/stage-boundary.js.map +1 -0
  818. package/src/workload-roles/standard-slice.d.ts +54 -0
  819. package/src/workload-roles/standard-slice.js +252 -0
  820. package/src/workload-roles/standard-slice.js.map +1 -0
  821. package/migrations/standard-pre-v1-to-v1.md +0 -11
  822. package/src/foundation/aurora-logical-database.d.ts +0 -76
  823. package/src/foundation/aurora-logical-database.js +0 -419
  824. package/src/foundation/aurora-logical-database.js.map +0 -1
@@ -0,0 +1,4628 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderProductDataRightsFiles = renderProductDataRightsFiles;
4
+ const built_in_plan_shared_js_1 = require("./built-in-plan-shared.js");
5
+ const PENDING_EXPORT_CLEANUP_REPORT_BOUND = 1_000;
6
+ const PENDING_EXPORT_CLEANUP_REPORT_BOUND_LITERAL = String(PENDING_EXPORT_CLEANUP_REPORT_BOUND).replace(/\B(?=(?:\d{3})+$)/gu, '_');
7
+ /** @internal */
8
+ function renderProductDataRightsFiles(name, pascalName) {
9
+ const identifierPrefix = (0, built_in_plan_shared_js_1.lowerCamelIdentifier)(pascalName);
10
+ return {
11
+ 'data/data-rights-shared.ts': `import type { AuthorizationPrincipal } from '@empire-builder-kit/runtime/authorization';
12
+ import {
13
+ createDataLifecycleAuditRecord,
14
+ createDataLifecyclePseudonymousReference,
15
+ evaluateDataLifecycleHolds,
16
+ type DataLifecycleAuditAction,
17
+ type DataLifecycleEvidenceOutcome,
18
+ type DataLifecycleRuntimeHoldV1,
19
+ } from '@empire-builder-kit/runtime/data-lifecycle';
20
+ import type { Client as PgClient } from 'pg';
21
+ import {
22
+ approvalReference,
23
+ productRecordReference,
24
+ } from './pseudonymous-reference.js';
25
+ import {
26
+ ${pascalName}DataLifecycleRuntimePolicy,
27
+ } from './runtime-policy.cjs';
28
+
29
+ export const DATA_RIGHTS_POLICY_VERSION =
30
+ ${pascalName}DataLifecycleRuntimePolicy.policyVersion;
31
+ export const DATA_RIGHTS_AUDIT_RETENTION_DAYS = 400;
32
+
33
+ export function canonicalDataRightsNow(now?: string): string {
34
+ const value = now ?? new Date().toISOString();
35
+ if (new Date(value).toISOString() !== value) {
36
+ throw new Error('Data-rights time must be a canonical ISO-8601 timestamp.');
37
+ }
38
+ return value;
39
+ }
40
+
41
+ export function plusDataRightsDays(value: string, days: number): string {
42
+ const result = new Date(value);
43
+ result.setUTCDate(result.getUTCDate() + days);
44
+ return result.toISOString();
45
+ }
46
+
47
+ export function plusDataRightsHours(value: string, hours: number): string {
48
+ return new Date(Date.parse(value) + hours * 60 * 60 * 1_000).toISOString();
49
+ }
50
+
51
+ export function dataRightsSubjectReference(subjectId: string): string {
52
+ return createDataLifecyclePseudonymousReference({
53
+ categoryId: 'authorization-subject',
54
+ slice: '${name}',
55
+ value: subjectId,
56
+ });
57
+ }
58
+
59
+ export function dataRightsCorrelationReference(correlationId: string): string {
60
+ return createDataLifecyclePseudonymousReference({
61
+ categoryId: 'correlation',
62
+ slice: '${name}',
63
+ value: correlationId,
64
+ });
65
+ }
66
+
67
+ export function dataRightsArtifactReference(value: string): string {
68
+ return createDataLifecyclePseudonymousReference({
69
+ categoryId: 'subject-export',
70
+ slice: '${name}',
71
+ value,
72
+ });
73
+ }
74
+
75
+ export function requireDirectHuman(
76
+ principal: AuthorizationPrincipal,
77
+ ): AuthorizationPrincipal {
78
+ if (
79
+ principal.actor.type !== 'user' ||
80
+ principal.subject.type !== 'user' ||
81
+ principal.authority.mode !== 'direct'
82
+ ) {
83
+ throw new Error('Data-rights request requires a direct human principal.');
84
+ }
85
+ return principal;
86
+ }
87
+
88
+ export function requireDirectAdmin(
89
+ principal: AuthorizationPrincipal,
90
+ ): AuthorizationPrincipal {
91
+ requireDirectHuman(principal);
92
+ if (principal.kind !== 'admin') {
93
+ throw new Error('Data-rights administration requires a direct admin.');
94
+ }
95
+ return principal;
96
+ }
97
+
98
+ export function requireExportCleanupAuthority(
99
+ principal: AuthorizationPrincipal,
100
+ ): AuthorizationPrincipal {
101
+ if (
102
+ principal.kind === 'agent' &&
103
+ principal.actor.type === 'service' &&
104
+ principal.actor.id === '${name}:data-export-cleanup' &&
105
+ principal.audience === '${name}:data-export-cleanup' &&
106
+ principal.subject.type === 'service' &&
107
+ principal.subject.id === '${name}:data-export-cleanup' &&
108
+ principal.authority.mode === 'machine' &&
109
+ principal.authority.machineIdentityId ===
110
+ '${name}:data-export-cleanup' &&
111
+ principal.scopes.length === 0
112
+ ) {
113
+ return principal;
114
+ }
115
+ return requireDirectAdmin(principal);
116
+ }
117
+
118
+ export function effectiveRecordOwner(
119
+ principal: AuthorizationPrincipal,
120
+ ): string {
121
+ if (principal.authority.mode === 'machine') {
122
+ throw new Error('Machine authority cannot mutate a subject-owned record.');
123
+ }
124
+ return principal.authority.mode === 'delegated'
125
+ ? principal.authority.delegatedBy.id
126
+ : principal.actor.id;
127
+ }
128
+
129
+ export async function loadActiveRecordHolds(
130
+ client: PgClient,
131
+ recordId: string,
132
+ now: string,
133
+ operation: 'deletion' | 'export',
134
+ ): Promise<readonly DataLifecycleRuntimeHoldV1[]> {
135
+ const scopeRef = productRecordReference(recordId);
136
+ const result = await client.query<{
137
+ holdJson: DataLifecycleRuntimeHoldV1;
138
+ }>(
139
+ \`SELECT hold_json AS "holdJson"
140
+ FROM data_lifecycle_holds
141
+ WHERE category_id = 'product-record'
142
+ AND scope_ref = $1
143
+ AND status = 'active'
144
+ ORDER BY hold_id
145
+ FOR UPDATE\`,
146
+ [scopeRef],
147
+ );
148
+ const holds = [
149
+ ...${pascalName}DataLifecycleRuntimePolicy.declaredHolds,
150
+ ...result.rows.map(({ holdJson }) => holdJson),
151
+ ];
152
+ evaluateDataLifecycleHolds({
153
+ categoryId: 'product-record',
154
+ holds,
155
+ now,
156
+ operation,
157
+ scopeRef,
158
+ });
159
+ return holds;
160
+ }
161
+
162
+ export async function writeDataRightsAudit(
163
+ client: PgClient,
164
+ input: {
165
+ action: DataLifecycleAuditAction;
166
+ approvalRequestId?: string;
167
+ correlationId: string;
168
+ now: string;
169
+ outcome: DataLifecycleEvidenceOutcome;
170
+ principal: AuthorizationPrincipal;
171
+ purposeCode: string;
172
+ recordId?: string;
173
+ resourceReference?: string;
174
+ summary: string;
175
+ },
176
+ ): Promise<void> {
177
+ const resourceReference =
178
+ input.resourceReference ??
179
+ (input.recordId ? productRecordReference(input.recordId) : undefined);
180
+ if (!resourceReference) {
181
+ throw new Error('Data-rights audit requires a resource reference.');
182
+ }
183
+ const audit = createDataLifecycleAuditRecord({
184
+ action: input.action,
185
+ actorRef: dataRightsSubjectReference(input.principal.actor.id),
186
+ authorityCode: input.principal.authority.mode,
187
+ correlationRef: dataRightsCorrelationReference(input.correlationId),
188
+ outcome: input.outcome,
189
+ policyVersion: DATA_RIGHTS_POLICY_VERSION,
190
+ purposeCode: input.purposeCode,
191
+ summary: input.summary,
192
+ targetCategoryId: 'product-record',
193
+ timestamp: input.now,
194
+ });
195
+ await client.query(
196
+ \`INSERT INTO data_lifecycle_audit
197
+ (action, actor_ref, approval_reference, audit_json, authority_code,
198
+ correlation_ref, expires_at, outcome, policy_version, principal_kind,
199
+ purpose_code, resource_reference, slice, stage, subject_type,
200
+ target_category_id, occurred_at)
201
+ VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9, $10, $11, $12,
202
+ $13, $14, $15, $16, $17)\`,
203
+ [
204
+ audit.action,
205
+ audit.actorRef,
206
+ input.approvalRequestId
207
+ ? approvalReference(input.approvalRequestId)
208
+ : null,
209
+ JSON.stringify(audit),
210
+ audit.authorityCode,
211
+ audit.correlationRef,
212
+ plusDataRightsDays(input.now, DATA_RIGHTS_AUDIT_RETENTION_DAYS),
213
+ audit.outcome,
214
+ audit.policyVersion,
215
+ input.principal.kind,
216
+ audit.purposeCode,
217
+ resourceReference,
218
+ '${name}',
219
+ process.env.EBK_STAGE ?? 'development',
220
+ input.principal.actor.type,
221
+ audit.targetCategoryId,
222
+ audit.timestamp,
223
+ ],
224
+ );
225
+ }
226
+ `,
227
+ 'data/record-holds.ts': `import { randomUUID } from 'node:crypto';
228
+ import type { AuthorizationPrincipal } from '@empire-builder-kit/runtime/authorization';
229
+ import {
230
+ evaluateDataLifecycleHolds,
231
+ type DataLifecycleRuntimeHoldEffect,
232
+ type DataLifecycleRuntimeHoldPurpose,
233
+ type DataLifecycleRuntimeHoldV1,
234
+ } from '@empire-builder-kit/runtime/data-lifecycle';
235
+ import { productRecordReference } from './pseudonymous-reference.js';
236
+ import {
237
+ canonicalDataRightsNow,
238
+ requireDirectAdmin,
239
+ writeDataRightsAudit,
240
+ } from './data-rights-shared.js';
241
+ import { withPrimaryDatabase } from '../database/runtime-client.js';
242
+
243
+ const MAXIMUM_HOLD_LIFETIME_MS = 90 * 24 * 60 * 60 * 1_000;
244
+
245
+ function holdId(): string {
246
+ return 'hold.' + randomUUID().replaceAll('-', '');
247
+ }
248
+
249
+ function requireBoundedHoldInterval(
250
+ startsAt: string,
251
+ reviewAt: string,
252
+ expiresAt: string,
253
+ ): void {
254
+ const start = Date.parse(startsAt);
255
+ const review = Date.parse(reviewAt);
256
+ const expiry = Date.parse(expiresAt);
257
+ if (
258
+ !Number.isFinite(start) ||
259
+ !Number.isFinite(review) ||
260
+ !Number.isFinite(expiry) ||
261
+ start >= review ||
262
+ review > expiry ||
263
+ expiry - start > MAXIMUM_HOLD_LIFETIME_MS
264
+ ) {
265
+ throw new Error(
266
+ 'Record hold requires a future review and a lifetime of at most 90 days.',
267
+ );
268
+ }
269
+ }
270
+
271
+ export function createRecordHold(
272
+ input: {
273
+ effects: readonly DataLifecycleRuntimeHoldEffect[];
274
+ expiresAt: string;
275
+ principal: AuthorizationPrincipal;
276
+ purpose: DataLifecycleRuntimeHoldPurpose;
277
+ recordId: string;
278
+ reviewAt: string;
279
+ },
280
+ now = canonicalDataRightsNow(),
281
+ ): DataLifecycleRuntimeHoldV1 {
282
+ requireDirectAdmin(input.principal);
283
+ requireBoundedHoldInterval(now, input.reviewAt, input.expiresAt);
284
+ const hold: DataLifecycleRuntimeHoldV1 = {
285
+ categoryId: 'product-record',
286
+ effects: [...input.effects],
287
+ expiresAt: input.expiresAt,
288
+ holdId: holdId(),
289
+ issuerCode: 'direct-admin',
290
+ purpose: input.purpose,
291
+ reviewAt: input.reviewAt,
292
+ scopeRef: productRecordReference(input.recordId),
293
+ startsAt: now,
294
+ status: 'active',
295
+ };
296
+ for (const operation of ['deletion', 'export'] as const) {
297
+ evaluateDataLifecycleHolds({
298
+ categoryId: hold.categoryId,
299
+ holds: [hold],
300
+ now,
301
+ operation,
302
+ scopeRef: hold.scopeRef,
303
+ });
304
+ }
305
+ return hold;
306
+ }
307
+
308
+ export async function issueRecordHold(input: {
309
+ correlationId: string;
310
+ effects: readonly DataLifecycleRuntimeHoldEffect[];
311
+ expiresAt: string;
312
+ now?: string;
313
+ principal: AuthorizationPrincipal;
314
+ purpose: DataLifecycleRuntimeHoldPurpose;
315
+ recordId: string;
316
+ reviewAt: string;
317
+ }): Promise<DataLifecycleRuntimeHoldV1> {
318
+ const now = canonicalDataRightsNow(input.now);
319
+ const hold = createRecordHold(input, now);
320
+ return withPrimaryDatabase(
321
+ { correlationId: input.correlationId },
322
+ 'api-database-records',
323
+ async (client) => {
324
+ await client.query('BEGIN');
325
+ try {
326
+ const record = await client.query(
327
+ 'SELECT 1 FROM records WHERE id = $1::uuid FOR UPDATE',
328
+ [input.recordId],
329
+ );
330
+ if (record.rowCount !== 1) {
331
+ throw new Error('The held record was not found.');
332
+ }
333
+ await client.query(
334
+ \`INSERT INTO data_lifecycle_holds
335
+ (hold_id, scope_ref, category_id, hold_json, status, expires_at,
336
+ review_at)
337
+ VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7)\`,
338
+ [
339
+ hold.holdId,
340
+ hold.scopeRef,
341
+ hold.categoryId,
342
+ JSON.stringify(hold),
343
+ hold.status,
344
+ hold.expiresAt,
345
+ hold.reviewAt,
346
+ ],
347
+ );
348
+ await writeDataRightsAudit(client, {
349
+ action: 'hold',
350
+ correlationId: input.correlationId,
351
+ now,
352
+ outcome: 'succeeded',
353
+ principal: input.principal,
354
+ purposeCode: 'hold-issue',
355
+ recordId: input.recordId,
356
+ summary: 'A bounded record hold was issued.',
357
+ });
358
+ await client.query('COMMIT');
359
+ return hold;
360
+ } catch (error) {
361
+ await client.query('ROLLBACK').catch(() => undefined);
362
+ throw error;
363
+ }
364
+ },
365
+ );
366
+ }
367
+
368
+ async function lockedRecordHold(
369
+ client: import('pg').Client,
370
+ recordId: string,
371
+ holdIdValue: string,
372
+ ): Promise<DataLifecycleRuntimeHoldV1> {
373
+ const result = await client.query<{
374
+ holdJson: DataLifecycleRuntimeHoldV1;
375
+ }>(
376
+ \`SELECT hold_json AS "holdJson"
377
+ FROM data_lifecycle_holds
378
+ WHERE hold_id = $1
379
+ AND category_id = 'product-record'
380
+ AND scope_ref = $2
381
+ FOR UPDATE\`,
382
+ [holdIdValue, productRecordReference(recordId)],
383
+ );
384
+ const hold = result.rows[0]?.holdJson;
385
+ if (!hold) throw new Error('The record hold was not found.');
386
+ return hold;
387
+ }
388
+
389
+ async function persistRecordHold(
390
+ client: import('pg').Client,
391
+ hold: DataLifecycleRuntimeHoldV1,
392
+ ): Promise<void> {
393
+ const result = await client.query(
394
+ \`UPDATE data_lifecycle_holds
395
+ SET hold_json = $2::jsonb,
396
+ status = $3,
397
+ expires_at = $4,
398
+ review_at = $5
399
+ WHERE hold_id = $1\`,
400
+ [
401
+ hold.holdId,
402
+ JSON.stringify(hold),
403
+ hold.status,
404
+ hold.expiresAt,
405
+ hold.reviewAt,
406
+ ],
407
+ );
408
+ if (result.rowCount !== 1) throw new Error('Record hold update was lost.');
409
+ }
410
+
411
+ export async function reviewRecordHold(input: {
412
+ correlationId: string;
413
+ expiresAt: string;
414
+ holdId: string;
415
+ now?: string;
416
+ principal: AuthorizationPrincipal;
417
+ recordId: string;
418
+ reviewAt: string;
419
+ }): Promise<DataLifecycleRuntimeHoldV1> {
420
+ requireDirectAdmin(input.principal);
421
+ const now = canonicalDataRightsNow(input.now);
422
+ return withPrimaryDatabase(
423
+ { correlationId: input.correlationId },
424
+ 'api-database-records',
425
+ async (client) => {
426
+ await client.query('BEGIN');
427
+ try {
428
+ const current = await lockedRecordHold(
429
+ client,
430
+ input.recordId,
431
+ input.holdId,
432
+ );
433
+ if (
434
+ current.status !== 'active' ||
435
+ Date.parse(now) >= Date.parse(current.expiresAt)
436
+ ) {
437
+ throw new Error('Only an unexpired active hold can be reviewed.');
438
+ }
439
+ requireBoundedHoldInterval(
440
+ current.startsAt,
441
+ input.reviewAt,
442
+ input.expiresAt,
443
+ );
444
+ if (Date.parse(input.reviewAt) <= Date.parse(now)) {
445
+ throw new Error('Reviewed hold must set a future review time.');
446
+ }
447
+ const hold: DataLifecycleRuntimeHoldV1 = {
448
+ ...current,
449
+ expiresAt: input.expiresAt,
450
+ reviewAt: input.reviewAt,
451
+ };
452
+ evaluateDataLifecycleHolds({
453
+ categoryId: hold.categoryId,
454
+ holds: [hold],
455
+ now,
456
+ operation: 'export',
457
+ scopeRef: hold.scopeRef,
458
+ });
459
+ await persistRecordHold(client, hold);
460
+ await writeDataRightsAudit(client, {
461
+ action: 'hold',
462
+ correlationId: input.correlationId,
463
+ now,
464
+ outcome: 'succeeded',
465
+ principal: input.principal,
466
+ purposeCode: 'hold-review',
467
+ recordId: input.recordId,
468
+ summary: 'A bounded record hold was reviewed.',
469
+ });
470
+ await client.query('COMMIT');
471
+ return hold;
472
+ } catch (error) {
473
+ await client.query('ROLLBACK').catch(() => undefined);
474
+ throw error;
475
+ }
476
+ },
477
+ );
478
+ }
479
+
480
+ export async function releaseRecordHold(input: {
481
+ correlationId: string;
482
+ holdId: string;
483
+ now?: string;
484
+ principal: AuthorizationPrincipal;
485
+ recordId: string;
486
+ }): Promise<DataLifecycleRuntimeHoldV1> {
487
+ requireDirectAdmin(input.principal);
488
+ const now = canonicalDataRightsNow(input.now);
489
+ return withPrimaryDatabase(
490
+ { correlationId: input.correlationId },
491
+ 'api-database-records',
492
+ async (client) => {
493
+ await client.query('BEGIN');
494
+ try {
495
+ const current = await lockedRecordHold(
496
+ client,
497
+ input.recordId,
498
+ input.holdId,
499
+ );
500
+ if (
501
+ current.status !== 'active' ||
502
+ Date.parse(now) > Date.parse(current.expiresAt)
503
+ ) {
504
+ throw new Error('Only an active record hold can be released.');
505
+ }
506
+ const hold: DataLifecycleRuntimeHoldV1 = {
507
+ ...current,
508
+ releasedAt: now,
509
+ status: 'released',
510
+ };
511
+ evaluateDataLifecycleHolds({
512
+ categoryId: hold.categoryId,
513
+ holds: [hold],
514
+ now,
515
+ operation: 'export',
516
+ scopeRef: hold.scopeRef,
517
+ });
518
+ await persistRecordHold(client, hold);
519
+ await writeDataRightsAudit(client, {
520
+ action: 'hold',
521
+ correlationId: input.correlationId,
522
+ now,
523
+ outcome: 'succeeded',
524
+ principal: input.principal,
525
+ purposeCode: 'hold-release',
526
+ recordId: input.recordId,
527
+ summary: 'A bounded record hold was released.',
528
+ });
529
+ await client.query('COMMIT');
530
+ return hold;
531
+ } catch (error) {
532
+ await client.query('ROLLBACK').catch(() => undefined);
533
+ throw error;
534
+ }
535
+ },
536
+ );
537
+ }
538
+
539
+ export async function expireRecordHold(input: {
540
+ correlationId: string;
541
+ holdId: string;
542
+ now?: string;
543
+ principal: AuthorizationPrincipal;
544
+ recordId: string;
545
+ }): Promise<DataLifecycleRuntimeHoldV1> {
546
+ requireDirectAdmin(input.principal);
547
+ const now = canonicalDataRightsNow(input.now);
548
+ return withPrimaryDatabase(
549
+ { correlationId: input.correlationId },
550
+ 'api-database-records',
551
+ async (client) => {
552
+ await client.query('BEGIN');
553
+ try {
554
+ const current = await lockedRecordHold(
555
+ client,
556
+ input.recordId,
557
+ input.holdId,
558
+ );
559
+ if (
560
+ current.status !== 'active' ||
561
+ Date.parse(now) < Date.parse(current.expiresAt)
562
+ ) {
563
+ throw new Error('Record hold is not due for expiry.');
564
+ }
565
+ const hold: DataLifecycleRuntimeHoldV1 = {
566
+ ...current,
567
+ releasedAt: current.expiresAt,
568
+ status: 'released',
569
+ };
570
+ evaluateDataLifecycleHolds({
571
+ categoryId: hold.categoryId,
572
+ holds: [hold],
573
+ now,
574
+ operation: 'export',
575
+ scopeRef: hold.scopeRef,
576
+ });
577
+ await persistRecordHold(client, hold);
578
+ await writeDataRightsAudit(client, {
579
+ action: 'hold',
580
+ correlationId: input.correlationId,
581
+ now,
582
+ outcome: 'succeeded',
583
+ principal: input.principal,
584
+ purposeCode: 'hold-expiry',
585
+ recordId: input.recordId,
586
+ summary: 'An expired record hold was closed.',
587
+ });
588
+ await client.query('COMMIT');
589
+ return hold;
590
+ } catch (error) {
591
+ await client.query('ROLLBACK').catch(() => undefined);
592
+ throw error;
593
+ }
594
+ },
595
+ );
596
+ }
597
+ `,
598
+ 'data/record-correction.ts': `import {
599
+ EventBridgeClient,
600
+ PutEventsCommand,
601
+ type PutEventsRequestEntry,
602
+ } from '@aws-sdk/client-eventbridge';
603
+ import type { AuthorizationPrincipal } from '@empire-builder-kit/runtime/authorization';
604
+ import {
605
+ createDataLifecycleLedgerEntry,
606
+ lifecycleIdempotencyKey,
607
+ } from '@empire-builder-kit/runtime/data-lifecycle';
608
+ import { buildEventEntry } from '@empire-builder-kit/runtime/events';
609
+ import {
610
+ ${pascalName}DataLifecycleChanged,
611
+ } from '../contracts/events.js';
612
+ import {
613
+ create${pascalName}DataLifecycleChanged,
614
+ } from './lifecycle-event.js';
615
+ import { productRecordReference } from './pseudonymous-reference.js';
616
+ import {
617
+ canonicalDataRightsNow,
618
+ DATA_RIGHTS_POLICY_VERSION,
619
+ effectiveRecordOwner,
620
+ plusDataRightsDays,
621
+ writeDataRightsAudit,
622
+ } from './data-rights-shared.js';
623
+ import { withPrimaryDatabase } from '../database/runtime-client.js';
624
+
625
+ const eventBridge = new EventBridgeClient({});
626
+
627
+ export interface RecordCorrectionInput {
628
+ correlationId: string;
629
+ name: string;
630
+ now?: string;
631
+ principal: AuthorizationPrincipal;
632
+ recordId: string;
633
+ runScope: string;
634
+ testExpiresAt?: string;
635
+ }
636
+
637
+ export function createRecordCorrectionArtifacts(input: RecordCorrectionInput) {
638
+ const now = canonicalDataRightsNow(input.now);
639
+ const recordRef = productRecordReference(input.recordId);
640
+ const requestRef = lifecycleIdempotencyKey('record-correction', [
641
+ recordRef,
642
+ input.correlationId,
643
+ input.name,
644
+ input.runScope,
645
+ input.testExpiresAt ?? 'none',
646
+ ]);
647
+ const ledger = createDataLifecycleLedgerEntry({
648
+ action: 'correction',
649
+ categoryId: 'product-record',
650
+ effectiveAt: now,
651
+ expiresAt: plusDataRightsDays(now, 400),
652
+ policyVersion: DATA_RIGHTS_POLICY_VERSION,
653
+ pseudonymousKey: recordRef,
654
+ });
655
+ const data = create${pascalName}DataLifecycleChanged({
656
+ action: 'correction',
657
+ occurredAt: now,
658
+ policyVersion: DATA_RIGHTS_POLICY_VERSION,
659
+ recordId: input.recordId,
660
+ requestRef,
661
+ });
662
+ const built = buildEventEntry(${pascalName}DataLifecycleChanged, data, {
663
+ busName: process.env.EVENT_BUS_NAME ?? '',
664
+ context: { correlationId: input.correlationId },
665
+ eventId: requestRef,
666
+ occurredAt: now,
667
+ stage: process.env.EBK_STAGE ?? 'development',
668
+ });
669
+ const event: PutEventsRequestEntry = {
670
+ Detail: built.entry.detail,
671
+ DetailType: built.entry.detailType,
672
+ EventBusName: built.entry.eventBusName,
673
+ Source: built.entry.source,
674
+ };
675
+ return { event, ledger, now, recordRef, requestRef };
676
+ }
677
+
678
+ export async function correctRecordWithLifecycle(
679
+ input: RecordCorrectionInput,
680
+ ): Promise<{
681
+ id: string;
682
+ name: string;
683
+ propagation: 'pending';
684
+ requestRef: string;
685
+ runScope: string;
686
+ }> {
687
+ const artifacts = createRecordCorrectionArtifacts(input);
688
+ const ownerId = effectiveRecordOwner(input.principal);
689
+ return withPrimaryDatabase(
690
+ { correlationId: input.correlationId },
691
+ 'api-database-records',
692
+ async (client) => {
693
+ await client.query('BEGIN');
694
+ try {
695
+ const recorded = await client.query(
696
+ \`INSERT INTO data_lifecycle_ledger
697
+ (entry_id, action, category_id, effective_at, expires_at,
698
+ idempotency_key, policy_version, pseudonymous_key)
699
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
700
+ ON CONFLICT (idempotency_key) DO NOTHING
701
+ RETURNING entry_id\`,
702
+ [
703
+ artifacts.ledger.entryId,
704
+ artifacts.ledger.action,
705
+ artifacts.ledger.categoryId,
706
+ artifacts.ledger.effectiveAt,
707
+ artifacts.ledger.expiresAt,
708
+ artifacts.requestRef,
709
+ artifacts.ledger.policyVersion,
710
+ artifacts.ledger.pseudonymousKey,
711
+ ],
712
+ );
713
+ const result =
714
+ recorded.rowCount === 1
715
+ ? await client.query<{
716
+ id: string;
717
+ name: string;
718
+ runScope: string;
719
+ }>(
720
+ \`UPDATE records
721
+ SET name = $2,
722
+ run_scope = $3,
723
+ last_activity_at = $4,
724
+ test_expires_at =
725
+ CASE WHEN $5::boolean
726
+ THEN $6::timestamptz
727
+ ELSE test_expires_at
728
+ END
729
+ WHERE id = $1::uuid AND owner_id = $7
730
+ RETURNING id::text, name, run_scope AS "runScope"\`,
731
+ [
732
+ input.recordId,
733
+ input.name,
734
+ input.runScope,
735
+ artifacts.now,
736
+ input.testExpiresAt !== undefined,
737
+ input.testExpiresAt ?? null,
738
+ ownerId,
739
+ ],
740
+ )
741
+ : await client.query<{
742
+ id: string;
743
+ name: string;
744
+ runScope: string;
745
+ }>(
746
+ \`SELECT id::text, name, run_scope AS "runScope"
747
+ FROM records
748
+ WHERE id = $1::uuid AND owner_id = $2\`,
749
+ [input.recordId, ownerId],
750
+ );
751
+ const record = result.rows[0];
752
+ if (!record) {
753
+ throw new Error('The correction target was not found.');
754
+ }
755
+ await client.query(
756
+ \`INSERT INTO data_lifecycle_outbox
757
+ (idempotency_key, event_json, expires_at)
758
+ VALUES ($1, $2::jsonb, $3)
759
+ ON CONFLICT (idempotency_key) DO NOTHING\`,
760
+ [
761
+ artifacts.requestRef,
762
+ JSON.stringify(artifacts.event),
763
+ plusDataRightsDays(artifacts.now, 30),
764
+ ],
765
+ );
766
+ if (recorded.rowCount === 1) {
767
+ await writeDataRightsAudit(client, {
768
+ action: 'correction',
769
+ correlationId: input.correlationId,
770
+ now: artifacts.now,
771
+ outcome: 'succeeded',
772
+ principal: input.principal,
773
+ purposeCode: 'record-correction',
774
+ recordId: input.recordId,
775
+ summary:
776
+ 'A record correction and its downstream event were committed.',
777
+ });
778
+ }
779
+ await client.query('COMMIT');
780
+ return {
781
+ ...record,
782
+ propagation: 'pending' as const,
783
+ requestRef: artifacts.requestRef,
784
+ };
785
+ } catch (error) {
786
+ await client.query('ROLLBACK').catch(() => undefined);
787
+ throw error;
788
+ }
789
+ },
790
+ );
791
+ }
792
+
793
+ export async function publishRecordCorrection(
794
+ correlationId: string,
795
+ requestRef: string,
796
+ ): Promise<'published' | 'retry-required'> {
797
+ const pending = await withPrimaryDatabase(
798
+ { correlationId },
799
+ 'api-database-records',
800
+ (client) =>
801
+ client.query<{ eventJson: PutEventsRequestEntry; publishedAt: Date | null }>(
802
+ \`SELECT event_json AS "eventJson", published_at AS "publishedAt"
803
+ FROM data_lifecycle_outbox
804
+ WHERE idempotency_key = $1\`,
805
+ [requestRef],
806
+ ),
807
+ );
808
+ const row = pending.rows[0];
809
+ if (!row) throw new Error('The correction outbox record was not found.');
810
+ if (row.publishedAt) return 'published';
811
+ try {
812
+ const response = await eventBridge.send(
813
+ new PutEventsCommand({ Entries: [row.eventJson] }),
814
+ );
815
+ if ((response.FailedEntryCount ?? 0) !== 0) {
816
+ throw new Error('event-entry-rejected');
817
+ }
818
+ await withPrimaryDatabase(
819
+ { correlationId },
820
+ 'api-database-records',
821
+ (client) =>
822
+ client.query(
823
+ \`UPDATE data_lifecycle_outbox
824
+ SET attempts = attempts + 1,
825
+ last_error_code = NULL,
826
+ published_at = now()
827
+ WHERE idempotency_key = $1 AND published_at IS NULL\`,
828
+ [requestRef],
829
+ ),
830
+ );
831
+ return 'published';
832
+ } catch {
833
+ await withPrimaryDatabase(
834
+ { correlationId },
835
+ 'api-database-records',
836
+ (client) =>
837
+ client.query(
838
+ \`UPDATE data_lifecycle_outbox
839
+ SET attempts = attempts + 1,
840
+ last_error_code = 'event-publication-failed'
841
+ WHERE idempotency_key = $1 AND published_at IS NULL\`,
842
+ [requestRef],
843
+ ),
844
+ );
845
+ return 'retry-required';
846
+ }
847
+ }
848
+ `,
849
+ 'data/record-export.ts': renderRecordExportSource(name, pascalName),
850
+ 'integration/data-rights-deadline-lock-order.spec.ts': renderDataRightsDeadlineLockOrderIntegrationSource(name),
851
+ 'jobs/data-export-cleanup.ts': `import { randomUUID } from 'node:crypto';
852
+ import type { AuthorizationPrincipal } from '@empire-builder-kit/runtime/authorization';
853
+ import {
854
+ createMetricEmitter,
855
+ type MetricEmitter,
856
+ } from '@empire-builder-kit/runtime/observability';
857
+ import {
858
+ ${identifierPrefix}AuthorizationActions,
859
+ } from '../authorization/policy.js';
860
+ import { authorize${pascalName}InternalOperation } from '../authorization/runtime.js';
861
+ import {
862
+ cleanupDueRecordExports,
863
+ reportExportCleanupDeadlineBreaches,
864
+ } from '../data/record-export.js';
865
+
866
+ const WORKLOAD_IDENTITY = '${name}:data-export-cleanup';
867
+ const MINIMUM_REMAINING_TIME_MS = 30_000;
868
+
869
+ export interface DataExportCleanupDependencies {
870
+ authorize: typeof authorize${pascalName}InternalOperation;
871
+ cleanup: typeof cleanupDueRecordExports;
872
+ metrics: MetricEmitter;
873
+ report: typeof reportExportCleanupDeadlineBreaches;
874
+ }
875
+
876
+ function cleanupPrincipal(): AuthorizationPrincipal {
877
+ const configured = process.env.EBK_WORKLOAD_IDENTITY ?? '';
878
+ if (configured !== WORKLOAD_IDENTITY) {
879
+ throw new Error('Data export cleanup workload identity is invalid.');
880
+ }
881
+ return {
882
+ actor: { id: configured, type: 'service' },
883
+ audience: configured,
884
+ authority: { machineIdentityId: configured, mode: 'machine' },
885
+ kind: 'agent',
886
+ roles: [],
887
+ scopes: [],
888
+ subject: { id: configured, type: 'service' },
889
+ };
890
+ }
891
+
892
+ function emitCleanupDeadlineBreaches(
893
+ metrics: MetricEmitter,
894
+ value: number,
895
+ ) {
896
+ return metrics.emit({
897
+ dimensions: {
898
+ component: 'data-export-cleanup',
899
+ feature: 'data-lifecycle',
900
+ operation: 'expiry-cleanup',
901
+ slice: '${name}',
902
+ stage: process.env.EBK_STAGE ?? 'local',
903
+ },
904
+ metricName: 'DataExportCleanupDeadlineBreaches',
905
+ namespace: 'EBK/DataLifecycle',
906
+ unit: 'Count',
907
+ value,
908
+ });
909
+ }
910
+
911
+ export function createDataExportCleanupHandler(
912
+ dependencies: DataExportCleanupDependencies,
913
+ ) {
914
+ return async (
915
+ event: { id?: string } = {},
916
+ context: { getRemainingTimeInMillis(): number },
917
+ ) => {
918
+ const correlationId = event.id ?? randomUUID();
919
+ const principal = cleanupPrincipal();
920
+ await dependencies.authorize({
921
+ action: ${identifierPrefix}AuthorizationActions.dataExportCleanup,
922
+ correlationId,
923
+ operationId: '${name}.cleanup-due-record-exports',
924
+ workloadIdentity: principal.actor.id,
925
+ });
926
+ let reportedDeadlineBreaches = 0;
927
+ let reportContinuationRequired = false;
928
+ let reportFailure: unknown;
929
+ const reportPendingBreaches = async () => {
930
+ try {
931
+ const reported = await dependencies.report({
932
+ correlationId,
933
+ report: (value) =>
934
+ emitCleanupDeadlineBreaches(dependencies.metrics, value) !==
935
+ undefined,
936
+ });
937
+ reportedDeadlineBreaches += reported.reported;
938
+ reportContinuationRequired ||= reported.continuationRequired;
939
+ } catch (error) {
940
+ reportFailure ??= error;
941
+ }
942
+ };
943
+ await reportPendingBreaches();
944
+ let result: Awaited<ReturnType<typeof cleanupDueRecordExports>> | undefined;
945
+ let cleanupFailure: unknown;
946
+ try {
947
+ result = await dependencies.cleanup({
948
+ correlationId,
949
+ maximumBatches: 1,
950
+ principal,
951
+ shouldContinue: () =>
952
+ context.getRemainingTimeInMillis() > MINIMUM_REMAINING_TIME_MS,
953
+ });
954
+ } catch (error) {
955
+ cleanupFailure = error;
956
+ }
957
+ await reportPendingBreaches();
958
+ if (reportFailure !== undefined) {
959
+ throw reportFailure;
960
+ }
961
+ if (reportedDeadlineBreaches > 0 || reportContinuationRequired) {
962
+ throw new Error('confidential-export-cleanup-deadline-breached');
963
+ }
964
+ if (cleanupFailure !== undefined) {
965
+ throw cleanupFailure;
966
+ }
967
+ if (!result) {
968
+ throw new Error('Confidential export cleanup returned no result.');
969
+ }
970
+ if (result.continuationRequired) {
971
+ throw new Error(
972
+ 'Confidential export cleanup requires scheduled retry continuation.',
973
+ );
974
+ }
975
+ return result;
976
+ };
977
+ }
978
+
979
+ export const handler = createDataExportCleanupHandler({
980
+ authorize: authorize${pascalName}InternalOperation,
981
+ cleanup: cleanupDueRecordExports,
982
+ metrics: createMetricEmitter(),
983
+ report: reportExportCleanupDeadlineBreaches,
984
+ });
985
+ `,
986
+ 'testing/data-rights-workflows.spec.ts': renderDataRightsTestSource(name, pascalName),
987
+ };
988
+ }
989
+ function renderRecordExportSource(name, pascalName) {
990
+ const identifierPrefix = (0, built_in_plan_shared_js_1.lowerCamelIdentifier)(pascalName);
991
+ return `import {
992
+ DeleteObjectsCommand,
993
+ GetObjectCommand,
994
+ HeadObjectCommand,
995
+ ListObjectVersionsCommand,
996
+ PutObjectCommand,
997
+ S3Client,
998
+ } from '@aws-sdk/client-s3';
999
+ import {
1000
+ assertAuthorizationAllowed,
1001
+ beginApprovalExecution,
1002
+ completeApprovalExecution,
1003
+ createApprovalRequest,
1004
+ decideApprovalRequest,
1005
+ type ApprovalRequest,
1006
+ type AuthorizationPrincipal,
1007
+ } from '@empire-builder-kit/runtime/authorization';
1008
+ import {
1009
+ createDataLifecycleExportRequest,
1010
+ evaluateDataLifecycleHolds,
1011
+ transitionDataLifecycleExport,
1012
+ type DataLifecycleExportRequestV1,
1013
+ } from '@empire-builder-kit/runtime/data-lifecycle';
1014
+ import type { Client as PgClient } from 'pg';
1015
+ import {
1016
+ ${identifierPrefix}AuthorizationPolicy,
1017
+ type ${pascalName}AuthorizationAction,
1018
+ } from '../authorization/policy.js';
1019
+ import {
1020
+ approvalReference,
1021
+ productRecordReference,
1022
+ } from './pseudonymous-reference.js';
1023
+ import {
1024
+ canonicalDataRightsNow,
1025
+ DATA_RIGHTS_POLICY_VERSION,
1026
+ dataRightsArtifactReference,
1027
+ dataRightsSubjectReference,
1028
+ loadActiveRecordHolds,
1029
+ plusDataRightsHours,
1030
+ requireDirectAdmin,
1031
+ requireExportCleanupAuthority,
1032
+ requireDirectHuman,
1033
+ writeDataRightsAudit,
1034
+ } from './data-rights-shared.js';
1035
+ import { withPrimaryDatabase } from '../database/runtime-client.js';
1036
+
1037
+ const EXPORT_ACTION = '${name}:record-export' as ${pascalName}AuthorizationAction;
1038
+ const EXPORT_SOURCE_COPY = 'primary-database';
1039
+ const CONFIDENTIAL_EXPORT_CLEANUP_FEATURE =
1040
+ 'confidential-export';
1041
+ const DEFAULT_EXPORT_CLEANUP_PAGE_SIZE = 25;
1042
+ const DEFAULT_EXPORT_CLEANUP_REPORT_PAGE_SIZE = 100;
1043
+ const MAXIMUM_PENDING_EXPORT_CLEANUP_REPORTS = ${PENDING_EXPORT_CLEANUP_REPORT_BOUND_LITERAL};
1044
+ const MAXIMUM_EXPORTS_PER_SUBJECT_PER_DAY = 5;
1045
+ const MAXIMUM_OUTSTANDING_CONFIDENTIAL_EXPORTS = 20;
1046
+ const APPROVAL_POLICY_VERSION =
1047
+ ${identifierPrefix}AuthorizationPolicy.version;
1048
+ const s3 = new S3Client({});
1049
+
1050
+ if (
1051
+ MAXIMUM_OUTSTANDING_CONFIDENTIAL_EXPORTS >=
1052
+ DEFAULT_EXPORT_CLEANUP_PAGE_SIZE
1053
+ ) {
1054
+ throw new Error(
1055
+ 'The global confidential-export bound must fit inside one cleanup page.',
1056
+ );
1057
+ }
1058
+
1059
+ export interface RecordExportObjectStore {
1060
+ send(command: unknown): Promise<unknown>;
1061
+ }
1062
+
1063
+ const defaultRecordExportObjectStore: RecordExportObjectStore = {
1064
+ send: (command) => s3.send(command as never),
1065
+ };
1066
+
1067
+ type ProductApproval = ApprovalRequest<${pascalName}AuthorizationAction>;
1068
+
1069
+ interface ExportWorkflow {
1070
+ approval: ProductApproval;
1071
+ artifactRef: string | null;
1072
+ cleanupDeadlineAt: Date | null;
1073
+ cleanupState: string;
1074
+ currentVersionId: string | null;
1075
+ expiresAt: Date | null;
1076
+ lifecycle: DataLifecycleExportRequestV1;
1077
+ objectKey: string;
1078
+ recordRef: string;
1079
+ subjectRef: string;
1080
+ uploadStartedAt: Date | null;
1081
+ }
1082
+
1083
+ function exportCommand(recordId: string) {
1084
+ return {
1085
+ format: 'application/json',
1086
+ recordRef: productRecordReference(recordId),
1087
+ };
1088
+ }
1089
+
1090
+ export function recordExportObjectKey(approvalRequestId: string): string {
1091
+ if (!/^[0-9a-f-]{36}$/i.test(approvalRequestId)) {
1092
+ throw new Error('Export object key requires an approval UUID.');
1093
+ }
1094
+ return 'confidential-exports/' + approvalRequestId + '/record.json';
1095
+ }
1096
+
1097
+ export function recordExportExpiresAt(now: string): string {
1098
+ // Leave bounded intervals for the hourly scan, EventBridge delivery retry,
1099
+ // and Lambda asynchronous execution retry inside the 24-hour maximum.
1100
+ // The S3 lifecycle rule remains defense in depth, not the deadline.
1101
+ return plusDataRightsHours(canonicalDataRightsNow(now), 21);
1102
+ }
1103
+
1104
+ export function recordExportCleanupDeadlineAt(now: string): string {
1105
+ return plusDataRightsHours(canonicalDataRightsNow(now), 24);
1106
+ }
1107
+
1108
+ export function recordExportSchedule(
1109
+ uploadStartedAt: string,
1110
+ persisted: {
1111
+ cleanupDeadlineAt?: string | null;
1112
+ expiresAt?: string | null;
1113
+ uploadStartedAt?: string | null;
1114
+ } = {},
1115
+ ): {
1116
+ cleanupDeadlineAt: string;
1117
+ expiresAt: string;
1118
+ uploadStartedAt: string;
1119
+ } {
1120
+ const persistedScheduleProvided =
1121
+ persisted.uploadStartedAt != null ||
1122
+ persisted.expiresAt != null ||
1123
+ persisted.cleanupDeadlineAt != null;
1124
+ if (
1125
+ persistedScheduleProvided &&
1126
+ (typeof persisted.uploadStartedAt !== 'string' ||
1127
+ typeof persisted.expiresAt !== 'string' ||
1128
+ typeof persisted.cleanupDeadlineAt !== 'string')
1129
+ ) {
1130
+ throw new Error(
1131
+ 'Persisted confidential-export cleanup schedule is incomplete.',
1132
+ );
1133
+ }
1134
+ uploadStartedAt = canonicalDataRightsNow(
1135
+ persisted.uploadStartedAt ?? uploadStartedAt,
1136
+ );
1137
+ const expectedExpiresAt = recordExportExpiresAt(uploadStartedAt);
1138
+ const expectedCleanupDeadlineAt =
1139
+ recordExportCleanupDeadlineAt(uploadStartedAt);
1140
+ const expiresAt = persisted.expiresAt ?? expectedExpiresAt;
1141
+ const cleanupDeadlineAt =
1142
+ persisted.cleanupDeadlineAt ?? expectedCleanupDeadlineAt;
1143
+ if (
1144
+ expiresAt !== expectedExpiresAt ||
1145
+ cleanupDeadlineAt !== expectedCleanupDeadlineAt
1146
+ ) {
1147
+ throw new Error(
1148
+ 'Persisted confidential-export cleanup schedule is inconsistent.',
1149
+ );
1150
+ }
1151
+ return { cleanupDeadlineAt, expiresAt, uploadStartedAt };
1152
+ }
1153
+
1154
+ function exactRecordExportSchedule(
1155
+ workflow: ExportWorkflow,
1156
+ fallbackUploadStartedAt: string,
1157
+ ): ReturnType<typeof recordExportSchedule> {
1158
+ return recordExportSchedule(
1159
+ fallbackUploadStartedAt,
1160
+ {
1161
+ cleanupDeadlineAt: workflow.cleanupDeadlineAt?.toISOString() ?? null,
1162
+ expiresAt: workflow.expiresAt?.toISOString() ?? null,
1163
+ uploadStartedAt: workflow.uploadStartedAt?.toISOString() ?? null,
1164
+ },
1165
+ );
1166
+ }
1167
+
1168
+ function exportResources(): { bucketName: string; kmsKeyArn: string } {
1169
+ const bucketName = process.env.DATA_EXPORT_BUCKET_NAME?.trim();
1170
+ const kmsKeyArn = process.env.DATA_EXPORT_KMS_KEY_ARN?.trim();
1171
+ if (!bucketName || !kmsKeyArn || !kmsKeyArn.startsWith('arn:aws:kms:')) {
1172
+ throw new Error(
1173
+ 'Confidential export requires linked DATA_EXPORT_BUCKET_NAME and DATA_EXPORT_KMS_KEY_ARN.',
1174
+ );
1175
+ }
1176
+ return { bucketName, kmsKeyArn };
1177
+ }
1178
+
1179
+ async function loadExportWorkflow(
1180
+ client: PgClient,
1181
+ approvalRequestId: string,
1182
+ ): Promise<ExportWorkflow | undefined> {
1183
+ const result = await client.query<ExportWorkflow>(
1184
+ \`SELECT a.request_json AS approval,
1185
+ d.request_json AS lifecycle,
1186
+ e.artifact_ref AS "artifactRef",
1187
+ e.cleanup_deadline_at AS "cleanupDeadlineAt",
1188
+ e.cleanup_state AS "cleanupState",
1189
+ e.current_version_id AS "currentVersionId",
1190
+ e.expires_at AS "expiresAt",
1191
+ e.object_key AS "objectKey",
1192
+ e.record_ref AS "recordRef",
1193
+ e.subject_ref AS "subjectRef",
1194
+ e.upload_started_at AS "uploadStartedAt"
1195
+ FROM approval_requests a
1196
+ JOIN data_lifecycle_requests d
1197
+ ON d.approval_request_id = a.id
1198
+ JOIN data_lifecycle_export_artifacts e
1199
+ ON e.approval_request_id = a.id
1200
+ WHERE a.id = $1::uuid
1201
+ FOR UPDATE OF a, d, e\`,
1202
+ [approvalRequestId],
1203
+ );
1204
+ return result.rows[0];
1205
+ }
1206
+
1207
+ async function persistApproval(
1208
+ client: PgClient,
1209
+ previousRevision: number,
1210
+ approval: ProductApproval,
1211
+ ): Promise<void> {
1212
+ const result = await client.query(
1213
+ \`UPDATE approval_requests
1214
+ SET expires_at = $3,
1215
+ policy_version = $4,
1216
+ request_digest = $5,
1217
+ request_json = $6::jsonb,
1218
+ revision = $7,
1219
+ status = $8
1220
+ WHERE id = $1::uuid AND revision = $2\`,
1221
+ [
1222
+ approval.id,
1223
+ previousRevision,
1224
+ approval.expiresAt,
1225
+ approval.policyVersion,
1226
+ approval.requestDigest,
1227
+ JSON.stringify(approval),
1228
+ approval.revision,
1229
+ approval.status,
1230
+ ],
1231
+ );
1232
+ if (result.rowCount !== 1) throw new Error('Approval compare-and-set failed.');
1233
+ }
1234
+
1235
+ async function persistExportState(
1236
+ client: PgClient,
1237
+ approvalRequestId: string,
1238
+ state: DataLifecycleExportRequestV1,
1239
+ ): Promise<void> {
1240
+ const result = await client.query(
1241
+ \`UPDATE data_lifecycle_requests
1242
+ SET request_json = $2::jsonb,
1243
+ revision = revision + 1,
1244
+ status = $3,
1245
+ updated_at = $4
1246
+ WHERE approval_request_id = $1::uuid\`,
1247
+ [
1248
+ approvalRequestId,
1249
+ JSON.stringify(state),
1250
+ state.status,
1251
+ state.updatedAt,
1252
+ ],
1253
+ );
1254
+ if (result.rowCount !== 1) {
1255
+ throw new Error('Export lifecycle compare-and-set failed.');
1256
+ }
1257
+ }
1258
+
1259
+ export async function requestAuthorizedRecordExport(input: {
1260
+ correlationId: string;
1261
+ now?: string;
1262
+ principal: AuthorizationPrincipal;
1263
+ recordId: string;
1264
+ }): Promise<{
1265
+ approvalRequestId: string;
1266
+ expiresAt: string;
1267
+ requestDigest: string;
1268
+ status: 'pending';
1269
+ }> {
1270
+ requireDirectHuman(input.principal);
1271
+ const now = canonicalDataRightsNow(input.now);
1272
+ const recordRef = productRecordReference(input.recordId);
1273
+ const subjectRef = dataRightsSubjectReference(input.principal.actor.id);
1274
+ const approval = createApprovalRequest<${pascalName}AuthorizationAction>({
1275
+ action: EXPORT_ACTION,
1276
+ author: input.principal,
1277
+ command: exportCommand(input.recordId),
1278
+ expiresAt: plusDataRightsHours(now, 1),
1279
+ now,
1280
+ policyVersion: APPROVAL_POLICY_VERSION,
1281
+ resource: { id: recordRef, type: 'record' },
1282
+ slice: '${name}',
1283
+ });
1284
+ const lifecycle = createDataLifecycleExportRequest({
1285
+ approvalRequirement: 'required',
1286
+ categoryId: 'product-record',
1287
+ createdAt: now,
1288
+ policyVersion: DATA_RIGHTS_POLICY_VERSION,
1289
+ requestRef: approvalReference(approval.id),
1290
+ scopeRef: recordRef,
1291
+ sourceCopyIds: [EXPORT_SOURCE_COPY],
1292
+ });
1293
+ await withPrimaryDatabase(
1294
+ { correlationId: input.correlationId },
1295
+ 'api-database-records',
1296
+ async (client) => {
1297
+ await client.query('BEGIN');
1298
+ try {
1299
+ await client.query(
1300
+ \`SELECT pg_advisory_xact_lock(
1301
+ hashtextextended('data-export-outstanding', 0)
1302
+ )\`,
1303
+ );
1304
+ await client.query(
1305
+ \`SELECT pg_advisory_xact_lock(
1306
+ hashtextextended('data-export-rate:' || $1, 0)
1307
+ )\`,
1308
+ [subjectRef],
1309
+ );
1310
+ const record = await client.query(
1311
+ \`SELECT 1
1312
+ FROM records
1313
+ WHERE id = $1::uuid AND owner_id = $2
1314
+ FOR UPDATE\`,
1315
+ [input.recordId, input.principal.actor.id],
1316
+ );
1317
+ if (record.rowCount !== 1) {
1318
+ throw new Error('The export target was not found.');
1319
+ }
1320
+ const outstanding = await client.query<{ count: string }>(
1321
+ \`SELECT count(*)::text AS count
1322
+ FROM data_lifecycle_export_artifacts
1323
+ WHERE cleanup_state <> 'completed'\`,
1324
+ );
1325
+ if (
1326
+ Number(outstanding.rows[0]?.count ?? 0) >=
1327
+ MAXIMUM_OUTSTANDING_CONFIDENTIAL_EXPORTS
1328
+ ) {
1329
+ throw new Error(
1330
+ 'The bounded global confidential-export capacity was exceeded.',
1331
+ );
1332
+ }
1333
+ const rate = await client.query<{ count: string }>(
1334
+ \`SELECT count(*)::text AS count
1335
+ FROM data_lifecycle_export_artifacts
1336
+ WHERE subject_ref = $1
1337
+ AND created_at >= $2::timestamptz - INTERVAL '24 hours'\`,
1338
+ [subjectRef, now],
1339
+ );
1340
+ if (
1341
+ Number(rate.rows[0]?.count ?? 0) >=
1342
+ MAXIMUM_EXPORTS_PER_SUBJECT_PER_DAY
1343
+ ) {
1344
+ throw new Error('The bounded subject export rate was exceeded.');
1345
+ }
1346
+ await client.query(
1347
+ \`INSERT INTO approval_requests
1348
+ (id, author_reference, expires_at, policy_version,
1349
+ request_digest, request_json, revision, status)
1350
+ VALUES ($1::uuid, $2, $3, $4, $5, $6::jsonb, $7, $8)\`,
1351
+ [
1352
+ approval.id,
1353
+ subjectRef,
1354
+ approval.expiresAt,
1355
+ approval.policyVersion,
1356
+ approval.requestDigest,
1357
+ JSON.stringify(approval),
1358
+ approval.revision,
1359
+ approval.status,
1360
+ ],
1361
+ );
1362
+ await client.query(
1363
+ \`INSERT INTO data_lifecycle_requests
1364
+ (id, approval_request_id, request_json, revision, status,
1365
+ updated_at)
1366
+ VALUES (gen_random_uuid(), $1::uuid, $2::jsonb, 0, $3, $4)\`,
1367
+ [approval.id, JSON.stringify(lifecycle), lifecycle.status, now],
1368
+ );
1369
+ await client.query(
1370
+ \`INSERT INTO data_lifecycle_export_artifacts
1371
+ (approval_request_id, cleanup_state, object_key, record_ref,
1372
+ subject_ref, updated_at)
1373
+ VALUES ($1::uuid, 'requested', $2, $3, $4, $5)\`,
1374
+ [
1375
+ approval.id,
1376
+ recordExportObjectKey(approval.id),
1377
+ recordRef,
1378
+ subjectRef,
1379
+ now,
1380
+ ],
1381
+ );
1382
+ await writeDataRightsAudit(client, {
1383
+ action: 'export',
1384
+ approvalRequestId: approval.id,
1385
+ correlationId: input.correlationId,
1386
+ now,
1387
+ outcome: 'succeeded',
1388
+ principal: input.principal,
1389
+ purposeCode: 'export-request',
1390
+ recordId: input.recordId,
1391
+ summary: 'A subject-scoped confidential export was requested.',
1392
+ });
1393
+ await client.query('COMMIT');
1394
+ } catch (error) {
1395
+ await client.query('ROLLBACK').catch(() => undefined);
1396
+ throw error;
1397
+ }
1398
+ },
1399
+ );
1400
+ return {
1401
+ approvalRequestId: approval.id,
1402
+ expiresAt: approval.expiresAt,
1403
+ requestDigest: approval.requestDigest,
1404
+ status: 'pending',
1405
+ };
1406
+ }
1407
+
1408
+ export async function decideRecordExport(input: {
1409
+ approvalRequestId: string;
1410
+ correlationId: string;
1411
+ expectedRequestDigest: string;
1412
+ now?: string;
1413
+ outcome: 'approved' | 'rejected';
1414
+ principal: AuthorizationPrincipal;
1415
+ reasonReference: string;
1416
+ recordId: string;
1417
+ }): Promise<{ status: 'approved' | 'rejected' }> {
1418
+ requireDirectAdmin(input.principal);
1419
+ const now = canonicalDataRightsNow(input.now);
1420
+ const status = await withPrimaryDatabase(
1421
+ { correlationId: input.correlationId },
1422
+ 'api-database-records',
1423
+ async (client) => {
1424
+ await client.query('BEGIN');
1425
+ try {
1426
+ const workflow = await loadExportWorkflow(
1427
+ client,
1428
+ input.approvalRequestId,
1429
+ );
1430
+ if (
1431
+ !workflow ||
1432
+ workflow.approval.action !== EXPORT_ACTION ||
1433
+ workflow.recordRef !== productRecordReference(input.recordId)
1434
+ ) {
1435
+ throw new Error('The export approval was not found.');
1436
+ }
1437
+ const transition = decideApprovalRequest(workflow.approval, {
1438
+ approver: input.principal,
1439
+ expectedRequestDigest: input.expectedRequestDigest,
1440
+ now,
1441
+ outcome: input.outcome,
1442
+ reasonReference: input.reasonReference,
1443
+ requiredPolicyVersion: APPROVAL_POLICY_VERSION,
1444
+ });
1445
+ assertAuthorizationAllowed(transition.decision);
1446
+ await persistApproval(
1447
+ client,
1448
+ workflow.approval.revision,
1449
+ transition.request,
1450
+ );
1451
+ if (transition.request.status === 'rejected') {
1452
+ const lifecycle = transitionDataLifecycleExport(
1453
+ workflow.lifecycle,
1454
+ {
1455
+ at: now,
1456
+ reasonCode: 'approval-rejected',
1457
+ type: 'reject',
1458
+ },
1459
+ );
1460
+ await persistExportState(
1461
+ client,
1462
+ input.approvalRequestId,
1463
+ lifecycle,
1464
+ );
1465
+ const artifact = await client.query(
1466
+ \`UPDATE data_lifecycle_export_artifacts
1467
+ SET cleanup_state = 'completed',
1468
+ last_error_code = NULL,
1469
+ updated_at = $2
1470
+ WHERE approval_request_id = $1::uuid
1471
+ AND cleanup_state = 'requested'
1472
+ AND upload_started_at IS NULL
1473
+ AND current_version_id IS NULL\`,
1474
+ [input.approvalRequestId, now],
1475
+ );
1476
+ if (artifact.rowCount !== 1) {
1477
+ throw new Error(
1478
+ 'Rejected export artifact lost compare-and-set.',
1479
+ );
1480
+ }
1481
+ }
1482
+ await writeDataRightsAudit(client, {
1483
+ action: 'export',
1484
+ approvalRequestId: input.approvalRequestId,
1485
+ correlationId: input.correlationId,
1486
+ now,
1487
+ outcome:
1488
+ transition.request.status === 'approved'
1489
+ ? 'succeeded'
1490
+ : 'rejected',
1491
+ principal: input.principal,
1492
+ purposeCode: 'export-decision',
1493
+ recordId: input.recordId,
1494
+ summary: 'A non-author decided a confidential export request.',
1495
+ });
1496
+ await client.query('COMMIT');
1497
+ return transition.request.status;
1498
+ } catch (error) {
1499
+ await client.query('ROLLBACK').catch(() => undefined);
1500
+ throw error;
1501
+ }
1502
+ },
1503
+ );
1504
+ return { status: status as 'approved' | 'rejected' };
1505
+ }
1506
+
1507
+ interface PreparedRecordExport {
1508
+ approval: ProductApproval;
1509
+ cleanupDeadlineAt: string;
1510
+ expiresAt: string;
1511
+ lifecycle: DataLifecycleExportRequestV1;
1512
+ objectKey: string;
1513
+ uploadStartedAt: string;
1514
+ }
1515
+
1516
+ async function prepareRecordExport(input: {
1517
+ approvalRequestId: string;
1518
+ correlationId: string;
1519
+ now: string;
1520
+ principal: AuthorizationPrincipal;
1521
+ recordId: string;
1522
+ }): Promise<PreparedRecordExport | { held: true }> {
1523
+ requireDirectHuman(input.principal);
1524
+ return withPrimaryDatabase(
1525
+ { correlationId: input.correlationId },
1526
+ 'api-database-records',
1527
+ async (client) => {
1528
+ await client.query('BEGIN');
1529
+ try {
1530
+ const workflow = await loadExportWorkflow(
1531
+ client,
1532
+ input.approvalRequestId,
1533
+ );
1534
+ if (
1535
+ !workflow ||
1536
+ workflow.approval.action !== EXPORT_ACTION ||
1537
+ workflow.recordRef !== productRecordReference(input.recordId) ||
1538
+ workflow.approval.author.actorId !== input.principal.actor.id
1539
+ ) {
1540
+ throw new Error('The export workflow was not found.');
1541
+ }
1542
+ if (
1543
+ workflow.lifecycle.status === 'ready' &&
1544
+ workflow.approval.status === 'executed'
1545
+ ) {
1546
+ const schedule = exactRecordExportSchedule(workflow, input.now);
1547
+ if (
1548
+ workflow.lifecycle.artifact?.expiresAt !== schedule.expiresAt
1549
+ ) {
1550
+ throw new Error(
1551
+ 'Published confidential-export expiry is inconsistent.',
1552
+ );
1553
+ }
1554
+ await client.query('COMMIT');
1555
+ return {
1556
+ approval: workflow.approval,
1557
+ cleanupDeadlineAt: schedule.cleanupDeadlineAt,
1558
+ expiresAt: schedule.expiresAt,
1559
+ lifecycle: workflow.lifecycle,
1560
+ objectKey: workflow.objectKey,
1561
+ uploadStartedAt: schedule.uploadStartedAt,
1562
+ };
1563
+ }
1564
+ const record = await client.query(
1565
+ \`SELECT 1
1566
+ FROM records
1567
+ WHERE id = $1::uuid AND owner_id = $2
1568
+ FOR UPDATE\`,
1569
+ [input.recordId, workflow.approval.author.actorId],
1570
+ );
1571
+ if (record.rowCount !== 1) {
1572
+ throw new Error('The export target was not found.');
1573
+ }
1574
+ let lifecycle = workflow.lifecycle;
1575
+ if (lifecycle.status === 'requested' || lifecycle.status === 'held') {
1576
+ const holds = await loadActiveRecordHolds(
1577
+ client,
1578
+ input.recordId,
1579
+ input.now,
1580
+ 'export',
1581
+ );
1582
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
1583
+ approvalRef: approvalReference(input.approvalRequestId),
1584
+ at: input.now,
1585
+ authorityRef: dataRightsSubjectReference(
1586
+ input.principal.actor.id,
1587
+ ),
1588
+ holds,
1589
+ type: 'verify',
1590
+ });
1591
+ await persistExportState(
1592
+ client,
1593
+ input.approvalRequestId,
1594
+ lifecycle,
1595
+ );
1596
+ if (lifecycle.status === 'held') {
1597
+ await writeDataRightsAudit(client, {
1598
+ action: 'export',
1599
+ approvalRequestId: input.approvalRequestId,
1600
+ correlationId: input.correlationId,
1601
+ now: input.now,
1602
+ outcome: 'held',
1603
+ principal: input.principal,
1604
+ purposeCode: 'export-execution',
1605
+ recordId: input.recordId,
1606
+ summary: 'An active record hold blocked confidential export.',
1607
+ });
1608
+ await client.query('COMMIT');
1609
+ return { held: true };
1610
+ }
1611
+ }
1612
+ const executionId = 'export:' + input.approvalRequestId;
1613
+ let approval = workflow.approval;
1614
+ if (approval.status === 'approved') {
1615
+ const begun = beginApprovalExecution(approval, {
1616
+ action: EXPORT_ACTION,
1617
+ command: exportCommand(input.recordId),
1618
+ executionId,
1619
+ now: input.now,
1620
+ requiredPolicyVersion: APPROVAL_POLICY_VERSION,
1621
+ resource: {
1622
+ id: productRecordReference(input.recordId),
1623
+ type: 'record',
1624
+ },
1625
+ });
1626
+ assertAuthorizationAllowed(begun.decision);
1627
+ await persistApproval(client, approval.revision, begun.request);
1628
+ approval = begun.request;
1629
+ } else if (
1630
+ approval.status !== 'executing' ||
1631
+ approval.execution?.executionId !== executionId
1632
+ ) {
1633
+ throw new Error('The exact export approval is not executable.');
1634
+ }
1635
+ if (lifecycle.status === 'verified') {
1636
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
1637
+ at: input.now,
1638
+ type: 'start-gathering',
1639
+ });
1640
+ }
1641
+ const copy = lifecycle.sourceCopies.find(
1642
+ ({ copyId }) => copyId === EXPORT_SOURCE_COPY,
1643
+ );
1644
+ if (copy?.status === 'pending' || copy?.status === 'failed') {
1645
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
1646
+ at: input.now,
1647
+ copyId: EXPORT_SOURCE_COPY,
1648
+ type: 'start-copy',
1649
+ });
1650
+ }
1651
+ await persistExportState(
1652
+ client,
1653
+ input.approvalRequestId,
1654
+ lifecycle,
1655
+ );
1656
+ const schedule = exactRecordExportSchedule(workflow, input.now);
1657
+ await client.query(
1658
+ \`UPDATE data_lifecycle_export_artifacts
1659
+ SET cleanup_state = 'gathering',
1660
+ updated_at = $2,
1661
+ upload_started_at = COALESCE(upload_started_at, $2),
1662
+ expires_at = COALESCE(expires_at, $3),
1663
+ cleanup_deadline_at = COALESCE(cleanup_deadline_at, $4)
1664
+ WHERE approval_request_id = $1::uuid\`,
1665
+ [
1666
+ input.approvalRequestId,
1667
+ input.now,
1668
+ schedule.expiresAt,
1669
+ schedule.cleanupDeadlineAt,
1670
+ ],
1671
+ );
1672
+ await client.query('COMMIT');
1673
+ return {
1674
+ approval,
1675
+ cleanupDeadlineAt: schedule.cleanupDeadlineAt,
1676
+ expiresAt: schedule.expiresAt,
1677
+ lifecycle,
1678
+ objectKey: workflow.objectKey,
1679
+ uploadStartedAt: schedule.uploadStartedAt,
1680
+ };
1681
+ } catch (error) {
1682
+ await client.query('ROLLBACK').catch(() => undefined);
1683
+ throw error;
1684
+ }
1685
+ },
1686
+ );
1687
+ }
1688
+
1689
+ export async function putEncryptedExport(
1690
+ objectKey: string,
1691
+ body: string,
1692
+ objectStore: RecordExportObjectStore = defaultRecordExportObjectStore,
1693
+ ): Promise<string> {
1694
+ const resources = exportResources();
1695
+ try {
1696
+ const result = (await objectStore.send(
1697
+ new PutObjectCommand({
1698
+ Body: body,
1699
+ Bucket: resources.bucketName,
1700
+ BucketKeyEnabled: true,
1701
+ ContentType: 'application/json',
1702
+ IfNoneMatch: '*',
1703
+ Key: objectKey,
1704
+ ServerSideEncryption: 'aws:kms',
1705
+ SSEKMSKeyId: resources.kmsKeyArn,
1706
+ }),
1707
+ )) as { VersionId?: string };
1708
+ if (!result.VersionId) {
1709
+ throw new Error('Versioned export put returned no version identifier.');
1710
+ }
1711
+ return result.VersionId;
1712
+ } catch (error) {
1713
+ const status = (error as { $metadata?: { httpStatusCode?: number } })
1714
+ .$metadata?.httpStatusCode;
1715
+ if (status !== 412) throw error;
1716
+ const existing = (await objectStore.send(
1717
+ new HeadObjectCommand({
1718
+ Bucket: resources.bucketName,
1719
+ Key: objectKey,
1720
+ }),
1721
+ )) as {
1722
+ SSEKMSKeyId?: string;
1723
+ ServerSideEncryption?: string;
1724
+ VersionId?: string;
1725
+ };
1726
+ if (
1727
+ !existing.VersionId ||
1728
+ existing.ServerSideEncryption !== 'aws:kms' ||
1729
+ existing.SSEKMSKeyId !== resources.kmsKeyArn
1730
+ ) {
1731
+ throw new Error('Existing export object failed encryption validation.');
1732
+ }
1733
+ return existing.VersionId;
1734
+ }
1735
+ }
1736
+
1737
+ export async function executeApprovedRecordExport(input: {
1738
+ approvalRequestId: string;
1739
+ correlationId: string;
1740
+ now?: string;
1741
+ principal: AuthorizationPrincipal;
1742
+ recordId: string;
1743
+ }): Promise<
1744
+ | { status: 'held' }
1745
+ | {
1746
+ artifactRef: string;
1747
+ expiresAt: string;
1748
+ status: 'ready';
1749
+ }
1750
+ > {
1751
+ const now = canonicalDataRightsNow(input.now);
1752
+ const prepared = await prepareRecordExport({ ...input, now });
1753
+ if ('held' in prepared) return { status: 'held' };
1754
+ if (
1755
+ prepared.lifecycle.status === 'ready' &&
1756
+ prepared.lifecycle.artifact
1757
+ ) {
1758
+ return {
1759
+ artifactRef: prepared.lifecycle.artifact.artifactRef,
1760
+ expiresAt: prepared.lifecycle.artifact.expiresAt,
1761
+ status: 'ready',
1762
+ };
1763
+ }
1764
+ const expiresAt = prepared.expiresAt;
1765
+ if (Date.parse(expiresAt) <= Date.parse(now)) {
1766
+ throw new Error(
1767
+ 'The confidential export expired before durable publication.',
1768
+ );
1769
+ }
1770
+ return withPrimaryDatabase(
1771
+ { correlationId: input.correlationId },
1772
+ 'api-database-records',
1773
+ async (client) => {
1774
+ await client.query('BEGIN');
1775
+ try {
1776
+ const workflow = await loadExportWorkflow(
1777
+ client,
1778
+ input.approvalRequestId,
1779
+ );
1780
+ if (
1781
+ !workflow ||
1782
+ workflow.approval.status !== 'executing' ||
1783
+ workflow.approval.execution?.executionId !==
1784
+ 'export:' + input.approvalRequestId ||
1785
+ workflow.lifecycle.status !== 'gathering' ||
1786
+ workflow.cleanupState !== 'gathering' ||
1787
+ workflow.expiresAt?.toISOString() !== expiresAt ||
1788
+ workflow.cleanupDeadlineAt?.toISOString() !==
1789
+ prepared.cleanupDeadlineAt
1790
+ ) {
1791
+ throw new Error('The executing export approval changed.');
1792
+ }
1793
+ const record = await client.query<{
1794
+ id: string;
1795
+ name: string;
1796
+ runScope: string;
1797
+ }>(
1798
+ \`SELECT id::text, name, run_scope AS "runScope"
1799
+ FROM records
1800
+ WHERE id = $1::uuid AND owner_id = $2
1801
+ FOR UPDATE\`,
1802
+ [input.recordId, workflow.approval.author.actorId],
1803
+ );
1804
+ const value = record.rows[0];
1805
+ if (!value) throw new Error('The export target was not found.');
1806
+ const holds = await loadActiveRecordHolds(
1807
+ client,
1808
+ input.recordId,
1809
+ now,
1810
+ 'export',
1811
+ );
1812
+ const holdEvaluation = evaluateDataLifecycleHolds({
1813
+ categoryId: 'product-record',
1814
+ holds,
1815
+ now,
1816
+ operation: 'export',
1817
+ scopeRef: productRecordReference(input.recordId),
1818
+ });
1819
+ if (holdEvaluation.blocked) {
1820
+ throw new Error('An active record hold blocks export publication.');
1821
+ }
1822
+ // Keep the workflow row locked across the versioned put. If the put
1823
+ // succeeds but a later database write fails, the durable gathering
1824
+ // row and its pre-persisted deadline remain available to cleanup.
1825
+ const versionId = await putEncryptedExport(
1826
+ prepared.objectKey,
1827
+ JSON.stringify(value),
1828
+ );
1829
+ const artifactRef = dataRightsArtifactReference(
1830
+ input.approvalRequestId + ':' + versionId,
1831
+ );
1832
+ let lifecycle = workflow.lifecycle;
1833
+ const activeCopy = lifecycle.sourceCopies.find(
1834
+ ({ copyId }) => copyId === EXPORT_SOURCE_COPY,
1835
+ );
1836
+ if (activeCopy?.status === 'in-progress') {
1837
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
1838
+ at: now,
1839
+ copyId: EXPORT_SOURCE_COPY,
1840
+ outcomeCode: 'gathered',
1841
+ type: 'complete-copy',
1842
+ });
1843
+ }
1844
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
1845
+ artifactRef,
1846
+ at: now,
1847
+ encryption: 'approved-profile',
1848
+ expiresAt,
1849
+ type: 'publish',
1850
+ });
1851
+ const completed = completeApprovalExecution(workflow.approval, {
1852
+ executionId: 'export:' + input.approvalRequestId,
1853
+ now,
1854
+ requiredPolicyVersion: APPROVAL_POLICY_VERSION,
1855
+ });
1856
+ assertAuthorizationAllowed(completed.decision);
1857
+ await persistApproval(
1858
+ client,
1859
+ workflow.approval.revision,
1860
+ completed.request,
1861
+ );
1862
+ await persistExportState(
1863
+ client,
1864
+ input.approvalRequestId,
1865
+ lifecycle,
1866
+ );
1867
+ await client.query(
1868
+ \`UPDATE data_lifecycle_export_artifacts
1869
+ SET artifact_ref = $2,
1870
+ cleanup_state = 'ready',
1871
+ current_version_id = $3,
1872
+ expires_at = $4,
1873
+ updated_at = $5
1874
+ WHERE approval_request_id = $1::uuid\`,
1875
+ [input.approvalRequestId, artifactRef, versionId, expiresAt, now],
1876
+ );
1877
+ await client.query(
1878
+ \`INSERT INTO data_lifecycle_export_versions
1879
+ (approval_request_id, is_delete_marker, object_key, status,
1880
+ version_id)
1881
+ VALUES ($1::uuid, false, $2, 'available', $3)
1882
+ ON CONFLICT (approval_request_id, version_id)
1883
+ DO UPDATE SET status = 'available'\`,
1884
+ [input.approvalRequestId, prepared.objectKey, versionId],
1885
+ );
1886
+ await writeDataRightsAudit(client, {
1887
+ action: 'export',
1888
+ approvalRequestId: input.approvalRequestId,
1889
+ correlationId: input.correlationId,
1890
+ now,
1891
+ outcome: 'succeeded',
1892
+ principal: input.principal,
1893
+ purposeCode: 'export-execution',
1894
+ recordId: input.recordId,
1895
+ summary:
1896
+ 'A KMS-encrypted subject export was created with bounded expiry.',
1897
+ });
1898
+ await client.query('COMMIT');
1899
+ return { artifactRef, expiresAt, status: 'ready' as const };
1900
+ } catch (error) {
1901
+ await client.query('ROLLBACK').catch(() => undefined);
1902
+ throw error;
1903
+ }
1904
+ },
1905
+ );
1906
+ }
1907
+
1908
+ interface DeliveredRecordExport {
1909
+ artifactRef: string;
1910
+ deliveredAt: string;
1911
+ expiresAt: string;
1912
+ record: {
1913
+ id: string;
1914
+ name: string;
1915
+ runScope: string;
1916
+ };
1917
+ }
1918
+
1919
+ function parseDeliveredRecordExport(
1920
+ value: string,
1921
+ recordId: string,
1922
+ ): DeliveredRecordExport['record'] {
1923
+ if (value.length > 64 * 1_024) {
1924
+ throw new Error('The confidential export artifact exceeds its bound.');
1925
+ }
1926
+ const candidate = JSON.parse(value) as {
1927
+ id?: unknown;
1928
+ name?: unknown;
1929
+ runScope?: unknown;
1930
+ };
1931
+ if (
1932
+ candidate.id !== recordId ||
1933
+ typeof candidate.name !== 'string' ||
1934
+ candidate.name.length < 1 ||
1935
+ candidate.name.length > 128 ||
1936
+ typeof candidate.runScope !== 'string' ||
1937
+ candidate.runScope.length < 1 ||
1938
+ candidate.runScope.length > 128
1939
+ ) {
1940
+ throw new Error('The confidential export artifact is invalid.');
1941
+ }
1942
+ return {
1943
+ id: candidate.id,
1944
+ name: candidate.name,
1945
+ runScope: candidate.runScope,
1946
+ };
1947
+ }
1948
+
1949
+ /**
1950
+ * Delivers an export through the authenticated API instead of exposing a
1951
+ * durable object URL. The workflow row, record, active holds, expiry, object
1952
+ * version, and author boundary are revalidated under one transaction, and the
1953
+ * successful download is committed to the Slice audit ledger before return.
1954
+ */
1955
+ export async function deliverReadyRecordExport(input: {
1956
+ approvalRequestId: string;
1957
+ correlationId: string;
1958
+ now?: string;
1959
+ principal: AuthorizationPrincipal;
1960
+ recordId: string;
1961
+ }): Promise<DeliveredRecordExport> {
1962
+ requireDirectHuman(input.principal);
1963
+ const now = canonicalDataRightsNow(input.now);
1964
+ const resources = exportResources();
1965
+ return withPrimaryDatabase(
1966
+ { correlationId: input.correlationId },
1967
+ 'api-database-records',
1968
+ async (client) => {
1969
+ await client.query('BEGIN');
1970
+ try {
1971
+ const workflow = await loadExportWorkflow(
1972
+ client,
1973
+ input.approvalRequestId,
1974
+ );
1975
+ const artifact = workflow?.lifecycle.artifact;
1976
+ if (
1977
+ !workflow ||
1978
+ workflow.approval.action !== EXPORT_ACTION ||
1979
+ workflow.recordRef !== productRecordReference(input.recordId) ||
1980
+ workflow.approval.author.actorId !== input.principal.actor.id
1981
+ ) {
1982
+ throw new Error('The export artifact was not found.');
1983
+ }
1984
+ if (
1985
+ workflow.approval.status !== 'executed' ||
1986
+ workflow.lifecycle.status !== 'ready' ||
1987
+ workflow.cleanupState !== 'ready' ||
1988
+ artifact?.status !== 'available' ||
1989
+ !workflow.currentVersionId ||
1990
+ !workflow.artifactRef
1991
+ ) {
1992
+ throw new Error('The export artifact is not ready for delivery.');
1993
+ }
1994
+ if (Date.parse(artifact.expiresAt) <= Date.parse(now)) {
1995
+ throw new Error('The export artifact has expired.');
1996
+ }
1997
+ const record = await client.query(
1998
+ \`SELECT 1
1999
+ FROM records
2000
+ WHERE id = $1::uuid AND owner_id = $2
2001
+ FOR UPDATE\`,
2002
+ [input.recordId, input.principal.actor.id],
2003
+ );
2004
+ if (record.rowCount !== 1) {
2005
+ throw new Error('The export target was not found.');
2006
+ }
2007
+ const holds = await loadActiveRecordHolds(
2008
+ client,
2009
+ input.recordId,
2010
+ now,
2011
+ 'export',
2012
+ );
2013
+ const holdEvaluation = evaluateDataLifecycleHolds({
2014
+ categoryId: 'product-record',
2015
+ holds,
2016
+ now,
2017
+ operation: 'export',
2018
+ scopeRef: productRecordReference(input.recordId),
2019
+ });
2020
+ if (holdEvaluation.blocked) {
2021
+ throw new Error('An active record hold blocks export delivery.');
2022
+ }
2023
+ const object = await s3.send(
2024
+ new GetObjectCommand({
2025
+ Bucket: resources.bucketName,
2026
+ Key: workflow.objectKey,
2027
+ VersionId: workflow.currentVersionId,
2028
+ }),
2029
+ );
2030
+ if (
2031
+ object.ContentLength === undefined ||
2032
+ object.ContentLength > 64 * 1_024 ||
2033
+ object.ContentType !== 'application/json' ||
2034
+ !object.Body
2035
+ ) {
2036
+ throw new Error('The confidential export artifact is invalid.');
2037
+ }
2038
+ const deliveredRecord = parseDeliveredRecordExport(
2039
+ await object.Body.transformToString(),
2040
+ input.recordId,
2041
+ );
2042
+ await writeDataRightsAudit(client, {
2043
+ action: 'export',
2044
+ approvalRequestId: input.approvalRequestId,
2045
+ correlationId: input.correlationId,
2046
+ now,
2047
+ outcome: 'succeeded',
2048
+ principal: input.principal,
2049
+ purposeCode: 'export-download',
2050
+ recordId: input.recordId,
2051
+ summary:
2052
+ 'An authorized subject downloaded an unexpired confidential export.',
2053
+ });
2054
+ await client.query('COMMIT');
2055
+ return {
2056
+ artifactRef: workflow.artifactRef,
2057
+ deliveredAt: now,
2058
+ expiresAt: artifact.expiresAt,
2059
+ record: deliveredRecord,
2060
+ };
2061
+ } catch (error) {
2062
+ await client.query('ROLLBACK').catch(() => undefined);
2063
+ throw error;
2064
+ }
2065
+ },
2066
+ );
2067
+ }
2068
+
2069
+ export interface ExportObjectVersion {
2070
+ isDeleteMarker: boolean;
2071
+ key: string;
2072
+ versionId: string;
2073
+ }
2074
+
2075
+ async function listExactExportVersions(
2076
+ objectKey: string,
2077
+ objectStore: RecordExportObjectStore,
2078
+ ): Promise<readonly ExportObjectVersion[]> {
2079
+ const { bucketName } = exportResources();
2080
+ const versions: ExportObjectVersion[] = [];
2081
+ let keyMarker: string | undefined;
2082
+ let versionIdMarker: string | undefined;
2083
+ do {
2084
+ const page = (await objectStore.send(
2085
+ new ListObjectVersionsCommand({
2086
+ Bucket: bucketName,
2087
+ KeyMarker: keyMarker,
2088
+ Prefix: objectKey,
2089
+ VersionIdMarker: versionIdMarker,
2090
+ }),
2091
+ )) as {
2092
+ DeleteMarkers?: Array<{ Key?: string; VersionId?: string }>;
2093
+ IsTruncated?: boolean;
2094
+ NextKeyMarker?: string;
2095
+ NextVersionIdMarker?: string;
2096
+ Versions?: Array<{ Key?: string; VersionId?: string }>;
2097
+ };
2098
+ for (const version of page.Versions ?? []) {
2099
+ if (version.Key === objectKey && version.VersionId) {
2100
+ versions.push({
2101
+ isDeleteMarker: false,
2102
+ key: objectKey,
2103
+ versionId: version.VersionId,
2104
+ });
2105
+ }
2106
+ }
2107
+ for (const marker of page.DeleteMarkers ?? []) {
2108
+ if (marker.Key === objectKey && marker.VersionId) {
2109
+ versions.push({
2110
+ isDeleteMarker: true,
2111
+ key: objectKey,
2112
+ versionId: marker.VersionId,
2113
+ });
2114
+ }
2115
+ }
2116
+ keyMarker = page.IsTruncated ? page.NextKeyMarker : undefined;
2117
+ versionIdMarker = page.IsTruncated
2118
+ ? page.NextVersionIdMarker
2119
+ : undefined;
2120
+ } while (keyMarker || versionIdMarker);
2121
+ return versions;
2122
+ }
2123
+
2124
+ async function deleteExactExportVersions(
2125
+ versions: readonly ExportObjectVersion[],
2126
+ objectStore: RecordExportObjectStore,
2127
+ ): Promise<ReadonlySet<string>> {
2128
+ const { bucketName } = exportResources();
2129
+ const failed = new Set<string>();
2130
+ for (let offset = 0; offset < versions.length; offset += 1_000) {
2131
+ const batch = versions.slice(offset, offset + 1_000);
2132
+ const batchVersionIds = new Set(batch.map(({ versionId }) => versionId));
2133
+ const result = (await objectStore.send(
2134
+ new DeleteObjectsCommand({
2135
+ Bucket: bucketName,
2136
+ Delete: {
2137
+ Objects: batch.map(({ key, versionId }) => ({
2138
+ Key: key,
2139
+ VersionId: versionId,
2140
+ })),
2141
+ Quiet: true,
2142
+ },
2143
+ }),
2144
+ )) as {
2145
+ Errors?: Array<{ Key?: string; VersionId?: string }>;
2146
+ };
2147
+ for (const error of result.Errors ?? []) {
2148
+ if (error.VersionId && batchVersionIds.has(error.VersionId)) {
2149
+ failed.add(error.VersionId);
2150
+ } else {
2151
+ // S3 does not require DeleteError.VersionId. An unmatched or
2152
+ // identifier-free error cannot be attributed safely when every
2153
+ // version shares one key, so fail the whole submitted batch closed.
2154
+ for (const { versionId } of batch) failed.add(versionId);
2155
+ }
2156
+ }
2157
+ }
2158
+ return failed;
2159
+ }
2160
+
2161
+ function mergeExportVersions(
2162
+ ...collections: ReadonlyArray<readonly ExportObjectVersion[]>
2163
+ ): readonly ExportObjectVersion[] {
2164
+ const versions = new Map<string, ExportObjectVersion>();
2165
+ for (const collection of collections) {
2166
+ for (const version of collection) {
2167
+ versions.set(version.versionId, version);
2168
+ }
2169
+ }
2170
+ return [...versions.values()].sort((left, right) =>
2171
+ left.versionId < right.versionId
2172
+ ? -1
2173
+ : left.versionId > right.versionId
2174
+ ? 1
2175
+ : 0,
2176
+ );
2177
+ }
2178
+
2179
+ export interface ExportObjectCleanupResult {
2180
+ attemptedVersions: readonly ExportObjectVersion[];
2181
+ failedVersionIds: readonly string[];
2182
+ remainingVersions: readonly ExportObjectVersion[];
2183
+ }
2184
+
2185
+ /**
2186
+ * Runs one idempotent exact-version cleanup attempt. Fresh S3 discovery is
2187
+ * durably recorded before deletion, while durable unfinished inventory is
2188
+ * retried even when a prior successful S3 delete was followed by database
2189
+ * rollback. A second exact listing is the completion proof.
2190
+ */
2191
+ export async function runExportObjectCleanupAttempt<Result>(input: {
2192
+ finalize(result: ExportObjectCleanupResult): Promise<Result>;
2193
+ objectKey: string;
2194
+ objectStore?: RecordExportObjectStore;
2195
+ recordDiscovered(
2196
+ versions: readonly ExportObjectVersion[],
2197
+ ): Promise<void>;
2198
+ trackedVersions: readonly ExportObjectVersion[];
2199
+ }): Promise<Result> {
2200
+ const objectStore = input.objectStore ?? defaultRecordExportObjectStore;
2201
+ const discovered = await listExactExportVersions(
2202
+ input.objectKey,
2203
+ objectStore,
2204
+ );
2205
+ await input.recordDiscovered(discovered);
2206
+ const attemptedVersions = mergeExportVersions(
2207
+ input.trackedVersions,
2208
+ discovered,
2209
+ );
2210
+ const failed = new Set(
2211
+ await deleteExactExportVersions(attemptedVersions, objectStore),
2212
+ );
2213
+ const remainingVersions = await listExactExportVersions(
2214
+ input.objectKey,
2215
+ objectStore,
2216
+ );
2217
+ await input.recordDiscovered(remainingVersions);
2218
+ for (const { versionId } of remainingVersions) failed.add(versionId);
2219
+ return input.finalize({
2220
+ attemptedVersions: mergeExportVersions(
2221
+ attemptedVersions,
2222
+ remainingVersions,
2223
+ ),
2224
+ failedVersionIds: [...failed].sort(),
2225
+ remainingVersions,
2226
+ });
2227
+ }
2228
+
2229
+ async function loadTrackedExportVersions(
2230
+ client: PgClient,
2231
+ approvalRequestId: string,
2232
+ ): Promise<readonly ExportObjectVersion[]> {
2233
+ const result = await client.query<{
2234
+ isDeleteMarker: boolean;
2235
+ key: string;
2236
+ versionId: string;
2237
+ }>(
2238
+ \`SELECT is_delete_marker AS "isDeleteMarker",
2239
+ object_key AS key,
2240
+ version_id AS "versionId"
2241
+ FROM data_lifecycle_export_versions
2242
+ WHERE approval_request_id = $1::uuid
2243
+ AND status <> 'deleted'
2244
+ ORDER BY version_id\`,
2245
+ [approvalRequestId],
2246
+ );
2247
+ return result.rows;
2248
+ }
2249
+
2250
+ async function recordDiscoveredExportVersions(
2251
+ client: PgClient,
2252
+ approvalRequestId: string,
2253
+ versions: readonly ExportObjectVersion[],
2254
+ ): Promise<void> {
2255
+ for (const version of versions) {
2256
+ await client.query(
2257
+ \`INSERT INTO data_lifecycle_export_versions
2258
+ (approval_request_id, is_delete_marker, object_key, status,
2259
+ version_id)
2260
+ VALUES ($1::uuid, $2, $3, 'deletion-in-progress', $4)
2261
+ ON CONFLICT (approval_request_id, version_id)
2262
+ DO UPDATE SET is_delete_marker = EXCLUDED.is_delete_marker,
2263
+ object_key = EXCLUDED.object_key,
2264
+ status = 'deletion-in-progress'\`,
2265
+ [
2266
+ approvalRequestId,
2267
+ version.isDeleteMarker,
2268
+ version.key,
2269
+ version.versionId,
2270
+ ],
2271
+ );
2272
+ }
2273
+ }
2274
+
2275
+ async function recordExportCleanupDeadlineReport(
2276
+ client: PgClient,
2277
+ input: {
2278
+ approvalRequestId: string;
2279
+ cleanupDeadlineAt: string;
2280
+ observedAt: string;
2281
+ },
2282
+ ): Promise<void> {
2283
+ if (Date.parse(input.cleanupDeadlineAt) > Date.parse(input.observedAt)) {
2284
+ return;
2285
+ }
2286
+ const capacity = await client.query<{ maximumPending: number }>(
2287
+ \`SELECT maximum_pending AS "maximumPending"
2288
+ FROM cleanup_deadline_report_capacity
2289
+ WHERE feature = $1
2290
+ FOR UPDATE\`,
2291
+ [CONFIDENTIAL_EXPORT_CLEANUP_FEATURE],
2292
+ );
2293
+ if (
2294
+ capacity.rowCount !== 1 ||
2295
+ capacity.rows[0]?.maximumPending !==
2296
+ MAXIMUM_PENDING_EXPORT_CLEANUP_REPORTS
2297
+ ) {
2298
+ throw new Error(
2299
+ 'Confidential-export cleanup deadline report capacity is invalid.',
2300
+ );
2301
+ }
2302
+ const inserted = await client.query(
2303
+ \`INSERT INTO cleanup_deadline_reports
2304
+ (deadline_at, feature, resource_ref)
2305
+ SELECT $1, $2, $3
2306
+ WHERE (
2307
+ SELECT count(*)
2308
+ FROM (
2309
+ SELECT 1
2310
+ FROM cleanup_deadline_reports
2311
+ WHERE feature = $2
2312
+ LIMIT $4
2313
+ ) AS bounded_pending_reports
2314
+ ) < $4
2315
+ ON CONFLICT (feature, resource_ref) DO NOTHING\`,
2316
+ [
2317
+ input.cleanupDeadlineAt,
2318
+ CONFIDENTIAL_EXPORT_CLEANUP_FEATURE,
2319
+ input.approvalRequestId,
2320
+ MAXIMUM_PENDING_EXPORT_CLEANUP_REPORTS,
2321
+ ],
2322
+ );
2323
+ const persistedDeadline = await client.query<{ deadlineAt: Date }>(
2324
+ \`SELECT deadline_at AS "deadlineAt"
2325
+ FROM cleanup_deadline_reports
2326
+ WHERE feature = $1 AND resource_ref = $2
2327
+ FOR UPDATE\`,
2328
+ [CONFIDENTIAL_EXPORT_CLEANUP_FEATURE, input.approvalRequestId],
2329
+ );
2330
+ const persisted = persistedDeadline.rows[0];
2331
+ if (persisted === undefined) {
2332
+ // The reporter acknowledged and deleted this row concurrently, or the
2333
+ // bounded inventory is saturated. Retained deletion authority must not
2334
+ // depend on the observability path; the next scan re-observes an
2335
+ // unresolved breach.
2336
+ if (inserted.rowCount === 1) {
2337
+ throw new Error(
2338
+ 'Persisted confidential-export cleanup deadline report is inconsistent.',
2339
+ );
2340
+ }
2341
+ return;
2342
+ }
2343
+ if (persisted.deadlineAt.toISOString() !== input.cleanupDeadlineAt) {
2344
+ throw new Error(
2345
+ 'Persisted confidential-export cleanup deadline report is inconsistent.',
2346
+ );
2347
+ }
2348
+ }
2349
+
2350
+ export async function recordPreparedExportCleanupDeadlineReport(
2351
+ client: PgClient,
2352
+ input: {
2353
+ approvalRequestId: string;
2354
+ cleanupDeadlineAt: Date | null;
2355
+ expiresAt: Date | null;
2356
+ lifecycleStatus: string;
2357
+ now: string;
2358
+ uploadStartedAt: Date | null;
2359
+ },
2360
+ ): Promise<void> {
2361
+ if (input.lifecycleStatus === 'completed') return;
2362
+ if (!input.uploadStartedAt || !input.cleanupDeadlineAt || !input.expiresAt) {
2363
+ throw new Error(
2364
+ 'Persisted confidential-export cleanup schedule is incomplete.',
2365
+ );
2366
+ }
2367
+ const exactSchedule = recordExportSchedule(input.now, {
2368
+ cleanupDeadlineAt: input.cleanupDeadlineAt.toISOString(),
2369
+ expiresAt: input.expiresAt.toISOString(),
2370
+ uploadStartedAt: input.uploadStartedAt.toISOString(),
2371
+ });
2372
+ await recordExportCleanupDeadlineReport(client, {
2373
+ approvalRequestId: input.approvalRequestId,
2374
+ cleanupDeadlineAt: exactSchedule.cleanupDeadlineAt,
2375
+ observedAt: input.now,
2376
+ });
2377
+ }
2378
+
2379
+ export async function recordFinalizedExportCleanupDeadlineReport(
2380
+ client: PgClient,
2381
+ input: {
2382
+ approvalRequestId: string;
2383
+ cleanupDeadlineAt: Date | null;
2384
+ expiresAt: Date | null;
2385
+ now: string;
2386
+ uploadStartedAt: Date | null;
2387
+ },
2388
+ ): Promise<void> {
2389
+ if (!input.uploadStartedAt || !input.cleanupDeadlineAt || !input.expiresAt) {
2390
+ throw new Error(
2391
+ 'Persisted confidential-export cleanup schedule is incomplete.',
2392
+ );
2393
+ }
2394
+ const exactSchedule = recordExportSchedule(input.now, {
2395
+ cleanupDeadlineAt: input.cleanupDeadlineAt.toISOString(),
2396
+ expiresAt: input.expiresAt.toISOString(),
2397
+ uploadStartedAt: input.uploadStartedAt.toISOString(),
2398
+ });
2399
+ const databaseClock = await client.query<{ observedAt: Date }>(
2400
+ \`SELECT clock_timestamp() AS "observedAt"\`,
2401
+ );
2402
+ const observedAt = databaseClock.rows[0]?.observedAt;
2403
+ if (!observedAt) {
2404
+ throw new Error(
2405
+ 'Confidential export cleanup database clock is unavailable.',
2406
+ );
2407
+ }
2408
+ await recordExportCleanupDeadlineReport(client, {
2409
+ approvalRequestId: input.approvalRequestId,
2410
+ cleanupDeadlineAt: exactSchedule.cleanupDeadlineAt,
2411
+ observedAt: observedAt.toISOString(),
2412
+ });
2413
+ }
2414
+
2415
+ export async function cleanupExpiredRecordExport(input: {
2416
+ approvalRequestId: string;
2417
+ correlationId: string;
2418
+ now?: string;
2419
+ principal: AuthorizationPrincipal;
2420
+ recordId?: string;
2421
+ recordReference?: string;
2422
+ }): Promise<{ deletedVersions: number; status: 'completed' | 'retry-required' }> {
2423
+ requireExportCleanupAuthority(input.principal);
2424
+ const now = canonicalDataRightsNow(input.now);
2425
+ if ((input.recordId === undefined) === (input.recordReference === undefined)) {
2426
+ throw new Error(
2427
+ 'Export cleanup requires exactly one record identifier or reference.',
2428
+ );
2429
+ }
2430
+ const expectedRecordRef =
2431
+ input.recordReference ?? productRecordReference(input.recordId!);
2432
+ const databaseWorkload =
2433
+ input.principal.kind === 'agent'
2434
+ ? ('data-export-cleanup' as const)
2435
+ : ('api-database-records' as const);
2436
+ const prepared = await withPrimaryDatabase(
2437
+ { correlationId: input.correlationId },
2438
+ databaseWorkload,
2439
+ async (client) => {
2440
+ await client.query('BEGIN');
2441
+ try {
2442
+ const workflow = await loadExportWorkflow(
2443
+ client,
2444
+ input.approvalRequestId,
2445
+ );
2446
+ if (
2447
+ !workflow ||
2448
+ workflow.recordRef !== expectedRecordRef
2449
+ ) {
2450
+ throw new Error('The export artifact was not found.');
2451
+ }
2452
+ if (workflow.lifecycle.status === 'completed') {
2453
+ await client.query('COMMIT');
2454
+ return {
2455
+ alreadyCompleted: true as const,
2456
+ objectKey: workflow.objectKey,
2457
+ };
2458
+ }
2459
+ if (
2460
+ !workflow.expiresAt ||
2461
+ Date.parse(workflow.expiresAt.toISOString()) > Date.parse(now)
2462
+ ) {
2463
+ throw new Error('The export artifact is not due for cleanup.');
2464
+ }
2465
+ await recordPreparedExportCleanupDeadlineReport(client, {
2466
+ approvalRequestId: input.approvalRequestId,
2467
+ cleanupDeadlineAt: workflow.cleanupDeadlineAt,
2468
+ expiresAt: workflow.expiresAt,
2469
+ lifecycleStatus: workflow.lifecycle.status,
2470
+ now,
2471
+ uploadStartedAt: workflow.uploadStartedAt,
2472
+ });
2473
+ let lifecycle = workflow.lifecycle;
2474
+ if (lifecycle.status === 'ready') {
2475
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
2476
+ at: now,
2477
+ type: 'expire',
2478
+ });
2479
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
2480
+ at: now,
2481
+ type: 'start-artifact-deletion',
2482
+ });
2483
+ } else if (
2484
+ lifecycle.status === 'failed' &&
2485
+ (lifecycle.failurePhase === 'artifact-deletion' ||
2486
+ lifecycle.failurePhase ===
2487
+ 'unpublished-artifact-deletion')
2488
+ ) {
2489
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
2490
+ at: now,
2491
+ type: 'retry',
2492
+ });
2493
+ } else if (lifecycle.status === 'gathering') {
2494
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
2495
+ at: now,
2496
+ type: 'start-unpublished-artifact-deletion',
2497
+ });
2498
+ } else if (
2499
+ lifecycle.status !== 'artifact-deletion-in-progress' &&
2500
+ lifecycle.status !==
2501
+ 'unpublished-artifact-deletion-in-progress'
2502
+ ) {
2503
+ throw new Error('The export artifact is not due for cleanup.');
2504
+ }
2505
+ await persistExportState(
2506
+ client,
2507
+ input.approvalRequestId,
2508
+ lifecycle,
2509
+ );
2510
+ await client.query(
2511
+ \`UPDATE data_lifecycle_export_artifacts
2512
+ SET cleanup_state = 'deletion-in-progress', updated_at = $2
2513
+ WHERE approval_request_id = $1::uuid\`,
2514
+ [input.approvalRequestId, now],
2515
+ );
2516
+ await client.query('COMMIT');
2517
+ return {
2518
+ alreadyCompleted: false as const,
2519
+ objectKey: workflow.objectKey,
2520
+ unpublished: lifecycle.artifact === undefined,
2521
+ };
2522
+ } catch (error) {
2523
+ await client.query('ROLLBACK').catch(() => undefined);
2524
+ throw error;
2525
+ }
2526
+ },
2527
+ );
2528
+ if (prepared.alreadyCompleted) {
2529
+ return { deletedVersions: 0, status: 'completed' };
2530
+ }
2531
+ const trackedVersions = await withPrimaryDatabase(
2532
+ { correlationId: input.correlationId },
2533
+ databaseWorkload,
2534
+ (client) =>
2535
+ loadTrackedExportVersions(client, input.approvalRequestId),
2536
+ );
2537
+ return runExportObjectCleanupAttempt({
2538
+ objectKey: prepared.objectKey,
2539
+ trackedVersions,
2540
+ recordDiscovered: (versions) =>
2541
+ withPrimaryDatabase(
2542
+ { correlationId: input.correlationId },
2543
+ databaseWorkload,
2544
+ async (client) => {
2545
+ await client.query('BEGIN');
2546
+ try {
2547
+ await recordDiscoveredExportVersions(
2548
+ client,
2549
+ input.approvalRequestId,
2550
+ versions,
2551
+ );
2552
+ await client.query('COMMIT');
2553
+ } catch (error) {
2554
+ await client.query('ROLLBACK').catch(() => undefined);
2555
+ throw error;
2556
+ }
2557
+ },
2558
+ ),
2559
+ finalize: ({ attemptedVersions, failedVersionIds }) =>
2560
+ withPrimaryDatabase(
2561
+ { correlationId: input.correlationId },
2562
+ databaseWorkload,
2563
+ async (client) => {
2564
+ await client.query('BEGIN');
2565
+ try {
2566
+ const workflow = await loadExportWorkflow(
2567
+ client,
2568
+ input.approvalRequestId,
2569
+ );
2570
+ if (!workflow) {
2571
+ throw new Error('The export workflow was not found.');
2572
+ }
2573
+ if (workflow.lifecycle.status === 'completed') {
2574
+ await client.query('COMMIT');
2575
+ return { deletedVersions: 0, status: 'completed' as const };
2576
+ }
2577
+ await recordFinalizedExportCleanupDeadlineReport(client, {
2578
+ approvalRequestId: input.approvalRequestId,
2579
+ cleanupDeadlineAt: workflow.cleanupDeadlineAt,
2580
+ expiresAt: workflow.expiresAt,
2581
+ now,
2582
+ uploadStartedAt: workflow.uploadStartedAt,
2583
+ });
2584
+ const failed = new Set(failedVersionIds);
2585
+ for (const version of attemptedVersions) {
2586
+ await client.query(
2587
+ \`UPDATE data_lifecycle_export_versions
2588
+ SET deleted_at = CASE WHEN $3 THEN NULL ELSE $4 END,
2589
+ status = CASE WHEN $3 THEN 'failed' ELSE 'deleted' END
2590
+ WHERE approval_request_id = $1::uuid AND version_id = $2\`,
2591
+ [
2592
+ input.approvalRequestId,
2593
+ version.versionId,
2594
+ failed.has(version.versionId),
2595
+ now,
2596
+ ],
2597
+ );
2598
+ }
2599
+ let lifecycle = workflow.lifecycle;
2600
+ const unpublished = lifecycle.artifact === undefined;
2601
+ if (
2602
+ lifecycle.status === 'failed' &&
2603
+ (lifecycle.failurePhase === 'artifact-deletion' ||
2604
+ lifecycle.failurePhase ===
2605
+ 'unpublished-artifact-deletion')
2606
+ ) {
2607
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
2608
+ at: now,
2609
+ type: 'retry',
2610
+ });
2611
+ }
2612
+ if (
2613
+ lifecycle.status !== 'artifact-deletion-in-progress' &&
2614
+ lifecycle.status !==
2615
+ 'unpublished-artifact-deletion-in-progress'
2616
+ ) {
2617
+ throw new Error('The export cleanup attempt changed.');
2618
+ }
2619
+ if (failed.size > 0) {
2620
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
2621
+ at: now,
2622
+ errorCode: 'object-version-delete-failed',
2623
+ type: 'fail-artifact-deletion',
2624
+ });
2625
+ } else {
2626
+ lifecycle = transitionDataLifecycleExport(lifecycle, {
2627
+ at: now,
2628
+ outcomeCode:
2629
+ attemptedVersions.length === 0 ? 'not-found' : 'deleted',
2630
+ type: 'complete-artifact-deletion',
2631
+ });
2632
+ }
2633
+ await persistExportState(
2634
+ client,
2635
+ input.approvalRequestId,
2636
+ lifecycle,
2637
+ );
2638
+ if (failed.size === 0 && unpublished) {
2639
+ const completed = completeApprovalExecution(
2640
+ workflow.approval,
2641
+ {
2642
+ executionId: 'export:' + input.approvalRequestId,
2643
+ now,
2644
+ requiredPolicyVersion: APPROVAL_POLICY_VERSION,
2645
+ },
2646
+ );
2647
+ assertAuthorizationAllowed(completed.decision);
2648
+ await persistApproval(
2649
+ client,
2650
+ workflow.approval.revision,
2651
+ completed.request,
2652
+ );
2653
+ }
2654
+ await client.query(
2655
+ \`UPDATE data_lifecycle_export_artifacts
2656
+ SET cleanup_state = $2,
2657
+ last_error_code = $3,
2658
+ updated_at = $4
2659
+ WHERE approval_request_id = $1::uuid\`,
2660
+ [
2661
+ input.approvalRequestId,
2662
+ failed.size > 0 ? 'failed' : 'completed',
2663
+ failed.size > 0
2664
+ ? 'object-version-delete-failed'
2665
+ : null,
2666
+ now,
2667
+ ],
2668
+ );
2669
+ await writeDataRightsAudit(client, {
2670
+ action: 'export',
2671
+ approvalRequestId: input.approvalRequestId,
2672
+ correlationId: input.correlationId,
2673
+ now,
2674
+ outcome:
2675
+ failed.size > 0 ? 'retry-required' : 'succeeded',
2676
+ principal: input.principal,
2677
+ purposeCode: 'export-expiry',
2678
+ ...(input.recordId
2679
+ ? { recordId: input.recordId }
2680
+ : { resourceReference: expectedRecordRef }),
2681
+ summary:
2682
+ failed.size > 0
2683
+ ? 'Confidential export version cleanup requires retry.'
2684
+ : 'Every discovered confidential export version was deleted.',
2685
+ });
2686
+ await client.query('COMMIT');
2687
+ return {
2688
+ deletedVersions: attemptedVersions.length - failed.size,
2689
+ status:
2690
+ failed.size > 0
2691
+ ? ('retry-required' as const)
2692
+ : ('completed' as const),
2693
+ };
2694
+ } catch (error) {
2695
+ await client.query('ROLLBACK').catch(() => undefined);
2696
+ throw error;
2697
+ }
2698
+ },
2699
+ ),
2700
+ });
2701
+ }
2702
+
2703
+ export interface RecordExportCleanupDependencies {
2704
+ cleanupExpiredRecordExport: typeof cleanupExpiredRecordExport;
2705
+ withDatabase: typeof withPrimaryDatabase;
2706
+ }
2707
+
2708
+ export async function reportExportCleanupDeadlineBreaches(
2709
+ input: {
2710
+ correlationId: string;
2711
+ pageSize?: number;
2712
+ report(value: number): boolean;
2713
+ },
2714
+ dependencies: Pick<RecordExportCleanupDependencies, 'withDatabase'> = {
2715
+ withDatabase: withPrimaryDatabase,
2716
+ },
2717
+ ): Promise<{ continuationRequired: boolean; reported: number }> {
2718
+ const pageSize =
2719
+ input.pageSize ?? DEFAULT_EXPORT_CLEANUP_REPORT_PAGE_SIZE;
2720
+ if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100) {
2721
+ throw new Error(
2722
+ 'Export cleanup deadline report page size must be from 1 through 100.',
2723
+ );
2724
+ }
2725
+ return dependencies.withDatabase(
2726
+ { correlationId: input.correlationId },
2727
+ 'data-export-cleanup',
2728
+ async (client) => {
2729
+ await client.query('BEGIN');
2730
+ try {
2731
+ const pending = await client.query<{ resourceRef: string }>(
2732
+ \`SELECT resource_ref AS "resourceRef"
2733
+ FROM cleanup_deadline_reports
2734
+ WHERE feature = $1
2735
+ ORDER BY resource_ref::uuid
2736
+ LIMIT $2
2737
+ FOR UPDATE\`,
2738
+ [CONFIDENTIAL_EXPORT_CLEANUP_FEATURE, pageSize + 1],
2739
+ );
2740
+ const selected = pending.rows.slice(0, pageSize);
2741
+ if (!input.report(selected.length)) {
2742
+ throw new Error(
2743
+ 'Confidential export cleanup deadline metric delivery failed.',
2744
+ );
2745
+ }
2746
+ if (selected.length > 0) {
2747
+ await client.query(
2748
+ \`DELETE FROM cleanup_deadline_reports
2749
+ WHERE feature = $1 AND resource_ref = ANY($2::text[])\`,
2750
+ [
2751
+ CONFIDENTIAL_EXPORT_CLEANUP_FEATURE,
2752
+ selected.map(({ resourceRef }) => resourceRef),
2753
+ ],
2754
+ );
2755
+ }
2756
+ await client.query('COMMIT');
2757
+ return {
2758
+ continuationRequired: pending.rows.length > pageSize,
2759
+ reported: selected.length,
2760
+ };
2761
+ } catch (error) {
2762
+ await client.query('ROLLBACK').catch(() => undefined);
2763
+ throw error;
2764
+ }
2765
+ },
2766
+ );
2767
+ }
2768
+
2769
+ export async function cleanupDueRecordExports(input: {
2770
+ correlationId: string;
2771
+ limit?: number;
2772
+ maximumBatches?: number;
2773
+ now?: string;
2774
+ principal: AuthorizationPrincipal;
2775
+ shouldContinue?: () => boolean;
2776
+ }, dependencies: Partial<RecordExportCleanupDependencies> = {}): Promise<{
2777
+ batches: number;
2778
+ completed: number;
2779
+ continuationRequired: boolean;
2780
+ deadlineBreached: boolean;
2781
+ processed: number;
2782
+ remainingDeadlineBreaches: number;
2783
+ remainingDue: number;
2784
+ retryRequired: number;
2785
+ }> {
2786
+ requireExportCleanupAuthority(input.principal);
2787
+ const now = canonicalDataRightsNow(input.now);
2788
+ const nowEpochMilliseconds = Date.parse(now);
2789
+ const limit = input.limit ?? DEFAULT_EXPORT_CLEANUP_PAGE_SIZE;
2790
+ const maximumBatches = input.maximumBatches ?? 100;
2791
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
2792
+ throw new Error('Export cleanup batch size must be from 1 through 100.');
2793
+ }
2794
+ if (
2795
+ !Number.isSafeInteger(maximumBatches) ||
2796
+ maximumBatches < 1 ||
2797
+ maximumBatches > 100
2798
+ ) {
2799
+ throw new Error(
2800
+ 'Export cleanup batch count must be from 1 through 100.',
2801
+ );
2802
+ }
2803
+ const cleanupExpired =
2804
+ dependencies.cleanupExpiredRecordExport ?? cleanupExpiredRecordExport;
2805
+ const withDatabase = dependencies.withDatabase ?? withPrimaryDatabase;
2806
+ const shouldContinue = input.shouldContinue ?? (() => true);
2807
+ let batches = 0;
2808
+ let completed = 0;
2809
+ let cursorApprovalRequestId: string | null = null;
2810
+ let cursorExpiresAt: string | null = null;
2811
+ let observedDeadlineBreach = await withDatabase(
2812
+ { correlationId: input.correlationId },
2813
+ 'data-export-cleanup',
2814
+ async (client) => {
2815
+ await client.query('BEGIN');
2816
+ try {
2817
+ const activeSchedules = await client.query<{
2818
+ approvalRequestId: string;
2819
+ cleanupDeadlineAt: Date | null;
2820
+ expiresAt: Date | null;
2821
+ uploadStartedAt: Date | null;
2822
+ }>(
2823
+ \`SELECT approval_request_id::text AS "approvalRequestId",
2824
+ cleanup_deadline_at AS "cleanupDeadlineAt",
2825
+ expires_at AS "expiresAt",
2826
+ upload_started_at AS "uploadStartedAt"
2827
+ FROM data_lifecycle_export_artifacts
2828
+ WHERE cleanup_state IN (
2829
+ 'gathering',
2830
+ 'ready',
2831
+ 'deletion-in-progress',
2832
+ 'failed'
2833
+ )
2834
+ ORDER BY approval_request_id
2835
+ LIMIT $1
2836
+ FOR UPDATE\`,
2837
+ [MAXIMUM_OUTSTANDING_CONFIDENTIAL_EXPORTS + 1],
2838
+ );
2839
+ if (
2840
+ activeSchedules.rows.length >
2841
+ MAXIMUM_OUTSTANDING_CONFIDENTIAL_EXPORTS
2842
+ ) {
2843
+ throw new Error(
2844
+ 'The bounded confidential-export cleanup inventory was exceeded.',
2845
+ );
2846
+ }
2847
+ let observed = false;
2848
+ for (const schedule of activeSchedules.rows) {
2849
+ if (
2850
+ schedule.uploadStartedAt === null ||
2851
+ schedule.expiresAt === null ||
2852
+ schedule.cleanupDeadlineAt === null
2853
+ ) {
2854
+ throw new Error(
2855
+ 'Persisted confidential-export cleanup schedule is incomplete.',
2856
+ );
2857
+ }
2858
+ const exactSchedule = recordExportSchedule(now, {
2859
+ cleanupDeadlineAt: schedule.cleanupDeadlineAt.toISOString(),
2860
+ expiresAt: schedule.expiresAt.toISOString(),
2861
+ uploadStartedAt: schedule.uploadStartedAt.toISOString(),
2862
+ });
2863
+ if (
2864
+ Date.parse(exactSchedule.cleanupDeadlineAt) <=
2865
+ nowEpochMilliseconds
2866
+ ) {
2867
+ observed = true;
2868
+ await recordExportCleanupDeadlineReport(client, {
2869
+ approvalRequestId: schedule.approvalRequestId,
2870
+ cleanupDeadlineAt: exactSchedule.cleanupDeadlineAt,
2871
+ observedAt: now,
2872
+ });
2873
+ }
2874
+ }
2875
+ await client.query('COMMIT');
2876
+ return observed;
2877
+ } catch (error) {
2878
+ await client.query('ROLLBACK').catch(() => undefined);
2879
+ throw error;
2880
+ }
2881
+ },
2882
+ );
2883
+ let processed = 0;
2884
+ let retryRequired = 0;
2885
+ while (batches < maximumBatches && shouldContinue()) {
2886
+ const due = await withDatabase(
2887
+ { correlationId: input.correlationId },
2888
+ 'data-export-cleanup',
2889
+ (client) =>
2890
+ client.query<{
2891
+ approvalRequestId: string;
2892
+ cleanupDeadlineAt: Date | null;
2893
+ expiresAt: Date;
2894
+ recordReference: string;
2895
+ uploadStartedAt: Date | null;
2896
+ }>(
2897
+ \`SELECT approval_request_id::text AS "approvalRequestId",
2898
+ cleanup_deadline_at AS "cleanupDeadlineAt",
2899
+ expires_at AS "expiresAt",
2900
+ record_ref AS "recordReference",
2901
+ upload_started_at AS "uploadStartedAt"
2902
+ FROM data_lifecycle_export_artifacts
2903
+ WHERE cleanup_state IN (
2904
+ 'gathering',
2905
+ 'ready',
2906
+ 'deletion-in-progress',
2907
+ 'failed'
2908
+ )
2909
+ AND expires_at IS NOT NULL
2910
+ AND expires_at <= $1
2911
+ AND (
2912
+ $3::timestamptz IS NULL OR
2913
+ (expires_at, approval_request_id) >
2914
+ ($3::timestamptz, $4::uuid)
2915
+ )
2916
+ ORDER BY expires_at, approval_request_id
2917
+ LIMIT $2\`,
2918
+ [now, limit, cursorExpiresAt, cursorApprovalRequestId],
2919
+ ),
2920
+ );
2921
+ if (due.rows.length === 0) {
2922
+ break;
2923
+ }
2924
+ batches += 1;
2925
+ for (const candidate of due.rows) {
2926
+ if (
2927
+ candidate.uploadStartedAt === null ||
2928
+ candidate.cleanupDeadlineAt === null
2929
+ ) {
2930
+ throw new Error(
2931
+ 'Persisted confidential-export cleanup schedule is incomplete.',
2932
+ );
2933
+ }
2934
+ const exactSchedule = recordExportSchedule(now, {
2935
+ cleanupDeadlineAt: candidate.cleanupDeadlineAt.toISOString(),
2936
+ expiresAt: candidate.expiresAt.toISOString(),
2937
+ uploadStartedAt: candidate.uploadStartedAt.toISOString(),
2938
+ });
2939
+ processed += 1;
2940
+ if (Date.parse(exactSchedule.cleanupDeadlineAt) <= nowEpochMilliseconds) {
2941
+ observedDeadlineBreach = true;
2942
+ }
2943
+ try {
2944
+ const result = await cleanupExpired({
2945
+ approvalRequestId: candidate.approvalRequestId,
2946
+ correlationId: input.correlationId,
2947
+ now,
2948
+ principal: input.principal,
2949
+ recordReference: candidate.recordReference,
2950
+ });
2951
+ if (result.status === 'completed') completed += 1;
2952
+ else retryRequired += 1;
2953
+ } catch {
2954
+ retryRequired += 1;
2955
+ }
2956
+ }
2957
+ const cursor = due.rows.at(-1);
2958
+ cursorApprovalRequestId = cursor?.approvalRequestId ?? null;
2959
+ cursorExpiresAt = cursor?.expiresAt.toISOString() ?? null;
2960
+ if (due.rows.length < limit) {
2961
+ break;
2962
+ }
2963
+ }
2964
+ const finalState = await withDatabase(
2965
+ { correlationId: input.correlationId },
2966
+ 'data-export-cleanup',
2967
+ (client) =>
2968
+ client.query<{
2969
+ remainingDeadlineBreaches: string;
2970
+ remainingDue: string;
2971
+ }>(
2972
+ \`SELECT
2973
+ count(*) FILTER (
2974
+ WHERE expires_at IS NOT NULL AND expires_at <= $1
2975
+ )::text AS "remainingDue",
2976
+ count(*) FILTER (
2977
+ WHERE cleanup_deadline_at IS NULL
2978
+ OR cleanup_deadline_at <= $1
2979
+ )::text AS "remainingDeadlineBreaches"
2980
+ FROM data_lifecycle_export_artifacts
2981
+ WHERE cleanup_state IN (
2982
+ 'gathering',
2983
+ 'ready',
2984
+ 'deletion-in-progress',
2985
+ 'failed'
2986
+ )\`,
2987
+ [now],
2988
+ ),
2989
+ );
2990
+ const remainingDeadlineBreaches = Number(
2991
+ finalState.rows[0]?.remainingDeadlineBreaches ?? 0,
2992
+ );
2993
+ const remainingDue = Number(finalState.rows[0]?.remainingDue ?? 0);
2994
+ return {
2995
+ batches,
2996
+ completed,
2997
+ continuationRequired: retryRequired > 0 || remainingDue > 0,
2998
+ deadlineBreached:
2999
+ observedDeadlineBreach || remainingDeadlineBreaches > 0,
3000
+ processed,
3001
+ remainingDeadlineBreaches,
3002
+ remainingDue,
3003
+ retryRequired,
3004
+ };
3005
+ }
3006
+ `;
3007
+ }
3008
+ function renderDataRightsDeadlineLockOrderIntegrationSource(name) {
3009
+ return `import type { AuthorizationPrincipal } from '@empire-builder-kit/runtime/authorization';
3010
+ import { Client } from 'pg';
3011
+ import { describe, expect, it } from 'vitest';
3012
+ import {
3013
+ cleanupDueRecordExports,
3014
+ recordPreparedExportCleanupDeadlineReport,
3015
+ reportExportCleanupDeadlineBreaches,
3016
+ } from '../data/record-export.js';
3017
+
3018
+ const databaseUrl = process.env.DATABASE_URL;
3019
+ if (!databaseUrl) {
3020
+ throw new Error(
3021
+ 'DATABASE_URL is required for ${name} deadline lock-order proof.',
3022
+ );
3023
+ }
3024
+
3025
+ const cleanupPrincipal: AuthorizationPrincipal = {
3026
+ actor: { id: '${name}:data-export-cleanup', type: 'service' },
3027
+ audience: '${name}:data-export-cleanup',
3028
+ authority: {
3029
+ machineIdentityId: '${name}:data-export-cleanup',
3030
+ mode: 'machine',
3031
+ },
3032
+ kind: 'agent',
3033
+ roles: [],
3034
+ scopes: [],
3035
+ subject: { id: '${name}:data-export-cleanup', type: 'service' },
3036
+ };
3037
+
3038
+ function deferred(): { promise: Promise<void>; resolve(): void } {
3039
+ let resolvePromise = () => undefined;
3040
+ const promise = new Promise<void>((resolve) => {
3041
+ resolvePromise = resolve;
3042
+ });
3043
+ return { promise, resolve: resolvePromise };
3044
+ }
3045
+
3046
+ // Time bounds in this proof are loaded-host budgets: hosted runners share
3047
+ // four cores between Vitest, the proof registry, and the dockerized
3048
+ // database, so idle-host margins intermittently fail under saturation.
3049
+ async function bounded<Result>(
3050
+ promise: Promise<Result>,
3051
+ label: string,
3052
+ timeoutMilliseconds = 15_000,
3053
+ ): Promise<Result> {
3054
+ let timer: ReturnType<typeof setTimeout> | undefined;
3055
+ try {
3056
+ return await Promise.race([
3057
+ promise,
3058
+ new Promise<never>((_resolve, reject) => {
3059
+ timer = setTimeout(
3060
+ () => reject(new Error(label + ' exceeded its time bound.')),
3061
+ timeoutMilliseconds,
3062
+ );
3063
+ }),
3064
+ ]);
3065
+ } finally {
3066
+ if (timer) clearTimeout(timer);
3067
+ }
3068
+ }
3069
+
3070
+ async function query(
3071
+ client: Client,
3072
+ text: string,
3073
+ values?: readonly unknown[],
3074
+ ) {
3075
+ return values === undefined
3076
+ ? client.query(text)
3077
+ : client.query(text, [...values]);
3078
+ }
3079
+
3080
+ async function waitForExpectedBlocker(
3081
+ observer: Client,
3082
+ blockedProcessId: number,
3083
+ blockerProcessId: number,
3084
+ ): Promise<void> {
3085
+ const deadline = Date.now() + 10_000;
3086
+ while (Date.now() < deadline) {
3087
+ const result = await observer.query<{ blockedByExpected: boolean }>(
3088
+ \`SELECT $2::integer = ANY(pg_blocking_pids($1))
3089
+ AS "blockedByExpected"\`,
3090
+ [blockedProcessId, blockerProcessId],
3091
+ );
3092
+ if (result.rows[0]?.blockedByExpected === true) return;
3093
+ await new Promise((resolve) => setTimeout(resolve, 10));
3094
+ }
3095
+ throw new Error('Backend did not reach the expected database blocker.');
3096
+ }
3097
+
3098
+ function databaseErrorCode(reason: unknown): string | undefined {
3099
+ if (!reason || typeof reason !== 'object' || !('code' in reason)) {
3100
+ return undefined;
3101
+ }
3102
+ return typeof reason.code === 'string' ? reason.code : undefined;
3103
+ }
3104
+
3105
+ describe('${name} confidential-export cleanup deadline lock order', () => {
3106
+ it('settles concurrent cleanup observation and metric acknowledgement in UUID order', async () => {
3107
+ const lowerResourceRef = '00000000-0000-4000-8000-000000000101';
3108
+ const higherResourceRef = '00000000-0000-4000-8000-000000000102';
3109
+ const resourceRefs = [lowerResourceRef, higherResourceRef] as const;
3110
+ const cleanupDeadlineAt = '2026-08-02T12:00:00.000Z';
3111
+ const expiresAt = '2026-08-02T09:00:00.000Z';
3112
+ const uploadStartedAt = '2026-08-01T12:00:00.000Z';
3113
+ const cleanup = new Client({ connectionString: databaseUrl });
3114
+ const reporter = new Client({ connectionString: databaseUrl });
3115
+ const observer = new Client({ connectionString: databaseUrl });
3116
+ try {
3117
+ await Promise.all([
3118
+ cleanup.connect(),
3119
+ reporter.connect(),
3120
+ observer.connect(),
3121
+ ]);
3122
+ await cleanup.query("SET lock_timeout = '10s'");
3123
+ // The reporter is deliberately blocked by cleanup. Its query therefore
3124
+ // cannot share the observer's deadline: equal time bounds race under a
3125
+ // loaded scheduler before the observer can prove the blocker identity.
3126
+ await reporter.query("SET lock_timeout = '0'");
3127
+ for (const client of [cleanup, reporter]) {
3128
+ await client.query("SET statement_timeout = '20s'");
3129
+ await client.query("SET deadlock_timeout = '100ms'");
3130
+ }
3131
+ await observer.query(
3132
+ \`DELETE FROM cleanup_deadline_reports
3133
+ WHERE feature = 'confidential-export'
3134
+ AND resource_ref = ANY($1::text[])\`,
3135
+ [resourceRefs],
3136
+ );
3137
+ await observer.query(
3138
+ 'DELETE FROM data_lifecycle_export_artifacts WHERE approval_request_id = ANY($1::uuid[])',
3139
+ [resourceRefs],
3140
+ );
3141
+ await observer.query(
3142
+ 'DELETE FROM approval_requests WHERE id = ANY($1::uuid[])',
3143
+ [resourceRefs],
3144
+ );
3145
+ for (const resourceRef of resourceRefs) {
3146
+ await observer.query(
3147
+ \`INSERT INTO approval_requests
3148
+ (id, author_reference, expires_at, policy_version,
3149
+ request_digest, request_json, revision, status)
3150
+ VALUES ($1::uuid, 'lock-order-proof', $2, 'lock-order-proof',
3151
+ 'lock-order-proof', '{}'::jsonb, 0, 'approved')\`,
3152
+ [resourceRef, cleanupDeadlineAt],
3153
+ );
3154
+ await observer.query(
3155
+ \`INSERT INTO data_lifecycle_export_artifacts
3156
+ (approval_request_id, cleanup_deadline_at, cleanup_state,
3157
+ expires_at, object_key, record_ref, subject_ref,
3158
+ upload_started_at)
3159
+ VALUES ($1::uuid, $2, 'ready', $3, $4, $5, $6, $7)\`,
3160
+ [
3161
+ resourceRef,
3162
+ cleanupDeadlineAt,
3163
+ expiresAt,
3164
+ 'exports/' + resourceRef,
3165
+ 'opaque:record:' + resourceRef,
3166
+ 'opaque:subject:' + resourceRef,
3167
+ uploadStartedAt,
3168
+ ],
3169
+ );
3170
+ }
3171
+ await observer.query(
3172
+ \`INSERT INTO cleanup_deadline_reports
3173
+ (deadline_at, feature, observed_at, resource_ref)
3174
+ VALUES
3175
+ ($1, 'confidential-export', $2, $3),
3176
+ ($1, 'confidential-export', $4, $5)\`,
3177
+ [
3178
+ cleanupDeadlineAt,
3179
+ '2026-08-02T12:00:01.000Z',
3180
+ lowerResourceRef,
3181
+ '2026-08-02T12:00:00.000Z',
3182
+ higherResourceRef,
3183
+ ],
3184
+ );
3185
+ const persistedOrder = await observer.query<{
3186
+ resourceRef: string;
3187
+ }>(
3188
+ \`SELECT resource_ref AS "resourceRef"
3189
+ FROM cleanup_deadline_reports
3190
+ WHERE feature = 'confidential-export'
3191
+ AND resource_ref = ANY($1::text[])
3192
+ ORDER BY observed_at, resource_ref\`,
3193
+ [resourceRefs],
3194
+ );
3195
+ expect(persistedOrder.rows.map(({ resourceRef }) => resourceRef)).toEqual([
3196
+ higherResourceRef,
3197
+ lowerResourceRef,
3198
+ ]);
3199
+
3200
+ const [cleanupBackend, reporterBackend] = await Promise.all([
3201
+ cleanup.query<{ processId: number }>(
3202
+ 'SELECT pg_backend_pid() AS "processId"',
3203
+ ),
3204
+ reporter.query<{ processId: number }>(
3205
+ 'SELECT pg_backend_pid() AS "processId"',
3206
+ ),
3207
+ ]);
3208
+ const cleanupProcessId = cleanupBackend.rows[0]?.processId;
3209
+ const reporterProcessId = reporterBackend.rows[0]?.processId;
3210
+ if (!cleanupProcessId || !reporterProcessId) {
3211
+ throw new Error('Database process identity is unavailable.');
3212
+ }
3213
+ const firstCleanupReportLocked = deferred();
3214
+ const reporterWaitingForCleanup = deferred();
3215
+ const cleanupCommitCompleted = deferred();
3216
+ let cleanupReportLocks = 0;
3217
+ let cleanupObservationCommitted = false;
3218
+ const cleanupDatabase = {
3219
+ query: async (text: string, values?: readonly unknown[]) => {
3220
+ const result = await query(cleanup, text, values);
3221
+ if (
3222
+ text.includes('SELECT deadline_at AS "deadlineAt"') &&
3223
+ text.includes('FOR UPDATE')
3224
+ ) {
3225
+ cleanupReportLocks += 1;
3226
+ if (cleanupReportLocks === 1) {
3227
+ firstCleanupReportLocked.resolve();
3228
+ await bounded(
3229
+ reporterWaitingForCleanup.promise,
3230
+ 'Reporter lock-order rendezvous',
3231
+ );
3232
+ }
3233
+ }
3234
+ if (text === 'COMMIT') {
3235
+ cleanupObservationCommitted = true;
3236
+ cleanupCommitCompleted.resolve();
3237
+ }
3238
+ return result;
3239
+ },
3240
+ };
3241
+ const reporterDatabase = {
3242
+ query: async (text: string, values?: readonly unknown[]) => {
3243
+ if (
3244
+ text.includes('SELECT resource_ref AS "resourceRef"') &&
3245
+ text.includes('FOR UPDATE')
3246
+ ) {
3247
+ await bounded(
3248
+ firstCleanupReportLocked.promise,
3249
+ 'Cleanup lock-order rendezvous',
3250
+ );
3251
+ const pending = query(reporter, text, values);
3252
+ void pending.catch(() => undefined);
3253
+ await waitForExpectedBlocker(
3254
+ observer,
3255
+ reporterProcessId,
3256
+ cleanupProcessId,
3257
+ );
3258
+ reporterWaitingForCleanup.resolve();
3259
+ const result = await pending;
3260
+ // PostgreSQL releases the row lock before the pg client's COMMIT
3261
+ // continuation is necessarily scheduled. Join that continuation
3262
+ // before the synchronous metric callback observes test state.
3263
+ await bounded(
3264
+ cleanupCommitCompleted.promise,
3265
+ 'Cleanup commit observation',
3266
+ );
3267
+ return result;
3268
+ }
3269
+ return query(reporter, text, values);
3270
+ },
3271
+ };
3272
+ const reportedValues: number[] = [];
3273
+ const settled = await bounded(
3274
+ Promise.allSettled([
3275
+ cleanupDueRecordExports(
3276
+ {
3277
+ correlationId: 'cleanup-deadline-lock-order',
3278
+ maximumBatches: 1,
3279
+ now: cleanupDeadlineAt,
3280
+ principal: cleanupPrincipal,
3281
+ shouldContinue: () => false,
3282
+ },
3283
+ { withDatabase: async (_context, _workload, operation) =>
3284
+ operation(cleanupDatabase as never) },
3285
+ ),
3286
+ reportExportCleanupDeadlineBreaches(
3287
+ {
3288
+ correlationId: 'report-deadline-lock-order',
3289
+ report: (value) => {
3290
+ expect(cleanupObservationCommitted).toBe(true);
3291
+ reportedValues.push(value);
3292
+ return true;
3293
+ },
3294
+ },
3295
+ { withDatabase: async (_context, _workload, operation) =>
3296
+ operation(reporterDatabase as never) },
3297
+ ),
3298
+ ]),
3299
+ 'Concurrent confidential-export deadline operations',
3300
+ 30_000,
3301
+ );
3302
+ const rejectionCodes = settled.flatMap((result) =>
3303
+ result.status === 'rejected'
3304
+ ? [databaseErrorCode(result.reason)]
3305
+ : [],
3306
+ );
3307
+ expect(rejectionCodes).not.toContain('40P01');
3308
+ expect(rejectionCodes).not.toContain('55P03');
3309
+ expect(settled.map(({ status }) => status)).toEqual([
3310
+ 'fulfilled',
3311
+ 'fulfilled',
3312
+ ]);
3313
+ if (settled[0]?.status !== 'fulfilled') throw settled[0]?.reason;
3314
+ if (settled[1]?.status !== 'fulfilled') throw settled[1]?.reason;
3315
+ expect(settled[0].value).toMatchObject({
3316
+ batches: 0,
3317
+ deadlineBreached: true,
3318
+ processed: 0,
3319
+ remainingDeadlineBreaches: 2,
3320
+ remainingDue: 2,
3321
+ });
3322
+ expect(settled[1].value).toEqual({
3323
+ continuationRequired: false,
3324
+ reported: 2,
3325
+ });
3326
+ expect(reportedValues).toEqual([2]);
3327
+
3328
+ const durableReports = await observer.query<{ resourceRef: string }>(
3329
+ \`SELECT resource_ref AS "resourceRef"
3330
+ FROM cleanup_deadline_reports
3331
+ WHERE feature = 'confidential-export'
3332
+ AND resource_ref = ANY($1::text[])
3333
+ ORDER BY resource_ref\`,
3334
+ [resourceRefs],
3335
+ );
3336
+ expect(durableReports.rows).toEqual([]);
3337
+ const activeArtifacts = await observer.query<{ approvalRequestId: string }>(
3338
+ \`SELECT approval_request_id::text AS "approvalRequestId"
3339
+ FROM data_lifecycle_export_artifacts
3340
+ WHERE approval_request_id = ANY($1::uuid[])
3341
+ ORDER BY approval_request_id\`,
3342
+ [resourceRefs],
3343
+ );
3344
+ expect(
3345
+ activeArtifacts.rows.map(({ approvalRequestId }) => approvalRequestId),
3346
+ ).toEqual(resourceRefs);
3347
+ } finally {
3348
+ await observer
3349
+ .query(
3350
+ \`DELETE FROM cleanup_deadline_reports
3351
+ WHERE feature = 'confidential-export'
3352
+ AND resource_ref = ANY($1::text[])\`,
3353
+ [resourceRefs],
3354
+ )
3355
+ .catch(() => undefined);
3356
+ await observer
3357
+ .query(
3358
+ 'DELETE FROM data_lifecycle_export_artifacts WHERE approval_request_id = ANY($1::uuid[])',
3359
+ [resourceRefs],
3360
+ )
3361
+ .catch(() => undefined);
3362
+ await observer
3363
+ .query(
3364
+ 'DELETE FROM approval_requests WHERE id = ANY($1::uuid[])',
3365
+ [resourceRefs],
3366
+ )
3367
+ .catch(() => undefined);
3368
+ await Promise.all([
3369
+ cleanup.end().catch(() => undefined),
3370
+ reporter.end().catch(() => undefined),
3371
+ observer.end().catch(() => undefined),
3372
+ ]);
3373
+ }
3374
+ });
3375
+
3376
+ it('serializes concurrent capacity decisions at the 1,000-report boundary', async () => {
3377
+ const deadlineAt = new Date('2026-08-02T12:00:00.000Z');
3378
+ const writerRefs = [
3379
+ 'capacity-proof-writer-a',
3380
+ 'capacity-proof-writer-b',
3381
+ ] as const;
3382
+ const first = new Client({ connectionString: databaseUrl });
3383
+ const second = new Client({ connectionString: databaseUrl });
3384
+ const observer = new Client({ connectionString: databaseUrl });
3385
+ const firstCapacityLocked = deferred();
3386
+ const releaseFirstCapacity = deferred();
3387
+ try {
3388
+ await Promise.all([first.connect(), second.connect(), observer.connect()]);
3389
+ await first.query("SET lock_timeout = '10s'");
3390
+ await second.query("SET lock_timeout = '0'");
3391
+ for (const client of [first, second]) {
3392
+ await client.query("SET statement_timeout = '20s'");
3393
+ }
3394
+ await observer.query(
3395
+ \`DELETE FROM cleanup_deadline_reports
3396
+ WHERE feature = 'confidential-export'
3397
+ AND resource_ref LIKE 'capacity-proof-%'\`,
3398
+ );
3399
+ const existing = await observer.query<{ count: string }>(
3400
+ \`SELECT count(*)::text AS count
3401
+ FROM cleanup_deadline_reports
3402
+ WHERE feature = 'confidential-export'\`,
3403
+ );
3404
+ const existingCount = Number(existing.rows[0]?.count ?? 0);
3405
+ if (existingCount > 999) {
3406
+ throw new Error(
3407
+ 'Capacity proof requires at most 999 pre-existing reports.',
3408
+ );
3409
+ }
3410
+ const seedCount = 999 - existingCount;
3411
+ await observer.query(
3412
+ \`INSERT INTO cleanup_deadline_reports
3413
+ (deadline_at, feature, resource_ref)
3414
+ SELECT $1, 'confidential-export',
3415
+ 'capacity-proof-seed-' || generate_series
3416
+ FROM generate_series(1, $2::integer)\`,
3417
+ [deadlineAt.toISOString(), seedCount],
3418
+ );
3419
+ const [firstBackend, secondBackend] = await Promise.all([
3420
+ first.query<{ processId: number }>(
3421
+ 'SELECT pg_backend_pid() AS "processId"',
3422
+ ),
3423
+ second.query<{ processId: number }>(
3424
+ 'SELECT pg_backend_pid() AS "processId"',
3425
+ ),
3426
+ ]);
3427
+ const firstProcessId = firstBackend.rows[0]?.processId;
3428
+ const secondProcessId = secondBackend.rows[0]?.processId;
3429
+ if (!firstProcessId || !secondProcessId) {
3430
+ throw new Error('Writer database process identity is unavailable.');
3431
+ }
3432
+
3433
+ const writeReport = async (
3434
+ client: Client,
3435
+ resourceRef: string,
3436
+ pauseAfterCapacityLock = false,
3437
+ ) => {
3438
+ await client.query('BEGIN');
3439
+ const database = {
3440
+ query: async (text: string, values?: readonly unknown[]) => {
3441
+ const result = await query(client, text, values);
3442
+ if (
3443
+ pauseAfterCapacityLock &&
3444
+ text.includes('FROM cleanup_deadline_report_capacity') &&
3445
+ text.includes('FOR UPDATE')
3446
+ ) {
3447
+ firstCapacityLocked.resolve();
3448
+ await bounded(
3449
+ releaseFirstCapacity.promise,
3450
+ 'Capacity serialization release',
3451
+ );
3452
+ }
3453
+ return result;
3454
+ },
3455
+ };
3456
+ try {
3457
+ await recordPreparedExportCleanupDeadlineReport(database as never, {
3458
+ approvalRequestId: resourceRef,
3459
+ cleanupDeadlineAt: deadlineAt,
3460
+ expiresAt: new Date('2026-08-02T09:00:00.000Z'),
3461
+ lifecycleStatus: 'artifact-deletion-in-progress',
3462
+ now: deadlineAt.toISOString(),
3463
+ uploadStartedAt: new Date('2026-08-01T12:00:00.000Z'),
3464
+ });
3465
+ await client.query('COMMIT');
3466
+ } catch (error) {
3467
+ await client.query('ROLLBACK').catch(() => undefined);
3468
+ throw error;
3469
+ }
3470
+ };
3471
+
3472
+ const firstWrite = writeReport(first, writerRefs[0], true);
3473
+ await bounded(firstCapacityLocked.promise, 'First capacity lock');
3474
+ const secondWrite = writeReport(second, writerRefs[1]);
3475
+ await waitForExpectedBlocker(
3476
+ observer,
3477
+ secondProcessId,
3478
+ firstProcessId,
3479
+ );
3480
+ releaseFirstCapacity.resolve();
3481
+ await bounded(
3482
+ Promise.all([firstWrite, secondWrite]),
3483
+ 'Concurrent capacity-bound report writes',
3484
+ 30_000,
3485
+ );
3486
+
3487
+ const finalInventory = await observer.query<{
3488
+ count: string;
3489
+ }>(
3490
+ \`SELECT count(*)::text AS count
3491
+ FROM cleanup_deadline_reports
3492
+ WHERE feature = 'confidential-export'\`,
3493
+ );
3494
+ expect(Number(finalInventory.rows[0]?.count)).toBe(1_000);
3495
+ const persistedWriters = await observer.query<{ resourceRef: string }>(
3496
+ \`SELECT resource_ref AS "resourceRef"
3497
+ FROM cleanup_deadline_reports
3498
+ WHERE feature = 'confidential-export'
3499
+ AND resource_ref = ANY($1::text[])
3500
+ ORDER BY resource_ref\`,
3501
+ [writerRefs],
3502
+ );
3503
+ expect(persistedWriters.rows).toHaveLength(1);
3504
+ } finally {
3505
+ releaseFirstCapacity.resolve();
3506
+ await Promise.all([
3507
+ first.query('ROLLBACK').catch(() => undefined),
3508
+ second.query('ROLLBACK').catch(() => undefined),
3509
+ ]);
3510
+ await observer
3511
+ .query(
3512
+ \`DELETE FROM cleanup_deadline_reports
3513
+ WHERE feature = 'confidential-export'
3514
+ AND resource_ref LIKE 'capacity-proof-%'\`,
3515
+ )
3516
+ .catch(() => undefined);
3517
+ await Promise.all([
3518
+ first.end().catch(() => undefined),
3519
+ second.end().catch(() => undefined),
3520
+ observer.end().catch(() => undefined),
3521
+ ]);
3522
+ }
3523
+ });
3524
+ });
3525
+ `;
3526
+ }
3527
+ function renderDataRightsTestSource(name, pascalName) {
3528
+ return `import {
3529
+ DeleteObjectsCommand,
3530
+ ListObjectVersionsCommand,
3531
+ PutObjectCommand,
3532
+ } from '@aws-sdk/client-s3';
3533
+ import {
3534
+ beginApprovalExecution,
3535
+ createApprovalRequest,
3536
+ decideApprovalRequest,
3537
+ type AuthorizationPrincipal,
3538
+ } from '@empire-builder-kit/runtime/authorization';
3539
+ import { parseDataLifecycleChangeEvent } from '@empire-builder-kit/runtime/data-lifecycle';
3540
+ import { describe, expect, it, vi } from 'vitest';
3541
+ import { createRecordCorrectionArtifacts } from '../data/record-correction.js';
3542
+ import {
3543
+ cleanupDueRecordExports,
3544
+ putEncryptedExport,
3545
+ recordExportCleanupDeadlineAt,
3546
+ recordExportExpiresAt,
3547
+ recordExportObjectKey,
3548
+ recordExportSchedule,
3549
+ recordFinalizedExportCleanupDeadlineReport,
3550
+ recordPreparedExportCleanupDeadlineReport,
3551
+ reportExportCleanupDeadlineBreaches,
3552
+ runExportObjectCleanupAttempt,
3553
+ type ExportObjectCleanupResult,
3554
+ type ExportObjectVersion,
3555
+ type RecordExportObjectStore,
3556
+ } from '../data/record-export.js';
3557
+ import { createRecordHold } from '../data/record-holds.js';
3558
+ import { createDataExportCleanupHandler } from '../jobs/data-export-cleanup.js';
3559
+ import {
3560
+ DATA_RIGHTS_POLICY_VERSION,
3561
+ dataRightsSubjectReference,
3562
+ requireDirectAdmin,
3563
+ } from '../data/data-rights-shared.js';
3564
+
3565
+ const directAdmin: AuthorizationPrincipal = {
3566
+ actor: { id: 'admin-1', type: 'user' },
3567
+ authority: { mode: 'direct' },
3568
+ kind: 'admin',
3569
+ roles: ['${name}-admin'],
3570
+ scopes: [],
3571
+ subject: { id: 'admin-1', type: 'user' },
3572
+ };
3573
+
3574
+ const directOwner: AuthorizationPrincipal = {
3575
+ actor: { id: 'record-owner', type: 'user' },
3576
+ authority: { mode: 'direct' },
3577
+ kind: 'customer',
3578
+ roles: [],
3579
+ scopes: [],
3580
+ subject: { id: 'record-owner', type: 'user' },
3581
+ };
3582
+
3583
+ const cleanupAgent: AuthorizationPrincipal = {
3584
+ actor: { id: '${name}:data-export-cleanup', type: 'service' },
3585
+ audience: '${name}:data-export-cleanup',
3586
+ authority: {
3587
+ machineIdentityId: '${name}:data-export-cleanup',
3588
+ mode: 'machine',
3589
+ },
3590
+ kind: 'agent',
3591
+ roles: [],
3592
+ scopes: [],
3593
+ subject: { id: '${name}:data-export-cleanup', type: 'service' },
3594
+ };
3595
+
3596
+ interface CleanupCandidate {
3597
+ approvalRequestId: string;
3598
+ cleanupDeadlineAt: Date | null;
3599
+ expiresAt: Date;
3600
+ recordReference: string;
3601
+ uploadStartedAt: Date | null;
3602
+ }
3603
+
3604
+ class FakeCleanupDatabase {
3605
+ clockTimestamp = new Date('2026-08-02T12:00:00.000Z');
3606
+ failCommitOnce = false;
3607
+ maximumPendingReports = ${PENDING_EXPORT_CLEANUP_REPORT_BOUND};
3608
+ readonly pendingReports = new Set<string>();
3609
+ readonly queries: string[] = [];
3610
+ readonly reportDeadlines = new Map<string, Date>();
3611
+ private reportDeadlineTransactionSnapshot: Map<string, Date> | null =
3612
+ null;
3613
+ private reportTransactionSnapshot: Set<string> | null = null;
3614
+
3615
+ constructor(readonly candidates: CleanupCandidate[]) {}
3616
+
3617
+ async query<Row>(
3618
+ text: string,
3619
+ values: readonly unknown[] = [],
3620
+ ): Promise<{ rowCount: number; rows: Row[] }> {
3621
+ this.queries.push(text);
3622
+ if (text === 'BEGIN') {
3623
+ this.reportTransactionSnapshot = new Set(this.pendingReports);
3624
+ this.reportDeadlineTransactionSnapshot = new Map(
3625
+ this.reportDeadlines,
3626
+ );
3627
+ return { rowCount: 0, rows: [] };
3628
+ }
3629
+ if (text === 'COMMIT') {
3630
+ if (this.failCommitOnce) {
3631
+ this.failCommitOnce = false;
3632
+ throw new Error('simulated post-emission commit failure');
3633
+ }
3634
+ this.reportDeadlineTransactionSnapshot = null;
3635
+ this.reportTransactionSnapshot = null;
3636
+ return { rowCount: 0, rows: [] };
3637
+ }
3638
+ if (text === 'ROLLBACK') {
3639
+ if (this.reportTransactionSnapshot) {
3640
+ this.pendingReports.clear();
3641
+ for (const resourceRef of this.reportTransactionSnapshot) {
3642
+ this.pendingReports.add(resourceRef);
3643
+ }
3644
+ }
3645
+ if (this.reportDeadlineTransactionSnapshot) {
3646
+ this.reportDeadlines.clear();
3647
+ for (const [resourceRef, deadlineAt] of
3648
+ this.reportDeadlineTransactionSnapshot) {
3649
+ this.reportDeadlines.set(resourceRef, deadlineAt);
3650
+ }
3651
+ }
3652
+ this.reportDeadlineTransactionSnapshot = null;
3653
+ this.reportTransactionSnapshot = null;
3654
+ return { rowCount: 0, rows: [] };
3655
+ }
3656
+ if (
3657
+ text.includes('FROM cleanup_deadline_report_capacity') &&
3658
+ text.includes('FOR UPDATE')
3659
+ ) {
3660
+ return {
3661
+ rowCount: 1,
3662
+ rows: [
3663
+ {
3664
+ maximumPending: this.maximumPendingReports,
3665
+ } as Row,
3666
+ ],
3667
+ };
3668
+ }
3669
+ if (
3670
+ text.includes('SELECT resource_ref AS "resourceRef"') &&
3671
+ text.includes('FROM cleanup_deadline_reports')
3672
+ ) {
3673
+ const limit = Number(values[1]);
3674
+ const rows = [...this.pendingReports]
3675
+ .sort()
3676
+ .slice(0, limit)
3677
+ .map((resourceRef) => ({ resourceRef })) as Row[];
3678
+ return { rowCount: rows.length, rows };
3679
+ }
3680
+ if (text.includes('DELETE FROM cleanup_deadline_reports')) {
3681
+ for (const resourceRef of values[1] as readonly string[]) {
3682
+ this.pendingReports.delete(resourceRef);
3683
+ this.reportDeadlines.delete(resourceRef);
3684
+ }
3685
+ return { rowCount: 0, rows: [] };
3686
+ }
3687
+ if (text.includes('INSERT INTO cleanup_deadline_reports')) {
3688
+ const deadlineAt = new Date(String(values[0]));
3689
+ const resourceRef = String(values[2]);
3690
+ const bound = Number(values[3]);
3691
+ if (this.reportDeadlines.has(resourceRef)) {
3692
+ return { rowCount: 0, rows: [] };
3693
+ }
3694
+ if (Number.isFinite(bound) && this.pendingReports.size >= bound) {
3695
+ return { rowCount: 0, rows: [] };
3696
+ }
3697
+ this.reportDeadlines.set(resourceRef, deadlineAt);
3698
+ this.pendingReports.add(resourceRef);
3699
+ return { rowCount: 1, rows: [] };
3700
+ }
3701
+ if (text.includes('SELECT deadline_at AS "deadlineAt"')) {
3702
+ const resourceRef = String(values[1]);
3703
+ const deadlineAt = this.reportDeadlines.get(resourceRef);
3704
+ return {
3705
+ rowCount: deadlineAt ? 1 : 0,
3706
+ rows: deadlineAt ? ([{ deadlineAt }] as Row[]) : [],
3707
+ };
3708
+ }
3709
+ if (text.includes('SELECT clock_timestamp() AS "observedAt"')) {
3710
+ return {
3711
+ rowCount: 1,
3712
+ rows: [{ observedAt: this.clockTimestamp } as Row],
3713
+ };
3714
+ }
3715
+ if (text.includes('bounded_pending_reports')) {
3716
+ return {
3717
+ rowCount: 1,
3718
+ rows: [{ count: String(this.pendingReports.size) } as Row],
3719
+ };
3720
+ }
3721
+ if (
3722
+ text.includes('ORDER BY approval_request_id') &&
3723
+ text.includes('upload_started_at AS "uploadStartedAt"')
3724
+ ) {
3725
+ const limit = Number(values[0]);
3726
+ return {
3727
+ rowCount: Math.min(this.candidates.length, limit),
3728
+ rows: this.candidates.slice(0, limit) as Row[],
3729
+ };
3730
+ }
3731
+ if (
3732
+ text.includes('ORDER BY expires_at, approval_request_id') &&
3733
+ text.includes('cleanup_deadline_at AS "cleanupDeadlineAt"')
3734
+ ) {
3735
+ const now = Date.parse(String(values[0]));
3736
+ const limit = Number(values[1]);
3737
+ const cursorExpiresAt = values[2]
3738
+ ? Date.parse(String(values[2]))
3739
+ : null;
3740
+ const cursorApprovalRequestId = values[3]
3741
+ ? String(values[3])
3742
+ : null;
3743
+ const rows = this.candidates
3744
+ .filter(({ approvalRequestId, expiresAt }) => {
3745
+ if (expiresAt.getTime() > now) return false;
3746
+ if (cursorExpiresAt === null) return true;
3747
+ return (
3748
+ expiresAt.getTime() > cursorExpiresAt ||
3749
+ (expiresAt.getTime() === cursorExpiresAt &&
3750
+ approvalRequestId > (cursorApprovalRequestId ?? ''))
3751
+ );
3752
+ })
3753
+ .sort(
3754
+ (left, right) =>
3755
+ left.expiresAt.getTime() - right.expiresAt.getTime() ||
3756
+ (left.approvalRequestId < right.approvalRequestId
3757
+ ? -1
3758
+ : left.approvalRequestId > right.approvalRequestId
3759
+ ? 1
3760
+ : 0),
3761
+ )
3762
+ .slice(0, limit);
3763
+ return { rowCount: rows.length, rows: rows as Row[] };
3764
+ }
3765
+ if (
3766
+ text.includes('count(*) FILTER') &&
3767
+ text.includes('AS "remainingDeadlineBreaches"')
3768
+ ) {
3769
+ const now = Date.parse(String(values[0]));
3770
+ const remainingDue = this.candidates.filter(
3771
+ ({ expiresAt }) => expiresAt.getTime() <= now,
3772
+ ).length;
3773
+ const remainingDeadlineBreaches = this.candidates.filter(
3774
+ ({ cleanupDeadlineAt }) =>
3775
+ cleanupDeadlineAt === null ||
3776
+ cleanupDeadlineAt.getTime() <= now,
3777
+ ).length;
3778
+ return {
3779
+ rowCount: 1,
3780
+ rows: [
3781
+ {
3782
+ remainingDeadlineBreaches:
3783
+ String(remainingDeadlineBreaches),
3784
+ remainingDue: String(remainingDue),
3785
+ } as Row,
3786
+ ],
3787
+ };
3788
+ }
3789
+ throw new Error('Unexpected fake cleanup database query.');
3790
+ }
3791
+ }
3792
+
3793
+ function cleanupDependencies(
3794
+ database: FakeCleanupDatabase,
3795
+ cleanup: (candidate: CleanupCandidate) => Promise<'completed' | 'failed'>,
3796
+ ) {
3797
+ return {
3798
+ cleanupExpiredRecordExport: (async (input: {
3799
+ approvalRequestId: string;
3800
+ }) => {
3801
+ const candidate = database.candidates.find(
3802
+ ({ approvalRequestId }) =>
3803
+ approvalRequestId === input.approvalRequestId,
3804
+ );
3805
+ if (!candidate) throw new Error('Cleanup candidate is absent.');
3806
+ const status = await cleanup(candidate);
3807
+ if (status === 'completed') {
3808
+ database.candidates.splice(
3809
+ database.candidates.indexOf(candidate),
3810
+ 1,
3811
+ );
3812
+ }
3813
+ return { deletedVersions: 1, status };
3814
+ }) as never,
3815
+ withDatabase: (async (
3816
+ _context: unknown,
3817
+ _workload: unknown,
3818
+ operation: (client: unknown) => Promise<unknown>,
3819
+ ) => operation(database)) as never,
3820
+ };
3821
+ }
3822
+
3823
+ class FakeExportObjectStore implements RecordExportObjectStore {
3824
+ ambiguousDeleteError = false;
3825
+ private nextVersion = 0;
3826
+ readonly versions = new Map<
3827
+ string,
3828
+ { isDeleteMarker: boolean; key: string }
3829
+ >();
3830
+
3831
+ async send(command: unknown): Promise<unknown> {
3832
+ if (command instanceof PutObjectCommand) {
3833
+ const versionId = 'version-' + String(++this.nextVersion);
3834
+ this.versions.set(versionId, {
3835
+ isDeleteMarker: false,
3836
+ key: command.input.Key ?? '',
3837
+ });
3838
+ return { VersionId: versionId };
3839
+ }
3840
+ if (command instanceof ListObjectVersionsCommand) {
3841
+ const prefix = command.input.Prefix ?? '';
3842
+ return {
3843
+ IsTruncated: false,
3844
+ Versions: [...this.versions.entries()]
3845
+ .filter(([, value]) => value.key === prefix)
3846
+ .map(([VersionId, value]) => ({
3847
+ Key: value.key,
3848
+ VersionId,
3849
+ })),
3850
+ };
3851
+ }
3852
+ if (command instanceof DeleteObjectsCommand) {
3853
+ const objects = command.input.Delete?.Objects ?? [];
3854
+ for (const object of objects) {
3855
+ if (object.VersionId) this.versions.delete(object.VersionId);
3856
+ }
3857
+ return this.ambiguousDeleteError && objects.length > 0
3858
+ ? {
3859
+ Errors: [
3860
+ {
3861
+ Code: 'InternalError',
3862
+ Key: objects[0]?.Key,
3863
+ },
3864
+ ],
3865
+ }
3866
+ : { Errors: [] };
3867
+ }
3868
+ throw new Error('Unexpected fake export object-store command.');
3869
+ }
3870
+ }
3871
+
3872
+ type DurableVersion = ExportObjectVersion & {
3873
+ status: 'deleted' | 'deletion-in-progress' | 'failed';
3874
+ };
3875
+
3876
+ function cleanupAttempt<Result>(
3877
+ objectKey: string,
3878
+ objectStore: FakeExportObjectStore,
3879
+ durable: Map<string, DurableVersion>,
3880
+ finalize: (result: ExportObjectCleanupResult) => Promise<Result>,
3881
+ ): Promise<Result> {
3882
+ return runExportObjectCleanupAttempt({
3883
+ finalize,
3884
+ objectKey,
3885
+ objectStore,
3886
+ recordDiscovered: async (versions) => {
3887
+ for (const version of versions) {
3888
+ durable.set(version.versionId, {
3889
+ ...version,
3890
+ status: 'deletion-in-progress',
3891
+ });
3892
+ }
3893
+ },
3894
+ trackedVersions: [...durable.values()].filter(
3895
+ ({ status }) => status !== 'deleted',
3896
+ ),
3897
+ });
3898
+ }
3899
+
3900
+ describe('${pascalName} generated data-rights workflows', () => {
3901
+ it('requires direct admin authority for hold and approval administration', () => {
3902
+ expect(requireDirectAdmin(directAdmin)).toBe(directAdmin);
3903
+ expect(() =>
3904
+ requireDirectAdmin({
3905
+ ...directAdmin,
3906
+ audience: 'admin-1',
3907
+ authority: {
3908
+ delegatedBy: { id: 'admin-1', type: 'user' },
3909
+ delegationId: 'delegation-1',
3910
+ mode: 'delegated',
3911
+ },
3912
+ }),
3913
+ ).toThrow(/direct human principal/i);
3914
+ expect(() => requireDirectAdmin(directOwner)).toThrow(/direct admin/i);
3915
+ });
3916
+
3917
+ it('builds an exact pseudonymous record hold with bounded effects', () => {
3918
+ const hold = createRecordHold(
3919
+ {
3920
+ effects: ['block-deletion', 'block-export'],
3921
+ expiresAt: '2026-08-20T12:00:00.000Z',
3922
+ principal: directAdmin,
3923
+ purpose: 'legal',
3924
+ recordId: '00000000-0000-4000-8000-000000000001',
3925
+ reviewAt: '2026-08-10T12:00:00.000Z',
3926
+ },
3927
+ '2026-08-01T12:00:00.000Z',
3928
+ );
3929
+ expect(hold).toMatchObject({
3930
+ categoryId: 'product-record',
3931
+ effects: ['block-deletion', 'block-export'],
3932
+ purpose: 'legal',
3933
+ status: 'active',
3934
+ });
3935
+ expect(hold.scopeRef).toMatch(/^opaque:/);
3936
+ expect(JSON.stringify(hold)).not.toContain(
3937
+ '00000000-0000-4000-8000-000000000001',
3938
+ );
3939
+ });
3940
+
3941
+ it('leaves scan and retry intervals inside the one-day maximum', () => {
3942
+ expect(recordExportExpiresAt('2026-08-01T12:00:00.000Z')).toBe(
3943
+ '2026-08-02T09:00:00.000Z',
3944
+ );
3945
+ expect(
3946
+ recordExportCleanupDeadlineAt('2026-08-01T12:00:00.000Z'),
3947
+ ).toBe('2026-08-02T12:00:00.000Z');
3948
+ expect(
3949
+ recordExportSchedule('2026-08-01T12:00:00.000Z'),
3950
+ ).toEqual({
3951
+ cleanupDeadlineAt: '2026-08-02T12:00:00.000Z',
3952
+ expiresAt: '2026-08-02T09:00:00.000Z',
3953
+ uploadStartedAt: '2026-08-01T12:00:00.000Z',
3954
+ });
3955
+ expect(() =>
3956
+ recordExportSchedule('2026-08-01T12:00:00.000Z', {
3957
+ cleanupDeadlineAt: '2026-08-02T12:00:00.000Z',
3958
+ expiresAt: '2026-08-02T08:59:59.999Z',
3959
+ uploadStartedAt: '2026-08-01T12:00:00.000Z',
3960
+ }),
3961
+ ).toThrow(/schedule is inconsistent/i);
3962
+ expect(() =>
3963
+ recordExportSchedule('2026-08-01T12:00:00.000Z', {
3964
+ cleanupDeadlineAt: '2026-08-02T12:00:00.000Z',
3965
+ expiresAt: '2026-08-02T09:00:00.000Z',
3966
+ }),
3967
+ ).toThrow(/schedule is incomplete/i);
3968
+ expect(
3969
+ recordExportObjectKey('00000000-0000-4000-8000-000000000001'),
3970
+ ).toBe(
3971
+ 'confidential-exports/00000000-0000-4000-8000-000000000001/record.json',
3972
+ );
3973
+ });
3974
+
3975
+ it('denies an export approval issued under a stale policy version', () => {
3976
+ const recordId = '00000000-0000-4000-8000-000000000001';
3977
+ const command = { recordId };
3978
+ const pending = createApprovalRequest({
3979
+ action: '${name}:record-export',
3980
+ author: directOwner,
3981
+ command,
3982
+ expiresAt: '2026-08-01T13:00:00.000Z',
3983
+ now: '2026-08-01T12:00:00.000Z',
3984
+ policyVersion: '1.0.0',
3985
+ resource: { id: recordId, type: 'record' },
3986
+ slice: '${name}',
3987
+ });
3988
+ expect(
3989
+ decideApprovalRequest(pending, {
3990
+ approver: directAdmin,
3991
+ expectedRequestDigest: pending.requestDigest,
3992
+ now: '2026-08-01T12:05:00.000Z',
3993
+ outcome: 'approved',
3994
+ reasonReference: 'privacy-review-stale',
3995
+ requiredPolicyVersion: '2.0.0',
3996
+ }),
3997
+ ).toMatchObject({
3998
+ decision: {
3999
+ category: 'policy-stale',
4000
+ decision: 'deny',
4001
+ policyVersion: '2.0.0',
4002
+ },
4003
+ request: pending,
4004
+ });
4005
+ const approved = decideApprovalRequest(pending, {
4006
+ approver: directAdmin,
4007
+ expectedRequestDigest: pending.requestDigest,
4008
+ now: '2026-08-01T12:05:00.000Z',
4009
+ outcome: 'approved',
4010
+ reasonReference: 'privacy-review-1',
4011
+ requiredPolicyVersion: '1.0.0',
4012
+ }).request;
4013
+
4014
+ expect(
4015
+ beginApprovalExecution(approved, {
4016
+ action: '${name}:record-export',
4017
+ command,
4018
+ executionId: 'export:' + pending.id,
4019
+ now: '2026-08-01T12:06:00.000Z',
4020
+ requiredPolicyVersion: '2.0.0',
4021
+ resource: { id: recordId, type: 'record' },
4022
+ }),
4023
+ ).toMatchObject({
4024
+ decision: {
4025
+ category: 'policy-stale',
4026
+ decision: 'deny',
4027
+ policyVersion: '2.0.0',
4028
+ },
4029
+ request: { status: 'approved' },
4030
+ });
4031
+ });
4032
+
4033
+ it('builds a correction ledger and minimized cross-Slice event together', () => {
4034
+ process.env.EVENT_BUS_NAME = 'fixture-events';
4035
+ process.env.EBK_STAGE = 'development';
4036
+ const artifacts = createRecordCorrectionArtifacts({
4037
+ correlationId: 'correction-1',
4038
+ name: 'Synthetic correction',
4039
+ now: '2026-08-01T12:00:00.000Z',
4040
+ principal: directAdmin,
4041
+ recordId: '00000000-0000-4000-8000-000000000001',
4042
+ runScope: 'unit3-fixture',
4043
+ });
4044
+ expect(artifacts.ledger.action).toBe('correction');
4045
+ expect(artifacts.ledger.pseudonymousKey).toMatch(/^opaque:/);
4046
+ const event = JSON.parse(artifacts.event.Detail ?? '{}');
4047
+ const lifecycle = parseDataLifecycleChangeEvent(event.data);
4048
+ expect(lifecycle).toMatchObject({
4049
+ action: 'correction',
4050
+ policyVersion: DATA_RIGHTS_POLICY_VERSION,
4051
+ sourceSlice: '${name}',
4052
+ });
4053
+ expect(JSON.stringify(lifecycle)).not.toContain(
4054
+ '00000000-0000-4000-8000-000000000001',
4055
+ );
4056
+ });
4057
+
4058
+ it('uses the runtime pseudonymous-reference implementation for subjects', () => {
4059
+ expect(dataRightsSubjectReference('subject-1')).toMatch(/^opaque:/);
4060
+ expect(dataRightsSubjectReference('subject-1')).toBe(
4061
+ dataRightsSubjectReference('subject-1'),
4062
+ );
4063
+ });
4064
+
4065
+ it('records cleanup deadline reports at exact production decision points', async () => {
4066
+ const schedule = {
4067
+ cleanupDeadlineAt: new Date('2026-08-02T12:00:00.000Z'),
4068
+ expiresAt: new Date('2026-08-02T09:00:00.000Z'),
4069
+ uploadStartedAt: new Date('2026-08-01T12:00:00.000Z'),
4070
+ };
4071
+ const completed = new FakeCleanupDatabase([]);
4072
+ await recordPreparedExportCleanupDeadlineReport(completed as never, {
4073
+ approvalRequestId: 'completed-export',
4074
+ ...schedule,
4075
+ lifecycleStatus: 'completed',
4076
+ now: '2026-08-03T12:00:00.000Z',
4077
+ });
4078
+ expect(completed.pendingReports.size).toBe(0);
4079
+
4080
+ const beforeDeadline = new FakeCleanupDatabase([]);
4081
+ await recordPreparedExportCleanupDeadlineReport(
4082
+ beforeDeadline as never,
4083
+ {
4084
+ approvalRequestId: 'before-deadline-export',
4085
+ ...schedule,
4086
+ lifecycleStatus: 'artifact-deletion-in-progress',
4087
+ now: '2026-08-02T11:59:59.999Z',
4088
+ },
4089
+ );
4090
+ expect(beforeDeadline.pendingReports.size).toBe(0);
4091
+
4092
+ const atDeadline = new FakeCleanupDatabase([]);
4093
+ await recordPreparedExportCleanupDeadlineReport(atDeadline as never, {
4094
+ approvalRequestId: 'at-deadline-export',
4095
+ ...schedule,
4096
+ lifecycleStatus: 'artifact-deletion-in-progress',
4097
+ now: '2026-08-02T12:00:00.000Z',
4098
+ });
4099
+ expect(atDeadline.pendingReports).toEqual(
4100
+ new Set(['at-deadline-export']),
4101
+ );
4102
+
4103
+ const crossingDeadline = new FakeCleanupDatabase([]);
4104
+ crossingDeadline.clockTimestamp =
4105
+ new Date('2026-08-02T12:00:00.000Z');
4106
+ await recordFinalizedExportCleanupDeadlineReport(
4107
+ crossingDeadline as never,
4108
+ {
4109
+ approvalRequestId: 'crossing-deadline-export',
4110
+ ...schedule,
4111
+ now: '2026-08-02T11:59:59.999Z',
4112
+ },
4113
+ );
4114
+ expect(crossingDeadline.pendingReports).toEqual(
4115
+ new Set(['crossing-deadline-export']),
4116
+ );
4117
+
4118
+ const conflict = new FakeCleanupDatabase([]);
4119
+ conflict.pendingReports.add('conflicting-export');
4120
+ conflict.reportDeadlines.set(
4121
+ 'conflicting-export',
4122
+ new Date('2026-08-03T12:00:00.000Z'),
4123
+ );
4124
+ await expect(
4125
+ recordPreparedExportCleanupDeadlineReport(conflict as never, {
4126
+ approvalRequestId: 'conflicting-export',
4127
+ ...schedule,
4128
+ lifecycleStatus: 'artifact-deletion-in-progress',
4129
+ now: '2026-08-02T12:00:00.000Z',
4130
+ }),
4131
+ ).rejects.toThrow(/deadline report is inconsistent/i);
4132
+ });
4133
+
4134
+ it('keeps retained deletion authority independent of report inventory saturation', async () => {
4135
+ const schedule = {
4136
+ cleanupDeadlineAt: new Date('2026-08-02T12:00:00.000Z'),
4137
+ expiresAt: new Date('2026-08-02T09:00:00.000Z'),
4138
+ uploadStartedAt: new Date('2026-08-01T12:00:00.000Z'),
4139
+ };
4140
+ const saturated = new FakeCleanupDatabase([]);
4141
+ for (let index = 0; index < ${PENDING_EXPORT_CLEANUP_REPORT_BOUND}; index += 1) {
4142
+ const resourceRef = 'saturating-export-' + String(index);
4143
+ saturated.pendingReports.add(resourceRef);
4144
+ saturated.reportDeadlines.set(resourceRef, schedule.cleanupDeadlineAt);
4145
+ }
4146
+
4147
+ await expect(
4148
+ recordPreparedExportCleanupDeadlineReport(saturated as never, {
4149
+ approvalRequestId: 'overflow-export',
4150
+ ...schedule,
4151
+ lifecycleStatus: 'artifact-deletion-in-progress',
4152
+ now: '2026-08-02T12:00:00.000Z',
4153
+ }),
4154
+ ).resolves.toBeUndefined();
4155
+ expect(saturated.pendingReports.size).toBe(
4156
+ ${PENDING_EXPORT_CLEANUP_REPORT_BOUND},
4157
+ );
4158
+ expect(saturated.pendingReports.has('overflow-export')).toBe(false);
4159
+
4160
+ const invalidCapacity = new FakeCleanupDatabase([]);
4161
+ invalidCapacity.maximumPendingReports =
4162
+ ${PENDING_EXPORT_CLEANUP_REPORT_BOUND} - 1;
4163
+ await expect(
4164
+ recordPreparedExportCleanupDeadlineReport(invalidCapacity as never, {
4165
+ approvalRequestId: 'invalid-capacity-export',
4166
+ ...schedule,
4167
+ lifecycleStatus: 'artifact-deletion-in-progress',
4168
+ now: '2026-08-02T12:00:00.000Z',
4169
+ }),
4170
+ ).rejects.toThrow(/report capacity is invalid/i);
4171
+ });
4172
+
4173
+ it('treats a concurrently acknowledged report as observed, not corrupt', async () => {
4174
+ const schedule = {
4175
+ cleanupDeadlineAt: new Date('2026-08-02T12:00:00.000Z'),
4176
+ expiresAt: new Date('2026-08-02T09:00:00.000Z'),
4177
+ uploadStartedAt: new Date('2026-08-01T12:00:00.000Z'),
4178
+ };
4179
+ const acknowledged = new FakeCleanupDatabase([]);
4180
+ acknowledged.pendingReports.add('acknowledged-export');
4181
+ acknowledged.reportDeadlines.set(
4182
+ 'acknowledged-export',
4183
+ schedule.cleanupDeadlineAt,
4184
+ );
4185
+ const query = acknowledged.query.bind(acknowledged);
4186
+ acknowledged.query = (async (text: string, values?: unknown[]) => {
4187
+ if (text.includes('INSERT INTO cleanup_deadline_reports')) {
4188
+ const result = await query(text, values);
4189
+ acknowledged.pendingReports.delete('acknowledged-export');
4190
+ acknowledged.reportDeadlines.delete('acknowledged-export');
4191
+ return result;
4192
+ }
4193
+ return query(text, values);
4194
+ }) as typeof acknowledged.query;
4195
+
4196
+ await expect(
4197
+ recordPreparedExportCleanupDeadlineReport(acknowledged as never, {
4198
+ approvalRequestId: 'acknowledged-export',
4199
+ ...schedule,
4200
+ lifecycleStatus: 'artifact-deletion-in-progress',
4201
+ now: '2026-08-02T12:00:00.000Z',
4202
+ }),
4203
+ ).resolves.toBeUndefined();
4204
+ });
4205
+
4206
+ it('reports cleanup deadline state before and after exact expiry', async () => {
4207
+ const approvalRequestId =
4208
+ '00000000-0000-4000-8000-000000000021';
4209
+ const candidate = (): CleanupCandidate => ({
4210
+ approvalRequestId,
4211
+ cleanupDeadlineAt: new Date('2026-08-02T12:00:00.000Z'),
4212
+ expiresAt: new Date('2026-08-02T09:00:00.000Z'),
4213
+ recordReference: 'opaque:record-21',
4214
+ uploadStartedAt: new Date('2026-08-01T12:00:00.000Z'),
4215
+ });
4216
+
4217
+ const beforeDeadline = new FakeCleanupDatabase([candidate()]);
4218
+ await expect(
4219
+ cleanupDueRecordExports(
4220
+ {
4221
+ correlationId: 'cleanup-before-deadline',
4222
+ maximumBatches: 1,
4223
+ now: '2026-08-02T11:59:59.999Z',
4224
+ principal: cleanupAgent,
4225
+ },
4226
+ cleanupDependencies(beforeDeadline, async () => 'completed'),
4227
+ ),
4228
+ ).resolves.toMatchObject({
4229
+ completed: 1,
4230
+ continuationRequired: false,
4231
+ deadlineBreached: false,
4232
+ remainingDeadlineBreaches: 0,
4233
+ remainingDue: 0,
4234
+ });
4235
+
4236
+ const atDeadline = new FakeCleanupDatabase([candidate()]);
4237
+ await expect(
4238
+ cleanupDueRecordExports(
4239
+ {
4240
+ correlationId: 'cleanup-at-deadline',
4241
+ maximumBatches: 1,
4242
+ now: '2026-08-02T12:00:00.000Z',
4243
+ principal: cleanupAgent,
4244
+ },
4245
+ cleanupDependencies(atDeadline, async () => 'completed'),
4246
+ ),
4247
+ ).resolves.toMatchObject({
4248
+ completed: 1,
4249
+ continuationRequired: false,
4250
+ deadlineBreached: true,
4251
+ remainingDeadlineBreaches: 0,
4252
+ remainingDue: 0,
4253
+ });
4254
+
4255
+ const failedCleanup = new FakeCleanupDatabase([candidate()]);
4256
+ await expect(
4257
+ cleanupDueRecordExports(
4258
+ {
4259
+ correlationId: 'cleanup-failed-at-deadline',
4260
+ maximumBatches: 1,
4261
+ now: '2026-08-02T12:00:00.000Z',
4262
+ principal: cleanupAgent,
4263
+ },
4264
+ cleanupDependencies(failedCleanup, async () => {
4265
+ throw new Error('simulated version cleanup failure');
4266
+ }),
4267
+ ),
4268
+ ).resolves.toMatchObject({
4269
+ completed: 0,
4270
+ continuationRequired: true,
4271
+ deadlineBreached: true,
4272
+ remainingDeadlineBreaches: 1,
4273
+ remainingDue: 1,
4274
+ retryRequired: 1,
4275
+ });
4276
+ expect(failedCleanup.queries).toEqual(
4277
+ expect.arrayContaining([
4278
+ expect.stringContaining(
4279
+ 'cleanup_deadline_at AS "cleanupDeadlineAt"',
4280
+ ),
4281
+ expect.stringContaining('AS "remainingDeadlineBreaches"'),
4282
+ ]),
4283
+ );
4284
+
4285
+ const driftedSchedule = new FakeCleanupDatabase([
4286
+ {
4287
+ ...candidate(),
4288
+ cleanupDeadlineAt: new Date('2026-08-03T12:00:00.000Z'),
4289
+ expiresAt: new Date('2026-08-03T09:00:00.000Z'),
4290
+ },
4291
+ ]);
4292
+ await expect(
4293
+ cleanupDueRecordExports(
4294
+ {
4295
+ correlationId: 'cleanup-drifted-schedule',
4296
+ maximumBatches: 1,
4297
+ now: '2026-08-02T11:59:59.999Z',
4298
+ principal: cleanupAgent,
4299
+ },
4300
+ cleanupDependencies(driftedSchedule, async () => 'completed'),
4301
+ ),
4302
+ ).rejects.toThrow(/schedule is inconsistent/i);
4303
+
4304
+ const incompleteSchedule = new FakeCleanupDatabase([
4305
+ { ...candidate(), cleanupDeadlineAt: null },
4306
+ ]);
4307
+ await expect(
4308
+ cleanupDueRecordExports(
4309
+ {
4310
+ correlationId: 'cleanup-incomplete-schedule',
4311
+ maximumBatches: 1,
4312
+ now: '2026-08-02T11:59:59.999Z',
4313
+ principal: cleanupAgent,
4314
+ },
4315
+ cleanupDependencies(incompleteSchedule, async () => 'completed'),
4316
+ ),
4317
+ ).rejects.toThrow(/schedule is incomplete/i);
4318
+ });
4319
+
4320
+ it('durably records an overdue bounded inventory when no cleanup loop can run', async () => {
4321
+ const approvalRequestId =
4322
+ '00000000-0000-4000-8000-000000000022';
4323
+ const database = new FakeCleanupDatabase([
4324
+ {
4325
+ approvalRequestId,
4326
+ cleanupDeadlineAt: new Date('2026-08-02T12:00:00.000Z'),
4327
+ expiresAt: new Date('2026-08-02T09:00:00.000Z'),
4328
+ recordReference: 'opaque:record-22',
4329
+ uploadStartedAt: new Date('2026-08-01T12:00:00.000Z'),
4330
+ },
4331
+ ]);
4332
+ const cleanup = vi.fn().mockResolvedValue('completed');
4333
+ const invocation = {
4334
+ correlationId: 'cleanup-no-loop-deadline',
4335
+ maximumBatches: 1,
4336
+ now: '2026-08-02T12:00:00.000Z',
4337
+ principal: cleanupAgent,
4338
+ shouldContinue: () => false,
4339
+ };
4340
+
4341
+ await expect(
4342
+ cleanupDueRecordExports(
4343
+ invocation,
4344
+ cleanupDependencies(database, cleanup),
4345
+ ),
4346
+ ).resolves.toMatchObject({
4347
+ batches: 0,
4348
+ continuationRequired: true,
4349
+ deadlineBreached: true,
4350
+ processed: 0,
4351
+ remainingDeadlineBreaches: 1,
4352
+ remainingDue: 1,
4353
+ });
4354
+ await expect(
4355
+ cleanupDueRecordExports(
4356
+ invocation,
4357
+ cleanupDependencies(database, cleanup),
4358
+ ),
4359
+ ).resolves.toMatchObject({
4360
+ batches: 0,
4361
+ deadlineBreached: true,
4362
+ processed: 0,
4363
+ });
4364
+ expect(cleanup).not.toHaveBeenCalled();
4365
+ expect(database.pendingReports).toEqual(
4366
+ new Set([approvalRequestId]),
4367
+ );
4368
+ expect(database.queries.filter((query) => query === 'BEGIN')).toHaveLength(2);
4369
+ expect(database.queries.filter((query) => query === 'COMMIT')).toHaveLength(2);
4370
+ });
4371
+
4372
+ it('commits an overdue deadline report before cleanup can fail', async () => {
4373
+ const approvalRequestId =
4374
+ '00000000-0000-4000-8000-000000000023';
4375
+ const database = new FakeCleanupDatabase([
4376
+ {
4377
+ approvalRequestId,
4378
+ cleanupDeadlineAt: new Date('2026-08-02T12:00:00.000Z'),
4379
+ expiresAt: new Date('2026-08-02T09:00:00.000Z'),
4380
+ recordReference: 'opaque:record-23',
4381
+ uploadStartedAt: new Date('2026-08-01T12:00:00.000Z'),
4382
+ },
4383
+ ]);
4384
+ const cleanup = vi.fn().mockImplementation(async () => {
4385
+ expect(database.pendingReports).toEqual(
4386
+ new Set([approvalRequestId]),
4387
+ );
4388
+ expect(database.queries).toContain('COMMIT');
4389
+ throw new Error('simulated failure before cleanup reporting');
4390
+ });
4391
+
4392
+ await expect(
4393
+ cleanupDueRecordExports(
4394
+ {
4395
+ correlationId: 'cleanup-pre-report-failure',
4396
+ maximumBatches: 1,
4397
+ now: '2026-08-02T12:00:00.000Z',
4398
+ principal: cleanupAgent,
4399
+ },
4400
+ cleanupDependencies(database, cleanup),
4401
+ ),
4402
+ ).resolves.toMatchObject({
4403
+ completed: 0,
4404
+ continuationRequired: true,
4405
+ deadlineBreached: true,
4406
+ retryRequired: 1,
4407
+ });
4408
+ expect(cleanup).toHaveBeenCalledOnce();
4409
+ expect(database.pendingReports).toEqual(
4410
+ new Set([approvalRequestId]),
4411
+ );
4412
+ });
4413
+
4414
+ it('emits one deadline signal and fails after best-effort cleanup', async () => {
4415
+ process.env.EBK_STAGE = 'test';
4416
+ process.env.EBK_WORKLOAD_IDENTITY =
4417
+ '${name}:data-export-cleanup';
4418
+ const emitted: Array<{ metricName?: string; value?: number }> = [];
4419
+ const authorize = vi.fn().mockResolvedValue(undefined);
4420
+ const cleanup = vi.fn().mockResolvedValue({
4421
+ batches: 1,
4422
+ completed: 1,
4423
+ continuationRequired: false,
4424
+ deadlineBreached: true,
4425
+ processed: 1,
4426
+ remainingDeadlineBreaches: 0,
4427
+ remainingDue: 0,
4428
+ retryRequired: 0,
4429
+ });
4430
+ const report = vi
4431
+ .fn()
4432
+ .mockImplementationOnce(async (input: { report(value: number): boolean }) => {
4433
+ expect(input.report(0)).toBe(true);
4434
+ return { continuationRequired: false, reported: 0 };
4435
+ })
4436
+ .mockImplementationOnce(async (input: { report(value: number): boolean }) => {
4437
+ expect(input.report(1)).toBe(true);
4438
+ return { continuationRequired: false, reported: 1 };
4439
+ });
4440
+ const handler = createDataExportCleanupHandler({
4441
+ authorize: authorize as never,
4442
+ cleanup: cleanup as never,
4443
+ metrics: {
4444
+ emit: (metric: { metricName?: string; value?: number }) => {
4445
+ emitted.push(metric);
4446
+ return {} as never;
4447
+ },
4448
+ } as never,
4449
+ report: report as never,
4450
+ });
4451
+ await expect(
4452
+ handler(
4453
+ { id: 'cleanup-deadline-proof' },
4454
+ { getRemainingTimeInMillis: () => 60_000 },
4455
+ ),
4456
+ ).rejects.toThrow('confidential-export-cleanup-deadline-breached');
4457
+ expect(authorize).toHaveBeenCalledOnce();
4458
+ expect(cleanup).toHaveBeenCalledOnce();
4459
+ expect(report).toHaveBeenCalledTimes(2);
4460
+ expect(emitted).toContainEqual(
4461
+ expect.objectContaining({
4462
+ metricName: 'DataExportCleanupDeadlineBreaches',
4463
+ value: 1,
4464
+ }),
4465
+ );
4466
+ });
4467
+
4468
+ it('retries a durable deadline report after post-emission commit failure', async () => {
4469
+ const database = new FakeCleanupDatabase([]);
4470
+ database.pendingReports.add(
4471
+ '00000000-0000-4000-8000-000000000021',
4472
+ );
4473
+ database.failCommitOnce = true;
4474
+ const firstEmission: number[] = [];
4475
+ await expect(
4476
+ reportExportCleanupDeadlineBreaches(
4477
+ {
4478
+ correlationId: 'cleanup-report-crash',
4479
+ report: (value) => {
4480
+ firstEmission.push(value);
4481
+ return true;
4482
+ },
4483
+ },
4484
+ { withDatabase: cleanupDependencies(database, async () => 'completed').withDatabase },
4485
+ ),
4486
+ ).rejects.toThrow('simulated post-emission commit failure');
4487
+ expect(firstEmission).toEqual([1]);
4488
+ expect(database.pendingReports.size).toBe(1);
4489
+
4490
+ const retryEmission: number[] = [];
4491
+ await expect(
4492
+ reportExportCleanupDeadlineBreaches(
4493
+ {
4494
+ correlationId: 'cleanup-report-retry',
4495
+ report: (value) => {
4496
+ retryEmission.push(value);
4497
+ return true;
4498
+ },
4499
+ },
4500
+ { withDatabase: cleanupDependencies(database, async () => 'completed').withDatabase },
4501
+ ),
4502
+ ).resolves.toEqual({ continuationRequired: false, reported: 1 });
4503
+ expect(retryEmission).toEqual([1]);
4504
+ expect(database.pendingReports.size).toBe(0);
4505
+
4506
+ const afterAcknowledgement: number[] = [];
4507
+ await reportExportCleanupDeadlineBreaches(
4508
+ {
4509
+ correlationId: 'cleanup-report-after-ack',
4510
+ report: (value) => {
4511
+ afterAcknowledgement.push(value);
4512
+ return true;
4513
+ },
4514
+ },
4515
+ { withDatabase: cleanupDependencies(database, async () => 'completed').withDatabase },
4516
+ );
4517
+ expect(afterAcknowledgement).toEqual([0]);
4518
+ });
4519
+
4520
+ it('recovers a post-put database rollback through durable orphan cleanup', async () => {
4521
+ process.env.DATA_EXPORT_BUCKET_NAME = 'fixture-export-bucket';
4522
+ process.env.DATA_EXPORT_KMS_KEY_ARN =
4523
+ 'arn:aws:kms:us-east-1:111122223333:key/fixture';
4524
+ const objectKey = recordExportObjectKey(
4525
+ '00000000-0000-4000-8000-000000000011',
4526
+ );
4527
+ const objectStore = new FakeExportObjectStore();
4528
+ await expect(
4529
+ (async () => {
4530
+ await putEncryptedExport(objectKey, '{"id":"fixture"}', objectStore);
4531
+ throw new Error('simulated database rollback after PutObject');
4532
+ })(),
4533
+ ).rejects.toThrow(/database rollback/);
4534
+ expect(objectStore.versions.size).toBe(1);
4535
+ const durable = new Map<string, DurableVersion>();
4536
+ const result = await cleanupAttempt(
4537
+ objectKey,
4538
+ objectStore,
4539
+ durable,
4540
+ async (attempt) => {
4541
+ for (const version of attempt.attemptedVersions) {
4542
+ durable.set(version.versionId, {
4543
+ ...version,
4544
+ status: attempt.failedVersionIds.includes(version.versionId)
4545
+ ? 'failed'
4546
+ : 'deleted',
4547
+ });
4548
+ }
4549
+ return attempt;
4550
+ },
4551
+ );
4552
+ expect(result.failedVersionIds).toEqual([]);
4553
+ expect(objectStore.versions.size).toBe(0);
4554
+ expect([...durable.values()].map(({ status }) => status)).toEqual([
4555
+ 'deleted',
4556
+ ]);
4557
+ });
4558
+
4559
+ it('retries durable inventory after delete succeeds and DB finalization rolls back', async () => {
4560
+ process.env.DATA_EXPORT_BUCKET_NAME = 'fixture-export-bucket';
4561
+ process.env.DATA_EXPORT_KMS_KEY_ARN =
4562
+ 'arn:aws:kms:us-east-1:111122223333:key/fixture';
4563
+ const objectKey = recordExportObjectKey(
4564
+ '00000000-0000-4000-8000-000000000012',
4565
+ );
4566
+ const objectStore = new FakeExportObjectStore();
4567
+ await putEncryptedExport(objectKey, '{"id":"fixture"}', objectStore);
4568
+ const durable = new Map<string, DurableVersion>();
4569
+ await expect(
4570
+ cleanupAttempt(objectKey, objectStore, durable, async () => {
4571
+ throw new Error('simulated cleanup finalization rollback');
4572
+ }),
4573
+ ).rejects.toThrow(/finalization rollback/);
4574
+ expect(objectStore.versions.size).toBe(0);
4575
+ expect([...durable.values()][0]?.status).toBe('deletion-in-progress');
4576
+ const retried = await cleanupAttempt(
4577
+ objectKey,
4578
+ objectStore,
4579
+ durable,
4580
+ async (attempt) => attempt,
4581
+ );
4582
+ expect(retried.attemptedVersions).toHaveLength(1);
4583
+ expect(retried.failedVersionIds).toEqual([]);
4584
+ expect(retried.remainingVersions).toEqual([]);
4585
+ });
4586
+
4587
+ it('fails a missing-VersionId delete error closed and tolerates overlapping finalization', async () => {
4588
+ process.env.DATA_EXPORT_BUCKET_NAME = 'fixture-export-bucket';
4589
+ process.env.DATA_EXPORT_KMS_KEY_ARN =
4590
+ 'arn:aws:kms:us-east-1:111122223333:key/fixture';
4591
+ const objectKey = recordExportObjectKey(
4592
+ '00000000-0000-4000-8000-000000000013',
4593
+ );
4594
+ const objectStore = new FakeExportObjectStore();
4595
+ const versionId = await putEncryptedExport(
4596
+ objectKey,
4597
+ '{"id":"fixture"}',
4598
+ objectStore,
4599
+ );
4600
+ objectStore.ambiguousDeleteError = true;
4601
+ const durable = new Map<string, DurableVersion>();
4602
+ const failed = await cleanupAttempt(
4603
+ objectKey,
4604
+ objectStore,
4605
+ durable,
4606
+ async (attempt) => attempt,
4607
+ );
4608
+ expect(failed.failedVersionIds).toEqual([versionId]);
4609
+
4610
+ objectStore.ambiguousDeleteError = false;
4611
+ let completed = false;
4612
+ const finalize = async (attempt: ExportObjectCleanupResult) => {
4613
+ await Promise.resolve();
4614
+ if (!completed && attempt.failedVersionIds.length === 0) {
4615
+ completed = true;
4616
+ }
4617
+ return completed ? 'completed' : 'retry-required';
4618
+ };
4619
+ const results = await Promise.all([
4620
+ cleanupAttempt(objectKey, objectStore, durable, finalize),
4621
+ cleanupAttempt(objectKey, objectStore, durable, finalize),
4622
+ ]);
4623
+ expect(results).toEqual(['completed', 'completed']);
4624
+ });
4625
+ });
4626
+ `;
4627
+ }
4628
+ //# sourceMappingURL=built-in-plan-data-rights-files.js.map