@classytic/arc 2.6.1 → 2.6.2

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 (42) hide show
  1. package/README.md +37 -2
  2. package/dist/{ResourceRegistry-DeCIFlix.mjs → ResourceRegistry-C6ngvOnn.mjs} +1 -0
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/audit/index.d.mts +31 -5
  5. package/dist/audit/index.mjs +21 -3
  6. package/dist/auth/index.d.mts +1 -1
  7. package/dist/cli/commands/docs.mjs +1 -1
  8. package/dist/cli/commands/introspect.mjs +1 -1
  9. package/dist/core/index.d.mts +2 -2
  10. package/dist/core/index.mjs +1 -1
  11. package/dist/{createApp-Bol7DLUf.mjs → createApp-D2w0LdYJ.mjs} +27 -11
  12. package/dist/{defineResource-bVKHjQzE.mjs → defineResource-Ckxg6HrZ.mjs} +9 -3
  13. package/dist/docs/index.d.mts +1 -1
  14. package/dist/dynamic/index.d.mts +1 -1
  15. package/dist/dynamic/index.mjs +1 -1
  16. package/dist/factory/index.d.mts +1 -1
  17. package/dist/factory/index.mjs +31 -15
  18. package/dist/hooks/index.d.mts +1 -1
  19. package/dist/{index-Cb3gtbg7.d.mts → index-B4uZm82R.d.mts} +1 -1
  20. package/dist/{index-BIsZ_su5.d.mts → index-DrCqa3Jq.d.mts} +1 -1
  21. package/dist/index.d.mts +3 -3
  22. package/dist/index.mjs +2 -2
  23. package/dist/integrations/index.d.mts +1 -1
  24. package/dist/integrations/mcp/index.d.mts +2 -2
  25. package/dist/integrations/mcp/testing.d.mts +1 -1
  26. package/dist/{interface-DDW43OmS.d.mts → interface-CrN45qz1.d.mts} +35 -0
  27. package/dist/org/index.d.mts +1 -1
  28. package/dist/plugins/index.d.mts +1 -1
  29. package/dist/plugins/index.mjs +1 -1
  30. package/dist/plugins/tracing-entry.mjs +1 -1
  31. package/dist/presets/index.d.mts +1 -1
  32. package/dist/presets/multiTenant.d.mts +1 -1
  33. package/dist/registry/index.d.mts +1 -1
  34. package/dist/registry/index.mjs +1 -1
  35. package/dist/testing/index.d.mts +26 -3
  36. package/dist/testing/index.mjs +46 -2
  37. package/dist/types/index.d.mts +1 -1
  38. package/dist/{types-D5hJ-k_3.d.mts → types-C1Z28coa.d.mts} +7 -1
  39. package/dist/{types-D5rjsS_i.d.mts → types-DurlBP2N.d.mts} +1 -1
  40. package/dist/utils/index.d.mts +1 -1
  41. package/package.json +1 -1
  42. package/skills/arc/SKILL.md +55 -8
package/README.md CHANGED
@@ -34,7 +34,7 @@ Three ways to register resources:
34
34
 
35
35
  ```typescript
36
36
  // Auto-discover from directory (recommended)
37
- resources: await loadResources('./src/resources'),
37
+ resources: await loadResources(import.meta.url), // dev/prod parity
38
38
 
39
39
  // Explicit array
40
40
  resources: [productResource, orderResource],
@@ -43,7 +43,42 @@ resources: [productResource, orderResource],
43
43
  plugins: async (f) => { await f.register(productResource.toPlugin()); },
44
44
  ```
45
45
 
46
- > **Note:** `loadResources()` works with standard relative imports and Node.js `#` subpath imports (`package.json` `imports` field). It does **not** support tsconfig path aliases (`@/*`, `~/`) — use explicit `resources: [...]` instead.
46
+ `loadResources()` discovers `default` exports, `export const resource`, OR any named export with `toPlugin()` (e.g. `export const userResource`). Per-resource opt-out of `resourcePrefix` via `skipGlobalPrefix: true` for webhooks/admin routes.
47
+
48
+ > **Import compatibility:** Works with relative imports and Node.js `#` subpath imports. Does **not** support tsconfig path aliases (`@/*`, `~/`) — use explicit `resources: [...]` instead.
49
+
50
+ ## Boot Sequence
51
+
52
+ ```typescript
53
+ const app = await createApp({
54
+ resourcePrefix: '/api/v1',
55
+ plugins: async (f) => { await connectDB(); }, // 1. infra (DB, docs)
56
+ bootstrap: [inventoryInit, accountingInit], // 2. domain init
57
+ resources: await loadResources(import.meta.url), // 3. routes
58
+ afterResources: async (f) => { subscribeEvents(f); }, // 4. post-wiring
59
+ onReady: async (f) => { logger.info('ready'); },
60
+ });
61
+ ```
62
+
63
+ ## Audit (per-resource opt-in)
64
+
65
+ Clean DX without growing exclude lists:
66
+
67
+ ```typescript
68
+ // app.ts — register once with perResource mode
69
+ await fastify.register(auditPlugin, { autoAudit: { perResource: true } });
70
+
71
+ // order.resource.ts — opt in
72
+ defineResource({ name: 'order', audit: true });
73
+
74
+ // payment.resource.ts — only audit deletes
75
+ defineResource({ name: 'payment', audit: { operations: ['delete'] } });
76
+
77
+ // Manual logging from MCP tools or custom routes
78
+ app.post('/orders/:id/refund', async (req) => {
79
+ await app.audit.custom('order', req.params.id, 'refund', { reason }, { user });
80
+ });
81
+ ```
47
82
 
48
83
  ## defineResource
49
84
 
@@ -51,6 +51,7 @@ var ResourceRegistry = class {
51
51
  fieldPermissions: extractFieldPermissions(resource.fields),
52
52
  pipelineSteps: extractPipelineSteps(resource.pipe),
53
53
  rateLimit: resource.rateLimit,
54
+ audit: resource.audit,
54
55
  plugin: resource.toPlugin()
55
56
  };
56
57
  this._resources.set(resource.name, entry);
@@ -1,3 +1,3 @@
1
- import { a as RepositoryLike, i as RelationMetadata, n as DataAdapter, o as SchemaMetadata, r as FieldMetadata, s as ValidationResult, t as AdapterFactory } from "../interface-DDW43OmS.mjs";
2
- import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter } from "../index-BIsZ_su5.mjs";
1
+ import { a as RepositoryLike, i as RelationMetadata, n as DataAdapter, o as SchemaMetadata, r as FieldMetadata, s as ValidationResult, t as AdapterFactory } from "../interface-CrN45qz1.mjs";
2
+ import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter } from "../index-DrCqa3Jq.mjs";
3
3
  export { AdapterFactory, DataAdapter, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, createMongooseAdapter, createPrismaAdapter };
@@ -19,14 +19,40 @@ interface AuditPluginOptions {
19
19
  * Automatically audit CRUD operations via the hook system (default: true when enabled).
20
20
  * When enabled, create/update/delete operations are auto-logged without manual calls.
21
21
  *
22
- * - `true`: Auto-audit all CRUD operations on all resources
23
- * - `{ operations: ['create', 'delete'] }`: Only auto-audit specific operations
24
- * - `{ exclude: ['health', 'metrics'] }`: Skip specific resources
25
- * - `false`: Disable auto-audit (manual calls only)
22
+ * **Three opt-in patterns** pick the one that matches your app:
23
+ *
24
+ * 1. **Per-resource opt-in (recommended for most apps)** set `audit: true` on each
25
+ * resource. Audit only fires for those resources. No global `include`/`exclude` needed.
26
+ * ```ts
27
+ * defineResource({ name: 'order', audit: true });
28
+ * // auditPlugin auto-detects which resources opted in
29
+ * ```
30
+ *
31
+ * 2. **Allowlist mode** — set `include: ['order', 'invoice']` for centralized config.
32
+ * Only listed resources are audited.
33
+ *
34
+ * 3. **Denylist mode** — set `exclude: ['health', 'metrics']` to audit everything except
35
+ * listed resources. Use sparingly — leads to growing exclude lists.
36
+ *
37
+ * Default behavior (`autoAudit: true`): denylist mode with no exclusions (audit everything).
38
+ * For most apps, switching to per-resource opt-in is cleaner.
39
+ *
40
+ * - `true`: Audit all CRUD operations on all resources (legacy default)
41
+ * - `{ operations: ['create', 'delete'] }`: Only specific operations
42
+ * - `{ include: ['order'] }`: Allowlist — only listed resources
43
+ * - `{ exclude: ['health'] }`: Denylist — all except listed
44
+ * - `{ perResource: true }`: Only resources with `audit: true` in their definition
45
+ * - `false`: Disable auto-audit (manual `fastify.audit.*()` calls only)
26
46
  */
27
47
  autoAudit?: boolean | {
28
- operations?: ("create" | "update" | "delete")[];
48
+ operations?: ("create" | "update" | "delete")[]; /** Allowlist — only listed resources are audited (mutually exclusive with exclude) */
49
+ include?: string[]; /** Denylist — audit everything except listed resources */
29
50
  exclude?: string[];
51
+ /**
52
+ * Per-resource opt-in mode: only audit resources with `audit: true` in their
53
+ * `defineResource()` config. The cleanest pattern for most apps.
54
+ */
55
+ perResource?: boolean;
30
56
  };
31
57
  }
32
58
  declare module "fastify" {
@@ -180,16 +180,34 @@ const auditPlugin = async (fastify, opts = {}) => {
180
180
  "update",
181
181
  "delete"
182
182
  ];
183
- const ops = typeof autoAuditConfig === "object" ? autoAuditConfig.operations ?? defaultOps : defaultOps;
184
- const excludeResources = new Set(typeof autoAuditConfig === "object" ? autoAuditConfig.exclude ?? [] : []);
183
+ const isObj = typeof autoAuditConfig === "object";
184
+ const ops = isObj ? autoAuditConfig.operations ?? defaultOps : defaultOps;
185
+ const includeResources = isObj && autoAuditConfig.include ? new Set(autoAuditConfig.include) : null;
186
+ const excludeResources = new Set(isObj ? autoAuditConfig.exclude ?? [] : []);
187
+ const perResourceMode = isObj ? autoAuditConfig.perResource === true : false;
188
+ if (includeResources && excludeResources.size > 0) fastify.log?.warn?.("Audit autoAudit: both 'include' and 'exclude' specified. Using 'include' (allowlist wins).");
185
189
  fastify.addHook("onReady", async () => {
186
190
  const arc = "arc" in fastify ? fastify.arc : void 0;
187
191
  if (!arc?.hooks) {
188
192
  fastify.log?.debug?.("Auto-audit skipped: arc-core plugin not registered");
189
193
  return;
190
194
  }
195
+ const optedInResources = /* @__PURE__ */ new Set();
196
+ const operationsByResource = /* @__PURE__ */ new Map();
197
+ if (perResourceMode && arc.registry) for (const entry of arc.registry.getAll()) {
198
+ const auditFlag = entry.audit;
199
+ if (!auditFlag) continue;
200
+ optedInResources.add(entry.name);
201
+ if (typeof auditFlag === "object" && auditFlag.operations) operationsByResource.set(entry.name, auditFlag.operations);
202
+ }
191
203
  for (const op of ops) arc.hooks.after("*", op, async (ctx) => {
192
- if (excludeResources.has(ctx.resource)) return;
204
+ if (perResourceMode) {
205
+ if (!optedInResources.has(ctx.resource)) return;
206
+ const allowedOps = operationsByResource.get(ctx.resource);
207
+ if (allowedOps && !allowedOps.includes(op)) return;
208
+ } else if (includeResources) {
209
+ if (!includeResources.has(ctx.resource)) return;
210
+ } else if (excludeResources.has(ctx.resource)) return;
193
211
  const docId = autoAuditExtractId(ctx.result);
194
212
  const scope = ctx.context?._scope;
195
213
  const auditCtx = {
@@ -1,4 +1,4 @@
1
- import { h as AuthPluginOptions, m as AuthHelpers } from "../interface-DDW43OmS.mjs";
1
+ import { h as AuthPluginOptions, m as AuthHelpers } from "../interface-CrN45qz1.mjs";
2
2
  import { t as PermissionCheck } from "../types-BNUccdcf.mjs";
3
3
  import { t as ExternalOpenApiPaths } from "../externalPaths-DpO-s7r8.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-wbkYj2HL.mjs";
@@ -1,4 +1,4 @@
1
- import { t as ResourceRegistry } from "../../ResourceRegistry-DeCIFlix.mjs";
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-C6ngvOnn.mjs";
2
2
  import { t as buildOpenApiSpec } from "../../openapi-CBmZ6EQN.mjs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
@@ -1,4 +1,4 @@
1
- import { t as ResourceRegistry } from "../../ResourceRegistry-DeCIFlix.mjs";
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-C6ngvOnn.mjs";
2
2
  import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  //#region src/cli/commands/introspect.ts
@@ -1,3 +1,3 @@
1
- import { At as AccessControlConfig, Bt as ResourceDefinition, Ct as BaseController, Dt as BodySanitizer, Et as QueryResolverConfig, Ot as BodySanitizerConfig, Tt as QueryResolver, Vt as defineResource, kt as AccessControl, wt as BaseControllerOptions } from "../interface-DDW43OmS.mjs";
2
- import { A as RESERVED_QUERY_PARAMS, C as HookOperation, D as MAX_SEARCH_LENGTH, E as MAX_REGEX_LENGTH, O as MUTATION_OPERATIONS, S as HOOK_PHASES, T as MAX_FILTER_DEPTH, _ as DEFAULT_MAX_LIMIT, a as getControllerScope, b as DEFAULT_UPDATE_METHOD, c as createPermissionMiddleware, d as IdempotencyService, f as createActionRouter, g as DEFAULT_LIMIT, h as DEFAULT_ID_FIELD, i as getControllerContext, j as SYSTEM_FIELDS, k as MutationOperation, l as ActionHandler, m as CrudOperation, n as createFastifyHandler, o as sendControllerResponse, p as CRUD_OPERATIONS, r as createRequestContext, s as createCrudRouter, t as createCrudHandlers, u as ActionRouterConfig, v as DEFAULT_SORT, w as HookPhase, x as HOOK_OPERATIONS, y as DEFAULT_TENANT_FIELD } from "../index-Cb3gtbg7.mjs";
1
+ import { At as AccessControlConfig, Bt as ResourceDefinition, Ct as BaseController, Dt as BodySanitizer, Et as QueryResolverConfig, Ot as BodySanitizerConfig, Tt as QueryResolver, Vt as defineResource, kt as AccessControl, wt as BaseControllerOptions } from "../interface-CrN45qz1.mjs";
2
+ import { A as RESERVED_QUERY_PARAMS, C as HookOperation, D as MAX_SEARCH_LENGTH, E as MAX_REGEX_LENGTH, O as MUTATION_OPERATIONS, S as HOOK_PHASES, T as MAX_FILTER_DEPTH, _ as DEFAULT_MAX_LIMIT, a as getControllerScope, b as DEFAULT_UPDATE_METHOD, c as createPermissionMiddleware, d as IdempotencyService, f as createActionRouter, g as DEFAULT_LIMIT, h as DEFAULT_ID_FIELD, i as getControllerContext, j as SYSTEM_FIELDS, k as MutationOperation, l as ActionHandler, m as CrudOperation, n as createFastifyHandler, o as sendControllerResponse, p as CRUD_OPERATIONS, r as createRequestContext, s as createCrudRouter, t as createCrudHandlers, u as ActionRouterConfig, v as DEFAULT_SORT, w as HookPhase, x as HOOK_OPERATIONS, y as DEFAULT_TENANT_FIELD } from "../index-B4uZm82R.mjs";
3
3
  export { AccessControl, AccessControlConfig, ActionHandler, ActionRouterConfig, BaseController, BaseControllerOptions, BodySanitizer, BodySanitizerConfig, 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, IdempotencyService, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, createActionRouter, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineResource, getControllerContext, getControllerScope, 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
2
  import { i as AccessControl, n as QueryResolver, r as BodySanitizer, t as BaseController } from "../BaseController-AbbRx3e0.mjs";
3
3
  import { t as createActionRouter } from "../core-C1XCMtqM.mjs";
4
- import { c as createCrudHandlers, d as getControllerContext, f as getControllerScope, l as createFastifyHandler, n as defineResource, o as createCrudRouter, p as sendControllerResponse, s as createPermissionMiddleware, t as ResourceDefinition, u as createRequestContext } from "../defineResource-bVKHjQzE.mjs";
4
+ import { c as createCrudHandlers, d as getControllerContext, f as getControllerScope, l as createFastifyHandler, n as defineResource, o as createCrudRouter, p as sendControllerResponse, s as createPermissionMiddleware, t as ResourceDefinition, u as createRequestContext } from "../defineResource-Ckxg6HrZ.mjs";
5
5
  export { AccessControl, BaseController, BodySanitizer, 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, createActionRouter, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineResource, getControllerContext, getControllerScope, sendControllerResponse };
@@ -66,20 +66,36 @@ const productionPreset = {
66
66
  }
67
67
  };
68
68
  /**
69
+ * Try to detect if `pino-pretty` is installed (devDep). Returns the transport
70
+ * config if available, or falls back to plain JSON logging. This prevents the
71
+ * common "pino-pretty not found" crash in production when someone uses the
72
+ * development preset by mistake (or via NODE_ENV-based preset selection).
73
+ */
74
+ function devLoggerConfig() {
75
+ try {
76
+ const req = eval("require") ?? null;
77
+ if (req?.resolve) {
78
+ req.resolve("pino-pretty");
79
+ return {
80
+ level: "debug",
81
+ transport: {
82
+ target: "pino-pretty",
83
+ options: {
84
+ colorize: true,
85
+ translateTime: "SYS:HH:MM:ss",
86
+ ignore: "pid,hostname"
87
+ }
88
+ }
89
+ };
90
+ }
91
+ } catch {}
92
+ return { level: "debug" };
93
+ }
94
+ /**
69
95
  * Development preset - relaxed security, verbose logging
70
96
  */
71
97
  const developmentPreset = {
72
- logger: {
73
- level: "debug",
74
- transport: {
75
- target: "pino-pretty",
76
- options: {
77
- colorize: true,
78
- translateTime: "SYS:HH:MM:ss",
79
- ignore: "pid,hostname"
80
- }
81
- }
82
- },
98
+ logger: devLoggerConfig(),
83
99
  trustProxy: true,
84
100
  helmet: { contentSecurityPolicy: false },
85
101
  cors: {
@@ -1013,19 +1013,23 @@ function defineResource(config) {
1013
1013
  resource._pendingHooks.push(...inlineHooks);
1014
1014
  }
1015
1015
  if (!config.skipRegistry) try {
1016
- let openApiSchemas = config.openApiSchemas;
1017
- if (!openApiSchemas && config.adapter?.generateSchemas) {
1016
+ let openApiSchemas;
1017
+ if (config.adapter?.generateSchemas) {
1018
1018
  const generated = config.adapter.generateSchemas(config.schemaOptions);
1019
1019
  if (generated) openApiSchemas = generated;
1020
1020
  }
1021
1021
  const queryParser = config.queryParser;
1022
- if (!openApiSchemas?.listQuery && queryParser?.getQuerySchema) {
1022
+ if (queryParser?.getQuerySchema) {
1023
1023
  const querySchema = queryParser.getQuerySchema();
1024
1024
  if (querySchema) openApiSchemas = {
1025
1025
  ...openApiSchemas,
1026
1026
  listQuery: querySchema
1027
1027
  };
1028
1028
  }
1029
+ if (config.openApiSchemas) openApiSchemas = {
1030
+ ...openApiSchemas,
1031
+ ...config.openApiSchemas
1032
+ };
1029
1033
  if (openApiSchemas) openApiSchemas = convertOpenApiSchemas(openApiSchemas);
1030
1034
  resource._registryMeta = {
1031
1035
  module: config.module,
@@ -1050,6 +1054,7 @@ var ResourceDefinition = class {
1050
1054
  disabledRoutes;
1051
1055
  events;
1052
1056
  rateLimit;
1057
+ audit;
1053
1058
  updateMethod;
1054
1059
  pipe;
1055
1060
  fields;
@@ -1078,6 +1083,7 @@ var ResourceDefinition = class {
1078
1083
  this.disabledRoutes = config.disabledRoutes ?? [];
1079
1084
  this.events = config.events ?? {};
1080
1085
  this.rateLimit = config.rateLimit;
1086
+ this.audit = config.audit;
1081
1087
  this.updateMethod = config.updateMethod;
1082
1088
  this.pipe = config.pipe;
1083
1089
  this.fields = config.fields;
@@ -1,4 +1,4 @@
1
- import { $ as RegistryEntry } from "../interface-DDW43OmS.mjs";
1
+ import { $ as RegistryEntry } from "../interface-CrN45qz1.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "../externalPaths-DpO-s7r8.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
 
@@ -1,4 +1,4 @@
1
- import { Bt as ResourceDefinition, n as DataAdapter } from "../interface-DDW43OmS.mjs";
1
+ import { Bt as ResourceDefinition, n as DataAdapter } from "../interface-CrN45qz1.mjs";
2
2
  import { t as PermissionCheck } from "../types-BNUccdcf.mjs";
3
3
 
4
4
  //#region src/dynamic/ArcDynamicLoader.d.ts
@@ -1,5 +1,5 @@
1
1
  import { t as ArcQueryParser } from "../queryParser-CgCtsjti.mjs";
2
- import { n as defineResource } from "../defineResource-bVKHjQzE.mjs";
2
+ import { n as defineResource } from "../defineResource-Ckxg6HrZ.mjs";
3
3
  import { S as readOnly, _ as fullPublic, b as publicRead, g as authenticated, h as adminOnly, v as ownerWithAdminBypass, x as publicReadAdminWrite } from "../permissions-C8ImI8gC.mjs";
4
4
  //#region src/dynamic/ArcDynamicLoader.ts
5
5
  const VALID_FIELD_TYPES = new Set([
@@ -1,4 +1,4 @@
1
- import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-D5hJ-k_3.mjs";
1
+ import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-C1Z28coa.mjs";
2
2
  import { FastifyInstance } from "fastify";
3
3
 
4
4
  //#region src/factory/createApp.d.ts
@@ -1,4 +1,4 @@
1
- import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-Bol7DLUf.mjs";
1
+ import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-D2w0LdYJ.mjs";
2
2
  import { readdir } from "node:fs/promises";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -110,32 +110,48 @@ async function loadResources(dir, options = {}) {
110
110
  const excludeSet = exclude ? new Set(exclude) : null;
111
111
  const skipped = [];
112
112
  const failed = [];
113
+ const isWindowsPath = (p) => /^[a-z]:[\\/]/i.test(p);
113
114
  const results = await Promise.all(files.map(async (file) => {
115
+ let mod;
116
+ let primaryError;
114
117
  try {
115
- let mod;
116
- try {
117
- mod = await import(pathToFileURL(file).href);
118
- } catch (_importErr) {
119
- mod = await import(file);
120
- }
118
+ mod = await import(pathToFileURL(file).href);
121
119
  return {
122
120
  file,
123
121
  mod
124
122
  };
125
123
  } catch (err) {
126
- const code = err.code;
127
- const msg = err instanceof Error ? err.message : String(err);
128
- if (code === "ERR_MODULE_NOT_FOUND" && msg.includes(".js")) failed.push(`${file}: ${msg}\n Hint: This file uses .js extension imports (TypeScript ESM convention).
129
- In production, ensure your build compiles .ts→.js before loadResources() runs.
130
- In tests, use vitest/tsx which resolves .js→.ts automatically.`);
131
- else failed.push(`${file}: ${msg}`);
132
- return null;
124
+ primaryError = err;
133
125
  }
126
+ if (!isWindowsPath(file)) try {
127
+ mod = await import(file);
128
+ return {
129
+ file,
130
+ mod
131
+ };
132
+ } catch {}
133
+ const err = primaryError;
134
+ const code = err.code;
135
+ const msg = err instanceof Error ? err.message : String(err);
136
+ if (code === "ERR_MODULE_NOT_FOUND" && msg.includes(".js")) failed.push(`${file}: ${msg}\n Hint: This file uses .js extension imports (TypeScript ESM convention).
137
+ • Production: ensure your build compiles .ts→.js before loadResources() runs.
138
+ • Node.js: use tsx, ts-node/esm, or a build step.
139
+ • Vitest: nested .js→.ts resolution may fail through dynamic imports.
140
+ Workaround: use import.meta.glob to preload resources statically.
141
+ See: https://github.com/classytic/arc/blob/main/docs/production-ops/factory.mdx#vitest-limitation`);
142
+ else failed.push(`${file}: ${msg}`);
143
+ return null;
134
144
  }));
135
145
  const resources = [];
136
146
  for (const result of results) {
137
147
  if (!result) continue;
138
- const resource = result.mod.default ?? result.mod.resource;
148
+ let resource = result.mod.default ?? result.mod.resource;
149
+ if (!resource || typeof resource.toPlugin !== "function") {
150
+ for (const value of Object.values(result.mod)) if (value && typeof value === "object" && typeof value.toPlugin === "function") {
151
+ resource = value;
152
+ break;
153
+ }
154
+ }
139
155
  if (!resource || typeof resource.toPlugin !== "function") {
140
156
  skipped.push(result.file);
141
157
  continue;
@@ -1,2 +1,2 @@
1
- import { an as HookPhase, cn as HookSystemOptions, dn as afterUpdate, fn as beforeCreate, gn as defineHook, hn as createHookSystem, in as HookOperation, ln as afterCreate, mn as beforeUpdate, nn as HookContext, on as HookRegistration, pn as beforeDelete, rn as HookHandler, sn as HookSystem, tn as DefineHookOptions, un as afterDelete } from "../interface-DDW43OmS.mjs";
1
+ import { an as HookPhase, cn as HookSystemOptions, dn as afterUpdate, fn as beforeCreate, gn as defineHook, hn as createHookSystem, in as HookOperation, ln as afterCreate, mn as beforeUpdate, nn as HookContext, on as HookRegistration, pn as beforeDelete, rn as HookHandler, sn as HookSystem, tn as DefineHookOptions, un as afterDelete } from "../interface-CrN45qz1.mjs";
2
2
  export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -1,5 +1,5 @@
1
1
  import { s as RequestScope } from "./elevation-C_taLQrM.mjs";
2
- import { Ft as IControllerResponse, It as IRequestContext, O as FastifyWithDecorators, Pt as IController, S as CrudRouterOptions, b as CrudController, rt as RequestWithExtras, tt as RequestContext } from "./interface-DDW43OmS.mjs";
2
+ import { Ft as IControllerResponse, It as IRequestContext, O as FastifyWithDecorators, Pt as IController, S as CrudRouterOptions, b as CrudController, rt as RequestWithExtras, tt as RequestContext } from "./interface-CrN45qz1.mjs";
3
3
  import { t as PermissionCheck } from "./types-BNUccdcf.mjs";
4
4
  import { FastifyInstance, FastifyReply, FastifyRequest, RouteHandlerMethod } from "fastify";
5
5
 
@@ -1,4 +1,4 @@
1
- import { Ht as CrudRepository, K as ParsedQuery, W as OpenApiSchemas, Z as QueryParserInterface, a as RepositoryLike, dt as RouteSchemaOptions, n as DataAdapter, o as SchemaMetadata, s as ValidationResult } from "./interface-DDW43OmS.mjs";
1
+ import { Ht as CrudRepository, K as ParsedQuery, W as OpenApiSchemas, Z as QueryParserInterface, a as RepositoryLike, dt as RouteSchemaOptions, n as DataAdapter, o as SchemaMetadata, s as ValidationResult } from "./interface-CrN45qz1.mjs";
2
2
  import { Model } from "mongoose";
3
3
 
4
4
  //#region src/adapters/mongoose.d.ts
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { $ as RegistryEntry, $t as PipelineStep, A as GracefulShutdownOptions, Bt as ResourceDefinition, C as CrudSchemas, Ct as BaseController, D as FastifyWithAuth, E as FastifyRequestExtras, F as InferResourceDoc, Ft as IControllerResponse, G as OwnershipCheck, H as MiddlewareHandler, Ht as CrudRepository, I as IntrospectionData, It as IRequestContext, J as PresetFunction, Jt as Interceptor, Kt as QueryOptions, L as IntrospectionPluginOptions, Lt as RouteHandler, M as HealthOptions, Mt as ControllerLike, N as InferAdapterDoc, O as FastifyWithDecorators, P as InferDocType, Pt as IController, Q as RateLimitConfig, Qt as PipelineContext, R as JWTPayload, S as CrudRouterOptions, V as MiddlewareConfig, Vt as defineResource, Wt as PaginatedResult, X as PresetResult, Xt as OperationFilter, Yt as NextFunction, Zt as PipelineConfig, a as RepositoryLike, at as ResourceConfig, b as CrudController, bt as ValidationResult$1, c as AdditionalRoute, ct as ResourceMetadata, dt as RouteSchemaOptions, en as Transform, et as RegistryStats, f as ArcInternalMetadata, ft as ServiceContext, gt as TypedResourceConfig, h as AuthPluginOptions, ht as TypedRepository, i as RelationMetadata, j as HealthCheck, k as FieldRule, l as AnyRecord, mt as TypedController, n as DataAdapter, nt as RequestIdOptions, o as SchemaMetadata, p as ArcRequest, qt as Guard, r as FieldMetadata, rt as RequestWithExtras, s as ValidationResult, tt as RequestContext, u as ApiResponse, ut as RouteHandlerMethod, v as ConfigError, vt as UserOrganization, w as EventDefinition, wt as BaseControllerOptions, x as CrudRouteKey, xt as envelope, yt as ValidateOptions } from "./interface-DDW43OmS.mjs";
1
+ import { $ as RegistryEntry, $t as PipelineStep, A as GracefulShutdownOptions, Bt as ResourceDefinition, C as CrudSchemas, Ct as BaseController, D as FastifyWithAuth, E as FastifyRequestExtras, F as InferResourceDoc, Ft as IControllerResponse, G as OwnershipCheck, H as MiddlewareHandler, Ht as CrudRepository, I as IntrospectionData, It as IRequestContext, J as PresetFunction, Jt as Interceptor, Kt as QueryOptions, L as IntrospectionPluginOptions, Lt as RouteHandler, M as HealthOptions, Mt as ControllerLike, N as InferAdapterDoc, O as FastifyWithDecorators, P as InferDocType, Pt as IController, Q as RateLimitConfig, Qt as PipelineContext, R as JWTPayload, S as CrudRouterOptions, V as MiddlewareConfig, Vt as defineResource, Wt as PaginatedResult, X as PresetResult, Xt as OperationFilter, Yt as NextFunction, Zt as PipelineConfig, a as RepositoryLike, at as ResourceConfig, b as CrudController, bt as ValidationResult$1, c as AdditionalRoute, ct as ResourceMetadata, dt as RouteSchemaOptions, en as Transform, et as RegistryStats, f as ArcInternalMetadata, ft as ServiceContext, gt as TypedResourceConfig, h as AuthPluginOptions, ht as TypedRepository, i as RelationMetadata, j as HealthCheck, k as FieldRule, l as AnyRecord, mt as TypedController, n as DataAdapter, nt as RequestIdOptions, o as SchemaMetadata, p as ArcRequest, qt as Guard, r as FieldMetadata, rt as RequestWithExtras, s as ValidationResult, tt as RequestContext, u as ApiResponse, ut as RouteHandlerMethod, v as ConfigError, vt as UserOrganization, w as EventDefinition, wt as BaseControllerOptions, x as CrudRouteKey, xt as envelope, yt as ValidateOptions } from "./interface-CrN45qz1.mjs";
2
2
  import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, n as FieldPermissionMap, o as fields, t as FieldPermission } from "./fields-DFwdaWCq.mjs";
3
3
  import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-BNUccdcf.mjs";
4
- import { l as createMongooseAdapter, o as createPrismaAdapter, s as MongooseAdapter, t as PrismaAdapter } from "./index-BIsZ_su5.mjs";
5
- import { A as RESERVED_QUERY_PARAMS, C as HookOperation, D as MAX_SEARCH_LENGTH, E as MAX_REGEX_LENGTH, O as MUTATION_OPERATIONS, S as HOOK_PHASES, T as MAX_FILTER_DEPTH, _ as DEFAULT_MAX_LIMIT, a as getControllerScope, b as DEFAULT_UPDATE_METHOD, g as DEFAULT_LIMIT, h as DEFAULT_ID_FIELD, j as SYSTEM_FIELDS, k as MutationOperation, m as CrudOperation, p as CRUD_OPERATIONS, v as DEFAULT_SORT, w as HookPhase, x as HOOK_OPERATIONS, y as DEFAULT_TENANT_FIELD } from "./index-Cb3gtbg7.mjs";
4
+ import { l as createMongooseAdapter, o as createPrismaAdapter, s as MongooseAdapter, t as PrismaAdapter } from "./index-DrCqa3Jq.mjs";
5
+ import { A as RESERVED_QUERY_PARAMS, C as HookOperation, D as MAX_SEARCH_LENGTH, E as MAX_REGEX_LENGTH, O as MUTATION_OPERATIONS, S as HOOK_PHASES, T as MAX_FILTER_DEPTH, _ as DEFAULT_MAX_LIMIT, a as getControllerScope, b as DEFAULT_UPDATE_METHOD, g as DEFAULT_LIMIT, h as DEFAULT_ID_FIELD, j as SYSTEM_FIELDS, k as MutationOperation, m as CrudOperation, p as CRUD_OPERATIONS, v as DEFAULT_SORT, w as HookPhase, x as HOOK_OPERATIONS, y as DEFAULT_TENANT_FIELD } from "./index-B4uZm82R.mjs";
6
6
  import { C as presets_d_exports, E as readOnly, S as ownerWithAdminBypass, T as publicReadAdminWrite, a as allOf, b as authenticated, c as createDynamicPermissionMatrix, d as requireAuth, f as requireOrgMembership, g as requireTeamMembership, h as requireRoles, l as createOrgPermissions, m as requireOwnership, n as DynamicPermissionMatrix, o as allowPublic, p as requireOrgRole, r as DynamicPermissionMatrixConfig, s as anyOf, u as denyAll, v as when, w as publicRead, x as fullPublic, y as adminOnly } from "./index-NGZksqM5.mjs";
7
7
  import { a as NotFoundError, d as ValidationError, f as createDomainError, i as ForbiddenError, t as ArcError, u as UnauthorizedError } from "./errors-CcVbl1-T.mjs";
8
8
  import { AsyncLocalStorage } from "node:async_hooks";
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import { envelope } from "./types/index.mjs";
5
5
  import { n as applyFieldWritePermissions, r as fields, t as applyFieldReadPermissions } from "./fields-ipsbIRPK.mjs";
6
6
  import { t as requestContext } from "./requestContext-DYtmNpm5.mjs";
7
7
  import { d as createDomainError, i as NotFoundError, l as UnauthorizedError, r as ForbiddenError, t as ArcError, u as ValidationError } from "./errors-NoQKsbAT.mjs";
8
- import { a as validateResourceConfig, f as getControllerScope, i as formatValidationErrors, m as pipe, n as defineResource, r as assertValidConfig, t as ResourceDefinition } from "./defineResource-bVKHjQzE.mjs";
8
+ import { a as validateResourceConfig, f as getControllerScope, i as formatValidationErrors, m as pipe, n as defineResource, r as assertValidConfig, t as ResourceDefinition } from "./defineResource-Ckxg6HrZ.mjs";
9
9
  import { S as readOnly, _ as fullPublic, a as createOrgPermissions, b as publicRead, c as requireOrgMembership, d as requireRoles, f as requireTeamMembership, g as authenticated, h as adminOnly, i as createDynamicPermissionMatrix, l as requireOrgRole, m as when, n as allowPublic, o as denyAll, r as anyOf, s as requireAuth, t as allOf, u as requireOwnership, v as ownerWithAdminBypass, x as publicReadAdminWrite, y as presets_exports } from "./permissions-C8ImI8gC.mjs";
10
10
  import { n as configureArcLogger, t as arcLog } from "./logger-Dz3j1ItV.mjs";
11
11
  //#region src/middleware/middleware.ts
@@ -127,6 +127,6 @@ function transform(name, handlerOrOptions) {
127
127
  }
128
128
  //#endregion
129
129
  //#region src/index.ts
130
- const version = "2.6.1";
130
+ const version = "2.6.2";
131
131
  //#endregion
132
132
  export { ArcError, BaseController, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, ForbiddenError, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MongooseAdapter, NotFoundError, PrismaAdapter, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, UnauthorizedError, ValidationError, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, arcLog, assertValidConfig, authenticated, configureArcLogger, createDomainError, createDynamicPermissionMatrix, createMongooseAdapter, createOrgPermissions, createPrismaAdapter, defineResource, denyAll, envelope, fields, formatValidationErrors, fullPublic, getControllerScope, guard, intercept, middleware, ownerWithAdminBypass, presets_exports as permissions, pipe, publicRead, publicReadAdminWrite, readOnly, requestContext, requireAuth, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireTeamMembership, sortMiddlewares, transform, validateResourceConfig, version, when };
@@ -1,7 +1,7 @@
1
1
  import { WebSocketClient, WebSocketMessage, WebSocketPluginOptions } from "./websocket.mjs";
2
2
  import { EventGatewayOptions } from "./event-gateway.mjs";
3
3
  import { JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobsPluginOptions, QueueStats } from "./jobs.mjs";
4
- import { c as McpResourceConfig, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler } from "../types-D5rjsS_i.mjs";
4
+ import { c as McpResourceConfig, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler } from "../types-DurlBP2N.mjs";
5
5
  import { StreamlinePluginOptions, WorkflowLike, WorkflowRunLike } from "./streamline.mjs";
6
6
  import { WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription } from "./webhooks.mjs";
7
7
  export { type BetterAuthHandler, type CallToolResult, type CreateMcpServerConfig, type CrudOperation, type EventGatewayOptions, type JobDefinition, type JobDispatchOptions, type JobDispatcher, type JobMeta, type JobsPluginOptions, type McpAuthResult, type McpPluginOptions, type McpResourceConfig, type PromptDefinition, type QueueStats, type StreamlinePluginOptions, type ToolAnnotations, type ToolContext, type ToolDefinition, type WebSocketClient, type WebSocketMessage, type WebSocketPluginOptions, type WebhookDeliveryRecord, type WebhookManager, type WebhookPluginOptions, type WebhookStore, type WebhookSubscription, type WorkflowLike, type WorkflowRunLike };
@@ -1,5 +1,5 @@
1
- import { Bt as ResourceDefinition } from "../../interface-DDW43OmS.mjs";
2
- import { a as McpAuthResolver, c as McpResourceConfig, d as SessionEntry, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler, u as PromptResult } from "../../types-D5rjsS_i.mjs";
1
+ import { Bt as ResourceDefinition } from "../../interface-CrN45qz1.mjs";
2
+ import { a as McpAuthResolver, c as McpResourceConfig, d as SessionEntry, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler, u as PromptResult } from "../../types-DurlBP2N.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  import { z } from "zod";
5
5
 
@@ -1,4 +1,4 @@
1
- import { o as McpAuthResult, s as McpPluginOptions } from "../../types-D5rjsS_i.mjs";
1
+ import { o as McpAuthResult, s as McpPluginOptions } from "../../types-DurlBP2N.mjs";
2
2
 
3
3
  //#region src/integrations/mcp/testing.d.ts
4
4
  interface TestMcpClientOptions {
@@ -470,6 +470,9 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
470
470
  readonly disabledRoutes: CrudRouteKey[];
471
471
  readonly events: Record<string, EventDefinition>;
472
472
  readonly rateLimit?: RateLimitConfig | false;
473
+ readonly audit?: boolean | {
474
+ operations?: ("create" | "update" | "delete")[];
475
+ };
473
476
  readonly updateMethod?: "PUT" | "PATCH" | "both";
474
477
  readonly pipe?: PipelineConfig;
475
478
  readonly fields?: FieldPermissionMap;
@@ -1558,6 +1561,34 @@ interface ResourceConfig<TDoc = AnyRecord> {
1558
1561
  * Requires `queryCachePlugin` to be registered.
1559
1562
  */
1560
1563
  cache?: ResourceCacheConfig;
1564
+ /**
1565
+ * Per-resource audit opt-in. When `auditPlugin` is registered with
1566
+ * `autoAudit: { perResource: true }`, only resources with this flag are audited.
1567
+ *
1568
+ * The cleanest pattern for apps where most resources don't need auditing —
1569
+ * no growing exclude lists, no centralized allowlist to maintain.
1570
+ *
1571
+ * - `true`: Audit create/update/delete on this resource
1572
+ * - `{ operations: ['delete'] }`: Audit only specific operations
1573
+ * - `false` or omit: Not audited (default)
1574
+ *
1575
+ * @example
1576
+ * ```ts
1577
+ * // app.ts
1578
+ * await fastify.register(auditPlugin, {
1579
+ * autoAudit: { perResource: true },
1580
+ * });
1581
+ *
1582
+ * // order.resource.ts
1583
+ * defineResource({ name: 'order', audit: true });
1584
+ *
1585
+ * // payment.resource.ts
1586
+ * defineResource({ name: 'payment', audit: { operations: ['delete'] } });
1587
+ * ```
1588
+ */
1589
+ audit?: boolean | {
1590
+ operations?: ("create" | "update" | "delete")[];
1591
+ };
1561
1592
  }
1562
1593
  /**
1563
1594
  * Resource-level permissions
@@ -2214,6 +2245,10 @@ interface RegistryEntry extends ResourceMetadata {
2214
2245
  disabledRoutes?: string[];
2215
2246
  /** Rate limit config */
2216
2247
  rateLimit?: RateLimitConfig | false;
2248
+ /** Per-resource audit opt-in flag (read by auditPlugin perResource mode) */
2249
+ audit?: boolean | {
2250
+ operations?: ("create" | "update" | "delete")[];
2251
+ };
2217
2252
  }
2218
2253
  interface RegistryStats {
2219
2254
  total?: number;
@@ -1,4 +1,4 @@
1
- import { Lt as RouteHandler } from "../interface-DDW43OmS.mjs";
1
+ import { Lt as RouteHandler } from "../interface-CrN45qz1.mjs";
2
2
  import { i as UserBase } from "../types-BNUccdcf.mjs";
3
3
  import { InvitationAdapter, InvitationDoc, MemberDoc, OrgAdapter, OrgDoc, OrgPermissionStatement, OrgRole, OrganizationPluginOptions } from "./types.mjs";
4
4
  import { FastifyPluginAsync, RouteHandlerMethod } from "fastify";
@@ -1,4 +1,4 @@
1
- import { V as MiddlewareConfig, Y as PresetHook, c as AdditionalRoute, dt as RouteSchemaOptions, l as AnyRecord, sn as HookSystem, zt as ResourceRegistry } from "../interface-DDW43OmS.mjs";
1
+ import { V as MiddlewareConfig, Y as PresetHook, c as AdditionalRoute, dt as RouteSchemaOptions, l as AnyRecord, sn as HookSystem, zt as ResourceRegistry } from "../interface-CrN45qz1.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "../externalPaths-DpO-s7r8.mjs";
3
3
  import { _ as cachingPlugin, a as versioningPlugin, c as MetricsOptions, d as SSEOptions, f as _default$6, g as _default$1, h as CachingRule, i as _default$7, l as _default$4, m as CachingOptions, n as errorHandlerPlugin, o as MetricEntry, p as ssePlugin, r as VersioningOptions, s as MetricsCollector, t as ErrorHandlerOptions, u as metricsPlugin } from "../errorHandler-Do4vVQ1f.mjs";
4
4
  import { t as TracingOptions } from "../tracing-bz_U4EM1.mjs";
@@ -3,7 +3,7 @@ import { i as getOrgId } from "../types-BhtYdxZU.mjs";
3
3
  import { t as requestContext } from "../requestContext-DYtmNpm5.mjs";
4
4
  import { t as hasEvents } from "../typeGuards-Cj5Rgvlg.mjs";
5
5
  import { t as HookSystem } from "../HookSystem-COkyWztM.mjs";
6
- import { t as ResourceRegistry } from "../ResourceRegistry-DeCIFlix.mjs";
6
+ import { t as ResourceRegistry } from "../ResourceRegistry-C6ngvOnn.mjs";
7
7
  import { n as caching_default, t as cachingPlugin } from "../caching-BSXB-Xr7.mjs";
8
8
  import { t as errorHandlerPlugin } from "../errorHandler-r2595m8T.mjs";
9
9
  import { n as metrics_default, t as metricsPlugin } from "../metrics-Csh4nsvv.mjs";
@@ -44,7 +44,7 @@ try {
44
44
  function createTracerProvider(options) {
45
45
  if (!isAvailable) return null;
46
46
  const { serviceName = "@classytic/arc", serviceVersion, exporterUrl = "http://localhost:4318/v1/traces" } = options;
47
- const resolvedVersion = serviceVersion ?? "2.6.1";
47
+ const resolvedVersion = serviceVersion ?? "2.6.2";
48
48
  const exporter = new OTLPTraceExporter({ url: exporterUrl });
49
49
  const provider = new NodeTracerProvider({ resource: { attributes: {
50
50
  "service.name": serviceName,
@@ -1,4 +1,4 @@
1
- import { Ft as IControllerResponse, It as IRequestContext, Wt as PaginatedResult, X as PresetResult, at as ResourceConfig, l as AnyRecord } from "../interface-DDW43OmS.mjs";
1
+ import { Ft as IControllerResponse, It as IRequestContext, Wt as PaginatedResult, X as PresetResult, at as ResourceConfig, l as AnyRecord } from "../interface-CrN45qz1.mjs";
2
2
  import { MultiTenantOptions, multiTenantPreset } from "./multiTenant.mjs";
3
3
 
4
4
  //#region src/presets/ownedByUser.d.ts
@@ -1,4 +1,4 @@
1
- import { X as PresetResult, x as CrudRouteKey } from "../interface-DDW43OmS.mjs";
1
+ import { X as PresetResult, x as CrudRouteKey } from "../interface-CrN45qz1.mjs";
2
2
 
3
3
  //#region src/presets/multiTenant.d.ts
4
4
  interface MultiTenantOptions {
@@ -1,4 +1,4 @@
1
- import { L as IntrospectionPluginOptions, Rt as RegisterOptions, zt as ResourceRegistry } from "../interface-DDW43OmS.mjs";
1
+ import { L as IntrospectionPluginOptions, Rt as RegisterOptions, zt as ResourceRegistry } from "../interface-CrN45qz1.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/registry/introspectionPlugin.d.ts
@@ -1,3 +1,3 @@
1
1
  import { n as introspectionPlugin_default, t as introspectionPlugin } from "../registry-I-ogLgL9.mjs";
2
- import { t as ResourceRegistry } from "../ResourceRegistry-DeCIFlix.mjs";
2
+ import { t as ResourceRegistry } from "../ResourceRegistry-C6ngvOnn.mjs";
3
3
  export { ResourceRegistry, introspectionPlugin_default as introspectionPlugin, introspectionPlugin as introspectionPluginFn };
@@ -1,5 +1,5 @@
1
- import { Bt as ResourceDefinition, Ht as CrudRepository, l as AnyRecord } from "../interface-DDW43OmS.mjs";
2
- import { r as CreateAppOptions } from "../types-D5hJ-k_3.mjs";
1
+ import { Bt as ResourceDefinition, Ht as CrudRepository, l as AnyRecord } from "../interface-CrN45qz1.mjs";
2
+ import { d as ResourceLike, r as CreateAppOptions } from "../types-C1Z28coa.mjs";
3
3
  import Fastify, { FastifyInstance, FastifyServerOptions } from "fastify";
4
4
  import { Connection } from "mongoose";
5
5
  import { Mock } from "vitest";
@@ -572,6 +572,29 @@ declare function createTestTimer(): {
572
572
  reset: () => void;
573
573
  };
574
574
  //#endregion
575
+ //#region src/testing/preloadResources.d.ts
576
+ /** Eager glob result: `{ '/path/to/file.ts': resourceModule }` */
577
+ type EagerGlobResult = Record<string, unknown>;
578
+ /** Lazy glob result: `{ '/path/to/file.ts': () => Promise<unknown> }` */
579
+ type LazyGlobResult = Record<string, () => Promise<unknown>>;
580
+ /**
581
+ * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
582
+ *
583
+ * Accepts either:
584
+ * - `{ import: 'default' }` form: values are the resource directly
585
+ * - default form: values are the full module — picks first export with `toPlugin()`
586
+ *
587
+ * Throws if any module doesn't yield a valid `ResourceLike`.
588
+ */
589
+ declare function preloadResources(globResult: EagerGlobResult): ResourceLike[];
590
+ /**
591
+ * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
592
+ *
593
+ * Use this when resources depend on prior bootstrap (e.g., engine init) and
594
+ * cannot be evaluated at import time of the preload file.
595
+ */
596
+ declare function preloadResourcesAsync(globResult: LazyGlobResult): Promise<ResourceLike[]>;
597
+ //#endregion
575
598
  //#region src/testing/TestHarness.d.ts
576
599
  /**
577
600
  * Test fixtures for a resource
@@ -898,4 +921,4 @@ declare class TestDataLoader {
898
921
  cleanup(): Promise<void>;
899
922
  }
900
923
  //#endregion
901
- export { type AuthProvider, type AuthResponse, type BetterAuthTestHelpers, type BetterAuthTestHelpersOptions, type CreateTestAppOptions, DatabaseSnapshot, TestFixtures as DbTestFixtures, type GenerateTestFileOptions, HttpTestHarness, type HttpTestHarnessOptions, InMemoryDatabase, type OrgResponse, type SetupBetterAuthOrgOptions, type SetupUserConfig, type TestAppResult, TestDataLoader, TestDatabase, type TestFixtures$1 as TestFixtures, TestHarness, type TestHarnessOptions, type TestOrgContext, TestRequestBuilder, TestSeeder, TestTransaction, type TestUserContext, createBetterAuthProvider, createBetterAuthTestHelpers, createConfigTestSuite, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSnapshotMatcher, createSpy, createTestApp, createTestAuth, createTestHarness, createTestTimer, generateTestFile, request, safeParseBody, setupBetterAuthOrg, waitFor, withTestDb };
924
+ export { type AuthProvider, type AuthResponse, type BetterAuthTestHelpers, type BetterAuthTestHelpersOptions, type CreateTestAppOptions, DatabaseSnapshot, TestFixtures as DbTestFixtures, type GenerateTestFileOptions, HttpTestHarness, type HttpTestHarnessOptions, InMemoryDatabase, type OrgResponse, type SetupBetterAuthOrgOptions, type SetupUserConfig, type TestAppResult, TestDataLoader, TestDatabase, type TestFixtures$1 as TestFixtures, TestHarness, type TestHarnessOptions, type TestOrgContext, TestRequestBuilder, TestSeeder, TestTransaction, type TestUserContext, createBetterAuthProvider, createBetterAuthTestHelpers, createConfigTestSuite, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSnapshotMatcher, createSpy, createTestApp, createTestAuth, createTestHarness, createTestTimer, generateTestFile, preloadResources, preloadResourcesAsync, request, safeParseBody, setupBetterAuthOrg, waitFor, withTestDb };
@@ -1066,6 +1066,50 @@ function createTestTimer() {
1066
1066
  };
1067
1067
  }
1068
1068
  //#endregion
1069
+ //#region src/testing/preloadResources.ts
1070
+ /**
1071
+ * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
1072
+ *
1073
+ * Accepts either:
1074
+ * - `{ import: 'default' }` form: values are the resource directly
1075
+ * - default form: values are the full module — picks first export with `toPlugin()`
1076
+ *
1077
+ * Throws if any module doesn't yield a valid `ResourceLike`.
1078
+ */
1079
+ function preloadResources(globResult) {
1080
+ const resources = [];
1081
+ for (const [path, value] of Object.entries(globResult)) {
1082
+ const resource = pickResource(value);
1083
+ if (!resource) throw new Error(`preloadResources: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
1084
+ resources.push(resource);
1085
+ }
1086
+ return resources.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
1087
+ }
1088
+ /**
1089
+ * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
1090
+ *
1091
+ * Use this when resources depend on prior bootstrap (e.g., engine init) and
1092
+ * cannot be evaluated at import time of the preload file.
1093
+ */
1094
+ async function preloadResourcesAsync(globResult) {
1095
+ return (await Promise.all(Object.entries(globResult).map(async ([path, loader]) => {
1096
+ const resource = pickResource(await loader());
1097
+ if (!resource) throw new Error(`preloadResourcesAsync: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
1098
+ return resource;
1099
+ }))).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
1100
+ }
1101
+ function pickResource(value) {
1102
+ if (!value || typeof value !== "object") return void 0;
1103
+ if (typeof value.toPlugin === "function") return value;
1104
+ const mod = value;
1105
+ const candidates = [
1106
+ mod.default,
1107
+ mod.resource,
1108
+ ...Object.values(mod)
1109
+ ];
1110
+ for (const c of candidates) if (c && typeof c === "object" && typeof c.toPlugin === "function") return c;
1111
+ }
1112
+ //#endregion
1069
1113
  //#region src/testing/TestHarness.ts
1070
1114
  /**
1071
1115
  * Resource Test Harness
@@ -1752,7 +1796,7 @@ function runEventTests(resourceName, displayName, events) {
1752
1796
  * ```
1753
1797
  */
1754
1798
  async function createTestApp(options = {}) {
1755
- const { createApp } = await import("../createApp-Bol7DLUf.mjs").then((n) => n.r);
1799
+ const { createApp } = await import("../createApp-D2w0LdYJ.mjs").then((n) => n.r);
1756
1800
  const { useInMemoryDb = true, mongoUri: providedMongoUri, ...appOptions } = options;
1757
1801
  const defaultAuth = {
1758
1802
  type: "jwt",
@@ -1950,4 +1994,4 @@ var TestDataLoader = class {
1950
1994
  }
1951
1995
  };
1952
1996
  //#endregion
1953
- export { DatabaseSnapshot, TestFixtures as DbTestFixtures, HttpTestHarness, InMemoryDatabase, TestDataLoader, TestDatabase, TestHarness, TestRequestBuilder, TestSeeder, TestTransaction, createBetterAuthProvider, createBetterAuthTestHelpers, createConfigTestSuite, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSnapshotMatcher, createSpy, createTestApp, createTestAuth, createTestHarness, createTestTimer, generateTestFile, request, safeParseBody, setupBetterAuthOrg, waitFor, withTestDb };
1997
+ export { DatabaseSnapshot, TestFixtures as DbTestFixtures, HttpTestHarness, InMemoryDatabase, TestDataLoader, TestDatabase, TestHarness, TestRequestBuilder, TestSeeder, TestTransaction, createBetterAuthProvider, createBetterAuthTestHelpers, createConfigTestSuite, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSnapshotMatcher, createSpy, createTestApp, createTestAuth, createTestHarness, createTestTimer, generateTestFile, preloadResources, preloadResourcesAsync, request, safeParseBody, setupBetterAuthOrg, waitFor, withTestDb };
@@ -1,4 +1,4 @@
1
1
  import { _ as isMember, a as AUTHENTICATED_SCOPE, d as getTeamId, g as isElevated, h as isAuthenticated, l as getOrgId, m as hasOrgAccess, n as ElevationOptions, o as PUBLIC_SCOPE, s as RequestScope, t as ElevationEvent, u as getOrgRoles } from "../elevation-C_taLQrM.mjs";
2
- import { $ as RegistryEntry, A as GracefulShutdownOptions, B as LookupOption, C as CrudSchemas, D as FastifyWithAuth, E as FastifyRequestExtras, F as InferResourceDoc, Ft as IControllerResponse, G as OwnershipCheck, Gt as PaginationParams, H as MiddlewareHandler, Ht as CrudRepository, I as IntrospectionData, It as IRequestContext, J as PresetFunction, K as ParsedQuery, Kt as QueryOptions, L as IntrospectionPluginOptions, Lt as RouteHandler, M as HealthOptions, Mt as ControllerLike, N as InferAdapterDoc, Nt as FastifyHandler, O as FastifyWithDecorators, P as InferDocType, Pt as IController, Q as RateLimitConfig, R as JWTPayload, S as CrudRouterOptions, St as getUserId, T as EventsDecorator, U as ObjectId, Ut as InferDoc, V as MiddlewareConfig, W as OpenApiSchemas, Wt as PaginatedResult, X as PresetResult, Y as PresetHook, Z as QueryParserInterface, _ as AuthenticatorContext, _t as UserLike, at as ResourceConfig, b as CrudController, bt as ValidationResult, c as AdditionalRoute, ct as ResourceMetadata, d as ArcDecorator, dt as RouteSchemaOptions, et as RegistryStats, f as ArcInternalMetadata, ft as ServiceContext, g as Authenticator, gt as TypedResourceConfig, h as AuthPluginOptions, ht as TypedRepository, it as ResourceCacheConfig, j as HealthCheck, jt as ControllerHandler, k as FieldRule, l as AnyRecord, lt as ResourcePermissions, m as AuthHelpers, mt as TypedController, nt as RequestIdOptions, ot as ResourceHookContext, p as ArcRequest, pt as TokenPair, q as PopulateOption, rt as RequestWithExtras, st as ResourceHooks, tt as RequestContext, u as ApiResponse, ut as RouteHandlerMethod, v as ConfigError, vt as UserOrganization, w as EventDefinition, wt as BaseControllerOptions, x as CrudRouteKey, xt as envelope, y as ControllerQueryOptions, yt as ValidateOptions, z as JwtContext } from "../interface-DDW43OmS.mjs";
2
+ import { $ as RegistryEntry, A as GracefulShutdownOptions, B as LookupOption, C as CrudSchemas, D as FastifyWithAuth, E as FastifyRequestExtras, F as InferResourceDoc, Ft as IControllerResponse, G as OwnershipCheck, Gt as PaginationParams, H as MiddlewareHandler, Ht as CrudRepository, I as IntrospectionData, It as IRequestContext, J as PresetFunction, K as ParsedQuery, Kt as QueryOptions, L as IntrospectionPluginOptions, Lt as RouteHandler, M as HealthOptions, Mt as ControllerLike, N as InferAdapterDoc, Nt as FastifyHandler, O as FastifyWithDecorators, P as InferDocType, Pt as IController, Q as RateLimitConfig, R as JWTPayload, S as CrudRouterOptions, St as getUserId, T as EventsDecorator, U as ObjectId, Ut as InferDoc, V as MiddlewareConfig, W as OpenApiSchemas, Wt as PaginatedResult, X as PresetResult, Y as PresetHook, Z as QueryParserInterface, _ as AuthenticatorContext, _t as UserLike, at as ResourceConfig, b as CrudController, bt as ValidationResult, c as AdditionalRoute, ct as ResourceMetadata, d as ArcDecorator, dt as RouteSchemaOptions, et as RegistryStats, f as ArcInternalMetadata, ft as ServiceContext, g as Authenticator, gt as TypedResourceConfig, h as AuthPluginOptions, ht as TypedRepository, it as ResourceCacheConfig, j as HealthCheck, jt as ControllerHandler, k as FieldRule, l as AnyRecord, lt as ResourcePermissions, m as AuthHelpers, mt as TypedController, nt as RequestIdOptions, ot as ResourceHookContext, p as ArcRequest, pt as TokenPair, q as PopulateOption, rt as RequestWithExtras, st as ResourceHooks, tt as RequestContext, u as ApiResponse, ut as RouteHandlerMethod, v as ConfigError, vt as UserOrganization, w as EventDefinition, wt as BaseControllerOptions, x as CrudRouteKey, xt as envelope, y as ControllerQueryOptions, yt as ValidateOptions, z as JwtContext } from "../interface-CrN45qz1.mjs";
3
3
  import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-BNUccdcf.mjs";
4
4
  export { AUTHENTICATED_SCOPE, AdditionalRoute, AnyRecord, ApiResponse, ArcDecorator, ArcInternalMetadata, ArcRequest, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRepository, CrudRouteKey, CrudRouterOptions, CrudSchemas, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OpenApiSchemas, OwnershipCheck, PUBLIC_SCOPE, PaginatedResult, PaginationParams, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryOptions, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResourceCacheConfig, ResourceConfig, ResourceHookContext, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteHandler, RouteHandlerMethod, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult, envelope, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
@@ -1,5 +1,5 @@
1
1
  import { n as ElevationOptions } from "./elevation-C_taLQrM.mjs";
2
- import { g as Authenticator } from "./interface-DDW43OmS.mjs";
2
+ import { g as Authenticator } from "./interface-CrN45qz1.mjs";
3
3
  import { t as ExternalOpenApiPaths } from "./externalPaths-DpO-s7r8.mjs";
4
4
  import { i as CacheStore } from "./interface-D_BWALyZ.mjs";
5
5
  import { r as QueryCachePluginOptions } from "./queryCachePlugin-DcmETvcB.mjs";
@@ -52,6 +52,12 @@ import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, Fast
52
52
  * // Minimal resource (plain object)
53
53
  * const simple: ResourceLike = { name: 'ping', toPlugin: () => () => {} };
54
54
  * ```
55
+ *
56
+ * **DO NOT add an index signature** (`[key: string]: unknown`) to this interface.
57
+ * Class instances (like `ResourceDefinition`) don't implicitly carry index signatures,
58
+ * so adding one here makes `ResourceDefinition` *unassignable* to `ResourceLike` —
59
+ * the exact opposite of the intent. TypeScript's structural typing already allows
60
+ * classes with extra properties to satisfy this interface without an index signature.
55
61
  */
56
62
  interface ResourceLike {
57
63
  /** Plugin factory — called by createApp to register routes */
@@ -1,4 +1,4 @@
1
- import { Bt as ResourceDefinition } from "./interface-DDW43OmS.mjs";
1
+ import { Bt as ResourceDefinition } from "./interface-CrN45qz1.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/integrations/mcp/types.d.ts
@@ -1,4 +1,4 @@
1
- import { K as ParsedQuery, W as OpenApiSchemas, Z as QueryParserInterface, l as AnyRecord } from "../interface-DDW43OmS.mjs";
1
+ import { K as ParsedQuery, W as OpenApiSchemas, Z as QueryParserInterface, l as AnyRecord } from "../interface-CrN45qz1.mjs";
2
2
  import { a as NotFoundError, c as RateLimitError, d as ValidationError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-CcVbl1-T.mjs";
3
3
  import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-JP2GdJ4b.mjs";
4
4
  import { FastifyInstance } from "fastify";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.6.1",
3
+ "version": "2.6.2",
4
4
  "description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -8,11 +8,11 @@ description: |
8
8
  Triggers: arc, fastify resource, defineResource, createApp, BaseController, arc preset,
9
9
  arc auth, arc events, arc jobs, arc websocket, arc mcp, arc plugin, arc testing, arc cli,
10
10
  arc permissions, arc hooks, arc pipeline, arc factory, arc cache, arc QueryCache.
11
- version: 2.6.0
11
+ version: 2.6.2
12
12
  license: MIT
13
13
  metadata:
14
14
  author: Classytic
15
- version: "2.6.0"
15
+ version: "2.6.2"
16
16
  tags:
17
17
  - fastify
18
18
  - rest-api
@@ -527,21 +527,68 @@ src/resources/order/
527
527
 
528
528
  Generate: `arc generate resource order --mcp` | Wire: `extraTools: [fulfillOrderTool]`
529
529
 
530
- **Auto-load resources** (v2.6.0) — no barrel files, no manual `toPlugin()`:
530
+ **Auto-load resources** — no barrel files, no manual `toPlugin()`:
531
531
 
532
532
  ```typescript
533
533
  import { createApp, loadResources } from '@classytic/arc/factory';
534
534
 
535
535
  const app = await createApp({
536
- resources: await loadResources('./src/resources'), // discovers *.resource.ts
536
+ resourcePrefix: '/api/v1', // optional URL prefix
537
+ resources: await loadResources(import.meta.url), // discovers *.resource.ts
537
538
  auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
538
539
  });
539
- // loadResources options: exclude, include, suffix, recursive
540
540
  ```
541
541
 
542
- **Import compatibility:** `loadResources()` uses runtime `import()`. Works with relative imports (`./foo.js`) and Node.js `#` subpath imports (`#shared/utils.js` via `package.json` `imports` both `.js` and `.ts` extensions). Does **NOT** work with tsconfig path aliases (`@/*`, `~/`) those are compile-time only, Node.js ignores them. Projects using tsconfig aliases should use explicit `resources: [r1, r2]` instead.
542
+ `loadResources()` discovers files matching `*.resource.{ts,js,mts,mjs}`, recursively. Pass `import.meta.url` for dev/prod parity (resolves to `src/` in dev, `dist/` in prod automatically). Discovers `default` export, `export const resource`, OR any named export with `toPlugin()` (e.g., `export const userResource`).
543
543
 
544
- **Unified role check** (v2.6.0) checks both platform AND org roles:
544
+ Options: `exclude`, `include`, `suffix`, `recursive`, `silent`.
545
+
546
+ **Per-resource opt-out of `resourcePrefix`** — for webhooks, admin routes:
547
+ ```typescript
548
+ defineResource({ name: 'webhook', prefix: '/hooks', skipGlobalPrefix: true })
549
+ // Registers at /hooks even with createApp({ resourcePrefix: '/api/v1' })
550
+ ```
551
+
552
+ **Boot sequence:**
553
+ ```typescript
554
+ const app = await createApp({
555
+ resourcePrefix: '/api/v1',
556
+ plugins: async (f) => { await connectDB(); }, // 1. infra (DB, docs)
557
+ bootstrap: [inventoryInit, accountingInit], // 2. domain init (engines)
558
+ resources: await loadResources(import.meta.url), // 3. routes
559
+ afterResources: async (f) => { subscribeEvents(f); }, // 4. post-wiring
560
+ onReady: async (f) => { logger.info('ready'); }, // 5. lifecycle
561
+ });
562
+ ```
563
+
564
+ **Audit per-resource opt-in** — no growing exclude lists:
565
+ ```typescript
566
+ // Register audit plugin with perResource mode
567
+ await fastify.register(auditPlugin, { autoAudit: { perResource: true } });
568
+
569
+ // Opt-in at the resource level
570
+ defineResource({ name: 'order', audit: true });
571
+ defineResource({ name: 'payment', audit: { operations: ['delete'] } });
572
+ defineResource({ name: 'product' }); // not audited
573
+
574
+ // Manual custom() for MCP/additionalRoutes/read auditing
575
+ app.post('/orders/:id/refund', async (req) => {
576
+ await app.audit.custom('order', req.params.id, 'refund', { reason }, { user });
577
+ });
578
+ ```
579
+
580
+ **Import compatibility:** `loadResources()` uses runtime `import()`. Works with relative imports (`./foo.js`) and Node.js `#` subpath imports (`#shared/utils.js` via `package.json` `imports`). Does **NOT** work with tsconfig path aliases (`@/*`, `~/`) — those are compile-time only.
581
+
582
+ **Vitest workaround** (rare): if resources need engine bootstrap or transitive `node_modules` imports that don't compose with dynamic import:
583
+ ```typescript
584
+ import { preloadResources } from '@classytic/arc/testing';
585
+
586
+ export const preloadedResources = preloadResources(
587
+ import.meta.glob('../../src/resources/**/*.resource.ts', { eager: true, import: 'default' }),
588
+ );
589
+ ```
590
+
591
+ **Unified role check** — checks both platform AND org roles:
545
592
 
546
593
  ```typescript
547
594
  import { roles } from '@classytic/arc/permissions';
@@ -552,7 +599,7 @@ permissions: {
552
599
  // Also: requireRoles(['admin'], { includeOrgRoles: true }) for backward compat
553
600
  ```
554
601
 
555
- **DX helpers** (v2.4.4):
602
+ **DX helpers:**
556
603
 
557
604
  ```typescript
558
605
  // Typed request for wrapHandler: false routes — no more (req as any).user