@classytic/arc 2.4.2 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -6
- package/dist/{BaseController-CkM5dUh_.mjs → BaseController-AbbRx3e0.mjs} +5 -2
- 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 +1 -1
- package/dist/audit/index.mjs +11 -1
- 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/cache/index.mjs +2 -2
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/generate.d.mts +1 -1
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.d.mts +1 -1
- package/dist/cli/commands/init.mjs +13 -10
- package/dist/cli/commands/introspect.d.mts +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.d.mts +4 -4
- package/dist/cli/index.mjs +4 -4
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +2 -2
- package/dist/{createApp-ByWNRsZj.mjs → createApp-Bol7DLUf.mjs} +404 -279
- package/dist/{defineResource-D9aY5Cy6.mjs → defineResource-bVKHjQzE.mjs} +116 -19
- package/dist/discovery/index.d.mts +1 -1
- package/dist/discovery/index.mjs +2 -2
- 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--zp54tGc.mjs → errorHandler-r2595m8T.mjs} +5 -5
- 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/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +1 -1
- package/dist/factory/index.d.mts +44 -23
- package/dist/factory/index.mjs +136 -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-yhxyjqNb.d.mts → index-BIsZ_su5.d.mts} +4 -8
- package/dist/{index-BL8CaQih.d.mts → index-Cb3gtbg7.d.mts} +2 -2
- 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.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +2 -2
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/jobs.d.mts +1 -1
- package/dist/integrations/jobs.mjs +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/integrations/streamline.d.mts +1 -1
- package/dist/integrations/streamline.mjs +1 -1
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket-redis.mjs +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +1 -1
- package/dist/{interface-DGmPxakH.d.mts → interface-DDW43OmS.d.mts} +194 -13
- package/dist/{memory-Cb_7iy9e.mjs → memory-BFAYkf8H.mjs} +1 -4
- 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-CA5zg0yK.mjs → permissions-C8ImI8gC.mjs} +45 -3
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +3 -3
- package/dist/plugins/response-cache.d.mts +1 -1
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/index.d.mts +2 -2
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +2 -2
- package/dist/presets/multiTenant.mjs +2 -2
- package/dist/{presets-C9QXJV1u.mjs → presets-BMfdy34e.mjs} +3 -3
- package/dist/{queryCachePlugin-ClosZdNS.mjs → queryCachePlugin-XtFplYO9.mjs} +1 -1
- package/dist/{redis-CQ5YxMC5.d.mts → redis-D0Qc-9EW.d.mts} +1 -1
- package/dist/registry/index.d.mts +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 +2 -2
- package/dist/testing/index.mjs +1 -1
- 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-D5hJ-k_3.d.mts} +189 -6
- package/dist/{types-BJmgxNbF.d.mts → types-D5rjsS_i.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +1 -1
- package/package.json +7 -5
- package/skills/arc/SKILL.md +115 -8
- package/skills/arc/references/mcp.md +160 -2
- /package/dist/{interface-B4awm1RJ.d.mts → interface-gr-7qo9j.d.mts} +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.0
|
|
12
12
|
license: MIT
|
|
13
13
|
metadata:
|
|
14
14
|
author: Classytic
|
|
15
|
-
version: "2.
|
|
15
|
+
version: "2.6.0"
|
|
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
|
|
|
@@ -360,7 +404,7 @@ GET /products?lookup[cat][from]=categories&...&lookup[cat][select]=name,slug
|
|
|
360
404
|
|
|
361
405
|
Operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `like`, `regex`, `exists`
|
|
362
406
|
|
|
363
|
-
**Custom query parser (e.g., MongoKit for $lookup
|
|
407
|
+
**Custom query parser (e.g., MongoKit >=3.4.5 for $lookup, whitelists, MCP auto-derive):**
|
|
364
408
|
|
|
365
409
|
```typescript
|
|
366
410
|
import { QueryParser } from '@classytic/mongokit';
|
|
@@ -368,11 +412,16 @@ import { QueryParser } from '@classytic/mongokit';
|
|
|
368
412
|
defineResource({
|
|
369
413
|
name: 'product',
|
|
370
414
|
adapter: createMongooseAdapter({ model: ProductModel, repository: productRepo }),
|
|
371
|
-
queryParser: new QueryParser(
|
|
415
|
+
queryParser: new QueryParser({
|
|
416
|
+
allowedFilterFields: ['status', 'category', 'orgId'], // whitelist filter fields
|
|
417
|
+
allowedSortFields: ['createdAt', 'price'], // whitelist sort fields
|
|
418
|
+
allowedOperators: ['eq', 'gte', 'lte', 'in'], // whitelist operators
|
|
419
|
+
}),
|
|
420
|
+
// MCP auto-derives filterableFields from queryParser — no duplication needed
|
|
372
421
|
schemaOptions: {
|
|
373
422
|
query: {
|
|
374
|
-
allowedPopulate: ['category', 'brand'],
|
|
375
|
-
allowedLookups: ['categories', 'brands'],
|
|
423
|
+
allowedPopulate: ['category', 'brand'],
|
|
424
|
+
allowedLookups: ['categories', 'brands'],
|
|
376
425
|
},
|
|
377
426
|
},
|
|
378
427
|
});
|
|
@@ -381,10 +430,14 @@ defineResource({
|
|
|
381
430
|
## Error Classes
|
|
382
431
|
|
|
383
432
|
```typescript
|
|
384
|
-
import { ArcError, NotFoundError, ValidationError,
|
|
385
|
-
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' });
|
|
386
437
|
```
|
|
387
438
|
|
|
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.
|
|
440
|
+
|
|
388
441
|
## Compensating Transaction
|
|
389
442
|
|
|
390
443
|
In-process rollback for multi-step operations. Not a distributed saga — use Temporal/Streamline for that.
|
|
@@ -474,6 +527,60 @@ src/resources/order/
|
|
|
474
527
|
|
|
475
528
|
Generate: `arc generate resource order --mcp` | Wire: `extraTools: [fulfillOrderTool]`
|
|
476
529
|
|
|
530
|
+
**Auto-load resources** (v2.6.0) — no barrel files, no manual `toPlugin()`:
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import { createApp, loadResources } from '@classytic/arc/factory';
|
|
534
|
+
|
|
535
|
+
const app = await createApp({
|
|
536
|
+
resources: await loadResources('./src/resources'), // discovers *.resource.ts
|
|
537
|
+
auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
|
|
538
|
+
});
|
|
539
|
+
// loadResources options: exclude, include, suffix, recursive
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**Import compatibility:** `loadResources()` uses runtime `import()`. Works with relative imports (`./foo.js`) and Node.js `#` subpath imports (`#shared/utils.js` via `package.json` `imports` — both `.js` and `.ts` extensions). Does **NOT** work with tsconfig path aliases (`@/*`, `~/`) — those are compile-time only, Node.js ignores them. Projects using tsconfig aliases should use explicit `resources: [r1, r2]` instead.
|
|
543
|
+
|
|
544
|
+
**Unified role check** (v2.6.0) — checks both platform AND org roles:
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import { roles } from '@classytic/arc/permissions';
|
|
548
|
+
permissions: {
|
|
549
|
+
create: roles('admin', 'editor'), // works with BA org roles + platform roles
|
|
550
|
+
delete: roles('admin'),
|
|
551
|
+
}
|
|
552
|
+
// Also: requireRoles(['admin'], { includeOrgRoles: true }) for backward compat
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**DX helpers** (v2.4.4):
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
// Typed request for wrapHandler: false routes — no more (req as any).user
|
|
559
|
+
import type { ArcRequest } from '@classytic/arc';
|
|
560
|
+
handler: async (req: ArcRequest, reply) => { req.user?.id; req.scope; req.signal; }
|
|
561
|
+
|
|
562
|
+
// Response envelope — no manual { success, data } wrapping
|
|
563
|
+
import { envelope } from '@classytic/arc';
|
|
564
|
+
reply.send(envelope(data, { total: 100 }));
|
|
565
|
+
|
|
566
|
+
// Canonical org extraction — replaces 19 duplicated patterns
|
|
567
|
+
import { getOrgContext } from '@classytic/arc/scope';
|
|
568
|
+
const { userId, organizationId, roles, orgRoles } = getOrgContext(request);
|
|
569
|
+
|
|
570
|
+
// Domain errors with auto HTTP status mapping
|
|
571
|
+
import { createDomainError } from '@classytic/arc';
|
|
572
|
+
throw createDomainError('SELF_REFERRAL', 'Cannot refer yourself', 422);
|
|
573
|
+
|
|
574
|
+
// Resource lifecycle hook — wire singletons during registration
|
|
575
|
+
defineResource({ name: 'notification', onRegister: (f) => setSseManager(f.sseManager) });
|
|
576
|
+
|
|
577
|
+
// SSE auth — preAuth runs BEFORE auth middleware (EventSource can't set headers)
|
|
578
|
+
additionalRoutes: [{ preAuth: [(req) => { req.headers.authorization = `Bearer ${req.query.token}`; }] }]
|
|
579
|
+
|
|
580
|
+
// SSE streaming — auto headers + bypasses response wrapper
|
|
581
|
+
additionalRoutes: [{ streamResponse: true, handler: async (req, reply) => reply.send(stream) }]
|
|
582
|
+
```
|
|
583
|
+
|
|
477
584
|
## Subpath Imports
|
|
478
585
|
|
|
479
586
|
```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
|