@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,724 +0,0 @@
1
- import { h as SYSTEM_FIELDS } from "./constants-TrJVIJl0.mjs";
2
- import { f as createError, r as ForbiddenError } from "./errors-j4aJm1Wg.mjs";
3
- import { b as isElevated, c as getOrgId, g as getUserId, n as PUBLIC_SCOPE, x as isMember } from "./types-Bi0r0vjG.mjs";
4
- import { n as requestContext } from "./requestContext-DDLS3NVU.mjs";
5
- import { I as evaluateAndApplyPermission, M as applyFieldWritePermissions, P as resolveEffectiveRoles, j as applyFieldReadPermissions } from "./permissions-B1Qy55fg.mjs";
6
- import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
7
- import { t as executePipeline } from "./pipe-DiCyvyPN.mjs";
8
- import { isPaginatedResult, toCanonicalList } from "@classytic/repo-core/pagination";
9
- //#region src/core/BodySanitizer.ts
10
- /**
11
- * BodySanitizer - Composable body sanitization logic extracted from BaseController.
12
- *
13
- * Strips readonly fields, system-managed fields, and applies field-level
14
- * write permissions from request bodies before create/update operations.
15
- *
16
- * Designed to be used standalone or composed into controllers.
17
- */
18
- const DEFAULT_FIELD_WRITE_DENIAL_POLICY = "reject";
19
- var BodySanitizer = class {
20
- schemaOptions;
21
- onFieldWriteDenied;
22
- constructor(config) {
23
- this.schemaOptions = config.schemaOptions;
24
- this.onFieldWriteDenied = config.onFieldWriteDenied ?? "reject";
25
- }
26
- /**
27
- * Strip readonly and system-managed fields from request body.
28
- * Prevents clients from overwriting _id, timestamps, __v, etc.
29
- *
30
- * Also applies field-level write permissions when the request has
31
- * field permission metadata.
32
- */
33
- sanitize(body, _operation, req, meta) {
34
- let sanitized = { ...body };
35
- for (const field of SYSTEM_FIELDS) delete sanitized[field];
36
- const scopeForRules = req ? (meta ?? req.metadata)?._scope ?? PUBLIC_SCOPE : void 0;
37
- const scopeIsElevated = scopeForRules ? isElevated(scopeForRules) : false;
38
- const fieldRules = this.schemaOptions.fieldRules ?? {};
39
- for (const [field, rules] of Object.entries(fieldRules)) {
40
- const bypass = Boolean(rules.preserveForElevated) && scopeIsElevated;
41
- if ((rules.systemManaged || rules.readonly) && !bypass) delete sanitized[field];
42
- if (_operation === "update" && (rules.immutable || rules.immutableAfterCreate) && !bypass) delete sanitized[field];
43
- }
44
- if (req) {
45
- const arcContext = meta ?? req.metadata;
46
- const scope = arcContext?._scope ?? PUBLIC_SCOPE;
47
- if (!isElevated(scope)) {
48
- const fieldPerms = arcContext?.arc?.fields;
49
- if (fieldPerms) {
50
- const effectiveRoles = resolveEffectiveRoles(getUserRoles(req.user), isMember(scope) ? scope.orgRoles : []);
51
- const { body: filtered, deniedFields } = applyFieldWritePermissions(sanitized, fieldPerms, effectiveRoles);
52
- if (deniedFields.length > 0 && this.onFieldWriteDenied === "reject") throw new ForbiddenError(`Not permitted to write field${deniedFields.length === 1 ? "" : "s"}: ${deniedFields.join(", ")}`);
53
- sanitized = filtered;
54
- }
55
- }
56
- }
57
- return sanitized;
58
- }
59
- };
60
- //#endregion
61
- //#region src/scope/projection.ts
62
- /**
63
- * Compute the request-scope projection. Returns `undefined` when no
64
- * scope is attached (public / unscoped routes) so hosts can idiomatically
65
- * write `ctx.scope?.organizationId` without a double-null check.
66
- */
67
- function buildRequestScopeProjection(scope) {
68
- if (!scope) return void 0;
69
- return {
70
- organizationId: getOrgId(scope),
71
- userId: getUserId(scope),
72
- orgRoles: isMember(scope) ? scope.orgRoles : void 0
73
- };
74
- }
75
- //#endregion
76
- //#region src/core/fastifyAdapter.ts
77
- /** Type guard for Mongoose-like documents with toObject() */
78
- function isMongooseDoc(obj) {
79
- return !!obj && typeof obj === "object" && "toObject" in obj && typeof obj.toObject === "function";
80
- }
81
- /**
82
- * Apply field mask to a single object
83
- * Filters fields based on include/exclude rules
84
- */
85
- function applyFieldMaskToObject(obj, fieldMask) {
86
- if (!obj || typeof obj !== "object") return obj;
87
- const plain = isMongooseDoc(obj) ? obj.toObject() : obj;
88
- const { include, exclude } = fieldMask;
89
- if (include && include.length > 0) {
90
- const filtered = {};
91
- for (const field of include) if (field in plain) filtered[field] = plain[field];
92
- return filtered;
93
- }
94
- if (exclude && exclude.length > 0) {
95
- const filtered = { ...plain };
96
- for (const field of exclude) delete filtered[field];
97
- return filtered;
98
- }
99
- return plain;
100
- }
101
- /**
102
- * Apply field mask to response data (handles both objects and arrays)
103
- */
104
- function applyFieldMask(data, fieldMask) {
105
- if (!fieldMask) return data;
106
- if (Array.isArray(data)) return data.map((item) => applyFieldMaskToObject(item, fieldMask));
107
- if (data && typeof data === "object") return applyFieldMaskToObject(data, fieldMask);
108
- return data;
109
- }
110
- /**
111
- * Project `FastifyRequest` into the curated `IRequestContext` shape that
112
- * arc's controllers receive. This is a developer-facing facade, not a
113
- * portability boundary — arc is Fastify-only. The projection lifts the
114
- * fields tenant-scoped controllers reach for (`organizationId`,
115
- * `teamId`, `scope`, `context`, server accessor) onto a flat, typed
116
- * surface so handler code doesn't have to dig through Fastify's
117
- * decorator surface or `req.metadata._scope` internals.
118
- */
119
- function createRequestContext(req) {
120
- const reqWithExtras = req;
121
- const requestContext = reqWithExtras.context ?? {};
122
- const srv = req.server;
123
- const serverAccessor = {
124
- events: srv && "events" in srv ? srv.events : void 0,
125
- audit: srv && "audit" in srv ? srv.audit : void 0,
126
- queryCache: srv && "queryCache" in srv ? srv.queryCache : void 0,
127
- log: req.log
128
- };
129
- const rawScope = reqWithExtras.scope;
130
- const scopeProjection = buildRequestScopeProjection(rawScope);
131
- return {
132
- query: reqWithExtras.query ?? {},
133
- body: reqWithExtras.body ?? {},
134
- params: reqWithExtras.params ?? {},
135
- headers: reqWithExtras.headers,
136
- user: reqWithExtras.user ? (() => {
137
- const user = reqWithExtras.user;
138
- const rawId = user._id ?? user.id;
139
- const normalizedId = rawId ? String(rawId) : void 0;
140
- return {
141
- ...user,
142
- id: normalizedId,
143
- _id: normalizedId
144
- };
145
- })() : null,
146
- context: requestContext,
147
- scope: scopeProjection,
148
- metadata: {
149
- ...reqWithExtras.context,
150
- arc: reqWithExtras.arc,
151
- _scope: rawScope,
152
- _ownershipCheck: reqWithExtras._ownershipCheck,
153
- _policyFilters: reqWithExtras._policyFilters ?? {},
154
- log: reqWithExtras.log
155
- },
156
- server: serverAccessor
157
- };
158
- }
159
- /**
160
- * Get typed auth context from an IRequestContext.
161
- * Use this in controller overrides to access request context.
162
- *
163
- * For org scope, use `getControllerScope(req)` instead.
164
- */
165
- function getControllerContext(req) {
166
- return req.context ?? req.metadata ?? {};
167
- }
168
- /**
169
- * Get request scope from an IRequestContext.
170
- * Returns the RequestScope set by auth adapters.
171
- */
172
- function getControllerScope(req) {
173
- return req.metadata?._scope ?? PUBLIC_SCOPE;
174
- }
175
- /**
176
- * Compute per-field capability metadata for the current user.
177
- * Only includes fields that have restrictions — unrestricted fields
178
- * are omitted (frontend defaults to { readable: true, writable: true }).
179
- */
180
- function computeFieldCapabilities(fieldPerms, effectiveRoles) {
181
- const caps = {};
182
- for (const [field, perm] of Object.entries(fieldPerms)) {
183
- let readable = true;
184
- let writable = true;
185
- switch (perm._type) {
186
- case "hidden":
187
- readable = false;
188
- writable = false;
189
- break;
190
- case "visibleTo":
191
- readable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
192
- break;
193
- case "writableBy":
194
- writable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
195
- break;
196
- }
197
- caps[field] = {
198
- readable,
199
- writable
200
- };
201
- }
202
- return caps;
203
- }
204
- /**
205
- * Send a controller's `IControllerResponse` through Fastify's reply.
206
- *
207
- * The `IControllerResponse<T>` envelope is arc's internal pipeline
208
- * contract — it's how custom `status`, `meta`, `headers`, and field
209
- * permissions thread from a handler through interceptors to the wire
210
- * without each step having to touch `reply` directly. Not a portability
211
- * abstraction (arc is Fastify-only); a pipeline-internal one.
212
- *
213
- * **Auto-envelope of bare returns.** Custom route handlers are
214
- * expected to return the envelope shape so this function can apply
215
- * field permissions, attach `fieldCaps`, and flatten paginated lists.
216
- * A handler that returns raw `T` (array, plain object, primitive) used
217
- * to silently produce a broken response — `response.data` was
218
- * `undefined`, the body sent was `undefined`, and any declared
219
- * `fields:` permission map was bypassed (the leak shape mentora
220
- * flagged). 2.17 detects bare returns and wraps them into
221
- * `{ data: response, status: 200 }` before the field-permission
222
- * pipeline runs, so `fields:` applies to ALL custom routes whether the
223
- * handler envelopes its return value or not. The envelope shape is
224
- * still the canonical contract — auto-wrapping is a safety net, not
225
- * an invitation to drop the envelope.
226
- */
227
- function sendControllerResponse(reply, response, request) {
228
- if (response === null || typeof response !== "object" || !Object.hasOwn(response, "data")) response = {
229
- data: response,
230
- status: 200
231
- };
232
- const reqWithExtras = request;
233
- const fieldMaskConfig = reqWithExtras?.fieldMask;
234
- const arcMeta = reqWithExtras?.arc;
235
- const scope = reqWithExtras?.scope ?? PUBLIC_SCOPE;
236
- const fieldPerms = isElevated(scope) ? void 0 : arcMeta?.fields;
237
- const effectiveRoles = fieldPerms ? resolveEffectiveRoles(getUserRoles(reqWithExtras?.user), isMember(scope) ? scope.orgRoles : []) : [];
238
- const fieldCaps = fieldPerms ? computeFieldCapabilities(fieldPerms, effectiveRoles) : void 0;
239
- const hasFieldRestrictions = !!(fieldMaskConfig || fieldPerms);
240
- /** Apply both field mask and field-level permissions to a data item */
241
- const applyPermissions = (data) => {
242
- let result = fieldMaskConfig ? applyFieldMask(data, fieldMaskConfig) : data;
243
- if (fieldPerms && result && typeof result === "object") if (Array.isArray(result)) result = result.map((item) => applyFieldReadPermissions(item, fieldPerms, effectiveRoles));
244
- else result = applyFieldReadPermissions(result, fieldPerms, effectiveRoles);
245
- return result;
246
- };
247
- if (response.headers) for (const [key, value] of Object.entries(response.headers)) reply.header(key, value);
248
- if (response.data && typeof response.data === "object" && Array.isArray(response.data.data)) {
249
- const payload = response.data;
250
- const filteredRows = hasFieldRestrictions ? applyPermissions(payload.data) : payload.data;
251
- let canonical;
252
- if (isPaginatedResult(payload)) canonical = toCanonicalList({
253
- ...payload,
254
- data: filteredRows
255
- });
256
- else if (typeof payload.page === "number" && typeof payload.limit === "number" && typeof payload.total === "number") canonical = toCanonicalList({
257
- method: "offset",
258
- data: filteredRows,
259
- page: payload.page,
260
- limit: payload.limit,
261
- total: payload.total,
262
- pages: payload.pages ?? Math.ceil(payload.total / Math.max(1, payload.limit)),
263
- hasNext: payload.hasNext ?? false,
264
- hasPrev: payload.hasPrev ?? false
265
- });
266
- else canonical = toCanonicalList(filteredRows);
267
- reply.code(response.status ?? 200).send({
268
- ...canonical,
269
- ...response.meta ?? {},
270
- ...fieldCaps ? { fieldPermissions: fieldCaps } : {}
271
- });
272
- return;
273
- }
274
- const rawData = hasFieldRestrictions ? applyPermissions(response.data) : response.data;
275
- const filteredData = rawData !== null && typeof rawData === "object" && typeof rawData.toJSON === "function" ? rawData.toJSON() : rawData;
276
- const status = response.status ?? 200;
277
- if (filteredData !== null && typeof filteredData === "object" && !Array.isArray(filteredData) && (response.meta || fieldCaps)) {
278
- reply.code(status).send({
279
- ...filteredData,
280
- ...response.meta ?? {},
281
- ...fieldCaps ? { fieldPermissions: fieldCaps } : {}
282
- });
283
- return;
284
- }
285
- reply.code(status).send(filteredData);
286
- }
287
- /**
288
- * Bridge an `IController` method (`(ctx: IRequestContext) => Promise<IControllerResponse<T>>`)
289
- * into a Fastify route handler. Translates between arc's curated request
290
- * context surface and Fastify's request/reply, and routes the envelope
291
- * through `sendControllerResponse` so the pipeline applies field
292
- * permissions, status, and meta uniformly. Internal plumbing, not a
293
- * portability layer — arc is Fastify-only.
294
- *
295
- * @example
296
- * ```typescript
297
- * const controller = new BaseController(repository);
298
- * const listHandler = createFastifyHandler(controller.list.bind(controller));
299
- * fastify.get('/products', listHandler);
300
- * ```
301
- */
302
- function createFastifyHandler(controllerMethod) {
303
- return async (req, reply) => {
304
- sendControllerResponse(reply, await controllerMethod(createRequestContext(req)), req);
305
- };
306
- }
307
- /**
308
- * Create Fastify adapters for all CRUD methods of an IController
309
- *
310
- * Returns Fastify-compatible handlers for each CRUD operation
311
- *
312
- * @example
313
- * ```typescript
314
- * const controller = new BaseController(repository);
315
- * const handlers = createCrudHandlers(controller);
316
- *
317
- * fastify.get('/', handlers.list);
318
- * fastify.get('/:id', handlers.get);
319
- * fastify.post('/', handlers.create);
320
- * fastify.patch('/:id', handlers.update);
321
- * fastify.delete('/:id', handlers.delete);
322
- * ```
323
- */
324
- function createCrudHandlers(controller) {
325
- return {
326
- list: createFastifyHandler(controller.list.bind(controller)),
327
- get: createFastifyHandler(controller.get.bind(controller)),
328
- create: createFastifyHandler(controller.create.bind(controller)),
329
- update: createFastifyHandler(controller.update.bind(controller)),
330
- delete: createFastifyHandler(controller.delete.bind(controller))
331
- };
332
- }
333
- //#endregion
334
- //#region src/core/routerShared.ts
335
- /**
336
- * Build the `arcDecorator` preHandler for a resource.
337
- *
338
- * The decorator is a closure over frozen metadata — allocated once per
339
- * resource and shared across every request. Stamps `req.arc` with the
340
- * resource's field permissions, hooks, events bus, and schema options
341
- * so `sendControllerResponse`, `BaseController.run*`, and custom
342
- * middleware can read a consistent view.
343
- *
344
- * Also populates `requestContext.resourceName` for async-context access
345
- * in code paths that can't reach `req.arc` directly (e.g. detached logger
346
- * formatters).
347
- */
348
- function buildArcDecorator(meta) {
349
- const frozen = Object.freeze({ ...meta });
350
- return async (req, _reply) => {
351
- req.arc = frozen;
352
- const store = requestContext.get();
353
- if (store) store.resourceName = frozen.resourceName;
354
- };
355
- }
356
- /**
357
- * A permission requires authentication unless it carries the `_isPublic`
358
- * marker set by `allowPublic()`. Absence of a permission is treated as
359
- * public (no auth) — matches historical CRUD behaviour.
360
- */
361
- function requiresAuthentication(permission) {
362
- if (!permission) return false;
363
- return !permission._isPublic;
364
- }
365
- /**
366
- * Pick the right Fastify auth decorator for a single-permission route:
367
- * - protected route → `fastify.authenticate` (401 on missing token)
368
- * - public route → `fastify.optionalAuthenticate` (parses token if present)
369
- *
370
- * Public routes still get optional auth so downstream multi-tenant filters
371
- * can narrow queries when a Bearer token IS supplied.
372
- */
373
- function buildAuthMiddleware(fastify, permission) {
374
- if (requiresAuthentication(permission)) return fastify.authenticate ?? null;
375
- return fastify.optionalAuthenticate ?? null;
376
- }
377
- /**
378
- * Pick the right auth decorator for a multi-permission route (Action router).
379
- *
380
- * The input is the array of resolved per-action permissions — one slot per
381
- * action, in registration order, already flattened against `globalAuth`
382
- * fallback by the caller (`actionPermissions[name] ?? globalAuth`). A slot
383
- * may be `undefined` when the action has no per-action check AND no
384
- * `globalAuth` fallback — that is "public by omission" and must be honored
385
- * here the same way `buildActionPermissionMw` honors it (by skipping the
386
- * permission evaluation entirely). If we filtered undefineds out at this
387
- * layer, a mixed endpoint like `{ ping: undefined, promote: requireRoles(...) }`
388
- * would collapse to "all protected" and 401 the public `ping` action at the
389
- * auth layer before the permission prehandler could let it through.
390
- *
391
- * Rules:
392
- * - ALL public (explicit allowPublic OR omission) → `optionalAuthenticate`
393
- * - ALL protected → `authenticate` (fail-fast)
394
- * - MIXED → `optionalAuthenticate`
395
- * (parse token if present; per-action check fails-closed when user=null)
396
- *
397
- * The mixed case was previously handled by an in-handler
398
- * `fastify.authenticate()` call that bypassed the preHandler chain; this
399
- * helper moves that logic back into the preHandler stack so the request
400
- * lifecycle is consistent across router types.
401
- */
402
- function buildAuthMiddlewareForPermissions(fastify, permissions) {
403
- if (permissions.length === 0) return fastify.optionalAuthenticate ?? null;
404
- const hasProtected = permissions.some((p) => requiresAuthentication(p));
405
- const hasPublic = permissions.some((p) => p && p._isPublic === true) || permissions.some((p) => !p);
406
- if (hasProtected && !hasPublic) return fastify.authenticate ?? null;
407
- return fastify.optionalAuthenticate ?? null;
408
- }
409
- /**
410
- * Build a PermissionContext from a Fastify request. Extracted so the CRUD
411
- * permission middleware and the dynamic action-permission check use the same
412
- * field layout — divergence here silently broke policy filters for actions.
413
- */
414
- function buildPermissionContext(req, opts) {
415
- const reqWithExtras = req;
416
- const params = req.params;
417
- return {
418
- user: reqWithExtras.user ?? null,
419
- request: req,
420
- resource: opts.resource,
421
- action: opts.action,
422
- resourceId: opts.resourceId ?? params?.id,
423
- params,
424
- data: opts.data ?? req.body
425
- };
426
- }
427
- /**
428
- * Static per-route CRUD permission gate. The permission and action are known
429
- * at route-registration time, so the gate is a plain preHandler.
430
- *
431
- * Actions use the dynamic counterpart `buildActionPermissionMw` — their
432
- * permission is resolved from `body.action` at request time.
433
- */
434
- function buildCrudPermissionMw(permissionCheck, resourceName, action) {
435
- if (!permissionCheck) return null;
436
- return async (req, reply) => {
437
- await evaluateAndApplyPermission(permissionCheck, buildPermissionContext(req, {
438
- resource: resourceName,
439
- action
440
- }), req, reply);
441
- };
442
- }
443
- /**
444
- * Dynamic per-action permission gate for the action router.
445
- *
446
- * Resolves the permission from `body.action` at request time and runs
447
- * `evaluateAndApplyPermission` from the canonical `permissionMw` slot — so
448
- * `_policyFilters` and `request.scope` are installed BEFORE `pluginMw`
449
- * (idempotency) and `routeGuards` run. Previously this check lived inside
450
- * the main action handler, which meant idempotency recorded unauthorized
451
- * requests and route guards saw unfiltered scope — the very divergence
452
- * routerShared exists to prevent.
453
- *
454
- * Also acts as a defensive fallback for invalid action names — the
455
- * `oneOf` body schema normally rejects these at AJV validation, but
456
- * hosts that disable schema validation still get a 400 here.
457
- */
458
- function buildActionPermissionMw(actionEnum, actionPermissions, globalAuth, resourceName) {
459
- const enumSet = new Set(actionEnum);
460
- const validActions = [...actionEnum];
461
- return async (req, reply) => {
462
- const body = req.body ?? {};
463
- const action = body.action;
464
- if (!action || !enumSet.has(action)) throw createError(400, `Invalid action '${action ?? ""}'. Valid actions: ${validActions.join(", ")}`, { validActions });
465
- const permissionCheck = actionPermissions[action] ?? globalAuth;
466
- if (!permissionCheck) return;
467
- const { action: _discard, ...data } = body;
468
- const params = req.params;
469
- await evaluateAndApplyPermission(permissionCheck, buildPermissionContext(req, {
470
- resource: resourceName,
471
- action,
472
- resourceId: params?.id,
473
- data
474
- }), req, reply, { defaultDenialMessage: (user) => user ? `Permission denied for '${action}'` : "Authentication required" });
475
- };
476
- }
477
- /**
478
- * Resolve pipeline steps for a specific operation.
479
- * Flat-array config applies to every op; map config applies per-op.
480
- */
481
- function resolvePipelineSteps(pipeline, operation) {
482
- if (!pipeline) return [];
483
- if (Array.isArray(pipeline)) return pipeline;
484
- return pipeline[operation] ?? [];
485
- }
486
- /**
487
- * Wrap a controller method (one that takes `IRequestContext` and returns
488
- * `IControllerResponse<T>`) with pipeline execution. Used by CRUD ops and
489
- * string-handler custom routes.
490
- */
491
- function buildPipelineHandler(controllerMethod, steps, operation, resourceName) {
492
- return async (req, reply) => {
493
- sendControllerResponse(reply, await executePipeline(steps, {
494
- ...createRequestContext(req),
495
- resource: resourceName,
496
- operation
497
- }, (ctx) => controllerMethod(ctx), operation), req);
498
- };
499
- }
500
- /**
501
- * Wrap an action handler (one that takes `(id, data, req)` and returns a raw
502
- * result) with pipeline execution. Returns a function that produces a full
503
- * `IControllerResponse<unknown>` — the action router feeds this directly into
504
- * `sendControllerResponse`, so field masking, custom status codes, `meta`,
505
- * `details`, and structured error codes from pipeline interceptors flow
506
- * through to the client unchanged.
507
- *
508
- * CRUD and actions now share the same parity invariant: a pipeline that
509
- * returns `{ success: false, status: 422, error, details, meta }` reaches the
510
- * client with all four fields intact. Previously the action path stringified
511
- * failures into a generic `Error` and dropped everything except `statusCode`.
512
- *
513
- * Handler throws still bubble out — the caller's try/catch handles `onError`
514
- * shaping and the generic `ACTION_FAILED` fallback.
515
- */
516
- function buildActionPipelineHandler(handler, steps, operation, resourceName) {
517
- if (steps.length === 0) return async (id, data, req) => ({
518
- status: 200,
519
- data: await handler(id, data, req)
520
- });
521
- return async (id, data, req) => {
522
- return executePipeline(steps, {
523
- ...createRequestContext(req),
524
- resource: resourceName,
525
- operation
526
- }, async (_ctx) => ({
527
- status: 200,
528
- data: await handler(id, data, req)
529
- }), operation);
530
- };
531
- }
532
- /**
533
- * Build the `config` object for Fastify route options so
534
- * @fastify/rate-limit picks up per-route overrides.
535
- *
536
- * - `undefined` → no override (inherits instance config)
537
- * - `false` → explicitly disable rate limiting
538
- * - `{ max, timeWindow }` → apply that limit
539
- */
540
- function buildRateLimitConfig(rateLimit) {
541
- if (rateLimit === void 0) return void 0;
542
- if (rateLimit === false) return { rateLimit: false };
543
- return { rateLimit: {
544
- max: rateLimit.max,
545
- timeWindow: rateLimit.timeWindow
546
- } };
547
- }
548
- /**
549
- * Pick the request-lifecycle plugin middleware for an HTTP method:
550
- * - GET / HEAD → response cache (if present)
551
- * - POST / PUT / PATCH → idempotency (if present)
552
- * - DELETE → none
553
- *
554
- * Either field may be `null` if the corresponding plugin wasn't registered.
555
- */
556
- function selectPluginMw(method, mws) {
557
- const upper = method.toUpperCase();
558
- if (upper === "GET" || upper === "HEAD") return mws.cacheMw;
559
- if (upper === "POST" || upper === "PUT" || upper === "PATCH") return mws.idempotencyMw;
560
- return null;
561
- }
562
- /**
563
- * Resolve the default cache/idempotency middlewares for a resource.
564
- *
565
- * Skips response-cache when the resource has QueryCache active — QueryCache
566
- * handles caching at the controller level with SWR, so the HTTP-level
567
- * response-cache would double-cache.
568
- */
569
- function resolveRouterPluginMw(fastify, resourceHasQueryCache) {
570
- return {
571
- cacheMw: !resourceHasQueryCache && fastify.hasDecorator("responseCache") ? fastify.responseCache.middleware : null,
572
- idempotencyMw: fastify.hasDecorator("idempotency") ? fastify.idempotency.middleware : null
573
- };
574
- }
575
- /**
576
- * Compose preHandler[] in the canonical order. Every null/undefined entry is
577
- * dropped. Keeps CRUD and Action routers from accidentally ordering the same
578
- * ingredients differently (regression risk: cache before auth → user-scoped
579
- * cache keys leak across users).
580
- *
581
- * Canonical order:
582
- * preAuth → arcDecorator → authMw → permissionMw → pluginMw → routeGuards → customMws
583
- */
584
- function buildPreHandlerChain(parts) {
585
- return [
586
- ...parts.preAuth ?? [],
587
- parts.arcDecorator,
588
- parts.authMw ?? null,
589
- parts.permissionMw ?? null,
590
- parts.pluginMw ?? null,
591
- ...parts.routeGuards ?? [],
592
- ...parts.customMws ?? []
593
- ].filter(Boolean);
594
- }
595
- /**
596
- * `RouteDefinition.preHandler` accepts two shapes:
597
- *
598
- * 1. **Array form** — `RouteHandlerMethod[]`. Used directly.
599
- * 2. **Factory form** — `(fastify) => RouteHandlerMethod[]`. Called once at
600
- * route-registration time with the Fastify instance, so handlers can
601
- * capture decorators (`fastify.authenticate`, `fastify.events`, etc.)
602
- * that aren't on the request.
603
- *
604
- * The two forms are equally idiomatic, but the discrimination is by
605
- * `typeof preHandler === "function"`. Single-function shapes such as
606
- * `multipartBody({...})` (a `RouteHandlerMethod`) **structurally satisfy
607
- * the factory branch** at the call site, then fail with a cryptic
608
- * `Cannot read properties of undefined (reading 'content-type')` once the
609
- * handler runs with `fastify` in the request slot.
610
- *
611
- * This resolver:
612
- * 1. Distinguishes the two valid shapes.
613
- * 2. Validates the factory's RETURN — must be an array of functions.
614
- * 3. Throws an actionable error pointing at the route + the fix when
615
- * a single `RouteHandlerMethod` was passed instead of an array, OR
616
- * when a factory returned the wrong shape.
617
- *
618
- * The error message names the route (`{method} {path}`) and the
619
- * canonical fix (`preHandler: [yourHandler]`) so the failure mode is
620
- * obvious instead of debug-archaeology.
621
- *
622
- * @param preHandler The `route.preHandler` value (any of the valid shapes
623
- * plus the common bare-handler mistake).
624
- * @param fastify Passed to factory-form preHandlers.
625
- * @param routeId `"GET /todos/:id/attach"` (or similar) — used in the
626
- * error message so a multi-route file points at the
627
- * actual offender.
628
- */
629
- function resolveRoutePreHandlers(preHandler, fastify, routeId) {
630
- if (preHandler === void 0 || preHandler === null) return [];
631
- if (Array.isArray(preHandler)) return preHandler.filter((h) => typeof h === "function");
632
- if (typeof preHandler === "function") {
633
- let result;
634
- try {
635
- result = preHandler(fastify);
636
- } catch (err) {
637
- const msg = err instanceof Error ? err.message : String(err);
638
- throw new TypeError(`Route ${routeId}: preHandler factory threw during route registration: ${msg}.\nIf you intended to pass a single handler (e.g. \`multipartBody({...})\`), wrap it in an array: \`preHandler: [yourHandler]\`. The factory form is \`(fastify) => RouteHandlerMethod[]\` — it must return an array.`, { cause: err instanceof Error ? err : void 0 });
639
- }
640
- if (result && typeof result.then === "function") result.catch(() => void 0);
641
- if (!Array.isArray(result)) throw new TypeError(`Route ${routeId}: preHandler factory must return an array of handlers, got ${describeValue(result)}.\nCommon cause: passing a single \`RouteHandlerMethod\` (e.g. \`multipartBody({...})\`) where an array was expected. Wrap it: \`preHandler: [yourHandler]\`. The factory form \`(fastify) => RouteHandlerMethod[]\` is for cases that need the Fastify instance — e.g. \`(fastify) => [fastify.authenticate, myHandler]\`.`);
642
- return result.filter((h) => typeof h === "function");
643
- }
644
- throw new TypeError(`Route ${routeId}: preHandler must be an array of handlers OR a factory \`(fastify) => RouteHandlerMethod[]\`. Got ${describeValue(preHandler)}.`);
645
- }
646
- function describeValue(v) {
647
- if (v === null) return "null";
648
- if (v === void 0) return "undefined";
649
- if (typeof v === "function") return "a function (single handler — wrap in array)";
650
- if (Array.isArray(v)) return `an array of length ${v.length}`;
651
- return `${typeof v} (${JSON.stringify(v).slice(0, 80)})`;
652
- }
653
- /**
654
- * Build a preHandler that enforces field-write permissions on the request body.
655
- *
656
- * Auto-CRUD's create/update routes get this for free via `BodySanitizer.sanitize()`
657
- * inside `BaseController`. Custom routes (`config.routes`, presets, action endpoints)
658
- * never went through that path — so a host that declared
659
- * `fields: { role: fields.writableBy(['admin']) }` and a custom
660
- * `POST /users/promote` happily accepted `{ role: 'admin' }` from any caller.
661
- *
662
- * This preHandler closes that gap. It applies ONLY the `applyFieldWritePermissions`
663
- * step — no `SYSTEM_FIELDS` strip, no `fieldRules` strip — because custom-route
664
- * bodies don't necessarily mirror the resource's adapter shape, and we'd
665
- * generate surprising 403s by enforcing rules tied to that shape.
666
- *
667
- * Returns `null` when the resource has no field permissions configured — the
668
- * caller should then skip wiring it into the preHandler chain.
669
- */
670
- function buildFieldWritePreHandler(fieldPermissions, policy = DEFAULT_FIELD_WRITE_DENIAL_POLICY) {
671
- if (!fieldPermissions || Object.keys(fieldPermissions).length === 0) return null;
672
- return async (request, _reply) => {
673
- const body = request.body;
674
- if (!body || typeof body !== "object" || Array.isArray(body)) return;
675
- const scope = request.scope ?? PUBLIC_SCOPE;
676
- if (isElevated(scope)) return;
677
- const { body: filtered, deniedFields } = applyFieldWritePermissions(body, fieldPermissions, resolveEffectiveRoles(getUserRoles(request.user), isMember(scope) ? scope.orgRoles : []));
678
- if (deniedFields.length > 0 && policy === "reject") throw new ForbiddenError(`Not permitted to write field${deniedFields.length === 1 ? "" : "s"}: ${deniedFields.join(", ")}`);
679
- request.body = filtered;
680
- };
681
- }
682
- /**
683
- * True for HTTP methods that carry a request body and should run the
684
- * field-write preHandler. GET/DELETE/HEAD/OPTIONS skip enforcement.
685
- */
686
- function methodCarriesBody(method) {
687
- const m = method.toUpperCase();
688
- return m === "POST" || m === "PUT" || m === "PATCH";
689
- }
690
- /**
691
- * Register a route with friendly handling of `FST_ERR_DUPLICATED_ROUTE`.
692
- *
693
- * Boot-time `validateRouteCrudCollisions()` catches custom-route → auto-CRUD
694
- * overlaps before Fastify ever runs, but a few cases still slip through to
695
- * Fastify itself: two presets contributing the same `routes` entry, hosts
696
- * mounting two resources at the same prefix, custom routes that share a
697
- * URL with another plugin. Fastify's default message ("Method 'GET'
698
- * already declared for route '/'") gives no hint at the resource-shaped
699
- * fix — `disabledRoutes: ['list']` — so wrap it with one.
700
- */
701
- function tryRegisterRoute(fastify, opts, context) {
702
- try {
703
- fastify.route(opts);
704
- } catch (err) {
705
- const fastifyErr = err;
706
- if (fastifyErr?.code !== "FST_ERR_DUPLICATED_ROUTE") throw err;
707
- const method = opts.method;
708
- const url = opts.url;
709
- const opHint = context.op ? ` (op: ${context.op})` : "";
710
- const fixHint = context.op && [
711
- "list",
712
- "get",
713
- "create",
714
- "update",
715
- "delete"
716
- ].includes(context.op) ? ` Suppress the auto-CRUD route with \`disabledRoutes: ['${context.op}']\`.` : " If a custom route in `routes:` collides with auto-CRUD, add `disabledRoutes: ['list' | 'get' | 'create' | 'update' | 'delete']` to `defineResource()` to suppress the matching auto-CRUD route.";
717
- const enhanced = /* @__PURE__ */ new Error(`[arc] Duplicate route ${method} ${url} on resource "${context.resourceName}"${opHint}.${fixHint} Original: ${fastifyErr.message ?? "FST_ERR_DUPLICATED_ROUTE"}`);
718
- enhanced.code = "FST_ERR_DUPLICATED_ROUTE";
719
- enhanced.cause = err;
720
- throw enhanced;
721
- }
722
- }
723
- //#endregion
724
- export { buildRequestScopeProjection as C, sendControllerResponse as S, createCrudHandlers as _, buildAuthMiddlewareForPermissions as a, getControllerContext as b, buildPipelineHandler as c, methodCarriesBody as d, resolvePipelineSteps as f, tryRegisterRoute as g, selectPluginMw as h, buildAuthMiddleware as i, buildPreHandlerChain as l, resolveRouterPluginMw as m, buildActionPipelineHandler as n, buildCrudPermissionMw as o, resolveRoutePreHandlers as p, buildArcDecorator as r, buildFieldWritePreHandler as s, buildActionPermissionMw as t, buildRateLimitConfig as u, createFastifyHandler as v, BodySanitizer as w, getControllerScope as x, createRequestContext as y };