@classytic/arc 2.23.0 → 2.28.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 (147) hide show
  1. package/README.md +2 -0
  2. package/dist/{BaseController-BQ8KNvWC.mjs → BaseController-B65zbz9m.mjs} +94 -23
  3. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  4. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  5. package/dist/{applyPermissionResult-Tw4iXtlf.mjs → applyPermissionResult-BnyVE9eD.mjs} +10 -0
  6. package/dist/audit/index.d.mts +32 -3
  7. package/dist/audit/index.mjs +7 -4
  8. package/dist/auth/index.d.mts +2 -2
  9. package/dist/auth/index.mjs +263 -259
  10. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  11. package/dist/cache/index.d.mts +41 -3
  12. package/dist/cache/index.mjs +33 -4
  13. package/dist/cleanup/index.d.mts +313 -0
  14. package/dist/cleanup/index.mjs +1267 -0
  15. package/dist/cli/commands/describe.d.mts +1 -1
  16. package/dist/cli/commands/docs.mjs +1 -1
  17. package/dist/cli/commands/generate.mjs +1 -1
  18. package/dist/cli/commands/init.mjs +1 -1
  19. package/dist/cli/index.mjs +2 -2
  20. package/dist/context/index.mjs +2 -64
  21. package/dist/core/index.d.mts +3 -3
  22. package/dist/core/index.mjs +5 -4
  23. package/dist/core-BGa8TXJR.mjs +221 -0
  24. package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-C-N7z2EL.mjs} +3 -2
  25. package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  26. package/dist/{createApp-CjOeWJ9_.mjs → createApp-Du5G54TM.mjs} +187 -96
  27. package/dist/{core-DQ6rDTyH.mjs → defineResource-2zvVVpxg.mjs} +297 -230
  28. package/dist/docs/index.d.mts +1 -1
  29. package/dist/docs/index.mjs +2 -2
  30. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  31. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  32. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  33. package/dist/events/index.d.mts +83 -314
  34. package/dist/events/index.mjs +183 -95
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  37. package/dist/factory/index.d.mts +5 -5
  38. package/dist/factory/index.mjs +30 -8
  39. package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-OzYGIiU1.d.mts} +26 -2
  40. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  41. package/dist/headers-BvlQSDMB.mjs +45 -0
  42. package/dist/health-CB_m4S32.mjs +377 -0
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/idempotency/index.mjs +27 -3
  45. package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
  46. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  47. package/dist/index-CZY4EGga.d.mts +107 -0
  48. package/dist/{index-CC5ruDGh.d.mts → index-CjVg-Epg.d.mts} +37 -5
  49. package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
  50. package/dist/index.d.mts +6 -6
  51. package/dist/index.mjs +7 -6
  52. package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
  53. package/dist/integrations/event-gateway.d.mts +1 -1
  54. package/dist/integrations/event-gateway.mjs +1 -1
  55. package/dist/integrations/index.d.mts +3 -3
  56. package/dist/integrations/jobs/index.d.mts +2 -0
  57. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  58. package/dist/integrations/mcp/index.d.mts +4 -4
  59. package/dist/integrations/mcp/index.mjs +2 -2
  60. package/dist/integrations/mcp/testing.d.mts +1 -1
  61. package/dist/integrations/mcp/testing.mjs +1 -1
  62. package/dist/integrations/webhooks.d.mts +37 -30
  63. package/dist/integrations/webhooks.mjs +51 -62
  64. package/dist/integrations/websocket-redis.d.mts +1 -1
  65. package/dist/integrations/websocket.d.mts +1 -1
  66. package/dist/integrations/websocket.mjs +41 -14
  67. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  68. package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cs6nRGCt.mjs} +12 -10
  69. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  70. package/dist/middleware/index.d.mts +17 -1
  71. package/dist/middleware/index.mjs +1 -1
  72. package/dist/migrations/index.d.mts +94 -44
  73. package/dist/migrations/index.mjs +196 -44
  74. package/dist/module-_-M7C5ES.mjs +29 -0
  75. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  76. package/dist/{openapi-jSDx-VDv.mjs → openapi-BLV4OMXb.mjs} +3 -3
  77. package/dist/permissions/index.d.mts +2 -2
  78. package/dist/permissions/index.mjs +3 -3
  79. package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
  80. package/dist/pipeline/index.d.mts +1 -1
  81. package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BGRnV-To.mjs} +4 -27
  82. package/dist/plugins/index.d.mts +6 -77
  83. package/dist/plugins/index.mjs +11 -95
  84. package/dist/plugins/response-cache.mjs +1 -0
  85. package/dist/plugins/tracing-entry.mjs +1 -1
  86. package/dist/{index-Ba10fdKp.d.mts → preloadResources-CM4uUaFv.d.mts} +51 -50
  87. package/dist/preloadResources-DSY5HGpS.mjs +45 -0
  88. package/dist/presets/filesUpload.d.mts +3 -3
  89. package/dist/presets/filesUpload.mjs +3 -3
  90. package/dist/presets/index.d.mts +2 -1
  91. package/dist/presets/index.mjs +1 -1
  92. package/dist/presets/multiTenant.d.mts +1 -1
  93. package/dist/presets/multiTenant.mjs +1 -1
  94. package/dist/presets/search.d.mts +2 -2
  95. package/dist/presets/search.mjs +1 -1
  96. package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
  97. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  98. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  99. package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  100. package/dist/{realtime-I8OrfnMR.mjs → realtime-DEuDu9io.mjs} +3 -3
  101. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  102. package/dist/registerRoute-DTp18Ah_.mjs +108 -0
  103. package/dist/registry/index.d.mts +2 -2
  104. package/dist/registry/index.mjs +1 -1
  105. package/dist/{registry-vcOCtPij.mjs → registry-9km_w1aP.mjs} +15 -0
  106. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  107. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  108. package/dist/schedules-CjOd2MG9.mjs +113 -0
  109. package/dist/schemas/index.d.mts +67 -67
  110. package/dist/schemas/index.mjs +2 -3
  111. package/dist/scim/index.mjs +1 -2
  112. package/dist/scope/index.d.mts +2 -2
  113. package/dist/scope/index.mjs +4 -4
  114. package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
  115. package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
  116. package/dist/testing/cleanupStoreContract.d.mts +10 -0
  117. package/dist/testing/cleanupStoreContract.mjs +375 -0
  118. package/dist/testing/index.d.mts +5 -3
  119. package/dist/testing/index.mjs +4 -2
  120. package/dist/testing/outboxStoreContract.d.mts +13 -0
  121. package/dist/testing/outboxStoreContract.mjs +247 -0
  122. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  123. package/dist/types/index.d.mts +4 -4
  124. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  125. package/dist/types-C_ZZRcD6.d.mts +450 -0
  126. package/dist/{module-B4u10iOJ.d.mts → types-Cu6vVJmh.d.mts} +196 -60
  127. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  128. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  129. package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
  130. package/dist/usage/index.d.mts +3 -52
  131. package/dist/usage/index.mjs +0 -0
  132. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  133. package/dist/utils/index.d.mts +58 -85
  134. package/dist/utils/index.mjs +4 -2
  135. package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
  136. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  137. package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
  138. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  139. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  140. package/package.json +541 -513
  141. package/skills/arc/references/api-reference.md +3 -1
  142. package/skills/arc/references/integrations.md +20 -11
  143. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  144. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  145. package/dist/health-ScyjIVo5.mjs +0 -202
  146. package/dist/requestId-5BhTVzBv.mjs +0 -113
  147. package/dist/types-XTApa8b6.mjs +0 -24
@@ -1,6 +1,6 @@
1
- import { t as __exportAll } from "../rolldown-runtime-8H4AJuhK.mjs";
2
- import { a as MemoryEventTransport, i as withRetry, o as createEvent, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-DJG2D7sy.mjs";
1
+ import { a as MemoryEventTransport, i as withRetry, o as createEvent, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-DrQ_FesK.mjs";
3
2
  import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DY6xq4sM.mjs";
3
+ import { InvalidOutboxEventError, OutboxOwnershipError } from "@classytic/primitives/outbox";
4
4
  import { and, anyOf, eq, lte, ne, or } from "@classytic/repo-core/filter";
5
5
  import { update } from "@classytic/repo-core/update";
6
6
  //#region src/events/defineEvent.ts
@@ -61,10 +61,6 @@ import { update } from "@classytic/repo-core/update";
61
61
  * const result = registry.validate('order.created', payload);
62
62
  * ```
63
63
  */
64
- var defineEvent_exports = /* @__PURE__ */ __exportAll({
65
- createEventRegistry: () => createEventRegistry,
66
- defineEvent: () => defineEvent
67
- });
68
64
  /**
69
65
  * Define a typed event with optional schema validation.
70
66
  *
@@ -243,10 +239,31 @@ const CACHE_EVENTS = Object.freeze({
243
239
  });
244
240
  //#endregion
245
241
  //#region src/events/memory-outbox.ts
242
+ /**
243
+ * In-memory outbox store — reference implementation for dev / testing.
244
+ *
245
+ * Supports the full `OutboxStore` capability set (claim/lease, fail/retry,
246
+ * dedupe, visibleAt). Production deployments should pass a `repository` to
247
+ * `EventOutbox` instead of using this; the memory store exists for unit
248
+ * tests and single-process dev where durable persistence is noise.
249
+ */
246
250
  const DEFAULT_LEASE_MS$2 = 3e4;
247
251
  var MemoryOutboxStore = class {
248
252
  entries = [];
249
253
  seenDedupeKeys = /* @__PURE__ */ new Set();
254
+ /**
255
+ * Drop every entry (including delivered / failed / dead-lettered rows) and
256
+ * the dedupe-key memory.
257
+ *
258
+ * For tests only — it is the in-memory analogue of truncating the table, and
259
+ * exists so a suite can isolate cases against ONE store instance (the
260
+ * `reset` hook of `runOutboxStoreContract`). Not part of `OutboxStore`; a
261
+ * production store must never expose this.
262
+ */
263
+ clear() {
264
+ this.entries.length = 0;
265
+ this.seenDedupeKeys.clear();
266
+ }
250
267
  async save(event, options) {
251
268
  if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
252
269
  if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
@@ -356,11 +373,117 @@ var MemoryOutboxStore = class {
356
373
  }
357
374
  };
358
375
  //#endregion
376
+ //#region src/events/outbox/backoff.ts
377
+ /**
378
+ * Compute a `retryAt` `Date` using exponential backoff with jitter.
379
+ *
380
+ * This is a convenience helper for store authors implementing
381
+ * {@link OutboxStore.fail}: call it to compute the retry visibility window
382
+ * based on the event's current attempt count.
383
+ *
384
+ * Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
385
+ *
386
+ * @example Basic usage inside a store's `fail()` method
387
+ * ```typescript
388
+ * async fail(eventId, error, options) {
389
+ * const entry = await this.findById(eventId);
390
+ * entry.attempts++;
391
+ * if (entry.attempts >= MAX_ATTEMPTS) {
392
+ * return this.deadLetter(eventId, error);
393
+ * }
394
+ * const retryAt = exponentialBackoff({ attempt: entry.attempts });
395
+ * entry.visibleAt = retryAt;
396
+ * await this.update(entry);
397
+ * }
398
+ * ```
399
+ *
400
+ * @example Tuning for a faster transport
401
+ * ```typescript
402
+ * exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
403
+ * // attempt=1 → ~250ms ±30%
404
+ * // attempt=2 → ~500ms ±30%
405
+ * // attempt=3 → ~1000ms ±30%
406
+ * // attempt=10 → capped at 10_000ms
407
+ * ```
408
+ */
409
+ function exponentialBackoff(options) {
410
+ const attempt = Math.max(1, Math.floor(options.attempt));
411
+ const baseMs = options.baseMs ?? 1e3;
412
+ const maxMs = options.maxMs ?? 6e4;
413
+ const jitter = Math.max(0, Math.min(1, options.jitter ?? .2));
414
+ const now = options.now ?? Date.now();
415
+ const exp = baseMs * 2 ** (attempt - 1);
416
+ const capped = Math.min(maxMs, exp);
417
+ const jittered = jitter > 0 ? capped * (1 + Math.random() * jitter) : capped;
418
+ return new Date(now + jittered);
419
+ }
420
+ //#endregion
359
421
  //#region src/events/repository-outbox-adapter.ts
422
+ /**
423
+ * RepositoryLike → OutboxStore adapter.
424
+ *
425
+ * Maps the `OutboxStore` vocabulary (save / claimPending / acknowledge /
426
+ * fail / getDeadLettered / purge) onto arc's own `RepositoryLike` primitives
427
+ * (create / getOne / findAll / deleteMany / findOneAndUpdate). `EventOutbox`
428
+ * wraps a passed repository with this helper when you use the
429
+ * `{ repository }` option; the function is also re-exported from
430
+ * `@classytic/arc/events` so consumers can build and decorate the store
431
+ * manually (metrics, tracing, multi-transport fan-out).
432
+ *
433
+ * Portability: filters compose via `@classytic/repo-core/filter` and
434
+ * updates via `@classytic/repo-core/update`. The primary-key column name
435
+ * is read from `repository.idField` — mongokit defaults to `_id`,
436
+ * sqlitekit / pgkit / prismakit to the schema's declared PK. The adapter
437
+ * therefore runs on any kit that implements `StandardRepo.findOneAndUpdate`
438
+ * + `getOne` + `getAll` + `deleteMany` + `create`.
439
+ *
440
+ * `fail()` uses a lease-gated read-then-write pair to preserve
441
+ * `firstFailedAt` across retries without relying on Mongo's aggregation-
442
+ * pipeline `$ifNull`. Leases guarantee single-writer during the failure
443
+ * window (`claimPending` filters out non-owned rows), so the two calls are
444
+ * safe under concurrent relayers.
445
+ *
446
+ * Why we DON'T use `StandardRepo.claim()` (repo-core 0.4+) anywhere in this
447
+ * adapter — design note for the next reader who wants to "modernize" us:
448
+ *
449
+ * • `claimPending` (FIFO claim-next loop): `claim(id, ...)` requires the
450
+ * candidate id upfront, but the relayer doesn't have it — that's the
451
+ * whole point of `findOneAndUpdate({ filter, sort: { createdAt: 1 } })`,
452
+ * which finds + claims in a SINGLE round-trip per claimed event.
453
+ * Replacing with `getOne(filter, sort) → claim(id, ...)` doubles the
454
+ * round-trips, opens a TOCTOU window where the candidate state can
455
+ * change between read and CAS (we'd need a retry loop on null-on-race),
456
+ * and gains nothing — the existing call is already atomic and
457
+ * race-free. Throughput regression with no upside; do not adopt.
458
+ *
459
+ * • `acknowledge` (pending → delivered): id is in scope, but the current
460
+ * filter uses `ne('status', 'delivered')` to allow ack from any
461
+ * non-delivered state (defensive; lease invariant means it's pending
462
+ * in practice). `claim` requires an exact `from` value, so adopting
463
+ * would tighten the source-state predicate from "not delivered" to
464
+ * "exactly pending". That's a behavior change — even if the new
465
+ * behavior is arguably tighter/safer, the constraint here is "no
466
+ * semantic regression". Keep `findOneAndUpdate`.
467
+ *
468
+ * • `fail` (any → pending|dead_letter): the current `baseFilter` is
469
+ * id-only (with optional `leaseOwner` guard) — there's NO source-state
470
+ * predicate at all. Adopting `claim` would ADD `from: 'pending'`,
471
+ * blocking `fail()` calls against rows in any other state. Same "no
472
+ * semantic regression" concern; keep `findOneAndUpdate`. The
473
+ * read-then-write pair also exists here to preserve `firstFailedAt`
474
+ * portably (see paragraph above) — `claim` doesn't simplify that.
475
+ *
476
+ * No kit currently ships a filter-based `claimNext`/`claimPending` (Path C
477
+ * in the v2.11.x evaluation) — checked mongokit 3.13.0 + sqlitekit 0.3.0.
478
+ * If/when one appears, the FIFO loop becomes a 1-call adoption candidate.
479
+ */
360
480
  const DEFAULT_LEASE_MS$1 = 3e4;
361
481
  const DEFAULT_CLAIM_LIMIT = 100;
362
482
  const DEFAULT_PURGE_BATCH = 500;
363
- function repositoryAsOutboxStore(repository) {
483
+ /** Column this adapter reads/writes for the visibility timestamp. */
484
+ const DEFAULT_VISIBLE_AT_FIELD = "visibleAt";
485
+ function repositoryAsOutboxStore(repository, options) {
486
+ const visibleAtField = options?.visibleAtField ?? DEFAULT_VISIBLE_AT_FIELD;
364
487
  const missing = [];
365
488
  if (typeof repository.create !== "function") missing.push("create");
366
489
  if (typeof repository.getOne !== "function") missing.push("getOne");
@@ -387,7 +510,7 @@ function repositoryAsOutboxStore(repository) {
387
510
  * lease. Used by `getPending` and `claimPending` — defined once so the
388
511
  * two code paths stay in lockstep.
389
512
  */
390
- const claimableFilter = (now) => and(eq("status", "pending"), lte("visibleAt", now), or(eq("leaseOwner", null), lte("leaseExpiresAt", now)));
513
+ const claimableFilter = (now) => and(eq("status", "pending"), lte(visibleAtField, now), or(eq("leaseOwner", null), lte("leaseExpiresAt", now)));
391
514
  return {
392
515
  async save(event, options) {
393
516
  if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
@@ -399,7 +522,7 @@ function repositoryAsOutboxStore(repository) {
399
522
  type: event.type,
400
523
  status: "pending",
401
524
  attempts: 0,
402
- visibleAt: options?.visibleAt ?? now,
525
+ [visibleAtField]: options?.visibleAt ?? now,
403
526
  leaseOwner: null,
404
527
  leaseExpiresAt: null,
405
528
  deliveredAt: null,
@@ -433,25 +556,26 @@ function repositoryAsOutboxStore(repository) {
433
556
  const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS$1;
434
557
  const consumerId = options?.consumerId ?? "anonymous";
435
558
  const typeFilter = options?.types?.length ? anyOf("type", options.types) : null;
436
- const claimed = [];
437
- for (let i = 0; i < limit; i++) {
438
- const now = /* @__PURE__ */ new Date();
439
- const leaseExpiresAt = new Date(now.getTime() + leaseMs);
440
- const filter = typeFilter ? and(claimableFilter(now), typeFilter) : claimableFilter(now);
441
- const doc = await r.findOneAndUpdate(filter, update({
559
+ const now = /* @__PURE__ */ new Date();
560
+ const candidateFilter = typeFilter ? and(claimableFilter(now), typeFilter) : claimableFilter(now);
561
+ const candidates = unwrapDocs(await r.getAll({
562
+ filters: candidateFilter,
563
+ sort: { createdAt: 1 },
564
+ page: 1,
565
+ limit
566
+ }));
567
+ if (candidates.length === 0) return [];
568
+ return (await Promise.all(candidates.map((candidate) => {
569
+ const claimNow = /* @__PURE__ */ new Date();
570
+ const leaseExpiresAt = new Date(claimNow.getTime() + leaseMs);
571
+ return r.findOneAndUpdate(and(eq(idField, candidate.event.meta.id), claimableFilter(claimNow)), update({
442
572
  set: {
443
573
  leaseOwner: consumerId,
444
574
  leaseExpiresAt
445
575
  },
446
576
  inc: { attempts: 1 }
447
- }), {
448
- sort: { createdAt: 1 },
449
- returnDocument: "after"
450
- });
451
- if (!doc) break;
452
- if (isWellFormed(doc.event)) claimed.push(doc.event);
453
- }
454
- return claimed;
577
+ }), { returnDocument: "after" });
578
+ }))).filter((doc) => doc !== null).map((doc) => doc.event).filter(isWellFormed);
455
579
  },
456
580
  async acknowledge(eventId, options) {
457
581
  const now = /* @__PURE__ */ new Date();
@@ -484,7 +608,7 @@ function repositoryAsOutboxStore(repository) {
484
608
  const firstFailedAt = current.firstFailedAt ?? now;
485
609
  if (await r.findOneAndUpdate(filter, update({ set: {
486
610
  status: targetStatus,
487
- visibleAt,
611
+ [visibleAtField]: visibleAt,
488
612
  leaseOwner: null,
489
613
  leaseExpiresAt: null,
490
614
  lastFailedAt: now,
@@ -537,7 +661,7 @@ function repositoryAsOutboxStore(repository) {
537
661
  };
538
662
  }
539
663
  //#endregion
540
- //#region src/events/outbox.ts
664
+ //#region src/events/outbox/relay.ts
541
665
  /** Default outbox retention — delivered events older than this are eligible for purge */
542
666
  const DEFAULT_OUTBOX_RETENTION_MS = 10080 * 60 * 1e3;
543
667
  const DEFAULT_LEASE_MS = 3e4;
@@ -552,34 +676,6 @@ const DEFAULT_LEASE_MS = 3e4;
552
676
  * the store).
553
677
  */
554
678
  const MAX_TRACKED_ATTEMPTS = 1e4;
555
- /**
556
- * Thrown by a store when `acknowledge` / `fail` is called by a consumer that
557
- * does not own the event's current lease.
558
- *
559
- * Stores that enforce lease ownership MUST throw this (not silently return)
560
- * so {@link EventOutbox} can detect the mismatch and avoid over-counting
561
- * successful deliveries. {@link EventOutbox.relay} catches it and reports
562
- * via {@link EventOutboxOptions.onError} instead of counting as relayed.
563
- */
564
- var OutboxOwnershipError = class extends Error {
565
- eventId;
566
- attemptedBy;
567
- currentOwner;
568
- constructor(eventId, attemptedBy, currentOwner) {
569
- super(`Outbox ownership mismatch for event "${eventId}": attempted by "${attemptedBy}", current owner is "${currentOwner ?? "none"}". The lease may have expired and been reclaimed by another worker.`);
570
- this.name = "OutboxOwnershipError";
571
- this.eventId = eventId;
572
- this.attemptedBy = attemptedBy;
573
- this.currentOwner = currentOwner;
574
- }
575
- };
576
- /** Thrown by {@link EventOutbox.store} when an event is missing `type` or `meta.id`. */
577
- var InvalidOutboxEventError = class extends Error {
578
- constructor(reason) {
579
- super(`Invalid outbox event: ${reason}`);
580
- this.name = "InvalidOutboxEventError";
581
- }
582
- };
583
679
  var EventOutbox = class {
584
680
  _store;
585
681
  _transport;
@@ -643,7 +739,7 @@ var EventOutbox = class {
643
739
  session: autoSession
644
740
  };
645
741
  }
646
- if (!effective?.headers?.["traceparent"] && this._traceContextProvider) {
742
+ if (!effective?.headers?.traceparent && this._traceContextProvider) {
647
743
  const traceHeaders = this._traceContextProvider();
648
744
  if (traceHeaders && Object.keys(traceHeaders).length > 0) effective = {
649
745
  ...effective ?? {},
@@ -726,11 +822,14 @@ var EventOutbox = class {
726
822
  usedPublishMany: false
727
823
  };
728
824
  if (!this._transport) return empty;
825
+ const claimStartedAt = Date.now();
729
826
  const pending = this._store.claimPending ? await this._store.claimPending({
730
827
  limit: this._batchSize,
731
828
  consumerId: this._consumerId,
732
829
  leaseMs: this._leaseMs
733
830
  }) : await this._store.getPending(this._batchSize);
831
+ const claimMs = Date.now() - claimStartedAt;
832
+ const publishStartedAt = Date.now();
734
833
  const valid = [];
735
834
  let malformed = 0;
736
835
  for (const event of pending) {
@@ -840,7 +939,9 @@ var EventOutbox = class {
840
939
  malformed,
841
940
  failHookErrors: counts.failHookErrors,
842
941
  deadLettered: counts.deadLettered,
843
- usedPublishMany: canPublishMany && valid.length > 0
942
+ usedPublishMany: canPublishMany && valid.length > 0,
943
+ claimMs,
944
+ publishMs: Date.now() - publishStartedAt
844
945
  };
845
946
  }
846
947
  /**
@@ -874,51 +975,39 @@ function normalizeError(err) {
874
975
  };
875
976
  return { message: String(err) };
876
977
  }
978
+ //#endregion
979
+ //#region src/events/subscribe-helpers.ts
877
980
  /**
878
- * Compute a `retryAt` `Date` using exponential backoff with jitter.
981
+ * Subscribe-side helpers — close the symmetry that arc's publish-side
982
+ * already has via [`eventPlugin({ registry, validateMode: 'reject' })`](./eventPlugin.ts).
879
983
  *
880
- * This is a convenience helper for store authors implementing
881
- * {@link OutboxStore.fail}: call it to compute the retry visibility window
882
- * based on the event's current attempt count.
984
+ * Two wrapper pairs, both transport-agnostic:
883
985
  *
884
- * Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
986
+ * - `wrapWithSchema` / `subscribeWithSchema` — read the schema from a
987
+ * typed `EventDefinitionOutput<T>` and validate `event.payload` before
988
+ * the handler runs. Single source of truth for event shape: declare
989
+ * once at `defineEvent`, enforced on BOTH sides without parallel Zod
990
+ * schemas in subscriber files.
885
991
  *
886
- * @example Basic usage inside a store's `fail()` method
887
- * ```typescript
888
- * async fail(eventId, error, options) {
889
- * const entry = await this.findById(eventId);
890
- * entry.attempts++;
891
- * if (entry.attempts >= MAX_ATTEMPTS) {
892
- * return this.deadLetter(eventId, error);
893
- * }
894
- * const retryAt = exponentialBackoff({ attempt: entry.attempts });
895
- * entry.visibleAt = retryAt;
896
- * await this.update(entry);
897
- * }
898
- * ```
992
+ * - `wrapWithBoundary` / `subscribeWithBoundary` — catch + log + swallow
993
+ * handler errors, for projection / cache-invalidation / fire-and-forget
994
+ * handlers where retry would just delay the next-event resync. Lighter
995
+ * than `withRetry` (no DLQ, no exponential backoff) and avoids the
996
+ * "single bad event poisons the bus" hazard.
899
997
  *
900
- * @example Tuning for a faster transport
901
- * ```typescript
902
- * exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
903
- * // attempt=1 → ~250ms ±30%
904
- * // attempt=2 → ~500ms ±30%
905
- * // attempt=3 → ~1000ms ±30%
906
- * // attempt=10 → capped at 10_000ms
998
+ * Both pairs compose with `withRetry` and with each other:
999
+ *
1000
+ * ```ts
1001
+ * await subscribeWithSchema(
1002
+ * fastify,
1003
+ * OrderPaid,
1004
+ * withRetry(handler, { maxRetries: 3 }),
1005
+ * );
907
1006
  * ```
1007
+ *
1008
+ * @see [retry.ts](./retry.ts) for the retry + DLQ wrapper.
1009
+ * @see [defineEvent.ts](./defineEvent.ts) for `defineEvent` / `EventDefinitionOutput`.
908
1010
  */
909
- function exponentialBackoff(options) {
910
- const attempt = Math.max(1, Math.floor(options.attempt));
911
- const baseMs = options.baseMs ?? 1e3;
912
- const maxMs = options.maxMs ?? 6e4;
913
- const jitter = Math.max(0, Math.min(1, options.jitter ?? .2));
914
- const now = options.now ?? Date.now();
915
- const exp = baseMs * 2 ** (attempt - 1);
916
- const capped = Math.min(maxMs, exp);
917
- const jittered = jitter > 0 ? capped * (1 + Math.random() * jitter) : capped;
918
- return new Date(now + jittered);
919
- }
920
- //#endregion
921
- //#region src/events/subscribe-helpers.ts
922
1011
  /**
923
1012
  * Pure handler wrapper — returns a new `EventHandler` that validates
924
1013
  * `event.payload` against the definition's schema before invoking the handler.
@@ -947,7 +1036,6 @@ function wrapWithSchema(definition, handler, options = {}) {
947
1036
  if (validate && definition.schema) result = validate(definition.schema, event.payload);
948
1037
  else if (registry) result = registry.validate(definition.name, event.payload, eventVersion);
949
1038
  else if (definition.schema) {
950
- const { createEventRegistry } = await Promise.resolve().then(() => defineEvent_exports);
951
1039
  const adhoc = createEventRegistry();
952
1040
  adhoc.register(definition);
953
1041
  result = adhoc.validate(definition.name, event.payload);
@@ -1041,4 +1129,4 @@ async function subscribeWithBoundary(fastify, pattern, handler, options) {
1041
1129
  return fastify.events.subscribe(pattern, wrapWithBoundary(handler, options));
1042
1130
  }
1043
1131
  //#endregion
1044
- export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, InvalidOutboxEventError, MemoryEventTransport, MemoryOutboxStore, OutboxOwnershipError, createDeadLetterPublisher, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };
1132
+ export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, MemoryEventTransport, MemoryOutboxStore, createDeadLetterPublisher, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };
@@ -1,2 +1,2 @@
1
- import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-iVR2l7aS.mjs";
1
+ import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-uIaauNpZ.mjs";
2
2
  export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
@@ -50,6 +50,7 @@ var RedisStreamTransport = class {
50
50
  consumer;
51
51
  blockTimeMs;
52
52
  batchSize;
53
+ processingConcurrency;
53
54
  maxRetries;
54
55
  claimTimeoutMs;
55
56
  deadLetterStream;
@@ -92,6 +93,7 @@ var RedisStreamTransport = class {
92
93
  this.consumer = options.consumer ?? `consumer-${crypto.randomUUID().slice(0, 8)}`;
93
94
  this.blockTimeMs = options.blockTimeMs ?? 5e3;
94
95
  this.batchSize = options.batchSize ?? 10;
96
+ this.processingConcurrency = Math.max(1, Math.floor(options.processingConcurrency ?? 1));
95
97
  this.maxRetries = options.maxRetries ?? 5;
96
98
  this.claimTimeoutMs = options.claimTimeoutMs ?? 3e4;
97
99
  this.deadLetterStream = options.deadLetterStream ?? "arc:events:dlq";
@@ -218,7 +220,27 @@ var RedisStreamTransport = class {
218
220
  async readNewMessages() {
219
221
  const result = await this.redis.xreadgroup("GROUP", this.group, this.consumer, "COUNT", this.batchSize, "BLOCK", this.blockTimeMs, "STREAMS", this.stream, ">");
220
222
  if (!result) return;
221
- for (const [, entries] of result) for (const [messageId, fields] of entries) await this.processEntry(messageId, fields);
223
+ for (const [, entries] of result) await this.processEntries(entries);
224
+ }
225
+ /**
226
+ * Process a batch's entries with bounded concurrency
227
+ * (`processingConcurrency`, default 1 = strict order). Worker-pool over a
228
+ * shared index — at most N entries in flight, each entry's own handlers
229
+ * still sequential inside `processEntry`.
230
+ */
231
+ async processEntries(entries) {
232
+ if (this.processingConcurrency <= 1) {
233
+ for (const [messageId, fields] of entries) await this.processEntry(messageId, fields);
234
+ return;
235
+ }
236
+ let next = 0;
237
+ const workers = Array.from({ length: Math.min(this.processingConcurrency, entries.length) }, async () => {
238
+ while (next < entries.length) {
239
+ const entry = entries[next++];
240
+ if (entry) await this.processEntry(entry[0], entry[1]);
241
+ }
242
+ });
243
+ await Promise.all(workers);
222
244
  }
223
245
  async claimPending() {
224
246
  try {
@@ -234,7 +256,7 @@ var RedisStreamTransport = class {
234
256
  if (overRetryIds.length > 0) await this.moveToDlq(overRetryIds);
235
257
  if (staleIds.length > 0) {
236
258
  const claimed = await this.redis.xclaim(this.stream, this.group, this.consumer, this.claimTimeoutMs, ...staleIds);
237
- for (const [messageId, fields] of claimed) await this.processEntry(messageId, fields);
259
+ await this.processEntries(claimed);
238
260
  }
239
261
  } catch (err) {
240
262
  this.logger.error("[RedisStreamTransport] claimPending error:", err);
@@ -1,7 +1,7 @@
1
- import { a as getModuleExports, c as LoadResourcesOptions, d as loadResources, i as defineModule, l as ResourceLike, n as ArcModuleInput, o as orderModules, r as ArcModuleRegistry, s as resolveModule, t as ArcModule, u as ResourceModule } from "../module-B4u10iOJ.mjs";
2
- import "../fastify-augmentation-g_f9omSc.mjs";
3
- import { a as AuthOption, c as CustomPluginAuthOption, d as preloadResourcesAsync, i as UnderPressureOptions, l as JwtAuthOption, n as MultipartOptions, o as BetterAuthOption, r as RawBodyOptions, s as CustomAuthenticatorOption, t as CreateAppOptions, u as preloadResources } from "../index-Ba10fdKp.mjs";
4
- import { b as HealthCheck } from "../versioning-D6PvCwpq.mjs";
1
+ import { a as ModuleContribution, c as ResourceModule, h as HealthCheck, i as EventHandlerDefinition, l as loadResources, n as ArcModuleInput, o as LoadResourcesOptions, r as ArcModuleRegistry, s as ResourceLike, t as ArcModule } from "../types-Cu6vVJmh.mjs";
2
+ import { a as orderModules, c as collectModuleScheduledJobs, i as resolveModule, l as collectModuleWorkflows, n as getModuleExports, o as subscribeModuleEventHandlers, r as resolveContribution, s as collectModuleHealthChecks, t as defineModule } from "../index-CZY4EGga.mjs";
3
+ import "../fastify-augmentation-OzYGIiU1.mjs";
4
+ import { a as RawBodyOptions, c as BetterAuthOption, d as JwtAuthOption, i as MultipartOptions, l as CustomAuthenticatorOption, n as preloadResourcesAsync, o as UnderPressureOptions, r as CreateAppOptions, s as AuthOption, t as preloadResources, u as CustomPluginAuthOption } from "../preloadResources-CM4uUaFv.mjs";
5
5
  import { FastifyInstance, FastifyServerOptions } from "fastify";
6
6
  //#region src/factory/createApp.d.ts
7
7
  /**
@@ -133,4 +133,4 @@ declare const testingPreset: Partial<CreateAppOptions>;
133
133
  declare const edgePreset: Partial<CreateAppOptions>;
134
134
  declare function getPreset(name: "production" | "development" | "testing" | "edge" | "worker"): Partial<CreateAppOptions>;
135
135
  //#endregion
136
- export { ArcFactory, type ArcModule, type ArcModuleInput, type ArcModuleRegistry, type ArcWorker, type AuthOption, type BetterAuthOption, type CreateAppOptions, type CreateWorkerOptions, type CustomAuthenticatorOption, type CustomPluginAuthOption, type FetchHandlerOptions, type JwtAuthOption, type LoadResourcesOptions, type MultipartOptions, type RawBodyOptions, type ResourceLike, type ResourceModule, type UnderPressureOptions, type WorkerHealthOptions, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
136
+ export { ArcFactory, type ArcModule, type ArcModuleInput, type ArcModuleRegistry, type ArcWorker, type AuthOption, type BetterAuthOption, type CreateAppOptions, type CreateWorkerOptions, type CustomAuthenticatorOption, type CustomPluginAuthOption, type EventHandlerDefinition, type FetchHandlerOptions, type JwtAuthOption, type LoadResourcesOptions, type ModuleContribution, type MultipartOptions, type RawBodyOptions, type ResourceLike, type ResourceModule, type UnderPressureOptions, type WorkerHealthOptions, collectModuleHealthChecks, collectModuleScheduledJobs, collectModuleWorkflows, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveContribution, resolveModule, subscribeModuleEventHandlers, testingPreset, toFetchHandler };
@@ -1,20 +1,42 @@
1
- import { a as edgePreset, c as testingPreset, d as getModuleExports, f as orderModules, h as preloadResourcesAsync, i as developmentPreset, l as workerPreset, m as preloadResources, n as createApp, o as getPreset, p as resolveModule, s as productionPreset, t as ArcFactory, u as defineModule } from "../createApp-CjOeWJ9_.mjs";
2
- import { t as healthPlugin } from "../health-ScyjIVo5.mjs";
1
+ import { r as mergeHealthChecks, t as healthPlugin } from "../health-CB_m4S32.mjs";
2
+ import { a as edgePreset, c as testingPreset, d as subscribeModuleEventHandlers, f as collectModuleHealthChecks, g as resolveModule, h as resolveContribution, i as developmentPreset, l as workerPreset, m as collectModuleWorkflows, n as createApp, o as getPreset, p as collectModuleScheduledJobs, s as productionPreset, t as ArcFactory, u as orderModules } from "../createApp-Du5G54TM.mjs";
3
+ import { n as getModuleExports, t as defineModule } from "../module-_-M7C5ES.mjs";
3
4
  import { t as loadResources } from "../loadResources-DpU8pfjV.mjs";
5
+ import { n as preloadResourcesAsync, t as preloadResources } from "../preloadResources-DSY5HGpS.mjs";
4
6
  //#region src/factory/createWorker.ts
5
7
  async function createWorker(options, workerOptions = {}) {
6
8
  const { auth: _auth, helmet: _helmet, cors: _cors, rateLimit: _rateLimit, multipart: _multipart, rawBody: _rawBody, sensible: _sensible, underPressure: _underPressure, replyHelpers: _replyHelpers, ...runtimeOptions } = options;
9
+ const sharedHealth = typeof runtimeOptions.arcPlugins?.health === "object" && runtimeOptions.arcPlugins.health !== null ? runtimeOptions.arcPlugins.health : {};
10
+ const workerArcPlugins = {
11
+ ...workerPreset.arcPlugins,
12
+ ...runtimeOptions.arcPlugins,
13
+ ...workerOptions.health ? { health: false } : {}
14
+ };
7
15
  const app = await createApp({
8
16
  ...runtimeOptions,
9
17
  preset: "worker",
10
- ...runtimeOptions.arcPlugins ? { arcPlugins: {
11
- ...workerPreset.arcPlugins,
12
- ...runtimeOptions.arcPlugins
13
- } } : {}
18
+ arcPlugins: workerArcPlugins
14
19
  });
15
20
  if (workerOptions.health) {
16
21
  const { port, host = "0.0.0.0", checks = [] } = workerOptions.health;
17
- await app.register(healthPlugin, { checks });
22
+ const mergedChecks = mergeHealthChecks([
23
+ {
24
+ owner: "the module graph",
25
+ checks: app.arc?.healthChecks ?? []
26
+ },
27
+ {
28
+ owner: "shared arcPlugins.health",
29
+ checks: sharedHealth.checks
30
+ },
31
+ {
32
+ owner: "the worker probe",
33
+ checks
34
+ }
35
+ ]);
36
+ await app.register(healthPlugin, {
37
+ ...sharedHealth,
38
+ checks: mergedChecks
39
+ });
18
40
  await app.listen({
19
41
  port,
20
42
  host
@@ -98,4 +120,4 @@ function payloadTooLarge(limit) {
98
120
  });
99
121
  }
100
122
  //#endregion
101
- export { ArcFactory, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
123
+ export { ArcFactory, collectModuleHealthChecks, collectModuleScheduledJobs, collectModuleWorkflows, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveContribution, resolveModule, subscribeModuleEventHandlers, testingPreset, toFetchHandler };
@@ -1,6 +1,7 @@
1
- import { xn as HookSystem, z as ResourceRegistry } from "./index-BBsvQt7U.mjs";
1
+ import { mn as HookSystem, z as ResourceRegistry } from "./index-DPRH0Cme.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "./externalPaths-BE6wFpYv.mjs";
3
- import { r as ArcModuleRegistry } from "./module-B4u10iOJ.mjs";
3
+ import { h as HealthCheck, r as ArcModuleRegistry, t as ArcModule, u as ScheduleDefinition } from "./types-Cu6vVJmh.mjs";
4
+ import "./index-CZY4EGga.mjs";
4
5
  import { FastifyPluginAsync } from "fastify";
5
6
  //#region src/core/arcCorePlugin.d.ts
6
7
  interface ArcCorePluginOptions {
@@ -38,6 +39,29 @@ interface ArcCore {
38
39
  * Populated lazily by `registerResources` — absent until a module exports.
39
40
  */
40
41
  modules?: Partial<ArcModuleRegistry> & Record<string, unknown>;
42
+ /**
43
+ * Module-contributed readiness checks, collected in dependency order and
44
+ * frozen by `createApp`. The API role merges these into its `healthPlugin`;
45
+ * `createWorker` reads this to give its probe listener the identical union
46
+ * (the worker preset disables the main health plugin, so this is how module
47
+ * checks reach the probe without a double registration). Empty until
48
+ * `createApp` populates it.
49
+ */
50
+ healthChecks?: readonly HealthCheck[];
51
+ /**
52
+ * The resolved, dependency-ordered module definitions (frozen by createApp).
53
+ * Integrations that collect a module arm at their OWN init time — e.g. the
54
+ * streamline integration calling `collectModuleWorkflows(fastify, defs)` after
55
+ * it has created + decorated its container — read the list from here. Empty
56
+ * until createApp populates it.
57
+ */
58
+ moduleDefinitions?: readonly ArcModule[];
59
+ /**
60
+ * Module-contributed recurring schedules, resolved once in dependency order
61
+ * and frozen after bootstraps. The same definitions are executed by Arc's
62
+ * schedules plugin and exposed here for introspection. Empty until collection.
63
+ */
64
+ scheduledJobs?: readonly Readonly<ScheduleDefinition>[];
41
65
  }
42
66
  declare const arcCorePlugin: FastifyPluginAsync<ArcCorePluginOptions>;
43
67
  declare const _default: FastifyPluginAsync<ArcCorePluginOptions>;
@@ -334,7 +334,7 @@ export default crudSchemas;
334
334
  const camel = toCamelCase(name);
335
335
  const useMongoKit = config.adapter === "mongokit" || !config.adapter;
336
336
  const schemaGeneratorImport = useMongoKit ? `import { buildCrudSchemasFromModel } from '@classytic/mongokit';\n` : "";
337
- const queryParserImport = useMongoKit ? `\nimport { QueryParser } from '@classytic/mongokit';\n\nconst queryParser = new QueryParser({\n // Whitelist the fields this resource accepts in URL filters.\n // Empty by default — only \`createdAt\` is implicit; add yours.\n allowedFilterFields: [],\n});\n` : "";
337
+ const queryParserImport = useMongoKit ? `\nimport { QueryParser } from '@classytic/mongokit';\n\n// Fail-closed by default (mongokit >=3.25): invalid/blocked query input →\n// HTTP 400 instead of silently broadening the result set.\nconst queryParser = new QueryParser({\n // Whitelist the fields this resource accepts in URL filters.\n // Empty by default — only \`createdAt\` is implicit; add yours.\n allowedFilterFields: [],\n});\n` : "";
338
338
  const adapterCall = useMongoKit ? `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository, schemaGenerator: buildCrudSchemasFromModel })` : `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository })`;
339
339
  const queryParserConfig = useMongoKit ? `\n queryParser,` : "";
340
340
  return isMultiTenant ? `/**
@@ -0,0 +1,45 @@
1
+ import { u as ValidationError } from "./errors-DkO8Nk6-.mjs";
2
+ //#region src/utils/headers.ts
3
+ /**
4
+ * Canonical header typing + accessor.
5
+ *
6
+ * Node/Fastify header values are `string | string[] | undefined` — repeated
7
+ * headers arrive as arrays. `IRequestContext.headers` is DECLARED as
8
+ * `Record<string, string | undefined>` for ergonomic host code, but the HTTP
9
+ * adapter passes Fastify's headers object through unchanged, so `string[]`
10
+ * CAN appear at runtime behind the narrower type (the declared type is
11
+ * scheduled to widen to {@link ArcHeaders} in v3). Until then, read headers
12
+ * through {@link getHeader} instead of indexing — it's honest about arrays
13
+ * and deterministic about duplicates.
14
+ */
15
+ /**
16
+ * Read a single header value. Lowercases the name (Node normalizes incoming
17
+ * header names to lowercase) and returns the FIRST value when the header
18
+ * was repeated.
19
+ *
20
+ * Duplicate policy: first-value is deterministic, but for security-sensitive
21
+ * headers (authorization material, forwarded-for chains, content-length) a
22
+ * duplicate usually signals smuggling or a broken proxy — consumers that
23
+ * care should check `Array.isArray(headers[name])` and REJECT instead of
24
+ * picking a winner.
25
+ */
26
+ function getHeader(headers, name) {
27
+ if (!headers) return void 0;
28
+ const value = headers[name.toLowerCase()];
29
+ if (Array.isArray(value)) return value[0];
30
+ return value;
31
+ }
32
+ /**
33
+ * Read an identity-bearing or protocol header and reject repeated values.
34
+ *
35
+ * Repeated security-sensitive headers are ambiguous at proxy boundaries and
36
+ * may indicate request smuggling. Callers must not silently choose a winner.
37
+ */
38
+ function requireSingleHeaderValue(headers, name) {
39
+ if (!headers) return void 0;
40
+ const value = headers[name.toLowerCase()];
41
+ if (Array.isArray(value)) throw new ValidationError(`Duplicate '${name}' header`);
42
+ return value;
43
+ }
44
+ //#endregion
45
+ export { requireSingleHeaderValue as n, getHeader as t };