@classytic/arc 2.4.3 → 2.5.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.
Files changed (58) hide show
  1. package/dist/{BaseController-CkM5dUh_.mjs → BaseController-CNwMYpDW.mjs} +1 -1
  2. package/dist/adapters/index.d.mts +2 -2
  3. package/dist/auth/index.d.mts +1 -1
  4. package/dist/auth/index.mjs +2 -2
  5. package/dist/core/index.d.mts +2 -2
  6. package/dist/core/index.mjs +2 -2
  7. package/dist/{createApp-CBgVaFyh.mjs → createApp-oic3-ieX.mjs} +3 -3
  8. package/dist/{defineResource-B22gcNvn.mjs → defineResource-BYm3CIoe.mjs} +85 -10
  9. package/dist/docs/index.d.mts +1 -1
  10. package/dist/dynamic/index.d.mts +1 -1
  11. package/dist/dynamic/index.mjs +2 -2
  12. package/dist/{elevation-Ca_yveIO.d.mts → elevation-C_taLQrM.d.mts} +27 -1
  13. package/dist/{errorHandler-DMbGdzBG.mjs → errorHandler-r2595m8T.mjs} +1 -1
  14. package/dist/{errors-CPpvPHT0.d.mts → errors-CcVbl1-T.d.mts} +17 -1
  15. package/dist/{errors-rxhfP7Hf.mjs → errors-NoQKsbAT.mjs} +23 -1
  16. package/dist/factory/index.d.mts +1 -1
  17. package/dist/factory/index.mjs +1 -1
  18. package/dist/hooks/index.d.mts +1 -1
  19. package/dist/{index-BL8CaQih.d.mts → index-TG7-pXDC.d.mts} +2 -2
  20. package/dist/{index-yhxyjqNb.d.mts → index-bX8T5bmn.d.mts} +4 -8
  21. package/dist/index.d.mts +5 -5
  22. package/dist/index.mjs +7 -6
  23. package/dist/integrations/event-gateway.mjs +1 -1
  24. package/dist/integrations/index.d.mts +1 -1
  25. package/dist/integrations/mcp/index.d.mts +4 -2
  26. package/dist/integrations/mcp/index.mjs +1 -1
  27. package/dist/integrations/mcp/testing.d.mts +1 -1
  28. package/dist/integrations/mcp/testing.mjs +1 -1
  29. package/dist/{interface-DGmPxakH.d.mts → interface-BnNjdl33.d.mts} +170 -8
  30. package/dist/org/index.d.mts +1 -1
  31. package/dist/org/index.mjs +1 -1
  32. package/dist/permissions/index.mjs +1 -1
  33. package/dist/{permissions-Jk5x3sxz.mjs → permissions-D9_AAtvz.mjs} +1 -1
  34. package/dist/plugins/index.d.mts +1 -1
  35. package/dist/plugins/index.mjs +3 -3
  36. package/dist/plugins/tracing-entry.mjs +1 -1
  37. package/dist/presets/index.d.mts +1 -1
  38. package/dist/presets/index.mjs +1 -1
  39. package/dist/presets/multiTenant.d.mts +1 -1
  40. package/dist/presets/multiTenant.mjs +1 -1
  41. package/dist/{presets-OMPaHMTY.mjs → presets-CD3e6M7c.mjs} +2 -2
  42. package/dist/registry/index.d.mts +1 -1
  43. package/dist/{resourceToTools-PMFE8HIv.mjs → resourceToTools-B1B1svLx.mjs} +81 -7
  44. package/dist/scope/index.d.mts +2 -2
  45. package/dist/scope/index.mjs +2 -2
  46. package/dist/{sse-BkViJPlT.mjs → sse-BF7GR7IB.mjs} +1 -1
  47. package/dist/testing/index.d.mts +2 -2
  48. package/dist/testing/index.mjs +1 -1
  49. package/dist/types/index.d.mts +3 -3
  50. package/dist/types/index.mjs +23 -2
  51. package/dist/{types-C6TQjtdi.mjs → types-BhtYdxZU.mjs} +26 -1
  52. package/dist/{types-BJmgxNbF.d.mts → types-ByCPlfZ1.d.mts} +1 -1
  53. package/dist/{types-Dt0-AI6E.d.mts → types-Guk83PDz.d.mts} +2 -2
  54. package/dist/utils/index.d.mts +2 -2
  55. package/dist/utils/index.mjs +1 -1
  56. package/package.json +4 -4
  57. package/skills/arc/SKILL.md +53 -2
  58. package/skills/arc/references/mcp.md +135 -0
@@ -1,4 +1,4 @@
1
- import { Lt as ResourceDefinition } from "./interface-DGmPxakH.mjs";
1
+ import { Bt as ResourceDefinition } from "./interface-BnNjdl33.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/integrations/mcp/types.d.ts
@@ -1,5 +1,5 @@
1
- import { n as ElevationOptions } from "./elevation-Ca_yveIO.mjs";
2
- import { h as Authenticator } from "./interface-DGmPxakH.mjs";
1
+ import { n as ElevationOptions } from "./elevation-C_taLQrM.mjs";
2
+ import { g as Authenticator } from "./interface-BnNjdl33.mjs";
3
3
  import { t as ExternalOpenApiPaths } from "./externalPaths-DpO-s7r8.mjs";
4
4
  import { i as CacheStore } from "./interface-D_BWALyZ.mjs";
5
5
  import { r as QueryCachePluginOptions } from "./queryCachePlugin-DcmETvcB.mjs";
@@ -1,5 +1,5 @@
1
- import { G as ParsedQuery, U as OpenApiSchemas, X as QueryParserInterface, l as AnyRecord } from "../interface-DGmPxakH.mjs";
2
- import { a as NotFoundError, c as RateLimitError, d as ValidationError, f as createError, i as ForbiddenError, l as ServiceUnavailableError, n as ConflictError, o as OrgAccessDeniedError, p as isArcError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-CPpvPHT0.mjs";
1
+ import { K as ParsedQuery, W as OpenApiSchemas, Z as QueryParserInterface, l as AnyRecord } from "../interface-BnNjdl33.mjs";
2
+ import { a as NotFoundError, c as RateLimitError, d as ValidationError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-CcVbl1-T.mjs";
3
3
  import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-JP2GdJ4b.mjs";
4
4
  import { FastifyInstance } from "fastify";
5
5
 
@@ -1,7 +1,7 @@
1
1
  import { n as createQueryParser, t as ArcQueryParser } from "../queryParser-CgCtsjti.mjs";
2
2
  import { a as createCircuitBreaker, i as CircuitState, n as CircuitBreakerError, o as createCircuitBreakerRegistry, r as CircuitBreakerRegistry, t as CircuitBreaker } from "../circuitBreaker-BOBOpN2w.mjs";
3
3
  import { _ as defineCompensation, a as getListQueryParams, c as listResponse, d as paginateWrapper, f as paginationSchema, g as wrapResponse, h as successResponseSchema, i as getDefaultCrudSchemas, l as messageWrapper, m as responses, n as deleteResponse, o as itemResponse, p as queryParams, r as errorResponseSchema, s as itemWrapper, t as createStateMachine, u as mutationResponse, v as withCompensation } from "../utils-Dc0WhlIl.mjs";
4
- import { a as OrgAccessDeniedError, c as ServiceUnavailableError, d as createError, f as isArcError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-rxhfP7Hf.mjs";
4
+ import { a as OrgAccessDeniedError, c as ServiceUnavailableError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-NoQKsbAT.mjs";
5
5
  import { a as toJsonSchema, i as isZodSchema, n as convertRouteSchema, r as isJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-DjzHpFam.mjs";
6
6
  import { t as hasEvents } from "../typeGuards-Cj5Rgvlg.mjs";
7
7
  export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UnauthorizedError, ValidationError, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createError, createQueryParser, createStateMachine, defineCompensation, deleteResponse, errorResponseSchema, getDefaultCrudSchemas, getListQueryParams, hasEvents, isArcError, isJsonSchema, isZodSchema, itemResponse, itemWrapper, listResponse, messageWrapper, mutationResponse, paginateWrapper, paginationSchema, queryParams, responses, successResponseSchema, toJsonSchema, withCompensation, wrapResponse };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.4.3",
3
+ "version": "2.5.1",
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.4.5",
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": "^8.0.0 || ^9.0.0",
247
+ "mongoose": ">=9.0.0",
248
248
  "pino-pretty": "^13.0.0",
249
249
  "zod": "^4.0.0"
250
250
  },
@@ -337,7 +337,7 @@
337
337
  },
338
338
  "devDependencies": {
339
339
  "@biomejs/biome": "^2.4.10",
340
- "@classytic/mongokit": "^3.4.5",
340
+ "@classytic/mongokit": "^3.5.2",
341
341
  "@fastify/jwt": "^10.0.0",
342
342
  "@fastify/multipart": "^9.0.0",
343
343
  "@fastify/type-provider-typebox": "^6.0.0",
@@ -314,6 +314,26 @@ const app = await createApp({
314
314
 
315
315
  ## Hooks
316
316
 
317
+ **Inline on resource (recommended):**
318
+
319
+ ```typescript
320
+ defineResource({
321
+ name: 'chat',
322
+ hooks: {
323
+ beforeCreate: async (ctx) => { ctx.data.slug = slugify(ctx.data.name); },
324
+ afterCreate: async (ctx) => { analytics.track('created', { id: ctx.data._id, user: ctx.user?.id }); },
325
+ beforeUpdate: async (ctx) => { console.log('Updating', ctx.meta?.id, 'existing:', ctx.meta?.existing); },
326
+ afterUpdate: async (ctx) => { await invalidateCache(ctx.data._id); },
327
+ beforeDelete: async (ctx) => { if (ctx.data.isProtected) throw new Error('Cannot delete'); },
328
+ afterDelete: async (ctx) => { await cleanupFiles(ctx.meta?.id); },
329
+ },
330
+ });
331
+ ```
332
+
333
+ `ResourceHookContext`: `{ data, user?, meta? }` — `data` is the document, `meta` has `id` and `existing` (for update/delete).
334
+
335
+ **App-level (cross-resource):**
336
+
317
337
  ```typescript
318
338
  import { createHookSystem, beforeCreate, afterUpdate } from '@classytic/arc/hooks';
319
339
 
@@ -386,8 +406,10 @@ defineResource({
386
406
  ## Error Classes
387
407
 
388
408
  ```typescript
389
- import { ArcError, NotFoundError, ValidationError, UnauthorizedError, ForbiddenError } from '@classytic/arc';
390
- throw new NotFoundError('Product not found'); // 404
409
+ import { ArcError, NotFoundError, ValidationError, createDomainError } from '@classytic/arc';
410
+ throw new NotFoundError('Product not found'); // 404
411
+ throw createDomainError('MEMBER_NOT_FOUND', 'Not found', 404); // domain error with code
412
+ throw createDomainError('SELF_REFERRAL', 'Cannot self-refer', 422, { field: 'referralCode' });
391
413
  ```
392
414
 
393
415
  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 +503,35 @@ src/resources/order/
481
503
 
482
504
  Generate: `arc generate resource order --mcp` | Wire: `extraTools: [fulfillOrderTool]`
483
505
 
506
+ **DX helpers** (v2.4.4):
507
+
508
+ ```typescript
509
+ // Typed request for wrapHandler: false routes — no more (req as any).user
510
+ import type { ArcRequest } from '@classytic/arc';
511
+ handler: async (req: ArcRequest, reply) => { req.user?.id; req.scope; req.signal; }
512
+
513
+ // Response envelope — no manual { success, data } wrapping
514
+ import { envelope } from '@classytic/arc';
515
+ reply.send(envelope(data, { total: 100 }));
516
+
517
+ // Canonical org extraction — replaces 19 duplicated patterns
518
+ import { getOrgContext } from '@classytic/arc/scope';
519
+ const { userId, organizationId, roles, orgRoles } = getOrgContext(request);
520
+
521
+ // Domain errors with auto HTTP status mapping
522
+ import { createDomainError } from '@classytic/arc';
523
+ throw createDomainError('SELF_REFERRAL', 'Cannot refer yourself', 422);
524
+
525
+ // Resource lifecycle hook — wire singletons during registration
526
+ defineResource({ name: 'notification', onRegister: (f) => setSseManager(f.sseManager) });
527
+
528
+ // SSE auth — preAuth runs BEFORE auth middleware (EventSource can't set headers)
529
+ additionalRoutes: [{ preAuth: [(req) => { req.headers.authorization = `Bearer ${req.query.token}`; }] }]
530
+
531
+ // SSE streaming — auto headers + bypasses response wrapper
532
+ additionalRoutes: [{ streamResponse: true, handler: async (req, reply) => reply.send(stream) }]
533
+ ```
534
+
484
535
  ## Subpath Imports
485
536
 
486
537
  ```typescript
@@ -429,3 +429,138 @@ await app.register(mcpPlugin, {
429
429
  - `DELETE /mcp` — terminates session
430
430
 
431
431
  Sessions: lazily created, TTL-cached, LRU-evicted at max capacity, auto-cleaned on shutdown.
432
+
433
+ ## Health Endpoint
434
+
435
+ `GET /mcp/health` — no MCP protocol needed, plain JSON:
436
+
437
+ ```json
438
+ {
439
+ "status": "ok",
440
+ "mode": "stateless",
441
+ "tools": 11,
442
+ "resources": 2,
443
+ "toolNames": ["list_products", "get_product", ...],
444
+ "sessions": null
445
+ }
446
+ ```
447
+
448
+ Use to verify the MCP server is alive before configuring Claude CLI.
449
+
450
+ ## DX Helpers (v2.4.4)
451
+
452
+ ### ArcRequest — Typed Fastify Request
453
+
454
+ For `wrapHandler: false` routes, use `ArcRequest` instead of `(req as any).user`:
455
+
456
+ ```typescript
457
+ import type { ArcRequest } from '@classytic/arc';
458
+
459
+ handler: async (req: ArcRequest, reply) => {
460
+ req.user?.id; // typed
461
+ req.scope.organizationId; // typed (when member)
462
+ req.signal; // AbortSignal (Fastify 5 built-in)
463
+ }
464
+ ```
465
+
466
+ ### envelope() — Response Helper
467
+
468
+ ```typescript
469
+ import { envelope } from '@classytic/arc';
470
+
471
+ handler: async (req, reply) => {
472
+ const data = await service.getResults();
473
+ return reply.send(envelope(data));
474
+ // → { success: true, data }
475
+ return reply.send(envelope(data, { total: 100, page: 1 }));
476
+ // → { success: true, data, total: 100, page: 1 }
477
+ }
478
+ ```
479
+
480
+ ### getOrgContext() — Canonical Org Extraction
481
+
482
+ Eliminates duplicated `req.user.organizationId || req.headers['x-organization-id']` patterns:
483
+
484
+ ```typescript
485
+ import { getOrgContext } from '@classytic/arc/scope';
486
+
487
+ handler: async (req, reply) => {
488
+ const { userId, organizationId, roles, orgRoles } = getOrgContext(req);
489
+ // Works regardless of auth type (JWT, Better Auth, custom)
490
+ }
491
+ ```
492
+
493
+ ### createDomainError() — Error Factory
494
+
495
+ Eliminates manual `if (err.code) return status` mapping:
496
+
497
+ ```typescript
498
+ import { createDomainError } from '@classytic/arc';
499
+
500
+ throw createDomainError('MEMBER_NOT_FOUND', 'Member does not exist', 404);
501
+ throw createDomainError('SELF_REFERRAL', 'Cannot refer yourself', 422);
502
+ throw createDomainError('INSUFFICIENT_BALANCE', 'Not enough credits', 402, { balance: 0 });
503
+ // Arc's error handler auto-maps statusCode to HTTP response
504
+ ```
505
+
506
+ ### onRegister — Resource Lifecycle Hook
507
+
508
+ Called during plugin registration with the scoped Fastify instance:
509
+
510
+ ```typescript
511
+ defineResource({
512
+ name: 'notification',
513
+ onRegister: (fastify) => {
514
+ setSseManager(fastify.sseManager);
515
+ },
516
+ })
517
+ ```
518
+
519
+ ### preAuth — Pre-Auth Handlers for SSE/WebSocket
520
+
521
+ Run before auth middleware. Use for promoting `?token=` to `Authorization` header (EventSource can't set headers):
522
+
523
+ ```typescript
524
+ additionalRoutes: [{
525
+ method: 'GET',
526
+ path: '/stream',
527
+ wrapHandler: false,
528
+ permissions: requireAuth(),
529
+ preAuth: [(req) => {
530
+ const token = req.query?.token;
531
+ if (token) req.headers.authorization = `Bearer ${token}`;
532
+ }],
533
+ handler: sseHandler,
534
+ }]
535
+ ```
536
+
537
+ ### streamResponse — SSE Route Flag
538
+
539
+ Auto-sets SSE headers and bypasses Arc's response wrapper:
540
+
541
+ ```typescript
542
+ additionalRoutes: [{
543
+ method: 'POST',
544
+ path: '/stream',
545
+ streamResponse: true, // SSE headers + no { success, data } wrapper
546
+ permissions: requireAuth(),
547
+ handler: async (request, reply) => {
548
+ const { stream } = await generateStream({ abortSignal: request.signal });
549
+ return reply.send(stream);
550
+ },
551
+ }]
552
+ ```
553
+
554
+ ## Test Coverage
555
+
556
+ 165 test files, 2439 tests. MCP-specific:
557
+
558
+ | Test File | Tests | Covers |
559
+ |-----------|-------|--------|
560
+ | `mcp-auth-e2e.test.ts` | 16 | All auth modes, multi-tenancy, permission filters, async permissions |
561
+ | `mcp-dx-features.test.ts` | 14 | include, names, prefix, disableDefaultRoutes, mcpHandler, guards, CRUD lifecycle |
562
+ | `resourceToTools.test.ts` | 12 | Tool generation, annotations, field hiding, soft delete |
563
+ | `createMcpServer.test.ts` | 10 | Server creation, tool registration, InMemoryTransport |
564
+ | `guards.test.ts` | 8 | requireAuth, requireOrg, requireRole, customGuard, composition |
565
+ | `dx-features.test.ts` | 17 | envelope, getOrgContext, createDomainError, onRegister, preAuth, streamResponse |
566
+ | Others | 32 | fieldRulesToZod, defineTool, definePrompt, buildRequestContext, sessionCache, authCache |