@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
@@ -67,7 +67,9 @@ const app = await createApp({
67
67
  await app.listen({ port: 8040, host: '0.0.0.0' });
68
68
  ```
69
69
 
70
- **Boot order (fixed):** `plugins` → `bootstrap[]` → `resources` → `afterResources` → `onReady`.
70
+ **Boot order (fixed):** `beforeBoot` → modules resolve → `plugins` → `bootstrap[]` → `resources` → `afterResources` → `onReady`.
71
+
72
+ `beforeBoot()` (2.21) runs before Fastify exists and before module thunks / resource files import — connect the DB there instead of dynamic-import ordering hacks.
71
73
 
72
74
  For async-booted engines, pass `resources` as a factory so it runs after `bootstrap[]`:
73
75
 
@@ -78,22 +80,19 @@ resources: async (fastify) => {
78
80
  }
79
81
  ```
80
82
 
81
- `loadResources(import.meta.url)` resolves `src/` in dev and `dist/` in prod. Use `loadResources(import.meta.url, { context: { engine } })` to thread engine handles into `(ctx) => defineResource(...)` default exports.
83
+ `loadResources(import.meta.url)` resolves `src/` in dev and `dist/` in prod. Use `loadResources(import.meta.url, { context: { engine } })` to thread engine handles into `(ctx) => defineResource(...)` default exports. `{ filter: (absPath) => boolean }` (2.21) gates files BEFORE they're imported — feature-flag manifests become one callback.
82
84
 
83
85
  ## defineResource()
84
86
 
85
87
  ```typescript
86
88
  import { defineResource, allowPublic, requireRoles, requireAuth } from '@classytic/arc';
87
89
  import { createMongooseAdapter } from '@classytic/mongokit/adapter';
88
- import { buildCrudSchemasFromModel } from '@classytic/mongokit';
89
90
 
90
91
  export default defineResource({
91
92
  name: 'product',
92
- adapter: createMongooseAdapter({
93
- model: ProductModel,
94
- repository: productRepo,
95
- schemaGenerator: buildCrudSchemasFromModel,
96
- }),
93
+ // mongokit >=3.21 defaults schemaGenerator to buildCrudSchemasFromModel;
94
+ // pass `schemaGenerator: false` to opt out.
95
+ adapter: createMongooseAdapter({ model: ProductModel, repository: productRepo }),
97
96
 
98
97
  presets: ['softDelete', 'slugLookup', { name: 'multiTenant', tenantField: 'organizationId' }],
99
98
 
@@ -495,6 +494,8 @@ errorHandler: {
495
494
  }
496
495
  ```
497
496
 
497
+ Modules ship their own via `defineModule({ errorMappers })` (2.21) — merged into the app handler at boot; host-level mappers keep priority.
498
+
498
499
  ## Testing
499
500
 
500
501
  ```typescript
@@ -625,6 +626,7 @@ Out of scope: SAML (use BA SAML plugin), session storage (BA `secondaryStorage`)
625
626
  await fastify.register(auditPlugin, { autoAudit: { perResource: true } });
626
627
 
627
628
  defineResource({ name: 'order', audit: true });
629
+ defineResource({ name: 'invoice', history: true }); // 2.22: injects GET /:id/history from audit rows; implies audit: true (gate: update→get→requireAuth; override via { permissions, limit })
628
630
  defineResource({ name: 'payment', audit: { operations: ['delete'] } });
629
631
  defineResource({ name: 'product' }); // not audited
630
632
 
@@ -632,6 +634,10 @@ defineResource({ name: 'product' }); // not audited
632
634
  await app.audit.custom('order', req.params.id, 'refund', { reason }, { user });
633
635
  ```
634
636
 
637
+ ## Usage counters + plan limits (2.22)
638
+
639
+ `@classytic/arc/usage` — `usagePlugin` decorates `fastify.usage` with per-actor, per-UTC-month counters (`record`/`summary`/`period`/`actorOf`; actor chain org → user → client → ip). Auto-tracks `api.requests` (default on) + `api.egress.bytes` (opt-in); recording is fail-safe (a throwing `UsageStore` never fails a request; `MemoryUsageStore` built in, Redis/kit backends are ~5 lines). Pair with `createApp({ rateLimit: { plan: { resolve, limits: { free: { max: 60 }, enterprise: false }, default: 'free' } } })` for per-plan ceilings — `false` = unlimited, unknown/throwing resolvers fall back to `default` then global `max`, and buckets follow the tenant key chain by default. Caveat: the limiter runs before route auth, so `resolve` sees the raw request.
640
+
635
641
  ## Adapters
636
642
 
637
643
  Cross-framework adapter contract lives in `@classytic/repo-core/adapter`. Every kit-specific adapter ships from its kit's `/adapter` subpath; arc itself has zero kit-bound adapters.
@@ -6,7 +6,19 @@ Arc ships heavily tree-shaken: every feature has its own subpath. Pay only for w
6
6
 
7
7
  ```typescript
8
8
  import { defineResource, BaseController, BaseCrudController, allowPublic } from '@classytic/arc';
9
+ // createApp rateLimit gains `plan: { resolve, limits, default }` (2.22) — per-plan ceilings, `false` = unlimited, fail-safe fallback.
9
10
  import { createApp, loadResources } from '@classytic/arc/factory';
11
+ import { defineModule, getModuleExports, orderModules } from '@classytic/arc/factory';
12
+ // Worker role (2.23) — same options object, headless process (no routes, no auth;
13
+ // events/jobs/schedules run). Primitive: `mountRoutes: false`; preset: 'worker'.
14
+ import { createWorker } from '@classytic/arc/factory';
15
+ import type { ArcWorker, CreateWorkerOptions } from '@classytic/arc/factory';
16
+
17
+ // Programmatic resource assembly (2.21) — for module authors composing
18
+ // base config + host seams without casts. Arrays concat, plain objects
19
+ // deep-merge, class instances last-win, `undefined` never clobbers.
20
+ import { mergeResourceConfig } from '@classytic/arc';
21
+ import type { ResourceSeams, SeamedResourceConfig, AdapterLike } from '@classytic/arc';
10
22
  ```
11
23
 
12
24
  ## Adapters (kit-owned, never arc)
@@ -52,6 +64,7 @@ import {
52
64
  requireAgentScope, requireMandate, requireDPoP,
53
65
  type RequireAgentScopeOptions, type RequireMandateOptions,
54
66
  allOf, anyOf, when, denyAll,
67
+ permissionMatrix, // 2.21: { read, write, delete? } → full CRUD map
55
68
  createDynamicPermissionMatrix, createRoleHierarchy,
56
69
  fields, roles,
57
70
  } from '@classytic/arc/permissions';
@@ -101,6 +114,8 @@ import { createEvent, createChildEvent, matchEventPattern } from '@classytic/pri
101
114
  import {
102
115
  healthPlugin, gracefulShutdownPlugin, ssePlugin,
103
116
  metricsPlugin, versioningPlugin,
117
+ schedulesPlugin, // 2.21: interval jobs, optional LockAdapter leader lease, no Redis
118
+ type ScheduleLockLike, type ScheduleDefinition,
104
119
  } from '@classytic/arc/plugins';
105
120
  import { tracingPlugin } from '@classytic/arc/plugins/tracing';
106
121
  import { auditPlugin } from '@classytic/arc/audit';
@@ -151,6 +166,21 @@ import {
151
166
  } from '@classytic/arc/presets';
152
167
  ```
153
168
 
169
+ ## Usage (2.22)
170
+
171
+ ```typescript
172
+ // Per-actor, per-period (UTC YYYY-MM) usage counters — the accounting layer for
173
+ // quotas, plan enforcement, and usage-based billing. Decorates `fastify.usage`.
174
+ // requireQuota = the enforcement half: PermissionCheck gating on counters
175
+ // (429 quota.exceeded with { kind, used, limit, period, resetsAt }; limit may
176
+ // be a number, false = unlimited, or a plan-aware resolver; fail-open default).
177
+ import { usagePlugin, MemoryUsageStore, usagePeriod, requireQuota } from '@classytic/arc/usage';
178
+ import type { UsageStore, UsageBucket, UsagePluginOptions, UsageMeter, QuotaOptions } from '@classytic/arc/usage';
179
+ // Canonical store contract + cross-kit conformance: @classytic/repo-core/usage +
180
+ // runUsageStoreContract from @classytic/repo-core/testing; Mongo adapter:
181
+ // createMongoUsageStore from @classytic/mongokit/usage (>=3.22).
182
+ ```
183
+
154
184
  ## Testing
155
185
 
156
186
  ```typescript
@@ -158,6 +188,10 @@ import {
158
188
  createTestApp, expectArc, createHttpTestHarness,
159
189
  TestAuthSession, TestAuthProvider, TestFixtures,
160
190
  runStorageContract,
191
+ bootModuleApp, // 2.22: real app around modules ("boots green ⇒ composes");
192
+ // t.exports<T>(name) typed accessor; DB via `database` seam —
193
+ // mongoMemoryDatabase default, any kit injectable
194
+ mongoMemoryDatabase,
161
195
  } from '@classytic/arc/testing';
162
196
  ```
163
197
 
@@ -71,6 +71,14 @@ const adapter = createBetterAuthAdapter({
71
71
  const app = await createApp({
72
72
  auth: { type: 'betterAuth', betterAuth: adapter },
73
73
  });
74
+
75
+ // BA-on-Mongo + `beforeBoot` (2.22): BA's mongodbAdapter needs an OPEN
76
+ // connection (`mongoose.connection.getClient()` throws pre-connect), so pass
77
+ // a THUNK — resolved during auth registration, after beforeBoot connects:
78
+ const app2 = await createApp({
79
+ beforeBoot: () => connectDatabase(),
80
+ auth: { type: 'betterAuth', betterAuth: () => createBetterAuthAdapter({ auth: getAuth(), orgContext: true }) },
81
+ });
74
82
  ```
75
83
 
76
84
  **Org context flow** (when `orgContext: true`):
@@ -80,6 +88,11 @@ const app = await createApp({
80
88
  4. Splits roles: `"admin,recruiter"` → `['admin', 'recruiter']`
81
89
  5. Sets `request.scope`: `{ kind: 'member', organizationId, orgRoles: string[], teamId? }`
82
90
 
91
+ **Sessions at scale (BA-native knobs — know these before reaching for arc's):**
92
+ - **Redis sessions**: use BA's `secondaryStorage` (sessions land there INSTEAD of the DB unless `session.storeSessionInDatabase: true`). Arc's `@classytic/arc/auth/redis` (`SessionStore`) is the JWT-strategy store — different layer, don't mix them.
93
+ - **Cookie cache gotcha that keeps orgContext correct**: BA's `session.cookieCache` never caches CUSTOM session fields (e.g. `activeOrganizationId`) — they're re-fetched per request, so arc's org scope can't go stale via the cookie. Cost: enabling cookieCache does NOT skip the org lookup.
94
+ - **Rate limiting**: BA ships its own (`rateLimit.storage: 'secondary-storage'`) for auth endpoints; arc's `rateLimit.skipPaths: ['/api/auth/*']` guidance composes — exempt BA routes from arc's bucket, let BA's limiter own its own surface.
95
+
83
96
  > **Arc 2.13+ requires `auth.api.*`** — pass a real `betterAuth()` instance. The pre-2.13 HTTP fallback for org/team lookups was retired.
84
97
 
85
98
  #### Table-side overlay — read BA's tables as arc resources
@@ -91,7 +104,7 @@ Better Auth writes its own tables (`user`, `organization`, `member`, `invitation
91
104
  ```typescript
92
105
  import { betterAuth } from 'better-auth';
93
106
  import { mongodbAdapter } from '@better-auth/mongo-adapter';
94
- import { organization } from 'better-auth/plugins';
107
+ import { organization } from 'better-auth/plugins/organization'; // dedicated path — tree-shaking (BA best practice)
95
108
  import mongoose from 'mongoose';
96
109
  import {
97
110
  createBetterAuthOverlay,
@@ -160,7 +173,7 @@ Same parallel structure as mongokit. The factory derives the Drizzle table dynam
160
173
  import Database from 'better-sqlite3';
161
174
  import { drizzle } from 'drizzle-orm/better-sqlite3';
162
175
  import { betterAuth } from 'better-auth';
163
- import { organization } from 'better-auth/plugins';
176
+ import { organization } from 'better-auth/plugins/organization'; // dedicated path — tree-shaking (BA best practice)
164
177
  import { createBetterAuthOverlay } from '@classytic/sqlitekit/better-auth';
165
178
 
166
179
  const sqlite = new Database('app.db');
@@ -239,7 +252,11 @@ The `auth.api.*` direct in-process API map and `auth.$context.tables` schema int
239
252
  ```typescript
240
253
  import { betterAuth } from 'better-auth';
241
254
  import { mongodbAdapter } from '@better-auth/mongo-adapter';
242
- import { admin, organization, twoFactor, bearer } from 'better-auth/plugins';
255
+ // Dedicated paths per plugin — tree-shaking (BA best practice; never the barrel)
256
+ import { admin } from 'better-auth/plugins/admin';
257
+ import { organization } from 'better-auth/plugins/organization';
258
+ import { twoFactor } from 'better-auth/plugins/two-factor';
259
+ import { bearer } from 'better-auth/plugins/bearer';
243
260
  import { apiKey } from '@better-auth/api-key';
244
261
  import { createBetterAuthOverlay, registerBetterAuthStubs } from '@classytic/mongokit/better-auth';
245
262
 
@@ -367,7 +384,7 @@ The plugin also adds a required `configId` field on `apikey` rows and uses `refe
367
384
  For non-cookie clients (React Native, native mobile, headless SPA), enable `bearer()` so sessions travel as `Authorization: Bearer <session-token>` instead of cookies. Same `auth.api.getSession()` path, just a different transport.
368
385
 
369
386
  ```typescript
370
- import { bearer } from 'better-auth/plugins';
387
+ import { bearer } from 'better-auth/plugins/bearer'; // dedicated path — tree-shaking
371
388
  const auth = betterAuth({ plugins: [bearer(), organization()] });
372
389
  ```
373
390
 
@@ -151,7 +151,7 @@ Full MCP spec-compliant OAuth 2.1: authorization code + PKCE, token exchange, dy
151
151
 
152
152
  ```typescript
153
153
  // auth.config.ts — add mcp() plugin to Better Auth
154
- import { mcp } from 'better-auth/plugins';
154
+ import { mcp } from 'better-auth/plugins/mcp'; // dedicated path — tree-shaking
155
155
  betterAuth({ plugins: [mcp({ loginPage: '/login' })] });
156
156
 
157
157
  // app.ts
@@ -79,7 +79,26 @@ await fastify.register(auditPlugin, {
79
79
  enabled: true,
80
80
  repository: new Repository(AuditModel),
81
81
  });
82
- // TTL / retention owned by your DB (TTL index on `timestamp`, cron DELETE, etc.)
82
+ // ── Retention (audit rows ARE `history: true` timelines — prune together) ──
83
+ // Option A (zero code, Mongo): TTL index — the DB does it server-side.
84
+ // AuditSchema.index({ timestamp: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 365 });
85
+ // Option B (built-in, single instance): retention: { maxAgeMs, purgeIntervalMs }.
86
+ // Option C (multi-replica, RECOMMENDED): purgeIntervalMs: 0 + a schedulesPlugin
87
+ // entry calling fastify.audit.purge(cutoff) — leader-safe, jittered, observable.
88
+ // (The built-in timer runs on EVERY replica — idempotent but wasteful.)
89
+ //
90
+ // ── Indexes (kit territory — the audit model is yours) ──
91
+ // Mongo: TTL index above + compound for the history timeline:
92
+ // AuditSchema.index({ resource: 1, documentId: 1, timestamp: -1 });
93
+ // SQL kits: index (resource, document_id, timestamp) + the kit's ttl/sweep plugin.
94
+ //
95
+ // ── Rich search (admin audit-log UI) ──
96
+ // audit.query() is deliberately narrow. For full repo-core filter/sort/keyset
97
+ // power + MCP tools, mount the audit repo AS a read-only resource:
98
+ // defineResource({ name: 'audit-log', crud: { list: true, get: true },
99
+ // adapter: { repository: auditRepo },
100
+ // queryParser: new QueryParser({ allowedFilterFields: ['resource','documentId','userId','action','organizationId'] }),
101
+ // permissions: { list: requireRoles(['auditor']), get: requireRoles(['auditor']) } });
83
102
 
84
103
  // Usage
85
104
  await fastify.audit.create('product', product._id, product, request.auditContext);
@@ -452,7 +471,30 @@ Production preset: `maxEventLoopDelay: 3000` (avoids false 503s on Render/Railwa
452
471
 
453
472
  ## CORS
454
473
 
455
- Production warns (not throws) when origin missing. `origin: '*'` from env is allowed. Smart CORS auto-converts `credentials: true` + `origin: '*'` to `origin: true`.
474
+ Production warns (not throws) when origin missing. `origin: '*'` from env is allowed. Smart CORS auto-converts `credentials: true` + `origin: '*'` to `origin: true`, and (2.22) auto-merges arc's protocol headers (`x-organization-id`, `x-arc-scope`, `x-request-id`) into any host-declared `allowedHeaders` allow-list — hosts never enumerate framework internals.
475
+
476
+ ### Mobile apps (Capacitor / Ionic / React Native webviews)
477
+
478
+ Arc needs no special mode — mobile is a CORS + auth-transport recipe:
479
+
480
+ ```typescript
481
+ cors: {
482
+ // Webview origins alongside your web domains. Also add these to
483
+ // Better Auth's trustedOrigins when using BA.
484
+ origin: [...webOrigins, 'https://localhost', 'capacitor://localhost'],
485
+ credentials: true,
486
+ // Optional allow-list — arc's own headers auto-merge (2.22); you only
487
+ // list YOUR custom ones. Omit entirely to use reflection mode.
488
+ allowedHeaders: ['Content-Type', 'Authorization'],
489
+ // BA bearer plugin returns the fresh session token in this RESPONSE
490
+ // header — without exposing it, cross-origin JS cannot READ it and
491
+ // app login dies silently. The one mobile knob arc can't infer.
492
+ exposedHeaders: ['set-auth-token'],
493
+ },
494
+ trustProxy: true, // webviews arrive through your LB/tunnel
495
+ ```
496
+
497
+ Native (non-webview) HTTP clients send no `Origin` header — CORS doesn't apply to them at all; bearer auth just works.
456
498
 
457
499
  ## Metrics Plugin
458
500
 
@@ -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:** 2.16.x · **Mongokit:** <version or "not installed"> · **Sqlitekit:** <version or "n/a"> · **Date:** <YYYY-MM-DD>
87
+ **Arc version:** <installed version> · **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
@@ -565,6 +565,8 @@ resources: async () => loadResources(import.meta.url, { context: { engine } }),
565
565
  // Each *.resource.ts then exports: (ctx) => defineResource({ adapter: ctx.engine.adapter() })
566
566
  ```
567
567
 
568
+ For the DB **connection** itself (as opposed to engines), arc 2.21 adds `createApp({ beforeBoot: async () => connectDatabase() })` — awaited before Fastify exists, before module thunks resolve, and before any resource file imports. A composition root that connects the DB then dynamic-imports the app to control evaluation order is the pre-2.21 workaround; flag it and move the connect into `beforeBoot`.
569
+
568
570
  ---
569
571
 
570
572
  ## §21. Field-write denied as silent strip when reject is needed (or vice versa) 🟡
@@ -803,31 +805,19 @@ const userId = requireUserId(scope, 'finalize-checkout'); // hint surfaces in
803
805
 
804
806
  ---
805
807
 
806
- ## §32f. `createMongooseAdapter` without `schemaGenerator` 🟠
808
+ ## §32f. Missing / dodged `schemaGenerator` 🟠
807
809
 
808
810
  **Detection:**
809
811
  ```
810
812
  pattern: "createMongooseAdapter\\s*\\(\\s*\\{[^}]*\\}\\s*\\)"
811
813
  multiline: true
812
814
  ```
813
- Then check whether the object literal includes a `schemaGenerator:` key.
814
-
815
- Also: `createDrizzleAdapter` without `schemaGenerator: buildCrudSchemasFromTable`.
815
+ Then check the `schemaGenerator:` key — what to flag depends on the kit version:
816
816
 
817
- **Why high:** Arc 2.12 deleted the built-in mongoose / drizzle schema-gen fallback (~290 LOC). If `schemaGenerator` is omitted, OpenAPI bodies are emitted as `null` rather than silently inferred — the route still serves traffic but the docs / MCP tool input schemas are empty.
817
+ - **mongokit >=3.21**: `schemaGenerator` DEFAULTS to `buildCrudSchemasFromModel` — omitting it is now correct. Flag the opposite: `schemaGenerator: false` used to dodge a wire-vs-model shape mismatch (minor-unit integers vs Money objects, enums, etc.). That opt-out silently kills OpenAPI bodies AND MCP tool input schemas for the whole resource. The right fix is a real wire contract: `customSchemas: { create: { body }, update: { body } }` with the generator ON — in arc 2.21 a custom part REPLACES the generated part wholesale (no `required[]` union), so wire and model shapes can differ cleanly.
818
+ - **mongokit <3.21**: omitted `schemaGenerator` emits `null` OpenAPI bodies (arc 2.12 deleted the built-in fallback). Fix: upgrade the kit, or wire `schemaGenerator: buildCrudSchemasFromModel` explicitly.
818
819
 
819
- **Fix:**
820
- ```typescript
821
- import { createMongooseAdapter } from '@classytic/mongokit/adapter'; // arc 2.12+
822
- import { buildCrudSchemasFromModel } from '@classytic/mongokit';
823
-
824
- createMongooseAdapter({
825
- model: ProductModel,
826
- repository: productRepo,
827
- schemaGenerator: buildCrudSchemasFromModel, // required
828
- });
829
- ```
830
- For drizzle: `import { createDrizzleAdapter } from '@classytic/sqlitekit/adapter'` + `schemaGenerator: buildCrudSchemasFromTable` from `@classytic/sqlitekit`.
820
+ For drizzle: `createDrizzleAdapter` still wants `schemaGenerator: buildCrudSchemasFromTable` from `@classytic/sqlitekit` — check the kit's own version before flagging.
831
821
 
832
822
  ---
833
823
 
@@ -937,6 +927,68 @@ For backends without `repo.aggregate`, declare a `materialized` hook on the aggr
937
927
 
938
928
  ---
939
929
 
930
+ ## §34. `as never` / `as unknown as` casts on engine bags in module packages 🟠
931
+
932
+ **Detection:**
933
+ ```
934
+ pattern: "as never|as unknown as"
935
+ ```
936
+ Concentrated hits in `@classytic/arc-*` (or `@scope/arc-*`) module packages — especially around `defineResource(...)` assembly, adapter construction from an engine, or hand-rolled config spread-merges.
937
+
938
+ **Anti-pattern (pre-2.21 module-author tax):**
939
+ ```typescript
940
+ interface EngineLike { models: Record<string, unknown>; repositories: Record<string, unknown> }
941
+ adapter: createMongooseAdapter(engine.models.Party as never, repo as never),
942
+ ...(cfg.extraRoutes ? { routes: [...base.routes, ...cfg.extraRoutes] as never } : {}),
943
+ ```
944
+
945
+ **Why high:** every cast is a hole where the kernel's real types (optional sub-module members, role unions, doc shapes) stop flowing — typos compile, wrong repos pair with wrong models, and hand-rolled spread-merges clobber host arrays.
946
+
947
+ **Fix (arc 2.21):** type the engine port with the KERNEL's own bags and assemble via `mergeResourceConfig`:
948
+ ```typescript
949
+ import { mergeResourceConfig, type ResourceSeams, type AdapterLike } from '@classytic/arc';
950
+
951
+ interface CatalogEngineLike { // kernel is already a peer — no new coupling
952
+ models: CatalogEngine['models'];
953
+ repositories: CatalogEngine['repositories'];
954
+ }
955
+ defineResource(mergeResourceConfig(
956
+ { name: 'product', adapter: createMongooseAdapter({ model: models.Product, repository: repositories.product }), ... },
957
+ hostSeams, // typed ResourceSeams — arrays concat, objects deep-merge,
958
+ )); // instances last-win, undefined never clobbers
959
+ ```
960
+ Remaining honest boundaries (hydrated-vs-lean id widening at a kernel bridge, wire-body narrowing in action handlers) get a comment, not a blanket `as never`.
961
+
962
+ ---
963
+
964
+ ## §35. Hand-rolled recurring jobs (`setInterval` / cron + DB lock) 🟠
965
+
966
+ **Detection:**
967
+ ```
968
+ pattern: "setInterval\\s*\\(|node-cron|cron\\.schedule|new CronJob"
969
+ ```
970
+ Plus home-grown "job registry" / "leader lock" / "acquireLock" modules paired with timers.
971
+
972
+ **Why high:** every host regrows the same layer — overlap prevention, multi-replica leader lock, shutdown timer cleanup, failure counters — and gets at least one of them wrong (stacking `setInterval` runs, timers holding the process open, no lease recovery after a crashed leader).
973
+
974
+ **Fix (arc 2.21):** `schedulesPlugin` from `@classytic/arc/plugins` — interval jobs with no-overlap by construction (timeout chain), optional `lock` (any `LockAdapter`: `@classytic/mongokit/lock`, sqlitekit, memory) for multi-replica lease safety, `jitterMs`, `runOnStart`, `getScheduleStats()`, clean `onClose` drain. Calendar cron-expressions / durable retries stay in the BullMQ tier (`@classytic/arc/integrations/jobs`) — don't flag hosts that need real cron semantics for using it.
975
+
976
+ ---
977
+
978
+ ## §36. Wrong tenant config key — `{ field }` instead of `{ tenantField }` 🔴
979
+
980
+ **Detection:**
981
+ ```
982
+ pattern: "tenant:\\s*\\{\\s*field\\s*:"
983
+ ```
984
+ Also any object passed as tenant config whose keys aren't in repo-core's `TenantConfig`.
985
+
986
+ **Why critical:** unknown keys are **silently ignored** — `{ tenant: { field: 'branchId' } }` compiles (loose host types), the resource falls back to the default tenant field, and cross-tenant reads/writes go unscoped for hosts on a custom field name. Found live in published spine packages (arc-assets / arc-pricelist 0.1.x).
987
+
988
+ **Fix:** the key is `tenantField` (repo-core `TenantConfig`). In arc resources prefer the top-level `tenantField: 'branchId'` shorthand or the `multiTenant` preset; when passing a tenant object through a module seam, type it as `TenantConfig` so unknown keys fail the build.
989
+
990
+ ---
991
+
940
992
  ## Detection checklist (run-order)
941
993
 
942
994
  Run sweeps in this order — early hits often invalidate later context:
@@ -951,5 +1003,6 @@ Run sweeps in this order — early hits often invalidate later context:
951
1003
  8. §7, §8, §22, §23 — events/cache/idempotency/audit (silent inconsistency)
952
1004
  9. §14, §18, §19 — preset adoption + custom controller wiring
953
1005
  10. §11, §12, §13, §29, §30, §31 — style and edges
954
- 11. §32a–§32g — canonical-contract drift (pagination / events / tenant / errors / adapter imports), missing `requireOrgId` accessors, missing `schemaGenerator`, kit-specific adapter imports from arc (3.0 break)
1006
+ 11. §32a–§32g — canonical-contract drift (pagination / events / tenant / errors / adapter imports), missing `requireOrgId` accessors, missing/dodged `schemaGenerator`, kit-specific adapter imports from arc (3.0 break)
955
1007
  12. §33 — hand-rolled `Model.aggregate([...])` in resource routes vs declarative `aggregations: { … }`
1008
+ 13. §36 — tenant config `{ field }` key (security: silent unscoped queries), then §34–§35 — module-package `as never` casts vs 2.21 seams; hand-rolled recurring jobs vs `schedulesPlugin`
@@ -23,12 +23,22 @@ A condensed map of what arc provides, so during audit you can spot what the team
23
23
  | Custom `withRetry` / `withDLQ` plumbing on events | `RedisStreamTransport` (durable, consumer groups, DLQ) |
24
24
  | Hand-rolled idempotency token check | `idempotencyPlugin` (header-based, configurable store) |
25
25
  | Manual SCIM provisioning endpoints | `@classytic/arc/scim` — `scimPlugin({ users, groups, bearer })` |
26
+ | `setInterval` sweeps + hand-rolled leader lock / job registry | `schedulesPlugin` from `@classytic/arc/plugins` (2.21 — interval jobs, LockAdapter lease, no Redis) |
27
+ | `as never` casts + spread-merges assembling resources in module packages | `mergeResourceConfig` + `ResourceSeams` (2.21) with kernel-typed engine ports |
28
+ | Same `view`/`manage` gate repeated across 5 CRUD keys | `permissionMatrix({ read, write, delete? })` (2.21) |
29
+ | DB connect + dynamic-import ordering in the composition root | `createApp({ beforeBoot })` (2.21 — runs before Fastify + all imports) |
30
+ | `import { x } from 'better-auth/plugins'` (barrel) | dedicated path `better-auth/plugins/<name>` — tree-shaking (BA best practice) |
31
+ | Hand-rolled per-tenant usage/billing counters | `usagePlugin` from `@classytic/arc/usage` (2.22 — per-actor per-period counters, fail-safe, `UsageStore` backends) |
32
+ | Per-plan rate limits via custom middleware | `rateLimit.plan { resolve, limits, default }` (2.22 — `false` = unlimited, fail-safe fallback, tenant-keyed buckets) |
33
+ | Hand-built audit-timeline endpoints (`GET /:id/history` handlers over audit rows) | `history: true` on `defineResource` (2.22 — implies `audit: true`, stricter-than-read gate) |
26
34
 
27
35
  ## Boot order (FIXED)
28
36
 
29
37
  ```
38
+ 0. beforeBoot() ← pre-everything (DB connect; before Fastify + imports) — 2.21
39
+ 0.5 module resolve ← thunk imports + dependsOn topo-sort; module errorMappers merge
30
40
  1. Arc core (security, auth, events)
31
- 2. plugins() ← user infra (DB, docs, webhooks)
41
+ 2. plugins() ← user infra (docs, webhooks)
32
42
  3. bootstrap[] ← domain init (engines, singletons)
33
43
  4. resources (factory runs after bootstrap)
34
44
  5. resources[] ← register each
@@ -54,7 +64,7 @@ defineResource({
54
64
  rateLimit?, tenantField?, idField?,
55
65
  prefix?, skipGlobalPrefix?,
56
66
  queryParser?, onFieldWriteDenied?,
57
- audit?, mcp?, // mcp: false to exclude from MCP tool gen
67
+ audit?, history?, mcp?, // mcp: false to exclude from MCP tool gen; history: true → GET /:id/history (2.22)
58
68
  displayName?, module?,
59
69
  onTenantDelete?, // GDPR cascade strategy
60
70
  });
@@ -69,6 +79,7 @@ requireRoles(['admin']) requireServiceScope('jobs:bulk')
69
79
  requireOwnership('userId') requireScopeContext('branchId')
70
80
  requireOrgMembership() requireOrgInScope(targetId)
71
81
  allOf(...) · anyOf(...) · not(...) · when(...) · denyAll()
82
+ permissionMatrix({ read, write, delete? }) // 2.21 — expands to the full CRUD map
72
83
  createDynamicPermissionMatrix({ resolveRolePermissions, cacheStore })
73
84
  ```
74
85
 
@@ -159,7 +170,9 @@ import type { DataAdapter, RepositoryLike } from '@classytic/repo-core/adapter';
159
170
 
160
171
  ```typescript
161
172
  import { defineResource, BaseController, allowPublic } from '@classytic/arc';
173
+ import { mergeResourceConfig, type ResourceSeams } from '@classytic/arc'; // 2.21
162
174
  import { createApp, loadResources } from '@classytic/arc/factory';
175
+ import { schedulesPlugin } from '@classytic/arc/plugins'; // 2.21
163
176
  import { MemoryCacheStore, RedisCacheStore, QueryCache } from '@classytic/arc/cache';
164
177
  import { eventPlugin, EventOutbox } from '@classytic/arc/events';
165
178
  import { RedisEventTransport, RedisStreamTransport } from '@classytic/arc/events/redis-stream';
@@ -21,13 +21,13 @@ In arc 2.12:
21
21
 
22
22
  ### Coordinated versions
23
23
 
24
- | Package | Min |
25
- |---|---|
26
- | `@classytic/arc` | 2.12.0 |
27
- | `@classytic/repo-core` | 0.4.0 |
28
- | `@classytic/mongokit` | 3.13.0 |
29
- | `@classytic/sqlitekit` | 0.3.0 |
30
- | `@classytic/prismakit` | 0.1.0 |
24
+ | Package | Min (adapter split) | Current best-practice floor |
25
+ |---|---|---|
26
+ | `@classytic/arc` | 2.12.0 | 2.21.0 — seams/`mergeResourceConfig`, `beforeBoot`, `permissionMatrix`, `schedulesPlugin` |
27
+ | `@classytic/repo-core` | 0.4.0 | 0.9.0 — `StandardRepo.getByIds` contract member |
28
+ | `@classytic/mongokit` | 3.13.0 | 3.21.0 — adapter `schemaGenerator` defaults ON |
29
+ | `@classytic/sqlitekit` | 0.3.0 | 0.7.0 |
30
+ | `@classytic/prismakit` | 0.1.0 | 0.1.0 |
31
31
 
32
32
  ### Import migration table
33
33
 
@@ -200,13 +200,14 @@ export const userRepo = new Repository<UserDoc>(User, [
200
200
  // resources/user/user.resource.ts — arc resource consuming the mongokit repo
201
201
  import { defineResource, requireRoles, allowPublic, fields } from '@classytic/arc';
202
202
  import { createMongooseAdapter } from '@classytic/mongokit/adapter'; // arc 2.12+: from the kit
203
- import { buildCrudSchemasFromModel } from '@classytic/mongokit';
204
203
  import { User } from '../../models/user.js';
205
204
  import { userRepo } from '../../repositories/userRepository.js';
206
205
 
207
206
  export const userResource = defineResource({
208
207
  name: 'user',
209
- adapter: createMongooseAdapter({ model: User, repository: userRepo, schemaGenerator: buildCrudSchemasFromModel }),
208
+ // mongokit >=3.21 defaults schemaGenerator to buildCrudSchemasFromModel
209
+ // (pre-3.21: wire it explicitly, or OpenAPI/MCP bodies come out null)
210
+ adapter: createMongooseAdapter({ model: User, repository: userRepo }),
210
211
  presets: ['softDelete'], // multiTenant is at the repo layer (mongokit), so we don't need the arc preset too — pick one
211
212
  permissions: {
212
213
  list: requireRoles(['admin']),
@@ -344,7 +345,7 @@ For each Mongoose model in the project:
344
345
 
345
346
  If consumers see `as unknown as RepositoryLike<T>` casts when wiring a repo into the mongoose adapter, that's drift. Mongokit's `tests/unit/standard-repo-assignment.test-d.ts` proves whole-interface assignment via TypeScript. Run `npm run typecheck:tests` in mongokit; if it fails, the published version drifted from the contract — pin to a known-good version or open a PR.
346
347
 
347
- In the client project, prefer `createMongooseAdapter({ model, repository, schemaGenerator })` from `@classytic/mongokit/adapter` over manual `RepositoryLike` shaping — it accepts mongokit-native repos with no casts.
348
+ In the client project, prefer `createMongooseAdapter({ model, repository })` from `@classytic/mongokit/adapter` over manual `RepositoryLike` shaping — it accepts mongokit-native repos with no casts, and since mongokit 3.21 `schemaGenerator` defaults to `buildCrudSchemasFromModel` (`schemaGenerator: false` opts out — see anti-patterns §32f before recommending that).
348
349
 
349
350
  ## arc 2.12 / mongokit 3.13.0 — adapter split
350
351