@classytic/arc 1.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
- package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
- package/dist/HookSystem-BsGV-j2l.mjs +405 -0
- package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +82 -0
- package/dist/audit/index.d.mts.map +1 -0
- package/dist/audit/index.mjs +276 -0
- package/dist/audit/index.mjs.map +1 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-C3T5DTUx.mjs +141 -0
- package/dist/audited-C3T5DTUx.mjs.map +1 -0
- package/dist/auth/index.d.mts +189 -0
- package/dist/auth/index.d.mts.map +1 -0
- package/dist/auth/index.mjs +1102 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth/redis-session.d.mts +44 -0
- package/dist/auth/redis-session.d.mts.map +1 -0
- package/dist/auth/redis-session.mjs +76 -0
- package/dist/auth/redis-session.mjs.map +1 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
- package/dist/cache/index.d.mts +146 -0
- package/dist/cache/index.d.mts.map +1 -0
- package/dist/cache/index.mjs +92 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/caching-Bl28lYsR.mjs +94 -0
- package/dist/caching-Bl28lYsR.mjs.map +1 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
- package/dist/cli/commands/describe.d.mts +19 -0
- package/dist/cli/commands/describe.d.mts.map +1 -0
- package/dist/cli/commands/describe.mjs +239 -0
- package/dist/cli/commands/describe.mjs.map +1 -0
- package/dist/cli/commands/docs.d.mts +14 -0
- package/dist/cli/commands/docs.d.mts.map +1 -0
- package/dist/cli/commands/docs.mjs +53 -0
- package/dist/cli/commands/docs.mjs.map +1 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
- package/dist/cli/commands/generate.d.mts.map +1 -0
- package/dist/cli/commands/generate.mjs +358 -0
- package/dist/cli/commands/generate.mjs.map +1 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
- package/dist/cli/commands/init.d.mts.map +1 -0
- package/dist/cli/commands/{init.js → init.mjs} +807 -616
- package/dist/cli/commands/init.mjs.map +1 -0
- package/dist/cli/commands/introspect.d.mts +11 -0
- package/dist/cli/commands/introspect.d.mts.map +1 -0
- package/dist/cli/commands/introspect.mjs +76 -0
- package/dist/cli/commands/introspect.mjs.map +1 -0
- package/dist/cli/index.d.mts +17 -0
- package/dist/cli/index.d.mts.map +1 -0
- package/dist/cli/index.mjs +157 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/constants-DdXFXQtN.mjs +85 -0
- package/dist/constants-DdXFXQtN.mjs.map +1 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-CUgNqegw.mjs +560 -0
- package/dist/createApp-CUgNqegw.mjs.map +1 -0
- package/dist/defineResource-k0_BDn8v.mjs +2197 -0
- package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
- package/dist/discovery/index.d.mts +47 -0
- package/dist/discovery/index.d.mts.map +1 -0
- package/dist/discovery/index.mjs +110 -0
- package/dist/discovery/index.mjs.map +1 -0
- package/dist/docs/index.d.mts +163 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +73 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/elevation-BRy3yFWT.mjs +113 -0
- package/dist/elevation-BRy3yFWT.mjs.map +1 -0
- package/dist/elevation-B_2dRLVP.d.mts +88 -0
- package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
- package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
- package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
- package/dist/errorHandler-C1okiriz.mjs +109 -0
- package/dist/errorHandler-C1okiriz.mjs.map +1 -0
- package/dist/errors-B9bZok84.mjs +212 -0
- package/dist/errors-B9bZok84.mjs.map +1 -0
- package/dist/errors-ChKiFz62.d.mts +125 -0
- package/dist/errors-ChKiFz62.d.mts.map +1 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
- package/dist/eventPlugin-DGR_B2on.mjs +230 -0
- package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
- package/dist/events/index.d.mts +54 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +52 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +178 -0
- package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
- package/dist/events/transports/redis.d.mts +77 -0
- package/dist/events/transports/redis.d.mts.map +1 -0
- package/dist/events/transports/redis.mjs +125 -0
- package/dist/events/transports/redis.mjs.map +1 -0
- package/dist/externalPaths-DlINfKbP.d.mts +51 -0
- package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
- package/dist/factory/index.d.mts +64 -0
- package/dist/factory/index.d.mts.map +1 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
- package/dist/fields-DyaDVX4J.d.mts +110 -0
- package/dist/fields-DyaDVX4J.d.mts.map +1 -0
- package/dist/fields-iagOozy0.mjs +115 -0
- package/dist/fields-iagOozy0.mjs.map +1 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +97 -0
- package/dist/idempotency/index.d.mts.map +1 -0
- package/dist/idempotency/index.mjs +320 -0
- package/dist/idempotency/index.mjs.map +1 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +115 -0
- package/dist/idempotency/mongodb.mjs.map +1 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +104 -0
- package/dist/idempotency/redis.mjs.map +1 -0
- package/dist/index.d.mts +261 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +105 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations/event-gateway.d.mts +47 -0
- package/dist/integrations/event-gateway.d.mts.map +1 -0
- package/dist/integrations/event-gateway.mjs +44 -0
- package/dist/integrations/event-gateway.mjs.map +1 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +104 -0
- package/dist/integrations/jobs.d.mts.map +1 -0
- package/dist/integrations/jobs.mjs +124 -0
- package/dist/integrations/jobs.mjs.map +1 -0
- package/dist/integrations/streamline.d.mts +61 -0
- package/dist/integrations/streamline.d.mts.map +1 -0
- package/dist/integrations/streamline.mjs +126 -0
- package/dist/integrations/streamline.mjs.map +1 -0
- package/dist/integrations/websocket.d.mts +83 -0
- package/dist/integrations/websocket.d.mts.map +1 -0
- package/dist/integrations/websocket.mjs +289 -0
- package/dist/integrations/websocket.mjs.map +1 -0
- package/dist/interface-B01JvPVc.d.mts +78 -0
- package/dist/interface-B01JvPVc.d.mts.map +1 -0
- package/dist/interface-CZe8IkMf.d.mts +55 -0
- package/dist/interface-CZe8IkMf.d.mts.map +1 -0
- package/dist/interface-Ch8HU9uM.d.mts +1098 -0
- package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
- package/dist/keys-BqNejWup.mjs +43 -0
- package/dist/keys-BqNejWup.mjs.map +1 -0
- package/dist/logger-Df2O2WsW.mjs +79 -0
- package/dist/logger-Df2O2WsW.mjs.map +1 -0
- package/dist/memory-cQgelFOj.mjs +144 -0
- package/dist/memory-cQgelFOj.mjs.map +1 -0
- package/dist/migrations/index.d.mts +157 -0
- package/dist/migrations/index.d.mts.map +1 -0
- package/dist/migrations/index.mjs +261 -0
- package/dist/migrations/index.mjs.map +1 -0
- package/dist/mongodb-BfJVlUJH.mjs +94 -0
- package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
- package/dist/mongodb-CGzRbfAK.d.mts +119 -0
- package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
- package/dist/mongodb-JN-9JA7K.d.mts +72 -0
- package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
- package/dist/openapi-G3Cw7XuM.mjs +524 -0
- package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
- package/dist/org/index.d.mts +69 -0
- package/dist/org/index.d.mts.map +1 -0
- package/dist/org/index.mjs +514 -0
- package/dist/org/index.mjs.map +1 -0
- package/dist/org/types.d.mts +83 -0
- package/dist/org/types.d.mts.map +1 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +279 -0
- package/dist/permissions/index.d.mts.map +1 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/permissions/index.mjs.map +1 -0
- package/dist/plugins/index.d.mts +173 -0
- package/dist/plugins/index.d.mts.map +1 -0
- package/dist/plugins/index.mjs +523 -0
- package/dist/plugins/index.mjs.map +1 -0
- package/dist/plugins/response-cache.d.mts +88 -0
- package/dist/plugins/response-cache.d.mts.map +1 -0
- package/dist/plugins/response-cache.mjs +284 -0
- package/dist/plugins/response-cache.mjs.map +1 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +186 -0
- package/dist/plugins/tracing-entry.mjs.map +1 -0
- package/dist/pluralize-CEweyOEm.mjs +87 -0
- package/dist/pluralize-CEweyOEm.mjs.map +1 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -169
- package/dist/policies/index.d.mts.map +1 -0
- package/dist/policies/index.mjs +322 -0
- package/dist/policies/index.mjs.map +1 -0
- package/dist/presets/{index.d.ts → index.d.mts} +63 -131
- package/dist/presets/index.d.mts.map +1 -0
- package/dist/presets/index.mjs +144 -0
- package/dist/presets/index.mjs.map +1 -0
- package/dist/presets/multiTenant.d.mts +25 -0
- package/dist/presets/multiTenant.d.mts.map +1 -0
- package/dist/presets/multiTenant.mjs +114 -0
- package/dist/presets/multiTenant.mjs.map +1 -0
- package/dist/presets-BITljm96.mjs +120 -0
- package/dist/presets-BITljm96.mjs.map +1 -0
- package/dist/presets-DzSMwlKj.d.mts +58 -0
- package/dist/presets-DzSMwlKj.d.mts.map +1 -0
- package/dist/prisma-DJbMt3yf.mjs +628 -0
- package/dist/prisma-DJbMt3yf.mjs.map +1 -0
- package/dist/prisma-Dg9GoVdj.d.mts +275 -0
- package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
- package/dist/redis-D-JAeLtm.d.mts +50 -0
- package/dist/redis-D-JAeLtm.d.mts.map +1 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
- package/dist/registry/index.d.mts +12 -0
- package/dist/registry/index.d.mts.map +1 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-QQD6ROJc.mjs +56 -0
- package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
- package/dist/schemaConverter-BwrmWroW.mjs +99 -0
- package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
- package/dist/schemas/index.d.mts +64 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +83 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/scope/index.d.mts +22 -0
- package/dist/scope/index.d.mts.map +1 -0
- package/dist/scope/index.mjs +66 -0
- package/dist/scope/index.mjs.map +1 -0
- package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
- package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
- package/dist/sse-B3c3_yZp.mjs +124 -0
- package/dist/sse-B3c3_yZp.mjs.map +1 -0
- package/dist/testing/index.d.mts +908 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +1977 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/tracing-Cc7vVQPp.d.mts +71 -0
- package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
- package/dist/typeGuards-DhMNLuvU.mjs +10 -0
- package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
- package/dist/types/index.d.mts +947 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +15 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-Beqn1Un7.mjs +39 -0
- package/dist/types-Beqn1Un7.mjs.map +1 -0
- package/dist/types-CIgB7UUl.d.mts +446 -0
- package/dist/types-CIgB7UUl.d.mts.map +1 -0
- package/dist/types-aYB4V7uN.d.mts +87 -0
- package/dist/types-aYB4V7uN.d.mts.map +1 -0
- package/dist/utils/index.d.mts +748 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +6 -0
- package/package.json +194 -68
- package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
- package/dist/adapters/index.d.ts +0 -237
- package/dist/adapters/index.js +0 -668
- package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
- package/dist/audit/index.d.ts +0 -195
- package/dist/audit/index.js +0 -319
- package/dist/auth/index.d.ts +0 -47
- package/dist/auth/index.js +0 -174
- package/dist/cli/commands/docs.d.ts +0 -11
- package/dist/cli/commands/docs.js +0 -474
- package/dist/cli/commands/generate.js +0 -334
- package/dist/cli/commands/introspect.d.ts +0 -8
- package/dist/cli/commands/introspect.js +0 -338
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.js +0 -3269
- package/dist/core/index.d.ts +0 -220
- package/dist/core/index.js +0 -2786
- package/dist/createApp-Ce9wl8W9.d.ts +0 -77
- package/dist/docs/index.d.ts +0 -166
- package/dist/docs/index.js +0 -658
- package/dist/errors-8WIxGS_6.d.ts +0 -122
- package/dist/events/index.d.ts +0 -117
- package/dist/events/index.js +0 -89
- package/dist/factory/index.d.ts +0 -38
- package/dist/factory/index.js +0 -1652
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -199
- package/dist/idempotency/index.d.ts +0 -323
- package/dist/idempotency/index.js +0 -500
- package/dist/index-B4t03KQ0.d.ts +0 -1366
- package/dist/index.d.ts +0 -135
- package/dist/index.js +0 -4756
- package/dist/migrations/index.d.ts +0 -185
- package/dist/migrations/index.js +0 -274
- package/dist/org/index.d.ts +0 -129
- package/dist/org/index.js +0 -220
- package/dist/permissions/index.d.ts +0 -144
- package/dist/permissions/index.js +0 -103
- package/dist/plugins/index.d.ts +0 -46
- package/dist/plugins/index.js +0 -1069
- package/dist/policies/index.js +0 -196
- package/dist/presets/index.js +0 -384
- package/dist/presets/multiTenant.d.ts +0 -39
- package/dist/presets/multiTenant.js +0 -112
- package/dist/registry/index.d.ts +0 -16
- package/dist/registry/index.js +0 -253
- package/dist/testing/index.d.ts +0 -618
- package/dist/testing/index.js +0 -48020
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -8
- package/dist/types-B99TBmFV.d.ts +0 -76
- package/dist/types-BvckRbs2.d.ts +0 -143
- package/dist/utils/index.d.ts +0 -679
- package/dist/utils/index.js +0 -931
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { i as SessionData, s as SessionStore } from "../sessionManager-jPKLbHE0.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/auth/redis-session.d.ts
|
|
4
|
+
/** Minimal Redis client interface — compatible with ioredis */
|
|
5
|
+
interface RedisLike {
|
|
6
|
+
get(key: string): Promise<string | null>;
|
|
7
|
+
set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
|
|
8
|
+
del(...keys: string[]): Promise<number>;
|
|
9
|
+
smembers(key: string): Promise<string[]>;
|
|
10
|
+
sadd(key: string, ...members: string[]): Promise<number>;
|
|
11
|
+
srem(key: string, ...members: string[]): Promise<number>;
|
|
12
|
+
expire(key: string, seconds: number): Promise<number>;
|
|
13
|
+
}
|
|
14
|
+
interface RedisSessionStoreOptions {
|
|
15
|
+
/** Redis client instance (ioredis or compatible) */
|
|
16
|
+
redis: RedisLike;
|
|
17
|
+
/** Key prefix for session keys (default: 'arc:session:') */
|
|
18
|
+
prefix?: string;
|
|
19
|
+
/** Key prefix for user-to-sessions index (default: 'arc:user-sessions:') */
|
|
20
|
+
userPrefix?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Redis-backed session store for distributed deployments.
|
|
24
|
+
*
|
|
25
|
+
* Uses two key patterns:
|
|
26
|
+
* - `{prefix}{sessionId}` — stores serialized SessionData with TTL
|
|
27
|
+
* - `{userPrefix}{userId}` — Redis Set of sessionIds for bulk operations
|
|
28
|
+
*
|
|
29
|
+
* Session expiration is handled by Redis TTL — no cleanup interval needed.
|
|
30
|
+
*/
|
|
31
|
+
declare class RedisSessionStore implements SessionStore {
|
|
32
|
+
private redis;
|
|
33
|
+
private prefix;
|
|
34
|
+
private userPrefix;
|
|
35
|
+
constructor(options: RedisSessionStoreOptions);
|
|
36
|
+
get(sessionId: string): Promise<SessionData | null>;
|
|
37
|
+
set(sessionId: string, data: SessionData): Promise<void>;
|
|
38
|
+
delete(sessionId: string): Promise<void>;
|
|
39
|
+
deleteAll(userId: string): Promise<void>;
|
|
40
|
+
deleteAllExcept(userId: string, currentSessionId: string): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { RedisLike, RedisSessionStore, RedisSessionStoreOptions };
|
|
44
|
+
//# sourceMappingURL=redis-session.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region src/auth/redis-session.ts
|
|
2
|
+
/**
|
|
3
|
+
* Redis-backed session store for distributed deployments.
|
|
4
|
+
*
|
|
5
|
+
* Uses two key patterns:
|
|
6
|
+
* - `{prefix}{sessionId}` — stores serialized SessionData with TTL
|
|
7
|
+
* - `{userPrefix}{userId}` — Redis Set of sessionIds for bulk operations
|
|
8
|
+
*
|
|
9
|
+
* Session expiration is handled by Redis TTL — no cleanup interval needed.
|
|
10
|
+
*/
|
|
11
|
+
var RedisSessionStore = class {
|
|
12
|
+
redis;
|
|
13
|
+
prefix;
|
|
14
|
+
userPrefix;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.redis = options.redis;
|
|
17
|
+
this.prefix = options.prefix ?? "arc:session:";
|
|
18
|
+
this.userPrefix = options.userPrefix ?? "arc:user-sessions:";
|
|
19
|
+
}
|
|
20
|
+
async get(sessionId) {
|
|
21
|
+
const raw = await this.redis.get(this.prefix + sessionId);
|
|
22
|
+
if (!raw) return null;
|
|
23
|
+
let session;
|
|
24
|
+
try {
|
|
25
|
+
session = JSON.parse(raw);
|
|
26
|
+
} catch {
|
|
27
|
+
await this.delete(sessionId);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (Date.now() > session.expiresAt) {
|
|
31
|
+
await this.delete(sessionId);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return session;
|
|
35
|
+
}
|
|
36
|
+
async set(sessionId, data) {
|
|
37
|
+
const ttlMs = data.expiresAt - Date.now();
|
|
38
|
+
if (ttlMs <= 0) return;
|
|
39
|
+
const ttlSeconds = Math.ceil(ttlMs / 1e3);
|
|
40
|
+
const serialized = JSON.stringify(data);
|
|
41
|
+
await this.redis.set(this.prefix + sessionId, serialized, "EX", ttlSeconds);
|
|
42
|
+
const userKey = this.userPrefix + data.userId;
|
|
43
|
+
await this.redis.sadd(userKey, sessionId);
|
|
44
|
+
await this.redis.expire(userKey, ttlSeconds + 3600);
|
|
45
|
+
}
|
|
46
|
+
async delete(sessionId) {
|
|
47
|
+
const raw = await this.redis.get(this.prefix + sessionId);
|
|
48
|
+
if (raw) try {
|
|
49
|
+
const session = JSON.parse(raw);
|
|
50
|
+
await this.redis.srem(this.userPrefix + session.userId, sessionId);
|
|
51
|
+
} catch {}
|
|
52
|
+
await this.redis.del(this.prefix + sessionId);
|
|
53
|
+
}
|
|
54
|
+
async deleteAll(userId) {
|
|
55
|
+
const userKey = this.userPrefix + userId;
|
|
56
|
+
const sessionIds = await this.redis.smembers(userKey);
|
|
57
|
+
if (sessionIds.length > 0) {
|
|
58
|
+
const keys = sessionIds.map((id) => this.prefix + id);
|
|
59
|
+
await this.redis.del(...keys);
|
|
60
|
+
}
|
|
61
|
+
await this.redis.del(userKey);
|
|
62
|
+
}
|
|
63
|
+
async deleteAllExcept(userId, currentSessionId) {
|
|
64
|
+
const userKey = this.userPrefix + userId;
|
|
65
|
+
const toDelete = (await this.redis.smembers(userKey)).filter((id) => id !== currentSessionId);
|
|
66
|
+
if (toDelete.length > 0) {
|
|
67
|
+
const keys = toDelete.map((id) => this.prefix + id);
|
|
68
|
+
await this.redis.del(...keys);
|
|
69
|
+
await this.redis.srem(userKey, ...toDelete);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { RedisSessionStore };
|
|
76
|
+
//# sourceMappingURL=redis-session.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-C7Uep-_p.mjs";
|
|
2
|
+
import { a as toJsonSchema } from "./schemaConverter-BwrmWroW.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/betterAuthOpenApi.ts
|
|
5
|
+
var betterAuthOpenApi_exports = /* @__PURE__ */ __exportAll({ extractBetterAuthOpenApi: () => extractBetterAuthOpenApi });
|
|
6
|
+
/**
|
|
7
|
+
* Check if a value looks like a Better Auth endpoint (has .path and .options)
|
|
8
|
+
*/
|
|
9
|
+
function isBetterAuthEndpoint(value) {
|
|
10
|
+
if (typeof value !== "function" && typeof value !== "object") return false;
|
|
11
|
+
if (!value) return false;
|
|
12
|
+
const v = value;
|
|
13
|
+
return typeof v.path === "string" && typeof v.options === "object" && v.options !== null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Convert Fastify-style path params (/:id) to OpenAPI-style (/{id})
|
|
17
|
+
*/
|
|
18
|
+
function toOpenApiPath(path) {
|
|
19
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Extract path parameters from a path string
|
|
23
|
+
*/
|
|
24
|
+
function extractPathParams(path) {
|
|
25
|
+
const params = [];
|
|
26
|
+
const matches = path.matchAll(/:(\w+)/g);
|
|
27
|
+
for (const match of matches) params.push({
|
|
28
|
+
name: match[1],
|
|
29
|
+
in: "path",
|
|
30
|
+
required: true,
|
|
31
|
+
schema: { type: "string" }
|
|
32
|
+
});
|
|
33
|
+
return params;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Extract OpenAPI paths from a Better Auth instance's API object.
|
|
37
|
+
*
|
|
38
|
+
* Walks `authApi` (the `auth.api` object from Better Auth), discovers
|
|
39
|
+
* endpoints, converts their Zod schemas to JSON Schema via `z.toJSONSchema()`,
|
|
40
|
+
* and returns a complete `ExternalOpenApiPaths` object ready for Arc's spec builder.
|
|
41
|
+
*/
|
|
42
|
+
function extractBetterAuthOpenApi(authApi, options = {}) {
|
|
43
|
+
const { basePath = "/api/auth", tagName = "Authentication", tagDescription = "Better Auth authentication endpoints", excludePaths = [], excludeServerOnly = true, userFields } = options;
|
|
44
|
+
const normalizedBase = basePath.replace(/\/+$/, "");
|
|
45
|
+
const paths = {};
|
|
46
|
+
const detectedPlugins = detectActivePlugins(authApi);
|
|
47
|
+
const securityOptions = [{ cookieAuth: [] }, { bearerAuth: [] }];
|
|
48
|
+
if (detectedPlugins.apiKey) securityOptions.push({ apiKeyAuth: [] });
|
|
49
|
+
for (const [key, value] of Object.entries(authApi)) {
|
|
50
|
+
if (!isBetterAuthEndpoint(value)) continue;
|
|
51
|
+
const { path: endpointPath, options: endpointOpts } = value;
|
|
52
|
+
if (excludePaths.includes(endpointPath)) continue;
|
|
53
|
+
if (excludeServerOnly && endpointOpts.metadata?.SERVER_ONLY) continue;
|
|
54
|
+
const fullPath = toOpenApiPath(`${normalizedBase}${endpointPath}`);
|
|
55
|
+
const methods = [];
|
|
56
|
+
if (endpointOpts.method) if (Array.isArray(endpointOpts.method)) methods.push(...endpointOpts.method.map((m) => m.toLowerCase()));
|
|
57
|
+
else methods.push(endpointOpts.method.toLowerCase());
|
|
58
|
+
else methods.push(endpointOpts.body ? "post" : "get");
|
|
59
|
+
const openApiMeta = endpointOpts.metadata?.openapi;
|
|
60
|
+
for (const method of methods) {
|
|
61
|
+
const operation = {
|
|
62
|
+
tags: openApiMeta?.tags ?? [tagName],
|
|
63
|
+
operationId: openApiMeta?.operationId ?? key,
|
|
64
|
+
summary: openApiMeta?.summary ?? formatOperationSummary(key),
|
|
65
|
+
security: securityOptions
|
|
66
|
+
};
|
|
67
|
+
if (openApiMeta?.description) operation.description = openApiMeta.description;
|
|
68
|
+
const parameters = [...extractPathParams(endpointPath)];
|
|
69
|
+
if ((method === "get" || method === "delete") && endpointOpts.query) {
|
|
70
|
+
const querySchema = toJsonSchema(endpointOpts.query);
|
|
71
|
+
if (querySchema && querySchema.type === "object" && querySchema.properties) {
|
|
72
|
+
const props = querySchema.properties;
|
|
73
|
+
const required = querySchema.required ?? [];
|
|
74
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
75
|
+
const paramEntry = {
|
|
76
|
+
name,
|
|
77
|
+
in: "query",
|
|
78
|
+
required: required.includes(name),
|
|
79
|
+
schema: prop
|
|
80
|
+
};
|
|
81
|
+
if (prop.description) paramEntry.description = prop.description;
|
|
82
|
+
parameters.push(paramEntry);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (parameters.length > 0) operation.parameters = parameters;
|
|
87
|
+
if (method === "post" || method === "put" || method === "patch") {
|
|
88
|
+
if (openApiMeta?.requestBody) operation.requestBody = structuredClone(openApiMeta.requestBody);
|
|
89
|
+
else if (endpointOpts.body) {
|
|
90
|
+
const bodySchema = toJsonSchema(endpointOpts.body);
|
|
91
|
+
if (bodySchema) operation.requestBody = {
|
|
92
|
+
required: true,
|
|
93
|
+
content: { "application/json": { schema: bodySchema } }
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (userFields && isUserFieldEndpoint(endpointPath) && operation.requestBody) mergeUserFieldsIntoRequestBody(operation.requestBody, userFields, endpointPath);
|
|
97
|
+
}
|
|
98
|
+
if (openApiMeta?.responses) operation.responses = openApiMeta.responses;
|
|
99
|
+
else operation.responses = {
|
|
100
|
+
"200": { description: "Success" },
|
|
101
|
+
"400": { description: "Bad request" },
|
|
102
|
+
"401": { description: "Unauthorized" }
|
|
103
|
+
};
|
|
104
|
+
if (!paths[fullPath]) paths[fullPath] = {};
|
|
105
|
+
paths[fullPath][method] = operation;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const schemas = {
|
|
109
|
+
User: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
id: { type: "string" },
|
|
113
|
+
name: { type: "string" },
|
|
114
|
+
email: {
|
|
115
|
+
type: "string",
|
|
116
|
+
format: "email"
|
|
117
|
+
},
|
|
118
|
+
emailVerified: { type: "boolean" },
|
|
119
|
+
image: {
|
|
120
|
+
type: "string",
|
|
121
|
+
nullable: true
|
|
122
|
+
},
|
|
123
|
+
createdAt: {
|
|
124
|
+
type: "string",
|
|
125
|
+
format: "date-time"
|
|
126
|
+
},
|
|
127
|
+
updatedAt: {
|
|
128
|
+
type: "string",
|
|
129
|
+
format: "date-time"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
Session: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
id: { type: "string" },
|
|
137
|
+
userId: { type: "string" },
|
|
138
|
+
token: { type: "string" },
|
|
139
|
+
expiresAt: {
|
|
140
|
+
type: "string",
|
|
141
|
+
format: "date-time"
|
|
142
|
+
},
|
|
143
|
+
ipAddress: {
|
|
144
|
+
type: "string",
|
|
145
|
+
nullable: true
|
|
146
|
+
},
|
|
147
|
+
userAgent: {
|
|
148
|
+
type: "string",
|
|
149
|
+
nullable: true
|
|
150
|
+
},
|
|
151
|
+
createdAt: {
|
|
152
|
+
type: "string",
|
|
153
|
+
format: "date-time"
|
|
154
|
+
},
|
|
155
|
+
updatedAt: {
|
|
156
|
+
type: "string",
|
|
157
|
+
format: "date-time"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
if (userFields) {
|
|
163
|
+
const userProps = schemas.User.properties;
|
|
164
|
+
for (const [name, field] of Object.entries(userFields)) {
|
|
165
|
+
const prop = { type: field.type };
|
|
166
|
+
if (field.description) prop.description = field.description;
|
|
167
|
+
userProps[name] = prop;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const securitySchemes = { cookieAuth: {
|
|
171
|
+
type: "apiKey",
|
|
172
|
+
in: "cookie",
|
|
173
|
+
name: "better-auth.session_token",
|
|
174
|
+
description: "Session cookie set by Better Auth after sign-in"
|
|
175
|
+
} };
|
|
176
|
+
if (detectedPlugins.apiKey) securitySchemes.apiKeyAuth = {
|
|
177
|
+
type: "apiKey",
|
|
178
|
+
in: "header",
|
|
179
|
+
name: "x-api-key",
|
|
180
|
+
description: "API key for programmatic access. Pass org context via x-organization-id header."
|
|
181
|
+
};
|
|
182
|
+
const resourceSecurity = [];
|
|
183
|
+
if (detectedPlugins.apiKey) resourceSecurity.push({
|
|
184
|
+
apiKeyAuth: [],
|
|
185
|
+
orgHeader: []
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
paths,
|
|
189
|
+
schemas,
|
|
190
|
+
securitySchemes,
|
|
191
|
+
tags: [{
|
|
192
|
+
name: tagName,
|
|
193
|
+
description: tagDescription
|
|
194
|
+
}],
|
|
195
|
+
resourceSecurity: resourceSecurity.length > 0 ? resourceSecurity : void 0
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Auto-detect active Better Auth plugins by inspecting the API object.
|
|
200
|
+
*
|
|
201
|
+
* Rather than hardcoding plugin-specific behavior, we check for known
|
|
202
|
+
* endpoint signatures that each plugin registers. This way the OpenAPI
|
|
203
|
+
* spec adapts automatically to whatever plugins the app has enabled —
|
|
204
|
+
* no Arc update needed when adding/removing plugins.
|
|
205
|
+
*/
|
|
206
|
+
function detectActivePlugins(authApi) {
|
|
207
|
+
const endpointPaths = /* @__PURE__ */ new Set();
|
|
208
|
+
for (const value of Object.values(authApi)) if (isBetterAuthEndpoint(value)) endpointPaths.add(value.path);
|
|
209
|
+
return {
|
|
210
|
+
apiKey: endpointPaths.has("/api-key/create"),
|
|
211
|
+
organization: endpointPaths.has("/organization/create")
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Convert a camelCase key like 'signInEmail' to a readable summary like 'Sign in email'
|
|
216
|
+
*/
|
|
217
|
+
function formatOperationSummary(key) {
|
|
218
|
+
return key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Check if an endpoint path should have userFields merged into its request body.
|
|
222
|
+
*/
|
|
223
|
+
function isUserFieldEndpoint(path) {
|
|
224
|
+
return path === "/sign-up/email" || path === "/update-user";
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Merge user-defined fields into an existing requestBody schema.
|
|
228
|
+
* For updateUser, all fields are treated as optional regardless of their `required` setting.
|
|
229
|
+
*/
|
|
230
|
+
function mergeUserFieldsIntoRequestBody(requestBody, userFields, endpointPath) {
|
|
231
|
+
const content = requestBody?.content?.["application/json"];
|
|
232
|
+
if (!content?.schema) return;
|
|
233
|
+
const schema = content.schema;
|
|
234
|
+
if (!schema.properties) schema.properties = {};
|
|
235
|
+
if (!schema.required) schema.required = [];
|
|
236
|
+
const props = schema.properties;
|
|
237
|
+
const required = schema.required;
|
|
238
|
+
for (const [name, field] of Object.entries(userFields)) {
|
|
239
|
+
if (field.input === false) continue;
|
|
240
|
+
const isRequired = endpointPath === "/update-user" ? false : field.required ?? false;
|
|
241
|
+
const prop = { type: field.type };
|
|
242
|
+
if (field.description) prop.description = field.description;
|
|
243
|
+
props[name] = prop;
|
|
244
|
+
if (isRequired && !required.includes(name)) required.push(name);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
//#endregion
|
|
249
|
+
export { extractBetterAuthOpenApi as n, betterAuthOpenApi_exports as t };
|
|
250
|
+
//# sourceMappingURL=betterAuthOpenApi-BrHKeSAx.mjs.map
|