@classytic/arc 2.11.3 → 2.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +27 -18
  2. package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
  3. package/dist/EventTransport-CT_52aWU.d.mts +34 -0
  4. package/dist/EventTransport-DLWoUMHy.mjs +103 -0
  5. package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
  6. package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
  7. package/dist/audit/index.d.mts +2 -2
  8. package/dist/audit/index.mjs +1 -1
  9. package/dist/auth/audit.d.mts +199 -0
  10. package/dist/auth/audit.mjs +288 -0
  11. package/dist/auth/index.d.mts +5 -5
  12. package/dist/auth/index.mjs +117 -191
  13. package/dist/auth/redis-session.d.mts +1 -1
  14. package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
  15. package/dist/buildHandler-olo-gt94.mjs +610 -0
  16. package/dist/cache/index.d.mts +3 -3
  17. package/dist/cache/index.mjs +3 -3
  18. package/dist/cli/commands/describe.d.mts +89 -13
  19. package/dist/cli/commands/describe.mjs +56 -2
  20. package/dist/cli/commands/docs.mjs +2 -2
  21. package/dist/cli/commands/generate.mjs +147 -48
  22. package/dist/cli/commands/init.d.mts +13 -0
  23. package/dist/cli/commands/init.mjs +237 -112
  24. package/dist/cli/commands/introspect.mjs +8 -1
  25. package/dist/context/index.mjs +1 -1
  26. package/dist/core/index.d.mts +3 -3
  27. package/dist/core/index.mjs +5 -5
  28. package/dist/core-D72ia0EH.mjs +1399 -0
  29. package/dist/{createActionRouter-u3ql2EDo.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
  30. package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
  31. package/dist/{createApp-BFxtdKy6.mjs → createApp-XX2-N0Yd.mjs} +31 -27
  32. package/dist/defineEvent-D5h7EvAx.mjs +188 -0
  33. package/dist/docs/index.d.mts +2 -2
  34. package/dist/docs/index.mjs +2 -2
  35. package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
  36. package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
  37. package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
  38. package/dist/errors-j4aJm1Wg.mjs +184 -0
  39. package/dist/{eventPlugin-KrFIQ097.mjs → eventPlugin-CaKTYkYM.mjs} +35 -137
  40. package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-qXpqTebY.d.mts} +57 -7
  41. package/dist/events/index.d.mts +164 -5
  42. package/dist/events/index.mjs +133 -209
  43. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  44. package/dist/events/transports/redis-stream-entry.mjs +204 -31
  45. package/dist/events/transports/redis.d.mts +1 -1
  46. package/dist/factory/index.d.mts +2 -2
  47. package/dist/factory/index.mjs +2 -2
  48. package/dist/{fields-C8Y0XLAu.d.mts → fields-COhcH3fk.d.mts} +23 -2
  49. package/dist/hooks/index.d.mts +1 -1
  50. package/dist/hooks/index.mjs +1 -1
  51. package/dist/idempotency/index.d.mts +3 -3
  52. package/dist/idempotency/index.mjs +1 -20
  53. package/dist/idempotency/redis.d.mts +1 -1
  54. package/dist/idempotency/redis.mjs +1 -1
  55. package/dist/{index-BYCqHCVu.d.mts → index-BTqLEvhu.d.mts} +164 -4
  56. package/dist/{index-6u4_Gg6G.d.mts → index-BtW7qYwa.d.mts} +661 -281
  57. package/dist/{index-BdXnTPRj.d.mts → index-Ds61mrJE.d.mts} +50 -4
  58. package/dist/{index-DdQ3O9Pg.d.mts → index-Dz5IKsrE.d.mts} +360 -219
  59. package/dist/index.d.mts +6 -7
  60. package/dist/index.mjs +9 -10
  61. package/dist/integrations/event-gateway.d.mts +2 -2
  62. package/dist/integrations/event-gateway.mjs +1 -1
  63. package/dist/integrations/index.d.mts +2 -2
  64. package/dist/integrations/mcp/index.d.mts +2 -2
  65. package/dist/integrations/mcp/index.mjs +1 -1
  66. package/dist/integrations/mcp/testing.d.mts +1 -1
  67. package/dist/integrations/mcp/testing.mjs +1 -1
  68. package/dist/integrations/streamline.d.mts +60 -11
  69. package/dist/integrations/streamline.mjs +75 -85
  70. package/dist/integrations/websocket-redis.d.mts +1 -1
  71. package/dist/integrations/websocket.d.mts +1 -1
  72. package/dist/integrations/websocket.mjs +2 -8
  73. package/dist/middleware/index.d.mts +1 -1
  74. package/dist/middleware/index.mjs +2 -2
  75. package/dist/migrations/index.d.mts +23 -3
  76. package/dist/migrations/index.mjs +0 -7
  77. package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
  78. package/dist/{openapi-BGUn7Ki1.mjs → openapi-CiOMVW1p.mjs} +143 -13
  79. package/dist/org/index.d.mts +2 -2
  80. package/dist/org/index.mjs +1 -1
  81. package/dist/permissions/index.d.mts +3 -3
  82. package/dist/permissions/index.mjs +3 -3
  83. package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
  84. package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
  85. package/dist/pipeline/index.d.mts +1 -1
  86. package/dist/pipeline/index.mjs +1 -1
  87. package/dist/plugins/index.d.mts +18 -33
  88. package/dist/plugins/index.mjs +33 -13
  89. package/dist/plugins/response-cache.mjs +1 -1
  90. package/dist/plugins/tracing-entry.d.mts +1 -1
  91. package/dist/plugins/tracing-entry.mjs +1 -1
  92. package/dist/presets/filesUpload.d.mts +5 -5
  93. package/dist/presets/filesUpload.mjs +6 -9
  94. package/dist/presets/index.d.mts +1 -1
  95. package/dist/presets/index.mjs +1 -1
  96. package/dist/presets/multiTenant.d.mts +1 -1
  97. package/dist/presets/multiTenant.mjs +2 -2
  98. package/dist/presets/search.d.mts +2 -2
  99. package/dist/presets/search.mjs +6 -8
  100. package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
  101. package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
  102. package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
  103. package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
  104. package/dist/redis-stream-D6HzR1Z_.d.mts +232 -0
  105. package/dist/registry/index.d.mts +1 -1
  106. package/dist/registry/index.mjs +2 -2
  107. package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
  108. package/dist/{resourceToTools-ByZpgjeH.mjs → resourceToTools-C5coh64w.mjs} +224 -71
  109. package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
  110. package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
  111. package/dist/schemas/index.d.mts +100 -30
  112. package/dist/schemas/index.mjs +86 -29
  113. package/dist/scim/index.d.mts +264 -0
  114. package/dist/scim/index.mjs +963 -0
  115. package/dist/scope/index.d.mts +3 -3
  116. package/dist/scope/index.mjs +4 -4
  117. package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
  118. package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
  119. package/dist/testing/index.d.mts +2 -8
  120. package/dist/testing/index.mjs +16 -24
  121. package/dist/testing/storageContract.d.mts +1 -1
  122. package/dist/types/index.d.mts +4 -4
  123. package/dist/types/storage.d.mts +1 -1
  124. package/dist/{types-BH7dEGvU.d.mts → types-BvqwCCSx.d.mts} +77 -29
  125. package/dist/{types-tgR4Pt8F.d.mts → types-CTYvcwHe.d.mts} +195 -1
  126. package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
  127. package/dist/{types-9beEMe25.d.mts → types-DQHFc8PM.d.mts} +1 -1
  128. package/dist/utils/index.d.mts +2 -2
  129. package/dist/utils/index.mjs +5 -5
  130. package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
  131. package/dist/{versioning-M9lNLhO8.d.mts → versioning-DTTvc80y.d.mts} +1 -1
  132. package/package.json +24 -34
  133. package/skills/arc/SKILL.md +521 -785
  134. package/skills/arc/references/agent-auth.md +238 -0
  135. package/skills/arc/references/api-reference.md +187 -0
  136. package/skills/arc/references/auth.md +354 -7
  137. package/skills/arc/references/enterprise-auth.md +94 -0
  138. package/skills/arc/references/events.md +8 -6
  139. package/skills/arc/references/mcp.md +2 -2
  140. package/skills/arc/references/multi-tenancy.md +11 -2
  141. package/skills/arc/references/production.md +10 -9
  142. package/skills/arc/references/scim.md +247 -0
  143. package/skills/arc/references/testing.md +1 -1
  144. package/skills/arc-code-review/SKILL.md +141 -0
  145. package/skills/arc-code-review/references/anti-patterns.md +911 -0
  146. package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
  147. package/skills/arc-code-review/references/migration-recipes.md +700 -0
  148. package/skills/arc-code-review/references/mongokit-migration.md +386 -0
  149. package/skills/arc-code-review/references/scaffolding.md +230 -0
  150. package/skills/arc-code-review/references/severity.md +127 -0
  151. package/dist/EventTransport-CfVEGaEl.d.mts +0 -293
  152. package/dist/adapters/index.d.mts +0 -3
  153. package/dist/adapters/index.mjs +0 -2
  154. package/dist/adapters-D0tT2Tyo.mjs +0 -949
  155. package/dist/auth/mongoose.d.mts +0 -191
  156. package/dist/auth/mongoose.mjs +0 -73
  157. package/dist/core-DnUsRpuX.mjs +0 -1049
  158. package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
  159. package/dist/errorHandler-Co3lnVmJ.d.mts +0 -114
  160. package/dist/errors-D5c-5BJL.mjs +0 -232
  161. package/dist/index-BbMrcvGp.d.mts +0 -362
  162. package/dist/redis-stream-CM8TXTix.d.mts +0 -110
  163. /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
  164. /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
  165. /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
  166. /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
  167. /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
  168. /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
  169. /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
  170. /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
  171. /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
  172. /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
  173. /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
  174. /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
  175. /package/dist/{pluralize-BneOJkpi.mjs → pluralize-DQgqgifU.mjs} +0 -0
  176. /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
  177. /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
  178. /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
  179. /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
  180. /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
  181. /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
  182. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
  183. /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
  184. /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
  185. /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
@@ -0,0 +1,380 @@
1
+ # Arc Cheatsheet — what arc provides, in one page
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/`.
4
+
5
+ ---
6
+
7
+ ## Boot order (FIXED — don't reorder)
8
+
9
+ ```
10
+ 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
17
+ ```
18
+
19
+ When auditing: top-level `await ensureCatalogEngine()` in a `*.resource.ts` file = lifecycle violation. Use `resources: async (fastify) => [...]`.
20
+
21
+ ---
22
+
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
44
+
45
+ ```typescript
46
+ 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,
75
+ });
76
+ ```
77
+
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.
92
+
93
+ ---
94
+
95
+ ## Permissions — combinators
96
+
97
+ From `@classytic/arc`:
98
+ ```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()
114
+ createDynamicPermissionMatrix({ resolveRolePermissions, cacheStore })
115
+ ```
116
+
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
+ ```
131
+
132
+ ---
133
+
134
+ ## RequestScope — five kinds
135
+
136
+ ```typescript
137
+ type RequestScope =
138
+ | { kind: 'public' }
139
+ | { kind: 'authenticated'; userId?; userRoles? }
140
+ | { kind: 'member'; userId?; userRoles; organizationId; orgRoles; teamId?; context?; ancestorOrgIds? }
141
+ | { kind: 'service'; clientId; organizationId; scopes?; context?; ancestorOrgIds? }
142
+ | { kind: 'elevated'; userId?; organizationId?; elevatedBy; context?; ancestorOrgIds? };
143
+ ```
144
+
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
+ ---
166
+
167
+ ## Presets
168
+
169
+ | Preset | Routes added | Config |
170
+ |---|---|---|
171
+ | `softDelete` | `GET /deleted`, `POST /:id/restore` | `{ deletedField }` |
172
+ | `slugLookup` | `GET /slug/:slug` | `{ slugField }` |
173
+ | `tree` | `GET /tree`, `GET /:parent/children` | `{ parentField }` |
174
+ | `ownedByUser` | none (middleware) | `{ ownerField }` |
175
+ | `multiTenant` | none (middleware) | `{ tenantField }` OR `{ tenantFields: TenantFieldSpec[] }` |
176
+ | `audited` | none (middleware) | — |
177
+ | `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
224
+
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)`.
230
+
231
+ **Transports:** `MemoryEventTransport` · `RedisEventTransport` (pub/sub fire-and-forget) · `RedisStreamTransport` (durable, at-least-once, consumer groups, DLQ) · `EventOutbox` (transactional outbox).
232
+
233
+ ---
234
+
235
+ ## Cache (QueryCache)
236
+
237
+ ```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
+ ## CLI
250
+
251
+ ```bash
252
+ arc init my-api --mongokit --jwt --ts # scaffold (also: --custom, --better-auth, --multi)
253
+ arc generate resource product # generate resource files
254
+ arc generate resource product --mcp # + MCP tools file
255
+ arc generate mcp analytics # standalone MCP tools file
256
+ arc docs ./openapi.json --entry ./dist/index.js # emit OpenAPI
257
+ arc introspect --entry ./dist/index.js # list registered resources
258
+ arc describe product # detail a resource's routes/actions/permissions
259
+ arc doctor # diagnose env
260
+ ```
261
+
262
+ `.arcrc`: project config used by `arc generate`. Set `"mcp": true` to always emit `.mcp.ts` alongside resources.
263
+
264
+ Generated layout:
265
+ ```
266
+ src/resources/{name}/
267
+ {name}.model.ts # Mongoose schema (with --mongokit)
268
+ {name}.repository.ts # Repository class (mongokit Repository)
269
+ {name}.resource.ts # defineResource() config
270
+ {name}.mcp.ts # (optional) custom MCP tools
271
+ ```
272
+
273
+ Naming: kebab input (`org-profile`) → PascalCase class (`OrgProfile`), camelCase var (`orgProfile`), kebab files.
274
+
275
+ ---
276
+
277
+ ## MCP
278
+
279
+ ```typescript
280
+ import { mcpPlugin } from '@classytic/arc/mcp';
281
+
282
+ await app.register(mcpPlugin, {
283
+ resources: [productResource, orderResource],
284
+ auth: false, // | getAuth() | custom function
285
+ exclude: ['credential'],
286
+ overrides: { product: { operations: ['list', 'get'] } },
287
+ });
288
+
289
+ // Stateful (server-initiated messages)
290
+ await app.register(mcpPlugin, { resources, stateful: true, sessionTtlMs: 600000 });
291
+ ```
292
+
293
+ 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`.
294
+
295
+ Custom tools alongside resources: co-locate `order.mcp.ts`, wire via `extraTools: [...]`. AI SDK bridge: `buildMcpToolsFromBridges([bridge])`.
296
+
297
+ ---
298
+
299
+ ## Adapters
300
+
301
+ 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`.
302
+
303
+ ```typescript
304
+ // Mongoose — from mongokit
305
+ import { createMongooseAdapter } from '@classytic/mongokit/adapter';
306
+ import { buildCrudSchemasFromModel, Repository } from '@classytic/mongokit';
307
+
308
+ const adapter = createMongooseAdapter({
309
+ model: ProductModel,
310
+ repository: new Repository(ProductModel),
311
+ schemaGenerator: buildCrudSchemasFromModel,
312
+ });
313
+
314
+ // Drizzle — from sqlitekit
315
+ import { createDrizzleAdapter } from '@classytic/sqlitekit/adapter';
316
+ import { buildCrudSchemasFromTable } from '@classytic/sqlitekit';
317
+
318
+ // Prisma — from prismakit
319
+ import { createPrismaAdapter } from '@classytic/prismakit/adapter';
320
+ ```
321
+
322
+ 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.
323
+
324
+ | Plugin | Required methods on repo |
325
+ |---|---|
326
+ | `auditPlugin` | `create`, `findAll` |
327
+ | `idempotencyPlugin` | `getOne`, `deleteMany`, `findOneAndUpdate` |
328
+ | `EventOutbox` | `create`, `getOne`, `findAll`, `deleteMany`, `findOneAndUpdate` |
329
+
330
+ ---
331
+
332
+ ## Plugins (`@classytic/arc/plugins`)
333
+
334
+ ```typescript
335
+ import {
336
+ healthPlugin, gracefulShutdownPlugin, ssePlugin,
337
+ metricsPlugin, versioningPlugin,
338
+ } from '@classytic/arc/plugins';
339
+ import { tracingPlugin } from '@classytic/arc/plugins/tracing';
340
+ import { auditPlugin } from '@classytic/arc/audit';
341
+ import { idempotencyPlugin } from '@classytic/arc/idempotency';
342
+ import { jobsPlugin } from '@classytic/arc/integrations/jobs';
343
+ import { websocketPlugin } from '@classytic/arc/integrations/websocket';
344
+ import { eventGatewayPlugin } from '@classytic/arc/integrations/event-gateway';
345
+ import { webhookPlugin } from '@classytic/arc/integrations/webhooks';
346
+ ```
347
+
348
+ ---
349
+
350
+ ## Subpath imports (tree-shakeable)
351
+
352
+ ```typescript
353
+ import { defineResource, BaseController, allowPublic, requireRoles } from '@classytic/arc';
354
+ import { createApp, loadResources } from '@classytic/arc/factory';
355
+ import { MemoryCacheStore, RedisCacheStore, QueryCache } from '@classytic/arc/cache';
356
+ import { createBetterAuthAdapter } from '@classytic/arc/auth';
357
+ import { eventPlugin, EventOutbox } from '@classytic/arc/events';
358
+ import { RedisEventTransport } from '@classytic/arc/events/redis';
359
+ import { mcpPlugin, defineTool } from '@classytic/arc/mcp';
360
+ import { bulkPreset, multiTenantPreset } from '@classytic/arc/presets';
361
+ import { isMember, getUserId, getOrgId, hasOrgAccess } from '@classytic/arc/scope';
362
+ import { createTestApp, expectArc } from '@classytic/arc/testing';
363
+ import { multipartBody } from '@classytic/arc/middleware';
364
+ import { defineGuard, withCompensation, CircuitBreaker, createStateMachine } from '@classytic/arc/utils';
365
+ ```
366
+
367
+ 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).
368
+
369
+ ---
370
+
371
+ ## Non-negotiable conventions (mirror in client projects)
372
+
373
+ 1. No `console.log` in `src/` (except `cli/`) — use logger.
374
+ 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.
375
+ 3. No `any` — use `unknown`. No `@ts-ignore` — fix the type.
376
+ 4. No default exports in `src/` (knip enforces in arc; recommend in clients).
377
+ 5. Always read `request.user` via guard or use `@classytic/arc/scope` accessors.
378
+ 6. Always use `req.rawBody` for `verifySignature(...)`, never parsed body.
379
+ 7. Set headers in `onRequest` or `preSerialization`, never `onSend`.
380
+ 8. `request.user: Record<string, unknown> | undefined` — required property, NOT optional.