@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
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.staffEmailListSchema = exports.ConfiguredStaffRecipients = void 0;
4
+ /**
5
+ * Staff alert recipients (messaging layer helper): who receives an alert raised for a permission.
6
+ * Today the lists come from configuration; an app can plug in a lookup (e.g. its auth service)
7
+ * by implementing {@link StaffRecipientResolver}.
8
+ */
9
+ const zod_1 = require("zod");
10
+ const notification_dispatch_js_1 = require("../contracts/notification-dispatch.js");
11
+ /**
12
+ * Recipients from configuration (env), keyed by the permission an alert targets.
13
+ *
14
+ * @param byPermission Map of permission → recipients, typically built with {@link staffEmailListSchema}.
15
+ * @example new ConfiguredStaffRecipients({ 'payouts.manage': env.FINANCE_ALERT_EMAILS })
16
+ */
17
+ class ConfiguredStaffRecipients {
18
+ byPermission;
19
+ constructor(byPermission) {
20
+ this.byPermission = byPermission;
21
+ }
22
+ /**
23
+ * @param permission The permission an alert targets.
24
+ * @returns A copy of the configured recipients, or `[]` when none are configured.
25
+ */
26
+ async resolve(permission) {
27
+ return [...(this.byPermission[permission] ?? [])];
28
+ }
29
+ }
30
+ exports.ConfiguredStaffRecipients = ConfiguredStaffRecipients;
31
+ const emailSchema = zod_1.z.string().email().max(notification_dispatch_js_1.DISPATCH_LIMITS.email);
32
+ /**
33
+ * A comma-separated staff address list from env (`"a@x.io, b@y.io"`): trimmed, blanks dropped,
34
+ * de-duplicated case-insensitively. An invalid address fails validation, so a typo is caught at boot
35
+ * instead of dead-lettering in the notification-service when the alert finally fires.
36
+ *
37
+ * @returns (on parse) `DispatchRecipient[]` with `email` only.
38
+ * @example const config = z.object({ FINANCE_ALERT_EMAILS: staffEmailListSchema }).parse(process.env);
39
+ */
40
+ exports.staffEmailListSchema = zod_1.z
41
+ .string()
42
+ .optional()
43
+ .transform((csv) => {
44
+ const seen = new Set();
45
+ return (csv ?? '')
46
+ .split(',')
47
+ .map((entry) => entry.trim())
48
+ .filter((email) => email && !seen.has(email.toLowerCase()) && seen.add(email.toLowerCase()));
49
+ })
50
+ .superRefine((emails, ctx) => {
51
+ for (const email of emails) {
52
+ if (!emailSchema.safeParse(email).success)
53
+ ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Not a valid email address: ${email}` });
54
+ }
55
+ })
56
+ .transform((emails) => emails.map((email) => ({ email })));
57
+ //# sourceMappingURL=staff-recipients.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staff-recipients.js","sourceRoot":"","sources":["../../../src/messaging/staff-recipients.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,6BAAwB;AACxB,oFAAgG;AAehG;;;;;GAKG;AACH,MAAa,yBAAyB;IACP;IAA7B,YAA6B,YAAoE;QAApE,iBAAY,GAAZ,YAAY,CAAwD;IAAG,CAAC;IAErG;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,UAAkB;QAC9B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACpD,CAAC;CACF;AAVD,8DAUC;AAED,MAAM,WAAW,GAAG,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,0CAAe,CAAC,KAAK,CAAC,CAAC;AAElE;;;;;;;GAOG;AACU,QAAA,oBAAoB,GAAG,OAAC;KAClC,MAAM,EAAE;KACR,QAAQ,EAAE;KACV,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE;IACjB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;SACf,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AACjG,CAAC,CAAC;KACD,WAAW,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;IAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO;YAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAC,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,8BAA8B,KAAK,EAAE,EAAE,CAAC,CAAC;IAC3I,CAAC;AACH,CAAC,CAAC;KACD,SAAS,CAAC,CAAC,MAAM,EAAuB,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC"}
@@ -1,22 +1,58 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.deadLetterQueueName = exports.retryQueueName = void 0;
3
+ exports.MIN_DELAY_BUCKET_MS = exports.deadLetterQueueName = exports.retryQueueName = void 0;
4
4
  exports.createTopology = createTopology;
5
+ exports.delayQueueName = delayQueueName;
6
+ const SEGMENT = /^[a-z0-9][a-z0-9_-]*$/;
7
+ const PURPOSE = /^[a-z0-9][a-z0-9._-]*$/;
8
+ function segment(label, value, pattern = SEGMENT) {
9
+ const v = value.trim();
10
+ if (!pattern.test(v))
11
+ throw new Error(`Topology ${label} must match ${pattern} (got "${value}")`);
12
+ return v;
13
+ }
14
+ /**
15
+ * Builds the naming scheme for one app and environment.
16
+ *
17
+ * @param options App slug and environment prefix.
18
+ * @returns The {@link Topology}.
19
+ * @throws Error when a segment is empty or has other characters.
20
+ * @example const topology = createTopology({ app: config.APP_SLUG, prefix: config.QUEUE_PREFIX });
21
+ */
5
22
  function createTopology(options) {
6
- const app = options.app.trim();
7
- const prefix = options.prefix.trim();
8
- if (!app || !prefix)
9
- throw new Error('Topology requires both app and prefix');
23
+ const app = segment('app', options.app);
24
+ const prefix = segment('prefix', options.prefix);
10
25
  return {
11
26
  app,
12
27
  prefix,
13
28
  eventsExchange: `${app}.${prefix}.events`,
14
29
  commandsExchange: `${app}.${prefix}.commands`,
15
30
  queue: (name) => (name.startsWith(`${prefix}.`) ? name : `${prefix}.${name}`),
31
+ queueName: (service, purpose) => `${prefix}.${segment('service', service)}.${segment('purpose', purpose, PURPOSE)}`,
32
+ deadLetterExchange: (service) => `${app}.${prefix}.${segment('service', service)}.dlx`,
16
33
  };
17
34
  }
35
+ /** Retry queue for one delay: a fixed TTL, dead-lettering back into `queue`. */
18
36
  const retryQueueName = (queue, delayMs) => `${queue}.retry.${delayMs}`;
19
37
  exports.retryQueueName = retryQueueName;
38
+ /** Dead-letter queue of `queue`. */
20
39
  const deadLetterQueueName = (queue) => `${queue}.dlq`;
21
40
  exports.deadLetterQueueName = deadLetterQueueName;
41
+ /** Smallest delay bucket for {@link delayQueueName}. */
42
+ exports.MIN_DELAY_BUCKET_MS = 1000;
43
+ /**
44
+ * Delay queue for `scheduleDelayed`: one per power-of-two bucket (1s, 2s, 4s …) at or above `delayMs`.
45
+ * Per-message TTLs only expire at the head of a queue, so bucketing bounds how long a short delay can
46
+ * wait behind a longer one (at most its own bucket size).
47
+ *
48
+ * @param queue Target queue (physical name).
49
+ * @param delayMs Requested delay in milliseconds.
50
+ * @returns `{queue}.delay.{bucketMs}`.
51
+ */
52
+ function delayQueueName(queue, delayMs) {
53
+ let bucket = exports.MIN_DELAY_BUCKET_MS;
54
+ while (bucket < delayMs)
55
+ bucket *= 2;
56
+ return `${queue}.delay.${bucket}`;
57
+ }
22
58
  //# sourceMappingURL=topology.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"topology.js","sourceRoot":"","sources":["../../../src/messaging/topology.ts"],"names":[],"mappings":";;;AAiBA,wCAWC;AAXD,SAAgB,cAAc,CAAC,OAAwC;IACrE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACrC,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC9E,OAAO;QACL,GAAG;QACH,MAAM;QACN,cAAc,EAAE,GAAG,GAAG,IAAI,MAAM,SAAS;QACzC,gBAAgB,EAAE,GAAG,GAAG,IAAI,MAAM,WAAW;QAC7C,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC;KACtF,CAAC;AACJ,CAAC;AAEM,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,OAAe,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,OAAO,EAAE,CAAC;AAAjF,QAAA,cAAc,kBAAmE;AACvF,MAAM,mBAAmB,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC;AAAxD,QAAA,mBAAmB,uBAAqC"}
1
+ {"version":3,"file":"topology.js","sourceRoot":"","sources":["../../../src/messaging/topology.ts"],"names":[],"mappings":";;;AAgDA,wCAYC;AAmBD,wCAIC;AApDD,MAAM,OAAO,GAAG,uBAAuB,CAAC;AACxC,MAAM,OAAO,GAAG,wBAAwB,CAAC;AAEzC,SAAS,OAAO,CAAC,KAAa,EAAE,KAAa,EAAE,OAAO,GAAG,OAAO;IAC9D,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IACvB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,YAAY,KAAK,eAAe,OAAO,UAAU,KAAK,IAAI,CAAC,CAAC;IAClG,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,cAAc,CAAC,OAAwB;IACrD,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,OAAO;QACL,GAAG;QACH,MAAM;QACN,cAAc,EAAE,GAAG,GAAG,IAAI,MAAM,SAAS;QACzC,gBAAgB,EAAE,GAAG,GAAG,IAAI,MAAM,WAAW;QAC7C,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC;QACrF,SAAS,EAAE,CAAC,OAAe,EAAE,OAAe,EAAE,EAAE,CAAC,GAAG,MAAM,IAAI,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QACnI,kBAAkB,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,MAAM,IAAI,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM;KAC/F,CAAC;AACJ,CAAC;AAED,gFAAgF;AACzE,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,OAAe,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,OAAO,EAAE,CAAC;AAAjF,QAAA,cAAc,kBAAmE;AAC9F,oCAAoC;AAC7B,MAAM,mBAAmB,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC;AAAxD,QAAA,mBAAmB,uBAAqC;AAErE,wDAAwD;AAC3C,QAAA,mBAAmB,GAAG,IAAI,CAAC;AAExC;;;;;;;;GAQG;AACH,SAAgB,cAAc,CAAC,KAAa,EAAE,OAAe;IAC3D,IAAI,MAAM,GAAG,2BAAmB,CAAC;IACjC,OAAO,MAAM,GAAG,OAAO;QAAE,MAAM,IAAI,CAAC,CAAC;IACrC,OAAO,GAAG,KAAK,UAAU,MAAM,EAAE,CAAC;AACpC,CAAC"}
@@ -1,5 +1,20 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.limiterKey = void 0;
3
18
  exports.createLimiter = createLimiter;
4
19
  exports.consumeOrThrow = consumeOrThrow;
5
20
  exports.rateLimitMiddleware = rateLimitMiddleware;
@@ -7,23 +22,49 @@ const rate_limiter_flexible_1 = require("rate-limiter-flexible");
7
22
  const response_js_1 = require("../common/response.js");
8
23
  const errors_js_1 = require("../common/errors.js");
9
24
  const index_js_1 = require("../logging/index.js");
10
- /** Redis limiter with an in-memory insurance limiter so a Redis outage never blocks traffic. */
25
+ const keyspace_js_1 = require("../cache/keyspace.js");
26
+ const keys_js_1 = require("./keys.js");
27
+ __exportStar(require("./keys.js"), exports);
28
+ /**
29
+ * Redis limiter with an in-memory insurance limiter so a Redis outage never blocks traffic. Its
30
+ * keys come from the keyspace; pass already-encoded keys (see {@link limiterKey}) to `consume`.
31
+ *
32
+ * @param options Points, window, keyspace and scope.
33
+ * @returns The limiter.
34
+ * @example const limiter = createLimiter({ keyspace: ks, scope: 'sign-in-ip', points: 10, durationSec: 60, redis });
35
+ */
11
36
  function createLimiter(options) {
12
- const memory = new rate_limiter_flexible_1.RateLimiterMemory({ keyPrefix: options.keyPrefix, points: options.points, duration: options.durationSec, blockDuration: options.blockDurationSec ?? 0 });
37
+ const keyPrefix = options.keyspace.key(keys_js_1.RATE_LIMIT_PURPOSE, options.scope);
38
+ const memory = new rate_limiter_flexible_1.RateLimiterMemory({ keyPrefix, points: options.points, duration: options.durationSec, blockDuration: options.blockDurationSec ?? 0 });
13
39
  if (!options.redis)
14
40
  return memory;
15
41
  return new rate_limiter_flexible_1.RateLimiterRedis({
16
42
  storeClient: options.redis,
17
- keyPrefix: options.keyPrefix,
43
+ keyPrefix,
18
44
  points: options.points,
19
45
  duration: options.durationSec,
20
46
  blockDuration: options.blockDurationSec ?? 0,
21
47
  insuranceLimiter: memory,
22
48
  });
23
49
  }
50
+ /**
51
+ * Encodes a raw limiter key (user id, IP …) so it cannot add segments to the limiter's Redis key.
52
+ *
53
+ * @param raw Raw identifier.
54
+ * @returns The encoded key part.
55
+ * @example limiter.consume(limiterKey(req.ip))
56
+ */
57
+ const limiterKey = (raw) => (0, keyspace_js_1.encodeKeyPart)(raw);
58
+ exports.limiterKey = limiterKey;
24
59
  /**
25
60
  * Consumes one point for `key`; throws TooManyRequestsError (429) when exhausted.
26
- * For in-service checks such as per-identifier login throttling.
61
+ * For in-service checks such as per-identifier sign-in throttling.
62
+ *
63
+ * @param limiter Limiter from {@link createLimiter}.
64
+ * @param key Encoded key (see {@link limiterKey}).
65
+ * @param message Optional public message for the 429.
66
+ * @throws TooManyRequestsError when the limit is exhausted; rethrows limiter failures.
67
+ * @example await consumeOrThrow(limiter, limiterKey(userId));
27
68
  */
28
69
  async function consumeOrThrow(limiter, key, message) {
29
70
  try {
@@ -35,13 +76,23 @@ async function consumeOrThrow(limiter, key, message) {
35
76
  throw rejection;
36
77
  }
37
78
  }
79
+ /**
80
+ * Express middleware that consumes one point per request. Sets `X-RateLimit-Limit` and
81
+ * `X-RateLimit-Remaining`; when exhausted answers 429 `RATE_LIMITED` with `Retry-After` (seconds).
82
+ * Fails open: a limiter error (not a rejection) is logged and the request continues.
83
+ *
84
+ * @param options Limiter options and `keyOf` (default user id, else client IP).
85
+ * @returns The middleware.
86
+ * @example router.post('/sessions', rateLimitMiddleware({ keyspace: ks, scope: 'sign-in-ip', points: 10, durationSec: 60, redis }), handler);
87
+ */
38
88
  function rateLimitMiddleware(options) {
39
89
  const limiter = createLimiter(options);
40
90
  const logger = options.logger ?? index_js_1.silentLogger;
41
91
  const keyOf = options.keyOf ?? ((req) => req.user?.id ?? req.ip ?? 'unknown');
92
+ const scope = options.scope;
42
93
  return async (req, res, next) => {
43
94
  try {
44
- const result = await limiter.consume(keyOf(req));
95
+ const result = await limiter.consume((0, exports.limiterKey)(keyOf(req)));
45
96
  res.setHeader('X-RateLimit-Limit', options.points);
46
97
  res.setHeader('X-RateLimit-Remaining', result.remainingPoints);
47
98
  next();
@@ -49,7 +100,7 @@ function rateLimitMiddleware(options) {
49
100
  catch (rejection) {
50
101
  const ms = rejection?.msBeforeNext;
51
102
  if (typeof ms !== 'number') {
52
- logger.error('Rate limiter failure; allowing request', { keyPrefix: options.keyPrefix, error: String(rejection) });
103
+ logger.error('Rate limiter failure; allowing request', { scope, error: String(rejection) });
53
104
  next();
54
105
  return;
55
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rate-limit/index.ts"],"names":[],"mappings":";;AAoBA,sCAWC;AAMD,wCAOC;AAOD,kDAsBC;AAvED,iEAAsG;AACtG,uDAAsD;AACtD,mDAA2D;AAC3D,kDAAmD;AAcnD,gGAAgG;AAChG,SAAgB,aAAa,CAAC,OAAyB;IACrD,MAAM,MAAM,GAAG,IAAI,yCAAiB,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,WAAW,EAAE,aAAa,EAAE,OAAO,CAAC,gBAAgB,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5K,IAAI,CAAC,OAAO,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC;IAClC,OAAO,IAAI,wCAAgB,CAAC;QAC1B,WAAW,EAAE,OAAO,CAAC,KAAK;QAC1B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,QAAQ,EAAE,OAAO,CAAC,WAAW;QAC7B,aAAa,EAAE,OAAO,CAAC,gBAAgB,IAAI,CAAC;QAC5C,gBAAgB,EAAE,MAAM;KACzB,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,cAAc,CAAC,OAA4B,EAAE,GAAW,EAAE,OAAgB;IAC9F,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,SAAS,EAAE,CAAC;QACnB,IAAI,OAAQ,SAAuC,EAAE,YAAY,KAAK,QAAQ;YAAE,MAAM,IAAI,gCAAoB,CAAC,OAAO,CAAC,CAAC;QACxH,MAAM,SAAS,CAAC;IAClB,CAAC;AACH,CAAC;AAOD,SAAgB,mBAAmB,CAAC,OAAmC;IACrE,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,uBAAY,CAAC;IAC9C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,GAAY,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,GAAG,CAAC,EAAE,IAAI,SAAS,CAAC,CAAC;IAEvF,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAiB,EAAE;QAC9E,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;YACjD,GAAG,CAAC,SAAS,CAAC,mBAAmB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YACnD,GAAG,CAAC,SAAS,CAAC,uBAAuB,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC;YAC/D,IAAI,EAAE,CAAC;QACT,CAAC;QAAC,OAAO,SAAS,EAAE,CAAC;YACnB,MAAM,EAAE,GAAI,SAAuC,EAAE,YAAY,CAAC;YAClE,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;gBAC3B,MAAM,CAAC,KAAK,CAAC,wCAAwC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;gBACnH,IAAI,EAAE,CAAC;gBACP,OAAO;YACT,CAAC;YACD,GAAG,CAAC,SAAS,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;YACnD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAA,2BAAa,EAAC,4CAA4C,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC;QAC/G,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rate-limit/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RATE_LIMIT_PURPOSE = void 0;
4
+ exports.rateLimitKey = rateLimitKey;
5
+ const index_js_1 = require("../config/index.js");
6
+ const keyspace_js_1 = require("../cache/keyspace.js");
7
+ /**
8
+ * Rate-limit key builder and the sliding-window limiter contract. Dependency-free (exported from the
9
+ * package root as well as `/rate-limit`); the consuming service implements the sliding window.
10
+ */
11
+ /** Purpose segment for every rate-limit key: `{prefix}rl:{scope}:…`. */
12
+ exports.RATE_LIMIT_PURPOSE = 'rl';
13
+ const SCOPE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14
+ const DIMENSIONS = ['ip', 'userId', 'email', 'host', 'route'];
15
+ const DIMENSION_SEGMENT = { ip: 'ip', userId: 'user', email: 'email', host: 'host', route: 'route' };
16
+ function dimensionValue(name, raw) {
17
+ const value = raw.trim();
18
+ if (name === 'email')
19
+ return (0, keyspace_js_1.sensitivePart)(value.toLowerCase());
20
+ if (name === 'host')
21
+ return value.toLowerCase();
22
+ return value;
23
+ }
24
+ /**
25
+ * Builds `{prefix}rl:{scope}[:ip:{ip}][:user:{id}][:email:{hash}][:host:{host}][:route:{route}]`.
26
+ *
27
+ * @param keyspace The service keyspace.
28
+ * @param input Scope plus at least one dimension.
29
+ * @returns The rate-limit key.
30
+ * @throws ConfigError when no dimension is given or the scope is invalid.
31
+ * @example rateLimitKey(ks, { scope: 'sign-in', ip: req.ip, email: body.email })
32
+ */
33
+ function rateLimitKey(keyspace, input) {
34
+ if (!SCOPE_PATTERN.test(input.scope))
35
+ throw new index_js_1.ConfigError(`Rate-limit scope "${input.scope}" must be lower-case kebab-case`);
36
+ const parts = [];
37
+ for (const name of DIMENSIONS) {
38
+ const raw = input[name];
39
+ if (raw === undefined || raw.trim() === '')
40
+ continue;
41
+ parts.push(DIMENSION_SEGMENT[name], dimensionValue(name, raw));
42
+ }
43
+ if (!parts.length)
44
+ throw new index_js_1.ConfigError(`Rate-limit key "${input.scope}" needs at least one of ${DIMENSIONS.join(', ')}`);
45
+ return keyspace.key(exports.RATE_LIMIT_PURPOSE, input.scope, ...parts);
46
+ }
47
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../src/rate-limit/keys.ts"],"names":[],"mappings":";;;AA+CA,oCAUC;AAzDD,iDAAiD;AACjD,sDAA2G;AAE3G;;;GAGG;AAEH,wEAAwE;AAC3D,QAAA,kBAAkB,GAAG,IAAI,CAAC;AAgBvC,MAAM,aAAa,GAAG,4BAA4B,CAAC;AACnD,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAU,CAAC;AAGvE,MAAM,iBAAiB,GAA8B,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAEhI,SAAS,cAAc,CAAC,IAAe,EAAE,GAAW;IAClD,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACzB,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,IAAA,2BAAa,EAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;IAChE,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;IAChD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,YAAY,CAAC,QAAuB,EAAE,KAAwB;IAC5E,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,sBAAW,CAAC,qBAAqB,KAAK,CAAC,KAAK,iCAAiC,CAAC,CAAC;IAC/H,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,SAAS;QACrD,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,MAAM,IAAI,sBAAW,CAAC,mBAAmB,KAAK,CAAC,KAAK,2BAA2B,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC3H,OAAO,QAAQ,CAAC,GAAG,CAAC,0BAAkB,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,CAAC;AACjE,CAAC"}
@@ -1,27 +1,48 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  Object.defineProperty(exports, "__esModule", { value: true });
3
17
  exports.userRoom = void 0;
4
18
  exports.createSocketServer = createSocketServer;
5
19
  exports.gatewaySocketAuth = gatewaySocketAuth;
20
+ exports.createTicketHandshake = createTicketHandshake;
21
+ exports.createTicketSocketServer = createTicketSocketServer;
22
+ const redis_adapter_1 = require("@socket.io/redis-adapter");
6
23
  const socket_io_1 = require("socket.io");
7
24
  const index_js_1 = require("../identity/index.js");
8
25
  const index_js_2 = require("../logging/index.js");
26
+ const ticket_js_1 = require("./ticket.js");
27
+ __exportStar(require("./ticket.js"), exports);
28
+ /** Name of the private room every connection of `userId` joins. */
9
29
  const userRoom = (userId) => `user:${userId}`;
10
30
  exports.userRoom = userRoom;
11
- function createSocketServer(httpServer, options) {
12
- const logger = options.logger ?? index_js_2.silentLogger;
31
+ /** Builds the server: authenticate every handshake, then join only server-derived rooms. */
32
+ function buildServer(httpServer, core) {
33
+ const { logger } = core;
13
34
  const io = new socket_io_1.Server(httpServer, {
14
- path: options.path,
15
- cors: { origin: options.corsOrigins?.length ? options.corsOrigins : false, credentials: true },
16
- ...(options.adapter ? { adapter: options.adapter } : {}),
17
- ...options.serverOptions,
35
+ path: core.path,
36
+ cors: core.cors,
37
+ ...(core.adapter ? { adapter: core.adapter } : {}),
38
+ ...core.serverOptions,
18
39
  });
19
40
  io.use(async (socket, next) => {
20
41
  try {
21
- const user = await options.authenticate(socket);
22
- if (!user)
42
+ const identity = await core.authenticate(socket);
43
+ if (!identity)
23
44
  return next(new Error('Authentication failed'));
24
- socket.data.user = user;
45
+ socket.data.identity = identity;
25
46
  next();
26
47
  }
27
48
  catch (error) {
@@ -30,11 +51,12 @@ function createSocketServer(httpServer, options) {
30
51
  }
31
52
  });
32
53
  io.on('connection', (socket) => {
33
- const user = socket.data.user;
34
- const rooms = options.rooms ? options.rooms(user) : [(0, exports.userRoom)(user.id)];
54
+ const identity = socket.data.identity;
55
+ const userId = core.userIdOf(identity);
56
+ const rooms = core.rooms(identity);
35
57
  void socket.join(rooms);
36
- logger.debug('Socket connected', { userId: user.id, rooms });
37
- socket.on('disconnect', (reason) => logger.debug('Socket disconnected', { userId: user.id, reason }));
58
+ logger.debug('Socket connected', { userId, rooms });
59
+ socket.on('disconnect', (reason) => logger.debug('Socket disconnected', { userId, reason }));
38
60
  });
39
61
  return {
40
62
  io,
@@ -43,11 +65,100 @@ function createSocketServer(httpServer, options) {
43
65
  close: () => new Promise((resolve) => { io.close(() => resolve()); }),
44
66
  };
45
67
  }
46
- /** Trusts the gateway-signed identity headers forwarded on the WebSocket upgrade request. */
47
- function gatewaySocketAuth(secret) {
68
+ /**
69
+ * Creates a Socket.IO server that authenticates every connection and joins it only to server-derived rooms.
70
+ * The verified user is in `socket.data.identity`.
71
+ *
72
+ * @example const sockets = createSocketServer(httpServer, { path: '/ws', authenticate: gatewaySocketAuth({ secret, protocol }) });
73
+ */
74
+ function createSocketServer(httpServer, options) {
75
+ return buildServer(httpServer, {
76
+ path: options.path,
77
+ cors: { origin: options.corsOrigins?.length ? options.corsOrigins : false, credentials: true },
78
+ adapter: options.adapter,
79
+ serverOptions: options.serverOptions,
80
+ logger: options.logger ?? index_js_2.silentLogger,
81
+ authenticate: options.authenticate,
82
+ userIdOf: (user) => user.id,
83
+ rooms: options.rooms ?? ((user) => [(0, exports.userRoom)(user.id)]),
84
+ });
85
+ }
86
+ /**
87
+ * Trusts the gateway-signed (v2) identity headers forwarded on the WebSocket upgrade request. The gateway
88
+ * signs the upgrade as `GET <handshake path>` (query ignored).
89
+ *
90
+ * @param options The shared secret (at least 32 characters) and the app's identity protocol.
91
+ * @returns An `authenticate` function for {@link createSocketServer}.
92
+ * @example createSocketServer(server, { path: '/ws', authenticate: gatewaySocketAuth({ secret, protocol }) });
93
+ */
94
+ function gatewaySocketAuth(options) {
95
+ const secret = (0, index_js_1.assertGatewaySecret)(options.secret);
48
96
  return (socket) => {
49
- const result = (0, index_js_1.verifyIdentityHeaders)(socket.handshake.headers, secret);
97
+ const result = (0, index_js_1.verifyGatewayIdentity)({
98
+ headers: socket.handshake.headers, method: 'GET', path: socket.handshake.url, secret, protocol: options.protocol,
99
+ });
50
100
  return result.status === 'authenticated' ? result.user : null;
51
101
  };
52
102
  }
103
+ /**
104
+ * The handshake check on its own (testable without a server): verifies the ticket from
105
+ * `socket.handshake.auth.ticket`, then the browser origin against `allowedOrigins(claims)`, then
106
+ * single use.
107
+ *
108
+ * @param options Ticket keys, audience, origin rule, replay check and rejection hook.
109
+ * @returns `authenticate(socket)` resolving to the claims, or null when refused.
110
+ */
111
+ function createTicketHandshake(options) {
112
+ const reject = (reason, userId, origin) => {
113
+ options.onRejected?.({ reason, userId, path: options.path, origin });
114
+ return null;
115
+ };
116
+ return async (socket) => {
117
+ const origin = typeof socket.handshake.headers.origin === 'string' ? socket.handshake.headers.origin : null;
118
+ const auth = (socket.handshake.auth ?? {});
119
+ const result = (0, ticket_js_1.verifySocketTicket)(auth.ticket, options);
120
+ if (!result.ok)
121
+ return reject(result.reason, null, origin);
122
+ const { claims } = result;
123
+ if (origin ? !(await options.allowedOrigins(claims)).includes(origin) : !options.allowMissingOrigin) {
124
+ return reject('origin_not_allowed', claims.userId, origin);
125
+ }
126
+ if (options.isFirstUse && !(await options.isFirstUse(claims)))
127
+ return reject('replayed', claims.userId, origin);
128
+ return claims;
129
+ };
130
+ }
131
+ /**
132
+ * Creates a Socket.IO server whose clients authenticate with a short-lived ticket
133
+ * (`io(url, { path, auth: (cb) => getTicket().then((ticket) => cb({ ticket })) })`). Each connection
134
+ * auto-joins `user:{userId}`; the verified claims are in `socket.data.identity` for later room checks
135
+ * with `canJoinScopeRoom`.
136
+ *
137
+ * @param options HTTP server, path, ticket keys, origin rule, Redis + keyspace, rejection hook.
138
+ * @returns The emitter.
139
+ * @example createTicketSocketServer({ httpServer, path: '/ws', secret, domain, audience: 'admin-app', redis, keyspace, allowedOrigins: () => origins, onRejected })
140
+ */
141
+ function createTicketSocketServer(options) {
142
+ const { redis, keyspace } = options;
143
+ if (redis && !keyspace)
144
+ throw new Error('createTicketSocketServer: a keyspace is required with redis');
145
+ const isFirstUse = redis && keyspace
146
+ ? async (claims) => {
147
+ const ttl = Math.max(1, claims.exp - Math.floor(Date.now() / 1000));
148
+ return (await redis.set(keyspace.key('socket-ticket', claims.jti), '1', 'EX', ttl, 'NX')) === 'OK';
149
+ }
150
+ : undefined;
151
+ const authenticate = createTicketHandshake({ ...options, isFirstUse });
152
+ return buildServer(options.httpServer, {
153
+ path: options.path,
154
+ // Browsers do not apply CORS to WebSocket upgrades; the per-ticket origin check above is the real gate.
155
+ cors: { origin: true, credentials: false },
156
+ adapter: redis && keyspace ? (0, redis_adapter_1.createAdapter)(redis, redis.duplicate(), { key: keyspace.key('socket-io') }) : undefined,
157
+ serverOptions: options.serverOptions,
158
+ logger: options.logger ?? index_js_2.silentLogger,
159
+ authenticate,
160
+ userIdOf: (claims) => claims.userId,
161
+ rooms: (claims) => [(0, exports.userRoom)(claims.userId), ...(options.rooms?.(claims) ?? [])],
162
+ });
163
+ }
53
164
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/socket/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/socket/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scopeRoom = exports.MIN_SOCKET_TICKET_SECRET_LENGTH = exports.SOCKET_TICKET_MAX_TTL_SECONDS = void 0;
4
+ exports.signSocketTicket = signSocketTicket;
5
+ exports.verifySocketTicket = verifySocketTicket;
6
+ exports.canJoinScopeRoom = canJoinScopeRoom;
7
+ const index_js_1 = require("../access/index.js");
8
+ const crypto_js_1 = require("../common/crypto.js");
9
+ const id_js_1 = require("../common/id.js");
10
+ /**
11
+ * Short-lived socket handshake tickets. The browser cannot send its in-memory access token on a
12
+ * WebSocket upgrade header, so it asks the API for a ticket (≤ 60 s) and passes it in the Socket.IO
13
+ * `auth` payload. A ticket is `v1.<base64url claims>.<base64url HMAC-SHA256>`, signed over a domain
14
+ * separator so a ticket minted for another app or purpose never verifies here.
15
+ *
16
+ * Dependency-free (no socket.io), so the issuer can sign tickets without the socket stack.
17
+ */
18
+ /** Longest allowed ticket lifetime (and the default), in seconds. */
19
+ exports.SOCKET_TICKET_MAX_TTL_SECONDS = 60;
20
+ /** Shortest accepted signing secret. */
21
+ exports.MIN_SOCKET_TICKET_SECRET_LENGTH = 32;
22
+ const VERSION = 'v1';
23
+ /** Tolerated clock skew for `iat` in the future, in seconds. */
24
+ const MAX_FUTURE_SKEW_SECONDS = 5;
25
+ function assertKey(options) {
26
+ if (!options.secret || options.secret.length < exports.MIN_SOCKET_TICKET_SECRET_LENGTH) {
27
+ throw new Error(`Socket ticket secret must be at least ${exports.MIN_SOCKET_TICKET_SECRET_LENGTH} characters`);
28
+ }
29
+ if (!options.domain)
30
+ throw new Error('Socket tickets need a domain separator');
31
+ }
32
+ const signature = (options, body) => Buffer.from((0, crypto_js_1.hmacSha256)(options.secret, `${options.domain}\n${VERSION}.${body}`), 'hex').toString('base64url');
33
+ const nowSeconds = (options) => Math.floor((options.now?.() ?? new Date()).getTime() / 1000);
34
+ /**
35
+ * Signs a ticket.
36
+ *
37
+ * @param input User, audience, scopes and permissions.
38
+ * @param options Secret, domain separator and `ttlSeconds` (1–60, default 60).
39
+ * @returns The ticket string.
40
+ * @example signSocketTicket({ userId, aud: 'admin-app', scopes: { tenant: [tenantId] }, permissions }, { secret, domain: 'myapp-socket-ticket:v1' })
41
+ */
42
+ function signSocketTicket(input, options) {
43
+ assertKey(options);
44
+ const ttl = options.ttlSeconds ?? exports.SOCKET_TICKET_MAX_TTL_SECONDS;
45
+ if (!Number.isInteger(ttl) || ttl < 1 || ttl > exports.SOCKET_TICKET_MAX_TTL_SECONDS) {
46
+ throw new RangeError(`Socket ticket TTL must be 1–${exports.SOCKET_TICKET_MAX_TTL_SECONDS} seconds`);
47
+ }
48
+ if (!input.userId || !input.aud)
49
+ throw new RangeError('Socket tickets need a user id and an audience');
50
+ const iat = nowSeconds(options);
51
+ const claims = {
52
+ userId: input.userId,
53
+ aud: input.aud,
54
+ scopes: input.scopes ?? {},
55
+ permissions: input.permissions ?? [],
56
+ isSuperAdmin: input.isSuperAdmin === true,
57
+ jti: (0, id_js_1.newId)(),
58
+ iat,
59
+ exp: iat + ttl,
60
+ };
61
+ const body = Buffer.from(JSON.stringify(claims)).toString('base64url');
62
+ return `${VERSION}.${body}.${signature(options, body)}`;
63
+ }
64
+ function parseClaims(body) {
65
+ try {
66
+ const c = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
67
+ const valid = typeof c.userId === 'string' && typeof c.aud === 'string' && typeof c.jti === 'string'
68
+ && Number.isInteger(c.iat) && Number.isInteger(c.exp) && Array.isArray(c.permissions)
69
+ && !!c.scopes && typeof c.scopes === 'object' && typeof c.isSuperAdmin === 'boolean';
70
+ return valid ? c : null;
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Verifies a ticket's signature, domain, lifetime and audience. Never throws for a bad ticket.
78
+ *
79
+ * @param ticket The string from the handshake.
80
+ * @param options Secret, domain separator and the accepted audience(s).
81
+ * @returns The claims, or the rejection reason.
82
+ * @example const result = verifySocketTicket(socket.handshake.auth.ticket, { secret, domain, audience: 'admin-app' });
83
+ */
84
+ function verifySocketTicket(ticket, options) {
85
+ assertKey(options);
86
+ if (typeof ticket !== 'string' || !ticket)
87
+ return { ok: false, reason: 'missing' };
88
+ const parts = ticket.split('.');
89
+ if (parts.length !== 3 || parts[0] !== VERSION || !parts[1] || !parts[2])
90
+ return { ok: false, reason: 'malformed' };
91
+ if (!(0, crypto_js_1.safeEqual)(parts[2], signature(options, parts[1])))
92
+ return { ok: false, reason: 'bad_signature' };
93
+ const claims = parseClaims(parts[1]);
94
+ if (!claims)
95
+ return { ok: false, reason: 'malformed' };
96
+ const now = nowSeconds(options);
97
+ if (claims.exp <= now || claims.exp - claims.iat > exports.SOCKET_TICKET_MAX_TTL_SECONDS || claims.iat > now + MAX_FUTURE_SKEW_SECONDS) {
98
+ return { ok: false, reason: 'expired' };
99
+ }
100
+ const audiences = typeof options.audience === 'string' ? [options.audience] : options.audience;
101
+ if (!audiences.includes(claims.aud))
102
+ return { ok: false, reason: 'wrong_audience' };
103
+ return { ok: true, claims };
104
+ }
105
+ /** Name of the room for one scope id, e.g. `tenant:01J…`. */
106
+ const scopeRoom = (scope, scopeId) => `${scope}:${scopeId}`;
107
+ exports.scopeRoom = scopeRoom;
108
+ /**
109
+ * True when the ticket holder may join the room of `scopeId` in `scope`: the id is in the ticket's scope
110
+ * ids (or the holder is a super admin) and, when given, the holder has `permission`.
111
+ *
112
+ * @param claims Verified ticket claims.
113
+ * @param scope Scope name, e.g. `tenant`.
114
+ * @param scopeId The id whose room is requested.
115
+ * @param permission Permission the room needs (optional).
116
+ * @example canJoinScopeRoom(claims, 'tenant', tenantId, 'widgets.view')
117
+ */
118
+ function canJoinScopeRoom(claims, scope, scopeId, permission) {
119
+ const inScope = claims.isSuperAdmin || (claims.scopes[scope] ?? []).includes(scopeId);
120
+ if (!inScope || !scopeId)
121
+ return false;
122
+ return permission ? (0, index_js_1.hasPermission)(claims, permission) : true;
123
+ }
124
+ //# sourceMappingURL=ticket.js.map