@classytic/arc 2.16.1 → 2.17.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 -2
- package/dist/{BaseController-Vk8I7C0x.mjs → BaseController-CgHomZ4d.mjs} +5 -47
- package/dist/audit/index.mjs +2 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +2 -2
- package/dist/auth/redis-session.mjs +28 -2
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-wKeGXn38.mjs → core-D8tpQ4ZL.mjs} +174 -31
- package/dist/{createActionRouter-eiOFtnC3.mjs → createActionRouter-BHUMb1-y.mjs} +5 -2
- package/dist/{createAggregationRouter-C4unhBnV.mjs → createAggregationRouter-BYVvhfc5.mjs} +1 -1
- package/dist/{createApp-DdPBtmzp.mjs → createApp-CzCEEsEm.mjs} +2 -2
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-Dci0AYLT.mjs → elevation-6mfTLBC-.mjs} +1 -1
- package/dist/{errorHandler-DNPzqaTT.mjs → errorHandler-wQejMBT3.mjs} +45 -4
- package/dist/events/transports/redis-stream-entry.mjs +34 -1
- package/dist/events/transports/redis.mjs +17 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +1 -1
- package/dist/{fastify-augmentation-CJm0vh-l.d.mts → fastify-augmentation-7LIR0nDv.d.mts} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +20 -4
- package/dist/{index-DBDgy6Hw.d.mts → index-C1JSBan-.d.mts} +177 -1
- package/dist/{index-CC_pgZjY.d.mts → index-CruZir1o.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +5 -5
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/jobs.d.mts +36 -1
- package/dist/integrations/jobs.mjs +82 -9
- package/dist/integrations/mcp/index.d.mts +65 -3
- package/dist/integrations/mcp/index.mjs +93 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/websocket.mjs +2 -1
- package/dist/{mcpPlugin-Dbgz9_Rj.mjs → mcpPlugin-C6FViZXw.mjs} +111 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/{multipartBody-BOvVSVCD.mjs → multipartBody-VjVWmZl8.mjs} +2 -1
- package/dist/{openapi-BRnd3pT7.mjs → openapi-DKDs9NfD.mjs} +2 -2
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-CTxMrreC.mjs → permissions-B1Qy55fg.mjs} +169 -169
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +3 -3
- package/dist/plugins/index.mjs +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/presets/filesUpload.mjs +2 -2
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +1 -1
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-C9BE6WaZ.mjs → presets-BYbPobbF.mjs} +5 -3
- package/dist/registry/index.d.mts +1 -1
- package/dist/{routerShared-ClV4cCaa.mjs → routerShared-nCyJGOKX.mjs} +127 -4
- package/dist/scope/index.mjs +2 -2
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.d.mts +1 -1
- package/dist/{types-Dw--SyTq.d.mts → types-BRmS8TNa.d.mts} +1 -1
- package/dist/{types-BD0BCdf6.d.mts → types-CoMjNmkF.d.mts} +12 -1
- package/dist/utils/index.d.mts +79 -2
- package/dist/utils/index.mjs +2 -2
- package/dist/{utils-DPXZnZUx.mjs → utils-BMQK8mRl.mjs} +111 -2
- package/package.json +1 -1
- package/skills/arc/SKILL.md +43 -1
- package/skills/arc/references/api-reference.md +2 -0
- package/skills/arc/references/mcp.md +30 -1
- /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
package/dist/testing/index.mjs
CHANGED
|
@@ -1036,7 +1036,7 @@ function pickDefaultAuth(authMode, callerAuth) {
|
|
|
1036
1036
|
};
|
|
1037
1037
|
}
|
|
1038
1038
|
async function createTestApp(options = {}) {
|
|
1039
|
-
const { createApp } = await import("../createApp-
|
|
1039
|
+
const { createApp } = await import("../createApp-CzCEEsEm.mjs").then((n) => n.r);
|
|
1040
1040
|
const { resources = [], db = "in-memory", connectMongoose = false, authMode = "jwt", defaultOrgId, plugins, auth: callerAuth, ...appOptions } = options;
|
|
1041
1041
|
let dbHandle;
|
|
1042
1042
|
let dbUri;
|
package/dist/types/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as OnTenantDeleteConfig, A as RequestIdOptions, Bt as AuthHelpers, C as RequestContext, D as HealthCheck, E as GracefulShutdownOptions, F as FastifyWithDecorators, G as ActionsMap, Gt as TokenPair, H as ActionDefinition, Ht as Authenticator, I as MiddlewareHandler, J as CrudRouteKey, Jt as ArcRequest, K as ArcFieldRule, Kt as AnyRecord, L as RequestWithExtras, M as EventsDecorator, N as FastifyRequestExtras, O as HealthOptions, P as FastifyWithAuth, Q as MiddlewareConfig, Qt as UserOrganization, S as QueryParserInterface, T as CrudRouterOptions, U as ActionEntry, Ut as AuthenticatorContext, Vt as AuthPluginOptions, W as ActionHandlerFn, Wt as JwtContext, X as EventDefinition, Xt as ObjectId, Y as CrudSchemas, Yt as JWTPayload, Z as FieldRule, Zt as UserLike, _ as ControllerQueryOptions, _t as FastifyHandler, a as InferAdapterDoc, at as ResolvedTenantPurge, b as ParsedQuery, bt as IRequestContext, c as TypedController, cn as BaseControllerOptions, ct as ResourceHookContext, d as PaginationResult, dt as RouteDefinition, et as OpenApiSchemas, f as IntrospectionData, ft as RouteMcpConfig, g as ArcInternalMetadata, gt as ControllerLike, h as ResourceMetadata, ht as ControllerHandler, i as ValidationResult, it as RateLimitConfig, j as ArcDecorator, k as IntrospectionPluginOptions, l as TypedRepository, lt as ResourceHooks, m as RegistryStats, mt as RouteSchemaOptions, n as ConfigError, nt as PresetHook, o as InferDocType, ot as ResourceCacheConfig, p as RegistryEntry, pt as RouteMethod, q as CrudController, qt as ApiResponse, r as ValidateOptions, rt as PresetResult, s as InferResourceDoc, st as ResourceConfig, t as RouteHandlerMethod, tt as PresetFunction, u as TypedResourceConfig, ut as ResourcePermissions, v as LookupOption, vt as IController, w as ServiceContext, x as PopulateOption, xt as RouteHandler, y as OwnershipCheck, yt as IControllerResponse } from "../index-
|
|
1
|
+
import { $ as OnTenantDeleteConfig, A as RequestIdOptions, Bt as AuthHelpers, C as RequestContext, D as HealthCheck, E as GracefulShutdownOptions, F as FastifyWithDecorators, G as ActionsMap, Gt as TokenPair, H as ActionDefinition, Ht as Authenticator, I as MiddlewareHandler, J as CrudRouteKey, Jt as ArcRequest, K as ArcFieldRule, Kt as AnyRecord, L as RequestWithExtras, M as EventsDecorator, N as FastifyRequestExtras, O as HealthOptions, P as FastifyWithAuth, Q as MiddlewareConfig, Qt as UserOrganization, S as QueryParserInterface, T as CrudRouterOptions, U as ActionEntry, Ut as AuthenticatorContext, Vt as AuthPluginOptions, W as ActionHandlerFn, Wt as JwtContext, X as EventDefinition, Xt as ObjectId, Y as CrudSchemas, Yt as JWTPayload, Z as FieldRule, Zt as UserLike, _ as ControllerQueryOptions, _t as FastifyHandler, a as InferAdapterDoc, at as ResolvedTenantPurge, b as ParsedQuery, bt as IRequestContext, c as TypedController, cn as BaseControllerOptions, ct as ResourceHookContext, d as PaginationResult, dt as RouteDefinition, et as OpenApiSchemas, f as IntrospectionData, ft as RouteMcpConfig, g as ArcInternalMetadata, gt as ControllerLike, h as ResourceMetadata, ht as ControllerHandler, i as ValidationResult, it as RateLimitConfig, j as ArcDecorator, k as IntrospectionPluginOptions, l as TypedRepository, lt as ResourceHooks, m as RegistryStats, mt as RouteSchemaOptions, n as ConfigError, nt as PresetHook, o as InferDocType, ot as ResourceCacheConfig, p as RegistryEntry, pt as RouteMethod, q as CrudController, qt as ApiResponse, r as ValidateOptions, rt as PresetResult, s as InferResourceDoc, st as ResourceConfig, t as RouteHandlerMethod, tt as PresetFunction, u as TypedResourceConfig, ut as ResourcePermissions, v as LookupOption, vt as IController, w as ServiceContext, x as PopulateOption, xt as RouteHandler, y as OwnershipCheck, yt as IControllerResponse } from "../index-C1JSBan-.mjs";
|
|
2
2
|
import { i as RequestScope } from "../types-CfapW2t8.mjs";
|
|
3
3
|
import { c as PermissionCheck, d as UserBase, l as PermissionContext, u as PermissionResult } from "../fields-DqZLp5d6.mjs";
|
|
4
4
|
import { n as ElevationOptions, t as ElevationEvent } from "../elevation-DvwtjDwu.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as CacheStore } from "./interface-BVkZsvEe.mjs";
|
|
2
|
-
import { Ht as Authenticator } from "./index-
|
|
2
|
+
import { Ht as Authenticator } from "./index-C1JSBan-.mjs";
|
|
3
3
|
import { n as ElevationOptions } from "./elevation-DvwtjDwu.mjs";
|
|
4
4
|
import { a as EventTransport } from "./EventTransport-B1GFiYUy.mjs";
|
|
5
5
|
import { t as ExternalOpenApiPaths } from "./externalPaths-ClykOQqa.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V as ResourceDefinition } from "./index-
|
|
1
|
+
import { V as ResourceDefinition } from "./index-C1JSBan-.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
//#region src/integrations/mcp/types.d.ts
|
|
@@ -93,6 +93,17 @@ interface ToolDefinition<TSession = McpAuthResult> {
|
|
|
93
93
|
outputSchema?: Record<string, z.ZodTypeAny>;
|
|
94
94
|
annotations?: ToolAnnotations;
|
|
95
95
|
handler: (input: Record<string, unknown>, ctx: ToolContext<TSession>) => Promise<CallToolResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Origin label used by collision diagnostics — names BOTH the resource
|
|
98
|
+
* and the surface that produced this tool (`'crud:post:list'`,
|
|
99
|
+
* `'action:post:approve'`, `'route:post:GET /export'`,
|
|
100
|
+
* `'preset:softDelete:post:restore'`). When two tools share a name,
|
|
101
|
+
* `createMcpServer()` prints both labels so the host can see which
|
|
102
|
+
* surfaces collided without spelunking through the MCP SDK's stack
|
|
103
|
+
* trace. Optional — hand-written tools without a `source` simply
|
|
104
|
+
* appear as `'(unknown)'` in collision messages.
|
|
105
|
+
*/
|
|
106
|
+
source?: string;
|
|
96
107
|
}
|
|
97
108
|
/** Output of definePrompt() — plain data, not yet registered */
|
|
98
109
|
interface PromptDefinition {
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Kt as AnyRecord, S as QueryParserInterface, Zt as UserLike, b as ParsedQuery, et as OpenApiSchemas, st as ResourceConfig } from "../index-
|
|
1
|
+
import { Kt as AnyRecord, S as QueryParserInterface, Zt as UserLike, b as ParsedQuery, et as OpenApiSchemas, st as ResourceConfig } from "../index-C1JSBan-.mjs";
|
|
2
2
|
import { n as ErrorMapper } from "../errorHandler-DuqTqeqn.mjs";
|
|
3
3
|
import { a as NotFoundError, c as RateLimitError, d as ValidationError, f as createDomainError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorOptions, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-4ZloYFvm.mjs";
|
|
4
4
|
import { errorContractSchema, errorDetailSchema } from "@classytic/repo-core/errors";
|
|
@@ -1255,6 +1255,83 @@ declare function createStateMachine(name: string, transitions?: TransitionConfig
|
|
|
1255
1255
|
trackHistory?: boolean;
|
|
1256
1256
|
}): StateMachine;
|
|
1257
1257
|
//#endregion
|
|
1258
|
+
//#region src/utils/streaming.d.ts
|
|
1259
|
+
/**
|
|
1260
|
+
* Canonical SSE headers for AI-SDK-style UI message streams.
|
|
1261
|
+
*
|
|
1262
|
+
* `x-vercel-ai-ui-message-stream: v1` is the marker the Vercel AI SDK
|
|
1263
|
+
* client uses to detect the v1 UI message stream protocol (vs the older
|
|
1264
|
+
* data stream protocol). Surfaced as a constant so hosts don't memorise it.
|
|
1265
|
+
*/
|
|
1266
|
+
declare const UI_MESSAGE_STREAM_HEADERS: {
|
|
1267
|
+
readonly "content-type": "text/event-stream";
|
|
1268
|
+
readonly "cache-control": "no-cache";
|
|
1269
|
+
readonly connection: "keep-alive";
|
|
1270
|
+
readonly "x-accel-buffering": "no";
|
|
1271
|
+
readonly "x-vercel-ai-ui-message-stream": "v1";
|
|
1272
|
+
};
|
|
1273
|
+
interface PipeUIMessageStreamOptions {
|
|
1274
|
+
/**
|
|
1275
|
+
* Headers to send before the first chunk. Defaults to
|
|
1276
|
+
* {@link UI_MESSAGE_STREAM_HEADERS}. Pass a fully-replaced bag when
|
|
1277
|
+
* you need a different protocol marker; merge manually otherwise —
|
|
1278
|
+
* the helper does not deep-merge to keep behaviour predictable.
|
|
1279
|
+
*/
|
|
1280
|
+
headers?: Record<string, string>;
|
|
1281
|
+
/**
|
|
1282
|
+
* Abort signal — when fired, the helper cancels the source stream and
|
|
1283
|
+
* ends the reply early. `request.raw` `close` is wired automatically
|
|
1284
|
+
* when {@link pipeUIMessageStreamToReply} sees `reply.request`, so most
|
|
1285
|
+
* hosts never pass this directly.
|
|
1286
|
+
*/
|
|
1287
|
+
signal?: AbortSignal;
|
|
1288
|
+
/**
|
|
1289
|
+
* Custom chunk serialiser. Default: `JSON.stringify(chunk)`. Override
|
|
1290
|
+
* when chunks are already strings (rare — most AI SDK transforms emit
|
|
1291
|
+
* objects) or when you need pretty-print / type-prefix shenanigans.
|
|
1292
|
+
*/
|
|
1293
|
+
serialize?: (chunk: unknown) => string;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Pipe a Web `ReadableStream` of JSON-serialisable chunks (typically
|
|
1297
|
+
* `UIMessageChunk` from the Vercel AI SDK) into a Fastify reply as an
|
|
1298
|
+
* SSE event stream.
|
|
1299
|
+
*
|
|
1300
|
+
* - Sets {@link UI_MESSAGE_STREAM_HEADERS} (or the caller's override)
|
|
1301
|
+
* before the first chunk.
|
|
1302
|
+
* - Writes each chunk as `data: ${JSON.stringify(chunk)}\n\n`.
|
|
1303
|
+
* - Cancels the source and closes the reply when the client disconnects.
|
|
1304
|
+
* - Resolves once the stream is fully written.
|
|
1305
|
+
*
|
|
1306
|
+
* @example
|
|
1307
|
+
* ```ts
|
|
1308
|
+
* import { pipeUIMessageStreamToReply } from '@classytic/arc/utils';
|
|
1309
|
+
* import { streamText, convertToUIMessageStream } from 'ai';
|
|
1310
|
+
*
|
|
1311
|
+
* defineResource({
|
|
1312
|
+
* name: 'chat',
|
|
1313
|
+
* routes: [{
|
|
1314
|
+
* method: 'POST', path: '/', streamResponse: true,
|
|
1315
|
+
* handler: async (req, reply) => {
|
|
1316
|
+
* const result = streamText({ model, messages: req.body.messages });
|
|
1317
|
+
* await pipeUIMessageStreamToReply(reply, result.toUIMessageStream());
|
|
1318
|
+
* },
|
|
1319
|
+
* }],
|
|
1320
|
+
* });
|
|
1321
|
+
* ```
|
|
1322
|
+
*/
|
|
1323
|
+
declare function pipeUIMessageStreamToReply(reply: FastifyReply, stream: ReadableStream<unknown>, options?: PipeUIMessageStreamOptions): Promise<void>;
|
|
1324
|
+
/**
|
|
1325
|
+
* Heuristic — does this value look like a Web `ReadableStream`?
|
|
1326
|
+
*
|
|
1327
|
+
* Used by `createCrudRouter` to auto-pipe `streamResponse: true` handlers
|
|
1328
|
+
* that return a UI message stream instead of writing to `reply.raw`
|
|
1329
|
+
* directly. Cross-realm safe (no `instanceof`) — checks the duck shape
|
|
1330
|
+
* the Streams spec mandates: `getReader`, `cancel`, and the `locked`
|
|
1331
|
+
* getter.
|
|
1332
|
+
*/
|
|
1333
|
+
declare function isReadableStream(value: unknown): value is ReadableStream<unknown>;
|
|
1334
|
+
//#endregion
|
|
1258
1335
|
//#region src/utils/typeGuards.d.ts
|
|
1259
1336
|
interface EventsDecorator {
|
|
1260
1337
|
publish: <T>(type: string, payload: T, meta?: Record<string, unknown>) => Promise<void>;
|
|
@@ -1284,4 +1361,4 @@ declare function hasEvents(instance: FastifyInstance): instance is FastifyInstan
|
|
|
1284
1361
|
*/
|
|
1285
1362
|
declare function getUserId(user: UserLike | null | undefined): string | undefined;
|
|
1286
1363
|
//#endregion
|
|
1287
|
-
export { ArcError, ArcQueryParser, type ArcQueryParserOptions, CircuitBreaker, CircuitBreakerError, type CircuitBreakerOptions, CircuitBreakerRegistry, type CircuitBreakerStats, CircuitState, type CompensationDefinition, type CompensationError, type CompensationHooks, type CompensationResult, type CompensationStep, type ConfigError, ConflictError, type ErrorOptions, type EventsDecorator, ForbiddenError, type Guard, type GuardConfig, type JsonSchema, type JsonSchemaTarget, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, type StateMachine, type TransitionConfig, UnauthorizedError, type ValidateOptions, ValidationError, type ValidationResult, aggregateListResponse, assertValidConfig, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, formatValidationErrors, getDefaultCrudSchemas, getListQueryParams, getUserId, handleRaw, hasEvents, isArcError, isJsonSchema, isZodSchema, keysetListResponse, listResponse, offsetListResponse, paginationSchema, queryParams, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, validateResourceConfig, withCompensation };
|
|
1364
|
+
export { ArcError, ArcQueryParser, type ArcQueryParserOptions, CircuitBreaker, CircuitBreakerError, type CircuitBreakerOptions, CircuitBreakerRegistry, type CircuitBreakerStats, CircuitState, type CompensationDefinition, type CompensationError, type CompensationHooks, type CompensationResult, type CompensationStep, type ConfigError, ConflictError, type ErrorOptions, type EventsDecorator, ForbiddenError, type Guard, type GuardConfig, type JsonSchema, type JsonSchemaTarget, NotFoundError, OrgAccessDeniedError, OrgRequiredError, type PipeUIMessageStreamOptions, RateLimitError, ServiceUnavailableError, type StateMachine, type TransitionConfig, UI_MESSAGE_STREAM_HEADERS, UnauthorizedError, type ValidateOptions, ValidationError, type ValidationResult, aggregateListResponse, assertValidConfig, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, formatValidationErrors, getDefaultCrudSchemas, getListQueryParams, getUserId, handleRaw, hasEvents, isArcError, isJsonSchema, isReadableStream, isZodSchema, keysetListResponse, listResponse, offsetListResponse, paginationSchema, pipeUIMessageStreamToReply, queryParams, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, validateResourceConfig, withCompensation };
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as OrgAccessDeniedError, c as ServiceUnavailableError, d as createDomainError, 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-j4aJm1Wg.mjs";
|
|
2
|
-
import { A as
|
|
2
|
+
import { A as CircuitState, C as defineGuard, D as CircuitBreaker, E as withCompensation, F as validateResourceConfig, I as simpleEqualityMatcher, M as createCircuitBreakerRegistry, N as assertValidConfig, O as CircuitBreakerError, P as formatValidationErrors, S as handleRaw, T as defineCompensation, _ as paginationSchema, a as createStateMachine, b as ArcQueryParser, c as bareListResponse, d as errorDetailSchema, f as getDefaultCrudSchemas, g as offsetListResponse, h as listResponse, i as pipeUIMessageStreamToReply, j as createCircuitBreaker, k as CircuitBreakerRegistry, l as deleteResponse, m as keysetListResponse, n as UI_MESSAGE_STREAM_HEADERS, o as scheduleBackground, p as getListQueryParams, r as isReadableStream, s as aggregateListResponse, t as getUserId, u as errorContractSchema, v as queryParams, w as defineErrorMapper, x as createQueryParser, y as responses } from "../utils-BMQK8mRl.mjs";
|
|
3
3
|
import { a as toJsonSchema, i as isZodSchema, n as convertRouteSchema, r as isJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-De34B1ZG.mjs";
|
|
4
4
|
import { t as hasEvents } from "../typeGuards-BzkXkvVv.mjs";
|
|
5
|
-
export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UnauthorizedError, ValidationError, aggregateListResponse, assertValidConfig, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, formatValidationErrors, getDefaultCrudSchemas, getListQueryParams, getUserId, handleRaw, hasEvents, isArcError, isJsonSchema, isZodSchema, keysetListResponse, listResponse, offsetListResponse, paginationSchema, queryParams, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, validateResourceConfig, withCompensation };
|
|
5
|
+
export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UI_MESSAGE_STREAM_HEADERS, UnauthorizedError, ValidationError, aggregateListResponse, assertValidConfig, bareListResponse, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, defineCompensation, defineErrorMapper, defineGuard, deleteResponse, errorContractSchema, errorDetailSchema, formatValidationErrors, getDefaultCrudSchemas, getListQueryParams, getUserId, handleRaw, hasEvents, isArcError, isJsonSchema, isReadableStream, isZodSchema, keysetListResponse, listResponse, offsetListResponse, paginationSchema, pipeUIMessageStreamToReply, queryParams, responses, scheduleBackground, simpleEqualityMatcher, toJsonSchema, validateResourceConfig, withCompensation };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { m as RESERVED_QUERY_PARAMS, t as CRUD_OPERATIONS } from "./constants-TrJVIJl0.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { t as ArcError } from "./errors-j4aJm1Wg.mjs";
|
|
4
|
-
import { r as getAvailablePresets } from "./presets-
|
|
4
|
+
import { r as getAvailablePresets } from "./presets-BYbPobbF.mjs";
|
|
5
5
|
import { errorContractSchema as errorContractSchema$1, errorDetailSchema as errorDetailSchema$1 } from "@classytic/repo-core/errors";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
6
7
|
//#region src/utils/simpleEqualityMatcher.ts
|
|
7
8
|
/**
|
|
8
9
|
* `simpleEqualityMatcher` — a minimal, dialect-agnostic flat-key equality
|
|
@@ -1670,6 +1671,114 @@ function createStateMachine(name, transitions = {}, options = {}) {
|
|
|
1670
1671
|
};
|
|
1671
1672
|
}
|
|
1672
1673
|
//#endregion
|
|
1674
|
+
//#region src/utils/streaming.ts
|
|
1675
|
+
/**
|
|
1676
|
+
* Streaming helpers — adapters between Web `ReadableStream` producers
|
|
1677
|
+
* (Vercel AI SDK, OpenAI SDK, `fetch().body`, hand-built `TransformStream`s)
|
|
1678
|
+
* and Fastify's raw Node-stream reply.
|
|
1679
|
+
*
|
|
1680
|
+
* Why this exists: returning a `ReadableStream<UIMessageChunk>` from a
|
|
1681
|
+
* `streamResponse: true` arc route used to crash with
|
|
1682
|
+
* `chunk must be a string or Buffer` because Fastify cannot serialise
|
|
1683
|
+
* `UIMessageChunk` JSON objects directly. Every arc-ai host (sniffer,
|
|
1684
|
+
* spawn, downstream) reimplemented the same `JsonToSseTransformStream` +
|
|
1685
|
+
* SSE header boilerplate. `pipeUIMessageStreamToReply()` is that
|
|
1686
|
+
* boilerplate, owned once.
|
|
1687
|
+
*
|
|
1688
|
+
* arc stays peer-coupled to nothing: the helper accepts any
|
|
1689
|
+
* `ReadableStream<unknown>` and JSON-stringifies each chunk. It works
|
|
1690
|
+
* with `UIMessageChunk` from the AI SDK without importing it.
|
|
1691
|
+
*/
|
|
1692
|
+
/**
|
|
1693
|
+
* Canonical SSE headers for AI-SDK-style UI message streams.
|
|
1694
|
+
*
|
|
1695
|
+
* `x-vercel-ai-ui-message-stream: v1` is the marker the Vercel AI SDK
|
|
1696
|
+
* client uses to detect the v1 UI message stream protocol (vs the older
|
|
1697
|
+
* data stream protocol). Surfaced as a constant so hosts don't memorise it.
|
|
1698
|
+
*/
|
|
1699
|
+
const UI_MESSAGE_STREAM_HEADERS = {
|
|
1700
|
+
"content-type": "text/event-stream",
|
|
1701
|
+
"cache-control": "no-cache",
|
|
1702
|
+
connection: "keep-alive",
|
|
1703
|
+
"x-accel-buffering": "no",
|
|
1704
|
+
"x-vercel-ai-ui-message-stream": "v1"
|
|
1705
|
+
};
|
|
1706
|
+
/**
|
|
1707
|
+
* Pipe a Web `ReadableStream` of JSON-serialisable chunks (typically
|
|
1708
|
+
* `UIMessageChunk` from the Vercel AI SDK) into a Fastify reply as an
|
|
1709
|
+
* SSE event stream.
|
|
1710
|
+
*
|
|
1711
|
+
* - Sets {@link UI_MESSAGE_STREAM_HEADERS} (or the caller's override)
|
|
1712
|
+
* before the first chunk.
|
|
1713
|
+
* - Writes each chunk as `data: ${JSON.stringify(chunk)}\n\n`.
|
|
1714
|
+
* - Cancels the source and closes the reply when the client disconnects.
|
|
1715
|
+
* - Resolves once the stream is fully written.
|
|
1716
|
+
*
|
|
1717
|
+
* @example
|
|
1718
|
+
* ```ts
|
|
1719
|
+
* import { pipeUIMessageStreamToReply } from '@classytic/arc/utils';
|
|
1720
|
+
* import { streamText, convertToUIMessageStream } from 'ai';
|
|
1721
|
+
*
|
|
1722
|
+
* defineResource({
|
|
1723
|
+
* name: 'chat',
|
|
1724
|
+
* routes: [{
|
|
1725
|
+
* method: 'POST', path: '/', streamResponse: true,
|
|
1726
|
+
* handler: async (req, reply) => {
|
|
1727
|
+
* const result = streamText({ model, messages: req.body.messages });
|
|
1728
|
+
* await pipeUIMessageStreamToReply(reply, result.toUIMessageStream());
|
|
1729
|
+
* },
|
|
1730
|
+
* }],
|
|
1731
|
+
* });
|
|
1732
|
+
* ```
|
|
1733
|
+
*/
|
|
1734
|
+
async function pipeUIMessageStreamToReply(reply, stream, options = {}) {
|
|
1735
|
+
const serialize = options.serialize ?? defaultSerialize;
|
|
1736
|
+
const headers = options.headers ?? UI_MESSAGE_STREAM_HEADERS;
|
|
1737
|
+
for (const [key, value] of Object.entries(headers)) reply.header(key, value);
|
|
1738
|
+
const reader = stream.getReader();
|
|
1739
|
+
let cancelled = false;
|
|
1740
|
+
const onAbort = () => {
|
|
1741
|
+
cancelled = true;
|
|
1742
|
+
reader.cancel().catch(() => {});
|
|
1743
|
+
};
|
|
1744
|
+
reply.request.raw.once("close", onAbort);
|
|
1745
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1746
|
+
async function* sseFrames() {
|
|
1747
|
+
try {
|
|
1748
|
+
while (true) {
|
|
1749
|
+
const { value, done } = await reader.read();
|
|
1750
|
+
if (done) break;
|
|
1751
|
+
yield `data: ${serialize(value)}\n\n`;
|
|
1752
|
+
}
|
|
1753
|
+
} catch (err) {
|
|
1754
|
+
if (!cancelled) yield `event: error\ndata: ${JSON.stringify({ message: String(err) })}\n\n`;
|
|
1755
|
+
} finally {
|
|
1756
|
+
reply.request.raw.removeListener("close", onAbort);
|
|
1757
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
1758
|
+
reader.releaseLock?.();
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
const nodeStream = Readable.from(sseFrames());
|
|
1762
|
+
await reply.send(nodeStream);
|
|
1763
|
+
}
|
|
1764
|
+
function defaultSerialize(chunk) {
|
|
1765
|
+
return typeof chunk === "string" ? chunk : JSON.stringify(chunk);
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Heuristic — does this value look like a Web `ReadableStream`?
|
|
1769
|
+
*
|
|
1770
|
+
* Used by `createCrudRouter` to auto-pipe `streamResponse: true` handlers
|
|
1771
|
+
* that return a UI message stream instead of writing to `reply.raw`
|
|
1772
|
+
* directly. Cross-realm safe (no `instanceof`) — checks the duck shape
|
|
1773
|
+
* the Streams spec mandates: `getReader`, `cancel`, and the `locked`
|
|
1774
|
+
* getter.
|
|
1775
|
+
*/
|
|
1776
|
+
function isReadableStream(value) {
|
|
1777
|
+
if (value === null || typeof value !== "object") return false;
|
|
1778
|
+
const obj = value;
|
|
1779
|
+
return typeof obj.getReader === "function" && typeof obj.cancel === "function" && typeof obj.locked === "boolean";
|
|
1780
|
+
}
|
|
1781
|
+
//#endregion
|
|
1673
1782
|
//#region src/utils/userHelpers.ts
|
|
1674
1783
|
/**
|
|
1675
1784
|
* Extract a user ID from a user object. Accepts `id` or `_id` — returns
|
|
@@ -1688,4 +1797,4 @@ function getUserId(user) {
|
|
|
1688
1797
|
return id ? String(id) : void 0;
|
|
1689
1798
|
}
|
|
1690
1799
|
//#endregion
|
|
1691
|
-
export {
|
|
1800
|
+
export { CircuitState as A, defineGuard as C, CircuitBreaker as D, withCompensation as E, validateResourceConfig as F, simpleEqualityMatcher as I, createCircuitBreakerRegistry as M, assertValidConfig as N, CircuitBreakerError as O, formatValidationErrors as P, handleRaw as S, defineCompensation as T, paginationSchema as _, createStateMachine as a, ArcQueryParser as b, bareListResponse as c, errorDetailSchema$1 as d, getDefaultCrudSchemas as f, offsetListResponse as g, listResponse as h, pipeUIMessageStreamToReply as i, createCircuitBreaker as j, CircuitBreakerRegistry as k, deleteResponse as l, keysetListResponse as m, UI_MESSAGE_STREAM_HEADERS as n, scheduleBackground as o, getListQueryParams as p, isReadableStream as r, aggregateListResponse as s, getUserId as t, errorContractSchema$1 as u, queryParams as v, defineErrorMapper as w, createQueryParser as x, responses as y };
|
package/package.json
CHANGED
package/skills/arc/SKILL.md
CHANGED
|
@@ -134,6 +134,23 @@ export default defineResource({
|
|
|
134
134
|
|
|
135
135
|
**Auto-generated:** `GET /`, `GET /:id`, `POST /`, `PATCH /:id`, `DELETE /:id`. Presets add their own routes (see table below).
|
|
136
136
|
|
|
137
|
+
**Resource shorthands:**
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// "fetch-all" reference data — read-only crud (list+get), defaultLimit/maxLimit=1000,
|
|
141
|
+
// cache { staleTime: 300, gcTime: 600 }. Explicit narrow flags still win.
|
|
142
|
+
defineResource({ name: 'currency', adapter, referenceData: true });
|
|
143
|
+
|
|
144
|
+
// Service resource — no adapter, no auto-CRUD, no validation, no registry entry.
|
|
145
|
+
// Equivalent to disableDefaultRoutes + skipValidation + skipRegistry.
|
|
146
|
+
defineResource({ name: 'health', customRoutesOnly: true, routes: [...] });
|
|
147
|
+
|
|
148
|
+
// Resource-level pagination knobs (no custom queryParser needed):
|
|
149
|
+
defineResource({ name: 'pipeline', adapter, defaultLimit: 200, maxLimit: 500 });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
400s on `?limit=` violations include the cap: `message: "Query parameter 'limit' must be <= 500 (got 800) (cap is 500)"` + `meta: { field: 'limit', cap: 500 }`.
|
|
153
|
+
|
|
137
154
|
## fieldRules
|
|
138
155
|
|
|
139
156
|
| Flag | Effect |
|
|
@@ -533,12 +550,37 @@ await app.register(mcpPlugin, {
|
|
|
533
550
|
|
|
534
551
|
**Per-resource opt-out:** `defineResource({ mcp: false })` — exclude a resource from MCP tool generation entirely. Useful for internal-admin or write-heavy resources where you don't want LLMs poking.
|
|
535
552
|
|
|
536
|
-
**Auth modes** — `false` | `getAuth()` (Better Auth OAuth 2.1) | custom function returning `{ userId, organizationId, roles }` (human) or `{ clientId, organizationId, scopes }` (service). `PermissionResult.filters` flow into MCP tools exactly like REST.
|
|
553
|
+
**Auth modes** — `false` | `getAuth()` (Better Auth OAuth 2.1) | `createMcpAuthFromBetterAuthApiKey(getAuth(), { orgFromMetadata: true })` (BA API-key plugin — handles `referenceId`/`userId` fallback + `metadata.organizationId` extraction) | custom function returning `{ userId, organizationId, roles }` (human) or `{ clientId, organizationId, scopes }` (service). `PermissionResult.filters` flow into MCP tools exactly like REST.
|
|
554
|
+
|
|
555
|
+
**Tool-name collisions** — preset vs user (e.g. `softDelete` route vs `actions.restore`) auto-namespace to `softdelete_restore_<resource>`; every other collision shape throws `ArcError('arc.mcp.tool_name_collision')` naming both sources.
|
|
537
556
|
|
|
538
557
|
**Custom tools** — co-locate with resources (`order.mcp.ts`), wire via `extraTools`. **AI SDK bridge** — expose AI SDK `tool()` defs via `buildMcpToolsFromBridges([...])`.
|
|
539
558
|
|
|
540
559
|
Connect Claude CLI: `claude mcp add --transport http my-api http://localhost:3000/mcp`. Full recipes → [references/mcp.md](references/mcp.md).
|
|
541
560
|
|
|
561
|
+
### AI SDK streaming → Fastify
|
|
562
|
+
|
|
563
|
+
`streamResponse: true` routes that return a Web `ReadableStream` (Vercel AI SDK's `result.toUIMessageStream()`, `fetch().body`) auto-pipe through `pipeUIMessageStreamToReply()` — no `JsonToSseTransformStream` boilerplate.
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
import { streamText } from 'ai';
|
|
567
|
+
|
|
568
|
+
defineResource({
|
|
569
|
+
name: 'chat',
|
|
570
|
+
customRoutesOnly: true,
|
|
571
|
+
routes: [{
|
|
572
|
+
method: 'POST', path: '/', streamResponse: true, raw: true,
|
|
573
|
+
permissions: requireAuth(),
|
|
574
|
+
handler: async (req) => {
|
|
575
|
+
const result = streamText({ model, messages: req.body.messages });
|
|
576
|
+
return result.toUIMessageStream(); // arc pipes it for you
|
|
577
|
+
},
|
|
578
|
+
}],
|
|
579
|
+
});
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
For manual control: `import { pipeUIMessageStreamToReply, UI_MESSAGE_STREAM_HEADERS } from '@classytic/arc/utils'`. Client disconnect cancels the source stream (no zombie LLM spend).
|
|
583
|
+
|
|
542
584
|
## Tenant cleanup (GDPR / SOX / HIPAA)
|
|
543
585
|
|
|
544
586
|
Per-resource strategy + one auth-lifecycle wire-up:
|
|
@@ -167,6 +167,8 @@ import {
|
|
|
167
167
|
import {
|
|
168
168
|
createStateMachine, CircuitBreaker, withCompensation, defineGuard,
|
|
169
169
|
retry, queryParser,
|
|
170
|
+
// AI SDK / OpenAI streams → Fastify reply (no JsonToSseTransformStream boilerplate)
|
|
171
|
+
pipeUIMessageStreamToReply, UI_MESSAGE_STREAM_HEADERS, isReadableStream,
|
|
170
172
|
} from '@classytic/arc/utils';
|
|
171
173
|
import { defineMigration, MigrationRunner } from '@classytic/arc/migrations';
|
|
172
174
|
import { Type, ArcListResponse } from '@classytic/arc/schemas';
|
|
@@ -168,7 +168,27 @@ Arc → auth.api.getMcpSession({ headers }) → { userId, organizationId }
|
|
|
168
168
|
Arc → BaseController scopes by org automatically
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
### 3.
|
|
171
|
+
### 3. Better Auth API Key (machine clients, no OAuth dance)
|
|
172
|
+
|
|
173
|
+
Hosts using Better Auth's `apiKey()` plugin should NOT hand-wire `verifyApiKey` — the helper handles the `referenceId` vs `userId` fallback, `metadata.organizationId` extraction, disabled / expired guards, and fail-closed exception handling.
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { createMcpAuthFromBetterAuthApiKey } from '@classytic/arc/mcp';
|
|
177
|
+
import { getAuth } from './auth.config.js';
|
|
178
|
+
|
|
179
|
+
await app.register(mcpPlugin, {
|
|
180
|
+
resources,
|
|
181
|
+
auth: createMcpAuthFromBetterAuthApiKey(getAuth(), {
|
|
182
|
+
orgFromMetadata: true, // reads key.metadata.organizationId (default)
|
|
183
|
+
// metadataOrgField: 'tenantId', // if you stored it under a different key
|
|
184
|
+
// permissionScope: 'mcp', // permissions[scope] → result.scopes (default 'mcp')
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Header precedence: `Authorization: Bearer <key>` first, then `x-api-key`. Keys with neither `userId` nor `referenceId` are treated as service principals (`clientId = key.id`) — `RequestScope.kind` becomes `"service"`.
|
|
190
|
+
|
|
191
|
+
### 4. Custom Auth Function (API key, gateway, static org)
|
|
172
192
|
|
|
173
193
|
Pass an `McpAuthResolver` — a function that receives headers and returns identity:
|
|
174
194
|
|
|
@@ -242,6 +262,15 @@ auth resolver returns { userId: 'alice', organizationId: 'org-a' }
|
|
|
242
262
|
|
|
243
263
|
Works with any `tenantField` (`organizationId`, `workspaceId`, `teamId`). Resources with `tenantField: false` are visible to all authenticated users.
|
|
244
264
|
|
|
265
|
+
## Tool-Name Collisions
|
|
266
|
+
|
|
267
|
+
`createMcpServer()` resolves duplicate tool names BEFORE the MCP SDK sees them — the SDK's native `Tool already registered` error gave zero source attribution.
|
|
268
|
+
|
|
269
|
+
- **Preset vs user** → auto-namespace. `softDelete`'s `restore_post` becomes `softdelete_restore_post` when the user defines `actions.restore`; the user's tool keeps the canonical name.
|
|
270
|
+
- **User vs user / preset vs preset / 3-way+** → typed throw: `ArcError('arc.mcp.tool_name_collision')` naming both `source` strings (`crud:post:list`, `action:post:approve`, `route:post:GET /export`, `preset:softDelete:post:restore`).
|
|
271
|
+
|
|
272
|
+
`resolveToolCollisions(tools)` is exported from `@classytic/arc/mcp` for test harnesses that want to exercise collision behaviour without booting a server.
|
|
273
|
+
|
|
245
274
|
## Level 2 — Custom Tools
|
|
246
275
|
|
|
247
276
|
```typescript
|
|
File without changes
|