@classytic/arc 2.4.3 → 2.6.2
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 +57 -6
- package/dist/{BaseController-CkM5dUh_.mjs → BaseController-AbbRx3e0.mjs} +5 -2
- package/dist/{ResourceRegistry-DeCIFlix.mjs → ResourceRegistry-C6ngvOnn.mjs} +1 -0
- package/dist/adapters/index.d.mts +2 -2
- package/dist/adapters/index.mjs +1 -1
- package/dist/{adapters-DTC4Ug66.mjs → adapters-CTn28N4y.mjs} +72 -11
- package/dist/audit/index.d.mts +32 -6
- package/dist/audit/index.mjs +32 -4
- package/dist/audit/mongodb.d.mts +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +2 -2
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/init.mjs +12 -9
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +2 -2
- package/dist/{createApp-CBgVaFyh.mjs → createApp-D2w0LdYJ.mjs} +431 -290
- package/dist/{defineResource-B22gcNvn.mjs → defineResource-Ckxg6HrZ.mjs} +125 -22
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/dynamic/index.d.mts +1 -1
- package/dist/dynamic/index.mjs +2 -2
- package/dist/{elevation-Ca_yveIO.d.mts → elevation-C_taLQrM.d.mts} +27 -1
- package/dist/{errorHandler-DMbGdzBG.mjs → errorHandler-r2595m8T.mjs} +1 -1
- package/dist/{errors-CPpvPHT0.d.mts → errors-CcVbl1-T.d.mts} +17 -1
- package/dist/{errors-rxhfP7Hf.mjs → errors-NoQKsbAT.mjs} +23 -1
- package/dist/{eventPlugin-iGrSEmwJ.d.mts → eventPlugin-DW45v4V5.d.mts} +30 -2
- package/dist/events/index.d.mts +2 -2
- package/dist/events/index.mjs +40 -10
- package/dist/factory/index.d.mts +44 -23
- package/dist/factory/index.mjs +152 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/mongodb.d.mts +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-BL8CaQih.d.mts → index-B4uZm82R.d.mts} +2 -2
- package/dist/{index-yhxyjqNb.d.mts → index-DrCqa3Jq.d.mts} +4 -8
- package/dist/{index-Diqcm14c.d.mts → index-NGZksqM5.d.mts} +30 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +8 -7
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +4 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/{interface-DGmPxakH.d.mts → interface-CrN45qz1.d.mts} +229 -13
- package/dist/{mongodb-CUpYfxfD.d.mts → mongodb-kltrBPa1.d.mts} +10 -0
- package/dist/{mongodb-bga9AbkD.d.mts → mongodb-pMvOlR5_.d.mts} +1 -1
- package/dist/org/index.d.mts +1 -1
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-Jk5x3sxz.mjs → permissions-C8ImI8gC.mjs} +44 -2
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +4 -4
- package/dist/plugins/tracing-entry.mjs +1 -1
- 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 +1 -1
- package/dist/{presets-OMPaHMTY.mjs → presets-BMfdy34e.mjs} +2 -2
- package/dist/{redis-CQ5YxMC5.d.mts → redis-D0Qc-9EW.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +1 -1
- package/dist/{resourceToTools-PMFE8HIv.mjs → resourceToTools-DH3c3e-T.mjs} +81 -7
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +2 -2
- package/dist/{sse-BkViJPlT.mjs → sse-BF7GR7IB.mjs} +1 -1
- package/dist/testing/index.d.mts +26 -3
- package/dist/testing/index.mjs +46 -2
- package/dist/types/index.d.mts +3 -3
- package/dist/types/index.mjs +23 -2
- package/dist/{types-C6TQjtdi.mjs → types-BhtYdxZU.mjs} +26 -1
- package/dist/{types-Dt0-AI6E.d.mts → types-C1Z28coa.d.mts} +195 -6
- package/dist/{types-BJmgxNbF.d.mts → types-DurlBP2N.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +1 -1
- package/package.json +6 -5
- package/skills/arc/SKILL.md +151 -4
- package/skills/arc/references/mcp.md +160 -2
- /package/dist/{interface-B4awm1RJ.d.mts → interface-gr-7qo9j.d.mts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
"node": ">=22"
|
|
221
221
|
},
|
|
222
222
|
"peerDependencies": {
|
|
223
|
-
"@classytic/mongokit": ">=3.
|
|
223
|
+
"@classytic/mongokit": ">=3.5.0",
|
|
224
224
|
"@classytic/streamline": ">=2.0.0",
|
|
225
225
|
"@fastify/cors": "^11.0.0",
|
|
226
226
|
"@fastify/helmet": "^13.0.0",
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
"fastify-raw-body": "^5.0.0",
|
|
245
245
|
"ioredis": "^5.0.0",
|
|
246
246
|
"mongodb": "^6.0.0 || ^7.0.0",
|
|
247
|
-
"mongoose": "
|
|
247
|
+
"mongoose": ">=9.0.0",
|
|
248
248
|
"pino-pretty": "^13.0.0",
|
|
249
249
|
"zod": "^4.0.0"
|
|
250
250
|
},
|
|
@@ -333,11 +333,12 @@
|
|
|
333
333
|
},
|
|
334
334
|
"dependencies": {
|
|
335
335
|
"fastify-plugin": "^5.0.1",
|
|
336
|
-
"qs": "^6.14.1"
|
|
336
|
+
"qs": "^6.14.1",
|
|
337
|
+
"secure-json-parse": "^4.1.0"
|
|
337
338
|
},
|
|
338
339
|
"devDependencies": {
|
|
339
340
|
"@biomejs/biome": "^2.4.10",
|
|
340
|
-
"@classytic/mongokit": "^3.
|
|
341
|
+
"@classytic/mongokit": "^3.5.2",
|
|
341
342
|
"@fastify/jwt": "^10.0.0",
|
|
342
343
|
"@fastify/multipart": "^9.0.0",
|
|
343
344
|
"@fastify/type-provider-typebox": "^6.0.0",
|
package/skills/arc/SKILL.md
CHANGED
|
@@ -8,11 +8,11 @@ description: |
|
|
|
8
8
|
Triggers: arc, fastify resource, defineResource, createApp, BaseController, arc preset,
|
|
9
9
|
arc auth, arc events, arc jobs, arc websocket, arc mcp, arc plugin, arc testing, arc cli,
|
|
10
10
|
arc permissions, arc hooks, arc pipeline, arc factory, arc cache, arc QueryCache.
|
|
11
|
-
version: 2.
|
|
11
|
+
version: 2.6.2
|
|
12
12
|
license: MIT
|
|
13
13
|
metadata:
|
|
14
14
|
author: Classytic
|
|
15
|
-
version: "2.
|
|
15
|
+
version: "2.6.2"
|
|
16
16
|
tags:
|
|
17
17
|
- fastify
|
|
18
18
|
- rest-api
|
|
@@ -196,6 +196,30 @@ presets: ['softDelete', { name: 'multiTenant', tenantField: 'organizationId' }]
|
|
|
196
196
|
// Bulk: presets: ['bulk'] or bulkPreset({ operations: ['createMany', 'updateMany'] })
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
+
### tenantField — When to Use and When to Disable
|
|
200
|
+
|
|
201
|
+
Arc defaults `tenantField` to `'organizationId'` on BaseController. This silently adds `{ organizationId: scope.organizationId }` to every query when the user has an org context. Correct for per-org resources, wrong for company-wide resources.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Per-org resource (default) — each org sees only its own data
|
|
205
|
+
defineResource({ name: 'invoice', ... });
|
|
206
|
+
// → queries auto-scoped: { organizationId: 'org-123' }
|
|
207
|
+
|
|
208
|
+
// Company-wide resource — ALL orgs share the same data
|
|
209
|
+
defineResource({ name: 'account-type', tenantField: false, ... });
|
|
210
|
+
// → no org filter applied, all users see all records
|
|
211
|
+
|
|
212
|
+
// Custom tenant field — your schema uses a different name
|
|
213
|
+
defineResource({ name: 'workspace-item', tenantField: 'workspaceId', ... });
|
|
214
|
+
// → queries scoped by workspaceId instead of organizationId
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
When to use `tenantField: false`:
|
|
218
|
+
- Lookup tables (account types, categories, currencies)
|
|
219
|
+
- Platform-wide settings or config
|
|
220
|
+
- Cross-org reports or analytics
|
|
221
|
+
- Single-tenant apps where org scoping isn't needed
|
|
222
|
+
|
|
199
223
|
## QueryCache
|
|
200
224
|
|
|
201
225
|
TanStack Query-inspired server cache with stale-while-revalidate and auto-invalidation on mutations.
|
|
@@ -314,6 +338,26 @@ const app = await createApp({
|
|
|
314
338
|
|
|
315
339
|
## Hooks
|
|
316
340
|
|
|
341
|
+
**Inline on resource (recommended):**
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
defineResource({
|
|
345
|
+
name: 'chat',
|
|
346
|
+
hooks: {
|
|
347
|
+
beforeCreate: async (ctx) => { ctx.data.slug = slugify(ctx.data.name); },
|
|
348
|
+
afterCreate: async (ctx) => { analytics.track('created', { id: ctx.data._id, user: ctx.user?.id }); },
|
|
349
|
+
beforeUpdate: async (ctx) => { console.log('Updating', ctx.meta?.id, 'existing:', ctx.meta?.existing); },
|
|
350
|
+
afterUpdate: async (ctx) => { await invalidateCache(ctx.data._id); },
|
|
351
|
+
beforeDelete: async (ctx) => { if (ctx.data.isProtected) throw new Error('Cannot delete'); },
|
|
352
|
+
afterDelete: async (ctx) => { await cleanupFiles(ctx.meta?.id); },
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`ResourceHookContext`: `{ data, user?, meta? }` — `data` is the document, `meta` has `id` and `existing` (for update/delete).
|
|
358
|
+
|
|
359
|
+
**App-level (cross-resource):**
|
|
360
|
+
|
|
317
361
|
```typescript
|
|
318
362
|
import { createHookSystem, beforeCreate, afterUpdate } from '@classytic/arc/hooks';
|
|
319
363
|
|
|
@@ -386,8 +430,10 @@ defineResource({
|
|
|
386
430
|
## Error Classes
|
|
387
431
|
|
|
388
432
|
```typescript
|
|
389
|
-
import { ArcError, NotFoundError, ValidationError,
|
|
390
|
-
throw new NotFoundError('Product not found');
|
|
433
|
+
import { ArcError, NotFoundError, ValidationError, createDomainError } from '@classytic/arc';
|
|
434
|
+
throw new NotFoundError('Product not found'); // 404
|
|
435
|
+
throw createDomainError('MEMBER_NOT_FOUND', 'Not found', 404); // domain error with code
|
|
436
|
+
throw createDomainError('SELF_REFERRAL', 'Cannot self-refer', 422, { field: 'referralCode' });
|
|
391
437
|
```
|
|
392
438
|
|
|
393
439
|
Error handler catches: `ArcError` → `.statusCode` (Fastify) → `.status` (MongoKit, http-errors) → `errorMap` → Mongoose/MongoDB → fallback 500. DB-agnostic — any error with `.status` or `.statusCode` gets the correct HTTP response.
|
|
@@ -481,6 +527,107 @@ src/resources/order/
|
|
|
481
527
|
|
|
482
528
|
Generate: `arc generate resource order --mcp` | Wire: `extraTools: [fulfillOrderTool]`
|
|
483
529
|
|
|
530
|
+
**Auto-load resources** — no barrel files, no manual `toPlugin()`:
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import { createApp, loadResources } from '@classytic/arc/factory';
|
|
534
|
+
|
|
535
|
+
const app = await createApp({
|
|
536
|
+
resourcePrefix: '/api/v1', // optional URL prefix
|
|
537
|
+
resources: await loadResources(import.meta.url), // discovers *.resource.ts
|
|
538
|
+
auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
|
|
539
|
+
});
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
`loadResources()` discovers files matching `*.resource.{ts,js,mts,mjs}`, recursively. Pass `import.meta.url` for dev/prod parity (resolves to `src/` in dev, `dist/` in prod automatically). Discovers `default` export, `export const resource`, OR any named export with `toPlugin()` (e.g., `export const userResource`).
|
|
543
|
+
|
|
544
|
+
Options: `exclude`, `include`, `suffix`, `recursive`, `silent`.
|
|
545
|
+
|
|
546
|
+
**Per-resource opt-out of `resourcePrefix`** — for webhooks, admin routes:
|
|
547
|
+
```typescript
|
|
548
|
+
defineResource({ name: 'webhook', prefix: '/hooks', skipGlobalPrefix: true })
|
|
549
|
+
// Registers at /hooks even with createApp({ resourcePrefix: '/api/v1' })
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
**Boot sequence:**
|
|
553
|
+
```typescript
|
|
554
|
+
const app = await createApp({
|
|
555
|
+
resourcePrefix: '/api/v1',
|
|
556
|
+
plugins: async (f) => { await connectDB(); }, // 1. infra (DB, docs)
|
|
557
|
+
bootstrap: [inventoryInit, accountingInit], // 2. domain init (engines)
|
|
558
|
+
resources: await loadResources(import.meta.url), // 3. routes
|
|
559
|
+
afterResources: async (f) => { subscribeEvents(f); }, // 4. post-wiring
|
|
560
|
+
onReady: async (f) => { logger.info('ready'); }, // 5. lifecycle
|
|
561
|
+
});
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
**Audit per-resource opt-in** — no growing exclude lists:
|
|
565
|
+
```typescript
|
|
566
|
+
// Register audit plugin with perResource mode
|
|
567
|
+
await fastify.register(auditPlugin, { autoAudit: { perResource: true } });
|
|
568
|
+
|
|
569
|
+
// Opt-in at the resource level
|
|
570
|
+
defineResource({ name: 'order', audit: true });
|
|
571
|
+
defineResource({ name: 'payment', audit: { operations: ['delete'] } });
|
|
572
|
+
defineResource({ name: 'product' }); // not audited
|
|
573
|
+
|
|
574
|
+
// Manual custom() for MCP/additionalRoutes/read auditing
|
|
575
|
+
app.post('/orders/:id/refund', async (req) => {
|
|
576
|
+
await app.audit.custom('order', req.params.id, 'refund', { reason }, { user });
|
|
577
|
+
});
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Import compatibility:** `loadResources()` uses runtime `import()`. Works with relative imports (`./foo.js`) and Node.js `#` subpath imports (`#shared/utils.js` via `package.json` `imports`). Does **NOT** work with tsconfig path aliases (`@/*`, `~/`) — those are compile-time only.
|
|
581
|
+
|
|
582
|
+
**Vitest workaround** (rare): if resources need engine bootstrap or transitive `node_modules` imports that don't compose with dynamic import:
|
|
583
|
+
```typescript
|
|
584
|
+
import { preloadResources } from '@classytic/arc/testing';
|
|
585
|
+
|
|
586
|
+
export const preloadedResources = preloadResources(
|
|
587
|
+
import.meta.glob('../../src/resources/**/*.resource.ts', { eager: true, import: 'default' }),
|
|
588
|
+
);
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Unified role check** — checks both platform AND org roles:
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
import { roles } from '@classytic/arc/permissions';
|
|
595
|
+
permissions: {
|
|
596
|
+
create: roles('admin', 'editor'), // works with BA org roles + platform roles
|
|
597
|
+
delete: roles('admin'),
|
|
598
|
+
}
|
|
599
|
+
// Also: requireRoles(['admin'], { includeOrgRoles: true }) for backward compat
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
**DX helpers:**
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
// Typed request for wrapHandler: false routes — no more (req as any).user
|
|
606
|
+
import type { ArcRequest } from '@classytic/arc';
|
|
607
|
+
handler: async (req: ArcRequest, reply) => { req.user?.id; req.scope; req.signal; }
|
|
608
|
+
|
|
609
|
+
// Response envelope — no manual { success, data } wrapping
|
|
610
|
+
import { envelope } from '@classytic/arc';
|
|
611
|
+
reply.send(envelope(data, { total: 100 }));
|
|
612
|
+
|
|
613
|
+
// Canonical org extraction — replaces 19 duplicated patterns
|
|
614
|
+
import { getOrgContext } from '@classytic/arc/scope';
|
|
615
|
+
const { userId, organizationId, roles, orgRoles } = getOrgContext(request);
|
|
616
|
+
|
|
617
|
+
// Domain errors with auto HTTP status mapping
|
|
618
|
+
import { createDomainError } from '@classytic/arc';
|
|
619
|
+
throw createDomainError('SELF_REFERRAL', 'Cannot refer yourself', 422);
|
|
620
|
+
|
|
621
|
+
// Resource lifecycle hook — wire singletons during registration
|
|
622
|
+
defineResource({ name: 'notification', onRegister: (f) => setSseManager(f.sseManager) });
|
|
623
|
+
|
|
624
|
+
// SSE auth — preAuth runs BEFORE auth middleware (EventSource can't set headers)
|
|
625
|
+
additionalRoutes: [{ preAuth: [(req) => { req.headers.authorization = `Bearer ${req.query.token}`; }] }]
|
|
626
|
+
|
|
627
|
+
// SSE streaming — auto headers + bypasses response wrapper
|
|
628
|
+
additionalRoutes: [{ streamResponse: true, handler: async (req, reply) => reply.send(stream) }]
|
|
629
|
+
```
|
|
630
|
+
|
|
484
631
|
## Subpath Imports
|
|
485
632
|
|
|
486
633
|
```typescript
|
|
@@ -12,11 +12,34 @@ npm install @modelcontextprotocol/sdk zod
|
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { mcpPlugin } from '@classytic/arc/mcp';
|
|
15
|
+
import { createApp, loadResources } from '@classytic/arc/factory';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
// Option A: Explicit resources
|
|
18
|
+
const app = await createApp({
|
|
17
19
|
resources: [productResource, taskResource],
|
|
18
20
|
auth: false,
|
|
19
|
-
|
|
21
|
+
plugins: async (f) => {
|
|
22
|
+
await f.register(mcpPlugin, { resources: [productResource, taskResource], auth: false });
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Option B: Auto-discover from directory
|
|
27
|
+
const resources = await loadResources('./src/resources');
|
|
28
|
+
const app = await createApp({
|
|
29
|
+
resources,
|
|
30
|
+
plugins: async (f) => {
|
|
31
|
+
await f.register(mcpPlugin, { resources, auth: false });
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Per-resource overrides:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
await app.register(mcpPlugin, {
|
|
40
|
+
resources,
|
|
41
|
+
auth: false,
|
|
42
|
+
include: ['product', 'order'], // only these get MCP tools
|
|
20
43
|
overrides: { product: { operations: ['list', 'get'] } },
|
|
21
44
|
});
|
|
22
45
|
```
|
|
@@ -429,3 +452,138 @@ await app.register(mcpPlugin, {
|
|
|
429
452
|
- `DELETE /mcp` — terminates session
|
|
430
453
|
|
|
431
454
|
Sessions: lazily created, TTL-cached, LRU-evicted at max capacity, auto-cleaned on shutdown.
|
|
455
|
+
|
|
456
|
+
## Health Endpoint
|
|
457
|
+
|
|
458
|
+
`GET /mcp/health` — no MCP protocol needed, plain JSON:
|
|
459
|
+
|
|
460
|
+
```json
|
|
461
|
+
{
|
|
462
|
+
"status": "ok",
|
|
463
|
+
"mode": "stateless",
|
|
464
|
+
"tools": 11,
|
|
465
|
+
"resources": 2,
|
|
466
|
+
"toolNames": ["list_products", "get_product", ...],
|
|
467
|
+
"sessions": null
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Use to verify the MCP server is alive before configuring Claude CLI.
|
|
472
|
+
|
|
473
|
+
## DX Helpers (v2.4.4)
|
|
474
|
+
|
|
475
|
+
### ArcRequest — Typed Fastify Request
|
|
476
|
+
|
|
477
|
+
For `wrapHandler: false` routes, use `ArcRequest` instead of `(req as any).user`:
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import type { ArcRequest } from '@classytic/arc';
|
|
481
|
+
|
|
482
|
+
handler: async (req: ArcRequest, reply) => {
|
|
483
|
+
req.user?.id; // typed
|
|
484
|
+
req.scope.organizationId; // typed (when member)
|
|
485
|
+
req.signal; // AbortSignal (Fastify 5 built-in)
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### envelope() — Response Helper
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
import { envelope } from '@classytic/arc';
|
|
493
|
+
|
|
494
|
+
handler: async (req, reply) => {
|
|
495
|
+
const data = await service.getResults();
|
|
496
|
+
return reply.send(envelope(data));
|
|
497
|
+
// → { success: true, data }
|
|
498
|
+
return reply.send(envelope(data, { total: 100, page: 1 }));
|
|
499
|
+
// → { success: true, data, total: 100, page: 1 }
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### getOrgContext() — Canonical Org Extraction
|
|
504
|
+
|
|
505
|
+
Eliminates duplicated `req.user.organizationId || req.headers['x-organization-id']` patterns:
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
import { getOrgContext } from '@classytic/arc/scope';
|
|
509
|
+
|
|
510
|
+
handler: async (req, reply) => {
|
|
511
|
+
const { userId, organizationId, roles, orgRoles } = getOrgContext(req);
|
|
512
|
+
// Works regardless of auth type (JWT, Better Auth, custom)
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### createDomainError() — Error Factory
|
|
517
|
+
|
|
518
|
+
Eliminates manual `if (err.code) return status` mapping:
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
import { createDomainError } from '@classytic/arc';
|
|
522
|
+
|
|
523
|
+
throw createDomainError('MEMBER_NOT_FOUND', 'Member does not exist', 404);
|
|
524
|
+
throw createDomainError('SELF_REFERRAL', 'Cannot refer yourself', 422);
|
|
525
|
+
throw createDomainError('INSUFFICIENT_BALANCE', 'Not enough credits', 402, { balance: 0 });
|
|
526
|
+
// Arc's error handler auto-maps statusCode to HTTP response
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### onRegister — Resource Lifecycle Hook
|
|
530
|
+
|
|
531
|
+
Called during plugin registration with the scoped Fastify instance:
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
defineResource({
|
|
535
|
+
name: 'notification',
|
|
536
|
+
onRegister: (fastify) => {
|
|
537
|
+
setSseManager(fastify.sseManager);
|
|
538
|
+
},
|
|
539
|
+
})
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### preAuth — Pre-Auth Handlers for SSE/WebSocket
|
|
543
|
+
|
|
544
|
+
Run before auth middleware. Use for promoting `?token=` to `Authorization` header (EventSource can't set headers):
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
additionalRoutes: [{
|
|
548
|
+
method: 'GET',
|
|
549
|
+
path: '/stream',
|
|
550
|
+
wrapHandler: false,
|
|
551
|
+
permissions: requireAuth(),
|
|
552
|
+
preAuth: [(req) => {
|
|
553
|
+
const token = req.query?.token;
|
|
554
|
+
if (token) req.headers.authorization = `Bearer ${token}`;
|
|
555
|
+
}],
|
|
556
|
+
handler: sseHandler,
|
|
557
|
+
}]
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### streamResponse — SSE Route Flag
|
|
561
|
+
|
|
562
|
+
Auto-sets SSE headers and bypasses Arc's response wrapper:
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
additionalRoutes: [{
|
|
566
|
+
method: 'POST',
|
|
567
|
+
path: '/stream',
|
|
568
|
+
streamResponse: true, // SSE headers + no { success, data } wrapper
|
|
569
|
+
permissions: requireAuth(),
|
|
570
|
+
handler: async (request, reply) => {
|
|
571
|
+
const { stream } = await generateStream({ abortSignal: request.signal });
|
|
572
|
+
return reply.send(stream);
|
|
573
|
+
},
|
|
574
|
+
}]
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
## Test Coverage
|
|
578
|
+
|
|
579
|
+
165 test files, 2439 tests. MCP-specific:
|
|
580
|
+
|
|
581
|
+
| Test File | Tests | Covers |
|
|
582
|
+
|-----------|-------|--------|
|
|
583
|
+
| `mcp-auth-e2e.test.ts` | 16 | All auth modes, multi-tenancy, permission filters, async permissions |
|
|
584
|
+
| `mcp-dx-features.test.ts` | 14 | include, names, prefix, disableDefaultRoutes, mcpHandler, guards, CRUD lifecycle |
|
|
585
|
+
| `resourceToTools.test.ts` | 12 | Tool generation, annotations, field hiding, soft delete |
|
|
586
|
+
| `createMcpServer.test.ts` | 10 | Server creation, tool registration, InMemoryTransport |
|
|
587
|
+
| `guards.test.ts` | 8 | requireAuth, requireOrg, requireRole, customGuard, composition |
|
|
588
|
+
| `dx-features.test.ts` | 17 | envelope, getOrgContext, createDomainError, onRegister, preAuth, streamResponse |
|
|
589
|
+
| Others | 32 | fieldRulesToZod, defineTool, definePrompt, buildRequestContext, sessionCache, authCache |
|
|
File without changes
|