@classytic/arc 2.8.0 → 2.8.1

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 (69) hide show
  1. package/README.md +10 -1
  2. package/dist/{BaseController-CpMfCXdn.mjs → BaseController-DAGGc5Xn.mjs} +76 -25
  3. package/dist/{EventTransport-n1KBxC_N.d.mts → EventTransport-CLXJUzyT.d.mts} +37 -1
  4. package/dist/{ResourceRegistry-BOtJuRCs.mjs → ResourceRegistry-Dtcojmu8.mjs} +14 -2
  5. package/dist/adapters/index.d.mts +2 -2
  6. package/dist/adapters/index.mjs +1 -1
  7. package/dist/{adapters-BxGgSHjj.mjs → adapters-BBqAVvPK.mjs} +11 -0
  8. package/dist/auth/index.d.mts +1 -1
  9. package/dist/auth/index.mjs +3 -3
  10. package/dist/{betterAuthOpenApi-CHCIuA-p.mjs → betterAuthOpenApi-C5lDyRH2.mjs} +1 -1
  11. package/dist/cli/commands/docs.mjs +2 -2
  12. package/dist/cli/commands/introspect.mjs +1 -1
  13. package/dist/core/index.d.mts +2 -2
  14. package/dist/core/index.mjs +4 -4
  15. package/dist/{core-BfrfxNqO.mjs → core-CrLDuqoT.mjs} +1 -1
  16. package/dist/{createActionRouter-CbkIAaGh.mjs → createActionRouter-Df1BuawX.mjs} +87 -21
  17. package/dist/{createApp-Cy8eUNKQ.mjs → createApp-p2OThysU.mjs} +2 -2
  18. package/dist/{defineResource-CovBXvTB.mjs → defineResource-CqeUltrW.mjs} +19 -7
  19. package/dist/docs/index.d.mts +1 -1
  20. package/dist/docs/index.mjs +1 -1
  21. package/dist/dynamic/index.d.mts +1 -1
  22. package/dist/dynamic/index.mjs +1 -1
  23. package/dist/{errorHandler-BW08lEiy.mjs → errorHandler-Cw34h_om.mjs} +1 -1
  24. package/dist/{errorHandler-BeN-ERN7.d.mts → errorHandler-DJ7OAB2V.d.mts} +1 -1
  25. package/dist/{eventPlugin-CAOWMQS8.d.mts → eventPlugin-Cdjwo0Gv.d.mts} +1 -1
  26. package/dist/{eventPlugin-x4jo3sG0.mjs → eventPlugin-XijlQmlL.mjs} +19 -1
  27. package/dist/events/index.d.mts +399 -28
  28. package/dist/events/index.mjs +345 -29
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/events/transports/redis.d.mts +1 -1
  31. package/dist/factory/index.d.mts +1 -1
  32. package/dist/factory/index.mjs +1 -1
  33. package/dist/hooks/index.d.mts +1 -1
  34. package/dist/{index-BpMhrFgn.d.mts → index-0zj73o2U.d.mts} +1 -1
  35. package/dist/{index-qct60lnl.d.mts → index-DadoLP51.d.mts} +35 -3
  36. package/dist/index.d.mts +4 -4
  37. package/dist/index.mjs +7 -7
  38. package/dist/integrations/event-gateway.d.mts +1 -1
  39. package/dist/integrations/index.d.mts +1 -1
  40. package/dist/integrations/mcp/index.d.mts +2 -2
  41. package/dist/integrations/mcp/index.mjs +1 -1
  42. package/dist/integrations/mcp/testing.d.mts +1 -1
  43. package/dist/integrations/mcp/testing.mjs +1 -1
  44. package/dist/{interface-IJqN3pXK.d.mts → interface-CS6d7HiB.d.mts} +549 -107
  45. package/dist/{openapi-AYLVjqVe.mjs → openapi-q6rNKfZy.mjs} +49 -2
  46. package/dist/org/index.d.mts +1 -1
  47. package/dist/plugins/index.d.mts +2 -2
  48. package/dist/plugins/index.mjs +3 -3
  49. package/dist/plugins/tracing-entry.mjs +1 -1
  50. package/dist/presets/index.d.mts +3 -3
  51. package/dist/presets/multiTenant.d.mts +1 -1
  52. package/dist/{redis-stream-CF1lrKVk.d.mts → redis-stream-BgrYzpeq.d.mts} +1 -1
  53. package/dist/registry/index.d.mts +1 -1
  54. package/dist/registry/index.mjs +1 -1
  55. package/dist/{resourceToTools-C_1SMiCz.mjs → resourceToTools-DNNWnZtx.mjs} +193 -63
  56. package/dist/rpc/index.mjs +1 -1
  57. package/dist/testing/index.d.mts +2 -2
  58. package/dist/testing/index.mjs +1 -1
  59. package/dist/types/index.d.mts +2 -2
  60. package/dist/{types-gUxAIZHp.d.mts → types-BlOuKTPw.d.mts} +4 -4
  61. package/dist/{types-Ct0PUUSp.d.mts → types-D3b7hA00.d.mts} +1 -1
  62. package/dist/utils/index.d.mts +2 -14
  63. package/dist/utils/index.mjs +5 -5
  64. package/dist/{utils-B-l6410F.mjs → utils-7sJ8X83I.mjs} +1 -13
  65. package/package.json +4 -3
  66. /package/dist/{circuitBreaker-l18oRgL5.mjs → circuitBreaker-cmi5XDv5.mjs} +0 -0
  67. /package/dist/{errors-Cg58SLNi.mjs → errors-BF2bIOIS.mjs} +0 -0
  68. /package/dist/{requestContext-xHIKedG6.mjs → requestContext-DYvHl113.mjs} +0 -0
  69. /package/dist/{schemaConverter-Y5EejTnJ.mjs → schemaConverter-OxfCshus.mjs} +0 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Database-agnostic resource framework for Fastify. Define resources, get CRUD routes, permissions, presets, caching, events, OpenAPI, and MCP tools — without boilerplate.
4
4
 
5
- **v2.8.0** | Fastify 5+ | Node.js 22+ | ESM only | 260+ test files, 3523+ tests
5
+ **v2.8.1** | Fastify 5+ | Node.js 22+ | ESM only | 260+ test files, 3600+ tests
6
6
 
7
7
  ## Install
8
8
 
@@ -694,6 +694,15 @@ npx @classytic/arc doctor # Health check
694
694
  | `@classytic/arc/docs` | OpenAPI generation |
695
695
  | `@classytic/arc/cli` | CLI commands (programmatic) |
696
696
 
697
+ ## v2.8.1 Highlights
698
+
699
+ - **Per-action discriminated validation** — `actions` schemas now enforce required fields via a `oneOf` body schema; missing inputs are rejected at the HTTP layer by AJV (no more silent bypass)
700
+ - **Actions in OpenAPI** — `POST /:id/action` endpoint auto-generated from `ResourceDefinition.actions`, with per-action descriptions and the same discriminated body schema as the runtime router
701
+ - **Route/action metadata preserved** — `mcp: false`, `description`, `annotations` no longer dropped during `routes → additionalRoutes` normalization
702
+ - **Canonical source retained** — `ResourceDefinition.routes` and `ResourceDefinition.actions` now kept as declared, so OpenAPI/MCP/registry can read the original shape
703
+ - **Outbox hardening** — expanded `OutboxStore` contract (`claimPending`, `fail`, write options, dedupe, visibleAt), ownership-mismatch throws, onError reporting, safe multi-worker relay
704
+ - **`slugLookup` fallback** — works with MongoKit's default Repository (no custom `getBySlug` needed)
705
+
697
706
  ## v2.8.0 Highlights
698
707
 
699
708
  - **MCP Integration** — expose resources as AI agent tools (stateless by default, service scope, multi-tenancy)
@@ -843,9 +843,11 @@ var BaseController = class {
843
843
  status: 400
844
844
  };
845
845
  }
846
+ const deleteMode = req.query?.hard === "true" || req.query?.hard === true || req.body?.mode === "hard" ? "hard" : void 0;
846
847
  const repoDelete = async () => this.repository.delete(repoId, {
847
848
  user,
848
- context: arcContext
849
+ context: arcContext,
850
+ ...deleteMode ? { mode: deleteMode } : {}
849
851
  });
850
852
  let result;
851
853
  if (hooks && this.resourceName) result = await hooks.executeAround(this.resourceName, "delete", existing, repoDelete, {
@@ -854,7 +856,13 @@ var BaseController = class {
854
856
  meta: { id }
855
857
  });
856
858
  else result = await repoDelete();
857
- if (!(typeof result === "object" && result !== null ? result.success : result)) return {
859
+ if (!(() => {
860
+ if (typeof result !== "object" || result === null) return !!result;
861
+ const r = result;
862
+ if (typeof r.success === "boolean") return r.success;
863
+ if (typeof r.deletedCount === "number") return r.deletedCount > 0;
864
+ return true;
865
+ })()) return {
858
866
  success: false,
859
867
  error: "Resource not found",
860
868
  status: 404
@@ -876,16 +884,23 @@ var BaseController = class {
876
884
  };
877
885
  }
878
886
  async getBySlug(req) {
887
+ const slugField = this._presetFields.slugField ?? "slug";
888
+ const slug = req.params[slugField] ?? req.params.slug;
889
+ const options = this.queryResolver.resolve(req, this.meta(req));
879
890
  const repo = this.repository;
880
- if (!repo.getBySlug) return {
891
+ let item = null;
892
+ if (repo.getBySlug) item = await repo.getBySlug(slug, options);
893
+ else if (repo.getOne) {
894
+ const filter = {
895
+ [slugField]: slug,
896
+ ...options?.filter ?? {}
897
+ };
898
+ item = await repo.getOne(filter, options);
899
+ } else return {
881
900
  success: false,
882
- error: "Slug lookup not implemented",
901
+ error: "Slug lookup not implemented — repository needs getBySlug() or getOne()",
883
902
  status: 501
884
903
  };
885
- const slugField = this._presetFields.slugField ?? "slug";
886
- const slug = req.params[slugField] ?? req.params.slug;
887
- const options = this.queryResolver.resolve(req, this.meta(req));
888
- const item = await repo.getBySlug(slug, options);
889
904
  if (!this.accessControl.validateItemAccess(item, req)) return {
890
905
  success: false,
891
906
  error: "Resource not found",
@@ -904,21 +919,25 @@ var BaseController = class {
904
919
  error: "Soft delete not implemented",
905
920
  status: 501
906
921
  };
907
- const options = this.queryResolver.resolve(req, this.meta(req));
908
- const result = await repo.getDeleted(options);
909
- if (Array.isArray(result)) return {
910
- success: true,
911
- data: {
912
- docs: result,
913
- page: 1,
914
- limit: result.length,
915
- total: result.length,
916
- pages: 1,
917
- hasNext: false,
918
- hasPrev: false
919
- },
920
- status: 200
921
- };
922
+ const parsed = this.queryResolver.resolve(req, this.meta(req));
923
+ const result = await repo.getDeleted(parsed, parsed);
924
+ if (Array.isArray(result)) {
925
+ const docs = result;
926
+ return {
927
+ success: true,
928
+ data: {
929
+ method: "offset",
930
+ docs,
931
+ page: 1,
932
+ limit: docs.length,
933
+ total: docs.length,
934
+ pages: 1,
935
+ hasNext: false,
936
+ hasPrev: false
937
+ },
938
+ status: 200
939
+ };
940
+ }
922
941
  return {
923
942
  success: true,
924
943
  data: result,
@@ -950,13 +969,45 @@ var BaseController = class {
950
969
  details: { code: "OWNERSHIP_DENIED" },
951
970
  status: 403
952
971
  };
972
+ const arcContext = this.meta(req);
973
+ const user = req.user;
953
974
  const repoId = this.resolveRepoId(id, existing);
954
- const item = await repo.restore(repoId);
975
+ const hooks = this.getHooks(req);
976
+ if (hooks && this.resourceName) try {
977
+ await hooks.executeBefore(this.resourceName, "restore", existing, {
978
+ user,
979
+ context: arcContext,
980
+ meta: { id }
981
+ });
982
+ } catch (err) {
983
+ return {
984
+ success: false,
985
+ error: "Hook execution failed",
986
+ details: {
987
+ code: "BEFORE_RESTORE_HOOK_ERROR",
988
+ message: err.message
989
+ },
990
+ status: 400
991
+ };
992
+ }
993
+ const repoRestore = () => repo.restore(repoId);
994
+ let item;
995
+ if (hooks && this.resourceName) item = await hooks.executeAround(this.resourceName, "restore", existing, repoRestore, {
996
+ user,
997
+ context: arcContext,
998
+ meta: { id }
999
+ });
1000
+ else item = await repoRestore();
955
1001
  if (!item) return {
956
1002
  success: false,
957
1003
  error: "Resource not found",
958
1004
  status: 404
959
1005
  };
1006
+ if (hooks && this.resourceName) await hooks.executeAfter(this.resourceName, "restore", item, {
1007
+ user,
1008
+ context: arcContext,
1009
+ meta: { id }
1010
+ });
960
1011
  return {
961
1012
  success: true,
962
1013
  data: item,
@@ -1223,7 +1274,7 @@ var BaseController = class {
1223
1274
  };
1224
1275
  return {
1225
1276
  success: true,
1226
- data: await repo.deleteMany(scopedFilter),
1277
+ data: req.query?.hard === "true" || req.query?.hard === true || body.mode === "hard" ? await repo.deleteMany(scopedFilter, { mode: "hard" }) : await repo.deleteMany(scopedFilter),
1227
1278
  status: 200
1228
1279
  };
1229
1280
  }
@@ -61,6 +61,25 @@ interface EventTransport {
61
61
  * Publish an event to the transport
62
62
  */
63
63
  publish(event: DomainEvent): Promise<void>;
64
+ /**
65
+ * Publish a batch of events to the transport (optional, v2.8.1+).
66
+ *
67
+ * Transports that can efficiently batch (Kafka producer, Redis pipeline,
68
+ * RabbitMQ publisher confirms, SQS send-message-batch) should implement
69
+ * this. {@link import('./outbox.js').EventOutbox.relay} auto-detects and
70
+ * uses it for much higher throughput than per-event publishing.
71
+ *
72
+ * **Contract**: the returned `PublishManyResult` must describe the
73
+ * per-event outcome so the caller can acknowledge successes and fail the
74
+ * rest. Partial success is allowed — the transport reports it per event.
75
+ *
76
+ * If not implemented, `EventOutbox.relay` falls back to calling
77
+ * {@link publish} once per event.
78
+ *
79
+ * @param events - Events to publish (in order)
80
+ * @returns Per-event outcome map keyed by `event.meta.id`
81
+ */
82
+ publishMany?(events: readonly DomainEvent[]): Promise<PublishManyResult>;
64
83
  /**
65
84
  * Subscribe to events matching a pattern
66
85
  * @param pattern - Event type pattern (e.g., 'product.*', '*')
@@ -73,6 +92,14 @@ interface EventTransport {
73
92
  */
74
93
  close?(): Promise<void>;
75
94
  }
95
+ /**
96
+ * Per-event outcome returned by {@link EventTransport.publishMany}.
97
+ *
98
+ * The key is `event.meta.id`; the value is `null` for success or an `Error`
99
+ * for per-event failure. Transports MUST include an entry for every event
100
+ * in the input batch.
101
+ */
102
+ type PublishManyResult = ReadonlyMap<string, Error | null>;
76
103
  interface MemoryEventTransportOptions {
77
104
  /** Logger for error/warning messages (default: console) */
78
105
  logger?: EventLogger;
@@ -88,6 +115,15 @@ declare class MemoryEventTransport implements EventTransport {
88
115
  private logger;
89
116
  constructor(options?: MemoryEventTransportOptions);
90
117
  publish(event: DomainEvent): Promise<void>;
118
+ /**
119
+ * Reference `publishMany` implementation — delegates to `publish()` in order.
120
+ *
121
+ * Production transports (Kafka, Redis pipeline, SQS batch) should override
122
+ * this with a single batched network call. Memory transport has nothing to
123
+ * batch, so we just loop — the loop still returns a proper result map so
124
+ * `EventOutbox.relay` can exercise the batched code path in tests.
125
+ */
126
+ publishMany(events: readonly DomainEvent[]): Promise<PublishManyResult>;
91
127
  subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
92
128
  close(): Promise<void>;
93
129
  }
@@ -96,4 +132,4 @@ declare class MemoryEventTransport implements EventTransport {
96
132
  */
97
133
  declare function createEvent<T>(type: string, payload: T, meta?: Partial<DomainEvent["meta"]>): DomainEvent<T>;
98
134
  //#endregion
99
- export { MemoryEventTransport as a, EventTransport as i, EventHandler as n, MemoryEventTransportOptions as o, EventLogger as r, createEvent as s, DomainEvent as t };
135
+ export { MemoryEventTransport as a, createEvent as c, EventTransport as i, EventHandler as n, MemoryEventTransportOptions as o, EventLogger as r, PublishManyResult as s, DomainEvent as t };
@@ -52,6 +52,17 @@ var ResourceRegistry = class {
52
52
  pipelineSteps: extractPipelineSteps(resource.pipe),
53
53
  rateLimit: resource.rateLimit,
54
54
  audit: resource.audit,
55
+ actionPermissions: resource.actionPermissions,
56
+ actions: resource.actions ? Object.entries(resource.actions).map(([name, entry]) => {
57
+ if (typeof entry === "function") return { name };
58
+ return {
59
+ name,
60
+ description: entry.description,
61
+ schema: entry.schema,
62
+ permissions: entry.permissions,
63
+ mcp: entry.mcp
64
+ };
65
+ }) : void 0,
55
66
  plugin: resource.toPlugin()
56
67
  };
57
68
  this._resources.set(resource.name, entry);
@@ -99,11 +110,12 @@ var ResourceRegistry = class {
99
110
  byModule: this._groupBy(resources, "module"),
100
111
  presetUsage: presetCounts,
101
112
  totalRoutes: resources.reduce((sum, r) => {
102
- if (r.disableDefaultRoutes) return sum + (r.additionalRoutes?.length ?? 0);
113
+ const actionsCount = (r.actions?.length ?? 0) > 0 ? 1 : 0;
114
+ if (r.disableDefaultRoutes) return sum + (r.additionalRoutes?.length ?? 0) + actionsCount;
103
115
  const disabledSet = new Set(r.disabledRoutes ?? []);
104
116
  let defaultCount = CRUD_OPERATIONS.filter((route) => !disabledSet.has(route)).length;
105
117
  if (!disabledSet.has("update") && r.updateMethod === "both") defaultCount += 1;
106
- return sum + defaultCount + (r.additionalRoutes?.length ?? 0);
118
+ return sum + defaultCount + (r.additionalRoutes?.length ?? 0) + actionsCount;
107
119
  }, 0),
108
120
  totalEvents: resources.reduce((sum, r) => sum + (r.events?.length ?? 0), 0)
109
121
  };
@@ -1,3 +1,3 @@
1
- import { a as RelationMetadata, c as ValidationResult, i as FieldMetadata, o as RepositoryLike, r as DataAdapter, s as SchemaMetadata, t as AdapterFactory } from "../interface-IJqN3pXK.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-BpMhrFgn.mjs";
1
+ import { a as RelationMetadata, c as ValidationResult, i as FieldMetadata, o as RepositoryLike, r as DataAdapter, s as SchemaMetadata, t as AdapterFactory } from "../interface-CS6d7HiB.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-0zj73o2U.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-BxGgSHjj.mjs";
1
+ import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-BBqAVvPK.mjs";
2
2
  export { MongooseAdapter, PrismaAdapter, PrismaQueryParser, createMongooseAdapter, createPrismaAdapter };
@@ -90,6 +90,17 @@ var MongooseAdapter = class {
90
90
  if (blockedFields.has(fieldName)) continue;
91
91
  const typeInfo = schemaType;
92
92
  properties[fieldName] = this.mongooseTypeToOpenApi(typeInfo);
93
+ const rule = fieldRules[fieldName];
94
+ if (rule) {
95
+ const prop = properties[fieldName];
96
+ if (rule.minLength != null && prop.minLength == null) prop.minLength = rule.minLength;
97
+ if (rule.maxLength != null && prop.maxLength == null) prop.maxLength = rule.maxLength;
98
+ if (rule.min != null && prop.minimum == null) prop.minimum = rule.min;
99
+ if (rule.max != null && prop.maximum == null) prop.maximum = rule.max;
100
+ if (rule.pattern != null && prop.pattern == null) prop.pattern = rule.pattern;
101
+ if (rule.enum != null && prop.enum == null) prop.enum = rule.enum;
102
+ if (rule.description != null && prop.description == null) prop.description = rule.description;
103
+ }
93
104
  if (typeInfo.isRequired && !optionalSet.has(fieldName) && !fieldRules[fieldName]?.optional) required.push(fieldName);
94
105
  }
95
106
  const readonlyForInput = new Set([...readonlySet]);
@@ -1,4 +1,4 @@
1
- import { b as AuthPluginOptions, y as AuthHelpers } from "../interface-IJqN3pXK.mjs";
1
+ import { b as AuthPluginOptions, y as AuthHelpers } from "../interface-CS6d7HiB.mjs";
2
2
  import { t as PermissionCheck } from "../types-BoaZHr-2.mjs";
3
3
  import { t as ExternalOpenApiPaths } from "../externalPaths-BQ8QijNH.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-BkzVU8h2.mjs";
@@ -1,7 +1,7 @@
1
1
  import { n as normalizeRoles, t as getUserRoles } from "../types-ZUu_h0jp.mjs";
2
- import { t as ArcError } from "../errors-Cg58SLNi.mjs";
2
+ import { t as ArcError } from "../errors-BF2bIOIS.mjs";
3
3
  import { h as requireTeamMembership, l as requireOrgMembership, u as requireOrgRole } from "../permissions-CH4cNwJi.mjs";
4
- import { n as extractBetterAuthOpenApi } from "../betterAuthOpenApi-CHCIuA-p.mjs";
4
+ import { n as extractBetterAuthOpenApi } from "../betterAuthOpenApi-C5lDyRH2.mjs";
5
5
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
6
6
  import fp from "fastify-plugin";
7
7
  //#region src/auth/authPlugin.ts
@@ -677,7 +677,7 @@ function createBetterAuthAdapter(options) {
677
677
  if (!fastify.hasDecorator("authenticate")) fastify.decorate("authenticate", authenticate);
678
678
  if (!fastify.hasDecorator("optionalAuthenticate")) fastify.decorate("optionalAuthenticate", optionalAuthenticate);
679
679
  if (!extractedOpenApi && openapiOpt !== false && auth.api && typeof auth.api === "object") {
680
- const { extractBetterAuthOpenApi } = await import("../betterAuthOpenApi-CHCIuA-p.mjs").then((n) => n.t);
680
+ const { extractBetterAuthOpenApi } = await import("../betterAuthOpenApi-C5lDyRH2.mjs").then((n) => n.t);
681
681
  extractedOpenApi = extractBetterAuthOpenApi(auth.api, {
682
682
  basePath,
683
683
  userFields
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { a as toJsonSchema } from "./schemaConverter-Y5EejTnJ.mjs";
2
+ import { a as toJsonSchema } from "./schemaConverter-OxfCshus.mjs";
3
3
  //#region src/auth/betterAuthOpenApi.ts
4
4
  var betterAuthOpenApi_exports = /* @__PURE__ */ __exportAll({ extractBetterAuthOpenApi: () => extractBetterAuthOpenApi });
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { t as ResourceRegistry } from "../../ResourceRegistry-BOtJuRCs.mjs";
2
- import { t as buildOpenApiSpec } from "../../openapi-AYLVjqVe.mjs";
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-Dtcojmu8.mjs";
2
+ import { t as buildOpenApiSpec } from "../../openapi-q6rNKfZy.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 ResourceRegistry } from "../../ResourceRegistry-BOtJuRCs.mjs";
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-Dtcojmu8.mjs";
2
2
  import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  //#region src/cli/commands/introspect.ts
@@ -1,3 +1,3 @@
1
- import { At as BaseController, Ft as BodySanitizerConfig, It as AccessControl, Jt as defineResource, Lt as AccessControlConfig, Mt as QueryResolver, Nt as QueryResolverConfig, Pt as BodySanitizer, jt as BaseControllerOptions, qt as ResourceDefinition } from "../interface-IJqN3pXK.mjs";
2
- import { A as MutationOperation, C as HOOK_PHASES, D as MAX_REGEX_LENGTH, E as MAX_FILTER_DEPTH, M as SYSTEM_FIELDS, O as MAX_SEARCH_LENGTH, S as HOOK_OPERATIONS, T as HookPhase, _ as DEFAULT_LIMIT, a as getControllerScope, b as DEFAULT_TENANT_FIELD, c as createCrudRouter, d as ActionRouterConfig, f as IdempotencyService, g as DEFAULT_ID_FIELD, h as CrudOperation, i as getControllerContext, j as RESERVED_QUERY_PARAMS, k as MUTATION_OPERATIONS, l as createPermissionMiddleware, m as CRUD_OPERATIONS, n as createFastifyHandler, o as sendControllerResponse, p as createActionRouter, r as createRequestContext, s as defineResourceVariants, t as createCrudHandlers, u as ActionHandler, v as DEFAULT_MAX_LIMIT, w as HookOperation, x as DEFAULT_UPDATE_METHOD, y as DEFAULT_SORT } from "../index-qct60lnl.mjs";
1
+ import { At as BaseController, Ft as BodySanitizerConfig, It as AccessControl, Jt as defineResource, Lt as AccessControlConfig, Mt as QueryResolver, Nt as QueryResolverConfig, Pt as BodySanitizer, jt as BaseControllerOptions, qt as ResourceDefinition } from "../interface-CS6d7HiB.mjs";
2
+ import { A as MutationOperation, C as HOOK_PHASES, D as MAX_REGEX_LENGTH, E as MAX_FILTER_DEPTH, M as SYSTEM_FIELDS, O as MAX_SEARCH_LENGTH, S as HOOK_OPERATIONS, T as HookPhase, _ as DEFAULT_LIMIT, a as getControllerScope, b as DEFAULT_TENANT_FIELD, c as createCrudRouter, d as ActionRouterConfig, f as IdempotencyService, g as DEFAULT_ID_FIELD, h as CrudOperation, i as getControllerContext, j as RESERVED_QUERY_PARAMS, k as MUTATION_OPERATIONS, l as createPermissionMiddleware, m as CRUD_OPERATIONS, n as createFastifyHandler, o as sendControllerResponse, p as createActionRouter, r as createRequestContext, s as defineResourceVariants, t as createCrudHandlers, u as ActionHandler, v as DEFAULT_MAX_LIMIT, w as HookOperation, x as DEFAULT_UPDATE_METHOD, y as DEFAULT_SORT } from "../index-DadoLP51.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, defineResourceVariants, getControllerContext, getControllerScope, sendControllerResponse };
@@ -1,6 +1,6 @@
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-CpMfCXdn.mjs";
3
- import { t as createActionRouter } from "../createActionRouter-CbkIAaGh.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-CovBXvTB.mjs";
5
- import { t as defineResourceVariants } from "../core-BfrfxNqO.mjs";
2
+ import { i as AccessControl, n as QueryResolver, r as BodySanitizer, t as BaseController } from "../BaseController-DAGGc5Xn.mjs";
3
+ import { n as createActionRouter } from "../createActionRouter-Df1BuawX.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-CqeUltrW.mjs";
5
+ import { t as defineResourceVariants } from "../core-CrLDuqoT.mjs";
6
6
  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, defineResourceVariants, getControllerContext, getControllerScope, sendControllerResponse };
@@ -1,4 +1,4 @@
1
- import { n as defineResource } from "./defineResource-CovBXvTB.mjs";
1
+ import { n as defineResource } from "./defineResource-CqeUltrW.mjs";
2
2
  //#region src/core/defineResourceVariants.ts
3
3
  /**
4
4
  * Define multiple resources from a shared base config and per-variant overrides.
@@ -1,7 +1,11 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
2
  import { n as normalizePermissionResult, t as applyPermissionResult } from "./applyPermissionResult-D6GPMsvh.mjs";
3
+ import { a as toJsonSchema } from "./schemaConverter-OxfCshus.mjs";
3
4
  //#region src/core/createActionRouter.ts
4
- var createActionRouter_exports = /* @__PURE__ */ __exportAll({ createActionRouter: () => createActionRouter });
5
+ var createActionRouter_exports = /* @__PURE__ */ __exportAll({
6
+ buildActionBodySchema: () => buildActionBodySchema,
7
+ createActionRouter: () => createActionRouter
8
+ });
5
9
  /**
6
10
  * Create action-based state transition endpoint
7
11
  *
@@ -18,20 +22,7 @@ function createActionRouter(fastify, config) {
18
22
  fastify.log.warn("[createActionRouter] No actions defined, skipping route creation");
19
23
  return;
20
24
  }
21
- const bodyProperties = { action: {
22
- type: "string",
23
- enum: actionEnum,
24
- description: `Action to perform: ${actionEnum.join(" | ")}`
25
- } };
26
- Object.entries(actionSchemas).forEach(([actionName, schema]) => {
27
- if (schema && typeof schema === "object") Object.entries(schema).forEach(([propName, propSchema]) => {
28
- const schemaObj = propSchema;
29
- bodyProperties[propName] = {
30
- ...schemaObj,
31
- description: `${schemaObj.description || ""} (for ${actionName} action)`.trim()
32
- };
33
- });
34
- });
25
+ const bodySchema = buildActionBodySchema(actionEnum, actionSchemas);
35
26
  const routeSchema = {
36
27
  tags: tag ? [tag] : void 0,
37
28
  summary: `Perform action (${actionEnum.join("/")})`,
@@ -44,11 +35,7 @@ function createActionRouter(fastify, config) {
44
35
  } },
45
36
  required: ["id"]
46
37
  },
47
- body: {
48
- type: "object",
49
- properties: bodyProperties,
50
- required: ["action"]
51
- }
38
+ body: bodySchema
52
39
  };
53
40
  const preHandler = [];
54
41
  const hasPublicActions = Object.entries(actionPermissions).some(([, p]) => p?._isPublic) || globalAuth && globalAuth?._isPublic;
@@ -167,6 +154,85 @@ function createActionRouter(fastify, config) {
167
154
  }, "[createActionRouter] Registered action endpoint: POST /:id/action");
168
155
  }
169
156
  /**
157
+ * Build a discriminated body schema for the unified action endpoint.
158
+ *
159
+ * Produces a schema of the form:
160
+ * ```json
161
+ * {
162
+ * "type": "object",
163
+ * "required": ["action"],
164
+ * "oneOf": [
165
+ * { "properties": { "action": { "const": "dispatch" }, "carrier": {...} }, "required": ["action", "carrier"] },
166
+ * { "properties": { "action": { "const": "approve" } }, "required": ["action"] }
167
+ * ]
168
+ * }
169
+ * ```
170
+ *
171
+ * AJV validates this natively, so an action call missing required fields is
172
+ * rejected with HTTP 400 before the handler ever runs.
173
+ *
174
+ * Exported so OpenAPI generation and MCP tool generation can reuse the same
175
+ * schema shape (single source of truth).
176
+ */
177
+ function buildActionBodySchema(actionEnum, actionSchemas = {}) {
178
+ const branches = [];
179
+ for (const actionName of actionEnum) {
180
+ const raw = actionSchemas[actionName];
181
+ const { properties, required } = normalizeActionSchema(raw);
182
+ const branchProperties = {
183
+ action: {
184
+ type: "string",
185
+ const: actionName
186
+ },
187
+ ...properties
188
+ };
189
+ const branchRequired = ["action", ...required.filter((r) => r !== "action")];
190
+ branches.push({
191
+ type: "object",
192
+ properties: branchProperties,
193
+ required: branchRequired
194
+ });
195
+ }
196
+ return {
197
+ type: "object",
198
+ required: ["action"],
199
+ oneOf: branches
200
+ };
201
+ }
202
+ /**
203
+ * Normalize the accepted schema shapes into `{ properties, required }`.
204
+ *
205
+ * Handles:
206
+ * 1. Full JSON Schema object (has `type: 'object'` + `properties`)
207
+ * 2. Zod v4 schema (has `_zod` marker) — converted via `toJsonSchema`
208
+ * 3. Legacy field map (`{ fieldName: { type: 'string' } }`) — every field required
209
+ * unless its schema has `nullable: true` or sentinel `required: false`
210
+ */
211
+ function normalizeActionSchema(raw) {
212
+ if (!raw || typeof raw !== "object") return {
213
+ properties: {},
214
+ required: []
215
+ };
216
+ const converted = toJsonSchema(raw);
217
+ if (converted && typeof converted === "object" && (converted.type === "object" || "properties" in converted)) return {
218
+ properties: converted.properties ?? {},
219
+ required: Array.isArray(converted.required) ? converted.required : []
220
+ };
221
+ const properties = {};
222
+ const required = [];
223
+ for (const [fieldName, fieldSchema] of Object.entries(raw)) {
224
+ if (fieldName === "type" || fieldName === "properties" || fieldName === "required") continue;
225
+ if (!fieldSchema || typeof fieldSchema !== "object") continue;
226
+ const fs = fieldSchema;
227
+ properties[fieldName] = fs;
228
+ if (fs.required !== false) required.push(fieldName);
229
+ }
230
+ return {
231
+ properties,
232
+ required
233
+ };
234
+ }
235
+ /**
170
236
  * Build description with action details
171
237
  * Uses _roles metadata from PermissionCheck functions for OpenAPI docs
172
238
  */
@@ -180,4 +246,4 @@ function buildActionDescription(actions, actionPermissions) {
180
246
  return lines.join("\n");
181
247
  }
182
248
  //#endregion
183
- export { createActionRouter_exports as n, createActionRouter as t };
249
+ export { createActionRouter as n, createActionRouter_exports as r, buildActionBodySchema as t };
@@ -207,7 +207,7 @@ async function registerArcCore(fastify, config, trackPlugin) {
207
207
  await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
208
208
  trackPlugin("arc-core");
209
209
  if (config.arcPlugins?.events !== false) {
210
- const { default: eventPlugin } = await import("./eventPlugin-x4jo3sG0.mjs").then((n) => n.n);
210
+ const { default: eventPlugin } = await import("./eventPlugin-XijlQmlL.mjs").then((n) => n.n);
211
211
  const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
212
212
  await fastify.register(eventPlugin, {
213
213
  ...eventOpts,
@@ -350,7 +350,7 @@ async function registerElevation(fastify, config, trackPlugin) {
350
350
  */
351
351
  async function registerErrorHandler(fastify, config, trackPlugin) {
352
352
  if (config.errorHandler === false) return;
353
- const { errorHandlerPlugin } = await import("./errorHandler-BW08lEiy.mjs").then((n) => n.n);
353
+ const { errorHandlerPlugin } = await import("./errorHandler-Cw34h_om.mjs").then((n) => n.n);
354
354
  const errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
355
355
  await fastify.register(errorHandlerPlugin, errorOpts);
356
356
  trackPlugin("arc-error-handler", errorOpts);
@@ -1,13 +1,13 @@
1
1
  import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-Cxde4rpC.mjs";
2
2
  import { _ as isElevated, n as PUBLIC_SCOPE, v as isMember } from "./types-AOD8fxIw.mjs";
3
- import { t as BaseController } from "./BaseController-CpMfCXdn.mjs";
3
+ import { t as BaseController } from "./BaseController-DAGGc5Xn.mjs";
4
4
  import { i as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-ipsbIRPK.mjs";
5
5
  import { t as getUserRoles } from "./types-ZUu_h0jp.mjs";
6
6
  import { n as normalizePermissionResult, t as applyPermissionResult } from "./applyPermissionResult-D6GPMsvh.mjs";
7
- import { t as requestContext } from "./requestContext-xHIKedG6.mjs";
8
- import { i as getDefaultCrudSchemas } from "./utils-B-l6410F.mjs";
9
- import { r as ForbiddenError } from "./errors-Cg58SLNi.mjs";
10
- import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-Y5EejTnJ.mjs";
7
+ import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-OxfCshus.mjs";
8
+ import { t as requestContext } from "./requestContext-DYvHl113.mjs";
9
+ import { i as getDefaultCrudSchemas } from "./utils-7sJ8X83I.mjs";
10
+ import { r as ForbiddenError } from "./errors-BF2bIOIS.mjs";
11
11
  import { t as hasEvents } from "./typeGuards-CcFZXgU7.mjs";
12
12
  import { r as getAvailablePresets, t as applyPresets } from "./presets-BFrGvvjL.mjs";
13
13
  //#region src/pipeline/pipe.ts
@@ -1087,6 +1087,16 @@ var ResourceDefinition = class {
1087
1087
  customSchemas;
1088
1088
  permissions;
1089
1089
  additionalRoutes;
1090
+ /**
1091
+ * Original v2.8 `routes` declaration — retained for downstream consumers
1092
+ * (OpenAPI, MCP, registry, CLI introspect). Preserves fields dropped during
1093
+ * normalization to `additionalRoutes` (notably `mcp`, `description`,
1094
+ * `annotations`). Undefined when the resource was defined with the legacy
1095
+ * `additionalRoutes` shape.
1096
+ *
1097
+ * Added in 2.8.1 — the source-of-truth fix for "canonical resource manifest".
1098
+ */
1099
+ routes;
1090
1100
  middlewares;
1091
1101
  disableDefaultRoutes;
1092
1102
  disabledRoutes;
@@ -1117,6 +1127,7 @@ var ResourceDefinition = class {
1117
1127
  this.schemaOptions = config.schemaOptions ?? {};
1118
1128
  this.customSchemas = config.customSchemas ?? {};
1119
1129
  this.permissions = config.permissions ?? {};
1130
+ this.routes = config.routes;
1120
1131
  this.additionalRoutes = config.routes ? convertRoutesToAdditionalRoutes(config.routes) : config.additionalRoutes ?? [];
1121
1132
  this.middlewares = config.middlewares ?? {};
1122
1133
  this.disableDefaultRoutes = config.disableDefaultRoutes ?? false;
@@ -1283,7 +1294,7 @@ var ResourceDefinition = class {
1283
1294
  fields: self.fields
1284
1295
  });
1285
1296
  if (self.actions && Object.keys(self.actions).length > 0) {
1286
- const { createActionRouter } = await import("./createActionRouter-CbkIAaGh.mjs").then((n) => n.n);
1297
+ const { createActionRouter } = await import("./createActionRouter-Df1BuawX.mjs").then((n) => n.r);
1287
1298
  createActionRouter(instance, normalizeActionsToRouterConfig(self.actions, self.actionPermissions, self.tag));
1288
1299
  }
1289
1300
  if (self.events && Object.keys(self.events).length > 0) typedInstance.log?.debug?.(`Resource '${self.name}' defined ${Object.keys(self.events).length} events`);
@@ -1358,7 +1369,8 @@ function convertRoutesToAdditionalRoutes(routes) {
1358
1369
  preAuth: route.preAuth,
1359
1370
  streamResponse: route.streamResponse,
1360
1371
  schema: route.schema,
1361
- mcpHandler: route.mcpHandler
1372
+ mcpHandler: route.mcpHandler,
1373
+ mcp: route.mcp
1362
1374
  }));
1363
1375
  }
1364
1376
  /**
@@ -1,4 +1,4 @@
1
- import { it as RegistryEntry } from "../interface-IJqN3pXK.mjs";
1
+ import { it as RegistryEntry } from "../interface-CS6d7HiB.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "../externalPaths-BQ8QijNH.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
 
@@ -1,5 +1,5 @@
1
1
  import { t as getUserRoles } from "../types-ZUu_h0jp.mjs";
2
- import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-AYLVjqVe.mjs";
2
+ import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-q6rNKfZy.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/docs/scalar.ts
5
5
  const scalarPlugin = async (fastify, opts = {}) => {
@@ -1,4 +1,4 @@
1
- import { qt as ResourceDefinition, r as DataAdapter } from "../interface-IJqN3pXK.mjs";
1
+ import { qt as ResourceDefinition, r as DataAdapter } from "../interface-CS6d7HiB.mjs";
2
2
  import { t as PermissionCheck } from "../types-BoaZHr-2.mjs";
3
3
 
4
4
  //#region src/dynamic/ArcDynamicLoader.d.ts
@@ -1,5 +1,5 @@
1
1
  import { t as ArcQueryParser } from "../queryParser-CgCtsjti.mjs";
2
- import { n as defineResource } from "../defineResource-CovBXvTB.mjs";
2
+ import { n as defineResource } from "../defineResource-CqeUltrW.mjs";
3
3
  import { C as publicRead, T as readOnly, b as fullPublic, v as adminOnly, w as publicReadAdminWrite, x as ownerWithAdminBypass, y as authenticated } from "../permissions-CH4cNwJi.mjs";
4
4
  //#region src/dynamic/ArcDynamicLoader.ts
5
5
  const VALID_FIELD_TYPES = new Set([