@flowdular/sdk 0.2.4 → 0.3.1

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 (930) hide show
  1. package/.ai/agents/sandbox/business-manager.md +5 -1
  2. package/.ai/blueprints/author-spec/README.md +1 -1
  3. package/.ai/blueprints/author-spec/spec-requirements.yaml +44 -0
  4. package/.ai/blueprints/author-spec/steps.yaml +5 -5
  5. package/.ai/blueprints/author-spec/templates/module.yaml +99 -3
  6. package/.ai/blueprints/edit-module/gates.yaml +4 -0
  7. package/.ai/blueprints/edit-module/required-files.yaml +9 -0
  8. package/.ai/blueprints/new-module/gates.yaml +4 -0
  9. package/.ai/blueprints/new-module/spec-requirements.yaml +2 -2
  10. package/.ai/blueprints/release/gates.yaml +4 -0
  11. package/.ai/platform-capabilities.md +128 -0
  12. package/.ai/policies/capabilities.yaml +130 -3
  13. package/.ai/policies/path-ownership.yaml +5 -2
  14. package/.ai/policies/task-budgets.yaml +5 -3
  15. package/.ai/references/catalog/module.json +4 -4
  16. package/.ai/references/catalog/package.json +2 -2
  17. package/.ai/references/catalog/spec/module.yaml +5 -3
  18. package/.ai/references/catalog/src/platform.ts +2 -0
  19. package/.ai/references/catalog/src/services/catalog-service.ts +89 -1
  20. package/.ai/references/catalog/src/services/data-classes.ts +47 -0
  21. package/.ai/references/catalog/src/services/database-repository.ts +98 -1
  22. package/.ai/references/catalog/src/services/repository.ts +22 -1
  23. package/.ai/references/catalog/tests/data-classes.test.ts +157 -0
  24. package/.ai/references/catalog.provenance.json +12 -10
  25. package/.ai/skills/README.md +10 -0
  26. package/.ai/skills/agent-tool-design/SKILL.md +1 -2
  27. package/.ai/skills/auth-security-review/SKILL.md +1 -1
  28. package/.ai/skills/business-agent-design/SKILL.md +0 -1
  29. package/.ai/skills/cli-extension/SKILL.md +1 -1
  30. package/.ai/skills/deploy-operate/SKILL.md +119 -0
  31. package/.ai/skills/module-new/SKILL.md +29 -3
  32. package/.ai/skills/module-update/SKILL.md +9 -3
  33. package/.ai/skills/perf-audit/SKILL.md +0 -1
  34. package/.ai/skills/release-eject-pr/SKILL.md +0 -1
  35. package/.ai/skills/spec-interview/SKILL.md +120 -0
  36. package/.ai/skills/test-hardening/SKILL.md +1 -0
  37. package/.ai/skills/ux-design/SKILL.md +34 -3
  38. package/.ai/skills/variables/SKILL.md +0 -2
  39. package/.ai/skills/workflow-development/SKILL.md +0 -1
  40. package/AGENTS.md +4 -0
  41. package/docs/agent-contract.md +2 -2
  42. package/docs/design-system.md +185 -31
  43. package/modules/access/migrations/0001_access_core.down.sql +1 -0
  44. package/modules/access/migrations/0001_access_core.up.sql +28 -0
  45. package/modules/access/migrations/README.md +3 -0
  46. package/modules/access/module.json +33 -0
  47. package/modules/access/package.json +43 -0
  48. package/modules/access/spec/module.yaml +225 -0
  49. package/modules/access/src/acl/permissions.ts +6 -0
  50. package/modules/access/src/api/endpoints.ts +244 -0
  51. package/modules/access/src/client/ActivityView.tsrx +245 -0
  52. package/modules/access/src/client/AttestationsView.tsrx +176 -0
  53. package/modules/access/src/client/ReviewView.tsrx +586 -0
  54. package/modules/access/src/client/api.ts +132 -0
  55. package/modules/access/src/client/contribution.tsrx +41 -0
  56. package/modules/access/src/client/index.ts +41 -0
  57. package/modules/access/src/client/navigation.ts +57 -0
  58. package/modules/access/src/client/presentation.ts +69 -0
  59. package/modules/access/src/client/state.ts +72 -0
  60. package/modules/access/src/domain/types.ts +189 -0
  61. package/modules/access/src/index.ts +38 -0
  62. package/modules/access/src/platform.ts +60 -0
  63. package/modules/access/src/server/index.ts +42 -0
  64. package/modules/access/src/server/runtime.ts +105 -0
  65. package/modules/access/src/services/access-service.ts +298 -0
  66. package/modules/access/src/services/audit-window.ts +78 -0
  67. package/modules/access/src/services/auth-directory.ts +72 -0
  68. package/modules/access/src/services/changes.ts +91 -0
  69. package/modules/access/src/services/data-classes.ts +28 -0
  70. package/modules/access/src/services/database-repository.ts +185 -0
  71. package/modules/access/src/services/directory.ts +102 -0
  72. package/modules/access/src/services/index.ts +33 -0
  73. package/modules/access/src/services/list-exports.ts +150 -0
  74. package/modules/access/src/services/migration.ts +54 -0
  75. package/modules/access/src/services/repository.ts +30 -0
  76. package/modules/access/src/services/review.ts +178 -0
  77. package/modules/access/translations/en.json +155 -0
  78. package/modules/access/translations/pl.json +155 -0
  79. package/modules/agents/migrations/0022_credential_rotation_inventory.down.sql +2 -0
  80. package/modules/agents/migrations/0022_credential_rotation_inventory.up.sql +7 -0
  81. package/modules/agents/migrations/0023_agents_retention_indexes.down.sql +2 -0
  82. package/modules/agents/migrations/0023_agents_retention_indexes.up.sql +9 -0
  83. package/modules/agents/migrations/0024_agent_meter_refusals.down.sql +3 -0
  84. package/modules/agents/migrations/0024_agent_meter_refusals.up.sql +22 -0
  85. package/modules/agents/migrations/0025_agent_list_indexes.down.sql +3 -0
  86. package/modules/agents/migrations/0025_agent_list_indexes.up.sql +11 -0
  87. package/modules/agents/module.json +30 -4
  88. package/modules/agents/package.json +2 -1
  89. package/modules/agents/spec/module.yaml +63 -5
  90. package/modules/agents/src/api/endpoints.ts +291 -33
  91. package/modules/agents/src/cli/commands.json +11 -0
  92. package/modules/agents/src/cli/index.ts +45 -0
  93. package/modules/agents/src/client/AgentDashboardWidget.tsrx +19 -11
  94. package/modules/agents/src/client/AgentDefinitionList.tsrx +53 -10
  95. package/modules/agents/src/client/AgentPlaygroundView.tsrx +4 -4
  96. package/modules/agents/src/client/AgentProceduresView.tsrx +2 -2
  97. package/modules/agents/src/client/AgentRunsView.tsrx +143 -40
  98. package/modules/agents/src/client/AgentsView.tsrx +109 -33
  99. package/modules/agents/src/client/api.ts +99 -21
  100. package/modules/agents/src/client/state.ts +97 -0
  101. package/modules/agents/src/domain/types.ts +44 -4
  102. package/modules/agents/src/platform.ts +42 -0
  103. package/modules/agents/src/server/action-execution.ts +201 -106
  104. package/modules/agents/src/server/runtime.ts +32 -0
  105. package/modules/agents/src/services/agent-service.ts +158 -30
  106. package/modules/agents/src/services/credential-rotation.ts +185 -0
  107. package/modules/agents/src/services/credential-vault.ts +71 -46
  108. package/modules/agents/src/services/data-classes.ts +176 -0
  109. package/modules/agents/src/services/database-repository.ts +393 -101
  110. package/modules/agents/src/services/metering.ts +111 -0
  111. package/modules/agents/src/services/migration.ts +109 -0
  112. package/modules/agents/src/services/module-agent-preflight.ts +17 -9
  113. package/modules/agents/src/services/notifications.ts +98 -0
  114. package/modules/agents/src/services/provider-repository.ts +9 -18
  115. package/modules/agents/src/services/provider-service.ts +1 -9
  116. package/modules/agents/src/services/reports.ts +107 -0
  117. package/modules/agents/src/services/repository.ts +86 -6
  118. package/modules/agents/src/services/usage-service.ts +2 -0
  119. package/modules/agents/src/services/worker.ts +114 -4
  120. package/modules/agents/src/settings.ts +14 -3
  121. package/modules/agents/translations/en.json +14 -4
  122. package/modules/agents/translations/pl.json +14 -4
  123. package/modules/approvals/migrations/0001_approvals_core.down.sql +3 -0
  124. package/modules/approvals/migrations/0001_approvals_core.up.sql +78 -0
  125. package/modules/approvals/migrations/0002_approvals_expiry_routing_role.down.sql +2 -0
  126. package/modules/approvals/migrations/0002_approvals_expiry_routing_role.up.sql +19 -0
  127. package/modules/approvals/migrations/0003_approvals_retention_indexes.down.sql +6 -0
  128. package/modules/approvals/migrations/0003_approvals_retention_indexes.up.sql +14 -0
  129. package/modules/approvals/migrations/0004_approvals_inbox_keyset_indexes.down.sql +5 -0
  130. package/modules/approvals/migrations/0004_approvals_inbox_keyset_indexes.up.sql +12 -0
  131. package/modules/approvals/migrations/0005_approvals_grant_audit.down.sql +4 -0
  132. package/modules/approvals/migrations/0005_approvals_grant_audit.up.sql +20 -0
  133. package/modules/approvals/migrations/README.md +3 -0
  134. package/modules/approvals/module.json +34 -0
  135. package/modules/approvals/package.json +43 -0
  136. package/modules/approvals/spec/module.yaml +327 -0
  137. package/modules/approvals/src/acl/permissions.ts +7 -0
  138. package/modules/approvals/src/api/endpoints.ts +457 -0
  139. package/modules/approvals/src/client/ApprovalRequestDrawer.tsrx +138 -0
  140. package/modules/approvals/src/client/ApprovalsView.tsrx +562 -0
  141. package/modules/approvals/src/client/PendingCountWidget.tsrx +52 -0
  142. package/modules/approvals/src/client/api.ts +203 -0
  143. package/modules/approvals/src/client/approvals.css +51 -0
  144. package/modules/approvals/src/client/contribution.tsrx +44 -0
  145. package/modules/approvals/src/client/index.ts +26 -0
  146. package/modules/approvals/src/client/navigation.ts +39 -0
  147. package/modules/approvals/src/client/pending-count.ts +26 -0
  148. package/modules/approvals/src/client/presentation.ts +52 -0
  149. package/modules/approvals/src/client/state.ts +141 -0
  150. package/modules/approvals/src/domain/capability.ts +107 -0
  151. package/modules/approvals/src/domain/grant.ts +69 -0
  152. package/modules/approvals/src/domain/types.ts +168 -0
  153. package/modules/approvals/src/index.ts +61 -0
  154. package/modules/approvals/src/platform.ts +136 -0
  155. package/modules/approvals/src/server/index.ts +42 -0
  156. package/modules/approvals/src/server/runtime.ts +185 -0
  157. package/modules/approvals/src/services/approvals-service.ts +637 -0
  158. package/modules/approvals/src/services/callbacks.ts +75 -0
  159. package/modules/approvals/src/services/data-classes.ts +167 -0
  160. package/modules/approvals/src/services/database-repository.ts +783 -0
  161. package/modules/approvals/src/services/eligibility.ts +121 -0
  162. package/modules/approvals/src/services/expiry-runner.ts +81 -0
  163. package/modules/approvals/src/services/index.ts +31 -0
  164. package/modules/approvals/src/services/migration.ts +256 -0
  165. package/modules/approvals/src/services/notifications.ts +95 -0
  166. package/modules/approvals/src/services/repository.ts +169 -0
  167. package/modules/approvals/src/services/service-error.ts +70 -0
  168. package/modules/approvals/src/settings.ts +102 -0
  169. package/modules/approvals/translations/en.json +104 -0
  170. package/modules/approvals/translations/pl.json +104 -0
  171. package/modules/audit/migrations/0001_audit_core.down.sql +8 -0
  172. package/modules/audit/migrations/0001_audit_core.up.sql +107 -0
  173. package/modules/audit/migrations/0002_audit_sweep_routing_role.down.sql +2 -0
  174. package/modules/audit/migrations/0002_audit_sweep_routing_role.up.sql +21 -0
  175. package/modules/audit/migrations/0003_audit_export_request.down.sql +12 -0
  176. package/modules/audit/migrations/0003_audit_export_request.up.sql +40 -0
  177. package/modules/audit/migrations/0004_audit_hold_seal_erasure.down.sql +17 -0
  178. package/modules/audit/migrations/0004_audit_hold_seal_erasure.up.sql +192 -0
  179. package/modules/audit/migrations/0005_audit_event_format.down.sql +17 -0
  180. package/modules/audit/migrations/0005_audit_event_format.up.sql +60 -0
  181. package/modules/audit/migrations/0006_audit_own_class_walks.down.sql +3 -0
  182. package/modules/audit/migrations/0006_audit_own_class_walks.up.sql +14 -0
  183. package/modules/audit/migrations/README.md +3 -0
  184. package/modules/audit/module.json +32 -0
  185. package/modules/audit/package.json +49 -0
  186. package/modules/audit/spec/module.yaml +809 -0
  187. package/modules/audit/src/acl/permissions.ts +7 -0
  188. package/modules/audit/src/api/endpoints.ts +315 -0
  189. package/modules/audit/src/cli/commands.json +95 -0
  190. package/modules/audit/src/cli/index.ts +658 -0
  191. package/modules/audit/src/client/DataClassesView.tsrx +420 -0
  192. package/modules/audit/src/client/ExportsView.tsrx +256 -0
  193. package/modules/audit/src/client/HoldsView.tsrx +604 -0
  194. package/modules/audit/src/client/SweepsView.tsrx +251 -0
  195. package/modules/audit/src/client/api.ts +185 -0
  196. package/modules/audit/src/client/contribution.tsrx +52 -0
  197. package/modules/audit/src/client/index.ts +23 -0
  198. package/modules/audit/src/client/navigation.ts +74 -0
  199. package/modules/audit/src/client/presentation.ts +105 -0
  200. package/modules/audit/src/client/state.ts +86 -0
  201. package/modules/audit/src/domain/data-classes.ts +16 -0
  202. package/modules/audit/src/domain/types.ts +339 -0
  203. package/modules/audit/src/index.ts +76 -0
  204. package/modules/audit/src/platform.ts +44 -0
  205. package/modules/audit/src/server/index.ts +177 -0
  206. package/modules/audit/src/server/runtime.ts +303 -0
  207. package/modules/audit/src/services/anchor-key.ts +143 -0
  208. package/modules/audit/src/services/anchor-rotation.ts +147 -0
  209. package/modules/audit/src/services/audit-runners.ts +278 -0
  210. package/modules/audit/src/services/backup-guard.ts +95 -0
  211. package/modules/audit/src/services/database-repository.ts +1946 -0
  212. package/modules/audit/src/services/declared-classes.ts +64 -0
  213. package/modules/audit/src/services/erasure-port.ts +126 -0
  214. package/modules/audit/src/services/erasure-service.ts +770 -0
  215. package/modules/audit/src/services/export-directory.ts +106 -0
  216. package/modules/audit/src/services/export-service.ts +714 -0
  217. package/modules/audit/src/services/hold-service.ts +295 -0
  218. package/modules/audit/src/services/index.ts +17 -0
  219. package/modules/audit/src/services/migration.ts +658 -0
  220. package/modules/audit/src/services/own-classes.ts +318 -0
  221. package/modules/audit/src/services/platform-version.ts +23 -0
  222. package/modules/audit/src/services/repository.ts +495 -0
  223. package/modules/audit/src/services/retention-service.ts +191 -0
  224. package/modules/audit/src/services/seal-service.ts +769 -0
  225. package/modules/audit/src/services/service-error.ts +61 -0
  226. package/modules/audit/src/services/subject-keys.ts +90 -0
  227. package/modules/audit/src/services/sweep-service.ts +298 -0
  228. package/modules/audit/src/services/zip.ts +217 -0
  229. package/modules/audit/src/settings.ts +69 -0
  230. package/modules/audit/translations/en.json +210 -0
  231. package/modules/audit/translations/pl.json +210 -0
  232. package/modules/auth/README.md +77 -29
  233. package/modules/auth/migrations/0016_external_identities.up.sql +16 -0
  234. package/modules/auth/migrations/0017_mfa_key_id.up.sql +8 -0
  235. package/modules/auth/migrations/0018_retire_bundled_module_scopes.up.sql +29 -0
  236. package/modules/auth/migrations/0019_notifications_member_scopes.up.sql +44 -0
  237. package/modules/auth/migrations/0020_identity_providers.up.sql +61 -0
  238. package/modules/auth/migrations/0021_membership_status.up.sql +19 -0
  239. package/modules/auth/migrations/0022_auth_provider_scopes.up.sql +45 -0
  240. package/modules/auth/migrations/0023_enterprise_module_scopes.down.sql +19 -0
  241. package/modules/auth/migrations/0023_enterprise_module_scopes.up.sql +71 -0
  242. package/modules/auth/migrations/0024_approvals_member_scopes.down.sql +22 -0
  243. package/modules/auth/migrations/0024_approvals_member_scopes.up.sql +48 -0
  244. package/modules/auth/migrations/0025_audit_holds_owner_scope.down.sql +21 -0
  245. package/modules/auth/migrations/0025_audit_holds_owner_scope.up.sql +47 -0
  246. package/modules/auth/migrations/0026_auth_export_keyset_indexes.down.sql +6 -0
  247. package/modules/auth/migrations/0026_auth_export_keyset_indexes.up.sql +15 -0
  248. package/modules/auth/migrations/0027_workflow_automation_profile_scopes.down.sql +34 -0
  249. package/modules/auth/migrations/0027_workflow_automation_profile_scopes.up.sql +72 -0
  250. package/modules/auth/migrations/0028_auth_member_search_prefix_indexes.down.sql +6 -0
  251. package/modules/auth/migrations/0028_auth_member_search_prefix_indexes.up.sql +21 -0
  252. package/modules/auth/migrations/0029_reports_exports_access_scopes.down.sql +22 -0
  253. package/modules/auth/migrations/0029_reports_exports_access_scopes.up.sql +56 -0
  254. package/modules/auth/migrations/0030_auth_membership_keyset_index.down.sql +4 -0
  255. package/modules/auth/migrations/0030_auth_membership_keyset_index.up.sql +10 -0
  256. package/modules/auth/migrations/0031_membership_scope_backfills_under_rls.down.sql +4 -0
  257. package/modules/auth/migrations/0031_membership_scope_backfills_under_rls.up.sql +31 -0
  258. package/modules/auth/migrations/0032_audit_actor_service.down.sql +9 -0
  259. package/modules/auth/migrations/0032_audit_actor_service.up.sql +10 -0
  260. package/modules/auth/migrations/0033_auth_account_keyset_indexes.down.sql +5 -0
  261. package/modules/auth/migrations/0033_auth_account_keyset_indexes.up.sql +23 -0
  262. package/modules/auth/module.json +3 -3
  263. package/modules/auth/package.json +2 -1
  264. package/modules/auth/spec/module.yaml +372 -3
  265. package/modules/auth/src/acl/scopes.ts +50 -6
  266. package/modules/auth/src/cli/commands.json +11 -0
  267. package/modules/auth/src/cli/greenfield.ts +2 -2
  268. package/modules/auth/src/cli/index.ts +14 -0
  269. package/modules/auth/src/cli/secrets.ts +114 -0
  270. package/modules/auth/src/client/AuthenticationCore.tsrx +61 -8
  271. package/modules/auth/src/client/api.ts +72 -6
  272. package/modules/auth/src/client/contribution.tsrx +23 -0
  273. package/modules/auth/src/client/index.ts +6 -0
  274. package/modules/auth/src/client/providers/IdentityProviderForm.tsrx +191 -0
  275. package/modules/auth/src/client/providers/IdentityProvidersView.tsrx +358 -0
  276. package/modules/auth/src/client/providers/api.ts +128 -0
  277. package/modules/auth/src/client/providers/provider-columns.tsrx +60 -0
  278. package/modules/auth/src/client/providers/state.ts +35 -0
  279. package/modules/auth/src/client/security/SecurityView.tsrx +9 -1
  280. package/modules/auth/src/client/state.ts +15 -1
  281. package/modules/auth/src/domain/types.ts +55 -0
  282. package/modules/auth/src/index.ts +11 -1
  283. package/modules/auth/src/middleware/authentication.ts +4 -0
  284. package/modules/auth/src/server/endpoints.ts +467 -171
  285. package/modules/auth/src/server/http.ts +3 -2
  286. package/modules/auth/src/server/index.ts +22 -1
  287. package/modules/auth/src/server/membership-endpoints.ts +62 -0
  288. package/modules/auth/src/server/mfa-enforcement.ts +156 -0
  289. package/modules/auth/src/server/oidc.ts +389 -0
  290. package/modules/auth/src/server/provider-endpoints.ts +246 -0
  291. package/modules/auth/src/server/provider-host.ts +77 -0
  292. package/modules/auth/src/server/runtime.ts +231 -55
  293. package/modules/auth/src/services/auth-service-error.ts +7 -2
  294. package/modules/auth/src/services/auth-service.ts +948 -51
  295. package/modules/auth/src/services/data-classes.ts +236 -0
  296. package/modules/auth/src/services/database-repository.ts +853 -71
  297. package/modules/auth/src/services/identity-provider-service.ts +571 -0
  298. package/modules/auth/src/services/mail-delivery.ts +16 -16
  299. package/modules/auth/src/services/mail-message.ts +54 -0
  300. package/modules/auth/src/services/mail-port.ts +24 -0
  301. package/modules/auth/src/services/mail-smtp.ts +85 -0
  302. package/modules/auth/src/services/mfa-rotation.ts +144 -0
  303. package/modules/auth/src/services/migration.ts +810 -0
  304. package/modules/auth/src/services/password.ts +14 -0
  305. package/modules/auth/src/services/provider-secret-rotation.ts +169 -0
  306. package/modules/auth/src/services/provider-secrets.ts +136 -0
  307. package/modules/auth/src/services/repository.ts +321 -5
  308. package/modules/auth/src/services/session-sweep-runner.ts +73 -0
  309. package/modules/auth/src/services/settings-store.ts +10 -83
  310. package/modules/auth/src/services/totp.ts +88 -62
  311. package/modules/auth/src/services/validation.ts +194 -7
  312. package/modules/auth/src/settings.ts +18 -0
  313. package/modules/auth/translations/en.json +84 -1
  314. package/modules/auth/translations/pl.json +84 -1
  315. package/modules/automations/migrations/0005_secret_rotation_inventory.down.sql +1 -0
  316. package/modules/automations/migrations/0005_secret_rotation_inventory.up.sql +6 -0
  317. package/modules/automations/migrations/0006_automations_list_sort_indexes.down.sql +4 -0
  318. package/modules/automations/migrations/0006_automations_list_sort_indexes.up.sql +11 -0
  319. package/modules/automations/module.json +24 -6
  320. package/modules/automations/package.json +4 -1
  321. package/modules/automations/spec/module.yaml +81 -7
  322. package/modules/automations/src/api/endpoints.ts +202 -28
  323. package/modules/automations/src/cli/commands.json +17 -0
  324. package/modules/automations/src/cli/index.ts +114 -0
  325. package/modules/automations/src/client/AutomationScheduleForm.tsrx +77 -19
  326. package/modules/automations/src/client/AutomationSchedulesView.tsrx +144 -40
  327. package/modules/automations/src/client/AutomationTriggersView.tsrx +131 -32
  328. package/modules/automations/src/client/api.ts +62 -19
  329. package/modules/automations/src/client/presentation.ts +20 -2
  330. package/modules/automations/src/client/state.ts +84 -0
  331. package/modules/automations/src/domain/cadence.ts +99 -3
  332. package/modules/automations/src/domain/cron.ts +366 -0
  333. package/modules/automations/src/domain/time-zone.ts +61 -0
  334. package/modules/automations/src/platform.ts +38 -4
  335. package/modules/automations/src/server/index.ts +11 -0
  336. package/modules/automations/src/server/runtime.ts +46 -22
  337. package/modules/{automations-workflows-integration/src/server/adapter.ts → automations/src/server/workflow-target.ts} +13 -14
  338. package/modules/{automations-workflows-integration/src/server/tools.ts → automations/src/server/workflow-tools.ts} +0 -0
  339. package/modules/automations/src/services/data-classes.ts +62 -0
  340. package/modules/automations/src/services/database-repository.ts +152 -38
  341. package/modules/automations/src/services/migration.ts +119 -9
  342. package/modules/automations/src/services/repository.ts +52 -1
  343. package/modules/automations/src/services/schedule-runner.ts +84 -0
  344. package/modules/automations/src/services/schedule-service.ts +152 -26
  345. package/modules/automations/src/services/secret-rotation.ts +176 -0
  346. package/modules/automations/src/services/secret-vault.ts +68 -41
  347. package/modules/automations/src/services/trigger-service.ts +13 -10
  348. package/modules/automations/translations/en.json +15 -2
  349. package/modules/automations/translations/pl.json +15 -2
  350. package/modules/connectors/migrations/0001_connectors_core.down.sql +3 -0
  351. package/modules/connectors/migrations/0001_connectors_core.up.sql +72 -0
  352. package/modules/connectors/migrations/0002_connectors_call_keys.down.sql +3 -0
  353. package/modules/connectors/migrations/0002_connectors_call_keys.up.sql +18 -0
  354. package/modules/connectors/migrations/0003_connectors_rotation_inventory.down.sql +2 -0
  355. package/modules/connectors/migrations/0003_connectors_rotation_inventory.up.sql +19 -0
  356. package/modules/connectors/migrations/README.md +3 -0
  357. package/modules/connectors/module.json +32 -0
  358. package/modules/{automations-workflows-integration → connectors}/package.json +28 -26
  359. package/modules/connectors/spec/module.yaml +448 -0
  360. package/modules/connectors/src/acl/permissions.ts +6 -0
  361. package/modules/connectors/src/agent/tools.ts +180 -0
  362. package/modules/connectors/src/api/endpoints.ts +572 -0
  363. package/modules/connectors/src/cli/commands.json +17 -0
  364. package/modules/connectors/src/cli/index.ts +126 -0
  365. package/modules/connectors/src/client/ConnectorCallsView.tsrx +358 -0
  366. package/modules/connectors/src/client/ConnectorForm.tsrx +424 -0
  367. package/modules/connectors/src/client/ConnectorsView.tsrx +711 -0
  368. package/modules/connectors/src/client/api.ts +281 -0
  369. package/modules/connectors/src/client/consent.ts +48 -0
  370. package/modules/connectors/src/client/contribution.tsrx +37 -0
  371. package/modules/connectors/src/client/index.ts +22 -0
  372. package/modules/connectors/src/client/navigation.ts +42 -0
  373. package/modules/connectors/src/client/presentation.ts +38 -0
  374. package/modules/connectors/src/client/state.ts +175 -0
  375. package/modules/connectors/src/domain/calls.ts +70 -0
  376. package/modules/connectors/src/domain/definitions.ts +190 -0
  377. package/modules/connectors/src/domain/http-json.ts +68 -0
  378. package/modules/connectors/src/domain/types.ts +185 -0
  379. package/modules/connectors/src/index.ts +64 -0
  380. package/modules/connectors/src/platform.ts +56 -0
  381. package/modules/connectors/src/server/index.ts +66 -0
  382. package/modules/connectors/src/server/runtime.ts +163 -0
  383. package/modules/connectors/src/services/call-service.ts +903 -0
  384. package/modules/connectors/src/services/connectors-service.ts +653 -0
  385. package/modules/connectors/src/services/credential-rotation.ts +221 -0
  386. package/modules/connectors/src/services/credential-vault.ts +194 -0
  387. package/modules/connectors/src/services/data-classes.ts +158 -0
  388. package/modules/connectors/src/services/database-repository.ts +897 -0
  389. package/modules/connectors/src/services/egress.ts +261 -0
  390. package/modules/connectors/src/services/index.ts +19 -0
  391. package/modules/connectors/src/services/migration.ts +202 -0
  392. package/modules/connectors/src/services/repository.ts +175 -0
  393. package/modules/connectors/src/services/service-error.ts +46 -0
  394. package/modules/connectors/src/settings.ts +69 -0
  395. package/modules/connectors/translations/en.json +191 -0
  396. package/modules/connectors/translations/pl.json +191 -0
  397. package/modules/directory/migrations/0001_directory_core.down.sql +5 -0
  398. package/modules/directory/migrations/0001_directory_core.up.sql +111 -0
  399. package/modules/directory/migrations/README.md +3 -0
  400. package/modules/directory/module.json +27 -0
  401. package/modules/directory/package.json +46 -0
  402. package/modules/directory/spec/module.yaml +351 -0
  403. package/modules/directory/src/acl/permissions.ts +7 -0
  404. package/modules/directory/src/api/endpoints.ts +478 -0
  405. package/modules/directory/src/api/scim-endpoints.ts +516 -0
  406. package/modules/directory/src/client/GroupMappingsView.tsrx +430 -0
  407. package/modules/directory/src/client/ProvisioningLogView.tsrx +248 -0
  408. package/modules/directory/src/client/ScimTokensView.tsrx +587 -0
  409. package/modules/directory/src/client/api.ts +222 -0
  410. package/modules/directory/src/client/contribution.tsrx +46 -0
  411. package/modules/directory/src/client/index.ts +22 -0
  412. package/modules/directory/src/client/navigation.ts +57 -0
  413. package/modules/directory/src/client/presentation.ts +143 -0
  414. package/modules/directory/src/client/state.ts +173 -0
  415. package/modules/directory/src/domain/scim.ts +346 -0
  416. package/modules/directory/src/domain/types.ts +166 -0
  417. package/modules/directory/src/index.ts +44 -0
  418. package/modules/directory/src/platform.ts +43 -0
  419. package/modules/directory/src/server/index.ts +17 -0
  420. package/modules/directory/src/server/runtime.ts +124 -0
  421. package/modules/directory/src/services/auth-port.ts +127 -0
  422. package/modules/directory/src/services/data-classes.ts +41 -0
  423. package/modules/directory/src/services/database-repository.ts +1067 -0
  424. package/modules/directory/src/services/directory-service.ts +311 -0
  425. package/modules/directory/src/services/index.ts +43 -0
  426. package/modules/directory/src/services/migration.ts +146 -0
  427. package/modules/directory/src/services/provisioning-service.ts +1420 -0
  428. package/modules/directory/src/services/rate-limiter.ts +103 -0
  429. package/modules/directory/src/services/repository.ts +208 -0
  430. package/modules/directory/src/services/service-error.ts +42 -0
  431. package/modules/directory/src/services/token-service.ts +215 -0
  432. package/modules/directory/src/settings.ts +86 -0
  433. package/modules/directory/translations/en.json +185 -0
  434. package/modules/directory/translations/pl.json +185 -0
  435. package/modules/documents/migrations/0001_documents_core.down.sql +1 -0
  436. package/modules/documents/migrations/0001_documents_core.up.sql +35 -0
  437. package/modules/documents/migrations/0002_documents_files_page.down.sql +1 -0
  438. package/modules/documents/migrations/0002_documents_files_page.up.sql +6 -0
  439. package/modules/documents/migrations/0003_documents_rotation_inventory.down.sql +2 -0
  440. package/modules/documents/migrations/0003_documents_rotation_inventory.up.sql +18 -0
  441. package/modules/documents/migrations/README.md +3 -0
  442. package/modules/documents/module.json +32 -0
  443. package/modules/documents/package.json +50 -0
  444. package/modules/documents/spec/module.yaml +248 -0
  445. package/modules/documents/src/acl/permissions.ts +6 -0
  446. package/modules/documents/src/api/endpoints.ts +401 -0
  447. package/modules/documents/src/cli/commands.json +17 -0
  448. package/modules/documents/src/cli/index.ts +145 -0
  449. package/modules/documents/src/client/DocumentUploader.tsrx +148 -0
  450. package/modules/documents/src/client/DocumentsView.tsrx +540 -0
  451. package/modules/documents/src/client/api.ts +209 -0
  452. package/modules/documents/src/client/contribution.tsrx +29 -0
  453. package/modules/documents/src/client/download.ts +70 -0
  454. package/modules/documents/src/client/index.ts +22 -0
  455. package/modules/documents/src/client/navigation.ts +25 -0
  456. package/modules/documents/src/client/presentation.ts +110 -0
  457. package/modules/documents/src/client/state.ts +117 -0
  458. package/modules/documents/src/domain/attachments.ts +84 -0
  459. package/modules/documents/src/domain/types.ts +80 -0
  460. package/modules/documents/src/index.ts +46 -0
  461. package/modules/documents/src/platform.ts +61 -0
  462. package/modules/documents/src/server/index.ts +30 -0
  463. package/modules/documents/src/server/runtime.ts +107 -0
  464. package/modules/documents/src/services/attachments.ts +39 -0
  465. package/modules/documents/src/services/data-classes.ts +25 -0
  466. package/modules/documents/src/services/database-repository.ts +270 -0
  467. package/modules/documents/src/services/documents-service.ts +590 -0
  468. package/modules/documents/src/services/index.ts +18 -0
  469. package/modules/documents/src/services/migration.ts +117 -0
  470. package/modules/documents/src/services/repository.ts +58 -0
  471. package/modules/documents/src/services/storage-rotation.ts +157 -0
  472. package/modules/documents/src/settings.ts +107 -0
  473. package/modules/documents/translations/en.json +111 -0
  474. package/modules/documents/translations/pl.json +111 -0
  475. package/modules/exports/migrations/0001_exports_core.down.sql +2 -0
  476. package/modules/exports/migrations/0001_exports_core.up.sql +28 -0
  477. package/modules/exports/migrations/0002_exports_job_routing_role.down.sql +4 -0
  478. package/modules/exports/migrations/0002_exports_job_routing_role.up.sql +21 -0
  479. package/modules/exports/migrations/0003_exports_rotation_inventory.down.sql +1 -0
  480. package/modules/exports/migrations/0003_exports_rotation_inventory.up.sql +9 -0
  481. package/modules/exports/migrations/README.md +3 -0
  482. package/modules/exports/module.json +32 -0
  483. package/modules/exports/package.json +45 -0
  484. package/modules/exports/spec/module.yaml +230 -0
  485. package/modules/exports/src/acl/permissions.ts +6 -0
  486. package/modules/exports/src/api/endpoints.ts +211 -0
  487. package/modules/exports/src/cli/commands.json +17 -0
  488. package/modules/exports/src/cli/index.ts +145 -0
  489. package/modules/exports/src/client/ExportsView.tsrx +327 -0
  490. package/modules/exports/src/client/api.ts +133 -0
  491. package/modules/exports/src/client/contribution.tsrx +31 -0
  492. package/modules/exports/src/client/index.ts +20 -0
  493. package/modules/exports/src/client/navigation.ts +25 -0
  494. package/modules/exports/src/client/presentation.ts +53 -0
  495. package/modules/exports/src/client/state.ts +56 -0
  496. package/modules/exports/src/domain/lists.ts +29 -0
  497. package/modules/exports/src/domain/types.ts +120 -0
  498. package/modules/exports/src/index.ts +41 -0
  499. package/modules/exports/src/platform.ts +80 -0
  500. package/modules/exports/src/server/index.ts +50 -0
  501. package/modules/exports/src/server/runtime.ts +160 -0
  502. package/modules/exports/src/services/data-classes.ts +59 -0
  503. package/modules/exports/src/services/database-repository.ts +403 -0
  504. package/modules/exports/src/services/export-runner.ts +123 -0
  505. package/modules/exports/src/services/export-service.ts +356 -0
  506. package/modules/exports/src/services/index.ts +6 -0
  507. package/modules/exports/src/services/list-registry.ts +105 -0
  508. package/modules/exports/src/services/migration.ts +116 -0
  509. package/modules/exports/src/services/repository.ts +96 -0
  510. package/modules/exports/src/services/storage-rotation.ts +138 -0
  511. package/modules/exports/src/settings.ts +70 -0
  512. package/modules/exports/translations/en.json +69 -0
  513. package/modules/exports/translations/pl.json +69 -0
  514. package/modules/import/migrations/0001_import_core.down.sql +6 -0
  515. package/modules/import/migrations/0001_import_core.up.sql +61 -0
  516. package/modules/import/migrations/0002_import_job_routing_role.down.sql +4 -0
  517. package/modules/import/migrations/0002_import_job_routing_role.up.sql +20 -0
  518. package/modules/import/migrations/0003_import_jobs_export_index.down.sql +1 -0
  519. package/modules/import/migrations/0003_import_jobs_export_index.up.sql +7 -0
  520. package/modules/import/migrations/0004_import_jobs_traceparent.down.sql +1 -0
  521. package/modules/import/migrations/0004_import_jobs_traceparent.up.sql +7 -0
  522. package/modules/import/migrations/README.md +3 -0
  523. package/modules/import/module.json +37 -0
  524. package/modules/import/package.json +50 -0
  525. package/modules/import/spec/module.yaml +335 -0
  526. package/modules/import/src/acl/permissions.ts +6 -0
  527. package/modules/import/src/api/endpoints.ts +400 -0
  528. package/modules/import/src/client/ImportJobDrawer.tsrx +229 -0
  529. package/modules/import/src/client/ImportsView.tsrx +455 -0
  530. package/modules/import/src/client/NewImportForm.tsrx +274 -0
  531. package/modules/import/src/client/api.ts +243 -0
  532. package/modules/import/src/client/contribution.tsrx +29 -0
  533. package/modules/import/src/client/index.ts +23 -0
  534. package/modules/import/src/client/navigation.ts +25 -0
  535. package/modules/import/src/client/presentation.ts +59 -0
  536. package/modules/import/src/client/state.ts +43 -0
  537. package/modules/import/src/domain/csv.ts +258 -0
  538. package/modules/import/src/domain/ports.ts +157 -0
  539. package/modules/import/src/domain/types.ts +179 -0
  540. package/modules/import/src/index.ts +58 -0
  541. package/modules/import/src/platform.ts +60 -0
  542. package/modules/import/src/server/index.ts +49 -0
  543. package/modules/import/src/server/runtime.ts +158 -0
  544. package/modules/import/src/services/csv-source.ts +121 -0
  545. package/modules/import/src/services/data-classes.ts +63 -0
  546. package/modules/import/src/services/database-repository.ts +780 -0
  547. package/modules/import/src/services/import-runner.ts +127 -0
  548. package/modules/import/src/services/import-service.ts +872 -0
  549. package/modules/import/src/services/index.ts +6 -0
  550. package/modules/import/src/services/migration.ts +156 -0
  551. package/modules/import/src/services/port-registry.ts +158 -0
  552. package/modules/import/src/services/repository.ts +147 -0
  553. package/modules/import/src/settings.ts +70 -0
  554. package/modules/import/translations/en.json +175 -0
  555. package/modules/import/translations/pl.json +175 -0
  556. package/modules/metering/migrations/0001_metering_core.down.sql +6 -0
  557. package/modules/metering/migrations/0001_metering_core.up.sql +119 -0
  558. package/modules/metering/migrations/0002_metering_bucket_export.down.sql +3 -0
  559. package/modules/metering/migrations/0002_metering_bucket_export.up.sql +5 -0
  560. package/modules/metering/migrations/README.md +3 -0
  561. package/modules/metering/module.json +46 -0
  562. package/modules/metering/package.json +50 -0
  563. package/modules/metering/spec/module.yaml +269 -0
  564. package/modules/metering/src/acl/permissions.ts +5 -0
  565. package/modules/metering/src/api/endpoints.ts +104 -0
  566. package/modules/metering/src/cli/commands.json +28 -0
  567. package/modules/metering/src/cli/index.ts +246 -0
  568. package/modules/metering/src/client/LimitsView.tsrx +170 -0
  569. package/modules/metering/src/client/UsageSummaryWidget.tsrx +63 -0
  570. package/modules/metering/src/client/UsageView.tsrx +205 -0
  571. package/modules/metering/src/client/api.ts +91 -0
  572. package/modules/metering/src/client/contribution.tsrx +42 -0
  573. package/modules/metering/src/client/index.ts +26 -0
  574. package/modules/metering/src/client/navigation.ts +54 -0
  575. package/modules/metering/src/client/presentation.ts +63 -0
  576. package/modules/metering/src/client/state.ts +31 -0
  577. package/modules/metering/src/domain/meters.ts +100 -0
  578. package/modules/metering/src/domain/types.ts +71 -0
  579. package/modules/metering/src/index.ts +51 -0
  580. package/modules/metering/src/platform.ts +84 -0
  581. package/modules/metering/src/server/index.ts +40 -0
  582. package/modules/metering/src/server/runtime.ts +165 -0
  583. package/modules/metering/src/services/data-classes.ts +34 -0
  584. package/modules/metering/src/services/database-repository.ts +574 -0
  585. package/modules/metering/src/services/index.ts +9 -0
  586. package/modules/metering/src/services/meter-registry.ts +145 -0
  587. package/modules/metering/src/services/metering-service.ts +441 -0
  588. package/modules/metering/src/services/migration.ts +183 -0
  589. package/modules/metering/src/services/notifications.ts +75 -0
  590. package/modules/metering/src/services/reports.ts +105 -0
  591. package/modules/metering/src/services/repository.ts +132 -0
  592. package/modules/metering/src/services/service-error.ts +46 -0
  593. package/modules/metering/src/settings.ts +47 -0
  594. package/modules/metering/translations/en.json +68 -0
  595. package/modules/metering/translations/pl.json +68 -0
  596. package/modules/notifications/migrations/0001_notifications_core.down.sql +1 -0
  597. package/modules/notifications/migrations/0001_notifications_core.up.sql +26 -0
  598. package/modules/notifications/migrations/0002_notifications_preferences.down.sql +1 -0
  599. package/modules/notifications/migrations/0002_notifications_preferences.up.sql +17 -0
  600. package/modules/notifications/migrations/0003_notifications_webhook_subscriptions.down.sql +1 -0
  601. package/modules/notifications/migrations/0003_notifications_webhook_subscriptions.up.sql +30 -0
  602. package/modules/notifications/migrations/0004_notifications_deliveries.down.sql +1 -0
  603. package/modules/notifications/migrations/0004_notifications_deliveries.up.sql +35 -0
  604. package/modules/notifications/migrations/0005_notifications_delivery_routing_role.down.sql +2 -0
  605. package/modules/notifications/migrations/0005_notifications_delivery_routing_role.up.sql +17 -0
  606. package/modules/notifications/migrations/0006_notifications_delivery_title.down.sql +1 -0
  607. package/modules/notifications/migrations/0006_notifications_delivery_title.up.sql +11 -0
  608. package/modules/notifications/migrations/0007_notifications_secret_rotation_inventory.down.sql +2 -0
  609. package/modules/notifications/migrations/0007_notifications_secret_rotation_inventory.up.sql +19 -0
  610. package/modules/notifications/migrations/0008_notifications_delivery_claim.down.sql +8 -0
  611. package/modules/notifications/migrations/0008_notifications_delivery_claim.up.sql +19 -0
  612. package/modules/notifications/migrations/0009_notifications_kind_approvals.down.sql +16 -0
  613. package/modules/notifications/migrations/0009_notifications_kind_approvals.up.sql +18 -0
  614. package/modules/notifications/migrations/0010_notifications_kind_meter_threshold.down.sql +16 -0
  615. package/modules/notifications/migrations/0010_notifications_kind_meter_threshold.up.sql +18 -0
  616. package/modules/notifications/migrations/0011_notifications_retention_keyset.down.sql +5 -0
  617. package/modules/notifications/migrations/0011_notifications_retention_keyset.up.sql +15 -0
  618. package/modules/notifications/migrations/0012_notifications_email_channel.down.sql +12 -0
  619. package/modules/notifications/migrations/0012_notifications_email_channel.up.sql +38 -0
  620. package/modules/notifications/migrations/0013_notifications_list_pages.down.sql +5 -0
  621. package/modules/notifications/migrations/0013_notifications_list_pages.up.sql +13 -0
  622. package/modules/notifications/migrations/README.md +3 -0
  623. package/modules/notifications/module.json +32 -0
  624. package/modules/notifications/package.json +49 -0
  625. package/modules/notifications/spec/module.yaml +718 -0
  626. package/modules/notifications/src/acl/permissions.ts +11 -0
  627. package/modules/notifications/src/api/endpoints.ts +814 -0
  628. package/modules/notifications/src/cli/commands.json +17 -0
  629. package/modules/notifications/src/cli/index.ts +126 -0
  630. package/modules/notifications/src/client/DeliveriesView.tsrx +483 -0
  631. package/modules/notifications/src/client/FactList.tsrx +24 -0
  632. package/modules/notifications/src/client/NotificationPreferencesView.tsrx +240 -0
  633. package/modules/notifications/src/client/NotificationsInboxView.tsrx +592 -0
  634. package/modules/notifications/src/client/UnreadCountWidget.tsrx +62 -0
  635. package/modules/notifications/src/client/WebhookForm.tsrx +297 -0
  636. package/modules/notifications/src/client/WebhooksView.tsrx +637 -0
  637. package/modules/notifications/src/client/api.ts +382 -0
  638. package/modules/notifications/src/client/contribution.tsrx +83 -0
  639. package/modules/notifications/src/client/inbox.ts +41 -0
  640. package/modules/notifications/src/client/index.ts +29 -0
  641. package/modules/notifications/src/client/navigation.ts +92 -0
  642. package/modules/notifications/src/client/notifications.css +70 -0
  643. package/modules/notifications/src/client/presentation.ts +81 -0
  644. package/modules/notifications/src/client/state.ts +246 -0
  645. package/modules/notifications/src/client/unread-count.ts +26 -0
  646. package/modules/notifications/src/domain/locale.ts +47 -0
  647. package/modules/notifications/src/domain/publish.ts +46 -0
  648. package/modules/notifications/src/domain/types.ts +186 -0
  649. package/modules/notifications/src/index.ts +62 -0
  650. package/modules/notifications/src/platform.ts +85 -0
  651. package/modules/notifications/src/server/index.ts +82 -0
  652. package/modules/notifications/src/server/runtime.ts +251 -0
  653. package/modules/notifications/src/services/data-classes.ts +139 -0
  654. package/modules/notifications/src/services/database-repository.ts +1334 -0
  655. package/modules/notifications/src/services/delivery-payload.ts +93 -0
  656. package/modules/notifications/src/services/delivery-runner.ts +170 -0
  657. package/modules/notifications/src/services/delivery-service.ts +811 -0
  658. package/modules/notifications/src/services/egress.ts +239 -0
  659. package/modules/notifications/src/services/email-channel.ts +93 -0
  660. package/modules/notifications/src/services/index.ts +23 -0
  661. package/modules/notifications/src/services/migration.ts +572 -0
  662. package/modules/notifications/src/services/notifications-service.ts +297 -0
  663. package/modules/notifications/src/services/paging.ts +60 -0
  664. package/modules/notifications/src/services/publish-service.ts +133 -0
  665. package/modules/notifications/src/services/repository.ts +342 -0
  666. package/modules/notifications/src/services/secret-rotation.ts +177 -0
  667. package/modules/notifications/src/services/secret-vault.ts +168 -0
  668. package/modules/notifications/src/services/service-error.ts +67 -0
  669. package/modules/notifications/src/services/signature.ts +81 -0
  670. package/modules/notifications/src/services/webhook-service.ts +307 -0
  671. package/modules/notifications/src/settings.ts +130 -0
  672. package/modules/notifications/translations/en.json +255 -0
  673. package/modules/notifications/translations/pl.json +255 -0
  674. package/modules/profile/module.json +3 -3
  675. package/modules/profile/package.json +1 -1
  676. package/modules/profile/spec/module.yaml +3 -3
  677. package/modules/profile/src/services/database-repository.ts +3 -13
  678. package/modules/reports/module.json +28 -0
  679. package/modules/reports/package.json +42 -0
  680. package/modules/reports/spec/module.yaml +231 -0
  681. package/modules/reports/src/acl/permissions.ts +5 -0
  682. package/modules/reports/src/api/endpoints.ts +72 -0
  683. package/modules/reports/src/client/ReportsSummaryWidget.tsrx +52 -0
  684. package/modules/reports/src/client/ReportsView.tsrx +246 -0
  685. package/modules/reports/src/client/api.ts +65 -0
  686. package/modules/reports/src/client/contribution.tsrx +36 -0
  687. package/modules/reports/src/client/index.ts +18 -0
  688. package/modules/reports/src/client/navigation.ts +39 -0
  689. package/modules/reports/src/client/presentation.ts +123 -0
  690. package/modules/reports/src/client/state.ts +28 -0
  691. package/modules/reports/src/domain/providers.ts +115 -0
  692. package/modules/reports/src/domain/types.ts +44 -0
  693. package/modules/reports/src/index.ts +47 -0
  694. package/modules/reports/src/platform.ts +37 -0
  695. package/modules/reports/src/server/index.ts +21 -0
  696. package/modules/reports/src/server/runtime.ts +40 -0
  697. package/modules/reports/src/services/index.ts +17 -0
  698. package/modules/reports/src/services/provider-registry.ts +144 -0
  699. package/modules/reports/src/services/range.ts +65 -0
  700. package/modules/reports/src/services/reports-service.ts +316 -0
  701. package/modules/reports/src/services/service-error.ts +26 -0
  702. package/modules/reports/src/settings.ts +49 -0
  703. package/modules/reports/translations/en.json +35 -0
  704. package/modules/reports/translations/pl.json +35 -0
  705. package/modules/sandbox/module.json +4 -4
  706. package/modules/sandbox/package.json +1 -1
  707. package/modules/sandbox/spec/module.yaml +3 -3
  708. package/modules/sandbox/src/services/database-repository.ts +14 -23
  709. package/modules/sandbox/src/services/directory.ts +16 -0
  710. package/modules/sandbox/src/services/sandbox-service.ts +13 -11
  711. package/modules/search/migrations/0001_search_core.down.sql +1 -0
  712. package/modules/search/migrations/0001_search_core.up.sql +22 -0
  713. package/modules/search/migrations/0002_search_recent_export.down.sql +3 -0
  714. package/modules/search/migrations/0002_search_recent_export.up.sql +5 -0
  715. package/modules/search/migrations/README.md +3 -0
  716. package/modules/search/module.json +28 -0
  717. package/modules/search/package.json +43 -0
  718. package/modules/search/spec/module.yaml +177 -0
  719. package/modules/search/src/acl/permissions.ts +5 -0
  720. package/modules/search/src/api/endpoints.ts +221 -0
  721. package/modules/search/src/client/SearchView.tsrx +346 -0
  722. package/modules/search/src/client/api.ts +157 -0
  723. package/modules/search/src/client/contribution.tsrx +31 -0
  724. package/modules/search/src/client/index.ts +17 -0
  725. package/modules/search/src/client/navigation.ts +97 -0
  726. package/modules/search/src/client/search.css +56 -0
  727. package/modules/search/src/client/state.ts +113 -0
  728. package/modules/search/src/domain/data-classes.ts +73 -0
  729. package/modules/search/src/domain/providers.ts +88 -0
  730. package/modules/search/src/domain/types.ts +56 -0
  731. package/modules/search/src/index.ts +46 -0
  732. package/modules/search/src/platform.ts +42 -0
  733. package/modules/search/src/server/index.ts +25 -0
  734. package/modules/search/src/server/runtime.ts +123 -0
  735. package/modules/search/src/services/database-repository.ts +163 -0
  736. package/modules/search/src/services/index.ts +21 -0
  737. package/modules/search/src/services/migration.ts +65 -0
  738. package/modules/search/src/services/provider-registry.ts +137 -0
  739. package/modules/search/src/services/repository.ts +43 -0
  740. package/modules/search/src/services/search-service.ts +442 -0
  741. package/modules/search/src/services/service-error.ts +35 -0
  742. package/modules/search/src/settings.ts +68 -0
  743. package/modules/search/translations/en.json +45 -0
  744. package/modules/search/translations/pl.json +45 -0
  745. package/modules/system/module.json +2 -2
  746. package/modules/system/package.json +1 -1
  747. package/modules/system/spec/module.yaml +30 -3
  748. package/modules/system/src/client/FlagsPanel.tsrx +127 -0
  749. package/modules/system/src/client/ModuleSettingRow.tsrx +17 -7
  750. package/modules/system/src/client/ModuleSettingsSection.tsrx +10 -71
  751. package/modules/system/src/client/ModulesView.tsrx +91 -65
  752. package/modules/system/src/client/flags.ts +35 -0
  753. package/modules/system/src/client/index.ts +8 -0
  754. package/modules/system/src/client/navigation.ts +8 -0
  755. package/modules/system/src/client/settings-state.ts +159 -0
  756. package/modules/system/src/client/state.ts +2 -0
  757. package/modules/system/src/domain/time-zone.ts +105 -0
  758. package/modules/system/src/index.ts +12 -0
  759. package/modules/system/src/platform.ts +5 -1
  760. package/modules/system/src/server/endpoints.ts +72 -21
  761. package/modules/system/src/settings.ts +28 -0
  762. package/modules/system/translations/en.json +17 -0
  763. package/modules/system/translations/pl.json +17 -0
  764. package/modules/users/module.json +30 -4
  765. package/modules/users/package.json +4 -1
  766. package/modules/users/spec/module.yaml +290 -7
  767. package/modules/users/src/api/endpoints.ts +228 -15
  768. package/modules/users/src/client/InviteMemberForm.tsrx +2 -2
  769. package/modules/users/src/client/MemberDrawer.tsrx +142 -26
  770. package/modules/users/src/client/UserAccountForm.tsrx +2 -2
  771. package/modules/users/src/client/UsersView.tsrx +432 -71
  772. package/modules/users/src/client/api.ts +106 -12
  773. package/modules/users/src/client/contribution.tsrx +5 -0
  774. package/modules/users/src/client/member-columns.tsrx +22 -9
  775. package/modules/users/src/client/state.ts +172 -7
  776. package/modules/users/src/domain/lists.ts +9 -0
  777. package/modules/users/src/index.ts +11 -0
  778. package/modules/users/src/platform.ts +50 -1
  779. package/modules/users/src/services/member-export.ts +51 -0
  780. package/modules/users/src/services/member-import.ts +249 -0
  781. package/modules/users/src/services/member-search.ts +145 -0
  782. package/modules/users/src/services/users-service.ts +117 -4
  783. package/modules/users/translations/en.json +57 -8
  784. package/modules/users/translations/pl.json +57 -8
  785. package/modules/workflows/migrations/0004_payload_rotation_inventory.up.sql +17 -0
  786. package/modules/workflows/migrations/0005_workflows_human_approval.up.sql +28 -0
  787. package/modules/workflows/migrations/0006_workflows_retention_indexes.up.sql +11 -0
  788. package/modules/workflows/migrations/0007_workflows_erasure_subject_indexes.up.sql +17 -0
  789. package/modules/workflows/migrations/0008_workflows_subject_account.up.sql +24 -0
  790. package/modules/workflows/migrations/0009_workflows_definition_list_indexes.up.sql +9 -0
  791. package/modules/workflows/module.json +34 -6
  792. package/modules/workflows/package.json +2 -1
  793. package/modules/workflows/spec/module.yaml +26 -9
  794. package/modules/workflows/src/api/endpoints.ts +131 -25
  795. package/modules/workflows/src/cli/commands.json +17 -0
  796. package/modules/workflows/src/cli/index.ts +114 -0
  797. package/modules/workflows/src/client/WorkflowExecutionHistory.tsrx +36 -27
  798. package/modules/workflows/src/client/WorkflowInspector.tsrx +116 -0
  799. package/modules/workflows/src/client/WorkflowNodePalette.tsrx +1 -0
  800. package/modules/workflows/src/client/WorkflowsView.tsrx +236 -99
  801. package/modules/workflows/src/client/api.ts +54 -34
  802. package/modules/workflows/src/client/canvas-model.ts +12 -0
  803. package/modules/workflows/src/client/state.ts +95 -0
  804. package/modules/workflows/src/domain/events.ts +25 -11
  805. package/modules/workflows/src/domain/graph.ts +135 -1
  806. package/modules/workflows/src/domain/types.ts +91 -4
  807. package/modules/workflows/src/platform.ts +12 -0
  808. package/modules/workflows/src/server/runtime.ts +78 -5
  809. package/modules/workflows/src/services/approvals.ts +62 -0
  810. package/modules/workflows/src/services/cursors.ts +37 -0
  811. package/modules/workflows/src/services/data-classes.ts +232 -0
  812. package/modules/workflows/src/services/database-repository.ts +660 -171
  813. package/modules/workflows/src/services/migration.ts +240 -0
  814. package/modules/workflows/src/services/notifications.ts +98 -0
  815. package/modules/workflows/src/services/payload-codec.ts +51 -28
  816. package/modules/workflows/src/services/payload-rotation.ts +171 -0
  817. package/modules/workflows/src/services/repository.ts +123 -8
  818. package/modules/workflows/src/services/simulation.ts +7 -0
  819. package/modules/workflows/src/services/worker.ts +446 -25
  820. package/modules/workflows/src/services/workflows-service.ts +264 -103
  821. package/modules/workflows/translations/en.json +12 -4
  822. package/modules/workflows/translations/pl.json +12 -4
  823. package/modules.json +48 -4
  824. package/package.json +64 -6
  825. package/packages/client/package.json +1 -0
  826. package/packages/client/src/ApplicationShell.tsrx +43 -0
  827. package/packages/client/src/contributions.ts +55 -0
  828. package/packages/client/src/i18n/locales/en.json +5 -0
  829. package/packages/client/src/i18n/locales/pl.json +5 -0
  830. package/packages/client/src/index.ts +13 -1
  831. package/packages/client/src/routing.ts +22 -0
  832. package/packages/client/src/shell/CommandPalette.tsrx +223 -11
  833. package/packages/client/src/shell/command-search.ts +126 -0
  834. package/packages/client/src/shell/command.ts +30 -0
  835. package/packages/client/src/shell/navigation.ts +12 -0
  836. package/packages/client/src/shell/palette-keys.ts +27 -0
  837. package/packages/client/src/shell/shell.css +7 -0
  838. package/packages/contracts/schemas/module-spec.schema.json +287 -1
  839. package/packages/contracts/schemas/module.schema.json +25 -0
  840. package/packages/contracts/src/index.ts +144 -1
  841. package/packages/database/src/backup.ts +180 -0
  842. package/packages/database/src/decoders.ts +25 -0
  843. package/packages/database/src/index.ts +17 -0
  844. package/packages/database/src/migrations.ts +7 -0
  845. package/packages/database/src/record-history.ts +5 -14
  846. package/packages/harness/src/index.ts +2 -0
  847. package/packages/harness/src/runtime.ts +378 -2
  848. package/packages/harness/src/tool-adapters.ts +20 -9
  849. package/packages/kernel/src/acl.ts +179 -0
  850. package/packages/kernel/src/approval-grant.ts +310 -0
  851. package/packages/kernel/src/capability-registry.ts +69 -2
  852. package/packages/kernel/src/data-class-registry.ts +354 -0
  853. package/packages/kernel/src/index.ts +85 -4
  854. package/packages/kernel/src/keyring.ts +208 -0
  855. package/packages/kernel/src/module-compatibility.ts +35 -4
  856. package/packages/kernel/src/module-registry.ts +49 -2
  857. package/packages/kernel/src/module-settings.ts +189 -27
  858. package/packages/server/package.json +2 -1
  859. package/packages/server/src/composition.ts +38 -0
  860. package/packages/server/src/endpoint.ts +104 -47
  861. package/packages/server/src/export/csv.ts +34 -0
  862. package/packages/server/src/export/definition.ts +346 -0
  863. package/packages/server/src/export/index.ts +61 -0
  864. package/packages/server/src/export/run.ts +116 -0
  865. package/packages/server/src/index.ts +155 -0
  866. package/packages/server/src/jobs/index.ts +93 -0
  867. package/packages/server/src/jobs/runner.ts +486 -0
  868. package/packages/server/src/log.ts +291 -0
  869. package/packages/server/src/mail/config.ts +184 -0
  870. package/packages/server/src/mail/contracts.ts +251 -0
  871. package/packages/server/src/mail/index.ts +31 -0
  872. package/packages/server/src/mail/port.ts +101 -0
  873. package/packages/server/src/mail/smtp.ts +191 -0
  874. package/packages/server/src/mail/template.ts +105 -0
  875. package/packages/server/src/metrics.ts +447 -0
  876. package/packages/server/src/pagination.ts +246 -0
  877. package/packages/server/src/trace/context.ts +103 -0
  878. package/packages/server/src/trace/egress.ts +66 -0
  879. package/packages/server/src/trace/error-sink.ts +296 -0
  880. package/packages/server/src/trace/exporter.ts +295 -0
  881. package/packages/server/src/trace/index.ts +60 -0
  882. package/packages/server/src/trace/job-sink.ts +172 -0
  883. package/packages/server/src/trace/tracer.ts +338 -0
  884. package/packages/server/src/web.ts +5 -1
  885. package/packages/storage/package.json +40 -0
  886. package/packages/storage/src/config.ts +189 -0
  887. package/packages/storage/src/content-type.ts +124 -0
  888. package/packages/storage/src/contracts.ts +112 -0
  889. package/packages/storage/src/envelope.ts +238 -0
  890. package/packages/storage/src/index.ts +49 -0
  891. package/packages/storage/src/local.ts +75 -0
  892. package/packages/storage/src/port.ts +262 -0
  893. package/packages/storage/src/read-token.ts +130 -0
  894. package/packages/storage/src/reseal.ts +128 -0
  895. package/packages/storage/src/s3.ts +144 -0
  896. package/packages/storage/src/scanner.ts +22 -0
  897. package/packages/storage/src/sigv4.ts +104 -0
  898. package/packages/storage/src/store.ts +13 -0
  899. package/packages/ui/package.json +1 -0
  900. package/packages/ui/src/components/Button.tsrx +3 -0
  901. package/packages/ui/src/components/ConfirmDialog.tsrx +13 -2
  902. package/packages/ui/src/components/DateField.tsrx +92 -0
  903. package/packages/ui/src/components/DatePicker.tsrx +525 -0
  904. package/packages/ui/src/components/DateRangeField.tsrx +93 -0
  905. package/packages/ui/src/components/Drawer.tsrx +10 -1
  906. package/packages/ui/src/components/FileUpload.tsrx +158 -0
  907. package/packages/ui/src/components/Pagination.tsrx +130 -0
  908. package/packages/ui/src/components/Select.tsrx +78 -0
  909. package/packages/ui/src/components/Table.tsrx +498 -81
  910. package/packages/ui/src/components/Tabs.tsrx +53 -0
  911. package/packages/ui/src/components/ToastHost.tsrx +48 -0
  912. package/packages/ui/src/components/calendar.ts +243 -0
  913. package/packages/ui/src/components/date-field.ts +62 -0
  914. package/packages/ui/src/components/file-upload.ts +49 -0
  915. package/packages/ui/src/components/focus-trap.ts +62 -0
  916. package/packages/ui/src/components/pagination.ts +68 -0
  917. package/packages/ui/src/components/table-sorting.ts +49 -0
  918. package/packages/ui/src/components/tabs.ts +61 -0
  919. package/packages/ui/src/components/toast-store.ts +116 -0
  920. package/packages/ui/src/icons/Icon.tsrx +5 -0
  921. package/packages/ui/src/index.ts +45 -0
  922. package/packages/ui/src/styles/components.css +598 -0
  923. package/modules/automations-workflows-integration/module.json +0 -27
  924. package/modules/automations-workflows-integration/spec/module.yaml +0 -93
  925. package/modules/automations-workflows-integration/src/index.ts +0 -14
  926. package/modules/automations-workflows-integration/src/platform.ts +0 -24
  927. package/modules/automations-workflows-integration/src/server/index.ts +0 -11
  928. package/modules/automations-workflows-integration/translations/en.json +0 -21
  929. package/modules/automations-workflows-integration/translations/pl.json +0 -21
  930. package/modules/workflows/src/services/cursor-codec.ts +0 -62
@@ -0,0 +1,1946 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import type {
3
+ DatabaseHandle,
4
+ DatabaseParameter,
5
+ DatabaseStatement,
6
+ DatabaseTransaction,
7
+ } from '@flowdular/sdk/database';
8
+ import { integer, runDatabaseMigrations } from '@flowdular/sdk/database';
9
+ import {
10
+ AUDIT_SEALED_MARKER,
11
+ type AuditChainAnchor,
12
+ type AuditDataClass,
13
+ type AuditErasureRun,
14
+ type AuditEvent,
15
+ type AuditExportRun,
16
+ type AuditLegalHold,
17
+ type AuditSubjectKey,
18
+ type AuditSubjectType,
19
+ type AuditSweepRun,
20
+ type ErasureClassOutcome,
21
+ type ErasureRunStatus,
22
+ type ExportRunSummary,
23
+ type ExportStatus,
24
+ type HoldScopeKind,
25
+ type HoldStatus,
26
+ type RetentionMode,
27
+ type SweepStatus,
28
+ } from '../domain/types.ts';
29
+ import { databaseMigrations } from './migration.ts';
30
+ import { AuditServiceError } from './service-error.ts';
31
+ import type {
32
+ AnchorKeyCount,
33
+ AnchorRouting,
34
+ AppendAuditEventInput,
35
+ ClaimErasureRunInput,
36
+ ErasureRunRouting,
37
+ FinishErasureRunInput,
38
+ StartErasureRunInput,
39
+ AppendSweepRunInput,
40
+ AuditRepository,
41
+ ClaimExportRunInput,
42
+ DataClassFacts,
43
+ DataClassRouting,
44
+ ExportRunRouting,
45
+ FinishExportRunInput,
46
+ InsertAnchorInput,
47
+ InsertHoldInput,
48
+ LiftHoldRecordInput,
49
+ StartExportRunInput,
50
+ StoredAuditEvent,
51
+ SubjectKeyMaterial,
52
+ } from './repository.ts';
53
+ import {
54
+ decodeSubjectKey,
55
+ encodeSubjectKey,
56
+ erasureSubjectMarker,
57
+ generateSubjectKey,
58
+ openSubjectPayload,
59
+ sealSubjectPayload,
60
+ subjectSealContext,
61
+ } from './subject-keys.ts';
62
+
63
+ export interface AuditDatabaseHandles {
64
+ /** Tenant-scoped runtime lease; every statement carries a tenant id. */
65
+ readonly runtime: DatabaseHandle;
66
+ /** Cross-tenant read-only lease, routing columns only. */
67
+ readonly background: DatabaseHandle;
68
+ }
69
+
70
+ interface DataClassRow {
71
+ id: string;
72
+ tenant_id: string;
73
+ class_id: string;
74
+ module_id: string;
75
+ label: string;
76
+ exportable: number | bigint | string;
77
+ sweepable: number | bigint | string;
78
+ default_retention_days: number | bigint | string | null;
79
+ retention_mode: RetentionMode;
80
+ retention_days: number | bigint | string | null;
81
+ last_swept_at: number | bigint | string | null;
82
+ created_at: number | bigint | string;
83
+ updated_at: number | bigint | string;
84
+ }
85
+
86
+ interface SweepRunRow {
87
+ id: string;
88
+ tenant_id: string;
89
+ class_id: string;
90
+ cutoff: number | bigint | string;
91
+ removed: number | bigint | string;
92
+ status: SweepStatus;
93
+ reason: string | null;
94
+ held_back: number | bigint | string | null;
95
+ occurred_at: number | bigint | string;
96
+ }
97
+
98
+ interface AnchorRow {
99
+ id: string;
100
+ tenant_id: string;
101
+ anchor_sequence: number | bigint | string;
102
+ from_sequence: number | bigint | string;
103
+ to_sequence: number | bigint | string;
104
+ row_count: number | bigint | string;
105
+ first_occurred_at: number | bigint | string;
106
+ last_occurred_at: number | bigint | string;
107
+ segment_hash: string;
108
+ previous_anchor_hash: string | null;
109
+ anchor_hash: string;
110
+ signature: string;
111
+ key_id: string;
112
+ segment_file: string;
113
+ sealed_by: string;
114
+ sealed_at: number | bigint | string;
115
+ }
116
+
117
+ interface LegalHoldRow {
118
+ id: string;
119
+ tenant_id: string;
120
+ scope_kind: HoldScopeKind;
121
+ account_id: string | null;
122
+ class_id: string | null;
123
+ from_at: number | bigint | string | null;
124
+ to_at: number | bigint | string | null;
125
+ reason: string;
126
+ status: HoldStatus;
127
+ placed_by: string;
128
+ placed_at: number | bigint | string;
129
+ lifted_by: string | null;
130
+ lift_reason: string | null;
131
+ lifted_at: number | bigint | string | null;
132
+ }
133
+
134
+ interface SubjectKeyRow {
135
+ id: string;
136
+ tenant_id: string;
137
+ subject: string | null;
138
+ subject_marker: string | null;
139
+ material: string | null;
140
+ sealed_at: number | bigint | string;
141
+ destroyed_at: number | bigint | string | null;
142
+ }
143
+
144
+ const SUBJECT_KEY_COLUMNS = `id, tenant_id, subject, subject_marker, material,
145
+ sealed_at, destroyed_at`;
146
+
147
+ interface ErasureRunRow {
148
+ id: string;
149
+ tenant_id: string;
150
+ subject: string | null;
151
+ subject_marker: string;
152
+ status: ErasureRunStatus;
153
+ dry_run: number | bigint | string;
154
+ destroy_key: number | bigint | string;
155
+ requested_by: string;
156
+ output_directory: string | null;
157
+ workspace_slug: string | null;
158
+ workspace_name: string | null;
159
+ classes: number | bigint | string;
160
+ row_count: number | bigint | string;
161
+ certificate_path: string | null;
162
+ outcome_json: string | null;
163
+ reason: string | null;
164
+ started_at: number | bigint | string;
165
+ completed_at: number | bigint | string | null;
166
+ }
167
+
168
+ const ERASURE_RUN_COLUMNS = `id, tenant_id, subject, subject_marker, status,
169
+ dry_run, destroy_key, requested_by, output_directory, workspace_slug,
170
+ workspace_name, classes, row_count, certificate_path, outcome_json, reason,
171
+ started_at, completed_at`;
172
+
173
+ interface ExportRunRow {
174
+ id: string;
175
+ tenant_id: string;
176
+ format_version: string;
177
+ status: ExportStatus;
178
+ dry_run: number | bigint | string;
179
+ classes: number | bigint | string;
180
+ row_count: number | bigint | string;
181
+ archive_digest: string | null;
182
+ requested_by: string;
183
+ reason: string | null;
184
+ output_directory: string | null;
185
+ archive_path: string | null;
186
+ workspace_slug: string | null;
187
+ workspace_name: string | null;
188
+ summary_json: string | null;
189
+ started_at: number | bigint | string;
190
+ completed_at: number | bigint | string | null;
191
+ }
192
+
193
+ interface AuditEventRow {
194
+ id: string;
195
+ tenant_id: string;
196
+ sequence: number | bigint | string;
197
+ actor_id: string;
198
+ action: string;
199
+ subject_type: AuditSubjectType;
200
+ subject_id: string;
201
+ metadata_json: string;
202
+ occurred_at: number | bigint | string;
203
+ previous_hash: string | null;
204
+ event_hash: string;
205
+ subject_key_id: string | null;
206
+ sealed_payload: string | null;
207
+ seal_format: string | null;
208
+ }
209
+
210
+ /**
211
+ * The event format this build writes. A stored row that carries it was written
212
+ * by a build that seals a person's fields; a row without it predates the marker
213
+ * and verify reports it as plaintext.
214
+ */
215
+ export const AUDIT_EVENT_FORMAT = 'audit-event/1';
216
+
217
+ const ANCHOR_COLUMNS = `id, tenant_id, anchor_sequence, from_sequence, to_sequence,
218
+ row_count, first_occurred_at, last_occurred_at, segment_hash,
219
+ previous_anchor_hash, anchor_hash, signature, key_id, segment_file,
220
+ sealed_by, sealed_at`;
221
+
222
+ const HOLD_COLUMNS = `id, tenant_id, scope_kind, account_id, class_id, from_at,
223
+ to_at, reason, status, placed_by, placed_at, lifted_by, lift_reason,
224
+ lifted_at`;
225
+
226
+ const DATA_CLASS_COLUMNS = `id, tenant_id, class_id, module_id, label, exportable,
227
+ sweepable, default_retention_days, retention_mode, retention_days,
228
+ last_swept_at, created_at, updated_at`;
229
+
230
+ /* Queries stay explicit. Values always travel in the adapter's parameter
231
+ channel; nothing from a request is concatenated into SQL. */
232
+ const SQL = {
233
+ listDataClasses: `SELECT ${DATA_CLASS_COLUMNS} FROM audit_data_classes
234
+ WHERE tenant_id = $1 ORDER BY module_id, class_id`,
235
+ getDataClass: `SELECT ${DATA_CLASS_COLUMNS} FROM audit_data_classes
236
+ WHERE tenant_id = $1 AND class_id = $2`,
237
+ setRetention: `UPDATE audit_data_classes
238
+ SET retention_mode = $3, retention_days = $4, updated_at = $5
239
+ WHERE tenant_id = $1 AND class_id = $2
240
+ RETURNING ${DATA_CLASS_COLUMNS}`,
241
+ stampSwept: `UPDATE audit_data_classes SET last_swept_at = $3, updated_at = $3
242
+ WHERE tenant_id = $1 AND class_id = $2`,
243
+ /* Read through the cross-tenant background lease; the class is read again
244
+ under the workspace this returned before a single row is removed. */
245
+ listDueDataClasses: `SELECT tenant_id, class_id, last_swept_at
246
+ FROM audit_data_classes
247
+ WHERE sweepable = 1
248
+ AND (last_swept_at IS NULL OR last_swept_at <= $1)
249
+ AND ((retention_mode = 'days' AND retention_days IS NOT NULL)
250
+ OR (retention_mode = 'default' AND default_retention_days IS NOT NULL))
251
+ ORDER BY last_swept_at NULLS FIRST, tenant_id, class_id
252
+ LIMIT $2`,
253
+
254
+ insertSweepRun: `INSERT INTO audit_sweep_runs
255
+ (id, tenant_id, class_id, cutoff, removed, status, reason, held_back,
256
+ occurred_at)
257
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
258
+ listSweepRuns: `SELECT * FROM audit_sweep_runs
259
+ WHERE tenant_id = $1 AND ($2::text IS NULL OR status = $2)
260
+ ORDER BY occurred_at DESC, id
261
+ LIMIT $3`,
262
+ latestSweepRun: `SELECT * FROM audit_sweep_runs
263
+ WHERE tenant_id = $1 AND class_id = $2
264
+ ORDER BY occurred_at DESC, id
265
+ LIMIT 1`,
266
+
267
+ insertExportRun: `INSERT INTO audit_export_runs
268
+ (id, tenant_id, format_version, status, classes, row_count, archive_digest,
269
+ requested_by, started_at, completed_at, dry_run, output_directory,
270
+ workspace_slug, workspace_name)
271
+ VALUES ($1, $2, $3, 'started', 0, 0, NULL, $4, $5, NULL, $6, $7, $8, $9)`,
272
+ finishExportRun: `UPDATE audit_export_runs
273
+ SET status = $3, classes = $4, row_count = $5, archive_digest = $6,
274
+ archive_path = $7, reason = $8, summary_json = $9, completed_at = $10
275
+ WHERE tenant_id = $1 AND id = $2 AND status = 'started'
276
+ RETURNING *`,
277
+ getExportRun: `SELECT * FROM audit_export_runs
278
+ WHERE tenant_id = $1 AND id = $2`,
279
+ /* Read through the cross-tenant background lease; the run is claimed and
280
+ performed under the workspace this returned. */
281
+ listPendingExportRuns: `SELECT tenant_id, id, started_at
282
+ FROM audit_export_runs
283
+ WHERE status = 'started'
284
+ ORDER BY started_at, tenant_id, id
285
+ LIMIT $1`,
286
+ /* One statement, so two platform processes cannot both take one run. A
287
+ claim older than the lease belonged to a process that is gone. */
288
+ claimExportRun: `UPDATE audit_export_runs SET claimed_at = $3
289
+ WHERE tenant_id = $1 AND id = $2 AND status = 'started'
290
+ AND (claimed_at IS NULL OR claimed_at <= $4)
291
+ RETURNING *`,
292
+ /* Renewed by the loop that holds the claim. The status guard keeps a run
293
+ that already settled from looking claimed again, and the claim guard keeps
294
+ a pass whose lease lapsed from renewing over the loop that reclaimed it. */
295
+ heartbeatExportRun: `UPDATE audit_export_runs SET claimed_at = $3
296
+ WHERE tenant_id = $1 AND id = $2 AND claimed_at = $4
297
+ AND status = 'started'`,
298
+ listExportRuns: `SELECT * FROM audit_export_runs
299
+ WHERE tenant_id = $1 AND ($2::text IS NULL OR status = $2)
300
+ ORDER BY started_at DESC, id
301
+ LIMIT $3`,
302
+
303
+ /* Two writers appending to one workspace would both read the same newest
304
+ sequence and the second insert would be refused by the unique constraint,
305
+ so the chain never forks but the loser loses its event. The lock is taken
306
+ on the workspace alone, is held for the transaction, and is released with
307
+ it however the transaction ends. */
308
+ lockTenantChain: `SELECT pg_advisory_xact_lock(hashtext($1))`,
309
+ latestAuditEvent: `SELECT sequence, event_hash FROM audit_events
310
+ WHERE tenant_id = $1 ORDER BY sequence DESC LIMIT 1`,
311
+ insertAuditEvent: `INSERT INTO audit_events
312
+ (id, tenant_id, sequence, actor_id, action, subject_type, subject_id,
313
+ metadata_json, occurred_at, previous_hash, event_hash, subject_key_id,
314
+ sealed_payload, seal_format)
315
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`,
316
+ listAuditEvents: `SELECT * FROM audit_events
317
+ WHERE tenant_id = $1 ORDER BY sequence DESC LIMIT $2`,
318
+
319
+ /* Keyset walks for the export. Ordering by the primary key is what makes a
320
+ page boundary stable while rows are still being written behind it. */
321
+ exportAuditEvents: `SELECT * FROM audit_events
322
+ WHERE tenant_id = $1 AND id > $2 ORDER BY id LIMIT $3`,
323
+ exportSweepRuns: `SELECT * FROM audit_sweep_runs
324
+ WHERE tenant_id = $1 AND id > $2 ORDER BY id LIMIT $3`,
325
+ exportExportRuns: `SELECT * FROM audit_export_runs
326
+ WHERE tenant_id = $1 AND id > $2 ORDER BY id LIMIT $3`,
327
+
328
+ /* The sealing walk. Ordering by the sequence the chain already carries is
329
+ what makes a segment a range rather than a second ordering to maintain. */
330
+ sealAuditEvents: `SELECT * FROM audit_events
331
+ WHERE tenant_id = $1 AND sequence > $2 ORDER BY sequence LIMIT $3`,
332
+ countAuditEventsBefore: `SELECT count(*) AS total FROM audit_events
333
+ WHERE tenant_id = $1 AND occurred_at < $2
334
+ AND ($3::bigint IS NULL OR sequence <= $3)`,
335
+ /* Bounded by the sealed sequence, so retention can never remove a link no
336
+ segment file holds. The inner select is what applies the batch limit. */
337
+ deleteAuditEventsBefore: `DELETE FROM audit_events WHERE id IN (
338
+ SELECT id FROM audit_events
339
+ WHERE tenant_id = $1 AND occurred_at < $2 AND sequence <= $3
340
+ ORDER BY sequence LIMIT $4)`,
341
+ deleteSweepRunsBefore: `DELETE FROM audit_sweep_runs WHERE id IN (
342
+ SELECT id FROM audit_sweep_runs
343
+ WHERE tenant_id = $1 AND occurred_at < $2
344
+ ORDER BY occurred_at LIMIT $3)`,
345
+ /* A run the platform has not answered is left alone whatever its age: the
346
+ command that recorded it is still polling the row. */
347
+ deleteExportRunsBefore: `DELETE FROM audit_export_runs WHERE id IN (
348
+ SELECT id FROM audit_export_runs
349
+ WHERE tenant_id = $1 AND started_at < $2 AND status <> 'started'
350
+ ORDER BY started_at LIMIT $3)`,
351
+
352
+ insertAnchor: `INSERT INTO audit_anchors (${ANCHOR_COLUMNS})
353
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`,
354
+ latestAnchor: `SELECT ${ANCHOR_COLUMNS} FROM audit_anchors
355
+ WHERE tenant_id = $1 ORDER BY anchor_sequence DESC LIMIT 1`,
356
+ listAnchors: `SELECT ${ANCHOR_COLUMNS} FROM audit_anchors
357
+ WHERE tenant_id = $1 ORDER BY anchor_sequence DESC LIMIT $2`,
358
+ getAnchor: `SELECT ${ANCHOR_COLUMNS} FROM audit_anchors
359
+ WHERE tenant_id = $1 AND id = $2`,
360
+ /* The stored signature is the optimistic check: an anchor another process
361
+ re-signed in between keeps its own row instead of being written twice. */
362
+ resignAnchor: `UPDATE audit_anchors SET signature = $3, key_id = $4
363
+ WHERE tenant_id = $1 AND id = $2 AND signature = $5`,
364
+ countAnchorsByKey: `SELECT key_id, count(*) AS anchors FROM audit_anchors
365
+ GROUP BY key_id ORDER BY key_id`,
366
+ /* Paged by primary key: an anchor the optimistic update skipped stays
367
+ stale, so a query that only asked for stale rows would return it for ever. */
368
+ anchorsNotOnKey: `SELECT tenant_id, id, key_id FROM audit_anchors
369
+ WHERE key_id <> $1 AND id > $2 ORDER BY id LIMIT $3`,
370
+
371
+ insertHold: `INSERT INTO audit_legal_holds (${HOLD_COLUMNS})
372
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'active', $9, $10, NULL, NULL, NULL)`,
373
+ liftHold: `UPDATE audit_legal_holds
374
+ SET status = 'lifted', lifted_by = $3, lift_reason = $4, lifted_at = $5
375
+ WHERE tenant_id = $1 AND id = $2 AND status = 'active'
376
+ RETURNING ${HOLD_COLUMNS}`,
377
+ getHold: `SELECT ${HOLD_COLUMNS} FROM audit_legal_holds
378
+ WHERE tenant_id = $1 AND id = $2`,
379
+ listHolds: `SELECT ${HOLD_COLUMNS} FROM audit_legal_holds
380
+ WHERE tenant_id = $1 AND ($2::text IS NULL OR status = $2)
381
+ ORDER BY placed_at DESC, id
382
+ LIMIT $3`,
383
+ listActiveHolds: `SELECT ${HOLD_COLUMNS} FROM audit_legal_holds
384
+ WHERE tenant_id = $1 AND status = 'active'
385
+ ORDER BY placed_at DESC, id
386
+ LIMIT $2`,
387
+
388
+ getSubjectKey: `SELECT ${SUBJECT_KEY_COLUMNS}
389
+ FROM audit_subject_keys WHERE tenant_id = $1 AND subject = $2`,
390
+ /* The marker outlives the account, so this is the only way to find a key
391
+ that was destroyed, and the only way to know a subject once had one. */
392
+ getSubjectKeyByMarker: `SELECT ${SUBJECT_KEY_COLUMNS}
393
+ FROM audit_subject_keys WHERE tenant_id = $1 AND subject_marker = $2`,
394
+ insertSubjectKey: `INSERT INTO audit_subject_keys
395
+ (id, tenant_id, subject, subject_marker, material, sealed_at, destroyed_at)
396
+ VALUES ($1, $2, $3, $4, $5, $6, NULL)
397
+ ON CONFLICT DO NOTHING`,
398
+ /* The material and the account go together: what is left names nobody and
399
+ opens nothing, and the events that point at it keep a target. The marker
400
+ stays, so the tombstone is still findable and no second key is ever made
401
+ for the subject it belonged to. */
402
+ destroySubjectKey: `UPDATE audit_subject_keys
403
+ SET subject = NULL, material = NULL, destroyed_at = $3
404
+ WHERE tenant_id = $1 AND subject = $2
405
+ RETURNING ${SUBJECT_KEY_COLUMNS}`,
406
+ listSubjectKeys: `SELECT ${SUBJECT_KEY_COLUMNS}
407
+ FROM audit_subject_keys WHERE tenant_id = $1
408
+ ORDER BY sealed_at DESC, id LIMIT $2`,
409
+
410
+ insertErasureRun: `INSERT INTO audit_erasure_runs
411
+ (id, tenant_id, subject, subject_marker, status, dry_run, destroy_key,
412
+ requested_by, output_directory, workspace_slug, workspace_name, classes,
413
+ row_count, certificate_path, outcome_json, reason, claimed_at, started_at,
414
+ completed_at)
415
+ VALUES ($1, $2, $3, $4, 'requested', $5, $6, $7, $8, $9, $10, 0, 0, NULL,
416
+ NULL, NULL, NULL, $11, NULL)`,
417
+ /* The subject is blanked in the same statement that finishes the run: a
418
+ history of erasures must not be a list of the people who asked. */
419
+ finishErasureRun: `UPDATE audit_erasure_runs
420
+ SET status = $3, classes = $4, row_count = $5, certificate_path = $6,
421
+ outcome_json = $7, reason = $8, completed_at = $9, subject = NULL
422
+ WHERE tenant_id = $1 AND id = $2 AND status = 'requested'
423
+ RETURNING ${ERASURE_RUN_COLUMNS}`,
424
+ getErasureRun: `SELECT ${ERASURE_RUN_COLUMNS} FROM audit_erasure_runs
425
+ WHERE tenant_id = $1 AND id = $2`,
426
+ listPendingErasureRuns: `SELECT tenant_id, id, started_at
427
+ FROM audit_erasure_runs
428
+ WHERE status = 'requested'
429
+ ORDER BY started_at, tenant_id, id
430
+ LIMIT $1`,
431
+ claimErasureRun: `UPDATE audit_erasure_runs SET claimed_at = $3
432
+ WHERE tenant_id = $1 AND id = $2 AND status = 'requested'
433
+ AND (claimed_at IS NULL OR claimed_at <= $4)
434
+ RETURNING ${ERASURE_RUN_COLUMNS}`,
435
+ heartbeatErasureRun: `UPDATE audit_erasure_runs SET claimed_at = $3
436
+ WHERE tenant_id = $1 AND id = $2 AND claimed_at = $4
437
+ AND status = 'requested'`,
438
+ listErasureRuns: `SELECT ${ERASURE_RUN_COLUMNS} FROM audit_erasure_runs
439
+ WHERE tenant_id = $1 ORDER BY started_at DESC, id LIMIT $2`,
440
+ exportLegalHolds: `SELECT ${HOLD_COLUMNS} FROM audit_legal_holds
441
+ WHERE tenant_id = $1 AND id > $2 ORDER BY id LIMIT $3`,
442
+ exportErasureRuns: `SELECT ${ERASURE_RUN_COLUMNS} FROM audit_erasure_runs
443
+ WHERE tenant_id = $1 AND id > $2 ORDER BY id LIMIT $3`,
444
+ countHoldsForAccount: `SELECT COUNT(*) AS total FROM audit_legal_holds
445
+ WHERE tenant_id = $1 AND account_id = $2`,
446
+ } as const;
447
+
448
+ /** Classes one upsert statement carries, so a statement stays a bounded size. */
449
+ const DATA_CLASS_UPSERT_CHUNK = 100;
450
+
451
+ /* The facts come from the registry, the period and the sweep stamp from the
452
+ workspace, so a refresh overwrites the first group and never the second. One
453
+ statement per chunk rather than one per class: a deployment declaring a
454
+ hundred classes paid a hundred round trips on every registry read. */
455
+ function upsertDataClasses(count: number): string {
456
+ const rows: string[] = [];
457
+ for (let index = 0; index < count; index += 1) {
458
+ const base = index * 9;
459
+ rows.push(
460
+ `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5},` +
461
+ ` $${base + 6}, $${base + 7}, $${base + 8}, 'default', NULL, NULL,` +
462
+ ` $${base + 9}, $${base + 9})`,
463
+ );
464
+ }
465
+ return `INSERT INTO audit_data_classes (${DATA_CLASS_COLUMNS})
466
+ VALUES ${rows.join(', ')}
467
+ ON CONFLICT (tenant_id, class_id) DO UPDATE SET
468
+ module_id = EXCLUDED.module_id,
469
+ label = EXCLUDED.label,
470
+ exportable = EXCLUDED.exportable,
471
+ sweepable = EXCLUDED.sweepable,
472
+ default_retention_days = EXCLUDED.default_retention_days,
473
+ updated_at = EXCLUDED.updated_at
474
+ WHERE audit_data_classes.label IS DISTINCT FROM EXCLUDED.label
475
+ OR audit_data_classes.module_id IS DISTINCT FROM EXCLUDED.module_id
476
+ OR audit_data_classes.exportable IS DISTINCT FROM EXCLUDED.exportable
477
+ OR audit_data_classes.sweepable IS DISTINCT FROM EXCLUDED.sweepable
478
+ OR audit_data_classes.default_retention_days
479
+ IS DISTINCT FROM EXCLUDED.default_retention_days`;
480
+ }
481
+
482
+ /**
483
+ * Whether the workspace already holds every declared class with the facts the
484
+ * registry declares now. O(classes) over one page the caller has read anyway,
485
+ * and it is what keeps a registry read a read: only a composition change or a
486
+ * workspace that has never been materialised opens the write.
487
+ */
488
+ function dataClassesCarry(
489
+ stored: readonly AuditDataClass[],
490
+ facts: readonly DataClassFacts[],
491
+ ): boolean {
492
+ if (stored.length < facts.length) return false;
493
+ const byClassId = new Map(stored.map((entry) => [entry.classId, entry]));
494
+ return facts.every((fact) => {
495
+ const record = byClassId.get(fact.classId);
496
+ return (
497
+ record !== undefined &&
498
+ record.moduleId === fact.moduleId &&
499
+ record.label === fact.label &&
500
+ record.exportable === fact.exportable &&
501
+ record.sweepable === fact.sweepable &&
502
+ record.defaultRetentionDays === fact.defaultRetentionDays
503
+ );
504
+ });
505
+ }
506
+
507
+ /* One page of events names at most this many distinct subject keys, so the
508
+ lookup that opens them is one statement with bound placeholders. */
509
+ function subjectKeysByIds(count: number): string {
510
+ const placeholders = Array.from(
511
+ { length: count },
512
+ (_, index) => `$${index + 2}`,
513
+ ).join(', ');
514
+ return `SELECT id, material FROM audit_subject_keys
515
+ WHERE tenant_id = $1 AND id IN (${placeholders})`;
516
+ }
517
+
518
+ function optionalInteger(
519
+ value: number | bigint | string | null,
520
+ field: string,
521
+ ): number | null {
522
+ return value === null ? null : integer(value, field);
523
+ }
524
+
525
+ function effectiveRetentionDays(
526
+ mode: RetentionMode,
527
+ workspaceDays: number | null,
528
+ defaultDays: number | null,
529
+ ): number | null {
530
+ if (mode === 'none') return null;
531
+ return mode === 'days' ? workspaceDays : defaultDays;
532
+ }
533
+
534
+ function dataClassFromRow(row: DataClassRow): AuditDataClass {
535
+ const defaultRetentionDays = optionalInteger(
536
+ row.default_retention_days,
537
+ 'retention period',
538
+ );
539
+ const retentionDays = optionalInteger(row.retention_days, 'retention period');
540
+ return {
541
+ id: row.id,
542
+ tenantId: row.tenant_id,
543
+ classId: row.class_id,
544
+ moduleId: row.module_id,
545
+ label: row.label,
546
+ exportable: integer(row.exportable, 'exportable flag') === 1,
547
+ sweepable: integer(row.sweepable, 'sweepable flag') === 1,
548
+ defaultRetentionDays,
549
+ retentionMode: row.retention_mode,
550
+ retentionDays,
551
+ effectiveRetentionDays: effectiveRetentionDays(
552
+ row.retention_mode,
553
+ retentionDays,
554
+ defaultRetentionDays,
555
+ ),
556
+ lastSweptAt: optionalInteger(row.last_swept_at, 'timestamp'),
557
+ createdAt: integer(row.created_at, 'timestamp'),
558
+ updatedAt: integer(row.updated_at, 'timestamp'),
559
+ };
560
+ }
561
+
562
+ function sweepRunFromRow(row: SweepRunRow): AuditSweepRun {
563
+ return {
564
+ id: row.id,
565
+ tenantId: row.tenant_id,
566
+ classId: row.class_id,
567
+ cutoff: integer(row.cutoff, 'timestamp'),
568
+ removed: integer(row.removed, 'row count'),
569
+ status: row.status,
570
+ reason: row.reason,
571
+ heldBack: optionalInteger(row.held_back, 'row count'),
572
+ occurredAt: integer(row.occurred_at, 'timestamp'),
573
+ };
574
+ }
575
+
576
+ function anchorFromRow(row: AnchorRow): AuditChainAnchor {
577
+ return {
578
+ id: row.id,
579
+ tenantId: row.tenant_id,
580
+ anchorSequence: integer(row.anchor_sequence, 'sequence'),
581
+ fromSequence: integer(row.from_sequence, 'sequence'),
582
+ toSequence: integer(row.to_sequence, 'sequence'),
583
+ rowCount: integer(row.row_count, 'row count'),
584
+ firstOccurredAt: integer(row.first_occurred_at, 'timestamp'),
585
+ lastOccurredAt: integer(row.last_occurred_at, 'timestamp'),
586
+ segmentHash: row.segment_hash,
587
+ previousAnchorHash: row.previous_anchor_hash,
588
+ anchorHash: row.anchor_hash,
589
+ signature: row.signature,
590
+ keyId: row.key_id,
591
+ segmentFile: row.segment_file,
592
+ sealedBy: row.sealed_by,
593
+ sealedAt: integer(row.sealed_at, 'timestamp'),
594
+ };
595
+ }
596
+
597
+ function holdFromRow(row: LegalHoldRow): AuditLegalHold {
598
+ return {
599
+ id: row.id,
600
+ tenantId: row.tenant_id,
601
+ scopeKind: row.scope_kind,
602
+ accountId: row.account_id,
603
+ classId: row.class_id,
604
+ fromAt: optionalInteger(row.from_at, 'timestamp'),
605
+ toAt: optionalInteger(row.to_at, 'timestamp'),
606
+ reason: row.reason,
607
+ status: row.status,
608
+ placedBy: row.placed_by,
609
+ placedAt: integer(row.placed_at, 'timestamp'),
610
+ liftedBy: row.lifted_by,
611
+ liftReason: row.lift_reason,
612
+ liftedAt: optionalInteger(row.lifted_at, 'timestamp'),
613
+ };
614
+ }
615
+
616
+ function erasureRunFromRow(row: ErasureRunRow): AuditErasureRun {
617
+ return {
618
+ id: row.id,
619
+ tenantId: row.tenant_id,
620
+ subject: row.subject,
621
+ subjectMarker: row.subject_marker,
622
+ status: row.status,
623
+ dryRun: integer(row.dry_run, 'dry run flag') === 1,
624
+ destroyKey: integer(row.destroy_key, 'destroy key flag') === 1,
625
+ requestedBy: row.requested_by,
626
+ outputDirectory: row.output_directory,
627
+ workspaceSlug: row.workspace_slug,
628
+ workspaceName: row.workspace_name,
629
+ classes: integer(row.classes, 'class count'),
630
+ rows: integer(row.row_count, 'row count'),
631
+ certificatePath: row.certificate_path,
632
+ outcome: parsedOutcome(row.outcome_json),
633
+ reason: row.reason,
634
+ startedAt: integer(row.started_at, 'timestamp'),
635
+ completedAt: optionalInteger(row.completed_at, 'timestamp'),
636
+ };
637
+ }
638
+
639
+ /* A row edited outside the module carries no readable outcome; the certificate
640
+ is what proves what was removed, not this view of it. */
641
+ function parsedOutcome(
642
+ value: string | null,
643
+ ): readonly ErasureClassOutcome[] | null {
644
+ if (value === null) return null;
645
+ try {
646
+ const parsed = JSON.parse(value) as ErasureClassOutcome[];
647
+ return Array.isArray(parsed) ? parsed : null;
648
+ } catch {
649
+ return null;
650
+ }
651
+ }
652
+
653
+ function subjectKeyFromRow(row: SubjectKeyRow): AuditSubjectKey {
654
+ return {
655
+ id: row.id,
656
+ tenantId: row.tenant_id,
657
+ subject: row.subject,
658
+ subjectMarker: row.subject_marker,
659
+ state: row.destroyed_at === null ? 'active' : 'destroyed',
660
+ sealedAt: integer(row.sealed_at, 'timestamp'),
661
+ destroyedAt: optionalInteger(row.destroyed_at, 'timestamp'),
662
+ };
663
+ }
664
+
665
+ function exportRunFromRow(row: ExportRunRow): AuditExportRun {
666
+ return {
667
+ id: row.id,
668
+ tenantId: row.tenant_id,
669
+ formatVersion: row.format_version,
670
+ status: row.status,
671
+ dryRun: integer(row.dry_run, 'dry run flag') === 1,
672
+ classes: integer(row.classes, 'class count'),
673
+ rows: integer(row.row_count, 'row count'),
674
+ archiveDigest: row.archive_digest,
675
+ requestedBy: row.requested_by,
676
+ reason: row.reason,
677
+ outputDirectory: row.output_directory,
678
+ archivePath: row.archive_path,
679
+ workspaceSlug: row.workspace_slug,
680
+ workspaceName: row.workspace_name,
681
+ summary: parsedSummary(row.summary_json),
682
+ startedAt: integer(row.started_at, 'timestamp'),
683
+ completedAt: optionalInteger(row.completed_at, 'timestamp'),
684
+ };
685
+ }
686
+
687
+ /* A row edited outside the module carries no readable summary; the archive
688
+ digest is what proves what was written, not this view of it. */
689
+ function parsedSummary(value: string | null): ExportRunSummary | null {
690
+ if (value === null) return null;
691
+ try {
692
+ const parsed = JSON.parse(value) as ExportRunSummary;
693
+ return typeof parsed === 'object' &&
694
+ parsed !== null &&
695
+ Array.isArray(parsed.classes) &&
696
+ Array.isArray(parsed.exclusions)
697
+ ? parsed
698
+ : null;
699
+ } catch {
700
+ return null;
701
+ }
702
+ }
703
+
704
+ function storedEventFromRow(row: AuditEventRow): StoredAuditEvent {
705
+ return {
706
+ id: row.id,
707
+ tenantId: row.tenant_id,
708
+ sequence: integer(row.sequence, 'sequence'),
709
+ actorId: row.actor_id,
710
+ action: row.action,
711
+ subjectType: row.subject_type,
712
+ subjectId: row.subject_id,
713
+ metadataJson: row.metadata_json,
714
+ occurredAt: integer(row.occurred_at, 'timestamp'),
715
+ previousHash: row.previous_hash,
716
+ eventHash: row.event_hash,
717
+ subjectKeyId: row.subject_key_id,
718
+ sealedPayload: row.sealed_payload,
719
+ sealFormat: row.seal_format,
720
+ };
721
+ }
722
+
723
+ /**
724
+ * The hash of one stored event. A sealed row keeps the marker in the actor and
725
+ * subject columns and the envelope beside them, and the envelope stands in the
726
+ * place the metadata holds for a plaintext row, so the hash of any row is a
727
+ * function of its stored bytes alone: destroying a subject key never touches
728
+ * the chain.
729
+ */
730
+ export function storedAuditEventHash(event: StoredAuditEvent): string {
731
+ return auditEventHash({
732
+ id: event.id,
733
+ tenantId: event.tenantId,
734
+ sequence: event.sequence,
735
+ actorId: event.actorId,
736
+ action: event.action,
737
+ subjectType: event.subjectType,
738
+ subjectId: event.subjectId,
739
+ metadataJson: event.sealedPayload ?? event.metadataJson,
740
+ occurredAt: event.occurredAt,
741
+ previousHash: event.previousHash,
742
+ sealed: event.sealedPayload !== null,
743
+ subjectKeyId: event.subjectKeyId,
744
+ sealFormat: event.sealFormat,
745
+ });
746
+ }
747
+
748
+ interface SealedEventContent {
749
+ readonly actorId: string;
750
+ readonly subjectId: string;
751
+ readonly metadata: Record<string, unknown>;
752
+ }
753
+
754
+ function auditEventFromRow(
755
+ row: AuditEventRow,
756
+ keys: ReadonlyMap<string, Buffer>,
757
+ ): AuditEvent {
758
+ const sequence = integer(row.sequence, 'sequence');
759
+ const base = {
760
+ id: row.id,
761
+ tenantId: row.tenant_id,
762
+ sequence,
763
+ action: row.action,
764
+ subjectType: row.subject_type,
765
+ occurredAt: integer(row.occurred_at, 'timestamp'),
766
+ previousHash: row.previous_hash,
767
+ eventHash: row.event_hash,
768
+ sealed: row.sealed_payload !== null,
769
+ subjectKeyId: row.subject_key_id,
770
+ };
771
+ if (row.sealed_payload === null) {
772
+ return {
773
+ ...base,
774
+ actorId: row.actor_id,
775
+ subjectId: row.subject_id,
776
+ metadata: parsedMetadata(row.metadata_json),
777
+ readable: true,
778
+ };
779
+ }
780
+ const key = row.subject_key_id ? keys.get(row.subject_key_id) : undefined;
781
+ const opened = key
782
+ ? openSubjectPayload(
783
+ key,
784
+ subjectSealContext(row.tenant_id, row.id),
785
+ row.sealed_payload,
786
+ )
787
+ : null;
788
+ const content = opened === null ? null : parsedSealedContent(opened);
789
+ return {
790
+ ...base,
791
+ actorId: content?.actorId ?? AUDIT_SEALED_MARKER,
792
+ subjectId: content?.subjectId ?? AUDIT_SEALED_MARKER,
793
+ metadata: content?.metadata ?? {},
794
+ readable: content !== null,
795
+ };
796
+ }
797
+
798
+ /* A destroyed key answers nothing and a corrupt envelope answers nothing; both
799
+ are the same to a reader, and the row still proves the event existed. */
800
+ function parsedSealedContent(value: string): SealedEventContent | null {
801
+ try {
802
+ const parsed = JSON.parse(value) as SealedEventContent;
803
+ return typeof parsed === 'object' &&
804
+ parsed !== null &&
805
+ typeof parsed.actorId === 'string' &&
806
+ typeof parsed.subjectId === 'string'
807
+ ? {
808
+ actorId: parsed.actorId,
809
+ subjectId: parsed.subjectId,
810
+ metadata:
811
+ typeof parsed.metadata === 'object' && parsed.metadata !== null
812
+ ? parsed.metadata
813
+ : {},
814
+ }
815
+ : null;
816
+ } catch {
817
+ return null;
818
+ }
819
+ }
820
+
821
+ function parsedMetadata(value: string): Readonly<Record<string, unknown>> {
822
+ try {
823
+ const parsed = JSON.parse(value) as unknown;
824
+ return typeof parsed === 'object' &&
825
+ parsed !== null &&
826
+ !Array.isArray(parsed)
827
+ ? (parsed as Record<string, unknown>)
828
+ : {};
829
+ } catch {
830
+ /* A row edited outside the module carries no readable metadata; the hash
831
+ over the stored text is what proves the chain, not this view of it. */
832
+ return {};
833
+ }
834
+ }
835
+
836
+ /* The hash covers the serialized metadata, not an object, so two events with
837
+ the same fields in a different key order still seal the same bytes. */
838
+ export function stableMetadata(
839
+ metadata: Readonly<Record<string, unknown>>,
840
+ ): string {
841
+ const sorted = Object.keys(metadata).sort();
842
+ return JSON.stringify(
843
+ Object.fromEntries(sorted.map((key) => [key, metadata[key]])),
844
+ );
845
+ }
846
+
847
+ /**
848
+ * Every stored column of the row is in the preimage. The identifier, the key the
849
+ * row points at, the format that wrote it and whether it is sealed are part of
850
+ * it too: without them a row could be repointed at another subject key, or read
851
+ * as plaintext where it was sealed, and still match its own hash.
852
+ */
853
+ export function auditEventHash(input: {
854
+ readonly id: string;
855
+ readonly tenantId: string;
856
+ readonly sequence: number;
857
+ readonly actorId: string;
858
+ readonly action: string;
859
+ readonly subjectType: string;
860
+ readonly subjectId: string;
861
+ readonly metadataJson: string;
862
+ readonly occurredAt: number;
863
+ readonly previousHash: string | null;
864
+ readonly sealed: boolean;
865
+ readonly subjectKeyId: string | null;
866
+ readonly sealFormat: string | null;
867
+ }): string {
868
+ return createHash('sha256')
869
+ .update(
870
+ JSON.stringify([
871
+ input.id,
872
+ input.tenantId,
873
+ input.sequence,
874
+ input.actorId,
875
+ input.action,
876
+ input.subjectType,
877
+ input.subjectId,
878
+ input.metadataJson,
879
+ input.occurredAt,
880
+ input.previousHash,
881
+ input.sealed,
882
+ input.subjectKeyId,
883
+ input.sealFormat,
884
+ ]),
885
+ )
886
+ .digest('hex');
887
+ }
888
+
889
+ export class DatabaseAuditRepository implements AuditRepository {
890
+ constructor(private readonly handles: AuditDatabaseHandles) {}
891
+
892
+ async #read<Row extends object>(
893
+ tenantId: string,
894
+ statement: DatabaseStatement,
895
+ ): Promise<readonly Row[]> {
896
+ const result = await this.handles.runtime.transaction(
897
+ (transaction) => transaction.query<Row>(statement),
898
+ { access: 'read', tenantId },
899
+ );
900
+ return result.rows;
901
+ }
902
+
903
+ async #write(
904
+ tenantId: string,
905
+ statement: DatabaseStatement,
906
+ ): Promise<number> {
907
+ const result = await this.handles.runtime.transaction(
908
+ (transaction) => transaction.execute(statement),
909
+ { access: 'write', tenantId },
910
+ );
911
+ return result.affectedRows;
912
+ }
913
+
914
+ async materializeDataClasses(
915
+ tenantId: string,
916
+ facts: readonly DataClassFacts[],
917
+ now: number,
918
+ ): Promise<readonly AuditDataClass[]> {
919
+ /* The declarations are a property of the composition, so a workspace whose
920
+ rows already carry them has nothing to write and this call is the read
921
+ it answers with. Writing on every read put a write transaction behind
922
+ the registry screen, which a GET must not open. */
923
+ const stored = await this.listDataClasses(tenantId);
924
+ if (dataClassesCarry(stored, facts)) return stored;
925
+ /* One transaction: a workspace reading the registry twice in parallel
926
+ cannot see half of the classes it is about to be told exist. */
927
+ const rows = await this.handles.runtime.transaction(
928
+ async (transaction) => {
929
+ for (
930
+ let offset = 0;
931
+ offset < facts.length;
932
+ offset += DATA_CLASS_UPSERT_CHUNK
933
+ ) {
934
+ const chunk = facts.slice(offset, offset + DATA_CLASS_UPSERT_CHUNK);
935
+ await transaction.execute({
936
+ text: upsertDataClasses(chunk.length),
937
+ parameters: chunk.flatMap((fact) => [
938
+ randomUUID(),
939
+ tenantId,
940
+ fact.classId,
941
+ fact.moduleId,
942
+ fact.label,
943
+ fact.exportable ? 1 : 0,
944
+ fact.sweepable ? 1 : 0,
945
+ fact.defaultRetentionDays,
946
+ now,
947
+ ]),
948
+ });
949
+ }
950
+ return transaction.query<DataClassRow>({
951
+ text: SQL.listDataClasses,
952
+ parameters: [tenantId],
953
+ });
954
+ },
955
+ { access: 'write', tenantId },
956
+ );
957
+ return rows.rows.map(dataClassFromRow);
958
+ }
959
+
960
+ async listDataClasses(tenantId: string): Promise<readonly AuditDataClass[]> {
961
+ const rows = await this.#read<DataClassRow>(tenantId, {
962
+ text: SQL.listDataClasses,
963
+ parameters: [tenantId],
964
+ });
965
+ return rows.map(dataClassFromRow);
966
+ }
967
+
968
+ async getDataClass(
969
+ tenantId: string,
970
+ classId: string,
971
+ ): Promise<AuditDataClass | null> {
972
+ const rows = await this.#read<DataClassRow>(tenantId, {
973
+ text: SQL.getDataClass,
974
+ parameters: [tenantId, classId],
975
+ });
976
+ return rows[0] ? dataClassFromRow(rows[0]) : null;
977
+ }
978
+
979
+ async setRetention(
980
+ tenantId: string,
981
+ classId: string,
982
+ mode: RetentionMode,
983
+ days: number | null,
984
+ now: number,
985
+ ): Promise<AuditDataClass | null> {
986
+ const result = await this.handles.runtime.transaction(
987
+ (transaction) =>
988
+ transaction.query<DataClassRow>({
989
+ text: SQL.setRetention,
990
+ parameters: [tenantId, classId, mode, days, now],
991
+ }),
992
+ { access: 'write', tenantId },
993
+ );
994
+ const row = result.rows[0];
995
+ return row ? dataClassFromRow(row) : null;
996
+ }
997
+
998
+ async stampSwept(
999
+ tenantId: string,
1000
+ classId: string,
1001
+ sweptAt: number,
1002
+ ): Promise<boolean> {
1003
+ const affected = await this.#write(tenantId, {
1004
+ text: SQL.stampSwept,
1005
+ parameters: [tenantId, classId, sweptAt],
1006
+ });
1007
+ return affected === 1;
1008
+ }
1009
+
1010
+ async listDueDataClasses(
1011
+ staleBefore: number,
1012
+ limit: number,
1013
+ ): Promise<readonly DataClassRouting[]> {
1014
+ const result = await this.handles.background.query<{
1015
+ tenant_id: string;
1016
+ class_id: string;
1017
+ last_swept_at: number | bigint | string | null;
1018
+ }>({
1019
+ text: SQL.listDueDataClasses,
1020
+ parameters: [staleBefore, limit],
1021
+ });
1022
+ return result.rows.map((row) => ({
1023
+ tenantId: row.tenant_id,
1024
+ classId: row.class_id,
1025
+ lastSweptAt: optionalInteger(row.last_swept_at, 'timestamp'),
1026
+ }));
1027
+ }
1028
+
1029
+ async appendSweepRun(input: AppendSweepRunInput): Promise<AuditSweepRun> {
1030
+ const record: AuditSweepRun = { ...input, id: randomUUID() };
1031
+ await this.#write(input.tenantId, {
1032
+ text: SQL.insertSweepRun,
1033
+ parameters: [
1034
+ record.id,
1035
+ record.tenantId,
1036
+ record.classId,
1037
+ record.cutoff,
1038
+ record.removed,
1039
+ record.status,
1040
+ record.reason,
1041
+ record.heldBack,
1042
+ record.occurredAt,
1043
+ ],
1044
+ });
1045
+ return record;
1046
+ }
1047
+
1048
+ async listSweepRuns(
1049
+ tenantId: string,
1050
+ status: SweepStatus | undefined,
1051
+ limit: number,
1052
+ ): Promise<readonly AuditSweepRun[]> {
1053
+ const rows = await this.#read<SweepRunRow>(tenantId, {
1054
+ text: SQL.listSweepRuns,
1055
+ parameters: [tenantId, status ?? null, limit],
1056
+ });
1057
+ return rows.map(sweepRunFromRow);
1058
+ }
1059
+
1060
+ async latestSweepRun(
1061
+ tenantId: string,
1062
+ classId: string,
1063
+ ): Promise<AuditSweepRun | null> {
1064
+ const rows = await this.#read<SweepRunRow>(tenantId, {
1065
+ text: SQL.latestSweepRun,
1066
+ parameters: [tenantId, classId],
1067
+ });
1068
+ return rows[0] ? sweepRunFromRow(rows[0]) : null;
1069
+ }
1070
+
1071
+ async startExportRun(input: StartExportRunInput): Promise<AuditExportRun> {
1072
+ const record: AuditExportRun = {
1073
+ id: randomUUID(),
1074
+ tenantId: input.tenantId,
1075
+ formatVersion: input.formatVersion,
1076
+ status: 'started',
1077
+ dryRun: input.dryRun,
1078
+ classes: 0,
1079
+ rows: 0,
1080
+ archiveDigest: null,
1081
+ requestedBy: input.requestedBy,
1082
+ reason: null,
1083
+ outputDirectory: input.outputDirectory,
1084
+ archivePath: null,
1085
+ workspaceSlug: input.workspaceSlug,
1086
+ workspaceName: input.workspaceName,
1087
+ summary: null,
1088
+ startedAt: input.startedAt,
1089
+ completedAt: null,
1090
+ };
1091
+ await this.#write(input.tenantId, {
1092
+ text: SQL.insertExportRun,
1093
+ parameters: [
1094
+ record.id,
1095
+ record.tenantId,
1096
+ record.formatVersion,
1097
+ record.requestedBy,
1098
+ record.startedAt,
1099
+ record.dryRun ? 1 : 0,
1100
+ record.outputDirectory,
1101
+ record.workspaceSlug,
1102
+ record.workspaceName,
1103
+ ],
1104
+ });
1105
+ return record;
1106
+ }
1107
+
1108
+ async finishExportRun(
1109
+ input: FinishExportRunInput,
1110
+ ): Promise<AuditExportRun | null> {
1111
+ const result = await this.handles.runtime.transaction(
1112
+ (transaction) =>
1113
+ transaction.query<ExportRunRow>({
1114
+ text: SQL.finishExportRun,
1115
+ parameters: [
1116
+ input.tenantId,
1117
+ input.id,
1118
+ input.status,
1119
+ input.classes,
1120
+ input.rows,
1121
+ input.archiveDigest,
1122
+ input.archivePath,
1123
+ input.reason,
1124
+ input.summary === null ? null : JSON.stringify(input.summary),
1125
+ input.completedAt,
1126
+ ],
1127
+ }),
1128
+ { access: 'write', tenantId: input.tenantId },
1129
+ );
1130
+ const row = result.rows[0];
1131
+ return row ? exportRunFromRow(row) : null;
1132
+ }
1133
+
1134
+ async getExportRun(
1135
+ tenantId: string,
1136
+ id: string,
1137
+ ): Promise<AuditExportRun | null> {
1138
+ const rows = await this.#read<ExportRunRow>(tenantId, {
1139
+ text: SQL.getExportRun,
1140
+ parameters: [tenantId, id],
1141
+ });
1142
+ return rows[0] ? exportRunFromRow(rows[0]) : null;
1143
+ }
1144
+
1145
+ async listPendingExportRuns(
1146
+ limit: number,
1147
+ ): Promise<readonly ExportRunRouting[]> {
1148
+ const result = await this.handles.background.query<{
1149
+ tenant_id: string;
1150
+ id: string;
1151
+ started_at: number | bigint | string;
1152
+ }>({
1153
+ text: SQL.listPendingExportRuns,
1154
+ parameters: [limit],
1155
+ });
1156
+ return result.rows.map((row) => ({
1157
+ tenantId: row.tenant_id,
1158
+ id: row.id,
1159
+ startedAt: integer(row.started_at, 'timestamp'),
1160
+ }));
1161
+ }
1162
+
1163
+ async claimExportRun(
1164
+ input: ClaimExportRunInput,
1165
+ ): Promise<AuditExportRun | null> {
1166
+ const result = await this.handles.runtime.transaction(
1167
+ (transaction) =>
1168
+ transaction.query<ExportRunRow>({
1169
+ text: SQL.claimExportRun,
1170
+ parameters: [
1171
+ input.tenantId,
1172
+ input.id,
1173
+ input.claimedAt,
1174
+ input.staleBefore,
1175
+ ],
1176
+ }),
1177
+ { access: 'write', tenantId: input.tenantId },
1178
+ );
1179
+ const row = result.rows[0];
1180
+ return row ? exportRunFromRow(row) : null;
1181
+ }
1182
+
1183
+ async heartbeatExportRun(
1184
+ tenantId: string,
1185
+ id: string,
1186
+ at: number,
1187
+ claimedAt: number,
1188
+ ): Promise<boolean> {
1189
+ const result = await this.handles.runtime.transaction(
1190
+ (transaction) =>
1191
+ transaction.execute({
1192
+ text: SQL.heartbeatExportRun,
1193
+ parameters: [tenantId, id, at, claimedAt],
1194
+ }),
1195
+ { access: 'write', tenantId },
1196
+ );
1197
+ return result.affectedRows > 0;
1198
+ }
1199
+
1200
+ async listExportRuns(
1201
+ tenantId: string,
1202
+ status: ExportStatus | undefined,
1203
+ limit: number,
1204
+ ): Promise<readonly AuditExportRun[]> {
1205
+ const rows = await this.#read<ExportRunRow>(tenantId, {
1206
+ text: SQL.listExportRuns,
1207
+ parameters: [tenantId, status ?? null, limit],
1208
+ });
1209
+ return rows.map(exportRunFromRow);
1210
+ }
1211
+
1212
+ /* The previous hash is read and the next event written inside one
1213
+ transaction, under the workspace's own advisory lock, so two writers
1214
+ neither fork the chain nor race for one sequence: the second waits and
1215
+ appends after the first. The subject key is taken in the same
1216
+ transaction, so a row is never written naming a key that does not exist. */
1217
+ async appendAuditEvent(input: AppendAuditEventInput): Promise<AuditEvent> {
1218
+ return this.handles.runtime.transaction(
1219
+ async (transaction) => {
1220
+ await transaction.execute({
1221
+ text: SQL.lockTenantChain,
1222
+ parameters: [input.tenantId],
1223
+ });
1224
+ const latest = await transaction.query<{
1225
+ sequence: number | bigint | string;
1226
+ event_hash: string;
1227
+ }>({
1228
+ text: SQL.latestAuditEvent,
1229
+ parameters: [input.tenantId],
1230
+ });
1231
+ const previous = latest.rows[0];
1232
+ const sequence =
1233
+ (previous ? integer(previous.sequence, 'sequence') : 0) + 1;
1234
+ const previousHash = previous?.event_hash ?? null;
1235
+ const id = randomUUID();
1236
+ const subject = input.subjectAccountId?.trim();
1237
+ const key = subject
1238
+ ? await subjectKeyIn(
1239
+ transaction,
1240
+ input.tenantId,
1241
+ subject,
1242
+ input.occurredAt,
1243
+ )
1244
+ : null;
1245
+ const sealedPayload = key
1246
+ ? sealSubjectPayload(
1247
+ key.key,
1248
+ subjectSealContext(input.tenantId, id),
1249
+ JSON.stringify({
1250
+ actorId: input.actorId,
1251
+ subjectId: input.subjectId,
1252
+ metadata: input.metadata,
1253
+ }),
1254
+ )
1255
+ : null;
1256
+ const metadataJson = key ? '{}' : stableMetadata(input.metadata);
1257
+ const stored: StoredAuditEvent = {
1258
+ id,
1259
+ tenantId: input.tenantId,
1260
+ sequence,
1261
+ actorId: key ? AUDIT_SEALED_MARKER : input.actorId,
1262
+ action: input.action,
1263
+ subjectType: input.subjectType,
1264
+ subjectId: key ? AUDIT_SEALED_MARKER : input.subjectId,
1265
+ metadataJson,
1266
+ occurredAt: input.occurredAt,
1267
+ previousHash,
1268
+ eventHash: '',
1269
+ subjectKeyId: key?.id ?? null,
1270
+ sealedPayload,
1271
+ sealFormat: AUDIT_EVENT_FORMAT,
1272
+ };
1273
+ const eventHash = storedAuditEventHash(stored);
1274
+ await transaction.execute({
1275
+ text: SQL.insertAuditEvent,
1276
+ parameters: [
1277
+ stored.id,
1278
+ stored.tenantId,
1279
+ stored.sequence,
1280
+ stored.actorId,
1281
+ stored.action,
1282
+ stored.subjectType,
1283
+ stored.subjectId,
1284
+ stored.metadataJson,
1285
+ stored.occurredAt,
1286
+ stored.previousHash,
1287
+ eventHash,
1288
+ stored.subjectKeyId,
1289
+ stored.sealedPayload,
1290
+ stored.sealFormat,
1291
+ ] satisfies readonly DatabaseParameter[],
1292
+ });
1293
+ return {
1294
+ id: stored.id,
1295
+ tenantId: stored.tenantId,
1296
+ sequence,
1297
+ actorId: input.actorId,
1298
+ action: input.action,
1299
+ subjectType: input.subjectType,
1300
+ subjectId: input.subjectId,
1301
+ metadata: input.metadata,
1302
+ occurredAt: input.occurredAt,
1303
+ previousHash,
1304
+ eventHash,
1305
+ sealed: sealedPayload !== null,
1306
+ subjectKeyId: stored.subjectKeyId,
1307
+ readable: true,
1308
+ };
1309
+ },
1310
+ { access: 'write', tenantId: input.tenantId },
1311
+ );
1312
+ }
1313
+
1314
+ async exportAuditEventsPage(
1315
+ tenantId: string,
1316
+ afterId: string,
1317
+ limit: number,
1318
+ ): Promise<readonly AuditEvent[]> {
1319
+ return this.#openedEvents(tenantId, {
1320
+ text: SQL.exportAuditEvents,
1321
+ parameters: [tenantId, afterId, limit],
1322
+ });
1323
+ }
1324
+
1325
+ async exportSweepRunsPage(
1326
+ tenantId: string,
1327
+ afterId: string,
1328
+ limit: number,
1329
+ ): Promise<readonly AuditSweepRun[]> {
1330
+ const rows = await this.#read<SweepRunRow>(tenantId, {
1331
+ text: SQL.exportSweepRuns,
1332
+ parameters: [tenantId, afterId, limit],
1333
+ });
1334
+ return rows.map(sweepRunFromRow);
1335
+ }
1336
+
1337
+ async exportExportRunsPage(
1338
+ tenantId: string,
1339
+ afterId: string,
1340
+ limit: number,
1341
+ ): Promise<readonly AuditExportRun[]> {
1342
+ const rows = await this.#read<ExportRunRow>(tenantId, {
1343
+ text: SQL.exportExportRuns,
1344
+ parameters: [tenantId, afterId, limit],
1345
+ });
1346
+ return rows.map(exportRunFromRow);
1347
+ }
1348
+
1349
+ async exportLegalHoldsPage(
1350
+ tenantId: string,
1351
+ afterId: string,
1352
+ limit: number,
1353
+ ): Promise<readonly AuditLegalHold[]> {
1354
+ const rows = await this.#read<LegalHoldRow>(tenantId, {
1355
+ text: SQL.exportLegalHolds,
1356
+ parameters: [tenantId, afterId, limit],
1357
+ });
1358
+ return rows.map(holdFromRow);
1359
+ }
1360
+
1361
+ async exportErasureRunsPage(
1362
+ tenantId: string,
1363
+ afterId: string,
1364
+ limit: number,
1365
+ ): Promise<readonly AuditErasureRun[]> {
1366
+ const rows = await this.#read<ErasureRunRow>(tenantId, {
1367
+ text: SQL.exportErasureRuns,
1368
+ parameters: [tenantId, afterId, limit],
1369
+ });
1370
+ return rows.map(erasureRunFromRow);
1371
+ }
1372
+
1373
+ async listAuditEvents(
1374
+ tenantId: string,
1375
+ limit: number,
1376
+ ): Promise<readonly AuditEvent[]> {
1377
+ return this.#openedEvents(tenantId, {
1378
+ text: SQL.listAuditEvents,
1379
+ parameters: [tenantId, limit],
1380
+ });
1381
+ }
1382
+
1383
+ /**
1384
+ * Reads a page of events and the subject keys that page needs in the same
1385
+ * transaction. Two statements per page rather than one per row, and no key
1386
+ * is cached between calls: a key destroyed a moment ago must not open a row
1387
+ * in a process that happened to read it earlier.
1388
+ */
1389
+ async #openedEvents(
1390
+ tenantId: string,
1391
+ statement: DatabaseStatement,
1392
+ ): Promise<readonly AuditEvent[]> {
1393
+ return this.handles.runtime.transaction(
1394
+ async (transaction) => {
1395
+ const rows = (await transaction.query<AuditEventRow>(statement)).rows;
1396
+ const ids = [
1397
+ ...new Set(
1398
+ rows.flatMap((row) =>
1399
+ row.subject_key_id === null ? [] : [row.subject_key_id],
1400
+ ),
1401
+ ),
1402
+ ];
1403
+ const keys = new Map<string, Buffer>();
1404
+ if (ids.length > 0) {
1405
+ const found = await transaction.query<{
1406
+ id: string;
1407
+ material: string | null;
1408
+ }>({
1409
+ text: subjectKeysByIds(ids.length),
1410
+ parameters: [tenantId, ...ids],
1411
+ });
1412
+ for (const row of found.rows) {
1413
+ if (row.material !== null) {
1414
+ keys.set(row.id, decodeSubjectKey(row.material));
1415
+ }
1416
+ }
1417
+ }
1418
+ return rows.map((row) => auditEventFromRow(row, keys));
1419
+ },
1420
+ { access: 'read', tenantId },
1421
+ );
1422
+ }
1423
+
1424
+ async sealAuditEventsPage(
1425
+ tenantId: string,
1426
+ afterSequence: number,
1427
+ limit: number,
1428
+ ): Promise<readonly StoredAuditEvent[]> {
1429
+ const rows = await this.#read<AuditEventRow>(tenantId, {
1430
+ text: SQL.sealAuditEvents,
1431
+ parameters: [tenantId, afterSequence, limit],
1432
+ });
1433
+ return rows.map(storedEventFromRow);
1434
+ }
1435
+
1436
+ async countAuditEventsBefore(
1437
+ tenantId: string,
1438
+ cutoff: number,
1439
+ maxSequence: number | null,
1440
+ ): Promise<number> {
1441
+ const rows = await this.#read<{ total: number | bigint | string }>(
1442
+ tenantId,
1443
+ {
1444
+ text: SQL.countAuditEventsBefore,
1445
+ parameters: [tenantId, cutoff, maxSequence],
1446
+ },
1447
+ );
1448
+ return integer(rows[0]?.total ?? 0, 'row count');
1449
+ }
1450
+
1451
+ async deleteAuditEventsBefore(
1452
+ tenantId: string,
1453
+ cutoff: number,
1454
+ maxSequence: number,
1455
+ limit: number,
1456
+ ): Promise<number> {
1457
+ return this.#write(tenantId, {
1458
+ text: SQL.deleteAuditEventsBefore,
1459
+ parameters: [tenantId, cutoff, maxSequence, limit],
1460
+ });
1461
+ }
1462
+
1463
+ async deleteSweepRunsBefore(
1464
+ tenantId: string,
1465
+ cutoff: number,
1466
+ limit: number,
1467
+ ): Promise<number> {
1468
+ return this.#write(tenantId, {
1469
+ text: SQL.deleteSweepRunsBefore,
1470
+ parameters: [tenantId, cutoff, limit],
1471
+ });
1472
+ }
1473
+
1474
+ async deleteExportRunsBefore(
1475
+ tenantId: string,
1476
+ cutoff: number,
1477
+ limit: number,
1478
+ ): Promise<number> {
1479
+ return this.#write(tenantId, {
1480
+ text: SQL.deleteExportRunsBefore,
1481
+ parameters: [tenantId, cutoff, limit],
1482
+ });
1483
+ }
1484
+
1485
+ async latestAnchor(tenantId: string): Promise<AuditChainAnchor | null> {
1486
+ const rows = await this.#read<AnchorRow>(tenantId, {
1487
+ text: SQL.latestAnchor,
1488
+ parameters: [tenantId],
1489
+ });
1490
+ return rows[0] ? anchorFromRow(rows[0]) : null;
1491
+ }
1492
+
1493
+ async listAnchors(
1494
+ tenantId: string,
1495
+ limit: number,
1496
+ ): Promise<readonly AuditChainAnchor[]> {
1497
+ const rows = await this.#read<AnchorRow>(tenantId, {
1498
+ text: SQL.listAnchors,
1499
+ parameters: [tenantId, limit],
1500
+ });
1501
+ return rows.map(anchorFromRow);
1502
+ }
1503
+
1504
+ async getAnchor(
1505
+ tenantId: string,
1506
+ id: string,
1507
+ ): Promise<AuditChainAnchor | null> {
1508
+ const rows = await this.#read<AnchorRow>(tenantId, {
1509
+ text: SQL.getAnchor,
1510
+ parameters: [tenantId, id],
1511
+ });
1512
+ return rows[0] ? anchorFromRow(rows[0]) : null;
1513
+ }
1514
+
1515
+ async insertAnchor(input: InsertAnchorInput): Promise<AuditChainAnchor> {
1516
+ const record: AuditChainAnchor = { ...input, id: randomUUID() };
1517
+ await this.#write(input.tenantId, {
1518
+ text: SQL.insertAnchor,
1519
+ parameters: [
1520
+ record.id,
1521
+ record.tenantId,
1522
+ record.anchorSequence,
1523
+ record.fromSequence,
1524
+ record.toSequence,
1525
+ record.rowCount,
1526
+ record.firstOccurredAt,
1527
+ record.lastOccurredAt,
1528
+ record.segmentHash,
1529
+ record.previousAnchorHash,
1530
+ record.anchorHash,
1531
+ record.signature,
1532
+ record.keyId,
1533
+ record.segmentFile,
1534
+ record.sealedBy,
1535
+ record.sealedAt,
1536
+ ],
1537
+ });
1538
+ return record;
1539
+ }
1540
+
1541
+ async resignAnchor(input: {
1542
+ readonly tenantId: string;
1543
+ readonly id: string;
1544
+ readonly signature: string;
1545
+ readonly keyId: string;
1546
+ readonly expectedSignature: string;
1547
+ }): Promise<boolean> {
1548
+ const affected = await this.#write(input.tenantId, {
1549
+ text: SQL.resignAnchor,
1550
+ parameters: [
1551
+ input.tenantId,
1552
+ input.id,
1553
+ input.signature,
1554
+ input.keyId,
1555
+ input.expectedSignature,
1556
+ ],
1557
+ });
1558
+ return affected === 1;
1559
+ }
1560
+
1561
+ async countAnchorsByKey(): Promise<readonly AnchorKeyCount[]> {
1562
+ const result = await this.handles.background.query<{
1563
+ key_id: string;
1564
+ anchors: number | bigint | string;
1565
+ }>({ text: SQL.countAnchorsByKey });
1566
+ return result.rows.map((row) => ({
1567
+ keyId: row.key_id,
1568
+ anchors: integer(row.anchors, 'anchor count'),
1569
+ }));
1570
+ }
1571
+
1572
+ async listAnchorsNotOnKey(
1573
+ keyId: string,
1574
+ afterId: string,
1575
+ limit: number,
1576
+ ): Promise<readonly AnchorRouting[]> {
1577
+ const result = await this.handles.background.query<{
1578
+ tenant_id: string;
1579
+ id: string;
1580
+ key_id: string;
1581
+ }>({
1582
+ text: SQL.anchorsNotOnKey,
1583
+ parameters: [keyId, afterId, limit],
1584
+ });
1585
+ return result.rows.map((row) => ({
1586
+ tenantId: row.tenant_id,
1587
+ id: row.id,
1588
+ keyId: row.key_id,
1589
+ }));
1590
+ }
1591
+
1592
+ async insertHold(input: InsertHoldInput): Promise<AuditLegalHold> {
1593
+ const record: AuditLegalHold = {
1594
+ ...input,
1595
+ id: randomUUID(),
1596
+ status: 'active',
1597
+ liftedBy: null,
1598
+ liftReason: null,
1599
+ liftedAt: null,
1600
+ };
1601
+ await this.#write(input.tenantId, {
1602
+ text: SQL.insertHold,
1603
+ parameters: [
1604
+ record.id,
1605
+ record.tenantId,
1606
+ record.scopeKind,
1607
+ record.accountId,
1608
+ record.classId,
1609
+ record.fromAt,
1610
+ record.toAt,
1611
+ record.reason,
1612
+ record.placedBy,
1613
+ record.placedAt,
1614
+ ],
1615
+ });
1616
+ return record;
1617
+ }
1618
+
1619
+ async liftHold(input: LiftHoldRecordInput): Promise<AuditLegalHold | null> {
1620
+ const result = await this.handles.runtime.transaction(
1621
+ (transaction) =>
1622
+ transaction.query<LegalHoldRow>({
1623
+ text: SQL.liftHold,
1624
+ parameters: [
1625
+ input.tenantId,
1626
+ input.id,
1627
+ input.liftedBy,
1628
+ input.liftReason,
1629
+ input.liftedAt,
1630
+ ],
1631
+ }),
1632
+ { access: 'write', tenantId: input.tenantId },
1633
+ );
1634
+ const row = result.rows[0];
1635
+ return row ? holdFromRow(row) : null;
1636
+ }
1637
+
1638
+ async getHold(tenantId: string, id: string): Promise<AuditLegalHold | null> {
1639
+ const rows = await this.#read<LegalHoldRow>(tenantId, {
1640
+ text: SQL.getHold,
1641
+ parameters: [tenantId, id],
1642
+ });
1643
+ return rows[0] ? holdFromRow(rows[0]) : null;
1644
+ }
1645
+
1646
+ async listHolds(
1647
+ tenantId: string,
1648
+ status: HoldStatus | undefined,
1649
+ limit: number,
1650
+ ): Promise<readonly AuditLegalHold[]> {
1651
+ const rows = await this.#read<LegalHoldRow>(tenantId, {
1652
+ text: SQL.listHolds,
1653
+ parameters: [tenantId, status ?? null, limit],
1654
+ });
1655
+ return rows.map(holdFromRow);
1656
+ }
1657
+
1658
+ async listActiveHolds(
1659
+ tenantId: string,
1660
+ limit: number,
1661
+ ): Promise<readonly AuditLegalHold[]> {
1662
+ const rows = await this.#read<LegalHoldRow>(tenantId, {
1663
+ text: SQL.listActiveHolds,
1664
+ parameters: [tenantId, limit],
1665
+ });
1666
+ return rows.map(holdFromRow);
1667
+ }
1668
+
1669
+ async countHoldsForAccount(
1670
+ tenantId: string,
1671
+ accountId: string,
1672
+ ): Promise<number> {
1673
+ const rows = await this.#read<{ total: number | bigint | string }>(
1674
+ tenantId,
1675
+ {
1676
+ text: SQL.countHoldsForAccount,
1677
+ parameters: [tenantId, accountId],
1678
+ },
1679
+ );
1680
+ return integer(rows[0]?.total ?? 0, 'row count');
1681
+ }
1682
+
1683
+ async subjectKey(
1684
+ tenantId: string,
1685
+ subject: string,
1686
+ now: number,
1687
+ ): Promise<SubjectKeyMaterial> {
1688
+ return this.handles.runtime.transaction(
1689
+ (transaction) => subjectKeyIn(transaction, tenantId, subject, now),
1690
+ { access: 'write', tenantId },
1691
+ );
1692
+ }
1693
+
1694
+ async getSubjectKey(
1695
+ tenantId: string,
1696
+ subject: string,
1697
+ ): Promise<AuditSubjectKey | null> {
1698
+ const rows = await this.#read<SubjectKeyRow>(tenantId, {
1699
+ text: SQL.getSubjectKey,
1700
+ parameters: [tenantId, subject],
1701
+ });
1702
+ return rows[0] ? subjectKeyFromRow(rows[0]) : null;
1703
+ }
1704
+
1705
+ async getSubjectKeyByMarker(
1706
+ tenantId: string,
1707
+ marker: string,
1708
+ ): Promise<AuditSubjectKey | null> {
1709
+ const rows = await this.#read<SubjectKeyRow>(tenantId, {
1710
+ text: SQL.getSubjectKeyByMarker,
1711
+ parameters: [tenantId, marker],
1712
+ });
1713
+ return rows[0] ? subjectKeyFromRow(rows[0]) : null;
1714
+ }
1715
+
1716
+ async destroySubjectKey(
1717
+ tenantId: string,
1718
+ subject: string,
1719
+ now: number,
1720
+ ): Promise<AuditSubjectKey | null> {
1721
+ const result = await this.handles.runtime.transaction(
1722
+ (transaction) =>
1723
+ transaction.query<SubjectKeyRow>({
1724
+ text: SQL.destroySubjectKey,
1725
+ parameters: [tenantId, subject, now],
1726
+ }),
1727
+ { access: 'write', tenantId },
1728
+ );
1729
+ const row = result.rows[0];
1730
+ return row ? subjectKeyFromRow(row) : null;
1731
+ }
1732
+
1733
+ async listSubjectKeys(
1734
+ tenantId: string,
1735
+ limit: number,
1736
+ ): Promise<readonly AuditSubjectKey[]> {
1737
+ const rows = await this.#read<SubjectKeyRow>(tenantId, {
1738
+ text: SQL.listSubjectKeys,
1739
+ parameters: [tenantId, limit],
1740
+ });
1741
+ return rows.map(subjectKeyFromRow);
1742
+ }
1743
+
1744
+ async startErasureRun(input: StartErasureRunInput): Promise<AuditErasureRun> {
1745
+ const record: AuditErasureRun = {
1746
+ id: randomUUID(),
1747
+ tenantId: input.tenantId,
1748
+ subject: input.subject,
1749
+ subjectMarker: input.subjectMarker,
1750
+ status: 'requested',
1751
+ dryRun: input.dryRun,
1752
+ destroyKey: input.destroyKey,
1753
+ requestedBy: input.requestedBy,
1754
+ outputDirectory: input.outputDirectory,
1755
+ workspaceSlug: input.workspaceSlug,
1756
+ workspaceName: input.workspaceName,
1757
+ classes: 0,
1758
+ rows: 0,
1759
+ certificatePath: null,
1760
+ outcome: null,
1761
+ reason: null,
1762
+ startedAt: input.startedAt,
1763
+ completedAt: null,
1764
+ };
1765
+ await this.#write(input.tenantId, {
1766
+ text: SQL.insertErasureRun,
1767
+ parameters: [
1768
+ record.id,
1769
+ record.tenantId,
1770
+ record.subject,
1771
+ record.subjectMarker,
1772
+ record.dryRun ? 1 : 0,
1773
+ record.destroyKey ? 1 : 0,
1774
+ record.requestedBy,
1775
+ record.outputDirectory,
1776
+ record.workspaceSlug,
1777
+ record.workspaceName,
1778
+ record.startedAt,
1779
+ ],
1780
+ });
1781
+ return record;
1782
+ }
1783
+
1784
+ async finishErasureRun(
1785
+ input: FinishErasureRunInput,
1786
+ ): Promise<AuditErasureRun | null> {
1787
+ const result = await this.handles.runtime.transaction(
1788
+ (transaction) =>
1789
+ transaction.query<ErasureRunRow>({
1790
+ text: SQL.finishErasureRun,
1791
+ parameters: [
1792
+ input.tenantId,
1793
+ input.id,
1794
+ input.status,
1795
+ input.classes,
1796
+ input.rows,
1797
+ input.certificatePath,
1798
+ input.outcome === null ? null : JSON.stringify(input.outcome),
1799
+ input.reason,
1800
+ input.completedAt,
1801
+ ],
1802
+ }),
1803
+ { access: 'write', tenantId: input.tenantId },
1804
+ );
1805
+ const row = result.rows[0];
1806
+ return row ? erasureRunFromRow(row) : null;
1807
+ }
1808
+
1809
+ async getErasureRun(
1810
+ tenantId: string,
1811
+ id: string,
1812
+ ): Promise<AuditErasureRun | null> {
1813
+ const rows = await this.#read<ErasureRunRow>(tenantId, {
1814
+ text: SQL.getErasureRun,
1815
+ parameters: [tenantId, id],
1816
+ });
1817
+ return rows[0] ? erasureRunFromRow(rows[0]) : null;
1818
+ }
1819
+
1820
+ async listPendingErasureRuns(
1821
+ limit: number,
1822
+ ): Promise<readonly ErasureRunRouting[]> {
1823
+ const result = await this.handles.background.query<{
1824
+ tenant_id: string;
1825
+ id: string;
1826
+ started_at: number | bigint | string;
1827
+ }>({
1828
+ text: SQL.listPendingErasureRuns,
1829
+ parameters: [limit],
1830
+ });
1831
+ return result.rows.map((row) => ({
1832
+ tenantId: row.tenant_id,
1833
+ id: row.id,
1834
+ startedAt: integer(row.started_at, 'timestamp'),
1835
+ }));
1836
+ }
1837
+
1838
+ async claimErasureRun(
1839
+ input: ClaimErasureRunInput,
1840
+ ): Promise<AuditErasureRun | null> {
1841
+ const result = await this.handles.runtime.transaction(
1842
+ (transaction) =>
1843
+ transaction.query<ErasureRunRow>({
1844
+ text: SQL.claimErasureRun,
1845
+ parameters: [
1846
+ input.tenantId,
1847
+ input.id,
1848
+ input.claimedAt,
1849
+ input.staleBefore,
1850
+ ],
1851
+ }),
1852
+ { access: 'write', tenantId: input.tenantId },
1853
+ );
1854
+ const row = result.rows[0];
1855
+ return row ? erasureRunFromRow(row) : null;
1856
+ }
1857
+
1858
+ async heartbeatErasureRun(
1859
+ tenantId: string,
1860
+ id: string,
1861
+ at: number,
1862
+ claimedAt: number,
1863
+ ): Promise<boolean> {
1864
+ const result = await this.handles.runtime.transaction(
1865
+ (transaction) =>
1866
+ transaction.execute({
1867
+ text: SQL.heartbeatErasureRun,
1868
+ parameters: [tenantId, id, at, claimedAt],
1869
+ }),
1870
+ { access: 'write', tenantId },
1871
+ );
1872
+ return result.affectedRows > 0;
1873
+ }
1874
+
1875
+ async listErasureRuns(
1876
+ tenantId: string,
1877
+ limit: number,
1878
+ ): Promise<readonly AuditErasureRun[]> {
1879
+ const rows = await this.#read<ErasureRunRow>(tenantId, {
1880
+ text: SQL.listErasureRuns,
1881
+ parameters: [tenantId, limit],
1882
+ });
1883
+ return rows.map(erasureRunFromRow);
1884
+ }
1885
+ }
1886
+
1887
+ /**
1888
+ * The subject's key inside a transaction that is already open. The insert
1889
+ * ignores a conflict and the read after it is what answers, so two events
1890
+ * written for one subject at the same moment share one key instead of one of
1891
+ * them failing.
1892
+ *
1893
+ * A subject whose key was destroyed is refused rather than given a new one: a
1894
+ * second key would seal the account into fresh events and undo the erasure the
1895
+ * first destruction performed. The caller writes such an event under the
1896
+ * subject's marker instead.
1897
+ */
1898
+ async function subjectKeyIn(
1899
+ transaction: DatabaseTransaction,
1900
+ tenantId: string,
1901
+ subject: string,
1902
+ now: number,
1903
+ ): Promise<SubjectKeyMaterial> {
1904
+ const read = async () =>
1905
+ (
1906
+ await transaction.query<SubjectKeyRow>({
1907
+ text: SQL.getSubjectKey,
1908
+ parameters: [tenantId, subject],
1909
+ })
1910
+ ).rows[0];
1911
+ const existing = await read();
1912
+ if (existing?.material) {
1913
+ return { id: existing.id, key: decodeSubjectKey(existing.material) };
1914
+ }
1915
+ const marker = erasureSubjectMarker(tenantId, subject);
1916
+ const tombstone = (
1917
+ await transaction.query<SubjectKeyRow>({
1918
+ text: SQL.getSubjectKeyByMarker,
1919
+ parameters: [tenantId, marker],
1920
+ })
1921
+ ).rows[0];
1922
+ if (tombstone) {
1923
+ throw new AuditServiceError(
1924
+ 'SUBJECT_KEY_DESTROYED',
1925
+ `The audit data key of this subject was destroyed on ${new Date(Number(tombstone.destroyed_at ?? 0)).toISOString()}; an event naming the subject again would create a new one and undo the erasure.`,
1926
+ 409,
1927
+ );
1928
+ }
1929
+ const key = generateSubjectKey();
1930
+ const id = randomUUID();
1931
+ await transaction.execute({
1932
+ text: SQL.insertSubjectKey,
1933
+ parameters: [id, tenantId, subject, marker, encodeSubjectKey(key), now],
1934
+ });
1935
+ const stored = await read();
1936
+ if (!stored?.material) {
1937
+ throw new Error('The audit subject key could not be stored.');
1938
+ }
1939
+ return { id: stored.id, key: decodeSubjectKey(stored.material) };
1940
+ }
1941
+
1942
+ export async function migrateAuditDatabase(
1943
+ database: DatabaseHandle,
1944
+ ): Promise<void> {
1945
+ await runDatabaseMigrations(database, 'audit.core', databaseMigrations);
1946
+ }