@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
@@ -648,7 +648,15 @@ function safeAsync(p, logger, op, ctx) {
648
648
  */
649
649
  var SendQueue = class {
650
650
  entries = [];
651
- /** `coalesceKey → entries[] index` for O(1) replacement. */
651
+ /**
652
+ * Logical queue head — `entries[head..]` is the live queue. Draining
653
+ * advances the pointer instead of `shift()`ing (which moves every
654
+ * trailing element AND invalidated every tracked coalesce index,
655
+ * making a full drain O(n²)). The prefix is garbage-collected by
656
+ * {@link compactIfNeeded} — amortized O(1) per drained frame.
657
+ */
658
+ head = 0;
659
+ /** `coalesceKey → ABSOLUTE entries[] index` for O(1) replacement. */
652
660
  coalesceIndex = /* @__PURE__ */ new Map();
653
661
  socket;
654
662
  capacity;
@@ -666,7 +674,7 @@ var SendQueue = class {
666
674
  /** Ordered, guaranteed-best-effort delivery. Overflow ⇒ terminate. */
667
675
  send(payload) {
668
676
  if (this.disposed) return;
669
- if (this.entries.length >= this.capacity) {
677
+ if (this.size() >= this.capacity) {
670
678
  this.terminate("Queue overflow");
671
679
  return;
672
680
  }
@@ -685,7 +693,7 @@ var SendQueue = class {
685
693
  if (this.disposed) return;
686
694
  if (coalesceKey !== void 0) {
687
695
  const idx = this.coalesceIndex.get(coalesceKey);
688
- if (idx !== void 0 && this.entries[idx]) {
696
+ if (idx !== void 0 && idx >= this.head && this.entries[idx]) {
689
697
  this.entries[idx] = {
690
698
  payload,
691
699
  cls: "droppable",
@@ -695,12 +703,16 @@ var SendQueue = class {
695
703
  return;
696
704
  }
697
705
  }
698
- if (this.entries.length >= this.capacity) {
699
- const shedIdx = this.entries.findIndex((e) => e.cls === "droppable");
706
+ if (this.size() >= this.capacity) {
707
+ let shedIdx = -1;
708
+ for (let i = this.head; i < this.entries.length; i++) if (this.entries[i]?.cls === "droppable") {
709
+ shedIdx = i;
710
+ break;
711
+ }
700
712
  if (shedIdx === -1) return;
701
713
  const evicted = this.entries.splice(shedIdx, 1)[0];
702
714
  if (evicted?.coalesceKey !== void 0) this.coalesceIndex.delete(evicted.coalesceKey);
703
- this.reindexCoalesceFrom(shedIdx);
715
+ for (const [key, idx] of this.coalesceIndex) if (idx > shedIdx) this.coalesceIndex.set(key, idx - 1);
704
716
  }
705
717
  this.entries.push({
706
718
  payload,
@@ -718,28 +730,44 @@ var SendQueue = class {
718
730
  */
719
731
  flush() {
720
732
  if (this.disposed) return;
721
- while (this.entries.length > 0 && this.socket.readyState === 1) {
733
+ while (this.size() > 0 && this.socket.readyState === 1) {
722
734
  if ((this.socket.bufferedAmount ?? 0) >= this.drainThreshold) {
723
735
  this.scheduleRetry();
724
- return;
736
+ break;
725
737
  }
726
- const entry = this.entries.shift();
738
+ const entry = this.entries[this.head];
727
739
  if (!entry) return;
740
+ this.head++;
728
741
  if (entry.coalesceKey !== void 0) this.coalesceIndex.delete(entry.coalesceKey);
729
- if (this.coalesceIndex.size > 0) this.reindexCoalesceFrom(0);
730
742
  try {
731
743
  this.socket.send(entry.payload);
732
744
  } catch {
733
745
  return;
734
746
  }
735
747
  }
748
+ this.compactIfNeeded();
736
749
  }
737
750
  size() {
738
- return this.entries.length;
751
+ return this.entries.length - this.head;
752
+ }
753
+ /**
754
+ * Drop the drained prefix once it dominates the array. Rebuilding the
755
+ * (small) coalesce index here is the amortized cost that replaces the
756
+ * per-frame O(n) reindex the old `shift()` drain paid.
757
+ */
758
+ compactIfNeeded() {
759
+ if (this.head < 64 || this.head * 2 < this.entries.length) return;
760
+ this.entries = this.entries.slice(this.head);
761
+ this.head = 0;
762
+ if (this.coalesceIndex.size > 0) {
763
+ this.coalesceIndex.clear();
764
+ this.reindexCoalesceFrom(0);
765
+ }
739
766
  }
740
767
  dispose() {
741
768
  this.disposed = true;
742
769
  this.entries.length = 0;
770
+ this.head = 0;
743
771
  this.coalesceIndex.clear();
744
772
  if (this.retryTimer) {
745
773
  clearTimeout(this.retryTimer);
@@ -829,7 +857,6 @@ async function handleConnection(ctx, socket, request) {
829
857
  clientId: serviceClientId,
830
858
  authMode: options.auth
831
859
  });
832
- options.messageEnvelope;
833
860
  const urlPushRef = readPushRefFromRequest(request);
834
861
  let pushRef;
835
862
  let envelope;
@@ -1279,7 +1306,7 @@ var RoomManager = class {
1279
1306
  for (const clientId of members) {
1280
1307
  if (clientId === excludeClientId) continue;
1281
1308
  const client = this.clients.get(clientId);
1282
- if (!client || client.socket.readyState !== 1) continue;
1309
+ if (client?.socket.readyState !== 1) continue;
1283
1310
  if ((client.socket.bufferedAmount ?? 0) > WS_BACKPRESSURE_LIMIT) continue;
1284
1311
  try {
1285
1312
  client.socket.send(message);
@@ -1303,7 +1330,7 @@ var RoomManager = class {
1303
1330
  if (!members) return;
1304
1331
  for (const clientId of members) {
1305
1332
  const client = this.clients.get(clientId);
1306
- if (!client || client.socket.readyState !== 1) continue;
1333
+ if (client?.socket.readyState !== 1) continue;
1307
1334
  if (client.queue) client.queue.sendRealtime(message, coalesceKey);
1308
1335
  else if ((client.socket.bufferedAmount ?? 0) <= WS_BACKPRESSURE_LIMIT) try {
1309
1336
  client.socket.send(message);
@@ -1,34 +1,5 @@
1
+ import { CacheAdapter } from "@classytic/repo-core/cache";
1
2
  //#region src/cache/interface.d.ts
2
- /**
3
- * Cache Store Interface — aligned with `@classytic/repo-core/cache.CacheAdapter`.
4
- *
5
- * Arc's cache layer speaks the same `get / set(ttlSeconds?) / del / clear(pattern?)`
6
- * transport-level contract published by `@classytic/repo-core`. One Redis
7
- * implementation drops into Arc's `QueryCache`, mongokit's cache plugin,
8
- * sqlitekit's cache plugin, and every future kit without wrapper shims.
9
- *
10
- * Arc extends the bare adapter with two optional observability fields —
11
- * `name` (for diagnostics) and `stats()` (for the response-cache plugin) —
12
- * that are opt-in: consumers implementing only `CacheAdapter` still
13
- * structurally satisfy `CacheStore`, so a raw repo-core adapter plugs
14
- * directly into Arc.
15
- *
16
- * ## TTL unit
17
- *
18
- * `ttlSeconds`, not milliseconds. Matches Redis (`SET … EX seconds`) which
19
- * is the dominant backend. `0` or `undefined` means no expiry; implementations
20
- * may apply their own default.
21
- *
22
- * ## Not-found semantics
23
- *
24
- * `get()` returns `undefined` on miss / expired. Matches repo-core.
25
- *
26
- * ## Sync-or-async
27
- *
28
- * Method returns are `Promise<T> | T` — in-memory `Map` adapters can be
29
- * synchronous; Redis adapters are async. Consumers always `await`, so
30
- * sync values just short-circuit the microtask.
31
- */
32
3
  interface CacheLogger {
33
4
  warn(message: string, ...args: unknown[]): void;
34
5
  error(message: string, ...args: unknown[]): void;
@@ -45,7 +16,14 @@ interface CacheStats {
45
16
  /** Number of entries evicted since creation */
46
17
  evictions: number;
47
18
  }
48
- interface CacheStore<TValue = unknown> {
19
+ /**
20
+ * `extends CacheAdapter` is load-bearing: repo-core owns the transport
21
+ * contract, and extending (instead of maintaining a structural copy) makes
22
+ * any drift between the two a COMPILE error in arc rather than a runtime
23
+ * incompatibility — e.g. an argument-order divergence on `increment` would
24
+ * make a conforming adapter misread a TTL as the increment amount.
25
+ */
26
+ interface CacheStore<TValue = unknown> extends CacheAdapter {
49
27
  /** Store name for logs/diagnostics. Optional to match repo-core's bare `CacheAdapter`. */
50
28
  readonly name?: string;
51
29
  /**
@@ -70,6 +48,21 @@ interface CacheStore<TValue = unknown> {
70
48
  * need strict invalidation must check for its presence: `store.clear?.(pattern)`.
71
49
  */
72
50
  clear?(pattern?: string): Promise<void> | void;
51
+ /**
52
+ * Atomic increment — adds `by` (default 1) to the integer at `key`,
53
+ * creating it with value `by` when absent; returns the NEW value.
54
+ * Signature is the canonical `@classytic/repo-core/cache.CacheAdapter`
55
+ * contract — Redis adapters map to `INCRBY` (+ `EXPIRE` when
56
+ * `ttlSeconds` is given).
57
+ *
58
+ * Optional — consumers feature-detect it. Arc's `QueryCache` uses it for
59
+ * version-based invalidation: with `increment`, concurrent bumps from
60
+ * multiple replicas are strictly monotonic and never collide; without it,
61
+ * QueryCache falls back to a read-modify-write that is monotonic within a
62
+ * process but can lose a concurrent replica's bump. Distributed hosts
63
+ * should use a store that implements this.
64
+ */
65
+ increment?(key: string, by?: number, ttlSeconds?: number): Promise<number> | number;
73
66
  /** Cache statistics for observability. Optional. */
74
67
  stats?(): CacheStats;
75
68
  }
@@ -1,16 +1,18 @@
1
1
  import { p as isArcError, t as ArcError } from "./errors-DkO8Nk6-.mjs";
2
- import { b as isElevated, c as getOrgId, g as getUserId, x as isMember } from "./types-bHblZ9YN.mjs";
3
- import { t as BaseController } from "./BaseController-BQ8KNvWC.mjs";
4
- import { i as applyFieldReadPermissions, r as normalizePermissionResult, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
2
+ import { n as requireSingleHeaderValue } from "./headers-BvlQSDMB.mjs";
3
+ import { b as isElevated, c as getOrgId, g as getUserId, x as isMember } from "./types-CSyCxl2V.mjs";
4
+ import { t as BaseController } from "./BaseController-B65zbz9m.mjs";
5
+ import { i as applyFieldReadPermissions, r as normalizePermissionResult, s as resolveEffectiveRoles } from "./applyPermissionResult-BnyVE9eD.mjs";
5
6
  import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
6
- import { d as buildRequestScopeProjection, i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-lszoJle1.mjs";
7
- import { n as validateAggregations } from "./validate-C2mGgSXg.mjs";
8
- import { n as executeAggregation } from "./buildHandler-D_2ULWxZ.mjs";
7
+ import { r as resolvePipelineSteps, u as buildRequestScopeProjection } from "./pipeline-BGRnV-To.mjs";
8
+ import { n as validateAggregations } from "./validate-Bk44F2yT.mjs";
9
+ import { n as executeAggregation } from "./buildHandler-Dtwd8fdz.mjs";
9
10
  import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
11
+ import { t as resolveActionPermission } from "./actionPermissions-CAphcmUG.mjs";
10
12
  import { t as createIdempotencyResult } from "./interface-n5hpKg1d.mjs";
11
13
  import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-Ca7ySNPx.mjs";
12
- import { createHash, randomUUID } from "node:crypto";
13
14
  import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
15
+ import { createHash, randomUUID } from "node:crypto";
14
16
  import fp from "fastify-plugin";
15
17
  import { z } from "zod";
16
18
  //#region src/integrations/mcp/createMcpServer.ts
@@ -2382,7 +2384,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
2382
2384
  }
2383
2385
  });
2384
2386
  }
2385
- const sessionId = request.headers["mcp-session-id"];
2387
+ const sessionId = requireSingleHeaderValue(request.headers, "mcp-session-id");
2386
2388
  if (sessionId) {
2387
2389
  const entry = cache.get(sessionId);
2388
2390
  if (entry) {
@@ -2417,7 +2419,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
2417
2419
  await transport.handleRequest(request.raw, reply.raw, request.body);
2418
2420
  });
2419
2421
  fastify.get(prefix, async (request, reply) => {
2420
- const sessionId = request.headers["mcp-session-id"];
2422
+ const sessionId = requireSingleHeaderValue(request.headers, "mcp-session-id");
2421
2423
  if (!sessionId) return reply.code(400).send({ error: "Missing Mcp-Session-Id header" });
2422
2424
  const entry = cache.get(sessionId);
2423
2425
  if (!entry) return reply.code(403).send({ error: "Unauthorized" });
@@ -2432,7 +2434,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
2432
2434
  await entry.transport.handleRequest(request.raw, reply.raw);
2433
2435
  });
2434
2436
  fastify.delete(prefix, async (request, reply) => {
2435
- const sessionId = request.headers["mcp-session-id"];
2437
+ const sessionId = requireSingleHeaderValue(request.headers, "mcp-session-id");
2436
2438
  if (!sessionId) return reply.code(400).send({ error: "Missing Mcp-Session-Id header" });
2437
2439
  const entry = cache.get(sessionId);
2438
2440
  if (!entry) return reply.code(204).send();
@@ -76,6 +76,35 @@ var MemoryCacheStore = class {
76
76
  const entry = this.cache.get(key);
77
77
  if (entry) this.removeEntry(key, entry);
78
78
  }
79
+ /**
80
+ * Atomic increment (canonical `CacheAdapter` signature: key, by, ttl) —
81
+ * synchronous on purpose: no `await` between read and write means
82
+ * concurrent bumps on the event loop can't interleave, which is the whole
83
+ * point of the `CacheStore.increment` contract.
84
+ *
85
+ * TTL semantics follow the repo-core contract: `ttlSeconds` applies only
86
+ * when the key is CREATED — an existing counter keeps its original expiry
87
+ * (Redis `EXPIRE ... NX` semantics), so every conforming adapter ages a
88
+ * counter identically.
89
+ */
90
+ increment(key, by = 1, ttlSeconds) {
91
+ const now = Date.now();
92
+ const entry = this.cache.get(key);
93
+ const live = entry && entry.expiresAt > now;
94
+ const next = (live && typeof entry.value === "number" ? entry.value : 0) + by;
95
+ const effectiveTtlSeconds = ttlSeconds ?? this.defaultTtlSeconds;
96
+ if (entry) this.currentBytes -= entry.size;
97
+ this.cache.delete(key);
98
+ const size = 8;
99
+ this.cache.set(key, {
100
+ value: next,
101
+ size,
102
+ expiresAt: live ? entry.expiresAt : now + Math.max(1, effectiveTtlSeconds) * 1e3
103
+ });
104
+ this.currentBytes += size;
105
+ this.evictToLimit();
106
+ return next;
107
+ }
79
108
  async clear(pattern) {
80
109
  if (pattern === void 0) {
81
110
  this.cache.clear();
@@ -1,4 +1,4 @@
1
- import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-BBsvQt7U.mjs";
1
+ import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-DPRH0Cme.mjs";
2
2
  import { RouteHandlerMethod } from "fastify";
3
3
  //#region src/middleware/middleware.d.ts
4
4
  interface NamedMiddleware {
@@ -90,6 +90,14 @@ interface MultipartBodyOptions {
90
90
  * @example ['avatar', 'cover'] // multi-field upload (profile editor)
91
91
  */
92
92
  requiredFields?: string[];
93
+ /**
94
+ * Cap on the TOTAL buffered file bytes per request (default: 25 MiB).
95
+ * Per-file and per-count limits bound individual files but not their
96
+ * SUM — without this, `maxFiles: 5` × `maxFileSize: 10 MiB` lets one
97
+ * request retain ~50 MiB of process memory, and concurrency multiplies
98
+ * it (20 such uploads ≈ 1 GiB). Exceeding the cap returns 413.
99
+ */
100
+ maxTotalBytes?: number;
93
101
  }
94
102
  /**
95
103
  * Create a multipart body parsing middleware.
@@ -102,6 +110,14 @@ interface MultipartBodyOptions {
102
110
  * For non-multipart requests (regular JSON), this is a no-op — the request
103
111
  * passes through unchanged. This makes it safe to add to create/update
104
112
  * middlewares without breaking JSON clients.
113
+ *
114
+ * **BUFFERED — for small uploads only.** Every accepted file is held fully
115
+ * in process memory for the request's lifetime; memory cost is
116
+ * `concurrency × maxTotalBytes`. Suitable for avatars, documents, and
117
+ * form attachments. For large files use PRESIGNED direct-to-object-storage
118
+ * uploads (see the files-upload preset / production guide) and pair upload
119
+ * routes with strict rate limits — buffering video through the app tier is
120
+ * the wrong architecture regardless of limits.
105
121
  */
106
122
  declare function multipartBody(options?: MultipartBodyOptions): RouteHandlerMethod;
107
123
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { t as CRUD_OPERATIONS } from "../constants-DNre0wRg.mjs";
2
- import { t as multipartBody } from "../multipartBody-CHEQSZ6G.mjs";
2
+ import { t as multipartBody } from "../multipartBody-D0eBvkeq.mjs";
3
3
  //#region src/middleware/middleware.ts
4
4
  /**
5
5
  * Named Middleware — Priority-based, conditional middleware execution.
@@ -1,41 +1,5 @@
1
+ import { LockAdapter } from "@classytic/repo-core/lock";
1
2
  //#region src/migrations/index.d.ts
2
- /**
3
- * Schema Versioning and Migrations System
4
- *
5
- * Manages database schema changes over time with version tracking.
6
- * Supports forward migrations, rollbacks, and schema compatibility layers.
7
- *
8
- * DB-agnostic: the `db` parameter is typed as `unknown` — the user passes
9
- * whatever connection object their adapter uses (Mongoose db, Prisma client,
10
- * Knex instance, etc.) and their `up`/`down` functions cast it internally.
11
- *
12
- * @example
13
- * import { defineMigration, MigrationRunner } from '@classytic/arc/migrations';
14
- *
15
- * const productV2 = defineMigration({
16
- * version: 2,
17
- * resource: 'product',
18
- * up: async (db) => {
19
- * const mongo = db as import('mongoose').mongo.Db;
20
- * await mongo.collection('products').updateMany(
21
- * {},
22
- * { $rename: { 'oldField': 'newField' } }
23
- * );
24
- * },
25
- * down: async (db) => {
26
- * const mongo = db as import('mongoose').mongo.Db;
27
- * await mongo.collection('products').updateMany(
28
- * {},
29
- * { $rename: { 'newField': 'oldField' } }
30
- * );
31
- * },
32
- * });
33
- *
34
- * const runner = new MigrationRunner(mongoose.connection.db, {
35
- * store: new MongoMigrationStore(mongoose.connection.db),
36
- * });
37
- * await runner.up(migrations);
38
- */
39
3
  interface Migration {
40
4
  /** Migration version (sequential number) */
41
5
  version: number;
@@ -63,6 +27,12 @@ interface MigrationRecord {
63
27
  description?: string;
64
28
  appliedAt: Date;
65
29
  executionTime: number;
30
+ /**
31
+ * SHA-256 of the migration's `up` + `down` source at apply time.
32
+ * Present on records written by 2.24+; older records lack it and are
33
+ * skipped by drift detection.
34
+ */
35
+ checksum?: string;
66
36
  }
67
37
  /**
68
38
  * DB-agnostic migration store interface.
@@ -75,8 +45,14 @@ interface MigrationRecord {
75
45
  interface MigrationStore {
76
46
  /** Get all applied migration records, sorted by appliedAt ascending */
77
47
  getApplied(): Promise<MigrationRecord[]>;
78
- /** Record a completed migration */
79
- record(migration: Migration, executionTime: number): Promise<void>;
48
+ /**
49
+ * Record a completed migration. `meta.checksum` (2.24+) is the source
50
+ * checksum for drift detection — stores should persist it when given;
51
+ * existing two-argument implementations remain valid and simply skip it.
52
+ */
53
+ record(migration: Migration, executionTime: number, meta?: {
54
+ checksum?: string;
55
+ }): Promise<void>;
80
56
  /** Remove a migration record (for rollback) */
81
57
  remove(migration: Migration): Promise<void>;
82
58
  }
@@ -123,7 +99,9 @@ declare class MongoMigrationStore implements MigrationStore {
123
99
  collectionName?: string;
124
100
  });
125
101
  getApplied(): Promise<MigrationRecord[]>;
126
- record(migration: Migration, executionTime: number): Promise<void>;
102
+ record(migration: Migration, executionTime: number, meta?: {
103
+ checksum?: string;
104
+ }): Promise<void>;
127
105
  remove(migration: Migration): Promise<void>;
128
106
  }
129
107
  interface MigrationRunnerOptions {
@@ -131,7 +109,41 @@ interface MigrationRunnerOptions {
131
109
  store: MigrationStore;
132
110
  /** Logger (defaults to process.stdout/stderr) */
133
111
  logger?: MigrationLogger;
112
+ /**
113
+ * Ecosystem lock adapter (`LockAdapter` from `@classytic/repo-core/lock`)
114
+ * for multi-replica / concurrent-deploy safety.
115
+ * When set, `up()` / `down()` / `downTo()` acquire the `arc:migrations`
116
+ * lease first and FAIL FAST if another runner holds it — two replicas
117
+ * starting together can no longer both execute the same migration.
118
+ * Omit only when a single owner (deploy job, CI step) runs migrations.
119
+ */
120
+ lock?: LockAdapter;
121
+ /**
122
+ * Lease duration for the migration lock (default: 10 minutes). Must
123
+ * comfortably exceed the slowest migration so the lease cannot lapse
124
+ * mid-run and admit a second runner.
125
+ */
126
+ lockLeaseMs?: number;
127
+ /** Lease holder identity (default: random per runner instance). */
128
+ holderId?: string;
129
+ /**
130
+ * What to do when an APPLIED migration's current source no longer matches
131
+ * the checksum recorded at apply time (the file was edited after being
132
+ * applied — drift that silently invalidates rollback correctness).
133
+ * - `'warn'` (default) — log and continue. Checksums come from
134
+ * `Function.prototype.toString()`, so a transpile-target change can
135
+ * shift them without semantic drift; warn keeps that honest.
136
+ * - `'error'` — throw before running anything.
137
+ * Records without a checksum (written by older stores) are skipped.
138
+ */
139
+ checksumMismatch?: "warn" | "error";
134
140
  }
141
+ /**
142
+ * Source checksum for drift detection — SHA-256 over the migration's
143
+ * `up`/`down` source. Stable for a given build; a transpile-target change
144
+ * can legitimately shift it (hence `checksumMismatch: 'warn'` default).
145
+ */
146
+ declare function computeMigrationChecksum(migration: Migration): string;
135
147
  /**
136
148
  * Define a migration
137
149
  */
@@ -162,9 +174,40 @@ declare class MigrationRunner {
162
174
  private readonly db;
163
175
  private readonly store;
164
176
  private readonly log;
177
+ private readonly lock?;
178
+ private readonly lockLeaseMs;
179
+ private readonly holderId;
180
+ private readonly checksumMismatch;
165
181
  constructor(db: unknown, opts: MigrationRunnerOptions);
166
182
  /**
167
- * Run all pending migrations
183
+ * Acquire the migration lease (when a lock is configured) and run `fn`.
184
+ * Fail-fast on contention: a concurrent deploy should error loudly, not
185
+ * queue — the OTHER runner is applying the same set.
186
+ *
187
+ * The lease renews while `fn` runs (`startRenewingLease` — serialized,
188
+ * awaited teardown), so a migration slower than `lockLeaseMs` can't let
189
+ * a second deployment acquire the lock mid-run. Ownership loss is
190
+ * ENFORCED, not just logged: `fn` receives a `leaseLost` probe to refuse
191
+ * starting further migrations, and even a successful `fn` fails the run
192
+ * when the lease was lost — a concurrent runner may have interleaved, so
193
+ * the applied state must be verified, not trusted. (A stale holder's
194
+ * in-flight write can only be truly fenced by a token from the lock
195
+ * contract — planned repo-core evolution.)
196
+ */
197
+ private withLock;
198
+ /**
199
+ * Drift detection: compare each APPLIED record's stored checksum against
200
+ * the current source of the same migration. A mismatch means the file was
201
+ * edited after being applied — its `down()` may no longer invert what
202
+ * actually ran.
203
+ */
204
+ private verifyChecksums;
205
+ /**
206
+ * Run all pending migrations.
207
+ *
208
+ * With a `lock` configured, the applied set is (re)read INSIDE the lease —
209
+ * a runner that raced another deploy computes pending against the winner's
210
+ * completed writes, not a stale pre-lock snapshot.
168
211
  */
169
212
  up(migrations: Migration[]): Promise<void>;
170
213
  /**
@@ -172,9 +215,16 @@ declare class MigrationRunner {
172
215
  */
173
216
  down(migrations: Migration[]): Promise<void>;
174
217
  /**
175
- * Rollback to specific version
218
+ * Rollback to a specific version.
219
+ *
220
+ * Versions are RESOURCE-scoped, so a bare numeric threshold is only
221
+ * meaningful within one resource. When the applied set spans multiple
222
+ * resources, `options.resource` is required — rolling every resource back
223
+ * across a shared number silently reverts unrelated schemas.
176
224
  */
177
- downTo(migrations: Migration[], targetVersion: number): Promise<void>;
225
+ downTo(migrations: Migration[], targetVersion: number, options?: {
226
+ resource?: string;
227
+ }): Promise<void>;
178
228
  /**
179
229
  * Get all applied migrations
180
230
  */
@@ -242,4 +292,4 @@ declare class MigrationRegistry {
242
292
  clear(): void;
243
293
  }
244
294
  //#endregion
245
- export { Migration, MigrationLogger, MigrationRecord, MigrationRegistry, MigrationRunner, MigrationRunnerOptions, MigrationStore, MongoMigrationStore, SchemaVersion, defineMigration, withSchemaVersion };
295
+ export { Migration, MigrationLogger, MigrationRecord, MigrationRegistry, MigrationRunner, MigrationRunnerOptions, MigrationStore, MongoMigrationStore, SchemaVersion, computeMigrationChecksum, defineMigration, withSchemaVersion };