@ddd-ts-kit/audit 0.0.0-stage → 0.1.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 (338) hide show
  1. package/README.md +559 -2
  2. package/dist/chain/audit-checkpoint.d.ts +7 -0
  3. package/dist/chain/audit-checkpoint.d.ts.map +1 -0
  4. package/dist/chain/audit-checkpoint.js +2 -0
  5. package/dist/chain/audit-checkpoint.js.map +1 -0
  6. package/dist/chain/canonicalize.d.ts +8 -0
  7. package/dist/chain/canonicalize.d.ts.map +1 -0
  8. package/dist/chain/canonicalize.js +26 -0
  9. package/dist/chain/canonicalize.js.map +1 -0
  10. package/dist/chain/chain-head.d.ts +8 -0
  11. package/dist/chain/chain-head.d.ts.map +1 -0
  12. package/dist/chain/chain-head.js +2 -0
  13. package/dist/chain/chain-head.js.map +1 -0
  14. package/dist/chain/chain-of-tenant.d.ts +9 -0
  15. package/dist/chain/chain-of-tenant.d.ts.map +1 -0
  16. package/dist/chain/chain-of-tenant.js +10 -0
  17. package/dist/chain/chain-of-tenant.js.map +1 -0
  18. package/dist/chain/empty-chain.d.ts +3 -0
  19. package/dist/chain/empty-chain.d.ts.map +1 -0
  20. package/dist/chain/empty-chain.js +5 -0
  21. package/dist/chain/empty-chain.js.map +1 -0
  22. package/dist/chain/genesis-hash.d.ts +3 -0
  23. package/dist/chain/genesis-hash.d.ts.map +1 -0
  24. package/dist/chain/genesis-hash.js +3 -0
  25. package/dist/chain/genesis-hash.js.map +1 -0
  26. package/dist/chain/hash-of.d.ts +5 -0
  27. package/dist/chain/hash-of.d.ts.map +1 -0
  28. package/dist/chain/hash-of.js +8 -0
  29. package/dist/chain/hash-of.js.map +1 -0
  30. package/dist/chain/head-after.d.ts +4 -0
  31. package/dist/chain/head-after.d.ts.map +1 -0
  32. package/dist/chain/head-after.js +4 -0
  33. package/dist/chain/head-after.js.map +1 -0
  34. package/dist/chain/seal-checkpoint.d.ts +4 -0
  35. package/dist/chain/seal-checkpoint.d.ts.map +1 -0
  36. package/dist/chain/seal-checkpoint.js +7 -0
  37. package/dist/chain/seal-checkpoint.js.map +1 -0
  38. package/dist/chain/seal.d.ts +5 -0
  39. package/dist/chain/seal.d.ts.map +1 -0
  40. package/dist/chain/seal.js +13 -0
  41. package/dist/chain/seal.js.map +1 -0
  42. package/dist/chain/sealing-options.d.ts +12 -0
  43. package/dist/chain/sealing-options.d.ts.map +1 -0
  44. package/dist/chain/sealing-options.js +2 -0
  45. package/dist/chain/sealing-options.js.map +1 -0
  46. package/dist/drizzle/audit-chains-table.d.ts +61 -0
  47. package/dist/drizzle/audit-chains-table.d.ts.map +1 -0
  48. package/dist/drizzle/audit-chains-table.js +12 -0
  49. package/dist/drizzle/audit-chains-table.js.map +1 -0
  50. package/dist/drizzle/audit-entries-table.d.ts +424 -0
  51. package/dist/drizzle/audit-entries-table.d.ts.map +1 -0
  52. package/dist/drizzle/audit-entries-table.js +44 -0
  53. package/dist/drizzle/audit-entries-table.js.map +1 -0
  54. package/dist/drizzle/audit-guard-statements.d.ts +7 -0
  55. package/dist/drizzle/audit-guard-statements.d.ts.map +1 -0
  56. package/dist/drizzle/audit-guard-statements.js +10 -0
  57. package/dist/drizzle/audit-guard-statements.js.map +1 -0
  58. package/dist/drizzle/drizzle-audit-context.d.ts +7 -0
  59. package/dist/drizzle/drizzle-audit-context.d.ts.map +1 -0
  60. package/dist/drizzle/drizzle-audit-context.js +2 -0
  61. package/dist/drizzle/drizzle-audit-context.js.map +1 -0
  62. package/dist/drizzle/drizzle-audit-trail.d.ts +17 -0
  63. package/dist/drizzle/drizzle-audit-trail.d.ts.map +1 -0
  64. package/dist/drizzle/drizzle-audit-trail.js +28 -0
  65. package/dist/drizzle/drizzle-audit-trail.js.map +1 -0
  66. package/dist/drizzle/table-in.d.ts +4 -0
  67. package/dist/drizzle/table-in.d.ts.map +1 -0
  68. package/dist/drizzle/table-in.js +6 -0
  69. package/dist/drizzle/table-in.js.map +1 -0
  70. package/dist/drizzle.d.ts +6 -0
  71. package/dist/drizzle.d.ts.map +1 -0
  72. package/dist/drizzle.js +5 -0
  73. package/dist/drizzle.js.map +1 -0
  74. package/dist/index.d.ts +36 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +18 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/knex/create-audit-tables.d.ts +9 -0
  79. package/dist/knex/create-audit-tables.d.ts.map +1 -0
  80. package/dist/knex/create-audit-tables.js +12 -0
  81. package/dist/knex/create-audit-tables.js.map +1 -0
  82. package/dist/knex/drop-audit-tables.d.ts +5 -0
  83. package/dist/knex/drop-audit-tables.d.ts.map +1 -0
  84. package/dist/knex/drop-audit-tables.js +8 -0
  85. package/dist/knex/drop-audit-tables.js.map +1 -0
  86. package/dist/knex/knex-audit-context.d.ts +6 -0
  87. package/dist/knex/knex-audit-context.d.ts.map +1 -0
  88. package/dist/knex/knex-audit-context.js +2 -0
  89. package/dist/knex/knex-audit-context.js.map +1 -0
  90. package/dist/knex/knex-audit-trail.d.ts +14 -0
  91. package/dist/knex/knex-audit-trail.d.ts.map +1 -0
  92. package/dist/knex/knex-audit-trail.js +22 -0
  93. package/dist/knex/knex-audit-trail.js.map +1 -0
  94. package/dist/knex.d.ts +6 -0
  95. package/dist/knex.d.ts.map +1 -0
  96. package/dist/knex.js +4 -0
  97. package/dist/knex.js.map +1 -0
  98. package/dist/redaction/audit-masker.d.ts +8 -0
  99. package/dist/redaction/audit-masker.d.ts.map +1 -0
  100. package/dist/redaction/audit-masker.js +5 -0
  101. package/dist/redaction/audit-masker.js.map +1 -0
  102. package/dist/redaction/audit-projection.d.ts +13 -0
  103. package/dist/redaction/audit-projection.d.ts.map +1 -0
  104. package/dist/redaction/audit-projection.js +2 -0
  105. package/dist/redaction/audit-projection.js.map +1 -0
  106. package/dist/redaction/audited-field.d.ts +3 -0
  107. package/dist/redaction/audited-field.d.ts.map +1 -0
  108. package/dist/redaction/audited-field.js +2 -0
  109. package/dist/redaction/audited-field.js.map +1 -0
  110. package/dist/redaction/audited-fields.d.ts +16 -0
  111. package/dist/redaction/audited-fields.d.ts.map +1 -0
  112. package/dist/redaction/audited-fields.js +24 -0
  113. package/dist/redaction/audited-fields.js.map +1 -0
  114. package/dist/redaction/audited-message.d.ts +9 -0
  115. package/dist/redaction/audited-message.d.ts.map +1 -0
  116. package/dist/redaction/audited-message.js +2 -0
  117. package/dist/redaction/audited-message.js.map +1 -0
  118. package/dist/redaction/audited-projection.d.ts +10 -0
  119. package/dist/redaction/audited-projection.d.ts.map +1 -0
  120. package/dist/redaction/audited-projection.js +9 -0
  121. package/dist/redaction/audited-projection.js.map +1 -0
  122. package/dist/redaction/changes-of.d.ts +11 -0
  123. package/dist/redaction/changes-of.d.ts.map +1 -0
  124. package/dist/redaction/changes-of.js +37 -0
  125. package/dist/redaction/changes-of.js.map +1 -0
  126. package/dist/redaction/hmac-masker.d.ts +12 -0
  127. package/dist/redaction/hmac-masker.d.ts.map +1 -0
  128. package/dist/redaction/hmac-masker.js +14 -0
  129. package/dist/redaction/hmac-masker.js.map +1 -0
  130. package/dist/redaction/masked-field.d.ts +6 -0
  131. package/dist/redaction/masked-field.d.ts.map +1 -0
  132. package/dist/redaction/masked-field.js +2 -0
  133. package/dist/redaction/masked-field.js.map +1 -0
  134. package/dist/redaction/masked.d.ts +3 -0
  135. package/dist/redaction/masked.d.ts.map +1 -0
  136. package/dist/redaction/masked.js +4 -0
  137. package/dist/redaction/masked.js.map +1 -0
  138. package/dist/redaction/masking-subject.d.ts +9 -0
  139. package/dist/redaction/masking-subject.d.ts.map +1 -0
  140. package/dist/redaction/masking-subject.js +2 -0
  141. package/dist/redaction/masking-subject.js.map +1 -0
  142. package/dist/redaction/missing-masker-error.d.ts +5 -0
  143. package/dist/redaction/missing-masker-error.d.ts.map +1 -0
  144. package/dist/redaction/missing-masker-error.js +8 -0
  145. package/dist/redaction/missing-masker-error.js.map +1 -0
  146. package/dist/search/comparing.d.ts +8 -0
  147. package/dist/search/comparing.d.ts.map +1 -0
  148. package/dist/search/comparing.js +23 -0
  149. package/dist/search/comparing.js.map +1 -0
  150. package/dist/search/cursor-position.d.ts +7 -0
  151. package/dist/search/cursor-position.d.ts.map +1 -0
  152. package/dist/search/cursor-position.js +2 -0
  153. package/dist/search/cursor-position.js.map +1 -0
  154. package/dist/search/decode-cursor.d.ts +3 -0
  155. package/dist/search/decode-cursor.d.ts.map +1 -0
  156. package/dist/search/decode-cursor.js +21 -0
  157. package/dist/search/decode-cursor.js.map +1 -0
  158. package/dist/search/encode-cursor.d.ts +3 -0
  159. package/dist/search/encode-cursor.d.ts.map +1 -0
  160. package/dist/search/encode-cursor.js +5 -0
  161. package/dist/search/encode-cursor.js.map +1 -0
  162. package/dist/search/matches.d.ts +5 -0
  163. package/dist/search/matches.d.ts.map +1 -0
  164. package/dist/search/matches.js +34 -0
  165. package/dist/search/matches.js.map +1 -0
  166. package/dist/search/order-of.d.ts +4 -0
  167. package/dist/search/order-of.d.ts.map +1 -0
  168. package/dist/search/order-of.js +5 -0
  169. package/dist/search/order-of.js.map +1 -0
  170. package/dist/search/search-order.d.ts +6 -0
  171. package/dist/search/search-order.d.ts.map +1 -0
  172. package/dist/search/search-order.js +2 -0
  173. package/dist/search/search-order.js.map +1 -0
  174. package/dist/sources/allowed-reads.d.ts +9 -0
  175. package/dist/sources/allowed-reads.d.ts.map +1 -0
  176. package/dist/sources/allowed-reads.js +2 -0
  177. package/dist/sources/allowed-reads.js.map +1 -0
  178. package/dist/sources/audit-decisions.d.ts +19 -0
  179. package/dist/sources/audit-decisions.d.ts.map +1 -0
  180. package/dist/sources/audit-decisions.js +48 -0
  181. package/dist/sources/audit-decisions.js.map +1 -0
  182. package/dist/sources/audit-fact.d.ts +4 -0
  183. package/dist/sources/audit-fact.d.ts.map +1 -0
  184. package/dist/sources/audit-fact.js +2 -0
  185. package/dist/sources/audit-fact.js.map +1 -0
  186. package/dist/sources/audit-integration-events.d.ts +23 -0
  187. package/dist/sources/audit-integration-events.d.ts.map +1 -0
  188. package/dist/sources/audit-integration-events.js +59 -0
  189. package/dist/sources/audit-integration-events.js.map +1 -0
  190. package/dist/sources/audit-middleware.d.ts +33 -0
  191. package/dist/sources/audit-middleware.d.ts.map +1 -0
  192. package/dist/sources/audit-middleware.js +113 -0
  193. package/dist/sources/audit-middleware.js.map +1 -0
  194. package/dist/sources/audit-record-of.d.ts +10 -0
  195. package/dist/sources/audit-record-of.d.ts.map +1 -0
  196. package/dist/sources/audit-record-of.js +10 -0
  197. package/dist/sources/audit-record-of.js.map +1 -0
  198. package/dist/sources/audit-source.d.ts +8 -0
  199. package/dist/sources/audit-source.d.ts.map +1 -0
  200. package/dist/sources/audit-source.js +2 -0
  201. package/dist/sources/audit-source.js.map +1 -0
  202. package/dist/sources/audit-subscription.d.ts +7 -0
  203. package/dist/sources/audit-subscription.d.ts.map +1 -0
  204. package/dist/sources/audit-subscription.js +2 -0
  205. package/dist/sources/audit-subscription.js.map +1 -0
  206. package/dist/sources/audited-events.d.ts +7 -0
  207. package/dist/sources/audited-events.d.ts.map +1 -0
  208. package/dist/sources/audited-events.js +2 -0
  209. package/dist/sources/audited-events.js.map +1 -0
  210. package/dist/sources/authorization-decision.d.ts +21 -0
  211. package/dist/sources/authorization-decision.d.ts.map +1 -0
  212. package/dist/sources/authorization-decision.js +2 -0
  213. package/dist/sources/authorization-decision.js.map +1 -0
  214. package/dist/sources/record-in.d.ts +5 -0
  215. package/dist/sources/record-in.d.ts.map +1 -0
  216. package/dist/sources/record-in.js +19 -0
  217. package/dist/sources/record-in.js.map +1 -0
  218. package/dist/sql/audit-chains-table-name.d.ts +2 -0
  219. package/dist/sql/audit-chains-table-name.d.ts.map +1 -0
  220. package/dist/sql/audit-chains-table-name.js +2 -0
  221. package/dist/sql/audit-chains-table-name.js.map +1 -0
  222. package/dist/sql/audit-entries-table-name.d.ts +2 -0
  223. package/dist/sql/audit-entries-table-name.d.ts.map +1 -0
  224. package/dist/sql/audit-entries-table-name.js +2 -0
  225. package/dist/sql/audit-entries-table-name.js.map +1 -0
  226. package/dist/sql/audit-guard-sql.d.ts +8 -0
  227. package/dist/sql/audit-guard-sql.d.ts.map +1 -0
  228. package/dist/sql/audit-guard-sql.js +17 -0
  229. package/dist/sql/audit-guard-sql.js.map +1 -0
  230. package/dist/sql/audit-tables-options.d.ts +5 -0
  231. package/dist/sql/audit-tables-options.d.ts.map +1 -0
  232. package/dist/sql/audit-tables-options.js +2 -0
  233. package/dist/sql/audit-tables-options.js.map +1 -0
  234. package/dist/sql/audit-tables-sql.d.ts +7 -0
  235. package/dist/sql/audit-tables-sql.d.ts.map +1 -0
  236. package/dist/sql/audit-tables-sql.js +48 -0
  237. package/dist/sql/audit-tables-sql.js.map +1 -0
  238. package/dist/sql/audit-tables.d.ts +8 -0
  239. package/dist/sql/audit-tables.d.ts.map +1 -0
  240. package/dist/sql/audit-tables.js +7 -0
  241. package/dist/sql/audit-tables.js.map +1 -0
  242. package/dist/sql/drop-audit-tables-sql.d.ts +4 -0
  243. package/dist/sql/drop-audit-tables-sql.d.ts.map +1 -0
  244. package/dist/sql/drop-audit-tables-sql.js +12 -0
  245. package/dist/sql/drop-audit-tables-sql.js.map +1 -0
  246. package/dist/sql/entry-of.d.ts +5 -0
  247. package/dist/sql/entry-of.d.ts.map +1 -0
  248. package/dist/sql/entry-of.js +109 -0
  249. package/dist/sql/entry-of.js.map +1 -0
  250. package/dist/sql/insert-of.d.ts +4 -0
  251. package/dist/sql/insert-of.d.ts.map +1 -0
  252. package/dist/sql/insert-of.js +51 -0
  253. package/dist/sql/insert-of.js.map +1 -0
  254. package/dist/sql/named.d.ts +3 -0
  255. package/dist/sql/named.d.ts.map +1 -0
  256. package/dist/sql/named.js +6 -0
  257. package/dist/sql/named.js.map +1 -0
  258. package/dist/sql/postgres-audit-trail.d.ts +25 -0
  259. package/dist/sql/postgres-audit-trail.d.ts.map +1 -0
  260. package/dist/sql/postgres-audit-trail.js +111 -0
  261. package/dist/sql/postgres-audit-trail.js.map +1 -0
  262. package/dist/sql/qualified.d.ts +3 -0
  263. package/dist/sql/qualified.d.ts.map +1 -0
  264. package/dist/sql/qualified.js +5 -0
  265. package/dist/sql/qualified.js.map +1 -0
  266. package/dist/sql/rows-of.d.ts +4 -0
  267. package/dist/sql/rows-of.d.ts.map +1 -0
  268. package/dist/sql/rows-of.js +14 -0
  269. package/dist/sql/rows-of.js.map +1 -0
  270. package/dist/sql/sql-row.d.ts +2 -0
  271. package/dist/sql/sql-row.d.ts.map +1 -0
  272. package/dist/sql/sql-row.js +2 -0
  273. package/dist/sql/sql-row.js.map +1 -0
  274. package/dist/sql/sql-session.d.ts +9 -0
  275. package/dist/sql/sql-session.d.ts.map +1 -0
  276. package/dist/sql/sql-session.js +2 -0
  277. package/dist/sql/sql-session.js.map +1 -0
  278. package/dist/sql/sql.d.ts +12 -0
  279. package/dist/sql/sql.d.ts.map +1 -0
  280. package/dist/sql/sql.js +35 -0
  281. package/dist/sql/sql.js.map +1 -0
  282. package/dist/testing/audit-trail-contract.d.ts +16 -0
  283. package/dist/testing/audit-trail-contract.d.ts.map +1 -0
  284. package/dist/testing/audit-trail-contract.js +208 -0
  285. package/dist/testing/audit-trail-contract.js.map +1 -0
  286. package/dist/testing/audit-trail-under-contract.d.ts +4 -0
  287. package/dist/testing/audit-trail-under-contract.d.ts.map +1 -0
  288. package/dist/testing/audit-trail-under-contract.js +2 -0
  289. package/dist/testing/audit-trail-under-contract.js.map +1 -0
  290. package/dist/testing/records/a-record.d.ts +3 -0
  291. package/dist/testing/records/a-record.d.ts.map +1 -0
  292. package/dist/testing/records/a-record.js +14 -0
  293. package/dist/testing/records/a-record.js.map +1 -0
  294. package/dist/testing/records/full-record.d.ts +3 -0
  295. package/dist/testing/records/full-record.d.ts.map +1 -0
  296. package/dist/testing/records/full-record.js +16 -0
  297. package/dist/testing/records/full-record.js.map +1 -0
  298. package/dist/testing/records/in-tenant.d.ts +3 -0
  299. package/dist/testing/records/in-tenant.d.ts.map +1 -0
  300. package/dist/testing/records/in-tenant.js +5 -0
  301. package/dist/testing/records/in-tenant.js.map +1 -0
  302. package/dist/testing/records/journal.d.ts +4 -0
  303. package/dist/testing/records/journal.d.ts.map +1 -0
  304. package/dist/testing/records/journal.js +32 -0
  305. package/dist/testing/records/journal.js.map +1 -0
  306. package/dist/testing/records/later.d.ts +4 -0
  307. package/dist/testing/records/later.d.ts.map +1 -0
  308. package/dist/testing/records/later.js +6 -0
  309. package/dist/testing/records/later.js.map +1 -0
  310. package/dist/testing.d.ts +3 -0
  311. package/dist/testing.d.ts.map +1 -0
  312. package/dist/testing.js +2 -0
  313. package/dist/testing.js.map +1 -0
  314. package/dist/trail/audit-trail-options.d.ts +8 -0
  315. package/dist/trail/audit-trail-options.d.ts.map +1 -0
  316. package/dist/trail/audit-trail-options.js +2 -0
  317. package/dist/trail/audit-trail-options.js.map +1 -0
  318. package/dist/trail/in-memory-audit-trail.d.ts +16 -0
  319. package/dist/trail/in-memory-audit-trail.d.ts.map +1 -0
  320. package/dist/trail/in-memory-audit-trail.js +48 -0
  321. package/dist/trail/in-memory-audit-trail.js.map +1 -0
  322. package/dist/verify/chain-break-kind.d.ts +8 -0
  323. package/dist/verify/chain-break-kind.d.ts.map +1 -0
  324. package/dist/verify/chain-break-kind.js +2 -0
  325. package/dist/verify/chain-break-kind.js.map +1 -0
  326. package/dist/verify/chain-break.d.ts +12 -0
  327. package/dist/verify/chain-break.d.ts.map +1 -0
  328. package/dist/verify/chain-break.js +2 -0
  329. package/dist/verify/chain-break.js.map +1 -0
  330. package/dist/verify/chain-verification.d.ts +10 -0
  331. package/dist/verify/chain-verification.d.ts.map +1 -0
  332. package/dist/verify/chain-verification.js +2 -0
  333. package/dist/verify/chain-verification.js.map +1 -0
  334. package/dist/verify/verify-chain.d.ts +20 -0
  335. package/dist/verify/verify-chain.d.ts.map +1 -0
  336. package/dist/verify/verify-chain.js +110 -0
  337. package/dist/verify/verify-chain.js.map +1 -0
  338. package/package.json +70 -4
package/README.md CHANGED
@@ -1,3 +1,560 @@
1
- # Temporary Holding Version
1
+ # @ddd-ts-kit/audit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A tamper-evident audit trail that the use cases never write to. Who was allowed or refused
4
+ what, which command ran and how it ended, which integration event occurred: each fact is an
5
+ append-only entry, hash-chained to the one before it, recorded by a bus middleware, the
6
+ authorization hook or an event subscription. No handler and no aggregate contains an audit
7
+ call.
8
+
9
+ ```
10
+ npm i @ddd-ts-kit/audit
11
+ npm i knex pg # only for @ddd-ts-kit/audit/knex
12
+ npm i drizzle-orm # only for @ddd-ts-kit/audit/drizzle
13
+ ```
14
+
15
+ | Entry | Holds | Imports |
16
+ |---|---|---|
17
+ | `@ddd-ts-kit/audit` | `InMemoryAuditTrail`, the three sources, `auditedFields`, the `AuditMasker` port and `hmacMasker`, `verifyChain`; re-exported from the kernel: `AuditEntry`, the `AuditTrail` and `AuditReader` ports and the types they name | the kernel only |
18
+ | `@ddd-ts-kit/audit/knex` | `KnexAuditTrail`, `createAuditTables`, `dropAuditTables` | `knex` (optional peer) |
19
+ | `@ddd-ts-kit/audit/drizzle` | `DrizzleAuditTrail`, `auditEntriesTable`, `auditChainsTable`, `auditGuardStatements` | `drizzle-orm` (optional peer) |
20
+ | `@ddd-ts-kit/audit/testing` | `auditTrailContract` | `@ddd-ts-kit/testing` (optional peer), the runner you pass |
21
+
22
+ ## The idea
23
+
24
+ An audit trail answers "who did what, when, and was it allowed" to somebody who does not trust
25
+ the application: an auditor, a customer, a regulator. Three consequences:
26
+
27
+ - **It is fed from outside the use cases.** A handler that must remember to call the audit
28
+ will forget, and an aggregate that calls it has stopped being a model. Authorization
29
+ decisions, commands and integration events already pass through three places; the entries are
30
+ written there.
31
+ - **It is append-only and each entry seals the previous one.** Changing, removing or
32
+ reordering an entry breaks the chain at a place `verifyChain` names.
33
+ - **It holds identifiers and codes, never content.** Same rule as tracing and the aspects
34
+ package: class names, permission ids, error codes; never arguments, results or messages.
35
+ A value only enters when the project declares it, field by field.
36
+
37
+ It is not a log (logs are sampled, rotated and lost), not event sourcing (it does not rebuild
38
+ state) and not the `AuditStamp` of the kernel (`createdBy`, `updatedBy`: the last writer of a
39
+ row, not the history).
40
+
41
+ ## The entry
42
+
43
+ The entry is a standard type of the kernel, and the two ports, `AuditTrail` and `AuditReader`,
44
+ are abstract classes of the kernel, so that code which records or lists depends on the kernel
45
+ alone. A port class is also what a binding is keyed by: there is no token beside it. This
46
+ package re-exports them, and holds everything that makes a trail tamper-evident: the chain, the
47
+ sources, redaction, verification and the stores.
48
+
49
+ ```ts
50
+ interface AuditEntry {
51
+ id: string;
52
+ chain: string; // 'tenant:<id>', or 'global' for work that names no tenant
53
+ sequence: number; // position in the chain: 1, 2, 3, without a gap
54
+ occurredAt: Date;
55
+ category: 'authorization' | 'command' | 'query' | 'integration-event' | 'authentication' | 'data-access' | string;
56
+ action: string; // a permission id, a command name, an event type
57
+ outcome: 'allowed' | 'denied' | 'succeeded' | 'failed';
58
+ reason?: string; // a stable code: NO_MATCHING_ROLE, the code of a DomainError
59
+ actor: { id: string; type: ActorType | 'unknown'; tenantId?: string };
60
+ tenantId?: string; // the tenant of the work in progress
61
+ impersonatedBy?: string;
62
+ sessionId?: string;
63
+ resource?: { type: string; id: string };
64
+ context?: string; // bounded context
65
+ correlationId: string;
66
+ causationId?: string;
67
+ origin: ExecutionOrigin; // 'http', 'event', 'schedule', 'cli', ...
68
+ changes?: Record<string, string | number | boolean | null | { masked: string }>;
69
+ extensions: AuditEntryExtensionValues;
70
+ previousHash: string;
71
+ hash: string;
72
+ }
73
+ ```
74
+
75
+ A source hands an `AuditRecord` (the entry without `chain`, `sequence`, `previousHash` and
76
+ `hash`) to the trail, which seals it. Actor, tenant, session, impersonation, correlation,
77
+ causation and origin are read from the kernel `ExecutionScope`, never threaded through a use
78
+ case.
79
+
80
+ A project adds its own fields by declaration merging, as for the kernel `Session`. They live
81
+ under `extensions`, are optional when read, and must be JSON (a `Date` or a class reads as
82
+ `never`), since they are hashed and stored as they are. The interface is the kernel's: augment
83
+ `@ddd-ts-kit/kernel`, next to the other extensions of the project (augmenting
84
+ `@ddd-ts-kit/audit` still works):
85
+
86
+ ```ts
87
+ declare module '@ddd-ts-kit/kernel' {
88
+ interface AuditEntryExtensions {
89
+ readonly region: 'eu' | 'us';
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### One chain per tenant
95
+
96
+ Each entry belongs to one chain: `tenant:<id>` when the work in progress names a tenant,
97
+ `global` otherwise (`chainOfTenant`). The reasons: a tenant can be verified, exported and
98
+ handed over without the entries of the others; appends to a chain are serialized, so one chain
99
+ for everything would make every command of the system wait for the same lock; and an
100
+ offboarded tenant leaves chains that still verify. The price: the order between entries of two
101
+ tenants is given by `occurredAt` only, not proven by a hash. Pass `chainOf` to a trail to
102
+ decide otherwise (one chain per bounded context, a single chain for a small system).
103
+
104
+ ### The hash
105
+
106
+ `hash` is the SHA-256, in lower-case hexadecimal, of the canonical form of the entry without
107
+ `hash`, so `previousHash`, `chain` and `sequence` are part of what is sealed. The canonical
108
+ form (`canonicalize`) is meant to be computed again by any program, in any language:
109
+
110
+ - JSON without whitespace, encoded in UTF-8;
111
+ - the keys of every object sorted by UTF-16 code unit, at every depth;
112
+ - a property whose value is `undefined` left out;
113
+ - `occurredAt` written in ISO 8601 UTC with milliseconds (`2026-03-01T10:00:00.000Z`);
114
+ - strings and numbers as `JSON.stringify` writes them.
115
+
116
+ The first entry of a chain has `previousHash` equal to `GENESIS_HASH` (64 zeros).
117
+
118
+ ## Wiring the sources
119
+
120
+ Everything happens in the composition root. Assembled by hand first; the same pieces bound in
121
+ an application of `@ddd-ts-kit/di` come [after the sources](#in-an-application-of-ddd-ts-kitdi).
122
+ The order of the command middlewares matters:
123
+
124
+ ```ts
125
+ // src/platform/audit/audit.ts
126
+ import { unitOfWorkMiddleware } from '@ddd-ts-kit/kernel';
127
+ import { authorizationMiddleware } from '@ddd-ts-kit/auth';
128
+ import { subscribeAll } from '@ddd-ts-kit/events';
129
+ import { auditDecisions, auditIntegrationEvents, auditMiddleware, auditedFields, hmacMasker, masked } from '@ddd-ts-kit/audit';
130
+ import { KnexAuditTrail } from '@ddd-ts-kit/audit/knex';
131
+
132
+ const trail = new KnexAuditTrail(unitOfWork, { schema: 'audit' }); // anything with `executor()`: the unit of work of @ddd-ts-kit/knex, or its context
133
+ const source = { scope, clock, ids };
134
+
135
+ commandBus
136
+ // 1. who may: refused before any transaction opens
137
+ .use(authorizationMiddleware({ authorizer, map, onDecision: auditDecisions(trail, { ...source, sensitivePermissions: ['records:medical-file.read'] }) }))
138
+ // 2. what ran and how it ended: opens the unit of work itself
139
+ .use(auditMiddleware(trail, {
140
+ ...source,
141
+ unitOfWork,
142
+ masker: hmacMasker(config.auditMaskingSecret, { keyId: 'k1' }),
143
+ fields: [auditedFields(RescheduleAppointmentCommand, ['status', 'startsAt', masked('patientEmail')], {
144
+ resource: (command) => ({ type: 'appointment', id: command.appointmentId }),
145
+ context: 'scheduling',
146
+ })],
147
+ }))
148
+ // 3. joins the transaction opened by 2
149
+ .use(unitOfWorkMiddleware(unitOfWork));
150
+
151
+ // 4. what occurred between contexts
152
+ subscribeAll(eventBus, auditIntegrationEvents(trail, { registry, clock, ids, processed, unitOfWork }));
153
+ ```
154
+
155
+ ### a. Authorization decisions
156
+
157
+ `auditDecisions(trail, options)` returns the `onDecision` function of the authorization
158
+ middleware of `@ddd-ts-kit/auth`. The types match structurally: this package does not depend on
159
+ the auth package. Each entry has category `authorization`, the permission id as `action`,
160
+ `allowed` or `denied` as outcome, the denial code as `reason`, the resource of the decision.
161
+
162
+ What is recorded by default:
163
+
164
+ | Decision | Recorded |
165
+ |---|---|
166
+ | denied, command or query | always |
167
+ | allowed, anything that is not a read | always |
168
+ | allowed read of a permission listed in `sensitivePermissions` | always |
169
+ | any other allowed read | no |
170
+
171
+ Recording every list and every page view drowns the trail and makes it expensive to keep, so
172
+ the allowed reads are the ones you choose. `allowedReads: 'all'` records them all,
173
+ `allowedReads: { sample: 0.01 }` one in a hundred. A read is, by default, a decision whose
174
+ `kind` is `query`: the middleware takes it from the bus that carried the message, whatever the
175
+ class is named. A decision taken outside a bus names no kind and is recorded. Pass `isRead` to
176
+ decide otherwise.
177
+
178
+ When the decision concerns another actor than the one of the execution context (a consumer
179
+ sending a command on behalf of a user), the entry names the actor of the decision with type
180
+ `unknown`: its type and tenant are not in what the hook receives.
181
+
182
+ ### b. Commands, and the unit of work
183
+
184
+ `auditMiddleware` records each command with outcome `succeeded` or `failed`, and the `code` of
185
+ the `DomainError` as reason. A failure that is not a `DomainError` is recorded as
186
+ `UNEXPECTED_ERROR`: neither its class nor its message enters the trail, they quote data.
187
+ `queries: [ReadMedicalFileQuery]` or `queries: 'all'` records queries too.
188
+
189
+ Two requirements pull in opposite directions: the entry of a **failed** command must survive
190
+ the rollback, and the entry of a **succeeded** command must not exist if its transaction did
191
+ not commit. A middleware placed inside the unit of work loses the first, one placed outside
192
+ loses the second (a crash between the commit and the record, or an entry written for a commit
193
+ that was then refused). Hence the arrangement: the middleware is installed **before**
194
+ `unitOfWorkMiddleware` and is given the unit of work.
195
+
196
+ - It opens the unit of work itself; `unitOfWorkMiddleware`, and any nested `run`, join that
197
+ transaction.
198
+ - After the handler returns, it writes the `succeeded` entry in that transaction. The entry
199
+ and the state change commit together, or neither exists. If the entry cannot be written, the
200
+ command fails and rolls back: no state change without its trace.
201
+ - When the handler throws, or the commit is refused, the transaction is gone; the middleware
202
+ then writes the `failed` entry outside of it, and rethrows the failure of the use case. If
203
+ that write fails too, the caller still receives the failure of the use case, and
204
+ `onLostRecord` is told.
205
+
206
+ Without `unitOfWork` (no database, the in-memory trail) the middleware records after the
207
+ handler. Recording through the outbox would also be atomic for successes, but an outbox row
208
+ shares the fate of the transaction, so it cannot carry the failures: it solves half of the
209
+ problem.
210
+
211
+ Put the authorization middleware before this one. A refused command then leaves one
212
+ `authorization denied` entry and opens no transaction; with the opposite order it would also
213
+ leave a `command failed` entry with reason `FORBIDDEN`.
214
+
215
+ ### c. Integration events
216
+
217
+ `auditIntegrationEvents(trail, options)` returns one subscription per event type of the
218
+ registry, to hand to `subscribeAll`. It reads the envelope only (type, id, correlation), never
219
+ the payload: the entry says that `billing.invoice.issued` occurred, with the event id as
220
+ `causationId` and the correlation of the envelope. Delivery is at least once, so it
221
+ deduplicates by event id through the `ProcessedEventStore` it is given, under the consumer
222
+ name `audit`; with `unitOfWork`, the mark and the entry commit together.
223
+
224
+ ### d. By hand, the exception
225
+
226
+ `AuditTrail.record` stays available for what none of the three sources can see: a
227
+ `data-access` entry written by infrastructure, such as an export. `auditRecordOf` fills
228
+ everything the scope knows:
229
+
230
+ ```ts
231
+ // infrastructure/external/appointment-export.ts
232
+ await trail.record(auditRecordOf({ scope, clock, ids }, {
233
+ category: 'data-access',
234
+ action: 'scheduling.appointments.export',
235
+ outcome: 'succeeded',
236
+ changes: { rows: 2000, format: 'csv' },
237
+ }));
238
+ ```
239
+
240
+ This is infrastructure code. A call to the trail in `domain/` or `application/` is the defect
241
+ this package exists to prevent.
242
+
243
+ ### In an application of `@ddd-ts-kit/di`
244
+
245
+ The store is bound to both ports, and the masker to its own, each under its class:
246
+
247
+ ```ts
248
+ // src/platform/audit/audit.module.ts
249
+ import { AuditReader, AuditTrail } from '@ddd-ts-kit/kernel';
250
+ import { defineContextModule } from '@ddd-ts-kit/di';
251
+ import { AuditMasker, hmacMasker } from '@ddd-ts-kit/audit';
252
+ import { KnexAuditTrail } from '@ddd-ts-kit/audit/knex';
253
+
254
+ export const trail = new KnexAuditTrail(unitOfWork, { schema: 'audit' }); // the unit of work bound to `UnitOfWork` and `KnexContext`
255
+
256
+ export const auditModule = defineContextModule({
257
+ name: 'audit',
258
+ register(builder) {
259
+ builder.bind(AuditTrail).toValue(trail);
260
+ builder.bind(AuditReader).toValue(trail);
261
+ builder.bind(AuditMasker).toValue(hmacMasker(config.auditMaskingSecret, { keyId: 'k1' }));
262
+ },
263
+ });
264
+ ```
265
+
266
+ `builder.bind(AuditTrail).toClass(KnexAuditTrail, [KnexContext])` binds a store with its default
267
+ options instead, `KnexContext` being the port of `@ddd-ts-kit/knex`.
268
+
269
+ The sources are installed where the buses are built. The command bus of `kernelModule()` runs
270
+ the use case guard, then the unit of work; the audit middleware has to sit between the two, so
271
+ the bus is given through the options:
272
+
273
+ ```ts
274
+ // src/main.ts
275
+ import { AuditTrail, InMemoryCommandBus, unitOfWorkMiddleware } from '@ddd-ts-kit/kernel';
276
+ import { UseCaseGuard, createApplication, kernelModule } from '@ddd-ts-kit/di';
277
+ import { authorizationModule } from '@ddd-ts-kit/auth/di';
278
+ import { AuditMasker, auditDecisions, auditMiddleware } from '@ddd-ts-kit/audit';
279
+
280
+ const source = { scope, clock, ids };
281
+
282
+ const app = createApplication({
283
+ modules: [
284
+ kernelModule({
285
+ executionScope: scope,
286
+ clock,
287
+ ids,
288
+ unitOfWork,
289
+ commandBus: (resolve) =>
290
+ new InMemoryCommandBus()
291
+ .use((message, next, info) => resolve(UseCaseGuard).check(message, next, info))
292
+ .use(auditMiddleware(resolve(AuditTrail), { ...source, unitOfWork, masker: resolve(AuditMasker), fields }))
293
+ .use(unitOfWorkMiddleware(unitOfWork)),
294
+ }),
295
+ auditModule,
296
+ schedulingModule,
297
+ authorizationModule({ authorizer, onDecision: auditDecisions(trail, { ...source, sensitivePermissions: ['records:medical-file.read'] }) }),
298
+ ],
299
+ authorization: 'required',
300
+ });
301
+ ```
302
+
303
+ A project generated by the CLI with `--audit on` (the default) has this wiring under
304
+ `src/platform/audit/`: `create-audit.ts` builds the trail and its three sources,
305
+ `audit.module.ts` binds the two ports, subscribes the integration events under the consumer
306
+ name `audit` and registers the protected `SearchAuditEntriesQuery`, and
307
+ `src/platform/buses/create-command-bus.ts` places the audit middleware between the use case
308
+ guard and the unit of work.
309
+
310
+ A handler or an adapter that needs the trail names the port in its dependencies and receives
311
+ the store: `[AuditReader]` for the query that lists entries (see [Querying](#querying)),
312
+ `[AuditTrail]` for the infrastructure adapter that records by hand.
313
+
314
+ ## Redaction
315
+
316
+ **By default an entry carries no payload.** `changes` exists only for what was declared:
317
+
318
+ ```ts
319
+ auditedFields(RescheduleAppointmentCommand, ['status', 'startsAt', masked('patientEmail')]);
320
+ auditedProjection(ExportAppointmentsCommand, (command) => ({ plain: { format: command.options.format } }));
321
+ ```
322
+
323
+ - A field that is not listed is never read. The compiler refuses a name the message does not
324
+ have.
325
+ - A value is kept only if it is a string, a number, a boolean, `null`, a bigint (as text) or
326
+ a `Date` (as an ISO instant). An object is never stored whole: pick its scalars with
327
+ `auditedProjection`.
328
+ - `masked('patientEmail')` stores `{ masked: '<keyId>:<hex>' }`: the HMAC-SHA-256 of the value
329
+ under a secret (`hmacMasker`). Two entries about the same email carry the same mask, so
330
+ "every entry about this person" can be answered by masking the email again; the email itself
331
+ is not there, and without the secret a guess cannot be checked. A plain hash would not do: an
332
+ email or a phone number is guessed in seconds.
333
+ - A field declared `masked` without a masker is refused when the middleware is built; a
334
+ projection that asks for a mask without one fails the command at its first use.
335
+
336
+ **What must never go in**, declared or not:
337
+
338
+ - free text (a comment, a reason typed by a user, a message body): it contains anything;
339
+ - credentials and tokens, session cookies, API keys, one-time codes, even hashed;
340
+ - whole payloads, request bodies, aggregates, results of queries;
341
+ - error messages and stack traces: record the `code`;
342
+ - special categories of personal data (health, opinions, biometrics), even masked, unless a
343
+ decision record says why.
344
+
345
+ ### The right to erasure and an append-only chain
346
+
347
+ An entry cannot be deleted without breaking its chain, and a regulation may require that
348
+ personal data be erased. The two coexist as long as the chain never holds the data:
349
+
350
+ 1. **Keep personal data out.** An actor id, a resource id, a permission id are identifiers. If
351
+ your identifiers are themselves personal data (an email as user id), fix that first, or
352
+ mask them at the boundary.
353
+ 2. **Mask what you must correlate.** A masked value is not readable; it is still linked to a
354
+ person for whoever holds the secret.
355
+ 3. **Crypto-shredding for erasure.** Write a masker that derives one key per data subject (or
356
+ per tenant) from a key store, using the `MaskingSubject` it receives (`actorId`, `tenantId`,
357
+ `resource`). `AuditMasker` is a port, an abstract class with the one method `mask`: the
358
+ adapter `implements` it and is bound under it:
359
+
360
+ ```ts
361
+ // infrastructure/external/per-subject-masker.ts
362
+ export class PerSubjectMasker implements AuditMasker {
363
+ constructor(private readonly keys: SubjectKeys) {}
364
+
365
+ async mask(value: AuditScalar, subject: MaskingSubject): Promise<string> {
366
+ const key = await this.keys.keyOf(subject.resource?.id ?? subject.actorId); // created on first use
367
+
368
+ return hmacMasker(key.secret, { keyId: key.id }).mask(value, subject);
369
+ }
370
+ }
371
+
372
+ builder.bind(AuditMasker).toClass(PerSubjectMasker, [SubjectKeys]);
373
+ ```
374
+
375
+ Erasing a person is deleting their key: every mask made with it becomes a random-looking
376
+ token nobody can recompute, the entries stay, the hashes stay valid. The key store is then
377
+ the system to protect and to back up with care; this package does not provide one.
378
+
379
+ What stays after erasure is "an actor with this opaque id did this at that time". Whether that
380
+ is acceptable is a legal question for the project, to answer before the first entry is written.
381
+
382
+ ## Verification and anchoring
383
+
384
+ ```ts
385
+ const outcome = await verifyChain(reader, { chain: 'tenant:acme' });
386
+ // { valid: true, checked: 1842 }
387
+ // { valid: false, checked: 311, break: { kind: 'altered-content', chain: 'tenant:acme', position: 311, sequence: 312, entryId: '...' } }
388
+ ```
389
+
390
+ `verifyChain(entries | reader, { chain?, from?, to?, checkpoints? })` walks a chain in the
391
+ order given and reports the **first** place where it stops holding:
392
+
393
+ | Kind | Meaning |
394
+ |---|---|
395
+ | `altered-content` | the entry does not hash to its `hash`, does not link to the entry before it, or disagrees with a checkpoint |
396
+ | `missing-entry` | the expected sequence is nowhere in what was examined (a hole, a missing head, a chain shorter than `to`) |
397
+ | `reordered` | the expected sequence exists elsewhere, or a position appears twice |
398
+
399
+ `position` is the index among the examined entries of the chain, `sequence` the sequence that
400
+ was expected there. Given entries (an export), every chain found is verified, and the entries
401
+ are never sorted first: a shuffled file is reported as such. Given a reader, `chain` is
402
+ required and the range is read page by page, then held in memory: verify long chains by ranges
403
+ (`from`, `to`). A range starting above 1 trusts the entry before it.
404
+
405
+ **Why anchoring.** The chain protects against whoever can change some rows. Whoever can
406
+ rewrite the whole table (a database administrator, an attacker with the owner's role) can
407
+ recompute every hash from the altered entry onwards: the result is a perfectly valid chain. The
408
+ only defence is a copy of some hashes where that person cannot write. `sealing` hands a
409
+ checkpoint over every N entries of a chain:
410
+
411
+ ```ts
412
+ new KnexAuditTrail(unitOfWork, {
413
+ schema: 'audit',
414
+ sealing: { every: 1000, onSeal: (checkpoint) => outbox.append([auditChainSealed.create(checkpoint, meta)]) },
415
+ });
416
+ // later, against the checkpoints kept elsewhere:
417
+ await verifyChain(reader, { chain, checkpoints });
418
+ ```
419
+
420
+ The hook runs when the entry is written, inside the unit of work when there is one. Appending
421
+ the checkpoint to the outbox there makes it leave only if the entry commits; a hook that calls
422
+ a remote service directly may anchor an entry whose transaction rolls back afterwards. Where
423
+ the checkpoints go is the project's choice: another database under another role, an object
424
+ store with retention lock, a transparency log, a signed timestamp. A truncated tail (the last
425
+ entries removed) is only seen by comparing with a later checkpoint or by asking for `to`.
426
+
427
+ ## Stores
428
+
429
+ `InMemoryAuditTrail` implements both ports: the fake for tests (`entries` is the store, to
430
+ assert and to tamper with) and enough for an application without a database.
431
+
432
+ The SQL stores are for **PostgreSQL**. They write on the executor of the unit of work
433
+ (`KnexContext.executor()`, `DrizzleContext.executor()`): inside a transaction the entry shares
434
+ its fate, outside it is written at once in a transaction of its own.
435
+
436
+ ```ts
437
+ // knex: a migration of the project
438
+ import { createAuditTables, dropAuditTables } from '@ddd-ts-kit/audit/knex';
439
+ export const up = (knex) => createAuditTables(knex, { schema: 'audit' });
440
+ export const down = (knex) => dropAuditTables(knex, { schema: 'audit' });
441
+
442
+ // drizzle: in the schema files drizzle-kit reads
443
+ import { auditChainsTable, auditEntriesTable, auditGuardStatements } from '@ddd-ts-kit/audit/drizzle';
444
+ export const auditEntries = auditEntriesTable('audit');
445
+ export const auditChains = auditChainsTable('audit');
446
+ // and in a custom migration (drizzle-kit generate --custom), since drizzle-kit writes no trigger:
447
+ for (const statement of auditGuardStatements('audit')) {
448
+ await db.execute(sql.raw(statement));
449
+ }
450
+
451
+ new DrizzleAuditTrail(unitOfWork, { schema: 'audit' });
452
+ ```
453
+
454
+ Two tables: `audit_entries` (primary key `(chain, sequence)`, unique `id`, indexes for the
455
+ reader) and `audit_chains`, one row per chain holding its last sequence and hash.
456
+
457
+ **Concurrency.** An append inserts the head row of its chain if it is missing, then takes it
458
+ with `SELECT ... FOR UPDATE`, computes the entry from it, inserts the entry and moves the head.
459
+ The row lock lasts until the transaction ends, so two appends to one chain never read the same
460
+ head and the chain cannot fork; appends to other chains do not wait. The primary key refuses a
461
+ duplicate position should the lock ever be bypassed. Under `REPEATABLE READ` or `SERIALIZABLE`
462
+ the second append fails with a serialization error instead of waiting, and the unit of work is
463
+ retried like any other.
464
+
465
+ **Append-only in the database too.** The migration helper creates the function
466
+ `audit_entries_append_only` and two triggers: `UPDATE`, `DELETE` and `TRUNCATE` on
467
+ `audit_entries` raise SQLSTATE `23001`, whoever asks. Triggers stop mistakes and application
468
+ bugs; a role that owns the table can drop them. So the application should not own the table.
469
+ Run the migrations with an owner role and give the application role only what it needs:
470
+
471
+ ```sql
472
+ revoke all on all tables in schema "audit" from public;
473
+ grant usage on schema "audit" to app;
474
+ grant select, insert on "audit"."audit_entries" to app;
475
+ grant select, insert, update on "audit"."audit_chains" to app;
476
+ ```
477
+
478
+ The application can then neither rewrite an entry, nor disable or drop the guard. What remains
479
+ is the owner role and the superuser, which is what anchoring is for.
480
+
481
+ A new store passes the same suite as these:
482
+
483
+ ```ts
484
+ import { auditTrailContract } from '@ddd-ts-kit/audit/testing';
485
+ auditTrailContract({ describe, it, expect, beforeEach }, { name: 'MyAuditTrail', createTrail: () => freshTrail(), tamper });
486
+ ```
487
+
488
+ ## Querying
489
+
490
+ ```ts
491
+ const page = await reader.search(
492
+ { tenantId: 'acme', actorId: 'ada', category: 'authorization', outcome: 'denied', from, to },
493
+ cursorPage({ limit: 50, sort: [{ field: 'occurredAt', direction: 'desc' }] }),
494
+ );
495
+ ```
496
+
497
+ Filters: `actorId`, `resource` (`{ type, id? }`), `action`, `category`, `outcome`, period
498
+ (`from` included, `to` excluded), `correlationId`, `tenantId`, `chain`, `fromSequence`,
499
+ `toSequence`. Pagination uses the kernel types: an offset page answers `total`, a cursor page
500
+ `nextCursor`. Sort by `occurredAt` (then chain and sequence, the order a person reads in) or by
501
+ `sequence` (chain by chain, the order of verification).
502
+
503
+ `AuditReader` is a port of its own because reading the trail is itself a sensitive act. Expose
504
+ it as a use case like any other, which makes it protected and audited by the mechanisms above:
505
+
506
+ ```ts
507
+ // application/queries/search-audit-entries/search-audit-entries.query.ts
508
+ import { AuthorizedQuery, type AuditEntry, type Page } from '@ddd-ts-kit/kernel';
509
+
510
+ export class SearchAuditEntriesQuery extends AuthorizedQuery<Page<AuditEntry>> { /* actor, tenantId, filter, page */ }
511
+
512
+ // authorization: deny by default, one permission, flagged sensitive
513
+ requires(SearchAuditEntriesQuery, auditPermissions['trail.read']);
514
+ auditDecisions(trail, { ...source, sensitivePermissions: ['audit:trail.read'] });
515
+ auditMiddleware(trail, { ...source, queries: [SearchAuditEntriesQuery] });
516
+ ```
517
+
518
+ ```ts
519
+ // application/queries/search-audit-entries/search-audit-entries.handler.ts
520
+ import { AuditReader, type AuditEntry, type Page } from '@ddd-ts-kit/kernel';
521
+ import { QueryHandler } from '@ddd-ts-kit/kernel/declarations';
522
+
523
+ @QueryHandler(SearchAuditEntriesQuery, [AuditReader])
524
+ export class SearchAuditEntriesHandler {
525
+ constructor(private readonly reader: AuditReader) {}
526
+
527
+ execute(query: SearchAuditEntriesQuery): Promise<Page<AuditEntry>> {
528
+ return this.reader.search({ ...query.filter, tenantId: query.tenantId }, query.page);
529
+ }
530
+ }
531
+ ```
532
+
533
+ Whoever reads the trail leaves an entry in it. The handler depends on `AuditReader` of the
534
+ kernel, which `application/` may import, and receives the store bound to it. It scopes the
535
+ filter to the tenant of the actor (an access scope computed by the authorizer), so one tenant
536
+ never lists another's entries.
537
+
538
+ ## Limits
539
+
540
+ - **PostgreSQL only** for the SQL stores: row locks, row comparison for cursors, `jsonb`,
541
+ triggers. Another database is another adapter of the two ports, held to the contract suite.
542
+ - **Appends to one chain are serialized**, and inside a unit of work the head stays locked
543
+ until the commit. `auditMiddleware` writes last, so the window is short; a handler that
544
+ records by hand early and then works for long makes the other writers of its tenant wait.
545
+ - **A rolled back unit of work frees its positions**: the next entry reuses the sequence.
546
+ There is never a gap, and never an entry for work that did not commit.
547
+ - **A command sent from inside another unit of work** (a consumer whose `consume` opened one)
548
+ shares its fate: if that outer transaction rolls back, the `failed` entry goes with it.
549
+ - **The chain proves integrity, not completeness of capture**: a fact that never reached a
550
+ source (a script writing to the database, a route bypassing the bus) is not there. Keeping
551
+ every route on the bus is what `authz/declared` and the dependency rules are for.
552
+ - **`occurredAt` is the clock of the instance that recorded**; inside a chain, the order is
553
+ the sequence.
554
+ - **Verification reads the range into memory**, and trusts the entry before `from`.
555
+ - **No retention or archiving policy** is provided: how long entries are kept, and where old
556
+ chains go, is a decision of the project. Deleting old entries means dropping the guard on
557
+ purpose, by the owner role, after anchoring the entry that becomes the new start.
558
+ - drizzle wraps driver errors and prints the parameters of the failed statement in the
559
+ message of its error. The entry of an audit insert holds identifiers and codes only; still,
560
+ log the `code` of such an error, not its message.
@@ -0,0 +1,7 @@
1
+ /** A position of a chain and the hash it had there, to publish where the store cannot write. */
2
+ export interface AuditCheckpoint {
3
+ readonly chain: string;
4
+ readonly sequence: number;
5
+ readonly hash: string;
6
+ }
7
+ //# sourceMappingURL=audit-checkpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-checkpoint.d.ts","sourceRoot":"","sources":["../../src/chain/audit-checkpoint.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=audit-checkpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-checkpoint.js","sourceRoot":"","sources":["../../src/chain/audit-checkpoint.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The canonical form the hash is computed over, so that any program can compute it again:
3
+ * JSON without whitespace, the keys of every object sorted by UTF-16 code unit (the order of
4
+ * `Array.prototype.sort`), a property whose value is `undefined` left out, a date written in
5
+ * ISO 8601 UTC with milliseconds, numbers and strings as `JSON.stringify` writes them.
6
+ */
7
+ export declare function canonicalize(value: unknown): string;
8
+ //# sourceMappingURL=canonicalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonicalize.d.ts","sourceRoot":"","sources":["../../src/chain/canonicalize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcnD"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The canonical form the hash is computed over, so that any program can compute it again:
3
+ * JSON without whitespace, the keys of every object sorted by UTF-16 code unit (the order of
4
+ * `Array.prototype.sort`), a property whose value is `undefined` left out, a date written in
5
+ * ISO 8601 UTC with milliseconds, numbers and strings as `JSON.stringify` writes them.
6
+ */
7
+ export function canonicalize(value) {
8
+ if (value instanceof Date) {
9
+ return JSON.stringify(value.toISOString());
10
+ }
11
+ if (Array.isArray(value)) {
12
+ return `[${value.map(canonicalize).join(',')}]`;
13
+ }
14
+ if (typeof value === 'object' && value !== null) {
15
+ return canonicalObject(value);
16
+ }
17
+ return JSON.stringify(value ?? null);
18
+ }
19
+ function canonicalObject(value) {
20
+ const members = Object.entries(value)
21
+ .filter(([, member]) => member !== undefined)
22
+ .sort(([left], [right]) => (left < right ? -1 : 1))
23
+ .map(([key, member]) => `${JSON.stringify(key)}:${canonicalize(member)}`);
24
+ return `{${members.join(',')}}`;
25
+ }
26
+ //# sourceMappingURL=canonicalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonicalize.js","sourceRoot":"","sources":["../../src/chain/canonicalize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAClD,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IAED,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,eAAe,CAAC,KAAa;IACpC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SAClC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,KAAK,SAAS,CAAC;SAC5C,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAClD,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAE5E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAClC,CAAC"}
@@ -0,0 +1,8 @@
1
+ /** The last entry of a chain, which the next one links to. */
2
+ export interface ChainHead {
3
+ readonly chain: string;
4
+ /** 0 for a chain that holds no entry yet. */
5
+ readonly sequence: number;
6
+ readonly hash: string;
7
+ }
8
+ //# sourceMappingURL=chain-head.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain-head.d.ts","sourceRoot":"","sources":["../../src/chain/chain-head.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=chain-head.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain-head.js","sourceRoot":"","sources":["../../src/chain/chain-head.ts"],"names":[],"mappings":""}
@@ -0,0 +1,9 @@
1
+ import type { AuditRecord } from '@ddd-ts-kit/kernel';
2
+ export declare const GLOBAL_CHAIN = "global";
3
+ /**
4
+ * One chain per tenant, and one `global` chain for work that names no tenant. A tenant can
5
+ * then be verified, exported and handed over alone, and two tenants never wait for each other
6
+ * to append. The price: the order of entries of different tenants is not proven by a chain.
7
+ */
8
+ export declare function chainOfTenant(record: AuditRecord): string;
9
+ //# sourceMappingURL=chain-of-tenant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain-of-tenant.d.ts","sourceRoot":"","sources":["../../src/chain/chain-of-tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,eAAO,MAAM,YAAY,WAAW,CAAC;AAErC;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD"}