@classytic/arc 2.21.0 → 2.23.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 (115) hide show
  1. package/dist/{BaseController-BnLYgE9A.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
  2. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  3. package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
  4. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
  5. package/dist/audit/index.d.mts +2 -2
  6. package/dist/auth/index.d.mts +30 -2
  7. package/dist/auth/index.mjs +6 -3
  8. package/dist/cli/commands/describe.d.mts +1 -1
  9. package/dist/cli/commands/describe.mjs +1 -1
  10. package/dist/cli/commands/docs.mjs +2 -2
  11. package/dist/cli/commands/init.mjs +1 -1
  12. package/dist/cli/commands/introspect.mjs +1 -1
  13. package/dist/cli/index.mjs +1 -1
  14. package/dist/core/index.d.mts +2 -2
  15. package/dist/core/index.mjs +3 -3
  16. package/dist/{core-DZ6Xe1jR.mjs → core-DQ6rDTyH.mjs} +126 -68
  17. package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-BuOv-m4Y.mjs} +10 -6
  18. package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-CgJv_BU_.mjs} +3 -3
  19. package/dist/{createApp-Bi61XxqU.mjs → createApp-CjOeWJ9_.mjs} +252 -57
  20. package/dist/docs/index.d.mts +1 -1
  21. package/dist/docs/index.mjs +1 -1
  22. package/dist/encryption/index.mjs +9 -5
  23. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-Bq2OeEQL.mjs} +34 -10
  24. package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
  25. package/dist/events/index.mjs +5 -2
  26. package/dist/factory/index.d.mts +46 -9
  27. package/dist/factory/index.mjs +64 -13
  28. package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-g_f9omSc.d.mts} +2 -2
  29. package/dist/fields-D3lhgzhD.d.mts +121 -0
  30. package/dist/health-ScyjIVo5.mjs +202 -0
  31. package/dist/hooks/index.d.mts +1 -1
  32. package/dist/idempotency/index.d.mts +7 -0
  33. package/dist/idempotency/index.mjs +7 -17
  34. package/dist/{index-Dtrj6XZk.d.mts → index-BBsvQt7U.d.mts} +1310 -1226
  35. package/dist/{types-DpxoNX5v.d.mts → index-Ba10fdKp.d.mts} +196 -66
  36. package/dist/{index-B5N-NaHs.d.mts → index-CC5ruDGh.d.mts} +3 -3
  37. package/dist/{index-B5PoNbfu.d.mts → index-wMoWBLkn.d.mts} +68 -2
  38. package/dist/index.d.mts +6 -5
  39. package/dist/index.mjs +6 -5
  40. package/dist/{init-0YhHkXGk.mjs → init-CjO4p_q1.mjs} +123 -4
  41. package/dist/integrations/event-gateway.mjs +1 -1
  42. package/dist/integrations/index.d.mts +1 -1
  43. package/dist/integrations/mcp/index.d.mts +67 -4
  44. package/dist/integrations/mcp/index.mjs +1 -1
  45. package/dist/integrations/mcp/testing.d.mts +1 -1
  46. package/dist/integrations/mcp/testing.mjs +1 -1
  47. package/dist/integrations/webhooks.d.mts +131 -7
  48. package/dist/integrations/webhooks.mjs +169 -10
  49. package/dist/interface-n5hpKg1d.mjs +16 -0
  50. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  51. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  52. package/dist/logger/index.d.mts +26 -1
  53. package/dist/logger/index.mjs +39 -1
  54. package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-BtzIL_kM.mjs} +312 -39
  55. package/dist/middleware/index.d.mts +1 -1
  56. package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
  57. package/dist/{openapi-Kyod_di6.mjs → openapi-jSDx-VDv.mjs} +3 -3
  58. package/dist/permissions/index.d.mts +4 -3
  59. package/dist/permissions/index.mjs +3 -2
  60. package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
  61. package/dist/pipeline/index.d.mts +1 -1
  62. package/dist/plugins/index.d.mts +24 -7
  63. package/dist/plugins/index.mjs +14 -282
  64. package/dist/plugins/response-cache.mjs +1 -0
  65. package/dist/plugins/tracing-entry.mjs +7 -4
  66. package/dist/presets/filesUpload.d.mts +2 -2
  67. package/dist/presets/filesUpload.mjs +1 -1
  68. package/dist/presets/index.d.mts +1 -1
  69. package/dist/presets/index.mjs +1 -1
  70. package/dist/presets/multiTenant.d.mts +1 -1
  71. package/dist/presets/search.d.mts +2 -2
  72. package/dist/presets/search.mjs +1 -1
  73. package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
  74. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  75. package/dist/realtime-I8OrfnMR.mjs +375 -0
  76. package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-CWO6mdYe.mjs} +38 -18
  77. package/dist/registry/index.d.mts +2 -2
  78. package/dist/registry/index.mjs +1 -1
  79. package/dist/requestId-5BhTVzBv.mjs +113 -0
  80. package/dist/schemaConverter-DJvodiHj.mjs +171 -0
  81. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-Ca7ySNPx.mjs} +2 -2
  82. package/dist/scim/index.mjs +6 -7
  83. package/dist/scope/index.mjs +2 -76
  84. package/dist/scope-LONQ3mfZ.mjs +37 -0
  85. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  86. package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
  87. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  88. package/dist/testing/index.d.mts +83 -3
  89. package/dist/testing/index.mjs +70 -3
  90. package/dist/types/index.d.mts +2 -2
  91. package/dist/{types-DRj-Ut4n.d.mts → types-5OuqeTDX.d.mts} +37 -2
  92. package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
  93. package/dist/usage/index.d.mts +142 -0
  94. package/dist/usage/index.mjs +0 -0
  95. package/dist/utils/index.d.mts +101 -19
  96. package/dist/utils/index.mjs +5 -3
  97. package/dist/{utils-CO8a3B_1.mjs → utils-CZyeRXTh.mjs} +80 -82
  98. package/dist/{versioning-DQ4mCvg0.d.mts → versioning-D6PvCwpq.d.mts} +43 -9
  99. package/llms.txt +39 -0
  100. package/package.json +513 -447
  101. package/skills/arc/SKILL.md +14 -8
  102. package/skills/arc/references/api-reference.md +34 -0
  103. package/skills/arc/references/auth.md +21 -4
  104. package/skills/arc/references/mcp.md +1 -1
  105. package/skills/arc/references/production.md +734 -654
  106. package/skills/arc-code-review/SKILL.md +1 -1
  107. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  108. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  109. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  110. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  111. package/skills/arc-module-publishing/SKILL.md +2 -1
  112. package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
  113. package/skills/arc-module-publishing/references/package-manifest.md +4 -2
  114. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  115. 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