@classytic/arc 2.11.3 → 2.11.4

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 (94) hide show
  1. package/README.md +16 -11
  2. package/dist/EventTransport-BFQjw9pB.mjs +133 -0
  3. package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
  4. package/dist/adapters/index.d.mts +3 -3
  5. package/dist/adapters/index.mjs +2 -2
  6. package/dist/{adapters-D0tT2Tyo.mjs → adapters-DUUiiimH.mjs} +17 -2
  7. package/dist/audit/index.d.mts +2 -2
  8. package/dist/auth/index.d.mts +4 -4
  9. package/dist/auth/redis-session.d.mts +1 -1
  10. package/dist/cache/index.d.mts +3 -3
  11. package/dist/cli/commands/docs.mjs +1 -1
  12. package/dist/cli/commands/generate.mjs +1 -1
  13. package/dist/cli/commands/init.mjs +125 -43
  14. package/dist/core/index.d.mts +2 -2
  15. package/dist/core/index.mjs +1 -1
  16. package/dist/{core-DnUsRpuX.mjs → core-CbcQRIch.mjs} +15 -10
  17. package/dist/{createActionRouter-u3ql2EDo.mjs → createActionRouter-CIKOcNA7.mjs} +1 -1
  18. package/dist/{createApp-BFxtdKy6.mjs → createApp-C9bRrqlX.mjs} +4 -6
  19. package/dist/defineEvent-D1Ky9M1D.mjs +188 -0
  20. package/dist/docs/index.d.mts +2 -2
  21. package/dist/docs/index.mjs +1 -1
  22. package/dist/{eventPlugin-KrFIQ097.mjs → eventPlugin-Cts2-Tfj.mjs} +8 -134
  23. package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-DDJoNEPL.d.mts} +34 -7
  24. package/dist/events/index.d.mts +164 -5
  25. package/dist/events/index.mjs +128 -180
  26. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  27. package/dist/events/transports/redis-stream-entry.mjs +204 -31
  28. package/dist/events/transports/redis.d.mts +1 -1
  29. package/dist/factory/index.d.mts +1 -1
  30. package/dist/factory/index.mjs +1 -1
  31. package/dist/{fields-C8Y0XLAu.d.mts → fields-BRjxOAFp.d.mts} +1 -1
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/idempotency/index.d.mts +3 -3
  34. package/dist/idempotency/index.mjs +1 -1
  35. package/dist/idempotency/redis.d.mts +1 -1
  36. package/dist/{index-6u4_Gg6G.d.mts → index-CXXRbnf8.d.mts} +51 -5
  37. package/dist/{index-DdQ3O9Pg.d.mts → index-D9t1KNaB.d.mts} +2 -2
  38. package/dist/{index-BbMrcvGp.d.mts → index-Rg8axYPz.d.mts} +12 -4
  39. package/dist/{index-BdXnTPRj.d.mts → index-m8mOOlFW.d.mts} +3 -3
  40. package/dist/{index-BYCqHCVu.d.mts → index-rHjXmJar.d.mts} +3 -3
  41. package/dist/index.d.mts +7 -7
  42. package/dist/index.mjs +3 -3
  43. package/dist/integrations/event-gateway.d.mts +2 -2
  44. package/dist/integrations/index.d.mts +2 -2
  45. package/dist/integrations/mcp/index.d.mts +2 -2
  46. package/dist/integrations/mcp/index.mjs +1 -1
  47. package/dist/integrations/mcp/testing.d.mts +1 -1
  48. package/dist/integrations/mcp/testing.mjs +1 -1
  49. package/dist/integrations/websocket-redis.d.mts +1 -1
  50. package/dist/integrations/websocket.d.mts +1 -1
  51. package/dist/middleware/index.d.mts +1 -1
  52. package/dist/{openapi-BGUn7Ki1.mjs → openapi-D7G1V7ex.mjs} +1 -1
  53. package/dist/org/index.d.mts +2 -2
  54. package/dist/permissions/index.d.mts +2 -2
  55. package/dist/pipeline/index.d.mts +1 -1
  56. package/dist/plugins/index.d.mts +5 -5
  57. package/dist/plugins/tracing-entry.d.mts +1 -1
  58. package/dist/plugins/tracing-entry.mjs +1 -1
  59. package/dist/presets/filesUpload.d.mts +4 -4
  60. package/dist/presets/index.d.mts +1 -1
  61. package/dist/presets/multiTenant.d.mts +1 -1
  62. package/dist/presets/search.d.mts +2 -2
  63. package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
  64. package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
  65. package/dist/redis-stream-xTGxB2bm.d.mts +232 -0
  66. package/dist/registry/index.d.mts +1 -1
  67. package/dist/{resourceToTools-ByZpgjeH.mjs → resourceToTools-CxNmI6xF.mjs} +2 -2
  68. package/dist/scope/index.d.mts +2 -2
  69. package/dist/testing/index.d.mts +2 -2
  70. package/dist/testing/index.mjs +1 -1
  71. package/dist/testing/storageContract.d.mts +1 -1
  72. package/dist/types/index.d.mts +4 -4
  73. package/dist/types/storage.d.mts +1 -1
  74. package/dist/{types-9beEMe25.d.mts → types-BQ9TJQNy.d.mts} +1 -1
  75. package/dist/{types-BH7dEGvU.d.mts → types-D7KpfiL1.d.mts} +10 -10
  76. package/dist/utils/index.d.mts +1 -1
  77. package/dist/{versioning-M9lNLhO8.d.mts → versioning-DsglKfM_.d.mts} +1 -1
  78. package/package.json +1 -1
  79. package/skills/arc/SKILL.md +409 -769
  80. package/dist/redis-stream-CM8TXTix.d.mts +0 -110
  81. /package/dist/{EventTransport-CfVEGaEl.d.mts → EventTransport-CYNUXdCJ.d.mts} +0 -0
  82. /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BQQXZ_VR.d.mts} +0 -0
  83. /package/dist/{errorHandler-Co3lnVmJ.d.mts → errorHandler-DEWmGWPz.d.mts} +0 -0
  84. /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
  85. /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
  86. /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
  87. /package/dist/{pluralize-BneOJkpi.mjs → pluralize-CWP6MB39.mjs} +0 -0
  88. /package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-Dy2p4MxS.mjs} +0 -0
  89. /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
  90. /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
  91. /package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-Cp4uKC1U.mjs} +0 -0
  92. /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
  93. /package/dist/{types-tgR4Pt8F.d.mts → types-DDyTPc6y.d.mts} +0 -0
  94. /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
package/README.md CHANGED
@@ -1,21 +1,19 @@
1
1
  # @classytic/arc
2
2
 
3
- Database-agnostic resource framework for Fastify. One `defineResource()` call → REST + auth + permissions + events + caching + OpenAPI + MCP tools — without boilerplate.
3
+ Database-agnostic resource framework for Fastify. One `defineResource()` call → REST + auth + permissions + events + caching + OpenAPI + MCP tools.
4
4
 
5
- **v2.11** · Fastify 5+ · Node.js 22+ · ESM only
5
+ Fastify 5+ · Node.js 22+ · ESM only
6
6
 
7
7
  ```bash
8
- # Core
9
8
  npm install @classytic/arc fastify
10
9
 
11
- # Security defaults that createApp() enables out of the box
10
+ # Security defaults createApp() loads (each opt-out via `cors: false` etc.)
12
11
  npm install @fastify/cors @fastify/helmet @fastify/rate-limit @fastify/under-pressure @fastify/sensible
13
- # (each is opt-out via `cors: false` / `helmet: false` / etc.)
14
12
 
15
- # Pick a storage adapter
16
- npm install @classytic/mongokit mongoose # MongoDB (most common)
13
+ # Storage adapter pick one
14
+ npm install @classytic/mongokit mongoose # MongoDB
17
15
  # OR @classytic/sqlitekit drizzle-orm better-sqlite3 (sqlite)
18
- # OR bring your own: implement RepositoryLike from @classytic/repo-core
16
+ # OR implement RepositoryLike from @classytic/repo-core
19
17
  ```
20
18
 
21
19
  ---
@@ -40,12 +38,19 @@ import { createApp, loadResources } from '@classytic/arc/factory';
40
38
 
41
39
  await mongoose.connect(process.env.DB_URI);
42
40
 
41
+ // Fail fast on missing CORS env — silent `undefined` here drops to surprising
42
+ // browser defaults. Browser apps: declare an explicit allowlist (below).
43
+ // Server-to-server / API-key services: `cors: { origin: '*', credentials: false }`
44
+ // or `cors: false` to disable entirely (CORS is a browser-only concern).
45
+ const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS;
46
+ if (!ALLOWED_ORIGINS) throw new Error('ALLOWED_ORIGINS env is required');
47
+
43
48
  const app = await createApp({
44
49
  preset: 'production',
45
50
  resourcePrefix: '/api/v1',
46
51
  resources: await loadResources(import.meta.url), // auto-discover *.resource.ts
47
52
  auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
48
- cors: { origin: process.env.ALLOWED_ORIGINS?.split(',') },
53
+ cors: { origin: ALLOWED_ORIGINS.split(','), credentials: true },
49
54
  });
50
55
 
51
56
  await app.listen({ port: 8040, host: '0.0.0.0' });
@@ -67,7 +72,7 @@ resources: async () => {
67
72
  },
68
73
  ```
69
74
 
70
- `loadResources({ context })` (2.11.1+) threads engine handles into resources whose default export is `(ctx) => defineResource(...)`. No parallel factory files, no `exclude: [...]` bookkeeping.
75
+ `loadResources({ context })` threads engine handles into resources whose default export is `(ctx) => defineResource(...)`. No parallel factory files, no `exclude: [...]` bookkeeping.
71
76
 
72
77
  ---
73
78
 
@@ -206,7 +211,7 @@ const ctx = await createTestApp({
206
211
  authMode: 'jwt',
207
212
  connectMongoose: true, // in-memory Mongo + Mongoose connect
208
213
  });
209
- ctx.auth.register('admin', { user: { id: '1', roles: ['admin'] }, orgId: 'org-1' });
214
+ ctx.auth.register('admin', { user: { id: '1', role: 'admin' }, orgId: 'org-1' });
210
215
 
211
216
  const res = await ctx.app.inject({
212
217
  method: 'POST',
@@ -0,0 +1,133 @@
1
+ //#region src/events/EventTransport.ts
2
+ /**
3
+ * In-memory event transport (default)
4
+ * Events are delivered synchronously within the process.
5
+ * Not suitable for multi-instance deployments.
6
+ */
7
+ var MemoryEventTransport = class {
8
+ name = "memory";
9
+ handlers = /* @__PURE__ */ new Map();
10
+ logger;
11
+ constructor(options) {
12
+ this.logger = options?.logger ?? console;
13
+ }
14
+ async publish(event) {
15
+ const exactHandlers = this.handlers.get(event.type) ?? /* @__PURE__ */ new Set();
16
+ const wildcardHandlers = this.handlers.get("*") ?? /* @__PURE__ */ new Set();
17
+ const patternHandlers = /* @__PURE__ */ new Set();
18
+ for (const [pattern, handlers] of this.handlers.entries()) if (pattern.endsWith(".*")) {
19
+ const prefix = pattern.slice(0, -2);
20
+ if (event.type.startsWith(`${prefix}.`)) for (const h of handlers) patternHandlers.add(h);
21
+ }
22
+ const allHandlers = new Set([
23
+ ...exactHandlers,
24
+ ...wildcardHandlers,
25
+ ...patternHandlers
26
+ ]);
27
+ for (const handler of allHandlers) try {
28
+ await handler(event);
29
+ } catch (err) {
30
+ this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
31
+ }
32
+ }
33
+ /**
34
+ * Reference `publishMany` implementation — delegates to `publish()` in order.
35
+ *
36
+ * Production transports (Kafka, Redis pipeline, SQS batch) should override
37
+ * this with a single batched network call. Memory transport has nothing to
38
+ * batch, so we just loop — the loop still returns a proper result map so
39
+ * `EventOutbox.relay` can exercise the batched code path in tests.
40
+ */
41
+ async publishMany(events) {
42
+ const results = /* @__PURE__ */ new Map();
43
+ for (const event of events) try {
44
+ await this.publish(event);
45
+ results.set(event.meta.id, null);
46
+ } catch (err) {
47
+ results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
48
+ }
49
+ return results;
50
+ }
51
+ async subscribe(pattern, handler) {
52
+ if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
53
+ this.handlers.get(pattern)?.add(handler);
54
+ return () => {
55
+ const set = this.handlers.get(pattern);
56
+ if (set) {
57
+ set.delete(handler);
58
+ if (set.size === 0) this.handlers.delete(pattern);
59
+ }
60
+ };
61
+ }
62
+ async close() {
63
+ this.handlers.clear();
64
+ }
65
+ };
66
+ /**
67
+ * Create a domain event with auto-generated metadata.
68
+ *
69
+ * `id` and `timestamp` are filled in; everything else is caller-controlled.
70
+ * Set `schemaVersion` explicitly for any event type you plan to evolve.
71
+ */
72
+ function createEvent(type, payload, meta) {
73
+ return {
74
+ type,
75
+ payload,
76
+ meta: {
77
+ id: crypto.randomUUID(),
78
+ timestamp: /* @__PURE__ */ new Date(),
79
+ ...meta
80
+ }
81
+ };
82
+ }
83
+ /**
84
+ * Create a child event that chains causation from a parent event.
85
+ *
86
+ * Rules:
87
+ * - `causationId` is set to the parent's `id` (direct cause)
88
+ * - `correlationId` is inherited from the parent if set, else falls back
89
+ * to the parent's `id` (root correlation)
90
+ * - `userId` / `organizationId` are inherited when not overridden so the
91
+ * whole chain stays scoped to the originating principal/tenant
92
+ *
93
+ * Caller-supplied `meta` wins over inherited fields — pass `{ userId: newActor }`
94
+ * to override when a subsystem acts on behalf of a different principal.
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * const orderPlaced = createEvent('order.placed', { orderId: 'o1' }, {
99
+ * correlationId: req.id, userId: user.id,
100
+ * });
101
+ * await events.publish(orderPlaced);
102
+ *
103
+ * // Downstream handler emits a child event:
104
+ * const reserved = createChildEvent(orderPlaced, 'inventory.reserved', {
105
+ * orderId: 'o1', skus: ['sku-1', 'sku-2'],
106
+ * });
107
+ * // reserved.meta.causationId === orderPlaced.meta.id
108
+ * // reserved.meta.correlationId === orderPlaced.meta.correlationId
109
+ * // reserved.meta.userId === user.id (inherited)
110
+ * ```
111
+ */
112
+ function createChildEvent(parent, type, payload, meta) {
113
+ const inherited = {
114
+ correlationId: parent.meta.correlationId ?? parent.meta.id,
115
+ causationId: parent.meta.id
116
+ };
117
+ if (parent.meta.userId !== void 0) inherited.userId = parent.meta.userId;
118
+ if (parent.meta.organizationId !== void 0) inherited.organizationId = parent.meta.organizationId;
119
+ if (parent.meta.source !== void 0) inherited.source = parent.meta.source;
120
+ if (parent.meta.idempotencyKey !== void 0) inherited.idempotencyKey = parent.meta.idempotencyKey;
121
+ return {
122
+ type,
123
+ payload,
124
+ meta: {
125
+ id: crypto.randomUUID(),
126
+ timestamp: /* @__PURE__ */ new Date(),
127
+ ...inherited,
128
+ ...meta
129
+ }
130
+ };
131
+ }
132
+ //#endregion
133
+ export { createChildEvent as n, createEvent as r, MemoryEventTransport as t };
@@ -1,4 +1,4 @@
1
- import { r as CacheStore } from "./interface-Da0r7Lna.mjs";
1
+ import { r as CacheStore } from "./interface-beEtJyWM.mjs";
2
2
 
3
3
  //#region src/cache/QueryCache.d.ts
4
4
  /** Metadata wrapper stored in CacheStore */
@@ -1,3 +1,3 @@
1
- import { An as RelationMetadata, En as AdapterFactory, Mn as SchemaMetadata, Nn as ValidationResult, On as DataAdapter, jn as RepositoryLike, kn as FieldMetadata } from "../index-6u4_Gg6G.mjs";
2
- import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, d as DrizzleAdapterOptions, f as createDrizzleAdapter, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter, u as DrizzleAdapter } from "../index-BbMrcvGp.mjs";
3
- export { AdapterFactory, DataAdapter, DrizzleAdapter, DrizzleAdapterOptions, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
1
+ import { An as FieldMetadata, Dn as AdapterRepositoryInput, En as AdapterFactory, Fn as asRepositoryLike, Mn as RepositoryLike, Nn as SchemaMetadata, Pn as ValidationResult, jn as RelationMetadata, kn as DataAdapter } from "../index-CXXRbnf8.mjs";
2
+ import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, d as DrizzleAdapterOptions, f as createDrizzleAdapter, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter, u as DrizzleAdapter } from "../index-Rg8axYPz.mjs";
3
+ export { AdapterFactory, AdapterRepositoryInput, DataAdapter, DrizzleAdapter, DrizzleAdapterOptions, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, asRepositoryLike, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
@@ -1,2 +1,2 @@
1
- import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, o as DrizzleAdapter, r as createPrismaAdapter, s as createDrizzleAdapter, t as PrismaAdapter } from "../adapters-D0tT2Tyo.mjs";
2
- export { DrizzleAdapter, MongooseAdapter, PrismaAdapter, PrismaQueryParser, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
1
+ import { a as createMongooseAdapter, c as createDrizzleAdapter, i as MongooseAdapter, n as PrismaQueryParser, o as asRepositoryLike, r as createPrismaAdapter, s as DrizzleAdapter, t as PrismaAdapter } from "../adapters-DUUiiimH.mjs";
2
+ export { DrizzleAdapter, MongooseAdapter, PrismaAdapter, PrismaQueryParser, asRepositoryLike, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
@@ -264,6 +264,21 @@ function createDrizzleAdapter(options) {
264
264
  return new DrizzleAdapter(options);
265
265
  }
266
266
  //#endregion
267
+ //#region src/adapters/interface.ts
268
+ /**
269
+ * Widen a permissive `AdapterRepositoryInput<TDoc>` to arc's strict
270
+ * `RepositoryLike<TDoc>` view. Single-source escape hatch for the
271
+ * filter-IR drift documented on `AdapterRepositoryInput`.
272
+ *
273
+ * Arc internals (audit / outbox / idempotency, BaseController) still see
274
+ * the IR-aware `RepositoryLike`; only the call paths arc exercises are
275
+ * shared between the two views, and those use the narrower
276
+ * `Record<string, unknown>` filter shape both sides agree on.
277
+ */
278
+ function asRepositoryLike(input) {
279
+ return input;
280
+ }
281
+ //#endregion
267
282
  //#region src/adapters/mongoose.ts
268
283
  /**
269
284
  * Mongoose data adapter with proper type safety
@@ -280,7 +295,7 @@ var MongooseAdapter = class {
280
295
  if (!isMongooseModel(options.model)) throw new TypeError("MongooseAdapter: Invalid model. Expected Mongoose Model instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })");
281
296
  if (!isRepository(options.repository)) throw new TypeError("MongooseAdapter: Invalid repository. Expected StandardRepo instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })");
282
297
  this.model = options.model;
283
- this.repository = options.repository;
298
+ this.repository = asRepositoryLike(options.repository);
284
299
  this.schemaGenerator = options.schemaGenerator;
285
300
  this.name = `MongooseAdapter<${options.model.modelName}>`;
286
301
  }
@@ -946,4 +961,4 @@ function createPrismaAdapter(options) {
946
961
  return new PrismaAdapter(options);
947
962
  }
948
963
  //#endregion
949
- export { createMongooseAdapter as a, MongooseAdapter as i, PrismaQueryParser as n, DrizzleAdapter as o, createPrismaAdapter as r, createDrizzleAdapter as s, PrismaAdapter as t };
964
+ export { createMongooseAdapter as a, createDrizzleAdapter as c, MongooseAdapter as i, PrismaQueryParser as n, asRepositoryLike as o, createPrismaAdapter as r, DrizzleAdapter as s, PrismaAdapter as t };
@@ -1,5 +1,5 @@
1
- import { jn as RepositoryLike } from "../index-6u4_Gg6G.mjs";
2
- import { d as UserBase } from "../fields-C8Y0XLAu.mjs";
1
+ import { Mn as RepositoryLike } from "../index-CXXRbnf8.mjs";
2
+ import { d as UserBase } from "../fields-BRjxOAFp.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
 
5
5
  //#region src/audit/stores/interface.d.ts
@@ -1,7 +1,7 @@
1
- import { Ot as AuthHelpers, kt as AuthPluginOptions } from "../index-6u4_Gg6G.mjs";
2
- import { c as PermissionCheck } from "../fields-C8Y0XLAu.mjs";
3
- import { t as ExternalOpenApiPaths } from "../externalPaths-Bapitwvd.mjs";
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-D-oNWHz3.mjs";
1
+ import { Ot as AuthHelpers, kt as AuthPluginOptions } from "../index-CXXRbnf8.mjs";
2
+ import { c as PermissionCheck } from "../fields-BRjxOAFp.mjs";
3
+ import { t as ExternalOpenApiPaths } from "../externalPaths-BD5nw6St.mjs";
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";
5
5
  import { FastifyPluginAsync, FastifyReply as FastifyReply$1, FastifyRequest } from "fastify";
6
6
 
7
7
  //#region src/auth/authPlugin.d.ts
@@ -1,4 +1,4 @@
1
- import { i as SessionData, s as SessionStore } from "../sessionManager-D-oNWHz3.mjs";
1
+ import { i as SessionData, s as SessionStore } from "../sessionManager-C4Le_UB3.mjs";
2
2
 
3
3
  //#region src/auth/redis-session.d.ts
4
4
  /** Minimal Redis client interface — compatible with ioredis */
@@ -1,6 +1,6 @@
1
- import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-Da0r7Lna.mjs";
2
- import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-DOBNHBE0.mjs";
3
- import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-BUXBSm4F.mjs";
1
+ import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-beEtJyWM.mjs";
2
+ import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-D41bfdBB.mjs";
3
+ import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-CqMdLI2-.mjs";
4
4
 
5
5
  //#region src/cache/keys.d.ts
6
6
  /**
@@ -1,5 +1,5 @@
1
1
  import { t as ResourceRegistry } from "../../ResourceRegistry-DkAeAuTX.mjs";
2
- import { t as buildOpenApiSpec } from "../../openapi-BGUn7Ki1.mjs";
2
+ import { t as buildOpenApiSpec } from "../../openapi-D7G1V7ex.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,4 +1,4 @@
1
- import { t as pluralize } from "../../pluralize-BneOJkpi.mjs";
1
+ import { t as pluralize } from "../../pluralize-CWP6MB39.mjs";
2
2
  import { join } from "node:path";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  //#region src/cli/commands/generate.ts
@@ -87,42 +87,103 @@ function existsSync$1(filePath) {
87
87
  }
88
88
  }
89
89
  /**
90
- * Install dependencies using the detected package manager
90
+ * Single source of truth for scaffolded project dependencies.
91
+ *
92
+ * Versions are pinned to the floor each subsystem requires — peer-dep
93
+ * minimums on Arc, kit minimums (mongokit ≥ 3.11, repo-core ≥ 0.2,
94
+ * mongoose ≥ 9.4.1), and major-version stable for the rest. The carets
95
+ * allow minor + patch upgrades without breaking arc's contract, while
96
+ * preventing the silent breakage of `@latest` on a kit floor bump.
97
+ *
98
+ * Used by both `packageJsonTemplate` (declares the deps in the generated
99
+ * `package.json` so `npm install` works without a pre-pass) and
100
+ * `installDependencies` (runs the package manager's `install` against
101
+ * the declared ranges). One source — no drift.
91
102
  */
92
- async function installDependencies(projectPath, config, pm) {
93
- const deps = [
94
- "@classytic/arc@latest",
95
- "fastify@latest",
96
- "@fastify/cors@latest",
97
- "@fastify/helmet@latest",
98
- "@fastify/rate-limit@latest",
99
- "@fastify/sensible@latest",
100
- "@fastify/under-pressure@latest",
101
- "dotenv@latest"
102
- ];
103
- if (config.auth === "better-auth") deps.push("better-auth@^1.6.0", "mongodb@latest");
104
- else deps.push("@fastify/jwt@latest", "bcryptjs@latest");
105
- if (config.adapter === "mongokit") deps.push("@classytic/mongokit@^3.11.0", "@classytic/repo-core@^0.2.0", "mongoose@^9.4.1");
106
- const devDeps = ["vitest@latest", "pino-pretty@latest"];
107
- if (config.typescript) devDeps.push("typescript@latest", "@types/node@latest", "tsx@latest");
108
- const installCmd = getInstallCommand(pm, deps, false);
109
- const installDevCmd = getInstallCommand(pm, devDeps, true);
103
+ const SCAFFOLD_DEP_VERSIONS = {
104
+ core: {
105
+ "@classytic/arc": "^2.11.3",
106
+ "@fastify/cors": "^11.0.0",
107
+ "@fastify/helmet": "^13.0.0",
108
+ "@fastify/rate-limit": "^10.0.0",
109
+ "@fastify/sensible": "^6.0.0",
110
+ "@fastify/under-pressure": "^9.0.0",
111
+ dotenv: "^17.0.0",
112
+ fastify: "^5.8.0"
113
+ },
114
+ authJwt: {
115
+ "@fastify/jwt": "^10.0.0",
116
+ bcryptjs: "^3.0.0"
117
+ },
118
+ authBetterAuth: {
119
+ "better-auth": "^1.6.0",
120
+ mongodb: "^6.10.0"
121
+ },
122
+ adapterMongokit: {
123
+ "@classytic/mongokit": "^3.11.0",
124
+ "@classytic/repo-core": "^0.2.0",
125
+ mongoose: "^9.4.1"
126
+ },
127
+ devCommon: {
128
+ "pino-pretty": "^13.0.0",
129
+ vitest: "^4.0.0"
130
+ },
131
+ devTypescript: {
132
+ "@types/node": "^22.0.0",
133
+ tsx: "^4.21.0",
134
+ typescript: "^5.6.0"
135
+ },
136
+ typesJwt: { "@types/bcryptjs": "^3.0.0" }
137
+ };
138
+ /**
139
+ * Resolve the dependency manifest for a scaffold configuration.
140
+ *
141
+ * Returns sorted records (alphabetical by package name) so the generated
142
+ * `package.json` is deterministic — diffs across re-runs stay clean.
143
+ */
144
+ function resolveScaffoldDependencies(config) {
145
+ const dependencies = { ...SCAFFOLD_DEP_VERSIONS.core };
146
+ const devDependencies = { ...SCAFFOLD_DEP_VERSIONS.devCommon };
147
+ if (config.auth === "better-auth") Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authBetterAuth);
148
+ else {
149
+ Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authJwt);
150
+ if (config.typescript) Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.typesJwt);
151
+ }
152
+ if (config.adapter === "mongokit") Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.adapterMongokit);
153
+ if (config.typescript) Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.devTypescript);
154
+ return {
155
+ dependencies: sortByKey(dependencies),
156
+ devDependencies: sortByKey(devDependencies)
157
+ };
158
+ }
159
+ /**
160
+ * Sort a record alphabetically by key — package.json convention.
161
+ */
162
+ function sortByKey(record) {
163
+ return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
164
+ }
165
+ /**
166
+ * Install dependencies using the detected package manager.
167
+ *
168
+ * Dependencies are already declared in the generated `package.json` (see
169
+ * `packageJsonTemplate`), so a single plain `install` resolves the full
170
+ * tree. No two-pass `npm add` flow — the manifest is the source of truth.
171
+ */
172
+ async function installDependencies(projectPath, _config, pm) {
110
173
  console.log(` Installing dependencies...`);
111
- await runCommand(installCmd, projectPath);
112
- console.log(` Installing dev dependencies...`);
113
- await runCommand(installDevCmd, projectPath);
174
+ await runCommand(getInstallCommand(pm), projectPath);
114
175
  console.log(`\nDependencies installed successfully.`);
115
176
  }
116
177
  /**
117
- * Get the install command for a package manager
178
+ * Get the plain `install` command for a package manager. Reads the declared
179
+ * dependencies from the project's `package.json`.
118
180
  */
119
- function getInstallCommand(pm, packages, isDev) {
120
- const pkgList = packages.join(" ");
181
+ function getInstallCommand(pm) {
121
182
  switch (pm) {
122
- case "pnpm": return `pnpm add ${isDev ? "-D" : ""} ${pkgList}`;
123
- case "yarn": return `yarn add ${isDev ? "-D" : ""} ${pkgList}`;
124
- case "bun": return `bun add ${isDev ? "-d" : ""} ${pkgList}`;
125
- default: return `npm install ${isDev ? "--save-dev" : ""} ${pkgList}`;
183
+ case "pnpm": return "pnpm install";
184
+ case "yarn": return "yarn install";
185
+ case "bun": return "bun install";
186
+ default: return "npm install";
126
187
  }
127
188
  }
128
189
  /**
@@ -267,6 +328,7 @@ async function createProjectStructure(projectPath, config) {
267
328
  }
268
329
  }
269
330
  function packageJsonTemplate(config) {
331
+ const { dependencies, devDependencies } = resolveScaffoldDependencies(config);
270
332
  const scripts = config.typescript ? config.edge ? {
271
333
  dev: "tsx watch src/index.ts",
272
334
  build: "tsc",
@@ -309,6 +371,8 @@ function packageJsonTemplate(config) {
309
371
  "#utils/*": "./src/utils/*"
310
372
  },
311
373
  scripts,
374
+ dependencies,
375
+ devDependencies,
312
376
  engines: { node: ">=22" }
313
377
  }, null, 2);
314
378
  }
@@ -1808,8 +1872,10 @@ describe('Example Resource', () => {
1808
1872
  authMode: 'jwt',
1809
1873
  ${config.adapter === "mongokit" ? " connectMongoose: true,\n" : ""} });
1810
1874
 
1875
+ // Arc's permission engine reads singular user.role — string,
1876
+ // comma-separated string, or array all normalise via getUserRoles().
1811
1877
  ctx.auth${ts ? "!" : ""}.register('admin', {
1812
- user: { id: '1', roles: ['admin'] },
1878
+ user: { id: '1', role: 'admin' },
1813
1879
  orgId: 'org-1',
1814
1880
  });
1815
1881
  });
@@ -1913,13 +1979,19 @@ userSchema.methods.removeOrganization = function(organizationId${ts ? ": Types.O
1913
1979
  userSchema.index({ 'organizations.organizationId': 1 });
1914
1980
  ` : "";
1915
1981
  const userType = ts ? `
1916
- type PlatformRole = 'user' | 'admin' | 'superadmin';
1982
+ const PLATFORM_ROLES = ['user', 'admin', 'superadmin'] as const;
1983
+ type PlatformRole = typeof PLATFORM_ROLES[number];
1917
1984
 
1985
+ /**
1986
+ * Comma-separated list of platform roles (Better Auth admin-plugin convention).
1987
+ * Single role: 'admin'. Multiple: 'admin,trainer'. Arc's permission engine
1988
+ * normalises both forms via getUserRoles() — see @classytic/arc/scope.
1989
+ */
1918
1990
  type User = {
1919
1991
  name: string;
1920
1992
  email: string;
1921
1993
  password: string;
1922
- roles: PlatformRole[];${config.tenant === "multi" ? `
1994
+ role: string;${config.tenant === "multi" ? `
1923
1995
  organizations: UserOrganization[];` : ""}
1924
1996
  resetPasswordToken?: string;
1925
1997
  resetPasswordExpires?: Date;
@@ -1958,11 +2030,21 @@ const userSchema = new Schema${ts ? "<User, UserModel, UserMethods>" : ""}(
1958
2030
  },
1959
2031
  password: { type: String, required: true },
1960
2032
 
1961
- // Platform roles
1962
- roles: {
1963
- type: [String],
1964
- enum: ['user', 'admin', 'superadmin'],
1965
- default: ['user'],
2033
+ // Platform role — singular field, matches Arc's permission engine
2034
+ // (req.user.role) and Better Auth's admin-plugin convention.
2035
+ // Comma-separated for multi-role users (e.g. 'admin,trainer');
2036
+ // getUserRoles() in @classytic/arc/scope normalises both forms.
2037
+ role: {
2038
+ type: String,
2039
+ required: true,
2040
+ default: 'user',
2041
+ index: true,
2042
+ validate: {
2043
+ validator: (v${ts ? ": string" : ""}) =>
2044
+ /^(user|admin|superadmin)(,(user|admin|superadmin))*$/.test(v),
2045
+ message: (props${ts ? ": { value: string }" : ""}) =>
2046
+ \`Invalid role "\${props.value}" — expected one or more of user|admin|superadmin\`,
2047
+ },
1966
2048
  },
1967
2049
  ${orgSchema}
1968
2050
  // Password reset
@@ -2381,7 +2463,7 @@ export async function register(request${ts ? ": FastifyRequest" : ""}, reply${ts
2381
2463
  }
2382
2464
 
2383
2465
  // Create user
2384
- await userRepository.create({ name, email, password, roles: ['user'] });
2466
+ await userRepository.create({ name, email, password, role: 'user' });
2385
2467
 
2386
2468
  return reply.code(201).send({ success: true, message: 'User registered successfully' });
2387
2469
  } catch (error) {
@@ -2506,10 +2588,10 @@ export async function updateUserProfile(request${ts ? ": FastifyRequest" : ""},
2506
2588
  const userId = (request${ts ? " as any" : ""}).user?._id || (request${ts ? " as any" : ""}).user?.id;
2507
2589
  const updates = { ...request.body${ts ? " as any" : ""} };
2508
2590
 
2509
- // Prevent updating protected fields
2510
- if ('password' in updates) delete updates.password;
2511
- if ('roles' in updates) delete updates.roles;
2512
- if ('organizations' in updates) delete updates.organizations;
2591
+ // Prevent updating protected fields — auth-managed only
2592
+ delete updates.password;
2593
+ delete updates.role;
2594
+ delete updates.organizations;
2513
2595
 
2514
2596
  const user = await userRepository.Model.findByIdAndUpdate(userId, updates, { new: true });
2515
2597
  return reply.send({ success: true, data: user });
@@ -2598,7 +2680,7 @@ export const loginResponse = {
2598
2680
  id: { type: 'string' },
2599
2681
  name: { type: 'string' },
2600
2682
  email: { type: 'string' },
2601
- roles: { type: 'array', items: { type: 'string' } },
2683
+ role: { type: 'string' },
2602
2684
  },
2603
2685
  },
2604
2686
  accessToken: { type: 'string' },
@@ -1,3 +1,3 @@
1
- import { B as ResourceDefinition, Gt as TreeMixin, Ht as SoftDeleteExt, Jt as BulkExt, Kt as SlugExt, Ut as SoftDeleteMixin, V as defineResource, Vt as BaseController, Wt as TreeExt, Yt as BulkMixin, an as QueryResolverConfig, cn as AccessControl, in as QueryResolver, ln as AccessControlConfig, nn as BaseCrudController, on as BodySanitizer, qt as SlugMixin, rn as ListResult, sn as BodySanitizerConfig, tn as BaseControllerOptions } from "../index-6u4_Gg6G.mjs";
2
- import { C as MAX_REGEX_LENGTH, D as RESERVED_QUERY_PARAMS, E as MutationOperation, O as SYSTEM_FIELDS, S as MAX_FILTER_DEPTH, T as MUTATION_OPERATIONS, _ as DEFAULT_UPDATE_METHOD, a as getControllerScope, b as HookOperation, c as createCrudRouter, d as CrudOperation, f as DEFAULT_ID_FIELD, g as DEFAULT_TENANT_FIELD, h as DEFAULT_SORT, i as getControllerContext, l as createPermissionMiddleware, m as DEFAULT_MAX_LIMIT, n as createFastifyHandler, o as sendControllerResponse, p as DEFAULT_LIMIT, r as createRequestContext, s as defineResourceVariants, t as createCrudHandlers, u as CRUD_OPERATIONS, v as HOOK_OPERATIONS, w as MAX_SEARCH_LENGTH, x as HookPhase, y as HOOK_PHASES } from "../index-BdXnTPRj.mjs";
1
+ import { B as ResourceDefinition, Gt as TreeMixin, Ht as SoftDeleteExt, Jt as BulkExt, Kt as SlugExt, Ut as SoftDeleteMixin, V as defineResource, Vt as BaseController, Wt as TreeExt, Yt as BulkMixin, an as QueryResolverConfig, cn as AccessControl, in as QueryResolver, ln as AccessControlConfig, nn as BaseCrudController, on as BodySanitizer, qt as SlugMixin, rn as ListResult, sn as BodySanitizerConfig, tn as BaseControllerOptions } from "../index-CXXRbnf8.mjs";
2
+ import { C as MAX_REGEX_LENGTH, D as RESERVED_QUERY_PARAMS, E as MutationOperation, O as SYSTEM_FIELDS, S as MAX_FILTER_DEPTH, T as MUTATION_OPERATIONS, _ as DEFAULT_UPDATE_METHOD, a as getControllerScope, b as HookOperation, c as createCrudRouter, d as CrudOperation, f as DEFAULT_ID_FIELD, g as DEFAULT_TENANT_FIELD, h as DEFAULT_SORT, i as getControllerContext, l as createPermissionMiddleware, m as DEFAULT_MAX_LIMIT, n as createFastifyHandler, o as sendControllerResponse, p as DEFAULT_LIMIT, r as createRequestContext, s as defineResourceVariants, t as createCrudHandlers, u as CRUD_OPERATIONS, v as HOOK_OPERATIONS, w as MAX_SEARCH_LENGTH, x as HookPhase, y as HOOK_PHASES } from "../index-m8mOOlFW.mjs";
3
3
  export { AccessControl, AccessControlConfig, 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, defineResource, defineResourceVariants, 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-BhY1OHoH.mjs";
2
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-swXruJ2_.mjs";
3
3
  import { _ as getControllerContext, g as createRequestContext, h as createFastifyHandler, m as createCrudHandlers, v as getControllerScope, y as sendControllerResponse } from "../routerShared-BqLRb5l7.mjs";
4
- import { a as createPermissionMiddleware, i as createCrudRouter, n as ResourceDefinition, r as defineResource, t as defineResourceVariants } from "../core-DnUsRpuX.mjs";
4
+ import { a as createPermissionMiddleware, i as createCrudRouter, n as ResourceDefinition, r as defineResource, t as defineResourceVariants } from "../core-CbcQRIch.mjs";
5
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, defineResource, defineResourceVariants, getControllerContext, getControllerScope, sendControllerResponse };
@@ -470,15 +470,20 @@ function resolveOrAutoCreateController(resolvedConfig, adapter, repository, hasC
470
470
  else arcLog("defineResource").warn(`Resource "${resolvedConfig.name}" declares a custom \`queryParser\` but its controller does not expose \`setQueryParser(qp)\`. The parser will NOT be threaded into the controller's query resolution — operator filters (\`[contains]\`, \`[like]\`, etc.) may fall back to the controller's internal default. Extend \`BaseController\` / \`BaseCrudController\` (both implement \`setQueryParser\`) OR add the method to your custom controller to honor the resource-level parser.`);
471
471
  }
472
472
  if (controller) {
473
- const droppedOptions = [];
474
- if (resolvedConfig.tenantField !== void 0) droppedOptions.push("tenantField");
475
- if (resolvedConfig.schemaOptions !== void 0 && Object.keys(resolvedConfig.schemaOptions).length > 0) droppedOptions.push("schemaOptions");
476
- if (resolvedConfig.idField !== void 0) droppedOptions.push("idField");
477
- if (resolvedConfig.defaultSort !== void 0) droppedOptions.push("defaultSort");
478
- if (resolvedConfig.cache !== void 0) droppedOptions.push("cache");
479
- if (resolvedConfig.onFieldWriteDenied !== void 0) droppedOptions.push("onFieldWriteDenied");
480
- if (resolvedConfig._controllerOptions !== void 0) droppedOptions.push("preset-injected fields (slug/softDelete/parent)");
481
- if (droppedOptions.length > 0) arcLog("defineResource").warn(`Resource "${resolvedConfig.name}" declares a custom controller AND resource-level option(s) [${droppedOptions.join(", ")}]. Arc only threads these when it auto-builds the controller — when you pass your own, they are dropped silently and the controller falls back to its own defaults (e.g. tenantField → 'organizationId'). Forward them to your controller's \`super(repo, { ... })\` call. Same root cause as the \`queryParser\` warn above.`);
473
+ const authorOptions = [];
474
+ if (resolvedConfig.tenantField !== void 0) authorOptions.push("tenantField");
475
+ if (resolvedConfig.schemaOptions !== void 0 && Object.keys(resolvedConfig.schemaOptions).length > 0) authorOptions.push("schemaOptions");
476
+ if (resolvedConfig.idField !== void 0) authorOptions.push("idField");
477
+ if (resolvedConfig.defaultSort !== void 0) authorOptions.push("defaultSort");
478
+ if (resolvedConfig.cache !== void 0) authorOptions.push("cache");
479
+ if (resolvedConfig.onFieldWriteDenied !== void 0) authorOptions.push("onFieldWriteDenied");
480
+ if (authorOptions.length > 0) arcLog("defineResource").warn(`Resource "${resolvedConfig.name}" declares a custom controller AND resource-level option(s) [${authorOptions.join(", ")}]. Arc only threads these when it auto-builds the controller — when you pass your own, they are dropped silently and the controller falls back to its own defaults (e.g. tenantField → 'organizationId'). Forward them to your controller's \`super(repo, { ... })\` call. Same root cause as the \`queryParser\` warn above.`);
481
+ if (resolvedConfig._controllerOptions !== void 0) {
482
+ const presetFields = [];
483
+ if (resolvedConfig._controllerOptions.slugField) presetFields.push("slugField");
484
+ if (resolvedConfig._controllerOptions.parentField) presetFields.push("parentField");
485
+ arcLog("defineResource").warn(`Resource "${resolvedConfig.name}" applies a preset that injects controller field(s) [${presetFields.join(", ") || "preset metadata"}] (e.g. slugLookup / softDelete / parent), but the resource also declares a custom controller. Preset metadata only reaches arc's auto-built BaseController — your custom controller will not see \`slugField\`/\`parentField\`/etc. Either (a) drop the preset on this resource (\`presets: [...]\` without it), or (b) extend \`BaseController\` / \`BaseCrudController\` so arc auto-builds the controller and threads the preset fields automatically.`);
486
+ }
482
487
  return controller;
483
488
  }
484
489
  if (!hasCrudRoutes || !repository) return controller;
@@ -881,7 +886,7 @@ var ResourceDefinition = class {
881
886
  fields: self.fields
882
887
  });
883
888
  if (self.actions && Object.keys(self.actions).length > 0) {
884
- const { createActionRouter } = await import("./createActionRouter-u3ql2EDo.mjs").then((n) => n.n);
889
+ const { createActionRouter } = await import("./createActionRouter-CIKOcNA7.mjs").then((n) => n.n);
885
890
  createActionRouter(typedInstance, {
886
891
  ...normalizeActionsToRouterConfig(self.actions, self.actionPermissions, self.tag, self.permissions, self.name, typedInstance.log),
887
892
  resourceName: self.name,
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
2
  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-BqLRb5l7.mjs";
3
- import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-BlG9bY7v.mjs";
3
+ import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-Dy2p4MxS.mjs";
4
4
  //#region src/core/createActionRouter.ts
5
5
  var createActionRouter_exports = /* @__PURE__ */ __exportAll({
6
6
  buildActionBodySchema: () => buildActionBodySchema,