@classytic/arc 2.8.1 → 2.8.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 (107) hide show
  1. package/README.md +22 -1
  2. package/dist/{ResourceRegistry-Dtcojmu8.mjs → ResourceRegistry-Dq3_zBQP.mjs} +5 -5
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/audit/index.d.mts +1 -1
  5. package/dist/audit/index.mjs +1 -1
  6. package/dist/audit/mongodb.d.mts +1 -1
  7. package/dist/audit/mongodb.mjs +1 -1
  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 +2 -2
  11. package/dist/cli/commands/describe.mjs +1 -1
  12. package/dist/cli/commands/docs.mjs +2 -2
  13. package/dist/cli/commands/generate.mjs +1 -1
  14. package/dist/cli/commands/init.mjs +10 -10
  15. package/dist/cli/commands/introspect.mjs +3 -3
  16. package/dist/core/index.d.mts +3 -3
  17. package/dist/core/index.mjs +4 -4
  18. package/dist/{core-CrLDuqoT.mjs → core-DKSwNSXf.mjs} +1 -1
  19. package/dist/{createApp-p2OThysU.mjs → createApp-BOYjBgdI.mjs} +16 -7
  20. package/dist/{defineResource-CqeUltrW.mjs → defineResource-Bb_Bdhtw.mjs} +42 -27
  21. package/dist/docs/index.d.mts +2 -2
  22. package/dist/docs/index.mjs +1 -1
  23. package/dist/dynamic/index.d.mts +2 -2
  24. package/dist/dynamic/index.mjs +1 -1
  25. package/dist/{errorHandler-DJ7OAB2V.d.mts → errorHandler-CdZDavNH.d.mts} +2 -2
  26. package/dist/{eventPlugin-Cdjwo0Gv.d.mts → eventPlugin-CVxlE6De.d.mts} +1 -1
  27. package/dist/events/index.d.mts +3 -3
  28. package/dist/events/index.mjs +1 -1
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/events/transports/redis-stream-entry.mjs +3 -1
  31. package/dist/events/transports/redis.d.mts +1 -1
  32. package/dist/factory/index.d.mts +1 -1
  33. package/dist/factory/index.mjs +2 -152
  34. package/dist/hooks/index.d.mts +1 -1
  35. package/dist/idempotency/index.d.mts +3 -3
  36. package/dist/idempotency/mongodb.d.mts +1 -1
  37. package/dist/idempotency/mongodb.mjs +18 -6
  38. package/dist/idempotency/redis.d.mts +1 -1
  39. package/dist/idempotency/redis.mjs +10 -1
  40. package/dist/{index-0zj73o2U.d.mts → index-BgmMdpm8.d.mts} +1 -1
  41. package/dist/{index-CBru2y5Y.d.mts → index-CSkeivBx.d.mts} +3 -3
  42. package/dist/{index-DadoLP51.d.mts → index-CpTSDqmD.d.mts} +26 -4
  43. package/dist/index.d.mts +7 -7
  44. package/dist/index.mjs +3 -3
  45. package/dist/integrations/event-gateway.d.mts +1 -1
  46. package/dist/integrations/event-gateway.mjs +1 -1
  47. package/dist/integrations/index.d.mts +1 -1
  48. package/dist/integrations/mcp/index.d.mts +51 -3
  49. package/dist/integrations/mcp/index.mjs +78 -19
  50. package/dist/integrations/mcp/testing.d.mts +1 -1
  51. package/dist/integrations/mcp/testing.mjs +1 -1
  52. package/dist/{interface-CS6d7HiB.d.mts → interface-BVuMfeVv.d.mts} +47 -18
  53. package/dist/loadResources-Bksk8ydA.mjs +154 -0
  54. package/dist/{mongodb-B1eVtFhw.d.mts → mongodb-B8U2xaLj.d.mts} +1 -1
  55. package/dist/{mongodb-NShVZDMr.d.mts → mongodb-X7LbEjTN.d.mts} +10 -1
  56. package/dist/{openapi-q6rNKfZy.mjs → openapi-CYCuekCn.mjs} +2 -2
  57. package/dist/org/index.d.mts +2 -2
  58. package/dist/permissions/index.d.mts +3 -3
  59. package/dist/plugins/index.d.mts +5 -5
  60. package/dist/plugins/index.mjs +7 -7
  61. package/dist/plugins/tracing-entry.d.mts +1 -1
  62. package/dist/plugins/tracing-entry.mjs +1 -1
  63. package/dist/policies/index.d.mts +1 -1
  64. package/dist/presets/index.d.mts +1 -1
  65. package/dist/presets/index.mjs +1 -1
  66. package/dist/presets/multiTenant.d.mts +1 -1
  67. package/dist/{presets-BFrGvvjL.mjs → presets-C2xgzW6x.mjs} +10 -18
  68. package/dist/{queryCachePlugin-BCFVXnxK.d.mts → queryCachePlugin-CnTZZTC5.d.mts} +1 -1
  69. package/dist/{redis-stream-BgrYzpeq.d.mts → redis-stream-D54N5oXs.d.mts} +1 -1
  70. package/dist/{redis-Bunu3qWg.d.mts → redis-z3sFr1UP.d.mts} +1 -1
  71. package/dist/registry/index.d.mts +1 -1
  72. package/dist/registry/index.mjs +1 -1
  73. package/dist/{resourceToTools-DNNWnZtx.mjs → resourceToTools-O_HwWXFa.mjs} +1 -1
  74. package/dist/rpc/index.d.mts +1 -1
  75. package/dist/scope/index.d.mts +2 -2
  76. package/dist/testing/index.d.mts +2 -2
  77. package/dist/testing/index.mjs +1 -1
  78. package/dist/types/index.d.mts +4 -4
  79. package/dist/{types-BlOuKTPw.d.mts → types-Bg2X42_m.d.mts} +30 -9
  80. package/dist/{types-BoaZHr-2.d.mts → types-CVC4HOKi.d.mts} +1 -1
  81. package/dist/{types-D3b7hA00.d.mts → types-CcG4avic.d.mts} +1 -1
  82. package/dist/utils/index.d.mts +43 -5
  83. package/dist/utils/index.mjs +3 -3
  84. package/dist/{utils-7sJ8X83I.mjs → utils-yYT3HDXt.mjs} +65 -1
  85. package/package.json +8 -8
  86. package/skills/arc/SKILL.md +101 -6
  87. package/skills/arc/references/mcp.md +37 -0
  88. /package/dist/{EventTransport-CLXJUzyT.d.mts → EventTransport-CinyO7zQ.d.mts} +0 -0
  89. /package/dist/{caching-CHH-iHs3.mjs → caching-CjybdRwx.mjs} +0 -0
  90. /package/dist/{circuitBreaker-BGVoB1hD.d.mts → circuitBreaker-CvXkjfrW.d.mts} +0 -0
  91. /package/dist/{elevation-UJO3-NvX.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  92. /package/dist/{errorHandler-Cw34h_om.mjs → errorHandler-mzqk4cGl.mjs} +0 -0
  93. /package/dist/{errors-BI8kEKsO.d.mts → errors-Bmn3eZT6.d.mts} +0 -0
  94. /package/dist/{eventPlugin-XijlQmlL.mjs → eventPlugin-D91S2YF4.mjs} +0 -0
  95. /package/dist/{externalPaths-BQ8QijNH.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
  96. /package/dist/{fields-DoeDgh2b.d.mts → fields-DC4So2M2.d.mts} +0 -0
  97. /package/dist/{interface-CkkWm5uR.d.mts → interface-B-pe8fhj.d.mts} +0 -0
  98. /package/dist/{interface-bpoLKKqx.d.mts → interface-DplgQO2e.d.mts} +0 -0
  99. /package/dist/{metrics-DuhiSEZI.mjs → metrics-TuOmguhi.mjs} +0 -0
  100. /package/dist/{mongodb-5Ff3w8jy.mjs → mongodb-B5O6xaW1.mjs} +0 -0
  101. /package/dist/{pluralize-BneOJkpi.mjs → pluralize-A0tWEl1K.mjs} +0 -0
  102. /package/dist/{replyHelpers-CXtJDAZ0.mjs → replyHelpers-BLojtuvR.mjs} +0 -0
  103. /package/dist/{sessionManager-BkzVU8h2.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
  104. /package/dist/{sse-CD5Hghpu.mjs → sse-CJpt7LGI.mjs} +0 -0
  105. /package/dist/{tracing-xqXzWeaf.d.mts → tracing-DxjKk7eW.d.mts} +0 -0
  106. /package/dist/{types-CN6JvmYz.d.mts → types-C72d3NDn.d.mts} +0 -0
  107. /package/dist/{versioning-CPU_5Xfs.mjs → versioning-Cm8qoFDg.mjs} +0 -0
@@ -41,13 +41,12 @@ function slugLookupPreset(options = {}) {
41
41
  const { slugField = "slug" } = options;
42
42
  return {
43
43
  name: "slugLookup",
44
- additionalRoutes: (permissions) => [{
44
+ routes: (permissions) => [{
45
45
  method: "GET",
46
46
  path: `/slug/:${slugField}`,
47
47
  handler: "getBySlug",
48
48
  summary: "Get by slug",
49
49
  permissions: permissions.get ?? allowPublic(),
50
- wrapHandler: true,
51
50
  operation: "getBySlug"
52
51
  }],
53
52
  controllerOptions: { slugField }
@@ -65,13 +64,12 @@ function slugLookupPreset(options = {}) {
65
64
  function softDeletePreset() {
66
65
  return {
67
66
  name: "softDelete",
68
- additionalRoutes: (permissions) => [{
67
+ routes: (permissions) => [{
69
68
  method: "GET",
70
69
  path: "/deleted",
71
70
  handler: "getDeleted",
72
71
  summary: "Get soft-deleted items",
73
72
  permissions: permissions.list ?? requireRoles(["admin"]),
74
- wrapHandler: true,
75
73
  operation: "listDeleted"
76
74
  }, {
77
75
  method: "POST",
@@ -79,7 +77,6 @@ function softDeletePreset() {
79
77
  handler: "restore",
80
78
  summary: "Restore soft-deleted item",
81
79
  permissions: permissions.update ?? requireRoles(["admin"]),
82
- wrapHandler: true,
83
80
  operation: "restore"
84
81
  }]
85
82
  };
@@ -153,13 +150,12 @@ function bulkPreset(opts) {
153
150
  const maxCreateItems = opts?.maxCreateItems ?? 1e3;
154
151
  return {
155
152
  name: "bulk",
156
- additionalRoutes: (permissions) => {
153
+ routes: (permissions) => {
157
154
  const routes = [];
158
155
  if (operations.includes("createMany")) routes.push({
159
156
  method: "POST",
160
157
  path: "/bulk",
161
158
  handler: "bulkCreate",
162
- wrapHandler: true,
163
159
  operation: "bulkCreate",
164
160
  summary: "Create multiple items",
165
161
  permissions: permissions.create ?? requireAuth(),
@@ -190,7 +186,6 @@ function bulkPreset(opts) {
190
186
  method: "PATCH",
191
187
  path: "/bulk",
192
188
  handler: "bulkUpdate",
193
- wrapHandler: true,
194
189
  operation: "bulkUpdate",
195
190
  summary: "Update multiple items matching filter",
196
191
  permissions: permissions.update ?? requireAuth(),
@@ -222,7 +217,6 @@ function bulkPreset(opts) {
222
217
  method: "DELETE",
223
218
  path: "/bulk",
224
219
  handler: "bulkDelete",
225
- wrapHandler: true,
226
220
  operation: "bulkDelete",
227
221
  summary: "Delete multiple items matching filter",
228
222
  permissions: permissions.delete ?? requireAuth(),
@@ -259,13 +253,12 @@ function treePreset(options = {}) {
259
253
  const { parentField = "parent" } = options;
260
254
  return {
261
255
  name: "tree",
262
- additionalRoutes: (permissions) => [{
256
+ routes: (permissions) => [{
263
257
  method: "GET",
264
258
  path: "/tree",
265
259
  handler: "getTree",
266
260
  summary: "Get hierarchical tree",
267
261
  permissions: permissions.list ?? allowPublic(),
268
- wrapHandler: true,
269
262
  operation: "getTree"
270
263
  }, {
271
264
  method: "GET",
@@ -273,7 +266,6 @@ function treePreset(options = {}) {
273
266
  handler: "getChildren",
274
267
  summary: "Get children of parent",
275
268
  permissions: permissions.list ?? allowPublic(),
276
- wrapHandler: true,
277
269
  operation: "getChildren"
278
270
  }],
279
271
  controllerOptions: { parentField }
@@ -341,8 +333,8 @@ function validatePresetCombination(presets) {
341
333
  const routeMap = /* @__PURE__ */ new Map();
342
334
  for (const preset of presets) {
343
335
  const name = preset.name ?? "unknown";
344
- const routes = typeof preset.additionalRoutes === "function" ? preset.additionalRoutes({}) : preset.additionalRoutes ?? [];
345
- for (const route of routes) {
336
+ const presetRoutes = preset.routes ? typeof preset.routes === "function" ? preset.routes({}) : preset.routes : [];
337
+ for (const route of presetRoutes) {
346
338
  const key = `${route.method} ${route.path}`;
347
339
  const existing = routeMap.get(key);
348
340
  if (existing) conflicts.push({
@@ -371,7 +363,7 @@ function applyPresets(config, presets = []) {
371
363
  * Resolve preset input to PresetResult
372
364
  */
373
365
  function resolvePresetInput(preset) {
374
- if (typeof preset === "object" && ("middlewares" in preset || "additionalRoutes" in preset)) return preset;
366
+ if (typeof preset === "object" && ("middlewares" in preset || "routes" in preset)) return preset;
375
367
  if (typeof preset === "object" && "name" in preset) {
376
368
  const { name, ...options } = preset;
377
369
  return resolvePreset(name, options);
@@ -383,9 +375,9 @@ function resolvePresetInput(preset) {
383
375
  */
384
376
  function mergePreset(config, preset) {
385
377
  const result = { ...config };
386
- if (preset.additionalRoutes) {
387
- const routes = typeof preset.additionalRoutes === "function" ? preset.additionalRoutes(config.permissions ?? {}) : preset.additionalRoutes;
388
- result.additionalRoutes = [...result.additionalRoutes ?? [], ...routes];
378
+ if (preset.routes) {
379
+ const resolved = typeof preset.routes === "function" ? preset.routes(config.permissions ?? {}) : preset.routes;
380
+ result.routes = [...result.routes ?? [], ...resolved];
389
381
  }
390
382
  if (preset.middlewares) {
391
383
  result.middlewares = result.middlewares ?? {};
@@ -1,4 +1,4 @@
1
- import { i as CacheStore } from "./interface-bpoLKKqx.mjs";
1
+ import { i as CacheStore } from "./interface-DplgQO2e.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/cache/QueryCache.d.ts
@@ -1,4 +1,4 @@
1
- import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-CLXJUzyT.mjs";
1
+ import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-CinyO7zQ.mjs";
2
2
 
3
3
  //#region src/events/transports/redis-stream.d.ts
4
4
  interface RedisStreamLike {
@@ -1,4 +1,4 @@
1
- import { n as IdempotencyResult, r as IdempotencyStore } from "./interface-CkkWm5uR.mjs";
1
+ import { n as IdempotencyResult, r as IdempotencyStore } from "./interface-B-pe8fhj.mjs";
2
2
 
3
3
  //#region src/idempotency/stores/redis.d.ts
4
4
  interface RedisClient {
@@ -1,4 +1,4 @@
1
- import { Gt as RegisterOptions, H as IntrospectionPluginOptions, Kt as ResourceRegistry } from "../interface-CS6d7HiB.mjs";
1
+ import { Gt as RegisterOptions, H as IntrospectionPluginOptions, Kt as ResourceRegistry } from "../interface-BVuMfeVv.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/registry/introspectionPlugin.d.ts
@@ -1,3 +1,3 @@
1
1
  import { n as introspectionPlugin_default, t as introspectionPlugin } from "../registry-B0Wl7uVV.mjs";
2
- import { t as ResourceRegistry } from "../ResourceRegistry-Dtcojmu8.mjs";
2
+ import { t as ResourceRegistry } from "../ResourceRegistry-Dq3_zBQP.mjs";
3
3
  export { ResourceRegistry, introspectionPlugin_default as introspectionPlugin, introspectionPlugin as introspectionPluginFn };
@@ -1,6 +1,6 @@
1
1
  import { t as BaseController } from "./BaseController-DAGGc5Xn.mjs";
2
2
  import { n as normalizePermissionResult } from "./applyPermissionResult-D6GPMsvh.mjs";
3
- import { t as pluralize } from "./pluralize-BneOJkpi.mjs";
3
+ import { t as pluralize } from "./pluralize-A0tWEl1K.mjs";
4
4
  import { z } from "zod";
5
5
  //#region src/integrations/mcp/createMcpServer.ts
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { r as CircuitBreakerOptions } from "../circuitBreaker-BGVoB1hD.mjs";
1
+ import { r as CircuitBreakerOptions } from "../circuitBreaker-CvXkjfrW.mjs";
2
2
 
3
3
  //#region src/rpc/serviceClient.d.ts
4
4
  interface RetryConfig {
@@ -1,5 +1,5 @@
1
- import { _ as isAuthenticated, a as getClientId, b as isOrgInScope, c as getOrgRoles, d as getScopeContextMap, f as getServiceScopes, g as hasOrgAccess, h as getUserRoles, i as getAncestorOrgIds, l as getRequestScope, m as getUserId, n as PUBLIC_SCOPE, o as getOrgContext, p as getTeamId, r as RequestScope, s as getOrgId, t as AUTHENTICATED_SCOPE, u as getScopeContext, v as isElevated, x as isService, y as isMember } from "../types-CN6JvmYz.mjs";
2
- import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-UJO3-NvX.mjs";
1
+ import { _ as isAuthenticated, a as getClientId, b as isOrgInScope, c as getOrgRoles, d as getScopeContextMap, f as getServiceScopes, g as hasOrgAccess, h as getUserRoles, i as getAncestorOrgIds, l as getRequestScope, m as getUserId, n as PUBLIC_SCOPE, o as getOrgContext, p as getTeamId, r as RequestScope, s as getOrgId, t as AUTHENTICATED_SCOPE, u as getScopeContext, v as isElevated, x as isService, y as isMember } from "../types-C72d3NDn.mjs";
2
+ import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-s5ykdNHr.mjs";
3
3
  import { FastifyReply, FastifyRequest } from "fastify";
4
4
 
5
5
  //#region src/scope/rateLimitKey.d.ts
@@ -1,5 +1,5 @@
1
- import { Zt as CrudRepository, m as AnyRecord, qt as ResourceDefinition } from "../interface-CS6d7HiB.mjs";
2
- import { d as ResourceLike, r as CreateAppOptions } from "../types-BlOuKTPw.mjs";
1
+ import { Zt as CrudRepository, m as AnyRecord, qt as ResourceDefinition } from "../interface-BVuMfeVv.mjs";
2
+ import { d as ResourceLike, r as CreateAppOptions } from "../types-Bg2X42_m.mjs";
3
3
  import Fastify, { FastifyInstance, FastifyServerOptions } from "fastify";
4
4
  import { Connection } from "mongoose";
5
5
  import { Mock } from "vitest";
@@ -1796,7 +1796,7 @@ function runEventTests(resourceName, displayName, events) {
1796
1796
  * ```
1797
1797
  */
1798
1798
  async function createTestApp(options = {}) {
1799
- const { createApp } = await import("../createApp-p2OThysU.mjs").then((n) => n.r);
1799
+ const { createApp } = await import("../createApp-BOYjBgdI.mjs").then((n) => n.r);
1800
1800
  const { useInMemoryDb = true, mongoUri: providedMongoUri, ...appOptions } = options;
1801
1801
  const defaultAuth = {
1802
1802
  type: "jwt",
@@ -1,5 +1,5 @@
1
- import { _ as isAuthenticated, c as getOrgRoles, g as hasOrgAccess, n as PUBLIC_SCOPE, p as getTeamId, r as RequestScope, s as getOrgId, t as AUTHENTICATED_SCOPE, v as isElevated, y as isMember } from "../types-CN6JvmYz.mjs";
2
- import { $ as PresetFunction, $t as DeleteOptions, A as EventsDecorator, B as InferResourceDoc, Bt as FastifyHandler, C as ConfigError, Ct as TypedResourceConfig, D as CrudRouterOptions, Dt as ValidationResult, E as CrudRouteKey, Et as ValidateOptions, F as GracefulShutdownOptions, G as LookupOption, H as IntrospectionPluginOptions, Ht as IControllerResponse, I as HealthCheck, J as ObjectId, K as MiddlewareConfig, L as HealthOptions, M as FastifyWithAuth, N as FastifyWithDecorators, O as CrudSchemas, Ot as envelope, P as FieldRule, Q as PopulateOption, Qt as DeleteManyResult, R as InferAdapterDoc, Rt as ControllerHandler, S as AuthenticatorContext, St as TypedRepository, T as CrudController, Tt as UserOrganization, U as JWTPayload, Ut as IRequestContext, V as IntrospectionData, Vt as IController, W as JwtContext, Wt as RouteHandler, X as OwnershipCheck, Xt as BulkWriteResult, Y as OpenApiSchemas, Yt as BulkWriteOperation, Z as ParsedQuery, Zt as CrudRepository, _ as ArcInternalMetadata, _t as RouteMcpConfig, an as PaginationParams, at as RegistryStats, b as AuthPluginOptions, bt as TokenPair, cn as RepositorySession, ct as RequestWithExtras, d as ActionHandlerFn, dt as ResourceHookContext, en as DeleteResult, et as PresetHook, f as ActionsMap, ft as ResourceHooks, g as ArcDecorator, gt as RouteHandlerMethod, h as ApiResponse, ht as RouteDefinition, in as PaginatedResult, it as RegistryEntry, j as FastifyRequestExtras, jt as BaseControllerOptions, k as EventDefinition, kt as getUserId, l as ActionDefinition, ln as UpdateManyResult, lt as ResourceCacheConfig, m as AnyRecord, mt as ResourcePermissions, nn as KeysetPaginatedResult, nt as QueryParserInterface, on as PaginationResult, ot as RequestContext, p as AdditionalRoute, pt as ResourceMetadata, q as MiddlewareHandler, rn as OffsetPaginatedResult, rt as RateLimitConfig, sn as QueryOptions, st as RequestIdOptions, tn as InferDoc, tt as PresetResult, u as ActionEntry, un as WriteOptions, ut as ResourceConfig, v as ArcRequest, vt as RouteSchemaOptions, w as ControllerQueryOptions, wt as UserLike, x as Authenticator, xt as TypedController, y as AuthHelpers, yt as ServiceContext, z as InferDocType, zt as ControllerLike } from "../interface-CS6d7HiB.mjs";
3
- import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-BoaZHr-2.mjs";
4
- import { n as ElevationOptions, t as ElevationEvent } from "../elevation-UJO3-NvX.mjs";
1
+ import { _ as isAuthenticated, c as getOrgRoles, g as hasOrgAccess, n as PUBLIC_SCOPE, p as getTeamId, r as RequestScope, s as getOrgId, t as AUTHENTICATED_SCOPE, v as isElevated, y as isMember } from "../types-C72d3NDn.mjs";
2
+ import { $ as PresetFunction, $t as DeleteOptions, A as EventsDecorator, B as InferResourceDoc, Bt as FastifyHandler, C as ConfigError, Ct as TypedResourceConfig, D as CrudRouterOptions, Dt as ValidationResult, E as CrudRouteKey, Et as ValidateOptions, F as GracefulShutdownOptions, G as LookupOption, H as IntrospectionPluginOptions, Ht as IControllerResponse, I as HealthCheck, J as ObjectId, K as MiddlewareConfig, L as HealthOptions, M as FastifyWithAuth, N as FastifyWithDecorators, O as CrudSchemas, Ot as envelope, P as FieldRule, Q as PopulateOption, Qt as DeleteManyResult, R as InferAdapterDoc, Rt as ControllerHandler, S as AuthenticatorContext, St as TypedRepository, T as CrudController, Tt as UserOrganization, U as JWTPayload, Ut as IRequestContext, V as IntrospectionData, Vt as IController, W as JwtContext, Wt as RouteHandler, X as OwnershipCheck, Xt as BulkWriteResult, Y as OpenApiSchemas, Yt as BulkWriteOperation, Z as ParsedQuery, Zt as CrudRepository, _ as ArcInternalMetadata, _t as RouteMcpConfig, an as PaginationParams, at as RegistryStats, b as AuthPluginOptions, bt as TokenPair, cn as RepositorySession, ct as RequestWithExtras, d as ActionHandlerFn, dt as ResourceHookContext, en as DeleteResult, et as PresetHook, f as ActionsMap, ft as ResourceHooks, g as ArcDecorator, gt as RouteHandlerMethod, h as ApiResponse, ht as RouteDefinition, in as PaginatedResult, it as RegistryEntry, j as FastifyRequestExtras, jt as BaseControllerOptions, k as EventDefinition, kt as getUserId, l as ActionDefinition, ln as UpdateManyResult, lt as ResourceCacheConfig, m as AnyRecord, mt as ResourcePermissions, nn as KeysetPaginatedResult, nt as QueryParserInterface, on as PaginationResult, ot as RequestContext, p as AdditionalRoute, pt as ResourceMetadata, q as MiddlewareHandler, rn as OffsetPaginatedResult, rt as RateLimitConfig, sn as QueryOptions, st as RequestIdOptions, tn as InferDoc, tt as PresetResult, u as ActionEntry, un as WriteOptions, ut as ResourceConfig, v as ArcRequest, vt as RouteSchemaOptions, w as ControllerQueryOptions, wt as UserLike, x as Authenticator, xt as TypedController, y as AuthHelpers, yt as ServiceContext, z as InferDocType, zt as ControllerLike } from "../interface-BVuMfeVv.mjs";
3
+ import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-CVC4HOKi.mjs";
4
+ import { n as ElevationOptions, t as ElevationEvent } from "../elevation-s5ykdNHr.mjs";
5
5
  export { AUTHENTICATED_SCOPE, ActionDefinition, ActionEntry, ActionHandlerFn, ActionsMap, AdditionalRoute, AnyRecord, ApiResponse, ArcDecorator, ArcInternalMetadata, ArcRequest, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, BulkWriteOperation, BulkWriteResult, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRepository, CrudRouteKey, CrudRouterOptions, CrudSchemas, DeleteManyResult, DeleteOptions, DeleteResult, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, KeysetPaginatedResult, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OffsetPaginatedResult, OpenApiSchemas, OwnershipCheck, PUBLIC_SCOPE, PaginatedResult, PaginationParams, PaginationResult, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryOptions, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RepositorySession, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResourceCacheConfig, ResourceConfig, ResourceHookContext, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteDefinition, RouteHandler, RouteHandlerMethod, RouteMcpConfig, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UpdateManyResult, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult, WriteOptions, envelope, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
@@ -1,12 +1,12 @@
1
- import { x as Authenticator } from "./interface-CS6d7HiB.mjs";
2
- import { n as ElevationOptions } from "./elevation-UJO3-NvX.mjs";
3
- import { t as ExternalOpenApiPaths } from "./externalPaths-BQ8QijNH.mjs";
4
- import { i as CacheStore } from "./interface-bpoLKKqx.mjs";
5
- import { r as QueryCachePluginOptions } from "./queryCachePlugin-BCFVXnxK.mjs";
6
- import { i as EventTransport } from "./EventTransport-CLXJUzyT.mjs";
7
- import { t as EventPluginOptions } from "./eventPlugin-Cdjwo0Gv.mjs";
8
- import { c as MetricsOptions, d as SSEOptions, m as CachingOptions, r as VersioningOptions, t as ErrorHandlerOptions } from "./errorHandler-DJ7OAB2V.mjs";
9
- import { r as IdempotencyStore } from "./interface-CkkWm5uR.mjs";
1
+ import { x as Authenticator } from "./interface-BVuMfeVv.mjs";
2
+ import { n as ElevationOptions } from "./elevation-s5ykdNHr.mjs";
3
+ import { t as ExternalOpenApiPaths } from "./externalPaths-Bapitwvd.mjs";
4
+ import { i as CacheStore } from "./interface-DplgQO2e.mjs";
5
+ import { r as QueryCachePluginOptions } from "./queryCachePlugin-CnTZZTC5.mjs";
6
+ import { i as EventTransport } from "./EventTransport-CinyO7zQ.mjs";
7
+ import { t as EventPluginOptions } from "./eventPlugin-CVxlE6De.mjs";
8
+ import { f as SSEOptions, h as CachingOptions, i as VersioningOptions, l as MetricsOptions, t as ErrorHandlerOptions } from "./errorHandler-CdZDavNH.mjs";
9
+ import { r as IdempotencyStore } from "./interface-B-pe8fhj.mjs";
10
10
  import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, FastifyServerOptions } from "fastify";
11
11
 
12
12
  //#region src/factory/loadResources.d.ts
@@ -664,6 +664,27 @@ interface CreateAppOptions {
664
664
  * ```
665
665
  */
666
666
  resourcePrefix?: string;
667
+ /**
668
+ * Auto-discover resources from a directory instead of passing an explicit
669
+ * `resources` array. Resolves relative to `process.cwd()`.
670
+ *
671
+ * This replaces the common pattern:
672
+ * ```ts
673
+ * resources: await loadResources(import.meta.url)
674
+ * ```
675
+ *
676
+ * When both `resourceDir` and `resources` are provided, `resources` wins
677
+ * (explicit always beats convention).
678
+ *
679
+ * @example
680
+ * ```ts
681
+ * const app = await createApp({
682
+ * resourceDir: 'src/resources',
683
+ * resourcePrefix: '/api/v1',
684
+ * });
685
+ * ```
686
+ */
687
+ resourceDir?: string;
667
688
  /**
668
689
  * Custom plugin registration — runs after Arc core (security, auth, events)
669
690
  * but before `bootstrap` and `resources`.
@@ -1,4 +1,4 @@
1
- import { r as RequestScope } from "./types-CN6JvmYz.mjs";
1
+ import { r as RequestScope } from "./types-C72d3NDn.mjs";
2
2
  import { FastifyRequest } from "fastify";
3
3
 
4
4
  //#region src/permissions/types.d.ts
@@ -1,4 +1,4 @@
1
- import { qt as ResourceDefinition } from "./interface-CS6d7HiB.mjs";
1
+ import { qt as ResourceDefinition } from "./interface-BVuMfeVv.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/integrations/mcp/types.d.ts
@@ -1,7 +1,7 @@
1
- import { Y as OpenApiSchemas, Z as ParsedQuery, m as AnyRecord, nt as QueryParserInterface } from "../interface-CS6d7HiB.mjs";
2
- import { a as NotFoundError, c as RateLimitError, d as ValidationError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-BI8kEKsO.mjs";
3
- import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-BGVoB1hD.mjs";
4
- import { FastifyInstance } from "fastify";
1
+ import { Y as OpenApiSchemas, Z as ParsedQuery, m as AnyRecord, nt as QueryParserInterface } from "../interface-BVuMfeVv.mjs";
2
+ import { a as NotFoundError, c as RateLimitError, d as ValidationError, f as createDomainError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-Bmn3eZT6.mjs";
3
+ import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-CvXkjfrW.mjs";
4
+ import { FastifyInstance, FastifyReply, FastifyRequest, RouteHandlerMethod } from "fastify";
5
5
 
6
6
  //#region src/utils/compensation.d.ts
7
7
  /**
@@ -92,6 +92,44 @@ interface CompensationDefinition<TCtx extends Record<string, unknown> = Record<s
92
92
  }
93
93
  declare function defineCompensation<TCtx extends Record<string, unknown> = Record<string, unknown>>(name: string, steps: readonly CompensationStep<TCtx>[]): CompensationDefinition<TCtx>;
94
94
  //#endregion
95
+ //#region src/utils/defineGuard.d.ts
96
+ interface GuardConfig<T> {
97
+ /** Unique name — used as the storage key on the request. */
98
+ readonly name: string;
99
+ /**
100
+ * Resolve the guard context from the request. Throw to abort the request
101
+ * (Fastify's error handler will produce the appropriate HTTP response).
102
+ * Return a value to stash it for `from()` extraction.
103
+ */
104
+ readonly resolve: (req: FastifyRequest, reply: FastifyReply) => T | Promise<T>;
105
+ }
106
+ interface Guard<T> {
107
+ /** Use in `routeGuards` or per-route `preHandler` arrays. */
108
+ readonly preHandler: RouteHandlerMethod;
109
+ /**
110
+ * Extract the resolved context from a request. Throws if the guard
111
+ * hasn't run yet (i.e. not in the preHandler chain).
112
+ */
113
+ from(req: FastifyRequest): T;
114
+ /** The guard name (for debugging). */
115
+ readonly name: string;
116
+ }
117
+ /**
118
+ * Create a typed guard. See module JSDoc for usage.
119
+ */
120
+ declare function defineGuard<T>(config: GuardConfig<T>): Guard<T>;
121
+ //#endregion
122
+ //#region src/utils/handleRaw.d.ts
123
+ /**
124
+ * Wrap a raw Fastify handler with Arc's response envelope and error handling.
125
+ *
126
+ * @param handler - Async function that receives `(request, reply)` and returns data.
127
+ * The return value is sent as `{ success: true, data }`. If it returns
128
+ * `undefined` or `null`, `{ success: true }` is sent (no `data` field).
129
+ * @param statusCode - HTTP status code for successful responses (default: 200)
130
+ */
131
+ declare function handleRaw<T>(handler: (request: FastifyRequest, reply: FastifyReply) => Promise<T>, statusCode?: number): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
132
+ //#endregion
95
133
  //#region src/utils/queryParser.d.ts
96
134
  interface ArcQueryParserOptions {
97
135
  /** Maximum allowed limit value (default: 1000) */
@@ -635,4 +673,4 @@ declare function hasEvents(instance: FastifyInstance): instance is FastifyInstan
635
673
  events: EventsDecorator;
636
674
  };
637
675
  //#endregion
638
- export { ArcError, ArcQueryParser, type ArcQueryParserOptions, CircuitBreaker, CircuitBreakerError, type CircuitBreakerOptions, CircuitBreakerRegistry, type CircuitBreakerStats, CircuitState, type CompensationDefinition, type CompensationError, type CompensationHooks, type CompensationResult, type CompensationStep, ConflictError, type ErrorDetails, type EventsDecorator, ForbiddenError, type JsonSchema, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, type StateMachine, type TransitionConfig, UnauthorizedError, ValidationError, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createError, createQueryParser, createStateMachine, defineCompensation, deleteResponse, errorResponseSchema, getDefaultCrudSchemas, getListQueryParams, hasEvents, isArcError, isJsonSchema, isZodSchema, itemResponse, listResponse, mutationResponse, paginationSchema, queryParams, responses, successResponseSchema, toJsonSchema, withCompensation, wrapResponse };
676
+ export { ArcError, ArcQueryParser, type ArcQueryParserOptions, CircuitBreaker, CircuitBreakerError, type CircuitBreakerOptions, CircuitBreakerRegistry, type CircuitBreakerStats, CircuitState, type CompensationDefinition, type CompensationError, type CompensationHooks, type CompensationResult, type CompensationStep, ConflictError, type ErrorDetails, type EventsDecorator, ForbiddenError, type Guard, type GuardConfig, type JsonSchema, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, type StateMachine, type TransitionConfig, UnauthorizedError, ValidationError, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, defineCompensation, defineGuard, deleteResponse, errorResponseSchema, getDefaultCrudSchemas, getListQueryParams, handleRaw, hasEvents, isArcError, isJsonSchema, isZodSchema, itemResponse, listResponse, mutationResponse, paginationSchema, queryParams, responses, successResponseSchema, toJsonSchema, withCompensation, wrapResponse };
@@ -1,7 +1,7 @@
1
1
  import { n as createQueryParser, t as ArcQueryParser } from "../queryParser-CgCtsjti.mjs";
2
2
  import { a as toJsonSchema, i as isZodSchema, n as convertRouteSchema, r as isJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-OxfCshus.mjs";
3
3
  import { a as createCircuitBreaker, i as CircuitState, n as CircuitBreakerError, o as createCircuitBreakerRegistry, r as CircuitBreakerRegistry, t as CircuitBreaker } from "../circuitBreaker-cmi5XDv5.mjs";
4
- import { a as getListQueryParams, c as mutationResponse, d as responses, f as successResponseSchema, h as withCompensation, i as getDefaultCrudSchemas, l as paginationSchema, m as defineCompensation, n as deleteResponse, o as itemResponse, p as wrapResponse, r as errorResponseSchema, s as listResponse, t as createStateMachine, u as queryParams } from "../utils-7sJ8X83I.mjs";
5
- import { a as OrgAccessDeniedError, c as ServiceUnavailableError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-BF2bIOIS.mjs";
4
+ import { _ as withCompensation, a as getListQueryParams, c as mutationResponse, d as responses, f as successResponseSchema, g as defineCompensation, h as defineGuard, i as getDefaultCrudSchemas, l as paginationSchema, m as handleRaw, n as deleteResponse, o as itemResponse, p as wrapResponse, r as errorResponseSchema, s as listResponse, t as createStateMachine, u as queryParams } from "../utils-yYT3HDXt.mjs";
5
+ import { a as OrgAccessDeniedError, c as ServiceUnavailableError, d as createDomainError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-BF2bIOIS.mjs";
6
6
  import { t as hasEvents } from "../typeGuards-CcFZXgU7.mjs";
7
- export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UnauthorizedError, ValidationError, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createError, createQueryParser, createStateMachine, defineCompensation, deleteResponse, errorResponseSchema, getDefaultCrudSchemas, getListQueryParams, hasEvents, isArcError, isJsonSchema, isZodSchema, itemResponse, listResponse, mutationResponse, paginationSchema, queryParams, responses, successResponseSchema, toJsonSchema, withCompensation, wrapResponse };
7
+ export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UnauthorizedError, ValidationError, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, defineCompensation, defineGuard, deleteResponse, errorResponseSchema, getDefaultCrudSchemas, getListQueryParams, handleRaw, hasEvents, isArcError, isJsonSchema, isZodSchema, itemResponse, listResponse, mutationResponse, paginationSchema, queryParams, responses, successResponseSchema, toJsonSchema, withCompensation, wrapResponse };
@@ -1,3 +1,4 @@
1
+ import { t as ArcError } from "./errors-BF2bIOIS.mjs";
1
2
  //#region src/utils/compensation.ts
2
3
  /**
3
4
  * Run steps in order with automatic compensation on failure.
@@ -69,6 +70,69 @@ function defineCompensation(name, steps) {
69
70
  };
70
71
  }
71
72
  //#endregion
73
+ //#region src/utils/defineGuard.ts
74
+ /** Hidden property key for guard context storage on the request object. */
75
+ const GUARD_STORE_KEY = "__arcGuardContext";
76
+ /**
77
+ * Create a typed guard. See module JSDoc for usage.
78
+ */
79
+ function defineGuard(config) {
80
+ const { name, resolve } = config;
81
+ const preHandler = async (req, reply) => {
82
+ const ctx = await resolve(req, reply);
83
+ if (!reply.sent) {
84
+ const store = req[GUARD_STORE_KEY] ?? {};
85
+ store[name] = ctx;
86
+ req[GUARD_STORE_KEY] = store;
87
+ }
88
+ };
89
+ return {
90
+ preHandler,
91
+ name,
92
+ from(req) {
93
+ const store = req[GUARD_STORE_KEY];
94
+ if (!store || !(name in store)) throw new Error(`Guard '${name}' not resolved on this request. Add it to routeGuards or the route's preHandler array.`);
95
+ return store[name];
96
+ }
97
+ };
98
+ }
99
+ //#endregion
100
+ //#region src/utils/handleRaw.ts
101
+ /**
102
+ * Wrap a raw Fastify handler with Arc's response envelope and error handling.
103
+ *
104
+ * @param handler - Async function that receives `(request, reply)` and returns data.
105
+ * The return value is sent as `{ success: true, data }`. If it returns
106
+ * `undefined` or `null`, `{ success: true }` is sent (no `data` field).
107
+ * @param statusCode - HTTP status code for successful responses (default: 200)
108
+ */
109
+ function handleRaw(handler, statusCode = 200) {
110
+ return async (request, reply) => {
111
+ try {
112
+ const result = await handler(request, reply);
113
+ if (reply.sent) return;
114
+ if (result === void 0 || result === null) reply.code(statusCode).send({ success: true });
115
+ else reply.code(statusCode).send({
116
+ success: true,
117
+ data: result
118
+ });
119
+ } catch (err) {
120
+ if (reply.sent) return;
121
+ if (err instanceof ArcError) {
122
+ reply.code(err.statusCode).send(err.toJSON());
123
+ return;
124
+ }
125
+ const error = err;
126
+ const code = error.statusCode ?? error.status ?? 500;
127
+ reply.code(code).send({
128
+ success: false,
129
+ error: error.message ?? "Internal server error",
130
+ ...error.code && { code: error.code }
131
+ });
132
+ }
133
+ };
134
+ }
135
+ //#endregion
72
136
  //#region src/utils/responseSchemas.ts
73
137
  /**
74
138
  * Base success response schema
@@ -579,4 +643,4 @@ function createStateMachine(name, transitions = {}, options = {}) {
579
643
  };
580
644
  }
581
645
  //#endregion
582
- export { getListQueryParams as a, mutationResponse as c, responses as d, successResponseSchema as f, withCompensation as h, getDefaultCrudSchemas as i, paginationSchema as l, defineCompensation as m, deleteResponse as n, itemResponse as o, wrapResponse as p, errorResponseSchema as r, listResponse as s, createStateMachine as t, queryParams as u };
646
+ export { withCompensation as _, getListQueryParams as a, mutationResponse as c, responses as d, successResponseSchema as f, defineCompensation as g, defineGuard as h, getDefaultCrudSchemas as i, paginationSchema as l, handleRaw as m, deleteResponse as n, itemResponse as o, wrapResponse as p, errorResponseSchema as r, listResponse as s, createStateMachine as t, queryParams as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.8.1",
3
+ "version": "2.8.4",
4
4
  "description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -340,19 +340,18 @@
340
340
  },
341
341
  "dependencies": {
342
342
  "fastify-plugin": "^5.0.1",
343
- "qs": "^6.14.1",
343
+ "qs": "^6.15.1",
344
344
  "secure-json-parse": "^4.1.0"
345
345
  },
346
346
  "devDependencies": {
347
- "@better-auth/mongo-adapter": "^1.6.0",
348
- "@biomejs/biome": "^2.4.10",
349
- "ajv": "^8.18.0",
347
+ "@better-auth/mongo-adapter": "^1.6.2",
348
+ "@biomejs/biome": "^2.4.11",
350
349
  "@classytic/mongokit": "file:../../packages/mongokit/classytic-mongokit-3.6.0.tgz",
351
350
  "@classytic/streamline": "^2.1.0",
352
351
  "@fastify/cors": "^11.2.0",
353
352
  "@fastify/helmet": "^13.0.2",
354
353
  "@fastify/jwt": "^10.0.0",
355
- "@fastify/multipart": "^9.0.0",
354
+ "@fastify/multipart": "^10.0.0",
356
355
  "@fastify/rate-limit": "^10.3.0",
357
356
  "@fastify/sensible": "^6.0.4",
358
357
  "@fastify/type-provider-typebox": "^6.0.0",
@@ -363,10 +362,11 @@
363
362
  "@types/node": "^22.10.0",
364
363
  "@types/qs": "^6.14.0",
365
364
  "@vitest/coverage-v8": "^3.2.4",
366
- "better-auth": "^1.6.0",
365
+ "ajv": "^8.18.0",
366
+ "better-auth": "^1.6.2",
367
367
  "fastify-raw-body": "^5.0.0",
368
368
  "jsonwebtoken": "^9.0.0",
369
- "knip": "^6.3.0",
369
+ "knip": "^6.4.1",
370
370
  "mongodb": "^7.1.0",
371
371
  "mongodb-memory-server": "^11.0.1",
372
372
  "mongoose": "^9.4.1",
@@ -8,7 +8,7 @@ description: |
8
8
  Triggers: arc, fastify resource, defineResource, createApp, BaseController, arc preset,
9
9
  arc auth, arc events, arc jobs, arc websocket, arc mcp, arc plugin, arc testing, arc cli,
10
10
  arc permissions, arc hooks, arc pipeline, arc factory, arc cache, arc QueryCache.
11
- version: 2.8.0
11
+ version: 2.8.1
12
12
  license: MIT
13
13
  metadata:
14
14
  author: Classytic
@@ -88,17 +88,28 @@ const productResource = defineResource({
88
88
  delete: requireRoles(['admin']),
89
89
  },
90
90
  cache: { staleTime: 30, gcTime: 300, tags: ['catalog'] },
91
- additionalRoutes: [
92
- { method: 'GET', path: '/featured', handler: 'getFeatured', permissions: allowPublic(), wrapHandler: true },
93
- ],
94
91
 
95
- // v2.8: routes (replaces additionalRoutes additionalRoutes still works but is deprecated)
92
+ // v2.8.1: routeGuards auto-apply to ALL routes (CRUD + custom + preset)
93
+ routeGuards: [modeGuard, orgGuard.preHandler],
94
+
95
+ // v2.8.1: fieldRules constraints → auto-map to OpenAPI + AJV validation
96
+ schemaOptions: {
97
+ fieldRules: {
98
+ name: { minLength: 2, maxLength: 200, description: 'Product name' },
99
+ price: { min: 0, max: 100000 },
100
+ sku: { pattern: '^[A-Z]{3}-\\d{3}$' },
101
+ status: { enum: ['draft', 'active', 'archived'] },
102
+ deletedAt: { systemManaged: true },
103
+ },
104
+ },
105
+
106
+ // Custom routes (compose with presets — softDelete adds /deleted, /:id/restore)
96
107
  routes: [
97
108
  { method: 'GET', path: '/stats', handler: 'getStats', permissions: auth() },
98
109
  { method: 'POST', path: '/webhook', handler: webhookFn, raw: true, permissions: auth() },
99
110
  ],
100
111
 
101
- // v2.8: actions (replaces onRegister + createActionRouter)
112
+ // Actions (replaces onRegister + createActionRouter)
102
113
  actions: {
103
114
  approve: async (id, data, req) => service.approve(id, req.user._id),
104
115
  cancel: {
@@ -112,8 +123,70 @@ const productResource = defineResource({
112
123
 
113
124
  await fastify.register(productResource.toPlugin());
114
125
  // Auto-generates: GET /, GET /:id, POST /, PATCH /:id, DELETE /:id
126
+ // + softDelete preset adds: GET /deleted, POST /:id/restore
115
127
  ```
116
128
 
129
+ ## routeGuards + defineGuard (v2.8.1)
130
+
131
+ Resource-level guards that apply to **every** route (CRUD + custom + preset):
132
+
133
+ ```typescript
134
+ import { defineGuard } from '@classytic/arc/utils';
135
+ import type { RouteHandlerMethod } from '@classytic/arc';
136
+
137
+ // Simple guard — reject if condition fails
138
+ const modeGuard: RouteHandlerMethod = async (req, reply) => {
139
+ if (!req.headers['x-mode']) {
140
+ reply.code(403).send({ error: 'Mode header required' });
141
+ }
142
+ };
143
+
144
+ // Typed guard — resolve context once, extract anywhere
145
+ const orgGuard = defineGuard({
146
+ name: 'org',
147
+ resolve: (req) => {
148
+ const orgId = req.headers['x-org-id'] as string;
149
+ if (!orgId) throw new Error('Missing x-org-id');
150
+ return { orgId, actorId: req.user?.id ?? 'system' };
151
+ },
152
+ });
153
+
154
+ defineResource({
155
+ name: 'procurement',
156
+ routeGuards: [modeGuard, orgGuard.preHandler], // all routes protected
157
+ routes: [{
158
+ method: 'GET', path: '/summary', raw: true, permissions: auth(),
159
+ handler: async (req, reply) => {
160
+ const { orgId } = orgGuard.from(req); // typed, no re-computation
161
+ reply.send({ orgId, count: await Model.countDocuments() });
162
+ },
163
+ }],
164
+ // ...
165
+ });
166
+ ```
167
+
168
+ **Execution order:** auth → permissions → cache/idempotency → `routeGuards` → per-route `preHandler`
169
+
170
+ ## fieldRules → OpenAPI + AJV (v2.8.1)
171
+
172
+ One definition, two outputs — constraints auto-map to OpenAPI schema + Fastify AJV validation:
173
+
174
+ ```typescript
175
+ schemaOptions: {
176
+ fieldRules: {
177
+ name: { minLength: 2, maxLength: 200, description: 'Product name' },
178
+ price: { min: 0, max: 100000 },
179
+ sku: { pattern: '^[A-Z]{3}-\\d{3}$' },
180
+ status: { enum: ['draft', 'active', 'archived'] },
181
+ password: { hidden: true }, // blocked from select + OpenAPI
182
+ deletedAt: { systemManaged: true }, // blocked from input schemas
183
+ slug: { immutable: true }, // excluded from update body
184
+ },
185
+ },
186
+ ```
187
+
188
+ Mongoose model-level constraints (`minlength`, `maxlength`, `min`, `max`, `enum`) take precedence. `fieldRules` supplements what the model doesn't declare.
189
+
117
190
  ## Authentication
118
191
 
119
192
  Auth uses a **discriminated union** with `type` field:
@@ -783,6 +856,28 @@ auth: async (headers) => ({
783
856
 
784
857
  **Guards** for custom tools: `guard(requireAuth, requireOrg, requireRole('admin'), handler)`
785
858
 
859
+ **AI SDK bridge** (v2.8.4+) — expose AI SDK `tool()` definitions over MCP without duplicating glue. Handles auth, guards, `{ error } → isError` translation, and thrown-error mapping:
860
+
861
+ ```typescript
862
+ import { bridgeToMcp, buildMcpToolsFromBridges, getUserId, hasOrg, type McpBridge } from '@classytic/arc/mcp';
863
+
864
+ export const triggerJobBridge: McpBridge = {
865
+ name: 'trigger_job',
866
+ description: 'Start a job.',
867
+ inputSchema: { phase: z.enum(['investigate', 'fix']) },
868
+ annotations: { destructiveHint: true },
869
+ buildTool: (ctx) => buildTriggerJobTool(getUserId(ctx) ?? ''),
870
+ guard: (ctx) => (hasOrg(ctx) ? null : 'Organization scope required'),
871
+ };
872
+
873
+ await app.register(mcpPlugin, {
874
+ resources,
875
+ extraTools: buildMcpToolsFromBridges([triggerJobBridge], {
876
+ exclude: process.env.DEPLOYMENT === 'readonly' ? ['trigger_job'] : [],
877
+ }),
878
+ });
879
+ ```
880
+
786
881
  **Service scope**: When `clientId` is set in auth result, MCP produces `kind: "service"` RequestScope — works with `requireServiceScope()`, `getClientId()`, `getServiceScopes()`. No synthetic userId needed for machine principals.
787
882
 
788
883
  **Multi-tenancy**: `organizationId` from auth flows into BaseController org-scoping automatically.