@classytic/arc 2.15.4 → 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.
- package/README.md +1 -0
- package/bin/arc.js +12 -0
- package/dist/{BaseController-dx3m2J8V.mjs → BaseController-DlCCTIxJ.mjs} +61 -19
- package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
- package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-SvmT_9ti.d.mts} +1 -1
- package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-f48hFk3m.mjs} +52 -9
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +4 -2
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +4 -4
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
- package/dist/buildHandler-BZX6zzDM.mjs +300 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/{caching-SM8gghN6.mjs → caching-TeHE8G-v.mjs} +1 -1
- package/dist/cli/commands/describe.d.mts +35 -1
- package/dist/cli/commands/describe.mjs +52 -12
- package/dist/cli/commands/docs.d.mts +1 -4
- package/dist/cli/commands/docs.mjs +4 -16
- package/dist/cli/commands/generate.d.mts +2 -20
- package/dist/cli/commands/generate.mjs +1 -546
- package/dist/cli/commands/init.d.mts +2 -40
- package/dist/cli/commands/init.mjs +1 -3045
- package/dist/cli/commands/introspect.mjs +53 -64
- package/dist/cli/index.d.mts +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/{core-CvmOqEms.mjs → core-DBJ_j6rX.mjs} +222 -44
- package/dist/createActionRouter-DUpN3Dd1.mjs +288 -0
- package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-Dq-TUCuY.mjs} +3 -2
- package/dist/{createApp-PFegs47-.mjs → createApp-DNccuhyI.mjs} +16 -14
- package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-DRwY0fYm.mjs} +1 -1
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DpoXQHZ9.mjs} +17 -14
- package/dist/errors-C1lX_jlm.d.mts +91 -0
- package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-C2cGqtRO.mjs} +1 -1
- package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-CtHC_av1.d.mts} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +5 -5
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-COhcH3fk.d.mts → fields-Anj0xdih.d.mts} +1 -1
- package/dist/generate-BWFwgcCM.d.mts +38 -0
- package/dist/generate-CYac-OLv.mjs +654 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +2 -2
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-BTqLEvhu.d.mts → index-3oIimXQn.d.mts} +12 -12
- package/dist/{index-BstGxcc3.d.mts → index-B-ulKx5P.d.mts} +55 -4
- package/dist/{index-BswOSJCE.d.mts → index-CkW0flkU.d.mts} +355 -16
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +7 -8
- package/dist/init-Dv71MsJr.d.mts +71 -0
- package/dist/init-HDvoO9L5.mjs +3098 -0
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/jobs.mjs +3 -3
- package/dist/integrations/mcp/index.d.mts +239 -7
- package/dist/integrations/mcp/index.mjs +2 -528
- package/dist/integrations/mcp/testing.d.mts +2 -2
- package/dist/integrations/mcp/testing.mjs +6 -10
- package/dist/integrations/streamline.mjs +26 -1
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +1 -0
- package/dist/loadResourcesFromEntry-BLMEI2Xa.mjs +51 -0
- package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-7vGV51ED.mjs} +1021 -318
- package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/{openapi-BHXhoX8O.mjs → openapi-34T9yNwd.mjs} +47 -36
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
- package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +5 -5
- package/dist/plugins/index.mjs +10 -10
- package/dist/plugins/response-cache.mjs +5 -5
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +2 -2
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +4 -3
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
- package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
- package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Biqzfbi5.d.mts} +2 -2
- package/dist/{redis-DiMkdHEl.d.mts → redis-Cyzrz6SX.d.mts} +1 -1
- package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-DT-YjzrB.d.mts} +1 -1
- package/dist/registry/index.d.mts +319 -2
- package/dist/registry/index.mjs +3 -3
- package/dist/registry-BBE23CDj.mjs +576 -0
- package/dist/{routerShared-DrOa-26E.mjs → routerShared-CZV5aabX.mjs} +3 -3
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +3 -3
- package/dist/{sse-Bz-5ZeTt.mjs → sse-BY6sTy4P.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +16 -7
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +5 -5
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
- package/dist/{types-BQsjgQzS.d.mts → types-BsJMEQ4D.d.mts} +106 -12
- package/dist/{types-DrBaUwyV.d.mts → types-D-fYtKjb.d.mts} +33 -10
- package/dist/{types-CTYvcwHe.d.mts → types-DVfpSfx2.d.mts} +42 -1
- package/dist/utils/index.d.mts +1286 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-_h9B3c57.mjs → utils-DC5ycPfr.mjs} +89 -40
- package/dist/{buildHandler-CcFOpJLh.mjs → validate-By96rH0r.mjs} +8 -299
- package/dist/{versioning-hmkPcDlX.d.mts → versioning-ZwX9tmbS.d.mts} +1 -1
- package/package.json +21 -28
- package/skills/arc/SKILL.md +300 -706
- package/skills/arc/references/auth.md +19 -7
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
- package/dist/createActionRouter-S3MLVYot.mjs +0 -220
- package/dist/index-bRjYu21O.d.mts +0 -1320
- package/dist/org/index.d.mts +0 -66
- package/dist/org/index.mjs +0 -486
- package/dist/org/types.d.mts +0 -82
- package/dist/org/types.mjs +0 -1
- package/dist/registry-I-ogLgL9.mjs +0 -46
- /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-C-2oAHtw.d.mts} +0 -0
- /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-Hxvv5QQz.mjs} +0 -0
- /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
- /package/dist/{elevation-BXOWoGCF.d.mts → elevation-0YBpa663.d.mts} +0 -0
- /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
- /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-mHuyWzZE.d.mts} +0 -0
- /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-DFg-2KTp.d.mts} +0 -0
- /package/dist/{interface-beEtJyWM.d.mts → interface-CH0OQudo.d.mts} +0 -0
- /package/dist/{interface-DfLGcus7.d.mts → interface-NwJ_qPlY.d.mts} +0 -0
- /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
- /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-ChQEj8ih.mjs} +0 -0
- /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-TuOmguhi.mjs} +0 -0
- /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-C-gD32oF.mjs} +0 -0
- /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-Ctc89DSn.mjs} +0 -0
- /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-BqFegc0W.d.mts} +0 -0
- /package/dist/{storage-Dfzt4VTl.d.mts → storage-D2KZJAmn.d.mts} +0 -0
- /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-B0sunfZZ.mjs} +0 -0
- /package/dist/{tracing-QJVprktp.d.mts → tracing-Dm8n7Cnn.d.mts} +0 -0
- /package/dist/{versioning-BUrT5aP4.mjs → versioning-B6mimogM.mjs} +0 -0
- /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
|
|
514
|
+
## Org-Scoped Permission Checks
|
|
515
515
|
|
|
516
516
|
```typescript
|
|
517
|
-
import {
|
|
517
|
+
import { allowPublic, requireAuth, requireOrgRole } from '@classytic/arc/permissions';
|
|
518
518
|
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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:**
|
|
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
|
|
1
|
+
# Arc capabilities at a glance — for gap detection
|
|
2
2
|
|
|
3
|
-
A condensed map of arc
|
|
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
|
-
|
|
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()
|
|
12
|
-
3. bootstrap[]
|
|
13
|
-
4. resources factory
|
|
14
|
-
5. resources[]
|
|
15
|
-
6. afterResources()
|
|
16
|
-
7. onReady / onClose
|
|
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
|
-
|
|
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
|
-
##
|
|
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',
|
|
48
|
-
adapter
|
|
49
|
-
controller
|
|
50
|
-
permissions: {
|
|
51
|
-
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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()
|
|
100
|
-
requireAuth()
|
|
101
|
-
requireRoles(['admin'
|
|
102
|
-
requireOwnership('userId')
|
|
103
|
-
requireOrgMembership()
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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` |
|
|
175
|
-
| `multiTenant` |
|
|
176
|
-
| `audited` |
|
|
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?,
|
|
179
|
-
| `search` | `POST /search`, `/search-similar`, `/embed` | `{ repository?, search?, similar?, embed
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 --
|
|
280
|
-
arc generate resource product
|
|
281
|
-
arc
|
|
282
|
-
arc
|
|
283
|
-
arc
|
|
284
|
-
arc
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
145
|
+
import { createDrizzleAdapter } from '@classytic/sqlitekit/adapter';
|
|
146
|
+
import { createPrismaAdapter } from '@classytic/prismakit/adapter';
|
|
334
147
|
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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
|
|
381
|
-
import { createApp, loadResources }
|
|
382
|
-
import { MemoryCacheStore, RedisCacheStore, QueryCache }
|
|
383
|
-
import {
|
|
384
|
-
import {
|
|
385
|
-
import {
|
|
386
|
-
import {
|
|
387
|
-
import {
|
|
388
|
-
import {
|
|
389
|
-
import {
|
|
390
|
-
import {
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
|
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.
|