@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.
Files changed (72) hide show
  1. package/README.md +22 -2
  2. package/dist/{BaseController-Vk8I7C0x.mjs → BaseController-CgHomZ4d.mjs} +5 -47
  3. package/dist/audit/index.mjs +2 -1
  4. package/dist/auth/index.d.mts +1 -1
  5. package/dist/auth/index.mjs +2 -2
  6. package/dist/auth/redis-session.mjs +28 -2
  7. package/dist/cli/commands/describe.d.mts +1 -1
  8. package/dist/cli/commands/docs.mjs +1 -1
  9. package/dist/core/index.d.mts +2 -2
  10. package/dist/core/index.mjs +3 -3
  11. package/dist/{core-wKeGXn38.mjs → core-D8tpQ4ZL.mjs} +174 -31
  12. package/dist/{createActionRouter-eiOFtnC3.mjs → createActionRouter-BHUMb1-y.mjs} +5 -2
  13. package/dist/{createAggregationRouter-C4unhBnV.mjs → createAggregationRouter-BYVvhfc5.mjs} +1 -1
  14. package/dist/{createApp-DdPBtmzp.mjs → createApp-CzCEEsEm.mjs} +2 -2
  15. package/dist/docs/index.d.mts +1 -1
  16. package/dist/docs/index.mjs +2 -2
  17. package/dist/{elevation-Dci0AYLT.mjs → elevation-6mfTLBC-.mjs} +1 -1
  18. package/dist/{errorHandler-DNPzqaTT.mjs → errorHandler-wQejMBT3.mjs} +45 -4
  19. package/dist/events/transports/redis-stream-entry.mjs +34 -1
  20. package/dist/events/transports/redis.mjs +17 -1
  21. package/dist/factory/index.d.mts +1 -1
  22. package/dist/factory/index.mjs +1 -1
  23. package/dist/{fastify-augmentation-CJm0vh-l.d.mts → fastify-augmentation-7LIR0nDv.d.mts} +1 -1
  24. package/dist/hooks/index.d.mts +1 -1
  25. package/dist/idempotency/index.mjs +20 -4
  26. package/dist/{index-DBDgy6Hw.d.mts → index-C1JSBan-.d.mts} +177 -1
  27. package/dist/{index-CC_pgZjY.d.mts → index-CruZir1o.d.mts} +1 -1
  28. package/dist/index.d.mts +2 -2
  29. package/dist/index.mjs +5 -5
  30. package/dist/integrations/index.d.mts +1 -1
  31. package/dist/integrations/jobs.d.mts +36 -1
  32. package/dist/integrations/jobs.mjs +82 -9
  33. package/dist/integrations/mcp/index.d.mts +65 -3
  34. package/dist/integrations/mcp/index.mjs +93 -2
  35. package/dist/integrations/mcp/testing.d.mts +1 -1
  36. package/dist/integrations/mcp/testing.mjs +1 -1
  37. package/dist/integrations/websocket.mjs +2 -1
  38. package/dist/{mcpPlugin-Dbgz9_Rj.mjs → mcpPlugin-C6FViZXw.mjs} +111 -8
  39. package/dist/middleware/index.d.mts +1 -1
  40. package/dist/middleware/index.mjs +1 -1
  41. package/dist/{multipartBody-BOvVSVCD.mjs → multipartBody-VjVWmZl8.mjs} +2 -1
  42. package/dist/{openapi-BRnd3pT7.mjs → openapi-DKDs9NfD.mjs} +2 -2
  43. package/dist/permissions/index.mjs +2 -2
  44. package/dist/{permissions-CTxMrreC.mjs → permissions-B1Qy55fg.mjs} +169 -169
  45. package/dist/pipeline/index.d.mts +1 -1
  46. package/dist/plugins/index.d.mts +3 -3
  47. package/dist/plugins/index.mjs +1 -1
  48. package/dist/plugins/tracing-entry.mjs +1 -1
  49. package/dist/presets/filesUpload.d.mts +1 -1
  50. package/dist/presets/filesUpload.mjs +2 -2
  51. package/dist/presets/index.d.mts +1 -1
  52. package/dist/presets/index.mjs +1 -1
  53. package/dist/presets/multiTenant.d.mts +1 -1
  54. package/dist/presets/search.d.mts +1 -1
  55. package/dist/presets/search.mjs +1 -1
  56. package/dist/{presets-C9BE6WaZ.mjs → presets-BYbPobbF.mjs} +5 -3
  57. package/dist/registry/index.d.mts +1 -1
  58. package/dist/{routerShared-ClV4cCaa.mjs → routerShared-nCyJGOKX.mjs} +127 -4
  59. package/dist/scope/index.mjs +2 -2
  60. package/dist/testing/index.d.mts +2 -2
  61. package/dist/testing/index.mjs +1 -1
  62. package/dist/types/index.d.mts +1 -1
  63. package/dist/{types-Dw--SyTq.d.mts → types-BRmS8TNa.d.mts} +1 -1
  64. package/dist/{types-BD0BCdf6.d.mts → types-CoMjNmkF.d.mts} +12 -1
  65. package/dist/utils/index.d.mts +79 -2
  66. package/dist/utils/index.mjs +2 -2
  67. package/dist/{utils-DPXZnZUx.mjs → utils-BMQK8mRl.mjs} +111 -2
  68. package/package.json +1 -1
  69. package/skills/arc/SKILL.md +43 -1
  70. package/skills/arc/references/api-reference.md +2 -0
  71. package/skills/arc/references/mcp.md +30 -1
  72. /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
@@ -1036,7 +1036,7 @@ function pickDefaultAuth(authMode, callerAuth) {
1036
1036
  };
1037
1037
  }
1038
1038
  async function createTestApp(options = {}) {
1039
- const { createApp } = await import("../createApp-DdPBtmzp.mjs").then((n) => n.r);
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;
@@ -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-DBDgy6Hw.mjs";
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-DBDgy6Hw.mjs";
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-DBDgy6Hw.mjs";
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 {
@@ -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-DBDgy6Hw.mjs";
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 };
@@ -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 assertValidConfig, C as withCompensation, D as CircuitState, E as CircuitBreakerRegistry, M as validateResourceConfig, N as simpleEqualityMatcher, O as createCircuitBreaker, S as defineCompensation, T as CircuitBreakerError, _ as ArcQueryParser, a as bareListResponse, b as defineGuard, c as errorDetailSchema, d as keysetListResponse, f as listResponse, g as responses, h as queryParams, i as aggregateListResponse, j as formatValidationErrors, k as createCircuitBreakerRegistry, l as getDefaultCrudSchemas, m as paginationSchema, n as createStateMachine, o as deleteResponse, p as offsetListResponse, r as scheduleBackground, s as errorContractSchema, t as getUserId, u as getListQueryParams, v as createQueryParser, w as CircuitBreaker, x as defineErrorMapper, y as handleRaw } from "../utils-DPXZnZUx.mjs";
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-C9BE6WaZ.mjs";
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 { assertValidConfig as A, withCompensation as C, CircuitState as D, CircuitBreakerRegistry as E, validateResourceConfig as M, simpleEqualityMatcher as N, createCircuitBreaker as O, defineCompensation as S, CircuitBreakerError as T, ArcQueryParser as _, bareListResponse as a, defineGuard as b, errorDetailSchema$1 as c, keysetListResponse as d, listResponse as f, responses as g, queryParams as h, aggregateListResponse as i, formatValidationErrors as j, createCircuitBreakerRegistry as k, getDefaultCrudSchemas as l, paginationSchema as m, createStateMachine as n, deleteResponse as o, offsetListResponse as p, scheduleBackground as r, errorContractSchema$1 as s, getUserId as t, getListQueryParams as u, createQueryParser as v, CircuitBreaker as w, defineErrorMapper as x, handleRaw as y };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.16.1",
3
+ "version": "2.17.1",
4
4
  "description": "Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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. Custom Auth Function (API key, gateway, static org)
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