@bgord/bun 1.21.11 → 1.22.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 (303) hide show
  1. package/dist/alert-channel-composite.adapter.d.ts.map +1 -1
  2. package/dist/alert-channel-composite.adapter.js +1 -0
  3. package/dist/alert-channel-composite.adapter.js.map +1 -1
  4. package/dist/api-version.middleware.d.ts +1 -0
  5. package/dist/api-version.middleware.d.ts.map +1 -1
  6. package/dist/api-version.middleware.js +3 -1
  7. package/dist/api-version.middleware.js.map +1 -1
  8. package/dist/cache-codec-identity.strategy.d.ts +7 -0
  9. package/dist/cache-codec-identity.strategy.d.ts.map +1 -0
  10. package/dist/cache-codec-identity.strategy.js +9 -0
  11. package/dist/cache-codec-identity.strategy.js.map +1 -0
  12. package/dist/cache-codec-sitemap-entries.strategy.d.ts +11 -0
  13. package/dist/cache-codec-sitemap-entries.strategy.d.ts.map +1 -0
  14. package/dist/cache-codec-sitemap-entries.strategy.js +13 -0
  15. package/dist/cache-codec-sitemap-entries.strategy.js.map +1 -0
  16. package/dist/cache-codec.strategy.d.ts +6 -0
  17. package/dist/cache-codec.strategy.d.ts.map +1 -0
  18. package/dist/cache-codec.strategy.js +2 -0
  19. package/dist/cache-codec.strategy.js.map +1 -0
  20. package/dist/cache-repository-node-cache.adapter.d.ts +3 -2
  21. package/dist/cache-repository-node-cache.adapter.d.ts.map +1 -1
  22. package/dist/cache-repository-node-cache.adapter.js +6 -4
  23. package/dist/cache-repository-node-cache.adapter.js.map +1 -1
  24. package/dist/cache-repository-noop.adapter.d.ts +3 -2
  25. package/dist/cache-repository-noop.adapter.d.ts.map +1 -1
  26. package/dist/cache-repository-noop.adapter.js.map +1 -1
  27. package/dist/cache-repository-redis.adapter.d.ts +3 -2
  28. package/dist/cache-repository-redis.adapter.d.ts.map +1 -1
  29. package/dist/cache-repository-redis.adapter.js +8 -5
  30. package/dist/cache-repository-redis.adapter.js.map +1 -1
  31. package/dist/cache-repository.port.d.ts +3 -2
  32. package/dist/cache-repository.port.d.ts.map +1 -1
  33. package/dist/cache-resolver-noop.strategy.d.ts +8 -0
  34. package/dist/cache-resolver-noop.strategy.d.ts.map +1 -0
  35. package/dist/cache-resolver-noop.strategy.js +7 -0
  36. package/dist/cache-resolver-noop.strategy.js.map +1 -0
  37. package/dist/cache-resolver-read-through.strategy.d.ts +15 -0
  38. package/dist/cache-resolver-read-through.strategy.d.ts.map +1 -0
  39. package/dist/cache-resolver-read-through.strategy.js +21 -0
  40. package/dist/cache-resolver-read-through.strategy.js.map +1 -0
  41. package/dist/cache-resolver.strategy.d.ts +2 -9
  42. package/dist/cache-resolver.strategy.d.ts.map +1 -1
  43. package/dist/cache-resolver.strategy.js +1 -5
  44. package/dist/cache-resolver.strategy.js.map +1 -1
  45. package/dist/cache-value.vo.d.ts +6 -0
  46. package/dist/cache-value.vo.d.ts.map +1 -0
  47. package/dist/cache-value.vo.js +2 -0
  48. package/dist/cache-value.vo.js.map +1 -0
  49. package/dist/correlation-id.vo.d.ts.map +1 -1
  50. package/dist/correlation-id.vo.js +1 -0
  51. package/dist/correlation-id.vo.js.map +1 -1
  52. package/dist/cron-scheduler-with-logger.adapter.d.ts +3 -0
  53. package/dist/cron-scheduler-with-logger.adapter.d.ts.map +1 -1
  54. package/dist/cron-scheduler-with-logger.adapter.js +29 -7
  55. package/dist/cron-scheduler-with-logger.adapter.js.map +1 -1
  56. package/dist/cron-task-handler-with-logger.strategy.js +1 -1
  57. package/dist/cron-task-handler-with-logger.strategy.js.map +1 -1
  58. package/dist/crypto-key-provider-with-memo.adapter.d.ts +12 -0
  59. package/dist/crypto-key-provider-with-memo.adapter.d.ts.map +1 -0
  60. package/dist/crypto-key-provider-with-memo.adapter.js +17 -0
  61. package/dist/crypto-key-provider-with-memo.adapter.js.map +1 -0
  62. package/dist/environment-loader-process-safe.adapter.d.ts +3 -1
  63. package/dist/environment-loader-process-safe.adapter.d.ts.map +1 -1
  64. package/dist/environment-loader-process-safe.adapter.js +4 -2
  65. package/dist/environment-loader-process-safe.adapter.js.map +1 -1
  66. package/dist/event-store-with-logger.adapter.d.ts +3 -0
  67. package/dist/event-store-with-logger.adapter.d.ts.map +1 -1
  68. package/dist/event-store-with-logger.adapter.js +88 -27
  69. package/dist/event-store-with-logger.adapter.js.map +1 -1
  70. package/dist/event-stream.vo.d.ts.map +1 -1
  71. package/dist/event-stream.vo.js +3 -1
  72. package/dist/event-stream.vo.js.map +1 -1
  73. package/dist/file-draft-tar-gz.service.d.ts +8 -0
  74. package/dist/file-draft-tar-gz.service.d.ts.map +1 -0
  75. package/dist/file-draft-tar-gz.service.js +19 -0
  76. package/dist/file-draft-tar-gz.service.js.map +1 -0
  77. package/dist/file-reader-json-with-cache.adapter.d.ts +1 -0
  78. package/dist/file-reader-json-with-cache.adapter.d.ts.map +1 -1
  79. package/dist/file-reader-json-with-cache.adapter.js +3 -1
  80. package/dist/file-reader-json-with-cache.adapter.js.map +1 -1
  81. package/dist/file-reader-text-with-cache.adapter.d.ts +1 -0
  82. package/dist/file-reader-text-with-cache.adapter.d.ts.map +1 -1
  83. package/dist/file-reader-text-with-cache.adapter.js +3 -1
  84. package/dist/file-reader-text-with-cache.adapter.js.map +1 -1
  85. package/dist/healthcheck-hono.handler.js +2 -2
  86. package/dist/healthcheck-hono.handler.js.map +1 -1
  87. package/dist/healthcheck.handler.d.ts +1 -0
  88. package/dist/healthcheck.handler.d.ts.map +1 -1
  89. package/dist/healthcheck.handler.js +1 -0
  90. package/dist/healthcheck.handler.js.map +1 -1
  91. package/dist/http-logger-hono.middleware.d.ts.map +1 -1
  92. package/dist/http-logger-hono.middleware.js +0 -3
  93. package/dist/http-logger-hono.middleware.js.map +1 -1
  94. package/dist/http-logger.middleware.d.ts +0 -1
  95. package/dist/http-logger.middleware.d.ts.map +1 -1
  96. package/dist/http-logger.middleware.js +0 -1
  97. package/dist/http-logger.middleware.js.map +1 -1
  98. package/dist/idempotency-store-cache.adapter.d.ts +13 -0
  99. package/dist/idempotency-store-cache.adapter.d.ts.map +1 -0
  100. package/dist/idempotency-store-cache.adapter.js +14 -0
  101. package/dist/idempotency-store-cache.adapter.js.map +1 -0
  102. package/dist/idempotency-store-noop.adapter.d.ts +6 -0
  103. package/dist/idempotency-store-noop.adapter.d.ts.map +1 -0
  104. package/dist/idempotency-store-noop.adapter.js +6 -0
  105. package/dist/idempotency-store-noop.adapter.js.map +1 -0
  106. package/dist/idempotency-store-with-logger.adapter.d.ts +17 -0
  107. package/dist/idempotency-store-with-logger.adapter.d.ts.map +1 -0
  108. package/dist/idempotency-store-with-logger.adapter.js +48 -0
  109. package/dist/idempotency-store-with-logger.adapter.js.map +1 -0
  110. package/dist/idempotency-store.port.d.ts +5 -0
  111. package/dist/idempotency-store.port.d.ts.map +1 -0
  112. package/dist/idempotency-store.port.js +2 -0
  113. package/dist/idempotency-store.port.js.map +1 -0
  114. package/dist/image-generator-web-view.adapter.js +8 -72
  115. package/dist/image-generator-web-view.adapter.js.map +1 -1
  116. package/dist/index.d.ts +19 -6
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +19 -6
  119. package/dist/index.js.map +1 -1
  120. package/dist/job-queue-with-logger.adapter.d.ts +2 -0
  121. package/dist/job-queue-with-logger.adapter.d.ts.map +1 -1
  122. package/dist/job-queue-with-logger.adapter.js +133 -36
  123. package/dist/job-queue-with-logger.adapter.js.map +1 -1
  124. package/dist/logger.port.d.ts +0 -1
  125. package/dist/logger.port.d.ts.map +1 -1
  126. package/dist/mailer-content-html.vo.js +1 -1
  127. package/dist/mailer-content-html.vo.js.map +1 -1
  128. package/dist/message-bus-with-logger.adapter.d.ts +2 -0
  129. package/dist/message-bus-with-logger.adapter.d.ts.map +1 -1
  130. package/dist/message-bus-with-logger.adapter.js +28 -8
  131. package/dist/message-bus-with-logger.adapter.js.map +1 -1
  132. package/dist/modules/preferences/invariants/user-language-has-changed.d.ts +0 -1
  133. package/dist/modules/preferences/invariants/user-language-has-changed.d.ts.map +1 -1
  134. package/dist/modules/preferences/invariants/user-language-has-changed.js +0 -4
  135. package/dist/modules/preferences/invariants/user-language-has-changed.js.map +1 -1
  136. package/dist/modules/system/invariants/feature-flag-enabled.d.ts +0 -1
  137. package/dist/modules/system/invariants/feature-flag-enabled.d.ts.map +1 -1
  138. package/dist/modules/system/invariants/feature-flag-enabled.js +0 -4
  139. package/dist/modules/system/invariants/feature-flag-enabled.js.map +1 -1
  140. package/dist/prerequisite-verifier-sqlite.adapter.js +1 -1
  141. package/dist/prerequisite-verifier-sqlite.adapter.js.map +1 -1
  142. package/dist/prerequisite-verifier-with-cache.adapter.d.ts +2 -0
  143. package/dist/prerequisite-verifier-with-cache.adapter.d.ts.map +1 -1
  144. package/dist/prerequisite-verifier-with-cache.adapter.js +8 -5
  145. package/dist/prerequisite-verifier-with-cache.adapter.js.map +1 -1
  146. package/dist/rate-limiter.service.d.ts +22 -0
  147. package/dist/rate-limiter.service.d.ts.map +1 -0
  148. package/dist/rate-limiter.service.js +31 -0
  149. package/dist/rate-limiter.service.js.map +1 -0
  150. package/dist/reactive-config-with-cache.adapter.d.ts +3 -1
  151. package/dist/reactive-config-with-cache.adapter.d.ts.map +1 -1
  152. package/dist/reactive-config-with-cache.adapter.js +3 -1
  153. package/dist/reactive-config-with-cache.adapter.js.map +1 -1
  154. package/dist/request-context-hono.adapter.d.ts.map +1 -1
  155. package/dist/request-context-hono.adapter.js +4 -1
  156. package/dist/request-context-hono.adapter.js.map +1 -1
  157. package/dist/secure-key-generator-crypto.adapter.js +1 -1
  158. package/dist/secure-key-generator-crypto.adapter.js.map +1 -1
  159. package/dist/security-rule-violation-threshold.strategy.d.ts.map +1 -1
  160. package/dist/security-rule-violation-threshold.strategy.js +5 -3
  161. package/dist/security-rule-violation-threshold.strategy.js.map +1 -1
  162. package/dist/shield-rate-limit-hono.strategy.d.ts +2 -2
  163. package/dist/shield-rate-limit-hono.strategy.d.ts.map +1 -1
  164. package/dist/shield-rate-limit.strategy.d.ts +2 -2
  165. package/dist/shield-rate-limit.strategy.d.ts.map +1 -1
  166. package/dist/shield-rate-limit.strategy.js +8 -2
  167. package/dist/shield-rate-limit.strategy.js.map +1 -1
  168. package/dist/shield-webhook-hono.strategy.d.ts +2 -2
  169. package/dist/shield-webhook-hono.strategy.d.ts.map +1 -1
  170. package/dist/shield-webhook-hono.strategy.js +7 -3
  171. package/dist/shield-webhook-hono.strategy.js.map +1 -1
  172. package/dist/shield-webhook.strategy.d.ts +23 -3
  173. package/dist/shield-webhook.strategy.d.ts.map +1 -1
  174. package/dist/shield-webhook.strategy.js +18 -4
  175. package/dist/shield-webhook.strategy.js.map +1 -1
  176. package/dist/sitemap-entries-provider-with-cache.adapter.d.ts +2 -0
  177. package/dist/sitemap-entries-provider-with-cache.adapter.d.ts.map +1 -1
  178. package/dist/sitemap-entries-provider-with-cache.adapter.js +8 -5
  179. package/dist/sitemap-entries-provider-with-cache.adapter.js.map +1 -1
  180. package/dist/sitemap-entry.vo.d.ts +11 -10
  181. package/dist/sitemap-entry.vo.d.ts.map +1 -1
  182. package/dist/sitemap-entry.vo.js +19 -0
  183. package/dist/sitemap-entry.vo.js.map +1 -1
  184. package/dist/smtp-port.vo.d.ts.map +1 -1
  185. package/dist/smtp-port.vo.js +1 -0
  186. package/dist/smtp-port.vo.js.map +1 -1
  187. package/dist/sse-registry-with-logger.adapter.d.ts +2 -0
  188. package/dist/sse-registry-with-logger.adapter.d.ts.map +1 -1
  189. package/dist/sse-registry-with-logger.adapter.js +80 -22
  190. package/dist/sse-registry-with-logger.adapter.js.map +1 -1
  191. package/dist/translations-provider-with-cache.adapter.d.ts +21 -0
  192. package/dist/translations-provider-with-cache.adapter.d.ts.map +1 -0
  193. package/dist/translations-provider-with-cache.adapter.js +22 -0
  194. package/dist/translations-provider-with-cache.adapter.js.map +1 -0
  195. package/dist/uuid.vo.js +1 -0
  196. package/dist/uuid.vo.js.map +1 -1
  197. package/dist/webhook-id-extractor-header.strategy.d.ts +9 -0
  198. package/dist/webhook-id-extractor-header.strategy.d.ts.map +1 -0
  199. package/dist/webhook-id-extractor-header.strategy.js +13 -0
  200. package/dist/webhook-id-extractor-header.strategy.js.map +1 -0
  201. package/dist/webhook-id-extractor-json-field.strategy.d.ts +9 -0
  202. package/dist/webhook-id-extractor-json-field.strategy.d.ts.map +1 -0
  203. package/dist/webhook-id-extractor-json-field.strategy.js +14 -0
  204. package/dist/webhook-id-extractor-json-field.strategy.js.map +1 -0
  205. package/dist/webhook-id-extractor.strategy.d.ts +6 -0
  206. package/dist/webhook-id-extractor.strategy.d.ts.map +1 -0
  207. package/dist/webhook-id-extractor.strategy.js +2 -0
  208. package/dist/webhook-id-extractor.strategy.js.map +1 -0
  209. package/dist/webhook-id.vo.d.ts +9 -0
  210. package/dist/webhook-id.vo.d.ts.map +1 -0
  211. package/dist/webhook-id.vo.js +10 -0
  212. package/dist/webhook-id.vo.js.map +1 -0
  213. package/dist/{webhook-signature-extractor-header-exact.strategy.d.ts → webhook-signature-extractor-header.strategy.d.ts} +2 -2
  214. package/dist/webhook-signature-extractor-header.strategy.d.ts.map +1 -0
  215. package/dist/{webhook-signature-extractor-header-exact.strategy.js → webhook-signature-extractor-header.strategy.js} +2 -2
  216. package/dist/webhook-signature-extractor-header.strategy.js.map +1 -0
  217. package/package.json +12 -18
  218. package/readme.md +19 -6
  219. package/src/alert-channel-composite.adapter.ts +1 -0
  220. package/src/api-version.middleware.ts +6 -2
  221. package/src/cache-codec-identity.strategy.ts +12 -0
  222. package/src/cache-codec-sitemap-entries.strategy.ts +17 -0
  223. package/src/cache-codec.strategy.ts +6 -0
  224. package/src/cache-repository-node-cache.adapter.ts +9 -5
  225. package/src/cache-repository-noop.adapter.ts +3 -2
  226. package/src/cache-repository-redis.adapter.ts +10 -7
  227. package/src/cache-repository.port.ts +3 -2
  228. package/src/cache-resolver-noop.strategy.ts +11 -0
  229. package/src/cache-resolver-read-through.strategy.ts +27 -0
  230. package/src/cache-resolver.strategy.ts +2 -11
  231. package/src/cache-value.vo.ts +9 -0
  232. package/src/correlation-id.vo.ts +1 -0
  233. package/src/cron-scheduler-with-logger.adapter.ts +35 -9
  234. package/src/cron-task-handler-with-logger.strategy.ts +1 -1
  235. package/src/crypto-key-provider-with-memo.adapter.ts +21 -0
  236. package/src/environment-loader-process-safe.adapter.ts +12 -4
  237. package/src/event-store-with-logger.adapter.ts +97 -27
  238. package/src/event-stream.vo.ts +1 -0
  239. package/src/file-draft-tar-gz.service.ts +23 -0
  240. package/src/file-reader-json-with-cache.adapter.ts +7 -2
  241. package/src/file-reader-text-with-cache.adapter.ts +8 -1
  242. package/src/healthcheck-hono.handler.ts +2 -2
  243. package/src/healthcheck.handler.ts +2 -0
  244. package/src/http-logger-hono.middleware.ts +0 -3
  245. package/src/http-logger.middleware.ts +0 -2
  246. package/src/idempotency-store-cache.adapter.ts +19 -0
  247. package/src/idempotency-store-noop.adapter.ts +8 -0
  248. package/src/idempotency-store-with-logger.adapter.ts +59 -0
  249. package/src/idempotency-store.port.ts +5 -0
  250. package/src/index.ts +19 -6
  251. package/src/job-queue-with-logger.adapter.ts +156 -43
  252. package/src/logger.port.ts +0 -1
  253. package/src/message-bus-with-logger.adapter.ts +37 -10
  254. package/src/modules/preferences/invariants/user-language-has-changed.ts +1 -6
  255. package/src/modules/system/invariants/feature-flag-enabled.ts +1 -6
  256. package/src/prerequisite-verifier-sqlite.adapter.ts +1 -1
  257. package/src/prerequisite-verifier-with-cache.adapter.ts +13 -7
  258. package/src/rate-limiter.service.ts +45 -0
  259. package/src/reactive-config-with-cache.adapter.ts +8 -2
  260. package/src/request-context-hono.adapter.ts +5 -1
  261. package/src/security-rule-violation-threshold.strategy.ts +5 -6
  262. package/src/shield-rate-limit-hono.strategy.ts +2 -2
  263. package/src/shield-rate-limit.strategy.ts +14 -7
  264. package/src/shield-webhook-hono.strategy.ts +11 -3
  265. package/src/shield-webhook.strategy.ts +41 -6
  266. package/src/sitemap-entries-provider-with-cache.adapter.ts +13 -7
  267. package/src/sitemap-entry.vo.ts +23 -11
  268. package/src/smtp-port.vo.ts +1 -0
  269. package/src/sse-registry-with-logger.adapter.ts +94 -25
  270. package/src/translations-provider-with-cache.adapter.ts +38 -0
  271. package/src/uuid.vo.ts +1 -1
  272. package/src/webhook-id-extractor-header.strategy.ts +14 -0
  273. package/src/webhook-id-extractor-json-field.strategy.ts +16 -0
  274. package/src/webhook-id-extractor.strategy.ts +6 -0
  275. package/src/webhook-id.vo.ts +17 -0
  276. package/src/{webhook-signature-extractor-header-exact.strategy.ts → webhook-signature-extractor-header.strategy.ts} +1 -1
  277. package/dist/cache-resolver-simple.strategy.d.ts +0 -18
  278. package/dist/cache-resolver-simple.strategy.d.ts.map +0 -1
  279. package/dist/cache-resolver-simple.strategy.js +0 -23
  280. package/dist/cache-resolver-simple.strategy.js.map +0 -1
  281. package/dist/cache-response-hono.middleware.d.ts +0 -9
  282. package/dist/cache-response-hono.middleware.d.ts.map +0 -1
  283. package/dist/cache-response-hono.middleware.js +0 -34
  284. package/dist/cache-response-hono.middleware.js.map +0 -1
  285. package/dist/cache-response.middleware.d.ts +0 -28
  286. package/dist/cache-response.middleware.d.ts.map +0 -1
  287. package/dist/cache-response.middleware.js +0 -20
  288. package/dist/cache-response.middleware.js.map +0 -1
  289. package/dist/crypto-key-provider-with-cache.adapter.d.ts +0 -19
  290. package/dist/crypto-key-provider-with-cache.adapter.d.ts.map +0 -1
  291. package/dist/crypto-key-provider-with-cache.adapter.js +0 -19
  292. package/dist/crypto-key-provider-with-cache.adapter.js.map +0 -1
  293. package/dist/file-draft-zip.service.d.ts +0 -14
  294. package/dist/file-draft-zip.service.d.ts.map +0 -1
  295. package/dist/file-draft-zip.service.js +0 -41
  296. package/dist/file-draft-zip.service.js.map +0 -1
  297. package/dist/webhook-signature-extractor-header-exact.strategy.d.ts.map +0 -1
  298. package/dist/webhook-signature-extractor-header-exact.strategy.js.map +0 -1
  299. package/src/cache-resolver-simple.strategy.ts +0 -33
  300. package/src/cache-response-hono.middleware.ts +0 -53
  301. package/src/cache-response.middleware.ts +0 -47
  302. package/src/crypto-key-provider-with-cache.adapter.ts +0 -30
  303. package/src/file-draft-zip.service.ts +0 -56
@@ -1,12 +1,18 @@
1
1
  import type * as tools from "@bgord/tools";
2
+ import type { ClockPort } from "./clock.port";
2
3
  import { CorrelationStorage } from "./correlation-storage.service";
3
4
  import type { GenericJob } from "./job.types";
4
5
  import type { JobQueuePort } from "./job-queue.port";
5
6
  import type { JobHandler } from "./job-registry.port";
6
7
  import type { JobRetryPolicyStrategy } from "./job-retry-policy.strategy";
7
8
  import type { LoggerPort } from "./logger.port";
9
+ import { Stopwatch } from "./stopwatch.service";
8
10
 
9
- type Dependencies<Job extends GenericJob> = { inner: JobQueuePort<Job>; Logger: LoggerPort };
11
+ type Dependencies<Job extends GenericJob> = {
12
+ inner: JobQueuePort<Job>;
13
+ Logger: LoggerPort;
14
+ Clock: ClockPort;
15
+ };
10
16
 
11
17
  export class JobQueueWithLoggerAdapter<Job extends GenericJob> implements JobQueuePort<Job> {
12
18
  private readonly base = { component: "infra", operation: "job_queue" };
@@ -14,49 +20,135 @@ export class JobQueueWithLoggerAdapter<Job extends GenericJob> implements JobQue
14
20
  constructor(private readonly deps: Dependencies<Job>) {}
15
21
 
16
22
  async enqueue<EnqueuedJob extends Job>(job: EnqueuedJob, delay?: tools.Duration): Promise<EnqueuedJob> {
17
- this.deps.Logger.info({
18
- message: `${job.name} enqueued`,
19
- metadata: job,
20
- correlationId: CorrelationStorage.get(),
21
- ...this.base,
22
- });
23
-
24
- return this.deps.inner.enqueue(job, delay);
23
+ const duration = new Stopwatch(this.deps);
24
+
25
+ try {
26
+ this.deps.Logger.info({
27
+ message: "Job queue enqueue attempt",
28
+ correlationId: CorrelationStorage.get(),
29
+ metadata: { job, delay },
30
+ ...this.base,
31
+ });
32
+
33
+ const result = await this.deps.inner.enqueue(job, delay);
34
+
35
+ this.deps.Logger.info({
36
+ message: "Job queue enqueue success",
37
+ correlationId: CorrelationStorage.get(),
38
+ metadata: { job, delay, duration: duration.stop() },
39
+ ...this.base,
40
+ });
41
+
42
+ return result;
43
+ } catch (error) {
44
+ this.deps.Logger.error({
45
+ message: "Job queue enqueue error",
46
+ correlationId: CorrelationStorage.get(),
47
+ error,
48
+ metadata: { job, delay, duration: duration.stop() },
49
+ ...this.base,
50
+ });
51
+
52
+ throw error;
53
+ }
25
54
  }
26
55
 
27
56
  async claim(limit: tools.IntegerPositiveType): Promise<ReadonlyArray<Job>> {
28
- const jobs = await this.deps.inner.claim(limit);
29
-
30
- this.deps.Logger.info({
31
- message: `Claimed ${jobs.length} job(s)`,
32
- metadata: { count: jobs.length, limit, jobs },
33
- correlationId: CorrelationStorage.get(),
34
- ...this.base,
35
- });
36
-
37
- return jobs;
57
+ const duration = new Stopwatch(this.deps);
58
+
59
+ try {
60
+ this.deps.Logger.info({
61
+ message: "Job queue claim attempt",
62
+ correlationId: CorrelationStorage.get(),
63
+ metadata: { limit },
64
+ ...this.base,
65
+ });
66
+
67
+ const jobs = await this.deps.inner.claim(limit);
68
+
69
+ this.deps.Logger.info({
70
+ message: "Job queue claim success",
71
+ correlationId: CorrelationStorage.get(),
72
+ metadata: { limit, count: jobs.length, jobs, duration: duration.stop() },
73
+ ...this.base,
74
+ });
75
+
76
+ return jobs;
77
+ } catch (error) {
78
+ this.deps.Logger.error({
79
+ message: "Job queue claim error",
80
+ correlationId: CorrelationStorage.get(),
81
+ error,
82
+ metadata: { limit, duration: duration.stop() },
83
+ ...this.base,
84
+ });
85
+
86
+ throw error;
87
+ }
38
88
  }
39
89
 
40
90
  async complete(id: GenericJob["id"]): Promise<void> {
41
- this.deps.Logger.info({
42
- message: "Job completed",
43
- metadata: { id },
44
- correlationId: CorrelationStorage.get(),
45
- ...this.base,
46
- });
47
-
48
- return this.deps.inner.complete(id);
91
+ const duration = new Stopwatch(this.deps);
92
+
93
+ try {
94
+ this.deps.Logger.info({
95
+ message: "Job queue complete attempt",
96
+ correlationId: CorrelationStorage.get(),
97
+ metadata: { id },
98
+ ...this.base,
99
+ });
100
+
101
+ await this.deps.inner.complete(id);
102
+
103
+ this.deps.Logger.info({
104
+ message: "Job queue complete success",
105
+ correlationId: CorrelationStorage.get(),
106
+ metadata: { id, duration: duration.stop() },
107
+ ...this.base,
108
+ });
109
+ } catch (error) {
110
+ this.deps.Logger.error({
111
+ message: "Job queue complete error",
112
+ correlationId: CorrelationStorage.get(),
113
+ error,
114
+ metadata: { id, duration: duration.stop() },
115
+ ...this.base,
116
+ });
117
+
118
+ throw error;
119
+ }
49
120
  }
50
121
 
51
122
  async fail(id: GenericJob["id"]): Promise<void> {
52
- this.deps.Logger.warn({
53
- message: "Job failed",
54
- metadata: { id },
55
- correlationId: CorrelationStorage.get(),
56
- ...this.base,
57
- });
58
-
59
- return this.deps.inner.fail(id);
123
+ const duration = new Stopwatch(this.deps);
124
+
125
+ try {
126
+ this.deps.Logger.warn({
127
+ message: "Job queue fail attempt",
128
+ correlationId: CorrelationStorage.get(),
129
+ metadata: { id },
130
+ ...this.base,
131
+ });
132
+
133
+ await this.deps.inner.fail(id);
134
+
135
+ this.deps.Logger.warn({
136
+ message: "Job queue fail success",
137
+ correlationId: CorrelationStorage.get(),
138
+ metadata: { id, duration: duration.stop() },
139
+ ...this.base,
140
+ });
141
+ } catch (error) {
142
+ this.deps.Logger.error({
143
+ message: "Job queue fail error",
144
+ correlationId: CorrelationStorage.get(),
145
+ error,
146
+ metadata: { id, duration: duration.stop() },
147
+ ...this.base,
148
+ });
149
+
150
+ throw error;
151
+ }
60
152
  }
61
153
 
62
154
  async requeue(
@@ -64,14 +156,35 @@ export class JobQueueWithLoggerAdapter<Job extends GenericJob> implements JobQue
64
156
  revision: GenericJob["revision"],
65
157
  delay: tools.Duration,
66
158
  ): Promise<void> {
67
- this.deps.Logger.warn({
68
- message: "Job requeued",
69
- metadata: { id, revision, delay },
70
- correlationId: CorrelationStorage.get(),
71
- ...this.base,
72
- });
73
-
74
- return this.deps.inner.requeue(id, revision, delay);
159
+ const duration = new Stopwatch(this.deps);
160
+
161
+ try {
162
+ this.deps.Logger.warn({
163
+ message: "Job queue requeue attempt",
164
+ correlationId: CorrelationStorage.get(),
165
+ metadata: { id, revision, delay },
166
+ ...this.base,
167
+ });
168
+
169
+ await this.deps.inner.requeue(id, revision, delay);
170
+
171
+ this.deps.Logger.warn({
172
+ message: "Job queue requeue success",
173
+ correlationId: CorrelationStorage.get(),
174
+ metadata: { id, revision, delay, duration: duration.stop() },
175
+ ...this.base,
176
+ });
177
+ } catch (error) {
178
+ this.deps.Logger.error({
179
+ message: "Job queue requeue error",
180
+ correlationId: CorrelationStorage.get(),
181
+ error,
182
+ metadata: { id, revision, delay, duration: duration.stop() },
183
+ ...this.base,
184
+ });
185
+
186
+ throw error;
187
+ }
75
188
  }
76
189
 
77
190
  getRetryPolicy(name: GenericJob["name"]): JobRetryPolicyStrategy {
@@ -35,7 +35,6 @@ export type LogHttpType = LogCoreType & {
35
35
  status?: number;
36
36
  ms?: tools.DurationMsType;
37
37
  client: HttpClientInfo;
38
- cacheHit?: boolean;
39
38
  };
40
39
 
41
40
  export type LogErrorType = LogCoreType & { level: LogLevelEnum.error; error?: unknown };
@@ -1,24 +1,51 @@
1
+ import type { ClockPort } from "./clock.port";
1
2
  import { CorrelationStorage } from "./correlation-storage.service";
2
3
  import type { LoggerPort } from "./logger.port";
3
4
  import type { Message, ToMessageMap } from "./message.types";
4
5
  import type { MessageBusPort } from "./message-bus.port";
6
+ import { Stopwatch } from "./stopwatch.service";
5
7
 
6
- type Dependencies<Messages extends Message> = { inner: MessageBusPort<Messages>; Logger: LoggerPort };
8
+ type Dependencies<Messages extends Message> = {
9
+ inner: MessageBusPort<Messages>;
10
+ Logger: LoggerPort;
11
+ Clock: ClockPort;
12
+ };
7
13
 
8
14
  export class MessageBusWithLoggerAdapter<Messages extends Message> implements MessageBusPort<Messages> {
9
- private readonly base = { component: "infra", operation: "message_bus_emit" };
15
+ private readonly base = { component: "infra", operation: "message_bus" };
10
16
 
11
17
  constructor(private readonly deps: Dependencies<Messages>) {}
12
18
 
13
19
  async emit<M extends Messages>(message: M): Promise<void> {
14
- this.deps.Logger.info({
15
- message: `${message.name} emitted`,
16
- metadata: message,
17
- correlationId: CorrelationStorage.get(),
18
- ...this.base,
19
- });
20
-
21
- return this.deps.inner.emit(message);
20
+ const duration = new Stopwatch(this.deps);
21
+
22
+ try {
23
+ this.deps.Logger.info({
24
+ message: "Message bus emit attempt",
25
+ correlationId: CorrelationStorage.get(),
26
+ metadata: { message },
27
+ ...this.base,
28
+ });
29
+
30
+ await this.deps.inner.emit(message);
31
+
32
+ this.deps.Logger.info({
33
+ message: "Message bus emit success",
34
+ correlationId: CorrelationStorage.get(),
35
+ metadata: { message, duration: duration.stop() },
36
+ ...this.base,
37
+ });
38
+ } catch (error) {
39
+ this.deps.Logger.error({
40
+ message: "Message bus emit error",
41
+ correlationId: CorrelationStorage.get(),
42
+ error,
43
+ metadata: { message, duration: duration.stop() },
44
+ ...this.base,
45
+ });
46
+
47
+ throw error;
48
+ }
22
49
  }
23
50
 
24
51
  on<MessageName extends keyof ToMessageMap<Messages>>(
@@ -3,12 +3,7 @@ import { Invariant, InvariantFailureKind } from "../../../invariant.service";
3
3
 
4
4
  type Config = { current?: tools.LanguageType | null; candidate: tools.LanguageType };
5
5
 
6
- class UserLanguageHasChangedError extends Error {
7
- constructor() {
8
- super();
9
- Object.setPrototypeOf(this, UserLanguageHasChangedError.prototype);
10
- }
11
- }
6
+ class UserLanguageHasChangedError extends Error {}
12
7
 
13
8
  class UserLanguageHasChangedFactory extends Invariant<Config> {
14
9
  passes(config: Config) {
@@ -1,12 +1,7 @@
1
1
  import * as tools from "@bgord/tools";
2
2
  import { Invariant, InvariantFailureKind } from "../../../invariant.service";
3
3
 
4
- class FeatureFlagEnabledError extends Error {
5
- constructor() {
6
- super();
7
- Object.setPrototypeOf(this, FeatureFlagEnabledError.prototype);
8
- }
9
- }
4
+ class FeatureFlagEnabledError extends Error {}
10
5
 
11
6
  type FeatureFlagEnabledConfigType = { flag: tools.FeatureFlagValueType };
12
7
 
@@ -17,7 +17,7 @@ export class PrerequisiteVerifierSQLiteAdapter implements PrerequisiteVerifierPo
17
17
  | { integrity_check?: string }
18
18
  | undefined;
19
19
 
20
- if (!integrity?.integrity_check || integrity.integrity_check.toLowerCase() !== "ok") {
20
+ if (integrity?.integrity_check?.toLowerCase() !== "ok") {
21
21
  return PrerequisiteVerification.failure("Integrity check failed");
22
22
  }
23
23
  return PrerequisiteVerification.success;
@@ -1,3 +1,4 @@
1
+ import { CacheCodecIdentityStrategy } from "./cache-codec-identity.strategy";
1
2
  import type { CacheResolverStrategy } from "./cache-resolver.strategy";
2
3
  import type { HashContentStrategy } from "./hash-content.strategy";
3
4
  import type { PrerequisiteVerificationResult, PrerequisiteVerifierPort } from "./prerequisite-verifier.port";
@@ -8,13 +9,14 @@ type Dependencies = { CacheResolver: CacheResolverStrategy; HashContent: HashCon
8
9
  type Config = { id: string; inner: PrerequisiteVerifierPort };
9
10
 
10
11
  export class PrerequisiteVerifierWithCacheAdapter implements PrerequisiteVerifierPort {
12
+ private readonly codec = new CacheCodecIdentityStrategy<PrerequisiteVerificationResult>();
13
+ private readonly resolver: SubjectApplicationResolver;
14
+
11
15
  constructor(
12
16
  private readonly config: Config,
13
17
  private readonly deps: Dependencies,
14
- ) {}
15
-
16
- async verify(): Promise<PrerequisiteVerificationResult> {
17
- const resolver = new SubjectApplicationResolver(
18
+ ) {
19
+ this.resolver = new SubjectApplicationResolver(
18
20
  [
19
21
  new SubjectSegmentFixedStrategy("prerequisite_verifier"),
20
22
  new SubjectSegmentFixedStrategy(this.kind),
@@ -22,11 +24,15 @@ export class PrerequisiteVerifierWithCacheAdapter implements PrerequisiteVerifie
22
24
  ],
23
25
  this.deps,
24
26
  );
27
+ }
25
28
 
26
- const subject = await resolver.resolve();
29
+ async verify(): Promise<PrerequisiteVerificationResult> {
30
+ const subject = await this.resolver.resolve();
27
31
 
28
- return this.deps.CacheResolver.resolve<PrerequisiteVerificationResult>(subject.hex, () =>
29
- this.config.inner.verify(),
32
+ return this.deps.CacheResolver.resolve<PrerequisiteVerificationResult>(
33
+ subject.hex,
34
+ async () => this.config.inner.verify(),
35
+ this.codec,
30
36
  );
31
37
  }
32
38
 
@@ -0,0 +1,45 @@
1
+ import * as tools from "@bgord/tools";
2
+
3
+ type RateLimiterResultSuccessType = { allowed: true };
4
+ type RateLimiterResultErrorType = { allowed: false; remaining: tools.Duration };
5
+ type RateLimiterResultType = RateLimiterResultSuccessType | RateLimiterResultErrorType;
6
+
7
+ export type RateLimiterStateType = { lastInvocation: tools.TimestampValueType | null };
8
+
9
+ export class RateLimiter {
10
+ private lastInvocation: tools.Timestamp | null = null;
11
+
12
+ constructor(private readonly duration: tools.Duration) {}
13
+
14
+ static fromState(duration: tools.Duration, state: RateLimiterStateType): RateLimiter {
15
+ const limiter = new RateLimiter(duration);
16
+
17
+ if (state.lastInvocation !== null) {
18
+ limiter.lastInvocation = tools.Timestamp.fromNumber(state.lastInvocation);
19
+ }
20
+
21
+ return limiter;
22
+ }
23
+
24
+ verify(now: tools.Timestamp): RateLimiterResultType {
25
+ if (this.lastInvocation === null) {
26
+ this.lastInvocation = now;
27
+
28
+ return { allowed: true };
29
+ }
30
+
31
+ const nextAllowedTimestamp = this.lastInvocation.add(this.duration);
32
+
33
+ if (nextAllowedTimestamp.isBeforeOrEqual(now)) {
34
+ this.lastInvocation = now;
35
+
36
+ return { allowed: true };
37
+ }
38
+
39
+ return { allowed: false, remaining: nextAllowedTimestamp.difference(now) };
40
+ }
41
+
42
+ toJSON(): RateLimiterStateType {
43
+ return { lastInvocation: this.lastInvocation === null ? null : this.lastInvocation.toJSON() };
44
+ }
45
+ }
@@ -1,10 +1,16 @@
1
+ import { CacheCodecIdentityStrategy } from "./cache-codec-identity.strategy";
1
2
  import type { CacheResolverStrategy } from "./cache-resolver.strategy";
3
+ import type { CacheValueType } from "./cache-value.vo";
2
4
  import type { HashContentStrategy } from "./hash-content.strategy";
3
5
  import type { ReactiveConfigPort } from "./reactive-config.port";
4
6
 
5
7
  type Dependencies = { CacheResolver: CacheResolverStrategy; HashContent: HashContentStrategy };
6
8
 
7
- export class ReactiveConfigWithCacheAdapter<T extends object> implements ReactiveConfigPort<T> {
9
+ export class ReactiveConfigWithCacheAdapter<T extends object & CacheValueType>
10
+ implements ReactiveConfigPort<T>
11
+ {
12
+ private readonly codec = new CacheCodecIdentityStrategy<T>();
13
+
8
14
  constructor(
9
15
  private readonly inner: ReactiveConfigPort<T>,
10
16
  private readonly subject: string,
@@ -14,6 +20,6 @@ export class ReactiveConfigWithCacheAdapter<T extends object> implements Reactiv
14
20
  async get(): Promise<Readonly<T>> {
15
21
  const key = await this.deps.HashContent.hash(this.subject);
16
22
 
17
- return Object.freeze(await this.deps.CacheResolver.resolve(key, () => this.inner.get()));
23
+ return Object.freeze(await this.deps.CacheResolver.resolve<T>(key, () => this.inner.get(), this.codec));
18
24
  }
19
25
  }
@@ -27,7 +27,11 @@ export class RequestContextHonoAdapter implements RequestContext {
27
27
  cookie: (name) => getCookie(context)[name],
28
28
  json: async () => {
29
29
  try {
30
- return await context.req.raw.clone().json();
30
+ const body = await context.req.raw.clone().json();
31
+
32
+ if (body === null || typeof body !== "object") return {};
33
+
34
+ return body;
31
35
  } catch {
32
36
  return {};
33
37
  }
@@ -32,14 +32,13 @@ export class SecurityRuleViolationThresholdStrategy implements SecurityRuleStrat
32
32
  if (!violated) return false;
33
33
 
34
34
  try {
35
- const count = (await this.deps.CacheRepository.get<tools.IntegerNonNegativeType>(subject.hex)) ?? 0;
35
+ const cached = await this.deps.CacheRepository.get(subject.hex);
36
+ const count = typeof cached === "number" ? cached : 0;
37
+ const next = tools.Int.nonNegative(count + 1);
36
38
 
37
- await this.deps.CacheRepository.set<tools.IntegerNonNegativeType>(
38
- subject.hex,
39
- tools.Int.nonNegative(count + 1),
40
- );
39
+ await this.deps.CacheRepository.set(subject.hex, next);
41
40
 
42
- if (count + 1 >= this.config.threshold) return true;
41
+ if (next >= this.config.threshold) return true;
43
42
  return false;
44
43
  } catch {
45
44
  return false;
@@ -1,6 +1,6 @@
1
1
  import type { MiddlewareHandler } from "hono";
2
2
  import { HTTPException } from "hono/http-exception";
3
- import type { CacheResolverStrategy } from "./cache-resolver.strategy";
3
+ import type { CacheRepositoryPort } from "./cache-repository.port";
4
4
  import type { ClockPort } from "./clock.port";
5
5
  import type { MiddlewareHonoPort } from "./middleware-hono.port";
6
6
  import { RequestContextHonoAdapter } from "./request-context-hono.adapter";
@@ -10,7 +10,7 @@ import {
10
10
  ShieldRateLimitStrategyError,
11
11
  } from "./shield-rate-limit.strategy";
12
12
 
13
- type Dependencies = { Clock: ClockPort; CacheResolver: CacheResolverStrategy };
13
+ type Dependencies = { Clock: ClockPort; CacheRepository: CacheRepositoryPort };
14
14
 
15
15
  export class ShieldRateLimitHonoStrategy implements MiddlewareHonoPort {
16
16
  private readonly strategy: ShieldRateLimitStrategy;
@@ -1,13 +1,14 @@
1
1
  import * as tools from "@bgord/tools";
2
- import type { CacheResolverStrategy } from "./cache-resolver.strategy";
2
+ import type { CacheRepositoryPort } from "./cache-repository.port";
3
3
  import type { ClockPort } from "./clock.port";
4
+ import { RateLimiter, type RateLimiterStateType } from "./rate-limiter.service";
4
5
  import type { RequestContext } from "./request-context.port";
5
6
  import type { SubjectRequestResolver } from "./subject-request-resolver.vo";
6
7
 
7
8
  export type ShieldRateLimitConfig = { resolver: SubjectRequestResolver; interval: tools.Duration };
8
9
  type ShieldRateLimitResult = { allowed: true } | { allowed: false; retryAfter: tools.Duration };
9
10
 
10
- type Dependencies = { Clock: ClockPort; CacheResolver: CacheResolverStrategy };
11
+ type Dependencies = { Clock: ClockPort; CacheRepository: CacheRepositoryPort };
11
12
 
12
13
  export const ShieldRateLimitStrategyError = { Rejected: "shield.rate.limit.rejected" };
13
14
 
@@ -22,14 +23,20 @@ export class ShieldRateLimitStrategy {
22
23
  async evaluate(context: RequestContext): Promise<ShieldRateLimitResult> {
23
24
  const subject = await this.config.resolver.resolve(context);
24
25
 
25
- const limiter = await this.deps.CacheResolver.resolve(
26
- subject.hex,
27
- async () => new tools.RateLimiter(this.config.interval),
28
- );
26
+ const state = await this.deps.CacheRepository.get(subject.hex);
27
+
28
+ const limiter =
29
+ state === null
30
+ ? new RateLimiter(this.config.interval)
31
+ : RateLimiter.fromState(this.config.interval, state as RateLimiterStateType);
29
32
 
30
33
  const decision = limiter.verify(this.deps.Clock.now());
31
34
 
32
- if (decision.allowed) return { allowed: true };
35
+ if (decision.allowed) {
36
+ await this.deps.CacheRepository.set(subject.hex, limiter.toJSON());
37
+
38
+ return { allowed: true };
39
+ }
33
40
 
34
41
  return {
35
42
  allowed: false,
@@ -5,21 +5,29 @@ import { RequestContextHonoAdapter } from "./request-context-hono.adapter";
5
5
  import {
6
6
  ShieldWebhookStrategy,
7
7
  type ShieldWebhookStrategyConfig,
8
+ type ShieldWebhookStrategyDependencies,
8
9
  ShieldWebhookStrategyError,
9
10
  } from "./shield-webhook.strategy";
10
11
 
11
12
  export class ShieldWebhookHonoStrategy implements MiddlewareHonoPort {
12
13
  private readonly strategy: ShieldWebhookStrategy;
13
14
 
14
- constructor(config: ShieldWebhookStrategyConfig) {
15
- this.strategy = new ShieldWebhookStrategy(config);
15
+ constructor(config: ShieldWebhookStrategyConfig, deps: ShieldWebhookStrategyDependencies) {
16
+ this.strategy = new ShieldWebhookStrategy(config, deps);
16
17
  }
17
18
 
18
19
  handle(): MiddlewareHandler {
19
20
  return async (c, next) => {
20
21
  const context = new RequestContextHonoAdapter(c);
21
22
 
22
- if (await this.strategy.evaluate(context)) return next();
23
+ const result = await this.strategy.evaluate(context);
24
+
25
+ if (result.accepted) return next();
26
+
27
+ if (result.reason === "duplicate") {
28
+ return c.json({ message: ShieldWebhookStrategyError.Duplicate }, 200);
29
+ }
30
+
23
31
  throw new HTTPException(401, { message: ShieldWebhookStrategyError.Rejected });
24
32
  };
25
33
  }
@@ -1,26 +1,61 @@
1
- import type { HasRequestHeader, HasRequestText } from "./request-context.port";
1
+ import type { HashContentStrategy } from "./hash-content.strategy";
2
+ import type { IdempotencyStorePort } from "./idempotency-store.port";
3
+ import type { HasRequestHeader, HasRequestJson, HasRequestText } from "./request-context.port";
2
4
  import type { WebhookBodyBuilderStrategy } from "./webhook-body-builder.strategy";
5
+ import type { WebhookIdExtractorStrategy } from "./webhook-id-extractor.strategy";
3
6
  import type { WebhookSignatureExtractorStrategy } from "./webhook-signature-extractor.strategy";
4
7
  import type { WebhookVerifierStrategy } from "./webhook-verifier.strategy";
5
8
 
6
- export const ShieldWebhookStrategyError = { Rejected: "shield.webhook.rejected" };
9
+ export const ShieldWebhookStrategyError = {
10
+ Rejected: "shield.webhook.rejected",
11
+ Duplicate: "shield.webhook.duplicate",
12
+ };
13
+
14
+ export type ShieldWebhookStrategyResult =
15
+ | { accepted: true }
16
+ | { accepted: false; reason: "rejected" }
17
+ | { accepted: false; reason: "duplicate" };
7
18
 
8
19
  export type ShieldWebhookStrategyConfig = {
20
+ namespace: string;
9
21
  WebhookBodyBuilder: WebhookBodyBuilderStrategy;
22
+ WebhookIdExtractor: WebhookIdExtractorStrategy;
10
23
  WebhookSignatureExtractor: WebhookSignatureExtractorStrategy;
11
24
  WebhookVerifier: WebhookVerifierStrategy;
12
25
  };
13
26
 
27
+ export type ShieldWebhookStrategyDependencies = {
28
+ HashContent: HashContentStrategy;
29
+ IdempotencyStore: IdempotencyStorePort;
30
+ };
31
+
14
32
  export class ShieldWebhookStrategy {
15
- constructor(private readonly config: ShieldWebhookStrategyConfig) {}
33
+ constructor(
34
+ private readonly config: ShieldWebhookStrategyConfig,
35
+ private readonly deps: ShieldWebhookStrategyDependencies,
36
+ ) {}
16
37
 
17
- async evaluate(context: HasRequestHeader & HasRequestText): Promise<boolean> {
38
+ async evaluate(
39
+ context: HasRequestHeader & HasRequestJson & HasRequestText,
40
+ ): Promise<ShieldWebhookStrategyResult> {
18
41
  const signature = this.config.WebhookSignatureExtractor.extract(context);
19
42
 
20
- if (!signature) return false;
43
+ if (!signature) return { accepted: false, reason: "rejected" };
21
44
 
22
45
  const body = await this.config.WebhookBodyBuilder.build(context);
23
46
 
24
- return this.config.WebhookVerifier.verify(body, signature);
47
+ if (!this.config.WebhookVerifier.verify(body, signature)) return { accepted: false, reason: "rejected" };
48
+
49
+ const id = await this.config.WebhookIdExtractor.extract(context);
50
+
51
+ if (!id) return { accepted: false, reason: "rejected" };
52
+
53
+ const subject = await this.deps.HashContent.hash(JSON.stringify([[this.config.namespace, id]]));
54
+
55
+ const claimed = await this.deps.IdempotencyStore.claim(subject);
56
+
57
+ if (!claimed) return { accepted: false, reason: "duplicate" };
58
+
59
+ return { accepted: true };
25
60
  }
26
61
  }