@classytic/arc 2.4.3 → 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 (82) hide show
  1. package/README.md +57 -6
  2. package/dist/{BaseController-CkM5dUh_.mjs → BaseController-AbbRx3e0.mjs} +5 -2
  3. package/dist/{ResourceRegistry-DeCIFlix.mjs → ResourceRegistry-C6ngvOnn.mjs} +1 -0
  4. package/dist/adapters/index.d.mts +2 -2
  5. package/dist/adapters/index.mjs +1 -1
  6. package/dist/{adapters-DTC4Ug66.mjs → adapters-CTn28N4y.mjs} +72 -11
  7. package/dist/audit/index.d.mts +32 -6
  8. package/dist/audit/index.mjs +32 -4
  9. package/dist/audit/mongodb.d.mts +1 -1
  10. package/dist/auth/index.d.mts +1 -1
  11. package/dist/auth/index.mjs +2 -2
  12. package/dist/cli/commands/docs.mjs +1 -1
  13. package/dist/cli/commands/init.mjs +12 -9
  14. package/dist/cli/commands/introspect.mjs +1 -1
  15. package/dist/core/index.d.mts +2 -2
  16. package/dist/core/index.mjs +2 -2
  17. package/dist/{createApp-CBgVaFyh.mjs → createApp-D2w0LdYJ.mjs} +431 -290
  18. package/dist/{defineResource-B22gcNvn.mjs → defineResource-Ckxg6HrZ.mjs} +125 -22
  19. package/dist/discovery/index.mjs +1 -1
  20. package/dist/docs/index.d.mts +1 -1
  21. package/dist/dynamic/index.d.mts +1 -1
  22. package/dist/dynamic/index.mjs +2 -2
  23. package/dist/{elevation-Ca_yveIO.d.mts → elevation-C_taLQrM.d.mts} +27 -1
  24. package/dist/{errorHandler-DMbGdzBG.mjs → errorHandler-r2595m8T.mjs} +1 -1
  25. package/dist/{errors-CPpvPHT0.d.mts → errors-CcVbl1-T.d.mts} +17 -1
  26. package/dist/{errors-rxhfP7Hf.mjs → errors-NoQKsbAT.mjs} +23 -1
  27. package/dist/{eventPlugin-iGrSEmwJ.d.mts → eventPlugin-DW45v4V5.d.mts} +30 -2
  28. package/dist/events/index.d.mts +2 -2
  29. package/dist/events/index.mjs +40 -10
  30. package/dist/factory/index.d.mts +44 -23
  31. package/dist/factory/index.mjs +152 -2
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/idempotency/index.d.mts +3 -3
  34. package/dist/idempotency/mongodb.d.mts +1 -1
  35. package/dist/idempotency/redis.d.mts +1 -1
  36. package/dist/{index-BL8CaQih.d.mts → index-B4uZm82R.d.mts} +2 -2
  37. package/dist/{index-yhxyjqNb.d.mts → index-DrCqa3Jq.d.mts} +4 -8
  38. package/dist/{index-Diqcm14c.d.mts → index-NGZksqM5.d.mts} +30 -1
  39. package/dist/index.d.mts +6 -6
  40. package/dist/index.mjs +8 -7
  41. package/dist/integrations/event-gateway.mjs +1 -1
  42. package/dist/integrations/index.d.mts +1 -1
  43. package/dist/integrations/mcp/index.d.mts +4 -2
  44. package/dist/integrations/mcp/index.mjs +1 -1
  45. package/dist/integrations/mcp/testing.d.mts +1 -1
  46. package/dist/integrations/mcp/testing.mjs +1 -1
  47. package/dist/{interface-DGmPxakH.d.mts → interface-CrN45qz1.d.mts} +229 -13
  48. package/dist/{mongodb-CUpYfxfD.d.mts → mongodb-kltrBPa1.d.mts} +10 -0
  49. package/dist/{mongodb-bga9AbkD.d.mts → mongodb-pMvOlR5_.d.mts} +1 -1
  50. package/dist/org/index.d.mts +1 -1
  51. package/dist/org/index.mjs +1 -1
  52. package/dist/permissions/index.d.mts +2 -2
  53. package/dist/permissions/index.mjs +2 -2
  54. package/dist/{permissions-Jk5x3sxz.mjs → permissions-C8ImI8gC.mjs} +44 -2
  55. package/dist/plugins/index.d.mts +1 -1
  56. package/dist/plugins/index.mjs +4 -4
  57. package/dist/plugins/tracing-entry.mjs +1 -1
  58. package/dist/presets/index.d.mts +1 -1
  59. package/dist/presets/index.mjs +1 -1
  60. package/dist/presets/multiTenant.d.mts +1 -1
  61. package/dist/presets/multiTenant.mjs +1 -1
  62. package/dist/{presets-OMPaHMTY.mjs → presets-BMfdy34e.mjs} +2 -2
  63. package/dist/{redis-CQ5YxMC5.d.mts → redis-D0Qc-9EW.d.mts} +1 -1
  64. package/dist/registry/index.d.mts +1 -1
  65. package/dist/registry/index.mjs +1 -1
  66. package/dist/{resourceToTools-PMFE8HIv.mjs → resourceToTools-DH3c3e-T.mjs} +81 -7
  67. package/dist/scope/index.d.mts +2 -2
  68. package/dist/scope/index.mjs +2 -2
  69. package/dist/{sse-BkViJPlT.mjs → sse-BF7GR7IB.mjs} +1 -1
  70. package/dist/testing/index.d.mts +26 -3
  71. package/dist/testing/index.mjs +46 -2
  72. package/dist/types/index.d.mts +3 -3
  73. package/dist/types/index.mjs +23 -2
  74. package/dist/{types-C6TQjtdi.mjs → types-BhtYdxZU.mjs} +26 -1
  75. package/dist/{types-Dt0-AI6E.d.mts → types-C1Z28coa.d.mts} +195 -6
  76. package/dist/{types-BJmgxNbF.d.mts → types-DurlBP2N.d.mts} +1 -1
  77. package/dist/utils/index.d.mts +2 -2
  78. package/dist/utils/index.mjs +1 -1
  79. package/package.json +6 -5
  80. package/skills/arc/SKILL.md +151 -4
  81. package/skills/arc/references/mcp.md +160 -2
  82. /package/dist/{interface-B4awm1RJ.d.mts → interface-gr-7qo9j.d.mts} +0 -0
package/README.md CHANGED
@@ -15,26 +15,77 @@ npm install @classytic/mongokit mongoose # MongoDB adapter
15
15
 
16
16
  ```typescript
17
17
  import mongoose from 'mongoose';
18
- import { createApp } from '@classytic/arc/factory';
18
+ import { createApp, loadResources } from '@classytic/arc/factory';
19
19
 
20
20
  await mongoose.connect(process.env.DB_URI);
21
21
 
22
22
  const app = await createApp({
23
23
  preset: 'production',
24
+ resourcePrefix: '/api/v1',
25
+ resources: await loadResources(import.meta.url), // auto-discovers *.resource.ts
24
26
  auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
25
27
  cors: { origin: process.env.ALLOWED_ORIGINS?.split(',') },
26
28
  });
27
29
 
28
- await app.register(productResource.toPlugin());
29
30
  await app.listen({ port: 8040, host: '0.0.0.0' });
30
31
  ```
31
32
 
33
+ Three ways to register resources:
34
+
35
+ ```typescript
36
+ // Auto-discover from directory (recommended)
37
+ resources: await loadResources(import.meta.url), // dev/prod parity
38
+
39
+ // Explicit array
40
+ resources: [productResource, orderResource],
41
+
42
+ // Via plugins callback (full Fastify control)
43
+ plugins: async (f) => { await f.register(productResource.toPlugin()); },
44
+ ```
45
+
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
+ ```
82
+
32
83
  ## defineResource
33
84
 
34
85
  Single API for a full REST resource with routes, permissions, and behaviors:
35
86
 
36
87
  ```typescript
37
- import { defineResource, createMongooseAdapter, allowPublic, requireRoles } from '@classytic/arc';
88
+ import { defineResource, createMongooseAdapter, allowPublic, roles } from '@classytic/arc';
38
89
 
39
90
  const productResource = defineResource({
40
91
  name: 'product',
@@ -43,9 +94,9 @@ const productResource = defineResource({
43
94
  permissions: {
44
95
  list: allowPublic(),
45
96
  get: allowPublic(),
46
- create: requireRoles(['admin']),
47
- update: requireRoles(['admin']),
48
- delete: requireRoles(['admin']),
97
+ create: roles('admin', 'editor'), // checks platform + org roles
98
+ update: roles('admin', 'editor'),
99
+ delete: roles('admin'),
49
100
  },
50
101
  cache: { staleTime: 30, gcTime: 300, tags: ['catalog'] }, // QueryCache (opt-in)
51
102
  additionalRoutes: [
@@ -1,5 +1,5 @@
1
1
  import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-Cxde4rpC.mjs";
2
- import { d as isMember, n as PUBLIC_SCOPE, r as getOrgId, u as isElevated } from "./types-C6TQjtdi.mjs";
2
+ import { d as isElevated, f as isMember, i as getOrgId, n as PUBLIC_SCOPE } from "./types-BhtYdxZU.mjs";
3
3
  import { t as buildQueryKey } from "./keys-qcD-TVJl.mjs";
4
4
  import { getUserId } from "./types/index.mjs";
5
5
  import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-ipsbIRPK.mjs";
@@ -247,7 +247,10 @@ var BodySanitizer = class {
247
247
  let sanitized = { ...body };
248
248
  for (const field of SYSTEM_FIELDS) delete sanitized[field];
249
249
  const fieldRules = this.schemaOptions.fieldRules ?? {};
250
- for (const [field, rules] of Object.entries(fieldRules)) if (rules.systemManaged || rules.readonly) delete sanitized[field];
250
+ for (const [field, rules] of Object.entries(fieldRules)) {
251
+ if (rules.systemManaged || rules.readonly) delete sanitized[field];
252
+ if (_operation === "update" && (rules.immutable || rules.immutableAfterCreate)) delete sanitized[field];
253
+ }
251
254
  if (req) {
252
255
  const arcContext = meta ?? req.metadata;
253
256
  const scope = arcContext?._scope ?? PUBLIC_SCOPE;
@@ -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-DGmPxakH.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-yhxyjqNb.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 };
@@ -1,2 +1,2 @@
1
- import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-DTC4Ug66.mjs";
1
+ import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-CTn28N4y.mjs";
2
2
  export { MongooseAdapter, PrismaAdapter, PrismaQueryParser, createMongooseAdapter, createPrismaAdapter };
@@ -78,17 +78,28 @@ var MongooseAdapter = class {
78
78
  const properties = {};
79
79
  const required = [];
80
80
  const fieldRules = schemaOptions?.fieldRules || {};
81
- const blockedFields = new Set(Object.entries(fieldRules).filter(([, rules]) => rules.systemManaged || rules.hidden).map(([field]) => field));
81
+ const blockedFields = new Set([
82
+ ...Object.entries(fieldRules).filter(([, rules]) => rules.systemManaged || rules.hidden).map(([field]) => field),
83
+ ...schemaOptions?.excludeFields ?? [],
84
+ ...schemaOptions?.hiddenFields ?? []
85
+ ]);
86
+ const readonlySet = new Set(schemaOptions?.readonlyFields ?? []);
87
+ const optionalSet = new Set(schemaOptions?.optionalFields ?? []);
82
88
  for (const [fieldName, schemaType] of Object.entries(paths)) {
83
89
  if (fieldName.startsWith("__")) continue;
84
90
  if (blockedFields.has(fieldName)) continue;
85
91
  const typeInfo = schemaType;
86
92
  properties[fieldName] = this.mongooseTypeToOpenApi(typeInfo);
87
- if (typeInfo.isRequired) required.push(fieldName);
93
+ if (typeInfo.isRequired && !optionalSet.has(fieldName) && !fieldRules[fieldName]?.optional) required.push(fieldName);
88
94
  }
89
- const systemFieldSet = new Set(SYSTEM_FIELDS);
90
- const inputProperties = Object.fromEntries(Object.entries(properties).filter(([field]) => !systemFieldSet.has(field)));
91
- const inputRequired = required.filter((field) => !systemFieldSet.has(field));
95
+ const readonlyForInput = new Set([...readonlySet]);
96
+ for (const [field, rules] of Object.entries(fieldRules)) if (rules.immutable || rules.immutableAfterCreate) readonlyForInput.add(field);
97
+ const inputBlockedSet = new Set([...SYSTEM_FIELDS, ...readonlyForInput]);
98
+ const inputProperties = Object.fromEntries(Object.entries(properties).filter(([field]) => !inputBlockedSet.has(field)));
99
+ const inputRequired = required.filter((field) => !inputBlockedSet.has(field) && !blockedFields.has(field));
100
+ const immutableSet = /* @__PURE__ */ new Set();
101
+ for (const [field, rules] of Object.entries(fieldRules)) if (rules.immutable || rules.immutableAfterCreate) immutableSet.add(field);
102
+ const updateProperties = Object.fromEntries(Object.entries(inputProperties).filter(([field]) => !immutableSet.has(field)));
92
103
  return {
93
104
  createBody: {
94
105
  type: "object",
@@ -97,11 +108,12 @@ var MongooseAdapter = class {
97
108
  },
98
109
  updateBody: {
99
110
  type: "object",
100
- properties: inputProperties
111
+ properties: updateProperties
101
112
  },
102
113
  response: {
103
114
  type: "object",
104
- properties
115
+ properties,
116
+ additionalProperties: true
105
117
  }
106
118
  };
107
119
  } catch {
@@ -147,18 +159,67 @@ var MongooseAdapter = class {
147
159
  break;
148
160
  case "Date":
149
161
  baseType.type = "string";
150
- baseType.format = "date-time";
151
162
  break;
152
163
  case "ObjectID":
153
164
  case "ObjectId":
154
165
  baseType.type = "string";
155
166
  baseType.pattern = "^[a-f\\d]{24}$";
156
167
  break;
157
- case "Array":
168
+ case "Array": {
158
169
  baseType.type = "array";
159
- baseType.items = { type: "string" };
170
+ const ti = typeInfo;
171
+ if (ti.$isMongooseDocumentArray && ti.schema) {
172
+ const subSchema = ti.schema;
173
+ const subProps = {};
174
+ const subRequired = [];
175
+ for (const [subField, subType] of Object.entries(subSchema.paths)) {
176
+ if (subField.startsWith("_")) continue;
177
+ subProps[subField] = this.mongooseTypeToOpenApi(subType);
178
+ if (subType.isRequired) subRequired.push(subField);
179
+ }
180
+ baseType.items = {
181
+ type: "object",
182
+ properties: subProps,
183
+ ...subRequired.length > 0 ? { required: subRequired } : {},
184
+ additionalProperties: true
185
+ };
186
+ } else if (ti.embeddedSchemaType?.instance) baseType.items = this.mongooseTypeToOpenApi(ti.embeddedSchemaType);
187
+ else baseType.items = {};
188
+ break;
189
+ }
190
+ case "Mixed":
191
+ baseType.type = [
192
+ "string",
193
+ "number",
194
+ "boolean",
195
+ "object",
196
+ "array"
197
+ ];
198
+ break;
199
+ case "Map":
200
+ baseType.type = "object";
201
+ baseType.additionalProperties = true;
202
+ break;
203
+ case "Embedded":
204
+ case "SubDocument":
205
+ baseType.type = "object";
206
+ baseType.additionalProperties = true;
207
+ break;
208
+ case "Buffer":
209
+ baseType.type = "string";
210
+ baseType.format = "binary";
211
+ break;
212
+ case "Decimal128":
213
+ baseType.type = "string";
214
+ baseType.description = "Decimal128 (high-precision number as string)";
215
+ break;
216
+ case "UUID":
217
+ baseType.type = "string";
218
+ baseType.format = "uuid";
160
219
  break;
161
- default: baseType.type = "object";
220
+ default:
221
+ baseType.type = "object";
222
+ baseType.additionalProperties = true;
162
223
  }
163
224
  return baseType;
164
225
  }
@@ -1,4 +1,4 @@
1
- import { a as AuditContext, c as AuditStore, i as AuditAction, l as AuditStoreOptions, n as MongoAuditStoreOptions, o as AuditEntry, r as MongoConnection, s as AuditQueryOptions, u as createAuditEntry } from "../mongodb-CUpYfxfD.mjs";
1
+ import { a as AuditContext, c as AuditStore, i as AuditAction, l as AuditStoreOptions, n as MongoAuditStoreOptions, o as AuditEntry, r as MongoConnection, s as AuditQueryOptions, u as createAuditEntry } from "../mongodb-kltrBPa1.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/audit/auditPlugin.d.ts
@@ -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" {
@@ -11,7 +11,7 @@ function createAuditEntry(resource, documentId, action, context, data) {
11
11
  resource,
12
12
  documentId,
13
13
  action,
14
- userId: context.user?._id?.toString() ?? context.user?.id,
14
+ userId: extractUserId(context.user),
15
15
  organizationId: context.organizationId,
16
16
  before: data?.before,
17
17
  after: data?.after,
@@ -24,6 +24,16 @@ function createAuditEntry(resource, documentId, action, context, data) {
24
24
  };
25
25
  }
26
26
  /**
27
+ * Extract userId from a user object — DB-agnostic.
28
+ * Handles Mongoose ObjectId, string, number, or any type with toString().
29
+ */
30
+ function extractUserId(user) {
31
+ if (!user) return void 0;
32
+ const raw = user._id ?? user.id;
33
+ if (raw == null) return void 0;
34
+ return typeof raw === "string" ? raw : String(raw);
35
+ }
36
+ /**
27
37
  * Detect changed fields between two objects
28
38
  */
29
39
  function detectChanges(before, after) {
@@ -170,16 +180,34 @@ const auditPlugin = async (fastify, opts = {}) => {
170
180
  "update",
171
181
  "delete"
172
182
  ];
173
- const ops = typeof autoAuditConfig === "object" ? autoAuditConfig.operations ?? defaultOps : defaultOps;
174
- 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).");
175
189
  fastify.addHook("onReady", async () => {
176
190
  const arc = "arc" in fastify ? fastify.arc : void 0;
177
191
  if (!arc?.hooks) {
178
192
  fastify.log?.debug?.("Auto-audit skipped: arc-core plugin not registered");
179
193
  return;
180
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
+ }
181
203
  for (const op of ops) arc.hooks.after("*", op, async (ctx) => {
182
- 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;
183
211
  const docId = autoAuditExtractId(ctx.result);
184
212
  const scope = ctx.context?._scope;
185
213
  const auditCtx = {
@@ -1,2 +1,2 @@
1
- import { n as MongoAuditStoreOptions, t as MongoAuditStore } from "../mongodb-CUpYfxfD.mjs";
1
+ import { n as MongoAuditStoreOptions, t as MongoAuditStore } from "../mongodb-kltrBPa1.mjs";
2
2
  export { MongoAuditStore, type MongoAuditStoreOptions };
@@ -1,4 +1,4 @@
1
- import { m as AuthPluginOptions, p as AuthHelpers } from "../interface-DGmPxakH.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,6 +1,6 @@
1
1
  import { n as normalizeRoles, t as getUserRoles } from "../types-ZUu_h0jp.mjs";
2
- import { t as ArcError } from "../errors-rxhfP7Hf.mjs";
3
- import { c as requireOrgMembership, f as requireTeamMembership, l as requireOrgRole } from "../permissions-Jk5x3sxz.mjs";
2
+ import { t as ArcError } from "../errors-NoQKsbAT.mjs";
3
+ import { c as requireOrgMembership, f as requireTeamMembership, l as requireOrgRole } from "../permissions-C8ImI8gC.mjs";
4
4
  import { n as extractBetterAuthOpenApi } from "../betterAuthOpenApi-lz0IRbXJ.mjs";
5
5
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
6
6
  import fp from "fastify-plugin";
@@ -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,7 +1,7 @@
1
+ import * as fs from "node:fs/promises";
1
2
  import * as path from "node:path";
2
3
  import { accessSync } from "node:fs";
3
4
  import { execSync, spawn } from "node:child_process";
4
- import * as fs from "node:fs/promises";
5
5
  import * as readline from "node:readline";
6
6
  //#region src/cli/commands/init.ts
7
7
  /**
@@ -694,13 +694,13 @@ import { getAuth } from './auth.js';
694
694
  */
695
695
 
696
696
  ${typeImport}import config from '#config/index.js';
697
- import { createApp } from '@classytic/arc/factory';
697
+ import { createApp, loadResources } from '@classytic/arc/factory';
698
698
  ${betterAuthImport}
699
699
  // App-specific plugins
700
700
  import { registerPlugins } from '#plugins/index.js';
701
701
 
702
702
  // Resource registry
703
- import { registerResources } from '#resources/index.js';
703
+ import { resources, registerResources } from '#resources/index.js';
704
704
 
705
705
  /**
706
706
  * Create a fully configured app instance
@@ -708,9 +708,10 @@ import { registerResources } from '#resources/index.js';
708
708
  * @returns Configured Fastify instance ready to use
709
709
  */
710
710
  export async function createAppInstance()${ts ? ": Promise<FastifyInstance>" : ""} {
711
- // Create Arc app with base configuration
711
+ // Create Arc app with resources and base configuration
712
712
  const app = await createApp({
713
713
  preset: config.env === 'production' ? (${config.edge ? "'edge'" : "'production'"}) : 'development',
714
+ resources,
714
715
  ${authConfig}
715
716
  cors: {
716
717
  origin: config.cors.origins,
@@ -727,9 +728,6 @@ export async function createAppInstance()${ts ? ": Promise<FastifyInstance>" : "
727
728
  // Register app-specific plugins (explicit dependency injection)
728
729
  await registerPlugins(app, { config });
729
730
 
730
- // Register all resources
731
- await registerResources(app);
732
-
733
731
  return app;
734
732
  }
735
733
 
@@ -1275,6 +1273,7 @@ import {
1275
1273
  requireRoles,
1276
1274
  requireOwnership,
1277
1275
  allowPublic,
1276
+ roles,
1278
1277
  anyOf,
1279
1278
  allOf,
1280
1279
  denyAll,
@@ -1287,6 +1286,7 @@ export {
1287
1286
  requireAuth,
1288
1287
  requireRoles,
1289
1288
  requireOwnership,
1289
+ roles,
1290
1290
  allOf,
1291
1291
  anyOf,
1292
1292
  denyAll,
@@ -1323,11 +1323,14 @@ export const requireSuperadmin = ()${returnType} =>
1323
1323
  /**
1324
1324
  * Organization-level guards (per-org member.role):
1325
1325
  *
1326
- * - requireOrgRole(['admin','owner']) — checks member.role in active org
1326
+ * - roles('admin') — checks BOTH user.role AND org member.role (recommended)
1327
+ * - requireOrgRole(['admin','owner']) — checks member.role in active org ONLY
1327
1328
  * - requireOrgMembership() — just checks if user is in the org (any role)
1328
1329
  * - requireTeamMembership() — checks if user is in the active team
1329
1330
  *
1330
- * These are DIFFERENT from platform-level helpers above (requireRoles checks user.role).
1331
+ * RECOMMENDED: Use roles() for most cases it checks platform + org roles automatically.
1332
+ * Use requireOrgRole() when you ONLY want org-level checks (exclude platform admins).
1333
+ *
1331
1334
  * Platform superadmin automatically bypasses all org role checks.
1332
1335
  *
1333
1336
  * IMPORTANT: When using Better Auth's Access Control (ac) with custom roles,
@@ -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 { Ct as QueryResolverConfig, Dt as AccessControlConfig, Et as AccessControl, Lt as ResourceDefinition, Rt as defineResource, St as QueryResolver, Tt as BodySanitizerConfig, bt as BaseController, wt as BodySanitizer, xt as BaseControllerOptions } from "../interface-DGmPxakH.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-BL8CaQih.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
- import { i as AccessControl, n as QueryResolver, r as BodySanitizer, t as BaseController } from "../BaseController-CkM5dUh_.mjs";
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-B22gcNvn.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 };