@classytic/arc 2.16.0 → 2.17.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 (140) hide show
  1. package/README.md +22 -2
  2. package/dist/{BaseController-DlCCTIxJ.mjs → BaseController-Dmeh4Y_y.mjs} +5 -47
  3. package/dist/{QueryCache-SvmT_9ti.d.mts → QueryCache-8YmAQ-zW.d.mts} +1 -1
  4. package/dist/{ResourceRegistry-f48hFk3m.mjs → ResourceRegistry-CzSzVK1m.mjs} +3 -0
  5. package/dist/audit/index.d.mts +1 -1
  6. package/dist/audit/index.mjs +2 -1
  7. package/dist/auth/index.d.mts +4 -4
  8. package/dist/auth/index.mjs +2 -2
  9. package/dist/auth/redis-session.d.mts +1 -1
  10. package/dist/auth/redis-session.mjs +28 -2
  11. package/dist/{buildHandler-BZX6zzDM.mjs → buildHandler-C_SkBNF3.mjs} +6 -4
  12. package/dist/cache/index.d.mts +3 -3
  13. package/dist/cli/commands/describe.d.mts +1 -1
  14. package/dist/cli/commands/describe.mjs +1 -1
  15. package/dist/cli/commands/docs.mjs +2 -2
  16. package/dist/cli/commands/generate.d.mts +1 -1
  17. package/dist/cli/commands/generate.mjs +1 -1
  18. package/dist/cli/commands/init.d.mts +1 -1
  19. package/dist/cli/commands/init.mjs +1 -1
  20. package/dist/cli/commands/introspect.mjs +1 -1
  21. package/dist/cli/index.d.mts +2 -2
  22. package/dist/cli/index.mjs +2 -2
  23. package/dist/core/index.d.mts +2 -2
  24. package/dist/core/index.mjs +3 -3
  25. package/dist/{core-DBJ_j6rX.mjs → core-DYJvu7aI.mjs} +174 -31
  26. package/dist/{createActionRouter-DUpN3Dd1.mjs → createActionRouter-BHUMb1-y.mjs} +6 -3
  27. package/dist/{createAggregationRouter-Dq-TUCuY.mjs → createAggregationRouter-BYVvhfc5.mjs} +3 -3
  28. package/dist/{createApp-DNccuhyI.mjs → createApp-CzCEEsEm.mjs} +9 -9
  29. package/dist/{defineEvent-DRwY0fYm.mjs → defineEvent-NPKUJRt8.mjs} +1 -1
  30. package/dist/docs/index.d.mts +2 -2
  31. package/dist/docs/index.mjs +2 -2
  32. package/dist/{elevation-Dci0AYLT.mjs → elevation-6mfTLBC-.mjs} +1 -1
  33. package/dist/{errorHandler-DpoXQHZ9.mjs → errorHandler-wQejMBT3.mjs} +45 -4
  34. package/dist/{eventPlugin-C2cGqtRO.mjs → eventPlugin-CnJCUyM0.mjs} +1 -1
  35. package/dist/{eventPlugin-CtHC_av1.d.mts → eventPlugin-DjEx_J3j.d.mts} +1 -1
  36. package/dist/events/index.d.mts +3 -3
  37. package/dist/events/index.mjs +5 -5
  38. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  39. package/dist/events/transports/redis-stream-entry.mjs +34 -1
  40. package/dist/events/transports/redis.d.mts +1 -1
  41. package/dist/events/transports/redis.mjs +17 -1
  42. package/dist/factory/index.d.mts +2 -2
  43. package/dist/factory/index.mjs +4 -3
  44. package/dist/fastify-augmentation-7LIR0nDv.d.mts +42 -0
  45. package/dist/{fields-Anj0xdih.d.mts → fields-DqZLp5d6.d.mts} +1 -1
  46. package/dist/hooks/index.d.mts +1 -1
  47. package/dist/idempotency/index.d.mts +2 -2
  48. package/dist/idempotency/index.mjs +21 -5
  49. package/dist/idempotency/redis.d.mts +1 -1
  50. package/dist/{index-3oIimXQn.d.mts → index-3L-adFJO.d.mts} +3 -3
  51. package/dist/{index-CkW0flkU.d.mts → index-C1JSBan-.d.mts} +256 -8
  52. package/dist/{index-B-ulKx5P.d.mts → index-CruZir1o.d.mts} +36 -16
  53. package/dist/index.d.mts +5 -5
  54. package/dist/index.mjs +5 -5
  55. package/dist/integrations/event-gateway.d.mts +2 -2
  56. package/dist/integrations/event-gateway.mjs +1 -1
  57. package/dist/integrations/index.d.mts +2 -2
  58. package/dist/integrations/jobs.d.mts +36 -1
  59. package/dist/integrations/jobs.mjs +82 -9
  60. package/dist/integrations/mcp/index.d.mts +67 -5
  61. package/dist/integrations/mcp/index.mjs +93 -2
  62. package/dist/integrations/mcp/testing.d.mts +1 -1
  63. package/dist/integrations/mcp/testing.mjs +1 -1
  64. package/dist/integrations/websocket-redis.d.mts +1 -1
  65. package/dist/integrations/websocket.d.mts +1 -1
  66. package/dist/integrations/websocket.mjs +2 -1
  67. package/dist/{loadResourcesFromEntry-BLMEI2Xa.mjs → loadResourcesFromEntry-DCjYMgfC.mjs} +1 -1
  68. package/dist/{mcpPlugin-7vGV51ED.mjs → mcpPlugin-hZE00H74.mjs} +114 -11
  69. package/dist/middleware/index.d.mts +1 -1
  70. package/dist/middleware/index.mjs +1 -1
  71. package/dist/{multipartBody-BOvVSVCD.mjs → multipartBody-VjVWmZl8.mjs} +2 -1
  72. package/dist/{openapi-34T9yNwd.mjs → openapi-DKDs9NfD.mjs} +37 -10
  73. package/dist/permissions/index.d.mts +2 -2
  74. package/dist/permissions/index.mjs +2 -2
  75. package/dist/{permissions-CTxMrreC.mjs → permissions-B1Qy55fg.mjs} +169 -169
  76. package/dist/pipeline/index.d.mts +1 -1
  77. package/dist/plugins/index.d.mts +5 -59
  78. package/dist/plugins/index.mjs +7 -7
  79. package/dist/plugins/tracing-entry.d.mts +1 -1
  80. package/dist/plugins/tracing-entry.mjs +1 -1
  81. package/dist/preloadResources-BOXzPhKU.mjs +45 -0
  82. package/dist/presets/filesUpload.d.mts +4 -4
  83. package/dist/presets/filesUpload.mjs +2 -2
  84. package/dist/presets/index.d.mts +1 -1
  85. package/dist/presets/index.mjs +1 -1
  86. package/dist/presets/multiTenant.d.mts +1 -1
  87. package/dist/presets/search.d.mts +2 -2
  88. package/dist/presets/search.mjs +1 -1
  89. package/dist/{presets-C9BE6WaZ.mjs → presets-BYbPobbF.mjs} +5 -3
  90. package/dist/{queryCachePlugin-Biqzfbi5.d.mts → queryCachePlugin-Do4P_WBC.d.mts} +2 -2
  91. package/dist/{redis-Cyzrz6SX.d.mts → redis-cpqVb85w.d.mts} +1 -1
  92. package/dist/{redis-stream-DT-YjzrB.d.mts → redis-stream-BuiAS3aE.d.mts} +1 -1
  93. package/dist/registry/index.d.mts +30 -8
  94. package/dist/registry/index.mjs +2 -2
  95. package/dist/{registry-BBE23CDj.mjs → registry-BrlHkrRS.mjs} +14 -8
  96. package/dist/{routerShared-CZV5aabX.mjs → routerShared-nCyJGOKX.mjs} +164 -17
  97. package/dist/scope/index.d.mts +2 -2
  98. package/dist/scope/index.mjs +2 -2
  99. package/dist/testing/index.d.mts +2 -25
  100. package/dist/testing/index.mjs +2 -45
  101. package/dist/testing/storageContract.d.mts +1 -1
  102. package/dist/types/index.d.mts +4 -4
  103. package/dist/types/storage.d.mts +1 -1
  104. package/dist/{types-D-fYtKjb.d.mts → types-BRmS8TNa.d.mts} +34 -11
  105. package/dist/{types-BsJMEQ4D.d.mts → types-CoMjNmkF.d.mts} +12 -1
  106. package/dist/utils/index.d.mts +83 -5
  107. package/dist/utils/index.mjs +2 -2
  108. package/dist/{utils-DC5ycPfr.mjs → utils-CvD3glpM.mjs} +168 -3
  109. package/dist/{validate-By96rH0r.mjs → validate-DFIP2VrM.mjs} +65 -3
  110. package/dist/{versioning-ZwX9tmbS.d.mts → versioning-5H4930tj.d.mts} +1 -1
  111. package/package.json +3 -3
  112. package/skills/arc/SKILL.md +43 -1
  113. package/skills/arc/references/api-reference.md +2 -0
  114. package/skills/arc/references/mcp.md +30 -1
  115. /package/dist/{EventTransport-C-2oAHtw.d.mts → EventTransport-B1GFiYUy.d.mts} +0 -0
  116. /package/dist/{EventTransport-Hxvv5QQz.mjs → EventTransport-ClEDfiMS.mjs} +0 -0
  117. /package/dist/{caching-TeHE8G-v.mjs → caching-8MfrrUYU.mjs} +0 -0
  118. /package/dist/{elevation-0YBpa663.d.mts → elevation-DvwtjDwu.d.mts} +0 -0
  119. /package/dist/{errorHandler-mHuyWzZE.d.mts → errorHandler-DuqTqeqn.d.mts} +0 -0
  120. /package/dist/{errors-C1lX_jlm.d.mts → errors-4ZloYFvm.d.mts} +0 -0
  121. /package/dist/{externalPaths-DFg-2KTp.d.mts → externalPaths-ClykOQqa.d.mts} +0 -0
  122. /package/dist/{generate-CYac-OLv.mjs → generate-CcM24hoq.mjs} +0 -0
  123. /package/dist/{generate-BWFwgcCM.d.mts → generate-cRnsafMS.d.mts} +0 -0
  124. /package/dist/{init-Dv71MsJr.d.mts → init-D-vrTZ1G.d.mts} +0 -0
  125. /package/dist/{init-HDvoO9L5.mjs → init-DYzfniuX.mjs} +0 -0
  126. /package/dist/{interface-CH0OQudo.d.mts → interface-BVkZsvEe.d.mts} +0 -0
  127. /package/dist/{interface-NwJ_qPlY.d.mts → interface-DoE6UoC2.d.mts} +0 -0
  128. /package/dist/{loadResources-ChQEj8ih.mjs → loadResources-fol6JuvW.mjs} +0 -0
  129. /package/dist/{metrics-TuOmguhi.mjs → metrics-B-PU4-Yu.mjs} +0 -0
  130. /package/dist/{replyHelpers-C-gD32oF.mjs → replyHelpers-boS5Rx26.mjs} +0 -0
  131. /package/dist/{schemaIR-Ctc89DSn.mjs → schemaIR-DAOvBWgT.mjs} +0 -0
  132. /package/dist/{sessionManager-BqFegc0W.d.mts → sessionManager-DlmaHUAl.d.mts} +0 -0
  133. /package/dist/{sse-BY6sTy4P.mjs → sse-jh5OqFjR.mjs} +0 -0
  134. /package/dist/{storage-D2KZJAmn.d.mts → storage-HcAgP5Ni.d.mts} +0 -0
  135. /package/dist/{store-helpers-B0sunfZZ.mjs → store-helpers-5JgaxieH.mjs} +0 -0
  136. /package/dist/{tracing-Dm8n7Cnn.d.mts → tracing-bRqqUxFO.d.mts} +0 -0
  137. /package/dist/{types-DVfpSfx2.d.mts → types-CfapW2t8.d.mts} +0 -0
  138. /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
  139. /package/dist/{versioning-B6mimogM.mjs → versioning-CuIwUQZ2.mjs} +0 -0
  140. /package/dist/{websocket-BkjeGZRn.d.mts → websocket-BJ0w-3Dt.d.mts} +0 -0
@@ -1,4 +1,4 @@
1
- import { J as CrudRouteKey, R as RegisterOptions, V as ResourceDefinition, at as ResolvedTenantPurge, k as IntrospectionPluginOptions, p as RegistryEntry, z as ResourceRegistry } from "../index-CkW0flkU.mjs";
1
+ import { J as CrudRouteKey, R as RegisterOptions, V as ResourceDefinition, at as ResolvedTenantPurge, k as IntrospectionPluginOptions, p as RegistryEntry, z as ResourceRegistry } from "../index-C1JSBan-.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
  import { TenantPurgeProgress, TenantPurgeStrategy } from "@classytic/repo-core/repository";
4
4
 
@@ -20,7 +20,14 @@ interface AssertNoTenantDataOptions {
20
20
  interface TenantDataLeak {
21
21
  readonly resource: string;
22
22
  readonly tenantField: string;
23
- readonly strategy: TenantPurgeStrategy["type"];
23
+ /**
24
+ * Full `TenantPurgeStrategy` discriminated union — not just `.type`.
25
+ * `anonymize` carries the field map; `custom` carries the handler
26
+ * descriptor; auditors get the actionable signal without re-resolving
27
+ * the resource definition. Narrow on `.strategy.type` for typed
28
+ * access.
29
+ */
30
+ readonly strategy: TenantPurgeStrategy;
24
31
  readonly expected: number;
25
32
  readonly actual: number;
26
33
  readonly reason?: string;
@@ -64,7 +71,14 @@ declare function assertNoTenantData(registry: ResourceRegistry, options: AssertN
64
71
  interface PurgeResourceOutcome {
65
72
  readonly resource: string;
66
73
  readonly tenantField: string;
67
- readonly strategy: TenantPurgeStrategy["type"];
74
+ /**
75
+ * Full `TenantPurgeStrategy` discriminated union. Audit consumers
76
+ * narrow on `.strategy.type` for typed access — `skip` carries the
77
+ * mandatory `reason`, `anonymize` carries the field map, `custom`
78
+ * carries the handler descriptor. Pre-2.17 this was the `.type` tag
79
+ * only, which dropped the audit-critical `reason` field.
80
+ */
81
+ readonly strategy: TenantPurgeStrategy;
68
82
  readonly processed: number;
69
83
  readonly ok: boolean;
70
84
  readonly path: "purgeByField" | "legacy-deleteMany" | "skipped" | "unsupported";
@@ -92,11 +106,13 @@ interface CascadeResourceReport {
92
106
  */
93
107
  readonly deletedCount: number;
94
108
  /**
95
- * Strategy that actually ran for this resource — `'hard'` / `'soft'` /
96
- * `'anonymize'` / `'skip'`. Audit consumers branch on this to answer
97
- * "did data physically leave the system?".
109
+ * Full `TenantPurgeStrategy` that ran for this resource. Branch on
110
+ * `.strategy.type` (`'hard'` / `'soft'` / `'anonymize'` / `'skip'` /
111
+ * `'custom'`) to answer "did data physically leave the system?". The
112
+ * `skip` variant carries the mandatory `reason`; `anonymize` carries
113
+ * the field map; `custom` carries the handler descriptor.
98
114
  */
99
- readonly strategy?: TenantPurgeStrategy["type"];
115
+ readonly strategy?: TenantPurgeStrategy;
100
116
  /**
101
117
  * Where the strategy came from — `'declared'` (host wrote
102
118
  * `onTenantDelete`) or `'disabled'` (filtered out before reaching
@@ -221,11 +237,17 @@ declare function getCascadingResources(registry: ResourceRegistry): readonly str
221
237
  * cascading resource. Use for audit dashboards that answer "what
222
238
  * happens to this resource on org-delete?" without grepping the
223
239
  * source.
240
+ *
241
+ * **Returns the full `TenantPurgeStrategy` discriminated union**, not
242
+ * just its `type` tag. The `skip` variant carries a required `reason`
243
+ * field — auditors need it to sign off on every "we deliberately don't
244
+ * purge this" entry. Anonymize carries the field map. Custom carries
245
+ * the handler reference. Narrow on `.strategy.type` for typed access.
224
246
  */
225
247
  declare function getCascadingResourcesWithMetadata(registry: ResourceRegistry): readonly {
226
248
  name: string;
227
249
  tenantField: string;
228
- strategy: TenantPurgeStrategy["type"];
250
+ strategy: TenantPurgeStrategy;
229
251
  source: ResolvedTenantPurge["source"];
230
252
  priority: number;
231
253
  }[];
@@ -1,3 +1,3 @@
1
- import { a as cascadeDeleteForOrganization, c as assertNoTenantData, i as introspectionPlugin_default, n as buildResourceManifestFromRegistry, o as getCascadingResources, r as introspectionPlugin, s as getCascadingResourcesWithMetadata, t as buildResourceManifest } from "../registry-BBE23CDj.mjs";
2
- import { t as ResourceRegistry } from "../ResourceRegistry-f48hFk3m.mjs";
1
+ import { a as cascadeDeleteForOrganization, c as assertNoTenantData, i as introspectionPlugin_default, n as buildResourceManifestFromRegistry, o as getCascadingResources, r as introspectionPlugin, s as getCascadingResourcesWithMetadata, t as buildResourceManifest } from "../registry-BrlHkrRS.mjs";
2
+ import { t as ResourceRegistry } from "../ResourceRegistry-CzSzVK1m.mjs";
3
3
  export { ResourceRegistry, assertNoTenantData, buildResourceManifest, buildResourceManifestFromRegistry, cascadeDeleteForOrganization, getCascadingResources, getCascadingResourcesWithMetadata, introspectionPlugin_default as introspectionPlugin, introspectionPlugin as introspectionPluginFn };
@@ -59,7 +59,7 @@ async function assertNoTenantData(registry, options) {
59
59
  if (actual !== expected) leaks.push({
60
60
  resource: r.name,
61
61
  tenantField,
62
- strategy: strategy.type,
62
+ strategy,
63
63
  expected,
64
64
  actual
65
65
  });
@@ -88,7 +88,7 @@ async function purgeResource(resourceName, tenantField, organizationId, resolved
88
88
  if (strategy.type === "skip") return {
89
89
  resource: resourceName,
90
90
  tenantField,
91
- strategy: "skip",
91
+ strategy,
92
92
  processed: 0,
93
93
  ok: true,
94
94
  path: "skipped",
@@ -103,7 +103,7 @@ async function purgeResource(resourceName, tenantField, organizationId, resolved
103
103
  return {
104
104
  resource: resourceName,
105
105
  tenantField,
106
- strategy: result.strategy,
106
+ strategy,
107
107
  processed: result.processed,
108
108
  ok: result.ok,
109
109
  path: "purgeByField",
@@ -114,7 +114,7 @@ async function purgeResource(resourceName, tenantField, organizationId, resolved
114
114
  if (strategy.type !== "hard") return {
115
115
  resource: resourceName,
116
116
  tenantField,
117
- strategy: strategy.type,
117
+ strategy,
118
118
  processed: 0,
119
119
  ok: false,
120
120
  path: "unsupported",
@@ -127,7 +127,7 @@ async function purgeResource(resourceName, tenantField, organizationId, resolved
127
127
  if (!op) return {
128
128
  resource: resourceName,
129
129
  tenantField,
130
- strategy: "hard",
130
+ strategy,
131
131
  processed: 0,
132
132
  ok: false,
133
133
  path: "unsupported",
@@ -142,7 +142,7 @@ async function purgeResource(resourceName, tenantField, organizationId, resolved
142
142
  return {
143
143
  resource: resourceName,
144
144
  tenantField,
145
- strategy: "hard",
145
+ strategy,
146
146
  processed: typeof result === "number" ? result : typeof result?.deletedCount === "number" ? result.deletedCount : typeof result?.count === "number" ? result.count : -1,
147
147
  ok: true,
148
148
  path: "legacy-deleteMany",
@@ -152,7 +152,7 @@ async function purgeResource(resourceName, tenantField, organizationId, resolved
152
152
  return {
153
153
  resource: resourceName,
154
154
  tenantField,
155
- strategy: "hard",
155
+ strategy,
156
156
  processed: 0,
157
157
  ok: false,
158
158
  path: "legacy-deleteMany",
@@ -178,6 +178,12 @@ function getCascadingResources(registry) {
178
178
  * cascading resource. Use for audit dashboards that answer "what
179
179
  * happens to this resource on org-delete?" without grepping the
180
180
  * source.
181
+ *
182
+ * **Returns the full `TenantPurgeStrategy` discriminated union**, not
183
+ * just its `type` tag. The `skip` variant carries a required `reason`
184
+ * field — auditors need it to sign off on every "we deliberately don't
185
+ * purge this" entry. Anonymize carries the field map. Custom carries
186
+ * the handler reference. Narrow on `.strategy.type` for typed access.
181
187
  */
182
188
  function getCascadingResourcesWithMetadata(registry) {
183
189
  return registry.getAll().filter((r) => isResourceCascading(r)).map((r) => {
@@ -191,7 +197,7 @@ function getCascadingResourcesWithMetadata(registry) {
191
197
  return {
192
198
  name: r.name,
193
199
  tenantField,
194
- strategy: r0.strategy.type,
200
+ strategy: r0.strategy,
195
201
  source: r0.source,
196
202
  priority: r0.priority
197
203
  };
@@ -1,10 +1,63 @@
1
- import { f as createError } from "./errors-j4aJm1Wg.mjs";
1
+ import { h as SYSTEM_FIELDS } from "./constants-TrJVIJl0.mjs";
2
+ import { f as createError, r as ForbiddenError } from "./errors-j4aJm1Wg.mjs";
2
3
  import { b as isElevated, c as getOrgId, g as getUserId, n as PUBLIC_SCOPE, x as isMember } from "./types-Bi0r0vjG.mjs";
3
4
  import { t as requestContext } from "./requestContext-SSaaTgW8.mjs";
4
- import { I as evaluateAndApplyPermission, p as resolveEffectiveRoles, u as applyFieldReadPermissions } from "./permissions-CTxMrreC.mjs";
5
- import { t as getUserRoles } from "./types-D57iXYb8.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";
6
7
  import { t as executePipeline } from "./pipe-DiCyvyPN.mjs";
7
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
8
61
  //#region src/scope/projection.ts
9
62
  /**
10
63
  * Compute the request-scope projection. Returns `undefined` when no
@@ -55,9 +108,13 @@ function applyFieldMask(data, fieldMask) {
55
108
  return data;
56
109
  }
57
110
  /**
58
- * Create IRequestContext from Fastify request
59
- *
60
- * Extracts framework-agnostic context from Fastify-specific request object
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.
61
118
  */
62
119
  function createRequestContext(req) {
63
120
  const reqWithExtras = req;
@@ -145,12 +202,33 @@ function computeFieldCapabilities(fieldPerms, effectiveRoles) {
145
202
  return caps;
146
203
  }
147
204
  /**
148
- * Send IControllerResponse via Fastify reply
205
+ * Send a controller's `IControllerResponse` through Fastify's reply.
149
206
  *
150
- * Converts framework-agnostic response to Fastify response
151
- * Applies field masking if specified in request
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.
152
226
  */
153
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
+ };
154
232
  const reqWithExtras = request;
155
233
  const fieldMaskConfig = reqWithExtras?.fieldMask;
156
234
  const arcMeta = reqWithExtras?.arc;
@@ -207,18 +285,17 @@ function sendControllerResponse(reply, response, request) {
207
285
  reply.code(status).send(filteredData);
208
286
  }
209
287
  /**
210
- * Create Fastify route handler from IController method
211
- *
212
- * Wraps framework-agnostic controller method in Fastify-specific handler
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.
213
294
  *
214
295
  * @example
215
296
  * ```typescript
216
297
  * const controller = new BaseController(repository);
217
- *
218
- * // Create Fastify handler
219
298
  * const listHandler = createFastifyHandler(controller.list.bind(controller));
220
- *
221
- * // Register route
222
299
  * fastify.get('/products', listHandler);
223
300
  * ```
224
301
  */
@@ -573,5 +650,75 @@ function describeValue(v) {
573
650
  if (Array.isArray(v)) return `an array of length ${v.length}`;
574
651
  return `${typeof v} (${JSON.stringify(v).slice(0, 80)})`;
575
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
+ }
576
723
  //#endregion
577
- export { getControllerContext as _, buildAuthMiddlewareForPermissions as a, buildRequestScopeProjection as b, buildPreHandlerChain as c, resolveRoutePreHandlers as d, resolveRouterPluginMw as f, createRequestContext as g, createFastifyHandler as h, buildAuthMiddleware as i, buildRateLimitConfig as l, createCrudHandlers as m, buildActionPipelineHandler as n, buildCrudPermissionMw as o, selectPluginMw as p, buildArcDecorator as r, buildPipelineHandler as s, buildActionPermissionMw as t, resolvePipelineSteps as u, getControllerScope as v, sendControllerResponse as y };
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 };
@@ -1,5 +1,5 @@
1
- import { C as isMember, D as requireOrgId, E as requireClientId, O as requireTeamId, S as isElevated, T as isService, _ as getTenantFromRequest, a as getAncestorOrgIds, b as hasOrgAccess, c as getMandate, d as getOrgRoles, f as getRequestScope, g as getTeamId, h as getServiceScopes, i as RequestScope, k as requireUserId, l as getOrgContext, m as getScopeContextMap, n as Mandate, o as getClientId, p as getScopeContext, r as PUBLIC_SCOPE, s as getDPoPJkt, t as AUTHENTICATED_SCOPE, u as getOrgId, v as getUserId, w as isOrgInScope, x as isAuthenticated, y as getUserRoles } from "../types-DVfpSfx2.mjs";
2
- import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-0YBpa663.mjs";
1
+ import { C as isMember, D as requireOrgId, E as requireClientId, O as requireTeamId, S as isElevated, T as isService, _ as getTenantFromRequest, a as getAncestorOrgIds, b as hasOrgAccess, c as getMandate, d as getOrgRoles, f as getRequestScope, g as getTeamId, h as getServiceScopes, i as RequestScope, k as requireUserId, l as getOrgContext, m as getScopeContextMap, n as Mandate, o as getClientId, p as getScopeContext, r as PUBLIC_SCOPE, s as getDPoPJkt, t as AUTHENTICATED_SCOPE, u as getOrgId, v as getUserId, w as isOrgInScope, x as isAuthenticated, y as getUserRoles } from "../types-CfapW2t8.mjs";
2
+ import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-DvwtjDwu.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 { C as isService, D as requireUserId, E as requireTeamId, S as isOrgInScope, T as requireOrgId, _ as getUserRoles, a as getDPoPJkt, b as isElevated, c as getOrgId, d as getScopeContext, f as getScopeContextMap, g as getUserId, h as getTenantFromRequest, i as getClientId, l as getOrgRoles, m as getTeamId, n as PUBLIC_SCOPE, o as getMandate, p as getServiceScopes, r as getAncestorOrgIds, s as getOrgContext, t as AUTHENTICATED_SCOPE, u as getRequestScope, v as hasOrgAccess, w as requireClientId, x as isMember, y as isAuthenticated } from "../types-Bi0r0vjG.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-6mfTLBC-.mjs";
4
4
  //#region src/scope/rateLimitKey.ts
5
5
  function createTenantKeyGenerator(opts) {
6
6
  if (opts?.strategy) return opts.strategy;
@@ -1,5 +1,5 @@
1
- import { Kt as AnyRecord, V as ResourceDefinition } from "../index-CkW0flkU.mjs";
2
- import { d as ResourceLike, r as CreateAppOptions } from "../types-D-fYtKjb.mjs";
1
+ import { Kt as AnyRecord, V as ResourceDefinition } from "../index-C1JSBan-.mjs";
2
+ import { d as preloadResourcesAsync, r as CreateAppOptions, u as preloadResources } from "../types-BRmS8TNa.mjs";
3
3
  import { StorageContractSetup, StorageContractSetupResult, runStorageContract } from "./storageContract.mjs";
4
4
  import { FastifyInstance, FastifyServerOptions } from "fastify";
5
5
  import { Mock } from "vitest";
@@ -464,29 +464,6 @@ declare function createTestTimer(): {
464
464
  reset: () => void;
465
465
  };
466
466
  //#endregion
467
- //#region src/testing/preloadResources.d.ts
468
- /** Eager glob result: `{ '/path/to/file.ts': resourceModule }` */
469
- type EagerGlobResult = Record<string, unknown>;
470
- /** Lazy glob result: `{ '/path/to/file.ts': () => Promise<unknown> }` */
471
- type LazyGlobResult = Record<string, () => Promise<unknown>>;
472
- /**
473
- * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
474
- *
475
- * Accepts either:
476
- * - `{ import: 'default' }` form: values are the resource directly
477
- * - default form: values are the full module — picks first export with `toPlugin()`
478
- *
479
- * Throws if any module doesn't yield a valid `ResourceLike`.
480
- */
481
- declare function preloadResources(globResult: EagerGlobResult): ResourceLike[];
482
- /**
483
- * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
484
- *
485
- * Use this when resources depend on prior bootstrap (e.g., engine init) and
486
- * cannot be evaluated at import time of the preload file.
487
- */
488
- declare function preloadResourcesAsync(globResult: LazyGlobResult): Promise<ResourceLike[]>;
489
- //#endregion
490
467
  //#region src/testing/testApp.d.ts
491
468
  type DbMode = "in-memory" | {
492
469
  uri: string;
@@ -1,4 +1,5 @@
1
1
  import { t as CRUD_OPERATIONS } from "../constants-TrJVIJl0.mjs";
2
+ import { n as preloadResourcesAsync, t as preloadResources } from "../preloadResources-BOXzPhKU.mjs";
2
3
  import { runStorageContract } from "./storageContract.mjs";
3
4
  import Fastify from "fastify";
4
5
  import { afterAll, describe, expect, it, vi } from "vitest";
@@ -970,50 +971,6 @@ function createTestTimer() {
970
971
  };
971
972
  }
972
973
  //#endregion
973
- //#region src/testing/preloadResources.ts
974
- /**
975
- * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
976
- *
977
- * Accepts either:
978
- * - `{ import: 'default' }` form: values are the resource directly
979
- * - default form: values are the full module — picks first export with `toPlugin()`
980
- *
981
- * Throws if any module doesn't yield a valid `ResourceLike`.
982
- */
983
- function preloadResources(globResult) {
984
- const resources = [];
985
- for (const [path, value] of Object.entries(globResult)) {
986
- const resource = pickResource(value);
987
- if (!resource) throw new Error(`preloadResources: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
988
- resources.push(resource);
989
- }
990
- return resources.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
991
- }
992
- /**
993
- * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
994
- *
995
- * Use this when resources depend on prior bootstrap (e.g., engine init) and
996
- * cannot be evaluated at import time of the preload file.
997
- */
998
- async function preloadResourcesAsync(globResult) {
999
- return (await Promise.all(Object.entries(globResult).map(async ([path, loader]) => {
1000
- const resource = pickResource(await loader());
1001
- if (!resource) throw new Error(`preloadResourcesAsync: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
1002
- return resource;
1003
- }))).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
1004
- }
1005
- function pickResource(value) {
1006
- if (!value || typeof value !== "object") return void 0;
1007
- if (typeof value.toPlugin === "function") return value;
1008
- const mod = value;
1009
- const candidates = [
1010
- mod.default,
1011
- mod.resource,
1012
- ...Object.values(mod)
1013
- ];
1014
- for (const c of candidates) if (c && typeof c === "object" && typeof c.toPlugin === "function") return c;
1015
- }
1016
- //#endregion
1017
974
  //#region src/testing/testApp.ts
1018
975
  /**
1019
976
  * createTestApp — test app factory for arc
@@ -1079,7 +1036,7 @@ function pickDefaultAuth(authMode, callerAuth) {
1079
1036
  };
1080
1037
  }
1081
1038
  async function createTestApp(options = {}) {
1082
- const { createApp } = await import("../createApp-DNccuhyI.mjs").then((n) => n.r);
1039
+ const { createApp } = await import("../createApp-CzCEEsEm.mjs").then((n) => n.r);
1083
1040
  const { resources = [], db = "in-memory", connectMongoose = false, authMode = "jwt", defaultOrgId, plugins, auth: callerAuth, ...appOptions } = options;
1084
1041
  let dbHandle;
1085
1042
  let dbUri;
@@ -1,4 +1,4 @@
1
- import { t as Storage } from "../storage-D2KZJAmn.mjs";
1
+ import { t as Storage } from "../storage-HcAgP5Ni.mjs";
2
2
 
3
3
  //#region src/testing/storageContract.d.ts
4
4
  interface StorageContractSetupResult {
@@ -1,5 +1,5 @@
1
- import { $ as OnTenantDeleteConfig, A as RequestIdOptions, Bt as AuthHelpers, C as RequestContext, D as HealthCheck, E as GracefulShutdownOptions, F as FastifyWithDecorators, G as ActionsMap, Gt as TokenPair, H as ActionDefinition, Ht as Authenticator, I as MiddlewareHandler, J as CrudRouteKey, Jt as ArcRequest, K as ArcFieldRule, Kt as AnyRecord, L as RequestWithExtras, M as EventsDecorator, N as FastifyRequestExtras, O as HealthOptions, P as FastifyWithAuth, Q as MiddlewareConfig, Qt as UserOrganization, S as QueryParserInterface, T as CrudRouterOptions, U as ActionEntry, Ut as AuthenticatorContext, Vt as AuthPluginOptions, W as ActionHandlerFn, Wt as JwtContext, X as EventDefinition, Xt as ObjectId, Y as CrudSchemas, Yt as JWTPayload, Z as FieldRule, Zt as UserLike, _ as ControllerQueryOptions, _t as FastifyHandler, a as InferAdapterDoc, at as ResolvedTenantPurge, b as ParsedQuery, bt as IRequestContext, c as TypedController, cn as BaseControllerOptions, ct as ResourceHookContext, d as PaginationResult, dt as RouteDefinition, et as OpenApiSchemas, f as IntrospectionData, ft as RouteMcpConfig, g as ArcInternalMetadata, gt as ControllerLike, h as ResourceMetadata, ht as ControllerHandler, i as ValidationResult, it as RateLimitConfig, j as ArcDecorator, k as IntrospectionPluginOptions, l as TypedRepository, lt as ResourceHooks, m as RegistryStats, mt as RouteSchemaOptions, n as ConfigError, nt as PresetHook, o as InferDocType, ot as ResourceCacheConfig, p as RegistryEntry, pt as RouteMethod, q as CrudController, qt as ApiResponse, r as ValidateOptions, rt as PresetResult, s as InferResourceDoc, st as ResourceConfig, t as RouteHandlerMethod, tt as PresetFunction, u as TypedResourceConfig, ut as ResourcePermissions, v as LookupOption, vt as IController, w as ServiceContext, x as PopulateOption, xt as RouteHandler, y as OwnershipCheck, yt as IControllerResponse } from "../index-CkW0flkU.mjs";
2
- import { i as RequestScope } from "../types-DVfpSfx2.mjs";
3
- import { c as PermissionCheck, d as UserBase, l as PermissionContext, u as PermissionResult } from "../fields-Anj0xdih.mjs";
4
- import { n as ElevationOptions, t as ElevationEvent } from "../elevation-0YBpa663.mjs";
1
+ import { $ as OnTenantDeleteConfig, A as RequestIdOptions, Bt as AuthHelpers, C as RequestContext, D as HealthCheck, E as GracefulShutdownOptions, F as FastifyWithDecorators, G as ActionsMap, Gt as TokenPair, H as ActionDefinition, Ht as Authenticator, I as MiddlewareHandler, J as CrudRouteKey, Jt as ArcRequest, K as ArcFieldRule, Kt as AnyRecord, L as RequestWithExtras, M as EventsDecorator, N as FastifyRequestExtras, O as HealthOptions, P as FastifyWithAuth, Q as MiddlewareConfig, Qt as UserOrganization, S as QueryParserInterface, T as CrudRouterOptions, U as ActionEntry, Ut as AuthenticatorContext, Vt as AuthPluginOptions, W as ActionHandlerFn, Wt as JwtContext, X as EventDefinition, Xt as ObjectId, Y as CrudSchemas, Yt as JWTPayload, Z as FieldRule, Zt as UserLike, _ as ControllerQueryOptions, _t as FastifyHandler, a as InferAdapterDoc, at as ResolvedTenantPurge, b as ParsedQuery, bt as IRequestContext, c as TypedController, cn as BaseControllerOptions, ct as ResourceHookContext, d as PaginationResult, dt as RouteDefinition, et as OpenApiSchemas, f as IntrospectionData, ft as RouteMcpConfig, g as ArcInternalMetadata, gt as ControllerLike, h as ResourceMetadata, ht as ControllerHandler, i as ValidationResult, it as RateLimitConfig, j as ArcDecorator, k as IntrospectionPluginOptions, l as TypedRepository, lt as ResourceHooks, m as RegistryStats, mt as RouteSchemaOptions, n as ConfigError, nt as PresetHook, o as InferDocType, ot as ResourceCacheConfig, p as RegistryEntry, pt as RouteMethod, q as CrudController, qt as ApiResponse, r as ValidateOptions, rt as PresetResult, s as InferResourceDoc, st as ResourceConfig, t as RouteHandlerMethod, tt as PresetFunction, u as TypedResourceConfig, ut as ResourcePermissions, v as LookupOption, vt as IController, w as ServiceContext, x as PopulateOption, xt as RouteHandler, y as OwnershipCheck, yt as IControllerResponse } from "../index-C1JSBan-.mjs";
2
+ import { i as RequestScope } from "../types-CfapW2t8.mjs";
3
+ import { c as PermissionCheck, d as UserBase, l as PermissionContext, u as PermissionResult } from "../fields-DqZLp5d6.mjs";
4
+ import { n as ElevationOptions, t as ElevationEvent } from "../elevation-DvwtjDwu.mjs";
5
5
  export { ActionDefinition, ActionEntry, ActionHandlerFn, ActionsMap, AnyRecord, ApiResponse, ArcDecorator, ArcFieldRule, ArcInternalMetadata, ArcRequest, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRouteKey, CrudRouterOptions, CrudSchemas, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OnTenantDeleteConfig, OpenApiSchemas, OwnershipCheck, PaginationResult, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResolvedTenantPurge, ResourceCacheConfig, ResourceConfig, ResourceHookContext, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteDefinition, RouteHandler, RouteHandlerMethod, RouteMcpConfig, RouteMethod, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult };
@@ -1,2 +1,2 @@
1
- import { a as StorageReadResult, i as StorageReadRange, n as StorageContext, o as StorageUploadInput, r as StorageFile, t as Storage } from "../storage-D2KZJAmn.mjs";
1
+ import { a as StorageReadResult, i as StorageReadRange, n as StorageContext, o as StorageUploadInput, r as StorageFile, t as Storage } from "../storage-HcAgP5Ni.mjs";
2
2
  export { Storage, StorageContext, StorageFile, StorageReadRange, StorageReadResult, StorageUploadInput };
@@ -1,13 +1,13 @@
1
- import { r as CacheStore } from "./interface-CH0OQudo.mjs";
2
- import { Ht as Authenticator } from "./index-CkW0flkU.mjs";
3
- import { n as ElevationOptions } from "./elevation-0YBpa663.mjs";
4
- import { a as EventTransport } from "./EventTransport-C-2oAHtw.mjs";
5
- import { t as ExternalOpenApiPaths } from "./externalPaths-DFg-2KTp.mjs";
6
- import { r as QueryCachePluginOptions } from "./queryCachePlugin-Biqzfbi5.mjs";
7
- import { t as EventPluginOptions } from "./eventPlugin-CtHC_av1.mjs";
8
- import { _ as HealthOptions, f as CachingOptions, l as SSEOptions, o as MetricsOptions, t as VersioningOptions } from "./versioning-ZwX9tmbS.mjs";
9
- import { t as ErrorHandlerOptions } from "./errorHandler-mHuyWzZE.mjs";
10
- import { r as IdempotencyStore } from "./interface-NwJ_qPlY.mjs";
1
+ import { r as CacheStore } from "./interface-BVkZsvEe.mjs";
2
+ import { Ht as Authenticator } from "./index-C1JSBan-.mjs";
3
+ import { n as ElevationOptions } from "./elevation-DvwtjDwu.mjs";
4
+ import { a as EventTransport } from "./EventTransport-B1GFiYUy.mjs";
5
+ import { t as ExternalOpenApiPaths } from "./externalPaths-ClykOQqa.mjs";
6
+ import { r as QueryCachePluginOptions } from "./queryCachePlugin-Do4P_WBC.mjs";
7
+ import { t as EventPluginOptions } from "./eventPlugin-DjEx_J3j.mjs";
8
+ import { _ as HealthOptions, f as CachingOptions, l as SSEOptions, o as MetricsOptions, t as VersioningOptions } from "./versioning-5H4930tj.mjs";
9
+ import { t as ErrorHandlerOptions } from "./errorHandler-DuqTqeqn.mjs";
10
+ import { r as IdempotencyStore } from "./interface-DoE6UoC2.mjs";
11
11
  import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, FastifyServerOptions } from "fastify";
12
12
 
13
13
  //#region src/factory/loadResources.d.ts
@@ -203,6 +203,29 @@ interface LoadResourcesOptions<TContext = unknown> {
203
203
  */
204
204
  declare function loadResources<TContext = unknown>(dir: string, options?: LoadResourcesOptions<TContext>): Promise<ResourceLike[]>;
205
205
  //#endregion
206
+ //#region src/testing/preloadResources.d.ts
207
+ /** Eager glob result: `{ '/path/to/file.ts': resourceModule }` */
208
+ type EagerGlobResult = Record<string, unknown>;
209
+ /** Lazy glob result: `{ '/path/to/file.ts': () => Promise<unknown> }` */
210
+ type LazyGlobResult = Record<string, () => Promise<unknown>>;
211
+ /**
212
+ * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
213
+ *
214
+ * Accepts either:
215
+ * - `{ import: 'default' }` form: values are the resource directly
216
+ * - default form: values are the full module — picks first export with `toPlugin()`
217
+ *
218
+ * Throws if any module doesn't yield a valid `ResourceLike`.
219
+ */
220
+ declare function preloadResources(globResult: EagerGlobResult): ResourceLike[];
221
+ /**
222
+ * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
223
+ *
224
+ * Use this when resources depend on prior bootstrap (e.g., engine init) and
225
+ * cannot be evaluated at import time of the preload file.
226
+ */
227
+ declare function preloadResourcesAsync(globResult: LazyGlobResult): Promise<ResourceLike[]>;
228
+ //#endregion
206
229
  //#region src/factory/types.d.ts
207
230
  type CorsOptions = Record<string, unknown> & {
208
231
  origin?: unknown;
@@ -1041,4 +1064,4 @@ interface RawBodyOptions {
1041
1064
  runFirst?: boolean;
1042
1065
  }
1043
1066
  //#endregion
1044
- export { CustomPluginAuthOption as a, RawBodyOptions as c, ResourceLike as d, ResourceModule as f, CustomAuthenticatorOption as i, UnderPressureOptions as l, BetterAuthOption as n, JwtAuthOption as o, loadResources as p, CreateAppOptions as r, MultipartOptions as s, AuthOption as t, LoadResourcesOptions as u };
1067
+ export { CustomPluginAuthOption as a, RawBodyOptions as c, preloadResourcesAsync as d, LoadResourcesOptions as f, loadResources as h, CustomAuthenticatorOption as i, UnderPressureOptions as l, ResourceModule as m, BetterAuthOption as n, JwtAuthOption as o, ResourceLike as p, CreateAppOptions as r, MultipartOptions as s, AuthOption as t, preloadResources as u };
@@ -1,4 +1,4 @@
1
- import { V as ResourceDefinition } from "./index-CkW0flkU.mjs";
1
+ import { V as ResourceDefinition } from "./index-C1JSBan-.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/integrations/mcp/types.d.ts
@@ -93,6 +93,17 @@ interface ToolDefinition<TSession = McpAuthResult> {
93
93
  outputSchema?: Record<string, z.ZodTypeAny>;
94
94
  annotations?: ToolAnnotations;
95
95
  handler: (input: Record<string, unknown>, ctx: ToolContext<TSession>) => Promise<CallToolResult>;
96
+ /**
97
+ * Origin label used by collision diagnostics — names BOTH the resource
98
+ * and the surface that produced this tool (`'crud:post:list'`,
99
+ * `'action:post:approve'`, `'route:post:GET /export'`,
100
+ * `'preset:softDelete:post:restore'`). When two tools share a name,
101
+ * `createMcpServer()` prints both labels so the host can see which
102
+ * surfaces collided without spelunking through the MCP SDK's stack
103
+ * trace. Optional — hand-written tools without a `source` simply
104
+ * appear as `'(unknown)'` in collision messages.
105
+ */
106
+ source?: string;
96
107
  }
97
108
  /** Output of definePrompt() — plain data, not yet registered */
98
109
  interface PromptDefinition {