@classytic/arc 2.10.8 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/{BaseController-DVNKvoX4.mjs → BaseController-JNV08qOT.mjs} +480 -442
  2. package/dist/{queryCachePlugin-Dumka73q.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
  6. package/dist/audit/index.d.mts +1 -1
  7. package/dist/auth/index.d.mts +1 -1
  8. package/dist/auth/index.mjs +5 -5
  9. package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
  10. package/dist/cache/index.d.mts +3 -2
  11. package/dist/cache/index.mjs +3 -3
  12. package/dist/cli/commands/docs.mjs +2 -2
  13. package/dist/cli/commands/generate.mjs +37 -27
  14. package/dist/cli/commands/init.mjs +46 -33
  15. package/dist/cli/commands/introspect.mjs +1 -1
  16. package/dist/context/index.mjs +1 -1
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -3
  19. package/dist/core-DXdSSFW-.mjs +1037 -0
  20. package/dist/createActionRouter-BwaSM0No.mjs +166 -0
  21. package/dist/{createApp-BwnEAO2h.mjs → createApp-DvNYEhpb.mjs} +75 -27
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-Dci0AYLT.mjs → elevation-DOFoxoDs.mjs} +1 -1
  25. package/dist/{errorHandler-CSxe7KIM.mjs → errorHandler-BQm8ZxTK.mjs} +1 -1
  26. package/dist/{eventPlugin-ByU4Cv0e.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
  27. package/dist/events/index.d.mts +3 -3
  28. package/dist/events/index.mjs +2 -2
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/factory/index.d.mts +1 -1
  31. package/dist/factory/index.mjs +2 -2
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/hooks/index.mjs +1 -1
  34. package/dist/idempotency/index.d.mts +3 -3
  35. package/dist/idempotency/index.mjs +1 -1
  36. package/dist/idempotency/redis.d.mts +1 -1
  37. package/dist/{index-C_Noptz-.d.mts → index-BYCqHCVu.d.mts} +2 -2
  38. package/dist/{index-BGbpGVyM.d.mts → index-Cm0vUrr_.d.mts} +699 -494
  39. package/dist/{index-BziRPS4H.d.mts → index-DAushRTt.d.mts} +29 -10
  40. package/dist/index-DsJ1MNfC.d.mts +1179 -0
  41. package/dist/{index-EqQN6p0W.d.mts → index-t8pLpPFW.d.mts} +11 -8
  42. package/dist/index.d.mts +6 -38
  43. package/dist/index.mjs +9 -9
  44. package/dist/integrations/event-gateway.d.mts +1 -1
  45. package/dist/integrations/event-gateway.mjs +1 -1
  46. package/dist/integrations/index.d.mts +2 -2
  47. package/dist/integrations/mcp/index.d.mts +2 -2
  48. package/dist/integrations/mcp/index.mjs +1 -1
  49. package/dist/integrations/mcp/testing.d.mts +1 -1
  50. package/dist/integrations/mcp/testing.mjs +1 -1
  51. package/dist/integrations/streamline.d.mts +46 -5
  52. package/dist/integrations/streamline.mjs +50 -21
  53. package/dist/integrations/websocket-redis.d.mts +1 -1
  54. package/dist/integrations/websocket.d.mts +2 -154
  55. package/dist/integrations/websocket.mjs +292 -224
  56. package/dist/{keys-nWQGUTu1.mjs → keys-CARyUjiR.mjs} +2 -0
  57. package/dist/{loadResources-Bksk8ydA.mjs → loadResources-YNwKHvRA.mjs} +3 -1
  58. package/dist/middleware/index.d.mts +1 -1
  59. package/dist/middleware/index.mjs +1 -1
  60. package/dist/{openapi-DpNpqBmo.mjs → openapi-C0L9ar7m.mjs} +4 -4
  61. package/dist/org/index.d.mts +1 -1
  62. package/dist/permissions/index.d.mts +1 -1
  63. package/dist/permissions/index.mjs +2 -4
  64. package/dist/{permissions-wkqRwicB.mjs → permissions-B4vU9L0Q.mjs} +221 -3
  65. package/dist/{pipe-CGJxqDGx.mjs → pipe-DVoIheVC.mjs} +1 -1
  66. package/dist/pipeline/index.d.mts +1 -1
  67. package/dist/pipeline/index.mjs +1 -1
  68. package/dist/plugins/index.d.mts +4 -4
  69. package/dist/plugins/index.mjs +10 -10
  70. package/dist/plugins/response-cache.mjs +1 -1
  71. package/dist/plugins/tracing-entry.d.mts +1 -1
  72. package/dist/plugins/tracing-entry.mjs +42 -24
  73. package/dist/presets/filesUpload.d.mts +1 -1
  74. package/dist/presets/filesUpload.mjs +3 -3
  75. package/dist/presets/index.d.mts +1 -1
  76. package/dist/presets/index.mjs +1 -1
  77. package/dist/presets/multiTenant.d.mts +1 -1
  78. package/dist/presets/multiTenant.mjs +6 -0
  79. package/dist/presets/search.d.mts +1 -1
  80. package/dist/presets/search.mjs +1 -1
  81. package/dist/{presets-CrwOvuXI.mjs → presets-k604Lj99.mjs} +1 -1
  82. package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
  83. package/dist/{queryCachePlugin-ChLNZvFT.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
  84. package/dist/{redis-MXLp1oOf.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
  85. package/dist/registry/index.d.mts +1 -1
  86. package/dist/registry/index.mjs +2 -2
  87. package/dist/{resourceToTools-BhF3JV5p.mjs → resourceToTools--okX6QBr.mjs} +534 -420
  88. package/dist/routerShared-DeESFp4a.mjs +515 -0
  89. package/dist/schemaIR-BlG9bY7v.mjs +137 -0
  90. package/dist/scope/index.mjs +2 -2
  91. package/dist/testing/index.d.mts +367 -711
  92. package/dist/testing/index.mjs +637 -1434
  93. package/dist/{tracing-xqXzWeaf.d.mts → tracing-DokiEsuz.d.mts} +9 -4
  94. package/dist/types/index.d.mts +3 -3
  95. package/dist/types/index.mjs +1 -3
  96. package/dist/{types-CVdgPXBW.d.mts → types-CgikqKAj.d.mts} +118 -19
  97. package/dist/{types-CVKBssX5.d.mts → types-D9NqiYIw.d.mts} +1 -1
  98. package/dist/utils/index.d.mts +2 -968
  99. package/dist/utils/index.mjs +5 -6
  100. package/dist/utils-D3Yxnrwr.mjs +1639 -0
  101. package/dist/websocket-CyJ1VIFI.d.mts +186 -0
  102. package/package.json +7 -5
  103. package/skills/arc/SKILL.md +123 -38
  104. package/skills/arc/references/testing.md +212 -183
  105. package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
  106. package/dist/core-3MWJosCH.mjs +0 -1459
  107. package/dist/createActionRouter-C8UUB3Px.mjs +0 -249
  108. package/dist/errors-BI8kEKsO.d.mts +0 -140
  109. package/dist/fields-CTMWOUDt.mjs +0 -126
  110. package/dist/queryParser-NR__Qiju.mjs +0 -419
  111. package/dist/types-CDnTEpga.mjs +0 -27
  112. package/dist/utils-LMwVidKy.mjs +0 -947
  113. /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
  114. /package/dist/{ResourceRegistry-CcN2LVrc.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
  115. /package/dist/{actionPermissions-TUVR3uiZ.mjs → actionPermissions-C8YYU92K.mjs} +0 -0
  116. /package/dist/{caching-3h93rkJM.mjs → caching-CheW3m-S.mjs} +0 -0
  117. /package/dist/{errorHandler-2ii4RIYr.d.mts → errorHandler-Co3lnVmJ.d.mts} +0 -0
  118. /package/dist/{errors-BqdUDja_.mjs → errors-D5c-5BJL.mjs} +0 -0
  119. /package/dist/{eventPlugin-D1ThQ1Pp.d.mts → eventPlugin-CUNjYYRY.d.mts} +0 -0
  120. /package/dist/{interface-B-pe8fhj.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  121. /package/dist/{interface-yhyb_pLY.d.mts → interface-Da0r7Lna.d.mts} +0 -0
  122. /package/dist/{memory-DqI-449b.mjs → memory-DikHSvWa.mjs} +0 -0
  123. /package/dist/{metrics-TuOmguhi.mjs → metrics-Csh4nsvv.mjs} +0 -0
  124. /package/dist/{multipartBody-CUQGVlM_.mjs → multipartBody-CvTR1Un6.mjs} +0 -0
  125. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-BneOJkpi.mjs} +0 -0
  126. /package/dist/{redis-stream-bkO88VHx.d.mts → redis-stream-CM8TXTix.d.mts} +0 -0
  127. /package/dist/{registry-B0Wl7uVV.mjs → registry-D63ee7fl.mjs} +0 -0
  128. /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
  129. /package/dist/{requestContext-C38GskNt.mjs → requestContext-CfRkaxwf.mjs} +0 -0
  130. /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
  131. /package/dist/{sse-D8UeDwis.mjs → sse-V7aXc3bW.mjs} +0 -0
  132. /package/dist/{store-helpers-DYYUQbQN.mjs → store-helpers-BhrzxvyQ.mjs} +0 -0
  133. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  134. /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
  135. /package/dist/{versioning-B6mimogM.mjs → versioning-CGPjkqAg.mjs} +0 -0
  136. /package/dist/{versioning-CeUXHfjw.d.mts → versioning-M9lNLhO8.d.mts} +0 -0
@@ -0,0 +1,515 @@
1
+ import { _ as isElevated, n as PUBLIC_SCOPE, o as getOrgId, p as getUserId, v as isMember } from "./types-AOD8fxIw.mjs";
2
+ import { P as resolveEffectiveRoles, j as applyFieldReadPermissions, k as evaluateAndApplyPermission } from "./permissions-B4vU9L0Q.mjs";
3
+ import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
4
+ import { t as requestContext } from "./requestContext-CfRkaxwf.mjs";
5
+ import { t as executePipeline } from "./pipe-DVoIheVC.mjs";
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
+ * Create IRequestContext from Fastify request
57
+ *
58
+ * Extracts framework-agnostic context from Fastify-specific request object
59
+ */
60
+ function createRequestContext(req) {
61
+ const reqWithExtras = req;
62
+ const requestContext = reqWithExtras.context ?? {};
63
+ const srv = req.server;
64
+ const serverAccessor = {
65
+ events: srv && "events" in srv ? srv.events : void 0,
66
+ audit: srv && "audit" in srv ? srv.audit : void 0,
67
+ queryCache: srv && "queryCache" in srv ? srv.queryCache : void 0,
68
+ log: req.log
69
+ };
70
+ const rawScope = reqWithExtras.scope;
71
+ const scopeProjection = buildRequestScopeProjection(rawScope);
72
+ return {
73
+ query: reqWithExtras.query ?? {},
74
+ body: reqWithExtras.body ?? {},
75
+ params: reqWithExtras.params ?? {},
76
+ headers: reqWithExtras.headers,
77
+ user: reqWithExtras.user ? (() => {
78
+ const user = reqWithExtras.user;
79
+ const rawId = user._id ?? user.id;
80
+ const normalizedId = rawId ? String(rawId) : void 0;
81
+ return {
82
+ ...user,
83
+ id: normalizedId,
84
+ _id: normalizedId
85
+ };
86
+ })() : null,
87
+ context: requestContext,
88
+ scope: scopeProjection,
89
+ metadata: {
90
+ ...reqWithExtras.context,
91
+ arc: reqWithExtras.arc,
92
+ _scope: rawScope,
93
+ _ownershipCheck: reqWithExtras._ownershipCheck,
94
+ _policyFilters: reqWithExtras._policyFilters ?? {},
95
+ log: reqWithExtras.log
96
+ },
97
+ server: serverAccessor
98
+ };
99
+ }
100
+ /**
101
+ * Get typed auth context from an IRequestContext.
102
+ * Use this in controller overrides to access request context.
103
+ *
104
+ * For org scope, use `getControllerScope(req)` instead.
105
+ */
106
+ function getControllerContext(req) {
107
+ return req.context ?? req.metadata ?? {};
108
+ }
109
+ /**
110
+ * Get request scope from an IRequestContext.
111
+ * Returns the RequestScope set by auth adapters.
112
+ */
113
+ function getControllerScope(req) {
114
+ return req.metadata?._scope ?? PUBLIC_SCOPE;
115
+ }
116
+ /**
117
+ * Compute per-field capability metadata for the current user.
118
+ * Only includes fields that have restrictions — unrestricted fields
119
+ * are omitted (frontend defaults to { readable: true, writable: true }).
120
+ */
121
+ function computeFieldCapabilities(fieldPerms, effectiveRoles) {
122
+ const caps = {};
123
+ for (const [field, perm] of Object.entries(fieldPerms)) {
124
+ let readable = true;
125
+ let writable = true;
126
+ switch (perm._type) {
127
+ case "hidden":
128
+ readable = false;
129
+ writable = false;
130
+ break;
131
+ case "visibleTo":
132
+ readable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
133
+ break;
134
+ case "writableBy":
135
+ writable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
136
+ break;
137
+ }
138
+ caps[field] = {
139
+ readable,
140
+ writable
141
+ };
142
+ }
143
+ return caps;
144
+ }
145
+ /**
146
+ * Send IControllerResponse via Fastify reply
147
+ *
148
+ * Converts framework-agnostic response to Fastify response
149
+ * Applies field masking if specified in request
150
+ */
151
+ function sendControllerResponse(reply, response, request) {
152
+ const reqWithExtras = request;
153
+ const fieldMaskConfig = reqWithExtras?.fieldMask;
154
+ const arcMeta = reqWithExtras?.arc;
155
+ const scope = reqWithExtras?.scope ?? PUBLIC_SCOPE;
156
+ const fieldPerms = isElevated(scope) ? void 0 : arcMeta?.fields;
157
+ const effectiveRoles = fieldPerms ? resolveEffectiveRoles(getUserRoles(reqWithExtras?.user), isMember(scope) ? scope.orgRoles : []) : [];
158
+ const fieldCaps = fieldPerms ? computeFieldCapabilities(fieldPerms, effectiveRoles) : void 0;
159
+ const hasFieldRestrictions = !!(fieldMaskConfig || fieldPerms);
160
+ /** Apply both field mask and field-level permissions to a data item */
161
+ const applyPermissions = (data) => {
162
+ let result = fieldMaskConfig ? applyFieldMask(data, fieldMaskConfig) : data;
163
+ if (fieldPerms && result && typeof result === "object") if (Array.isArray(result)) result = result.map((item) => applyFieldReadPermissions(item, fieldPerms, effectiveRoles));
164
+ else result = applyFieldReadPermissions(result, fieldPerms, effectiveRoles);
165
+ return result;
166
+ };
167
+ if (response.headers) for (const [key, value] of Object.entries(response.headers)) reply.header(key, value);
168
+ if (response.success && response.data && typeof response.data === "object" && "docs" in response.data) {
169
+ const paginatedData = response.data;
170
+ const filteredDocs = hasFieldRestrictions ? applyPermissions(paginatedData.docs) : paginatedData.docs;
171
+ reply.code(response.status ?? 200).send({
172
+ success: true,
173
+ docs: filteredDocs,
174
+ page: paginatedData.page,
175
+ limit: paginatedData.limit,
176
+ total: paginatedData.total,
177
+ pages: paginatedData.pages,
178
+ hasNext: paginatedData.hasNext,
179
+ hasPrev: paginatedData.hasPrev,
180
+ ...response.meta ?? {},
181
+ ...fieldCaps ? { fieldPermissions: fieldCaps } : {}
182
+ });
183
+ return;
184
+ }
185
+ const filteredData = hasFieldRestrictions ? applyPermissions(response.data) : response.data;
186
+ reply.code(response.status ?? (response.success ? 200 : 400)).send({
187
+ success: response.success,
188
+ data: filteredData,
189
+ error: response.error,
190
+ details: response.details,
191
+ ...response.meta ?? {},
192
+ ...fieldCaps ? { fieldPermissions: fieldCaps } : {}
193
+ });
194
+ }
195
+ /**
196
+ * Create Fastify route handler from IController method
197
+ *
198
+ * Wraps framework-agnostic controller method in Fastify-specific handler
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * const controller = new BaseController(repository);
203
+ *
204
+ * // Create Fastify handler
205
+ * const listHandler = createFastifyHandler(controller.list.bind(controller));
206
+ *
207
+ * // Register route
208
+ * fastify.get('/products', listHandler);
209
+ * ```
210
+ */
211
+ function createFastifyHandler(controllerMethod) {
212
+ return async (req, reply) => {
213
+ sendControllerResponse(reply, await controllerMethod(createRequestContext(req)), req);
214
+ };
215
+ }
216
+ /**
217
+ * Create Fastify adapters for all CRUD methods of an IController
218
+ *
219
+ * Returns Fastify-compatible handlers for each CRUD operation
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * const controller = new BaseController(repository);
224
+ * const handlers = createCrudHandlers(controller);
225
+ *
226
+ * fastify.get('/', handlers.list);
227
+ * fastify.get('/:id', handlers.get);
228
+ * fastify.post('/', handlers.create);
229
+ * fastify.patch('/:id', handlers.update);
230
+ * fastify.delete('/:id', handlers.delete);
231
+ * ```
232
+ */
233
+ function createCrudHandlers(controller) {
234
+ return {
235
+ list: createFastifyHandler(controller.list.bind(controller)),
236
+ get: createFastifyHandler(controller.get.bind(controller)),
237
+ create: createFastifyHandler(controller.create.bind(controller)),
238
+ update: createFastifyHandler(controller.update.bind(controller)),
239
+ delete: createFastifyHandler(controller.delete.bind(controller))
240
+ };
241
+ }
242
+ //#endregion
243
+ //#region src/core/routerShared.ts
244
+ /**
245
+ * Build the `arcDecorator` preHandler for a resource.
246
+ *
247
+ * The decorator is a closure over frozen metadata — allocated once per
248
+ * resource and shared across every request. Stamps `req.arc` with the
249
+ * resource's field permissions, hooks, events bus, and schema options
250
+ * so `sendControllerResponse`, `BaseController.run*`, and custom
251
+ * middleware can read a consistent view.
252
+ *
253
+ * Also populates `requestContext.resourceName` for async-context access
254
+ * in code paths that can't reach `req.arc` directly (e.g. detached logger
255
+ * formatters).
256
+ */
257
+ function buildArcDecorator(meta) {
258
+ const frozen = Object.freeze({ ...meta });
259
+ return async (req, _reply) => {
260
+ req.arc = frozen;
261
+ const store = requestContext.get();
262
+ if (store) store.resourceName = frozen.resourceName;
263
+ };
264
+ }
265
+ /**
266
+ * A permission requires authentication unless it carries the `_isPublic`
267
+ * marker set by `allowPublic()`. Absence of a permission is treated as
268
+ * public (no auth) — matches historical CRUD behaviour.
269
+ */
270
+ function requiresAuthentication(permission) {
271
+ if (!permission) return false;
272
+ return !permission._isPublic;
273
+ }
274
+ /**
275
+ * Pick the right Fastify auth decorator for a single-permission route:
276
+ * - protected route → `fastify.authenticate` (401 on missing token)
277
+ * - public route → `fastify.optionalAuthenticate` (parses token if present)
278
+ *
279
+ * Public routes still get optional auth so downstream multi-tenant filters
280
+ * can narrow queries when a Bearer token IS supplied.
281
+ */
282
+ function buildAuthMiddleware(fastify, permission) {
283
+ if (requiresAuthentication(permission)) return fastify.authenticate ?? null;
284
+ return fastify.optionalAuthenticate ?? null;
285
+ }
286
+ /**
287
+ * Pick the right auth decorator for a multi-permission route (Action router).
288
+ *
289
+ * The input is the array of resolved per-action permissions — one slot per
290
+ * action, in registration order, already flattened against `globalAuth`
291
+ * fallback by the caller (`actionPermissions[name] ?? globalAuth`). A slot
292
+ * may be `undefined` when the action has no per-action check AND no
293
+ * `globalAuth` fallback — that is "public by omission" and must be honored
294
+ * here the same way `buildActionPermissionMw` honors it (by skipping the
295
+ * permission evaluation entirely). If we filtered undefineds out at this
296
+ * layer, a mixed endpoint like `{ ping: undefined, promote: requireRoles(...) }`
297
+ * would collapse to "all protected" and 401 the public `ping` action at the
298
+ * auth layer before the permission prehandler could let it through.
299
+ *
300
+ * Rules:
301
+ * - ALL public (explicit allowPublic OR omission) → `optionalAuthenticate`
302
+ * - ALL protected → `authenticate` (fail-fast)
303
+ * - MIXED → `optionalAuthenticate`
304
+ * (parse token if present; per-action check fails-closed when user=null)
305
+ *
306
+ * The mixed case was previously handled by an in-handler
307
+ * `fastify.authenticate()` call that bypassed the preHandler chain; this
308
+ * helper moves that logic back into the preHandler stack so the request
309
+ * lifecycle is consistent across router types.
310
+ */
311
+ function buildAuthMiddlewareForPermissions(fastify, permissions) {
312
+ if (permissions.length === 0) return fastify.optionalAuthenticate ?? null;
313
+ const hasProtected = permissions.some((p) => requiresAuthentication(p));
314
+ const hasPublic = permissions.some((p) => p && p._isPublic === true) || permissions.some((p) => !p);
315
+ if (hasProtected && !hasPublic) return fastify.authenticate ?? null;
316
+ return fastify.optionalAuthenticate ?? null;
317
+ }
318
+ /**
319
+ * Build a PermissionContext from a Fastify request. Extracted so the CRUD
320
+ * permission middleware and the dynamic action-permission check use the same
321
+ * field layout — divergence here silently broke policy filters for actions.
322
+ */
323
+ function buildPermissionContext(req, opts) {
324
+ const reqWithExtras = req;
325
+ const params = req.params;
326
+ return {
327
+ user: reqWithExtras.user ?? null,
328
+ request: req,
329
+ resource: opts.resource,
330
+ action: opts.action,
331
+ resourceId: opts.resourceId ?? params?.id,
332
+ params,
333
+ data: opts.data ?? req.body
334
+ };
335
+ }
336
+ /**
337
+ * Static per-route CRUD permission gate. The permission and action are known
338
+ * at route-registration time, so the gate is a plain preHandler.
339
+ *
340
+ * Actions use the dynamic counterpart `buildActionPermissionMw` — their
341
+ * permission is resolved from `body.action` at request time.
342
+ */
343
+ function buildCrudPermissionMw(permissionCheck, resourceName, action) {
344
+ if (!permissionCheck) return null;
345
+ return async (req, reply) => {
346
+ await evaluateAndApplyPermission(permissionCheck, buildPermissionContext(req, {
347
+ resource: resourceName,
348
+ action
349
+ }), req, reply);
350
+ };
351
+ }
352
+ /**
353
+ * Dynamic per-action permission gate for the action router.
354
+ *
355
+ * Resolves the permission from `body.action` at request time and runs
356
+ * `evaluateAndApplyPermission` from the canonical `permissionMw` slot — so
357
+ * `_policyFilters` and `request.scope` are installed BEFORE `pluginMw`
358
+ * (idempotency) and `routeGuards` run. Previously this check lived inside
359
+ * the main action handler, which meant idempotency recorded unauthorized
360
+ * requests and route guards saw unfiltered scope — the very divergence
361
+ * routerShared exists to prevent.
362
+ *
363
+ * Also acts as a defensive fallback for invalid action names — the
364
+ * `oneOf` body schema normally rejects these at AJV validation, but
365
+ * hosts that disable schema validation still get a 400 here.
366
+ */
367
+ function buildActionPermissionMw(actionEnum, actionPermissions, globalAuth, resourceName) {
368
+ const enumSet = new Set(actionEnum);
369
+ const validActions = [...actionEnum];
370
+ return async (req, reply) => {
371
+ const body = req.body ?? {};
372
+ const action = body.action;
373
+ if (!action || !enumSet.has(action)) {
374
+ sendControllerResponse(reply, {
375
+ success: false,
376
+ status: 400,
377
+ error: `Invalid action '${action ?? ""}'. Valid actions: ${validActions.join(", ")}`,
378
+ meta: { validActions }
379
+ }, req);
380
+ return;
381
+ }
382
+ const permissionCheck = actionPermissions[action] ?? globalAuth;
383
+ if (!permissionCheck) return;
384
+ const { action: _discard, ...data } = body;
385
+ const params = req.params;
386
+ await evaluateAndApplyPermission(permissionCheck, buildPermissionContext(req, {
387
+ resource: resourceName,
388
+ action,
389
+ resourceId: params?.id,
390
+ data
391
+ }), req, reply, { defaultDenialMessage: (user) => user ? `Permission denied for '${action}'` : "Authentication required" });
392
+ };
393
+ }
394
+ /**
395
+ * Resolve pipeline steps for a specific operation.
396
+ * Flat-array config applies to every op; map config applies per-op.
397
+ */
398
+ function resolvePipelineSteps(pipeline, operation) {
399
+ if (!pipeline) return [];
400
+ if (Array.isArray(pipeline)) return pipeline;
401
+ return pipeline[operation] ?? [];
402
+ }
403
+ /**
404
+ * Wrap a controller method (one that takes `IRequestContext` and returns
405
+ * `IControllerResponse<T>`) with pipeline execution. Used by CRUD ops and
406
+ * string-handler custom routes.
407
+ */
408
+ function buildPipelineHandler(controllerMethod, steps, operation, resourceName) {
409
+ return async (req, reply) => {
410
+ sendControllerResponse(reply, await executePipeline(steps, {
411
+ ...createRequestContext(req),
412
+ resource: resourceName,
413
+ operation
414
+ }, (ctx) => controllerMethod(ctx), operation), req);
415
+ };
416
+ }
417
+ /**
418
+ * Wrap an action handler (one that takes `(id, data, req)` and returns a raw
419
+ * result) with pipeline execution. Returns a function that produces a full
420
+ * `IControllerResponse<unknown>` — the action router feeds this directly into
421
+ * `sendControllerResponse`, so field masking, custom status codes, `meta`,
422
+ * `details`, and structured error codes from pipeline interceptors flow
423
+ * through to the client unchanged.
424
+ *
425
+ * CRUD and actions now share the same parity invariant: a pipeline that
426
+ * returns `{ success: false, status: 422, error, details, meta }` reaches the
427
+ * client with all four fields intact. Previously the action path stringified
428
+ * failures into a generic `Error` and dropped everything except `statusCode`.
429
+ *
430
+ * Handler throws still bubble out — the caller's try/catch handles `onError`
431
+ * shaping and the generic `ACTION_FAILED` fallback.
432
+ */
433
+ function buildActionPipelineHandler(handler, steps, operation, resourceName) {
434
+ if (steps.length === 0) return async (id, data, req) => ({
435
+ success: true,
436
+ status: 200,
437
+ data: await handler(id, data, req)
438
+ });
439
+ return async (id, data, req) => {
440
+ return executePipeline(steps, {
441
+ ...createRequestContext(req),
442
+ resource: resourceName,
443
+ operation
444
+ }, async (_ctx) => ({
445
+ success: true,
446
+ status: 200,
447
+ data: await handler(id, data, req)
448
+ }), operation);
449
+ };
450
+ }
451
+ /**
452
+ * Build the `config` object for Fastify route options so
453
+ * @fastify/rate-limit picks up per-route overrides.
454
+ *
455
+ * - `undefined` → no override (inherits instance config)
456
+ * - `false` → explicitly disable rate limiting
457
+ * - `{ max, timeWindow }` → apply that limit
458
+ */
459
+ function buildRateLimitConfig(rateLimit) {
460
+ if (rateLimit === void 0) return void 0;
461
+ if (rateLimit === false) return { rateLimit: false };
462
+ return { rateLimit: {
463
+ max: rateLimit.max,
464
+ timeWindow: rateLimit.timeWindow
465
+ } };
466
+ }
467
+ /**
468
+ * Pick the request-lifecycle plugin middleware for an HTTP method:
469
+ * - GET / HEAD → response cache (if present)
470
+ * - POST / PUT / PATCH → idempotency (if present)
471
+ * - DELETE → none
472
+ *
473
+ * Either field may be `null` if the corresponding plugin wasn't registered.
474
+ */
475
+ function selectPluginMw(method, mws) {
476
+ const upper = method.toUpperCase();
477
+ if (upper === "GET" || upper === "HEAD") return mws.cacheMw;
478
+ if (upper === "POST" || upper === "PUT" || upper === "PATCH") return mws.idempotencyMw;
479
+ return null;
480
+ }
481
+ /**
482
+ * Resolve the default cache/idempotency middlewares for a resource.
483
+ *
484
+ * Skips response-cache when the resource has QueryCache active — QueryCache
485
+ * handles caching at the controller level with SWR, so the HTTP-level
486
+ * response-cache would double-cache.
487
+ */
488
+ function resolveRouterPluginMw(fastify, resourceHasQueryCache) {
489
+ return {
490
+ cacheMw: !resourceHasQueryCache && fastify.hasDecorator("responseCache") ? fastify.responseCache.middleware : null,
491
+ idempotencyMw: fastify.hasDecorator("idempotency") ? fastify.idempotency.middleware : null
492
+ };
493
+ }
494
+ /**
495
+ * Compose preHandler[] in the canonical order. Every null/undefined entry is
496
+ * dropped. Keeps CRUD and Action routers from accidentally ordering the same
497
+ * ingredients differently (regression risk: cache before auth → user-scoped
498
+ * cache keys leak across users).
499
+ *
500
+ * Canonical order:
501
+ * preAuth → arcDecorator → authMw → permissionMw → pluginMw → routeGuards → customMws
502
+ */
503
+ function buildPreHandlerChain(parts) {
504
+ return [
505
+ ...parts.preAuth ?? [],
506
+ parts.arcDecorator,
507
+ parts.authMw ?? null,
508
+ parts.permissionMw ?? null,
509
+ parts.pluginMw ?? null,
510
+ ...parts.routeGuards ?? [],
511
+ ...parts.customMws ?? []
512
+ ].filter(Boolean);
513
+ }
514
+ //#endregion
515
+ export { getControllerScope as _, buildAuthMiddlewareForPermissions as a, buildPreHandlerChain as c, resolveRouterPluginMw as d, selectPluginMw as f, getControllerContext as g, createRequestContext as h, buildAuthMiddleware as i, buildRateLimitConfig as l, createFastifyHandler as m, buildActionPipelineHandler as n, buildCrudPermissionMw as o, createCrudHandlers as p, buildArcDecorator as r, buildPipelineHandler as s, buildActionPermissionMw as t, resolvePipelineSteps as u, sendControllerResponse as v, buildRequestScopeProjection as y };
@@ -0,0 +1,137 @@
1
+ import { a as toJsonSchema } from "./schemaConverter-B0oKLuqI.mjs";
2
+ import { z } from "zod";
3
+ //#region src/core/schemaIR.ts
4
+ /**
5
+ * Schema IR — one canonical representation, two adapters.
6
+ *
7
+ * arc's action-schema handling used to live in two parallel translators:
8
+ * `normalizeActionSchema()` in [createActionRouter.ts](./createActionRouter.ts)
9
+ * produced JSON Schema for AJV, and `convertActionSchemaToZod()` in
10
+ * [../integrations/mcp/action-tools.ts](../integrations/mcp/action-tools.ts)
11
+ * produced Zod shapes for MCP. Same input shape, two implementations — the
12
+ * exact drift pattern routerShared exists to eliminate.
13
+ *
14
+ * This module is the single source of truth: every caller normalizes to
15
+ * `SchemaIR` first, then emits whichever surface they need. If a future
16
+ * refactor adds a field to the IR (e.g. `propertyOrder`, `examples`),
17
+ * both adapters pick it up automatically.
18
+ *
19
+ * **The IR preserves `additionalProperties`.** The previous implementation
20
+ * dropped the flag during normalization, so `additionalProperties: false`
21
+ * silently no-opped even though [createActionRouter.ts:425-428](./createActionRouter.ts#L425-L428)
22
+ * documented it as the opt-in escape hatch for strict validation. The IR
23
+ * carries the flag verbatim; both adapters honor it.
24
+ */
25
+ /**
26
+ * Normalize anything the author handed us (Zod schema, plain JSON Schema,
27
+ * or `undefined`) into a canonical `SchemaIR`.
28
+ *
29
+ * Accepts:
30
+ * - `undefined` / non-object → empty IR (no properties, no required)
31
+ * - Zod v4 object schema — converted via `toJsonSchema` from the shared utility
32
+ * - Plain JSON Schema with `type: 'object'` or `properties`
33
+ *
34
+ * Anything that can't be read as an object schema collapses to an empty IR
35
+ * (no throw — the caller decides whether that's a validation error).
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * normalizeSchemaIR({
40
+ * type: 'object',
41
+ * properties: { carrier: { type: 'string' } },
42
+ * required: ['carrier'],
43
+ * additionalProperties: false,
44
+ * });
45
+ * // → { properties: { carrier: { type: 'string' } }, required: ['carrier'], additionalProperties: false }
46
+ * ```
47
+ */
48
+ function normalizeSchemaIR(raw) {
49
+ if (!raw || typeof raw !== "object") return {
50
+ properties: {},
51
+ required: []
52
+ };
53
+ const converted = toJsonSchema(raw);
54
+ if (!converted || typeof converted !== "object" || converted.type !== "object" && !("properties" in converted)) return {
55
+ properties: {},
56
+ required: []
57
+ };
58
+ const properties = converted.properties ?? {};
59
+ const required = Array.isArray(converted.required) ? converted.required : [];
60
+ const additionalProperties = converted.additionalProperties;
61
+ return {
62
+ properties,
63
+ required,
64
+ ...additionalProperties !== void 0 ? { additionalProperties } : {}
65
+ };
66
+ }
67
+ /**
68
+ * Emit a JSON Schema branch from the IR, with optional extra properties
69
+ * merged in (e.g. the `action: { const: 'approve' }` discriminator added
70
+ * by `buildActionBodySchema`).
71
+ *
72
+ * Preserves `additionalProperties` verbatim — strict schemas (`false`)
73
+ * reach AJV intact, so HTTP validation rejects unknown fields before the
74
+ * handler runs. This closes the bug where the documented strict-mode
75
+ * escape hatch silently no-opped because normalization dropped the flag.
76
+ */
77
+ function schemaIRToJsonSchemaBranch(ir, extras = {}) {
78
+ return {
79
+ type: "object",
80
+ properties: {
81
+ ...extras.properties ?? {},
82
+ ...ir.properties
83
+ },
84
+ required: [...extras.required ?? [], ...ir.required.filter((f) => !(extras.required ?? []).includes(f))],
85
+ ...ir.additionalProperties !== void 0 ? { additionalProperties: ir.additionalProperties } : {}
86
+ };
87
+ }
88
+ /**
89
+ * Emit a flat Zod shape from the IR. The MCP SDK wraps the returned record
90
+ * in `z.object()` internally, so we return the bare shape (same contract
91
+ * as `ToolDefinition.inputSchema`).
92
+ *
93
+ * `additionalProperties: false` is honored at the MCP handler layer rather
94
+ * than baked into the Zod shape — the SDK's input validation happens before
95
+ * the handler runs, and flat shapes can't express `.strict()` mode.
96
+ * `strictAdditionalProperties(ir)` returns the flag so callers can gate
97
+ * their handler on it.
98
+ */
99
+ function schemaIRToZodShape(ir) {
100
+ const requiredSet = new Set(ir.required);
101
+ const result = {};
102
+ for (const [name, prop] of Object.entries(ir.properties)) {
103
+ const desc = typeof prop.description === "string" && prop.description.length > 0 ? prop.description : name;
104
+ const base = jsonSchemaPropToZod(prop);
105
+ result[name] = requiredSet.has(name) ? base.describe(desc) : base.optional().describe(desc);
106
+ }
107
+ return result;
108
+ }
109
+ /**
110
+ * Returns `true` when the IR declares `additionalProperties: false`. MCP
111
+ * tool handlers should reject inputs with unknown keys when this is true,
112
+ * matching HTTP's AJV-level strict enforcement.
113
+ */
114
+ function shouldRejectAdditionalProperties(ir) {
115
+ return ir.additionalProperties === false;
116
+ }
117
+ /**
118
+ * Convert a single JSON Schema property to a Zod type. Understands enum,
119
+ * numeric/integer/boolean/array/object, and falls back to string for
120
+ * unrecognized types (matches MCP's "strings for opaque fields" convention).
121
+ *
122
+ * Internal — use `schemaIRToZodShape` which wires this up with required/optional
123
+ * + description handling.
124
+ */
125
+ function jsonSchemaPropToZod(schema) {
126
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return z.enum(schema.enum);
127
+ switch (typeof schema.type === "string" ? schema.type : "string") {
128
+ case "number":
129
+ case "integer": return z.number();
130
+ case "boolean": return z.boolean();
131
+ case "array": return z.array(z.unknown());
132
+ case "object": return z.record(z.string(), z.unknown());
133
+ default: return z.string();
134
+ }
135
+ }
136
+ //#endregion
137
+ export { shouldRejectAdditionalProperties as i, schemaIRToJsonSchemaBranch as n, schemaIRToZodShape as r, normalizeSchemaIR as t };
@@ -1,6 +1,6 @@
1
1
  import { _ as isElevated, a as getOrgContext, b as isService, c as getRequestScope, d as getServiceScopes, f as getTeamId, g as isAuthenticated, h as hasOrgAccess, i as getClientId, l as getScopeContext, m as getUserRoles, n as PUBLIC_SCOPE, o as getOrgId, p as getUserId, r as getAncestorOrgIds, s as getOrgRoles, t as AUTHENTICATED_SCOPE, u as getScopeContextMap, v as isMember, y as isOrgInScope } from "../types-AOD8fxIw.mjs";
2
- import { n as normalizeRoles } from "../types-D57iXYb8.mjs";
3
- import { n as elevation_default, t as elevationPlugin } from "../elevation-Dci0AYLT.mjs";
2
+ import { n as normalizeRoles } from "../types-DV9WDfeg.mjs";
3
+ import { n as elevation_default, t as elevationPlugin } from "../elevation-DOFoxoDs.mjs";
4
4
  //#region src/scope/rateLimitKey.ts
5
5
  function createTenantKeyGenerator(opts) {
6
6
  if (opts?.strategy) return opts.strategy;