@classytic/arc 2.4.2 → 2.5.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/dist/{BaseController-CkM5dUh_.mjs → BaseController-CNwMYpDW.mjs} +1 -1
- package/dist/adapters/index.d.mts +2 -2
- 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 +1 -1
- 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-oic3-ieX.mjs} +5 -5
- package/dist/{defineResource-D9aY5Cy6.mjs → defineResource-BYm3CIoe.mjs} +85 -10
- package/dist/discovery/index.d.mts +1 -1
- 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--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/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/{index-BL8CaQih.d.mts → index-TG7-pXDC.d.mts} +2 -2
- package/dist/{index-yhxyjqNb.d.mts → index-bX8T5bmn.d.mts} +4 -8
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +7 -6
- 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 +2 -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-BnNjdl33.d.mts} +170 -8
- package/dist/{memory-Cb_7iy9e.mjs → memory-BFAYkf8H.mjs} +1 -4
- package/dist/org/index.d.mts +1 -1
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-CA5zg0yK.mjs → permissions-D9_AAtvz.mjs} +2 -2
- 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-CD3e6M7c.mjs} +3 -3
- package/dist/{queryCachePlugin-ClosZdNS.mjs → queryCachePlugin-XtFplYO9.mjs} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/{resourceToTools-PMFE8HIv.mjs → resourceToTools-CN0lwJrL.mjs} +1 -1
- 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-BJmgxNbF.d.mts → types-ByCPlfZ1.d.mts} +1 -1
- package/dist/{types-Dt0-AI6E.d.mts → types-Guk83PDz.d.mts} +2 -2
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +1 -1
- package/package.json +5 -4
- package/skills/arc/SKILL.md +64 -6
- package/skills/arc/references/mcp.md +135 -0
package/dist/testing/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as CreateAppOptions } from "../types-
|
|
1
|
+
import { Bt as ResourceDefinition, Ht as CrudRepository, l as AnyRecord } from "../interface-BnNjdl33.mjs";
|
|
2
|
+
import { r as CreateAppOptions } from "../types-Guk83PDz.mjs";
|
|
3
3
|
import Fastify, { FastifyInstance, FastifyServerOptions } from "fastify";
|
|
4
4
|
import { Connection } from "mongoose";
|
|
5
5
|
import { Mock } from "vitest";
|
package/dist/testing/index.mjs
CHANGED
|
@@ -1752,7 +1752,7 @@ function runEventTests(resourceName, displayName, events) {
|
|
|
1752
1752
|
* ```
|
|
1753
1753
|
*/
|
|
1754
1754
|
async function createTestApp(options = {}) {
|
|
1755
|
-
const { createApp } = await import("../createApp-
|
|
1755
|
+
const { createApp } = await import("../createApp-oic3-ieX.mjs").then((n) => n.r);
|
|
1756
1756
|
const { useInMemoryDb = true, mongoUri: providedMongoUri, ...appOptions } = options;
|
|
1757
1757
|
const defaultAuth = {
|
|
1758
1758
|
type: "jwt",
|
package/dist/types/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AUTHENTICATED_SCOPE,
|
|
2
|
-
import { $ as
|
|
1
|
+
import { _ as isMember, a as AUTHENTICATED_SCOPE, d as getTeamId, g as isElevated, h as isAuthenticated, l as getOrgId, m as hasOrgAccess, n as ElevationOptions, o as PUBLIC_SCOPE, s as RequestScope, t as ElevationEvent, u as getOrgRoles } from "../elevation-C_taLQrM.mjs";
|
|
2
|
+
import { $ as RegistryEntry, A as GracefulShutdownOptions, B as LookupOption, C as CrudSchemas, D as FastifyWithAuth, E as FastifyRequestExtras, F as InferResourceDoc, Ft as IControllerResponse, G as OwnershipCheck, Gt as PaginationParams, H as MiddlewareHandler, Ht as CrudRepository, I as IntrospectionData, It as IRequestContext, J as PresetFunction, K as ParsedQuery, Kt as QueryOptions, L as IntrospectionPluginOptions, Lt as RouteHandler, M as HealthOptions, Mt as ControllerLike, N as InferAdapterDoc, Nt as FastifyHandler, O as FastifyWithDecorators, P as InferDocType, Pt as IController, Q as RateLimitConfig, R as JWTPayload, S as CrudRouterOptions, St as getUserId, T as EventsDecorator, U as ObjectId, Ut as InferDoc, V as MiddlewareConfig, W as OpenApiSchemas, Wt as PaginatedResult, X as PresetResult, Y as PresetHook, Z as QueryParserInterface, _ as AuthenticatorContext, _t as UserLike, at as ResourceConfig, b as CrudController, bt as ValidationResult, c as AdditionalRoute, ct as ResourceMetadata, d as ArcDecorator, dt as RouteSchemaOptions, et as RegistryStats, f as ArcInternalMetadata, ft as ServiceContext, g as Authenticator, gt as TypedResourceConfig, h as AuthPluginOptions, ht as TypedRepository, it as ResourceCacheConfig, j as HealthCheck, jt as ControllerHandler, k as FieldRule, l as AnyRecord, lt as ResourcePermissions, m as AuthHelpers, mt as TypedController, nt as RequestIdOptions, ot as ResourceHookContext, p as ArcRequest, pt as TokenPair, q as PopulateOption, rt as RequestWithExtras, st as ResourceHooks, tt as RequestContext, u as ApiResponse, ut as RouteHandlerMethod, v as ConfigError, vt as UserOrganization, w as EventDefinition, wt as BaseControllerOptions, x as CrudRouteKey, xt as envelope, y as ControllerQueryOptions, yt as ValidateOptions, z as JwtContext } from "../interface-BnNjdl33.mjs";
|
|
3
3
|
import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-BNUccdcf.mjs";
|
|
4
|
-
export { AUTHENTICATED_SCOPE, AdditionalRoute, AnyRecord, ApiResponse, ArcDecorator, ArcInternalMetadata, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRepository, CrudRouteKey, CrudRouterOptions, CrudSchemas, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OpenApiSchemas, OwnershipCheck, PUBLIC_SCOPE, PaginatedResult, PaginationParams, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryOptions, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResourceCacheConfig, ResourceConfig, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteHandler, RouteHandlerMethod, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
|
|
4
|
+
export { AUTHENTICATED_SCOPE, AdditionalRoute, AnyRecord, ApiResponse, ArcDecorator, ArcInternalMetadata, ArcRequest, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRepository, CrudRouteKey, CrudRouterOptions, CrudSchemas, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OpenApiSchemas, OwnershipCheck, PUBLIC_SCOPE, PaginatedResult, PaginationParams, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryOptions, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResourceCacheConfig, ResourceConfig, ResourceHookContext, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteHandler, RouteHandlerMethod, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult, envelope, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
|
package/dist/types/index.mjs
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as getOrgRoles, d as isElevated, f as isMember, i as getOrgId, l as hasOrgAccess, n as PUBLIC_SCOPE, o as getTeamId, t as AUTHENTICATED_SCOPE, u as isAuthenticated } from "../types-BhtYdxZU.mjs";
|
|
2
2
|
//#region src/types/index.ts
|
|
3
3
|
/**
|
|
4
|
+
* Response envelope helper — wraps data in Arc's standard `{ success, data }` format.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { envelope } from '@classytic/arc';
|
|
9
|
+
*
|
|
10
|
+
* handler: async (req, reply) => {
|
|
11
|
+
* const data = await getResults();
|
|
12
|
+
* return envelope(data);
|
|
13
|
+
* // → { success: true, data }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
function envelope(data, meta) {
|
|
18
|
+
return {
|
|
19
|
+
success: true,
|
|
20
|
+
data,
|
|
21
|
+
...meta
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
4
25
|
* Extract user ID from a user object (supports both id and _id)
|
|
5
26
|
*/
|
|
6
27
|
function getUserId(user) {
|
|
@@ -9,4 +30,4 @@ function getUserId(user) {
|
|
|
9
30
|
return id ? String(id) : void 0;
|
|
10
31
|
}
|
|
11
32
|
//#endregion
|
|
12
|
-
export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
|
|
33
|
+
export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, envelope, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
|
|
@@ -58,9 +58,34 @@ function getUserRoles(scope) {
|
|
|
58
58
|
if (scope.kind === "member") return scope.userRoles;
|
|
59
59
|
return [];
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Org context — canonical extraction from a Fastify request.
|
|
63
|
+
*
|
|
64
|
+
* Works regardless of auth type (JWT, Better Auth, custom) by reading
|
|
65
|
+
* `request.scope` and `request.user`. Eliminates the need for each resource
|
|
66
|
+
* to re-invent org extraction from headers/user/scope.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* import { getOrgContext } from '@classytic/arc/scope';
|
|
71
|
+
*
|
|
72
|
+
* handler: async (request, reply) => {
|
|
73
|
+
* const { userId, organizationId, roles, orgRoles } = getOrgContext(request);
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
function getOrgContext(request) {
|
|
78
|
+
const scope = request.scope ?? { kind: "public" };
|
|
79
|
+
return {
|
|
80
|
+
userId: getUserId(scope) ?? request.user?.id ?? request.user?._id,
|
|
81
|
+
organizationId: getOrgId(scope) ?? request.user?.organizationId ?? request.headers?.["x-organization-id"],
|
|
82
|
+
roles: getUserRoles(scope),
|
|
83
|
+
orgRoles: getOrgRoles(scope)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
61
86
|
/** Default public scope — used as initial decoration value */
|
|
62
87
|
const PUBLIC_SCOPE = Object.freeze({ kind: "public" });
|
|
63
88
|
/** Default authenticated scope — used when user is logged in but no org */
|
|
64
89
|
const AUTHENTICATED_SCOPE = Object.freeze({ kind: "authenticated" });
|
|
65
90
|
//#endregion
|
|
66
|
-
export {
|
|
91
|
+
export { getOrgRoles as a, getUserRoles as c, isElevated as d, isMember as f, getOrgId as i, hasOrgAccess as l, PUBLIC_SCOPE as n, getTeamId as o, getOrgContext as r, getUserId as s, AUTHENTICATED_SCOPE as t, isAuthenticated as u };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as ElevationOptions } from "./elevation-
|
|
2
|
-
import {
|
|
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";
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as NotFoundError, c as RateLimitError, d as ValidationError,
|
|
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
|
|
package/dist/utils/index.mjs
CHANGED
|
@@ -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,
|
|
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.
|
|
3
|
+
"version": "2.5.0",
|
|
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
|
},
|
|
@@ -337,7 +337,7 @@
|
|
|
337
337
|
},
|
|
338
338
|
"devDependencies": {
|
|
339
339
|
"@biomejs/biome": "^2.4.10",
|
|
340
|
-
"@classytic/mongokit": "^3.
|
|
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",
|
|
@@ -349,6 +349,7 @@
|
|
|
349
349
|
"@vitest/coverage-v8": "^3.2.4",
|
|
350
350
|
"fastify-raw-body": "^5.0.0",
|
|
351
351
|
"jsonwebtoken": "^9.0.0",
|
|
352
|
+
"knip": "^6.3.0",
|
|
352
353
|
"mongodb": "^7.1.0",
|
|
353
354
|
"mongodb-memory-server": "^11.0.1",
|
|
354
355
|
"tsdown": "^0.21.7",
|
package/skills/arc/SKILL.md
CHANGED
|
@@ -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
|
|
|
@@ -360,7 +380,7 @@ GET /products?lookup[cat][from]=categories&...&lookup[cat][select]=name,slug
|
|
|
360
380
|
|
|
361
381
|
Operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `like`, `regex`, `exists`
|
|
362
382
|
|
|
363
|
-
**Custom query parser (e.g., MongoKit for $lookup
|
|
383
|
+
**Custom query parser (e.g., MongoKit >=3.4.5 for $lookup, whitelists, MCP auto-derive):**
|
|
364
384
|
|
|
365
385
|
```typescript
|
|
366
386
|
import { QueryParser } from '@classytic/mongokit';
|
|
@@ -368,11 +388,16 @@ import { QueryParser } from '@classytic/mongokit';
|
|
|
368
388
|
defineResource({
|
|
369
389
|
name: 'product',
|
|
370
390
|
adapter: createMongooseAdapter({ model: ProductModel, repository: productRepo }),
|
|
371
|
-
queryParser: new QueryParser(
|
|
391
|
+
queryParser: new QueryParser({
|
|
392
|
+
allowedFilterFields: ['status', 'category', 'orgId'], // whitelist filter fields
|
|
393
|
+
allowedSortFields: ['createdAt', 'price'], // whitelist sort fields
|
|
394
|
+
allowedOperators: ['eq', 'gte', 'lte', 'in'], // whitelist operators
|
|
395
|
+
}),
|
|
396
|
+
// MCP auto-derives filterableFields from queryParser — no duplication needed
|
|
372
397
|
schemaOptions: {
|
|
373
398
|
query: {
|
|
374
|
-
allowedPopulate: ['category', 'brand'],
|
|
375
|
-
allowedLookups: ['categories', 'brands'],
|
|
399
|
+
allowedPopulate: ['category', 'brand'],
|
|
400
|
+
allowedLookups: ['categories', 'brands'],
|
|
376
401
|
},
|
|
377
402
|
},
|
|
378
403
|
});
|
|
@@ -381,10 +406,14 @@ defineResource({
|
|
|
381
406
|
## Error Classes
|
|
382
407
|
|
|
383
408
|
```typescript
|
|
384
|
-
import { ArcError, NotFoundError, ValidationError,
|
|
385
|
-
throw new NotFoundError('Product not found');
|
|
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' });
|
|
386
413
|
```
|
|
387
414
|
|
|
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.
|
|
416
|
+
|
|
388
417
|
## Compensating Transaction
|
|
389
418
|
|
|
390
419
|
In-process rollback for multi-step operations. Not a distributed saga — use Temporal/Streamline for that.
|
|
@@ -474,6 +503,35 @@ src/resources/order/
|
|
|
474
503
|
|
|
475
504
|
Generate: `arc generate resource order --mcp` | Wire: `extraTools: [fulfillOrderTool]`
|
|
476
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
|
+
|
|
477
535
|
## Subpath Imports
|
|
478
536
|
|
|
479
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 |
|