@classytic/arc 2.11.4 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +16 -12
  2. package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
  3. package/dist/EventTransport-CT_52aWU.d.mts +34 -0
  4. package/dist/EventTransport-DLWoUMHy.mjs +103 -0
  5. package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
  6. package/dist/audit/index.d.mts +2 -2
  7. package/dist/audit/index.mjs +1 -1
  8. package/dist/auth/audit.d.mts +199 -0
  9. package/dist/auth/audit.mjs +288 -0
  10. package/dist/auth/index.d.mts +3 -3
  11. package/dist/auth/index.mjs +117 -191
  12. package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
  13. package/dist/buildHandler-olo-gt94.mjs +610 -0
  14. package/dist/cache/index.mjs +3 -3
  15. package/dist/cli/commands/describe.d.mts +89 -13
  16. package/dist/cli/commands/describe.mjs +56 -2
  17. package/dist/cli/commands/docs.mjs +2 -2
  18. package/dist/cli/commands/generate.mjs +147 -48
  19. package/dist/cli/commands/init.d.mts +13 -0
  20. package/dist/cli/commands/init.mjs +130 -87
  21. package/dist/cli/commands/introspect.mjs +8 -1
  22. package/dist/context/index.mjs +1 -1
  23. package/dist/core/index.d.mts +3 -3
  24. package/dist/core/index.mjs +5 -5
  25. package/dist/core-DECn6zaU.mjs +1399 -0
  26. package/dist/{createActionRouter-CIKOcNA7.mjs → createActionRouter-CBxLLbn3.mjs} +7 -20
  27. package/dist/createAggregationRouter-CRIBv4sC.mjs +114 -0
  28. package/dist/{createApp-C9bRrqlX.mjs → createApp-XX2-N0Yd.mjs} +28 -22
  29. package/dist/{defineEvent-D1Ky9M1D.mjs → defineEvent-D5h7EvAx.mjs} +1 -1
  30. package/dist/docs/index.d.mts +24 -11
  31. package/dist/docs/index.mjs +2 -2
  32. package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
  33. package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
  34. package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
  35. package/dist/errors-j4aJm1Wg.mjs +184 -0
  36. package/dist/{eventPlugin-Cts2-Tfj.mjs → eventPlugin-CaKTYkYM.mjs} +28 -4
  37. package/dist/{eventPlugin-DDJoNEPL.d.mts → eventPlugin-qXpqTebY.d.mts} +24 -1
  38. package/dist/events/index.d.mts +6 -6
  39. package/dist/events/index.mjs +11 -35
  40. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  41. package/dist/events/transports/redis.d.mts +1 -1
  42. package/dist/factory/index.d.mts +2 -2
  43. package/dist/factory/index.mjs +2 -2
  44. package/dist/{fields-BRjxOAFp.d.mts → fields-COhcH3fk.d.mts} +23 -2
  45. package/dist/hooks/index.d.mts +1 -1
  46. package/dist/hooks/index.mjs +1 -1
  47. package/dist/idempotency/index.d.mts +1 -1
  48. package/dist/idempotency/index.mjs +1 -20
  49. package/dist/idempotency/redis.mjs +1 -1
  50. package/dist/{index-rHjXmJar.d.mts → index-BTqLEvhu.d.mts} +163 -3
  51. package/dist/{index-CXXRbnf8.d.mts → index-BtW7qYwa.d.mts} +660 -326
  52. package/dist/{index-m8mOOlFW.d.mts → index-Ds61mrJE.d.mts} +50 -4
  53. package/dist/{index-D9t1KNaB.d.mts → index-Dz5IKsrE.d.mts} +360 -219
  54. package/dist/index.d.mts +6 -7
  55. package/dist/index.mjs +9 -10
  56. package/dist/integrations/event-gateway.d.mts +1 -1
  57. package/dist/integrations/event-gateway.mjs +1 -1
  58. package/dist/integrations/index.d.mts +1 -1
  59. package/dist/integrations/mcp/index.d.mts +2 -2
  60. package/dist/integrations/mcp/index.mjs +1 -1
  61. package/dist/integrations/mcp/testing.d.mts +1 -1
  62. package/dist/integrations/mcp/testing.mjs +1 -1
  63. package/dist/integrations/streamline.d.mts +60 -11
  64. package/dist/integrations/streamline.mjs +75 -85
  65. package/dist/integrations/websocket.mjs +2 -8
  66. package/dist/middleware/index.d.mts +1 -1
  67. package/dist/middleware/index.mjs +2 -2
  68. package/dist/migrations/index.d.mts +23 -3
  69. package/dist/migrations/index.mjs +0 -7
  70. package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
  71. package/dist/openapi-noXno2CV.mjs +968 -0
  72. package/dist/org/index.d.mts +2 -2
  73. package/dist/org/index.mjs +1 -1
  74. package/dist/permissions/index.d.mts +3 -3
  75. package/dist/permissions/index.mjs +3 -3
  76. package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
  77. package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
  78. package/dist/pipeline/index.d.mts +1 -1
  79. package/dist/pipeline/index.mjs +1 -1
  80. package/dist/plugins/index.d.mts +16 -31
  81. package/dist/plugins/index.mjs +33 -13
  82. package/dist/plugins/response-cache.mjs +1 -1
  83. package/dist/plugins/tracing-entry.mjs +1 -1
  84. package/dist/presets/filesUpload.d.mts +4 -4
  85. package/dist/presets/filesUpload.mjs +6 -9
  86. package/dist/presets/index.d.mts +1 -1
  87. package/dist/presets/index.mjs +1 -1
  88. package/dist/presets/multiTenant.d.mts +1 -1
  89. package/dist/presets/multiTenant.mjs +2 -2
  90. package/dist/presets/search.d.mts +2 -2
  91. package/dist/presets/search.mjs +6 -8
  92. package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
  93. package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
  94. package/dist/{redis-stream-xTGxB2bm.d.mts → redis-stream-D6HzR1Z_.d.mts} +1 -1
  95. package/dist/registry/index.d.mts +1 -1
  96. package/dist/registry/index.mjs +2 -2
  97. package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
  98. package/dist/{resourceToTools-CxNmI6xF.mjs → resourceToTools-DLL32us3.mjs} +224 -71
  99. package/dist/{routerShared-BqLRb5l7.mjs → routerShared-DrOa-26E.mjs} +41 -36
  100. package/dist/{schemaIR-Dy2p4MxS.mjs → schemaIR-lYhC2gE5.mjs} +1 -1
  101. package/dist/schemas/index.d.mts +100 -30
  102. package/dist/schemas/index.mjs +86 -29
  103. package/dist/scim/index.d.mts +264 -0
  104. package/dist/scim/index.mjs +963 -0
  105. package/dist/scope/index.d.mts +3 -3
  106. package/dist/scope/index.mjs +4 -4
  107. package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
  108. package/dist/{store-helpers-Cp4uKC1U.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
  109. package/dist/testing/index.d.mts +2 -8
  110. package/dist/testing/index.mjs +16 -24
  111. package/dist/types/index.d.mts +4 -4
  112. package/dist/{types-D7KpfiL1.d.mts → types-BvqwCCSx.d.mts} +73 -25
  113. package/dist/{types-DDyTPc6y.d.mts → types-CTYvcwHe.d.mts} +195 -1
  114. package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
  115. package/dist/{types-BQ9TJQNy.d.mts → types-DQHFc8PM.d.mts} +1 -1
  116. package/dist/utils/index.d.mts +2 -2
  117. package/dist/utils/index.mjs +5 -5
  118. package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
  119. package/dist/{versioning-DsglKfM_.d.mts → versioning-DTTvc80y.d.mts} +1 -1
  120. package/package.json +24 -34
  121. package/skills/arc/SKILL.md +147 -51
  122. package/skills/arc/references/agent-auth.md +238 -0
  123. package/skills/arc/references/api-reference.md +187 -0
  124. package/skills/arc/references/auth.md +354 -7
  125. package/skills/arc/references/enterprise-auth.md +94 -0
  126. package/skills/arc/references/events.md +8 -6
  127. package/skills/arc/references/mcp.md +2 -2
  128. package/skills/arc/references/multi-tenancy.md +11 -2
  129. package/skills/arc/references/production.md +10 -9
  130. package/skills/arc/references/scim.md +247 -0
  131. package/skills/arc/references/testing.md +1 -1
  132. package/skills/arc-code-review/SKILL.md +141 -0
  133. package/skills/arc-code-review/references/anti-patterns.md +911 -0
  134. package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
  135. package/skills/arc-code-review/references/migration-recipes.md +700 -0
  136. package/skills/arc-code-review/references/mongokit-migration.md +386 -0
  137. package/skills/arc-code-review/references/scaffolding.md +230 -0
  138. package/skills/arc-code-review/references/severity.md +127 -0
  139. package/dist/EventTransport-BFQjw9pB.mjs +0 -133
  140. package/dist/EventTransport-CYNUXdCJ.d.mts +0 -293
  141. package/dist/adapters/index.d.mts +0 -3
  142. package/dist/adapters/index.mjs +0 -2
  143. package/dist/adapters-DUUiiimH.mjs +0 -964
  144. package/dist/auth/mongoose.d.mts +0 -191
  145. package/dist/auth/mongoose.mjs +0 -73
  146. package/dist/core-CbcQRIch.mjs +0 -1054
  147. package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
  148. package/dist/errorHandler-DEWmGWPz.d.mts +0 -114
  149. package/dist/errors-D5c-5BJL.mjs +0 -232
  150. package/dist/index-Rg8axYPz.d.mts +0 -370
  151. package/dist/openapi-D7G1V7ex.mjs +0 -557
  152. /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
  153. /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
  154. /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
  155. /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
  156. /package/dist/{elevation-BQQXZ_VR.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
  157. /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
  158. /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
  159. /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
  160. /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
  161. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-DQgqgifU.mjs} +0 -0
  162. /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
  163. /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
  164. /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
  165. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
  166. /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
  167. /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
@@ -1,18 +1,94 @@
1
+ import { V as ResourceDefinition, ft as RouteSchemaOptions, rt as RateLimitConfig } from "../../index-BtW7qYwa.mjs";
2
+
1
3
  //#region src/cli/commands/describe.d.ts
4
+ interface DescribedResource {
5
+ name: string;
6
+ displayName: string;
7
+ prefix: string;
8
+ tag: string;
9
+ module?: string;
10
+ adapter: {
11
+ type: string;
12
+ name: string;
13
+ } | null;
14
+ permissions: Record<string, PermissionDescription>;
15
+ presets: string[];
16
+ fields?: Record<string, FieldDescription>;
17
+ pipeline?: PipelineDescription;
18
+ routes: RouteDescription[];
19
+ events: EventDescription[];
20
+ /**
21
+ * Declarative `POST /:id/action` entries surfaced for tooling parity
22
+ * with OpenAPI/MCP. Empty when the resource declares no actions.
23
+ */
24
+ actions: ActionDescription[];
25
+ /** Per-resource fallback for actions without their own permission. */
26
+ actionPermissions?: PermissionDescription;
27
+ /**
28
+ * Declarative `GET /aggregations/:name` entries (v2.13). Empty when
29
+ * the resource declares no aggregations.
30
+ */
31
+ aggregations: AggregationDescription[];
32
+ schemaOptions?: RouteSchemaOptions;
33
+ rateLimit?: RateLimitConfig | false;
34
+ middlewares: string[];
35
+ }
36
+ interface ActionDescription {
37
+ name: string;
38
+ description?: string;
39
+ hasSchema: boolean;
40
+ permission: PermissionDescription;
41
+ }
42
+ interface AggregationDescription {
43
+ name: string;
44
+ summary?: string;
45
+ description?: string;
46
+ groupBy: unknown;
47
+ measures: string[];
48
+ permission: PermissionDescription;
49
+ requiresDateRange: boolean;
50
+ hasRequiredFilters: boolean;
51
+ }
52
+ interface PermissionDescription {
53
+ type: "public" | "requireAuth" | "requireRoles" | "custom";
54
+ roles?: readonly string[];
55
+ }
56
+ interface FieldDescription {
57
+ type: string;
58
+ roles?: readonly string[];
59
+ redactValue?: unknown;
60
+ }
61
+ interface PipelineDescription {
62
+ guards: PipelineStepDescription[];
63
+ transforms: PipelineStepDescription[];
64
+ interceptors: PipelineStepDescription[];
65
+ }
66
+ interface PipelineStepDescription {
67
+ name: string;
68
+ operations?: string[];
69
+ }
70
+ interface RouteDescription {
71
+ method: string;
72
+ path: string;
73
+ operation: string;
74
+ summary?: string;
75
+ description?: string;
76
+ permission?: PermissionDescription;
77
+ }
78
+ interface EventDescription {
79
+ name: string;
80
+ description?: string;
81
+ hasSchema: boolean;
82
+ hasHandler: boolean;
83
+ }
2
84
  /**
3
- * Arc CLI - Describe Command
4
- *
5
- * Machine-readable resource metadata for AI agents.
6
- * Outputs JSON with fields, permissions, pipeline, routes, events
7
- * everything an LLM needs to understand and generate code for the API.
8
- *
9
- * @example
10
- * ```bash
11
- * arc describe ./src/resources.js --json
12
- * arc describe ./src/resources.js --resource product
13
- * arc describe ./src/resources.js --pretty
14
- * ```
85
+ * Programmatic entry for the same surface the `arc describe` CLI emits per
86
+ * resource. Exported so cross-surface contract tests can verify that the
87
+ * CLI describe output stays in lockstep with registry / OpenAPI / MCP.
88
+ * Hosts that want a JSON dump of one resource without spawning the CLI
89
+ * can call this directly.
15
90
  */
91
+ declare function describeResource(resource: ResourceDefinition<unknown>, module?: string): DescribedResource;
16
92
  declare function describe(args: string[]): Promise<void>;
17
93
  //#endregion
18
- export { describe };
94
+ export { describe, describeResource };
@@ -1,4 +1,5 @@
1
- import { t as CRUD_OPERATIONS } from "../../constants-BhY1OHoH.mjs";
1
+ import { t as CRUD_OPERATIONS } from "../../constants-Cxde4rpC.mjs";
2
+ import { n as stringifyMeasureMap } from "../../ResourceRegistry-CTERg_2x.mjs";
2
3
  import { resolve } from "node:path";
3
4
  import { pathToFileURL } from "node:url";
4
5
  //#region src/cli/commands/describe.ts
@@ -147,12 +148,60 @@ function describeEvents(resourceName, events) {
147
148
  hasHandler: !!def.handler
148
149
  }));
149
150
  }
151
+ /**
152
+ * Surface declarative `actions:` so the CLI matches what OpenAPI / MCP /
153
+ * the registry already emit for the same definition. Resolves each
154
+ * action's permission via the same fallback chain runtime uses:
155
+ * per-action `permissions` → resource-level `actionPermissions` → custom.
156
+ */
157
+ function describeActions(actions, fallback) {
158
+ if (!actions) return [];
159
+ return Object.entries(actions).map(([name, entry]) => {
160
+ if (typeof entry === "function") return {
161
+ name,
162
+ hasSchema: false,
163
+ permission: describePermission(fallback)
164
+ };
165
+ return {
166
+ name,
167
+ description: entry.description,
168
+ hasSchema: !!entry.schema,
169
+ permission: describePermission(entry.permissions ?? fallback)
170
+ };
171
+ });
172
+ }
173
+ /**
174
+ * Surface declarative `aggregations:` (v2.13). Reuses
175
+ * `stringifyMeasureMap` from the registry so the measure render matches
176
+ * OpenAPI/MCP byte-for-byte — single source of truth for `'sum:price'`
177
+ * vs `'count'` vs `'percentile:latency:0.95'`.
178
+ */
179
+ function describeAggregations(aggregations) {
180
+ if (!aggregations) return [];
181
+ return Object.entries(aggregations).map(([name, entry]) => ({
182
+ name,
183
+ summary: entry.summary,
184
+ description: entry.description,
185
+ groupBy: entry.groupBy,
186
+ measures: Object.values(stringifyMeasureMap(entry.measures)),
187
+ permission: describePermission(entry.permissions),
188
+ requiresDateRange: !!entry.requireDateRange,
189
+ hasRequiredFilters: !!entry.requireFilters && entry.requireFilters.length > 0
190
+ }));
191
+ }
150
192
  function describeMiddlewares(middlewares) {
151
193
  if (!middlewares) return [];
152
194
  const ops = [];
153
195
  for (const [op, handlers] of Object.entries(middlewares)) if (handlers?.length) ops.push(`${op}(${handlers.length})`);
154
196
  return ops;
155
197
  }
198
+ /**
199
+ * Programmatic entry for the same surface the `arc describe` CLI emits per
200
+ * resource. Exported so cross-surface contract tests can verify that the
201
+ * CLI describe output stays in lockstep with registry / OpenAPI / MCP.
202
+ * Hosts that want a JSON dump of one resource without spawning the CLI
203
+ * can call this directly.
204
+ */
156
205
  function describeResource(resource, module) {
157
206
  return {
158
207
  name: resource.name,
@@ -170,6 +219,9 @@ function describeResource(resource, module) {
170
219
  pipeline: describePipeline(resource.pipe),
171
220
  routes: describeRoutes(resource),
172
221
  events: describeEvents(resource.name, resource.events),
222
+ actions: describeActions(resource.actions, resource.actionPermissions),
223
+ actionPermissions: resource.actionPermissions ? describePermission(resource.actionPermissions) : void 0,
224
+ aggregations: describeAggregations(resource.aggregations),
173
225
  schemaOptions: Object.keys(resource.schemaOptions ?? {}).length > 0 ? resource.schemaOptions : void 0,
174
226
  rateLimit: resource.rateLimit,
175
227
  middlewares: describeMiddlewares(resource.middlewares)
@@ -240,6 +292,8 @@ async function describe(args) {
240
292
  totalEvents: described.reduce((sum, r) => sum + r.events.length, 0),
241
293
  totalCatalogedEvents: eventCatalog?.length ?? 0,
242
294
  totalFields,
295
+ totalActions: described.reduce((sum, r) => sum + r.actions.length, 0),
296
+ totalAggregations: described.reduce((sum, r) => sum + r.aggregations.length, 0),
243
297
  presetUsage: presetCounts,
244
298
  pipelineSteps: totalPipelineSteps
245
299
  }
@@ -252,4 +306,4 @@ async function describe(args) {
252
306
  }
253
307
  }
254
308
  //#endregion
255
- export { describe };
309
+ export { describe, describeResource };
@@ -1,5 +1,5 @@
1
- import { t as ResourceRegistry } from "../../ResourceRegistry-DkAeAuTX.mjs";
2
- import { t as buildOpenApiSpec } from "../../openapi-D7G1V7ex.mjs";
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-CTERg_2x.mjs";
2
+ import { t as buildOpenApiSpec } from "../../openapi-noXno2CV.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-CWP6MB39.mjs";
1
+ import { t as pluralize } from "../../pluralize-DQgqgifU.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
@@ -49,37 +49,73 @@ function getTemplates(ts, config = {}) {
49
49
  * Generated by Arc CLI
50
50
  */
51
51
 
52
- import mongoose${ts ? ", { type HydratedDocument }" : ""} from 'mongoose';
52
+ ${ts ? "import mongoose, { type HydratedDocument, type Model, type Types } from 'mongoose';" : "import mongoose from 'mongoose';"}
53
53
 
54
54
  const { Schema } = mongoose;
55
- ${ts ? `
56
- export interface I${name} {
57
- name: string;
58
- description?: string;
59
- ${isMultiTenant ? "organizationId: string;\n " : ""}isActive: boolean;
60
- }
61
-
62
- export type ${name}Document = HydratedDocument<I${name}>;
63
- ` : ""}
55
+ ${ts ? [
56
+ "",
57
+ "/**",
58
+ " * Persisted shape — what `.lean()` and `.toObject()` return. Carrying this",
59
+ ` * through \`Model<I${name}>\` lets \`.select(...)\` / \`.find(...)\` / \`.lean()\``,
60
+ " * infer correctly so domain methods don't need `as` casts.",
61
+ " *",
62
+ " * Replace the placeholder fields with your real domain shape.",
63
+ " */",
64
+ `export interface I${name} {`,
65
+ " _id: Types.ObjectId;",
66
+ ...isMultiTenant ? [" organizationId: Types.ObjectId;"] : [],
67
+ " // TODO: define your fields here",
68
+ " createdAt: Date;",
69
+ " updatedAt: Date;",
70
+ "}",
71
+ "",
72
+ `export type ${name}Document = HydratedDocument<I${name}>;`,
73
+ ""
74
+ ].join("\n") : ""}
64
75
  const ${camel}Schema = new Schema${ts ? `<I${name}>` : ""}(
65
76
  {
66
- name: { type: String, required: true, trim: true },
67
- description: { type: String, trim: true },
68
- ${isMultiTenant ? "organizationId: { type: String, required: true, index: true },\n " : ""}isActive: { type: Boolean, default: true },
77
+ ${isMultiTenant ? " organizationId: { type: Schema.Types.ObjectId, required: true, index: true },\n" : ""} // TODO: declare your fields here, e.g. name: { type: String, required: true, trim: true },
69
78
  },
70
79
  { timestamps: true }
71
80
  );
72
81
 
73
- // Indexes
74
- ${camel}Schema.index({ name: 1 });
75
- ${isMultiTenant ? `${camel}Schema.index({ organizationId: 1, isActive: 1 });\n` : ""}${camel}Schema.index({ isActive: 1 });
82
+ ${isMultiTenant ? `${camel}Schema.index({ organizationId: 1, createdAt: -1 });\n` : ""}${ts ? [
83
+ `const ${name}: Model<I${name}> =`,
84
+ ` (mongoose.models.${name} as Model<I${name}> | undefined) ??`,
85
+ ` mongoose.model<I${name}>('${name}', ${camel}Schema);`
86
+ ].join("\n") : `const ${name} = mongoose.models.${name} || mongoose.model('${name}', ${camel}Schema);`}
76
87
 
77
- const ${name} = mongoose.models.${name} || mongoose.model('${name}', ${camel}Schema);
78
88
  export default ${name};
79
89
  `;
80
90
  },
81
91
  repository: (name, fileName) => {
82
92
  const camel = toCamelCase(name);
93
+ if (!(config.adapter === "mongokit" || !config.adapter)) {
94
+ const generic = ts ? `<I${name}>` : "";
95
+ return `/**
96
+ * ${name} Repository
97
+ * Generated by Arc CLI
98
+ *
99
+ * This project uses a custom adapter — wire your repository to whichever
100
+ * kit you're using (sqlitekit, prismakit, custom). Replace the body below
101
+ * with your kit's Repository constructor. Arc only requires the
102
+ * \`MinimalRepo\` floor (getAll/getById/create/update/delete) declared in
103
+ * \`@classytic/repo-core/repository\`.
104
+ */
105
+ ${ts ? `\nimport type { I${name} } from './${fileName}.model.js';` : ""}
106
+
107
+ // Replace with your kit's repository instance:
108
+ // import { Repository } from '@classytic/sqlitekit';
109
+ // const ${camel}Repository = new Repository${generic}(${name}Table);
110
+ // export default ${camel}Repository;
111
+
112
+ const ${camel}Repository = {
113
+ // TODO: implement MinimalRepo<${ts ? `I${name}` : "any"}>
114
+ } as never;
115
+
116
+ export default ${camel}Repository;
117
+ `;
118
+ }
83
119
  const generic = ts ? `<I${name}>` : "";
84
120
  return `/**
85
121
  * ${name} Repository
@@ -90,25 +126,17 @@ import {
90
126
  Repository,
91
127
  methodRegistryPlugin,
92
128
  softDeletePlugin,
93
- mongoOperationsPlugin,
94
129
  } from '@classytic/mongokit';
95
130
  import ${name} from './${fileName}.model.js';${ts ? `\nimport type { I${name} } from './${fileName}.model.js';` : ""}
96
131
 
97
132
  class ${name}Repository extends Repository${generic} {
98
133
  constructor() {
99
- super(${name}, [
100
- methodRegistryPlugin(),
101
- softDeletePlugin(),
102
- mongoOperationsPlugin(),
103
- ]);
134
+ super(${name}, [methodRegistryPlugin(), softDeletePlugin()]);
104
135
  }
105
136
 
106
- /**
107
- * Find all active records
108
- */
109
- async findActive() {
110
- return this.Model.find({ isActive: true, deletedAt: null }).lean();
111
- }
137
+ // Add domain methods here. arc + mongokit ship the standard CRUD
138
+ // (getAll/getById/create/update/delete) on the base class, so only
139
+ // write a method when there's real domain logic to capture.
112
140
  }
113
141
 
114
142
  const ${camel}Repository = new ${name}Repository();
@@ -149,7 +177,7 @@ export default ${camel}Controller;
149
177
  */
150
178
 
151
179
  import ${name} from './${fileName}.model.js';
152
- import { buildCrudSchemasFromModel } from '@classytic/mongokit/utils';
180
+ import { buildCrudSchemasFromModel } from '@classytic/mongokit';
153
181
 
154
182
  /**
155
183
  * CRUD Schemas with Field Rules
@@ -171,8 +199,10 @@ const crudSchemas = buildCrudSchemasFromModel(${name}, {
171
199
  // organizationId: { systemManaged: true, preserveForElevated: true },
172
200
  },
173
201
  query: {
202
+ // Add your filterable fields here. createdAt is the default so the
203
+ // generated routes accept ?createdAt[gte]=2026-01-01 with no extra
204
+ // wiring. Add domain fields below as your model grows.
174
205
  filterableFields: {
175
- isActive: 'boolean',
176
206
  createdAt: 'date',
177
207
  },
178
208
  },
@@ -183,23 +213,38 @@ export default crudSchemas;
183
213
  resource: (name, fileName) => {
184
214
  const camel = toCamelCase(name);
185
215
  const useMongoKit = config.adapter === "mongokit" || !config.adapter;
186
- const queryParserImport = useMongoKit ? `\nimport { QueryParser } from '@classytic/mongokit';\n\nconst queryParser = new QueryParser({\n allowedFilterFields: ['isActive'],\n});\n` : "";
216
+ const schemaGeneratorImport = useMongoKit ? `import { buildCrudSchemasFromModel } from '@classytic/mongokit';\n` : "";
217
+ const queryParserImport = useMongoKit ? `\nimport { QueryParser } from '@classytic/mongokit';\n\nconst queryParser = new QueryParser({\n // Whitelist the fields this resource accepts in URL filters.\n // Empty by default — only \`createdAt\` is implicit; add yours.\n allowedFilterFields: [],\n});\n` : "";
218
+ const adapterCall = useMongoKit ? `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository, schemaGenerator: buildCrudSchemasFromModel })` : `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository })`;
187
219
  const queryParserConfig = useMongoKit ? `\n queryParser,` : "";
188
220
  return isMultiTenant ? `/**
189
221
  * ${name} Resource
190
222
  * Generated by Arc CLI
191
223
  */
192
224
 
193
- import { defineResource, createMongooseAdapter } from '@classytic/arc';
225
+ import { defineResource } from '@classytic/arc';
226
+ import { createMongooseAdapter } from '@classytic/mongokit/adapter';
194
227
  import { allOf, requireOrgMembership, requireRoles } from '@classytic/arc/permissions';
195
228
  import { multiTenantPreset } from '@classytic/arc/presets';
196
- import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
229
+ ${schemaGeneratorImport}import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
197
230
  import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
198
231
 
199
232
  const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
200
233
  name: '${fileName}',
201
- adapter: createMongooseAdapter({ model: ${name}, repository: ${camel}Repository }),${queryParserConfig}
234
+ adapter: ${adapterCall},${queryParserConfig}
235
+
236
+ // Multi-tenant default: scope reads/writes by \`organizationId\`. For
237
+ // company-wide tables (lookup data, platform settings) set
238
+ // \`tenantField: false\` instead — otherwise queries silently return
239
+ // nothing because the column doesn't exist.
240
+ //
241
+ // Multi-level tenancy (org + branch + project, etc.) — replace with:
242
+ // multiTenantPreset({ tenantFields: [
243
+ // { field: 'organizationId', type: 'org' },
244
+ // { field: 'branchId', contextKey: 'branchId' },
245
+ // ] })
202
246
  presets: ['softDelete', multiTenantPreset({ tenantField: 'organizationId' })],
247
+
203
248
  permissions: {
204
249
  list: requireOrgMembership(),
205
250
  get: requireOrgMembership(),
@@ -215,15 +260,22 @@ export default ${camel}Resource;
215
260
  * Generated by Arc CLI
216
261
  */
217
262
 
218
- import { defineResource, createMongooseAdapter } from '@classytic/arc';
263
+ import { defineResource } from '@classytic/arc';
264
+ import { createMongooseAdapter } from '@classytic/mongokit/adapter';
219
265
  import { requireAuth, requireRoles } from '@classytic/arc/permissions';
220
- import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
266
+ ${schemaGeneratorImport}import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
221
267
  import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
222
268
 
223
269
  const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
224
270
  name: '${fileName}',
225
- adapter: createMongooseAdapter({ model: ${name}, repository: ${camel}Repository }),${queryParserConfig}
271
+ adapter: ${adapterCall},${queryParserConfig}
272
+
273
+ // Single-tenant default: arc auto-infers \`tenantField: false\` when
274
+ // the model has no \`organizationId\` path (silent-zero-results
275
+ // footgun closed in 2.12). Set \`tenantField: '<field>'\` to opt INTO
276
+ // tenant scoping, or \`tenantField: false\` to make it explicit.
226
277
  presets: ['softDelete'],
278
+
227
279
  permissions: {
228
280
  list: requireAuth(),
229
281
  get: requireAuth(),
@@ -278,13 +330,20 @@ ${ts ? "import { z } from 'zod';\n" : "const { z } = require('zod');\n"}
278
330
  * ${name} Tests
279
331
  * Generated by Arc CLI
280
332
  *
281
- * 2.11 testing surface:
333
+ * Testing surface (arc 2.12+):
282
334
  * - createTestApp turnkey Fastify + in-memory Mongo + auth + fixtures
283
- * - expectArc fluent envelope matchers (.ok, .unauthorized, .forbidden, ...)
335
+ * - expectArc fluent matchers .ok / .failed / .unauthorized /
336
+ * .forbidden / .notFound / .conflict / .validationError /
337
+ * .paginated / .hidesField / .hasData / .hasStatus
284
338
  * - ctx.auth unified TestAuthProvider — register a role, reuse .headers
339
+ *
340
+ * Wire shape (post-2.12): single-doc responses are flat (\`{_id, name, ...}\`),
341
+ * paginated responses are \`{ method: 'offset', data: [...], page, ... }\`.
342
+ * No \`success\` envelope — HTTP status discriminates success vs error;
343
+ * errors carry the canonical ErrorContract \`{ code, message, status }\`.
285
344
  */
286
345
 
287
- import { describe, it, beforeAll, afterAll } from 'vitest';
346
+ import { describe, it, beforeAll, afterAll, expect } from 'vitest';
288
347
  import { createTestApp, expectArc } from '@classytic/arc/testing';
289
348
  import type { TestAppContext } from '@classytic/arc/testing';
290
349
  import ${camel}Resource from '../src/resources/${fileName}/${fileName}.resource.js';
@@ -308,17 +367,42 @@ describe('${name} Resource', () => {
308
367
  afterAll(() => ctx.close());
309
368
 
310
369
  describe('GET /${pluralize(fileName)}', () => {
311
- it('should return a paginated list', async () => {
312
- const res = await ctx.app.inject({ method: 'GET', url: '/${pluralize(fileName)}' });
370
+ it('returns a paginated list', async () => {
371
+ const res = await ctx.app.inject({
372
+ method: 'GET',
373
+ url: '/${pluralize(fileName)}',
374
+ headers: ctx.auth${ts ? "!" : ""}.as('admin').headers,
375
+ });
313
376
  expectArc(res).ok().paginated();
314
377
  });
378
+
379
+ it('rejects unauthenticated requests', async () => {
380
+ const res = await ctx.app.inject({ method: 'GET', url: '/${pluralize(fileName)}' });
381
+ expectArc(res).unauthorized();
382
+ });
383
+ });
384
+
385
+ describe('POST /${pluralize(fileName)}', () => {
386
+ it('creates a record (flat wire shape — no envelope)', async () => {
387
+ const res = await ctx.app.inject({
388
+ method: 'POST',
389
+ url: '/${pluralize(fileName)}',
390
+ headers: ctx.auth${ts ? "!" : ""}.as('admin').headers,
391
+ payload: { name: 'Example' },
392
+ });
393
+ expectArc(res).ok(201);
394
+ // Single-doc response is flat: \`body._id\` (not \`body.data._id\`).
395
+ const body = res.json()${ts ? " as { _id: string; name: string }" : ""};
396
+ expect(body._id).toBeDefined();
397
+ expect(body.name).toBe('Example');
398
+ });
315
399
  });
316
400
 
317
- // More coverage patterns:
318
- // - expectArc(res).unauthorized() for missing auth
319
- // - expectArc(res).forbidden() for wrong role
320
- // - ctx.auth.as('admin').headers for authenticated requests
401
+ // More patterns to extend:
402
+ // - expectArc(res).forbidden() / .notFound() / .validationError()
403
+ // - expectArc(res).hidesField('password') for field-level perms
321
404
  // - ctx.fixtures.create('${fileName}', {...}) for seeded data
405
+ // - error wire shape: body.code === 'arc.not_found' (ErrorContract)
322
406
  });
323
407
  `;
324
408
  }
@@ -333,6 +417,21 @@ async function generate(type, args) {
333
417
  if (!name) throw new Error("Missing name argument\nUsage: arc generate <type> <name>\nExample: arc generate resource product");
334
418
  const capitalizedName = toPascalCase(name);
335
419
  const lowerName = name.toLowerCase();
420
+ if ((type === "resource" || type === "r" || type === "model" || type === "m") && new Set([
421
+ "user",
422
+ "session",
423
+ "account",
424
+ "verification",
425
+ "organization",
426
+ "member",
427
+ "invitation",
428
+ "team",
429
+ "team-member",
430
+ "apikey"
431
+ ]).has(lowerName)) {
432
+ console.warn(`\n[arc generate] "${lowerName}" is a Better Auth-owned collection.\nBetter Auth's organization/admin/bearer plugins write this collection directly,\nso generating a parallel arc model would create a duplicate registration.\n\nRecommended pattern:\n import { createBetterAuthOverlay } from '@classytic/mongokit/better-auth';\n const adapter = await createBetterAuthOverlay({\n auth, mongoose, collection: '${lowerName}',\n });\n // ...then \`defineResource({ name: '${lowerName}', adapter, ... })\` reads\n // BA's collection through arc with full pagination/filters/permissions.\n\nAborting. Re-run with a different name if you need a separate domain model.\n`);
433
+ return;
434
+ }
336
435
  const projectConfig = readProjectConfig();
337
436
  const ts = projectConfig.typescript ?? isTypeScriptProject();
338
437
  const ext = ts ? "ts" : "js";
@@ -14,6 +14,19 @@ interface InitOptions {
14
14
  adapter?: "mongokit" | "custom";
15
15
  auth?: "jwt" | "better-auth";
16
16
  tenant?: "multi" | "single";
17
+ /**
18
+ * Enable Better Auth's `apiKey` plugin (`@better-auth/api-key`) for
19
+ * machine-to-machine authentication alongside cookie/session auth.
20
+ * Only used when `auth === 'better-auth'`. Default: false.
21
+ */
22
+ apiKey?: boolean;
23
+ /**
24
+ * Session strategy when using Better Auth.
25
+ * - `cookie` (default): browser cookie + DB-backed session (BA's default)
26
+ * - `bearer`: Authorization: Bearer header (mobile apps, SPA, M2M)
27
+ * Both can coexist — `bearer: true` adds bearer alongside cookies.
28
+ */
29
+ session?: "cookie" | "bearer" | "both";
17
30
  typescript?: boolean;
18
31
  edge?: boolean;
19
32
  skipInstall?: boolean;