@classytic/arc 2.13.1 → 2.14.1

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 (49) hide show
  1. package/dist/{BaseController-DX_T-bDB.mjs → BaseController-Dv60tU83.mjs} +47 -11
  2. package/dist/auth/index.d.mts +79 -2
  3. package/dist/auth/index.mjs +19 -1
  4. package/dist/{buildHandler-olo-gt94.mjs → buildHandler-BamHHpH8.mjs} +26 -5
  5. package/dist/cli/commands/describe.d.mts +1 -1
  6. package/dist/cli/commands/docs.mjs +1 -1
  7. package/dist/core/index.d.mts +3 -3
  8. package/dist/core/index.mjs +4 -4
  9. package/dist/{core-D72ia0EH.mjs → core-DEdN6zKD.mjs} +105 -8
  10. package/dist/{createActionRouter-CEvzKcy8.mjs → createActionRouter-S3MLVYot.mjs} +11 -4
  11. package/dist/{createAggregationRouter-CyecOxnO.mjs → createAggregationRouter-Bk-58SbZ.mjs} +2 -2
  12. package/dist/{createApp-XX2-N0Yd.mjs → createApp-BarYhXCZ.mjs} +4 -3
  13. package/dist/docs/index.d.mts +24 -11
  14. package/dist/docs/index.mjs +1 -1
  15. package/dist/factory/index.d.mts +1 -1
  16. package/dist/factory/index.mjs +1 -1
  17. package/dist/hooks/index.d.mts +1 -1
  18. package/dist/{index-Dz5IKsrE.d.mts → index-Bt0F3nJj.d.mts} +1 -1
  19. package/dist/{index-Ds61mrJE.d.mts → index-D1-Kp_dP.d.mts} +48 -2
  20. package/dist/{index-BtW7qYwa.d.mts → index-Dwc0orNd.d.mts} +68 -7
  21. package/dist/index.d.mts +3 -3
  22. package/dist/index.mjs +4 -4
  23. package/dist/integrations/index.d.mts +1 -1
  24. package/dist/integrations/mcp/index.d.mts +2 -2
  25. package/dist/integrations/mcp/index.mjs +1 -1
  26. package/dist/integrations/mcp/testing.d.mts +1 -1
  27. package/dist/integrations/mcp/testing.mjs +1 -1
  28. package/dist/middleware/index.d.mts +1 -1
  29. package/dist/openapi-BHXhoX8O.mjs +968 -0
  30. package/dist/org/index.d.mts +1 -1
  31. package/dist/pipeline/index.d.mts +1 -1
  32. package/dist/plugins/index.d.mts +1 -1
  33. package/dist/plugins/tracing-entry.mjs +1 -1
  34. package/dist/presets/filesUpload.d.mts +1 -1
  35. package/dist/presets/index.d.mts +1 -1
  36. package/dist/presets/multiTenant.d.mts +1 -1
  37. package/dist/presets/search.d.mts +1 -1
  38. package/dist/registry/index.d.mts +1 -1
  39. package/dist/{resourceToTools-C5coh64w.mjs → resourceToTools-CZ-ZhS7v.mjs} +4 -4
  40. package/dist/{routerShared-D6_fEGHh.mjs → routerShared-DrOa-26E.mjs} +1 -0
  41. package/dist/testing/index.d.mts +2 -2
  42. package/dist/testing/index.mjs +1 -1
  43. package/dist/types/index.d.mts +1 -1
  44. package/dist/{types-DQHFc8PM.d.mts → types-C6ONJ_Z2.d.mts} +1 -1
  45. package/dist/{types-BvqwCCSx.d.mts → types-NGtx3uxV.d.mts} +1 -1
  46. package/dist/utils/index.d.mts +1 -1
  47. package/package.json +1 -1
  48. package/dist/openapi-CiOMVW1p.mjs +0 -687
  49. /package/dist/{schemaIR-7Vl611Qs.mjs → schemaIR-lYhC2gE5.mjs} +0 -0
@@ -283,6 +283,36 @@ var BodySanitizer = class {
283
283
  }
284
284
  };
285
285
  //#endregion
286
+ //#region src/core/fieldRulePredicates.ts
287
+ /**
288
+ * True when the field is allowed to appear in client-readable surfaces
289
+ * (response payloads, `select=` whitelists, `_distinct` queries).
290
+ *
291
+ * Mirror of every read-side gate. Don't reach for `rules.systemManaged`
292
+ * here — that's a write rule.
293
+ */
294
+ function isFieldReadable(rule) {
295
+ if (!rule) return true;
296
+ return rule.hidden !== true;
297
+ }
298
+ /**
299
+ * The set of field names blocked from read-side surfaces (used by
300
+ * `QueryResolver.sanitizeSelectAny` and `BaseCrudController._distinct`).
301
+ *
302
+ * Returns `null` (not an empty array) when there are no rules to apply,
303
+ * so call-sites can early-out without creating empty allocations.
304
+ */
305
+ function collectReadBlockedFields(schemaOptions) {
306
+ const fieldRules = schemaOptions?.fieldRules;
307
+ if (!fieldRules) return null;
308
+ const blocked = /* @__PURE__ */ new Set();
309
+ for (const [field, rule] of Object.entries(fieldRules)) {
310
+ if (!rule) continue;
311
+ if (!isFieldReadable(rule)) blocked.add(field);
312
+ }
313
+ return blocked.size > 0 ? blocked : null;
314
+ }
315
+ //#endregion
286
316
  //#region src/core/QueryResolver.ts
287
317
  /**
288
318
  * QueryResolver - Composable query resolution logic extracted from BaseController.
@@ -419,10 +449,15 @@ var QueryResolver = class {
419
449
  });
420
450
  return sanitized.length > 0 ? sanitized : void 0;
421
451
  }
422
- /** Get blocked fields from schema options */
452
+ /**
453
+ * Read-side allowlist gate for `select=` / `populate=`.
454
+ *
455
+ * Only `hidden: true` blocks. `systemManaged` is a *write* rule and
456
+ * doesn't gate visibility — see `core/fieldRulePredicates.ts`.
457
+ */
423
458
  getBlockedFields(schemaOptions) {
424
- const fieldRules = schemaOptions.fieldRules ?? {};
425
- return Object.entries(fieldRules).filter(([, rules]) => rules.systemManaged || rules.hidden).map(([field]) => field);
459
+ const blocked = collectReadBlockedFields(schemaOptions);
460
+ return blocked ? Array.from(blocked) : [];
426
461
  }
427
462
  };
428
463
  //#endregion
@@ -873,15 +908,16 @@ var BaseCrudController = class {
873
908
  };
874
909
  }
875
910
  /**
876
- * True when `field` is safe to expose via `_distinct`. Mirrors the
877
- * `select` allowlist — fields marked `hidden` or `systemManaged` in
878
- * `schemaOptions.fieldRules` are NOT exposed (would leak password
879
- * hashes, internal flags, etc).
911
+ * True when `field` is safe to expose via `_distinct`.
912
+ *
913
+ * Read-side gate only only `hidden: true` blocks. `systemManaged`
914
+ * is a *write* rule (clients can't PATCH the value); the field is
915
+ * still in every list response, so blocking `_distinct` adds nothing
916
+ * but inconvenience. See `core/fieldRulePredicates.ts` for the
917
+ * canonical predicate shared with `QueryResolver`.
880
918
  */
881
919
  isFieldExposedForRead(field) {
882
- const rules = this.schemaOptions.fieldRules?.[field];
883
- if (!rules) return true;
884
- return !(rules.hidden || rules.systemManaged);
920
+ return isFieldReadable(this.schemaOptions.fieldRules?.[field]);
885
921
  }
886
922
  /** Execute list query through hooks (extracted for cache revalidation) */
887
923
  async executeListQuery(options, req) {
@@ -1353,4 +1389,4 @@ function TreeMixin(Base) {
1353
1389
  */
1354
1390
  var BaseController = class extends SoftDeleteMixin(TreeMixin(SlugMixin(BulkMixin(BaseCrudController)))) {};
1355
1391
  //#endregion
1356
- export { BulkMixin as a, BodySanitizer as c, SlugMixin as i, AccessControl as l, TreeMixin as n, BaseCrudController as o, SoftDeleteMixin as r, QueryResolver as s, BaseController as t };
1392
+ export { BulkMixin as a, collectReadBlockedFields as c, AccessControl as d, SlugMixin as i, isFieldReadable as l, TreeMixin as n, BaseCrudController as o, SoftDeleteMixin as r, QueryResolver as s, BaseController as t, BodySanitizer as u };
@@ -1,4 +1,4 @@
1
- import { Rt as AuthHelpers, zt as AuthPluginOptions } from "../index-BtW7qYwa.mjs";
1
+ import { Rt as AuthHelpers, zt as AuthPluginOptions } from "../index-Dwc0orNd.mjs";
2
2
  import { c as PermissionCheck } from "../fields-COhcH3fk.mjs";
3
3
  import { t as ExternalOpenApiPaths } from "../externalPaths-BD5nw6St.mjs";
4
4
  import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-C4Le_UB3.mjs";
@@ -191,4 +191,81 @@ interface BetterAuthOpenApiOptions {
191
191
  */
192
192
  declare function extractBetterAuthOpenApi(authApi: Record<string, unknown>, options?: BetterAuthOpenApiOptions): ExternalOpenApiPaths;
193
193
  //#endregion
194
- export { type AuthPluginOptions, type BetterAuthAdapterOptions, type BetterAuthAdapterResult, type BetterAuthHandler, type BetterAuthOpenApiOptions, MemorySessionStore, type MemorySessionStoreOptions, type SessionCookieOptions, type SessionData, type SessionManagerOptions, type SessionManagerResult, type SessionStore, _default as authPlugin, authPlugin as authPluginFn, createBetterAuthAdapter, createSessionManager, extractBetterAuthOpenApi };
194
+ //#region src/auth/trustedOrigins.d.ts
195
+ /**
196
+ * `trustedOrigins` ↔ CORS-allowlist union helper.
197
+ *
198
+ * Better Auth's `trustedOrigins` (CSRF / origin guard) and Fastify's CORS
199
+ * `origin` allowlist are independent — a request that survives CORS still
200
+ * has to pass BA's origin check. When they drift, sign-in throws
201
+ * `Invalid origin` 401s that don't surface CORS errors in the network
202
+ * panel.
203
+ *
204
+ * Hosts have written the same union by hand in every app, with the same
205
+ * three corner cases (array → union with canonical URL, `true` → `["*"]`,
206
+ * `false`/undefined → just the canonical URL). This helper centralises
207
+ * that rule so a future change happens in one place.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * import { betterAuth } from "better-auth";
212
+ * import { mirrorTrustedOriginsFromCors } from "@classytic/arc/auth";
213
+ * import config from "#config";
214
+ *
215
+ * export const auth = betterAuth({
216
+ * secret: config.betterAuth.secret,
217
+ * baseURL: process.env.BETTER_AUTH_URL,
218
+ * trustedOrigins: mirrorTrustedOriginsFromCors({
219
+ * corsOrigins: config.cors.origins, // string[] | true | false
220
+ * canonicalUrl: config.frontend.url, // FRONTEND_URL — used for email link templates
221
+ * }),
222
+ * // ...
223
+ * });
224
+ * ```
225
+ *
226
+ * Why both inputs:
227
+ * - `canonicalUrl` is the single URL embedded in BA's email templates
228
+ * (invitation-accept, password-reset). It MUST be in `trustedOrigins`.
229
+ * - `corsOrigins` is the browser allowlist. Every entry there is a real
230
+ * FE host that may attempt sign-in; missing one yields the silent 401.
231
+ *
232
+ * The union dedupes — passing `canonicalUrl` already in `corsOrigins` is
233
+ * fine and won't duplicate the entry.
234
+ */
235
+ /**
236
+ * Shape arc's CORS plugin and most apps use:
237
+ * - `string[]` — explicit allowlist
238
+ * - `true` — wildcard (`*`)
239
+ * - `false` / `undefined` — no extra origins beyond `canonicalUrl`
240
+ *
241
+ * Other shapes (regex, predicate function) aren't supported here — pass
242
+ * an explicit array if you have dynamic logic upstream.
243
+ */
244
+ type CorsOriginsConfig = readonly string[] | boolean | undefined;
245
+ interface MirrorTrustedOriginsOptions {
246
+ /**
247
+ * CORS allowlist as configured for Fastify's CORS plugin.
248
+ * Most apps read this from a `CORS_ORIGINS` env var (`*` → `true`,
249
+ * comma-separated → `string[]`).
250
+ */
251
+ corsOrigins: CorsOriginsConfig;
252
+ /**
253
+ * The single canonical FE URL used for email-link templates
254
+ * (invitation-accept, password-reset). Must be a trusted origin.
255
+ * Typically `FRONTEND_URL`.
256
+ */
257
+ canonicalUrl: string;
258
+ }
259
+ /**
260
+ * Compute BA's `trustedOrigins` as the union of `canonicalUrl` and the
261
+ * CORS allowlist.
262
+ *
263
+ * Returns:
264
+ * - `["*"]` when `corsOrigins === true` (wildcard CORS).
265
+ * - `[canonicalUrl, ...corsOrigins]` deduped when `corsOrigins` is an
266
+ * array.
267
+ * - `[canonicalUrl]` when `corsOrigins` is `false` / `undefined`.
268
+ */
269
+ declare function mirrorTrustedOriginsFromCors(options: MirrorTrustedOriginsOptions): string[];
270
+ //#endregion
271
+ export { type AuthPluginOptions, type BetterAuthAdapterOptions, type BetterAuthAdapterResult, type BetterAuthHandler, type BetterAuthOpenApiOptions, type CorsOriginsConfig, MemorySessionStore, type MemorySessionStoreOptions, type MirrorTrustedOriginsOptions, type SessionCookieOptions, type SessionData, type SessionManagerOptions, type SessionManagerResult, type SessionStore, _default as authPlugin, authPlugin as authPluginFn, createBetterAuthAdapter, createSessionManager, extractBetterAuthOpenApi, mirrorTrustedOriginsFromCors };
@@ -1029,4 +1029,22 @@ function createSessionManager(options) {
1029
1029
  };
1030
1030
  }
1031
1031
  //#endregion
1032
- export { MemorySessionStore, authPlugin_default as authPlugin, authPlugin as authPluginFn, createBetterAuthAdapter, createSessionManager, extractBetterAuthOpenApi };
1032
+ //#region src/auth/trustedOrigins.ts
1033
+ /**
1034
+ * Compute BA's `trustedOrigins` as the union of `canonicalUrl` and the
1035
+ * CORS allowlist.
1036
+ *
1037
+ * Returns:
1038
+ * - `["*"]` when `corsOrigins === true` (wildcard CORS).
1039
+ * - `[canonicalUrl, ...corsOrigins]` deduped when `corsOrigins` is an
1040
+ * array.
1041
+ * - `[canonicalUrl]` when `corsOrigins` is `false` / `undefined`.
1042
+ */
1043
+ function mirrorTrustedOriginsFromCors(options) {
1044
+ const { corsOrigins, canonicalUrl } = options;
1045
+ if (corsOrigins === true) return ["*"];
1046
+ if (!corsOrigins) return [canonicalUrl];
1047
+ return Array.from(new Set([canonicalUrl, ...corsOrigins]));
1048
+ }
1049
+ //#endregion
1050
+ export { MemorySessionStore, authPlugin_default as authPlugin, authPlugin as authPluginFn, createBetterAuthAdapter, createSessionManager, extractBetterAuthOpenApi, mirrorTrustedOriginsFromCors };
@@ -195,10 +195,10 @@ function assertBucketFieldAllowed(input) {
195
195
  if (dot > 0) {
196
196
  const a = field.slice(0, dot);
197
197
  if (lookupAliases.has(a)) return;
198
- if (blockedFields.has(a)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket "${alias}" references field "${field}" whose root "${a}" is marked hidden or systemManaged in schemaOptions.fieldRules. Bucketing on hidden fields would leak temporal info.`);
198
+ if (blockedFields.has(a)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket "${alias}" references field "${field}" whose root "${a}" is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Bucketing hidden fields would leak temporal info.`);
199
199
  return;
200
200
  }
201
- if (blockedFields.has(field)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket "${alias}" references field "${field}", but the field is marked hidden or systemManaged in schemaOptions.fieldRules. Bucketing on hidden fields would leak temporal info.`);
201
+ if (blockedFields.has(field)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket "${alias}" references field "${field}", but the field is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Bucketing hidden fields would leak temporal info.`);
202
202
  }
203
203
  /**
204
204
  * Map arc's declarative knobs onto repo-core's portable `AggExecutionHints`.
@@ -307,13 +307,34 @@ function collectLookupAliases(lookups) {
307
307
  for (const lookup of lookups) aliases.add(lookup.as ?? lookup.from);
308
308
  return aliases;
309
309
  }
310
+ /**
311
+ * Collect fields that aggregation MUST NOT reference.
312
+ *
313
+ * Default rule: only `hidden: true` blocks. `hidden` means the field is
314
+ * omitted from list/get responses, so exposing it via aggregation would
315
+ * leak data the client can't otherwise see. `systemManaged` is a write
316
+ * rule (server stamps the value, clients can't PATCH it) — those fields
317
+ * are still visible per-row, so aggregating them leaks nothing.
318
+ *
319
+ * Two opt-in overrides via `ArcFieldRule.aggregable`:
320
+ * - `aggregable: false` — explicit deny on a visible field (rare; use
321
+ * when the per-row value is fine but the across-row distribution is
322
+ * itself sensitive).
323
+ * - `aggregable: true` — explicit allow, even on a `hidden` field
324
+ * (escape hatch — caller asserts cardinality leak isn't a concern).
325
+ */
310
326
  function collectBlockedFields(schemaOptions) {
311
327
  const blocked = /* @__PURE__ */ new Set();
312
328
  const fieldRules = schemaOptions?.fieldRules;
313
329
  if (!fieldRules) return blocked;
314
330
  for (const [field, rules] of Object.entries(fieldRules)) {
315
331
  if (!rules) continue;
316
- if (rules.hidden || rules.systemManaged) blocked.add(field);
332
+ if (rules.aggregable === true) continue;
333
+ if (rules.aggregable === false) {
334
+ blocked.add(field);
335
+ continue;
336
+ }
337
+ if (rules.hidden) blocked.add(field);
317
338
  }
318
339
  return blocked;
319
340
  }
@@ -348,10 +369,10 @@ function assertFieldAllowed(context, ref, input) {
348
369
  if (dot > 0) {
349
370
  const alias = ref.slice(0, dot);
350
371
  if (lookupAliases.has(alias)) return;
351
- if (blockedFields.has(alias)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context} whose root "${alias}" is marked hidden or systemManaged in schemaOptions.fieldRules. Aggregating hidden fields would leak cardinality information.`);
372
+ if (blockedFields.has(alias)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context} whose root "${alias}" is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Aggregating hidden fields would leak cardinality information.`);
352
373
  return;
353
374
  }
354
- if (blockedFields.has(ref)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context}, but the field is marked hidden or systemManaged in schemaOptions.fieldRules. Aggregating hidden fields would leak cardinality information.`);
375
+ if (blockedFields.has(ref)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context}, but the field is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Aggregating hidden fields would leak cardinality information.`);
355
376
  }
356
377
  function extractTenantFilter(tenantOptions) {
357
378
  const out = {};
@@ -1,4 +1,4 @@
1
- import { V as ResourceDefinition, ft as RouteSchemaOptions, rt as RateLimitConfig } from "../../index-BtW7qYwa.mjs";
1
+ import { V as ResourceDefinition, ft as RouteSchemaOptions, rt as RateLimitConfig } from "../../index-Dwc0orNd.mjs";
2
2
 
3
3
  //#region src/cli/commands/describe.d.ts
4
4
  interface DescribedResource {
@@ -1,5 +1,5 @@
1
1
  import { t as ResourceRegistry } from "../../ResourceRegistry-CTERg_2x.mjs";
2
- import { t as buildOpenApiSpec } from "../../openapi-CiOMVW1p.mjs";
2
+ import { t as buildOpenApiSpec } from "../../openapi-BHXhoX8O.mjs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { mkdirSync, writeFileSync } from "node:fs";
@@ -1,3 +1,3 @@
1
- import { $t as SoftDeleteMixin, B as defineResource, Ct as AggregationConfig, Dt as AggregationMaterializedResult, Et as AggregationMaterializedContext, Ot as AggregationRateLimit, Qt as SoftDeleteExt, St as AggregationCacheConfig, Tt as AggregationIndexHint, V as ResourceDefinition, Zt as BaseController, _n as BodySanitizerConfig, an as BulkMixin, bt as AggMeasureInput, cn as QueryResolver, en as TreeExt, gn as BodySanitizer, hn as ListResult, in as BulkExt, kt as AggregationsMap, ln as QueryResolverConfig, nn as SlugExt, on as BaseControllerOptions, rn as SlugMixin, sn as BaseCrudController, tn as TreeMixin, vn as AccessControl, wt as AggregationDateRangeRequirement, xt as AggMeasureShorthand, yn as AccessControlConfig } from "../index-BtW7qYwa.mjs";
2
- import { C as MAX_FILTER_DEPTH, D as MutationOperation, E as MUTATION_OPERATIONS, O as RESERVED_QUERY_PARAMS, S as HookPhase, T as MAX_SEARCH_LENGTH, _ as DEFAULT_TENANT_FIELD, a as getControllerScope, b as HOOK_PHASES, c as createCrudRouter, d as CRUD_OPERATIONS, f as CrudOperation, g as DEFAULT_SORT, h as DEFAULT_MAX_LIMIT, i as getControllerContext, k as SYSTEM_FIELDS, l as createPermissionMiddleware, m as DEFAULT_LIMIT, n as createFastifyHandler, o as sendControllerResponse, p as DEFAULT_ID_FIELD, r as createRequestContext, s as defineResourceVariants, t as createCrudHandlers, u as defineAggregation, v as DEFAULT_UPDATE_METHOD, w as MAX_REGEX_LENGTH, x as HookOperation, y as HOOK_OPERATIONS } from "../index-Ds61mrJE.mjs";
3
- export { AccessControl, AccessControlConfig, AggMeasureInput, AggMeasureShorthand, AggregationCacheConfig, AggregationConfig, AggregationDateRangeRequirement, AggregationIndexHint, AggregationMaterializedContext, AggregationMaterializedResult, AggregationRateLimit, AggregationsMap, BaseController, BaseControllerOptions, BaseCrudController, BodySanitizer, BodySanitizerConfig, BulkExt, BulkMixin, CRUD_OPERATIONS, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugExt, SlugMixin, SoftDeleteExt, SoftDeleteMixin, TreeExt, TreeMixin, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, sendControllerResponse };
1
+ import { $t as SoftDeleteMixin, B as defineResource, Ct as AggregationConfig, Dt as AggregationMaterializedResult, Et as AggregationMaterializedContext, Ot as AggregationRateLimit, Qt as SoftDeleteExt, St as AggregationCacheConfig, Tt as AggregationIndexHint, V as ResourceDefinition, Zt as BaseController, _n as BodySanitizerConfig, an as BulkMixin, bt as AggMeasureInput, cn as QueryResolver, en as TreeExt, gn as BodySanitizer, hn as ListResult, in as BulkExt, kt as AggregationsMap, ln as QueryResolverConfig, nn as SlugExt, on as BaseControllerOptions, rn as SlugMixin, sn as BaseCrudController, tn as TreeMixin, vn as AccessControl, wt as AggregationDateRangeRequirement, xt as AggMeasureShorthand, yn as AccessControlConfig } from "../index-Dwc0orNd.mjs";
2
+ import { A as MAX_SEARCH_LENGTH, C as DEFAULT_UPDATE_METHOD, D as HookPhase, E as HookOperation, M as MutationOperation, N as RESERVED_QUERY_PARAMS, O as MAX_FILTER_DEPTH, P as SYSTEM_FIELDS, S as DEFAULT_TENANT_FIELD, T as HOOK_PHASES, _ as CrudOperation, a as createRequestContext, b as DEFAULT_MAX_LIMIT, c as sendControllerResponse, d as getEntityQuery, f as defineResourceVariants, g as CRUD_OPERATIONS, h as defineAggregation, i as createFastifyHandler, j as MUTATION_OPERATIONS, k as MAX_REGEX_LENGTH, l as getEntityId, m as createPermissionMiddleware, n as isFieldReadable, o as getControllerContext, p as createCrudRouter, r as createCrudHandlers, s as getControllerScope, t as collectReadBlockedFields, u as getEntityIdField, v as DEFAULT_ID_FIELD, w as HOOK_OPERATIONS, x as DEFAULT_SORT, y as DEFAULT_LIMIT } from "../index-D1-Kp_dP.mjs";
3
+ export { AccessControl, AccessControlConfig, AggMeasureInput, AggMeasureShorthand, AggregationCacheConfig, AggregationConfig, AggregationDateRangeRequirement, AggregationIndexHint, AggregationMaterializedContext, AggregationMaterializedResult, AggregationRateLimit, AggregationsMap, BaseController, BaseControllerOptions, BaseCrudController, BodySanitizer, BodySanitizerConfig, BulkExt, BulkMixin, CRUD_OPERATIONS, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugExt, SlugMixin, SoftDeleteExt, SoftDeleteMixin, TreeExt, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
@@ -1,5 +1,5 @@
1
1
  import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-Cxde4rpC.mjs";
2
- import { a as BulkMixin, c as BodySanitizer, i as SlugMixin, l as AccessControl, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController } from "../BaseController-DX_T-bDB.mjs";
3
- import { _ as getControllerContext, g as createRequestContext, h as createFastifyHandler, m as createCrudHandlers, v as getControllerScope, y as sendControllerResponse } from "../routerShared-D6_fEGHh.mjs";
4
- import { a as createPermissionMiddleware, i as createCrudRouter, n as defineResource, o as defineAggregation, r as ResourceDefinition, t as defineResourceVariants } from "../core-D72ia0EH.mjs";
5
- export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, sendControllerResponse };
2
+ import { a as BulkMixin, c as collectReadBlockedFields, d as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController-Dv60tU83.mjs";
3
+ import { _ as getControllerContext, g as createRequestContext, h as createFastifyHandler, m as createCrudHandlers, v as getControllerScope, y as sendControllerResponse } from "../routerShared-DrOa-26E.mjs";
4
+ import { a as defineResource, c as createPermissionMiddleware, i as defineResourceVariants, l as defineAggregation, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as createCrudRouter, t as getEntityId } from "../core-DEdN6zKD.mjs";
5
+ export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
@@ -1,11 +1,11 @@
1
1
  import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-Cxde4rpC.mjs";
2
2
  import { arcLog } from "./logger/index.mjs";
3
3
  import { A as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-_h9B3c57.mjs";
4
- import { t as BaseController } from "./BaseController-DX_T-bDB.mjs";
4
+ import { t as BaseController } from "./BaseController-Dv60tU83.mjs";
5
5
  import { t as applyPresets } from "./presets-BbkjdPeH.mjs";
6
6
  import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-De34B1ZG.mjs";
7
7
  import { t as hasEvents } from "./typeGuards-BzkXkvVv.mjs";
8
- import { b as buildRequestScopeProjection, c as buildPreHandlerChain, d as resolveRoutePreHandlers, f as resolveRouterPluginMw, h as createFastifyHandler, i as buildAuthMiddleware, l as buildRateLimitConfig, m as createCrudHandlers, o as buildCrudPermissionMw, p as selectPluginMw, r as buildArcDecorator, s as buildPipelineHandler, u as resolvePipelineSteps } from "./routerShared-D6_fEGHh.mjs";
8
+ import { b as buildRequestScopeProjection, c as buildPreHandlerChain, d as resolveRoutePreHandlers, f as resolveRouterPluginMw, h as createFastifyHandler, i as buildAuthMiddleware, l as buildRateLimitConfig, m as createCrudHandlers, o as buildCrudPermissionMw, p as selectPluginMw, r as buildArcDecorator, s as buildPipelineHandler, u as resolvePipelineSteps } from "./routerShared-DrOa-26E.mjs";
9
9
  import { t as resolveActionPermission } from "./actionPermissions-CyUkQu6O.mjs";
10
10
  //#region src/core/aggregation/defineAggregation.ts
11
11
  /**
@@ -121,7 +121,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
121
121
  * @param options - Router configuration
122
122
  */
123
123
  function createCrudRouter(fastify, controller, options = {}) {
124
- const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, routeGuards = [], routes: customRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, updateMethod = DEFAULT_UPDATE_METHOD } = options;
124
+ const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, routeGuards = [], routes: customRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, updateMethod = DEFAULT_UPDATE_METHOD, idField } = options;
125
125
  const rateLimitConfig = buildRateLimitConfig(rateLimit);
126
126
  const resourceHasQueryCache = fastify.hasDecorator("queryCache") && controller && typeof controller._cacheConfig !== "undefined" && controller._cacheConfig !== void 0;
127
127
  const pluginMw = resolveRouterPluginMw(fastify, Boolean(resourceHasQueryCache));
@@ -131,7 +131,8 @@ function createCrudRouter(fastify, controller, options = {}) {
131
131
  permissions,
132
132
  hooks: fastify.arc?.hooks,
133
133
  events: fastify.events,
134
- fields: fieldPermissions
134
+ fields: fieldPermissions,
135
+ idField
135
136
  });
136
137
  const mw = {
137
138
  list: middlewares.list ?? [],
@@ -926,15 +927,17 @@ function buildResourcePlugin(resource) {
926
927
  rateLimit: resource.rateLimit,
927
928
  updateMethod: resource.updateMethod,
928
929
  pipe: resource.pipe,
929
- fields: resource.fields
930
+ fields: resource.fields,
931
+ idField: resource.idField
930
932
  });
931
933
  if (resource.actions && Object.keys(resource.actions).length > 0) {
932
- const { createActionRouter } = await import("./createActionRouter-CEvzKcy8.mjs").then((n) => n.n);
934
+ const { createActionRouter } = await import("./createActionRouter-S3MLVYot.mjs").then((n) => n.n);
933
935
  createActionRouter(typedInstance, {
934
936
  ...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
935
937
  resourceName: resource.name,
936
938
  fields: resource.fields,
937
939
  schemaOptions: resource.schemaOptions,
940
+ idField: resource.idField,
938
941
  permissions: resource.permissions,
939
942
  routeGuards: resource.routeGuards,
940
943
  pipeline: resource.pipe,
@@ -942,7 +945,7 @@ function buildResourcePlugin(resource) {
942
945
  });
943
946
  }
944
947
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
945
- const { createAggregationRouter } = await import("./createAggregationRouter-CyecOxnO.mjs");
948
+ const { createAggregationRouter } = await import("./createAggregationRouter-Bk-58SbZ.mjs");
946
949
  const repoForAgg = resource.controller?.repository;
947
950
  const buildOptions = (req) => {
948
951
  return resource.controller?.tenantRepoOptions?.(req) ?? {};
@@ -1293,6 +1296,7 @@ function validateDefineResourceConfig(config) {
1293
1296
  validatePermissionsShape(config);
1294
1297
  validateCustomRoutePermissions(config);
1295
1298
  validateActionsShape(config);
1299
+ warnRedundantFieldRules(config);
1296
1300
  }
1297
1301
  /** Permissions must be `PermissionCheck` functions, not arbitrary values. */
1298
1302
  function validatePermissionsShape(config) {
@@ -1308,6 +1312,33 @@ function validateCustomRoutePermissions(config) {
1308
1312
  for (const route of config.routes ?? []) if (typeof route.permissions !== "function") throw new Error(`[Arc] Resource '${config.name}' route ${route.method} ${route.path}: permissions is required and must be a PermissionCheck function.`);
1309
1313
  }
1310
1314
  /**
1315
+ * Surface common field-rule misconfigurations at boot — non-fatal,
1316
+ * just a `console.warn` so hosts notice and clean up.
1317
+ *
1318
+ * Catches:
1319
+ * 1. `immutable: true` + `immutableAfterCreate: true` — `immutable`
1320
+ * already covers `immutableAfterCreate`. Picking both signals the
1321
+ * author wasn't sure which to use.
1322
+ * 2. `systemManaged: true` + `readonly: true` — both are write rules
1323
+ * and `BodySanitizer` strips on either; the second flag is dead.
1324
+ * 3. `hidden: true` + `aggregable: false` — `hidden` already blocks
1325
+ * aggregation; `aggregable: false` is redundant.
1326
+ *
1327
+ * NOT a hard error — write-rule overlap is harmless at runtime, just
1328
+ * noisy in code review.
1329
+ */
1330
+ function warnRedundantFieldRules(config) {
1331
+ const fieldRules = config.schemaOptions?.fieldRules;
1332
+ if (!fieldRules) return;
1333
+ for (const [field, rule] of Object.entries(fieldRules)) {
1334
+ if (!rule) continue;
1335
+ const r = rule;
1336
+ if (r.immutable === true && r.immutableAfterCreate === true) console.warn(`[Arc] Resource '${config.name}' fieldRules.${field}: \`immutable: true\` already implies \`immutableAfterCreate: true\` — drop the second flag.`);
1337
+ if (r.systemManaged === true && r.readonly === true) console.warn(`[Arc] Resource '${config.name}' fieldRules.${field}: \`systemManaged\` and \`readonly\` both strip writes — pick one (\`systemManaged\` is the canonical name).`);
1338
+ if (r.hidden === true && r.aggregable === false) console.warn(`[Arc] Resource '${config.name}' fieldRules.${field}: \`hidden: true\` already blocks aggregation — \`aggregable: false\` is redundant.`);
1339
+ }
1340
+ }
1341
+ /**
1311
1342
  * Actions (v2.8) — name must not collide with CRUD ops; handler +
1312
1343
  * permissions must have the right shapes. Fail at boot so production
1313
1344
  * never ships a misconfigured action endpoint.
@@ -1396,4 +1427,70 @@ function defineResourceVariants(base, variants) {
1396
1427
  return out;
1397
1428
  }
1398
1429
  //#endregion
1399
- export { createPermissionMiddleware as a, createCrudRouter as i, defineResource as n, defineAggregation as o, ResourceDefinition as r, defineResourceVariants as t };
1430
+ //#region src/core/entityHelpers.ts
1431
+ /**
1432
+ * Per-request entity helpers — read the resource binding (idField + the
1433
+ * URL `:id` value) off `req.arc`.
1434
+ *
1435
+ * Action handlers receive their `id` argument as the raw URL `:id` value.
1436
+ * When the resource declares a custom `idField` (`slug`, `reportId`, …)
1437
+ * that's NOT the document `_id`, a naive `Model.findById(id)` silently
1438
+ * returns null. The historical footgun was a `findById(id)` typo where
1439
+ * the handler author hadn't realised that `:id` resolves to the friendly
1440
+ * handle.
1441
+ *
1442
+ * `getEntityQuery(req)` produces the canonical filter shape so handlers
1443
+ * compose lookups with no resource-config recall:
1444
+ *
1445
+ * ```ts
1446
+ * actions: {
1447
+ * archive: {
1448
+ * handler: async (id, data, req) => {
1449
+ * const doc = await Model.findOne(getEntityQuery(req));
1450
+ * if (!doc) throw new NotFoundError("Order");
1451
+ * // ...
1452
+ * },
1453
+ * },
1454
+ * }
1455
+ * ```
1456
+ *
1457
+ * The router populates `req.arc.idField` and `req.arc.entityId` before
1458
+ * invoking the handler — these helpers are zero-cost reads.
1459
+ */
1460
+ /**
1461
+ * Read the resource's configured `idField` for the current request.
1462
+ * Falls back to the framework default (`_id`) when the route hasn't
1463
+ * bound an idField — keeps handlers safe to author without checking
1464
+ * the resource config.
1465
+ */
1466
+ function getEntityIdField(req) {
1467
+ return req.arc?.idField ?? "_id";
1468
+ }
1469
+ /**
1470
+ * Read the URL `:id` path param value as the resource handle.
1471
+ * Returns `undefined` when the route has no `:id` segment (collection
1472
+ * routes) — handlers that need the entity must be on row routes.
1473
+ */
1474
+ function getEntityId(req) {
1475
+ if (req.arc?.entityId !== void 0) return req.arc.entityId;
1476
+ return req.params?.id;
1477
+ }
1478
+ /**
1479
+ * Compose a `findOne` filter that resolves the current request's
1480
+ * entity, regardless of whether the resource binds `_id` or a custom
1481
+ * field. Idiomatic shape for arc action handlers.
1482
+ *
1483
+ * ```ts
1484
+ * const doc = await Model.findOne(getEntityQuery(req));
1485
+ * ```
1486
+ *
1487
+ * Returns `{}` when the route has no entity context (collection routes
1488
+ * or tests bypassing the router) — caller decides what that means.
1489
+ */
1490
+ function getEntityQuery(req) {
1491
+ const id = getEntityId(req);
1492
+ if (id === void 0) return {};
1493
+ return { [getEntityIdField(req)]: id };
1494
+ }
1495
+ //#endregion
1496
+ export { defineResource as a, createPermissionMiddleware as c, defineResourceVariants as i, defineAggregation as l, getEntityIdField as n, ResourceDefinition as o, getEntityQuery as r, createCrudRouter as s, getEntityId as t };
@@ -1,7 +1,8 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
+ import "./constants-Cxde4rpC.mjs";
2
3
  import { f as createError } from "./errors-j4aJm1Wg.mjs";
3
- import { a as buildAuthMiddlewareForPermissions, c as buildPreHandlerChain, f as resolveRouterPluginMw, l as buildRateLimitConfig, n as buildActionPipelineHandler, p as selectPluginMw, r as buildArcDecorator, t as buildActionPermissionMw, u as resolvePipelineSteps, y as sendControllerResponse } from "./routerShared-D6_fEGHh.mjs";
4
- import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-7Vl611Qs.mjs";
4
+ import { a as buildAuthMiddlewareForPermissions, c as buildPreHandlerChain, f as resolveRouterPluginMw, l as buildRateLimitConfig, n as buildActionPipelineHandler, p as selectPluginMw, r as buildArcDecorator, t as buildActionPermissionMw, u as resolvePipelineSteps, y as sendControllerResponse } from "./routerShared-DrOa-26E.mjs";
5
+ import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-lYhC2gE5.mjs";
5
6
  //#region src/core/createActionRouter.ts
6
7
  var createActionRouter_exports = /* @__PURE__ */ __exportAll({
7
8
  buildActionBodySchema: () => buildActionBodySchema,
@@ -16,7 +17,7 @@ var createActionRouter_exports = /* @__PURE__ */ __exportAll({
16
17
  * (keyed by `body.action` at request time).
17
18
  */
18
19
  function createActionRouter(fastify, config) {
19
- const { tag, resourceName = tag ?? "action", actions, actionPermissions = {}, actionSchemas = {}, globalAuth, onError, fields: fieldPermissions, schemaOptions, permissions: resourcePermissions, routeGuards = [], pipeline, rateLimit } = config;
20
+ const { tag, resourceName = tag ?? "action", actions, actionPermissions = {}, actionSchemas = {}, globalAuth, onError, fields: fieldPermissions, schemaOptions, idField = "_id", permissions: resourcePermissions, routeGuards = [], pipeline, rateLimit } = config;
20
21
  const actionEnum = Object.keys(actions);
21
22
  if (actionEnum.length === 0) {
22
23
  fastify.log.warn("[createActionRouter] No actions defined, skipping route creation");
@@ -43,7 +44,8 @@ function createActionRouter(fastify, config) {
43
44
  permissions: resourcePermissions,
44
45
  hooks: fastify.arc?.hooks,
45
46
  events: fastify.events,
46
- fields: fieldPermissions
47
+ fields: fieldPermissions,
48
+ idField
47
49
  });
48
50
  const authMw = buildAuthMiddlewareForPermissions(fastify, actionEnum.map((name) => actionPermissions[name] ?? globalAuth));
49
51
  const pluginMw = resolveRouterPluginMw(fastify, false);
@@ -69,6 +71,11 @@ function createActionRouter(fastify, config) {
69
71
  handler: async (req, reply) => {
70
72
  const { action, ...data } = req.body;
71
73
  const { id } = req.params;
74
+ const reqWithExtras = req;
75
+ reqWithExtras.arc = {
76
+ ...reqWithExtras.arc ?? {},
77
+ entityId: id
78
+ };
72
79
  const handler = wrappedHandlers.get(action);
73
80
  if (!handler) throw createError(400, `Invalid action '${action}'. Valid actions: ${actionEnum.join(", ")}`, { validActions: actionEnum });
74
81
  try {
@@ -1,6 +1,6 @@
1
1
  import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-j4aJm1Wg.mjs";
2
- import { c as buildPreHandlerChain, f as resolveRouterPluginMw, i as buildAuthMiddleware, l as buildRateLimitConfig, p as selectPluginMw, r as buildArcDecorator } from "./routerShared-D6_fEGHh.mjs";
3
- import { r as validateAggregations, t as buildAggregationHandler } from "./buildHandler-olo-gt94.mjs";
2
+ import { c as buildPreHandlerChain, f as resolveRouterPluginMw, i as buildAuthMiddleware, l as buildRateLimitConfig, p as selectPluginMw, r as buildArcDecorator } from "./routerShared-DrOa-26E.mjs";
3
+ import { r as validateAggregations, t as buildAggregationHandler } from "./buildHandler-BamHHpH8.mjs";
4
4
  //#region src/core/aggregation/createAggregationRouter.ts
5
5
  /**
6
6
  * Register one Fastify route per aggregation. No-op when the map is
@@ -393,9 +393,10 @@ async function registerOne(parent, resource) {
393
393
  try {
394
394
  await parent.register(resource.toPlugin());
395
395
  } catch (err) {
396
- const msg = err instanceof Error ? err.message : String(err);
397
- parent.log.error(`Failed to register resource "${name}": ${msg}`);
398
- throw new Error(`Resource "${name}" failed to register: ${msg}. Check the resource definition, adapter, and permissions.`, { cause: err });
396
+ const rawMsg = err instanceof Error ? err.message : String(err);
397
+ const stripped = rawMsg.replace(new RegExp(`^Resource "${name}"\\s*`), "").replace(/\.+\s*$/, "");
398
+ parent.log.error(`Failed to register resource "${name}": ${rawMsg}`);
399
+ throw new Error(`Resource "${name}" failed to register — ${stripped}.`, { cause: err });
399
400
  }
400
401
  }
401
402
  /**
@@ -1,8 +1,14 @@
1
- import { p as RegistryEntry } from "../index-BtW7qYwa.mjs";
1
+ import { p as RegistryEntry } from "../index-Dwc0orNd.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "../externalPaths-BD5nw6St.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
 
5
- //#region src/docs/openapi.d.ts
5
+ //#region src/docs/openapi/types.d.ts
6
+ /**
7
+ * OpenAPI 3.0 type primitives used by arc's spec emitter.
8
+ *
9
+ * Internal to `src/docs/openapi/*` — public exports are surfaced via
10
+ * `src/docs/index.ts` (which re-exports `OpenApiSpec` only).
11
+ */
6
12
  interface OpenApiOptions {
7
13
  /** API title */
8
14
  title?: string;
@@ -23,6 +29,13 @@ interface OpenApiOptions {
23
29
  /** Custom OpenAPI extensions */
24
30
  extensions?: Record<string, unknown>;
25
31
  }
32
+ interface OpenApiBuildOptions {
33
+ title?: string;
34
+ version?: string;
35
+ description?: string;
36
+ serverUrl?: string;
37
+ apiPrefix?: string;
38
+ }
26
39
  interface OpenApiSpec {
27
40
  openapi: string;
28
41
  info: {
@@ -45,13 +58,6 @@ interface OpenApiSpec {
45
58
  }>;
46
59
  security?: Array<Record<string, string[]>>;
47
60
  }
48
- interface OpenApiBuildOptions {
49
- title?: string;
50
- version?: string;
51
- description?: string;
52
- serverUrl?: string;
53
- apiPrefix?: string;
54
- }
55
61
  interface PathItem {
56
62
  get?: Operation;
57
63
  post?: Operation;
@@ -101,7 +107,7 @@ interface Response {
101
107
  }>;
102
108
  }
103
109
  interface SchemaObject {
104
- type?: string;
110
+ type?: string | string[];
105
111
  format?: string;
106
112
  properties?: Record<string, SchemaObject>;
107
113
  items?: SchemaObject;
@@ -110,12 +116,17 @@ interface SchemaObject {
110
116
  description?: string;
111
117
  example?: unknown;
112
118
  additionalProperties?: boolean | SchemaObject;
113
- enum?: string[];
119
+ enum?: (string | number | boolean | null)[];
114
120
  minimum?: number;
115
121
  maximum?: number;
116
122
  minLength?: number;
117
123
  maxLength?: number;
118
124
  pattern?: string;
125
+ oneOf?: SchemaObject[];
126
+ anyOf?: SchemaObject[];
127
+ allOf?: SchemaObject[];
128
+ default?: unknown;
129
+ nullable?: boolean;
119
130
  }
120
131
  interface SecurityScheme {
121
132
  type: string;
@@ -124,6 +135,8 @@ interface SecurityScheme {
124
135
  in?: string;
125
136
  name?: string;
126
137
  }
138
+ //#endregion
139
+ //#region src/docs/openapi/index.d.ts
127
140
  declare const openApiPlugin: FastifyPluginAsync<OpenApiOptions>;
128
141
  /**
129
142
  * Build OpenAPI spec from registry resources.
@@ -1,5 +1,5 @@
1
1
  import { t as getUserRoles } from "../types-D57iXYb8.mjs";
2
- import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-CiOMVW1p.mjs";
2
+ import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-BHXhoX8O.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/docs/scalar.ts
5
5
  const scalarPlugin = async (fastify, opts = {}) => {