@classytic/arc 2.19.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +1 -1
  2. package/bin/arc.js +37 -0
  3. package/dist/{BaseController-Dy6EQHjM.mjs → BaseController-BnLYgE9A.mjs} +4 -4
  4. package/dist/{EventTransport-IEn7Tn-Q.d.mts → EventTransport-QmY7A258.d.mts} +0 -1
  5. package/dist/{QueryCache-TRfXau3W.d.mts → QueryCache-Z1B38koh.d.mts} +0 -1
  6. package/dist/{ResourceRegistry-5fKZwhK_.mjs → ResourceRegistry-7RnrEWNP.mjs} +1 -1
  7. package/dist/{actionPermissions-D7NGwcAR.mjs → actionPermissions-ze70xXJi.mjs} +1 -1
  8. package/dist/audit/index.d.mts +9 -5
  9. package/dist/audit/index.mjs +1 -1
  10. package/dist/auth/audit.d.mts +7 -4
  11. package/dist/auth/audit.mjs +5 -0
  12. package/dist/auth/index.d.mts +7 -14
  13. package/dist/auth/index.mjs +7 -5
  14. package/dist/auth/redis-session.d.mts +1 -2
  15. package/dist/{buildHandler-B0qvAguf.mjs → buildHandler-D_2ULWxZ.mjs} +3 -3
  16. package/dist/cache/index.d.mts +2 -3
  17. package/dist/cache/index.mjs +2 -2
  18. package/dist/cli/commands/describe.d.mts +1 -2
  19. package/dist/cli/commands/describe.mjs +2 -2
  20. package/dist/cli/commands/docs.mjs +2 -2
  21. package/dist/cli/commands/generate.mjs +1 -1
  22. package/dist/cli/commands/init.mjs +1 -1
  23. package/dist/cli/commands/introspect.mjs +4 -13
  24. package/dist/cli/index.mjs +2 -2
  25. package/dist/{constants-gaEeMlz-.mjs → constants-DNre0wRg.mjs} +1 -1
  26. package/dist/context/index.d.mts +2 -2
  27. package/dist/core/index.d.mts +3 -3
  28. package/dist/core/index.mjs +5 -5
  29. package/dist/{core-Ba-6Sue6.mjs → core-DZ6Xe1jR.mjs} +182 -55
  30. package/dist/{createActionRouter-B_WzBFyf.mjs → createActionRouter-BWMDW-dL.mjs} +4 -4
  31. package/dist/{createAggregationRouter-B_G_nIjm.mjs → createAggregationRouter-l5mNdYrZ.mjs} +5 -4
  32. package/dist/{createApp--703hQE1.mjs → createApp-Bi61XxqU.mjs} +229 -24
  33. package/dist/discovery/index.d.mts +0 -1
  34. package/dist/docs/index.d.mts +1 -2
  35. package/dist/docs/index.mjs +1 -1
  36. package/dist/{elevation-xGUBq6p_.d.mts → elevation-C9s5hFP_.d.mts} +0 -1
  37. package/dist/encryption/index.d.mts +2 -144
  38. package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-B0e-bq-O.d.mts} +0 -1
  39. package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
  40. package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
  41. package/dist/events/index.d.mts +9 -6
  42. package/dist/events/index.mjs +9 -6
  43. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  44. package/dist/events/transports/redis.d.mts +1 -2
  45. package/dist/factory/index.d.mts +5 -4
  46. package/dist/factory/index.mjs +3 -3
  47. package/dist/{fastify-augmentation-D-5_yQJi.d.mts → fastify-augmentation-DafigGQp.d.mts} +12 -2
  48. package/dist/{fields-C7ahdGYf.d.mts → fields-BGqrAa_u.d.mts} +1 -2
  49. package/dist/{generate-Brans3fW.mjs → generate-usRSdQmP.mjs} +4 -3
  50. package/dist/hooks/index.d.mts +1 -1
  51. package/dist/idempotency/index.d.mts +4 -3
  52. package/dist/idempotency/index.mjs +1 -1
  53. package/dist/idempotency/redis.d.mts +1 -1
  54. package/dist/{index-CP6A1Ffj.d.mts → index-B5N-NaHs.d.mts} +88 -7
  55. package/dist/{index-CRKiiP5o.d.mts → index-B5PoNbfu.d.mts} +41 -7
  56. package/dist/{index-CUTG2Cuo.d.mts → index-Dtrj6XZk.d.mts} +90 -29
  57. package/dist/index.d.mts +7 -7
  58. package/dist/index.mjs +7 -7
  59. package/dist/{init-Bqpt1Hxf.mjs → init-0YhHkXGk.mjs} +239 -140
  60. package/dist/integrations/event-gateway.d.mts +2 -3
  61. package/dist/integrations/index.d.mts +2 -2
  62. package/dist/integrations/jobs.d.mts +0 -1
  63. package/dist/integrations/mcp/index.d.mts +6 -7
  64. package/dist/integrations/mcp/index.mjs +1 -1
  65. package/dist/integrations/mcp/testing.d.mts +1 -2
  66. package/dist/integrations/mcp/testing.mjs +1 -1
  67. package/dist/integrations/streamline.d.mts +19 -7
  68. package/dist/integrations/streamline.mjs +9 -5
  69. package/dist/integrations/webhooks.d.mts +0 -1
  70. package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
  71. package/dist/integrations/websocket-redis.d.mts +1 -2
  72. package/dist/integrations/websocket.d.mts +2 -2
  73. package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
  74. package/dist/{loadResourcesFromEntry-BTlFEx05.mjs → loadResourcesFromEntry-OMCvIEE7.mjs} +1 -1
  75. package/dist/{mcpPlugin-C088Nd-G.mjs → mcpPlugin-B9XjjNVG.mjs} +148 -49
  76. package/dist/middleware/index.d.mts +1 -2
  77. package/dist/middleware/index.mjs +1 -1
  78. package/dist/module-WeuU5jfd.d.mts +429 -0
  79. package/dist/{openapi-DZ-5a5Op.mjs → openapi-Kyod_di6.mjs} +3 -3
  80. package/dist/permissions/index.d.mts +3 -3
  81. package/dist/permissions/index.mjs +2 -2
  82. package/dist/{permissions-BPB1xqGf.mjs → permissions-BJMuKMl7.mjs} +43 -3
  83. package/dist/pipeline/index.d.mts +1 -2
  84. package/dist/plugins/index.d.mts +87 -10
  85. package/dist/plugins/index.mjs +91 -4
  86. package/dist/plugins/response-cache.d.mts +5 -3
  87. package/dist/plugins/tracing-entry.d.mts +1 -1
  88. package/dist/plugins/tracing-entry.mjs +1 -1
  89. package/dist/{pluralize-DccFV9R7.mjs → pluralize-BhrgwZb9.mjs} +3 -3
  90. package/dist/presets/filesUpload.d.mts +4 -5
  91. package/dist/presets/filesUpload.mjs +1 -1
  92. package/dist/presets/index.d.mts +16 -3
  93. package/dist/presets/index.mjs +1 -1
  94. package/dist/presets/multiTenant.d.mts +1 -2
  95. package/dist/presets/multiTenant.mjs +1 -1
  96. package/dist/presets/search.d.mts +2 -3
  97. package/dist/presets/search.mjs +3 -2
  98. package/dist/{presets-BtyFwTO4.mjs → presets-gw0ciIUD.mjs} +1 -1
  99. package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
  100. package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
  101. package/dist/{queryCachePlugin-emHb0CuX.mjs → queryCachePlugin-DzqLN904.mjs} +2 -2
  102. package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
  103. package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
  104. package/dist/{registerRoute-BdV8DDwU.mjs → registerRoute-oGAl0oA8.mjs} +1 -1
  105. package/dist/registry/index.d.mts +2 -2
  106. package/dist/registry/index.mjs +2 -2
  107. package/dist/{registry-Bm9LjWKs.mjs → registry-vcOCtPij.mjs} +1 -1
  108. package/dist/{schemaIR-D1MJwqmp.mjs → schemaIR-ajetw3Fv.mjs} +1 -42
  109. package/dist/schemas/index.d.mts +0 -1
  110. package/dist/scim/index.d.mts +0 -1
  111. package/dist/scim/index.mjs +2 -2
  112. package/dist/scope/index.d.mts +2 -3
  113. package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
  114. package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
  115. package/dist/sync/changelog.d.mts +2 -0
  116. package/dist/sync/changelog.mjs +2 -0
  117. package/dist/testing/index.d.mts +2 -3
  118. package/dist/testing/index.mjs +6 -3
  119. package/dist/testing/storageContract.d.mts +1 -2
  120. package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
  121. package/dist/types/index.d.mts +4 -4
  122. package/dist/types/storage.d.mts +1 -1
  123. package/dist/types-Cc91nXuA.d.mts +132 -0
  124. package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
  125. package/dist/{types-DpvSF5Hh.d.mts → types-DRj-Ut4n.d.mts} +1 -2
  126. package/dist/{types-9_wtNPsI.d.mts → types-DpxoNX5v.d.mts} +112 -214
  127. package/dist/utils/index.d.mts +182 -128
  128. package/dist/utils/index.mjs +2 -2
  129. package/dist/{utils-C6QQ9-hd.mjs → utils-CO8a3B_1.mjs} +1435 -1391
  130. package/dist/{validate-CRcaWzp2.mjs → validate-C2mGgSXg.mjs} +3 -3
  131. package/dist/{versioning-D_eBCfNr.d.mts → versioning-DQ4mCvg0.d.mts} +1 -2
  132. package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
  133. package/package.json +18 -13
  134. package/skills/arc/SKILL.md +10 -0
  135. package/skills/arc/references/mcp.md +2 -0
  136. package/skills/arc-module-publishing/SKILL.md +242 -0
  137. package/skills/arc-module-publishing/references/authoring-modules.md +272 -0
  138. package/skills/arc-module-publishing/references/package-manifest.md +155 -0
  139. package/skills/arc-module-publishing/references/publishing-workflow.md +182 -0
@@ -1,6 +1,8 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { accessSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { accessSync, existsSync, readFileSync } from "node:fs";
4
6
  import * as readline from "node:readline";
5
7
  import { execSync, spawn } from "node:child_process";
6
8
  //#region src/cli/commands/init/templates/adapter.ts
@@ -15,7 +17,7 @@ function createAdapterTemplate(config) {
15
17
 
16
18
  import { createMongooseAdapter } from '@classytic/mongokit/adapter';
17
19
  import { buildCrudSchemasFromModel } from '@classytic/mongokit';
18
- ${ts ? "import type { Model } from 'mongoose';\nimport type { Repository } from '@classytic/mongokit';" : ""}
20
+ ${ts ? "import type { DataAdapter } from '@classytic/repo-core/adapter';\nimport type { Model } from 'mongoose';\nimport type { Repository } from '@classytic/mongokit';" : ""}
19
21
 
20
22
  /**
21
23
  * Create a MongoKit-powered adapter for a resource.
@@ -28,7 +30,10 @@ ${ts ? "import type { Model } from 'mongoose';\nimport type { Repository } from
28
30
  export function createAdapter${ts ? "<TDoc = unknown>" : ""}(
29
31
  model${ts ? ": Model<TDoc>" : ""},
30
32
  repository${ts ? ": Repository<TDoc>" : ""}
31
- ) {
33
+ )${ts ? ": DataAdapter<TDoc>" : ""} {
34
+ // Explicit return type keeps the declaration portable — under pnpm's
35
+ // non-hoisted layout the inferred type references mongokit's internal
36
+ // paths and tsc fails with TS2742/TS2883.
32
37
  return createMongooseAdapter({
33
38
  model,
34
39
  repository,
@@ -482,20 +487,13 @@ ${[
482
487
  ].filter(Boolean).join("\n")}
483
488
  import config from '#config/index.js';
484
489
 
485
- let _auth${ts ? ": ReturnType<typeof betterAuth> | null" : ""} = null;
486
-
487
- /**
488
- * Get the Better Auth instance (lazy singleton)
489
- *
490
- * Must be called AFTER database connection is established.
491
- */
492
- export function getAuth()${ts ? ": ReturnType<typeof betterAuth>" : ""} {
493
- if (process.env.NODE_ENV === 'production' && !process.env.BETTER_AUTH_SECRET) {
494
- throw new Error('BETTER_AUTH_SECRET is required in production (min 32 chars)');
495
- }
496
-
497
- if (!_auth) {
498
- _auth = betterAuth({
490
+ // Factory keeps the CONCRETE betterAuth({...}) instantiation as the source
491
+ // of truth for the instance type — annotating with the wide
492
+ // \`Auth<BetterAuthOptions>\` (aka \`ReturnType<typeof betterAuth>\`) does not
493
+ // typecheck because Better Auth's \`Auth<T>\` is generic over the exact
494
+ // options object.
495
+ function createAuth() {
496
+ return betterAuth({
499
497
  secret: config.betterAuth.secret,
500
498
  baseURL: process.env.BETTER_AUTH_URL || \`http://localhost:\${config.server.port}\`,
501
499
  basePath: '/api/auth',
@@ -543,6 +541,26 @@ ${orgPluginUsage}
543
541
  enabled: process.env.NODE_ENV === 'production',
544
542
  },
545
543
  });
544
+ }
545
+
546
+ let _auth${ts ? ": ReturnType<typeof createAuth> | null" : ""} = null;
547
+
548
+ /**
549
+ * Get the Better Auth instance (lazy singleton)
550
+ *
551
+ * Must be called AFTER database connection is established.
552
+ */
553
+ export function getAuth()${ts ? ": ReturnType<typeof createAuth>" : ""} {
554
+ if (process.env.NODE_ENV === 'production' && !process.env.BETTER_AUTH_SECRET) {
555
+ throw new Error('BETTER_AUTH_SECRET is required in production (min 32 chars)');
556
+ }
557
+
558
+ // Narrow through a local — assigning the module-level \`_auth\` directly
559
+ // would lose the non-null narrowing across the calls below.
560
+ let auth = _auth;
561
+ if (!auth) {
562
+ auth = createAuth();
563
+ _auth = auth;
546
564
  ${useStubs ? `
547
565
  // Register stub Mongoose models for Better Auth's collections so
548
566
  // \`.populate('user')\` / \`ref: 'organization'\` resolve against BA-owned
@@ -550,7 +568,7 @@ ${useStubs ? `
550
568
  registerBetterAuthStubs(mongoose, {${stubPluginsList ? ` plugins: [${stubPluginsList}]` : ""}${stubExtras} });
551
569
  ` : ""} }
552
570
 
553
- return _auth;
571
+ return auth;
554
572
  }
555
573
 
556
574
  export default getAuth;
@@ -690,6 +708,9 @@ export async function getUserProfile(request${ts ? ": FastifyRequest" : ""}, rep
690
708
  try {
691
709
  const requestUser = (request${ts ? " as unknown as { user?: { _id?: string; id?: string } }" : ""}).user;
692
710
  const userId = requestUser?._id || requestUser?.id;
711
+ if (!userId) {
712
+ return reply.code(401).send({ error: 'Authentication required', code: 'arc.unauthorized' });
713
+ }
693
714
  const user = await userRepository.getById(userId);
694
715
 
695
716
  if (!user) {
@@ -710,6 +731,9 @@ export async function updateUserProfile(request${ts ? ": FastifyRequest" : ""},
710
731
  try {
711
732
  const requestUser = (request${ts ? " as unknown as { user?: { _id?: string; id?: string } }" : ""}).user;
712
733
  const userId = requestUser?._id || requestUser?.id;
734
+ if (!userId) {
735
+ return reply.code(401).send({ error: 'Authentication required', code: 'arc.unauthorized' });
736
+ }
713
737
  const updates${ts ? ": Record<string, unknown>" : ""} = { ...request.body${ts ? " as Record<string, unknown>" : ""} };
714
738
 
715
739
  // Prevent updating protected fields — auth-managed only
@@ -838,20 +862,29 @@ function authTestTemplate(config) {
838
862
  */
839
863
 
840
864
  import { describe, it, expect, beforeAll, afterAll } from 'vitest';
841
- ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';\n" : ""}import { createAppInstance } from '../src/app.js';
865
+ ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';\nimport { MongoMemoryServer } from 'mongodb-memory-server';\n" : ""}import { createAppInstance } from '../src/app.js';
842
866
  ${ts ? "import type { FastifyInstance } from 'fastify';\n" : ""}
867
+ // Resources mount under the app's resourcePrefix (see src/app.${ts ? "ts" : "js"}).
868
+ const API = '/api';
869
+
843
870
  describe('Auth', () => {
844
871
  let app${ts ? ": FastifyInstance" : ""};
845
- const testUser = {
872
+ ${config.adapter === "mongokit" ? ` let mongo${ts ? ": MongoMemoryServer | undefined" : ""};\n` : ""} const testUser = {
846
873
  name: 'Test User',
847
874
  email: 'test@example.com',
848
875
  password: 'password123',
849
876
  };
850
877
 
851
878
  beforeAll(async () => {
852
- ${config.adapter === "mongokit" ? ` const testDbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}-test';
853
- await mongoose.connect(testDbUri);
854
- // Clean up test data
879
+ ${config.adapter === "mongokit" ? ` // In-memory MongoDB — no local mongod required. Set MONGODB_URI to use
880
+ // a real instance instead (e.g. a CI service container).
881
+ let uri = process.env.MONGODB_URI;
882
+ if (!uri) {
883
+ mongo = await MongoMemoryServer.create();
884
+ uri = mongo.getUri();
885
+ }
886
+ await mongoose.connect(uri);
887
+ // Clean up test data (matters when MONGODB_URI points at a shared DB)
855
888
  await mongoose.connection.collection('users').deleteMany({ email: testUser.email });
856
889
  ` : ""}
857
890
  app = await createAppInstance();
@@ -860,27 +893,29 @@ ${config.adapter === "mongokit" ? ` const testDbUri = process.env.MONGODB_URI
860
893
 
861
894
  afterAll(async () => {
862
895
  ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('users').deleteMany({ email: testUser.email });
896
+ await app.close();
863
897
  await mongoose.connection.close();
864
- ` : ""} await app.close();
865
- });
898
+ await mongo?.stop();
899
+ ` : ` await app.close();
900
+ `} });
866
901
 
867
902
  describe('POST /auth/register', () => {
868
903
  it('should register a new user', async () => {
869
904
  const response = await app.inject({
870
905
  method: 'POST',
871
- url: '/auth/register',
906
+ url: \`\${API}/auth/register\`,
872
907
  payload: testUser,
873
908
  });
874
909
 
875
910
  expect(response.statusCode).toBe(201);
876
911
  const body = JSON.parse(response.body);
877
- expect(body.success).toBe(true);
912
+ expect(body.message).toBeDefined();
878
913
  });
879
914
 
880
915
  it('should reject duplicate email', async () => {
881
916
  const response = await app.inject({
882
917
  method: 'POST',
883
- url: '/auth/register',
918
+ url: \`\${API}/auth/register\`,
884
919
  payload: testUser,
885
920
  });
886
921
 
@@ -892,13 +927,13 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
892
927
  it('should login with valid credentials', async () => {
893
928
  const response = await app.inject({
894
929
  method: 'POST',
895
- url: '/auth/login',
930
+ url: \`\${API}/auth/login\`,
896
931
  payload: { email: testUser.email, password: testUser.password },
897
932
  });
898
933
 
899
934
  expect(response.statusCode).toBe(200);
900
935
  const body = JSON.parse(response.body);
901
- expect(body.success).toBe(true);
936
+ expect(body.user).toBeDefined();
902
937
  expect(body.accessToken).toBeDefined();
903
938
  expect(body.refreshToken).toBeDefined();
904
939
  });
@@ -906,7 +941,7 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
906
941
  it('should reject invalid credentials', async () => {
907
942
  const response = await app.inject({
908
943
  method: 'POST',
909
- url: '/auth/login',
944
+ url: \`\${API}/auth/login\`,
910
945
  payload: { email: testUser.email, password: 'wrongpassword' },
911
946
  });
912
947
 
@@ -918,7 +953,7 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
918
953
  it('should require authentication', async () => {
919
954
  const response = await app.inject({
920
955
  method: 'GET',
921
- url: '/users/me',
956
+ url: \`\${API}/users/me\`,
922
957
  });
923
958
 
924
959
  expect(response.statusCode).toBe(401);
@@ -929,11 +964,32 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
929
964
  }
930
965
  //#endregion
931
966
  //#region src/cli/commands/init/dependency-plan.ts
967
+ /**
968
+ * Single source of truth for scaffolded project dependencies.
969
+ *
970
+ * Two layers keep the scaffold from ever contradicting the arc release
971
+ * that ships it:
972
+ *
973
+ * 1. The static table below — tracks published npm latest, bumped on
974
+ * release. Serves as the offline fallback.
975
+ * 2. A runtime overlay from arc's OWN package.json (version + declared
976
+ * peer floors). The 2.20.0 review found the static pins had drifted
977
+ * (`arc ^2.18.5` + `primitives ^0.6.0` while arc 2.20 peers demand
978
+ * >=0.9.0 → ERESOLVE on the first `arc init` a new user runs).
979
+ * Deriving from the installed package.json makes that class of
980
+ * breakage structurally impossible: the scaffold always pins the
981
+ * arc version actually running and ranges that satisfy its peers.
982
+ *
983
+ * Used by both `packageJsonTemplate` (declares the deps in the generated
984
+ * `package.json` so `npm install` works without a pre-pass) and
985
+ * `installDependencies` (runs the package manager's `install` against
986
+ * the declared ranges). One source — no drift.
987
+ */
932
988
  const SCAFFOLD_DEP_VERSIONS = {
933
989
  core: {
934
- "@classytic/arc": "^2.18.5",
935
- "@classytic/primitives": "^0.6.0",
936
- "@classytic/repo-core": "^0.6.0",
990
+ "@classytic/arc": "^2.20.0",
991
+ "@classytic/primitives": "^0.11.0",
992
+ "@classytic/repo-core": "^0.8.0",
937
993
  "@fastify/cors": "^11.2.0",
938
994
  "@fastify/helmet": "^13.0.2",
939
995
  "@fastify/rate-limit": "^10.3.0",
@@ -953,7 +1009,7 @@ const SCAFFOLD_DEP_VERSIONS = {
953
1009
  },
954
1010
  authBetterAuthApiKey: { "@better-auth/api-key": "^1.6.11" },
955
1011
  adapterMongokit: {
956
- "@classytic/mongokit": "^3.16.1",
1012
+ "@classytic/mongokit": "^3.21.0",
957
1013
  mongoose: "^9.6.2"
958
1014
  },
959
1015
  devCommon: {
@@ -965,11 +1021,69 @@ const SCAFFOLD_DEP_VERSIONS = {
965
1021
  devTypescript: {
966
1022
  "@types/node": "^22.10.0",
967
1023
  tsx: "^4.22.3",
968
- typescript: "^6.0.3"
1024
+ typescript: "^7.0.2"
969
1025
  },
970
1026
  typesJwt: { "@types/bcryptjs": "^3.0.0" }
971
1027
  };
972
1028
  /**
1029
+ * Locate arc's own package.json by walking up from the executing file.
1030
+ * A path walk (vs `require.resolve('@classytic/arc/package.json')`) works
1031
+ * regardless of bundle chunk layout and of package.json not being in the
1032
+ * exports map. Returns undefined when not found — callers fall back to
1033
+ * the static table.
1034
+ */
1035
+ function readOwnPackageJson() {
1036
+ try {
1037
+ let dir = dirname(fileURLToPath(import.meta.url));
1038
+ for (let depth = 0; depth < 6; depth++) {
1039
+ const candidate = join(dir, "package.json");
1040
+ if (existsSync(candidate)) {
1041
+ const parsed = JSON.parse(readFileSync(candidate, "utf8"));
1042
+ if (parsed.name === "@classytic/arc") return parsed;
1043
+ }
1044
+ const parent = dirname(dir);
1045
+ if (parent === dir) break;
1046
+ dir = parent;
1047
+ }
1048
+ } catch {}
1049
+ }
1050
+ /** First x.y.z triple in a range string, or undefined. */
1051
+ function parseFloor(range) {
1052
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(range);
1053
+ if (!m) return void 0;
1054
+ return [
1055
+ Number(m[1]),
1056
+ Number(m[2]),
1057
+ Number(m[3])
1058
+ ];
1059
+ }
1060
+ function floorLessThan(a, b) {
1061
+ if (a[0] !== b[0]) return a[0] < b[0];
1062
+ if (a[1] !== b[1]) return a[1] < b[1];
1063
+ return a[2] < b[2];
1064
+ }
1065
+ /**
1066
+ * Overlay live truth from arc's own package.json onto the static pins:
1067
+ * - `@classytic/arc` pins the exact running version (`^<version>`).
1068
+ * - Any scaffolded dep that is also an arc peer gets raised to `^<peer
1069
+ * floor>` when the static pin's floor is BELOW the declared peer floor
1070
+ * (never lowered — static pins may legitimately be ahead of the floor).
1071
+ */
1072
+ function overlayLiveArcVersions(dependencies) {
1073
+ const own = readOwnPackageJson();
1074
+ if (!own) return;
1075
+ if (own.version && dependencies["@classytic/arc"]) dependencies["@classytic/arc"] = `^${own.version}`;
1076
+ const peers = own.peerDependencies ?? {};
1077
+ for (const [name, staticRange] of Object.entries(dependencies)) {
1078
+ const peerRange = peers[name];
1079
+ if (!peerRange) continue;
1080
+ const peerFloor = parseFloor(peerRange);
1081
+ const staticFloor = parseFloor(staticRange);
1082
+ if (!peerFloor || !staticFloor) continue;
1083
+ if (floorLessThan(staticFloor, peerFloor)) dependencies[name] = `^${peerFloor.join(".")}`;
1084
+ }
1085
+ }
1086
+ /**
973
1087
  * Resolve the dependency manifest for a scaffold configuration.
974
1088
  *
975
1089
  * Returns sorted records (alphabetical by package name) so the generated
@@ -980,6 +1094,7 @@ function resolveScaffoldDependencies(config) {
980
1094
  const devDependencies = { ...SCAFFOLD_DEP_VERSIONS.devCommon };
981
1095
  if (config.auth === "better-auth") {
982
1096
  Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authBetterAuth);
1097
+ devDependencies["@fastify/jwt"] = SCAFFOLD_DEP_VERSIONS.authJwt["@fastify/jwt"];
983
1098
  if (config.apiKey) Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authBetterAuthApiKey);
984
1099
  } else {
985
1100
  Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authJwt);
@@ -987,6 +1102,7 @@ function resolveScaffoldDependencies(config) {
987
1102
  }
988
1103
  if (config.adapter === "mongokit") Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.adapterMongokit);
989
1104
  if (config.typescript) Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.devTypescript);
1105
+ overlayLiveArcVersions(dependencies);
990
1106
  return {
991
1107
  dependencies: sortByKey(dependencies),
992
1108
  devDependencies: sortByKey(devDependencies)
@@ -1077,6 +1193,7 @@ function tsconfigTemplate() {
1077
1193
  declarationMap: true,
1078
1194
  sourceMap: true,
1079
1195
  resolveJsonModule: true,
1196
+ types: ["node"],
1080
1197
  paths: {
1081
1198
  "#shared/*": ["./src/shared/*"],
1082
1199
  "#resources/*": ["./src/resources/*"],
@@ -1971,7 +2088,13 @@ import { describe, it, beforeAll, afterAll } from 'vitest';
1971
2088
  import { createTestApp, expectArc } from '@classytic/arc/testing';
1972
2089
  import type { TestAppContext${ts ? ", TestAuthProvider" : ""} } from '@classytic/arc/testing';
1973
2090
  import exampleResource from '../src/resources/example/example.resource.js';
1974
-
2091
+ ${config.tenant === "multi" ? `
2092
+ // Valid ObjectId hex — the example model types organizationId and createdBy
2093
+ // as ObjectId (createdBy is auto-stamped from the caller's user id).
2094
+ const TEST_ORG_ID = '64f000000000000000000001';
2095
+ const MANAGER_ID = '64f0000000000000000000a1';
2096
+ const STAFF_ID = '64f0000000000000000000a2';
2097
+ ` : ""}
1975
2098
  describe('Example Resource', () => {
1976
2099
  let ctx${ts ? ": TestAppContext" : ""};
1977
2100
  // Narrowed once below — \`authMode: 'jwt'\` guarantees a provider, but
@@ -1986,17 +2109,70 @@ ${config.adapter === "mongokit" ? " connectMongoose: true,\n" : ""} });
1986
2109
 
1987
2110
  if (!ctx.auth) throw new Error('Test auth provider was not configured');
1988
2111
  auth = ctx.auth;
1989
-
2112
+ ${config.tenant === "multi" ? `
2113
+ // Arc's JWT auth builds a MEMBER scope straight from token claims —
2114
+ // \`organizationId\` + \`orgRoles\` land on \`request.scope\`. The org-staff
2115
+ // permission set reads those org roles; the tenant preset filters and
2116
+ // stamps rows by the org id.
2117
+ auth.register('manager', {
2118
+ user: { id: MANAGER_ID, role: 'user', organizationId: TEST_ORG_ID, orgRoles: ['manager'] },
2119
+ orgId: TEST_ORG_ID,
2120
+ });
2121
+ auth.register('staff', {
2122
+ user: { id: STAFF_ID, role: 'user', organizationId: TEST_ORG_ID, orgRoles: ['staff'] },
2123
+ orgId: TEST_ORG_ID,
2124
+ });
2125
+ ` : `
1990
2126
  // Arc's permission engine reads singular user.role — string,
1991
2127
  // comma-separated string, or array all normalise via getUserRoles().
2128
+ // The id must be ObjectId-shaped: the example model's createdBy is an
2129
+ // ObjectId, auto-stamped from the caller's user id.
1992
2130
  auth.register('admin', {
1993
- user: { id: '1', role: 'admin' },
1994
- orgId: 'org-1',
2131
+ user: { id: '64f0000000000000000000ad', role: 'admin' },
1995
2132
  });
1996
- });
2133
+ `} });
1997
2134
 
1998
2135
  afterAll(() => ctx.close());
2136
+ ${config.tenant === "multi" ? `
2137
+ describe('GET /examples', () => {
2138
+ it('should reject unauthenticated listing (org-staff gated)', async () => {
2139
+ const res = await ctx.app.inject({ method: 'GET', url: '/examples' });
2140
+ expectArc(res).unauthorized();
2141
+ });
2142
+
2143
+ it('should list for an org member (rows scoped to their org)', async () => {
2144
+ const res = await ctx.app.inject({
2145
+ method: 'GET',
2146
+ url: '/examples',
2147
+ headers: auth.as('staff').headers,
2148
+ });
2149
+ expectArc(res).ok().paginated();
2150
+ });
2151
+ });
1999
2152
 
2153
+ describe('POST /examples', () => {
2154
+ it('should require authentication', async () => {
2155
+ const res = await ctx.app.inject({
2156
+ method: 'POST',
2157
+ url: '/examples',
2158
+ payload: { name: 'Test Example' },
2159
+ });
2160
+ expectArc(res).unauthorized();
2161
+ });
2162
+
2163
+ it('should create for an org manager — organizationId stamped server-side', async () => {
2164
+ const res = await ctx.app.inject({
2165
+ method: 'POST',
2166
+ url: '/examples',
2167
+ headers: auth.as('manager').headers,
2168
+ // No organizationId in the body — the tenant preset injects it from
2169
+ // the caller's scope (the field is systemManaged in the schema).
2170
+ payload: { name: 'Test Example' },
2171
+ });
2172
+ expectArc(res).ok(201);
2173
+ });
2174
+ });
2175
+ ` : `
2000
2176
  describe('GET /examples', () => {
2001
2177
  it('should return a list of examples (public)', async () => {
2002
2178
  const res = await ctx.app.inject({ method: 'GET', url: '/examples' });
@@ -2021,9 +2197,10 @@ ${config.adapter === "mongokit" ? " connectMongoose: true,\n" : ""} });
2021
2197
  headers: auth.as('admin').headers,
2022
2198
  payload: { name: 'Test Example' },
2023
2199
  });
2024
- expectArc(res).ok();
2200
+ expectArc(res).ok(201);
2025
2201
  });
2026
2202
  });
2203
+ `}
2027
2204
 
2028
2205
  // Add more tests as needed:
2029
2206
  // - GET /examples/:id (expectArc(res).ok().hasData({ name: '...' }))
@@ -2413,113 +2590,35 @@ function flexibleMultiTenantPresetTemplate(config) {
2413
2590
  return `/**
2414
2591
  * Flexible Multi-Tenant Preset
2415
2592
  *
2416
- * Smarter tenant filtering that works with public + authenticated routes.
2593
+ * Thin wrapper around arc's built-in 'multiTenantPreset' with public reads:
2417
2594
  *
2418
- * Philosophy:
2419
- * - No org scope → No filtering (public data, all orgs)
2420
- * - Org scope present → Filter by org
2421
- * - Elevated scope → No filter (platform admin sees all)
2595
+ * - list/get WITHOUT org context → allowed through unfiltered (public data)
2596
+ * - org context present → rows filtered + stamped to the caller's org
2597
+ * - elevated (platform admin) → unfiltered, cross-tenant
2598
+ * - create/update fail closed without org context, and update overwrites any
2599
+ * client-supplied tenant field (no cross-tenant document hops)
2422
2600
  *
2423
- * Uses request.scope (RequestScope) from Arc's scope system.
2601
+ * The built-in preset also registers 'systemManaged' field rules for the
2602
+ * tenant field, so generated request schemas never demand it in the body —
2603
+ * the server stamps it from the caller's scope instead. Don't hand-roll
2604
+ * tenant middleware: without those field rules Fastify's validation rejects
2605
+ * creates before injection can run.
2606
+ *
2607
+ * Want members-only reads? Drop 'allowPublic' below, or use
2608
+ * 'multiTenantPreset({ tenantField })' directly on the resource.
2424
2609
  */
2425
2610
  ${ts ? `
2426
- import { getOrgId, isElevated, isMember } from '@classytic/arc/scope';
2427
- import type { RequestScope } from '@classytic/arc/scope';
2611
+ import { multiTenantPreset } from '@classytic/arc/presets/tenant';
2428
2612
 
2429
2613
  interface FlexibleMultiTenantOptions {
2430
2614
  tenantField?: string;
2431
2615
  }
2432
-
2433
- interface PresetMiddlewares {
2434
- list: ((request: any, reply: any) => Promise<void>)[];
2435
- get: ((request: any, reply: any) => Promise<void>)[];
2436
- create: ((request: any, reply: any) => Promise<void>)[];
2437
- update: ((request: any, reply: any) => Promise<void>)[];
2438
- delete: ((request: any, reply: any) => Promise<void>)[];
2439
- }
2440
-
2441
- interface Preset {
2442
- [key: string]: unknown;
2443
- name: string;
2444
- middlewares: PresetMiddlewares;
2445
- }
2446
2616
  ` : `
2447
- const { getOrgId, isElevated, isMember } = require('@classytic/arc/scope');
2617
+ const { multiTenantPreset } = require('@classytic/arc/presets/tenant');
2448
2618
  `}
2449
- /**
2450
- * Create flexible tenant filter middleware.
2451
- * Only filters when org context is present.
2452
- */
2453
- function createFlexibleTenantFilter(tenantField${ts ? ": string" : ""}) {
2454
- return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
2455
- const scope${ts ? ": RequestScope" : ""} = request.scope ?? { kind: 'public' };
2456
-
2457
- // Elevated scope — platform admin sees all, no filter
2458
- if (isElevated(scope)) {
2459
- request.log?.debug?.({ msg: 'Elevated scope — no tenant filter' });
2460
- return;
2461
- }
2462
-
2463
- // Member scope — filter by org
2464
- if (isMember(scope)) {
2465
- request.query = request.query ?? {};
2466
- request.query._policyFilters = {
2467
- ...(request.query._policyFilters ?? {}),
2468
- [tenantField]: scope.organizationId,
2469
- };
2470
- request.log?.debug?.({ msg: 'Tenant filter applied', orgId: scope.organizationId, tenantField });
2471
- return;
2472
- }
2473
-
2474
- // Public / authenticated — no org context, show all data (public routes)
2475
- request.log?.debug?.({ msg: 'No org context — showing all data' });
2476
- };
2477
- }
2478
-
2479
- /**
2480
- * Create tenant injection middleware.
2481
- * Injects tenant ID into request body on create.
2482
- */
2483
- function createTenantInjection(tenantField${ts ? ": string" : ""}) {
2484
- return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
2485
- const scope${ts ? ": RequestScope" : ""} = request.scope ?? { kind: 'public' };
2486
- const orgId = getOrgId(scope);
2487
-
2488
- // Fail-closed: Require orgId for create operations
2489
- if (!orgId) {
2490
- return reply.code(403).send({
2491
- error: 'Organization context required to create resources',
2492
- code: 'arc.org_required',
2493
- });
2494
- }
2495
-
2496
- if (request.body) {
2497
- request.body[tenantField] = orgId;
2498
- }
2499
- };
2500
- }
2501
-
2502
- /**
2503
- * Flexible Multi-Tenant Preset
2504
- *
2505
- * @param options.tenantField - Field name in database (default: 'organizationId')
2506
- */
2507
- export function flexibleMultiTenantPreset(options${ts ? ": FlexibleMultiTenantOptions = {}" : " = {}"})${ts ? ": Preset" : ""} {
2619
+ export function flexibleMultiTenantPreset(options${ts ? ": FlexibleMultiTenantOptions = {}" : " = {}"}) {
2508
2620
  const { tenantField = 'organizationId' } = options;
2509
-
2510
- const tenantFilter = createFlexibleTenantFilter(tenantField);
2511
- const tenantInjection = createTenantInjection(tenantField);
2512
-
2513
- return {
2514
- name: 'flexibleMultiTenant',
2515
- middlewares: {
2516
- list: [tenantFilter],
2517
- get: [tenantFilter],
2518
- create: [tenantInjection],
2519
- update: [tenantFilter],
2520
- delete: [tenantFilter],
2521
- },
2522
- };
2621
+ return multiTenantPreset({ tenantField, allowPublic: ['list', 'get'] });
2523
2622
  }
2524
2623
 
2525
2624
  export default flexibleMultiTenantPreset;
@@ -3022,7 +3121,7 @@ async function gatherConfig(options) {
3022
3121
  output: process.stdout
3023
3122
  });
3024
3123
  const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
3025
- const nonInteractive = !!options.name;
3124
+ const nonInteractive = !!options.name || !process.stdin.isTTY;
3026
3125
  try {
3027
3126
  const name = options.name || await question("Project name: ") || "my-arc-app";
3028
3127
  let adapter = options.adapter || "mongokit";
@@ -1,7 +1,6 @@
1
- import { n as DomainEvent } from "../EventTransport-IEn7Tn-Q.mjs";
2
- import { c as WebSocketClient, l as WebSocketMessage } from "../websocket-MlYlrYq0.mjs";
1
+ import { n as DomainEvent } from "../EventTransport-QmY7A258.mjs";
2
+ import { c as WebSocketClient, l as WebSocketMessage } from "../websocket-uuWqM6UY.mjs";
3
3
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
4
-
5
4
  //#region src/integrations/event-gateway.d.ts
6
5
  interface EventGatewayOptions {
7
6
  /** Require auth for all real-time connections (default: true) */
@@ -1,7 +1,7 @@
1
- import { c as WebSocketClient, l as WebSocketMessage, u as WebSocketPluginOptions } from "../websocket-MlYlrYq0.mjs";
1
+ import { c as WebSocketClient, l as WebSocketMessage, u as WebSocketPluginOptions } from "../websocket-uuWqM6UY.mjs";
2
2
  import { EventGatewayOptions } from "./event-gateway.mjs";
3
3
  import { JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobsPluginOptions, QueueStats } from "./jobs.mjs";
4
- import { c as McpAuthResult, d as PromptDefinition, g as ToolDefinition, h as ToolContext, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, r as CreateMcpServerConfig, t as BetterAuthHandler, u as McpResourceConfig } from "../types-DpvSF5Hh.mjs";
4
+ import { c as McpAuthResult, d as PromptDefinition, g as ToolDefinition, h as ToolContext, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, r as CreateMcpServerConfig, t as BetterAuthHandler, u as McpResourceConfig } from "../types-DRj-Ut4n.mjs";
5
5
  import { StreamlinePluginOptions, WorkflowLike, WorkflowRunLike } from "./streamline.mjs";
6
6
  import { WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription } from "./webhooks.mjs";
7
7
  export type { BetterAuthHandler, CallToolResult, CreateMcpServerConfig, CrudOperation, EventGatewayOptions, JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobsPluginOptions, McpAuthResult, McpPluginOptions, McpResourceConfig, PromptDefinition, QueueStats, StreamlinePluginOptions, ToolAnnotations, ToolContext, ToolDefinition, WebSocketClient, WebSocketMessage, WebSocketPluginOptions, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, WorkflowLike, WorkflowRunLike };
@@ -1,5 +1,4 @@
1
1
  import { FastifyPluginAsync } from "fastify";
2
-
3
2
  //#region src/integrations/jobs.d.ts
4
3
  /** Repeat schedule — cron pattern or fixed interval. Explicit timezone is required. */
5
4
  interface JobRepeatOptions {
@@ -1,11 +1,10 @@
1
- import { V as ResourceDefinition, bt as IControllerResponse, xt as IRequestContext } from "../../index-CUTG2Cuo.mjs";
2
- import { i as RequestScope } from "../../types-LNebCoJx.mjs";
3
- import { c as PermissionCheck } from "../../fields-C7ahdGYf.mjs";
4
- import { a as CrudDescriptionOverride, c as McpAuthResult, d as PromptDefinition, f as PromptResult, g as ToolDefinition, h as ToolContext, i as CrudDescriptionMeta, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, p as SessionEntry, r as CreateMcpServerConfig, s as McpAuthResolver, t as BetterAuthHandler, u as McpResourceConfig } from "../../types-DpvSF5Hh.mjs";
1
+ import { V as ResourceDefinition, bt as IControllerResponse, xt as IRequestContext } from "../../index-Dtrj6XZk.mjs";
2
+ import { i as RequestScope } from "../../types-DH0GupI7.mjs";
3
+ import { c as PermissionCheck } from "../../fields-BGqrAa_u.mjs";
4
+ import { a as CrudDescriptionOverride, c as McpAuthResult, d as PromptDefinition, f as PromptResult, g as ToolDefinition, h as ToolContext, i as CrudDescriptionMeta, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, p as SessionEntry, r as CreateMcpServerConfig, s as McpAuthResolver, t as BetterAuthHandler, u as McpResourceConfig } from "../../types-DRj-Ut4n.mjs";
5
5
  import { ErrorContract } from "@classytic/repo-core/errors";
6
6
  import { FastifyPluginAsync } from "fastify";
7
7
  import { z } from "zod";
8
-
9
8
  //#region src/integrations/mcp/aiSdkBridge.d.ts
10
9
  /** Minimal AI SDK tool shape we need to invoke. */
11
10
  interface AiSdkExecutable {
@@ -170,7 +169,7 @@ interface DefinePromptConfig<TArgs extends Record<string, z.ZodTypeAny>> {
170
169
  title?: string;
171
170
  /** Flat Zod shape for prompt arguments */
172
171
  args?: TArgs;
173
- handler: (args: { [K in keyof TArgs]: z.infer<TArgs[K]> }) => PromptResult;
172
+ handler: (args: { [K in keyof TArgs]: z.infer<TArgs[K]>; }) => PromptResult;
174
173
  }
175
174
  /**
176
175
  * Define a type-safe MCP prompt.
@@ -197,7 +196,7 @@ interface DefineToolConfig<TInput extends Record<string, z.ZodTypeAny>, TSession
197
196
  /** Flat Zod shape for structured output */
198
197
  output?: Record<string, z.ZodTypeAny>;
199
198
  annotations?: ToolAnnotations;
200
- handler: (input: { [K in keyof TInput]: z.infer<TInput[K]> }, ctx: ToolContext<TSession>) => Promise<CallToolResult>;
199
+ handler: (input: { [K in keyof TInput]: z.infer<TInput[K]>; }, ctx: ToolContext<TSession>) => Promise<CallToolResult>;
201
200
  }
202
201
  /**
203
202
  * Define a type-safe MCP tool.
@@ -1,4 +1,4 @@
1
- import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, h as resolveToolCollisions, i as fieldRulesToZod, l as permissionDeniedResult, m as createMcpServer, n as mcpPlugin, o as resolveCrudDescription, p as buildRequestContext, r as resourceToTools, s as invokeController, t as filterResourcesForMcp, u as toCallToolError } from "../../mcpPlugin-C088Nd-G.mjs";
1
+ import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, h as resolveToolCollisions, i as fieldRulesToZod, l as permissionDeniedResult, m as createMcpServer, n as mcpPlugin, o as resolveCrudDescription, p as buildRequestContext, r as resourceToTools, s as invokeController, t as filterResourcesForMcp, u as toCallToolError } from "../../mcpPlugin-B9XjjNVG.mjs";
2
2
  //#region src/integrations/mcp/defineTool.ts
3
3
  /**
4
4
  * Define a type-safe MCP tool.