@classytic/arc 2.18.2 → 2.18.3

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 (153) hide show
  1. package/dist/{BaseController-DPwe89qs.mjs → BaseController-B-uXzf6I.mjs} +403 -216
  2. package/dist/{QueryCache-8YmAQ-zW.d.mts → QueryCache-Dsyv2mLS.d.mts} +1 -1
  3. package/dist/audit/index.d.mts +1 -1
  4. package/dist/auth/index.d.mts +4 -4
  5. package/dist/auth/index.mjs +4 -4
  6. package/dist/auth/redis-session.d.mts +1 -1
  7. package/dist/{betterAuthOpenApi-ClWxaceA.mjs → betterAuthOpenApi-DAAhMZff.mjs} +1 -1
  8. package/dist/{buildHandler-C_SkBNF3.mjs → buildHandler-Cvrc2p7r.mjs} +1 -1
  9. package/dist/cache/index.d.mts +3 -3
  10. package/dist/cache/index.mjs +3 -3
  11. package/dist/cli/commands/describe.d.mts +1 -1
  12. package/dist/cli/commands/describe.mjs +1 -1
  13. package/dist/cli/commands/docs.mjs +2 -2
  14. package/dist/cli/commands/generate.d.mts +1 -1
  15. package/dist/cli/commands/generate.mjs +1 -1
  16. package/dist/cli/commands/init.d.mts +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.d.mts +2 -2
  20. package/dist/cli/index.mjs +2 -2
  21. package/dist/core/index.d.mts +2 -2
  22. package/dist/core/index.mjs +3 -3
  23. package/dist/{core-B41eZxyM.mjs → core-C88uIwd3.mjs} +161 -103
  24. package/dist/{createActionRouter-CRaIwB5S.mjs → createActionRouter-DGCCp-kP.mjs} +4 -2
  25. package/dist/{createAggregationRouter-n3O6WdLO.mjs → createAggregationRouter-Ca77b8iK.mjs} +3 -3
  26. package/dist/{createApp-DZvrb587.mjs → createApp-DLuUGVZQ.mjs} +11 -11
  27. package/dist/{defineEvent-NPKUJRt8.mjs → defineEvent-DC981g-o.mjs} +1 -1
  28. package/dist/docs/index.d.mts +2 -2
  29. package/dist/docs/index.mjs +2 -2
  30. package/dist/{elevation-6mfTLBC-.mjs → elevation-BzhyVWfn.mjs} +1 -1
  31. package/dist/{eventPlugin-Dxp9JlKK.mjs → eventPlugin-Bw4bUDEO.mjs} +2 -2
  32. package/dist/{eventPlugin-DjEx_J3j.d.mts → eventPlugin-C1W1CRbf.d.mts} +1 -1
  33. package/dist/events/index.d.mts +8 -5
  34. package/dist/events/index.mjs +26 -7
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis-stream-entry.mjs +52 -5
  37. package/dist/events/transports/redis.d.mts +1 -1
  38. package/dist/factory/index.d.mts +1 -1
  39. package/dist/factory/index.mjs +3 -3
  40. package/dist/{fastify-augmentation-CL48B6gW.d.mts → fastify-augmentation-BdCl_eNo.d.mts} +2 -2
  41. package/dist/{fields-DqZLp5d6.d.mts → fields-CKLrhyOb.d.mts} +1 -1
  42. package/dist/{generate-CcM24hoq.mjs → generate-BaxDdmJ8.mjs} +1 -1
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/hooks/index.mjs +1 -1
  45. package/dist/idempotency/index.d.mts +2 -2
  46. package/dist/idempotency/index.mjs +1 -1
  47. package/dist/idempotency/redis.d.mts +1 -1
  48. package/dist/{index-CNiryUMN.d.mts → index-Cvo1u9n6.d.mts} +3 -3
  49. package/dist/{index-C000faom.d.mts → index-QYma8mdC.d.mts} +215 -241
  50. package/dist/{index-3L-adFJO.d.mts → index-vcWUGkwf.d.mts} +3 -3
  51. package/dist/index.d.mts +5 -5
  52. package/dist/index.mjs +5 -5
  53. package/dist/{init-DQ_rY1J7.mjs → init-DSsdRfk9.mjs} +3 -3
  54. package/dist/integrations/event-gateway.d.mts +2 -2
  55. package/dist/integrations/event-gateway.mjs +1 -1
  56. package/dist/integrations/index.d.mts +2 -2
  57. package/dist/integrations/jobs.mjs +66 -1
  58. package/dist/integrations/mcp/index.d.mts +4 -4
  59. package/dist/integrations/mcp/index.mjs +1 -1
  60. package/dist/integrations/mcp/testing.d.mts +1 -1
  61. package/dist/integrations/mcp/testing.mjs +1 -1
  62. package/dist/integrations/streamline.d.mts +16 -1
  63. package/dist/integrations/streamline.mjs +33 -2
  64. package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
  65. package/dist/integrations/websocket-redis.d.mts +1 -1
  66. package/dist/integrations/websocket.d.mts +2 -2
  67. package/dist/{loadResourcesFromEntry-DCjYMgfC.mjs → loadResourcesFromEntry-XHNeUWpu.mjs} +1 -1
  68. package/dist/{mcpPlugin-BrepZ0cV.mjs → mcpPlugin-CzKdt3B8.mjs} +9 -9
  69. package/dist/middleware/index.d.mts +1 -1
  70. package/dist/middleware/index.mjs +1 -1
  71. package/dist/{openapi-CLZx9bwS.mjs → openapi-ButnRyOM.mjs} +5 -5
  72. package/dist/permissions/index.d.mts +2 -2
  73. package/dist/permissions/index.mjs +2 -2
  74. package/dist/{permissions-B1Qy55fg.mjs → permissions-3uzwkbQt.mjs} +99 -99
  75. package/dist/pipeline/index.d.mts +1 -1
  76. package/dist/pipeline/index.mjs +1 -1
  77. package/dist/pipeline-D4sqiokR.mjs +336 -0
  78. package/dist/plugins/index.d.mts +5 -5
  79. package/dist/plugins/index.mjs +9 -9
  80. package/dist/plugins/response-cache.mjs +1 -1
  81. package/dist/plugins/tracing-entry.d.mts +1 -1
  82. package/dist/plugins/tracing-entry.mjs +1 -1
  83. package/dist/presets/filesUpload.d.mts +4 -4
  84. package/dist/presets/filesUpload.mjs +2 -2
  85. package/dist/presets/index.d.mts +1 -1
  86. package/dist/presets/index.mjs +1 -1
  87. package/dist/presets/multiTenant.d.mts +1 -1
  88. package/dist/presets/search.d.mts +2 -2
  89. package/dist/presets/search.mjs +1 -1
  90. package/dist/{presets-BYbPobbF.mjs → presets-CF79RHEo.mjs} +1 -1
  91. package/dist/{queryCachePlugin-Do4P_WBC.d.mts → queryCachePlugin-Bd2tYgh5.d.mts} +2 -2
  92. package/dist/{queryCachePlugin-B4XMSSe7.mjs → queryCachePlugin-CnAeFqju.mjs} +3 -3
  93. package/dist/rateLimit-yKvxWAky.mjs +207 -0
  94. package/dist/{redis-cpqVb85w.d.mts → redis-3hqH_Qk_.d.mts} +1 -1
  95. package/dist/{redis-stream-BuiAS3aE.d.mts → redis-stream-BtEUtjxZ.d.mts} +5 -4
  96. package/dist/registerRoute-BEYn0ylk.mjs +108 -0
  97. package/dist/registry/index.d.mts +1 -1
  98. package/dist/registry/index.mjs +2 -2
  99. package/dist/{schemaIR-Bk6Tns82.mjs → schemaIR-CE6USIRi.mjs} +1 -1
  100. package/dist/scope/index.d.mts +2 -2
  101. package/dist/scope/index.mjs +2 -2
  102. package/dist/testing/index.d.mts +2 -2
  103. package/dist/testing/index.mjs +12 -2
  104. package/dist/testing/storageContract.d.mts +1 -1
  105. package/dist/types/index.d.mts +4 -4
  106. package/dist/types/storage.d.mts +1 -1
  107. package/dist/{types-DH-7WO64.d.mts → types-BWBcb88c.d.mts} +1 -1
  108. package/dist/{types-DEEWwQFR.d.mts → types-WVlAVp8T.d.mts} +10 -10
  109. package/dist/utils/index.d.mts +3 -3
  110. package/dist/utils/index.mjs +3 -3
  111. package/dist/{utils-BMQK8mRl.mjs → utils-xAQeDyU_.mjs} +1 -1
  112. package/dist/{validate-DFIP2VrM.mjs → validate-0__MuUsK.mjs} +201 -190
  113. package/dist/{versioning-5H4930tj.d.mts → versioning-BSEDGXKC.d.mts} +1 -1
  114. package/dist/{websocket-BZMUP7Gr.d.mts → websocket-BFBT5rH-.d.mts} +1 -1
  115. package/package.json +5 -5
  116. package/skills/arc/references/api-reference.md +1 -1
  117. package/dist/routerShared-Dl0A77Ba.mjs +0 -724
  118. /package/dist/{EventTransport-ClEDfiMS.mjs → EventTransport-7LPXLEUX.mjs} +0 -0
  119. /package/dist/{EventTransport-B1GFiYUy.d.mts → EventTransport-CaaXM5Y1.d.mts} +0 -0
  120. /package/dist/{HookSystem-Cmf7-Etp.mjs → HookSystem-BfCpWbwM.mjs} +0 -0
  121. /package/dist/{ResourceRegistry-CzSzVK1m.mjs → ResourceRegistry-CWmOggxS.mjs} +0 -0
  122. /package/dist/{actionPermissions-Bjmvn7Eb.mjs → actionPermissions-CX9QIMTd.mjs} +0 -0
  123. /package/dist/{caching-8MfrrUYU.mjs → caching-C0DdKiYB.mjs} +0 -0
  124. /package/dist/{elevation-DvwtjDwu.d.mts → elevation-DW90z5bT.d.mts} +0 -0
  125. /package/dist/{errorHandler-wQejMBT3.mjs → errorHandler-Bua97SEt.mjs} +0 -0
  126. /package/dist/{errorHandler-DuqTqeqn.d.mts → errorHandler-_Lp2k9M3.d.mts} +0 -0
  127. /package/dist/{errors-4ZloYFvm.d.mts → errors-DZOemeGa.d.mts} +0 -0
  128. /package/dist/{externalPaths-ClykOQqa.d.mts → externalPaths-DLle0R07.d.mts} +0 -0
  129. /package/dist/{generate-cRnsafMS.d.mts → generate-Dmk8cfvX.d.mts} +0 -0
  130. /package/dist/{init-D-vrTZ1G.d.mts → init-BS4Daqnv.d.mts} +0 -0
  131. /package/dist/{interface-DoE6UoC2.d.mts → interface-CsrGR8LF.d.mts} +0 -0
  132. /package/dist/{interface-BVkZsvEe.d.mts → interface-DL7LlWS5.d.mts} +0 -0
  133. /package/dist/{keys-DopsCuyQ.mjs → keys-B3yyBEyH.mjs} +0 -0
  134. /package/dist/{loadResources-fol6JuvW.mjs → loadResources-CwXt9MCh.mjs} +0 -0
  135. /package/dist/{memory-QOLe11D5.mjs → memory-TdTHCS9-.mjs} +0 -0
  136. /package/dist/{metrics-B-PU4-Yu.mjs → metrics-Da2EUPi9.mjs} +0 -0
  137. /package/dist/{multipartBody-VjVWmZl8.mjs → multipartBody-709YcXi1.mjs} +0 -0
  138. /package/dist/{pipe-DiCyvyPN.mjs → pipe-9Tl-OAus.mjs} +0 -0
  139. /package/dist/{pluralize-B9M8xvy-.mjs → pluralize-BxdUl2P4.mjs} +0 -0
  140. /package/dist/{preloadResources-BOXzPhKU.mjs → preloadResources-B-j-iqo-.mjs} +0 -0
  141. /package/dist/{pushref-registry-Bq_-EVsC.d.mts → pushref-registry-Dqclzjy7.d.mts} +0 -0
  142. /package/dist/{registry-BrlHkrRS.mjs → registry-Br15xdGM.mjs} +0 -0
  143. /package/dist/{replyHelpers-boS5Rx26.mjs → replyHelpers-DXHVxrrj.mjs} +0 -0
  144. /package/dist/{schemaConverter-De34B1ZG.mjs → schemaConverter-DatAaVNj.mjs} +0 -0
  145. /package/dist/{sessionManager-DlmaHUAl.d.mts → sessionManager-CyHpYXoB.d.mts} +0 -0
  146. /package/dist/{sse-jh5OqFjR.mjs → sse-Xq8UtuQx.mjs} +0 -0
  147. /package/dist/{storage-BFmamN5m.d.mts → storage-C5q8fuEt.d.mts} +0 -0
  148. /package/dist/{store-helpers-5JgaxieH.mjs → store-helpers-DjSYI2uP.mjs} +0 -0
  149. /package/dist/{tracing-BYQ3hsbM.d.mts → tracing-Bnb1-YVd.d.mts} +0 -0
  150. /package/dist/{typeGuards-BzkXkvVv.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  151. /package/dist/{types-CfapW2t8.d.mts → types-BJwJN2Pg.d.mts} +0 -0
  152. /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
  153. /package/dist/{versioning-CuIwUQZ2.mjs → versioning-C0K8KQLx.mjs} +0 -0
@@ -1,4 +1,192 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
+ //#region src/core/aggregation/normalize.ts
3
+ /**
4
+ * Framework cap on URL-driven aggregation limits when the host opts
5
+ * in to `defaultLimit` without explicit `maxLimit`. Set to the same
6
+ * order of magnitude as a "large dashboard page" — bigger than every
7
+ * realistic grouped tile, smaller than "we returned the whole table."
8
+ */
9
+ const AGG_FRAMEWORK_MAX_LIMIT = 1e3;
10
+ function parseMeasureShorthand(s) {
11
+ if (s === "count") return { op: "count" };
12
+ const colon = s.indexOf(":");
13
+ if (colon < 0) return null;
14
+ const op = s.slice(0, colon);
15
+ const rest = s.slice(colon + 1);
16
+ if (!rest) return null;
17
+ if (op === "percentile") {
18
+ const lastColon = rest.lastIndexOf(":");
19
+ if (lastColon < 0) return null;
20
+ const field = rest.slice(0, lastColon);
21
+ const pStr = rest.slice(lastColon + 1);
22
+ if (!field || !pStr) return null;
23
+ const p = Number(pStr);
24
+ if (!Number.isFinite(p)) return null;
25
+ return {
26
+ op: "percentile",
27
+ field,
28
+ p
29
+ };
30
+ }
31
+ switch (op) {
32
+ case "count": return {
33
+ op: "count",
34
+ field: rest
35
+ };
36
+ case "countDistinct": return {
37
+ op: "countDistinct",
38
+ field: rest
39
+ };
40
+ case "sum": return {
41
+ op: "sum",
42
+ field: rest
43
+ };
44
+ case "avg": return {
45
+ op: "avg",
46
+ field: rest
47
+ };
48
+ case "min": return {
49
+ op: "min",
50
+ field: rest
51
+ };
52
+ case "max": return {
53
+ op: "max",
54
+ field: rest
55
+ };
56
+ default: return null;
57
+ }
58
+ }
59
+ function normalizeGroupBy(groupBy) {
60
+ if (!groupBy) return [];
61
+ if (typeof groupBy === "string") return [groupBy];
62
+ return [...groupBy];
63
+ }
64
+ function collectLookupAliases(lookups) {
65
+ const aliases = /* @__PURE__ */ new Set();
66
+ if (!lookups) return aliases;
67
+ for (const lookup of lookups) aliases.add(lookup.as ?? lookup.from);
68
+ return aliases;
69
+ }
70
+ /**
71
+ * Collect fields that aggregation MUST NOT reference.
72
+ *
73
+ * Default rule: only `hidden: true` blocks. `hidden` means the field is
74
+ * omitted from list/get responses, so exposing it via aggregation would
75
+ * leak data the client can't otherwise see. `systemManaged` is a write
76
+ * rule (server stamps the value, clients can't PATCH it) — those fields
77
+ * are still visible per-row, so aggregating them leaks nothing.
78
+ *
79
+ * Two opt-in overrides via `ArcFieldRule.aggregable`:
80
+ * - `aggregable: false` — explicit deny on a visible field (rare; use
81
+ * when the per-row value is fine but the across-row distribution is
82
+ * itself sensitive).
83
+ * - `aggregable: true` — explicit allow, even on a `hidden` field
84
+ * (escape hatch — caller asserts cardinality leak isn't a concern).
85
+ */
86
+ function collectBlockedFields(schemaOptions) {
87
+ const blocked = /* @__PURE__ */ new Set();
88
+ const fieldRules = schemaOptions?.fieldRules;
89
+ if (!fieldRules) return blocked;
90
+ for (const [field, rules] of Object.entries(fieldRules)) {
91
+ if (!rules) continue;
92
+ if (rules.aggregable === true) continue;
93
+ if (rules.aggregable === false) {
94
+ blocked.add(field);
95
+ continue;
96
+ }
97
+ if (rules.hidden) blocked.add(field);
98
+ }
99
+ return blocked;
100
+ }
101
+ /**
102
+ * Resolve the URL-driven limit policy into a concrete `{ default, max }`
103
+ * pair. Returns `undefined` when the host uses the static `limit` form
104
+ * (or no limit at all) — the request handler reads the absence to know
105
+ * "do not parse `?limit=N` from the URL."
106
+ */
107
+ function resolveLimitPolicy(config) {
108
+ if (config.defaultLimit === void 0) return void 0;
109
+ return {
110
+ defaultLimit: config.defaultLimit,
111
+ maxLimit: config.maxLimit ?? AGG_FRAMEWORK_MAX_LIMIT
112
+ };
113
+ }
114
+ /**
115
+ * Resolve the effective per-request `limit` — three cases:
116
+ *
117
+ * 1. Host declared static `limit` → URL `?limit` ignored, static wins.
118
+ * 2. Host declared `defaultLimit` → parse `?limit=N`, cap at
119
+ * `maxLimit`, fall back to default on absent / invalid input.
120
+ * 3. Host declared neither → returns `undefined` (no limit applied,
121
+ * preserves pre-2.16.1 behavior).
122
+ */
123
+ function resolveAggRequestLimit(normalized, query) {
124
+ const policy = normalized.compiled.limitPolicy;
125
+ if (!policy) return normalized.compiled.limit;
126
+ const raw = query["limit"];
127
+ if (raw === void 0 || raw === "") return policy.defaultLimit;
128
+ const parsed = typeof raw === "number" ? raw : Number.parseInt(String(raw), 10);
129
+ if (!Number.isFinite(parsed) || parsed <= 0) return policy.defaultLimit;
130
+ return Math.min(Math.floor(parsed), policy.maxLimit);
131
+ }
132
+ /** Compile to canonical `AggRequest` for `repo.aggregate()` at request time. */
133
+ function compileAggRequest(normalized, callerFilter, tenantOptions, query) {
134
+ const filter = {
135
+ ...normalized.compiled.filter ?? {},
136
+ ...callerFilter
137
+ };
138
+ const executionHints = buildExecutionHints(normalized.base);
139
+ const cache = buildCacheOptions(normalized.base);
140
+ const limit = query ? resolveAggRequestLimit(normalized, query) : normalized.compiled.limit;
141
+ return {
142
+ measures: normalized.compiled.measures,
143
+ ...Object.keys(filter).length > 0 ? { filter } : {},
144
+ ...normalized.compiled.lookups ? { lookups: normalized.compiled.lookups } : {},
145
+ ...normalized.compiled.groupBy ? { groupBy: normalized.compiled.groupBy } : {},
146
+ ...normalized.compiled.dateBuckets ? { dateBuckets: normalized.compiled.dateBuckets } : {},
147
+ ...normalized.compiled.having ? { having: normalized.compiled.having } : {},
148
+ ...normalized.compiled.sort ? { sort: normalized.compiled.sort } : {},
149
+ ...limit !== void 0 ? { limit } : {},
150
+ ...normalized.compiled.topN ? { topN: normalized.compiled.topN } : {},
151
+ ...executionHints ? { executionHints } : {},
152
+ ...cache ? { cache } : {}
153
+ };
154
+ }
155
+ /**
156
+ * Translate the host's declarative `cache:` config into the TanStack-
157
+ * shaped `CacheOptions` repo-core's unified cache plugin reads from
158
+ * `req.cache`. The plugin handles SWR semantics, version-bump
159
+ * invalidation, and tag side-index — arc just declares the policy.
160
+ *
161
+ * No translation needed when the host disabled cache (returns
162
+ * undefined, kit falls through to a non-cached call).
163
+ */
164
+ function buildCacheOptions(config) {
165
+ const c = config.cache;
166
+ if (!c) return void 0;
167
+ return {
168
+ ...c.staleTime !== void 0 ? { staleTime: c.staleTime } : {},
169
+ ...c.gcTime !== void 0 ? { gcTime: c.gcTime } : {},
170
+ ...c.tags ? { tags: c.tags } : {},
171
+ swr: c.swr ?? true
172
+ };
173
+ }
174
+ /**
175
+ * Map arc's declarative knobs onto repo-core's portable `AggExecutionHints`.
176
+ * Kits that don't honor a given hint silently ignore it (per IR contract);
177
+ * mongokit threads `maxTimeMs` → `maxTimeMS` and `indexHint` → `aggregate.option({ hint })`.
178
+ */
179
+ function buildExecutionHints(config) {
180
+ const hints = {};
181
+ if (typeof config.timeout === "number" && config.timeout > 0) hints.maxTimeMs = config.timeout;
182
+ if (config.indexHint && config.indexHint.leadingKeys.length > 0) {
183
+ const hintObj = {};
184
+ for (const key of config.indexHint.leadingKeys) hintObj[key] = 1;
185
+ hints.indexHint = hintObj;
186
+ }
187
+ return Object.keys(hints).length > 0 ? hints : void 0;
188
+ }
189
+ //#endregion
2
190
  //#region src/core/aggregation/validate.ts
3
191
  var validate_exports = /* @__PURE__ */ __exportAll({
4
192
  ArcAggregationConfigError: () => ArcAggregationConfigError,
@@ -11,13 +199,6 @@ var ArcAggregationConfigError = class extends Error {
11
199
  name = "ArcAggregationConfigError";
12
200
  };
13
201
  /**
14
- * Framework cap on URL-driven aggregation limits when the host opts
15
- * in to `defaultLimit` without explicit `maxLimit`. Set to the same
16
- * order of magnitude as a "large dashboard page" — bigger than every
17
- * realistic grouped tile, smaller than "we returned the whole table."
18
- */
19
- const AGG_FRAMEWORK_MAX_LIMIT = 1e3;
20
- /**
21
202
  * Validate + normalize all aggregations on a resource. Throws on first
22
203
  * misconfig with the offending aggregation name in the message — hosts
23
204
  * see exactly which entry needs fixing.
@@ -32,9 +213,9 @@ function validateAggregations(resourceName, aggregations, schemaOptions) {
32
213
  const out = [];
33
214
  const blockedFields = collectBlockedFields(schemaOptions);
34
215
  for (const [name, config] of Object.entries(aggregations)) {
35
- if (!isValidAggregationName(name)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation key "${name}" is invalid — keys map to URL segments and must be alphanumeric or underscore/hyphen.`);
36
- if (typeof config.permissions !== "function") throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${name}" is missing a "permissions" check. Aggregations must declare permissions explicitly — no default-allow. Use a permission helper from @classytic/arc/permissions.`);
37
- if (!config.measures || Object.keys(config.measures).length === 0) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${name}" has no measures. An empty "measures" map is a wiring bug — at least one measure is required.`);
216
+ validateAggregationName(resourceName, name);
217
+ validatePermissionsDeclared(resourceName, name, config);
218
+ validateMeasuresNonEmpty(resourceName, name, config);
38
219
  const lookupAliases = collectLookupAliases(config.lookups);
39
220
  const measures = compileMeasures(resourceName, name, config.measures);
40
221
  const groupBy = normalizeGroupBy(config.groupBy);
@@ -81,6 +262,15 @@ function validateAggregations(resourceName, aggregations, schemaOptions) {
81
262
  }
82
263
  return out;
83
264
  }
265
+ function validateAggregationName(resourceName, name) {
266
+ if (!isValidAggregationName(name)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation key "${name}" is invalid — keys map to URL segments and must be alphanumeric or underscore/hyphen.`);
267
+ }
268
+ function validatePermissionsDeclared(resourceName, name, config) {
269
+ if (typeof config.permissions !== "function") throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${name}" is missing a "permissions" check. Aggregations must declare permissions explicitly — no default-allow. Use a permission helper from @classytic/arc/permissions.`);
270
+ }
271
+ function validateMeasuresNonEmpty(resourceName, name, config) {
272
+ if (!config.measures || Object.keys(config.measures).length === 0) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${name}" has no measures. An empty "measures" map is a wiring bug — at least one measure is required.`);
273
+ }
84
274
  /**
85
275
  * Validate `limit` / `defaultLimit` / `maxLimit` triad. Throws on
86
276
  * misconfig at boot — boundary value, hosts see exactly what to fix.
@@ -102,19 +292,6 @@ function validateLimitConfig(resourceName, aggName, config) {
102
292
  }
103
293
  }
104
294
  /**
105
- * Resolve the URL-driven limit policy into a concrete `{ default, max }`
106
- * pair. Returns `undefined` when the host uses the static `limit` form
107
- * (or no limit at all) — the request handler reads the absence to know
108
- * "do not parse `?limit=N` from the URL."
109
- */
110
- function resolveLimitPolicy(config) {
111
- if (config.defaultLimit === void 0) return void 0;
112
- return {
113
- defaultLimit: config.defaultLimit,
114
- maxLimit: config.maxLimit ?? AGG_FRAMEWORK_MAX_LIMIT
115
- };
116
- }
117
- /**
118
295
  * Adapter feature-detect for `aggregate()`. Called at boot when the
119
296
  * repository instance is available. Returns `true` when the kit ships
120
297
  * `aggregate`; `false` when missing.
@@ -127,66 +304,6 @@ function adapterSupportsAggregate(repo) {
127
304
  return typeof repo.aggregate === "function";
128
305
  }
129
306
  /**
130
- * Resolve the effective per-request `limit` — three cases:
131
- *
132
- * 1. Host declared static `limit` → URL `?limit` ignored, static wins.
133
- * 2. Host declared `defaultLimit` → parse `?limit=N`, cap at
134
- * `maxLimit`, fall back to default on absent / invalid input.
135
- * 3. Host declared neither → returns `undefined` (no limit applied,
136
- * preserves pre-2.16.1 behavior).
137
- */
138
- function resolveAggRequestLimit(normalized, query) {
139
- const policy = normalized.compiled.limitPolicy;
140
- if (!policy) return normalized.compiled.limit;
141
- const raw = query["limit"];
142
- if (raw === void 0 || raw === "") return policy.defaultLimit;
143
- const parsed = typeof raw === "number" ? raw : Number.parseInt(String(raw), 10);
144
- if (!Number.isFinite(parsed) || parsed <= 0) return policy.defaultLimit;
145
- return Math.min(Math.floor(parsed), policy.maxLimit);
146
- }
147
- /** Compile to canonical `AggRequest` for `repo.aggregate()` at request time. */
148
- function compileAggRequest(normalized, callerFilter, tenantOptions, query) {
149
- const filter = {
150
- ...normalized.compiled.filter ?? {},
151
- ...callerFilter
152
- };
153
- const executionHints = buildExecutionHints(normalized.base);
154
- const cache = buildCacheOptions(normalized.base);
155
- const limit = query ? resolveAggRequestLimit(normalized, query) : normalized.compiled.limit;
156
- return {
157
- measures: normalized.compiled.measures,
158
- ...Object.keys(filter).length > 0 ? { filter } : {},
159
- ...normalized.compiled.lookups ? { lookups: normalized.compiled.lookups } : {},
160
- ...normalized.compiled.groupBy ? { groupBy: normalized.compiled.groupBy } : {},
161
- ...normalized.compiled.dateBuckets ? { dateBuckets: normalized.compiled.dateBuckets } : {},
162
- ...normalized.compiled.having ? { having: normalized.compiled.having } : {},
163
- ...normalized.compiled.sort ? { sort: normalized.compiled.sort } : {},
164
- ...limit !== void 0 ? { limit } : {},
165
- ...normalized.compiled.topN ? { topN: normalized.compiled.topN } : {},
166
- ...executionHints ? { executionHints } : {},
167
- ...cache ? { cache } : {}
168
- };
169
- }
170
- /**
171
- * Translate the host's declarative `cache:` config into the TanStack-
172
- * shaped `CacheOptions` repo-core's unified cache plugin reads from
173
- * `req.cache`. The plugin handles SWR semantics, version-bump
174
- * invalidation, and tag side-index — arc just declares the policy.
175
- *
176
- * No translation needed when the host disabled cache (returns
177
- * undefined, kit falls through to a non-cached call).
178
- */
179
- function buildCacheOptions(config) {
180
- const c = config.cache;
181
- if (!c) return void 0;
182
- return {
183
- ...c.staleTime !== void 0 ? { staleTime: c.staleTime } : {},
184
- ...c.gcTime !== void 0 ? { gcTime: c.gcTime } : {},
185
- ...c.tags ? { tags: c.tags } : {},
186
- swr: c.swr ?? true
187
- };
188
- }
189
- /**
190
307
  * Boot validation for `topN`. Mirrors the contract mongokit + sqlitekit
191
308
  * enforce at request time — both kits check the same three rules and
192
309
  * throw with kit-prefixed messages. Running them at boot gives hosts
@@ -267,21 +384,6 @@ function assertBucketFieldAllowed(input) {
267
384
  }
268
385
  if (blockedFields.has(field)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket "${alias}" references field "${field}", but the field is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Bucketing hidden fields would leak temporal info.`);
269
386
  }
270
- /**
271
- * Map arc's declarative knobs onto repo-core's portable `AggExecutionHints`.
272
- * Kits that don't honor a given hint silently ignore it (per IR contract);
273
- * mongokit threads `maxTimeMs` → `maxTimeMS` and `indexHint` → `aggregate.option({ hint })`.
274
- */
275
- function buildExecutionHints(config) {
276
- const hints = {};
277
- if (typeof config.timeout === "number" && config.timeout > 0) hints.maxTimeMs = config.timeout;
278
- if (config.indexHint && config.indexHint.leadingKeys.length > 0) {
279
- const hintObj = {};
280
- for (const key of config.indexHint.leadingKeys) hintObj[key] = 1;
281
- hints.indexHint = hintObj;
282
- }
283
- return Object.keys(hints).length > 0 ? hints : void 0;
284
- }
285
387
  const NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
286
388
  function isValidAggregationName(name) {
287
389
  return NAME_PATTERN.test(name);
@@ -302,55 +404,6 @@ function expandMeasure(input, resource, aggregation, alias) {
302
404
  validateMeasure(measure, resource, aggregation, alias);
303
405
  return measure;
304
406
  }
305
- function parseMeasureShorthand(s) {
306
- if (s === "count") return { op: "count" };
307
- const colon = s.indexOf(":");
308
- if (colon < 0) return null;
309
- const op = s.slice(0, colon);
310
- const rest = s.slice(colon + 1);
311
- if (!rest) return null;
312
- if (op === "percentile") {
313
- const lastColon = rest.lastIndexOf(":");
314
- if (lastColon < 0) return null;
315
- const field = rest.slice(0, lastColon);
316
- const pStr = rest.slice(lastColon + 1);
317
- if (!field || !pStr) return null;
318
- const p = Number(pStr);
319
- if (!Number.isFinite(p)) return null;
320
- return {
321
- op: "percentile",
322
- field,
323
- p
324
- };
325
- }
326
- switch (op) {
327
- case "count": return {
328
- op: "count",
329
- field: rest
330
- };
331
- case "countDistinct": return {
332
- op: "countDistinct",
333
- field: rest
334
- };
335
- case "sum": return {
336
- op: "sum",
337
- field: rest
338
- };
339
- case "avg": return {
340
- op: "avg",
341
- field: rest
342
- };
343
- case "min": return {
344
- op: "min",
345
- field: rest
346
- };
347
- case "max": return {
348
- op: "max",
349
- field: rest
350
- };
351
- default: return null;
352
- }
353
- }
354
407
  /**
355
408
  * Per-measure boot validation. Currently only `percentile` carries a
356
409
  * numeric constraint — `p ∈ [0, 1]` matches mongokit's request-time
@@ -363,48 +416,6 @@ function validateMeasure(measure, resource, aggregation, alias) {
363
416
  if (!Number.isFinite(measure.p) || measure.p < 0 || measure.p > 1) throw new ArcAggregationConfigError(`Resource "${resource}" aggregation "${aggregation}" measure "${alias}" has invalid percentile p=${String(measure.p)} — must be a finite number in [0, 1] (e.g. 0.5 for median, 0.95 for P95).`);
364
417
  }
365
418
  }
366
- function normalizeGroupBy(groupBy) {
367
- if (!groupBy) return [];
368
- if (typeof groupBy === "string") return [groupBy];
369
- return [...groupBy];
370
- }
371
- function collectLookupAliases(lookups) {
372
- const aliases = /* @__PURE__ */ new Set();
373
- if (!lookups) return aliases;
374
- for (const lookup of lookups) aliases.add(lookup.as ?? lookup.from);
375
- return aliases;
376
- }
377
- /**
378
- * Collect fields that aggregation MUST NOT reference.
379
- *
380
- * Default rule: only `hidden: true` blocks. `hidden` means the field is
381
- * omitted from list/get responses, so exposing it via aggregation would
382
- * leak data the client can't otherwise see. `systemManaged` is a write
383
- * rule (server stamps the value, clients can't PATCH it) — those fields
384
- * are still visible per-row, so aggregating them leaks nothing.
385
- *
386
- * Two opt-in overrides via `ArcFieldRule.aggregable`:
387
- * - `aggregable: false` — explicit deny on a visible field (rare; use
388
- * when the per-row value is fine but the across-row distribution is
389
- * itself sensitive).
390
- * - `aggregable: true` — explicit allow, even on a `hidden` field
391
- * (escape hatch — caller asserts cardinality leak isn't a concern).
392
- */
393
- function collectBlockedFields(schemaOptions) {
394
- const blocked = /* @__PURE__ */ new Set();
395
- const fieldRules = schemaOptions?.fieldRules;
396
- if (!fieldRules) return blocked;
397
- for (const [field, rules] of Object.entries(fieldRules)) {
398
- if (!rules) continue;
399
- if (rules.aggregable === true) continue;
400
- if (rules.aggregable === false) {
401
- blocked.add(field);
402
- continue;
403
- }
404
- if (rules.hidden) blocked.add(field);
405
- }
406
- return blocked;
407
- }
408
419
  /**
409
420
  * Reject:
410
421
  * - groupBy / measure.field / sort key referencing a hidden /
@@ -442,4 +453,4 @@ function assertFieldAllowed(context, ref, input) {
442
453
  if (blockedFields.has(ref)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context}, but the field is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Aggregating hidden fields would leak cardinality information.`);
443
454
  }
444
455
  //#endregion
445
- export { validate_exports as i, compileAggRequest as n, validateAggregations as r, adapterSupportsAggregate as t };
456
+ export { compileAggRequest as i, validateAggregations as n, validate_exports as r, adapterSupportsAggregate as t };
@@ -1,4 +1,4 @@
1
- import { n as DomainEvent } from "./EventTransport-B1GFiYUy.mjs";
1
+ import { n as DomainEvent } from "./EventTransport-CaaXM5Y1.mjs";
2
2
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
3
3
 
4
4
  //#region src/plugins/health.d.ts
@@ -1,4 +1,4 @@
1
- import { a as PushRefRegistry, l as EnvelopeWriter, s as PushRefStore } from "./pushref-registry-Bq_-EVsC.mjs";
1
+ import { a as PushRefRegistry, l as EnvelopeWriter, s as PushRefStore } from "./pushref-registry-Dqclzjy7.mjs";
2
2
  import { FastifyBaseLogger, FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/integrations/websocket/adapter.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.18.2",
3
+ "version": "2.18.3",
4
4
  "description": "Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -237,7 +237,7 @@
237
237
  "push": "classytic-push",
238
238
  "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
239
239
  "release": "npm run push -- main && npm run release:tag && npm publish",
240
- "prepublishOnly": "npm run typecheck && npm run lint && npm run build && npm run test:main && npm run smoke"
240
+ "prepublishOnly": "npm run typecheck && npm run lint && npm run build && npm run test:ci && npm run smoke"
241
241
  },
242
242
  "engines": {
243
243
  "node": ">=22"
@@ -359,10 +359,10 @@
359
359
  "@better-auth/mongo-adapter": "^1.6.9",
360
360
  "@biomejs/biome": "^2.4.15",
361
361
  "@classytic/dev-tools": "^0.2.0",
362
- "@classytic/mongokit": "^3.14.0",
362
+ "@classytic/mongokit": "^3.16.0",
363
363
  "@classytic/primitives": "^0.6.0",
364
- "@classytic/repo-core": "^0.5.0",
365
- "@classytic/sqlitekit": "^0.5.0",
364
+ "@classytic/repo-core": "^0.6.0",
365
+ "@classytic/sqlitekit": "^0.6.0",
366
366
  "@classytic/streamline": "^2.3.3",
367
367
  "@fastify/cors": "^11.2.0",
368
368
  "@fastify/helmet": "^13.0.2",
@@ -183,7 +183,7 @@ import type { ArcRequest, IRequestContext, IControllerResponse } from '@classyti
183
183
 
184
184
  ## Notes
185
185
 
186
- - **No default exports** outside Fastify plugin entry files (`auditPlugin`, `authPlugin`, `eventPlugin`, `idempotencyPlugin`, `introspectionPlugin`).
186
+ - **No default exports** outside Fastify plugin entry files (`fp()`-wrapped plugin entries — grep `export default fp` in src/ for the authoritative list).
187
187
  - **Type-only subpaths** produce `export {}` at runtime — interfaces are erased.
188
188
  - **Adapters live in kits**, not arc. Arc 2.12+ ships zero kit-bound adapters.
189
189
  - **Event types live in `@classytic/primitives/events`**, not `@classytic/arc/events`. Arc re-exports the runtime `MemoryEventTransport` only.