@classytic/arc 2.1.2 → 2.1.3
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/{EventTransport-BD2U0BTc.d.mts → EventTransport-BkUDYZEb.d.mts} +1 -2
- package/dist/HookSystem-BsGV-j2l.mjs +1 -2
- package/dist/{ResourceRegistry-DsN4KJjV.mjs → ResourceRegistry-7Ic20ZMw.mjs} +1 -2
- package/dist/adapters/index.d.mts +4 -4
- package/dist/audit/index.d.mts +5 -6
- package/dist/audit/index.mjs +2 -3
- package/dist/audit/mongodb.d.mts +4 -4
- package/dist/audit/mongodb.mjs +1 -1
- package/dist/{audited-C3T5DTUx.mjs → audited-CGdLiSlE.mjs} +1 -2
- package/dist/auth/index.d.mts +6 -7
- package/dist/auth/index.mjs +10 -16
- package/dist/auth/redis-session.d.mts +2 -3
- package/dist/auth/redis-session.mjs +1 -2
- package/dist/{betterAuthOpenApi-BrHKeSAx.mjs → betterAuthOpenApi-DjWDddNc.mjs} +2 -3
- package/dist/cache/index.d.mts +3 -4
- package/dist/cache/index.mjs +4 -5
- package/dist/{caching-Bl28lYsR.mjs → caching-GSDJcA6-.mjs} +1 -2
- package/dist/{circuitBreaker-DeY4FCjs.mjs → circuitBreaker-DYhWBW_D.mjs} +1 -2
- package/dist/cli/commands/describe.d.mts +1 -2
- package/dist/cli/commands/describe.mjs +1 -2
- package/dist/cli/commands/docs.d.mts +1 -2
- package/dist/cli/commands/docs.mjs +3 -4
- package/dist/cli/commands/generate.d.mts +1 -2
- package/dist/cli/commands/generate.mjs +2 -3
- package/dist/cli/commands/init.d.mts +1 -2
- package/dist/cli/commands/init.mjs +6 -7
- package/dist/cli/commands/introspect.d.mts +1 -2
- package/dist/cli/commands/introspect.mjs +2 -3
- package/dist/cli/index.d.mts +1 -2
- package/dist/cli/index.mjs +1 -2
- package/dist/constants-DdXFXQtN.mjs +1 -2
- package/dist/core/index.d.mts +4 -4
- package/dist/core/index.mjs +1 -1
- package/dist/{createApp-CUgNqegw.mjs → createApp-D2D5XXaV.mjs} +9 -10
- package/dist/{defineResource-k0_BDn8v.mjs → defineResource-PXzSJ15_.mjs} +11 -11
- package/dist/discovery/index.d.mts +1 -2
- package/dist/discovery/index.mjs +1 -2
- package/dist/docs/index.d.mts +5 -6
- package/dist/docs/index.mjs +5 -4
- package/dist/{elevation-B_2dRLVP.d.mts → elevation-DGo5shaX.d.mts} +1 -2
- package/dist/{elevation-BRy3yFWT.mjs → elevation-DSTbVvYj.mjs} +4 -4
- package/dist/{errorHandler-C1okiriz.mjs → errorHandler-C3GY3_ow.mjs} +2 -3
- package/dist/{errorHandler-BbcgBmIH.d.mts → errorHandler-CW3OOeYq.d.mts} +2 -3
- package/dist/{errors-ChKiFz62.d.mts → errors-DAWRdiYP.d.mts} +1 -2
- package/dist/{errors-B9bZok84.mjs → errors-DBANPbGr.mjs} +1 -2
- package/dist/{eventPlugin-DGR_B2on.mjs → eventPlugin-BEOvaDqo.mjs} +2 -3
- package/dist/{eventPlugin-CTrLH3mt.d.mts → eventPlugin-H6wDDjGO.d.mts} +2 -3
- package/dist/events/index.d.mts +4 -5
- package/dist/events/index.mjs +2 -3
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +1 -2
- package/dist/events/transports/redis.d.mts +2 -3
- package/dist/events/transports/redis.mjs +1 -2
- package/dist/{externalPaths-DlINfKbP.d.mts → externalPaths-SyPF2tgK.d.mts} +1 -2
- package/dist/factory/index.d.mts +8 -9
- package/dist/factory/index.mjs +1 -1
- package/dist/{fastifyAdapter-BkrGrlFi.d.mts → fastifyAdapter-C8DlE0YH.d.mts} +4 -5
- package/dist/{fields-DyaDVX4J.d.mts → fields-Bi_AVKSo.d.mts} +2 -3
- package/dist/{fields-iagOozy0.mjs → fields-CTd_CrKr.mjs} +2 -3
- package/dist/hooks/index.d.mts +3 -3
- package/dist/idempotency/index.d.mts +4 -5
- package/dist/idempotency/index.mjs +1 -2
- package/dist/idempotency/mongodb.d.mts +1 -1
- package/dist/idempotency/mongodb.mjs +1 -2
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +1 -2
- package/dist/index.d.mts +9 -10
- package/dist/index.mjs +7 -8
- package/dist/integrations/event-gateway.d.mts +2 -3
- package/dist/integrations/event-gateway.mjs +2 -3
- package/dist/integrations/jobs.d.mts +1 -2
- package/dist/integrations/jobs.mjs +1 -2
- package/dist/integrations/streamline.d.mts +1 -2
- package/dist/integrations/streamline.mjs +1 -2
- package/dist/integrations/websocket.d.mts +1 -2
- package/dist/integrations/websocket.mjs +1 -2
- package/dist/{interface-B01JvPVc.d.mts → interface-CSNjltAc.d.mts} +1 -2
- package/dist/{interface-CZe8IkMf.d.mts → interface-DTbsvIWe.d.mts} +1 -2
- package/dist/{interface-Ch8HU9uM.d.mts → interface-e9XfSsUV.d.mts} +3 -4
- package/dist/{introspectionPlugin-rFdO8ZUa.mjs → introspectionPlugin-B3JkrjwU.mjs} +1 -2
- package/dist/{keys-BqNejWup.mjs → keys-DhqDRxv3.mjs} +1 -2
- package/dist/{logger-Df2O2WsW.mjs → logger-ByrvQWZO.mjs} +1 -2
- package/dist/{memory-cQgelFOj.mjs → memory-B2v7KrCB.mjs} +1 -2
- package/dist/migrations/index.d.mts +1 -2
- package/dist/migrations/index.mjs +1 -2
- package/dist/{mongodb-CGzRbfAK.d.mts → mongodb-ClykrfGo.d.mts} +2 -3
- package/dist/{mongodb-BfJVlUJH.mjs → mongodb-DNKEExbf.mjs} +1 -2
- package/dist/{mongodb-JN-9JA7K.d.mts → mongodb-Dg8O_gvd.d.mts} +2 -3
- package/dist/{openapi-G3Cw7XuM.mjs → openapi-9nB_kiuR.mjs} +5 -4
- package/dist/org/index.d.mts +4 -5
- package/dist/org/index.mjs +1 -2
- package/dist/org/types.d.mts +1 -2
- package/dist/permissions/index.d.mts +5 -6
- package/dist/permissions/index.mjs +7 -7
- package/dist/plugins/index.d.mts +7 -8
- package/dist/plugins/index.mjs +7 -8
- package/dist/plugins/response-cache.d.mts +1 -2
- package/dist/plugins/response-cache.mjs +2 -3
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -2
- package/dist/{pluralize-CEweyOEm.mjs → pluralize-CM-jZg7p.mjs} +1 -2
- package/dist/policies/index.d.mts +4 -5
- package/dist/policies/index.mjs +1 -2
- package/dist/presets/index.d.mts +4 -5
- package/dist/presets/index.mjs +2 -3
- package/dist/presets/multiTenant.d.mts +4 -5
- package/dist/presets/multiTenant.mjs +1 -2
- package/dist/{presets-DzSMwlKj.d.mts → presets-BTeYbw7h.d.mts} +2 -3
- package/dist/{presets-BITljm96.mjs → presets-CeFtfDR8.mjs} +1 -2
- package/dist/{prisma-Dg9GoVdj.d.mts → prisma-C3iornoK.d.mts} +2 -3
- package/dist/prisma-DJbMt3yf.mjs +1 -2
- package/dist/{queryCachePlugin-DMBnp2Q0.mjs → queryCachePlugin-B6R0d4av.mjs} +4 -5
- package/dist/{queryCachePlugin-7THaI5mt.d.mts → queryCachePlugin-Q6SYuHZ6.d.mts} +2 -3
- package/dist/{redis-D-JAeLtm.d.mts → redis-UwjEp8Ea.d.mts} +2 -3
- package/dist/{redis-stream-Bdh_vUU8.d.mts → redis-stream-CBg0upHI.d.mts} +2 -3
- package/dist/registry/index.d.mts +4 -5
- package/dist/registry/index.mjs +2 -2
- package/dist/{requestContext-QQD6ROJc.mjs → requestContext-xi6OKBL-.mjs} +1 -2
- package/dist/{schemaConverter-BwrmWroW.mjs → schemaConverter-Dtg0Kt9T.mjs} +1 -2
- package/dist/schemas/index.d.mts +1 -2
- package/dist/schemas/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -3
- package/dist/scope/index.mjs +2 -3
- package/dist/{sessionManager-jPKLbHE0.d.mts → sessionManager-D_iEHjQl.d.mts} +1 -2
- package/dist/{sse-B3c3_yZp.mjs → sse-DkqQ1uxb.mjs} +2 -3
- package/dist/testing/index.d.mts +8 -9
- package/dist/testing/index.mjs +3 -4
- package/dist/{tracing-Cc7vVQPp.d.mts → tracing-8CEbhF0w.d.mts} +1 -2
- package/dist/{typeGuards-DhMNLuvU.mjs → typeGuards-DwxA1t_L.mjs} +1 -2
- package/dist/types/index.d.mts +6 -7
- package/dist/types/index.mjs +1 -2
- package/dist/{types-CIgB7UUl.d.mts → types-B0dhNrnd.d.mts} +9 -10
- package/dist/types-Beqn1Un7.mjs +1 -2
- package/dist/types-DelU6kln.mjs +25 -0
- package/dist/{types-aYB4V7uN.d.mts → types-RLkFVgaw.d.mts} +18 -4
- package/dist/utils/index.d.mts +5 -6
- package/dist/utils/index.mjs +4 -4
- package/package.json +1 -1
- package/dist/EventTransport-BD2U0BTc.d.mts.map +0 -1
- package/dist/HookSystem-BsGV-j2l.mjs.map +0 -1
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +0 -1
- package/dist/audit/index.d.mts.map +0 -1
- package/dist/audit/index.mjs.map +0 -1
- package/dist/audited-C3T5DTUx.mjs.map +0 -1
- package/dist/auth/index.d.mts.map +0 -1
- package/dist/auth/index.mjs.map +0 -1
- package/dist/auth/redis-session.d.mts.map +0 -1
- package/dist/auth/redis-session.mjs.map +0 -1
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +0 -1
- package/dist/cache/index.d.mts.map +0 -1
- package/dist/cache/index.mjs.map +0 -1
- package/dist/caching-Bl28lYsR.mjs.map +0 -1
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +0 -1
- package/dist/cli/commands/describe.d.mts.map +0 -1
- package/dist/cli/commands/describe.mjs.map +0 -1
- package/dist/cli/commands/docs.d.mts.map +0 -1
- package/dist/cli/commands/docs.mjs.map +0 -1
- package/dist/cli/commands/generate.d.mts.map +0 -1
- package/dist/cli/commands/generate.mjs.map +0 -1
- package/dist/cli/commands/init.d.mts.map +0 -1
- package/dist/cli/commands/init.mjs.map +0 -1
- package/dist/cli/commands/introspect.d.mts.map +0 -1
- package/dist/cli/commands/introspect.mjs.map +0 -1
- package/dist/cli/index.d.mts.map +0 -1
- package/dist/cli/index.mjs.map +0 -1
- package/dist/constants-DdXFXQtN.mjs.map +0 -1
- package/dist/createApp-CUgNqegw.mjs.map +0 -1
- package/dist/defineResource-k0_BDn8v.mjs.map +0 -1
- package/dist/discovery/index.d.mts.map +0 -1
- package/dist/discovery/index.mjs.map +0 -1
- package/dist/docs/index.d.mts.map +0 -1
- package/dist/docs/index.mjs.map +0 -1
- package/dist/elevation-BRy3yFWT.mjs.map +0 -1
- package/dist/elevation-B_2dRLVP.d.mts.map +0 -1
- package/dist/errorHandler-BbcgBmIH.d.mts.map +0 -1
- package/dist/errorHandler-C1okiriz.mjs.map +0 -1
- package/dist/errors-B9bZok84.mjs.map +0 -1
- package/dist/errors-ChKiFz62.d.mts.map +0 -1
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +0 -1
- package/dist/eventPlugin-DGR_B2on.mjs.map +0 -1
- package/dist/events/index.d.mts.map +0 -1
- package/dist/events/index.mjs.map +0 -1
- package/dist/events/transports/redis-stream-entry.mjs.map +0 -1
- package/dist/events/transports/redis.d.mts.map +0 -1
- package/dist/events/transports/redis.mjs.map +0 -1
- package/dist/externalPaths-DlINfKbP.d.mts.map +0 -1
- package/dist/factory/index.d.mts.map +0 -1
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +0 -1
- package/dist/fields-DyaDVX4J.d.mts.map +0 -1
- package/dist/fields-iagOozy0.mjs.map +0 -1
- package/dist/idempotency/index.d.mts.map +0 -1
- package/dist/idempotency/index.mjs.map +0 -1
- package/dist/idempotency/mongodb.mjs.map +0 -1
- package/dist/idempotency/redis.mjs.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/integrations/event-gateway.d.mts.map +0 -1
- package/dist/integrations/event-gateway.mjs.map +0 -1
- package/dist/integrations/jobs.d.mts.map +0 -1
- package/dist/integrations/jobs.mjs.map +0 -1
- package/dist/integrations/streamline.d.mts.map +0 -1
- package/dist/integrations/streamline.mjs.map +0 -1
- package/dist/integrations/websocket.d.mts.map +0 -1
- package/dist/integrations/websocket.mjs.map +0 -1
- package/dist/interface-B01JvPVc.d.mts.map +0 -1
- package/dist/interface-CZe8IkMf.d.mts.map +0 -1
- package/dist/interface-Ch8HU9uM.d.mts.map +0 -1
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +0 -1
- package/dist/keys-BqNejWup.mjs.map +0 -1
- package/dist/logger-Df2O2WsW.mjs.map +0 -1
- package/dist/memory-cQgelFOj.mjs.map +0 -1
- package/dist/migrations/index.d.mts.map +0 -1
- package/dist/migrations/index.mjs.map +0 -1
- package/dist/mongodb-BfJVlUJH.mjs.map +0 -1
- package/dist/mongodb-CGzRbfAK.d.mts.map +0 -1
- package/dist/mongodb-JN-9JA7K.d.mts.map +0 -1
- package/dist/openapi-G3Cw7XuM.mjs.map +0 -1
- package/dist/org/index.d.mts.map +0 -1
- package/dist/org/index.mjs.map +0 -1
- package/dist/org/types.d.mts.map +0 -1
- package/dist/permissions/index.d.mts.map +0 -1
- package/dist/permissions/index.mjs.map +0 -1
- package/dist/plugins/index.d.mts.map +0 -1
- package/dist/plugins/index.mjs.map +0 -1
- package/dist/plugins/response-cache.d.mts.map +0 -1
- package/dist/plugins/response-cache.mjs.map +0 -1
- package/dist/plugins/tracing-entry.mjs.map +0 -1
- package/dist/pluralize-CEweyOEm.mjs.map +0 -1
- package/dist/policies/index.d.mts.map +0 -1
- package/dist/policies/index.mjs.map +0 -1
- package/dist/presets/index.d.mts.map +0 -1
- package/dist/presets/index.mjs.map +0 -1
- package/dist/presets/multiTenant.d.mts.map +0 -1
- package/dist/presets/multiTenant.mjs.map +0 -1
- package/dist/presets-BITljm96.mjs.map +0 -1
- package/dist/presets-DzSMwlKj.d.mts.map +0 -1
- package/dist/prisma-DJbMt3yf.mjs.map +0 -1
- package/dist/prisma-Dg9GoVdj.d.mts.map +0 -1
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +0 -1
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +0 -1
- package/dist/redis-D-JAeLtm.d.mts.map +0 -1
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +0 -1
- package/dist/registry/index.d.mts.map +0 -1
- package/dist/requestContext-QQD6ROJc.mjs.map +0 -1
- package/dist/schemaConverter-BwrmWroW.mjs.map +0 -1
- package/dist/schemas/index.d.mts.map +0 -1
- package/dist/schemas/index.mjs.map +0 -1
- package/dist/scope/index.d.mts.map +0 -1
- package/dist/scope/index.mjs.map +0 -1
- package/dist/sessionManager-jPKLbHE0.d.mts.map +0 -1
- package/dist/sse-B3c3_yZp.mjs.map +0 -1
- package/dist/testing/index.d.mts.map +0 -1
- package/dist/testing/index.mjs.map +0 -1
- package/dist/tracing-Cc7vVQPp.d.mts.map +0 -1
- package/dist/typeGuards-DhMNLuvU.mjs.map +0 -1
- package/dist/types/index.d.mts.map +0 -1
- package/dist/types/index.mjs.map +0 -1
- package/dist/types-Beqn1Un7.mjs.map +0 -1
- package/dist/types-CIgB7UUl.d.mts.map +0 -1
- package/dist/types-aYB4V7uN.d.mts.map +0 -1
- package/dist/utils/index.d.mts.map +0 -1
package/dist/auth/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/auth/authPlugin.ts","../../src/auth/betterAuth.ts","../../src/auth/sessionManager.ts"],"sourcesContent":["/**\n * Auth Plugin - Flexible, Database-Agnostic Authentication\n *\n * Arc provides JWT infrastructure and calls your authenticator.\n * You control ALL authentication logic.\n *\n * Design principles:\n * - Arc handles plumbing (JWT sign/verify utilities)\n * - App handles business logic (how to authenticate, where users live)\n * - Works with any database (Prisma, MongoDB, Postgres, none)\n * - Supports multiple auth strategies (JWT, API keys, sessions, etc.)\n *\n * @example\n * ```typescript\n * // In createApp\n * auth: {\n * jwt: { secret: process.env.JWT_SECRET },\n * authenticate: async (request, { jwt }) => {\n * // Your auth logic - Arc never touches your database\n * const token = request.headers.authorization?.split(' ')[1];\n * if (!token) return null;\n * const decoded = jwt.verify(token);\n * return userRepo.findById(decoded.id);\n * },\n * }\n * ```\n */\n\nimport fp from 'fastify-plugin';\nimport type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';\nimport type {\n AuthPluginOptions,\n AuthHelpers,\n JwtContext,\n AuthenticatorContext,\n TokenPair,\n} from '../types/index.js';\nimport type { RequestScope } from '../scope/types.js';\nimport { AUTHENTICATED_SCOPE } from '../scope/types.js';\n\n// ============================================================================\n// Fastify Type Extensions\n// ============================================================================\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n /** Authenticate middleware - use in preHandler for protected routes */\n authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;\n /** Optional authenticate - parses JWT if present, doesn't fail if absent */\n optionalAuthenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;\n /** Authorize middleware factory - checks if user has required roles */\n authorize: (...roles: string[]) => (request: FastifyRequest, reply: FastifyReply) => Promise<void>;\n /** Auth helpers - issueTokens, jwt utilities */\n auth: AuthHelpers;\n }\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Parse expiration string to seconds\n */\nfunction parseExpiresIn(input: string | undefined, defaultValue: number): number {\n if (!input) return defaultValue;\n if (/^\\d+$/.test(input)) return parseInt(input, 10);\n\n const match = /^(\\d+)\\s*([smhd])$/i.exec(input);\n if (!match) return defaultValue;\n\n const value = parseInt(match[1]!, 10);\n const unit = match[2]!.toLowerCase();\n\n const multipliers: Record<string, number> = { s: 1, m: 60, h: 3600, d: 86400 };\n return value * (multipliers[unit] ?? 1);\n}\n\n/**\n * Extract Bearer token from Authorization header\n */\nfunction extractBearerToken(request: FastifyRequest): string | null {\n const auth = request.headers.authorization;\n if (!auth?.startsWith('Bearer ')) return null;\n return auth.slice(7);\n}\n\n// ============================================================================\n// Auth Plugin\n// ============================================================================\n\nconst authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (\n fastify: FastifyInstance,\n opts: AuthPluginOptions = {}\n) => {\n const { jwt: jwtConfig, authenticate: appAuthenticator, onFailure, userProperty = 'user', exposeAuthErrors = false } = opts;\n\n // ========================================\n // 1. Setup JWT Infrastructure (Optional)\n // ========================================\n\n let jwtContext: JwtContext | null = null;\n\n if (jwtConfig?.secret) {\n // Validate secret strength\n if (jwtConfig.secret.length < 32) {\n throw new Error(\n `JWT secret must be at least 32 characters (current: ${jwtConfig.secret.length}).\\n` +\n 'Use a strong random secret for production.'\n );\n }\n\n // Register @fastify/jwt\n const jwtPlugin = await import('@fastify/jwt');\n await fastify.register(jwtPlugin.default ?? jwtPlugin, {\n secret: jwtConfig.secret,\n sign: {\n expiresIn: jwtConfig.expiresIn ?? '15m',\n ...(jwtConfig.sign ?? {}),\n },\n verify: { ...(jwtConfig.verify ?? {}) },\n });\n\n // Create JWT context for authenticator\n // @fastify/jwt v10 uses fast-jwt under the hood\n const fastifyWithJwt = fastify as FastifyInstance & {\n jwt: {\n sign: (payload: Record<string, unknown>, options?: { expiresIn?: string | number; key?: string }) => string;\n verify: <T>(token: string, options?: { key?: string }) => T;\n decode: <T>(token: string) => T | null;\n };\n };\n\n jwtContext = {\n verify: <T = Record<string, unknown>>(token: string): T => {\n return fastifyWithJwt.jwt.verify<T>(token);\n },\n sign: (payload: Record<string, unknown>, options?: { expiresIn?: string }): string => {\n return fastifyWithJwt.jwt.sign(payload, options);\n },\n decode: <T = Record<string, unknown>>(token: string): T | null => {\n try {\n return fastifyWithJwt.jwt.decode<T>(token);\n } catch {\n return null;\n }\n },\n };\n\n fastify.log.debug('Auth: JWT infrastructure enabled');\n }\n\n // ========================================\n // 2. Create Authenticator Context\n // ========================================\n\n const authContext: AuthenticatorContext = {\n jwt: jwtContext,\n fastify,\n };\n\n // ========================================\n // 3. Create Authenticate Middleware\n // ========================================\n\n /**\n * Authenticate middleware\n *\n * Arc adds this to preHandler for non-public routes.\n * Calls app's authenticator or falls back to default JWT verify.\n */\n const authenticate = async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n try {\n let user: unknown = null;\n\n if (appAuthenticator) {\n // App-provided authenticator - full control\n user = await appAuthenticator(request, authContext);\n } else if (jwtContext) {\n // Default: JWT Bearer token verification\n const token = extractBearerToken(request);\n if (token) {\n const decoded = jwtContext.verify(token) as Record<string, unknown>;\n // Reject refresh tokens — they must only be used at the refresh endpoint\n if (decoded.type === 'refresh') {\n throw new Error('Refresh tokens cannot be used for authentication');\n }\n user = decoded;\n }\n } else {\n // No authenticator and no JWT - configuration error\n throw new Error(\n 'No authenticator configured. Provide auth.authenticate function or auth.jwt.secret.'\n );\n }\n\n if (!user) {\n throw new Error('Authentication required');\n }\n\n // Always set canonical `request.user` for Arc internals, plus custom alias.\n const reqRecord = request as unknown as Record<string, unknown>;\n reqRecord.user = user;\n reqRecord[userProperty] = user;\n\n // Resolve scope from user claims (skip if custom authenticator already set it)\n if (!request.scope || request.scope.kind === 'public') {\n const userRecord = user as Record<string, unknown>;\n if (userRecord.organizationId) {\n // User has org context — set member scope\n request.scope = {\n kind: 'member',\n organizationId: String(userRecord.organizationId),\n orgRoles: Array.isArray(userRecord.orgRoles) ? userRecord.orgRoles as string[] : [],\n } satisfies RequestScope;\n } else {\n // No org context — authenticated only (can be upgraded via resolveOrgFromHeader hook)\n request.scope = AUTHENTICATED_SCOPE;\n }\n }\n\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n\n // Custom failure handler\n if (onFailure) {\n await onFailure(request, reply, error);\n return;\n }\n\n // Default 401 response — hide internal details unless explicitly opted-in\n const message = exposeAuthErrors ? error.message : 'Authentication required';\n\n reply.code(401).send({\n success: false,\n error: 'Unauthorized',\n message,\n });\n }\n };\n\n // ========================================\n // 3b. Optional Authenticate Middleware\n // ========================================\n\n /**\n * Optional authenticate middleware\n *\n * Parses JWT if a Bearer token is present and populates request.user.\n * Does NOT fail if no token or invalid token — treats as unauthenticated.\n *\n * Used on allowPublic() routes so that downstream middleware (e.g. multiTenant\n * flexible filter) can apply org-scoped queries when a user IS authenticated.\n */\n const optionalAuthenticate = async (request: FastifyRequest, _reply: FastifyReply): Promise<void> => {\n try {\n let user: unknown = null;\n\n if (appAuthenticator) {\n user = await appAuthenticator(request, authContext);\n } else if (jwtContext) {\n const token = extractBearerToken(request);\n if (token) {\n const decoded = jwtContext.verify(token) as Record<string, unknown>;\n // Silently ignore refresh tokens\n if (decoded.type === 'refresh') return;\n user = decoded;\n }\n }\n\n if (user) {\n const reqRecord = request as unknown as Record<string, unknown>;\n reqRecord.user = user;\n reqRecord[userProperty] = user;\n\n // Resolve scope from user claims (skip if custom authenticator already set it)\n if (!request.scope || request.scope.kind === 'public') {\n const userRecord = user as Record<string, unknown>;\n if (userRecord.organizationId) {\n request.scope = {\n kind: 'member',\n organizationId: String(userRecord.organizationId),\n orgRoles: Array.isArray(userRecord.orgRoles) ? userRecord.orgRoles as string[] : [],\n } satisfies RequestScope;\n } else {\n request.scope = AUTHENTICATED_SCOPE;\n }\n }\n }\n // No user = continue as unauthenticated (scope stays 'public')\n } catch {\n // Silently ignore auth errors — invalid/expired token = treat as unauthenticated\n }\n };\n\n // ========================================\n // 4. Create Auth Helpers\n // ========================================\n\n const refreshSecret = jwtConfig?.refreshSecret ?? jwtConfig?.secret;\n const accessExpiresIn = jwtConfig?.expiresIn ?? '15m';\n const refreshExpiresIn = jwtConfig?.refreshExpiresIn ?? '7d';\n\n /**\n * Issue access + refresh tokens\n * App calls this after validating credentials (login, OAuth, etc.)\n */\n const issueTokens = (\n payload: Record<string, unknown>,\n options?: { expiresIn?: string; refreshExpiresIn?: string }\n ): TokenPair => {\n if (!jwtContext) {\n throw new Error('JWT not configured. Provide auth.jwt.secret to use issueTokens.');\n }\n\n const accessTtl = options?.expiresIn ?? accessExpiresIn;\n const refreshTtl = options?.refreshExpiresIn ?? refreshExpiresIn;\n\n // Access token with full payload + explicit type\n const accessToken = jwtContext.sign({ ...payload, type: 'access' }, { expiresIn: accessTtl });\n\n // Refresh token with minimal payload (just id)\n const refreshPayload = payload.id\n ? { id: payload.id, type: 'refresh' }\n : payload._id\n ? { id: payload._id, type: 'refresh' }\n : { ...payload, type: 'refresh' };\n\n let refreshToken: string | undefined;\n if (refreshSecret) {\n const fastifyWithJwt = fastify as FastifyInstance & {\n jwt: { sign: (payload: Record<string, unknown>, options?: Record<string, unknown>) => string };\n };\n refreshToken = fastifyWithJwt.jwt.sign(refreshPayload, {\n expiresIn: refreshTtl,\n // Use refresh key if different from main secret (@fastify/jwt v10 uses 'key' instead of 'secret')\n ...(refreshSecret !== jwtConfig?.secret ? { key: refreshSecret } : {}),\n });\n }\n\n return {\n accessToken,\n refreshToken,\n expiresIn: parseExpiresIn(accessTtl, 900),\n refreshExpiresIn: refreshToken ? parseExpiresIn(refreshTtl, 604800) : undefined,\n tokenType: 'Bearer',\n };\n };\n\n /**\n * Verify refresh token\n * App calls this in refresh endpoint\n */\n const verifyRefreshToken = <T = Record<string, unknown>>(token: string): T => {\n if (!jwtContext) {\n throw new Error('JWT not configured. Provide auth.jwt.secret to use verifyRefreshToken.');\n }\n\n const fastifyWithJwt = fastify as FastifyInstance & {\n jwt: { verify: <T>(token: string, options?: Record<string, unknown>) => T };\n };\n\n const decoded = fastifyWithJwt.jwt.verify<Record<string, unknown>>(token, {\n // @fastify/jwt v10 uses 'key' instead of 'secret' for per-operation overrides\n ...(refreshSecret !== jwtConfig?.secret ? { key: refreshSecret } : {}),\n });\n\n // Enforce token type — reject access tokens used at the refresh endpoint\n if (decoded.type !== 'refresh') {\n throw new Error('Invalid token type: expected refresh token');\n }\n\n return decoded as T;\n };\n\n // ========================================\n // 5. Create Authorize Middleware Factory\n // ========================================\n\n /**\n * Authorize middleware factory\n * Creates a middleware that checks if user has required roles\n *\n * @example\n * preHandler: [fastify.authenticate, fastify.authorize('admin', 'superadmin')]\n */\n const authorize = (...allowedRoles: string[]) => {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n const reqRecord = request as unknown as Record<string, unknown>;\n const user = (reqRecord[userProperty] ?? reqRecord.user) as\n | { roles?: string[] }\n | undefined;\n\n if (!user) {\n reply.code(401).send({\n success: false,\n error: 'Unauthorized',\n message: 'No user context',\n });\n return;\n }\n\n const userRoles = user.roles ?? [];\n\n // Special case: ['*'] means any authenticated user\n if (allowedRoles.length === 1 && allowedRoles[0] === '*') {\n return;\n }\n\n // Check if user has one of the required roles\n const hasRole = allowedRoles.some((role) => userRoles.includes(role));\n\n if (!hasRole) {\n reply.code(403).send({\n success: false,\n error: 'Forbidden',\n message: `Requires one of: ${allowedRoles.join(', ')}`,\n });\n return;\n }\n };\n };\n\n // ========================================\n // 6. Decorate Fastify Instance\n // ========================================\n\n const authHelpers: AuthHelpers = {\n jwt: jwtContext,\n issueTokens,\n verifyRefreshToken,\n };\n\n fastify.decorate('authenticate', authenticate);\n fastify.decorate('optionalAuthenticate', optionalAuthenticate);\n fastify.decorate('authorize', authorize);\n fastify.decorate('auth', authHelpers);\n\n fastify.log.debug(\n `Auth: Plugin registered (jwt=${!!jwtContext}, customAuth=${!!appAuthenticator})`\n );\n};\n\n// ============================================================================\n// Export\n// ============================================================================\n\nexport default fp(authPlugin, {\n name: 'arc-auth',\n fastify: '5.x',\n});\n\nexport { authPlugin };\nexport type { AuthPluginOptions };\n","/**\n * Better Auth Adapter for Arc/Fastify\n *\n * Bridges Fastify <-> Better Auth's Fetch API (Request/Response).\n * Better Auth is the USER's dependency -- Arc only provides this thin adapter.\n *\n * @example\n * import { betterAuth } from 'better-auth';\n * import { createBetterAuthAdapter } from '@classytic/arc/auth';\n *\n * const auth = betterAuth({ ... });\n *\n * const app = await createApp({\n * auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth }) },\n * });\n */\n\nimport fp from 'fastify-plugin';\nimport type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';\nimport { requireOrgRole, requireOrgMembership, requireTeamMembership } from '../permissions/index.js';\nimport type { ExternalOpenApiPaths } from '../docs/externalPaths.js';\nimport type { RequestScope } from '../scope/types.js';\nimport { AUTHENTICATED_SCOPE } from '../scope/types.js';\nimport { ArcError } from '../utils/errors.js';\n\n// Plugin-local augmentation for @fastify/raw-body compatibility\ndeclare module 'fastify' {\n interface FastifyRequest {\n /** Raw request body (from @fastify/raw-body plugin, if registered) */\n rawBody?: Buffer | string;\n }\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Minimal interface for a Better Auth instance.\n * We only require the `handler` method -- the full Better Auth type\n * comes from the user's `better-auth` installation.\n */\nexport interface BetterAuthHandler {\n handler: (request: Request) => Promise<Response>;\n /** The API endpoint map — each value has .path and .options. Used for OpenAPI docs extraction. */\n api?: Record<string, unknown>;\n}\n\nexport interface BetterAuthAdapterOptions {\n /** Better Auth instance (from betterAuth() in user's app) */\n auth: BetterAuthHandler;\n /** Base path for auth routes (default: '/api/auth') */\n basePath?: string;\n /**\n * Enable org context extraction from Better Auth's organization plugin.\n * When enabled, the adapter will look up the user's active organization\n * membership and populate `request.scope` with org roles.\n *\n * @default false\n */\n orgContext?: boolean;\n /**\n * OpenAPI documentation for auth endpoints.\n * - `true` (default): auto-extract from auth.api if available\n * - `false`: disable (auth routes won't appear in OpenAPI docs)\n * - `ExternalOpenApiPaths`: manual spec override\n */\n openapi?: boolean | ExternalOpenApiPaths;\n /**\n * Additional user fields from Better Auth config.\n * These get merged into signUpEmail/updateUser request body schemas\n * and the User component schema in OpenAPI docs.\n *\n * Fields with `input: false` are excluded from request bodies\n * but still appear in the User component schema (output-only).\n *\n * @example\n * ```typescript\n * userFields: {\n * department: { type: 'string', description: 'Department', required: true },\n * roles: { type: 'array', description: 'User roles', input: false },\n * }\n * ```\n */\n userFields?: Record<string, {\n type: string;\n description?: string;\n required?: boolean;\n input?: boolean;\n }>;\n /**\n * Expose detailed auth error messages in 401 responses.\n * When false (default), returns generic \"Authentication required\".\n * When true, includes the actual error message for debugging.\n */\n exposeAuthErrors?: boolean;\n}\n\nexport interface BetterAuthAdapterResult {\n /** Fastify plugin that registers catch-all auth routes */\n plugin: FastifyPluginAsync;\n /** Authenticate preHandler -- validates session via Better Auth */\n authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;\n /** Optional authenticate -- resolves session silently, continues as unauthenticated on failure */\n optionalAuthenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;\n /** Permission helpers bound to this auth adapter (available when orgContext is enabled) */\n permissions: {\n requireOrgRole: (...roles: string[]) => import('../permissions/types.js').PermissionCheck;\n requireOrgMembership: () => import('../permissions/types.js').PermissionCheck;\n requireTeamMembership: () => import('../permissions/types.js').PermissionCheck;\n };\n /** OpenAPI paths extracted from Better Auth endpoints (undefined if openapi: false) */\n openapi?: ExternalOpenApiPaths;\n}\n\n// ============================================================================\n// Fastify Type Extensions\n// ============================================================================\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n /**\n * Authenticate middleware (Better Auth variant).\n * Validates session by calling Better Auth's session endpoint internally.\n * Set by the Better Auth adapter plugin.\n */\n authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;\n /**\n * Optional authenticate middleware (Better Auth variant).\n * Tries to resolve session silently — populates request.user if valid,\n * continues as unauthenticated if no session or invalid session.\n * Used on allowPublic() routes so downstream middleware can apply\n * org-scoped queries when a user IS authenticated.\n */\n optionalAuthenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;\n }\n}\n\n// ============================================================================\n// Conversion Helpers\n// ============================================================================\n\n/**\n * Convert a Fastify request into a Fetch API Request.\n *\n * Better Auth expects standard Web API Request objects.\n * We reconstruct one from Fastify's request properties.\n */\nfunction toFetchRequest(request: FastifyRequest): Request {\n // Build full URL from Fastify's protocol, hostname, and original URL\n const protocol = request.protocol ?? 'http';\n const host = request.hostname ?? 'localhost';\n const url = `${protocol}://${host}${request.url}`;\n\n // Convert Fastify headers to a Headers object.\n // Fastify headers can be string | string[] | undefined.\n const headers = new Headers();\n for (const [key, value] of Object.entries(request.headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const v of value) {\n headers.append(key, v);\n }\n } else {\n headers.set(key, value);\n }\n }\n\n // Determine if this method can carry a body\n const hasBody = request.method !== 'GET' && request.method !== 'HEAD';\n\n // Reconstruct the body with content-type fidelity.\n // Fastify already parsed the body — we serialize it back respecting the original format\n // so that Better Auth can handle form/urlencoded, multipart, and JSON payloads correctly.\n let body: string | Buffer | undefined;\n if (hasBody && request.body != null) {\n const contentType = (request.headers['content-type'] ?? '').toLowerCase();\n if (request.rawBody) {\n // rawBody plugin preserves the original bytes — use as-is\n body = request.rawBody;\n } else if (contentType.includes('application/x-www-form-urlencoded')) {\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(request.body as Record<string, unknown>)) {\n if (v != null) params.set(k, String(v));\n }\n body = params.toString();\n } else if (typeof request.body === 'string') {\n body = request.body;\n } else if (\n contentType.includes('application/json') ||\n contentType.includes('text/') ||\n !contentType // Fastify defaults to JSON parsing when no content-type\n ) {\n body = JSON.stringify(request.body);\n } else {\n // Non-JSON/non-string content (e.g. multipart/form-data) without rawBody\n // cannot be faithfully reconstructed. Enable @fastify/raw-body for full fidelity.\n request.log?.warn?.(\n 'toFetchRequest: cannot reconstruct %s body without rawBody plugin',\n contentType,\n );\n }\n }\n\n return new Request(url, {\n method: request.method,\n headers,\n body,\n });\n}\n\n/**\n * Pipe a Fetch API Response back into Fastify's reply.\n *\n * Transfers status code, all response headers, and the body.\n * Handles both buffered (JSON) and streaming (SSE) responses.\n */\nasync function sendFetchResponse(response: Response, reply: FastifyReply): Promise<void> {\n // Set status code\n reply.status(response.status);\n\n // Copy response headers to Fastify reply\n response.headers.forEach((value, key) => {\n // Skip transfer-encoding -- Fastify manages this itself\n if (key.toLowerCase() === 'transfer-encoding') return;\n reply.header(key, value);\n });\n\n // Stream the body if it's a streaming content type (e.g. SSE),\n // otherwise buffer as text to avoid holding large chunks in memory.\n const contentType = response.headers.get('content-type') ?? '';\n if (response.body && (contentType.includes('text/event-stream') || contentType.includes('application/octet-stream'))) {\n // Pipe the ReadableStream directly — Fastify v5 supports web streams\n await reply.send(response.body);\n } else {\n const body = await response.text();\n await reply.send(body);\n }\n}\n\n// ============================================================================\n// Direct API Helpers (bypass HTTP round-trips)\n// ============================================================================\n\n/** Type for Better Auth's direct API methods (optional, available in newer versions) */\ninterface BetterAuthDirectApi {\n getSession?: (opts: { headers: Headers }) => Promise<{ user: Record<string, unknown>; session: Record<string, unknown> } | null>;\n [key: string]: unknown;\n}\n\n/**\n * Try to get session via Better Auth's direct JS API.\n * Returns null if the API method is not available (older Better Auth versions).\n */\nasync function tryDirectGetSession(\n auth: BetterAuthHandler,\n headers: Headers,\n): Promise<{ user: Record<string, unknown>; session: Record<string, unknown> } | null> {\n const api = auth.api as BetterAuthDirectApi | undefined;\n if (!api || typeof api.getSession !== 'function') return null;\n\n try {\n const result = await api.getSession({ headers });\n if (result?.user) return result;\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Try to get active org member via direct JS API.\n * Returns roles array or null if not available.\n */\nasync function tryDirectGetActiveMember(\n auth: BetterAuthHandler,\n headers: Headers,\n): Promise<string[] | null> {\n const api = auth.api as Record<string, unknown> | undefined;\n const orgApi = api as { organization?: Record<string, unknown> } | undefined;\n const getActiveMember = orgApi?.organization?.getActiveMember as\n ((opts: { headers: Headers }) => Promise<Record<string, unknown> | null>) | undefined;\n\n if (typeof getActiveMember !== 'function') return null;\n\n try {\n const memberData = await getActiveMember({ headers });\n if (memberData) return extractRolesFromMembership(memberData);\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Look up member role by explicit organizationId (query param).\n *\n * Better Auth's `getActiveMemberRole` endpoint accepts an `organizationId`\n * query parameter, bypassing the session's `activeOrganizationId`.\n * This is essential for API key auth where the synthetic session has no\n * active organization set — callers pass org context via `x-organization-id` header.\n */\nasync function tryDirectGetMemberRole(\n auth: BetterAuthHandler,\n headers: Headers,\n organizationId: string,\n): Promise<string[] | null> {\n const api = auth.api as Record<string, unknown> | undefined;\n const orgApi = api as { organization?: Record<string, unknown> } | undefined;\n const getActiveMemberRole = orgApi?.organization?.getActiveMemberRole as\n | ((opts: { headers: Headers; query: { organizationId: string } }) => Promise<{ role?: unknown } | null>)\n | undefined;\n\n if (typeof getActiveMemberRole !== 'function') return null;\n\n try {\n const result = await getActiveMemberRole({ headers, query: { organizationId } });\n if (result?.role) return parseRoles(result.role);\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Try to list teams via direct JS API.\n */\nasync function tryDirectListTeams(\n auth: BetterAuthHandler,\n headers: Headers,\n): Promise<Array<Record<string, unknown>> | null> {\n const api = auth.api as Record<string, unknown> | undefined;\n const orgApi = api as { organization?: Record<string, unknown> } | undefined;\n const listTeams = orgApi?.organization?.listTeams as\n ((opts: { headers: Headers }) => Promise<unknown>) | undefined;\n\n if (typeof listTeams !== 'function') return null;\n\n try {\n const result = await listTeams({ headers });\n const teams = Array.isArray(result) ? result : (result as Record<string, unknown>)?.teams;\n return Array.isArray(teams) ? teams : null;\n } catch {\n return null;\n }\n}\n\n// ============================================================================\n// Shared Helpers\n// ============================================================================\n\n/** Build a Headers object from Fastify request headers */\nfunction buildHeaders(request: FastifyRequest): Headers {\n const headers = new Headers();\n for (const [key, value] of Object.entries(request.headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const v of value) {\n headers.append(key, v);\n }\n } else {\n headers.set(key, value);\n }\n }\n return headers;\n}\n\n/** Normalize unknown ID-like values to comparable string form */\nfunction normalizeId(value: unknown): string | null {\n if (value == null) return null;\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'bigint') return String(value);\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n const nested = obj._id ?? obj.id ?? obj.organizationId;\n if (nested != null && nested !== value) return normalizeId(nested);\n }\n return String(value);\n}\n\n/** Parse role payload from Better Auth (string, csv, array) into normalized roles[] */\nfunction parseRoles(value: unknown): string[] {\n if (Array.isArray(value)) {\n return value.map((r) => String(r).trim()).filter(Boolean);\n }\n if (typeof value === 'string') {\n return value.split(',').map((r) => r.trim()).filter(Boolean);\n }\n return [];\n}\n\n/** Extract role field from heterogeneous org membership shapes */\nfunction extractRolesFromMembership(membership: Record<string, unknown>): string[] {\n const direct = parseRoles(membership.role ?? membership.roles ?? membership.orgRole);\n if (direct.length > 0) return direct;\n\n const nestedMembership = membership.membership as Record<string, unknown> | undefined;\n if (nestedMembership) {\n const nested = parseRoles(nestedMembership.role ?? nestedMembership.roles);\n if (nested.length > 0) return nested;\n }\n\n return [];\n}\n\n/** Match an organization membership entry against the active org id */\nfunction membershipMatchesOrg(membership: Record<string, unknown>, activeOrgId: string): boolean {\n const candidates = [\n normalizeId(membership.organizationId),\n normalizeId(membership.orgId),\n normalizeId(membership.id),\n normalizeId((membership.organization as Record<string, unknown> | undefined)?._id),\n normalizeId((membership.organization as Record<string, unknown> | undefined)?.id),\n normalizeId((membership.organization as Record<string, unknown> | undefined)?.organizationId),\n ].filter(Boolean) as string[];\n\n return candidates.includes(activeOrgId);\n}\n\n/**\n * Resolve org roles with fallback chain:\n * 1) GET /organization/get-active-member (requires activeOrganizationId in session)\n * 2) GET /organization/get-active-member-role?organizationId=... (explicit org — works for API key auth)\n * 3) GET /organization/list (fallback for type mismatch/legacy ID storage)\n */\nasync function resolveOrgRoles(\n auth: BetterAuthHandler,\n protocol: string,\n host: string,\n normalizedBase: string,\n headers: Headers,\n activeOrgId: string,\n): Promise<string[] | null> {\n // 1) Primary lookup — works when session has activeOrganizationId\n const memberUrl = `${protocol}://${host}${normalizedBase}/organization/get-active-member`;\n const memberRequest = new Request(memberUrl, { method: 'GET', headers });\n const memberResponse = await auth.handler(memberRequest);\n\n if (memberResponse.ok) {\n const memberData = await memberResponse.json() as Record<string, unknown> | null;\n if (memberData) {\n return extractRolesFromMembership(memberData);\n }\n }\n\n // 2) Explicit org lookup — works for API key auth (no activeOrganizationId in session)\n const roleUrl = `${protocol}://${host}${normalizedBase}/organization/get-active-member-role?organizationId=${encodeURIComponent(activeOrgId)}`;\n const roleRequest = new Request(roleUrl, { method: 'GET', headers });\n const roleResponse = await auth.handler(roleRequest);\n\n if (roleResponse.ok) {\n const roleData = await roleResponse.json() as Record<string, unknown> | null;\n if (roleData?.role) {\n return parseRoles(roleData.role);\n }\n }\n\n // 3) Fallback lookup via org list\n const listUrl = `${protocol}://${host}${normalizedBase}/organization/list`;\n const listRequest = new Request(listUrl, { method: 'GET', headers });\n const listResponse = await auth.handler(listRequest);\n if (!listResponse.ok) return null;\n\n const listData = await listResponse.json() as unknown;\n const memberships = Array.isArray(listData)\n ? listData\n : ((listData as Record<string, unknown>)?.organizations\n ?? (listData as Record<string, unknown>)?.data\n ?? []);\n if (!Array.isArray(memberships)) return null;\n\n const target = memberships.find((entry) => {\n if (!entry || typeof entry !== 'object') return false;\n return membershipMatchesOrg(entry as Record<string, unknown>, activeOrgId);\n }) as Record<string, unknown> | undefined;\n\n if (!target) return null;\n return extractRolesFromMembership(target);\n}\n\n// ============================================================================\n// Adapter Factory\n// ============================================================================\n\n/**\n * Create a Better Auth adapter for Arc/Fastify.\n *\n * Returns a Fastify plugin (registers catch-all auth routes) and an\n * `authenticate` preHandler that validates sessions via Better Auth.\n *\n * @example\n * ```typescript\n * import { betterAuth } from 'better-auth';\n * import { createBetterAuthAdapter } from '@classytic/arc/auth';\n *\n * const auth = betterAuth({\n * database: ...,\n * emailAndPassword: { enabled: true },\n * });\n *\n * const { plugin, authenticate } = createBetterAuthAdapter({ auth });\n *\n * // Register the plugin (catch-all auth routes)\n * await fastify.register(plugin);\n *\n * // Use authenticate as a preHandler on protected routes\n * fastify.get('/me', { preHandler: [authenticate] }, handler);\n * ```\n */\nexport function createBetterAuthAdapter(\n options: BetterAuthAdapterOptions,\n): BetterAuthAdapterResult {\n const { auth, basePath = '/api/auth', orgContext: orgContextOpt = false, openapi: openapiOpt = true, userFields, exposeAuthErrors = false } = options;\n\n // Normalize basePath -- strip trailing slash\n const normalizedBase = basePath.replace(/\\/+$/, '');\n\n // Org context config\n const orgEnabled = !!orgContextOpt;\n\n // ========================================\n // Authenticate preHandler\n // ========================================\n\n /**\n * Validates the current session by forwarding cookies/headers\n * to Better Auth's `GET /api/auth/get-session` endpoint.\n *\n * On success, sets `request.user` and `request.session`.\n * When orgContext is enabled, also sets `request.scope` to\n * `{ kind: 'member', organizationId, orgRoles, teamId? }`.\n * On failure, replies with 401.\n */\n const authenticate = async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n try {\n const protocol = request.protocol ?? 'http';\n const host = request.hostname ?? 'localhost';\n const headers = buildHeaders(request);\n\n // 1. Get session — prefer direct API (avoids HTTP round-trip)\n let sessionData: { user: Record<string, unknown>; session: Record<string, unknown> } | null = null;\n\n sessionData = await tryDirectGetSession(auth, headers);\n\n if (!sessionData) {\n // Fallback: HTTP round-trip via auth.handler\n const sessionUrl = `${protocol}://${host}${normalizedBase}/get-session`;\n const sessionRequest = new Request(sessionUrl, { method: 'GET', headers });\n const sessionResponse = await auth.handler(sessionRequest);\n\n if (!sessionResponse.ok) {\n reply.code(401).send({\n success: false,\n error: 'Unauthorized',\n message: 'Invalid or expired session',\n });\n return;\n }\n\n sessionData = await sessionResponse.json() as {\n user: Record<string, unknown>;\n session: Record<string, unknown>;\n };\n }\n\n if (!sessionData?.user) {\n reply.code(401).send({\n success: false,\n error: 'Unauthorized',\n message: 'No active session',\n });\n return;\n }\n\n // Attach user and session to request\n const req = request as unknown as Record<string, unknown>;\n req.user = sessionData.user;\n req.session = sessionData.session;\n\n // 2. Set default scope for authenticated users\n req.scope = AUTHENTICATED_SCOPE;\n\n // 3. Org context bridge (when enabled)\n if (orgEnabled) {\n const session = sessionData.session as Record<string, unknown> | undefined;\n // Prefer session's activeOrganizationId; fall back to x-organization-id header\n // (needed for API key auth where sessions don't carry org context)\n const activeOrgId = (session?.activeOrganizationId as string | undefined)\n || (request.headers['x-organization-id'] as string | undefined);\n\n if (activeOrgId) {\n // Try direct API first (session-based), then explicit org lookup, then HTTP fallback\n let orgRoles = await tryDirectGetActiveMember(auth, headers);\n if (!orgRoles) {\n orgRoles = await tryDirectGetMemberRole(auth, headers, activeOrgId);\n }\n if (!orgRoles) {\n orgRoles = await resolveOrgRoles(auth, protocol, host, normalizedBase, headers, activeOrgId);\n }\n\n if (orgRoles) {\n // Valid membership — set member scope\n const scope: RequestScope = {\n kind: 'member',\n organizationId: activeOrgId,\n orgRoles,\n };\n\n // Team context bridge: validate activeTeamId belongs to current org\n const activeTeamId = session?.activeTeamId as string | undefined;\n if (activeTeamId) {\n // Try direct API first\n let teams = await tryDirectListTeams(auth, headers);\n\n if (!teams) {\n // Fallback: HTTP round-trip\n const teamsUrl = `${protocol}://${host}${normalizedBase}/organization/list-teams`;\n const teamsRequest = new Request(teamsUrl, { method: 'GET', headers });\n const teamsResponse = await auth.handler(teamsRequest);\n\n if (teamsResponse.ok) {\n const teamsData = await teamsResponse.json();\n teams = Array.isArray(teamsData) ? teamsData : (teamsData as any)?.teams ?? [];\n }\n }\n\n if (teams && teams.some((t: any) => t.id === activeTeamId)) {\n scope.teamId = activeTeamId;\n }\n }\n\n req.scope = scope;\n }\n // No membership → scope stays 'authenticated'.\n // Elevation (if needed) is handled by the elevation plugin.\n }\n // No activeOrgId → scope stays 'authenticated'.\n }\n } catch (err) {\n // Don't leak internal error details to clients unless explicitly opted-in\n const message = exposeAuthErrors\n ? (err instanceof Error ? err.message : String(err))\n : 'Authentication required';\n\n reply.code(401).send({\n success: false,\n error: 'Unauthorized',\n message,\n });\n }\n };\n\n // ========================================\n // Optional Authenticate preHandler\n // ========================================\n\n /**\n * Silently resolves session without failing.\n * Populates request.user + request.scope if a valid session exists.\n * On failure or missing session, continues as unauthenticated (scope stays 'public').\n *\n * Used by allowPublic() routes so downstream middleware (e.g. multiTenant\n * flexible filter) can apply org-scoped queries when a user IS authenticated.\n */\n const optionalAuthenticate = async (request: FastifyRequest, _reply: FastifyReply): Promise<void> => {\n try {\n const headers = buildHeaders(request);\n\n // Try direct API first, fall back to HTTP\n let sessionData: { user: Record<string, unknown>; session: Record<string, unknown> } | null = null;\n\n sessionData = await tryDirectGetSession(auth, headers);\n\n if (!sessionData) {\n const protocol = request.protocol ?? 'http';\n const host = request.hostname ?? 'localhost';\n const sessionUrl = `${protocol}://${host}${normalizedBase}/get-session`;\n const sessionRequest = new Request(sessionUrl, { method: 'GET', headers });\n const sessionResponse = await auth.handler(sessionRequest);\n\n if (sessionResponse.ok) {\n sessionData = await sessionResponse.json() as {\n user: Record<string, unknown>;\n session: Record<string, unknown>;\n };\n }\n }\n\n if (!sessionData?.user) return; // No session — continue as unauthenticated\n\n // Attach user and session to request\n const req = request as unknown as Record<string, unknown>;\n req.user = sessionData.user;\n req.session = sessionData.session;\n\n // Set scope for authenticated users\n req.scope = AUTHENTICATED_SCOPE;\n\n // Org context bridge (when enabled)\n if (orgEnabled) {\n const session = sessionData.session as Record<string, unknown> | undefined;\n // Prefer session's activeOrganizationId; fall back to x-organization-id header\n // (needed for API key auth where sessions don't carry org context)\n const activeOrgId = (session?.activeOrganizationId as string | undefined)\n || (request.headers['x-organization-id'] as string | undefined);\n\n if (activeOrgId) {\n let orgRoles = await tryDirectGetActiveMember(auth, headers);\n if (!orgRoles) {\n orgRoles = await tryDirectGetMemberRole(auth, headers, activeOrgId);\n }\n if (!orgRoles) {\n const protocol = request.protocol ?? 'http';\n const host = request.hostname ?? 'localhost';\n orgRoles = await resolveOrgRoles(auth, protocol, host, normalizedBase, headers, activeOrgId);\n }\n\n if (orgRoles) {\n req.scope = {\n kind: 'member',\n organizationId: activeOrgId,\n orgRoles,\n } satisfies RequestScope;\n }\n }\n }\n } catch {\n // Silently ignore — invalid/expired session = treat as unauthenticated\n }\n };\n\n // ========================================\n // OpenAPI Extraction (synchronous — no dynamic import)\n // ========================================\n\n let extractedOpenApi: ExternalOpenApiPaths | undefined;\n\n if (openapiOpt === false) {\n // User explicitly disabled OpenAPI for auth routes\n extractedOpenApi = undefined;\n } else if (typeof openapiOpt === 'object') {\n // User provided a manual spec override\n extractedOpenApi = openapiOpt;\n }\n // Note: auto-extraction from auth.api is deferred to plugin registration\n // (async context) to avoid making createBetterAuthAdapter async.\n\n // ========================================\n // Fastify Plugin\n // ========================================\n\n const betterAuthPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {\n // Register catch-all route for Better Auth endpoints\n fastify.all(`${normalizedBase}/*`, async (request: FastifyRequest, reply: FastifyReply) => {\n try {\n const fetchRequest = toFetchRequest(request);\n const fetchResponse = await auth.handler(fetchRequest);\n await sendFetchResponse(fetchResponse, reply);\n } catch (err) {\n // Throw ArcError so the centralized errorHandlerPlugin handles it\n // with consistent envelope, logging, requestId, stack traces, etc.\n throw new ArcError('Authentication service error', {\n code: 'AUTH_SERVICE_ERROR',\n statusCode: 500,\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n }\n });\n\n // Decorate fastify with authenticate functions\n if (!fastify.hasDecorator('authenticate')) {\n fastify.decorate('authenticate', authenticate);\n }\n if (!fastify.hasDecorator('optionalAuthenticate')) {\n fastify.decorate('optionalAuthenticate', optionalAuthenticate);\n }\n\n // Auto-extract OpenAPI from auth.api if not already set\n if (!extractedOpenApi && openapiOpt !== false && auth.api && typeof auth.api === 'object') {\n const { extractBetterAuthOpenApi } = await import('./betterAuthOpenApi.js');\n extractedOpenApi = extractBetterAuthOpenApi(auth.api as Record<string, unknown>, {\n basePath,\n userFields,\n });\n }\n\n // Push extracted OpenAPI paths to arc core (if available)\n if (extractedOpenApi) {\n const arc = (fastify as unknown as { arc?: { externalOpenApiPaths?: ExternalOpenApiPaths[] } }).arc;\n if (arc?.externalOpenApiPaths) {\n arc.externalOpenApiPaths.push(extractedOpenApi);\n }\n }\n\n fastify.log.debug(`Better Auth: Routes registered at ${normalizedBase}/*`);\n };\n\n // Wrap with fastify-plugin for encapsulation transparency\n const plugin = fp(betterAuthPlugin, {\n name: 'arc-better-auth',\n fastify: '5.x',\n }) as FastifyPluginAsync;\n\n return {\n plugin,\n authenticate,\n optionalAuthenticate,\n permissions: {\n requireOrgRole: (...roles: string[]) => requireOrgRole(roles),\n requireOrgMembership: () => requireOrgMembership(),\n requireTeamMembership: () => requireTeamMembership(),\n },\n openapi: extractedOpenApi,\n };\n}\n","/**\n * Session Management for Arc\n *\n * Lightweight cookie-based session manager that coexists with JWT and Better Auth.\n * Users pick their auth strategy — this is one option alongside authPlugin and\n * createBetterAuthAdapter.\n *\n * Features:\n * - Cookie-based session tokens (HMAC-signed)\n * - Session refresh with throttling (updateAge)\n * - Fresh session concept for sensitive operations (freshAge)\n * - Session revocation (single, all, all-except-current)\n * - Pluggable session stores (Memory, Redis, etc.)\n *\n * @example\n * ```typescript\n * import { createSessionManager, MemorySessionStore } from '@classytic/arc/auth';\n *\n * const sessions = createSessionManager({\n * store: new MemorySessionStore(),\n * secret: process.env.SESSION_SECRET,\n * maxAge: 7 * 24 * 60 * 60, // 7 days\n * updateAge: 24 * 60 * 60, // refresh every 24h\n * freshAge: 10 * 60, // 10 min for sensitive ops\n * });\n *\n * // Register plugin\n * await fastify.register(sessions.plugin);\n *\n * // Protect sensitive routes\n * fastify.post('/change-password', {\n * preHandler: [fastify.authenticate, sessions.requireFresh],\n * }, handler);\n * ```\n */\n\nimport { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';\nimport fp from 'fastify-plugin';\nimport type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Session data stored in the session store.\n */\nexport interface SessionData {\n /** User ID associated with this session */\n userId: string;\n /** Timestamp (ms since epoch) when session was created */\n createdAt: number;\n /** Timestamp (ms since epoch) when session was last refreshed */\n updatedAt: number;\n /** Timestamp (ms since epoch) when session expires */\n expiresAt: number;\n /** Optional metadata attached to the session */\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Session store interface.\n * Implement this for custom storage backends (Redis, database, etc.).\n */\nexport interface SessionStore {\n /** Retrieve a session by ID. Returns null if not found or expired. */\n get(sessionId: string): Promise<SessionData | null>;\n /** Create or update a session. */\n set(sessionId: string, data: SessionData): Promise<void>;\n /** Delete a single session. */\n delete(sessionId: string): Promise<void>;\n /** Delete all sessions for a user. */\n deleteAll(userId: string): Promise<void>;\n /** Delete all sessions for a user except the specified one. */\n deleteAllExcept(userId: string, currentSessionId: string): Promise<void>;\n}\n\n/**\n * Cookie configuration options.\n */\nexport interface SessionCookieOptions {\n /** Send cookie only over HTTPS (default: true in production) */\n secure?: boolean;\n /** Prevent client-side JavaScript access (default: true) */\n httpOnly?: boolean;\n /** SameSite attribute (default: 'lax') */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Cookie path (default: '/') */\n path?: string;\n /** Cookie domain */\n domain?: string;\n}\n\n/**\n * Options for creating a session manager.\n */\nexport interface SessionManagerOptions {\n /** Session store implementation */\n store: SessionStore;\n /** Secret for signing session cookies (min 32 characters) */\n secret: string;\n /** Session max age in seconds (default: 604800 = 7 days) */\n maxAge?: number;\n /** Minimum interval between session updates in seconds (default: 86400 = 24h) */\n updateAge?: number;\n /** Time in seconds after which a session is no longer \"fresh\" (default: 600 = 10 min) */\n freshAge?: number;\n /** Cookie name (default: 'arc.session') */\n cookieName?: string;\n /** Cookie options */\n cookie?: SessionCookieOptions;\n}\n\n/**\n * Return type from createSessionManager.\n */\nexport interface SessionManagerResult {\n /** Fastify plugin that adds session middleware */\n plugin: FastifyPluginAsync;\n /** PreHandler that rejects requests without a fresh session */\n requireFresh: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;\n}\n\n// ============================================================================\n// Fastify Type Extensions\n// ============================================================================\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n /** Authenticate middleware — validates session and sets request.user */\n authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;\n /** Session management helpers */\n sessionManager: {\n /** Create a new session for a user */\n createSession: (\n userId: string,\n metadata?: Record<string, unknown>,\n ) => Promise<{ sessionId: string; cookie: string }>;\n /** Revoke a specific session */\n revokeSession: (sessionId: string) => Promise<void>;\n /** Revoke all sessions for a user */\n revokeAllSessions: (userId: string) => Promise<void>;\n /** Revoke all sessions except the current one */\n revokeOtherSessions: (userId: string, currentSessionId: string) => Promise<void>;\n /** Refresh a session (reset updatedAt, extend expiry if needed) */\n refreshSession: (sessionId: string) => Promise<SessionData | null>;\n };\n }\n\n interface FastifyRequest {\n /** Current session data (set by session plugin) */\n session?: SessionData & { id: string };\n }\n}\n\n// ============================================================================\n// Cookie Helpers\n// ============================================================================\n\n/**\n * Sign a session ID using HMAC-SHA256.\n * Returns `sessionId.signature` format.\n */\nfunction signSessionId(sessionId: string, secret: string): string {\n const signature = createHmac('sha256', secret)\n .update(sessionId)\n .digest('base64url');\n return `${sessionId}.${signature}`;\n}\n\n/**\n * Verify and extract session ID from a signed cookie value.\n * Returns the session ID if valid, null otherwise.\n */\nfunction verifySessionId(signedValue: string, secret: string): string | null {\n const lastDotIndex = signedValue.lastIndexOf('.');\n if (lastDotIndex === -1) return null;\n\n const sessionId = signedValue.slice(0, lastDotIndex);\n const signature = signedValue.slice(lastDotIndex + 1);\n\n if (!sessionId || !signature) return null;\n\n const expectedSignature = createHmac('sha256', secret)\n .update(sessionId)\n .digest('base64url');\n\n // Constant-time comparison to prevent timing attacks\n const sigBuf = Buffer.from(signature);\n const expectedBuf = Buffer.from(expectedSignature);\n if (sigBuf.length !== expectedBuf.length) return null;\n\n return timingSafeEqual(sigBuf, expectedBuf) ? sessionId : null;\n}\n\n/**\n * Parse cookies from a Cookie header string.\n * Returns a map of cookie name to value.\n */\nfunction parseCookies(header: string | undefined): Map<string, string> {\n const cookies = new Map<string, string>();\n if (!header) return cookies;\n\n const pairs = header.split(';');\n for (const pair of pairs) {\n const eqIndex = pair.indexOf('=');\n if (eqIndex === -1) continue;\n\n const name = pair.slice(0, eqIndex).trim();\n const value = pair.slice(eqIndex + 1).trim();\n if (name) {\n try {\n cookies.set(name, decodeURIComponent(value));\n } catch {\n // Malformed percent-encoding — use raw value rather than crashing\n cookies.set(name, value);\n }\n }\n }\n\n return cookies;\n}\n\n/**\n * Build a Set-Cookie header value.\n */\nfunction buildSetCookieHeader(\n name: string,\n value: string,\n maxAgeSeconds: number,\n options: SessionCookieOptions,\n): string {\n const parts = [\n `${name}=${encodeURIComponent(value)}`,\n `Max-Age=${maxAgeSeconds}`,\n `Path=${options.path ?? '/'}`,\n ];\n\n if (options.httpOnly !== false) {\n parts.push('HttpOnly');\n }\n\n if (options.secure ?? (process.env.NODE_ENV === 'production')) {\n parts.push('Secure');\n }\n\n parts.push(`SameSite=${capitalize(options.sameSite ?? 'lax')}`);\n\n if (options.domain) {\n parts.push(`Domain=${options.domain}`);\n }\n\n return parts.join('; ');\n}\n\n/**\n * Build a Set-Cookie header that clears (expires) the cookie.\n */\nfunction buildClearCookieHeader(name: string, options: SessionCookieOptions): string {\n return buildSetCookieHeader(name, '', 0, options);\n}\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\n// ============================================================================\n// MemorySessionStore\n// ============================================================================\n\nexport interface MemorySessionStoreOptions {\n /** Cleanup interval in milliseconds (default: 60000 = 1 min) */\n cleanupIntervalMs?: number;\n}\n\n/**\n * In-memory session store for development and single-instance deployments.\n * NOT suitable for multi-instance/clustered deployments — use Redis or similar.\n */\nexport class MemorySessionStore implements SessionStore {\n private sessions: Map<string, SessionData> = new Map();\n /** Reverse index: userId -> Set<sessionId> for efficient bulk operations */\n private userIndex: Map<string, Set<string>> = new Map();\n private cleanupInterval: ReturnType<typeof setInterval> | null = null;\n\n constructor(options: MemorySessionStoreOptions = {}) {\n const intervalMs = options.cleanupIntervalMs ?? 60_000;\n this.cleanupInterval = setInterval(() => {\n this.cleanup();\n }, intervalMs);\n\n // Don't keep Node process alive just for cleanup\n if (this.cleanupInterval.unref) {\n this.cleanupInterval.unref();\n }\n }\n\n async get(sessionId: string): Promise<SessionData | null> {\n const session = this.sessions.get(sessionId);\n if (!session) return null;\n\n // Check expiration\n if (Date.now() > session.expiresAt) {\n await this.delete(sessionId);\n return null;\n }\n\n return session;\n }\n\n async set(sessionId: string, data: SessionData): Promise<void> {\n this.sessions.set(sessionId, data);\n\n // Update user index\n let userSessions = this.userIndex.get(data.userId);\n if (!userSessions) {\n userSessions = new Set();\n this.userIndex.set(data.userId, userSessions);\n }\n userSessions.add(sessionId);\n }\n\n async delete(sessionId: string): Promise<void> {\n const session = this.sessions.get(sessionId);\n if (session) {\n // Clean up user index\n const userSessions = this.userIndex.get(session.userId);\n if (userSessions) {\n userSessions.delete(sessionId);\n if (userSessions.size === 0) {\n this.userIndex.delete(session.userId);\n }\n }\n }\n this.sessions.delete(sessionId);\n }\n\n async deleteAll(userId: string): Promise<void> {\n const userSessions = this.userIndex.get(userId);\n if (!userSessions) return;\n\n for (const sessionId of userSessions) {\n this.sessions.delete(sessionId);\n }\n this.userIndex.delete(userId);\n }\n\n async deleteAllExcept(userId: string, currentSessionId: string): Promise<void> {\n const userSessions = this.userIndex.get(userId);\n if (!userSessions) return;\n\n for (const sessionId of userSessions) {\n if (sessionId !== currentSessionId) {\n this.sessions.delete(sessionId);\n }\n }\n\n // Rebuild the set with only the current session\n if (userSessions.has(currentSessionId)) {\n this.userIndex.set(userId, new Set([currentSessionId]));\n } else {\n this.userIndex.delete(userId);\n }\n }\n\n /**\n * Close the store and clean up resources.\n */\n close(): void {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n this.sessions.clear();\n this.userIndex.clear();\n }\n\n /**\n * Get current stats (for debugging/monitoring).\n */\n getStats(): { sessions: number; users: number } {\n return {\n sessions: this.sessions.size,\n users: this.userIndex.size,\n };\n }\n\n /**\n * Remove expired sessions.\n */\n private cleanup(): void {\n const now = Date.now();\n\n for (const [sessionId, session] of this.sessions) {\n if (now > session.expiresAt) {\n // Clean up user index\n const userSessions = this.userIndex.get(session.userId);\n if (userSessions) {\n userSessions.delete(sessionId);\n if (userSessions.size === 0) {\n this.userIndex.delete(session.userId);\n }\n }\n this.sessions.delete(sessionId);\n }\n }\n }\n}\n\n// ============================================================================\n// Session Manager Factory\n// ============================================================================\n\n/**\n * Create a session manager for Arc.\n *\n * Returns a Fastify plugin and a `requireFresh` preHandler.\n *\n * The plugin:\n * - Parses session cookie on each request\n * - Validates session against the store\n * - Sets `request.user` and `request.session` from session data\n * - Refreshes session token if older than `updateAge`\n * - Provides `fastify.authenticate` decorator\n * - Provides `fastify.sessionManager` decorator for session CRUD\n *\n * @example\n * ```typescript\n * import { createSessionManager, MemorySessionStore } from '@classytic/arc/auth';\n *\n * const sessions = createSessionManager({\n * store: new MemorySessionStore(),\n * secret: process.env.SESSION_SECRET!,\n * maxAge: 7 * 24 * 60 * 60,\n * updateAge: 24 * 60 * 60,\n * freshAge: 10 * 60,\n * });\n *\n * await fastify.register(sessions.plugin);\n *\n * // Login route\n * fastify.post('/login', async (request, reply) => {\n * const user = await authenticateUser(request.body);\n * const { cookie } = await fastify.sessionManager.createSession(user.id);\n * reply.header('Set-Cookie', cookie);\n * return { success: true, user };\n * });\n *\n * // Protected route\n * fastify.get('/me', {\n * preHandler: [fastify.authenticate],\n * }, async (request) => {\n * return { user: request.user };\n * });\n *\n * // Sensitive route (requires fresh session)\n * fastify.post('/change-password', {\n * preHandler: [fastify.authenticate, sessions.requireFresh],\n * }, handler);\n * ```\n */\nexport function createSessionManager(options: SessionManagerOptions): SessionManagerResult {\n const {\n store,\n secret,\n maxAge: maxAgeSeconds = 7 * 24 * 60 * 60, // 7 days\n updateAge: updateAgeSeconds = 24 * 60 * 60, // 24 hours\n freshAge: freshAgeSeconds = 10 * 60, // 10 minutes\n cookieName = 'arc.session',\n cookie: cookieOptions = {},\n } = options;\n\n // Validate secret strength\n if (secret.length < 32) {\n throw new Error(\n `Session secret must be at least 32 characters (current: ${secret.length}). ` +\n 'Use a strong random secret for production.',\n );\n }\n\n // Convert to milliseconds for internal use\n const maxAgeMs = maxAgeSeconds * 1000;\n const updateAgeMs = updateAgeSeconds * 1000;\n const freshAgeMs = freshAgeSeconds * 1000;\n\n // ========================================\n // Internal Helpers\n // ========================================\n\n /**\n * Create a new session and return the signed cookie value.\n */\n async function createSession(\n userId: string,\n metadata?: Record<string, unknown>,\n ): Promise<{ sessionId: string; cookie: string }> {\n const sessionId = randomUUID();\n const now = Date.now();\n\n const sessionData: SessionData = {\n userId,\n createdAt: now,\n updatedAt: now,\n expiresAt: now + maxAgeMs,\n metadata,\n };\n\n await store.set(sessionId, sessionData);\n\n const signedId = signSessionId(sessionId, secret);\n const cookie = buildSetCookieHeader(cookieName, signedId, maxAgeSeconds, cookieOptions);\n\n return { sessionId, cookie };\n }\n\n /**\n * Refresh a session: update the updatedAt timestamp and optionally extend expiry.\n */\n async function refreshSession(sessionId: string): Promise<SessionData | null> {\n const session = await store.get(sessionId);\n if (!session) return null;\n\n const now = Date.now();\n const updatedSession: SessionData = {\n ...session,\n updatedAt: now,\n // Extend expiry from now if less than maxAge remaining\n expiresAt: Math.max(session.expiresAt, now + maxAgeMs),\n };\n\n await store.set(sessionId, updatedSession);\n return updatedSession;\n }\n\n // ========================================\n // requireFresh preHandler\n // ========================================\n\n /**\n * PreHandler that rejects requests if the session is not \"fresh\".\n * A session is fresh if it was last updated within `freshAge` seconds.\n * Use this for sensitive operations like password changes, email changes, etc.\n */\n const requireFresh = async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n const session = request.session;\n\n if (!session) {\n reply.code(401).send({\n success: false,\n error: 'Unauthorized',\n message: 'Authentication required',\n });\n return;\n }\n\n const elapsed = Date.now() - session.updatedAt;\n if (elapsed > freshAgeMs) {\n reply.code(403).send({\n success: false,\n error: 'SessionNotFresh',\n message: 'Session is not fresh. Please re-authenticate to perform this action.',\n code: 'SESSION_NOT_FRESH',\n });\n return;\n }\n };\n\n // ========================================\n // Fastify Plugin\n // ========================================\n\n const sessionPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {\n // ---- authenticate decorator ----\n\n const authenticate = async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n // Parse cookies from header\n const cookieHeader = request.headers.cookie;\n const cookies = parseCookies(\n typeof cookieHeader === 'string' ? cookieHeader : undefined,\n );\n\n const signedValue = cookies.get(cookieName);\n if (!signedValue) {\n reply.code(401).send({\n success: false,\n error: 'Unauthorized',\n message: 'No session cookie',\n });\n return;\n }\n\n // Verify signature\n const sessionId = verifySessionId(signedValue, secret);\n if (!sessionId) {\n // Tampered or invalid cookie — clear it\n reply.header('Set-Cookie', buildClearCookieHeader(cookieName, cookieOptions));\n reply.code(401).send({\n success: false,\n error: 'Unauthorized',\n message: 'Invalid session',\n });\n return;\n }\n\n // Load session from store\n const session = await store.get(sessionId);\n if (!session) {\n // Session deleted or expired — clear cookie\n reply.header('Set-Cookie', buildClearCookieHeader(cookieName, cookieOptions));\n reply.code(401).send({\n success: false,\n error: 'Unauthorized',\n message: 'Session expired or revoked',\n });\n return;\n }\n\n // Check expiration (belt-and-suspenders, store should handle this too)\n if (Date.now() > session.expiresAt) {\n await store.delete(sessionId);\n reply.header('Set-Cookie', buildClearCookieHeader(cookieName, cookieOptions));\n reply.code(401).send({\n success: false,\n error: 'Unauthorized',\n message: 'Session expired',\n });\n return;\n }\n\n // Set user and session on request\n (request as unknown as Record<string, unknown>).user = {\n id: session.userId,\n ...session.metadata,\n };\n (request as unknown as Record<string, unknown>).session = {\n ...session,\n id: sessionId,\n };\n\n // Throttled session refresh: only update if older than updateAge\n const timeSinceUpdate = Date.now() - session.updatedAt;\n if (timeSinceUpdate > updateAgeMs) {\n const updatedSession = await refreshSession(sessionId);\n if (updatedSession) {\n // Re-sign and send updated cookie\n const signedId = signSessionId(sessionId, secret);\n const newCookie = buildSetCookieHeader(\n cookieName,\n signedId,\n maxAgeSeconds,\n cookieOptions,\n );\n reply.header('Set-Cookie', newCookie);\n\n // Update the session on request with refreshed data\n (request as unknown as Record<string, unknown>).session = {\n ...updatedSession,\n id: sessionId,\n };\n }\n }\n };\n\n // ---- Decorate fastify ----\n\n if (!fastify.hasDecorator('authenticate')) {\n fastify.decorate('authenticate', authenticate);\n }\n\n fastify.decorate('sessionManager', {\n createSession,\n revokeSession: (sessionId: string) => store.delete(sessionId),\n revokeAllSessions: (userId: string) => store.deleteAll(userId),\n revokeOtherSessions: (userId: string, currentSessionId: string) =>\n store.deleteAllExcept(userId, currentSessionId),\n refreshSession,\n });\n\n fastify.log.debug(\n `Session: Plugin registered (cookieName=${cookieName}, maxAge=${maxAgeSeconds}s, updateAge=${updateAgeSeconds}s, freshAge=${freshAgeSeconds}s)`,\n );\n };\n\n // Wrap with fastify-plugin for encapsulation transparency\n const plugin = fp(sessionPlugin, {\n name: 'arc-session',\n fastify: '5.x',\n }) as FastifyPluginAsync;\n\n return { plugin, requireFresh };\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis-session.d.mts","names":[],"sources":["../../src/auth/redis-session.ts"],"mappings":";;;;UAiCiB,SAAA;EACf,GAAA,CAAI,GAAA,WAAc,OAAA;EAClB,GAAA,CAAI,GAAA,UAAa,KAAA,aAAkB,IAAA,cAAkB,OAAA;EACrD,GAAA,IAAO,IAAA,aAAiB,OAAA;EACxB,QAAA,CAAS,GAAA,WAAc,OAAA;EACvB,IAAA,CAAK,GAAA,aAAgB,OAAA,aAAoB,OAAA;EACzC,IAAA,CAAK,GAAA,aAAgB,OAAA,aAAoB,OAAA;EACzC,MAAA,CAAO,GAAA,UAAa,OAAA,WAAkB,OAAA;AAAA;AAAA,UAGvB,wBAAA;EAHf;EAKA,KAAA,EAAO,SAAA;EALa;EAOpB,MAAA;EAP6C;EAS7C,UAAA;AAAA;;;;;;;;;;cAgBW,iBAAA,YAA6B,YAAA;EAAA,QAChC,KAAA;EAAA,QACA,MAAA;EAAA,QACA,UAAA;cAEI,OAAA,EAAS,wBAAA;EAMf,GAAA,CAAI,SAAA,WAAoB,OAAA,CAAQ,WAAA;EAsBhC,GAAA,CAAI,SAAA,UAAmB,IAAA,EAAM,WAAA,GAAc,OAAA;EAiB3C,MAAA,CAAO,SAAA,WAAoB,OAAA;EAe3B,SAAA,CAAU,MAAA,WAAiB,OAAA;EAc3B,eAAA,CAAgB,MAAA,UAAgB,gBAAA,WAA2B,OAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis-session.mjs","names":[],"sources":["../../src/auth/redis-session.ts"],"sourcesContent":["/**\n * Redis Session Store for Arc\n *\n * Implements the SessionStore interface using Redis for distributed session storage.\n * Use this in multi-instance/clustered deployments where MemorySessionStore won't work.\n *\n * This is a SEPARATE subpath import — only loaded when explicitly used:\n * import { RedisSessionStore } from '@classytic/arc/auth/redis';\n *\n * @example\n * ```typescript\n * import { createSessionManager } from '@classytic/arc/auth';\n * import { RedisSessionStore } from '@classytic/arc/auth/redis';\n * import Redis from 'ioredis';\n *\n * const redis = new Redis(process.env.REDIS_URL);\n *\n * const sessions = createSessionManager({\n * store: new RedisSessionStore({ redis }),\n * secret: process.env.SESSION_SECRET!,\n * });\n *\n * await fastify.register(sessions.plugin);\n * ```\n */\n\nimport type { SessionData, SessionStore } from './sessionManager.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/** Minimal Redis client interface — compatible with ioredis */\nexport interface RedisLike {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, ...args: unknown[]): Promise<unknown>;\n del(...keys: string[]): Promise<number>;\n smembers(key: string): Promise<string[]>;\n sadd(key: string, ...members: string[]): Promise<number>;\n srem(key: string, ...members: string[]): Promise<number>;\n expire(key: string, seconds: number): Promise<number>;\n}\n\nexport interface RedisSessionStoreOptions {\n /** Redis client instance (ioredis or compatible) */\n redis: RedisLike;\n /** Key prefix for session keys (default: 'arc:session:') */\n prefix?: string;\n /** Key prefix for user-to-sessions index (default: 'arc:user-sessions:') */\n userPrefix?: string;\n}\n\n// ============================================================================\n// RedisSessionStore\n// ============================================================================\n\n/**\n * Redis-backed session store for distributed deployments.\n *\n * Uses two key patterns:\n * - `{prefix}{sessionId}` — stores serialized SessionData with TTL\n * - `{userPrefix}{userId}` — Redis Set of sessionIds for bulk operations\n *\n * Session expiration is handled by Redis TTL — no cleanup interval needed.\n */\nexport class RedisSessionStore implements SessionStore {\n private redis: RedisLike;\n private prefix: string;\n private userPrefix: string;\n\n constructor(options: RedisSessionStoreOptions) {\n this.redis = options.redis;\n this.prefix = options.prefix ?? 'arc:session:';\n this.userPrefix = options.userPrefix ?? 'arc:user-sessions:';\n }\n\n async get(sessionId: string): Promise<SessionData | null> {\n const raw = await this.redis.get(this.prefix + sessionId);\n if (!raw) return null;\n\n let session: SessionData;\n try {\n session = JSON.parse(raw) as SessionData;\n } catch {\n // Corrupted data — clean up\n await this.delete(sessionId);\n return null;\n }\n\n // Belt-and-suspenders expiration check (Redis TTL should handle this)\n if (Date.now() > session.expiresAt) {\n await this.delete(sessionId);\n return null;\n }\n\n return session;\n }\n\n async set(sessionId: string, data: SessionData): Promise<void> {\n const ttlMs = data.expiresAt - Date.now();\n if (ttlMs <= 0) return; // Already expired, don't store\n\n const ttlSeconds = Math.ceil(ttlMs / 1000);\n const serialized = JSON.stringify(data);\n\n // Store session with TTL\n await this.redis.set(this.prefix + sessionId, serialized, 'EX', ttlSeconds);\n\n // Add to user index set (with generous TTL)\n const userKey = this.userPrefix + data.userId;\n await this.redis.sadd(userKey, sessionId);\n // Set TTL on user index slightly longer than session TTL\n await this.redis.expire(userKey, ttlSeconds + 3600);\n }\n\n async delete(sessionId: string): Promise<void> {\n // Get session first to clean up user index\n const raw = await this.redis.get(this.prefix + sessionId);\n if (raw) {\n try {\n const session = JSON.parse(raw) as SessionData;\n await this.redis.srem(this.userPrefix + session.userId, sessionId);\n } catch {\n // Best effort — session data may be corrupted\n }\n }\n\n await this.redis.del(this.prefix + sessionId);\n }\n\n async deleteAll(userId: string): Promise<void> {\n const userKey = this.userPrefix + userId;\n const sessionIds = await this.redis.smembers(userKey);\n\n if (sessionIds.length > 0) {\n // Delete all session keys\n const keys = sessionIds.map((id) => this.prefix + id);\n await this.redis.del(...keys);\n }\n\n // Delete the user index\n await this.redis.del(userKey);\n }\n\n async deleteAllExcept(userId: string, currentSessionId: string): Promise<void> {\n const userKey = this.userPrefix + userId;\n const sessionIds = await this.redis.smembers(userKey);\n\n const toDelete = sessionIds.filter((id) => id !== currentSessionId);\n if (toDelete.length > 0) {\n const keys = toDelete.map((id) => this.prefix + id);\n await this.redis.del(...keys);\n await this.redis.srem(userKey, ...toDelete);\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAiEA,IAAa,oBAAb,MAAuD;CACrD,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,YAAY,SAAmC;AAC7C,OAAK,QAAQ,QAAQ;AACrB,OAAK,SAAS,QAAQ,UAAU;AAChC,OAAK,aAAa,QAAQ,cAAc;;CAG1C,MAAM,IAAI,WAAgD;EACxD,MAAM,MAAM,MAAM,KAAK,MAAM,IAAI,KAAK,SAAS,UAAU;AACzD,MAAI,CAAC,IAAK,QAAO;EAEjB,IAAI;AACJ,MAAI;AACF,aAAU,KAAK,MAAM,IAAI;UACnB;AAEN,SAAM,KAAK,OAAO,UAAU;AAC5B,UAAO;;AAIT,MAAI,KAAK,KAAK,GAAG,QAAQ,WAAW;AAClC,SAAM,KAAK,OAAO,UAAU;AAC5B,UAAO;;AAGT,SAAO;;CAGT,MAAM,IAAI,WAAmB,MAAkC;EAC7D,MAAM,QAAQ,KAAK,YAAY,KAAK,KAAK;AACzC,MAAI,SAAS,EAAG;EAEhB,MAAM,aAAa,KAAK,KAAK,QAAQ,IAAK;EAC1C,MAAM,aAAa,KAAK,UAAU,KAAK;AAGvC,QAAM,KAAK,MAAM,IAAI,KAAK,SAAS,WAAW,YAAY,MAAM,WAAW;EAG3E,MAAM,UAAU,KAAK,aAAa,KAAK;AACvC,QAAM,KAAK,MAAM,KAAK,SAAS,UAAU;AAEzC,QAAM,KAAK,MAAM,OAAO,SAAS,aAAa,KAAK;;CAGrD,MAAM,OAAO,WAAkC;EAE7C,MAAM,MAAM,MAAM,KAAK,MAAM,IAAI,KAAK,SAAS,UAAU;AACzD,MAAI,IACF,KAAI;GACF,MAAM,UAAU,KAAK,MAAM,IAAI;AAC/B,SAAM,KAAK,MAAM,KAAK,KAAK,aAAa,QAAQ,QAAQ,UAAU;UAC5D;AAKV,QAAM,KAAK,MAAM,IAAI,KAAK,SAAS,UAAU;;CAG/C,MAAM,UAAU,QAA+B;EAC7C,MAAM,UAAU,KAAK,aAAa;EAClC,MAAM,aAAa,MAAM,KAAK,MAAM,SAAS,QAAQ;AAErD,MAAI,WAAW,SAAS,GAAG;GAEzB,MAAM,OAAO,WAAW,KAAK,OAAO,KAAK,SAAS,GAAG;AACrD,SAAM,KAAK,MAAM,IAAI,GAAG,KAAK;;AAI/B,QAAM,KAAK,MAAM,IAAI,QAAQ;;CAG/B,MAAM,gBAAgB,QAAgB,kBAAyC;EAC7E,MAAM,UAAU,KAAK,aAAa;EAGlC,MAAM,YAFa,MAAM,KAAK,MAAM,SAAS,QAAQ,EAEzB,QAAQ,OAAO,OAAO,iBAAiB;AACnE,MAAI,SAAS,SAAS,GAAG;GACvB,MAAM,OAAO,SAAS,KAAK,OAAO,KAAK,SAAS,GAAG;AACnD,SAAM,KAAK,MAAM,IAAI,GAAG,KAAK;AAC7B,SAAM,KAAK,MAAM,KAAK,SAAS,GAAG,SAAS"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"betterAuthOpenApi-BrHKeSAx.mjs","names":[],"sources":["../src/auth/betterAuthOpenApi.ts"],"sourcesContent":["/**\n * Better Auth OpenAPI Extractor\n *\n * Introspects a Better Auth instance's `api` object and extracts\n * OpenAPI-compatible path definitions from endpoint metadata.\n *\n * Schema conversion uses Zod v4's native `z.toJSONSchema()` via\n * the shared `schemaConverter` utility.\n *\n * @example\n * ```typescript\n * import { extractBetterAuthOpenApi } from '@classytic/arc/auth';\n *\n * const auth = betterAuth({ ... });\n * const openapi = extractBetterAuthOpenApi(auth.api, { basePath: '/api/auth' });\n * // openapi.paths, openapi.securitySchemes, openapi.tags\n * ```\n */\n\nimport type { ExternalOpenApiPaths } from '../docs/externalPaths.js';\nimport { toJsonSchema } from '../utils/schemaConverter.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BetterAuthOpenApiOptions {\n /** Base path prefix for auth routes (default: '/api/auth') */\n basePath?: string;\n /** Tag name for auth routes in OpenAPI (default: 'Authentication') */\n tagName?: string;\n /** Tag description */\n tagDescription?: string;\n /** Exclude specific paths from the spec (e.g. ['/ok', '/error']) */\n excludePaths?: string[];\n /** Exclude SERVER_ONLY endpoints (default: true) */\n excludeServerOnly?: boolean;\n /**\n * Additional user fields from Better Auth config.\n * These get merged into signUpEmail/updateUser request body schemas\n * and the User component schema for $ref resolution.\n *\n * Fields with `input: false` are excluded from request bodies\n * but still appear in the User component schema (output-only).\n */\n userFields?: Record<string, {\n type: string;\n description?: string;\n /** Whether this field is required in sign-up (default: false) */\n required?: boolean;\n /** Whether this field is accepted in request body (default: true). Set false for output-only fields. */\n input?: boolean;\n }>;\n}\n\n// ============================================================================\n// Better Auth Endpoint Discovery\n// ============================================================================\n\n/** Shape of a Better Auth endpoint function with metadata */\ninterface BetterAuthEndpoint {\n path: string;\n options: {\n method?: string | string[];\n body?: unknown;\n query?: unknown;\n metadata?: {\n openapi?: {\n summary?: string;\n description?: string;\n operationId?: string;\n responses?: Record<string, unknown>;\n requestBody?: {\n content: Record<string, { schema: Record<string, unknown> }>;\n required?: boolean;\n };\n tags?: string[];\n };\n SERVER_ONLY?: boolean;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n };\n}\n\n/**\n * Check if a value looks like a Better Auth endpoint (has .path and .options)\n */\nfunction isBetterAuthEndpoint(value: unknown): value is BetterAuthEndpoint {\n if (typeof value !== 'function' && typeof value !== 'object') return false;\n if (!value) return false;\n\n const v = value as Record<string, unknown>;\n return (\n typeof v.path === 'string' &&\n typeof v.options === 'object' &&\n v.options !== null\n );\n}\n\n/**\n * Convert Fastify-style path params (/:id) to OpenAPI-style (/{id})\n */\nfunction toOpenApiPath(path: string): string {\n return path.replace(/:([^/]+)/g, '{$1}');\n}\n\n/**\n * Extract path parameters from a path string\n */\nfunction extractPathParams(path: string): Array<{ name: string; in: 'path'; required: true; schema: { type: string } }> {\n const params: Array<{ name: string; in: 'path'; required: true; schema: { type: string } }> = [];\n const matches = path.matchAll(/:(\\w+)/g);\n for (const match of matches) {\n params.push({\n name: match[1]!,\n in: 'path',\n required: true,\n schema: { type: 'string' },\n });\n }\n return params;\n}\n\n/**\n * Extract OpenAPI paths from a Better Auth instance's API object.\n *\n * Walks `authApi` (the `auth.api` object from Better Auth), discovers\n * endpoints, converts their Zod schemas to JSON Schema via `z.toJSONSchema()`,\n * and returns a complete `ExternalOpenApiPaths` object ready for Arc's spec builder.\n */\nexport function extractBetterAuthOpenApi(\n authApi: Record<string, unknown>,\n options: BetterAuthOpenApiOptions = {},\n): ExternalOpenApiPaths {\n const {\n basePath = '/api/auth',\n tagName = 'Authentication',\n tagDescription = 'Better Auth authentication endpoints',\n excludePaths = [],\n excludeServerOnly = true,\n userFields,\n } = options;\n\n const normalizedBase = basePath.replace(/\\/+$/, '');\n const paths: Record<string, Record<string, unknown>> = {};\n\n // Auto-detect active plugins by inspecting available endpoints.\n // This avoids hardcoding plugin-specific schemes — the spec adapts to\n // whatever plugins the app has registered with Better Auth.\n const detectedPlugins = detectActivePlugins(authApi);\n\n // Build security options dynamically: session + bearer are always available,\n // others are added based on detected plugins.\n const securityOptions: Array<Record<string, unknown[]>> = [\n { cookieAuth: [] },\n { bearerAuth: [] },\n ];\n if (detectedPlugins.apiKey) {\n securityOptions.push({ apiKeyAuth: [] });\n }\n\n for (const [key, value] of Object.entries(authApi)) {\n if (!isBetterAuthEndpoint(value)) continue;\n\n const endpoint = value;\n const { path: endpointPath, options: endpointOpts } = endpoint;\n\n // Skip excluded paths\n if (excludePaths.includes(endpointPath)) continue;\n\n // Skip SERVER_ONLY endpoints\n if (excludeServerOnly && endpointOpts.metadata?.SERVER_ONLY) continue;\n\n // Build full path\n const fullPath = toOpenApiPath(`${normalizedBase}${endpointPath}`);\n\n // Determine HTTP method(s)\n const methods: string[] = [];\n if (endpointOpts.method) {\n if (Array.isArray(endpointOpts.method)) {\n methods.push(...endpointOpts.method.map(m => m.toLowerCase()));\n } else {\n methods.push(endpointOpts.method.toLowerCase());\n }\n } else {\n // Default: GET if no body, POST if body exists\n methods.push(endpointOpts.body ? 'post' : 'get');\n }\n\n // Extract OpenAPI metadata from endpoint\n const openApiMeta = endpointOpts.metadata?.openapi;\n\n // Build operation for each method\n for (const method of methods) {\n const operation: Record<string, unknown> = {\n tags: openApiMeta?.tags ?? [tagName],\n operationId: openApiMeta?.operationId ?? key,\n summary: openApiMeta?.summary ?? formatOperationSummary(key),\n security: securityOptions,\n };\n\n if (openApiMeta?.description) {\n operation.description = openApiMeta.description;\n }\n\n // Path parameters\n const pathParams = extractPathParams(endpointPath);\n const parameters: unknown[] = [...pathParams];\n\n // Query parameters (for GET/DELETE)\n if ((method === 'get' || method === 'delete') && endpointOpts.query) {\n const querySchema = toJsonSchema(endpointOpts.query);\n if (querySchema && querySchema.type === 'object' && querySchema.properties) {\n const props = querySchema.properties as Record<string, Record<string, unknown>>;\n const required = (querySchema.required as string[]) ?? [];\n for (const [name, prop] of Object.entries(props)) {\n const paramEntry: Record<string, unknown> = {\n name,\n in: 'query',\n required: required.includes(name),\n schema: prop,\n };\n if (prop.description) {\n paramEntry.description = prop.description;\n }\n parameters.push(paramEntry);\n }\n }\n }\n\n if (parameters.length > 0) {\n operation.parameters = parameters;\n }\n\n // Request body (for POST/PUT/PATCH)\n if (method === 'post' || method === 'put' || method === 'patch') {\n if (openApiMeta?.requestBody) {\n // Prefer metadata.openapi.requestBody (cleaner than Zod conversion)\n operation.requestBody = structuredClone(openApiMeta.requestBody);\n } else if (endpointOpts.body) {\n // Fall back to Zod body conversion\n const bodySchema = toJsonSchema(endpointOpts.body);\n if (bodySchema) {\n operation.requestBody = {\n required: true,\n content: {\n 'application/json': { schema: bodySchema },\n },\n };\n }\n }\n\n // Merge userFields into sign-up and update-user request bodies\n if (userFields && isUserFieldEndpoint(endpointPath) && operation.requestBody) {\n mergeUserFieldsIntoRequestBody(\n operation.requestBody as Record<string, unknown>,\n userFields,\n endpointPath,\n );\n }\n }\n\n // Responses\n if (openApiMeta?.responses) {\n operation.responses = openApiMeta.responses;\n } else {\n // Default responses\n operation.responses = {\n '200': { description: 'Success' },\n '400': { description: 'Bad request' },\n '401': { description: 'Unauthorized' },\n };\n }\n\n // Add to paths\n if (!paths[fullPath]) paths[fullPath] = {};\n (paths[fullPath] as Record<string, unknown>)[method] = operation;\n }\n }\n\n // Build component schemas for $ref resolution (e.g. $ref: \"#/components/schemas/User\")\n const schemas: Record<string, Record<string, unknown>> = {\n User: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n name: { type: 'string' },\n email: { type: 'string', format: 'email' },\n emailVerified: { type: 'boolean' },\n image: { type: 'string', nullable: true },\n createdAt: { type: 'string', format: 'date-time' },\n updatedAt: { type: 'string', format: 'date-time' },\n },\n },\n Session: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n userId: { type: 'string' },\n token: { type: 'string' },\n expiresAt: { type: 'string', format: 'date-time' },\n ipAddress: { type: 'string', nullable: true },\n userAgent: { type: 'string', nullable: true },\n createdAt: { type: 'string', format: 'date-time' },\n updatedAt: { type: 'string', format: 'date-time' },\n },\n },\n };\n\n // Merge userFields into User component schema (all fields, including input: false)\n if (userFields) {\n const userProps = schemas.User!.properties as Record<string, unknown>;\n for (const [name, field] of Object.entries(userFields)) {\n const prop: Record<string, unknown> = { type: field.type };\n if (field.description) prop.description = field.description;\n userProps[name] = prop;\n }\n }\n\n // Build security schemes — always include cookieAuth, add plugin-specific schemes dynamically\n const securitySchemes: Record<string, Record<string, unknown>> = {\n cookieAuth: {\n type: 'apiKey',\n in: 'cookie',\n name: 'better-auth.session_token',\n description: 'Session cookie set by Better Auth after sign-in',\n },\n };\n\n if (detectedPlugins.apiKey) {\n securitySchemes.apiKeyAuth = {\n type: 'apiKey',\n in: 'header',\n name: 'x-api-key',\n description: 'API key for programmatic access. Pass org context via x-organization-id header.',\n };\n }\n\n // Build resourceSecurity — additional auth alternatives for Arc resource paths.\n // Each item is OR'd with bearerAuth; keys within the same object are AND'd.\n const resourceSecurity: Array<Record<string, string[]>> = [];\n if (detectedPlugins.apiKey) {\n // API key requires org header for tenant context (AND = same object)\n resourceSecurity.push({ apiKeyAuth: [], orgHeader: [] });\n }\n\n return {\n paths,\n schemas,\n securitySchemes,\n tags: [{ name: tagName, description: tagDescription }],\n resourceSecurity: resourceSecurity.length > 0 ? resourceSecurity : undefined,\n };\n}\n\n// ============================================================================\n// Plugin Detection\n// ============================================================================\n\ninterface DetectedPlugins {\n apiKey: boolean;\n organization: boolean;\n}\n\n/**\n * Auto-detect active Better Auth plugins by inspecting the API object.\n *\n * Rather than hardcoding plugin-specific behavior, we check for known\n * endpoint signatures that each plugin registers. This way the OpenAPI\n * spec adapts automatically to whatever plugins the app has enabled —\n * no Arc update needed when adding/removing plugins.\n */\nfunction detectActivePlugins(authApi: Record<string, unknown>): DetectedPlugins {\n const endpointPaths = new Set<string>();\n\n for (const value of Object.values(authApi)) {\n if (isBetterAuthEndpoint(value)) {\n endpointPaths.add(value.path);\n }\n }\n\n return {\n // apiKey plugin registers /api-key/create\n apiKey: endpointPaths.has('/api-key/create'),\n // organization plugin registers /organization/create\n organization: endpointPaths.has('/organization/create'),\n };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Convert a camelCase key like 'signInEmail' to a readable summary like 'Sign in email'\n */\nfunction formatOperationSummary(key: string): string {\n return key\n .replace(/([A-Z])/g, ' $1')\n .replace(/^./, (s) => s.toUpperCase())\n .trim();\n}\n\n/**\n * Check if an endpoint path should have userFields merged into its request body.\n */\nfunction isUserFieldEndpoint(path: string): boolean {\n return path === '/sign-up/email' || path === '/update-user';\n}\n\n/**\n * Merge user-defined fields into an existing requestBody schema.\n * For updateUser, all fields are treated as optional regardless of their `required` setting.\n */\nfunction mergeUserFieldsIntoRequestBody(\n requestBody: Record<string, unknown>,\n userFields: NonNullable<BetterAuthOpenApiOptions['userFields']>,\n endpointPath: string,\n): void {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const content = (requestBody as any)?.content?.['application/json'];\n if (!content?.schema) return;\n\n const schema = content.schema as Record<string, unknown>;\n\n if (!schema.properties) schema.properties = {};\n if (!schema.required) schema.required = [];\n\n const props = schema.properties as Record<string, unknown>;\n const required = schema.required as string[];\n\n for (const [name, field] of Object.entries(userFields)) {\n // Skip input: false fields (output-only, not accepted in request body)\n if (field.input === false) continue;\n\n // For updateUser, all fields are optional\n const isRequired = endpointPath === '/update-user' ? false : (field.required ?? false);\n\n const prop: Record<string, unknown> = { type: field.type };\n if (field.description) prop.description = field.description;\n props[name] = prop;\n\n if (isRequired && !required.includes(name)) {\n required.push(name);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/cache/memory.ts","../../src/cache/redis.ts","../../src/cache/keys.ts"],"mappings":";;;;UAQiB,uBAAA;;EAEf,YAAA;EAFe;EAIf,UAAA;;EAEA,iBAAA;EAJA;;;;EASA,aAAA;EAWA;;;;;EALA,cAAA;EAiB2B;;;;EAZ3B,iBAAA;EA0CwB;EAxCxB,MAAA,GAAS,WAAA;AAAA;;;;;;;;cAUE,gBAAA,8BAA8C,UAAA,CAAW,MAAA;EAAA,SAC3D,IAAA;EAAA,iBAEQ,KAAA;EAAA,iBACA,YAAA;EAAA,iBACA,UAAA;EAAA,iBACA,aAAA;EAAA,iBACA,cAAA;EAAA,iBACA,iBAAA;EAAA,iBACA,MAAA;EAAA,iBACA,YAAA;EAAA,QAET,YAAA;EAAA,QACA,KAAA;EAAA,QACA,OAAA;EAAA,QACA,UAAA;cAEI,OAAA,GAAS,uBAAA;EAaf,GAAA,CAAI,GAAA,WAAc,OAAA,CAAQ,MAAA;EAoB1B,GAAA,CAAI,GAAA,UAAa,KAAA,EAAO,MAAA,EAAQ,OAAA,GAAS,eAAA,GAAuB,OAAA;EA0BhE,MAAA,CAAO,GAAA,WAAc,OAAA;EAKrB,KAAA,CAAA,GAAS,OAAA;EAKT,KAAA,CAAA,GAAS,OAAA;EAMf,KAAA,CAAA,GAAS,UAAA;EAAA,QAUD,WAAA;EAAA,QAKA,YAAA;EAAA,QAUA,kBAAA;EAAA,QAYA,cAAA;EAAA,QASA,YAAA;AAAA;;;UClLO,gBAAA;EACf,GAAA,CAAI,GAAA,WAAc,OAAA;EAClB,GAAA,CACE,GAAA,UACA,KAAA,UACA,OAAA;IACE,EAAA;IACA,EAAA;IACA,EAAA;IACA,EAAA;EAAA,IAED,OAAA;EACH,GAAA,CAAI,GAAA,sBAAyB,OAAA;EDA7B;;;;;ECMA,IAAA,EACE,MAAA,sBACG,IAAA,wBACF,OAAA;EDSiB;ECPpB,QAAA,KAAa,aAAA;AAAA;AAAA,UAGE,aAAA;EACf,GAAA,CAAI,GAAA;EACJ,IAAA,IAAQ,OAAA;AAAA;AAAA,UAGO,sBAAA;EDuCS;ECrCxB,MAAA,EAAQ,gBAAA;EDyDuC;ECvD/C,MAAA;EDiF2B;EC/E3B,YAAA;EDyFe;ECvFf,aAAA;AAAA;;;;;;cAQW,eAAA,8BAA6C,UAAA,CAAW,MAAA;EAAA,SAC1D,IAAA;EAAA,iBAEQ,MAAA;EAAA,iBACA,MAAA;EAAA,iBACA,YAAA;EAAA,iBACA,aAAA;EAAA,QAET,KAAA;EAAA,QACA,OAAA;cAEI,OAAA,EAAS,sBAAA;EAOf,GAAA,CAAI,GAAA,WAAc,OAAA,CAAQ,MAAA;EAiB1B,GAAA,CAAI,GAAA,UAAa,KAAA,EAAO,MAAA,EAAQ,OAAA,GAAS,eAAA,GAAuB,OAAA;EAahE,MAAA,CAAO,GAAA,WAAc,OAAA;EAIrB,KAAA,CAAA,GAAS,OAAA;;EAKT,cAAA,CAAe,MAAA,WAAiB,OAAA;EAItC,KAAA,CAAA,GAAS,UAAA;EAAA,QAUK,aAAA;EAAA,QA2BN,UAAA;AAAA;;;;;;;AD3IV;;;;iBECgB,aAAA,CACd,QAAA,UACA,SAAA,UACA,eAAA,UACA,MAAA,EAAQ,MAAA,mBACR,MAAA,WACA,KAAA;;iBASc,UAAA,CAAW,QAAA;;iBAKX,aAAA,CAAc,GAAA;;;;;;iBASd,UAAA,CAAW,MAAA,EAAQ,MAAA"}
|
package/dist/cache/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/cache/redis.ts"],"sourcesContent":["import type { CacheSetOptions, CacheStats, CacheStore } from './interface.js';\n\nexport interface RedisCacheClient {\n get(key: string): Promise<string | null>;\n set(\n key: string,\n value: string,\n options?: {\n EX?: number;\n PX?: number;\n NX?: boolean;\n XX?: boolean;\n }\n ): Promise<string | null | unknown>;\n del(key: string | string[]): Promise<number>;\n /**\n * Optional: enables prefix-based `clear()` and `deleteByPrefix()` via SCAN.\n * Compatible with both ioredis and node-redis.\n * If not provided, `clear()` is a safe no-op.\n */\n scan?(\n cursor: string | number,\n ...args: (string | number)[]\n ): Promise<[string | number, string[]]>;\n /** Optional: pipeline for batched commands (ioredis compatible) */\n pipeline?(): RedisPipeline;\n}\n\nexport interface RedisPipeline {\n del(key: string): unknown;\n exec(): Promise<unknown>;\n}\n\nexport interface RedisCacheStoreOptions {\n /** Redis client instance */\n client: RedisCacheClient;\n /** Key prefix for namespacing (default: 'arc:cache:') */\n prefix?: string;\n /** Default TTL in milliseconds (default: 60_000) */\n defaultTtlMs?: number;\n /** Maximum serialized entry size in bytes. Oversized entries are skipped. */\n maxEntryBytes?: number;\n}\n\n/**\n * Redis-backed cache store.\n * Suitable for multi-instance and horizontally scaled deployments.\n * Uses pipeline batching when available for bulk operations.\n */\nexport class RedisCacheStore<TValue = unknown> implements CacheStore<TValue> {\n readonly name = 'redis-cache';\n\n private readonly client: RedisCacheClient;\n private readonly prefix: string;\n private readonly defaultTtlMs: number;\n private readonly maxEntryBytes: number;\n\n private _hits = 0;\n private _misses = 0;\n\n constructor(options: RedisCacheStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? 'arc:cache:';\n this.defaultTtlMs = options.defaultTtlMs ?? 60_000;\n this.maxEntryBytes = options.maxEntryBytes ?? 0; // 0 = no limit\n }\n\n async get(key: string): Promise<TValue | undefined> {\n const data = await this.client.get(this.withPrefix(key));\n if (!data) {\n this._misses++;\n return undefined;\n }\n\n try {\n this._hits++;\n return JSON.parse(data) as TValue;\n } catch {\n this._misses++;\n this._hits--; // undo the hit — it's a corrupt entry\n return undefined;\n }\n }\n\n async set(key: string, value: TValue, options: CacheSetOptions = {}): Promise<void> {\n const ttlMs = options.ttlMs ?? this.defaultTtlMs;\n if (!Number.isFinite(ttlMs) || ttlMs <= 0) return;\n\n const payload = JSON.stringify(value);\n\n if (this.maxEntryBytes > 0 && Buffer.byteLength(payload, 'utf8') > this.maxEntryBytes) {\n return; // skip oversized entry\n }\n\n await this.client.set(this.withPrefix(key), payload, { PX: Math.ceil(ttlMs) });\n }\n\n async delete(key: string): Promise<void> {\n await this.client.del(this.withPrefix(key));\n }\n\n async clear(): Promise<void> {\n await this.scanAndDelete(`${this.prefix}*`);\n }\n\n /** Delete all keys matching `this.prefix + prefix + *`. Returns count deleted. */\n async deleteByPrefix(prefix: string): Promise<number> {\n return this.scanAndDelete(`${this.prefix}${prefix}*`);\n }\n\n stats(): CacheStats {\n return {\n entries: -1, // not cheaply available in Redis\n memoryBytes: -1,\n hits: this._hits,\n misses: this._misses,\n evictions: -1, // Redis handles eviction internally\n };\n }\n\n private async scanAndDelete(pattern: string): Promise<number> {\n if (!this.client.scan) return 0;\n\n const BATCH_SIZE = 200;\n let cursor: string | number = '0';\n let deleted = 0;\n\n do {\n const [nextCursor, keys] = await this.client.scan(\n cursor, 'MATCH', pattern, 'COUNT', BATCH_SIZE,\n );\n cursor = nextCursor;\n if (keys.length > 0) {\n if (this.client.pipeline) {\n const pipe = this.client.pipeline();\n for (const key of keys) pipe.del(key);\n await pipe.exec();\n } else {\n await this.client.del(keys);\n }\n deleted += keys.length;\n }\n } while (String(cursor) !== '0');\n\n return deleted;\n }\n\n private withPrefix(key: string): string {\n return `${this.prefix}${key}`;\n }\n}\n\nexport default RedisCacheStore;\n"],"mappings":";;;;;;;;;;AAiDA,IAAa,kBAAb,MAA6E;CAC3E,AAAS,OAAO;CAEhB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CAEjB,AAAQ,QAAQ;CAChB,AAAQ,UAAU;CAElB,YAAY,SAAiC;AAC3C,OAAK,SAAS,QAAQ;AACtB,OAAK,SAAS,QAAQ,UAAU;AAChC,OAAK,eAAe,QAAQ,gBAAgB;AAC5C,OAAK,gBAAgB,QAAQ,iBAAiB;;CAGhD,MAAM,IAAI,KAA0C;EAClD,MAAM,OAAO,MAAM,KAAK,OAAO,IAAI,KAAK,WAAW,IAAI,CAAC;AACxD,MAAI,CAAC,MAAM;AACT,QAAK;AACL;;AAGF,MAAI;AACF,QAAK;AACL,UAAO,KAAK,MAAM,KAAK;UACjB;AACN,QAAK;AACL,QAAK;AACL;;;CAIJ,MAAM,IAAI,KAAa,OAAe,UAA2B,EAAE,EAAiB;EAClF,MAAM,QAAQ,QAAQ,SAAS,KAAK;AACpC,MAAI,CAAC,OAAO,SAAS,MAAM,IAAI,SAAS,EAAG;EAE3C,MAAM,UAAU,KAAK,UAAU,MAAM;AAErC,MAAI,KAAK,gBAAgB,KAAK,OAAO,WAAW,SAAS,OAAO,GAAG,KAAK,cACtE;AAGF,QAAM,KAAK,OAAO,IAAI,KAAK,WAAW,IAAI,EAAE,SAAS,EAAE,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;;CAGhF,MAAM,OAAO,KAA4B;AACvC,QAAM,KAAK,OAAO,IAAI,KAAK,WAAW,IAAI,CAAC;;CAG7C,MAAM,QAAuB;AAC3B,QAAM,KAAK,cAAc,GAAG,KAAK,OAAO,GAAG;;;CAI7C,MAAM,eAAe,QAAiC;AACpD,SAAO,KAAK,cAAc,GAAG,KAAK,SAAS,OAAO,GAAG;;CAGvD,QAAoB;AAClB,SAAO;GACL,SAAS;GACT,aAAa;GACb,MAAM,KAAK;GACX,QAAQ,KAAK;GACb,WAAW;GACZ;;CAGH,MAAc,cAAc,SAAkC;AAC5D,MAAI,CAAC,KAAK,OAAO,KAAM,QAAO;EAE9B,MAAM,aAAa;EACnB,IAAI,SAA0B;EAC9B,IAAI,UAAU;AAEd,KAAG;GACD,MAAM,CAAC,YAAY,QAAQ,MAAM,KAAK,OAAO,KAC3C,QAAQ,SAAS,SAAS,SAAS,WACpC;AACD,YAAS;AACT,OAAI,KAAK,SAAS,GAAG;AACnB,QAAI,KAAK,OAAO,UAAU;KACxB,MAAM,OAAO,KAAK,OAAO,UAAU;AACnC,UAAK,MAAM,OAAO,KAAM,MAAK,IAAI,IAAI;AACrC,WAAM,KAAK,MAAM;UAEjB,OAAM,KAAK,OAAO,IAAI,KAAK;AAE7B,eAAW,KAAK;;WAEX,OAAO,OAAO,KAAK;AAE5B,SAAO;;CAGT,AAAQ,WAAW,KAAqB;AACtC,SAAO,GAAG,KAAK,SAAS"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"caching-Bl28lYsR.mjs","names":[],"sources":["../src/plugins/caching.ts"],"sourcesContent":["/**\n * Caching Plugin\n *\n * Adds ETag and Cache-Control headers to GET/HEAD responses.\n * Supports conditional requests (304 Not Modified) for bandwidth savings.\n *\n * @example\n * import { cachingPlugin } from '@classytic/arc/plugins';\n *\n * // Basic — ETag + conditional requests, no browser caching\n * await fastify.register(cachingPlugin);\n *\n * // With cache rules per path\n * await fastify.register(cachingPlugin, {\n * rules: [\n * { match: '/api/products', maxAge: 60 },\n * { match: '/api/categories', maxAge: 300, staleWhileRevalidate: 60 },\n * ],\n * });\n */\n\nimport fp from 'fastify-plugin';\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\n\nexport interface CachingRule {\n /** Path prefix to match (e.g., '/api/products') */\n match: string;\n /** Cache-Control max-age in seconds */\n maxAge: number;\n /** Cache-Control: private vs public (default: public) */\n private?: boolean;\n /** stale-while-revalidate directive in seconds */\n staleWhileRevalidate?: number;\n}\n\nexport interface CachingOptions {\n /** Default max-age in seconds for Cache-Control (default: 0 = no-cache) */\n maxAge?: number;\n /** Enable ETag generation (default: true) */\n etag?: boolean;\n /** Enable conditional requests — 304 Not Modified (default: true) */\n conditional?: boolean;\n /** HTTP methods to cache (default: ['GET', 'HEAD']) */\n methods?: string[];\n /** Paths to exclude from caching (prefix match) */\n exclude?: string[];\n /** Custom cache rules per path prefix */\n rules?: CachingRule[];\n}\n\n// ============================================================================\n// FNV-1a Hash (fast, non-cryptographic)\n// ============================================================================\n\nconst FNV_OFFSET = 2166136261;\nconst FNV_PRIME = 16777619;\n\n/** Fast non-cryptographic hash for ETag generation */\nfunction fnv1a(data: string): string {\n let hash = FNV_OFFSET;\n for (let i = 0; i < data.length; i++) {\n hash ^= data.charCodeAt(i);\n hash = (hash * FNV_PRIME) >>> 0;\n }\n return hash.toString(36);\n}\n\n// ============================================================================\n// Plugin\n// ============================================================================\n\nconst cachingPlugin: FastifyPluginAsync<CachingOptions> = async (\n fastify: FastifyInstance,\n opts: CachingOptions = {},\n) => {\n const {\n maxAge = 0,\n etag = true,\n conditional = true,\n methods = ['GET', 'HEAD'],\n exclude = [],\n rules = [],\n } = opts;\n\n const methodSet = new Set(methods.map((m) => m.toUpperCase()));\n\n /** Find the first matching rule for a URL path */\n function findRule(url: string): CachingRule | undefined {\n // Strip query string\n const path = url.split('?')[0]!;\n return rules.find((r) => path.startsWith(r.match));\n }\n\n /** Build Cache-Control header value */\n function buildCacheControl(rule?: CachingRule): string {\n const age = rule?.maxAge ?? maxAge;\n if (age <= 0) return 'no-cache';\n\n const parts: string[] = [];\n parts.push(rule?.private ? 'private' : 'public');\n parts.push(`max-age=${age}`);\n if (rule?.staleWhileRevalidate) {\n parts.push(`stale-while-revalidate=${rule.staleWhileRevalidate}`);\n }\n return parts.join(', ');\n }\n\n // onSend hook — runs just before the response is sent\n fastify.addHook('onSend', async (request, reply, payload) => {\n const url = request.url;\n\n // Skip excluded paths\n if (exclude.some((p) => url.startsWith(p))) {\n return payload;\n }\n\n const method = request.method.toUpperCase();\n\n // Mutation methods always get no-store\n if (!methodSet.has(method)) {\n if (!reply.hasHeader('cache-control')) {\n reply.header('cache-control', 'no-store');\n }\n return payload;\n }\n\n // Only cache 2xx responses\n const statusCode = reply.statusCode;\n if (statusCode < 200 || statusCode >= 300) {\n return payload;\n }\n\n // Don't override user-set Cache-Control\n if (!reply.hasHeader('cache-control')) {\n const rule = findRule(url);\n reply.header('cache-control', buildCacheControl(rule));\n }\n\n // ETag generation\n if (etag && payload) {\n const body = typeof payload === 'string' ? payload : String(payload);\n const tag = `\"${fnv1a(body)}\"`;\n reply.header('etag', tag);\n\n // Conditional request: check If-None-Match\n if (conditional) {\n const ifNoneMatch = request.headers['if-none-match'];\n if (ifNoneMatch && ifNoneMatch === tag) {\n reply.code(304);\n return '';\n }\n }\n }\n\n return payload;\n });\n\n fastify.log?.debug?.('Caching plugin registered');\n};\n\nexport default fp(cachingPlugin, {\n name: 'arc-caching',\n fastify: '5.x',\n});\n\nexport { cachingPlugin };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAM,aAAa;AACnB,MAAM,YAAY;;AAGlB,SAAS,MAAM,MAAsB;CACnC,IAAI,OAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAQ,KAAK,WAAW,EAAE;AAC1B,SAAQ,OAAO,cAAe;;AAEhC,QAAO,KAAK,SAAS,GAAG;;AAO1B,MAAM,gBAAoD,OACxD,SACA,OAAuB,EAAE,KACtB;CACH,MAAM,EACJ,SAAS,GACT,OAAO,MACP,cAAc,MACd,UAAU,CAAC,OAAO,OAAO,EACzB,UAAU,EAAE,EACZ,QAAQ,EAAE,KACR;CAEJ,MAAM,YAAY,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,aAAa,CAAC,CAAC;;CAG9D,SAAS,SAAS,KAAsC;EAEtD,MAAM,OAAO,IAAI,MAAM,IAAI,CAAC;AAC5B,SAAO,MAAM,MAAM,MAAM,KAAK,WAAW,EAAE,MAAM,CAAC;;;CAIpD,SAAS,kBAAkB,MAA4B;EACrD,MAAM,MAAM,MAAM,UAAU;AAC5B,MAAI,OAAO,EAAG,QAAO;EAErB,MAAM,QAAkB,EAAE;AAC1B,QAAM,KAAK,MAAM,UAAU,YAAY,SAAS;AAChD,QAAM,KAAK,WAAW,MAAM;AAC5B,MAAI,MAAM,qBACR,OAAM,KAAK,0BAA0B,KAAK,uBAAuB;AAEnE,SAAO,MAAM,KAAK,KAAK;;AAIzB,SAAQ,QAAQ,UAAU,OAAO,SAAS,OAAO,YAAY;EAC3D,MAAM,MAAM,QAAQ;AAGpB,MAAI,QAAQ,MAAM,MAAM,IAAI,WAAW,EAAE,CAAC,CACxC,QAAO;EAGT,MAAM,SAAS,QAAQ,OAAO,aAAa;AAG3C,MAAI,CAAC,UAAU,IAAI,OAAO,EAAE;AAC1B,OAAI,CAAC,MAAM,UAAU,gBAAgB,CACnC,OAAM,OAAO,iBAAiB,WAAW;AAE3C,UAAO;;EAIT,MAAM,aAAa,MAAM;AACzB,MAAI,aAAa,OAAO,cAAc,IACpC,QAAO;AAIT,MAAI,CAAC,MAAM,UAAU,gBAAgB,EAAE;GACrC,MAAM,OAAO,SAAS,IAAI;AAC1B,SAAM,OAAO,iBAAiB,kBAAkB,KAAK,CAAC;;AAIxD,MAAI,QAAQ,SAAS;GAEnB,MAAM,MAAM,IAAI,MADH,OAAO,YAAY,WAAW,UAAU,OAAO,QAAQ,CACzC,CAAC;AAC5B,SAAM,OAAO,QAAQ,IAAI;AAGzB,OAAI,aAAa;IACf,MAAM,cAAc,QAAQ,QAAQ;AACpC,QAAI,eAAe,gBAAgB,KAAK;AACtC,WAAM,KAAK,IAAI;AACf,YAAO;;;;AAKb,SAAO;GACP;AAEF,SAAQ,KAAK,QAAQ,4BAA4B;;AAGnD,sBAAe,GAAG,eAAe;CAC/B,MAAM;CACN,SAAS;CACV,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"circuitBreaker-DeY4FCjs.mjs","names":["MAX_LIMIT"],"sources":["../src/utils/queryParser.ts","../src/utils/responseSchemas.ts","../src/utils/stateMachine.ts","../src/utils/circuitBreaker.ts"],"sourcesContent":["/**\r\n * Arc Query Parser - Default URL-to-Query Parser\r\n *\r\n * Framework-agnostic query parser that converts URL parameters to query options.\r\n * This is Arc's built-in parser; users can swap in MongoKit's QueryParser,\r\n * pgkit's parser, or any custom parser implementing QueryParserInterface.\r\n *\r\n * @example\r\n * // Use Arc default parser (auto-applied if no queryParser option)\r\n * defineResource({ name: 'product', adapter: ... });\r\n *\r\n * // Use MongoKit's QueryParser (recommended for MongoDB - has $lookup, aggregations, etc.)\r\n * import { QueryParser } from '@classytic/mongokit';\r\n * defineResource({\r\n * name: 'product',\r\n * adapter: ...,\r\n * queryParser: new QueryParser(),\r\n * });\r\n *\r\n * // Use custom parser for SQL databases\r\n * defineResource({\r\n * name: 'user',\r\n * adapter: ...,\r\n * queryParser: new PgQueryParser(),\r\n * });\r\n */\r\n\r\nimport type { ParsedQuery, PopulateOption, QueryParserInterface } from '../types/index.js';\r\nimport { MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MAX_FILTER_DEPTH, DEFAULT_MAX_LIMIT as MAX_LIMIT, DEFAULT_LIMIT, RESERVED_QUERY_PARAMS } from '../constants.js';\r\n\r\n// ============================================================================\r\n// Dangerous Patterns (ReDoS protection)\r\n// ============================================================================\r\n\r\n/**\r\n * Regex patterns that can cause catastrophic backtracking (ReDoS attacks)\r\n * Detects:\r\n * - Quantifiers: {n,m}\r\n * - Possessive quantifiers: *+, ++, ?+\r\n * - Nested quantifiers: (a+)+, (a*)*\r\n * - Backreferences: \\1, \\2, etc.\r\n */\r\nconst DANGEROUS_REGEX_PATTERNS = /(\\{[0-9,]+\\}|\\*\\+|\\+\\+|\\?\\+|(\\(.+\\))\\+|\\(\\?\\:|\\\\[0-9]|(\\[.+\\]).+(\\[.+\\]))/;\r\n\r\n// ============================================================================\r\n// Arc Query Parser\r\n// ============================================================================\r\n\r\nexport interface ArcQueryParserOptions {\r\n /** Maximum allowed limit value (default: 1000) */\r\n maxLimit?: number;\r\n /** Default limit for pagination (default: 20) */\r\n defaultLimit?: number;\r\n /** Maximum regex pattern length (default: 500) */\r\n maxRegexLength?: number;\r\n /** Maximum search query length (default: 200) */\r\n maxSearchLength?: number;\r\n /** Maximum filter nesting depth (default: 10) */\r\n maxFilterDepth?: number;\r\n}\r\n\r\n/**\r\n * Arc's default query parser\r\n *\r\n * Converts URL query parameters to a structured query format:\r\n * - Pagination: ?page=1&limit=20\r\n * - Sorting: ?sort=-createdAt,name (- prefix = descending)\r\n * - Filtering: ?status=active&price[gte]=100&price[lte]=500\r\n * - Search: ?search=keyword\r\n * - Populate: ?populate=author,category\r\n * - Field selection: ?select=name,price,status\r\n * - Keyset pagination: ?after=cursor_value\r\n *\r\n * For advanced MongoDB features ($lookup, aggregations), use MongoKit's QueryParser.\r\n */\r\nexport class ArcQueryParser implements QueryParserInterface {\r\n private readonly maxLimit: number;\r\n private readonly defaultLimit: number;\r\n private readonly maxRegexLength: number;\r\n private readonly maxSearchLength: number;\r\n private readonly maxFilterDepth: number;\r\n\r\n /** Supported filter operators */\r\n private readonly operators: Record<string, string> = {\r\n eq: '$eq',\r\n ne: '$ne',\r\n gt: '$gt',\r\n gte: '$gte',\r\n lt: '$lt',\r\n lte: '$lte',\r\n in: '$in',\r\n nin: '$nin',\r\n like: '$regex',\r\n contains: '$regex',\r\n regex: '$regex',\r\n exists: '$exists',\r\n };\r\n\r\n constructor(options: ArcQueryParserOptions = {}) {\r\n this.maxLimit = options.maxLimit ?? MAX_LIMIT;\r\n this.defaultLimit = options.defaultLimit ?? DEFAULT_LIMIT;\r\n this.maxRegexLength = options.maxRegexLength ?? MAX_REGEX_LENGTH;\r\n this.maxSearchLength = options.maxSearchLength ?? MAX_SEARCH_LENGTH;\r\n this.maxFilterDepth = options.maxFilterDepth ?? MAX_FILTER_DEPTH;\r\n }\r\n\r\n /**\r\n * Parse URL query parameters into structured query options\r\n */\r\n parse(query: Record<string, unknown> | null | undefined): ParsedQuery {\r\n const q = query ?? {};\r\n\r\n // Extract pagination params\r\n const page = this.parseNumber(q.page, 1);\r\n const limit = Math.min(this.parseNumber(q.limit, this.defaultLimit), this.maxLimit);\r\n const after = this.parseString(q.after ?? q.cursor);\r\n\r\n // Extract sort\r\n const sort = this.parseSort(q.sort);\r\n\r\n // Extract populate — handles both simple string and bracket notation object\r\n const { populate, populateOptions } = this.parsePopulate(q.populate);\r\n\r\n // Extract search\r\n const search = this.parseSearch(q.search);\r\n\r\n // Extract select\r\n const select = this.parseSelect(q.select);\r\n\r\n // Extract filters (everything else)\r\n const filters = this.parseFilters(q);\r\n\r\n return {\r\n filters,\r\n limit,\r\n sort,\r\n populate,\r\n populateOptions,\r\n search,\r\n page: after ? undefined : page,\r\n after,\r\n select,\r\n };\r\n }\r\n\r\n // ============================================================================\r\n // Parse Helpers\r\n // ============================================================================\r\n\r\n private parseNumber(value: unknown, defaultValue: number): number {\r\n if (value === undefined || value === null) return defaultValue;\r\n const num = parseInt(String(value), 10);\r\n return Number.isNaN(num) ? defaultValue : Math.max(1, num);\r\n }\r\n\r\n private parseString(value: unknown): string | undefined {\r\n if (value === undefined || value === null) return undefined;\r\n const str = String(value).trim();\r\n return str.length > 0 ? str : undefined;\r\n }\r\n\r\n /**\r\n * Parse populate parameter — handles both simple string and bracket notation.\r\n *\r\n * Simple: ?populate=author,category → { populate: 'author,category' }\r\n * Bracket: ?populate[author][select]=name,email → { populateOptions: [{ path: 'author', select: 'name email' }] }\r\n */\r\n private parsePopulate(value: unknown): { populate?: string; populateOptions?: PopulateOption[] } {\r\n if (value === undefined || value === null) return {};\r\n\r\n // Simple string: ?populate=author,category\r\n if (typeof value === 'string') {\r\n const trimmed = value.trim();\r\n return trimmed.length > 0 ? { populate: trimmed } : {};\r\n }\r\n\r\n // Bracket notation object: ?populate[author][select]=name,email\r\n // qs parses this as { author: { select: 'name,email' } }\r\n if (typeof value === 'object' && !Array.isArray(value)) {\r\n const obj = value as Record<string, unknown>;\r\n const keys = Object.keys(obj);\r\n if (keys.length === 0) return {};\r\n\r\n const options: PopulateOption[] = [];\r\n for (const path of keys) {\r\n // Validate path name (prevent injection)\r\n if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(path)) continue;\r\n\r\n const config = obj[path];\r\n if (typeof config === 'object' && config !== null && !Array.isArray(config)) {\r\n const cfg = config as Record<string, unknown>;\r\n const option: PopulateOption = { path };\r\n\r\n // Parse select: convert comma-separated to space-separated (Mongoose format)\r\n if (typeof cfg.select === 'string') {\r\n option.select = cfg.select.split(',').map(s => s.trim()).filter(Boolean).join(' ');\r\n }\r\n\r\n // Parse match (filter conditions)\r\n if (typeof cfg.match === 'object' && cfg.match !== null) {\r\n option.match = cfg.match as Record<string, unknown>;\r\n }\r\n\r\n options.push(option);\r\n } else {\r\n // Simple value like populate[author]=true → treat as simple populate\r\n options.push({ path });\r\n }\r\n }\r\n\r\n return options.length > 0 ? { populateOptions: options } : {};\r\n }\r\n\r\n return {};\r\n }\r\n\r\n private parseSort(value: unknown): Record<string, 1 | -1> | undefined {\r\n if (!value) return undefined;\r\n\r\n const sortStr = String(value);\r\n const result: Record<string, 1 | -1> = {};\r\n\r\n for (const field of sortStr.split(',')) {\r\n const trimmed = field.trim();\r\n if (!trimmed) continue;\r\n\r\n // Validate field name (prevent injection)\r\n if (!/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;\r\n\r\n if (trimmed.startsWith('-')) {\r\n result[trimmed.slice(1)] = -1;\r\n } else {\r\n result[trimmed] = 1;\r\n }\r\n }\r\n\r\n return Object.keys(result).length > 0 ? result : undefined;\r\n }\r\n\r\n private parseSearch(value: unknown): string | undefined {\r\n if (!value) return undefined;\r\n\r\n const search = String(value).trim();\r\n if (search.length === 0) return undefined;\r\n if (search.length > this.maxSearchLength) {\r\n return search.slice(0, this.maxSearchLength);\r\n }\r\n\r\n return search;\r\n }\r\n\r\n private parseSelect(value: unknown): Record<string, 0 | 1> | undefined {\r\n if (!value) return undefined;\r\n\r\n const selectStr = String(value);\r\n const result: Record<string, 0 | 1> = {};\r\n\r\n for (const field of selectStr.split(',')) {\r\n const trimmed = field.trim();\r\n if (!trimmed) continue;\r\n\r\n // Validate field name (prevent injection)\r\n if (!/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;\r\n\r\n if (trimmed.startsWith('-')) {\r\n result[trimmed.slice(1)] = 0;\r\n } else {\r\n result[trimmed] = 1;\r\n }\r\n }\r\n\r\n return Object.keys(result).length > 0 ? result : undefined;\r\n }\r\n\r\n /**\r\n * Check if a value exceeds the maximum nesting depth.\r\n * Prevents filter bombs where deeply nested objects consume excessive memory/CPU.\r\n */\r\n private exceedsDepth(obj: unknown, currentDepth: number = 0): boolean {\r\n if (currentDepth > this.maxFilterDepth) return true;\r\n if (obj === null || obj === undefined) return false;\r\n if (Array.isArray(obj)) {\r\n return obj.some(v => this.exceedsDepth(v, currentDepth));\r\n }\r\n if (typeof obj !== 'object') return false;\r\n return Object.values(obj as Record<string, unknown>).some(\r\n v => this.exceedsDepth(v, currentDepth + 1)\r\n );\r\n }\r\n\r\n private parseFilters(query: Record<string, unknown>): Record<string, unknown> {\r\n const filters: Record<string, unknown> = {};\r\n\r\n for (const [key, value] of Object.entries(query)) {\r\n if (RESERVED_QUERY_PARAMS.has(key)) continue;\r\n if (value === undefined || value === null) continue;\r\n\r\n // Validate field name (prevent injection)\r\n if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key)) continue;\r\n\r\n // Enforce max filter depth (prevents filter bombs)\r\n if (this.exceedsDepth(value)) continue;\r\n\r\n // Handle nested object format from qs parser: { price: { gte: '40', lte: '100' } }\r\n // This happens when URL is ?price[gte]=40&price[lte]=100 and qs parses it\r\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\r\n const operatorObj = value as Record<string, unknown>;\r\n const operatorKeys = Object.keys(operatorObj);\r\n\r\n // Check if all keys are known operators\r\n const allOperators = operatorKeys.every(op => this.operators[op]);\r\n\r\n if (allOperators && operatorKeys.length > 0) {\r\n // Convert operator object: { gte: '40', lte: '100' } → { $gte: 40, $lte: 100 }\r\n const mongoFilters: Record<string, unknown> = {};\r\n for (const [op, opValue] of Object.entries(operatorObj)) {\r\n const mongoOp = this.operators[op];\r\n if (mongoOp) {\r\n mongoFilters[mongoOp] = this.parseFilterValue(opValue, op);\r\n }\r\n }\r\n filters[key] = mongoFilters;\r\n continue;\r\n }\r\n }\r\n\r\n // Handle key-based bracket notation: price[gte]=100 (when not parsed by qs)\r\n const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\\[([a-z]+)\\])?$/);\r\n if (!match) continue;\r\n\r\n const [, fieldName, operator] = match;\r\n if (!fieldName) continue;\r\n\r\n if (operator && this.operators[operator]) {\r\n // Operator filter: status[ne]=deleted → { status: { $ne: 'deleted' } }\r\n const mongoOp = this.operators[operator];\r\n const parsedValue = this.parseFilterValue(value, operator);\r\n\r\n if (!filters[fieldName]) {\r\n filters[fieldName] = {};\r\n }\r\n (filters[fieldName] as Record<string, unknown>)[mongoOp] = parsedValue;\r\n } else if (!operator) {\r\n // Direct equality: status=active → { status: 'active' }\r\n filters[fieldName] = this.parseFilterValue(value);\r\n }\r\n }\r\n\r\n return filters;\r\n }\r\n\r\n private parseFilterValue(value: unknown, operator?: string): unknown {\r\n // Handle arrays (for $in, $nin operators)\r\n if (operator === 'in' || operator === 'nin') {\r\n if (Array.isArray(value)) {\r\n return value.map(v => this.coerceValue(v));\r\n }\r\n if (typeof value === 'string' && value.includes(',')) {\r\n return value.split(',').map(v => this.coerceValue(v.trim()));\r\n }\r\n return [this.coerceValue(value)];\r\n }\r\n\r\n // Handle regex operators\r\n if (operator === 'like' || operator === 'contains' || operator === 'regex') {\r\n return this.sanitizeRegex(String(value));\r\n }\r\n\r\n // Handle exists operator\r\n if (operator === 'exists') {\r\n const str = String(value).toLowerCase();\r\n return str === 'true' || str === '1';\r\n }\r\n\r\n return this.coerceValue(value);\r\n }\r\n\r\n private coerceValue(value: unknown): unknown {\r\n if (value === 'true') return true;\r\n if (value === 'false') return false;\r\n if (value === 'null') return null;\r\n\r\n // Try to parse as number\r\n if (typeof value === 'string') {\r\n const num = Number(value);\r\n if (!Number.isNaN(num) && value.trim() !== '') {\r\n return num;\r\n }\r\n }\r\n\r\n return value;\r\n }\r\n\r\n // ============================================================================\r\n // OpenAPI Schema Generation\r\n // ============================================================================\r\n\r\n /**\r\n * Generate OpenAPI-compatible JSON Schema for query parameters.\r\n * Arc's defineResource() auto-detects this method and uses it\r\n * to document list endpoint query parameters in OpenAPI/Swagger.\r\n */\r\n getQuerySchema(): {\r\n type: 'object';\r\n properties: Record<string, unknown>;\r\n required?: string[];\r\n } {\r\n const operatorEntries = Object.entries(this.operators);\r\n const operatorLines = operatorEntries.map(([op, mongoOp]) => {\r\n const desc: Record<string, string> = {\r\n eq: 'Equal (default when no operator specified)',\r\n ne: 'Not equal',\r\n gt: 'Greater than',\r\n gte: 'Greater than or equal',\r\n lt: 'Less than',\r\n lte: 'Less than or equal',\r\n in: 'In list (comma-separated values)',\r\n nin: 'Not in list',\r\n like: 'Pattern match (case-insensitive)',\r\n contains: 'Contains substring (case-insensitive)',\r\n regex: 'Regex pattern',\r\n exists: 'Field exists (true/false)',\r\n };\r\n return ` ${op} → ${mongoOp}: ${desc[op] || op}`;\r\n });\r\n\r\n return {\r\n type: 'object',\r\n properties: {\r\n page: {\r\n type: 'integer',\r\n description: 'Page number for offset pagination',\r\n default: 1,\r\n minimum: 1,\r\n },\r\n limit: {\r\n type: 'integer',\r\n description: 'Number of items per page',\r\n default: this.defaultLimit,\r\n minimum: 1,\r\n maximum: this.maxLimit,\r\n },\r\n sort: {\r\n type: 'string',\r\n description: 'Sort fields (comma-separated). Prefix with - for descending. Example: -createdAt,name',\r\n },\r\n search: {\r\n type: 'string',\r\n description: 'Full-text search query',\r\n maxLength: this.maxSearchLength,\r\n },\r\n select: {\r\n type: 'string',\r\n description: 'Fields to include/exclude (comma-separated). Prefix with - to exclude. Example: name,email,-password',\r\n },\r\n populate: {\r\n type: 'string',\r\n description: 'Fields to populate/join (comma-separated). Example: author,category',\r\n },\r\n after: {\r\n type: 'string',\r\n description: 'Cursor value for keyset pagination',\r\n },\r\n _filterOperators: {\r\n type: 'string',\r\n description: ['Available filter operators (use as field[operator]=value):', ...operatorLines].join('\\n'),\r\n 'x-internal': true,\r\n },\r\n },\r\n };\r\n }\r\n\r\n // ============================================================================\r\n // Regex Sanitization\r\n // ============================================================================\r\n\r\n private sanitizeRegex(pattern: string): string {\r\n // Limit length\r\n let sanitized = pattern.slice(0, this.maxRegexLength);\r\n\r\n // Check for dangerous patterns\r\n if (DANGEROUS_REGEX_PATTERNS.test(sanitized)) {\r\n // Escape the entire pattern to treat as literal string\r\n sanitized = sanitized.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\r\n }\r\n\r\n return sanitized;\r\n }\r\n}\r\n\r\n/**\r\n * Create a new ArcQueryParser instance\r\n */\r\nexport function createQueryParser(options?: ArcQueryParserOptions): ArcQueryParser {\r\n return new ArcQueryParser(options);\r\n}\r\n\r\nexport default ArcQueryParser;\r\n","/**\n * Response Schemas\n *\n * Standard JSON Schema definitions for API responses.\n */\n\nimport type { AnyRecord } from '../types/index.js';\n\n// ============================================================================\n// Schema Types\n// ============================================================================\n\nexport interface JsonSchema {\n type: string;\n properties?: Record<string, JsonSchema | AnyRecord>;\n required?: string[];\n items?: JsonSchema | AnyRecord;\n additionalProperties?: boolean | JsonSchema;\n description?: string;\n example?: unknown;\n [key: string]: unknown;\n}\n\n// ============================================================================\n// Response Wrapper Schemas\n// ============================================================================\n\n/**\n * Base success response schema\n */\nexport const successResponseSchema: JsonSchema = {\n type: 'object',\n properties: {\n success: { type: 'boolean', example: true },\n },\n required: ['success'],\n};\n\n/**\n * Error response schema\n */\nexport const errorResponseSchema: JsonSchema = {\n type: 'object',\n properties: {\n success: { type: 'boolean', example: false },\n error: { type: 'string', description: 'Error message' },\n code: { type: 'string', description: 'Error code' },\n message: { type: 'string', description: 'Detailed message' },\n },\n required: ['success', 'error'],\n};\n\n/**\n * Pagination schema - matches MongoKit/Arc runtime format\n *\n * Runtime format (flat fields):\n * { page, limit, total, pages, hasNext, hasPrev }\n */\nexport const paginationSchema: JsonSchema = {\n type: 'object',\n properties: {\n page: { type: 'integer', example: 1 },\n limit: { type: 'integer', example: 20 },\n total: { type: 'integer', example: 100 },\n pages: { type: 'integer', example: 5 },\n hasNext: { type: 'boolean', example: true },\n hasPrev: { type: 'boolean', example: false },\n },\n required: ['page', 'limit', 'total', 'pages', 'hasNext', 'hasPrev'],\n};\n\n// ============================================================================\n// Schema Builders\n// ============================================================================\n\n/**\n * Wrap a data schema in a success response\n */\nexport function wrapResponse(dataSchema: JsonSchema): JsonSchema {\n return {\n type: 'object',\n properties: {\n success: { type: 'boolean', example: true },\n data: dataSchema,\n },\n required: ['success', 'data'],\n // Allow extra fields (fieldPermissions, meta spreads, etc.)\n additionalProperties: true,\n };\n}\n\n/**\n * Create a list response schema with pagination - matches MongoKit/Arc runtime format\n *\n * Runtime format:\n * { success, docs: [...], page, limit, total, pages, hasNext, hasPrev }\n *\n * Note: Uses 'docs' array (not 'data') with flat pagination fields\n */\nexport function listResponse(itemSchema: JsonSchema): JsonSchema {\n return {\n type: 'object',\n properties: {\n success: { type: 'boolean', example: true },\n docs: {\n type: 'array',\n items: itemSchema,\n },\n // Flat pagination fields (not nested)\n page: { type: 'integer', example: 1 },\n limit: { type: 'integer', example: 20 },\n total: { type: 'integer', example: 100 },\n pages: { type: 'integer', example: 5 },\n hasNext: { type: 'boolean', example: false },\n hasPrev: { type: 'boolean', example: false },\n },\n required: ['success', 'docs'],\n // Allow extra fields (fieldPermissions, meta spreads, etc.)\n additionalProperties: true,\n };\n}\n\n/**\n * Alias for listResponse - matches local responseSchemas.js naming\n */\nexport const paginateWrapper = listResponse;\n\n/**\n * Create a single item response schema\n *\n * Runtime format: { success, data: {...} }\n */\nexport function itemResponse(itemSchema: JsonSchema): JsonSchema {\n return wrapResponse(itemSchema);\n}\n\n/**\n * Alias for itemResponse - matches local responseSchemas.js naming\n */\nexport const itemWrapper = itemResponse;\n\n/**\n * Create a create/update response schema\n */\nexport function mutationResponse(itemSchema: JsonSchema): JsonSchema {\n return {\n type: 'object',\n properties: {\n success: { type: 'boolean', example: true },\n data: itemSchema,\n message: { type: 'string', example: 'Created successfully' },\n },\n required: ['success', 'data'],\n // Allow extra fields (fieldPermissions, meta spreads, etc.)\n additionalProperties: true,\n };\n}\n\n/**\n * Create a delete response schema\n *\n * Runtime format: { success, message }\n */\nexport function deleteResponse(): JsonSchema {\n return {\n type: 'object',\n properties: {\n success: { type: 'boolean', example: true },\n message: { type: 'string', example: 'Deleted successfully' },\n },\n required: ['success'],\n // Allow extra fields (fieldPermissions, meta spreads, etc.)\n additionalProperties: true,\n };\n}\n\n/**\n * Alias for deleteResponse - matches local responseSchemas.js naming\n */\nexport const messageWrapper = deleteResponse;\n\n// ============================================================================\n// HTTP Status Response Schemas\n// ============================================================================\n\nexport const responses = {\n 200: (schema: JsonSchema) => ({\n description: 'Successful response',\n content: {\n 'application/json': { schema },\n },\n }),\n\n 201: (schema: JsonSchema) => ({\n description: 'Created successfully',\n content: {\n 'application/json': { schema: mutationResponse(schema) },\n },\n }),\n\n 400: {\n description: 'Bad Request',\n content: {\n 'application/json': {\n schema: {\n ...errorResponseSchema,\n properties: {\n ...errorResponseSchema.properties,\n code: { type: 'string', example: 'VALIDATION_ERROR' },\n details: {\n type: 'object',\n properties: {\n errors: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n field: { type: 'string' },\n message: { type: 'string' },\n },\n },\n },\n },\n },\n },\n },\n },\n },\n },\n\n 401: {\n description: 'Unauthorized',\n content: {\n 'application/json': {\n schema: {\n ...errorResponseSchema,\n properties: {\n ...errorResponseSchema.properties,\n code: { type: 'string', example: 'UNAUTHORIZED' },\n },\n },\n },\n },\n },\n\n 403: {\n description: 'Forbidden',\n content: {\n 'application/json': {\n schema: {\n ...errorResponseSchema,\n properties: {\n ...errorResponseSchema.properties,\n code: { type: 'string', example: 'FORBIDDEN' },\n },\n },\n },\n },\n },\n\n 404: {\n description: 'Not Found',\n content: {\n 'application/json': {\n schema: {\n ...errorResponseSchema,\n properties: {\n ...errorResponseSchema.properties,\n code: { type: 'string', example: 'NOT_FOUND' },\n },\n },\n },\n },\n },\n\n 409: {\n description: 'Conflict',\n content: {\n 'application/json': {\n schema: {\n ...errorResponseSchema,\n properties: {\n ...errorResponseSchema.properties,\n code: { type: 'string', example: 'CONFLICT' },\n },\n },\n },\n },\n },\n\n 500: {\n description: 'Internal Server Error',\n content: {\n 'application/json': {\n schema: {\n ...errorResponseSchema,\n properties: {\n ...errorResponseSchema.properties,\n code: { type: 'string', example: 'INTERNAL_ERROR' },\n },\n },\n },\n },\n },\n};\n\n// ============================================================================\n// Query Parameter Schemas\n// ============================================================================\n\nexport const queryParams = {\n pagination: {\n page: {\n type: 'integer',\n minimum: 1,\n default: 1,\n description: 'Page number',\n },\n limit: {\n type: 'integer',\n minimum: 1,\n maximum: 100,\n default: 20,\n description: 'Items per page',\n },\n },\n\n sorting: {\n sort: {\n type: 'string',\n description: 'Sort field (prefix with - for descending)',\n example: '-createdAt',\n },\n },\n\n filtering: {\n select: {\n description: 'Fields to include (space-separated or object)',\n example: 'name email createdAt',\n },\n populate: {\n description: 'Relations to populate (comma-separated string or bracket-notation object)',\n example: 'author,category',\n },\n },\n};\n\n/**\n * Get standard list query parameters schema\n */\nexport function getListQueryParams(): AnyRecord {\n return {\n type: 'object',\n properties: {\n ...queryParams.pagination,\n ...queryParams.sorting,\n ...queryParams.filtering,\n },\n // Allow additional/complex query params (e.g., bracket-notation populate, filters)\n // Without this, qs-parsed nested objects like ?populate[author][select]=name would be rejected\n additionalProperties: true,\n };\n}\n\n// ============================================================================\n// Default CRUD Schemas\n// ============================================================================\n\n/**\n * Generic item schema that allows any properties.\n * Used as default when no user schema is provided.\n * Enables fast-json-stringify while still passing through all fields.\n */\nconst genericItemSchema: JsonSchema = {\n type: 'object',\n additionalProperties: true,\n};\n\n/**\n * Recursively strip `example` keys from a schema object.\n * The `example` keyword is OpenAPI metadata — not standard JSON Schema —\n * and triggers Ajv strict mode errors when used on routes without the\n * `keywords: ['example']` AJV config (e.g., raw Fastify without createApp).\n */\nfunction stripExamples<T>(schema: T): T {\n if (schema === null || typeof schema !== 'object') return schema;\n if (Array.isArray(schema)) return schema.map(stripExamples) as T;\n\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(schema as Record<string, unknown>)) {\n if (key === 'example') continue;\n result[key] = stripExamples(value);\n }\n return result as T;\n}\n\n/**\n * Get default response schemas for all CRUD operations.\n *\n * When routes have response schemas, Fastify compiles them with\n * fast-json-stringify for 2-3x faster serialization and prevents\n * accidental field disclosure.\n *\n * These defaults use `additionalProperties: true` so all fields pass through.\n * Override with specific schemas for full serialization performance + safety.\n *\n * Note: `example` properties are stripped from defaults so they work with\n * any Fastify instance (not just createApp which adds `keywords: ['example']`).\n */\nexport function getDefaultCrudSchemas(): Record<string, Record<string, unknown>> {\n return stripExamples({\n list: {\n querystring: getListQueryParams(),\n response: { 200: listResponse(genericItemSchema) },\n },\n get: {\n response: { 200: itemResponse(genericItemSchema) },\n },\n create: {\n response: { 201: mutationResponse(genericItemSchema) },\n },\n update: {\n response: { 200: itemResponse(genericItemSchema) },\n },\n delete: {\n response: { 200: deleteResponse() },\n },\n });\n}\n","/**\n * State Machine Utility\n *\n * Pure utility for validating state transitions in workflow systems.\n * Zero dependencies, framework-agnostic.\n *\n * @example\n * const orderState = createStateMachine('Order', {\n * approve: ['pending', 'draft'],\n * cancel: ['pending', 'approved'],\n * fulfill: ['approved'],\n * });\n *\n * // Check if transition is allowed\n * if (orderState.can('approve', currentStatus)) {\n * // Perform approval\n * }\n *\n * // Assert transition (throws if invalid)\n * orderState.assert('approve', currentStatus, ValidationError);\n */\n\nexport interface StateMachine {\n /**\n * Synchronously check if action can be performed from current status.\n * Only checks the transition map — does NOT evaluate guards.\n * Use `canAsync()` when guards need to be evaluated.\n */\n can(action: string, status: string | null | undefined): boolean;\n\n /**\n * Asynchronously check if action can be performed, including guard evaluation.\n * Falls back to simple transition check when no guard is defined.\n */\n canAsync(action: string, status: string | null | undefined, context?: any): Promise<boolean>;\n\n /**\n * Assert action can be performed, throw error if invalid\n * @param action - Action to perform\n * @param status - Current status\n * @param errorFactory - Optional error constructor\n * @param message - Optional custom error message\n */\n assert(\n action: string,\n status: string | null | undefined,\n errorFactory?: (msg: string) => Error,\n message?: string\n ): void;\n\n /**\n * Get transition history\n */\n getHistory?(): TransitionHistoryEntry[];\n\n /**\n * Record a transition\n */\n recordTransition?(from: string, to: string, action: string, metadata?: any): void;\n\n /**\n * Clear history\n */\n clearHistory?(): void;\n\n /**\n * Get available actions for current status\n */\n getAvailableActions?(status: string): string[];\n}\n\nexport interface TransitionHistoryEntry {\n from: string;\n to: string;\n action: string;\n timestamp: Date;\n metadata?: any;\n}\n\nexport interface TransitionGuard {\n (context: { from: string; to: string; action: string; data?: any }): boolean | Promise<boolean>;\n}\n\nexport interface TransitionAction {\n (context: { from: string; to: string; action: string; data?: any }): void | Promise<void>;\n}\n\nexport interface TransitionHook {\n before?: TransitionAction;\n after?: TransitionAction;\n}\n\nexport type TransitionConfig = Record<\n string,\n | string[]\n | {\n from: string[];\n to?: string;\n guard?: TransitionGuard;\n before?: TransitionAction;\n after?: TransitionAction;\n }\n>;\n\n/**\n * Create a state machine for validating transitions\n *\n * @param name - Name of the state machine (used in error messages)\n * @param transitions - Map of actions to allowed source statuses\n * @param options - Additional options (history, guards, actions)\n * @returns State machine with can() and assert() methods\n *\n * @example\n * // Basic usage\n * const transferState = createStateMachine('Transfer', {\n * approve: ['draft'],\n * dispatch: ['approved'],\n * receive: ['dispatched', 'in_transit'],\n * cancel: ['draft', 'approved'],\n * });\n *\n * @example\n * // With guards and actions\n * const orderState = createStateMachine('Order', {\n * approve: {\n * from: ['pending'],\n * to: 'approved',\n * guard: ({ data }) => data.paymentConfirmed,\n * before: ({ from, to }) => console.log(`Approving order from ${from} to ${to}`),\n * after: ({ data }) => sendApprovalEmail(data.customerId),\n * },\n * }, { trackHistory: true });\n */\nexport function createStateMachine(\n name: string,\n transitions: TransitionConfig = {},\n options: { trackHistory?: boolean } = {}\n): StateMachine {\n const normalized = new Map<\n string,\n {\n from: Set<string>;\n to?: string;\n guard?: TransitionGuard;\n before?: TransitionAction;\n after?: TransitionAction;\n }\n >();\n const history: TransitionHistoryEntry[] | undefined = options.trackHistory ? [] : undefined;\n\n // Normalize transition config (support both array and object formats)\n Object.entries(transitions).forEach(([action, allowed]) => {\n if (Array.isArray(allowed)) {\n // Simple array format: action: ['state1', 'state2']\n normalized.set(action, { from: new Set(allowed) });\n } else if (typeof allowed === 'object' && 'from' in allowed) {\n // Object format with guards/actions\n normalized.set(action, {\n from: new Set(Array.isArray(allowed.from) ? allowed.from : [allowed.from]),\n to: allowed.to,\n guard: allowed.guard,\n before: allowed.before,\n after: allowed.after,\n });\n }\n });\n\n const can = (action: string, status: string | null | undefined): boolean => {\n const transition = normalized.get(action);\n if (!transition || !status) return false;\n return transition.from.has(status);\n };\n\n const canAsync = async (\n action: string,\n status: string | null | undefined,\n context?: any\n ): Promise<boolean> => {\n const transition = normalized.get(action);\n if (!transition || !status) return false;\n\n // Check if transition is allowed from current state\n if (!transition.from.has(status)) return false;\n\n // Check guard condition if present\n if (transition.guard) {\n try {\n const guardResult = await transition.guard({\n from: status,\n to: transition.to || '',\n action,\n data: context,\n });\n return guardResult;\n } catch {\n return false;\n }\n }\n\n return true;\n };\n\n const assert = (\n action: string,\n status: string | null | undefined,\n errorFactory?: (msg: string) => Error,\n message?: string\n ): void => {\n if (can(action, status)) return;\n\n const errorMessage =\n message || `${name} cannot '${action}' when status is '${status || 'unknown'}'`;\n\n if (typeof errorFactory === 'function') {\n throw errorFactory(errorMessage);\n }\n throw new Error(errorMessage);\n };\n\n const recordTransition = (from: string, to: string, action: string, metadata?: any): void => {\n if (history) {\n history.push({\n from,\n to,\n action,\n timestamp: new Date(),\n metadata,\n });\n }\n };\n\n const getHistory = (): TransitionHistoryEntry[] => {\n return history ? [...history] : [];\n };\n\n const clearHistory = (): void => {\n if (history) {\n history.length = 0;\n }\n };\n\n const getAvailableActions = (status: string): string[] => {\n const actions: string[] = [];\n for (const [action, transition] of normalized.entries()) {\n if (transition.from.has(status)) {\n actions.push(action);\n }\n }\n return actions;\n };\n\n return {\n can,\n canAsync,\n assert,\n recordTransition,\n getHistory,\n clearHistory,\n getAvailableActions,\n };\n}\n\n/**\n * Execute a state transition with guards and actions\n *\n * @example\n * const result = await executeTransition(\n * orderState,\n * 'approve',\n * 'pending',\n * 'approved',\n * { paymentConfirmed: true }\n * );\n *\n * if (result.success) {\n * console.log('Transition successful');\n * } else {\n * console.error(result.error);\n * }\n */\nexport async function executeTransition(\n stateMachine: StateMachine,\n action: string,\n from: string,\n to: string,\n context?: any\n): Promise<{ success: boolean; error?: string }> {\n try {\n // Check if transition is allowed (use canAsync for guard evaluation)\n const canTransition = await stateMachine.canAsync(action, from, context);\n if (!canTransition) {\n return {\n success: false,\n error: `Cannot transition from ${from} to ${to} via ${action}`,\n };\n }\n\n // Record the transition if history is enabled\n if (stateMachine.recordTransition) {\n stateMachine.recordTransition(from, to, action, context);\n }\n\n return { success: true };\n } catch (error: any) {\n return {\n success: false,\n error: error.message || 'Transition failed',\n };\n }\n}\n","/**\n * Circuit Breaker Pattern\n *\n * Wraps external service calls with failure protection.\n * Prevents cascading failures by \"opening\" the circuit when\n * a service is failing, allowing it time to recover.\n *\n * States:\n * - CLOSED: Normal operation, requests pass through\n * - OPEN: Too many failures, all requests fail fast\n * - HALF_OPEN: Testing if service recovered, limited requests\n *\n * @example\n * import { CircuitBreaker } from '@classytic/arc/utils';\n *\n * const paymentBreaker = new CircuitBreaker(async (amount) => {\n * return await stripe.charges.create({ amount });\n * }, {\n * failureThreshold: 5,\n * resetTimeout: 30000,\n * timeout: 5000,\n * });\n *\n * try {\n * const result = await paymentBreaker.call(100);\n * } catch (error) {\n * // Handle failure or circuit open\n * }\n */\n\nexport const CircuitState = {\n CLOSED: 'CLOSED',\n OPEN: 'OPEN',\n HALF_OPEN: 'HALF_OPEN',\n} as const;\n\nexport type CircuitState = (typeof CircuitState)[keyof typeof CircuitState];\n\nexport interface CircuitBreakerOptions {\n /**\n * Number of failures before opening circuit\n * @default 5\n */\n failureThreshold?: number;\n\n /**\n * Time in ms before attempting to close circuit\n * @default 60000 (60 seconds)\n */\n resetTimeout?: number;\n\n /**\n * Request timeout in ms\n * @default 10000 (10 seconds)\n */\n timeout?: number;\n\n /**\n * Number of successful requests in HALF_OPEN before closing\n * @default 1\n */\n successThreshold?: number;\n\n /**\n * Fallback function when circuit is open\n */\n fallback?: (...args: any[]) => Promise<any>;\n\n /**\n * Callback when state changes\n */\n onStateChange?: (from: CircuitState, to: CircuitState) => void;\n\n /**\n * Callback on error\n */\n onError?: (error: Error) => void;\n\n /**\n * Name for logging/monitoring\n */\n name?: string;\n}\n\nexport interface CircuitBreakerStats {\n name?: string;\n state: CircuitState;\n failures: number;\n successes: number;\n totalCalls: number;\n openedAt: number | null;\n lastCallAt: number | null;\n}\n\nexport class CircuitBreakerError extends Error {\n state: CircuitState;\n\n constructor(\n message: string,\n state: CircuitState\n ) {\n super(message);\n this.name = 'CircuitBreakerError';\n this.state = state;\n }\n}\n\nexport class CircuitBreaker<T extends (...args: any[]) => Promise<any>> {\n private state: CircuitState = CircuitState.CLOSED;\n private failures: number = 0;\n private successes: number = 0;\n private totalCalls: number = 0;\n private nextAttempt: number = 0;\n private lastCallAt: number | null = null;\n private openedAt: number | null = null;\n\n private readonly failureThreshold: number;\n private readonly resetTimeout: number;\n private readonly timeout: number;\n private readonly successThreshold: number;\n private readonly fallback?: (...args: any[]) => Promise<any>;\n private readonly onStateChange?: (from: CircuitState, to: CircuitState) => void;\n private readonly onError?: (error: Error) => void;\n private readonly name: string;\n\n private readonly fn: T;\n\n constructor(\n fn: T,\n options: CircuitBreakerOptions = {}\n ) {\n this.fn = fn;\n this.failureThreshold = options.failureThreshold ?? 5;\n this.resetTimeout = options.resetTimeout ?? 60000;\n this.timeout = options.timeout ?? 10000;\n this.successThreshold = options.successThreshold ?? 1;\n this.fallback = options.fallback;\n this.onStateChange = options.onStateChange;\n this.onError = options.onError;\n this.name = options.name ?? 'CircuitBreaker';\n }\n\n /**\n * Call the wrapped function with circuit breaker protection\n */\n async call(...args: Parameters<T>): Promise<ReturnType<T>> {\n this.totalCalls++;\n this.lastCallAt = Date.now();\n\n // Check circuit state\n if (this.state === CircuitState.OPEN) {\n if (Date.now() < this.nextAttempt) {\n // Circuit still open, fail fast\n const error = new CircuitBreakerError(\n `Circuit breaker is OPEN for ${this.name}`,\n CircuitState.OPEN\n );\n\n // Use fallback if available\n if (this.fallback) {\n return this.fallback(...args);\n }\n\n throw error;\n }\n\n // Try transitioning to HALF_OPEN\n this.setState(CircuitState.HALF_OPEN);\n }\n\n try {\n // Execute with timeout\n const result = await this.executeWithTimeout(args);\n\n // Success\n this.onSuccess();\n return result;\n } catch (error) {\n // Failure\n this.onFailure(error as Error);\n throw error;\n }\n }\n\n /**\n * Execute function with timeout\n */\n private async executeWithTimeout(args: Parameters<T>): Promise<ReturnType<T>> {\n return new Promise<ReturnType<T>>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n reject(new Error(`Request timeout after ${this.timeout}ms`));\n }, this.timeout);\n\n this.fn(...args)\n .then((result) => {\n clearTimeout(timeoutId);\n resolve(result);\n })\n .catch((error) => {\n clearTimeout(timeoutId);\n reject(error);\n });\n });\n }\n\n /**\n * Handle successful call\n */\n private onSuccess(): void {\n this.failures = 0;\n this.successes++;\n\n if (this.state === CircuitState.HALF_OPEN) {\n // Check if we should close the circuit\n if (this.successes >= this.successThreshold) {\n this.setState(CircuitState.CLOSED);\n this.successes = 0;\n }\n }\n }\n\n /**\n * Handle failed call\n */\n private onFailure(error: Error): void {\n this.failures++;\n this.successes = 0;\n\n if (this.onError) {\n this.onError(error);\n }\n\n if (this.state === CircuitState.HALF_OPEN || this.failures >= this.failureThreshold) {\n this.setState(CircuitState.OPEN);\n this.nextAttempt = Date.now() + this.resetTimeout;\n this.openedAt = Date.now();\n }\n }\n\n /**\n * Change circuit state\n */\n private setState(newState: CircuitState): void {\n const oldState = this.state;\n if (oldState !== newState) {\n this.state = newState;\n\n if (this.onStateChange) {\n this.onStateChange(oldState, newState);\n }\n }\n }\n\n /**\n * Manually open the circuit\n */\n open(): void {\n this.setState(CircuitState.OPEN);\n this.nextAttempt = Date.now() + this.resetTimeout;\n this.openedAt = Date.now();\n }\n\n /**\n * Manually close the circuit\n */\n close(): void {\n this.failures = 0;\n this.successes = 0;\n this.setState(CircuitState.CLOSED);\n this.openedAt = null;\n }\n\n /**\n * Get current statistics\n */\n getStats(): CircuitBreakerStats {\n return {\n name: this.name,\n state: this.state,\n failures: this.failures,\n successes: this.successes,\n totalCalls: this.totalCalls,\n openedAt: this.openedAt,\n lastCallAt: this.lastCallAt,\n };\n }\n\n /**\n * Get current state\n */\n getState(): CircuitState {\n return this.state;\n }\n\n /**\n * Check if circuit is open\n */\n isOpen(): boolean {\n return this.state === CircuitState.OPEN;\n }\n\n /**\n * Check if circuit is closed\n */\n isClosed(): boolean {\n return this.state === CircuitState.CLOSED;\n }\n\n /**\n * Reset statistics\n */\n reset(): void {\n this.failures = 0;\n this.successes = 0;\n this.totalCalls = 0;\n this.lastCallAt = null;\n this.openedAt = null;\n this.setState(CircuitState.CLOSED);\n }\n}\n\n/**\n * Create a circuit breaker with sensible defaults\n *\n * @example\n * const emailBreaker = createCircuitBreaker(\n * async (to, subject, body) => sendEmail(to, subject, body),\n * { name: 'email-service' }\n * );\n */\nexport function createCircuitBreaker<T extends (...args: any[]) => Promise<any>>(\n fn: T,\n options?: CircuitBreakerOptions\n): CircuitBreaker<T> {\n return new CircuitBreaker(fn, options);\n}\n\n/**\n * Circuit breaker registry for managing multiple breakers\n */\nexport class CircuitBreakerRegistry {\n private breakers: Map<string, CircuitBreaker<any>> = new Map();\n\n /**\n * Register a circuit breaker\n */\n register<T extends (...args: any[]) => Promise<any>>(\n name: string,\n fn: T,\n options?: Omit<CircuitBreakerOptions, 'name'>\n ): CircuitBreaker<T> {\n const breaker = new CircuitBreaker(fn, { ...options, name });\n this.breakers.set(name, breaker);\n return breaker;\n }\n\n /**\n * Get a circuit breaker by name\n */\n get(name: string): CircuitBreaker<any> | undefined {\n return this.breakers.get(name);\n }\n\n /**\n * Get all breakers\n */\n getAll(): Map<string, CircuitBreaker<any>> {\n return this.breakers;\n }\n\n /**\n * Get statistics for all breakers\n */\n getAllStats(): Record<string, CircuitBreakerStats> {\n const stats: Record<string, CircuitBreakerStats> = {};\n for (const [name, breaker] of this.breakers.entries()) {\n stats[name] = breaker.getStats();\n }\n return stats;\n }\n\n /**\n * Reset all breakers\n */\n resetAll(): void {\n for (const breaker of this.breakers.values()) {\n breaker.reset();\n }\n }\n\n /**\n * Open all breakers\n */\n openAll(): void {\n for (const breaker of this.breakers.values()) {\n breaker.open();\n }\n }\n\n /**\n * Close all breakers\n */\n closeAll(): void {\n for (const breaker of this.breakers.values()) {\n breaker.close();\n }\n }\n}\n\n/**\n * Create a new CircuitBreakerRegistry instance.\n * Use this instead of a global singleton — attach to fastify.arc or pass explicitly.\n */\nexport function createCircuitBreakerRegistry(): CircuitBreakerRegistry {\n return new CircuitBreakerRegistry();\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"describe.d.mts","names":[],"sources":["../../../src/cli/commands/describe.ts"],"mappings":";;AAmVA;;;;;;;;;;;;;iBAAsB,QAAA,CAAS,IAAA,aAAiB,OAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"describe.mjs","names":[],"sources":["../../../src/cli/commands/describe.ts"],"sourcesContent":["/**\n * Arc CLI - Describe Command\n *\n * Machine-readable resource metadata for AI agents.\n * Outputs JSON with fields, permissions, pipeline, routes, events —\n * everything an LLM needs to understand and generate code for the API.\n *\n * @example\n * ```bash\n * arc describe ./src/resources.js --json\n * arc describe ./src/resources.js --resource product\n * arc describe ./src/resources.js --pretty\n * ```\n */\n\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { ResourceDefinition } from '../../core/defineResource.js';\nimport type {\n AnyRecord,\n ResourcePermissions,\n RouteSchemaOptions,\n EventDefinition,\n RateLimitConfig,\n MiddlewareConfig,\n} from '../../types/index.js';\nimport type { PermissionCheck } from '../../permissions/types.js';\nimport type { FieldPermissionMap, FieldPermission } from '../../permissions/fields.js';\nimport type { PipelineConfig, PipelineStep } from '../../pipeline/types.js';\nimport { CRUD_OPERATIONS } from '../../constants.js';\n\n// ---------------------------------------------------------------------------\n// Output Schema\n// ---------------------------------------------------------------------------\n\ninterface DescribeOutput {\n $schema: 'arc-describe/v1';\n generatedAt: string;\n resources: DescribedResource[];\n stats: DescribeStats;\n}\n\ninterface DescribedResource {\n name: string;\n displayName: string;\n prefix: string;\n tag: string;\n module?: string;\n adapter: { type: string; name: string } | null;\n\n permissions: Record<string, PermissionDescription>;\n presets: string[];\n\n fields?: Record<string, FieldDescription>;\n pipeline?: PipelineDescription;\n\n routes: RouteDescription[];\n events: EventDescription[];\n\n schemaOptions?: RouteSchemaOptions;\n rateLimit?: RateLimitConfig | false;\n middlewares: string[];\n}\n\ninterface PermissionDescription {\n type: 'public' | 'requireAuth' | 'requireRoles' | 'custom';\n roles?: readonly string[];\n}\n\ninterface FieldDescription {\n type: string;\n roles?: readonly string[];\n redactValue?: unknown;\n}\n\ninterface PipelineDescription {\n guards: PipelineStepDescription[];\n transforms: PipelineStepDescription[];\n interceptors: PipelineStepDescription[];\n}\n\ninterface PipelineStepDescription {\n name: string;\n operations?: string[];\n}\n\ninterface RouteDescription {\n method: string;\n path: string;\n operation: string;\n summary?: string;\n description?: string;\n permission?: PermissionDescription;\n}\n\ninterface EventDescription {\n name: string;\n description?: string;\n hasSchema: boolean;\n}\n\ninterface DescribeStats {\n totalResources: number;\n totalRoutes: number;\n totalEvents: number;\n totalFields: number;\n presetUsage: Record<string, number>;\n pipelineSteps: number;\n}\n\n// ---------------------------------------------------------------------------\n// Permission Introspection\n// ---------------------------------------------------------------------------\n\nfunction describePermission(check: unknown): PermissionDescription {\n if (!check || typeof check !== 'function') {\n return { type: 'custom' };\n }\n\n const fn = check as PermissionCheck;\n\n // allowPublic() sets _isPublic = true\n if (fn._isPublic === true) {\n return { type: 'public' };\n }\n\n // requireRoles() sets _roles = [...]\n if (Array.isArray(fn._roles)) {\n return { type: 'requireRoles', roles: fn._roles as string[] };\n }\n\n // requireAuth() — function that checks ctx.user\n // Infer from function source as a best-effort heuristic\n const src = check.toString();\n if (src.includes('ctx.user') && !src.includes('roles') && src.length < 200) {\n return { type: 'requireAuth' };\n }\n\n return { type: 'custom' };\n}\n\nfunction describePermissions(perms?: ResourcePermissions): Record<string, PermissionDescription> {\n if (!perms) return {};\n\n const result: Record<string, PermissionDescription> = {};\n for (const op of CRUD_OPERATIONS) {\n const check = perms[op];\n if (check) {\n result[op] = describePermission(check);\n }\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Field Permission Introspection\n// ---------------------------------------------------------------------------\n\nfunction describeFields(fieldPerms?: FieldPermissionMap): Record<string, FieldDescription> | undefined {\n if (!fieldPerms || Object.keys(fieldPerms).length === 0) return undefined;\n\n const result: Record<string, FieldDescription> = {};\n for (const [field, perm] of Object.entries(fieldPerms)) {\n const desc: FieldDescription = { type: perm._type };\n if (perm.roles?.length) desc.roles = perm.roles;\n if (perm.redactValue !== undefined) desc.redactValue = perm.redactValue;\n result[field] = desc;\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Pipeline Introspection\n// ---------------------------------------------------------------------------\n\nfunction describePipeline(pipe?: PipelineConfig): PipelineDescription | undefined {\n if (!pipe) return undefined;\n\n const steps: PipelineStep[] = [];\n\n if (Array.isArray(pipe)) {\n steps.push(...pipe);\n } else {\n // Per-operation map — collect all unique steps\n const seen = new Set<string>();\n for (const opSteps of Object.values(pipe)) {\n if (Array.isArray(opSteps)) {\n for (const step of opSteps) {\n const key = `${step._type}:${step.name}`;\n if (!seen.has(key)) {\n seen.add(key);\n steps.push(step);\n }\n }\n }\n }\n }\n\n if (steps.length === 0) return undefined;\n\n const guards: PipelineStepDescription[] = [];\n const transforms: PipelineStepDescription[] = [];\n const interceptors: PipelineStepDescription[] = [];\n\n for (const step of steps) {\n const desc: PipelineStepDescription = { name: step.name };\n if (step.operations?.length) desc.operations = [...step.operations];\n\n switch (step._type) {\n case 'guard': guards.push(desc); break;\n case 'transform': transforms.push(desc); break;\n case 'interceptor': interceptors.push(desc); break;\n }\n }\n\n return { guards, transforms, interceptors };\n}\n\n// ---------------------------------------------------------------------------\n// Route Introspection\n// ---------------------------------------------------------------------------\n\nfunction describeRoutes(resource: ResourceDefinition<unknown>): RouteDescription[] {\n const routes: RouteDescription[] = [];\n\n // Default CRUD routes\n if (!resource.disableDefaultRoutes) {\n const disabled = new Set(resource.disabledRoutes ?? []);\n const crudOps = [\n { method: 'GET', suffix: '', op: 'list' },\n { method: 'GET', suffix: '/:id', op: 'get' },\n { method: 'POST', suffix: '', op: 'create' },\n { method: 'PATCH', suffix: '/:id', op: 'update' },\n { method: 'DELETE', suffix: '/:id', op: 'delete' },\n ] as const;\n\n for (const { method, suffix, op } of crudOps) {\n if (!disabled.has(op)) {\n const route: RouteDescription = {\n method,\n path: `${resource.prefix}${suffix}`,\n operation: op,\n };\n const perm = resource.permissions[op];\n if (perm) route.permission = describePermission(perm);\n routes.push(route);\n }\n }\n }\n\n // Additional routes\n for (const ar of resource.additionalRoutes) {\n routes.push({\n method: ar.method,\n path: `${resource.prefix}${ar.path}`,\n operation: typeof ar.handler === 'string' ? ar.handler : 'custom',\n summary: ar.summary,\n description: ar.description,\n permission: describePermission(ar.permissions),\n });\n }\n\n return routes;\n}\n\n// ---------------------------------------------------------------------------\n// Event Introspection\n// ---------------------------------------------------------------------------\n\nfunction describeEvents(\n resourceName: string,\n events?: Record<string, EventDefinition>,\n): EventDescription[] {\n if (!events) return [];\n\n return Object.entries(events).map(([action, def]) => ({\n name: `${resourceName}:${action}`,\n description: def.description,\n hasSchema: !!def.schema,\n }));\n}\n\n// ---------------------------------------------------------------------------\n// Middleware Introspection\n// ---------------------------------------------------------------------------\n\nfunction describeMiddlewares(middlewares?: MiddlewareConfig): string[] {\n if (!middlewares) return [];\n\n const ops: string[] = [];\n for (const [op, handlers] of Object.entries(middlewares)) {\n if (handlers?.length) {\n ops.push(`${op}(${handlers.length})`);\n }\n }\n return ops;\n}\n\n// ---------------------------------------------------------------------------\n// Main Describe Function\n// ---------------------------------------------------------------------------\n\nfunction describeResource(\n resource: ResourceDefinition<unknown>,\n module?: string,\n): DescribedResource {\n return {\n name: resource.name,\n displayName: resource.displayName,\n prefix: resource.prefix,\n tag: resource.tag,\n module,\n\n adapter: resource.adapter\n ? { type: resource.adapter.type, name: resource.adapter.name }\n : null,\n\n permissions: describePermissions(resource.permissions),\n presets: resource._appliedPresets ?? [],\n\n fields: describeFields(resource.fields),\n pipeline: describePipeline(resource.pipe),\n\n routes: describeRoutes(resource),\n events: describeEvents(resource.name, resource.events),\n\n schemaOptions: Object.keys(resource.schemaOptions ?? {}).length > 0\n ? resource.schemaOptions\n : undefined,\n rateLimit: resource.rateLimit,\n middlewares: describeMiddlewares(resource.middlewares),\n };\n}\n\n// ---------------------------------------------------------------------------\n// CLI Entry\n// ---------------------------------------------------------------------------\n\nexport async function describe(args: string[]): Promise<void> {\n try {\n // Parse flags\n const flags = new Set(args.filter(a => a.startsWith('--')));\n const positional = args.filter(a => !a.startsWith('--'));\n const pretty = flags.has('--pretty') || !flags.has('--json');\n const filterResource = positional[1]; // optional resource name filter\n\n const entryPath = positional[0];\n if (!entryPath) {\n console.log('Usage: arc describe <entry-file> [resource-name] [--json] [--pretty]\\n');\n console.log('Outputs machine-readable JSON metadata for AI agents.\\n');\n console.log('Options:');\n console.log(' --json Output compact JSON (default if piped)');\n console.log(' --pretty Output formatted JSON (default if terminal)');\n console.log('\\nExamples:');\n console.log(' arc describe ./src/resources.js');\n console.log(' arc describe ./src/resources.js product');\n console.log(' arc describe ./src/resources.js --json | jq .');\n return;\n }\n\n // Dynamically import user's entry file (pathToFileURL needed for Windows)\n const entryFileUrl = pathToFileURL(resolve(process.cwd(), entryPath)).href;\n const entryModule = await import(entryFileUrl);\n\n // Collect ResourceDefinition objects\n // Also handles arrays of resources (e.g. `export const resources = [r1, r2]`)\n const resources: ResourceDefinition<unknown>[] = [];\n\n function tryCollect(value: unknown): void {\n if (\n value &&\n typeof value === 'object' &&\n 'name' in value &&\n '_registryMeta' in value &&\n 'toPlugin' in value\n ) {\n resources.push(value as ResourceDefinition<unknown>);\n }\n }\n\n for (const exported of Object.values(entryModule)) {\n if (Array.isArray(exported)) {\n exported.forEach(tryCollect);\n } else {\n tryCollect(exported);\n }\n }\n\n if (resources.length === 0) {\n throw new Error(\n 'No resource definitions found in entry file.\\nMake sure your file exports defineResource() results:\\n export const productResource = defineResource({ ... });',\n );\n }\n\n // Filter to single resource if requested\n const filtered = filterResource\n ? resources.filter(r => r.name === filterResource)\n : resources;\n\n if (filterResource && filtered.length === 0) {\n throw new Error(\n `Resource '${filterResource}' not found.\\nAvailable: ${resources.map(r => r.name).join(', ')}`,\n );\n }\n\n // Build described resources\n const described = filtered.map(r =>\n describeResource(r, (r._registryMeta as AnyRecord | undefined)?.module as string | undefined),\n );\n\n // Compute stats\n const presetCounts: Record<string, number> = {};\n let totalPipelineSteps = 0;\n let totalFields = 0;\n\n for (const res of described) {\n for (const preset of res.presets) {\n presetCounts[preset] = (presetCounts[preset] ?? 0) + 1;\n }\n if (res.pipeline) {\n totalPipelineSteps += res.pipeline.guards.length\n + res.pipeline.transforms.length\n + res.pipeline.interceptors.length;\n }\n if (res.fields) {\n totalFields += Object.keys(res.fields).length;\n }\n }\n\n const output: DescribeOutput = {\n $schema: 'arc-describe/v1',\n generatedAt: new Date().toISOString(),\n resources: described,\n stats: {\n totalResources: described.length,\n totalRoutes: described.reduce((sum, r) => sum + r.routes.length, 0),\n totalEvents: described.reduce((sum, r) => sum + r.events.length, 0),\n totalFields,\n presetUsage: presetCounts,\n pipelineSteps: totalPipelineSteps,\n },\n };\n\n // Output\n const json = pretty\n ? JSON.stringify(output, null, 2)\n : JSON.stringify(output);\n\n console.log(json);\n } catch (error: unknown) {\n if (error instanceof Error) throw error;\n throw new Error(String(error));\n }\n}\n\nexport default describe;\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"docs.d.mts","names":[],"sources":["../../../src/cli/commands/docs.ts"],"mappings":";;AAyBA;;;;;iBAAsB,UAAA,CAAW,IAAA,aAAiB,OAAA;AAAA,cA8DjD,QAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"docs.mjs","names":[],"sources":["../../../src/cli/commands/docs.ts"],"sourcesContent":["/**\n * Arc CLI - Docs Command\n *\n * Export OpenAPI specification from registered resources.\n * Requires an entry file that exports defineResource() results.\n */\n\nimport { writeFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { ResourceRegistry } from '../../registry/index.js';\nimport type { RegistryEntry } from '../../types/index.js';\nimport { buildOpenApiSpec } from '../../docs/openapi.js';\n\ninterface ParsedDocsArgs {\n entryPath?: string;\n outputPath: string;\n}\n\nfunction parseDocsArgs(args: string[]): ParsedDocsArgs {\n const outputPath = args.find((a) => a.endsWith('.json')) ?? './openapi.json';\n const entryPath = args.find((a) => !a.endsWith('.json'));\n return { entryPath, outputPath };\n}\n\nexport async function exportDocs(args: string[]): Promise<void> {\n const { entryPath, outputPath } = parseDocsArgs(args);\n\n console.log('Exporting OpenAPI specification...\\n');\n\n if (!entryPath) {\n throw new Error(\n 'Missing entry file.\\n\\nUsage: arc docs <entry-file> [output.json]\\nExample: arc docs ./src/resources.js ./openapi.json',\n );\n }\n\n // Dynamically import user's entry file\n const entryFileUrl = pathToFileURL(resolve(process.cwd(), entryPath)).href;\n const entryModule = await import(entryFileUrl);\n\n // Collect ResourceDefinition objects from exports\n // Also handles arrays of resources (e.g. `export const resources = [r1, r2]`)\n const registry = new ResourceRegistry();\n let registered = 0;\n\n function tryRegister(value: unknown): void {\n if (\n value &&\n typeof value === 'object' &&\n 'name' in value &&\n '_registryMeta' in value &&\n 'toPlugin' in value\n ) {\n registry.register(value as any, (value as any)._registryMeta ?? {});\n registered++;\n }\n }\n\n for (const exported of Object.values(entryModule)) {\n if (Array.isArray(exported)) {\n exported.forEach(tryRegister);\n } else {\n tryRegister(exported);\n }\n }\n\n if (registered === 0) {\n throw new Error(\n 'No resource definitions found in entry file.\\nMake sure your file exports defineResource() results:\\n export const productResource = defineResource({ ... });',\n );\n }\n\n const resources: RegistryEntry[] = registry.getAll();\n\n const spec = buildOpenApiSpec(resources, {\n title: 'Arc API',\n version: '1.0.0',\n description: 'Auto-generated from Arc resources',\n });\n\n // Write to file (resolve handles both relative and absolute paths)\n const fullPath = resolve(process.cwd(), outputPath);\n writeFileSync(fullPath, JSON.stringify(spec, null, 2));\n\n console.log(`OpenAPI spec exported to: ${fullPath}`);\n console.log(`\\nResources included: ${resources.length}`);\n console.log(`Total endpoints: ${Object.keys(spec.paths).length}`);\n}\n\nexport default { exportDocs };\n"],"mappings":";;;;;;;;;;;;;AAmBA,SAAS,cAAc,MAAgC;CACrD,MAAM,aAAa,KAAK,MAAM,MAAM,EAAE,SAAS,QAAQ,CAAC,IAAI;AAE5D,QAAO;EAAE,WADS,KAAK,MAAM,MAAM,CAAC,EAAE,SAAS,QAAQ,CAAC;EACpC;EAAY;;AAGlC,eAAsB,WAAW,MAA+B;CAC9D,MAAM,EAAE,WAAW,eAAe,cAAc,KAAK;AAErD,SAAQ,IAAI,uCAAuC;AAEnD,KAAI,CAAC,UACH,OAAM,IAAI,MACR,yHACD;CAKH,MAAM,cAAc,MAAM,OADL,cAAc,QAAQ,QAAQ,KAAK,EAAE,UAAU,CAAC,CAAC;CAKtE,MAAM,WAAW,IAAI,kBAAkB;CACvC,IAAI,aAAa;CAEjB,SAAS,YAAY,OAAsB;AACzC,MACE,SACA,OAAO,UAAU,YACjB,UAAU,SACV,mBAAmB,SACnB,cAAc,OACd;AACA,YAAS,SAAS,OAAe,MAAc,iBAAiB,EAAE,CAAC;AACnE;;;AAIJ,MAAK,MAAM,YAAY,OAAO,OAAO,YAAY,CAC/C,KAAI,MAAM,QAAQ,SAAS,CACzB,UAAS,QAAQ,YAAY;KAE7B,aAAY,SAAS;AAIzB,KAAI,eAAe,EACjB,OAAM,IAAI,MACR,iKACD;CAGH,MAAM,YAA6B,SAAS,QAAQ;CAEpD,MAAM,OAAO,iBAAiB,WAAW;EACvC,OAAO;EACP,SAAS;EACT,aAAa;EACd,CAAC;CAGF,MAAM,WAAW,QAAQ,QAAQ,KAAK,EAAE,WAAW;AACnD,eAAc,UAAU,KAAK,UAAU,MAAM,MAAM,EAAE,CAAC;AAEtD,SAAQ,IAAI,6BAA6B,WAAW;AACpD,SAAQ,IAAI,yBAAyB,UAAU,SAAS;AACxD,SAAQ,IAAI,oBAAoB,OAAO,KAAK,KAAK,MAAM,CAAC,SAAS;;AAGnE,mBAAe,EAAE,YAAY"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"generate.d.mts","names":[],"sources":["../../../src/cli/commands/generate.ts"],"mappings":";;AAmRA;;;;;;;;;;;;iBAAsB,QAAA,CACpB,IAAA,sBACA,IAAA,aACC,OAAA"}
|