@heavybit/shared-lib 1.3.2 → 2.7.0

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 (366) hide show
  1. package/README.md +261 -0
  2. package/dist/cjs/access/index.js +9 -1
  3. package/dist/cjs/access/index.js.map +1 -1
  4. package/dist/cjs/approvals/index.js +137 -0
  5. package/dist/cjs/approvals/index.js.map +1 -0
  6. package/dist/cjs/audit/index.js +74 -0
  7. package/dist/cjs/audit/index.js.map +1 -0
  8. package/dist/cjs/cache/index.js +90 -16
  9. package/dist/cjs/cache/index.js.map +1 -1
  10. package/dist/cjs/cache/keyspace.js +101 -0
  11. package/dist/cjs/cache/keyspace.js.map +1 -0
  12. package/dist/cjs/common/circuit-breaker.js +74 -0
  13. package/dist/cjs/common/circuit-breaker.js.map +1 -0
  14. package/dist/cjs/common/errors.js +44 -5
  15. package/dist/cjs/common/errors.js.map +1 -1
  16. package/dist/cjs/common/index.js +4 -0
  17. package/dist/cjs/common/index.js.map +1 -1
  18. package/dist/cjs/common/internal-client.js +177 -0
  19. package/dist/cjs/common/internal-client.js.map +1 -0
  20. package/dist/cjs/common/internal-constants.js +12 -0
  21. package/dist/cjs/common/internal-constants.js.map +1 -0
  22. package/dist/cjs/common/internal-service-auth.js +160 -0
  23. package/dist/cjs/common/internal-service-auth.js.map +1 -0
  24. package/dist/cjs/common/money.js +215 -0
  25. package/dist/cjs/common/money.js.map +1 -0
  26. package/dist/cjs/common/phone.js +63 -11
  27. package/dist/cjs/common/phone.js.map +1 -1
  28. package/dist/cjs/common/response.js +78 -5
  29. package/dist/cjs/common/response.js.map +1 -1
  30. package/dist/cjs/common/time.js +28 -2
  31. package/dist/cjs/common/time.js.map +1 -1
  32. package/dist/cjs/config/index.js +140 -20
  33. package/dist/cjs/config/index.js.map +1 -1
  34. package/dist/cjs/context/index.js +49 -0
  35. package/dist/cjs/context/index.js.map +1 -0
  36. package/dist/cjs/contracts/index.js +0 -2
  37. package/dist/cjs/contracts/index.js.map +1 -1
  38. package/dist/cjs/contracts/notification-dispatch.js +87 -63
  39. package/dist/cjs/contracts/notification-dispatch.js.map +1 -1
  40. package/dist/cjs/database/db.js +37 -0
  41. package/dist/cjs/database/db.js.map +1 -0
  42. package/dist/cjs/database/index.js +22 -51
  43. package/dist/cjs/database/index.js.map +1 -1
  44. package/dist/cjs/database/pagination.js +88 -0
  45. package/dist/cjs/database/pagination.js.map +1 -0
  46. package/dist/cjs/database/pool.js +60 -0
  47. package/dist/cjs/database/pool.js.map +1 -0
  48. package/dist/cjs/database/scoped-repository.js +132 -0
  49. package/dist/cjs/database/scoped-repository.js.map +1 -0
  50. package/dist/cjs/database/scopes.js +166 -0
  51. package/dist/cjs/database/scopes.js.map +1 -0
  52. package/dist/cjs/database/table-names.js +14 -0
  53. package/dist/cjs/database/table-names.js.map +1 -0
  54. package/dist/cjs/database/tables.js +71 -0
  55. package/dist/cjs/database/tables.js.map +1 -0
  56. package/dist/cjs/express/auth.js +88 -41
  57. package/dist/cjs/express/auth.js.map +1 -1
  58. package/dist/cjs/express/index.js +3 -0
  59. package/dist/cjs/express/index.js.map +1 -1
  60. package/dist/cjs/express/internal-caller.js +52 -0
  61. package/dist/cjs/express/internal-caller.js.map +1 -0
  62. package/dist/cjs/express/middleware.js +154 -34
  63. package/dist/cjs/express/middleware.js.map +1 -1
  64. package/dist/cjs/express/respond.js +47 -0
  65. package/dist/cjs/express/respond.js.map +1 -0
  66. package/dist/cjs/express/step-up.js +41 -0
  67. package/dist/cjs/express/step-up.js.map +1 -0
  68. package/dist/cjs/http/index.js +3 -71
  69. package/dist/cjs/http/index.js.map +1 -1
  70. package/dist/cjs/identity/index.js +269 -67
  71. package/dist/cjs/identity/index.js.map +1 -1
  72. package/dist/cjs/identity/step-up.js +52 -0
  73. package/dist/cjs/identity/step-up.js.map +1 -0
  74. package/dist/cjs/index.js +7 -0
  75. package/dist/cjs/index.js.map +1 -1
  76. package/dist/cjs/knex/columns.js +36 -0
  77. package/dist/cjs/knex/columns.js.map +1 -0
  78. package/dist/cjs/knex/index.js +26 -43
  79. package/dist/cjs/knex/index.js.map +1 -1
  80. package/dist/cjs/knex/migration-helpers.js +57 -0
  81. package/dist/cjs/knex/migration-helpers.js.map +1 -0
  82. package/dist/cjs/logging/index.js +76 -40
  83. package/dist/cjs/logging/index.js.map +1 -1
  84. package/dist/cjs/logging/redact.js +112 -0
  85. package/dist/cjs/logging/redact.js.map +1 -0
  86. package/dist/cjs/messaging/base-consumer.js +36 -0
  87. package/dist/cjs/messaging/base-consumer.js.map +1 -0
  88. package/dist/cjs/messaging/consumer.js +142 -73
  89. package/dist/cjs/messaging/consumer.js.map +1 -1
  90. package/dist/cjs/messaging/envelope.js +53 -10
  91. package/dist/cjs/messaging/envelope.js.map +1 -1
  92. package/dist/cjs/messaging/idempotency.js +95 -8
  93. package/dist/cjs/messaging/idempotency.js.map +1 -1
  94. package/dist/cjs/messaging/index.js +3 -1
  95. package/dist/cjs/messaging/index.js.map +1 -1
  96. package/dist/cjs/messaging/outbox.js +97 -55
  97. package/dist/cjs/messaging/outbox.js.map +1 -1
  98. package/dist/cjs/messaging/publisher.js +86 -38
  99. package/dist/cjs/messaging/publisher.js.map +1 -1
  100. package/dist/cjs/messaging/staff-notifier.js +36 -44
  101. package/dist/cjs/messaging/staff-notifier.js.map +1 -1
  102. package/dist/cjs/messaging/staff-recipients.js +57 -0
  103. package/dist/cjs/messaging/staff-recipients.js.map +1 -0
  104. package/dist/cjs/messaging/topology.js +41 -5
  105. package/dist/cjs/messaging/topology.js.map +1 -1
  106. package/dist/cjs/rate-limit/index.js +57 -6
  107. package/dist/cjs/rate-limit/index.js.map +1 -1
  108. package/dist/cjs/rate-limit/keys.js +47 -0
  109. package/dist/cjs/rate-limit/keys.js.map +1 -0
  110. package/dist/cjs/socket/index.js +127 -16
  111. package/dist/cjs/socket/index.js.map +1 -1
  112. package/dist/cjs/socket/ticket.js +124 -0
  113. package/dist/cjs/socket/ticket.js.map +1 -0
  114. package/dist/cjs/testing/index.js +23 -14
  115. package/dist/cjs/testing/index.js.map +1 -1
  116. package/dist/cjs/validation/index.js +27 -3
  117. package/dist/cjs/validation/index.js.map +1 -1
  118. package/dist/esm/access/index.js +9 -1
  119. package/dist/esm/access/index.js.map +1 -1
  120. package/dist/esm/approvals/index.js +127 -0
  121. package/dist/esm/approvals/index.js.map +1 -0
  122. package/dist/esm/audit/index.js +70 -0
  123. package/dist/esm/audit/index.js.map +1 -0
  124. package/dist/esm/cache/index.js +70 -13
  125. package/dist/esm/cache/index.js.map +1 -1
  126. package/dist/esm/cache/keyspace.js +95 -0
  127. package/dist/esm/cache/keyspace.js.map +1 -0
  128. package/dist/esm/common/circuit-breaker.js +70 -0
  129. package/dist/esm/common/circuit-breaker.js.map +1 -0
  130. package/dist/esm/common/errors.js +42 -4
  131. package/dist/esm/common/errors.js.map +1 -1
  132. package/dist/esm/common/index.js +4 -0
  133. package/dist/esm/common/index.js.map +1 -1
  134. package/dist/esm/common/internal-client.js +173 -0
  135. package/dist/esm/common/internal-client.js.map +1 -0
  136. package/dist/esm/common/internal-constants.js +9 -0
  137. package/dist/esm/common/internal-constants.js.map +1 -0
  138. package/dist/esm/common/internal-service-auth.js +150 -0
  139. package/dist/esm/common/internal-service-auth.js.map +1 -0
  140. package/dist/esm/common/money.js +207 -0
  141. package/dist/esm/common/money.js.map +1 -0
  142. package/dist/esm/common/phone.js +63 -11
  143. package/dist/esm/common/phone.js.map +1 -1
  144. package/dist/esm/common/response.js +75 -5
  145. package/dist/esm/common/response.js.map +1 -1
  146. package/dist/esm/common/time.js +27 -2
  147. package/dist/esm/common/time.js.map +1 -1
  148. package/dist/esm/config/index.js +136 -19
  149. package/dist/esm/config/index.js.map +1 -1
  150. package/dist/esm/context/index.js +44 -0
  151. package/dist/esm/context/index.js.map +1 -0
  152. package/dist/esm/contracts/index.js +0 -2
  153. package/dist/esm/contracts/index.js.map +1 -1
  154. package/dist/esm/contracts/notification-dispatch.js +85 -62
  155. package/dist/esm/contracts/notification-dispatch.js.map +1 -1
  156. package/dist/esm/database/db.js +32 -0
  157. package/dist/esm/database/db.js.map +1 -0
  158. package/dist/esm/database/index.js +9 -45
  159. package/dist/esm/database/index.js.map +1 -1
  160. package/dist/esm/database/pagination.js +81 -0
  161. package/dist/esm/database/pagination.js.map +1 -0
  162. package/dist/esm/database/pool.js +51 -0
  163. package/dist/esm/database/pool.js.map +1 -0
  164. package/dist/esm/database/scoped-repository.js +128 -0
  165. package/dist/esm/database/scoped-repository.js.map +1 -0
  166. package/dist/esm/database/scopes.js +157 -0
  167. package/dist/esm/database/scopes.js.map +1 -0
  168. package/dist/esm/database/table-names.js +11 -0
  169. package/dist/esm/database/table-names.js.map +1 -0
  170. package/dist/esm/database/tables.js +61 -0
  171. package/dist/esm/database/tables.js.map +1 -0
  172. package/dist/esm/express/auth.js +87 -40
  173. package/dist/esm/express/auth.js.map +1 -1
  174. package/dist/esm/express/index.js +3 -0
  175. package/dist/esm/express/index.js.map +1 -1
  176. package/dist/esm/express/internal-caller.js +49 -0
  177. package/dist/esm/express/internal-caller.js.map +1 -0
  178. package/dist/esm/express/middleware.js +146 -27
  179. package/dist/esm/express/middleware.js.map +1 -1
  180. package/dist/esm/express/respond.js +42 -0
  181. package/dist/esm/express/respond.js.map +1 -0
  182. package/dist/esm/express/step-up.js +38 -0
  183. package/dist/esm/express/step-up.js.map +1 -0
  184. package/dist/esm/http/index.js +3 -70
  185. package/dist/esm/http/index.js.map +1 -1
  186. package/dist/esm/identity/index.js +246 -64
  187. package/dist/esm/identity/index.js.map +1 -1
  188. package/dist/esm/identity/step-up.js +47 -0
  189. package/dist/esm/identity/step-up.js.map +1 -0
  190. package/dist/esm/index.js +7 -0
  191. package/dist/esm/index.js.map +1 -1
  192. package/dist/esm/knex/columns.js +28 -0
  193. package/dist/esm/knex/columns.js.map +1 -0
  194. package/dist/esm/knex/index.js +8 -34
  195. package/dist/esm/knex/index.js.map +1 -1
  196. package/dist/esm/knex/migration-helpers.js +51 -0
  197. package/dist/esm/knex/migration-helpers.js.map +1 -0
  198. package/dist/esm/logging/index.js +68 -39
  199. package/dist/esm/logging/index.js.map +1 -1
  200. package/dist/esm/logging/redact.js +106 -0
  201. package/dist/esm/logging/redact.js.map +1 -0
  202. package/dist/esm/messaging/base-consumer.js +32 -0
  203. package/dist/esm/messaging/base-consumer.js.map +1 -0
  204. package/dist/esm/messaging/consumer.js +139 -71
  205. package/dist/esm/messaging/consumer.js.map +1 -1
  206. package/dist/esm/messaging/envelope.js +50 -10
  207. package/dist/esm/messaging/envelope.js.map +1 -1
  208. package/dist/esm/messaging/idempotency.js +91 -7
  209. package/dist/esm/messaging/idempotency.js.map +1 -1
  210. package/dist/esm/messaging/index.js +3 -1
  211. package/dist/esm/messaging/index.js.map +1 -1
  212. package/dist/esm/messaging/outbox.js +97 -54
  213. package/dist/esm/messaging/outbox.js.map +1 -1
  214. package/dist/esm/messaging/publisher.js +86 -38
  215. package/dist/esm/messaging/publisher.js.map +1 -1
  216. package/dist/esm/messaging/staff-notifier.js +36 -43
  217. package/dist/esm/messaging/staff-notifier.js.map +1 -1
  218. package/dist/esm/messaging/staff-recipients.js +53 -0
  219. package/dist/esm/messaging/staff-recipients.js.map +1 -0
  220. package/dist/esm/messaging/topology.js +39 -4
  221. package/dist/esm/messaging/topology.js.map +1 -1
  222. package/dist/esm/rate-limit/index.js +41 -6
  223. package/dist/esm/rate-limit/index.js.map +1 -1
  224. package/dist/esm/rate-limit/keys.js +43 -0
  225. package/dist/esm/rate-limit/keys.js.map +1 -0
  226. package/dist/esm/socket/index.js +112 -17
  227. package/dist/esm/socket/index.js.map +1 -1
  228. package/dist/esm/socket/ticket.js +117 -0
  229. package/dist/esm/socket/ticket.js.map +1 -0
  230. package/dist/esm/testing/index.js +24 -15
  231. package/dist/esm/testing/index.js.map +1 -1
  232. package/dist/esm/validation/index.js +27 -2
  233. package/dist/esm/validation/index.js.map +1 -1
  234. package/dist/types/access/index.d.ts +9 -3
  235. package/dist/types/access/index.d.ts.map +1 -1
  236. package/dist/types/alerting/index.d.ts +1 -1
  237. package/dist/types/alerting/index.d.ts.map +1 -1
  238. package/dist/types/approvals/index.d.ts +99 -0
  239. package/dist/types/approvals/index.d.ts.map +1 -0
  240. package/dist/types/audit/index.d.ts +82 -0
  241. package/dist/types/audit/index.d.ts.map +1 -0
  242. package/dist/types/cache/index.d.ts +64 -14
  243. package/dist/types/cache/index.d.ts.map +1 -1
  244. package/dist/types/cache/keyspace.d.ts +84 -0
  245. package/dist/types/cache/keyspace.d.ts.map +1 -0
  246. package/dist/types/common/circuit-breaker.d.ts +51 -0
  247. package/dist/types/common/circuit-breaker.d.ts.map +1 -0
  248. package/dist/types/common/errors.d.ts +48 -7
  249. package/dist/types/common/errors.d.ts.map +1 -1
  250. package/dist/types/common/index.d.ts +4 -0
  251. package/dist/types/common/index.d.ts.map +1 -1
  252. package/dist/types/common/internal-client.d.ts +116 -0
  253. package/dist/types/common/internal-client.d.ts.map +1 -0
  254. package/dist/types/common/internal-constants.d.ts +9 -0
  255. package/dist/types/common/internal-constants.d.ts.map +1 -0
  256. package/dist/types/common/internal-service-auth.d.ts +102 -0
  257. package/dist/types/common/internal-service-auth.d.ts.map +1 -0
  258. package/dist/types/common/money.d.ts +104 -0
  259. package/dist/types/common/money.d.ts.map +1 -0
  260. package/dist/types/common/phone.d.ts +45 -6
  261. package/dist/types/common/phone.d.ts.map +1 -1
  262. package/dist/types/common/response.d.ts +66 -5
  263. package/dist/types/common/response.d.ts.map +1 -1
  264. package/dist/types/common/time.d.ts +20 -2
  265. package/dist/types/common/time.d.ts.map +1 -1
  266. package/dist/types/config/index.d.ts +252 -28
  267. package/dist/types/config/index.d.ts.map +1 -1
  268. package/dist/types/context/index.d.ts +39 -0
  269. package/dist/types/context/index.d.ts.map +1 -0
  270. package/dist/types/contracts/index.d.ts +0 -2
  271. package/dist/types/contracts/index.d.ts.map +1 -1
  272. package/dist/types/contracts/notification-dispatch.d.ts +204 -86
  273. package/dist/types/contracts/notification-dispatch.d.ts.map +1 -1
  274. package/dist/types/database/db.d.ts +46 -0
  275. package/dist/types/database/db.d.ts.map +1 -0
  276. package/dist/types/database/index.d.ts +9 -22
  277. package/dist/types/database/index.d.ts.map +1 -1
  278. package/dist/types/database/pagination.d.ts +68 -0
  279. package/dist/types/database/pagination.d.ts.map +1 -0
  280. package/dist/types/database/pool.d.ts +39 -0
  281. package/dist/types/database/pool.d.ts.map +1 -0
  282. package/dist/types/database/scoped-repository.d.ts +92 -0
  283. package/dist/types/database/scoped-repository.d.ts.map +1 -0
  284. package/dist/types/database/scopes.d.ts +159 -0
  285. package/dist/types/database/scopes.d.ts.map +1 -0
  286. package/dist/types/database/table-names.d.ts +13 -0
  287. package/dist/types/database/table-names.d.ts.map +1 -0
  288. package/dist/types/database/tables.d.ts +748 -0
  289. package/dist/types/database/tables.d.ts.map +1 -0
  290. package/dist/types/express/auth.d.ts +63 -11
  291. package/dist/types/express/auth.d.ts.map +1 -1
  292. package/dist/types/express/index.d.ts +3 -0
  293. package/dist/types/express/index.d.ts.map +1 -1
  294. package/dist/types/express/internal-caller.d.ts +53 -0
  295. package/dist/types/express/internal-caller.d.ts.map +1 -0
  296. package/dist/types/express/middleware.d.ts +86 -7
  297. package/dist/types/express/middleware.d.ts.map +1 -1
  298. package/dist/types/express/respond.d.ts +41 -0
  299. package/dist/types/express/respond.d.ts.map +1 -0
  300. package/dist/types/express/step-up.d.ts +38 -0
  301. package/dist/types/express/step-up.d.ts.map +1 -0
  302. package/dist/types/http/index.d.ts +3 -31
  303. package/dist/types/http/index.d.ts.map +1 -1
  304. package/dist/types/identity/index.d.ts +159 -21
  305. package/dist/types/identity/index.d.ts.map +1 -1
  306. package/dist/types/identity/step-up.d.ts +41 -0
  307. package/dist/types/identity/step-up.d.ts.map +1 -0
  308. package/dist/types/index.d.ts +7 -0
  309. package/dist/types/index.d.ts.map +1 -1
  310. package/dist/types/knex/columns.d.ts +15 -0
  311. package/dist/types/knex/columns.d.ts.map +1 -0
  312. package/dist/types/knex/index.d.ts +3 -11
  313. package/dist/types/knex/index.d.ts.map +1 -1
  314. package/dist/types/knex/migration-helpers.d.ts +28 -0
  315. package/dist/types/knex/migration-helpers.d.ts.map +1 -0
  316. package/dist/types/logging/index.d.ts +30 -4
  317. package/dist/types/logging/index.d.ts.map +1 -1
  318. package/dist/types/logging/redact.d.ts +54 -0
  319. package/dist/types/logging/redact.d.ts.map +1 -0
  320. package/dist/types/messaging/base-consumer.d.ts +33 -0
  321. package/dist/types/messaging/base-consumer.d.ts.map +1 -0
  322. package/dist/types/messaging/consumer.d.ts +59 -21
  323. package/dist/types/messaging/consumer.d.ts.map +1 -1
  324. package/dist/types/messaging/envelope.d.ts +50 -11
  325. package/dist/types/messaging/envelope.d.ts.map +1 -1
  326. package/dist/types/messaging/idempotency.d.ts +60 -3
  327. package/dist/types/messaging/idempotency.d.ts.map +1 -1
  328. package/dist/types/messaging/index.d.ts +8 -0
  329. package/dist/types/messaging/index.d.ts.map +1 -1
  330. package/dist/types/messaging/outbox.d.ts +62 -22
  331. package/dist/types/messaging/outbox.d.ts.map +1 -1
  332. package/dist/types/messaging/publisher.d.ts +54 -21
  333. package/dist/types/messaging/publisher.d.ts.map +1 -1
  334. package/dist/types/messaging/staff-notifier.d.ts +69 -38
  335. package/dist/types/messaging/staff-notifier.d.ts.map +1 -1
  336. package/dist/types/messaging/staff-recipients.d.ts +50 -0
  337. package/dist/types/messaging/staff-recipients.d.ts.map +1 -0
  338. package/dist/types/messaging/topology.d.ts +40 -8
  339. package/dist/types/messaging/topology.d.ts.map +1 -1
  340. package/dist/types/rate-limit/index.d.ts +40 -4
  341. package/dist/types/rate-limit/index.d.ts.map +1 -1
  342. package/dist/types/rate-limit/keys.d.ts +52 -0
  343. package/dist/types/rate-limit/keys.d.ts.map +1 -0
  344. package/dist/types/socket/index.d.ts +86 -2
  345. package/dist/types/socket/index.d.ts.map +1 -1
  346. package/dist/types/socket/ticket.d.ts +90 -0
  347. package/dist/types/socket/ticket.d.ts.map +1 -0
  348. package/dist/types/testing/index.d.ts +31 -10
  349. package/dist/types/testing/index.d.ts.map +1 -1
  350. package/dist/types/types/index.d.ts +62 -15
  351. package/dist/types/types/index.d.ts.map +1 -1
  352. package/dist/types/validation/index.d.ts +33 -2
  353. package/dist/types/validation/index.d.ts.map +1 -1
  354. package/package.json +3 -1
  355. package/dist/cjs/contracts/editorial-draft.js +0 -120
  356. package/dist/cjs/contracts/editorial-draft.js.map +0 -1
  357. package/dist/cjs/contracts/upload-import.js +0 -54
  358. package/dist/cjs/contracts/upload-import.js.map +0 -1
  359. package/dist/esm/contracts/editorial-draft.js +0 -117
  360. package/dist/esm/contracts/editorial-draft.js.map +0 -1
  361. package/dist/esm/contracts/upload-import.js +0 -51
  362. package/dist/esm/contracts/upload-import.js.map +0 -1
  363. package/dist/types/contracts/editorial-draft.d.ts +0 -610
  364. package/dist/types/contracts/editorial-draft.d.ts.map +0 -1
  365. package/dist/types/contracts/upload-import.d.ts +0 -96
  366. package/dist/types/contracts/upload-import.d.ts.map +0 -1
package/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # @heavybit/shared-lib
2
+
3
+ Building blocks for Express/TypeScript microservices that any app can use: response envelope and
4
+ errors, gateway identity, access scoping, validation, logging, config, RabbitMQ
5
+ (events/commands/RPC, outbox, idempotent consumers, DLQs), cache-aside, rate limiting, sockets,
6
+ signing, metrics and scheduling.
7
+
8
+ ## Rule: shared concerns only
9
+
10
+ This package must drop into **any** service of **any** app unchanged.
11
+
12
+ - **In:** plumbing and contracts that every service needs in the same form, such as the error
13
+ envelope, signed identity headers, the messaging envelope and key builders.
14
+ - **Out:** anything one app or one service owns, such as domain events, payload schemas, permission
15
+ names, money or tax rules, and app names. Those live in the app's services or in a thin app-level
16
+ package built on top of this one.
17
+ - App-specific values come in as **parameters or config** (app slug, key prefixes, queue prefixes,
18
+ default phone region), never as hardcoded constants.
19
+ - A change that only one app needs is not made here.
20
+
21
+ ## Install and build
22
+
23
+ ```bash
24
+ npm ci
25
+ npm run build # ESM + CJS + .d.ts into dist/
26
+ npm test # jest --runInBand
27
+ npx tsc --noEmit
28
+ ```
29
+
30
+ ## Export map
31
+
32
+ | Import path | What it holds |
33
+ |---|---|
34
+ | `@heavybit/shared-lib` | Root: `types`, `common`, `identity`, `access`, `validation`, `logging`, `config`, `contracts`, `alerting` (no optional peers) |
35
+ | `@heavybit/shared-lib/express` | `correlationMiddleware`, `correlationId`, `validate`, `asyncHandler`, `errorHandler`, `notFoundHandler`, `ok`, `created`, `okPage`, gateway auth guards, `createInternalCallerGuards`, health and security middleware |
36
+ | `@heavybit/shared-lib/http` | SSRF-guarded `safeFetch` (service-to-service calls use root `createInternalClient`) |
37
+ | `@heavybit/shared-lib/messaging` | RabbitMQ connection, publisher, consumer, RPC, outbox, idempotency, topology |
38
+ | `@heavybit/shared-lib/cache` · `/database` · `/knex` · `/rate-limit` · `/socket` · `/security` · `/metrics` · `/scheduling` · `/testing` | As named |
39
+
40
+ ### Response envelope and errors (`common`, `express`)
41
+
42
+ ```ts
43
+ import { createLogger, httpError, NotFoundError, TooManyRequestsError } from '@heavybit/shared-lib';
44
+ import { correlationMiddleware, errorHandler, notFoundHandler, ok, created, okPage } from '@heavybit/shared-lib/express';
45
+
46
+ const logger = createLogger('orders');
47
+ app.use(correlationMiddleware('orders')); // 1. first: correlation id + request context
48
+ app.use(express.json()); // 2. body parser
49
+ // 3. …routers…
50
+ app.use(notFoundHandler()); // 4.
51
+ app.use(errorHandler(logger)); // 5. last
52
+
53
+ ok(res, data); // 200 { success: true, message, data }
54
+ created(res, { id }); // 201
55
+ okPage(res, { data, meta }); // 200 with meta { limit, nextCursor, hasMore }
56
+ throw httpError(409, 'SLUG_TAKEN', 'That address is already in use');
57
+ throw new TooManyRequestsError(undefined, { retryAfterSeconds: 30 }); // sets Retry-After
58
+ ```
59
+
60
+ Error body: `{ success: false, code, message, errors?, requestId }`.
61
+
62
+ - 4xx responses keep their message.
63
+ - Every 5xx response, and anything that is not an `AppError`, gets a generic message and is logged in full.
64
+ - A thrown `ZodError` becomes 422 `VALIDATION_FAILED` with `errors` keyed by field path.
65
+
66
+ | Status | Default code | Class |
67
+ |---|---|---|
68
+ | 400 | `BAD_REQUEST` | `BadRequestError` |
69
+ | 401 | `UNAUTHENTICATED` | `UnauthorizedError` |
70
+ | 403 | `FORBIDDEN` | `ForbiddenError` |
71
+ | 404 | `NOT_FOUND` | `NotFoundError` |
72
+ | 409 | `CONFLICT` | `ConflictError` |
73
+ | 422 | `VALIDATION_FAILED` | `ValidationError`, `NonRetryableError` |
74
+ | 429 | `RATE_LIMITED` | `TooManyRequestsError` |
75
+ | 503 | `SERVICE_UNAVAILABLE` | `ServiceUnavailableError` |
76
+ | 5xx | `INTERNAL_ERROR` | anything else |
77
+
78
+ See [CHANGELOG.md](CHANGELOG.md).
79
+
80
+ ### Request context, validation and logging (`context`, `express`, `logging`)
81
+
82
+ ```ts
83
+ import { createLogger, getRequestContext, runWithContext, updateRequestContext, idParam, z } from '@heavybit/shared-lib';
84
+ import { validate } from '@heavybit/shared-lib/express';
85
+
86
+ router.patch('/:id', validate({ params: idParam, query: listQuery, body: updateBody }), controller.update);
87
+ updateRequestContext({ userId: user.id, scopes: { tenant: tenantId } }); // auth middleware
88
+ logger.info('Order placed', { orderId }); // line carries correlationId, userId, scopes automatically
89
+ await runWithContext({ correlationId: msg.correlationId, service: 'worker' }, () => handle(msg)); // consumers/jobs
90
+ ```
91
+
92
+ - `correlationMiddleware` keeps an incoming `x-correlation-id` only if it is 8–64 of `[A-Za-z0-9-]`, else makes a ULID.
93
+ - `validate` replaces `req.params/query/body` with parsed values (unknown keys stripped; `.strict()` rejects them) and
94
+ throws one 422 with every part's field errors (`params.id`, `query.limit`, body fields unprefixed).
95
+ - `createLogger(service, { redactKeys?, allowKeys?, filePath?, stream? })` writes JSON lines
96
+ `{ level, time, msg, service, correlationId, userId, scopes, ...meta }`. Keys containing the words password,
97
+ pass, token, secret, code, otp, authorization, cookie, credential, pin, apikey, jwt or signature are
98
+ `[REDACTED]` at any depth (except `errorCode`, `statusCode`, `countryCode`, `currencyCode`, `postalCode`,
99
+ `zipCode`); phone numbers are masked to their last 4 digits.
100
+
101
+
102
+ ### Gateway identity v2 and route guards (`identity`, `express`)
103
+
104
+ The gateway signs each request's identity; services verify it before trusting anything.
105
+
106
+ ```ts
107
+ import { defineIdentityProtocol, defineStepUpScopes } from '@heavybit/shared-lib';
108
+ import { authenticateFromGateway, createStepUpGuards, requirePermission, requireOtpVerified } from '@heavybit/shared-lib/express';
109
+
110
+ // App binding (lives in the app's contracts package, not here):
111
+ const protocol = defineIdentityProtocol({
112
+ domain: 'myapp-gateway-identity:v2', // domain separator: two apps never verify each other
113
+ scopeHeaders: { tenant: 'x-tenant-id' }, // → req.user.scopes.tenant
114
+ attrHeaders: { tenantSlug: 'x-tenant-slug' }, // → req.user.attrs.tenantSlug
115
+ });
116
+ const stepUpScopes = defineStepUpScopes({ 'record-delete': {}, 'secret-rotate': { singleUse: true } });
117
+ const { requireStepUp, declaredStepUpRoutes } = createStepUpGuards({ catalogue: stepUpScopes }); // window 5 min
118
+
119
+ router.use(authenticateFromGateway({ protocol, logger, onRejected: publishSecurityEvent })); // secret = env GATEWAY_SIGNING_SECRET (≥ 32 chars)
120
+ router.delete('/records/:id', requirePermission('records.delete'), requireStepUp('record-delete', 'DELETE /records/:id'), controller.remove);
121
+ ```
122
+
123
+ - **Signature:** `x-gateway-signature: v2=<hex>` = HMAC-SHA256 over `[domain, METHOD, path (no query), x-gateway-ts, …identity headers]`
124
+ (JSON-array encoded) in this order: `x-user-id, x-user-email, <mapped scope headers>, <mapped attr headers>,
125
+ x-user-roles, x-user-permissions, x-is-superadmin, x-otp-verified, x-step-up-at, x-session-id, x-user-scopes, x-user-attrs`.
126
+ Missing headers sign as empty strings. `x-gateway-ts` and `x-step-up-at` are epoch milliseconds.
127
+ - **Rejected (401 `UNAUTHENTICATED`):** missing signature, wrong version, timestamp > 30 s old or > 5 s ahead,
128
+ another method/path/secret/domain, any tampered header. No `x-user-id` → anonymous (`optionalAuthFromGateway` continues).
129
+ - **Guards:** `requirePermission(...)` (any), `requireAllPermissions([...])`, `requireRole`, `requireOtpVerified()`
130
+ → 403 `FORBIDDEN`; a signed super admin passes every permission guard. `requireStepUp(scope)` → 403
131
+ `STEP_UP_REQUIRED` with `{ scope }` unless `stepUpAt` is within the window. Single-use proofs are consumed by the gateway.
132
+ - **Gateway side:** `stripProtectedHeaders(req.headers, protocol)` on ingress, then `signedIdentityHeaders({ user, method, path, secret, protocol })`.
133
+ - **Tests:** `identityHeaders({ user, secret, protocol, method, path })` from `/testing`.
134
+
135
+ ### Internal service calls (`common`, `express`)
136
+
137
+ Each service has its own credential; each internal route lists its callers.
138
+
139
+ ```ts
140
+ import { assertInternalServiceAuthConfigured, createInternalClient } from '@heavybit/shared-lib';
141
+ import { createInternalCallerGuards } from '@heavybit/shared-lib/express';
142
+
143
+ assertInternalServiceAuthConfigured({ calls: true, serves: true }); // throws in staging/production if missing
144
+
145
+ // Serving side (credentials = env INTERNAL_SERVICE_CREDENTIALS, {"caller-name":"<secret>"}):
146
+ const { requireInternalCaller } = createInternalCallerGuards({ pathPrefix: '/internal', logger });
147
+ router.get('/internal/records/:id', requireInternalCaller(['billing-service']), controller.get);
148
+
149
+ // Calling side (caller = env INTERNAL_SERVICE_NAME + INTERNAL_SERVICE_CREDENTIAL):
150
+ const records = createInternalClient({ target: 'records-service', baseUrl: env.RECORDS_INTERNAL_URL, logger });
151
+ try {
152
+ const record = await records.get('/records/01J…', { schema: recordSchema });
153
+ } catch (error) {
154
+ if (error instanceof records.UnavailableError) { /* down: degrade or 503 */ } else throw error; // 4xx: AppError with remote code
155
+ }
156
+ ```
157
+
158
+ - **Guard:** 401 `UNAUTHENTICATED` (missing/unknown name, wrong secret; constant-time), 403 `FORBIDDEN` (known caller
159
+ not listed, or any request outside `pathPrefix`). Sets `req.internalCaller`. Logs reason, caller and path only.
160
+ - **Client:** timeout 3 s per attempt; GET retried twice (100 ms, 400 ms) on network errors, timeouts and 502/503/504;
161
+ breaker opens after 5 unavailable calls for 30 s, then one trial. Network/timeout/5xx/open breaker/unreadable or
162
+ schema-mismatched body → `{Target}UnavailableError` (503). 4xx → `AppError` with the remote status, code, message, errors.
163
+ - **Headers sent:** `x-internal-service-name`, `x-internal-service-secret`, `x-correlation-id` (from the request
164
+ context), `idempotency-key` (when given) and `identityHeaders` unchanged. Gateway signatures are bound to method and
165
+ path, so re-sign the user's identity for the exact internal call with `signedIdentityHeaders`.
166
+
167
+ ### Config, Redis keyspace and rate-limit keys (`config`, `cache`, `rate-limit`)
168
+
169
+ ```ts
170
+ import { z } from 'zod';
171
+ import { loadEnv, createRedisKeyspace, rateLimitKey, sensitivePart } from '@heavybit/shared-lib';
172
+ import { createCache, createRedisClient, setWithTtl } from '@heavybit/shared-lib/cache';
173
+
174
+ // Base env (NODE_ENV, PORT, SERVICE_NAME, DB_*, REDIS_*, RABBITMQ_*, QUEUE_PREFIX, CACHE_KEY_PREFIX,
175
+ // GATEWAY_SIGNING_SECRET, LOG_LEVEL, INTERNAL_SERVICE_*) + the service's own. Bad config → names on stderr, exit 1.
176
+ export const config = loadEnv(z.object({ UPSTREAM_INTERNAL_URL: z.string().url() }));
177
+
178
+ const ks = createRedisKeyspace({ app: APP_SLUG, service: config.SERVICE_NAME, env: config.CACHE_KEY_PREFIX });
179
+ const redis = createRedisClient(config, { logger }); // no keyPrefix: the keyspace prefixes
180
+ const cache = createCache({ redis, keyspace: ks, logger });
181
+ await cache.set(ks.key('widget', id), widget, { ttlSec: 300 }); // TTL required, > 0
182
+ await setWithTtl(redis, ks.key('otp', sensitivePart(email)), hash, 600);
183
+ const key = rateLimitKey(ks, { scope: 'sign-in', ip: req.ip, email });
184
+ ```
185
+
186
+ - Keys read `{app}:{env}:{service}:{purpose}:…`; the app slug is the caller's parameter. In staging and
187
+ production a missing `CACHE_KEY_PREFIX` stops startup (both `loadEnv` and `createRedisKeyspace`).
188
+ - Parts are encoded (`:`, `*`, `?`, `[`, `]` cannot add segments or widen a SCAN). Parts that are long,
189
+ email-like, phone-like or wrapped in `sensitivePart` are hashed to `h.<sha256>`.
190
+ - No `KEYS`/`FLUSH*` helpers exist; scan with `ks.pattern(purpose)` and `SCAN` if you must.
191
+ - `createLock(redis, prefix)` takes a string prefix: pass `` `${ks.key('lock')}:` `` so its keys stay inside the
192
+ keyspace. `RedisIdempotencyStore(redis, keyspace)` builds its keys from the keyspace itself.
193
+
194
+ ### Messaging: envelope, outbox, consumers, DLQ (`messaging`)
195
+
196
+ ```ts
197
+ // container.ts — app slug, prefix and service always come from config
198
+ const messaging = createMessaging({ url: amqpUrlFromEnv(config), app: APP_SLUG, prefix: config.QUEUE_PREFIX, service: config.SERVICE_NAME });
199
+ const relay = new OutboxRelay({ db, bus: messaging.publisher, onMetrics: (m) => lagGauge.set(m.lagMs) });
200
+
201
+ // publish: stage in the same transaction as the change; the relay publishes after commit
202
+ await db.transaction(async (tx) => { await repo.insert(ctx, row, { tx }); await enqueueOutbox(tx, 'thing.created', { tenantId, thingId: row.id }); });
203
+
204
+ // consume: validated, de-duplicated in the handler's transaction, scope and correlation restored
205
+ await messaging.consumer.subscribe({ queue: 'thing-created', bindings: [{ exchange: 'events', routingKey: 'thing.created' }],
206
+ schema, dedupe: new ProcessedEventsDeduper(db), scopeFrom: (d) => ({ tenantId: d.tenantId }),
207
+ handler: async ({ data, tx, scope }) => service.onCreated(scope!, data, tx!) });
208
+ ```
209
+
210
+ - Envelope `{ routingKey, data: { ...payload, eventId, version, actorUserId? }, timestamp }`; AMQP `messageId` =
211
+ `eventId`, persistent JSON, header `x-correlation-id`.
212
+ - Names: exchanges `{app}.{prefix}.events` (topic) / `.commands` (direct); queues `{prefix}.{service}.{purpose}`;
213
+ per-service DLX `{app}.{prefix}.{service}.dlx` → `{queue}.dlq`; retry queues `{queue}.retry.{ms}`.
214
+ - Consumer: manual ack; invalid JSON/payload/scope → DLQ at once; handler errors retry after 2, 4, 8, 16 s
215
+ (`maxAttempts` 5) then DLQ; `NonRetryableError` → DLQ at once. `BaseConsumer` is the class form.
216
+ - Relay: batches of 100 `FOR UPDATE SKIP LOCKED`, marks `published_at` after the broker confirm, keeps unroutable
217
+ commands pending, backs off exponentially while the broker is down.
218
+ - `publisher.scheduleDelayed(queue, payload, delayMs)` for delayed delivery (bucketed per-message TTL).
219
+
220
+ ### Data access: Drizzle, scopes, pagination, migrations (`database`, `knex`)
221
+
222
+ **Repositories use Drizzle through these bases; raw scoped queries and Knex in `src/` are forbidden; Knex is
223
+ for migrations and seeds only.**
224
+
225
+ ```ts
226
+ import { createDb, defineScope, definePlatformScope, OwnerScope, ScopedRepository, knexConfig } from '@heavybit/shared-lib/database';
227
+ import { parseCursorQuery } from '@heavybit/shared-lib';
228
+
229
+ // container.ts — no module-level singleton
230
+ const { db, ping, close } = createDb({ env: config, schema });
231
+
232
+ // Scope bindings are app parameters (the app's contracts package defines its own)
233
+ const workspaceScope = defineScope({ name: 'workspace', column: 'workspace_id', contextKey: 'workspaceId' });
234
+ const ownerScope = new OwnerScope({ name: 'owner', column: 'user_id' });
235
+ const platformScope = definePlatformScope({ name: 'platform', column: 'workspace_id', predicate: (u: AuthUser) => u.isSuperAdmin });
236
+
237
+ class NoteRepository extends ScopedRepository<typeof notes, ScopeContext<'workspaceId'>> {
238
+ findByTitle(ctx: ScopeContext<'workspaceId'>, title: string, tx?: DbExecutor) {
239
+ return this.executor(tx).select().from(this.table).where(this.scoped(ctx, eq(this.table.title, title)));
240
+ }
241
+ }
242
+ const notesRepo = new NoteRepository(db, notes, workspaceScope);
243
+ const page = await notesRepo.findPage(ctx, { ...parseCursorQuery(req.query), orderBy: { column: 'created_at' } });
244
+ await db.transaction(async (tx) => notesRepo.insert(ctx, row, { tx }));
245
+
246
+ // knexfile.js
247
+ module.exports = knexConfig(config); // ./migrations, ./seeds, .cjs only
248
+ ```
249
+
250
+ - `ScopedRepository` methods: `findById` (null when absent or out of scope), `findMany`, `findPage`, `count`,
251
+ `insert` (stamps the scope column; a row naming another scope → 403 `SCOPE_MISMATCH`), `update` (refuses to
252
+ change the scope column, `SCOPE_IMMUTABLE`) and `delete`. Every method takes the scope context first and an
253
+ optional `{ tx }` last. Subclasses build WHERE clauses only with `this.scoped(ctx, ...)`.
254
+ - A missing scope value is 403 `SCOPE_REQUIRED` (`OWNER_REQUIRED` for an owner scope). A platform grant comes only
255
+ from `platformScope.authorize(identity)` (403 `PLATFORM_SCOPE_DENIED` otherwise); its queries still name the scope
256
+ column (`IS NOT NULL`) and inserts must carry the scope value.
257
+ - Pagination: `parseCursorQuery` (limit 1–100, default 20), `encodeCursor`/`decodeCursor` (opaque base64url of
258
+ `{ sortKey, id }`; bad cursors → 422), `buildCursorPage`.
259
+ - Migrations (`/knex`): `outboxColumns(t, knex)` (incl. `exchange`, `correlation_id`), `processedEventsColumns(t, knex)` (unique `consumer` + `event_id`),
260
+ `schedulerRunsColumns(t, knex)` (unique `job` + `run_key`), `scopedIndex(t, scopeColumn, cols)` (always leads with
261
+ the scope column). Matching Drizzle tables: `outboxTable`, `processedEventsTable`, `schedulerRunsTable`.
@@ -26,25 +26,33 @@ function grants(granted, required) {
26
26
  return true;
27
27
  return granted.endsWith('.*') && required.startsWith(granted.slice(0, -1));
28
28
  }
29
- /** True when the user holds at least one of `required`. */
29
+ /** True when the user holds at least one of `required`. A super admin holds every permission. */
30
30
  function hasPermission(user, ...required) {
31
31
  if (!user)
32
32
  return false;
33
+ if (user.isSuperAdmin)
34
+ return true;
33
35
  return required.some((perm) => user.permissions.some((g) => grants(g, perm)));
34
36
  }
37
+ /** True when the user holds every one of `required`. A super admin holds every permission. */
35
38
  function hasAllPermissions(user, ...required) {
36
39
  if (!user)
37
40
  return false;
41
+ if (user.isSuperAdmin)
42
+ return true;
38
43
  return required.every((perm) => user.permissions.some((g) => grants(g, perm)));
39
44
  }
45
+ /** True when the user has at least one of `roles`. Prefer permission checks; roles are coarse. */
40
46
  function hasRole(user, ...roles) {
41
47
  return !!user && roles.some((role) => user.roles.includes(role));
42
48
  }
49
+ /** Returns the user, or throws UnauthorizedError (401) when there is none. */
43
50
  function requireUser(user) {
44
51
  if (!user)
45
52
  throw new errors_js_1.UnauthorizedError();
46
53
  return user;
47
54
  }
55
+ /** Returns the user when they hold any of `required` (super admin always does); throws 401/403 otherwise. */
48
56
  function assertPermission(user, ...required) {
49
57
  const u = requireUser(user);
50
58
  if (!hasPermission(u, ...required))
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/access/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/access/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.APPROVAL_STATES = void 0;
4
+ exports.canTransition = canTransition;
5
+ exports.assertTransition = assertTransition;
6
+ exports.isTerminalApprovalState = isTerminalApprovalState;
7
+ exports.assertCanDecide = assertCanDecide;
8
+ exports.assertCanCancel = assertCanCancel;
9
+ exports.approvalExpiresAt = approvalExpiresAt;
10
+ exports.createApprovalPolicy = createApprovalPolicy;
11
+ const index_js_1 = require("../access/index.js");
12
+ const errors_js_1 = require("../common/errors.js");
13
+ /**
14
+ * Two-person approval state machine. A requester asks for a sensitive operation; a different person who
15
+ * holds both the operation's permission and the app's "decide" permission approves or rejects it; the
16
+ * approved operation is then executed once (or fails). Pending requests expire after a TTL.
17
+ *
18
+ * ```
19
+ * pending ──► approved ──► executed
20
+ * │ └──────► failed
21
+ * ├──► rejected
22
+ * ├──► cancelled
23
+ * └──► expired
24
+ * ```
25
+ *
26
+ * App values (the decide permission and the TTL) are passed in through {@link createApprovalPolicy}.
27
+ */
28
+ /** Every approval state. */
29
+ exports.APPROVAL_STATES = ['pending', 'approved', 'rejected', 'cancelled', 'expired', 'executed', 'failed'];
30
+ const TRANSITIONS = Object.freeze({
31
+ pending: ['approved', 'rejected', 'cancelled', 'expired'],
32
+ approved: ['executed', 'failed'],
33
+ rejected: [],
34
+ cancelled: [],
35
+ expired: [],
36
+ executed: [],
37
+ failed: [],
38
+ });
39
+ /**
40
+ * True when `from → to` is an allowed move.
41
+ *
42
+ * @example canTransition('pending', 'approved') // true
43
+ */
44
+ function canTransition(from, to) {
45
+ return TRANSITIONS[from]?.includes(to) ?? false;
46
+ }
47
+ /**
48
+ * Throws unless `from → to` is allowed.
49
+ *
50
+ * @throws ConflictError (`APPROVAL_INVALID_TRANSITION`).
51
+ */
52
+ function assertTransition(from, to) {
53
+ if (!canTransition(from, to)) {
54
+ throw new errors_js_1.ConflictError(`This request can no longer move from ${from} to ${to}`, { code: 'APPROVAL_INVALID_TRANSITION' });
55
+ }
56
+ }
57
+ /** True when the state allows no further moves. */
58
+ function isTerminalApprovalState(state) {
59
+ return TRANSITIONS[state].length === 0;
60
+ }
61
+ function assertPending(approval, now) {
62
+ if (approval.state !== 'pending') {
63
+ throw new errors_js_1.ConflictError('This request has already been decided', { code: 'APPROVAL_NOT_PENDING' });
64
+ }
65
+ if (approval.expiresAt && new Date(approval.expiresAt).getTime() <= now.getTime()) {
66
+ throw new errors_js_1.ConflictError('This request has expired', { code: 'APPROVAL_EXPIRED' });
67
+ }
68
+ }
69
+ /**
70
+ * Throws unless `approver` may approve or reject `approval`: it is pending and not expired, the approver
71
+ * is not the requester (even a super admin cannot approve their own request), and holds both the
72
+ * operation permission and `decidePermission`.
73
+ *
74
+ * @param approval The request.
75
+ * @param approver The deciding user.
76
+ * @param decidePermission The app's "may decide approvals" permission.
77
+ * @param now Clock (tests).
78
+ * @throws ConflictError (`APPROVAL_NOT_PENDING`, `APPROVAL_EXPIRED`) or ForbiddenError
79
+ * (`APPROVAL_SELF_DECISION`, `APPROVAL_FORBIDDEN`).
80
+ */
81
+ function assertCanDecide(approval, approver, decidePermission, now = new Date()) {
82
+ assertPending(approval, now);
83
+ if (approver.id === approval.requesterUserId) {
84
+ throw new errors_js_1.ForbiddenError('You cannot decide your own request', { code: 'APPROVAL_SELF_DECISION' });
85
+ }
86
+ if (!(0, index_js_1.hasAllPermissions)(approver, approval.operationPermission, decidePermission)) {
87
+ throw new errors_js_1.ForbiddenError('You do not have permission to decide this request', { code: 'APPROVAL_FORBIDDEN' });
88
+ }
89
+ }
90
+ /**
91
+ * Throws unless `user` may cancel `approval`: only the requester, and only while it is pending.
92
+ *
93
+ * @throws ConflictError (`APPROVAL_NOT_PENDING`) or ForbiddenError (`APPROVAL_NOT_REQUESTER`).
94
+ */
95
+ function assertCanCancel(approval, user) {
96
+ if (user.id !== approval.requesterUserId) {
97
+ throw new errors_js_1.ForbiddenError('Only the person who made this request can cancel it', { code: 'APPROVAL_NOT_REQUESTER' });
98
+ }
99
+ if (approval.state !== 'pending') {
100
+ throw new errors_js_1.ConflictError('This request has already been decided', { code: 'APPROVAL_NOT_PENDING' });
101
+ }
102
+ }
103
+ /**
104
+ * When a request made at `from` expires.
105
+ *
106
+ * @param ttlSeconds Positive lifetime in seconds.
107
+ * @param from Creation time (default now).
108
+ * @returns The expiry instant.
109
+ * @example approvalExpiresAt(3600, new Date('2026-01-01T00:00:00Z')) // 2026-01-01T01:00:00Z
110
+ */
111
+ function approvalExpiresAt(ttlSeconds, from = new Date()) {
112
+ if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)
113
+ throw new RangeError('Approval TTL must be a positive number of seconds');
114
+ return new Date(from.getTime() + ttlSeconds * 1000);
115
+ }
116
+ /**
117
+ * Binds the approval rules to an app's decide permission and TTL.
118
+ *
119
+ * @param options Decide permission and TTL.
120
+ * @returns The bound policy.
121
+ * @example const approvals = createApprovalPolicy({ decidePermission: 'requests.decide', ttlSeconds: 86_400 });
122
+ */
123
+ function createApprovalPolicy(options) {
124
+ if (!options.decidePermission)
125
+ throw new RangeError('An approval policy needs a decide permission');
126
+ approvalExpiresAt(options.ttlSeconds); // validates the TTL up front
127
+ return Object.freeze({
128
+ decidePermission: options.decidePermission,
129
+ ttlSeconds: options.ttlSeconds,
130
+ canTransition,
131
+ assertTransition,
132
+ assertCanDecide: (approval, approver, now) => assertCanDecide(approval, approver, options.decidePermission, now),
133
+ assertCanCancel,
134
+ expiresAt: (from) => approvalExpiresAt(options.ttlSeconds, from),
135
+ });
136
+ }
137
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/approvals/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAuditEvent = exports.MAX_USER_AGENT_LENGTH = void 0;
4
+ exports.createAuditEventBuilder = createAuditEventBuilder;
5
+ const phone_js_1 = require("../common/phone.js");
6
+ const time_js_1 = require("../common/time.js");
7
+ const redact_js_1 = require("../logging/redact.js");
8
+ /** User-agent strings are cut to this length. */
9
+ exports.MAX_USER_AGENT_LENGTH = 255;
10
+ /** Stable JSON form for deep comparison (object keys sorted; Dates as ISO strings). */
11
+ function canonical(value) {
12
+ return JSON.stringify(value ?? null, (_key, v) => {
13
+ if (v && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date)) {
14
+ return Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b)));
15
+ }
16
+ return v;
17
+ });
18
+ }
19
+ function diff(before, after, classifier) {
20
+ const changes = {};
21
+ const redactedFields = [];
22
+ const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
23
+ for (const key of keys) {
24
+ const was = before[key] ?? null;
25
+ const now = after[key] ?? null;
26
+ if (canonical(was) === canonical(now))
27
+ continue;
28
+ if (classifier.isSecret(key)) {
29
+ redactedFields.push(key);
30
+ continue;
31
+ }
32
+ const clean = (value) => classifier.isPhone(key) && (typeof value === 'string' || typeof value === 'number') ? (0, phone_js_1.maskPhone)(String(value)) : (0, redact_js_1.redactValue)(value, classifier);
33
+ changes[key] = { before: clean(was), after: clean(now) };
34
+ }
35
+ return { changes, redactedFields };
36
+ }
37
+ /**
38
+ * Creates an audit-event builder with the app's extra redaction words.
39
+ *
40
+ * @param options Extra redaction words.
41
+ * @returns `buildAuditEvent` bound to that list.
42
+ * @example const buildAudit = createAuditEventBuilder({ redactKeys: ['nationalId'] });
43
+ */
44
+ function createAuditEventBuilder(options = {}) {
45
+ const classifier = (0, redact_js_1.createKeyClassifier)(options.redactKeys ?? []);
46
+ return (input) => {
47
+ if (!input.action || !input.entityType)
48
+ throw new RangeError('Audit events need an action and an entity type');
49
+ const { changes, redactedFields } = diff(input.before ?? {}, input.after ?? {}, classifier);
50
+ return {
51
+ action: input.action,
52
+ entityType: input.entityType,
53
+ entityId: input.entityId,
54
+ scope: input.scope ? { key: input.scope.key, id: input.scope.id } : null,
55
+ actorUserId: input.actor?.id ?? null,
56
+ outcome: input.outcome ?? 'success',
57
+ occurredAt: (0, time_js_1.toUtcIso)(input.occurredAt ?? new Date()),
58
+ changes,
59
+ redactedFields,
60
+ ip: input.ip ?? null,
61
+ userAgent: input.userAgent ? input.userAgent.slice(0, exports.MAX_USER_AGENT_LENGTH) : null,
62
+ };
63
+ };
64
+ }
65
+ /**
66
+ * Builds an audit event with the default redaction list: the diff holds only changed fields; secret
67
+ * fields are dropped (named in `redactedFields`); phone fields are masked to their last 4 digits.
68
+ *
69
+ * @param input Action, entity, scope, actor, before/after state and request facts.
70
+ * @returns The event.
71
+ * @example buildAuditEvent({ action: 'widget.updated', entityType: 'widget', entityId, scope: { key: 'tenant', id: tenantId }, actor: req.user ?? null, before, after, ip: req.ip, userAgent: req.get('user-agent') })
72
+ */
73
+ exports.buildAuditEvent = createAuditEventBuilder();
74
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/audit/index.ts"],"names":[],"mappings":"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"}