@classytic/arc 2.15.3 → 2.16.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 (159) hide show
  1. package/README.md +1 -0
  2. package/bin/arc.js +12 -0
  3. package/dist/{BaseController-dx3m2J8V.mjs → BaseController-DlCCTIxJ.mjs} +61 -19
  4. package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
  5. package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-SvmT_9ti.d.mts} +1 -1
  6. package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-f48hFk3m.mjs} +52 -9
  7. package/dist/audit/index.d.mts +1 -1
  8. package/dist/audit/index.mjs +4 -2
  9. package/dist/auth/index.d.mts +4 -4
  10. package/dist/auth/index.mjs +4 -4
  11. package/dist/auth/redis-session.d.mts +1 -1
  12. package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
  13. package/dist/buildHandler-BZX6zzDM.mjs +300 -0
  14. package/dist/cache/index.d.mts +3 -3
  15. package/dist/cache/index.mjs +3 -3
  16. package/dist/{caching-SM8gghN6.mjs → caching-TeHE8G-v.mjs} +1 -1
  17. package/dist/cli/commands/describe.d.mts +35 -1
  18. package/dist/cli/commands/describe.mjs +52 -12
  19. package/dist/cli/commands/docs.d.mts +1 -4
  20. package/dist/cli/commands/docs.mjs +4 -16
  21. package/dist/cli/commands/generate.d.mts +2 -20
  22. package/dist/cli/commands/generate.mjs +1 -546
  23. package/dist/cli/commands/init.d.mts +2 -40
  24. package/dist/cli/commands/init.mjs +1 -3036
  25. package/dist/cli/commands/introspect.mjs +53 -64
  26. package/dist/cli/index.d.mts +2 -2
  27. package/dist/cli/index.mjs +2 -2
  28. package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
  29. package/dist/core/index.d.mts +3 -3
  30. package/dist/core/index.mjs +5 -5
  31. package/dist/{core-CvmOqEms.mjs → core-DBJ_j6rX.mjs} +222 -44
  32. package/dist/createActionRouter-DUpN3Dd1.mjs +288 -0
  33. package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-Dq-TUCuY.mjs} +3 -2
  34. package/dist/{createApp-PFegs47-.mjs → createApp-DNccuhyI.mjs} +16 -14
  35. package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-DRwY0fYm.mjs} +1 -1
  36. package/dist/docs/index.d.mts +2 -2
  37. package/dist/docs/index.mjs +1 -1
  38. package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DpoXQHZ9.mjs} +17 -14
  39. package/dist/errors-C1lX_jlm.d.mts +91 -0
  40. package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-C2cGqtRO.mjs} +1 -1
  41. package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-CtHC_av1.d.mts} +1 -1
  42. package/dist/events/index.d.mts +3 -3
  43. package/dist/events/index.mjs +5 -5
  44. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  45. package/dist/events/transports/redis.d.mts +1 -1
  46. package/dist/factory/index.d.mts +1 -1
  47. package/dist/factory/index.mjs +2 -2
  48. package/dist/{fields-COhcH3fk.d.mts → fields-Anj0xdih.d.mts} +1 -1
  49. package/dist/generate-BWFwgcCM.d.mts +38 -0
  50. package/dist/generate-CYac-OLv.mjs +654 -0
  51. package/dist/hooks/index.d.mts +1 -1
  52. package/dist/hooks/index.mjs +1 -1
  53. package/dist/idempotency/index.d.mts +2 -2
  54. package/dist/idempotency/index.mjs +1 -1
  55. package/dist/idempotency/redis.d.mts +1 -1
  56. package/dist/{index-BTqLEvhu.d.mts → index-3oIimXQn.d.mts} +12 -12
  57. package/dist/{index-BstGxcc3.d.mts → index-B-ulKx5P.d.mts} +55 -4
  58. package/dist/{index-BswOSJCE.d.mts → index-CkW0flkU.d.mts} +355 -16
  59. package/dist/index.d.mts +6 -6
  60. package/dist/index.mjs +7 -8
  61. package/dist/init-Dv71MsJr.d.mts +71 -0
  62. package/dist/init-HDvoO9L5.mjs +3098 -0
  63. package/dist/integrations/event-gateway.d.mts +2 -2
  64. package/dist/integrations/event-gateway.mjs +1 -1
  65. package/dist/integrations/index.d.mts +2 -2
  66. package/dist/integrations/jobs.mjs +3 -3
  67. package/dist/integrations/mcp/index.d.mts +239 -7
  68. package/dist/integrations/mcp/index.mjs +2 -528
  69. package/dist/integrations/mcp/testing.d.mts +2 -2
  70. package/dist/integrations/mcp/testing.mjs +6 -10
  71. package/dist/integrations/streamline.d.mts +71 -2
  72. package/dist/integrations/streamline.mjs +81 -8
  73. package/dist/integrations/websocket-redis.d.mts +1 -1
  74. package/dist/integrations/websocket.d.mts +1 -1
  75. package/dist/integrations/websocket.mjs +1 -0
  76. package/dist/loadResourcesFromEntry-BLMEI2Xa.mjs +51 -0
  77. package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-7vGV51ED.mjs} +1021 -318
  78. package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
  79. package/dist/middleware/index.d.mts +1 -1
  80. package/dist/middleware/index.mjs +1 -1
  81. package/dist/{openapi-BHXhoX8O.mjs → openapi-34T9yNwd.mjs} +47 -36
  82. package/dist/permissions/index.d.mts +2 -2
  83. package/dist/permissions/index.mjs +1 -1
  84. package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
  85. package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
  86. package/dist/pipeline/index.d.mts +1 -1
  87. package/dist/pipeline/index.mjs +1 -1
  88. package/dist/plugins/index.d.mts +5 -5
  89. package/dist/plugins/index.mjs +10 -10
  90. package/dist/plugins/response-cache.mjs +5 -5
  91. package/dist/plugins/tracing-entry.d.mts +1 -1
  92. package/dist/plugins/tracing-entry.mjs +1 -1
  93. package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
  94. package/dist/presets/filesUpload.d.mts +4 -4
  95. package/dist/presets/filesUpload.mjs +2 -2
  96. package/dist/presets/index.d.mts +1 -1
  97. package/dist/presets/index.mjs +1 -1
  98. package/dist/presets/multiTenant.d.mts +1 -1
  99. package/dist/presets/multiTenant.mjs +4 -3
  100. package/dist/presets/search.d.mts +2 -2
  101. package/dist/presets/search.mjs +1 -1
  102. package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
  103. package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
  104. package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Biqzfbi5.d.mts} +2 -2
  105. package/dist/{redis-DiMkdHEl.d.mts → redis-Cyzrz6SX.d.mts} +1 -1
  106. package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-DT-YjzrB.d.mts} +1 -1
  107. package/dist/registry/index.d.mts +319 -2
  108. package/dist/registry/index.mjs +3 -3
  109. package/dist/registry-BBE23CDj.mjs +576 -0
  110. package/dist/{routerShared-DrOa-26E.mjs → routerShared-CZV5aabX.mjs} +3 -3
  111. package/dist/scope/index.d.mts +3 -3
  112. package/dist/scope/index.mjs +3 -3
  113. package/dist/{sse-Bz-5ZeTt.mjs → sse-BY6sTy4P.mjs} +1 -1
  114. package/dist/testing/index.d.mts +2 -2
  115. package/dist/testing/index.mjs +16 -7
  116. package/dist/testing/storageContract.d.mts +1 -1
  117. package/dist/types/index.d.mts +5 -5
  118. package/dist/types/storage.d.mts +1 -1
  119. package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
  120. package/dist/{types-BQsjgQzS.d.mts → types-BsJMEQ4D.d.mts} +106 -12
  121. package/dist/{types-DrBaUwyV.d.mts → types-D-fYtKjb.d.mts} +33 -10
  122. package/dist/{types-CTYvcwHe.d.mts → types-DVfpSfx2.d.mts} +42 -1
  123. package/dist/utils/index.d.mts +1286 -2
  124. package/dist/utils/index.mjs +1 -1
  125. package/dist/{utils-_h9B3c57.mjs → utils-DC5ycPfr.mjs} +89 -40
  126. package/dist/{buildHandler-CcFOpJLh.mjs → validate-By96rH0r.mjs} +8 -299
  127. package/dist/{versioning-hmkPcDlX.d.mts → versioning-ZwX9tmbS.d.mts} +1 -1
  128. package/package.json +22 -29
  129. package/skills/arc/SKILL.md +299 -689
  130. package/skills/arc/references/auth.md +19 -7
  131. package/skills/arc-code-review/SKILL.md +1 -1
  132. package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
  133. package/dist/createActionRouter-S3MLVYot.mjs +0 -220
  134. package/dist/index-bRjYu21O.d.mts +0 -1320
  135. package/dist/org/index.d.mts +0 -66
  136. package/dist/org/index.mjs +0 -486
  137. package/dist/org/types.d.mts +0 -82
  138. package/dist/org/types.mjs +0 -1
  139. package/dist/registry-I-ogLgL9.mjs +0 -46
  140. /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-C-2oAHtw.d.mts} +0 -0
  141. /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-Hxvv5QQz.mjs} +0 -0
  142. /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
  143. /package/dist/{elevation-BXOWoGCF.d.mts → elevation-0YBpa663.d.mts} +0 -0
  144. /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
  145. /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-mHuyWzZE.d.mts} +0 -0
  146. /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-DFg-2KTp.d.mts} +0 -0
  147. /package/dist/{interface-beEtJyWM.d.mts → interface-CH0OQudo.d.mts} +0 -0
  148. /package/dist/{interface-DfLGcus7.d.mts → interface-NwJ_qPlY.d.mts} +0 -0
  149. /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
  150. /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-ChQEj8ih.mjs} +0 -0
  151. /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-TuOmguhi.mjs} +0 -0
  152. /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-C-gD32oF.mjs} +0 -0
  153. /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-Ctc89DSn.mjs} +0 -0
  154. /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-BqFegc0W.d.mts} +0 -0
  155. /package/dist/{storage-Dfzt4VTl.d.mts → storage-D2KZJAmn.d.mts} +0 -0
  156. /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-B0sunfZZ.mjs} +0 -0
  157. /package/dist/{tracing-QJVprktp.d.mts → tracing-Dm8n7Cnn.d.mts} +0 -0
  158. /package/dist/{versioning-BUrT5aP4.mjs → versioning-B6mimogM.mjs} +0 -0
  159. /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BkjeGZRn.d.mts} +0 -0
@@ -511,18 +511,30 @@ permissions: { list: acl.canAction('product', 'read') }
511
511
  - Supports `*` wildcard for resource/action
512
512
  - Cache failures fail open to resolver
513
513
 
514
- ## Org Guards
514
+ ## Org-Scoped Permission Checks
515
515
 
516
516
  ```typescript
517
- import { orgGuard, requireOrg, requireOrgRole } from '@classytic/arc/org';
517
+ import { allowPublic, requireAuth, requireOrgRole } from '@classytic/arc/permissions';
518
518
 
519
- // Require org context
520
- fastify.get('/invoices', { preHandler: [fastify.authenticate, requireOrg()] }, handler);
521
-
522
- // Require specific org role
523
- fastify.post('/invoices', { preHandler: [fastify.authenticate, requireOrgRole('admin')] }, handler);
519
+ // Resource-level permission check on the active org's member.role
520
+ defineResource({
521
+ name: 'invoice',
522
+ permissions: {
523
+ list: requireAuth(),
524
+ create: requireOrgRole('admin', 'owner'),
525
+ },
526
+ });
524
527
  ```
525
528
 
529
+ The 2.16 release removed the legacy `@classytic/arc/org` REST plugin
530
+ and its `orgGuard` / `requireOrg` middleware (zero verified consumers).
531
+ The PermissionCheck variant — `requireOrgRole` from
532
+ `@classytic/arc/permissions` — is the canonical path: it composes with
533
+ `anyOf` / `allOf`, integrates with the resource pipeline, and reads
534
+ `request.scope.orgRoles` (set by auth adapters). For preHandler-style
535
+ org enforcement on custom routes outside a resource, build a one-line
536
+ preHandler that throws if `getOrgId(request.scope)` is missing.
537
+
526
538
  ## Request Scope
527
539
 
528
540
  ```typescript
@@ -84,7 +84,7 @@ One `defineResource()` call **replaces all of these** in a typical Fastify servi
84
84
  ```markdown
85
85
  # Arc Convention Audit — <project-name>
86
86
 
87
- **Arc version:** 3.0.x · **Mongokit:** <version or "not installed"> · **Sqlitekit:** <version or "n/a"> · **Date:** <YYYY-MM-DD>
87
+ **Arc version:** 2.16.x · **Mongokit:** <version or "not installed"> · **Sqlitekit:** <version or "n/a"> · **Date:** <YYYY-MM-DD>
88
88
  **Files scanned:** <N> · **Findings:** <N critical · <N high · <N medium · <N low>
89
89
 
90
90
  ## Executive summary
@@ -1,168 +1,93 @@
1
- # Arc Cheatsheet what arc provides, in one page
1
+ # Arc capabilities at a glance for gap detection
2
2
 
3
- A condensed map of arc's capabilities so you can spot, during audit, what the team is *missing* vs hand-rolling. For deep API, see the existing `skills/arc/SKILL.md` and its `references/`.
3
+ A condensed map of what arc provides, so during audit you can spot what the team is **hand-rolling** vs what's already a one-liner. For full API details, read [`skills/arc/SKILL.md`](../../arc/SKILL.md) and its references.
4
4
 
5
- ---
5
+ ## What arc replaces (audit signals)
6
6
 
7
- ## Boot order (FIXED don't reorder)
7
+ | If you see this in the codebase… | …it should be this arc surface |
8
+ |---|---|
9
+ | 5× `fastify.get/post/patch/delete` for one resource | `defineResource({ name, adapter, permissions, … })` — CRUD auto-generated |
10
+ | `if (req.user.role !== 'admin') return reply.code(403)…` | `permissions: { update: requireRoles(['admin']) }` |
11
+ | `req.user._id`, `req.user.orgId` direct reads | `getUserId(scope)` / `getOrgId(scope)` from `@classytic/arc/scope` |
12
+ | Hand-written `schema: { body, response }` per route | `schemaOptions.fieldRules` |
13
+ | `schema.set('toJSON', { transform })` to strip `password`/`__v` | `fieldRules: { password: { hidden: true } }` |
14
+ | Manual `req.query.filter` parsing, `$or`/`$and` building | `ArcQueryParser` / mongokit `QueryParser` |
15
+ | Hand-maintained `openapi.yaml` | `arc docs ./openapi.json` |
16
+ | `eventBus.emit('product.created', …)` in handler | CRUD events auto-emit; `events: { created: {} }` for custom |
17
+ | `cache.del('products-*')` after mutation | `cache: { tags: ['catalog'] }` — auto-invalidated |
18
+ | Soft-delete: hand-rolled `/deleted` route + `deletedAt` field + restore handler | `presets: ['softDelete']` |
19
+ | `class UserRepository { async create() { Model.create() } }` | `new Repository(Model)` from mongokit |
20
+ | Per-schema `schema.pre('save', …)` for timestamps/validation | mongokit's `timestampPlugin()`, `validationChainPlugin()` |
21
+ | Hand-written MCP tool handlers | `mcpPlugin({ resources })` — auto-generated, same perms |
22
+ | `Model.aggregate([…])` in route handler | `aggregations: { name: defineAggregation({ … }) }` |
23
+ | Custom `withRetry` / `withDLQ` plumbing on events | `RedisStreamTransport` (durable, consumer groups, DLQ) |
24
+ | Hand-rolled idempotency token check | `idempotencyPlugin` (header-based, configurable store) |
25
+ | Manual SCIM provisioning endpoints | `@classytic/arc/scim` — `scimPlugin({ users, groups, bearer })` |
26
+
27
+ ## Boot order (FIXED)
8
28
 
9
29
  ```
10
30
  1. Arc core (security, auth, events)
11
- 2. plugins() ← user infra (DB, docs, webhooks)
12
- 3. bootstrap[] ← domain init (engines, singletons)
13
- 4. resources factory if async: resolved here, after bootstrap
14
- 5. resources[] ← register each resource
15
- 6. afterResources() ← post-registration wiring
16
- 7. onReady / onClose ← Fastify lifecycle hooks
31
+ 2. plugins() ← user infra (DB, docs, webhooks)
32
+ 3. bootstrap[] ← domain init (engines, singletons)
33
+ 4. resources (factory runs after bootstrap)
34
+ 5. resources[] ← register each
35
+ 6. afterResources()
36
+ 7. onReady / onClose
17
37
  ```
18
38
 
19
- When auditing: top-level `await ensureCatalogEngine()` in a `*.resource.ts` file = lifecycle violation. Use `resources: async (fastify) => [...]`.
20
-
21
- ---
39
+ **Lifecycle smell:** top-level `await ensureCatalogEngine()` in a `*.resource.ts` file. Fix: pass `resources` as `async (fastify) => [...]` so it runs after `bootstrap[]`.
22
40
 
23
- ## `createApp()` essentials
24
-
25
- ```typescript
26
- const app = await createApp({
27
- preset: 'production', // production | development | testing | edge
28
- runtime: 'memory', // memory (default) | distributed
29
- auth: { type: 'jwt', jwt: { secret } }, // | 'betterAuth' | 'custom' | false
30
- resources: [resource1, resource2], // OR async (fastify) => [...]
31
- arcPlugins: { events: true, queryCache: false, sse: false, caching: true },
32
- stores: { events: ..., queryCache: ..., idempotency: ... }, // distributed only
33
- cors: { origin: [...], credentials: true },
34
- helmet: true, rateLimit: { max: 100 },
35
- resourcePrefix: '/api/v1',
36
- bootstrap: [async () => { ... }],
37
- afterResources: async (app) => { ... },
38
- });
39
- ```
40
-
41
- ---
42
-
43
- ## `defineResource()` — full surface
41
+ ## defineResource — full surface
44
42
 
45
43
  ```typescript
46
44
  defineResource({
47
- name: 'product', // required
48
- adapter: createMongooseAdapter({ model, repository, schemaGenerator? }), // required
49
- controller?: new MyController(), // optional — auto-built if omitted
50
- permissions: { // required
51
- list, get, create, update, delete: PermissionCheck,
52
- },
53
- presets?: ['softDelete', { name: 'multiTenant', tenantField: 'orgId' }, ...],
54
- schemaOptions?: {
55
- fieldRules: { [field]: FieldRuleEntry },
56
- query: { allowedPopulate, allowedLookups, filterableFields },
57
- },
58
- routes?: [{ method, path, handler, permissions, raw?, mcp?, summary? }],
59
- actions?: { [name]: { handler, permissions?, schema?, mcp?, description? } },
60
- actionPermissions?: PermissionCheck,
61
- hooks?: { beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete },
62
- events?: { created: {}, updated: {}, deleted: {}, [custom]: { description?, schema? } },
63
- cache?: { staleTime, gcTime, tags, invalidateOn?, list?, byId? },
64
- routeGuards?: [preHandlerFn],
65
- middlewares?: { create: [multipartBody(...)], ... },
66
- pipe?: { create: [guard(...), transform(...), intercept(...)] },
67
- rateLimit?: { max, timeWindow },
68
- tenantField?: string | false,
69
- idField?: string,
70
- prefix?: string, skipGlobalPrefix?: boolean,
71
- queryParser?: QueryParserInterface,
72
- onFieldWriteDenied?: 'reject' | 'strip',
73
- audit?: boolean | { operations: [...] },
74
- displayName?: string, module?: string,
45
+ name: 'product', // required
46
+ adapter, // required from kit's /adapter subpath
47
+ controller?, // optional — auto-built if omitted
48
+ permissions: { list, get, create, update, delete },
49
+ presets?: [...],
50
+ schemaOptions?: { fieldRules, query },
51
+ routes?, actions?, actionPermissions?,
52
+ hooks?, events?, cache?,
53
+ routeGuards?, middlewares?, pipe?,
54
+ rateLimit?, tenantField?, idField?,
55
+ prefix?, skipGlobalPrefix?,
56
+ queryParser?, onFieldWriteDenied?,
57
+ audit?, mcp?, // mcp: false to exclude from MCP tool gen
58
+ displayName?, module?,
59
+ onTenantDelete?, // GDPR cascade strategy
75
60
  });
76
61
  ```
77
62
 
78
- ### `FieldRuleEntry` flags
79
-
80
- | Flag | Effect |
81
- |---|---|
82
- | `systemManaged` | Strip from body, drop from `required[]`. Framework stamps the value. |
83
- | `preserveForElevated` | Elevated admins keep the field on ingest (cross-tenant writes). |
84
- | `immutable` / `immutableAfterCreate` | Omit from update body. |
85
- | `optional` | Strip from `required[]` without touching `properties`. |
86
- | `nullable` | Widen JSON-Schema `type` to include null. |
87
- | `hidden` | Block from response projection + OpenAPI. |
88
- | `minLength`/`maxLength`/`min`/`max`/`pattern`/`enum` | Map to AJV + OpenAPI. |
89
- | `description` | OpenAPI `description`. |
90
-
91
- Mongoose model-level constraints take precedence; `fieldRules` supplements.
63
+ ## Permissions
92
64
 
93
- ---
94
-
95
- ## Permissions — combinators
96
-
97
- From `@classytic/arc`:
98
65
  ```typescript
99
- allowPublic() // always grant
100
- requireAuth() // any authenticated user
101
- requireRoles(['admin', 'editor'])// platform OR org roles
102
- requireOwnership('userId') // row-level: filters → { userId: scope.userId }
103
- requireOrgMembership() // member | service | elevated
104
- requireOrgRole(['admin']) // human-only role within org
105
- requireTeamMembership()
106
- requireServiceScope('jobs:bulk-write') // OAuth-style API-key scopes
107
- requireScopeContext('branchId') // app-defined dimensions
108
- requireOrgInScope(targetId) // parent-child org hierarchy
109
- allOf(check1, check2, ...)
110
- anyOf(check1, check2, ...)
111
- not(check)
112
- when(condition, ifTrue, ifFalse)
113
- denyAll()
66
+ allowPublic() requireOrgRole(['admin'])
67
+ requireAuth() requireTeamMembership()
68
+ requireRoles(['admin']) requireServiceScope('jobs:bulk')
69
+ requireOwnership('userId') requireScopeContext('branchId')
70
+ requireOrgMembership() requireOrgInScope(targetId)
71
+ allOf(...) · anyOf(...) · not(...) · when(...) · denyAll()
114
72
  createDynamicPermissionMatrix({ resolveRolePermissions, cacheStore })
115
73
  ```
116
74
 
117
- Convenience bundles: `publicRead()`, `publicReadAdminWrite()`, `adminOnly()`, `ownerWithAdminBypass()`.
118
-
119
- `PermissionCheck` returns `boolean | { granted, reason?, filters?, scope? }`. `filters` flow into the repo query (row-level ABAC). `scope` stamps attributes downstream.
120
-
121
- ### Field-level
122
- ```typescript
123
- import { fields } from '@classytic/arc';
124
- fieldRules: {
125
- password: fields.hidden(),
126
- salary: fields.visibleTo(['admin', 'hr']),
127
- role: fields.writableBy(['admin']),
128
- email: fields.redactFor(['viewer'], '***'),
129
- }
130
- ```
75
+ Returns `boolean | { granted, reason?, filters?, scope? }`. `filters` propagate into the repo query (row-level ABAC).
131
76
 
132
- ---
77
+ Field-level: `fields.hidden()`, `fields.visibleTo([...])`, `fields.writableBy([...])`, `fields.redactFor([...], '***')`.
133
78
 
134
- ## RequestScope — five kinds
79
+ ## RequestScope
135
80
 
136
81
  ```typescript
137
82
  type RequestScope =
138
83
  | { kind: 'public' }
139
84
  | { kind: 'authenticated'; userId?; userRoles? }
140
85
  | { kind: 'member'; userId?; userRoles; organizationId; orgRoles; teamId?; context?; ancestorOrgIds? }
141
- | { kind: 'service'; clientId; organizationId; scopes?; context?; ancestorOrgIds? }
86
+ | { kind: 'service'; clientId; organizationId; scopes?; context?; ancestorOrgIds?; mandate?; dpopJkt? }
142
87
  | { kind: 'elevated'; userId?; organizationId?; elevatedBy; context?; ancestorOrgIds? };
143
88
  ```
144
89
 
145
- **Always read via accessors from `@classytic/arc/scope`:**
146
- ```typescript
147
- isPublic, isAuthenticated, isMember, isService, isElevated, hasOrgAccess,
148
- getUserId, getUserRoles, getOrgId, getOrgRoles, getTeamId, getClientId,
149
- getServiceScopes, getScopeContext, getScopeContextMap,
150
- getAncestorOrgIds, isOrgInScope, getRequestScope,
151
- requireUserId, requireClientId, // throw 401 (UnauthorizedError) if absent
152
- requireOrgId, requireTeamId, // throw 403 (OrgRequiredError) if absent
153
- createTenantKeyGenerator,
154
- ```
155
-
156
- | Helper | `member` | `service` | `elevated` |
157
- |---|---|---|---|
158
- | `requireOrgMembership()` | ✅ | ✅ | ✅ |
159
- | `requireOrgRole(roles)` | role match | ❌ deny | ✅ bypass |
160
- | `requireServiceScope(scopes)` | ❌ | scope match | ✅ bypass |
161
- | `requireScopeContext(...)` | key match | key match | ✅ bypass |
162
- | `requireTeamMembership()` | `teamId` set | n/a | ✅ bypass |
163
- | `requireOrgInScope(target)` | target in chain | target in chain | ✅ bypass |
164
-
165
- ---
90
+ Always access via `@classytic/arc/scope`: `getUserId`, `getOrgId`, `hasOrgAccess`, `requireOrgId` (throws 403), `requireUserId` (throws 401), `getScopeContext`, `isOrgInScope`. **Never read `scope.organizationId` directly.**
166
91
 
167
92
  ## Presets
168
93
 
@@ -171,237 +96,90 @@ createTenantKeyGenerator,
171
96
  | `softDelete` | `GET /deleted`, `POST /:id/restore` | `{ deletedField }` |
172
97
  | `slugLookup` | `GET /slug/:slug` | `{ slugField }` |
173
98
  | `tree` | `GET /tree`, `GET /:parent/children` | `{ parentField }` |
174
- | `ownedByUser` | none (middleware) | `{ ownerField }` |
175
- | `multiTenant` | none (middleware) | `{ tenantField }` OR `{ tenantFields: TenantFieldSpec[] }` |
176
- | `audited` | none (middleware) | — |
99
+ | `ownedByUser` | (middleware) | `{ ownerField }` |
100
+ | `multiTenant` | (middleware) | `{ tenantField }` or `{ tenantFields: [...] }` |
101
+ | `audited` | (middleware) | — |
177
102
  | `bulk` | `POST/PATCH/DELETE /bulk` | `{ operations?, maxCreateItems? }` |
178
- | `filesUpload` | `POST /upload`, `GET /:id`, `DELETE /:id` | `{ storage, sanitizeFilename?, allowedMimeTypes?, maxFileSize? }` |
179
- | `search` | `POST /search`, `/search-similar`, `/embed` | `{ repository?, search?, similar?, embed?, routes? }` |
180
-
181
- ```typescript
182
- presets: ['softDelete', { name: 'multiTenant', tenantField: 'organizationId' }]
183
- ```
184
-
185
- ---
186
-
187
- ## Hooks
188
-
189
- Inline (per-resource):
190
- ```typescript
191
- hooks: {
192
- beforeCreate: async (ctx) => { /* ctx.data, ctx.user, ctx.meta */ },
193
- afterCreate: async (ctx) => { ... },
194
- beforeUpdate: async (ctx) => { /* ctx.meta.existing has the pre-image */ },
195
- afterUpdate: async (ctx) => { ... },
196
- beforeDelete: async (ctx) => { ... },
197
- afterDelete: async (ctx) => { ... },
198
- }
199
- ```
200
-
201
- App-level (cross-resource):
202
- ```typescript
203
- import { createHookSystem, beforeCreate, afterUpdate } from '@classytic/arc/hooks';
204
-
205
- const hooks = createHookSystem();
206
- beforeCreate(hooks, 'product', async (ctx) => { ctx.data.slug = slugify(ctx.data.name); });
207
- ```
208
-
209
- Pipeline (for finer control):
210
- ```typescript
211
- import { guard, transform, intercept } from '@classytic/arc/pipeline';
212
-
213
- pipe: {
214
- create: [
215
- guard('verified', async (ctx) => ctx.user?.verified === true),
216
- transform('inject', async (ctx) => { ctx.body.createdBy = ctx.user._id; }),
217
- ],
218
- }
219
- ```
220
-
221
- ---
222
-
223
- ## Events
103
+ | `filesUpload` | `POST /upload`, `GET /:id`, `DELETE /:id` | `{ storage, sanitizeFilename?, }` |
104
+ | `search` | `POST /search`, `/search-similar`, `/embed` | `{ repository?, search?, similar?, embed? }` |
224
105
 
225
- ```typescript
226
- events: { created: {}, updated: {}, deleted: {}, custom: { description, schema } }
227
- ```
228
-
229
- CRUD events auto-emit. Custom: `await req.fastify.events.publish(eventType, payload)`. Subscribe: `app.events.subscribe('order.*', handler)`.
106
+ ## fieldRules flags
230
107
 
231
- **Transports:** `MemoryEventTransport` · `RedisEventTransport` (pub/sub fire-and-forget) · `RedisStreamTransport` (durable, at-least-once, consumer groups, DLQ) · `EventOutbox` (transactional outbox).
108
+ `systemManaged` · `preserveForElevated` · `immutable` / `immutableAfterCreate` · `optional` · `nullable` · `hidden` · `minLength` / `maxLength` / `min` / `max` / `pattern` / `enum` · `description`.
232
109
 
233
- ---
234
-
235
- ## Cache (QueryCache)
110
+ ## Aggregations
236
111
 
237
112
  ```typescript
238
- cache: {
239
- staleTime: 30, gcTime: 300, tags: ['catalog'],
240
- invalidateOn: { 'category.*': ['catalog'] },
241
- list: { staleTime: 60 },
242
- byId: { staleTime: 10 },
243
- }
244
- ```
245
- Modes: `memory` (default) | `distributed` (`stores.queryCache: RedisCacheStore`). Response: `x-cache: HIT | STALE | MISS`.
246
-
247
- ---
248
-
249
- ## Aggregations (declarative dashboards)
250
-
251
- ```typescript
252
- import { defineAggregation } from '@classytic/arc';
253
-
254
113
  aggregations: {
255
114
  byMethod: defineAggregation({
256
115
  groupBy: 'method',
257
116
  measures: { total: 'sum:amount', count: 'count' },
258
- sort: { total: -1 },
259
- cache: { staleTime: 60, swr: true, tags: ['revenue'] },
260
- permissions: canViewRevenue(),
261
- }),
262
- byDay: defineAggregation({
263
- dateBuckets: { day: { field: 'createdAt', interval: 'day' } },
264
- groupBy: 'flow',
265
- measures: { total: 'sum:amount' },
266
117
  requireDateRange: { field: 'createdAt', maxRangeDays: 365 },
118
+ cache: { staleTime: 60, swr: true, tags: ['revenue'] },
267
119
  permissions: canViewRevenue(),
268
120
  }),
269
121
  }
270
122
  ```
271
123
 
272
- Registers `GET /:prefix/aggregations/:name` per entry. Same permissions, OpenAPI, MCP tool, cache + tag invalidation as CRUD. Tenant flows via the kit's multi-tenant plugin (string → ObjectId casting handled by the kit, **not** by hand). Caller filters via query string (`?status=verified&createdAt[gte]=...`) compose with the declaration. Safety: `requireFilters`, `requireDateRange`, `maxGroups`. **Anti-pattern:** custom routes calling `Model.aggregate([...])` directly — see anti-patterns §33.
273
-
274
- ---
124
+ Registers `GET /:prefix/aggregations/:name`. Same perms + cache + tag invalidation + MCP tool as CRUD. **Anti-pattern:** custom routes calling `Model.aggregate([...])` directly.
275
125
 
276
126
  ## CLI
277
127
 
278
128
  ```bash
279
- arc init my-api --mongokit --jwt --ts # scaffold (also: --custom, --better-auth, --multi)
280
- arc generate resource product # generate resource files
281
- arc generate resource product --mcp # + MCP tools file
282
- arc generate mcp analytics # standalone MCP tools file
283
- arc docs ./openapi.json --entry ./dist/index.js # emit OpenAPI
284
- arc introspect --entry ./dist/index.js # list registered resources
285
- arc describe product # detail a resource's routes/actions/permissions
286
- arc doctor # diagnose env
287
- ```
288
-
289
- `.arcrc`: project config used by `arc generate`. Set `"mcp": true` to always emit `.mcp.ts` alongside resources.
290
-
291
- Generated layout:
292
- ```
293
- src/resources/{name}/
294
- {name}.model.ts # Mongoose schema (with --mongokit)
295
- {name}.repository.ts # Repository class (mongokit Repository)
296
- {name}.resource.ts # defineResource() config
297
- {name}.mcp.ts # (optional) custom MCP tools
298
- ```
299
-
300
- Naming: kebab input (`org-profile`) → PascalCase class (`OrgProfile`), camelCase var (`orgProfile`), kebab files.
301
-
302
- ---
303
-
304
- ## MCP
305
-
306
- ```typescript
307
- import { mcpPlugin } from '@classytic/arc/mcp';
308
-
309
- await app.register(mcpPlugin, {
310
- resources: [productResource, orderResource],
311
- auth: false, // | getAuth() | custom function
312
- exclude: ['credential'],
313
- overrides: { product: { operations: ['list', 'get'] } },
314
- });
315
-
316
- // Stateful (server-initiated messages)
317
- await app.register(mcpPlugin, { resources, stateful: true, sessionTtlMs: 600000 });
129
+ arc init my-api --mongokit --better-auth --ts
130
+ arc generate resource product [--mcp]
131
+ arc docs ./openapi.json --entry ./dist/index.js
132
+ arc introspect --entry ./dist/index.js
133
+ arc describe ./dist/resources.js --json
134
+ arc doctor
318
135
  ```
319
136
 
320
- Auto-generates 5 CRUD tools per resource + custom routes + actions. Permissions and field rules carry through. Connect via `claude mcp add --transport http my-api http://localhost:3000/mcp`.
321
-
322
- Custom tools alongside resources: co-locate `order.mcp.ts`, wire via `extraTools: [...]`. AI SDK bridge: `buildMcpToolsFromBridges([bridge])`.
323
-
324
- ---
137
+ Generated layout: `src/resources/{name}/{name}.{model,repository,resource,mcp}.ts`. Naming: `org-profile` (kebab input) `OrgProfile` (class) / `orgProfile` (var) / `org-profile.*.ts` (files).
325
138
 
326
139
  ## Adapters
327
140
 
328
- In arc 2.12, every kit-specific adapter ships from its kit's `/adapter` subpath. Arc has zero kit-bound adapters. The cross-framework contract lives in `@classytic/repo-core/adapter`.
141
+ Every kit ships its adapter from `@classytic/<kit>/adapter`. Arc has **zero** kit-bound adapters in `src/` since 2.12.
329
142
 
330
143
  ```typescript
331
- // Mongoose — from mongokit
332
144
  import { createMongooseAdapter } from '@classytic/mongokit/adapter';
333
- import { buildCrudSchemasFromModel, Repository } from '@classytic/mongokit';
145
+ import { createDrizzleAdapter } from '@classytic/sqlitekit/adapter';
146
+ import { createPrismaAdapter } from '@classytic/prismakit/adapter';
334
147
 
335
- const adapter = createMongooseAdapter({
336
- model: ProductModel,
337
- repository: new Repository(ProductModel),
338
- schemaGenerator: buildCrudSchemasFromModel,
339
- });
340
-
341
- // Drizzle — from sqlitekit
342
- import { createDrizzleAdapter } from '@classytic/sqlitekit/adapter';
343
- import { buildCrudSchemasFromTable } from '@classytic/sqlitekit';
344
-
345
- // Prisma — from prismakit
346
- import { createPrismaAdapter } from '@classytic/prismakit/adapter';
148
+ import type { DataAdapter, RepositoryLike } from '@classytic/repo-core/adapter';
149
+ // RepositoryLike<TDoc> = MinimalRepo<TDoc> & Partial<StandardRepo<TDoc>>
347
150
  ```
348
151
 
349
- Custom adapter: implement `DataAdapter` / `MinimalRepo<TDoc>` from `@classytic/repo-core/adapter` (5-method floor). Any kit (mongokit, sqlitekit, prismakit, future pgkit, custom) plugs in identically. `RepositoryLike<TDoc> = MinimalRepo<TDoc> & Partial<StandardRepo<TDoc>>` — arc feature-detects optional methods at call sites. Arc re-exports `RepositoryLike`; the rest of the contract types come from `@classytic/repo-core/adapter` directly.
350
-
351
- | Plugin | Required methods on repo |
152
+ | Plugin | Required repo methods |
352
153
  |---|---|
353
154
  | `auditPlugin` | `create`, `findAll` |
354
155
  | `idempotencyPlugin` | `getOne`, `deleteMany`, `findOneAndUpdate` |
355
156
  | `EventOutbox` | `create`, `getOne`, `findAll`, `deleteMany`, `findOneAndUpdate` |
356
157
 
357
- ---
358
-
359
- ## Plugins (`@classytic/arc/plugins`)
360
-
361
- ```typescript
362
- import {
363
- healthPlugin, gracefulShutdownPlugin, ssePlugin,
364
- metricsPlugin, versioningPlugin,
365
- } from '@classytic/arc/plugins';
366
- import { tracingPlugin } from '@classytic/arc/plugins/tracing';
367
- import { auditPlugin } from '@classytic/arc/audit';
368
- import { idempotencyPlugin } from '@classytic/arc/idempotency';
369
- import { jobsPlugin } from '@classytic/arc/integrations/jobs';
370
- import { websocketPlugin } from '@classytic/arc/integrations/websocket';
371
- import { eventGatewayPlugin } from '@classytic/arc/integrations/event-gateway';
372
- import { webhookPlugin } from '@classytic/arc/integrations/webhooks';
373
- ```
374
-
375
- ---
376
-
377
- ## Subpath imports (tree-shakeable)
158
+ ## Subpath imports — audit signals
378
159
 
379
160
  ```typescript
380
- import { defineResource, BaseController, allowPublic, requireRoles } from '@classytic/arc';
381
- import { createApp, loadResources } from '@classytic/arc/factory';
382
- import { MemoryCacheStore, RedisCacheStore, QueryCache } from '@classytic/arc/cache';
383
- import { createBetterAuthAdapter } from '@classytic/arc/auth';
384
- import { eventPlugin, EventOutbox } from '@classytic/arc/events';
385
- import { RedisEventTransport } from '@classytic/arc/events/redis';
386
- import { mcpPlugin, defineTool } from '@classytic/arc/mcp';
387
- import { bulkPreset, multiTenantPreset } from '@classytic/arc/presets';
388
- import { isMember, getUserId, getOrgId, hasOrgAccess } from '@classytic/arc/scope';
389
- import { createTestApp, expectArc } from '@classytic/arc/testing';
390
- import { multipartBody } from '@classytic/arc/middleware';
391
- import { defineGuard, withCompensation, CircuitBreaker, createStateMachine } from '@classytic/arc/utils';
161
+ import { defineResource, BaseController, allowPublic } from '@classytic/arc';
162
+ import { createApp, loadResources } from '@classytic/arc/factory';
163
+ import { MemoryCacheStore, RedisCacheStore, QueryCache } from '@classytic/arc/cache';
164
+ import { eventPlugin, EventOutbox } from '@classytic/arc/events';
165
+ import { RedisEventTransport, RedisStreamTransport } from '@classytic/arc/events/redis-stream';
166
+ import { mcpPlugin, defineTool } from '@classytic/arc/mcp';
167
+ import { bulkPreset, multiTenantPreset } from '@classytic/arc/presets';
168
+ import { isMember, getUserId, getOrgId, requireOrgId } from '@classytic/arc/scope';
169
+ import { createTestApp, expectArc } from '@classytic/arc/testing';
170
+ import { multipartBody } from '@classytic/arc/middleware';
171
+ import { defineGuard, withCompensation } from '@classytic/arc/utils';
392
172
  ```
393
173
 
394
- Audit signal: a project importing only from the `@classytic/arc` root barrel is probably under-using subpath features (caching, scope accessors, presets, MCP, testing harness).
395
-
396
- ---
174
+ A project importing only from the root barrel is probably under-using subpath features (caching, scope accessors, presets, MCP, testing harness).
397
175
 
398
- ## Non-negotiable conventions (mirror in client projects)
176
+ ## Non-negotiables (mirror in client projects)
399
177
 
400
178
  1. No `console.log` in `src/` (except `cli/`) — use logger.
401
- 2. No `mongoose`/`drizzle-orm`/`@prisma/client` imports anywhere in the host outside the host's adapter wiring file. Every kit-specific adapter factory (`createMongooseAdapter` / `createDrizzleAdapter` / `createPrismaAdapter`) MUST come from the kit's `/adapter` subpath, never from `@classytic/arc` — the `@classytic/arc/adapters` subpath was removed in arc 2.12.
179
+ 2. No `mongoose` / `drizzle-orm` / `@prisma/client` imports outside the host's adapter wiring file.
402
180
  3. No `any` — use `unknown`. No `@ts-ignore` — fix the type.
403
- 4. No default exports in `src/` (knip enforces in arc; recommend in clients).
404
- 5. Always read `request.user` via guard or use `@classytic/arc/scope` accessors.
181
+ 4. No default exports in `src/` (knip enforces in arc).
182
+ 5. Always read `request.user` via guard, or use `@classytic/arc/scope` accessors.
405
183
  6. Always use `req.rawBody` for `verifySignature(...)`, never parsed body.
406
- 7. Set headers in `onRequest` or `preSerialization`, never `onSend`.
184
+ 7. Set response headers in `onRequest` or `preSerialization`, never `onSend`.
407
185
  8. `request.user: Record<string, unknown> | undefined` — required property, NOT optional.