@classytic/arc 2.20.0 → 2.22.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 (143) hide show
  1. package/dist/{BaseController-D-MHZ27_.mjs → BaseController-C6FlRWxF.mjs} +2 -2
  2. package/dist/{EventTransport-IEn7Tn-Q.d.mts → EventTransport-QmY7A258.d.mts} +0 -1
  3. package/dist/{QueryCache-TRfXau3W.d.mts → QueryCache-Z1B38koh.d.mts} +0 -1
  4. package/dist/{actionPermissions-C9RMiIpc.mjs → actionPermissions-ze70xXJi.mjs} +1 -1
  5. package/dist/audit/index.d.mts +9 -5
  6. package/dist/auth/audit.d.mts +7 -4
  7. package/dist/auth/audit.mjs +5 -0
  8. package/dist/auth/index.d.mts +35 -14
  9. package/dist/auth/index.mjs +6 -3
  10. package/dist/auth/redis-session.d.mts +1 -2
  11. package/dist/cache/index.d.mts +2 -3
  12. package/dist/cli/commands/describe.d.mts +1 -2
  13. package/dist/cli/commands/docs.mjs +1 -1
  14. package/dist/cli/commands/init.mjs +1 -1
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/context/index.d.mts +2 -2
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -4
  19. package/dist/{core-CWhyGkiM.mjs → core-b6fgFgnd.mjs} +129 -17
  20. package/dist/{createActionRouter-HlQso0jO.mjs → createActionRouter-h1cEEaEX.mjs} +10 -6
  21. package/dist/{createAggregationRouter-5LzPzgw1.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
  22. package/dist/{createApp-D6uwzCKu.mjs → createApp-Ci7yG5gv.mjs} +390 -192
  23. package/dist/discovery/index.d.mts +0 -1
  24. package/dist/docs/index.d.mts +1 -2
  25. package/dist/docs/index.mjs +1 -1
  26. package/dist/{elevation-xGUBq6p_.d.mts → elevation-C9s5hFP_.d.mts} +0 -1
  27. package/dist/encryption/index.d.mts +2 -144
  28. package/dist/encryption/index.mjs +9 -5
  29. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
  30. package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -1
  31. package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
  32. package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
  33. package/dist/events/index.d.mts +9 -6
  34. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  35. package/dist/events/transports/redis.d.mts +1 -2
  36. package/dist/factory/index.d.mts +30 -9
  37. package/dist/factory/index.mjs +35 -4
  38. package/dist/{fastify-augmentation-CCQJEDgh.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -3
  39. package/dist/fields-D3lhgzhD.d.mts +121 -0
  40. package/dist/health-ScyjIVo5.mjs +202 -0
  41. package/dist/hooks/index.d.mts +1 -1
  42. package/dist/idempotency/index.d.mts +4 -3
  43. package/dist/idempotency/index.mjs +2 -0
  44. package/dist/idempotency/redis.d.mts +1 -1
  45. package/dist/{types-BHMKlMBU.d.mts → index-BEu4wjbL.d.mts} +241 -80
  46. package/dist/{index-DUa8WHz3.d.mts → index-D8MKYGDa.d.mts} +88 -7
  47. package/dist/{index-_--GyzcO.d.mts → index-DbtgCOCj.d.mts} +1324 -1228
  48. package/dist/{index-CdFkIMEd.d.mts → index-ogIHvOjo.d.mts} +36 -7
  49. package/dist/index.d.mts +8 -7
  50. package/dist/index.mjs +6 -6
  51. package/dist/{init-YH_nslQR.mjs → init-DleK3p9f.mjs} +272 -139
  52. package/dist/integrations/event-gateway.d.mts +2 -3
  53. package/dist/integrations/index.d.mts +2 -2
  54. package/dist/integrations/jobs.d.mts +0 -1
  55. package/dist/integrations/mcp/index.d.mts +6 -7
  56. package/dist/integrations/mcp/index.mjs +1 -1
  57. package/dist/integrations/mcp/testing.d.mts +1 -2
  58. package/dist/integrations/mcp/testing.mjs +1 -1
  59. package/dist/integrations/streamline.d.mts +2 -2
  60. package/dist/integrations/webhooks.d.mts +8 -5
  61. package/dist/integrations/webhooks.mjs +9 -5
  62. package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
  63. package/dist/integrations/websocket-redis.d.mts +1 -2
  64. package/dist/integrations/websocket.d.mts +2 -2
  65. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  66. package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
  67. package/dist/logger/index.d.mts +26 -1
  68. package/dist/logger/index.mjs +39 -1
  69. package/dist/{mcpPlugin-CuW6L_uD.mjs → mcpPlugin-ByA2tf6D.mjs} +8 -4
  70. package/dist/middleware/index.d.mts +1 -2
  71. package/dist/{module-JYH9V0I8.d.mts → module-B4u10iOJ.d.mts} +71 -1
  72. package/dist/{openapi-Dq6RbEGK.mjs → openapi-BRpMZD7C.mjs} +3 -3
  73. package/dist/permissions/index.d.mts +4 -3
  74. package/dist/permissions/index.mjs +2 -2
  75. package/dist/{permissions-CgbOLJZG.mjs → permissions-BJMuKMl7.mjs} +33 -1
  76. package/dist/pipeline/index.d.mts +1 -2
  77. package/dist/plugins/index.d.mts +96 -7
  78. package/dist/plugins/index.mjs +86 -268
  79. package/dist/plugins/response-cache.d.mts +5 -3
  80. package/dist/plugins/response-cache.mjs +1 -0
  81. package/dist/plugins/tracing-entry.d.mts +1 -1
  82. package/dist/plugins/tracing-entry.mjs +7 -4
  83. package/dist/presets/filesUpload.d.mts +4 -5
  84. package/dist/presets/filesUpload.mjs +1 -1
  85. package/dist/presets/index.d.mts +1 -2
  86. package/dist/presets/index.mjs +1 -1
  87. package/dist/presets/multiTenant.d.mts +1 -2
  88. package/dist/presets/search.d.mts +2 -3
  89. package/dist/presets/search.mjs +1 -1
  90. package/dist/{presets-DWsYQdbV.mjs → presets-gw0ciIUD.mjs} +1 -1
  91. package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
  92. package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
  93. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  94. package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
  95. package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
  96. package/dist/{registerRoute-DGSREwPw.mjs → registerRoute-xHc42fwB.mjs} +38 -18
  97. package/dist/registry/index.d.mts +2 -2
  98. package/dist/requestId-5BhTVzBv.mjs +113 -0
  99. package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
  100. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
  101. package/dist/schemas/index.d.mts +0 -1
  102. package/dist/scim/index.d.mts +0 -1
  103. package/dist/scim/index.mjs +6 -7
  104. package/dist/scope/index.d.mts +2 -3
  105. package/dist/scope/index.mjs +2 -76
  106. package/dist/scope-k15RXfcq.mjs +64 -0
  107. package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
  108. package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
  109. package/dist/testing/index.d.mts +83 -4
  110. package/dist/testing/index.mjs +74 -4
  111. package/dist/testing/storageContract.d.mts +1 -2
  112. package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
  113. package/dist/types/index.d.mts +4 -4
  114. package/dist/types/storage.d.mts +1 -1
  115. package/dist/{types-C9b2HR5W.d.mts → types-Bc42CUyk.d.mts} +1 -2
  116. package/dist/types-Cc91nXuA.d.mts +132 -0
  117. package/dist/{fields-C7ahdGYf.d.mts → types-Csxga7d3.d.mts} +2 -123
  118. package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
  119. package/dist/usage/index.d.mts +142 -0
  120. package/dist/usage/index.mjs +0 -0
  121. package/dist/utils/index.d.mts +220 -144
  122. package/dist/utils/index.mjs +3 -3
  123. package/dist/{utils-ChjYQpjn.mjs → utils-FhJ88TJE.mjs} +129 -39
  124. package/dist/{versioning-D_eBCfNr.d.mts → versioning-DLwklPlG.d.mts} +1 -7
  125. package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
  126. package/llms.txt +39 -0
  127. package/package.json +453 -447
  128. package/skills/arc/SKILL.md +14 -8
  129. package/skills/arc/references/api-reference.md +34 -0
  130. package/skills/arc/references/auth.md +21 -4
  131. package/skills/arc/references/mcp.md +1 -1
  132. package/skills/arc/references/production.md +44 -2
  133. package/skills/arc-code-review/SKILL.md +1 -1
  134. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  135. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  136. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  137. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  138. package/skills/arc-module-publishing/SKILL.md +243 -0
  139. package/skills/arc-module-publishing/references/authoring-modules.md +327 -0
  140. package/skills/arc-module-publishing/references/package-manifest.md +157 -0
  141. package/skills/arc-module-publishing/references/publishing-workflow.md +182 -0
  142. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  143. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -1,3 +1,4 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import { dirname, join } from "node:path";
@@ -17,7 +18,7 @@ function createAdapterTemplate(config) {
17
18
 
18
19
  import { createMongooseAdapter } from '@classytic/mongokit/adapter';
19
20
  import { buildCrudSchemasFromModel } from '@classytic/mongokit';
20
- ${ts ? "import type { Model } from 'mongoose';\nimport type { Repository } from '@classytic/mongokit';" : ""}
21
+ ${ts ? "import type { DataAdapter } from '@classytic/repo-core/adapter';\nimport type { Model } from 'mongoose';\nimport type { Repository } from '@classytic/mongokit';" : ""}
21
22
 
22
23
  /**
23
24
  * Create a MongoKit-powered adapter for a resource.
@@ -30,7 +31,10 @@ ${ts ? "import type { Model } from 'mongoose';\nimport type { Repository } from
30
31
  export function createAdapter${ts ? "<TDoc = unknown>" : ""}(
31
32
  model${ts ? ": Model<TDoc>" : ""},
32
33
  repository${ts ? ": Repository<TDoc>" : ""}
33
- ) {
34
+ )${ts ? ": DataAdapter<TDoc>" : ""} {
35
+ // Explicit return type keeps the declaration portable — under pnpm's
36
+ // non-hoisted layout the inferred type references mongokit's internal
37
+ // paths and tsc fails with TS2742/TS2883.
34
38
  return createMongooseAdapter({
35
39
  model,
36
40
  repository,
@@ -73,6 +77,60 @@ export function createAdapter${ts ? "<TDoc = unknown>" : ""}(
73
77
  `;
74
78
  }
75
79
  //#endregion
80
+ //#region src/cli/commands/init/templates/agents.ts
81
+ function agentsGuideTemplate(config) {
82
+ return `# CLAUDE.md — agent guide for this app
83
+
84
+ > This file doubles as AGENTS.md — copy or symlink if your tool reads that name.
85
+
86
+ **This is an \`@classytic/arc\` application.** Resources are DECLARED, not hand-rolled:
87
+ one \`defineResource()\` = REST + validation + auth + permissions + events + caching +
88
+ OpenAPI + MCP tools. Data layer: ${config.adapter === "mongokit" ? "@classytic/mongokit (Mongoose)" : "a custom DataAdapter kit"}.
89
+
90
+ ## The one rule
91
+
92
+ **Hand-rolling is a bug.** Before writing ANY route, validation, pagination, auth check,
93
+ cron, counter, or audit code by hand, prove arc lacks it — check the table below, then
94
+ \`node_modules/@classytic/arc/skills/arc/SKILL.md\` (full framework guide + references).
95
+
96
+ ## About to write X? Arc already has Y
97
+
98
+ | If you're about to write… | Use instead |
99
+ |---|---|
100
+ | \`fastify.get/post/...\` CRUD routes for an entity | \`defineResource({ name, adapter, permissions })\` |
101
+ | \`if (req.user.role !== 'admin') return 403\` | \`permissions: { update: requireRoles(['admin']) }\` |
102
+ | Reading \`req.user._id\` / org id directly | \`getUserId(scope)\` / \`requireOrgId(scope)\` from \`@classytic/arc/scope\` |
103
+ | Hand-written \`schema: { body }\` / JSON Schema | \`schemaOptions.fieldRules\` (or \`customSchemas\` for wire≠model) |
104
+ | toJSON transforms hiding \`password\`/internals | \`fieldRules: { password: { hidden: true } }\` |
105
+ | Manual \`req.query\` filter/sort/pagination parsing | the resource's \`queryParser\` (allow-listed fields) |
106
+ | Soft delete / slug lookup / tree / bulk / file upload | \`presets: ['softDelete', 'slugLookup', ...]\` |
107
+ | \`setInterval\` sweeps + hand-rolled leader lock | \`schedulesPlugin\` (\`@classytic/arc/plugins\`) |
108
+ | A background/queue worker entrypoint | \`createWorker(sameOptions)\` (\`@classytic/arc/factory\`) |
109
+ | Usage counters / quotas / plan rate limits | \`usagePlugin\` + \`requireQuota\` + \`rateLimit.plan\` |
110
+ | Audit log rows / "who changed what" endpoints | \`audit: true\` + \`history: true\` on the resource |
111
+ | Manual idempotency keys / dedupe | \`idempotencyPlugin\` |
112
+ | Event emitters / webhooks plumbing | \`eventPlugin\` + resource \`events:\` (+ outbox for guarantees) |
113
+ | Custom MCP/AI tool handlers | generated automatically from every resource |
114
+
115
+ ## Load-bearing conventions
116
+
117
+ - \`request.user\` is \`undefined\` on public routes — always guard.
118
+ - Never read \`scope.organizationId\` directly — use \`@classytic/arc/scope\` accessors.
119
+ - The DB connection belongs in \`createApp({ beforeBoot })\` — not before \`createApp\`.
120
+ - Custom endpoints: \`routes:\`/\`actions:\` on the resource (they inherit auth/permissions/
121
+ OpenAPI/MCP) — a bare \`fastify.get()\` gets none of that.
122
+
123
+ ## Commands
124
+
125
+ \`\`\`bash
126
+ npm run dev # watch mode
127
+ npm test # vitest
128
+ npx arc generate resource <name> # scaffold a resource (model/repo/resource files)
129
+ npx arc describe # list registered resources + routes
130
+ \`\`\`
131
+ `;
132
+ }
133
+ //#endregion
76
134
  //#region src/cli/commands/init/templates/app.ts
77
135
  function indexTemplate(config) {
78
136
  const ts = config.typescript;
@@ -439,8 +497,8 @@ function betterAuthSetupTemplate(config) {
439
497
  import { mongodbAdapter } from '@better-auth/mongo-adapter';` : "";
440
498
  const stubsImport = useStubs ? `import { registerBetterAuthStubs } from '@classytic/mongokit/better-auth';` : "";
441
499
  const pluginImports = [
442
- useOrg ? `import { organization } from 'better-auth/plugins';` : "",
443
- useBearer ? `import { bearer } from 'better-auth/plugins';` : "",
500
+ useOrg ? `import { organization } from 'better-auth/plugins/organization';` : "",
501
+ useBearer ? `import { bearer } from 'better-auth/plugins/bearer';` : "",
444
502
  useApiKey ? `import { apiKey } from '@better-auth/api-key';` : ""
445
503
  ].filter(Boolean).join("\n");
446
504
  const dbAdapter = useMongo ? config.typescript ? `database: mongodbAdapter(mongoose.connection.getClient().db() as unknown as never),` : `database: mongodbAdapter(mongoose.connection.getClient().db()),` : `// Configure your database adapter here
@@ -484,20 +542,13 @@ ${[
484
542
  ].filter(Boolean).join("\n")}
485
543
  import config from '#config/index.js';
486
544
 
487
- let _auth${ts ? ": ReturnType<typeof betterAuth> | null" : ""} = null;
488
-
489
- /**
490
- * Get the Better Auth instance (lazy singleton)
491
- *
492
- * Must be called AFTER database connection is established.
493
- */
494
- export function getAuth()${ts ? ": ReturnType<typeof betterAuth>" : ""} {
495
- if (process.env.NODE_ENV === 'production' && !process.env.BETTER_AUTH_SECRET) {
496
- throw new Error('BETTER_AUTH_SECRET is required in production (min 32 chars)');
497
- }
498
-
499
- if (!_auth) {
500
- _auth = betterAuth({
545
+ // Factory keeps the CONCRETE betterAuth({...}) instantiation as the source
546
+ // of truth for the instance type — annotating with the wide
547
+ // \`Auth<BetterAuthOptions>\` (aka \`ReturnType<typeof betterAuth>\`) does not
548
+ // typecheck because Better Auth's \`Auth<T>\` is generic over the exact
549
+ // options object.
550
+ function createAuth() {
551
+ return betterAuth({
501
552
  secret: config.betterAuth.secret,
502
553
  baseURL: process.env.BETTER_AUTH_URL || \`http://localhost:\${config.server.port}\`,
503
554
  basePath: '/api/auth',
@@ -545,6 +596,26 @@ ${orgPluginUsage}
545
596
  enabled: process.env.NODE_ENV === 'production',
546
597
  },
547
598
  });
599
+ }
600
+
601
+ let _auth${ts ? ": ReturnType<typeof createAuth> | null" : ""} = null;
602
+
603
+ /**
604
+ * Get the Better Auth instance (lazy singleton)
605
+ *
606
+ * Must be called AFTER database connection is established.
607
+ */
608
+ export function getAuth()${ts ? ": ReturnType<typeof createAuth>" : ""} {
609
+ if (process.env.NODE_ENV === 'production' && !process.env.BETTER_AUTH_SECRET) {
610
+ throw new Error('BETTER_AUTH_SECRET is required in production (min 32 chars)');
611
+ }
612
+
613
+ // Narrow through a local — assigning the module-level \`_auth\` directly
614
+ // would lose the non-null narrowing across the calls below.
615
+ let auth = _auth;
616
+ if (!auth) {
617
+ auth = createAuth();
618
+ _auth = auth;
548
619
  ${useStubs ? `
549
620
  // Register stub Mongoose models for Better Auth's collections so
550
621
  // \`.populate('user')\` / \`ref: 'organization'\` resolve against BA-owned
@@ -552,7 +623,7 @@ ${useStubs ? `
552
623
  registerBetterAuthStubs(mongoose, {${stubPluginsList ? ` plugins: [${stubPluginsList}]` : ""}${stubExtras} });
553
624
  ` : ""} }
554
625
 
555
- return _auth;
626
+ return auth;
556
627
  }
557
628
 
558
629
  export default getAuth;
@@ -692,6 +763,9 @@ export async function getUserProfile(request${ts ? ": FastifyRequest" : ""}, rep
692
763
  try {
693
764
  const requestUser = (request${ts ? " as unknown as { user?: { _id?: string; id?: string } }" : ""}).user;
694
765
  const userId = requestUser?._id || requestUser?.id;
766
+ if (!userId) {
767
+ return reply.code(401).send({ error: 'Authentication required', code: 'arc.unauthorized' });
768
+ }
695
769
  const user = await userRepository.getById(userId);
696
770
 
697
771
  if (!user) {
@@ -712,6 +786,9 @@ export async function updateUserProfile(request${ts ? ": FastifyRequest" : ""},
712
786
  try {
713
787
  const requestUser = (request${ts ? " as unknown as { user?: { _id?: string; id?: string } }" : ""}).user;
714
788
  const userId = requestUser?._id || requestUser?.id;
789
+ if (!userId) {
790
+ return reply.code(401).send({ error: 'Authentication required', code: 'arc.unauthorized' });
791
+ }
715
792
  const updates${ts ? ": Record<string, unknown>" : ""} = { ...request.body${ts ? " as Record<string, unknown>" : ""} };
716
793
 
717
794
  // Prevent updating protected fields — auth-managed only
@@ -840,20 +917,29 @@ function authTestTemplate(config) {
840
917
  */
841
918
 
842
919
  import { describe, it, expect, beforeAll, afterAll } from 'vitest';
843
- ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';\n" : ""}import { createAppInstance } from '../src/app.js';
920
+ ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';\nimport { MongoMemoryServer } from 'mongodb-memory-server';\n" : ""}import { createAppInstance } from '../src/app.js';
844
921
  ${ts ? "import type { FastifyInstance } from 'fastify';\n" : ""}
922
+ // Resources mount under the app's resourcePrefix (see src/app.${ts ? "ts" : "js"}).
923
+ const API = '/api';
924
+
845
925
  describe('Auth', () => {
846
926
  let app${ts ? ": FastifyInstance" : ""};
847
- const testUser = {
927
+ ${config.adapter === "mongokit" ? ` let mongo${ts ? ": MongoMemoryServer | undefined" : ""};\n` : ""} const testUser = {
848
928
  name: 'Test User',
849
929
  email: 'test@example.com',
850
930
  password: 'password123',
851
931
  };
852
932
 
853
933
  beforeAll(async () => {
854
- ${config.adapter === "mongokit" ? ` const testDbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}-test';
855
- await mongoose.connect(testDbUri);
856
- // Clean up test data
934
+ ${config.adapter === "mongokit" ? ` // In-memory MongoDB — no local mongod required. Set MONGODB_URI to use
935
+ // a real instance instead (e.g. a CI service container).
936
+ let uri = process.env.MONGODB_URI;
937
+ if (!uri) {
938
+ mongo = await MongoMemoryServer.create();
939
+ uri = mongo.getUri();
940
+ }
941
+ await mongoose.connect(uri);
942
+ // Clean up test data (matters when MONGODB_URI points at a shared DB)
857
943
  await mongoose.connection.collection('users').deleteMany({ email: testUser.email });
858
944
  ` : ""}
859
945
  app = await createAppInstance();
@@ -862,27 +948,29 @@ ${config.adapter === "mongokit" ? ` const testDbUri = process.env.MONGODB_URI
862
948
 
863
949
  afterAll(async () => {
864
950
  ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('users').deleteMany({ email: testUser.email });
951
+ await app.close();
865
952
  await mongoose.connection.close();
866
- ` : ""} await app.close();
867
- });
953
+ await mongo?.stop();
954
+ ` : ` await app.close();
955
+ `} });
868
956
 
869
957
  describe('POST /auth/register', () => {
870
958
  it('should register a new user', async () => {
871
959
  const response = await app.inject({
872
960
  method: 'POST',
873
- url: '/auth/register',
961
+ url: \`\${API}/auth/register\`,
874
962
  payload: testUser,
875
963
  });
876
964
 
877
965
  expect(response.statusCode).toBe(201);
878
966
  const body = JSON.parse(response.body);
879
- expect(body.success).toBe(true);
967
+ expect(body.message).toBeDefined();
880
968
  });
881
969
 
882
970
  it('should reject duplicate email', async () => {
883
971
  const response = await app.inject({
884
972
  method: 'POST',
885
- url: '/auth/register',
973
+ url: \`\${API}/auth/register\`,
886
974
  payload: testUser,
887
975
  });
888
976
 
@@ -894,13 +982,13 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
894
982
  it('should login with valid credentials', async () => {
895
983
  const response = await app.inject({
896
984
  method: 'POST',
897
- url: '/auth/login',
985
+ url: \`\${API}/auth/login\`,
898
986
  payload: { email: testUser.email, password: testUser.password },
899
987
  });
900
988
 
901
989
  expect(response.statusCode).toBe(200);
902
990
  const body = JSON.parse(response.body);
903
- expect(body.success).toBe(true);
991
+ expect(body.user).toBeDefined();
904
992
  expect(body.accessToken).toBeDefined();
905
993
  expect(body.refreshToken).toBeDefined();
906
994
  });
@@ -908,7 +996,7 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
908
996
  it('should reject invalid credentials', async () => {
909
997
  const response = await app.inject({
910
998
  method: 'POST',
911
- url: '/auth/login',
999
+ url: \`\${API}/auth/login\`,
912
1000
  payload: { email: testUser.email, password: 'wrongpassword' },
913
1001
  });
914
1002
 
@@ -920,7 +1008,7 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
920
1008
  it('should require authentication', async () => {
921
1009
  const response = await app.inject({
922
1010
  method: 'GET',
923
- url: '/users/me',
1011
+ url: \`\${API}/users/me\`,
924
1012
  });
925
1013
 
926
1014
  expect(response.statusCode).toBe(401);
@@ -952,11 +1040,16 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
952
1040
  * `installDependencies` (runs the package manager's `install` against
953
1041
  * the declared ranges). One source — no drift.
954
1042
  */
1043
+ var dependency_plan_exports = /* @__PURE__ */ __exportAll({
1044
+ SCAFFOLD_DEP_VERSIONS: () => SCAFFOLD_DEP_VERSIONS,
1045
+ primeLatestScaffoldVersions: () => primeLatestScaffoldVersions,
1046
+ resolveScaffoldDependencies: () => resolveScaffoldDependencies
1047
+ });
955
1048
  const SCAFFOLD_DEP_VERSIONS = {
956
1049
  core: {
957
1050
  "@classytic/arc": "^2.20.0",
958
- "@classytic/primitives": "^0.9.1",
959
- "@classytic/repo-core": "^0.7.0",
1051
+ "@classytic/primitives": "^0.11.0",
1052
+ "@classytic/repo-core": "^0.8.0",
960
1053
  "@fastify/cors": "^11.2.0",
961
1054
  "@fastify/helmet": "^13.0.2",
962
1055
  "@fastify/rate-limit": "^10.3.0",
@@ -976,7 +1069,7 @@ const SCAFFOLD_DEP_VERSIONS = {
976
1069
  },
977
1070
  authBetterAuthApiKey: { "@better-auth/api-key": "^1.6.11" },
978
1071
  adapterMongokit: {
979
- "@classytic/mongokit": "^3.16.1",
1072
+ "@classytic/mongokit": "^3.21.0",
980
1073
  mongoose: "^9.6.2"
981
1074
  },
982
1075
  devCommon: {
@@ -988,7 +1081,7 @@ const SCAFFOLD_DEP_VERSIONS = {
988
1081
  devTypescript: {
989
1082
  "@types/node": "^22.10.0",
990
1083
  tsx: "^4.22.3",
991
- typescript: "^6.0.3"
1084
+ typescript: "^7.0.2"
992
1085
  },
993
1086
  typesJwt: { "@types/bcryptjs": "^3.0.0" }
994
1087
  };
@@ -1036,6 +1129,51 @@ function floorLessThan(a, b) {
1036
1129
  * floor>` when the static pin's floor is BELOW the declared peer floor
1037
1130
  * (never lowered — static pins may legitimately be ahead of the floor).
1038
1131
  */
1132
+ const latestCache = /* @__PURE__ */ new Map();
1133
+ function allScaffoldPackageNames() {
1134
+ const names = /* @__PURE__ */ new Set();
1135
+ for (const group of Object.values(SCAFFOLD_DEP_VERSIONS)) for (const name of Object.keys(group)) names.add(name);
1136
+ return [...names];
1137
+ }
1138
+ async function primeLatestScaffoldVersions(options) {
1139
+ const timeoutMs = options?.timeoutMs ?? 3500;
1140
+ const registry = (options?.registry ?? process.env.npm_config_registry ?? "https://registry.npmjs.org").replace(/\/$/, "");
1141
+ const names = allScaffoldPackageNames();
1142
+ const results = await Promise.allSettled(names.map(async (name) => {
1143
+ const res = await fetch(`${registry}/${name}/latest`, {
1144
+ signal: AbortSignal.timeout(timeoutMs),
1145
+ headers: { accept: "application/json" }
1146
+ });
1147
+ if (!res.ok) throw new Error(String(res.status));
1148
+ const body = await res.json();
1149
+ if (typeof body.version === "string" && /^\d+\.\d+\.\d+$/.test(body.version)) latestCache.set(name, body.version);
1150
+ }));
1151
+ return {
1152
+ resolved: latestCache.size,
1153
+ total: results.length
1154
+ };
1155
+ }
1156
+ function parseBareVersion(v) {
1157
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v);
1158
+ return m ? [
1159
+ Number(m[1]),
1160
+ Number(m[2]),
1161
+ Number(m[3])
1162
+ ] : void 0;
1163
+ }
1164
+ function applyLatestOverlay(dependencies) {
1165
+ if (latestCache.size === 0) return;
1166
+ for (const [name, currentRange] of Object.entries(dependencies)) {
1167
+ const latest = latestCache.get(name);
1168
+ if (!latest || latest.includes("-")) continue;
1169
+ const latestV = parseBareVersion(latest);
1170
+ const currentFloor = parseFloor(currentRange);
1171
+ if (!latestV || !currentFloor) continue;
1172
+ if (!floorLessThan(currentFloor, latestV)) continue;
1173
+ if (latestV[0] !== currentFloor[0] && !name.startsWith("@classytic/")) continue;
1174
+ dependencies[name] = `^${latest}`;
1175
+ }
1176
+ }
1039
1177
  function overlayLiveArcVersions(dependencies) {
1040
1178
  const own = readOwnPackageJson();
1041
1179
  if (!own) return;
@@ -1061,6 +1199,7 @@ function resolveScaffoldDependencies(config) {
1061
1199
  const devDependencies = { ...SCAFFOLD_DEP_VERSIONS.devCommon };
1062
1200
  if (config.auth === "better-auth") {
1063
1201
  Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authBetterAuth);
1202
+ devDependencies["@fastify/jwt"] = SCAFFOLD_DEP_VERSIONS.authJwt["@fastify/jwt"];
1064
1203
  if (config.apiKey) Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authBetterAuthApiKey);
1065
1204
  } else {
1066
1205
  Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authJwt);
@@ -1068,6 +1207,8 @@ function resolveScaffoldDependencies(config) {
1068
1207
  }
1069
1208
  if (config.adapter === "mongokit") Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.adapterMongokit);
1070
1209
  if (config.typescript) Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.devTypescript);
1210
+ applyLatestOverlay(dependencies);
1211
+ applyLatestOverlay(devDependencies);
1071
1212
  overlayLiveArcVersions(dependencies);
1072
1213
  return {
1073
1214
  dependencies: sortByKey(dependencies),
@@ -1159,6 +1300,7 @@ function tsconfigTemplate() {
1159
1300
  declarationMap: true,
1160
1301
  sourceMap: true,
1161
1302
  resolveJsonModule: true,
1303
+ types: ["node"],
1162
1304
  paths: {
1163
1305
  "#shared/*": ["./src/shared/*"],
1164
1306
  "#resources/*": ["./src/resources/*"],
@@ -2053,7 +2195,13 @@ import { describe, it, beforeAll, afterAll } from 'vitest';
2053
2195
  import { createTestApp, expectArc } from '@classytic/arc/testing';
2054
2196
  import type { TestAppContext${ts ? ", TestAuthProvider" : ""} } from '@classytic/arc/testing';
2055
2197
  import exampleResource from '../src/resources/example/example.resource.js';
2056
-
2198
+ ${config.tenant === "multi" ? `
2199
+ // Valid ObjectId hex — the example model types organizationId and createdBy
2200
+ // as ObjectId (createdBy is auto-stamped from the caller's user id).
2201
+ const TEST_ORG_ID = '64f000000000000000000001';
2202
+ const MANAGER_ID = '64f0000000000000000000a1';
2203
+ const STAFF_ID = '64f0000000000000000000a2';
2204
+ ` : ""}
2057
2205
  describe('Example Resource', () => {
2058
2206
  let ctx${ts ? ": TestAppContext" : ""};
2059
2207
  // Narrowed once below — \`authMode: 'jwt'\` guarantees a provider, but
@@ -2068,17 +2216,70 @@ ${config.adapter === "mongokit" ? " connectMongoose: true,\n" : ""} });
2068
2216
 
2069
2217
  if (!ctx.auth) throw new Error('Test auth provider was not configured');
2070
2218
  auth = ctx.auth;
2071
-
2219
+ ${config.tenant === "multi" ? `
2220
+ // Arc's JWT auth builds a MEMBER scope straight from token claims —
2221
+ // \`organizationId\` + \`orgRoles\` land on \`request.scope\`. The org-staff
2222
+ // permission set reads those org roles; the tenant preset filters and
2223
+ // stamps rows by the org id.
2224
+ auth.register('manager', {
2225
+ user: { id: MANAGER_ID, role: 'user', organizationId: TEST_ORG_ID, orgRoles: ['manager'] },
2226
+ orgId: TEST_ORG_ID,
2227
+ });
2228
+ auth.register('staff', {
2229
+ user: { id: STAFF_ID, role: 'user', organizationId: TEST_ORG_ID, orgRoles: ['staff'] },
2230
+ orgId: TEST_ORG_ID,
2231
+ });
2232
+ ` : `
2072
2233
  // Arc's permission engine reads singular user.role — string,
2073
2234
  // comma-separated string, or array all normalise via getUserRoles().
2235
+ // The id must be ObjectId-shaped: the example model's createdBy is an
2236
+ // ObjectId, auto-stamped from the caller's user id.
2074
2237
  auth.register('admin', {
2075
- user: { id: '1', role: 'admin' },
2076
- orgId: 'org-1',
2238
+ user: { id: '64f0000000000000000000ad', role: 'admin' },
2077
2239
  });
2078
- });
2240
+ `} });
2079
2241
 
2080
2242
  afterAll(() => ctx.close());
2243
+ ${config.tenant === "multi" ? `
2244
+ describe('GET /examples', () => {
2245
+ it('should reject unauthenticated listing (org-staff gated)', async () => {
2246
+ const res = await ctx.app.inject({ method: 'GET', url: '/examples' });
2247
+ expectArc(res).unauthorized();
2248
+ });
2249
+
2250
+ it('should list for an org member (rows scoped to their org)', async () => {
2251
+ const res = await ctx.app.inject({
2252
+ method: 'GET',
2253
+ url: '/examples',
2254
+ headers: auth.as('staff').headers,
2255
+ });
2256
+ expectArc(res).ok().paginated();
2257
+ });
2258
+ });
2259
+
2260
+ describe('POST /examples', () => {
2261
+ it('should require authentication', async () => {
2262
+ const res = await ctx.app.inject({
2263
+ method: 'POST',
2264
+ url: '/examples',
2265
+ payload: { name: 'Test Example' },
2266
+ });
2267
+ expectArc(res).unauthorized();
2268
+ });
2081
2269
 
2270
+ it('should create for an org manager — organizationId stamped server-side', async () => {
2271
+ const res = await ctx.app.inject({
2272
+ method: 'POST',
2273
+ url: '/examples',
2274
+ headers: auth.as('manager').headers,
2275
+ // No organizationId in the body — the tenant preset injects it from
2276
+ // the caller's scope (the field is systemManaged in the schema).
2277
+ payload: { name: 'Test Example' },
2278
+ });
2279
+ expectArc(res).ok(201);
2280
+ });
2281
+ });
2282
+ ` : `
2082
2283
  describe('GET /examples', () => {
2083
2284
  it('should return a list of examples (public)', async () => {
2084
2285
  const res = await ctx.app.inject({ method: 'GET', url: '/examples' });
@@ -2103,9 +2304,10 @@ ${config.adapter === "mongokit" ? " connectMongoose: true,\n" : ""} });
2103
2304
  headers: auth.as('admin').headers,
2104
2305
  payload: { name: 'Test Example' },
2105
2306
  });
2106
- expectArc(res).ok();
2307
+ expectArc(res).ok(201);
2107
2308
  });
2108
2309
  });
2310
+ `}
2109
2311
 
2110
2312
  // Add more tests as needed:
2111
2313
  // - GET /examples/:id (expectArc(res).ok().hasData({ name: '...' }))
@@ -2495,113 +2697,35 @@ function flexibleMultiTenantPresetTemplate(config) {
2495
2697
  return `/**
2496
2698
  * Flexible Multi-Tenant Preset
2497
2699
  *
2498
- * Smarter tenant filtering that works with public + authenticated routes.
2700
+ * Thin wrapper around arc's built-in 'multiTenantPreset' with public reads:
2701
+ *
2702
+ * - list/get WITHOUT org context → allowed through unfiltered (public data)
2703
+ * - org context present → rows filtered + stamped to the caller's org
2704
+ * - elevated (platform admin) → unfiltered, cross-tenant
2705
+ * - create/update fail closed without org context, and update overwrites any
2706
+ * client-supplied tenant field (no cross-tenant document hops)
2499
2707
  *
2500
- * Philosophy:
2501
- * - No org scope → No filtering (public data, all orgs)
2502
- * - Org scope present → Filter by org
2503
- * - Elevated scope → No filter (platform admin sees all)
2708
+ * The built-in preset also registers 'systemManaged' field rules for the
2709
+ * tenant field, so generated request schemas never demand it in the body —
2710
+ * the server stamps it from the caller's scope instead. Don't hand-roll
2711
+ * tenant middleware: without those field rules Fastify's validation rejects
2712
+ * creates before injection can run.
2504
2713
  *
2505
- * Uses request.scope (RequestScope) from Arc's scope system.
2714
+ * Want members-only reads? Drop 'allowPublic' below, or use
2715
+ * 'multiTenantPreset({ tenantField })' directly on the resource.
2506
2716
  */
2507
2717
  ${ts ? `
2508
- import { getOrgId, isElevated, isMember } from '@classytic/arc/scope';
2509
- import type { RequestScope } from '@classytic/arc/scope';
2718
+ import { multiTenantPreset } from '@classytic/arc/presets/tenant';
2510
2719
 
2511
2720
  interface FlexibleMultiTenantOptions {
2512
2721
  tenantField?: string;
2513
2722
  }
2514
-
2515
- interface PresetMiddlewares {
2516
- list: ((request: any, reply: any) => Promise<void>)[];
2517
- get: ((request: any, reply: any) => Promise<void>)[];
2518
- create: ((request: any, reply: any) => Promise<void>)[];
2519
- update: ((request: any, reply: any) => Promise<void>)[];
2520
- delete: ((request: any, reply: any) => Promise<void>)[];
2521
- }
2522
-
2523
- interface Preset {
2524
- [key: string]: unknown;
2525
- name: string;
2526
- middlewares: PresetMiddlewares;
2527
- }
2528
2723
  ` : `
2529
- const { getOrgId, isElevated, isMember } = require('@classytic/arc/scope');
2724
+ const { multiTenantPreset } = require('@classytic/arc/presets/tenant');
2530
2725
  `}
2531
- /**
2532
- * Create flexible tenant filter middleware.
2533
- * Only filters when org context is present.
2534
- */
2535
- function createFlexibleTenantFilter(tenantField${ts ? ": string" : ""}) {
2536
- return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
2537
- const scope${ts ? ": RequestScope" : ""} = request.scope ?? { kind: 'public' };
2538
-
2539
- // Elevated scope — platform admin sees all, no filter
2540
- if (isElevated(scope)) {
2541
- request.log?.debug?.({ msg: 'Elevated scope — no tenant filter' });
2542
- return;
2543
- }
2544
-
2545
- // Member scope — filter by org
2546
- if (isMember(scope)) {
2547
- request.query = request.query ?? {};
2548
- request.query._policyFilters = {
2549
- ...(request.query._policyFilters ?? {}),
2550
- [tenantField]: scope.organizationId,
2551
- };
2552
- request.log?.debug?.({ msg: 'Tenant filter applied', orgId: scope.organizationId, tenantField });
2553
- return;
2554
- }
2555
-
2556
- // Public / authenticated — no org context, show all data (public routes)
2557
- request.log?.debug?.({ msg: 'No org context — showing all data' });
2558
- };
2559
- }
2560
-
2561
- /**
2562
- * Create tenant injection middleware.
2563
- * Injects tenant ID into request body on create.
2564
- */
2565
- function createTenantInjection(tenantField${ts ? ": string" : ""}) {
2566
- return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
2567
- const scope${ts ? ": RequestScope" : ""} = request.scope ?? { kind: 'public' };
2568
- const orgId = getOrgId(scope);
2569
-
2570
- // Fail-closed: Require orgId for create operations
2571
- if (!orgId) {
2572
- return reply.code(403).send({
2573
- error: 'Organization context required to create resources',
2574
- code: 'arc.org_required',
2575
- });
2576
- }
2577
-
2578
- if (request.body) {
2579
- request.body[tenantField] = orgId;
2580
- }
2581
- };
2582
- }
2583
-
2584
- /**
2585
- * Flexible Multi-Tenant Preset
2586
- *
2587
- * @param options.tenantField - Field name in database (default: 'organizationId')
2588
- */
2589
- export function flexibleMultiTenantPreset(options${ts ? ": FlexibleMultiTenantOptions = {}" : " = {}"})${ts ? ": Preset" : ""} {
2726
+ export function flexibleMultiTenantPreset(options${ts ? ": FlexibleMultiTenantOptions = {}" : " = {}"}) {
2590
2727
  const { tenantField = 'organizationId' } = options;
2591
-
2592
- const tenantFilter = createFlexibleTenantFilter(tenantField);
2593
- const tenantInjection = createTenantInjection(tenantField);
2594
-
2595
- return {
2596
- name: 'flexibleMultiTenant',
2597
- middlewares: {
2598
- list: [tenantFilter],
2599
- get: [tenantFilter],
2600
- create: [tenantInjection],
2601
- update: [tenantFilter],
2602
- delete: [tenantFilter],
2603
- },
2604
- };
2728
+ return multiTenantPreset({ tenantField, allowPublic: ['list', 'get'] });
2605
2729
  }
2606
2730
 
2607
2731
  export default flexibleMultiTenantPreset;
@@ -3013,6 +3137,7 @@ async function createProjectStructure(projectPath, config) {
3013
3137
  files["docker-compose.yml"] = dockerComposeTemplate(config);
3014
3138
  }
3015
3139
  if (config.edge) files["wrangler.toml"] = wranglerTemplate(config);
3140
+ files["CLAUDE.md"] = agentsGuideTemplate(config);
3016
3141
  files[".arcrc"] = `${JSON.stringify({
3017
3142
  adapter: config.adapter,
3018
3143
  auth: config.auth,
@@ -3312,6 +3437,14 @@ async function init(options = {}) {
3312
3437
  }
3313
3438
  const packageManager = detectPackageManager();
3314
3439
  console.log(`Using package manager: ${packageManager}\n`);
3440
+ if (!process.env.VITEST && process.env.ARC_INIT_OFFLINE !== "1") {
3441
+ const { primeLatestScaffoldVersions } = await Promise.resolve().then(() => dependency_plan_exports);
3442
+ const { resolved, total } = await primeLatestScaffoldVersions().catch(() => ({
3443
+ resolved: 0,
3444
+ total: 0
3445
+ }));
3446
+ console.log(resolved > 0 ? `Resolved ${resolved}/${total} dependency versions from npm (latest stable)\n` : "Registry unreachable — using pinned fallback versions\n");
3447
+ }
3315
3448
  await createProjectStructure(projectPath, config);
3316
3449
  if (!options.skipInstall) {
3317
3450
  console.log("\n📥 Installing dependencies...\n");
@@ -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-C9b2HR5W.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-Bc42CUyk.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 };