@heavybit/shared-lib 0.0.0-stage → 1.2.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 (310) hide show
  1. package/dist/cjs/access/index.js +118 -0
  2. package/dist/cjs/access/index.js.map +1 -0
  3. package/dist/cjs/alerting/index.js +56 -0
  4. package/dist/cjs/alerting/index.js.map +1 -0
  5. package/dist/cjs/cache/index.js +167 -0
  6. package/dist/cjs/cache/index.js.map +1 -0
  7. package/dist/cjs/common/async.js +40 -0
  8. package/dist/cjs/common/async.js.map +1 -0
  9. package/dist/cjs/common/audit.js +21 -0
  10. package/dist/cjs/common/audit.js.map +1 -0
  11. package/dist/cjs/common/crypto.js +75 -0
  12. package/dist/cjs/common/crypto.js.map +1 -0
  13. package/dist/cjs/common/errors.js +90 -0
  14. package/dist/cjs/common/errors.js.map +1 -0
  15. package/dist/cjs/common/id.js +40 -0
  16. package/dist/cjs/common/id.js.map +1 -0
  17. package/dist/cjs/common/index.js +28 -0
  18. package/dist/cjs/common/index.js.map +1 -0
  19. package/dist/cjs/common/jwt.js +59 -0
  20. package/dist/cjs/common/jwt.js.map +1 -0
  21. package/dist/cjs/common/password.js +38 -0
  22. package/dist/cjs/common/password.js.map +1 -0
  23. package/dist/cjs/common/phone.js +42 -0
  24. package/dist/cjs/common/phone.js.map +1 -0
  25. package/dist/cjs/common/response.js +32 -0
  26. package/dist/cjs/common/response.js.map +1 -0
  27. package/dist/cjs/common/text.js +46 -0
  28. package/dist/cjs/common/text.js.map +1 -0
  29. package/dist/cjs/common/time.js +109 -0
  30. package/dist/cjs/common/time.js.map +1 -0
  31. package/dist/cjs/config/index.js +96 -0
  32. package/dist/cjs/config/index.js.map +1 -0
  33. package/dist/cjs/contracts/editorial-draft.js +111 -0
  34. package/dist/cjs/contracts/editorial-draft.js.map +1 -0
  35. package/dist/cjs/contracts/index.js +20 -0
  36. package/dist/cjs/contracts/index.js.map +1 -0
  37. package/dist/cjs/contracts/notification-dispatch.js +92 -0
  38. package/dist/cjs/contracts/notification-dispatch.js.map +1 -0
  39. package/dist/cjs/contracts/upload-import.js +54 -0
  40. package/dist/cjs/contracts/upload-import.js.map +1 -0
  41. package/dist/cjs/database/index.js +57 -0
  42. package/dist/cjs/database/index.js.map +1 -0
  43. package/dist/cjs/express/auth.js +72 -0
  44. package/dist/cjs/express/auth.js.map +1 -0
  45. package/dist/cjs/express/health.js +31 -0
  46. package/dist/cjs/express/health.js.map +1 -0
  47. package/dist/cjs/express/index.js +21 -0
  48. package/dist/cjs/express/index.js.map +1 -0
  49. package/dist/cjs/express/middleware.js +95 -0
  50. package/dist/cjs/express/middleware.js.map +1 -0
  51. package/dist/cjs/express/security.js +18 -0
  52. package/dist/cjs/express/security.js.map +1 -0
  53. package/dist/cjs/http/index.js +90 -0
  54. package/dist/cjs/http/index.js.map +1 -0
  55. package/dist/cjs/http/safe-fetch.js +199 -0
  56. package/dist/cjs/http/safe-fetch.js.map +1 -0
  57. package/dist/cjs/identity/index.js +117 -0
  58. package/dist/cjs/identity/index.js.map +1 -0
  59. package/dist/cjs/index.js +29 -0
  60. package/dist/cjs/index.js.map +1 -0
  61. package/dist/cjs/knex/index.js +76 -0
  62. package/dist/cjs/knex/index.js.map +1 -0
  63. package/dist/cjs/logging/index.js +75 -0
  64. package/dist/cjs/logging/index.js.map +1 -0
  65. package/dist/cjs/messaging/connection.js +115 -0
  66. package/dist/cjs/messaging/connection.js.map +1 -0
  67. package/dist/cjs/messaging/consumer.js +134 -0
  68. package/dist/cjs/messaging/consumer.js.map +1 -0
  69. package/dist/cjs/messaging/envelope.js +23 -0
  70. package/dist/cjs/messaging/envelope.js.map +1 -0
  71. package/dist/cjs/messaging/idempotency.js +57 -0
  72. package/dist/cjs/messaging/idempotency.js.map +1 -0
  73. package/dist/cjs/messaging/index.js +62 -0
  74. package/dist/cjs/messaging/index.js.map +1 -0
  75. package/dist/cjs/messaging/monitor.js +48 -0
  76. package/dist/cjs/messaging/monitor.js.map +1 -0
  77. package/dist/cjs/messaging/outbox.js +135 -0
  78. package/dist/cjs/messaging/outbox.js.map +1 -0
  79. package/dist/cjs/messaging/publisher.js +149 -0
  80. package/dist/cjs/messaging/publisher.js.map +1 -0
  81. package/dist/cjs/messaging/rpc.js +125 -0
  82. package/dist/cjs/messaging/rpc.js.map +1 -0
  83. package/dist/cjs/messaging/staff-notifier.js +78 -0
  84. package/dist/cjs/messaging/staff-notifier.js.map +1 -0
  85. package/dist/cjs/messaging/topology.js +22 -0
  86. package/dist/cjs/messaging/topology.js.map +1 -0
  87. package/dist/cjs/metrics/index.js +15 -0
  88. package/dist/cjs/metrics/index.js.map +1 -0
  89. package/dist/cjs/package.json +1 -0
  90. package/dist/cjs/rate-limit/index.js +61 -0
  91. package/dist/cjs/rate-limit/index.js.map +1 -0
  92. package/dist/cjs/scheduling/index.js +88 -0
  93. package/dist/cjs/scheduling/index.js.map +1 -0
  94. package/dist/cjs/security/index.js +39 -0
  95. package/dist/cjs/security/index.js.map +1 -0
  96. package/dist/cjs/socket/index.js +53 -0
  97. package/dist/cjs/socket/index.js.map +1 -0
  98. package/dist/cjs/testing/index.js +73 -0
  99. package/dist/cjs/testing/index.js.map +1 -0
  100. package/dist/cjs/types/index.js +3 -0
  101. package/dist/cjs/types/index.js.map +1 -0
  102. package/dist/cjs/validation/index.js +41 -0
  103. package/dist/cjs/validation/index.js.map +1 -0
  104. package/dist/esm/access/index.js +104 -0
  105. package/dist/esm/access/index.js.map +1 -0
  106. package/dist/esm/alerting/index.js +52 -0
  107. package/dist/esm/alerting/index.js.map +1 -0
  108. package/dist/esm/cache/index.js +163 -0
  109. package/dist/esm/cache/index.js.map +1 -0
  110. package/dist/esm/common/async.js +33 -0
  111. package/dist/esm/common/async.js.map +1 -0
  112. package/dist/esm/common/audit.js +18 -0
  113. package/dist/esm/common/audit.js.map +1 -0
  114. package/dist/esm/common/crypto.js +64 -0
  115. package/dist/esm/common/crypto.js.map +1 -0
  116. package/dist/esm/common/errors.js +75 -0
  117. package/dist/esm/common/errors.js.map +1 -0
  118. package/dist/esm/common/id.js +33 -0
  119. package/dist/esm/common/id.js.map +1 -0
  120. package/dist/esm/common/index.js +12 -0
  121. package/dist/esm/common/index.js.map +1 -0
  122. package/dist/esm/common/jwt.js +49 -0
  123. package/dist/esm/common/jwt.js.map +1 -0
  124. package/dist/esm/common/password.js +30 -0
  125. package/dist/esm/common/password.js.map +1 -0
  126. package/dist/esm/common/phone.js +33 -0
  127. package/dist/esm/common/phone.js.map +1 -0
  128. package/dist/esm/common/response.js +24 -0
  129. package/dist/esm/common/response.js.map +1 -0
  130. package/dist/esm/common/text.js +39 -0
  131. package/dist/esm/common/text.js.map +1 -0
  132. package/dist/esm/common/time.js +96 -0
  133. package/dist/esm/common/time.js.map +1 -0
  134. package/dist/esm/config/index.js +88 -0
  135. package/dist/esm/config/index.js.map +1 -0
  136. package/dist/esm/contracts/editorial-draft.js +108 -0
  137. package/dist/esm/contracts/editorial-draft.js.map +1 -0
  138. package/dist/esm/contracts/index.js +4 -0
  139. package/dist/esm/contracts/index.js.map +1 -0
  140. package/dist/esm/contracts/notification-dispatch.js +89 -0
  141. package/dist/esm/contracts/notification-dispatch.js.map +1 -0
  142. package/dist/esm/contracts/upload-import.js +51 -0
  143. package/dist/esm/contracts/upload-import.js.map +1 -0
  144. package/dist/esm/database/index.js +48 -0
  145. package/dist/esm/database/index.js.map +1 -0
  146. package/dist/esm/express/auth.js +63 -0
  147. package/dist/esm/express/auth.js.map +1 -0
  148. package/dist/esm/express/health.js +27 -0
  149. package/dist/esm/express/health.js.map +1 -0
  150. package/dist/esm/express/index.js +5 -0
  151. package/dist/esm/express/index.js.map +1 -0
  152. package/dist/esm/express/middleware.js +84 -0
  153. package/dist/esm/express/middleware.js.map +1 -0
  154. package/dist/esm/express/security.js +11 -0
  155. package/dist/esm/express/security.js.map +1 -0
  156. package/dist/esm/http/index.js +73 -0
  157. package/dist/esm/http/index.js.map +1 -0
  158. package/dist/esm/http/safe-fetch.js +193 -0
  159. package/dist/esm/http/safe-fetch.js.map +1 -0
  160. package/dist/esm/identity/index.js +111 -0
  161. package/dist/esm/identity/index.js.map +1 -0
  162. package/dist/esm/index.js +13 -0
  163. package/dist/esm/index.js.map +1 -0
  164. package/dist/esm/knex/index.js +66 -0
  165. package/dist/esm/knex/index.js.map +1 -0
  166. package/dist/esm/logging/index.js +67 -0
  167. package/dist/esm/logging/index.js.map +1 -0
  168. package/dist/esm/messaging/connection.js +108 -0
  169. package/dist/esm/messaging/connection.js.map +1 -0
  170. package/dist/esm/messaging/consumer.js +130 -0
  171. package/dist/esm/messaging/consumer.js.map +1 -0
  172. package/dist/esm/messaging/envelope.js +19 -0
  173. package/dist/esm/messaging/envelope.js.map +1 -0
  174. package/dist/esm/messaging/idempotency.js +52 -0
  175. package/dist/esm/messaging/idempotency.js.map +1 -0
  176. package/dist/esm/messaging/index.js +45 -0
  177. package/dist/esm/messaging/index.js.map +1 -0
  178. package/dist/esm/messaging/monitor.js +44 -0
  179. package/dist/esm/messaging/monitor.js.map +1 -0
  180. package/dist/esm/messaging/outbox.js +129 -0
  181. package/dist/esm/messaging/outbox.js.map +1 -0
  182. package/dist/esm/messaging/publisher.js +142 -0
  183. package/dist/esm/messaging/publisher.js.map +1 -0
  184. package/dist/esm/messaging/rpc.js +120 -0
  185. package/dist/esm/messaging/rpc.js.map +1 -0
  186. package/dist/esm/messaging/staff-notifier.js +73 -0
  187. package/dist/esm/messaging/staff-notifier.js.map +1 -0
  188. package/dist/esm/messaging/topology.js +16 -0
  189. package/dist/esm/messaging/topology.js.map +1 -0
  190. package/dist/esm/metrics/index.js +6 -0
  191. package/dist/esm/metrics/index.js.map +1 -0
  192. package/dist/esm/package.json +1 -0
  193. package/dist/esm/rate-limit/index.js +56 -0
  194. package/dist/esm/rate-limit/index.js.map +1 -0
  195. package/dist/esm/scheduling/index.js +83 -0
  196. package/dist/esm/scheduling/index.js.map +1 -0
  197. package/dist/esm/security/index.js +36 -0
  198. package/dist/esm/security/index.js.map +1 -0
  199. package/dist/esm/socket/index.js +47 -0
  200. package/dist/esm/socket/index.js.map +1 -0
  201. package/dist/esm/testing/index.js +66 -0
  202. package/dist/esm/testing/index.js.map +1 -0
  203. package/dist/esm/types/index.js +2 -0
  204. package/dist/esm/types/index.js.map +1 -0
  205. package/dist/esm/validation/index.js +36 -0
  206. package/dist/esm/validation/index.js.map +1 -0
  207. package/dist/types/access/index.d.ts +50 -0
  208. package/dist/types/access/index.d.ts.map +1 -0
  209. package/dist/types/alerting/index.d.ts +43 -0
  210. package/dist/types/alerting/index.d.ts.map +1 -0
  211. package/dist/types/cache/index.d.ts +45 -0
  212. package/dist/types/cache/index.d.ts.map +1 -0
  213. package/dist/types/common/async.d.ts +14 -0
  214. package/dist/types/common/async.d.ts.map +1 -0
  215. package/dist/types/common/audit.d.ts +17 -0
  216. package/dist/types/common/audit.d.ts.map +1 -0
  217. package/dist/types/common/crypto.d.ts +18 -0
  218. package/dist/types/common/crypto.d.ts.map +1 -0
  219. package/dist/types/common/errors.d.ts +71 -0
  220. package/dist/types/common/errors.d.ts.map +1 -0
  221. package/dist/types/common/id.d.ts +14 -0
  222. package/dist/types/common/id.d.ts.map +1 -0
  223. package/dist/types/common/index.d.ts +12 -0
  224. package/dist/types/common/index.d.ts.map +1 -0
  225. package/dist/types/common/jwt.d.ts +28 -0
  226. package/dist/types/common/jwt.d.ts.map +1 -0
  227. package/dist/types/common/password.d.ts +4 -0
  228. package/dist/types/common/password.d.ts.map +1 -0
  229. package/dist/types/common/phone.d.ts +10 -0
  230. package/dist/types/common/phone.d.ts.map +1 -0
  231. package/dist/types/common/response.d.ts +12 -0
  232. package/dist/types/common/response.d.ts.map +1 -0
  233. package/dist/types/common/text.d.ts +10 -0
  234. package/dist/types/common/text.d.ts.map +1 -0
  235. package/dist/types/common/time.d.ts +40 -0
  236. package/dist/types/common/time.d.ts.map +1 -0
  237. package/dist/types/config/index.d.ts +115 -0
  238. package/dist/types/config/index.d.ts.map +1 -0
  239. package/dist/types/contracts/editorial-draft.d.ts +577 -0
  240. package/dist/types/contracts/editorial-draft.d.ts.map +1 -0
  241. package/dist/types/contracts/index.d.ts +4 -0
  242. package/dist/types/contracts/index.d.ts.map +1 -0
  243. package/dist/types/contracts/notification-dispatch.d.ts +175 -0
  244. package/dist/types/contracts/notification-dispatch.d.ts.map +1 -0
  245. package/dist/types/contracts/upload-import.d.ts +96 -0
  246. package/dist/types/contracts/upload-import.d.ts.map +1 -0
  247. package/dist/types/database/index.d.ts +25 -0
  248. package/dist/types/database/index.d.ts.map +1 -0
  249. package/dist/types/express/auth.d.ts +27 -0
  250. package/dist/types/express/auth.d.ts.map +1 -0
  251. package/dist/types/express/health.d.ts +14 -0
  252. package/dist/types/express/health.d.ts.map +1 -0
  253. package/dist/types/express/index.d.ts +5 -0
  254. package/dist/types/express/index.d.ts.map +1 -0
  255. package/dist/types/express/middleware.d.ts +20 -0
  256. package/dist/types/express/middleware.d.ts.map +1 -0
  257. package/dist/types/express/security.d.ts +9 -0
  258. package/dist/types/express/security.d.ts.map +1 -0
  259. package/dist/types/http/index.d.ts +34 -0
  260. package/dist/types/http/index.d.ts.map +1 -0
  261. package/dist/types/http/safe-fetch.d.ts +58 -0
  262. package/dist/types/http/safe-fetch.d.ts.map +1 -0
  263. package/dist/types/identity/index.d.ts +51 -0
  264. package/dist/types/identity/index.d.ts.map +1 -0
  265. package/dist/types/index.d.ts +10 -0
  266. package/dist/types/index.d.ts.map +1 -0
  267. package/dist/types/knex/index.d.ts +25 -0
  268. package/dist/types/knex/index.d.ts.map +1 -0
  269. package/dist/types/logging/index.d.ts +22 -0
  270. package/dist/types/logging/index.d.ts.map +1 -0
  271. package/dist/types/messaging/connection.d.ts +48 -0
  272. package/dist/types/messaging/connection.d.ts.map +1 -0
  273. package/dist/types/messaging/consumer.d.ts +56 -0
  274. package/dist/types/messaging/consumer.d.ts.map +1 -0
  275. package/dist/types/messaging/envelope.d.ts +16 -0
  276. package/dist/types/messaging/envelope.d.ts.map +1 -0
  277. package/dist/types/messaging/idempotency.d.ts +29 -0
  278. package/dist/types/messaging/idempotency.d.ts.map +1 -0
  279. package/dist/types/messaging/index.d.ts +39 -0
  280. package/dist/types/messaging/index.d.ts.map +1 -0
  281. package/dist/types/messaging/monitor.d.ts +23 -0
  282. package/dist/types/messaging/monitor.d.ts.map +1 -0
  283. package/dist/types/messaging/outbox.d.ts +51 -0
  284. package/dist/types/messaging/outbox.d.ts.map +1 -0
  285. package/dist/types/messaging/publisher.d.ts +84 -0
  286. package/dist/types/messaging/publisher.d.ts.map +1 -0
  287. package/dist/types/messaging/rpc.d.ts +46 -0
  288. package/dist/types/messaging/rpc.d.ts.map +1 -0
  289. package/dist/types/messaging/staff-notifier.d.ts +56 -0
  290. package/dist/types/messaging/staff-notifier.d.ts.map +1 -0
  291. package/dist/types/messaging/topology.d.ts +23 -0
  292. package/dist/types/messaging/topology.d.ts.map +1 -0
  293. package/dist/types/metrics/index.d.ts +7 -0
  294. package/dist/types/metrics/index.d.ts.map +1 -0
  295. package/dist/types/rate-limit/index.d.ts +28 -0
  296. package/dist/types/rate-limit/index.d.ts.map +1 -0
  297. package/dist/types/scheduling/index.d.ts +33 -0
  298. package/dist/types/scheduling/index.d.ts.map +1 -0
  299. package/dist/types/security/index.d.ts +20 -0
  300. package/dist/types/security/index.d.ts.map +1 -0
  301. package/dist/types/socket/index.d.ts +30 -0
  302. package/dist/types/socket/index.d.ts.map +1 -0
  303. package/dist/types/testing/index.d.ts +41 -0
  304. package/dist/types/testing/index.d.ts.map +1 -0
  305. package/dist/types/types/index.d.ts +65 -0
  306. package/dist/types/types/index.d.ts.map +1 -0
  307. package/dist/types/validation/index.d.ts +34 -0
  308. package/dist/types/validation/index.d.ts.map +1 -0
  309. package/package.json +181 -4
  310. package/README.md +0 -3
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OutboxRelay = exports.DEFAULT_OUTBOX_TABLE = void 0;
4
+ exports.outboxRoutingKey = outboxRoutingKey;
5
+ exports.enqueueOutbox = enqueueOutbox;
6
+ const index_js_1 = require("../logging/index.js");
7
+ const publisher_js_1 = require("./publisher.js");
8
+ exports.DEFAULT_OUTBOX_TABLE = 'event_outbox';
9
+ const COMMAND_PREFIX = 'command:';
10
+ /** Outbox `routing_key` column value: the event type, or `command:<name>` for commands. */
11
+ function outboxRoutingKey(envelope, kind = 'event') {
12
+ return kind === 'command' ? `${COMMAND_PREFIX}${envelope.type}` : envelope.type;
13
+ }
14
+ /**
15
+ * Transactional outbox: write the domain change and the event in one DB transaction, then a
16
+ * relay publishes pending rows with broker confirms. Closes the "row committed, event lost"
17
+ * gap of publish-after-commit. Consumers must be idempotent (they are: see IdempotencyStore).
18
+ *
19
+ * Table (see `createOutboxTable` in `@heavybit/shared-lib/knex`):
20
+ * id CHAR(26) PK = eventId, routing_key, payload JSON, attempts, last_error, created_at, published_at
21
+ */
22
+ async function enqueueOutbox(db, envelope, options = {}) {
23
+ await db(options.table ?? exports.DEFAULT_OUTBOX_TABLE).insert({
24
+ id: envelope.eventId,
25
+ routing_key: outboxRoutingKey(envelope, options.kind),
26
+ payload: JSON.stringify(envelope),
27
+ attempts: 0,
28
+ created_at: new Date(),
29
+ });
30
+ }
31
+ class OutboxRelay {
32
+ options;
33
+ timer = null;
34
+ running = null;
35
+ rerun = false;
36
+ logger;
37
+ table;
38
+ constructor(options) {
39
+ this.options = options;
40
+ this.logger = options.logger ?? index_js_1.silentLogger;
41
+ this.table = options.table ?? exports.DEFAULT_OUTBOX_TABLE;
42
+ }
43
+ start() {
44
+ if (this.timer)
45
+ return;
46
+ this.timer = setInterval(() => { void this.trigger(); }, this.options.intervalMs ?? 2000);
47
+ void this.trigger();
48
+ }
49
+ stop() {
50
+ if (this.timer)
51
+ clearInterval(this.timer);
52
+ this.timer = null;
53
+ }
54
+ /** Publishes pending rows now. Concurrent triggers coalesce into one follow-up run. */
55
+ async trigger() {
56
+ if (this.running) {
57
+ this.rerun = true;
58
+ return;
59
+ }
60
+ do {
61
+ this.rerun = false;
62
+ this.running = this.flush();
63
+ try {
64
+ await this.running;
65
+ }
66
+ finally {
67
+ this.running = null;
68
+ }
69
+ } while (this.rerun);
70
+ }
71
+ /** One batch. Returns how many rows were published. */
72
+ async flush() {
73
+ const { knex, bus } = this.options;
74
+ let published = 0;
75
+ try {
76
+ await knex.transaction(async (trx) => {
77
+ // SKIP LOCKED lets several replicas relay concurrently without double-publishing.
78
+ const rows = await trx(this.table)
79
+ .select('id', 'routing_key', 'payload', 'attempts')
80
+ .whereNull('published_at')
81
+ .orderBy('id', 'asc')
82
+ .limit(this.options.batchSize ?? 100)
83
+ .forUpdate()
84
+ .skipLocked();
85
+ for (const row of rows) {
86
+ const envelope = (typeof row.payload === 'string' ? JSON.parse(row.payload) : row.payload);
87
+ try {
88
+ if (row.routing_key.startsWith(COMMAND_PREFIX))
89
+ await bus.sendEnvelope(envelope);
90
+ else
91
+ await bus.publishEnvelope(envelope);
92
+ await trx(this.table).where({ id: row.id }).update({ published_at: new Date(), last_error: null });
93
+ published += 1;
94
+ }
95
+ catch (error) {
96
+ await trx(this.table).where({ id: row.id }).update({ attempts: knex.raw('attempts + 1'), last_error: String(error.message).slice(0, 500) });
97
+ // Unroutable: only this command's owner is missing. Keep the row for the next tick and carry on,
98
+ // so one undeclared queue never holds back every other event behind it.
99
+ if ((0, publisher_js_1.isUnroutableError)(error)) {
100
+ this.reportUnroutable(row, error);
101
+ continue;
102
+ }
103
+ // Broker down: stop this batch; the next tick retries in order.
104
+ break;
105
+ }
106
+ }
107
+ });
108
+ }
109
+ catch (error) {
110
+ this.logger.error('Outbox relay batch failed', { error: error.message });
111
+ }
112
+ if (published)
113
+ this.logger.debug('Outbox relayed', { published });
114
+ return published;
115
+ }
116
+ /**
117
+ * Logged on the 1st, 2nd, 4th, 8th… failure of a row (the attempt count before this one is in `attempts`),
118
+ * so a queue that stays missing for hours is still visible without a line every tick.
119
+ */
120
+ reportUnroutable(row, error) {
121
+ const attempt = Number(row.attempts ?? 0) + 1;
122
+ if ((attempt & (attempt - 1)) !== 0)
123
+ return;
124
+ this.logger.error('Outbox command is unroutable: its consumer queue is not declared; will retry', {
125
+ eventId: row.id, routingKey: row.routing_key, target: error.routingKey, attempt,
126
+ });
127
+ }
128
+ /** Deletes published rows older than `olderThanDays`. */
129
+ async prune(olderThanDays = 7) {
130
+ const cutoff = new Date(Date.now() - olderThanDays * 86_400_000);
131
+ return this.options.knex(this.table).whereNotNull('published_at').andWhere('published_at', '<', cutoff).delete();
132
+ }
133
+ }
134
+ exports.OutboxRelay = OutboxRelay;
135
+ //# sourceMappingURL=outbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.js","sourceRoot":"","sources":["../../../src/messaging/outbox.ts"],"names":[],"mappings":";;;AAYA,4CAEC;AAUD,sCAQC;AA/BD,kDAAmD;AAEnD,iDAAgH;AAEnG,QAAA,oBAAoB,GAAG,cAAc,CAAC;AAInD,MAAM,cAAc,GAAG,UAAU,CAAC;AAElC,2FAA2F;AAC3F,SAAgB,gBAAgB,CAAC,QAAqC,EAAE,OAAmB,OAAO;IAChG,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,cAAc,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;AAClF,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,aAAa,CAAC,EAA2B,EAAE,QAAuB,EAAE,UAAiD,EAAE;IAC3I,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,IAAI,4BAAoB,CAAC,CAAC,MAAM,CAAC;QACrD,EAAE,EAAE,QAAQ,CAAC,OAAO;QACpB,WAAW,EAAE,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC;QACrD,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;QACjC,QAAQ,EAAE,CAAC;QACX,UAAU,EAAE,IAAI,IAAI,EAAE;KACvB,CAAC,CAAC;AACL,CAAC;AAYD,MAAa,WAAW;IAOO;IANrB,KAAK,GAA0C,IAAI,CAAC;IACpD,OAAO,GAA2B,IAAI,CAAC;IACvC,KAAK,GAAG,KAAK,CAAC;IACL,MAAM,CAAS;IACf,KAAK,CAAS;IAE/B,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;QACtD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,uBAAY,CAAC;QAC7C,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,4BAAoB,CAAC;IACrD,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC;QAC1F,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;IACtB,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,KAAK;YAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,uFAAuF;IACvF,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,OAAO;QACT,CAAC;QACD,GAAG,CAAC;YACF,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;YACnB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YAC5B,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,OAAO,CAAC;YACrB,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACtB,CAAC;QACH,CAAC,QAAQ,IAAI,CAAC,KAAK,EAAE;IACvB,CAAC;IAED,uDAAuD;IACvD,KAAK,CAAC,KAAK;QACT,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QACnC,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;gBACnC,kFAAkF;gBAClF,MAAM,IAAI,GAA2F,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;qBACvH,MAAM,CAAC,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,CAAC;qBAClD,SAAS,CAAC,cAAc,CAAC;qBACzB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;qBACpB,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,GAAG,CAAC;qBACpC,SAAS,EAAE;qBACX,UAAU,EAAE,CAAC;gBAChB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;oBACvB,MAAM,QAAQ,GAAG,CAAC,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAkB,CAAC;oBAC5G,IAAI,CAAC;wBACH,IAAI,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,cAAc,CAAC;4BAAE,MAAM,GAAG,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;;4BAC5E,MAAM,GAAG,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;wBACzC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,IAAI,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;wBACnG,SAAS,IAAI,CAAC,CAAC;oBACjB,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,UAAU,EAAE,MAAM,CAAE,KAAe,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;wBACvJ,iGAAiG;wBACjG,wEAAwE;wBACxE,IAAI,IAAA,gCAAiB,EAAC,KAAK,CAAC,EAAE,CAAC;4BAC7B,IAAI,CAAC,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;4BAClC,SAAS;wBACX,CAAC;wBACD,gEAAgE;wBAChE,MAAM;oBACR,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,EAAE,EAAE,KAAK,EAAG,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;QACtF,CAAC;QACD,IAAI,SAAS;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;QAClE,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;;OAGG;IACK,gBAAgB,CAAC,GAA2D,EAAE,KAA6B;QACjH,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC9C,IAAI,CAAC,OAAO,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO;QAC5C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,8EAA8E,EAAE;YAChG,OAAO,EAAE,GAAG,CAAC,EAAE,EAAE,UAAU,EAAE,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,OAAO;SAChF,CAAC,CAAC;IACL,CAAC;IAED,yDAAyD;IACzD,KAAK,CAAC,KAAK,CAAC,aAAa,GAAG,CAAC;QAC3B,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,aAAa,GAAG,UAAU,CAAC,CAAC;QACjE,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC;IACnH,CAAC;CACF;AAlGD,kCAkGC"}
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NoopMessageBus = exports.MessagePublisher = exports.UnroutableMessageError = void 0;
4
+ exports.isUnroutableError = isUnroutableError;
5
+ const errors_js_1 = require("../common/errors.js");
6
+ const id_js_1 = require("../common/id.js");
7
+ const index_js_1 = require("../logging/index.js");
8
+ const envelope_js_1 = require("./envelope.js");
9
+ /**
10
+ * The broker accepted a `mandatory` message but no queue was bound to receive it (the consumer has never
11
+ * started, or its binding is wrong). Retryable: the message is not lost — the caller (or the outbox relay)
12
+ * keeps it and tries again, and it goes through once the owner declares its queue.
13
+ */
14
+ class UnroutableMessageError extends errors_js_1.ServiceUnavailableError {
15
+ exchange;
16
+ routingKey;
17
+ constructor(exchange, routingKey) {
18
+ super(`No queue is bound for ${exchange || '(default exchange)'} → ${routingKey}`, { code: 'UNROUTABLE' });
19
+ this.exchange = exchange;
20
+ this.routingKey = routingKey;
21
+ }
22
+ }
23
+ exports.UnroutableMessageError = UnroutableMessageError;
24
+ function isUnroutableError(error) {
25
+ return error instanceof UnroutableMessageError;
26
+ }
27
+ const envelopeProps = (envelope, mandatory) => ({
28
+ mandatory, messageId: envelope.eventId, correlationId: envelope.correlationId, type: envelope.type,
29
+ });
30
+ /**
31
+ * Event and command publisher on a confirm channel: `publish()` resolves only once the broker
32
+ * has persisted the message, and rejects with 503 when the broker is unreachable so callers
33
+ * (or the outbox relay) can retry.
34
+ *
35
+ * Commands and direct queue sends are `mandatory`: a command has exactly one owner, so one that no
36
+ * queue receives is a fault, not a no-op. The broker returns it (`basic.return`, always before its
37
+ * confirm), and the send rejects with `UnroutableMessageError` instead of resolving as if delivered.
38
+ * Events stay non-mandatory: a topic event with no subscriber yet is normal fan-out.
39
+ */
40
+ class MessagePublisher {
41
+ options;
42
+ handle = null;
43
+ logger;
44
+ /** Message ids the broker returned as unroutable, awaiting their confirm. */
45
+ returned = new Set();
46
+ constructor(options) {
47
+ this.options = options;
48
+ this.logger = options.logger ?? index_js_1.silentLogger;
49
+ }
50
+ async init() {
51
+ const { topology } = this.options;
52
+ this.handle = await this.options.connection.registerChannel('publisher', async (channel) => {
53
+ await channel.assertExchange(topology.eventsExchange, 'topic', { durable: true });
54
+ await channel.assertExchange(topology.commandsExchange, 'direct', { durable: true });
55
+ channel.on('return', (message) => {
56
+ const id = message.properties.messageId;
57
+ if (typeof id === 'string')
58
+ this.returned.add(id);
59
+ });
60
+ }, { confirm: true });
61
+ }
62
+ async publish(type, data, meta = {}) {
63
+ const envelope = (0, envelope_js_1.buildEnvelope)(type, data, this.options.source, meta);
64
+ await this.publishEnvelope(envelope);
65
+ return envelope;
66
+ }
67
+ async publishEnvelope(envelope) {
68
+ await this.write(this.options.topology.eventsExchange, envelope.type, envelope, envelopeProps(envelope, false));
69
+ }
70
+ async send(command, data, meta = {}) {
71
+ const envelope = (0, envelope_js_1.buildEnvelope)(command, data, this.options.source, meta);
72
+ await this.sendEnvelope(envelope);
73
+ return envelope;
74
+ }
75
+ async sendEnvelope(envelope) {
76
+ await this.write(this.options.topology.commandsExchange, this.options.topology.queue(envelope.type), envelope, envelopeProps(envelope, true));
77
+ }
78
+ /**
79
+ * Sends `payload` as-is (no envelope) to `queue` through the default exchange. The queue is the
80
+ * consumer's to declare, so it is not asserted here; a missing queue rejects as unroutable.
81
+ */
82
+ async sendToQueue(queue, payload, options = {}) {
83
+ await this.write('', queue, payload, { mandatory: true, messageId: options.messageId ?? (0, id_js_1.newId)(), correlationId: options.correlationId });
84
+ }
85
+ write(exchange, routingKey, body, props) {
86
+ const channel = this.handle?.current();
87
+ if (!channel) {
88
+ return Promise.reject(new errors_js_1.ServiceUnavailableError('Message broker unavailable'));
89
+ }
90
+ const { messageId } = props;
91
+ return new Promise((resolve, reject) => {
92
+ channel.publish(exchange, routingKey, Buffer.from(JSON.stringify(body)), {
93
+ persistent: true,
94
+ mandatory: props.mandatory,
95
+ contentType: 'application/json',
96
+ messageId,
97
+ correlationId: props.correlationId,
98
+ type: props.type,
99
+ timestamp: Math.floor(Date.now() / 1000),
100
+ appId: this.options.source,
101
+ }, (error) => {
102
+ // The broker sends `basic.return` before the confirm, so a returned message is already recorded here.
103
+ const unroutable = this.returned.delete(messageId);
104
+ if (error) {
105
+ this.logger.error('Broker rejected message', { routingKey, messageId, error: String(error) });
106
+ reject(new errors_js_1.ServiceUnavailableError('Message broker rejected the message', { cause: error }));
107
+ }
108
+ else if (unroutable) {
109
+ this.logger.warn('Broker could not route message: no queue is bound', { exchange, routingKey, messageId });
110
+ reject(new UnroutableMessageError(exchange, routingKey));
111
+ }
112
+ else {
113
+ resolve();
114
+ }
115
+ });
116
+ });
117
+ }
118
+ }
119
+ exports.MessagePublisher = MessagePublisher;
120
+ /** Stand-in when messaging is disabled (local tooling, tests): logs and drops. */
121
+ class NoopMessageBus {
122
+ source;
123
+ logger;
124
+ constructor(source, logger = index_js_1.silentLogger) {
125
+ this.source = source;
126
+ this.logger = logger;
127
+ }
128
+ async publish(type, data, meta = {}) {
129
+ const envelope = (0, envelope_js_1.buildEnvelope)(type, data, this.source, meta);
130
+ this.logger.debug('Messaging disabled; event dropped', { type, eventId: envelope.eventId });
131
+ return envelope;
132
+ }
133
+ async publishEnvelope(envelope) {
134
+ this.logger.debug('Messaging disabled; event dropped', { type: envelope.type, eventId: envelope.eventId });
135
+ }
136
+ async send(command, data, meta = {}) {
137
+ const envelope = (0, envelope_js_1.buildEnvelope)(command, data, this.source, meta);
138
+ this.logger.debug('Messaging disabled; command dropped', { command, eventId: envelope.eventId });
139
+ return envelope;
140
+ }
141
+ async sendEnvelope(envelope) {
142
+ this.logger.debug('Messaging disabled; command dropped', { command: envelope.type, eventId: envelope.eventId });
143
+ }
144
+ async sendToQueue(queue) {
145
+ this.logger.debug('Messaging disabled; queue message dropped', { queue });
146
+ }
147
+ }
148
+ exports.NoopMessageBus = NoopMessageBus;
149
+ //# sourceMappingURL=publisher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publisher.js","sourceRoot":"","sources":["../../../src/messaging/publisher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RpcClient = exports.RpcServer = void 0;
4
+ const errors_js_1 = require("../common/errors.js");
5
+ const id_js_1 = require("../common/id.js");
6
+ const index_js_1 = require("../logging/index.js");
7
+ const REPLY_TO = 'amq.rabbitmq.reply-to';
8
+ /**
9
+ * Request/response over RabbitMQ for the few synchronous cross-service calls that must fail
10
+ * cleanly (e.g. "create the profile before committing the account"). Uses direct reply-to, so
11
+ * no reply queue is declared per call, and sets the request TTL to the caller's timeout so a
12
+ * request the caller has given up on is never executed late.
13
+ */
14
+ class RpcServer {
15
+ connection;
16
+ topology;
17
+ handles = [];
18
+ logger;
19
+ constructor(connection, topology, logger) {
20
+ this.connection = connection;
21
+ this.topology = topology;
22
+ this.logger = logger ?? index_js_1.silentLogger;
23
+ }
24
+ async handle(method, handler, options = {}) {
25
+ const queue = this.topology.queue(method);
26
+ this.handles.push(await this.connection.registerChannel(`rpc-server:${queue}`, async (channel) => {
27
+ await channel.assertQueue(queue, { durable: true });
28
+ await channel.prefetch(options.prefetch ?? 5);
29
+ await channel.consume(queue, (msg) => { if (msg)
30
+ void this.respond(channel, msg, handler); });
31
+ }));
32
+ this.logger.info('RPC method registered', { queue });
33
+ }
34
+ async respond(channel, msg, handler) {
35
+ let reply;
36
+ try {
37
+ const payload = JSON.parse(msg.content.toString('utf8'));
38
+ const data = await handler(payload, { correlationId: msg.properties.correlationId, callerService: msg.properties.appId });
39
+ reply = { ok: true, data };
40
+ }
41
+ catch (error) {
42
+ const status = (0, errors_js_1.isAppError)(error) ? error.statusCode : 500;
43
+ const message = (0, errors_js_1.isAppError)(error) && status < 500 ? error.message : 'RPC handler failed';
44
+ if (status >= 500)
45
+ this.logger.error('RPC handler error', { queue: msg.fields.routingKey, error: error.message });
46
+ reply = { ok: false, error: { status, code: (0, errors_js_1.isAppError)(error) ? error.code : 'INTERNAL_ERROR', message } };
47
+ }
48
+ if (msg.properties.replyTo) {
49
+ channel.sendToQueue(msg.properties.replyTo, Buffer.from(JSON.stringify(reply)), { correlationId: msg.properties.correlationId, contentType: 'application/json' });
50
+ }
51
+ channel.ack(msg);
52
+ }
53
+ }
54
+ exports.RpcServer = RpcServer;
55
+ class RpcClient {
56
+ connection;
57
+ topology;
58
+ callerService;
59
+ handle = null;
60
+ pending = new Map();
61
+ logger;
62
+ constructor(connection, topology, callerService, logger) {
63
+ this.connection = connection;
64
+ this.topology = topology;
65
+ this.callerService = callerService;
66
+ this.logger = logger ?? index_js_1.silentLogger;
67
+ }
68
+ async init() {
69
+ this.handle = await this.connection.registerChannel('rpc-client', async (channel) => {
70
+ // A reconnect invalidates every in-flight reply.
71
+ this.failAll(new errors_js_1.ServiceUnavailableError('RPC channel reset'));
72
+ await channel.consume(REPLY_TO, (msg) => { if (msg)
73
+ this.onReply(msg); }, { noAck: true });
74
+ });
75
+ }
76
+ call(method, payload, options = {}) {
77
+ const channel = this.handle?.current();
78
+ if (!channel)
79
+ return Promise.reject(new errors_js_1.ServiceUnavailableError('Message broker unavailable'));
80
+ const timeoutMs = options.timeoutMs ?? 5000;
81
+ const id = (0, id_js_1.newId)();
82
+ return new Promise((resolve, reject) => {
83
+ const timer = setTimeout(() => {
84
+ this.pending.delete(id);
85
+ this.logger.warn('RPC timed out', { method, timeoutMs });
86
+ reject(new errors_js_1.ServiceUnavailableError(`Upstream did not respond in time (${method})`));
87
+ }, timeoutMs);
88
+ this.pending.set(id, { resolve: resolve, reject, timer });
89
+ channel.sendToQueue(this.topology.queue(method), Buffer.from(JSON.stringify(payload)), {
90
+ correlationId: id,
91
+ replyTo: REPLY_TO,
92
+ contentType: 'application/json',
93
+ expiration: String(timeoutMs),
94
+ appId: this.callerService,
95
+ headers: options.correlationId ? { 'x-correlation-id': options.correlationId } : undefined,
96
+ });
97
+ });
98
+ }
99
+ onReply(msg) {
100
+ const entry = this.pending.get(msg.properties.correlationId);
101
+ if (!entry)
102
+ return;
103
+ this.pending.delete(msg.properties.correlationId);
104
+ clearTimeout(entry.timer);
105
+ try {
106
+ const reply = JSON.parse(msg.content.toString('utf8'));
107
+ if (reply.ok)
108
+ entry.resolve(reply.data);
109
+ else
110
+ entry.reject(new errors_js_1.AppError(reply.error.status, reply.error.message, { code: reply.error.code }));
111
+ }
112
+ catch (error) {
113
+ entry.reject(new errors_js_1.ServiceUnavailableError('Malformed RPC reply', { cause: error }));
114
+ }
115
+ }
116
+ failAll(error) {
117
+ for (const [id, entry] of this.pending) {
118
+ clearTimeout(entry.timer);
119
+ entry.reject(error);
120
+ this.pending.delete(id);
121
+ }
122
+ }
123
+ }
124
+ exports.RpcClient = RpcClient;
125
+ //# sourceMappingURL=rpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.js","sourceRoot":"","sources":["../../../src/messaging/rpc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.staffEmailListSchema = exports.StaffNotifier = exports.ConfiguredStaffRecipients = void 0;
4
+ const zod_1 = require("zod");
5
+ const notification_dispatch_js_1 = require("../contracts/notification-dispatch.js");
6
+ const index_js_1 = require("../logging/index.js");
7
+ /** Recipients from configuration (env), keyed by the permission an alert targets. */
8
+ class ConfiguredStaffRecipients {
9
+ byPermission;
10
+ constructor(byPermission) {
11
+ this.byPermission = byPermission;
12
+ }
13
+ async resolve(permission) {
14
+ return [...(this.byPermission[permission] ?? [])];
15
+ }
16
+ }
17
+ exports.ConfiguredStaffRecipients = ConfiguredStaffRecipients;
18
+ /**
19
+ * Stages one `notification:dispatch` per recipient through the outbox, in the caller's transaction, so an
20
+ * alert is sent if and only if the state change that raised it commits. Never sends email itself.
21
+ * An alert that resolves to nobody is logged as a warning (an unconfigured list must not fail silently).
22
+ */
23
+ class StaffNotifier {
24
+ outbox;
25
+ recipients;
26
+ logger;
27
+ constructor(outbox, recipients, logger = index_js_1.silentLogger) {
28
+ this.outbox = outbox;
29
+ this.recipients = recipients;
30
+ this.logger = logger;
31
+ }
32
+ /** Returns how many commands were staged. */
33
+ async alert(executor, alert, meta = {}) {
34
+ const targets = await this.recipients.resolve(alert.permission);
35
+ if (targets.length === 0) {
36
+ this.logger.warn('Staff alert not sent: no recipients are configured', { template: alert.template, permission: alert.permission });
37
+ return 0;
38
+ }
39
+ for (const recipient of targets) {
40
+ const dispatch = {
41
+ template: alert.template,
42
+ category: 'staff',
43
+ recipient,
44
+ channels: alert.channels ?? ['email'],
45
+ data: alert.data,
46
+ bypassPreferences: true,
47
+ targetPermission: alert.permission,
48
+ };
49
+ await this.outbox.command(executor, notification_dispatch_js_1.NOTIFICATION_DISPATCH, dispatch, { ...meta, actorId: null });
50
+ }
51
+ return targets.length;
52
+ }
53
+ }
54
+ exports.StaffNotifier = StaffNotifier;
55
+ const emailSchema = zod_1.z.string().email().max(notification_dispatch_js_1.DISPATCH_LIMITS.email);
56
+ /**
57
+ * A comma-separated staff address list from env (`"a@x.io, b@y.io"`): trimmed, blanks dropped,
58
+ * de-duplicated case-insensitively. An invalid address fails validation, so a typo is caught at boot
59
+ * instead of dead-lettering in notification-service when the alert finally fires.
60
+ */
61
+ exports.staffEmailListSchema = zod_1.z
62
+ .string()
63
+ .optional()
64
+ .transform((csv) => {
65
+ const seen = new Set();
66
+ return (csv ?? '')
67
+ .split(',')
68
+ .map((entry) => entry.trim())
69
+ .filter((email) => email && !seen.has(email.toLowerCase()) && seen.add(email.toLowerCase()));
70
+ })
71
+ .superRefine((emails, ctx) => {
72
+ for (const email of emails) {
73
+ if (!emailSchema.safeParse(email).success)
74
+ ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Not a valid email address: ${email}` });
75
+ }
76
+ })
77
+ .transform((emails) => emails.map((email) => ({ email })));
78
+ //# sourceMappingURL=staff-notifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staff-notifier.js","sourceRoot":"","sources":["../../../src/messaging/staff-notifier.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deadLetterQueueName = exports.retryQueueName = void 0;
4
+ exports.createTopology = createTopology;
5
+ 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');
10
+ return {
11
+ app,
12
+ prefix,
13
+ eventsExchange: `${app}.${prefix}.events`,
14
+ commandsExchange: `${app}.${prefix}.commands`,
15
+ queue: (name) => (name.startsWith(`${prefix}.`) ? name : `${prefix}.${name}`),
16
+ };
17
+ }
18
+ const retryQueueName = (queue, delayMs) => `${queue}.retry.${delayMs}`;
19
+ exports.retryQueueName = retryQueueName;
20
+ const deadLetterQueueName = (queue) => `${queue}.dlq`;
21
+ exports.deadLetterQueueName = deadLetterQueueName;
22
+ //# sourceMappingURL=topology.js.map
@@ -0,0 +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"}
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectDefaultMetrics = exports.Summary = exports.Registry = exports.Histogram = exports.Gauge = exports.Counter = void 0;
4
+ /**
5
+ * Prometheus metrics. Re-exports prom-client so every service runs one version of it;
6
+ * services keep their own metric definitions behind a narrow interface.
7
+ */
8
+ var prom_client_1 = require("prom-client");
9
+ Object.defineProperty(exports, "Counter", { enumerable: true, get: function () { return prom_client_1.Counter; } });
10
+ Object.defineProperty(exports, "Gauge", { enumerable: true, get: function () { return prom_client_1.Gauge; } });
11
+ Object.defineProperty(exports, "Histogram", { enumerable: true, get: function () { return prom_client_1.Histogram; } });
12
+ Object.defineProperty(exports, "Registry", { enumerable: true, get: function () { return prom_client_1.Registry; } });
13
+ Object.defineProperty(exports, "Summary", { enumerable: true, get: function () { return prom_client_1.Summary; } });
14
+ Object.defineProperty(exports, "collectDefaultMetrics", { enumerable: true, get: function () { return prom_client_1.collectDefaultMetrics; } });
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/metrics/index.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACH,2CAAkG;AAAzF,sGAAA,OAAO,OAAA;AAAE,oGAAA,KAAK,OAAA;AAAE,wGAAA,SAAS,OAAA;AAAE,uGAAA,QAAQ,OAAA;AAAE,sGAAA,OAAO,OAAA;AAAE,oHAAA,qBAAqB,OAAA"}
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLimiter = createLimiter;
4
+ exports.consumeOrThrow = consumeOrThrow;
5
+ exports.rateLimitMiddleware = rateLimitMiddleware;
6
+ const rate_limiter_flexible_1 = require("rate-limiter-flexible");
7
+ const response_js_1 = require("../common/response.js");
8
+ const errors_js_1 = require("../common/errors.js");
9
+ const index_js_1 = require("../logging/index.js");
10
+ /** Redis limiter with an in-memory insurance limiter so a Redis outage never blocks traffic. */
11
+ 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 });
13
+ if (!options.redis)
14
+ return memory;
15
+ return new rate_limiter_flexible_1.RateLimiterRedis({
16
+ storeClient: options.redis,
17
+ keyPrefix: options.keyPrefix,
18
+ points: options.points,
19
+ duration: options.durationSec,
20
+ blockDuration: options.blockDurationSec ?? 0,
21
+ insuranceLimiter: memory,
22
+ });
23
+ }
24
+ /**
25
+ * Consumes one point for `key`; throws TooManyRequestsError (429) when exhausted.
26
+ * For in-service checks such as per-identifier login throttling.
27
+ */
28
+ async function consumeOrThrow(limiter, key, message) {
29
+ try {
30
+ await limiter.consume(key);
31
+ }
32
+ catch (rejection) {
33
+ if (typeof rejection?.msBeforeNext === 'number')
34
+ throw new errors_js_1.TooManyRequestsError(message);
35
+ throw rejection;
36
+ }
37
+ }
38
+ function rateLimitMiddleware(options) {
39
+ const limiter = createLimiter(options);
40
+ const logger = options.logger ?? index_js_1.silentLogger;
41
+ const keyOf = options.keyOf ?? ((req) => req.user?.id ?? req.ip ?? 'unknown');
42
+ return async (req, res, next) => {
43
+ try {
44
+ const result = await limiter.consume(keyOf(req));
45
+ res.setHeader('X-RateLimit-Limit', options.points);
46
+ res.setHeader('X-RateLimit-Remaining', result.remainingPoints);
47
+ next();
48
+ }
49
+ catch (rejection) {
50
+ const ms = rejection?.msBeforeNext;
51
+ if (typeof ms !== 'number') {
52
+ logger.error('Rate limiter failure; allowing request', { keyPrefix: options.keyPrefix, error: String(rejection) });
53
+ next();
54
+ return;
55
+ }
56
+ res.setHeader('Retry-After', Math.ceil(ms / 1000));
57
+ res.status(429).json((0, response_js_1.errorResponse)('Too many requests. Please try again later.', undefined, 'RATE_LIMITED'));
58
+ }
59
+ };
60
+ }
61
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}