@classytic/arc 2.6.1 → 2.6.3

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 (48) hide show
  1. package/README.md +48 -2
  2. package/dist/{BaseController-AbbRx3e0.mjs → BaseController-DzRtluEF.mjs} +88 -8
  3. package/dist/{ResourceRegistry-DeCIFlix.mjs → ResourceRegistry-C6ngvOnn.mjs} +1 -0
  4. package/dist/adapters/index.d.mts +2 -2
  5. package/dist/adapters/index.mjs +1 -1
  6. package/dist/{adapters-CTn28N4y.mjs → adapters-gM-WYjNe.mjs} +6 -4
  7. package/dist/audit/index.d.mts +31 -5
  8. package/dist/audit/index.mjs +21 -3
  9. package/dist/auth/index.d.mts +1 -1
  10. package/dist/cli/commands/docs.mjs +1 -1
  11. package/dist/cli/commands/introspect.mjs +1 -1
  12. package/dist/core/index.d.mts +2 -2
  13. package/dist/core/index.mjs +2 -2
  14. package/dist/{createApp-Bol7DLUf.mjs → createApp-D2w0LdYJ.mjs} +27 -11
  15. package/dist/{defineResource-bVKHjQzE.mjs → defineResource-wWMBB4GP.mjs} +48 -30
  16. package/dist/docs/index.d.mts +1 -1
  17. package/dist/dynamic/index.d.mts +1 -1
  18. package/dist/dynamic/index.mjs +1 -1
  19. package/dist/factory/index.d.mts +1 -1
  20. package/dist/factory/index.mjs +31 -15
  21. package/dist/hooks/index.d.mts +1 -1
  22. package/dist/{index-BIsZ_su5.d.mts → index-CHeJa4Zd.d.mts} +3 -3
  23. package/dist/{index-Cb3gtbg7.d.mts → index-gz6iuzCp.d.mts} +1 -1
  24. package/dist/index.d.mts +3 -3
  25. package/dist/index.mjs +4 -4
  26. package/dist/integrations/index.d.mts +1 -1
  27. package/dist/integrations/mcp/index.d.mts +2 -2
  28. package/dist/integrations/mcp/index.mjs +1 -1
  29. package/dist/integrations/mcp/testing.d.mts +1 -1
  30. package/dist/integrations/mcp/testing.mjs +1 -1
  31. package/dist/{interface-DDW43OmS.d.mts → interface-DYH8AXGe.d.mts} +89 -4
  32. package/dist/org/index.d.mts +1 -1
  33. package/dist/plugins/index.d.mts +1 -1
  34. package/dist/plugins/index.mjs +1 -1
  35. package/dist/plugins/tracing-entry.mjs +1 -1
  36. package/dist/presets/index.d.mts +1 -1
  37. package/dist/presets/multiTenant.d.mts +1 -1
  38. package/dist/registry/index.d.mts +1 -1
  39. package/dist/registry/index.mjs +1 -1
  40. package/dist/{resourceToTools-DH3c3e-T.mjs → resourceToTools-nCJWnG1r.mjs} +250 -13
  41. package/dist/testing/index.d.mts +26 -3
  42. package/dist/testing/index.mjs +46 -2
  43. package/dist/types/index.d.mts +1 -1
  44. package/dist/{types-D5rjsS_i.d.mts → types-B4_TDdPe.d.mts} +1 -1
  45. package/dist/{types-D5hJ-k_3.d.mts → types-By-5mIfn.d.mts} +7 -1
  46. package/dist/utils/index.d.mts +1 -1
  47. package/package.json +18 -18
  48. package/skills/arc/SKILL.md +80 -8
@@ -1,4 +1,4 @@
1
- import { t as BaseController } from "./BaseController-AbbRx3e0.mjs";
1
+ import { t as BaseController } from "./BaseController-DzRtluEF.mjs";
2
2
  import { t as pluralize } from "./pluralize-CcT6qF0a.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/integrations/mcp/createMcpServer.ts
@@ -314,6 +314,146 @@ function buildScope(auth) {
314
314
  };
315
315
  }
316
316
  //#endregion
317
+ //#region src/integrations/mcp/jsonSchemaToZod.ts
318
+ /**
319
+ * @classytic/arc — JSON Schema → Zod shape converter
320
+ *
321
+ * Converts an adapter-emitted JSON Schema body shape (`createBody` / `updateBody`)
322
+ * to a flat Zod shape compatible with the MCP SDK's `registerTool({ inputSchema })`
323
+ * contract.
324
+ *
325
+ * Why this exists:
326
+ * - The MCP SDK expects a flat `Record<string, ZodType>` shape (it wraps it in
327
+ * z.object() internally).
328
+ * - When users don't supply explicit `schemaOptions.fieldRules`, MCP would
329
+ * otherwise see an empty schema and silently strip every body field — that's
330
+ * a real DX footgun.
331
+ * - Adapters (Mongoose, MongoKit's buildCrudSchemasFromModel, custom) already
332
+ * emit JSON Schema describing the body. We translate it to Zod so MCP tools
333
+ * can validate input the same way REST routes do.
334
+ *
335
+ * Supported JSON Schema features:
336
+ * - Primitives: string, number, integer, boolean, null (skipped)
337
+ * - Constraints: minLength, maxLength, minimum, maximum, pattern, enum, format
338
+ * - Arrays: typed items + nested object items
339
+ * - Nested objects with `properties` (recursive)
340
+ * - Type unions: ["string", "null"] → string (null skipped)
341
+ * - Composition: oneOf / anyOf / allOf → first viable branch
342
+ * - $ref → permissive (z.unknown()) — refs are not resolved
343
+ * - Unknown types → z.unknown() (lenient — let the controller validate)
344
+ *
345
+ * NOT supported (intentionally — keeps the surface small + deterministic):
346
+ * - Conditional schemas (if/then/else)
347
+ * - dependencies / dependentRequired
348
+ * - Custom keywords beyond standard JSON Schema
349
+ */
350
+ /**
351
+ * Convert a JSON Schema **object** body to a flat Zod shape.
352
+ * Returns `undefined` if the input has no usable properties.
353
+ *
354
+ * @param schema Top-level JSON Schema (must be `type: 'object'` with `properties`)
355
+ * @param mode 'create' enforces required fields, 'update' makes everything optional
356
+ */
357
+ function jsonSchemaToZodShape(schema, mode = "create") {
358
+ if (!schema || typeof schema !== "object") return void 0;
359
+ if (!schema.properties || typeof schema.properties !== "object") return void 0;
360
+ const requiredSet = new Set(schema.required ?? []);
361
+ const shape = {};
362
+ for (const [name, propSchema] of Object.entries(schema.properties)) {
363
+ if (!propSchema || typeof propSchema !== "object") continue;
364
+ const fieldZod = jsonSchemaPropertyToZod(propSchema);
365
+ if (!fieldZod) continue;
366
+ shape[name] = mode === "create" && requiredSet.has(name) ? fieldZod : fieldZod.optional();
367
+ }
368
+ return Object.keys(shape).length > 0 ? shape : void 0;
369
+ }
370
+ /**
371
+ * Convert one JSON Schema node to a Zod type.
372
+ * Handles primitives, arrays, nested objects, type unions, and composition.
373
+ */
374
+ function jsonSchemaPropertyToZod(prop) {
375
+ if (prop.$ref) return applyDescription(z.unknown(), prop);
376
+ if (Array.isArray(prop.oneOf) && prop.oneOf.length > 0) for (const branch of prop.oneOf) {
377
+ const z1 = jsonSchemaPropertyToZod(branch);
378
+ if (z1) return applyDescription(z1, prop);
379
+ }
380
+ if (Array.isArray(prop.anyOf) && prop.anyOf.length > 0) for (const branch of prop.anyOf) {
381
+ const z1 = jsonSchemaPropertyToZod(branch);
382
+ if (z1) return applyDescription(z1, prop);
383
+ }
384
+ if (Array.isArray(prop.allOf) && prop.allOf.length > 0) for (let i = prop.allOf.length - 1; i >= 0; i--) {
385
+ const branch = prop.allOf[i];
386
+ const z1 = jsonSchemaPropertyToZod(branch);
387
+ if (z1) return applyDescription(z1, prop);
388
+ }
389
+ if (Array.isArray(prop.enum) && prop.enum.length > 0) {
390
+ const stringValues = prop.enum.filter((v) => typeof v === "string");
391
+ if (stringValues.length > 0) return applyDescription(z.enum(stringValues), prop);
392
+ return applyDescription(z.number(), prop);
393
+ }
394
+ const typeCandidates = pickEffectiveType(prop.type);
395
+ for (const t of typeCandidates) switch (t) {
396
+ case "string": return applyStringConstraints(z.string(), prop);
397
+ case "number":
398
+ case "integer": return applyNumberConstraints(z.number(), prop);
399
+ case "boolean": return applyDescription(z.boolean(), prop);
400
+ case "array": return applyDescription(arrayToZod(prop), prop);
401
+ case "object": return applyDescription(objectToZod(prop), prop);
402
+ case "null": continue;
403
+ default: break;
404
+ }
405
+ return applyDescription(z.unknown(), prop);
406
+ }
407
+ function pickEffectiveType(rawType) {
408
+ if (!rawType) return [];
409
+ if (Array.isArray(rawType)) return rawType.filter((t) => t !== "null");
410
+ return rawType === "null" ? [] : [rawType];
411
+ }
412
+ function arrayToZod(prop) {
413
+ const items = prop.items;
414
+ if (items && typeof items === "object") {
415
+ const itemZod = jsonSchemaPropertyToZod(items);
416
+ if (itemZod) return z.array(itemZod);
417
+ }
418
+ return z.array(z.unknown());
419
+ }
420
+ function objectToZod(prop) {
421
+ if (prop.properties && typeof prop.properties === "object") {
422
+ const requiredSet = new Set(prop.required ?? []);
423
+ const innerShape = {};
424
+ for (const [k, v] of Object.entries(prop.properties)) {
425
+ if (!v || typeof v !== "object") continue;
426
+ const inner = jsonSchemaPropertyToZod(v);
427
+ if (!inner) continue;
428
+ innerShape[k] = requiredSet.has(k) ? inner : inner.optional();
429
+ }
430
+ if (Object.keys(innerShape).length > 0) return z.object(innerShape);
431
+ }
432
+ return z.record(z.string(), z.unknown());
433
+ }
434
+ function applyStringConstraints(base, prop) {
435
+ let s = base;
436
+ if (typeof prop.minLength === "number") s = s.min(prop.minLength);
437
+ if (typeof prop.maxLength === "number") s = s.max(prop.maxLength);
438
+ if (typeof prop.pattern === "string") try {
439
+ s = s.regex(new RegExp(prop.pattern));
440
+ } catch {}
441
+ if (prop.format === "email") s = s.email();
442
+ if (prop.format === "uuid") s = s.uuid();
443
+ if (prop.format === "uri" || prop.format === "url") s = s.url();
444
+ return applyDescription(s, prop);
445
+ }
446
+ function applyNumberConstraints(base, prop) {
447
+ let n = base;
448
+ if (typeof prop.minimum === "number") n = n.min(prop.minimum);
449
+ if (typeof prop.maximum === "number") n = n.max(prop.maximum);
450
+ return applyDescription(n, prop);
451
+ }
452
+ function applyDescription(zodType, prop) {
453
+ if (typeof prop.description === "string" && prop.description.length > 0) return zodType.describe(prop.description);
454
+ return zodType;
455
+ }
456
+ //#endregion
317
457
  //#region src/integrations/mcp/resourceToTools.ts
318
458
  /**
319
459
  * @classytic/arc — Resource → MCP Tools Generator
@@ -359,9 +499,11 @@ const ANNOTATIONS = {
359
499
  function resourceToTools(resource, config = {}) {
360
500
  const controller = resource.controller ?? (resource.adapter ? createMcpController(resource) : void 0);
361
501
  if (!controller) return [];
362
- const fieldRules = resource.schemaOptions?.fieldRules;
502
+ const explicitFieldRules = resource.schemaOptions?.fieldRules;
363
503
  const hiddenFields = resource.schemaOptions?.hiddenFields;
364
504
  const readonlyFields = resource.schemaOptions?.readonlyFields;
505
+ const adapterBodies = explicitFieldRules ? void 0 : getAdapterBodies(resource);
506
+ const fieldRules = explicitFieldRules ?? deriveFieldRulesFromAdapter(resource);
365
507
  const filterableFields = resource.schemaOptions?.filterableFields ?? resource.queryParser?.allowedFilterFields;
366
508
  const sortableFields = resource.queryParser?.allowedSortFields;
367
509
  const allowedOperators = resource.queryParser?.allowedOperators;
@@ -388,7 +530,8 @@ function resourceToTools(resource, config = {}) {
388
530
  readonlyFields,
389
531
  extraHideFields: config.hideFields,
390
532
  filterableFields,
391
- allowedOperators
533
+ allowedOperators,
534
+ adapterBodies
392
535
  }),
393
536
  handler: createHandler(op, controller, resource.name, resource.permissions)
394
537
  });
@@ -442,20 +585,57 @@ function buildInputSchema(op, fieldRules, opts) {
442
585
  ...opts
443
586
  });
444
587
  case "get": return { id: z.string().describe("Resource ID") };
445
- case "create": return fieldRulesToZod(fieldRules, {
446
- mode: "create",
447
- ...opts
448
- });
449
- case "update": return {
450
- id: z.string().describe("Resource ID"),
451
- ...fieldRulesToZod(fieldRules, {
452
- mode: "update",
588
+ case "create":
589
+ if (!fieldRules && opts.adapterBodies?.createBody) {
590
+ const shape = jsonSchemaToZodShape(opts.adapterBodies.createBody, "create");
591
+ if (shape) return shape;
592
+ }
593
+ return fieldRulesToZod(fieldRules, {
594
+ mode: "create",
453
595
  ...opts
454
- })
455
- };
596
+ });
597
+ case "update": {
598
+ const idShape = { id: z.string().describe("Resource ID") };
599
+ if (!fieldRules && opts.adapterBodies?.updateBody) {
600
+ const shape = jsonSchemaToZodShape(opts.adapterBodies.updateBody, "update");
601
+ if (shape) return {
602
+ ...idShape,
603
+ ...shape
604
+ };
605
+ }
606
+ return {
607
+ ...idShape,
608
+ ...fieldRulesToZod(fieldRules, {
609
+ mode: "update",
610
+ ...opts
611
+ })
612
+ };
613
+ }
456
614
  case "delete": return { id: z.string().describe("Resource ID") };
457
615
  }
458
616
  }
617
+ /**
618
+ * Pull the adapter's `createBody` / `updateBody` schemas, if any.
619
+ * Returns `undefined` when the adapter doesn't generate schemas or throws.
620
+ */
621
+ function getAdapterBodies(resource) {
622
+ const adapter = resource.adapter;
623
+ if (!adapter || typeof adapter.generateSchemas !== "function") return void 0;
624
+ try {
625
+ const generated = adapter.generateSchemas(resource.schemaOptions, {
626
+ idField: resource.idField,
627
+ resourceName: resource.name
628
+ });
629
+ if (!generated || typeof generated !== "object") return void 0;
630
+ const schemas = generated;
631
+ return {
632
+ createBody: schemas.createBody,
633
+ updateBody: schemas.updateBody
634
+ };
635
+ } catch {
636
+ return;
637
+ }
638
+ }
459
639
  function createHandler(op, controller, resourceName, permissions) {
460
640
  const ctrl = controller;
461
641
  return async (input, ctx) => {
@@ -550,6 +730,63 @@ async function evaluatePermission(check, session, resource, action, input) {
550
730
  if (!permResult.granted) return false;
551
731
  return permResult.filters ?? null;
552
732
  }
733
+ /**
734
+ * Derive a fieldRules-shaped object from the adapter's auto-generated body
735
+ * schemas. Used as a fallback when the resource doesn't supply explicit
736
+ * fieldRules — this lets MCP create/update tools accept the same body fields
737
+ * that the REST routes already accept.
738
+ *
739
+ * Returns `undefined` if no usable schema can be extracted, in which case
740
+ * `fieldRulesToZod` falls back to its own behavior (empty shape).
741
+ */
742
+ function deriveFieldRulesFromAdapter(resource) {
743
+ const adapter = resource.adapter;
744
+ if (!adapter || typeof adapter.generateSchemas !== "function") return void 0;
745
+ let generated;
746
+ try {
747
+ generated = adapter.generateSchemas(resource.schemaOptions, {
748
+ idField: resource.idField,
749
+ resourceName: resource.name
750
+ });
751
+ } catch {
752
+ return;
753
+ }
754
+ if (!generated || typeof generated !== "object") return void 0;
755
+ const schemas = generated;
756
+ const createBody = schemas.createBody;
757
+ const updateBody = schemas.updateBody;
758
+ const properties = createBody?.properties ?? updateBody?.properties;
759
+ if (!properties || typeof properties !== "object") return void 0;
760
+ const requiredSet = new Set(createBody?.required ?? []);
761
+ const rules = {};
762
+ for (const [name, propSchema] of Object.entries(properties)) {
763
+ if (!propSchema || typeof propSchema !== "object") continue;
764
+ const prop = propSchema;
765
+ const rawType = prop.type;
766
+ const rule = { type: mapJsonSchemaTypeToArcType((Array.isArray(rawType) ? rawType.filter((t) => typeof t === "string") : typeof rawType === "string" ? [rawType] : [])[0]) };
767
+ if (requiredSet.has(name)) rule.required = true;
768
+ if (typeof prop.description === "string") rule.description = prop.description;
769
+ if (Array.isArray(prop.enum)) rule.enum = prop.enum.filter((v) => typeof v === "string");
770
+ if (typeof prop.minLength === "number") rule.minLength = prop.minLength;
771
+ if (typeof prop.maxLength === "number") rule.maxLength = prop.maxLength;
772
+ if (typeof prop.minimum === "number") rule.min = prop.minimum;
773
+ if (typeof prop.maximum === "number") rule.max = prop.maximum;
774
+ if (typeof prop.pattern === "string") rule.pattern = prop.pattern;
775
+ rules[name] = rule;
776
+ }
777
+ return Object.keys(rules).length > 0 ? rules : void 0;
778
+ }
779
+ function mapJsonSchemaTypeToArcType(jsonType) {
780
+ switch (jsonType) {
781
+ case "string": return "string";
782
+ case "number":
783
+ case "integer": return "number";
784
+ case "boolean": return "boolean";
785
+ case "array": return "array";
786
+ case "object": return "object";
787
+ default: return "string";
788
+ }
789
+ }
553
790
  function toCallToolResult(result) {
554
791
  if (!result.success) return {
555
792
  content: [{
@@ -1,5 +1,5 @@
1
- import { Bt as ResourceDefinition, Ht as CrudRepository, l as AnyRecord } from "../interface-DDW43OmS.mjs";
2
- import { r as CreateAppOptions } from "../types-D5hJ-k_3.mjs";
1
+ import { Ut as CrudRepository, Vt as ResourceDefinition, u as AnyRecord } from "../interface-DYH8AXGe.mjs";
2
+ import { d as ResourceLike, r as CreateAppOptions } from "../types-By-5mIfn.mjs";
3
3
  import Fastify, { FastifyInstance, FastifyServerOptions } from "fastify";
4
4
  import { Connection } from "mongoose";
5
5
  import { Mock } from "vitest";
@@ -572,6 +572,29 @@ declare function createTestTimer(): {
572
572
  reset: () => void;
573
573
  };
574
574
  //#endregion
575
+ //#region src/testing/preloadResources.d.ts
576
+ /** Eager glob result: `{ '/path/to/file.ts': resourceModule }` */
577
+ type EagerGlobResult = Record<string, unknown>;
578
+ /** Lazy glob result: `{ '/path/to/file.ts': () => Promise<unknown> }` */
579
+ type LazyGlobResult = Record<string, () => Promise<unknown>>;
580
+ /**
581
+ * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
582
+ *
583
+ * Accepts either:
584
+ * - `{ import: 'default' }` form: values are the resource directly
585
+ * - default form: values are the full module — picks first export with `toPlugin()`
586
+ *
587
+ * Throws if any module doesn't yield a valid `ResourceLike`.
588
+ */
589
+ declare function preloadResources(globResult: EagerGlobResult): ResourceLike[];
590
+ /**
591
+ * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
592
+ *
593
+ * Use this when resources depend on prior bootstrap (e.g., engine init) and
594
+ * cannot be evaluated at import time of the preload file.
595
+ */
596
+ declare function preloadResourcesAsync(globResult: LazyGlobResult): Promise<ResourceLike[]>;
597
+ //#endregion
575
598
  //#region src/testing/TestHarness.d.ts
576
599
  /**
577
600
  * Test fixtures for a resource
@@ -898,4 +921,4 @@ declare class TestDataLoader {
898
921
  cleanup(): Promise<void>;
899
922
  }
900
923
  //#endregion
901
- export { type AuthProvider, type AuthResponse, type BetterAuthTestHelpers, type BetterAuthTestHelpersOptions, type CreateTestAppOptions, DatabaseSnapshot, TestFixtures as DbTestFixtures, type GenerateTestFileOptions, HttpTestHarness, type HttpTestHarnessOptions, InMemoryDatabase, type OrgResponse, type SetupBetterAuthOrgOptions, type SetupUserConfig, type TestAppResult, TestDataLoader, TestDatabase, type TestFixtures$1 as TestFixtures, TestHarness, type TestHarnessOptions, type TestOrgContext, TestRequestBuilder, TestSeeder, TestTransaction, type TestUserContext, createBetterAuthProvider, createBetterAuthTestHelpers, createConfigTestSuite, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSnapshotMatcher, createSpy, createTestApp, createTestAuth, createTestHarness, createTestTimer, generateTestFile, request, safeParseBody, setupBetterAuthOrg, waitFor, withTestDb };
924
+ export { type AuthProvider, type AuthResponse, type BetterAuthTestHelpers, type BetterAuthTestHelpersOptions, type CreateTestAppOptions, DatabaseSnapshot, TestFixtures as DbTestFixtures, type GenerateTestFileOptions, HttpTestHarness, type HttpTestHarnessOptions, InMemoryDatabase, type OrgResponse, type SetupBetterAuthOrgOptions, type SetupUserConfig, type TestAppResult, TestDataLoader, TestDatabase, type TestFixtures$1 as TestFixtures, TestHarness, type TestHarnessOptions, type TestOrgContext, TestRequestBuilder, TestSeeder, TestTransaction, type TestUserContext, createBetterAuthProvider, createBetterAuthTestHelpers, createConfigTestSuite, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSnapshotMatcher, createSpy, createTestApp, createTestAuth, createTestHarness, createTestTimer, generateTestFile, preloadResources, preloadResourcesAsync, request, safeParseBody, setupBetterAuthOrg, waitFor, withTestDb };
@@ -1066,6 +1066,50 @@ function createTestTimer() {
1066
1066
  };
1067
1067
  }
1068
1068
  //#endregion
1069
+ //#region src/testing/preloadResources.ts
1070
+ /**
1071
+ * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
1072
+ *
1073
+ * Accepts either:
1074
+ * - `{ import: 'default' }` form: values are the resource directly
1075
+ * - default form: values are the full module — picks first export with `toPlugin()`
1076
+ *
1077
+ * Throws if any module doesn't yield a valid `ResourceLike`.
1078
+ */
1079
+ function preloadResources(globResult) {
1080
+ const resources = [];
1081
+ for (const [path, value] of Object.entries(globResult)) {
1082
+ const resource = pickResource(value);
1083
+ if (!resource) throw new Error(`preloadResources: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
1084
+ resources.push(resource);
1085
+ }
1086
+ return resources.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
1087
+ }
1088
+ /**
1089
+ * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
1090
+ *
1091
+ * Use this when resources depend on prior bootstrap (e.g., engine init) and
1092
+ * cannot be evaluated at import time of the preload file.
1093
+ */
1094
+ async function preloadResourcesAsync(globResult) {
1095
+ return (await Promise.all(Object.entries(globResult).map(async ([path, loader]) => {
1096
+ const resource = pickResource(await loader());
1097
+ if (!resource) throw new Error(`preloadResourcesAsync: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
1098
+ return resource;
1099
+ }))).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
1100
+ }
1101
+ function pickResource(value) {
1102
+ if (!value || typeof value !== "object") return void 0;
1103
+ if (typeof value.toPlugin === "function") return value;
1104
+ const mod = value;
1105
+ const candidates = [
1106
+ mod.default,
1107
+ mod.resource,
1108
+ ...Object.values(mod)
1109
+ ];
1110
+ for (const c of candidates) if (c && typeof c === "object" && typeof c.toPlugin === "function") return c;
1111
+ }
1112
+ //#endregion
1069
1113
  //#region src/testing/TestHarness.ts
1070
1114
  /**
1071
1115
  * Resource Test Harness
@@ -1752,7 +1796,7 @@ function runEventTests(resourceName, displayName, events) {
1752
1796
  * ```
1753
1797
  */
1754
1798
  async function createTestApp(options = {}) {
1755
- const { createApp } = await import("../createApp-Bol7DLUf.mjs").then((n) => n.r);
1799
+ const { createApp } = await import("../createApp-D2w0LdYJ.mjs").then((n) => n.r);
1756
1800
  const { useInMemoryDb = true, mongoUri: providedMongoUri, ...appOptions } = options;
1757
1801
  const defaultAuth = {
1758
1802
  type: "jwt",
@@ -1950,4 +1994,4 @@ var TestDataLoader = class {
1950
1994
  }
1951
1995
  };
1952
1996
  //#endregion
1953
- export { DatabaseSnapshot, TestFixtures as DbTestFixtures, HttpTestHarness, InMemoryDatabase, TestDataLoader, TestDatabase, TestHarness, TestRequestBuilder, TestSeeder, TestTransaction, createBetterAuthProvider, createBetterAuthTestHelpers, createConfigTestSuite, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSnapshotMatcher, createSpy, createTestApp, createTestAuth, createTestHarness, createTestTimer, generateTestFile, request, safeParseBody, setupBetterAuthOrg, waitFor, withTestDb };
1997
+ export { DatabaseSnapshot, TestFixtures as DbTestFixtures, HttpTestHarness, InMemoryDatabase, TestDataLoader, TestDatabase, TestHarness, TestRequestBuilder, TestSeeder, TestTransaction, createBetterAuthProvider, createBetterAuthTestHelpers, createConfigTestSuite, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSnapshotMatcher, createSpy, createTestApp, createTestAuth, createTestHarness, createTestTimer, generateTestFile, preloadResources, preloadResourcesAsync, request, safeParseBody, setupBetterAuthOrg, waitFor, withTestDb };
@@ -1,4 +1,4 @@
1
1
  import { _ as isMember, a as AUTHENTICATED_SCOPE, d as getTeamId, g as isElevated, h as isAuthenticated, l as getOrgId, m as hasOrgAccess, n as ElevationOptions, o as PUBLIC_SCOPE, s as RequestScope, t as ElevationEvent, u as getOrgRoles } from "../elevation-C_taLQrM.mjs";
2
- import { $ as RegistryEntry, A as GracefulShutdownOptions, B as LookupOption, C as CrudSchemas, D as FastifyWithAuth, E as FastifyRequestExtras, F as InferResourceDoc, Ft as IControllerResponse, G as OwnershipCheck, Gt as PaginationParams, H as MiddlewareHandler, Ht as CrudRepository, I as IntrospectionData, It as IRequestContext, J as PresetFunction, K as ParsedQuery, Kt as QueryOptions, L as IntrospectionPluginOptions, Lt as RouteHandler, M as HealthOptions, Mt as ControllerLike, N as InferAdapterDoc, Nt as FastifyHandler, O as FastifyWithDecorators, P as InferDocType, Pt as IController, Q as RateLimitConfig, R as JWTPayload, S as CrudRouterOptions, St as getUserId, T as EventsDecorator, U as ObjectId, Ut as InferDoc, V as MiddlewareConfig, W as OpenApiSchemas, Wt as PaginatedResult, X as PresetResult, Y as PresetHook, Z as QueryParserInterface, _ as AuthenticatorContext, _t as UserLike, at as ResourceConfig, b as CrudController, bt as ValidationResult, c as AdditionalRoute, ct as ResourceMetadata, d as ArcDecorator, dt as RouteSchemaOptions, et as RegistryStats, f as ArcInternalMetadata, ft as ServiceContext, g as Authenticator, gt as TypedResourceConfig, h as AuthPluginOptions, ht as TypedRepository, it as ResourceCacheConfig, j as HealthCheck, jt as ControllerHandler, k as FieldRule, l as AnyRecord, lt as ResourcePermissions, m as AuthHelpers, mt as TypedController, nt as RequestIdOptions, ot as ResourceHookContext, p as ArcRequest, pt as TokenPair, q as PopulateOption, rt as RequestWithExtras, st as ResourceHooks, tt as RequestContext, u as ApiResponse, ut as RouteHandlerMethod, v as ConfigError, vt as UserOrganization, w as EventDefinition, wt as BaseControllerOptions, x as CrudRouteKey, xt as envelope, y as ControllerQueryOptions, yt as ValidateOptions, z as JwtContext } from "../interface-DDW43OmS.mjs";
2
+ import { $ as RateLimitConfig, A as FieldRule, B as JwtContext, C as CrudRouterOptions, Ct as getUserId, D as FastifyRequestExtras, E as EventsDecorator, F as InferDocType, Ft as IController, G as OpenApiSchemas, Gt as PaginatedResult, H as MiddlewareConfig, I as InferResourceDoc, It as IControllerResponse, J as PopulateOption, K as OwnershipCheck, Kt as PaginationParams, L as IntrospectionData, Lt as IRequestContext, M as HealthCheck, Mt as ControllerHandler, N as HealthOptions, Nt as ControllerLike, O as FastifyWithAuth, P as InferAdapterDoc, Pt as FastifyHandler, Q as QueryParserInterface, R as IntrospectionPluginOptions, Rt as RouteHandler, S as CrudRouteKey, St as envelope, T as EventDefinition, Tt as BaseControllerOptions, U as MiddlewareHandler, Ut as CrudRepository, V as LookupOption, W as ObjectId, Wt as InferDoc, X as PresetHook, Y as PresetFunction, Z as PresetResult, _ as Authenticator, _t as TypedResourceConfig, at as ResourceCacheConfig, b as ControllerQueryOptions, bt as ValidateOptions, ct as ResourceHooks, d as ApiResponse, dt as RouteHandlerMethod, et as RegistryEntry, f as ArcDecorator, ft as RouteSchemaOptions, g as AuthPluginOptions, gt as TypedRepository, h as AuthHelpers, ht as TypedController, it as RequestWithExtras, j as GracefulShutdownOptions, k as FastifyWithDecorators, l as AdditionalRoute, lt as ResourceMetadata, m as ArcRequest, mt as TokenPair, nt as RequestContext, ot as ResourceConfig, p as ArcInternalMetadata, pt as ServiceContext, q as ParsedQuery, qt as QueryOptions, rt as RequestIdOptions, st as ResourceHookContext, tt as RegistryStats, u as AnyRecord, ut as ResourcePermissions, v as AuthenticatorContext, vt as UserLike, w as CrudSchemas, x as CrudController, xt as ValidationResult, y as ConfigError, yt as UserOrganization, z as JWTPayload } from "../interface-DYH8AXGe.mjs";
3
3
  import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-BNUccdcf.mjs";
4
4
  export { AUTHENTICATED_SCOPE, AdditionalRoute, AnyRecord, ApiResponse, ArcDecorator, ArcInternalMetadata, ArcRequest, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRepository, CrudRouteKey, CrudRouterOptions, CrudSchemas, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OpenApiSchemas, OwnershipCheck, PUBLIC_SCOPE, PaginatedResult, PaginationParams, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryOptions, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResourceCacheConfig, ResourceConfig, ResourceHookContext, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteHandler, RouteHandlerMethod, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult, envelope, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
@@ -1,4 +1,4 @@
1
- import { Bt as ResourceDefinition } from "./interface-DDW43OmS.mjs";
1
+ import { Vt as ResourceDefinition } from "./interface-DYH8AXGe.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/integrations/mcp/types.d.ts
@@ -1,5 +1,5 @@
1
1
  import { n as ElevationOptions } from "./elevation-C_taLQrM.mjs";
2
- import { g as Authenticator } from "./interface-DDW43OmS.mjs";
2
+ import { _ as Authenticator } from "./interface-DYH8AXGe.mjs";
3
3
  import { t as ExternalOpenApiPaths } from "./externalPaths-DpO-s7r8.mjs";
4
4
  import { i as CacheStore } from "./interface-D_BWALyZ.mjs";
5
5
  import { r as QueryCachePluginOptions } from "./queryCachePlugin-DcmETvcB.mjs";
@@ -52,6 +52,12 @@ import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, Fast
52
52
  * // Minimal resource (plain object)
53
53
  * const simple: ResourceLike = { name: 'ping', toPlugin: () => () => {} };
54
54
  * ```
55
+ *
56
+ * **DO NOT add an index signature** (`[key: string]: unknown`) to this interface.
57
+ * Class instances (like `ResourceDefinition`) don't implicitly carry index signatures,
58
+ * so adding one here makes `ResourceDefinition` *unassignable* to `ResourceLike` —
59
+ * the exact opposite of the intent. TypeScript's structural typing already allows
60
+ * classes with extra properties to satisfy this interface without an index signature.
55
61
  */
56
62
  interface ResourceLike {
57
63
  /** Plugin factory — called by createApp to register routes */
@@ -1,4 +1,4 @@
1
- import { K as ParsedQuery, W as OpenApiSchemas, Z as QueryParserInterface, l as AnyRecord } from "../interface-DDW43OmS.mjs";
1
+ import { G as OpenApiSchemas, Q as QueryParserInterface, q as ParsedQuery, u as AnyRecord } from "../interface-DYH8AXGe.mjs";
2
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-CcVbl1-T.mjs";
3
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-JP2GdJ4b.mjs";
4
4
  import { FastifyInstance } from "fastify";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.6.1",
3
+ "version": "2.6.3",
4
4
  "description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -222,31 +222,31 @@
222
222
  "peerDependencies": {
223
223
  "@classytic/mongokit": ">=3.5.0",
224
224
  "@classytic/streamline": ">=2.0.0",
225
- "@fastify/cors": "^11.0.0",
226
- "@fastify/helmet": "^13.0.0",
227
- "@fastify/jwt": "^10.0.0",
228
- "@fastify/multipart": "^9.0.0",
229
- "@fastify/rate-limit": "^10.0.0",
230
- "@fastify/sensible": "^6.0.0",
231
- "@fastify/type-provider-typebox": "^6.0.0",
232
- "@fastify/under-pressure": "^9.0.0",
233
- "@fastify/websocket": "^11.0.0",
225
+ "@fastify/cors": ">=11.0.0",
226
+ "@fastify/helmet": ">=13.0.0",
227
+ "@fastify/jwt": ">=10.0.0",
228
+ "@fastify/multipart": ">=9.0.0",
229
+ "@fastify/rate-limit": ">=10.0.0",
230
+ "@fastify/sensible": ">=6.0.0",
231
+ "@fastify/type-provider-typebox": ">=6.0.0",
232
+ "@fastify/under-pressure": ">=9.0.0",
233
+ "@fastify/websocket": ">=11.0.0",
234
234
  "@modelcontextprotocol/sdk": ">=1.28.0",
235
235
  "@opentelemetry/auto-instrumentations-node": ">=0.40.0",
236
236
  "@opentelemetry/exporter-trace-otlp-http": ">=0.50.0",
237
237
  "@opentelemetry/instrumentation-http": ">=0.50.0",
238
238
  "@opentelemetry/instrumentation-mongodb": ">=0.40.0",
239
239
  "@opentelemetry/sdk-node": ">=0.50.0",
240
- "@sinclair/typebox": "^0.34.0",
240
+ "@sinclair/typebox": ">=0.34.0",
241
241
  "better-auth": ">=1.5.5",
242
- "bullmq": "^5.0.0",
243
- "fastify": "^5.7.4",
244
- "fastify-raw-body": "^5.0.0",
245
- "ioredis": "^5.0.0",
246
- "mongodb": "^6.0.0 || ^7.0.0",
242
+ "bullmq": ">=5.0.0",
243
+ "fastify": ">=5.0.0",
244
+ "fastify-raw-body": ">=5.0.0",
245
+ "ioredis": ">=5.0.0",
246
+ "mongodb": ">=6.0.0",
247
247
  "mongoose": ">=9.0.0",
248
- "pino-pretty": "^13.0.0",
249
- "zod": "^4.0.0"
248
+ "pino-pretty": ">=13.0.0",
249
+ "zod": ">=4.0.0"
250
250
  },
251
251
  "peerDependenciesMeta": {
252
252
  "@classytic/mongokit": {