@classytic/arc 2.22.0 → 2.25.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/dist/{BaseController-C6FlRWxF.mjs → BaseController--M8EMzt0.mjs} +92 -22
  2. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  3. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  4. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  5. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -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 +264 -260
  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/cli/commands/describe.d.mts +1 -1
  14. package/dist/cli/commands/describe.mjs +1 -1
  15. package/dist/cli/commands/docs.mjs +2 -2
  16. package/dist/cli/commands/generate.mjs +1 -1
  17. package/dist/cli/commands/init.mjs +1 -1
  18. package/dist/cli/commands/introspect.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 +4 -4
  23. package/dist/{core-b6fgFgnd.mjs → core-EUNu0u07.mjs} +298 -12
  24. package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-B6GvAuAX.mjs} +4 -3
  25. package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  26. package/dist/docs/index.d.mts +1 -1
  27. package/dist/docs/index.mjs +2 -2
  28. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  29. package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
  30. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  31. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  32. package/dist/events/index.d.mts +57 -314
  33. package/dist/events/index.mjs +168 -93
  34. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  35. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  36. package/dist/factory/index.d.mts +21 -6
  37. package/dist/factory/index.mjs +58 -18
  38. package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
  39. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  40. package/dist/headers-BvlQSDMB.mjs +45 -0
  41. package/dist/health-CB_m4S32.mjs +377 -0
  42. package/dist/hooks/index.d.mts +1 -1
  43. package/dist/idempotency/index.d.mts +7 -0
  44. package/dist/idempotency/index.mjs +32 -20
  45. package/dist/{index-ogIHvOjo.d.mts → index-BMnbzvtO.d.mts} +70 -4
  46. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  47. package/dist/{index-DbtgCOCj.d.mts → index-DPRH0Cme.d.mts} +46 -94
  48. package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
  49. package/dist/{index-D8MKYGDa.d.mts → index-f4PTrk3B.d.mts} +37 -5
  50. package/dist/index.d.mts +6 -6
  51. package/dist/index.mjs +6 -5
  52. package/dist/{init-DleK3p9f.mjs → init-tlRSt667.mjs} +206 -205
  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 +68 -5
  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 +149 -22
  63. package/dist/integrations/webhooks.mjs +186 -42
  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/interface-n5hpKg1d.mjs +16 -0
  69. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  70. package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-Cb7olw0j.mjs} +316 -45
  71. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  72. package/dist/middleware/index.d.mts +17 -1
  73. package/dist/middleware/index.mjs +1 -1
  74. package/dist/migrations/index.d.mts +94 -44
  75. package/dist/migrations/index.mjs +196 -44
  76. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  77. package/dist/{openapi-BRpMZD7C.mjs → openapi-DrLtog_B.mjs} +4 -4
  78. package/dist/permissions/index.d.mts +3 -3
  79. package/dist/permissions/index.mjs +4 -3
  80. package/dist/{permissions-BJMuKMl7.mjs → permissions-BrtiBKGs.mjs} +305 -492
  81. package/dist/pipeline/index.d.mts +1 -1
  82. package/dist/{actionPermissions-ze70xXJi.mjs → pipeline-BN5G05rd.mjs} +4 -27
  83. package/dist/plugins/index.d.mts +7 -78
  84. package/dist/plugins/index.mjs +14 -97
  85. package/dist/plugins/response-cache.mjs +1 -0
  86. package/dist/plugins/tracing-entry.mjs +1 -1
  87. package/dist/{index-BEu4wjbL.d.mts → preloadResources-0oe-JSSS.d.mts} +59 -50
  88. package/dist/{createApp-Ci7yG5gv.mjs → preloadResources-DfyngIN9.mjs} +240 -68
  89. package/dist/presets/filesUpload.d.mts +3 -3
  90. package/dist/presets/filesUpload.mjs +3 -3
  91. package/dist/presets/index.d.mts +2 -1
  92. package/dist/presets/index.mjs +1 -1
  93. package/dist/presets/multiTenant.d.mts +1 -1
  94. package/dist/presets/multiTenant.mjs +1 -1
  95. package/dist/presets/search.d.mts +2 -2
  96. package/dist/presets/search.mjs +1 -1
  97. package/dist/{presets-gw0ciIUD.mjs → presets-DwFDVKP0.mjs} +2 -2
  98. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  99. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  100. package/dist/{registerRoute-xHc42fwB.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  101. package/dist/realtime-Cuw_deA-.mjs +375 -0
  102. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  103. package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
  104. package/dist/registry/index.d.mts +2 -2
  105. package/dist/registry/index.mjs +1 -1
  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/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
  110. package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
  111. package/dist/schemas/index.d.mts +67 -67
  112. package/dist/schemas/index.mjs +2 -3
  113. package/dist/scim/index.mjs +1 -2
  114. package/dist/scope/index.d.mts +2 -2
  115. package/dist/scope/index.mjs +4 -4
  116. package/dist/scope-DfaPntpo.mjs +36 -0
  117. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  118. package/dist/{sse-CfUjwjDk.mjs → sse-BQ1w0dfl.mjs} +8 -31
  119. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  120. package/dist/testing/index.d.mts +4 -3
  121. package/dist/testing/index.mjs +2 -2
  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-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  126. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  127. package/dist/{types-Bc42CUyk.d.mts → types-QFGc3Cjv.d.mts} +37 -2
  128. package/dist/usage/index.d.mts +3 -52
  129. package/dist/usage/index.mjs +0 -0
  130. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  131. package/dist/utils/index.d.mts +90 -57
  132. package/dist/utils/index.mjs +7 -3
  133. package/dist/{utils-FhJ88TJE.mjs → utils-taUVD4p3.mjs} +47 -385
  134. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  135. package/dist/{versioning-DLwklPlG.d.mts → versioning-DXgY5BUm.d.mts} +52 -31
  136. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  137. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  138. package/package.json +526 -453
  139. package/skills/arc/references/api-reference.md +3 -1
  140. package/skills/arc/references/integrations.md +20 -11
  141. package/skills/arc/references/production.md +734 -696
  142. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  143. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  144. package/dist/health-ScyjIVo5.mjs +0 -202
  145. package/dist/requestId-5BhTVzBv.mjs +0 -113
  146. package/dist/scope-k15RXfcq.mjs +0 -64
  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
  *
@@ -176,8 +172,11 @@ function validatePayload(payload, schema) {
176
172
  for (const [key, propSchema] of Object.entries(schema.properties)) if (key in record && record[key] !== void 0 && record[key] !== null) {
177
173
  const expectedType = propSchema.type;
178
174
  if (expectedType) {
179
- const actualType = Array.isArray(record[key]) ? "array" : typeof record[key];
180
- if (expectedType !== actualType) errors.push(`Field '${key}': expected ${expectedType}, got ${actualType}`);
175
+ const value = record[key];
176
+ const actualType = Array.isArray(value) ? "array" : typeof value;
177
+ if (expectedType === "integer") {
178
+ if (actualType !== "number" || !Number.isInteger(value)) errors.push(`Field '${key}': expected integer, got ${actualType}`);
179
+ } else if (expectedType !== actualType) errors.push(`Field '${key}': expected ${expectedType}, got ${actualType}`);
181
180
  }
182
181
  }
183
182
  }
@@ -240,6 +239,14 @@ const CACHE_EVENTS = Object.freeze({
240
239
  });
241
240
  //#endregion
242
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
+ */
243
250
  const DEFAULT_LEASE_MS$2 = 3e4;
244
251
  var MemoryOutboxStore = class {
245
252
  entries = [];
@@ -353,7 +360,110 @@ var MemoryOutboxStore = class {
353
360
  }
354
361
  };
355
362
  //#endregion
363
+ //#region src/events/outbox/backoff.ts
364
+ /**
365
+ * Compute a `retryAt` `Date` using exponential backoff with jitter.
366
+ *
367
+ * This is a convenience helper for store authors implementing
368
+ * {@link OutboxStore.fail}: call it to compute the retry visibility window
369
+ * based on the event's current attempt count.
370
+ *
371
+ * Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
372
+ *
373
+ * @example Basic usage inside a store's `fail()` method
374
+ * ```typescript
375
+ * async fail(eventId, error, options) {
376
+ * const entry = await this.findById(eventId);
377
+ * entry.attempts++;
378
+ * if (entry.attempts >= MAX_ATTEMPTS) {
379
+ * return this.deadLetter(eventId, error);
380
+ * }
381
+ * const retryAt = exponentialBackoff({ attempt: entry.attempts });
382
+ * entry.visibleAt = retryAt;
383
+ * await this.update(entry);
384
+ * }
385
+ * ```
386
+ *
387
+ * @example Tuning for a faster transport
388
+ * ```typescript
389
+ * exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
390
+ * // attempt=1 → ~250ms ±30%
391
+ * // attempt=2 → ~500ms ±30%
392
+ * // attempt=3 → ~1000ms ±30%
393
+ * // attempt=10 → capped at 10_000ms
394
+ * ```
395
+ */
396
+ function exponentialBackoff(options) {
397
+ const attempt = Math.max(1, Math.floor(options.attempt));
398
+ const baseMs = options.baseMs ?? 1e3;
399
+ const maxMs = options.maxMs ?? 6e4;
400
+ const jitter = Math.max(0, Math.min(1, options.jitter ?? .2));
401
+ const now = options.now ?? Date.now();
402
+ const exp = baseMs * 2 ** (attempt - 1);
403
+ const capped = Math.min(maxMs, exp);
404
+ const jittered = jitter > 0 ? capped * (1 + Math.random() * jitter) : capped;
405
+ return new Date(now + jittered);
406
+ }
407
+ //#endregion
356
408
  //#region src/events/repository-outbox-adapter.ts
409
+ /**
410
+ * RepositoryLike → OutboxStore adapter.
411
+ *
412
+ * Maps the `OutboxStore` vocabulary (save / claimPending / acknowledge /
413
+ * fail / getDeadLettered / purge) onto arc's own `RepositoryLike` primitives
414
+ * (create / getOne / findAll / deleteMany / findOneAndUpdate). `EventOutbox`
415
+ * wraps a passed repository with this helper when you use the
416
+ * `{ repository }` option; the function is also re-exported from
417
+ * `@classytic/arc/events` so consumers can build and decorate the store
418
+ * manually (metrics, tracing, multi-transport fan-out).
419
+ *
420
+ * Portability: filters compose via `@classytic/repo-core/filter` and
421
+ * updates via `@classytic/repo-core/update`. The primary-key column name
422
+ * is read from `repository.idField` — mongokit defaults to `_id`,
423
+ * sqlitekit / pgkit / prismakit to the schema's declared PK. The adapter
424
+ * therefore runs on any kit that implements `StandardRepo.findOneAndUpdate`
425
+ * + `getOne` + `getAll` + `deleteMany` + `create`.
426
+ *
427
+ * `fail()` uses a lease-gated read-then-write pair to preserve
428
+ * `firstFailedAt` across retries without relying on Mongo's aggregation-
429
+ * pipeline `$ifNull`. Leases guarantee single-writer during the failure
430
+ * window (`claimPending` filters out non-owned rows), so the two calls are
431
+ * safe under concurrent relayers.
432
+ *
433
+ * Why we DON'T use `StandardRepo.claim()` (repo-core 0.4+) anywhere in this
434
+ * adapter — design note for the next reader who wants to "modernize" us:
435
+ *
436
+ * • `claimPending` (FIFO claim-next loop): `claim(id, ...)` requires the
437
+ * candidate id upfront, but the relayer doesn't have it — that's the
438
+ * whole point of `findOneAndUpdate({ filter, sort: { createdAt: 1 } })`,
439
+ * which finds + claims in a SINGLE round-trip per claimed event.
440
+ * Replacing with `getOne(filter, sort) → claim(id, ...)` doubles the
441
+ * round-trips, opens a TOCTOU window where the candidate state can
442
+ * change between read and CAS (we'd need a retry loop on null-on-race),
443
+ * and gains nothing — the existing call is already atomic and
444
+ * race-free. Throughput regression with no upside; do not adopt.
445
+ *
446
+ * • `acknowledge` (pending → delivered): id is in scope, but the current
447
+ * filter uses `ne('status', 'delivered')` to allow ack from any
448
+ * non-delivered state (defensive; lease invariant means it's pending
449
+ * in practice). `claim` requires an exact `from` value, so adopting
450
+ * would tighten the source-state predicate from "not delivered" to
451
+ * "exactly pending". That's a behavior change — even if the new
452
+ * behavior is arguably tighter/safer, the constraint here is "no
453
+ * semantic regression". Keep `findOneAndUpdate`.
454
+ *
455
+ * • `fail` (any → pending|dead_letter): the current `baseFilter` is
456
+ * id-only (with optional `leaseOwner` guard) — there's NO source-state
457
+ * predicate at all. Adopting `claim` would ADD `from: 'pending'`,
458
+ * blocking `fail()` calls against rows in any other state. Same "no
459
+ * semantic regression" concern; keep `findOneAndUpdate`. The
460
+ * read-then-write pair also exists here to preserve `firstFailedAt`
461
+ * portably (see paragraph above) — `claim` doesn't simplify that.
462
+ *
463
+ * No kit currently ships a filter-based `claimNext`/`claimPending` (Path C
464
+ * in the v2.11.x evaluation) — checked mongokit 3.13.0 + sqlitekit 0.3.0.
465
+ * If/when one appears, the FIFO loop becomes a 1-call adoption candidate.
466
+ */
357
467
  const DEFAULT_LEASE_MS$1 = 3e4;
358
468
  const DEFAULT_CLAIM_LIMIT = 100;
359
469
  const DEFAULT_PURGE_BATCH = 500;
@@ -430,25 +540,26 @@ function repositoryAsOutboxStore(repository) {
430
540
  const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS$1;
431
541
  const consumerId = options?.consumerId ?? "anonymous";
432
542
  const typeFilter = options?.types?.length ? anyOf("type", options.types) : null;
433
- const claimed = [];
434
- for (let i = 0; i < limit; i++) {
435
- const now = /* @__PURE__ */ new Date();
436
- const leaseExpiresAt = new Date(now.getTime() + leaseMs);
437
- const filter = typeFilter ? and(claimableFilter(now), typeFilter) : claimableFilter(now);
438
- const doc = await r.findOneAndUpdate(filter, update({
543
+ const now = /* @__PURE__ */ new Date();
544
+ const candidateFilter = typeFilter ? and(claimableFilter(now), typeFilter) : claimableFilter(now);
545
+ const candidates = unwrapDocs(await r.getAll({
546
+ filters: candidateFilter,
547
+ sort: { createdAt: 1 },
548
+ page: 1,
549
+ limit
550
+ }));
551
+ if (candidates.length === 0) return [];
552
+ return (await Promise.all(candidates.map((candidate) => {
553
+ const claimNow = /* @__PURE__ */ new Date();
554
+ const leaseExpiresAt = new Date(claimNow.getTime() + leaseMs);
555
+ return r.findOneAndUpdate(and(eq(idField, candidate.event.meta.id), claimableFilter(claimNow)), update({
439
556
  set: {
440
557
  leaseOwner: consumerId,
441
558
  leaseExpiresAt
442
559
  },
443
560
  inc: { attempts: 1 }
444
- }), {
445
- sort: { createdAt: 1 },
446
- returnDocument: "after"
447
- });
448
- if (!doc) break;
449
- if (isWellFormed(doc.event)) claimed.push(doc.event);
450
- }
451
- return claimed;
561
+ }), { returnDocument: "after" });
562
+ }))).filter((doc) => doc !== null).map((doc) => doc.event).filter(isWellFormed);
452
563
  },
453
564
  async acknowledge(eventId, options) {
454
565
  const now = /* @__PURE__ */ new Date();
@@ -534,7 +645,7 @@ function repositoryAsOutboxStore(repository) {
534
645
  };
535
646
  }
536
647
  //#endregion
537
- //#region src/events/outbox.ts
648
+ //#region src/events/outbox/relay.ts
538
649
  /** Default outbox retention — delivered events older than this are eligible for purge */
539
650
  const DEFAULT_OUTBOX_RETENTION_MS = 10080 * 60 * 1e3;
540
651
  const DEFAULT_LEASE_MS = 3e4;
@@ -549,34 +660,6 @@ const DEFAULT_LEASE_MS = 3e4;
549
660
  * the store).
550
661
  */
551
662
  const MAX_TRACKED_ATTEMPTS = 1e4;
552
- /**
553
- * Thrown by a store when `acknowledge` / `fail` is called by a consumer that
554
- * does not own the event's current lease.
555
- *
556
- * Stores that enforce lease ownership MUST throw this (not silently return)
557
- * so {@link EventOutbox} can detect the mismatch and avoid over-counting
558
- * successful deliveries. {@link EventOutbox.relay} catches it and reports
559
- * via {@link EventOutboxOptions.onError} instead of counting as relayed.
560
- */
561
- var OutboxOwnershipError = class extends Error {
562
- eventId;
563
- attemptedBy;
564
- currentOwner;
565
- constructor(eventId, attemptedBy, currentOwner) {
566
- 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.`);
567
- this.name = "OutboxOwnershipError";
568
- this.eventId = eventId;
569
- this.attemptedBy = attemptedBy;
570
- this.currentOwner = currentOwner;
571
- }
572
- };
573
- /** Thrown by {@link EventOutbox.store} when an event is missing `type` or `meta.id`. */
574
- var InvalidOutboxEventError = class extends Error {
575
- constructor(reason) {
576
- super(`Invalid outbox event: ${reason}`);
577
- this.name = "InvalidOutboxEventError";
578
- }
579
- };
580
663
  var EventOutbox = class {
581
664
  _store;
582
665
  _transport;
@@ -640,7 +723,7 @@ var EventOutbox = class {
640
723
  session: autoSession
641
724
  };
642
725
  }
643
- if (!effective?.headers?.["traceparent"] && this._traceContextProvider) {
726
+ if (!effective?.headers?.traceparent && this._traceContextProvider) {
644
727
  const traceHeaders = this._traceContextProvider();
645
728
  if (traceHeaders && Object.keys(traceHeaders).length > 0) effective = {
646
729
  ...effective ?? {},
@@ -723,11 +806,14 @@ var EventOutbox = class {
723
806
  usedPublishMany: false
724
807
  };
725
808
  if (!this._transport) return empty;
809
+ const claimStartedAt = Date.now();
726
810
  const pending = this._store.claimPending ? await this._store.claimPending({
727
811
  limit: this._batchSize,
728
812
  consumerId: this._consumerId,
729
813
  leaseMs: this._leaseMs
730
814
  }) : await this._store.getPending(this._batchSize);
815
+ const claimMs = Date.now() - claimStartedAt;
816
+ const publishStartedAt = Date.now();
731
817
  const valid = [];
732
818
  let malformed = 0;
733
819
  for (const event of pending) {
@@ -837,7 +923,9 @@ var EventOutbox = class {
837
923
  malformed,
838
924
  failHookErrors: counts.failHookErrors,
839
925
  deadLettered: counts.deadLettered,
840
- usedPublishMany: canPublishMany && valid.length > 0
926
+ usedPublishMany: canPublishMany && valid.length > 0,
927
+ claimMs,
928
+ publishMs: Date.now() - publishStartedAt
841
929
  };
842
930
  }
843
931
  /**
@@ -871,51 +959,39 @@ function normalizeError(err) {
871
959
  };
872
960
  return { message: String(err) };
873
961
  }
962
+ //#endregion
963
+ //#region src/events/subscribe-helpers.ts
874
964
  /**
875
- * Compute a `retryAt` `Date` using exponential backoff with jitter.
965
+ * Subscribe-side helpers — close the symmetry that arc's publish-side
966
+ * already has via [`eventPlugin({ registry, validateMode: 'reject' })`](./eventPlugin.ts).
876
967
  *
877
- * This is a convenience helper for store authors implementing
878
- * {@link OutboxStore.fail}: call it to compute the retry visibility window
879
- * based on the event's current attempt count.
968
+ * Two wrapper pairs, both transport-agnostic:
880
969
  *
881
- * Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
970
+ * - `wrapWithSchema` / `subscribeWithSchema` — read the schema from a
971
+ * typed `EventDefinitionOutput<T>` and validate `event.payload` before
972
+ * the handler runs. Single source of truth for event shape: declare
973
+ * once at `defineEvent`, enforced on BOTH sides without parallel Zod
974
+ * schemas in subscriber files.
882
975
  *
883
- * @example Basic usage inside a store's `fail()` method
884
- * ```typescript
885
- * async fail(eventId, error, options) {
886
- * const entry = await this.findById(eventId);
887
- * entry.attempts++;
888
- * if (entry.attempts >= MAX_ATTEMPTS) {
889
- * return this.deadLetter(eventId, error);
890
- * }
891
- * const retryAt = exponentialBackoff({ attempt: entry.attempts });
892
- * entry.visibleAt = retryAt;
893
- * await this.update(entry);
894
- * }
895
- * ```
976
+ * - `wrapWithBoundary` / `subscribeWithBoundary` — catch + log + swallow
977
+ * handler errors, for projection / cache-invalidation / fire-and-forget
978
+ * handlers where retry would just delay the next-event resync. Lighter
979
+ * than `withRetry` (no DLQ, no exponential backoff) and avoids the
980
+ * "single bad event poisons the bus" hazard.
896
981
  *
897
- * @example Tuning for a faster transport
898
- * ```typescript
899
- * exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
900
- * // attempt=1 → ~250ms ±30%
901
- * // attempt=2 → ~500ms ±30%
902
- * // attempt=3 → ~1000ms ±30%
903
- * // attempt=10 → capped at 10_000ms
982
+ * Both pairs compose with `withRetry` and with each other:
983
+ *
984
+ * ```ts
985
+ * await subscribeWithSchema(
986
+ * fastify,
987
+ * OrderPaid,
988
+ * withRetry(handler, { maxRetries: 3 }),
989
+ * );
904
990
  * ```
991
+ *
992
+ * @see [retry.ts](./retry.ts) for the retry + DLQ wrapper.
993
+ * @see [defineEvent.ts](./defineEvent.ts) for `defineEvent` / `EventDefinitionOutput`.
905
994
  */
906
- function exponentialBackoff(options) {
907
- const attempt = Math.max(1, Math.floor(options.attempt));
908
- const baseMs = options.baseMs ?? 1e3;
909
- const maxMs = options.maxMs ?? 6e4;
910
- const jitter = Math.max(0, Math.min(1, options.jitter ?? .2));
911
- const now = options.now ?? Date.now();
912
- const exp = baseMs * 2 ** (attempt - 1);
913
- const capped = Math.min(maxMs, exp);
914
- const jittered = jitter > 0 ? capped * (1 + Math.random() * jitter) : capped;
915
- return new Date(now + jittered);
916
- }
917
- //#endregion
918
- //#region src/events/subscribe-helpers.ts
919
995
  /**
920
996
  * Pure handler wrapper — returns a new `EventHandler` that validates
921
997
  * `event.payload` against the definition's schema before invoking the handler.
@@ -944,7 +1020,6 @@ function wrapWithSchema(definition, handler, options = {}) {
944
1020
  if (validate && definition.schema) result = validate(definition.schema, event.payload);
945
1021
  else if (registry) result = registry.validate(definition.name, event.payload, eventVersion);
946
1022
  else if (definition.schema) {
947
- const { createEventRegistry } = await Promise.resolve().then(() => defineEvent_exports);
948
1023
  const adhoc = createEventRegistry();
949
1024
  adhoc.register(definition);
950
1025
  result = adhoc.validate(definition.name, event.payload);
@@ -1038,4 +1113,4 @@ async function subscribeWithBoundary(fastify, pattern, handler, options) {
1038
1113
  return fastify.events.subscribe(pattern, wrapWithBoundary(handler, options));
1039
1114
  }
1040
1115
  //#endregion
1041
- 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 };
1116
+ 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,6 @@
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-9_fuetB4.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-BEu4wjbL.mjs";
4
- import { g as HealthCheck } from "../versioning-DLwklPlG.mjs";
1
+ import { _ as ResourceModule, a as orderModules, c as collectModuleScheduledJobs, d as ArcModuleInput, f as ArcModuleRegistry, g as ResourceLike, h as LoadResourcesOptions, i as resolveModule, l as collectModuleWorkflows, m as ModuleContribution, n as getModuleExports, o as subscribeModuleEventHandlers, p as EventHandlerDefinition, r as resolveContribution, s as collectModuleHealthChecks, t as defineModule, u as ArcModule, v as loadResources, w as HealthCheck } from "../index-KUoxjIq1.mjs";
2
+ import "../fastify-augmentation-dPNBVeIj.mjs";
3
+ 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-0oe-JSSS.mjs";
5
4
  import { FastifyInstance, FastifyServerOptions } from "fastify";
6
5
  //#region src/factory/createApp.d.ts
7
6
  /**
@@ -64,15 +63,31 @@ interface FetchHandlerOptions {
64
63
  * @default true
65
64
  */
66
65
  autoReady?: boolean;
66
+ /**
67
+ * Reject request bodies larger than this many bytes with 413
68
+ * `arc.payload_too_large`. Enforcement precision matters: bodies with a
69
+ * declared `Content-Length` are rejected BEFORE buffering; chunked or
70
+ * undeclared bodies are necessarily buffered first and checked AFTER
71
+ * (`arrayBuffer()` allocates the full body — a consequence of the
72
+ * buffered-adapter design, not a bug). Platform/gateway body limits
73
+ * remain the real pre-allocation protection for hostile senders.
74
+ * Fastify's own `bodyLimit` still applies after this gate. Set to `0`
75
+ * to disable the gate.
76
+ * @default 1_048_576 (1 MiB — matches Fastify's default bodyLimit)
77
+ */
78
+ maxRequestBytes?: number;
67
79
  }
68
80
  /**
69
81
  * Convert a Fastify app into a Web Standards fetch handler.
70
82
  *
71
83
  * The returned function accepts a Web Standard `Request` and returns
72
- * a Web Standard `Response` — the universal serverless/edge contract.
84
+ * a Web Standard `Response` — the universal serverless contract.
73
85
  *
74
86
  * Internally uses `app.inject()` which processes the request through
75
87
  * the full Fastify pipeline (hooks, plugins, routes) without TCP.
88
+ * Bodies are read as raw bytes (`arrayBuffer()`), so binary payloads
89
+ * (protobuf, images, non-UTF-8 charsets) survive intact — the
90
+ * content-type parser decides how to decode, same as a real socket.
76
91
  */
77
92
  declare function toFetchHandler(app: FastifyInstance, options?: FetchHandlerOptions): (request: Request) => Promise<Response>;
78
93
  //#endregion
@@ -117,4 +132,4 @@ declare const testingPreset: Partial<CreateAppOptions>;
117
132
  declare const edgePreset: Partial<CreateAppOptions>;
118
133
  declare function getPreset(name: "production" | "development" | "testing" | "edge" | "worker"): Partial<CreateAppOptions>;
119
134
  //#endregion
120
- 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 };
135
+ 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,40 @@
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-Ci7yG5gv.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 { _ as collectModuleScheduledJobs, b as resolveModule, c as getPreset, d as workerPreset, f as defineModule, g as collectModuleHealthChecks, h as subscribeModuleEventHandlers, i as createApp, l as productionPreset, m as orderModules, n as preloadResourcesAsync, o as developmentPreset, p as getModuleExports, r as ArcFactory, s as edgePreset, t as preloadResources, u as testingPreset, v as collectModuleWorkflows, y as resolveContribution } from "../preloadResources-DfyngIN9.mjs";
3
3
  import { t as loadResources } from "../loadResources-DpU8pfjV.mjs";
4
4
  //#region src/factory/createWorker.ts
5
5
  async function createWorker(options, workerOptions = {}) {
6
6
  const { auth: _auth, helmet: _helmet, cors: _cors, rateLimit: _rateLimit, multipart: _multipart, rawBody: _rawBody, sensible: _sensible, underPressure: _underPressure, replyHelpers: _replyHelpers, ...runtimeOptions } = options;
7
+ const sharedHealth = typeof runtimeOptions.arcPlugins?.health === "object" && runtimeOptions.arcPlugins.health !== null ? runtimeOptions.arcPlugins.health : {};
8
+ const workerArcPlugins = {
9
+ ...workerPreset.arcPlugins,
10
+ ...runtimeOptions.arcPlugins,
11
+ ...workerOptions.health ? { health: false } : {}
12
+ };
7
13
  const app = await createApp({
8
14
  ...runtimeOptions,
9
15
  preset: "worker",
10
- ...runtimeOptions.arcPlugins ? { arcPlugins: {
11
- ...workerPreset.arcPlugins,
12
- ...runtimeOptions.arcPlugins
13
- } } : {}
16
+ arcPlugins: workerArcPlugins
14
17
  });
15
18
  if (workerOptions.health) {
16
19
  const { port, host = "0.0.0.0", checks = [] } = workerOptions.health;
17
- await app.register(healthPlugin, { checks });
20
+ const mergedChecks = mergeHealthChecks([
21
+ {
22
+ owner: "the module graph",
23
+ checks: app.arc?.healthChecks ?? []
24
+ },
25
+ {
26
+ owner: "shared arcPlugins.health",
27
+ checks: sharedHealth.checks
28
+ },
29
+ {
30
+ owner: "the worker probe",
31
+ checks
32
+ }
33
+ ]);
34
+ await app.register(healthPlugin, {
35
+ ...sharedHealth,
36
+ checks: mergedChecks
37
+ });
18
38
  await app.listen({
19
39
  port,
20
40
  host
@@ -37,28 +57,37 @@ async function createWorker(options, workerOptions = {}) {
37
57
  * Convert a Fastify app into a Web Standards fetch handler.
38
58
  *
39
59
  * The returned function accepts a Web Standard `Request` and returns
40
- * a Web Standard `Response` — the universal serverless/edge contract.
60
+ * a Web Standard `Response` — the universal serverless contract.
41
61
  *
42
62
  * Internally uses `app.inject()` which processes the request through
43
63
  * the full Fastify pipeline (hooks, plugins, routes) without TCP.
64
+ * Bodies are read as raw bytes (`arrayBuffer()`), so binary payloads
65
+ * (protobuf, images, non-UTF-8 charsets) survive intact — the
66
+ * content-type parser decides how to decode, same as a real socket.
44
67
  */
45
68
  function toFetchHandler(app, options = {}) {
46
- const { autoReady = true } = options;
47
- let ready = false;
69
+ const { autoReady = true, maxRequestBytes = 1048576 } = options;
70
+ let readyPromise = null;
48
71
  return async (request) => {
49
- if (autoReady && !ready) {
50
- await app.ready();
51
- ready = true;
72
+ if (autoReady) {
73
+ readyPromise ??= Promise.resolve(app.ready());
74
+ await readyPromise;
52
75
  }
53
76
  const url = new URL(request.url);
54
77
  const headers = {};
55
78
  request.headers.forEach((value, key) => {
56
79
  headers[key] = value;
57
80
  });
81
+ if (maxRequestBytes > 0) {
82
+ const declared = Number(request.headers.get("content-length"));
83
+ if (Number.isFinite(declared) && declared > maxRequestBytes) return payloadTooLarge(maxRequestBytes);
84
+ }
58
85
  let payload;
59
- if (request.method !== "GET" && request.method !== "HEAD") try {
60
- payload = await request.text();
61
- } catch {}
86
+ if (request.method !== "GET" && request.method !== "HEAD" && request.body !== null) {
87
+ const bytes = await request.arrayBuffer();
88
+ if (maxRequestBytes > 0 && bytes.byteLength > maxRequestBytes) return payloadTooLarge(maxRequestBytes);
89
+ if (bytes.byteLength > 0) payload = Buffer.from(bytes);
90
+ }
62
91
  const response = await app.inject({
63
92
  method: request.method,
64
93
  url: url.pathname + url.search,
@@ -71,11 +100,22 @@ function toFetchHandler(app, options = {}) {
71
100
  if (Array.isArray(value)) for (const v of value) responseHeaders.append(key, v);
72
101
  else responseHeaders.set(key, String(value));
73
102
  }
74
- return new Response(response.payload, {
103
+ return new Response(response.rawPayload, {
75
104
  status: response.statusCode,
76
105
  headers: responseHeaders
77
106
  });
78
107
  };
79
108
  }
109
+ /** Canonical ErrorContract shape for the pre-dispatch 413. */
110
+ function payloadTooLarge(limit) {
111
+ return new Response(JSON.stringify({
112
+ code: "arc.payload_too_large",
113
+ message: `Request body exceeds ${limit} bytes`,
114
+ status: 413
115
+ }), {
116
+ status: 413,
117
+ headers: { "content-type": "application/json" }
118
+ });
119
+ }
80
120
  //#endregion
81
- export { ArcFactory, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
121
+ 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,6 @@
1
- import { bn as HookSystem, z as ResourceRegistry } from "./index-DbtgCOCj.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 { f as ArcModuleRegistry, u as ArcModule, w as HealthCheck, y as ScheduleDefinition } from "./index-KUoxjIq1.mjs";
4
4
  import { FastifyPluginAsync } from "fastify";
5
5
  //#region src/core/arcCorePlugin.d.ts
6
6
  interface ArcCorePluginOptions {
@@ -38,6 +38,29 @@ interface ArcCore {
38
38
  * Populated lazily by `registerResources` — absent until a module exports.
39
39
  */
40
40
  modules?: Partial<ArcModuleRegistry> & Record<string, unknown>;
41
+ /**
42
+ * Module-contributed readiness checks, collected in dependency order and
43
+ * frozen by `createApp`. The API role merges these into its `healthPlugin`;
44
+ * `createWorker` reads this to give its probe listener the identical union
45
+ * (the worker preset disables the main health plugin, so this is how module
46
+ * checks reach the probe without a double registration). Empty until
47
+ * `createApp` populates it.
48
+ */
49
+ healthChecks?: readonly HealthCheck[];
50
+ /**
51
+ * The resolved, dependency-ordered module definitions (frozen by createApp).
52
+ * Integrations that collect a module arm at their OWN init time — e.g. the
53
+ * streamline integration calling `collectModuleWorkflows(fastify, defs)` after
54
+ * it has created + decorated its container — read the list from here. Empty
55
+ * until createApp populates it.
56
+ */
57
+ moduleDefinitions?: readonly ArcModule[];
58
+ /**
59
+ * Module-contributed recurring schedules, resolved once in dependency order
60
+ * and frozen after bootstraps. The same definitions are executed by Arc's
61
+ * schedules plugin and exposed here for introspection. Empty until collection.
62
+ */
63
+ scheduledJobs?: readonly Readonly<ScheduleDefinition>[];
41
64
  }
42
65
  declare const arcCorePlugin: FastifyPluginAsync<ArcCorePluginOptions>;
43
66
  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 ? `/**