@classytic/arc 2.18.1 → 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 +35 -4
  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
@@ -0,0 +1,336 @@
1
+ import { b as isElevated, c as getOrgId, g as getUserId, n as PUBLIC_SCOPE, x as isMember } from "./types-Bi0r0vjG.mjs";
2
+ import { L as resolveEffectiveRoles, P as applyFieldReadPermissions } from "./permissions-3uzwkbQt.mjs";
3
+ import { t as getUserRoles } from "./types-D57iXYb8.mjs";
4
+ import { t as executePipeline } from "./pipe-9Tl-OAus.mjs";
5
+ import { isPaginatedResult, toCanonicalList } from "@classytic/repo-core/pagination";
6
+ //#region src/scope/projection.ts
7
+ /**
8
+ * Compute the request-scope projection. Returns `undefined` when no
9
+ * scope is attached (public / unscoped routes) so hosts can idiomatically
10
+ * write `ctx.scope?.organizationId` without a double-null check.
11
+ */
12
+ function buildRequestScopeProjection(scope) {
13
+ if (!scope) return void 0;
14
+ return {
15
+ organizationId: getOrgId(scope),
16
+ userId: getUserId(scope),
17
+ orgRoles: isMember(scope) ? scope.orgRoles : void 0
18
+ };
19
+ }
20
+ //#endregion
21
+ //#region src/core/fastifyAdapter.ts
22
+ /** Type guard for Mongoose-like documents with toObject() */
23
+ function isMongooseDoc(obj) {
24
+ return !!obj && typeof obj === "object" && "toObject" in obj && typeof obj.toObject === "function";
25
+ }
26
+ /**
27
+ * Apply field mask to a single object
28
+ * Filters fields based on include/exclude rules
29
+ */
30
+ function applyFieldMaskToObject(obj, fieldMask) {
31
+ if (!obj || typeof obj !== "object") return obj;
32
+ const plain = isMongooseDoc(obj) ? obj.toObject() : obj;
33
+ const { include, exclude } = fieldMask;
34
+ if (include && include.length > 0) {
35
+ const filtered = {};
36
+ for (const field of include) if (field in plain) filtered[field] = plain[field];
37
+ return filtered;
38
+ }
39
+ if (exclude && exclude.length > 0) {
40
+ const filtered = { ...plain };
41
+ for (const field of exclude) delete filtered[field];
42
+ return filtered;
43
+ }
44
+ return plain;
45
+ }
46
+ /**
47
+ * Apply field mask to response data (handles both objects and arrays)
48
+ */
49
+ function applyFieldMask(data, fieldMask) {
50
+ if (!fieldMask) return data;
51
+ if (Array.isArray(data)) return data.map((item) => applyFieldMaskToObject(item, fieldMask));
52
+ if (data && typeof data === "object") return applyFieldMaskToObject(data, fieldMask);
53
+ return data;
54
+ }
55
+ /**
56
+ * Project `FastifyRequest` into the curated `IRequestContext` shape that
57
+ * arc's controllers receive. This is a developer-facing facade, not a
58
+ * portability boundary — arc is Fastify-only. The projection lifts the
59
+ * fields tenant-scoped controllers reach for (`organizationId`,
60
+ * `teamId`, `scope`, `context`, server accessor) onto a flat, typed
61
+ * surface so handler code doesn't have to dig through Fastify's
62
+ * decorator surface or `req.metadata._scope` internals.
63
+ */
64
+ function createRequestContext(req) {
65
+ const reqWithExtras = req;
66
+ const requestContext = reqWithExtras.context ?? {};
67
+ const srv = req.server;
68
+ const serverAccessor = {
69
+ events: srv && "events" in srv ? srv.events : void 0,
70
+ audit: srv && "audit" in srv ? srv.audit : void 0,
71
+ queryCache: srv && "queryCache" in srv ? srv.queryCache : void 0,
72
+ log: req.log
73
+ };
74
+ const rawScope = reqWithExtras.scope;
75
+ const scopeProjection = buildRequestScopeProjection(rawScope);
76
+ return {
77
+ query: reqWithExtras.query ?? {},
78
+ body: reqWithExtras.body ?? {},
79
+ params: reqWithExtras.params ?? {},
80
+ headers: reqWithExtras.headers,
81
+ user: reqWithExtras.user ? (() => {
82
+ const user = reqWithExtras.user;
83
+ const rawId = user._id ?? user.id;
84
+ const normalizedId = rawId ? String(rawId) : void 0;
85
+ return {
86
+ ...user,
87
+ id: normalizedId,
88
+ _id: normalizedId
89
+ };
90
+ })() : null,
91
+ context: requestContext,
92
+ scope: scopeProjection,
93
+ metadata: {
94
+ ...reqWithExtras.context,
95
+ arc: reqWithExtras.arc,
96
+ _scope: rawScope,
97
+ _ownershipCheck: reqWithExtras._ownershipCheck,
98
+ _policyFilters: reqWithExtras._policyFilters ?? {},
99
+ log: reqWithExtras.log
100
+ },
101
+ server: serverAccessor
102
+ };
103
+ }
104
+ /**
105
+ * Get typed auth context from an IRequestContext.
106
+ * Use this in controller overrides to access request context.
107
+ *
108
+ * For org scope, use `getControllerScope(req)` instead.
109
+ */
110
+ function getControllerContext(req) {
111
+ return req.context ?? req.metadata ?? {};
112
+ }
113
+ /**
114
+ * Get request scope from an IRequestContext.
115
+ * Returns the RequestScope set by auth adapters.
116
+ */
117
+ function getControllerScope(req) {
118
+ return req.metadata?._scope ?? PUBLIC_SCOPE;
119
+ }
120
+ /**
121
+ * Compute per-field capability metadata for the current user.
122
+ * Only includes fields that have restrictions — unrestricted fields
123
+ * are omitted (frontend defaults to { readable: true, writable: true }).
124
+ */
125
+ function computeFieldCapabilities(fieldPerms, effectiveRoles) {
126
+ const caps = {};
127
+ for (const [field, perm] of Object.entries(fieldPerms)) {
128
+ let readable = true;
129
+ let writable = true;
130
+ switch (perm._type) {
131
+ case "hidden":
132
+ readable = false;
133
+ writable = false;
134
+ break;
135
+ case "visibleTo":
136
+ readable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
137
+ break;
138
+ case "writableBy":
139
+ writable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
140
+ break;
141
+ }
142
+ caps[field] = {
143
+ readable,
144
+ writable
145
+ };
146
+ }
147
+ return caps;
148
+ }
149
+ /**
150
+ * Send a controller's `IControllerResponse` through Fastify's reply.
151
+ *
152
+ * The `IControllerResponse<T>` envelope is arc's internal pipeline
153
+ * contract — it's how custom `status`, `meta`, `headers`, and field
154
+ * permissions thread from a handler through interceptors to the wire
155
+ * without each step having to touch `reply` directly. Not a portability
156
+ * abstraction (arc is Fastify-only); a pipeline-internal one.
157
+ *
158
+ * **Auto-envelope of bare returns.** Custom route handlers are
159
+ * expected to return the envelope shape so this function can apply
160
+ * field permissions, attach `fieldCaps`, and flatten paginated lists.
161
+ * A handler that returns raw `T` (array, plain object, primitive) used
162
+ * to silently produce a broken response — `response.data` was
163
+ * `undefined`, the body sent was `undefined`, and any declared
164
+ * `fields:` permission map was bypassed (the leak shape mentora
165
+ * flagged). 2.17 detects bare returns and wraps them into
166
+ * `{ data: response, status: 200 }` before the field-permission
167
+ * pipeline runs, so `fields:` applies to ALL custom routes whether the
168
+ * handler envelopes its return value or not. The envelope shape is
169
+ * still the canonical contract — auto-wrapping is a safety net, not
170
+ * an invitation to drop the envelope.
171
+ */
172
+ function sendControllerResponse(reply, response, request) {
173
+ if (response === null || typeof response !== "object" || !Object.hasOwn(response, "data")) response = {
174
+ data: response,
175
+ status: 200
176
+ };
177
+ const reqWithExtras = request;
178
+ const fieldMaskConfig = reqWithExtras?.fieldMask;
179
+ const arcMeta = reqWithExtras?.arc;
180
+ const scope = reqWithExtras?.scope ?? PUBLIC_SCOPE;
181
+ const fieldPerms = isElevated(scope) ? void 0 : arcMeta?.fields;
182
+ const effectiveRoles = fieldPerms ? resolveEffectiveRoles(getUserRoles(reqWithExtras?.user), isMember(scope) ? scope.orgRoles : []) : [];
183
+ const fieldCaps = fieldPerms ? computeFieldCapabilities(fieldPerms, effectiveRoles) : void 0;
184
+ const hasFieldRestrictions = !!(fieldMaskConfig || fieldPerms);
185
+ /** Apply both field mask and field-level permissions to a data item */
186
+ const applyPermissions = (data) => {
187
+ let result = fieldMaskConfig ? applyFieldMask(data, fieldMaskConfig) : data;
188
+ if (fieldPerms && result && typeof result === "object") if (Array.isArray(result)) result = result.map((item) => applyFieldReadPermissions(item, fieldPerms, effectiveRoles));
189
+ else result = applyFieldReadPermissions(result, fieldPerms, effectiveRoles);
190
+ return result;
191
+ };
192
+ if (response.headers) for (const [key, value] of Object.entries(response.headers)) reply.header(key, value);
193
+ if (response.data && typeof response.data === "object" && Array.isArray(response.data.data)) {
194
+ const payload = response.data;
195
+ const filteredRows = hasFieldRestrictions ? applyPermissions(payload.data) : payload.data;
196
+ let canonical;
197
+ if (isPaginatedResult(payload)) canonical = toCanonicalList({
198
+ ...payload,
199
+ data: filteredRows
200
+ });
201
+ else if (typeof payload.page === "number" && typeof payload.limit === "number" && typeof payload.total === "number") canonical = toCanonicalList({
202
+ method: "offset",
203
+ data: filteredRows,
204
+ page: payload.page,
205
+ limit: payload.limit,
206
+ total: payload.total,
207
+ pages: payload.pages ?? Math.ceil(payload.total / Math.max(1, payload.limit)),
208
+ hasNext: payload.hasNext ?? false,
209
+ hasPrev: payload.hasPrev ?? false
210
+ });
211
+ else canonical = toCanonicalList(filteredRows);
212
+ reply.code(response.status ?? 200).send({
213
+ ...canonical,
214
+ ...response.meta ?? {},
215
+ ...fieldCaps ? { fieldPermissions: fieldCaps } : {}
216
+ });
217
+ return;
218
+ }
219
+ const rawData = hasFieldRestrictions ? applyPermissions(response.data) : response.data;
220
+ const filteredData = rawData !== null && typeof rawData === "object" && typeof rawData.toJSON === "function" ? rawData.toJSON() : rawData;
221
+ const status = response.status ?? 200;
222
+ if (filteredData !== null && typeof filteredData === "object" && !Array.isArray(filteredData) && (response.meta || fieldCaps)) {
223
+ reply.code(status).send({
224
+ ...filteredData,
225
+ ...response.meta ?? {},
226
+ ...fieldCaps ? { fieldPermissions: fieldCaps } : {}
227
+ });
228
+ return;
229
+ }
230
+ reply.code(status).send(filteredData);
231
+ }
232
+ /**
233
+ * Bridge an `IController` method (`(ctx: IRequestContext) => Promise<IControllerResponse<T>>`)
234
+ * into a Fastify route handler. Translates between arc's curated request
235
+ * context surface and Fastify's request/reply, and routes the envelope
236
+ * through `sendControllerResponse` so the pipeline applies field
237
+ * permissions, status, and meta uniformly. Internal plumbing, not a
238
+ * portability layer — arc is Fastify-only.
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * const controller = new BaseController(repository);
243
+ * const listHandler = createFastifyHandler(controller.list.bind(controller));
244
+ * fastify.get('/products', listHandler);
245
+ * ```
246
+ */
247
+ function createFastifyHandler(controllerMethod) {
248
+ return async (req, reply) => {
249
+ sendControllerResponse(reply, await controllerMethod(createRequestContext(req)), req);
250
+ };
251
+ }
252
+ /**
253
+ * Create Fastify adapters for all CRUD methods of an IController
254
+ *
255
+ * Returns Fastify-compatible handlers for each CRUD operation
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * const controller = new BaseController(repository);
260
+ * const handlers = createCrudHandlers(controller);
261
+ *
262
+ * fastify.get('/', handlers.list);
263
+ * fastify.get('/:id', handlers.get);
264
+ * fastify.post('/', handlers.create);
265
+ * fastify.patch('/:id', handlers.update);
266
+ * fastify.delete('/:id', handlers.delete);
267
+ * ```
268
+ */
269
+ function createCrudHandlers(controller) {
270
+ return {
271
+ list: createFastifyHandler(controller.list.bind(controller)),
272
+ get: createFastifyHandler(controller.get.bind(controller)),
273
+ create: createFastifyHandler(controller.create.bind(controller)),
274
+ update: createFastifyHandler(controller.update.bind(controller)),
275
+ delete: createFastifyHandler(controller.delete.bind(controller))
276
+ };
277
+ }
278
+ //#endregion
279
+ //#region src/core/middlewares/pipeline.ts
280
+ /**
281
+ * Resolve pipeline steps for a specific operation.
282
+ * Flat-array config applies to every op; map config applies per-op.
283
+ */
284
+ function resolvePipelineSteps(pipeline, operation) {
285
+ if (!pipeline) return [];
286
+ if (Array.isArray(pipeline)) return pipeline;
287
+ return pipeline[operation] ?? [];
288
+ }
289
+ /**
290
+ * Wrap a controller method (one that takes `IRequestContext` and returns
291
+ * `IControllerResponse<T>`) with pipeline execution. Used by CRUD ops and
292
+ * string-handler custom routes.
293
+ */
294
+ function buildPipelineHandler(controllerMethod, steps, operation, resourceName) {
295
+ return async (req, reply) => {
296
+ sendControllerResponse(reply, await executePipeline(steps, {
297
+ ...createRequestContext(req),
298
+ resource: resourceName,
299
+ operation
300
+ }, (ctx) => controllerMethod(ctx), operation), req);
301
+ };
302
+ }
303
+ /**
304
+ * Wrap an action handler (one that takes `(id, data, req)` and returns a raw
305
+ * result) with pipeline execution. Returns a function that produces a full
306
+ * `IControllerResponse<unknown>` — the action router feeds this directly into
307
+ * `sendControllerResponse`, so field masking, custom status codes, `meta`,
308
+ * `details`, and structured error codes from pipeline interceptors flow
309
+ * through to the client unchanged.
310
+ *
311
+ * CRUD and actions now share the same parity invariant: a pipeline that
312
+ * returns `{ success: false, status: 422, error, details, meta }` reaches the
313
+ * client with all four fields intact. Previously the action path stringified
314
+ * failures into a generic `Error` and dropped everything except `statusCode`.
315
+ *
316
+ * Handler throws still bubble out — the caller's try/catch handles `onError`
317
+ * shaping and the generic `ACTION_FAILED` fallback.
318
+ */
319
+ function buildActionPipelineHandler(handler, steps, operation, resourceName) {
320
+ if (steps.length === 0) return async (id, data, req) => ({
321
+ status: 200,
322
+ data: await handler(id, data, req)
323
+ });
324
+ return async (id, data, req) => {
325
+ return executePipeline(steps, {
326
+ ...createRequestContext(req),
327
+ resource: resourceName,
328
+ operation
329
+ }, async (_ctx) => ({
330
+ status: 200,
331
+ data: await handler(id, data, req)
332
+ }), operation);
333
+ };
334
+ }
335
+ //#endregion
336
+ export { createFastifyHandler as a, getControllerScope as c, createCrudHandlers as i, sendControllerResponse as l, buildPipelineHandler as n, createRequestContext as o, resolvePipelineSteps as r, getControllerContext as s, buildActionPipelineHandler as t, buildRequestScopeProjection as u };
@@ -1,8 +1,8 @@
1
- import { Kt as AnyRecord, Q as MiddlewareConfig, dt as RouteDefinition, mt as RouteSchemaOptions, nt as PresetHook } from "../index-C000faom.mjs";
2
- import { a as arcCorePlugin, i as _default, n as ArcCorePluginOptions, r as PluginMeta, t as ArcCore } from "../fastify-augmentation-CL48B6gW.mjs";
3
- import { _ as HealthOptions, a as MetricsCollector, c as metricsPlugin, d as ssePlugin, f as CachingOptions, g as HealthCheck, h as cachingPlugin, i as MetricEntry, l as SSEOptions, m as _default$1, n as _default$7, o as MetricsOptions, p as CachingRule, r as versioningPlugin, s as _default$4, t as VersioningOptions, u as _default$6, v as _default$3, y as healthPlugin } from "../versioning-5H4930tj.mjs";
4
- import { i as errorHandlerPlugin, n as ErrorMapper, r as defaultIsDuplicateKeyError, t as ErrorHandlerOptions } from "../errorHandler-DuqTqeqn.mjs";
5
- import { t as TracingOptions } from "../tracing-BYQ3hsbM.mjs";
1
+ import { Kt as AnyRecord, Q as MiddlewareConfig, dt as RouteDefinition, mt as RouteSchemaOptions, nt as PresetHook } from "../index-QYma8mdC.mjs";
2
+ import { a as arcCorePlugin, i as _default, n as ArcCorePluginOptions, r as PluginMeta, t as ArcCore } from "../fastify-augmentation-BdCl_eNo.mjs";
3
+ import { _ as HealthOptions, a as MetricsCollector, c as metricsPlugin, d as ssePlugin, f as CachingOptions, g as HealthCheck, h as cachingPlugin, i as MetricEntry, l as SSEOptions, m as _default$1, n as _default$7, o as MetricsOptions, p as CachingRule, r as versioningPlugin, s as _default$4, t as VersioningOptions, u as _default$6, v as _default$3, y as healthPlugin } from "../versioning-BSEDGXKC.mjs";
4
+ import { i as errorHandlerPlugin, n as ErrorMapper, r as defaultIsDuplicateKeyError, t as ErrorHandlerOptions } from "../errorHandler-_Lp2k9M3.mjs";
5
+ import { t as TracingOptions } from "../tracing-Bnb1-YVd.mjs";
6
6
  import * as _$node_stream0 from "node:stream";
7
7
  import { PaginatedResult } from "@classytic/repo-core/pagination";
8
8
  import { FastifyInstance, FastifyPluginAsync } from "fastify";
@@ -1,15 +1,15 @@
1
1
  import { p as MUTATION_OPERATIONS } from "../constants-TrJVIJl0.mjs";
2
2
  import { c as getOrgId } from "../types-Bi0r0vjG.mjs";
3
3
  import { n as requestContext } from "../requestContext-DDLS3NVU.mjs";
4
- import { t as hasEvents } from "../typeGuards-BzkXkvVv.mjs";
5
- import { t as HookSystem } from "../HookSystem-Cmf7-Etp.mjs";
6
- import { t as ResourceRegistry } from "../ResourceRegistry-CzSzVK1m.mjs";
7
- import { n as caching_default, t as cachingPlugin } from "../caching-8MfrrUYU.mjs";
8
- import { n as errorHandlerPlugin, t as defaultIsDuplicateKeyError } from "../errorHandler-wQejMBT3.mjs";
9
- import { n as metrics_default, t as metricsPlugin } from "../metrics-B-PU4-Yu.mjs";
10
- import { t as replyHelpersPlugin } from "../replyHelpers-boS5Rx26.mjs";
11
- import { n as sse_default, t as ssePlugin } from "../sse-jh5OqFjR.mjs";
12
- import { n as versioning_default, t as versioningPlugin } from "../versioning-CuIwUQZ2.mjs";
4
+ import { t as hasEvents } from "../typeGuards-CcFZXgU7.mjs";
5
+ import { t as HookSystem } from "../HookSystem-BfCpWbwM.mjs";
6
+ import { t as ResourceRegistry } from "../ResourceRegistry-CWmOggxS.mjs";
7
+ import { n as caching_default, t as cachingPlugin } from "../caching-C0DdKiYB.mjs";
8
+ import { n as errorHandlerPlugin, t as defaultIsDuplicateKeyError } from "../errorHandler-Bua97SEt.mjs";
9
+ import { n as metrics_default, t as metricsPlugin } from "../metrics-Da2EUPi9.mjs";
10
+ import { t as replyHelpersPlugin } from "../replyHelpers-DXHVxrrj.mjs";
11
+ import { n as sse_default, t as ssePlugin } from "../sse-Xq8UtuQx.mjs";
12
+ import { n as versioning_default, t as versioningPlugin } from "../versioning-C0K8KQLx.mjs";
13
13
  import { randomUUID } from "node:crypto";
14
14
  import fp from "fastify-plugin";
15
15
  //#region src/core/arcCorePlugin.ts
@@ -1,4 +1,4 @@
1
- import { t as hasEvents } from "../typeGuards-BzkXkvVv.mjs";
1
+ import { t as hasEvents } from "../typeGuards-CcFZXgU7.mjs";
2
2
  import fp from "fastify-plugin";
3
3
  //#region src/plugins/response-cache.ts
4
4
  /**
@@ -1,2 +1,2 @@
1
- import { a as traced, i as isTracingAvailable, n as _default, r as createSpan, t as TracingOptions } from "../tracing-BYQ3hsbM.mjs";
1
+ import { a as traced, i as isTracingAvailable, n as _default, r as createSpan, t as TracingOptions } from "../tracing-Bnb1-YVd.mjs";
2
2
  export { type TracingOptions, createSpan, isTracingAvailable, traced, _default as tracingPlugin };
@@ -58,7 +58,7 @@ try {
58
58
  function createTracerProvider(options) {
59
59
  if (!isAvailable || !NodeTracerProvider || !BatchSpanProcessor || !OTLPTraceExporter) return null;
60
60
  const { serviceName = "@classytic/arc", serviceVersion, exporterUrl = "http://localhost:4318/v1/traces" } = options;
61
- const resolvedVersion = serviceVersion ?? "2.18.1";
61
+ const resolvedVersion = serviceVersion ?? "2.18.3";
62
62
  const exporter = new OTLPTraceExporter({ url: exporterUrl });
63
63
  const provider = new NodeTracerProvider({ resource: { attributes: {
64
64
  "service.name": serviceName,
@@ -1,7 +1,7 @@
1
- import { rt as PresetResult } from "../index-C000faom.mjs";
2
- import { i as RequestScope } from "../types-CfapW2t8.mjs";
3
- import { c as PermissionCheck } from "../fields-DqZLp5d6.mjs";
4
- import { a as StorageReadResult, i as StorageReadRange, n as StorageContext, o as StorageUploadInput, r as StorageFile, t as Storage } from "../storage-BFmamN5m.mjs";
1
+ import { rt as PresetResult } from "../index-QYma8mdC.mjs";
2
+ import { i as RequestScope } from "../types-BJwJN2Pg.mjs";
3
+ import { c as PermissionCheck } from "../fields-CKLrhyOb.mjs";
4
+ import { a as StorageReadResult, i as StorageReadRange, n as StorageContext, o as StorageUploadInput, r as StorageFile, t as Storage } from "../storage-C5q8fuEt.mjs";
5
5
 
6
6
  //#region src/presets/filesUpload.d.ts
7
7
  interface FilesUploadPresetRoutes {
@@ -1,7 +1,7 @@
1
1
  import { i as NotFoundError, u as ValidationError } from "../errors-j4aJm1Wg.mjs";
2
2
  import { c as getOrgId, g as getUserId } from "../types-Bi0r0vjG.mjs";
3
- import { C as requireAuth, y as allowPublic } from "../permissions-B1Qy55fg.mjs";
4
- import { t as multipartBody } from "../multipartBody-VjVWmZl8.mjs";
3
+ import { C as requireAuth, y as allowPublic } from "../permissions-3uzwkbQt.mjs";
4
+ import { t as multipartBody } from "../multipartBody-709YcXi1.mjs";
5
5
  //#region src/presets/filesUpload.ts
6
6
  const DEFAULT_FIELD_NAME = "file";
7
7
  const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
@@ -1,4 +1,4 @@
1
- import { Kt as AnyRecord, bt as IRequestContext, d as PaginationResult, rt as PresetResult, st as ResourceConfig, yt as IControllerResponse } from "../index-C000faom.mjs";
1
+ import { Kt as AnyRecord, bt as IRequestContext, d as PaginationResult, rt as PresetResult, st as ResourceConfig, yt as IControllerResponse } from "../index-QYma8mdC.mjs";
2
2
  import { FilesUploadPresetOptions, FilesUploadPresetPermissions, FilesUploadPresetRoutes, filesUploadPreset } from "./filesUpload.mjs";
3
3
  import { MultiTenantOptions, TenantFieldSpec, multiTenantPreset } from "./multiTenant.mjs";
4
4
  import { SearchHandler, SearchPresetOptions, SearchRouteConfig, searchPreset } from "./search.mjs";
@@ -1,5 +1,5 @@
1
1
  import { multiTenantPreset } from "./multiTenant.mjs";
2
- import { a as registerPreset, c as auditedPreset, d as ownedByUserPreset, i as getPreset, l as softDeletePreset, n as flexibleMultiTenantPreset, o as treePreset, r as getAvailablePresets, s as bulkPreset, t as applyPresets, u as slugLookupPreset } from "../presets-BYbPobbF.mjs";
2
+ import { a as registerPreset, c as auditedPreset, d as ownedByUserPreset, i as getPreset, l as softDeletePreset, n as flexibleMultiTenantPreset, o as treePreset, r as getAvailablePresets, s as bulkPreset, t as applyPresets, u as slugLookupPreset } from "../presets-CF79RHEo.mjs";
3
3
  import { filesUploadPreset } from "./filesUpload.mjs";
4
4
  import { searchPreset } from "./search.mjs";
5
5
  export { applyPresets, auditedPreset, bulkPreset, filesUploadPreset, flexibleMultiTenantPreset, getAvailablePresets, getPreset, multiTenantPreset, ownedByUserPreset, registerPreset, searchPreset, slugLookupPreset, softDeletePreset, treePreset };
@@ -1,4 +1,4 @@
1
- import { J as CrudRouteKey, rt as PresetResult } from "../index-C000faom.mjs";
1
+ import { J as CrudRouteKey, rt as PresetResult } from "../index-QYma8mdC.mjs";
2
2
 
3
3
  //#region src/presets/multiTenant.d.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
- import { dt as RouteDefinition, ft as RouteMcpConfig, ht as ControllerHandler, rt as PresetResult } from "../index-C000faom.mjs";
2
- import { c as PermissionCheck } from "../fields-DqZLp5d6.mjs";
1
+ import { dt as RouteDefinition, ft as RouteMcpConfig, ht as ControllerHandler, rt as PresetResult } from "../index-QYma8mdC.mjs";
2
+ import { c as PermissionCheck } from "../fields-CKLrhyOb.mjs";
3
3
 
4
4
  //#region src/presets/search.d.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { C as requireAuth, y as allowPublic } from "../permissions-B1Qy55fg.mjs";
1
+ import { C as requireAuth, y as allowPublic } from "../permissions-3uzwkbQt.mjs";
2
2
  //#region src/presets/search.ts
3
3
  const BUILTINS = [
4
4
  {
@@ -1,5 +1,5 @@
1
1
  import { b as isElevated, n as PUBLIC_SCOPE } from "./types-Bi0r0vjG.mjs";
2
- import { C as requireAuth, T as requireRoles, y as allowPublic } from "./permissions-B1Qy55fg.mjs";
2
+ import { C as requireAuth, T as requireRoles, y as allowPublic } from "./permissions-3uzwkbQt.mjs";
3
3
  import { multiTenantPreset } from "./presets/multiTenant.mjs";
4
4
  //#region src/presets/ownedByUser.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { r as CacheStore } from "./interface-BVkZsvEe.mjs";
2
- import { i as QueryCache } from "./QueryCache-8YmAQ-zW.mjs";
1
+ import { r as CacheStore } from "./interface-DL7LlWS5.mjs";
2
+ import { i as QueryCache } from "./QueryCache-Dsyv2mLS.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
 
5
5
  //#region src/cache/queryCachePlugin.d.ts
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { t as MemoryCacheStore } from "./memory-QOLe11D5.mjs";
3
- import { t as hasEvents } from "./typeGuards-BzkXkvVv.mjs";
4
- import { i as versionKey, r as tagVersionKey } from "./keys-DopsCuyQ.mjs";
2
+ import { t as MemoryCacheStore } from "./memory-TdTHCS9-.mjs";
3
+ import { t as hasEvents } from "./typeGuards-CcFZXgU7.mjs";
4
+ import { i as versionKey, r as tagVersionKey } from "./keys-B3yyBEyH.mjs";
5
5
  import fp from "fastify-plugin";
6
6
  //#region src/cache/QueryCache.ts
7
7
  var QueryCache = class {