@classytic/arc 2.10.3 → 2.10.8

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 (135) hide show
  1. package/README.md +1 -1
  2. package/dist/{BaseController-CbKKIflT.mjs → BaseController-DVNKvoX4.mjs} +151 -131
  3. package/dist/actionPermissions-TUVR3uiZ.mjs +22 -0
  4. package/dist/adapters/index.d.mts +2 -2
  5. package/dist/audit/index.d.mts +2 -2
  6. package/dist/audit/index.mjs +15 -17
  7. package/dist/auth/index.d.mts +4 -4
  8. package/dist/auth/index.mjs +5 -5
  9. package/dist/auth/redis-session.d.mts +1 -1
  10. package/dist/cache/index.d.mts +2 -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 +1 -1
  14. package/dist/cli/commands/init.mjs +1 -1
  15. package/dist/cli/commands/introspect.mjs +1 -1
  16. package/dist/context/index.d.mts +58 -0
  17. package/dist/context/index.mjs +2 -0
  18. package/dist/core/index.d.mts +2 -2
  19. package/dist/core/index.mjs +2 -2
  20. package/dist/{core-CcR01lup.mjs → core-3MWJosCH.mjs} +139 -91
  21. package/dist/{createApp-BuvPma24.mjs → createApp-BwnEAO2h.mjs} +54 -20
  22. package/dist/docs/index.d.mts +2 -2
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-C7hgL_aI.mjs → elevation-Dci0AYLT.mjs} +2 -2
  25. package/dist/errorHandler-2ii4RIYr.d.mts +114 -0
  26. package/dist/{errorHandler-Bb49BvPD.mjs → errorHandler-CSxe7KIM.mjs} +1 -1
  27. package/dist/{eventPlugin-DCUjuiQT.mjs → eventPlugin-ByU4Cv0e.mjs} +1 -1
  28. package/dist/{eventPlugin-CxWgpd6K.d.mts → eventPlugin-D1ThQ1Pp.d.mts} +1 -1
  29. package/dist/events/index.d.mts +4 -4
  30. package/dist/events/index.mjs +69 -51
  31. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  32. package/dist/events/transports/redis.d.mts +1 -1
  33. package/dist/factory/index.d.mts +1 -1
  34. package/dist/factory/index.mjs +2 -2
  35. package/dist/{fields-Lo1VUDpt.d.mts → fields-C8Y0XLAu.d.mts} +1 -1
  36. package/dist/hooks/index.d.mts +1 -1
  37. package/dist/hooks/index.mjs +1 -1
  38. package/dist/idempotency/index.d.mts +3 -3
  39. package/dist/idempotency/index.mjs +38 -27
  40. package/dist/idempotency/redis.d.mts +1 -1
  41. package/dist/{index-Cl0uoKd5.d.mts → index-BGbpGVyM.d.mts} +2362 -2155
  42. package/dist/{index-DStwgFUK.d.mts → index-BziRPS4H.d.mts} +1 -1
  43. package/dist/{index-ChIw3776.d.mts → index-C_Noptz-.d.mts} +3 -3
  44. package/dist/{index-8qw4y6ff.d.mts → index-EqQN6p0W.d.mts} +3 -3
  45. package/dist/index.d.mts +7 -219
  46. package/dist/index.mjs +8 -128
  47. package/dist/integrations/event-gateway.d.mts +1 -1
  48. package/dist/integrations/event-gateway.mjs +1 -1
  49. package/dist/integrations/index.d.mts +1 -1
  50. package/dist/integrations/mcp/index.d.mts +2 -2
  51. package/dist/integrations/mcp/index.mjs +1 -1
  52. package/dist/integrations/mcp/testing.d.mts +1 -1
  53. package/dist/integrations/mcp/testing.mjs +1 -1
  54. package/dist/logger/index.d.mts +81 -0
  55. package/dist/{logger-DLg8-Ueg.mjs → logger/index.mjs} +1 -6
  56. package/dist/middleware/index.d.mts +109 -0
  57. package/dist/middleware/index.mjs +70 -0
  58. package/dist/multipartBody-CUQGVlM_.mjs +123 -0
  59. package/dist/{openapi-B5F8AddX.mjs → openapi-DpNpqBmo.mjs} +9 -7
  60. package/dist/org/index.d.mts +2 -2
  61. package/dist/permissions/index.d.mts +2 -2
  62. package/dist/permissions/index.mjs +3 -3
  63. package/dist/{permissions-Dk6mshja.mjs → permissions-wkqRwicB.mjs} +2 -2
  64. package/dist/pipe-CGJxqDGx.mjs +62 -0
  65. package/dist/pipeline/index.d.mts +62 -0
  66. package/dist/pipeline/index.mjs +53 -0
  67. package/dist/plugins/index.d.mts +25 -5
  68. package/dist/plugins/index.mjs +9 -9
  69. package/dist/plugins/tracing-entry.d.mts +1 -1
  70. package/dist/plugins/tracing-entry.mjs +1 -1
  71. package/dist/presets/filesUpload.d.mts +4 -4
  72. package/dist/presets/filesUpload.mjs +255 -1
  73. package/dist/presets/index.d.mts +1 -1
  74. package/dist/presets/index.mjs +2 -2
  75. package/dist/presets/multiTenant.d.mts +1 -1
  76. package/dist/presets/multiTenant.mjs +42 -8
  77. package/dist/presets/search.d.mts +2 -2
  78. package/dist/presets/search.mjs +1 -1
  79. package/dist/{presets-fLJVXdVn.mjs → presets-CrwOvuXI.mjs} +1 -1
  80. package/dist/{queryCachePlugin-DQCEfJis.mjs → queryCachePlugin-ChLNZvFT.mjs} +2 -2
  81. package/dist/{queryCachePlugin-BKbWjgDG.d.mts → queryCachePlugin-Dumka73q.d.mts} +1 -1
  82. package/dist/{queryParser-DBqBB6AC.mjs → queryParser-NR__Qiju.mjs} +68 -1
  83. package/dist/{redis-DqyeggCa.d.mts → redis-MXLp1oOf.d.mts} +1 -1
  84. package/dist/{redis-stream-CakIQmwR.d.mts → redis-stream-bkO88VHx.d.mts} +1 -1
  85. package/dist/registry/index.d.mts +1 -1
  86. package/dist/registry/index.mjs +2 -2
  87. package/dist/{requestContext-xHIKedG6.mjs → requestContext-C38GskNt.mjs} +1 -1
  88. package/dist/{resourceToTools-BElv3xPT.mjs → resourceToTools-BhF3JV5p.mjs} +8 -3
  89. package/dist/scope/index.d.mts +2 -2
  90. package/dist/scope/index.mjs +2 -2
  91. package/dist/{sse-yBCgOLGu.mjs → sse-D8UeDwis.mjs} +1 -1
  92. package/dist/{store-helpers-ZCSMJJAX.mjs → store-helpers-DYYUQbQN.mjs} +4 -0
  93. package/dist/testing/index.d.mts +2 -2
  94. package/dist/testing/index.mjs +11 -2
  95. package/dist/testing/storageContract.d.mts +1 -1
  96. package/dist/types/index.d.mts +4 -4
  97. package/dist/types/index.mjs +1 -1
  98. package/dist/types/storage.d.mts +1 -1
  99. package/dist/{types-Btdda02s.d.mts → types-CVKBssX5.d.mts} +1 -1
  100. package/dist/{types-Co8k3NyS.d.mts → types-CVdgPXBW.d.mts} +22 -9
  101. package/dist/utils/index.d.mts +73 -3
  102. package/dist/utils/index.mjs +4 -4
  103. package/dist/{utils-B2fNOD_i.mjs → utils-LMwVidKy.mjs} +20 -2
  104. package/dist/versioning-CeUXHfjw.d.mts +117 -0
  105. package/package.json +22 -6
  106. package/skills/arc/SKILL.md +1 -1
  107. package/dist/errorHandler-DRQ3EqfL.d.mts +0 -218
  108. package/dist/filesUpload-t21LS-py.mjs +0 -377
  109. /package/dist/{EventTransport-CUw5NNWe.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
  110. /package/dist/{HookSystem-BNYKnrXF.mjs → HookSystem-BjFu7zf1.mjs} +0 -0
  111. /package/dist/{ResourceRegistry-BPd6NQDm.mjs → ResourceRegistry-CcN2LVrc.mjs} +0 -0
  112. /package/dist/{betterAuthOpenApi-BBRVhjQN.mjs → betterAuthOpenApi--rdY15Ld.mjs} +0 -0
  113. /package/dist/{caching-CBpK_SCM.mjs → caching-3h93rkJM.mjs} +0 -0
  114. /package/dist/{createActionRouter-Bp_5c_2b.mjs → createActionRouter-C8UUB3Px.mjs} +0 -0
  115. /package/dist/{elevation-C5SwtkAn.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  116. /package/dist/{errors-CCSsMpXE.d.mts → errors-BI8kEKsO.d.mts} +0 -0
  117. /package/dist/{errors-D5c-5BJL.mjs → errors-BqdUDja_.mjs} +0 -0
  118. /package/dist/{externalPaths-BQ8QijNH.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
  119. /package/dist/{fields-bxkeltzz.mjs → fields-CTMWOUDt.mjs} +0 -0
  120. /package/dist/{interface-CSbZdv_3.d.mts → interface-B-pe8fhj.d.mts} +0 -0
  121. /package/dist/{interface-D218ikEo.d.mts → interface-yhyb_pLY.d.mts} +0 -0
  122. /package/dist/{keys-qcD-TVJl.mjs → keys-nWQGUTu1.mjs} +0 -0
  123. /package/dist/{loadResources-BAzJItAJ.mjs → loadResources-Bksk8ydA.mjs} +0 -0
  124. /package/dist/{memory-B5Amv9A1.mjs → memory-DqI-449b.mjs} +0 -0
  125. /package/dist/{metrics-DuhiSEZI.mjs → metrics-TuOmguhi.mjs} +0 -0
  126. /package/dist/{pluralize-A0tWEl1K.mjs → pluralize-CWP6MB39.mjs} +0 -0
  127. /package/dist/{registry-B3lRFBWo.mjs → registry-B0Wl7uVV.mjs} +0 -0
  128. /package/dist/{replyHelpers-CXtJDAZ0.mjs → replyHelpers-BLojtuvR.mjs} +0 -0
  129. /package/dist/{sessionManager-BkzVU8h2.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
  130. /package/dist/{storage-CVk_SEn2.d.mts → storage-BwGQXUpd.d.mts} +0 -0
  131. /package/dist/{tracing-65B51Dw3.d.mts → tracing-xqXzWeaf.d.mts} +0 -0
  132. /package/dist/{types-Csi3FLfq.mjs → types-CDnTEpga.mjs} +0 -0
  133. /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
  134. /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
  135. /package/dist/{versioning-C2U_bLY0.mjs → versioning-B6mimogM.mjs} +0 -0
@@ -1,2 +1,256 @@
1
- import { t as filesUploadPreset } from "../filesUpload-t21LS-py.mjs";
1
+ import { o as getOrgId, p as getUserId } from "../types-AOD8fxIw.mjs";
2
+ import { i as NotFoundError, u as ValidationError } from "../errors-BqdUDja_.mjs";
3
+ import { C as requireAuth, y as allowPublic } from "../permissions-wkqRwicB.mjs";
4
+ import { t as multipartBody } from "../multipartBody-CUQGVlM_.mjs";
5
+ //#region src/presets/filesUpload.ts
6
+ const DEFAULT_FIELD_NAME = "file";
7
+ const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
8
+ function defaultContextFrom(scope) {
9
+ if (!scope) return {};
10
+ const userId = getUserId(scope);
11
+ const organizationId = getOrgId(scope);
12
+ const ctx = {};
13
+ if (userId !== void 0) ctx.userId = userId;
14
+ if (organizationId !== void 0) ctx.organizationId = organizationId;
15
+ return ctx;
16
+ }
17
+ function buildStorageContext(request, contextFrom) {
18
+ const scope = request.scope;
19
+ return {
20
+ scope: contextFrom(scope),
21
+ requestId: request.id
22
+ };
23
+ }
24
+ /**
25
+ * Parse a single-range `Range: bytes=start-end` header.
26
+ *
27
+ * Returns `undefined` when the header is missing or unparseable. Only
28
+ * satisfiable single ranges are supported — multi-range requests fall through
29
+ * to the full-object response (per RFC 7233 §4.1 a server MAY ignore ranges).
30
+ */
31
+ function parseRangeHeader(header, totalSize) {
32
+ if (!header || !header.startsWith("bytes=")) return void 0;
33
+ const spec = header.slice(6).split(",")[0]?.trim();
34
+ if (!spec) return void 0;
35
+ const dashIndex = spec.indexOf("-");
36
+ if (dashIndex === -1) return void 0;
37
+ const startRaw = spec.slice(0, dashIndex);
38
+ const endRaw = spec.slice(dashIndex + 1);
39
+ if (startRaw === "") {
40
+ if (totalSize === void 0) return void 0;
41
+ const suffix = Number(endRaw);
42
+ if (!Number.isFinite(suffix) || suffix <= 0) return void 0;
43
+ return {
44
+ start: Math.max(0, totalSize - suffix),
45
+ end: totalSize - 1
46
+ };
47
+ }
48
+ const start = Number(startRaw);
49
+ if (!Number.isFinite(start) || start < 0) return void 0;
50
+ if (endRaw === "") {
51
+ if (totalSize === void 0) return void 0;
52
+ return {
53
+ start,
54
+ end: totalSize - 1
55
+ };
56
+ }
57
+ const end = Number(endRaw);
58
+ if (!Number.isFinite(end) || end < start) return void 0;
59
+ if (totalSize !== void 0 && end >= totalSize) return {
60
+ start,
61
+ end: totalSize - 1
62
+ };
63
+ return {
64
+ start,
65
+ end
66
+ };
67
+ }
68
+ /**
69
+ * Strict policy — rejects filenames that could escape a storage root or
70
+ * confuse a filesystem. Safe default for disk/S3 adapters that compose
71
+ * `${prefix}/${filename}` or `path.join(root, filename)`.
72
+ */
73
+ function strictFilenamePolicy(filename) {
74
+ if (filename.length === 0) throw new ValidationError("Upload filename is empty");
75
+ if (filename.length > 255) throw new ValidationError("Upload filename exceeds 255 characters");
76
+ if (filename.includes("\0")) throw new ValidationError("Upload filename contains a NUL byte");
77
+ if (filename.includes("/") || filename.includes("\\")) throw new ValidationError("Upload filename contains a path separator");
78
+ if (filename === "." || filename === "..") throw new ValidationError("Upload filename is a path traversal component");
79
+ return filename;
80
+ }
81
+ /** Resolve the user-supplied policy option into a concrete validator. */
82
+ function resolveFilenamePolicy(policy) {
83
+ if (policy === void 0 || policy === true) return strictFilenamePolicy;
84
+ if (policy === false || policy === "*") return (f) => f;
85
+ if (typeof policy === "function") return (filename) => {
86
+ const result = policy(filename);
87
+ if (result === false) throw new ValidationError(`Upload filename rejected: ${filename}`);
88
+ if (typeof result === "string") return result;
89
+ return filename;
90
+ };
91
+ return strictFilenamePolicy;
92
+ }
93
+ function makeUploadHandler(deps) {
94
+ return async function uploadHandler(request, reply) {
95
+ const file = (request.body?._files)?.[deps.fieldName];
96
+ if (!file) throw new ValidationError(`Missing file field '${deps.fieldName}' in multipart body`);
97
+ const filename = deps.applyFilenamePolicy(file.filename);
98
+ const ctx = buildStorageContext(request, deps.contextFrom);
99
+ const result = await deps.storage.upload({
100
+ buffer: file.buffer,
101
+ filename,
102
+ mimeType: file.mimetype,
103
+ size: file.size
104
+ }, ctx);
105
+ return reply.code(201).send({
106
+ success: true,
107
+ data: toResponseFile(result)
108
+ });
109
+ };
110
+ }
111
+ function toResponseFile(file) {
112
+ const payload = {
113
+ id: file.id,
114
+ url: file.url,
115
+ pathname: file.pathname,
116
+ contentType: file.contentType,
117
+ bytes: file.bytes
118
+ };
119
+ if (file.metadata !== void 0) payload.metadata = file.metadata;
120
+ return payload;
121
+ }
122
+ function makeReadHandler(deps) {
123
+ return async function readHandler(request, reply) {
124
+ const { id } = request.params;
125
+ const ctx = buildStorageContext(request, deps.contextFrom);
126
+ reply.header("Accept-Ranges", "bytes");
127
+ const rangeHeader = request.headers.range;
128
+ let result;
129
+ try {
130
+ const parsed = rangeHeader ? parseRangeHeader(rangeHeader, void 0) : void 0;
131
+ result = await deps.storage.read(id, ctx, parsed);
132
+ } catch (err) {
133
+ throw toNotFound(err, "File", id);
134
+ }
135
+ if (result.kind === "buffer") return sendBuffer(reply, result, rangeHeader);
136
+ return sendStream(reply, result, rangeHeader);
137
+ };
138
+ }
139
+ function sendBuffer(reply, result, rangeHeader) {
140
+ reply.type(result.contentType);
141
+ const total = result.totalBytes ?? result.buffer.length;
142
+ if (result.range) {
143
+ const { start, end } = result.range;
144
+ reply.code(206);
145
+ reply.header("Content-Range", `bytes ${start}-${end}/${total}`);
146
+ reply.header("Content-Length", String(result.buffer.length));
147
+ return reply.send(result.buffer);
148
+ }
149
+ if (rangeHeader) {
150
+ const parsed = parseRangeHeader(rangeHeader, total);
151
+ if (parsed) {
152
+ const slice = result.buffer.subarray(parsed.start, parsed.end + 1);
153
+ reply.code(206);
154
+ reply.header("Content-Range", `bytes ${parsed.start}-${parsed.end}/${total}`);
155
+ reply.header("Content-Length", String(slice.length));
156
+ return reply.send(slice);
157
+ }
158
+ }
159
+ reply.header("Content-Length", String(result.buffer.length));
160
+ return reply.send(result.buffer);
161
+ }
162
+ function sendStream(reply, result, rangeHeader) {
163
+ reply.type(result.contentType);
164
+ if (result.range && result.bytes !== void 0) {
165
+ const { start, end } = result.range;
166
+ reply.code(206);
167
+ reply.header("Content-Range", `bytes ${start}-${end}/${result.bytes}`);
168
+ reply.header("Content-Length", String(end - start + 1));
169
+ } else if (result.bytes !== void 0) {
170
+ reply.header("Content-Length", String(result.bytes));
171
+ if (rangeHeader) reply.request.log.debug({ url: reply.request.url }, "filesUploadPreset: adapter returned unsliced stream for a range request — sending full object");
172
+ }
173
+ return reply.send(result.stream);
174
+ }
175
+ function makeDeleteHandler(deps) {
176
+ return async function deleteHandler(request, reply) {
177
+ const { id } = request.params;
178
+ const ctx = buildStorageContext(request, deps.contextFrom);
179
+ if (!await deps.storage.delete(id, ctx)) throw new NotFoundError("File", id);
180
+ return reply.code(204).send();
181
+ };
182
+ }
183
+ function toNotFound(err, resource, id) {
184
+ if (err instanceof NotFoundError) return err;
185
+ const maybe = err;
186
+ if (maybe?.statusCode === 404 || maybe?.code === "NOT_FOUND") return new NotFoundError(resource, id);
187
+ if (typeof maybe?.message === "string" && /not\s*found/i.test(maybe.message)) return new NotFoundError(resource, id);
188
+ return err;
189
+ }
190
+ /**
191
+ * Create a files-upload preset bound to a `Storage` adapter.
192
+ *
193
+ * The preset uses `raw: true` routes so binary responses bypass arc's JSON
194
+ * envelope. Upload still returns the standard `{ success: true, data }`
195
+ * envelope manually because the response is structured metadata, not bytes.
196
+ */
197
+ function filesUploadPreset(options) {
198
+ if (!options?.storage) throw new Error("filesUploadPreset: `storage` is required");
199
+ const deps = {
200
+ storage: options.storage,
201
+ fieldName: options.fieldName ?? DEFAULT_FIELD_NAME,
202
+ contextFrom: options.contextFrom ?? defaultContextFrom,
203
+ applyFilenamePolicy: resolveFilenamePolicy(options.sanitizeFilename)
204
+ };
205
+ const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
206
+ const allowedMimeTypes = options.allowedMimeTypes;
207
+ const includeRoutes = {
208
+ upload: options.includeRoutes?.upload ?? true,
209
+ read: options.includeRoutes?.read ?? true,
210
+ delete: options.includeRoutes?.delete ?? true
211
+ };
212
+ return {
213
+ name: "filesUpload",
214
+ routes: (permissions) => {
215
+ const routes = [];
216
+ if (includeRoutes.upload) routes.push({
217
+ method: "POST",
218
+ path: "/upload",
219
+ operation: "filesUpload.upload",
220
+ summary: "Upload a file",
221
+ description: "Accepts a multipart/form-data request and persists the bytes via the configured Storage adapter.",
222
+ permissions: options.permissions?.upload ?? permissions.create ?? requireAuth(),
223
+ preHandler: [multipartBody({
224
+ maxFileSize,
225
+ allowedMimeTypes,
226
+ requiredFields: [deps.fieldName]
227
+ })],
228
+ raw: true,
229
+ handler: makeUploadHandler(deps)
230
+ });
231
+ if (includeRoutes.read) routes.push({
232
+ method: "GET",
233
+ path: "/:id",
234
+ operation: "filesUpload.read",
235
+ summary: "Download a file",
236
+ description: "Streams the stored bytes. Supports single-range `Range: bytes=start-end`.",
237
+ permissions: options.permissions?.read ?? permissions.get ?? allowPublic(),
238
+ raw: true,
239
+ handler: makeReadHandler(deps),
240
+ mcp: false
241
+ });
242
+ if (includeRoutes.delete) routes.push({
243
+ method: "DELETE",
244
+ path: "/:id",
245
+ operation: "filesUpload.delete",
246
+ summary: "Delete a file",
247
+ permissions: options.permissions?.delete ?? permissions.delete ?? requireAuth(),
248
+ raw: true,
249
+ handler: makeDeleteHandler(deps)
250
+ });
251
+ return routes;
252
+ }
253
+ };
254
+ }
255
+ //#endregion
2
256
  export { filesUploadPreset };
@@ -1,4 +1,4 @@
1
- import { St as IRequestContext, dn as AnyRecord, lt as ResourceConfig, ot as PresetResult, u as PaginationResult, xt as IControllerResponse } from "../index-Cl0uoKd5.mjs";
1
+ import { Mt as IControllerResponse, Nt as IRequestContext, Yt as AnyRecord, _t as PresetResult, bt as ResourceConfig, d as PaginationResult } from "../index-BGbpGVyM.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-fLJVXdVn.mjs";
3
- import { t as filesUploadPreset } from "../filesUpload-t21LS-py.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-CrwOvuXI.mjs";
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 { Q as CrudRouteKey, ot as PresetResult } from "../index-Cl0uoKd5.mjs";
1
+ import { _t as PresetResult, lt as CrudRouteKey } from "../index-BGbpGVyM.mjs";
2
2
 
3
3
  //#region src/presets/multiTenant.d.ts
4
4
  /**
@@ -10,6 +10,20 @@ function resolveSpec(scope, spec) {
10
10
  if (spec.type === "org") return getOrgId(scope);
11
11
  if (spec.type === "team") return getTeamId(scope);
12
12
  }
13
+ /**
14
+ * Stash the resolved tenant field map on the request so `BaseController`
15
+ * can forward it to the repository layer as top-level options. Needed by
16
+ * plugin-scoped repos (mongokit's `multiTenantPlugin`) that read tenant
17
+ * from `context.<field>` rather than from filter/query/data stamping.
18
+ */
19
+ function stashTenantFields(request, resolved) {
20
+ if (Object.keys(resolved).length === 0) return;
21
+ const target = request;
22
+ target._tenantFields = {
23
+ ...target._tenantFields ?? {},
24
+ ...resolved
25
+ };
26
+ }
13
27
  /** Resolve every spec — returns the partial map of fields that have a value. */
14
28
  function resolveAll(scope, specs) {
15
29
  const resolved = {};
@@ -34,10 +48,13 @@ function createTenantFilter(specs) {
34
48
  const scope = getRequestScope(request);
35
49
  if (isElevated(scope)) {
36
50
  const { resolved } = resolveAll(scope, specs);
37
- if (Object.keys(resolved).length > 0) request._policyFilters = {
38
- ...request._policyFilters ?? {},
39
- ...resolved
40
- };
51
+ if (Object.keys(resolved).length > 0) {
52
+ request._policyFilters = {
53
+ ...request._policyFilters ?? {},
54
+ ...resolved
55
+ };
56
+ stashTenantFields(request, resolved);
57
+ }
41
58
  return;
42
59
  }
43
60
  if (hasOrgAccess(scope)) {
@@ -47,6 +64,7 @@ function createTenantFilter(specs) {
47
64
  ...request._policyFilters ?? {},
48
65
  ...resolved
49
66
  };
67
+ stashTenantFields(request, resolved);
50
68
  return;
51
69
  }
52
70
  reply.code(403).send({
@@ -81,10 +99,13 @@ function createFlexibleTenantFilter(specs) {
81
99
  const scope = getRequestScope(request);
82
100
  if (isElevated(scope)) {
83
101
  const { resolved } = resolveAll(scope, specs);
84
- if (Object.keys(resolved).length > 0) request._policyFilters = {
85
- ...request._policyFilters ?? {},
86
- ...resolved
87
- };
102
+ if (Object.keys(resolved).length > 0) {
103
+ request._policyFilters = {
104
+ ...request._policyFilters ?? {},
105
+ ...resolved
106
+ };
107
+ stashTenantFields(request, resolved);
108
+ }
88
109
  return;
89
110
  }
90
111
  if (hasOrgAccess(scope)) {
@@ -94,6 +115,7 @@ function createFlexibleTenantFilter(specs) {
94
115
  ...request._policyFilters ?? {},
95
116
  ...resolved
96
117
  };
118
+ stashTenantFields(request, resolved);
97
119
  return;
98
120
  }
99
121
  reply.code(403).send({
@@ -109,6 +131,14 @@ function createFlexibleTenantFilter(specs) {
109
131
  * Create tenant injection middleware.
110
132
  * Walks the configured tenant fields and writes each into the request body.
111
133
  * Fails closed if any required dimension is missing for non-elevated callers.
134
+ *
135
+ * Also stashes the resolved fields on `request._tenantFields` so
136
+ * `BaseController.tenantRepoOptions()` can forward them to the repo layer
137
+ * as top-level options — needed by plugin-scoped repos like mongokit's
138
+ * `multiTenantPlugin`, which reads tenant from `context.<field>` rather
139
+ * than from `data.<field>`. Without this forwarding, multi-field preset
140
+ * writes (update/delete) work only when the plugin's `allowDataInjection`
141
+ * fallback covers the operation's policy key, which is write-only.
112
142
  */
113
143
  function createTenantInjection(specs) {
114
144
  return async (request, reply) => {
@@ -124,6 +154,10 @@ function createTenantInjection(specs) {
124
154
  return;
125
155
  }
126
156
  if (request.body) Object.assign(request.body, resolved);
157
+ request._tenantFields = {
158
+ ...request._tenantFields ?? {},
159
+ ...resolved
160
+ };
127
161
  };
128
162
  }
129
163
  function multiTenantPreset(options = {}) {
@@ -1,5 +1,5 @@
1
- import { c as PermissionCheck } from "../fields-Lo1VUDpt.mjs";
2
- import { _t as ControllerHandler, mt as RouteMcpConfig, ot as PresetResult, pt as RouteDefinition } from "../index-Cl0uoKd5.mjs";
1
+ import { Ot as ControllerHandler, Tt as RouteMcpConfig, _t as PresetResult, wt as RouteDefinition } from "../index-BGbpGVyM.mjs";
2
+ import { c as PermissionCheck } from "../fields-C8Y0XLAu.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-Dk6mshja.mjs";
1
+ import { C as requireAuth, y as allowPublic } from "../permissions-wkqRwicB.mjs";
2
2
  //#region src/presets/search.ts
3
3
  const BUILTINS = [
4
4
  {
@@ -1,6 +1,6 @@
1
1
  import { _ as isElevated, n as PUBLIC_SCOPE } from "./types-AOD8fxIw.mjs";
2
2
  import { multiTenantPreset } from "./presets/multiTenant.mjs";
3
- import { C as requireAuth, T as requireRoles, y as allowPublic } from "./permissions-Dk6mshja.mjs";
3
+ import { C as requireAuth, T as requireRoles, y as allowPublic } from "./permissions-wkqRwicB.mjs";
4
4
  //#region src/presets/ownedByUser.ts
5
5
  /**
6
6
  * Create ownership check middleware.
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { i as versionKey, r as tagVersionKey } from "./keys-qcD-TVJl.mjs";
2
+ import { i as versionKey, r as tagVersionKey } from "./keys-nWQGUTu1.mjs";
3
3
  import { t as hasEvents } from "./typeGuards-Cj5Rgvlg.mjs";
4
- import { t as MemoryCacheStore } from "./memory-B5Amv9A1.mjs";
4
+ import { t as MemoryCacheStore } from "./memory-DqI-449b.mjs";
5
5
  import fp from "fastify-plugin";
6
6
  //#region src/cache/QueryCache.ts
7
7
  var QueryCache = class {
@@ -1,4 +1,4 @@
1
- import { r as CacheStore } from "./interface-D218ikEo.mjs";
1
+ import { r as CacheStore } from "./interface-yhyb_pLY.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/cache/QueryCache.d.ts
@@ -1,4 +1,71 @@
1
1
  import { m as RESERVED_QUERY_PARAMS } from "./constants-BhY1OHoH.mjs";
2
+ //#region src/utils/simpleEqualityMatcher.ts
3
+ /**
4
+ * `simpleEqualityMatcher` — a minimal, dialect-agnostic flat-key equality
5
+ * matcher for `DataAdapter.matchesFilter` / `BaseController({ matchesFilter })`.
6
+ *
7
+ * **What it does:** for each `[key, expected]` in the filter, compares
8
+ * `item[key]` to `expected` via string coercion (so Mongo `ObjectId` values
9
+ * match their string representation) and returns `true` only if every
10
+ * filter entry matches. Array item values are matched implicitly (contains).
11
+ *
12
+ * **What it does NOT do:**
13
+ * - No `$eq` / `$ne` / `$in` / `$nin` / `$gt` / `$lt` / `$regex` / `$exists`
14
+ * - No `$and` / `$or`
15
+ * - No dot-path traversal (`"owner.id"`)
16
+ * - No schema-specific coercion
17
+ *
18
+ * **Why it exists:** 95%+ of arc's `_policyFilters` are produced by built-in
19
+ * permission helpers and are shaped like `{ ownerId: "u1" }` or
20
+ * `{ organizationId: "org_x" }` — flat equality. For that common shape,
21
+ * this helper is a safe, tested, 15-line defense-in-depth matcher that
22
+ * hosts using minimal repos (no `getOne(compoundFilter)` DB path) can opt
23
+ * into without arc shipping a full Mongo-syntax engine.
24
+ *
25
+ * **When to use:**
26
+ * - Your adapter/repo doesn't natively filter on `getOne(compoundFilter)`
27
+ * - Your `_policyFilters` are flat equality (from arc's built-in permission helpers)
28
+ * - You want defense-in-depth on `validateItemAccess` / `fetchDetailed`'s `getById` fallback
29
+ *
30
+ * **When NOT to use:**
31
+ * - Your `_policyFilters` use operators (`$in`, `$ne`, etc.) — supply a
32
+ * native matcher (mongokit's repo does the filter at the DB layer; for
33
+ * custom repos, wrap the kit's own predicate engine).
34
+ * - You're a mongokit / sqlitekit / Prisma user — the DB-level filter
35
+ * applied by `getOne(compoundFilter)` already covers this.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * import { simpleEqualityMatcher } from '@classytic/arc/utils';
40
+ *
41
+ * // On a custom adapter
42
+ * const adapter: DataAdapter = {
43
+ * repository,
44
+ * type: 'custom',
45
+ * name: 'in-memory',
46
+ * matchesFilter: simpleEqualityMatcher,
47
+ * };
48
+ *
49
+ * // Or directly on BaseController for ad-hoc controllers
50
+ * new BaseController(repo, { matchesFilter: simpleEqualityMatcher });
51
+ * ```
52
+ */
53
+ function simpleEqualityMatcher(item, filters) {
54
+ if (!item || typeof item !== "object") return false;
55
+ const obj = item;
56
+ for (const [key, expected] of Object.entries(filters)) {
57
+ if (expected && typeof expected === "object" && !Array.isArray(expected) && Object.getPrototypeOf(expected) === Object.prototype && Object.keys(expected).some((k) => k.startsWith("$"))) return false;
58
+ const actual = obj[key];
59
+ if (Array.isArray(actual)) {
60
+ const expectedStr = String(expected);
61
+ if (!actual.some((v) => String(v) === expectedStr)) return false;
62
+ continue;
63
+ }
64
+ if (String(actual) !== String(expected)) return false;
65
+ }
66
+ return true;
67
+ }
68
+ //#endregion
2
69
  //#region src/utils/queryParser.ts
3
70
  /**
4
71
  * Arc Query Parser - Default URL-to-Query Parser
@@ -349,4 +416,4 @@ function createQueryParser(options) {
349
416
  return new ArcQueryParser(options);
350
417
  }
351
418
  //#endregion
352
- export { createQueryParser as n, ArcQueryParser as t };
419
+ export { createQueryParser as n, simpleEqualityMatcher as r, ArcQueryParser as t };
@@ -1,4 +1,4 @@
1
- import { n as IdempotencyResult, r as IdempotencyStore } from "./interface-CSbZdv_3.mjs";
1
+ import { n as IdempotencyResult, r as IdempotencyStore } from "./interface-B-pe8fhj.mjs";
2
2
 
3
3
  //#region src/idempotency/stores/redis.d.ts
4
4
  interface RedisClient {
@@ -1,4 +1,4 @@
1
- import { i as EventLogger, n as DomainEvent, o as EventTransport, r as EventHandler } from "./EventTransport-CUw5NNWe.mjs";
1
+ import { i as EventLogger, n as DomainEvent, o as EventTransport, r as EventHandler } from "./EventTransport-CfVEGaEl.mjs";
2
2
 
3
3
  //#region src/events/transports/redis-stream.d.ts
4
4
  interface RedisStreamLike {
@@ -1,4 +1,4 @@
1
- import { O as IntrospectionPluginOptions, U as RegisterOptions, W as ResourceRegistry } from "../index-Cl0uoKd5.mjs";
1
+ import { R as RegisterOptions, k as IntrospectionPluginOptions, z as ResourceRegistry } from "../index-BGbpGVyM.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/registry/introspectionPlugin.d.ts
@@ -1,3 +1,3 @@
1
- import { n as introspectionPlugin_default, t as introspectionPlugin } from "../registry-B3lRFBWo.mjs";
2
- import { t as ResourceRegistry } from "../ResourceRegistry-BPd6NQDm.mjs";
1
+ import { n as introspectionPlugin_default, t as introspectionPlugin } from "../registry-B0Wl7uVV.mjs";
2
+ import { t as ResourceRegistry } from "../ResourceRegistry-CcN2LVrc.mjs";
3
3
  export { ResourceRegistry, introspectionPlugin_default as introspectionPlugin, introspectionPlugin as introspectionPluginFn };
@@ -11,7 +11,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
11
11
  *
12
12
  * @example
13
13
  * ```typescript
14
- * import { requestContext } from '@classytic/arc';
14
+ * import { requestContext } from '@classytic/arc/context';
15
15
  *
16
16
  * // Anywhere in the call stack — no parameter passing needed
17
17
  * async function auditAction(action: string) {
@@ -1,6 +1,7 @@
1
- import { t as BaseController } from "./BaseController-CbKKIflT.mjs";
1
+ import { t as BaseController } from "./BaseController-DVNKvoX4.mjs";
2
2
  import { n as normalizePermissionResult } from "./applyPermissionResult-QhV1Pa-g.mjs";
3
- import { t as pluralize } from "./pluralize-A0tWEl1K.mjs";
3
+ import { t as resolveActionPermission } from "./actionPermissions-TUVR3uiZ.mjs";
4
+ import { t as pluralize } from "./pluralize-CWP6MB39.mjs";
4
5
  import { z } from "zod";
5
6
  //#region src/integrations/mcp/createMcpServer.ts
6
7
  /**
@@ -655,7 +656,11 @@ function resourceToTools(resource, config = {}) {
655
656
  }
656
657
  const toolName = prefix ? `${prefix}_${actionName}_${resource.name}` : `${actionName}_${resource.name}`;
657
658
  const handler = typeof entry === "function" ? entry : def.handler;
658
- const actionPerms = (typeof def !== "function" ? def.permissions : void 0) ?? resource.actionPermissions;
659
+ const actionPerms = resolveActionPermission({
660
+ action: entry,
661
+ resourcePermissions: resource.permissions,
662
+ resourceActionPermissions: resource.actionPermissions
663
+ });
659
664
  tools.push({
660
665
  name: toolName,
661
666
  description: String(description),
@@ -1,5 +1,5 @@
1
- import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-C5SwtkAn.mjs";
2
- import { _ as isAuthenticated, a as getClientId, b as isOrgInScope, c as getOrgRoles, d as getScopeContextMap, f as getServiceScopes, g as hasOrgAccess, h as getUserRoles, i as getAncestorOrgIds, l as getRequestScope, m as getUserId, n as PUBLIC_SCOPE, o as getOrgContext, p as getTeamId, r as RequestScope, s as getOrgId, t as AUTHENTICATED_SCOPE, u as getScopeContext, v as isElevated, x as isService, y as isMember } from "../types-BD85MlEK.mjs";
1
+ import { _ as isAuthenticated, a as getClientId, b as isOrgInScope, c as getOrgRoles, d as getScopeContextMap, f as getServiceScopes, g as hasOrgAccess, h as getUserRoles, i as getAncestorOrgIds, l as getRequestScope, m as getUserId, n as PUBLIC_SCOPE, o as getOrgContext, p as getTeamId, r as RequestScope, s as getOrgId, t as AUTHENTICATED_SCOPE, u as getScopeContext, v as isElevated, x as isService, y as isMember } from "../types-tgR4Pt8F.mjs";
2
+ import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-s5ykdNHr.mjs";
3
3
  import { FastifyReply, FastifyRequest } from "fastify";
4
4
 
5
5
  //#region src/scope/rateLimitKey.d.ts
@@ -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-DV9WDfeg.mjs";
3
- import { n as elevation_default, t as elevationPlugin } from "../elevation-C7hgL_aI.mjs";
2
+ import { n as normalizeRoles } from "../types-D57iXYb8.mjs";
3
+ import { n as elevation_default, t as elevationPlugin } from "../elevation-Dci0AYLT.mjs";
4
4
  //#region src/scope/rateLimitKey.ts
5
5
  function createTenantKeyGenerator(opts) {
6
6
  if (opts?.strategy) return opts.strategy;
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
+ import { arcLog } from "./logger/index.mjs";
2
3
  import { n as PUBLIC_SCOPE, o as getOrgId } from "./types-AOD8fxIw.mjs";
3
- import { t as arcLog } from "./logger-DLg8-Ueg.mjs";
4
4
  import fp from "fastify-plugin";
5
5
  //#region src/plugins/sse.ts
6
6
  var sse_exports = /* @__PURE__ */ __exportAll({
@@ -18,6 +18,10 @@ function isNotFoundError(err) {
18
18
  * Build a `safeGetOne(filter)` that papers over the throw-vs-null split
19
19
  * in kit implementations. Real errors propagate; miss returns `null`.
20
20
  * Throws if the repository lacks `getOne` — callers must check.
21
+ *
22
+ * Accepts `FilterInput` (the repo-core union) so callers can compose
23
+ * portable Filter IR (`and(eq(...), gt(...))`) OR pass a flat kit-native
24
+ * record. Both forms reach the kit's `getOne` unchanged — kits dispatch.
21
25
  */
22
26
  function createSafeGetOne(repository) {
23
27
  if (typeof repository.getOne !== "function") throw new Error("createSafeGetOne: repository.getOne is required");
@@ -1,5 +1,5 @@
1
- import { G as ResourceDefinition, dn as AnyRecord } from "../index-Cl0uoKd5.mjs";
2
- import { d as ResourceLike, r as CreateAppOptions } from "../types-Co8k3NyS.mjs";
1
+ import { B as ResourceDefinition, Yt as AnyRecord } from "../index-BGbpGVyM.mjs";
2
+ import { d as ResourceLike, r as CreateAppOptions } from "../types-CVdgPXBW.mjs";
3
3
  import { StorageContractSetup, StorageContractSetupResult, runStorageContract } from "./storageContract.mjs";
4
4
  import Fastify, { FastifyInstance, FastifyServerOptions } from "fastify";
5
5
  import { Connection } from "mongoose";
@@ -1,5 +1,5 @@
1
1
  import { t as CRUD_OPERATIONS } from "../constants-BhY1OHoH.mjs";
2
- import { n as applyFieldWritePermissions, t as applyFieldReadPermissions } from "../fields-bxkeltzz.mjs";
2
+ import { n as applyFieldWritePermissions, t as applyFieldReadPermissions } from "../fields-CTMWOUDt.mjs";
3
3
  import { runStorageContract } from "./storageContract.mjs";
4
4
  import Fastify from "fastify";
5
5
  import mongoose from "mongoose";
@@ -915,6 +915,15 @@ function createMockRepository(overrides = {}) {
915
915
  success: true,
916
916
  message: "Deleted"
917
917
  }),
918
+ updateMany: vi.fn().mockResolvedValue({
919
+ acknowledged: true,
920
+ matchedCount: 0,
921
+ modifiedCount: 0
922
+ }),
923
+ deleteMany: vi.fn().mockResolvedValue({
924
+ acknowledged: true,
925
+ deletedCount: 0
926
+ }),
918
927
  getBySlug: vi.fn().mockResolvedValue(null),
919
928
  getDeleted: vi.fn().mockResolvedValue([]),
920
929
  restore: vi.fn().mockResolvedValue(null),
@@ -1741,7 +1750,7 @@ function runEventTests(resourceName, displayName, events) {
1741
1750
  * ```
1742
1751
  */
1743
1752
  async function createTestApp(options = {}) {
1744
- const { createApp } = await import("../createApp-BuvPma24.mjs").then((n) => n.r);
1753
+ const { createApp } = await import("../createApp-BwnEAO2h.mjs").then((n) => n.r);
1745
1754
  const { useInMemoryDb = true, mongoUri: providedMongoUri, ...appOptions } = options;
1746
1755
  const defaultAuth = {
1747
1756
  type: "jwt",
@@ -1,4 +1,4 @@
1
- import { t as Storage } from "../storage-CVk_SEn2.mjs";
1
+ import { t as Storage } from "../storage-BwGQXUpd.mjs";
2
2
 
3
3
  //#region src/testing/storageContract.d.ts
4
4
  interface StorageContractSetupResult {