@eminuckan/spine 0.3.6 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +39 -6
  3. package/dist/api-client/api-config.server.d.ts +11 -1
  4. package/dist/api-client/api-config.server.d.ts.map +1 -1
  5. package/dist/api-client/fetch-client.server.d.ts.map +1 -1
  6. package/dist/api-client/index.d.ts +1 -1
  7. package/dist/api-client/index.d.ts.map +1 -1
  8. package/dist/api-client/server.mjs +1 -1
  9. package/dist/api-client/types.d.ts +9 -1
  10. package/dist/api-client/types.d.ts.map +1 -1
  11. package/dist/auth/auth.server.d.ts.map +1 -1
  12. package/dist/auth/callback-errors.d.ts +42 -0
  13. package/dist/auth/callback-errors.d.ts.map +1 -0
  14. package/dist/auth/oauth-state-config.d.ts +7 -0
  15. package/dist/auth/oauth-state-config.d.ts.map +1 -0
  16. package/dist/auth/redis-session-storage.server.d.ts +3 -1
  17. package/dist/auth/redis-session-storage.server.d.ts.map +1 -1
  18. package/dist/auth/route-protection.server.d.ts.map +1 -1
  19. package/dist/auth/server.d.ts +1 -0
  20. package/dist/auth/server.d.ts.map +1 -1
  21. package/dist/auth/server.mjs +3 -3
  22. package/dist/auth/token-refresh.server.d.ts.map +1 -1
  23. package/dist/react-router/server.mjs +5 -5
  24. package/dist/{redis-session-storage.server-DGgQcJ9B.mjs → redis-session-storage.server-CI_IYIn8.mjs} +46 -15
  25. package/dist/redis-session-storage.server-CI_IYIn8.mjs.map +1 -0
  26. package/dist/{server-DPCu40ud.mjs → server-Cw86FdT-.mjs} +273 -64
  27. package/dist/server-Cw86FdT-.mjs.map +1 -0
  28. package/dist/{server-ByScGRXA.mjs → server-pnsLybIH.mjs} +63 -16
  29. package/dist/server-pnsLybIH.mjs.map +1 -0
  30. package/dist/server.mjs +5 -5
  31. package/dist/tanstack-start/index.d.ts +9 -0
  32. package/dist/tanstack-start/index.d.ts.map +1 -0
  33. package/dist/tanstack-start/index.mjs +11 -0
  34. package/dist/tanstack-start/server.d.ts +9 -0
  35. package/dist/tanstack-start/server.d.ts.map +1 -0
  36. package/dist/tanstack-start/server.mjs +17 -0
  37. package/dist/tenant/server.mjs +47 -7
  38. package/dist/tenant/server.mjs.map +1 -1
  39. package/dist/tenant/tenant-cookie.server.d.ts +5 -1
  40. package/dist/tenant/tenant-cookie.server.d.ts.map +1 -1
  41. package/dist/tenant/tenant.server.d.ts +9 -1
  42. package/dist/tenant/tenant.server.d.ts.map +1 -1
  43. package/dist/types-DqCXinAk.mjs.map +1 -1
  44. package/docs/adapters.md +64 -1
  45. package/docs/architecture.md +6 -1
  46. package/docs/module-reference.md +5 -0
  47. package/package.json +13 -4
  48. package/dist/redis-session-storage.server-DGgQcJ9B.mjs.map +0 -1
  49. package/dist/server-ByScGRXA.mjs.map +0 -1
  50. package/dist/server-DPCu40ud.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-session-storage.server-CI_IYIn8.mjs","names":["resolvePositiveIntegerEnv"],"sources":["../src/auth/oauth-state-config.ts","../src/auth/redis-session-storage.server.ts"],"sourcesContent":["const DEFAULT_OAUTH_STATE_TTL_SECONDS = 10 * 60;\n\nfunction resolvePositiveIntegerEnv(name: string, defaultValue: number): number {\n const value = process.env[name]?.trim();\n if (!value) {\n return defaultValue;\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed <= 0) {\n throw new Error(`${name} must be a positive integer.`);\n }\n\n return parsed;\n}\n\n/**\n * OAuth state lifetime shared by Redis expiry and callback age validation.\n * This module is internal and is not part of a package export entry point.\n */\nexport const OAUTH_STATE_TTL_SECONDS = resolvePositiveIntegerEnv(\n 'OAUTH_STATE_TTL',\n DEFAULT_OAUTH_STATE_TTL_SECONDS,\n);\n\nexport const OAUTH_STATE_TTL_MS = OAUTH_STATE_TTL_SECONDS * 1000;\n","/**\n * Redis Session Storage Server Module\n *\n * Redis-backed session storage for authentication.\n * Provides secure session management with automatic expiration,\n * without depending on a framework-specific session implementation.\n */\n\nimport Redis from 'ioredis';\nimport { createCipheriv, createDecipheriv, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';\nimport type { AuthSessionSummary, SessionData, OAuthState } from './types';\nimport { OAUTH_STATE_TTL_SECONDS } from './oauth-state-config';\nimport { logger } from '../logging';\n\n// ============================================================================\n// Redis Client Setup\n// ============================================================================\n\nlet redis: Redis | null = null;\n\nfunction getRedis(): Redis {\n if (!redis) {\n redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');\n\n redis.on('error', (err) => {\n logger.error('Redis Client Error', err);\n });\n\n redis.on('connect', () => {\n logger.info('Redis connected successfully');\n });\n }\n return redis;\n}\n\n// ============================================================================\n// Redis Key Patterns\n// ============================================================================\n\nconst KEY_PREFIX = process.env.REDIS_KEY_PREFIX || '';\n\nconst REDIS_KEYS = {\n oauthState: (stateId: string) => `${KEY_PREFIX}oauth:state:${stateId}`,\n session: (sessionId: string) => `${KEY_PREFIX}session:${sessionId}`,\n sessionByUser: (userId: string) => `${KEY_PREFIX}session:index:user:${userId}`,\n sessionBySid: (sid: string) => `${KEY_PREFIX}session:index:sid:${sid}`,\n sessionBySessionState: (sessionState: string) => `${KEY_PREFIX}session:index:session_state:${sessionState}`,\n};\n\nconst DEFAULT_SESSION_SECRET = 'default-secret-change-in-production';\nconst DEFAULT_SESSION_TTL = 60 * 60 * 24 * 7;\n\nconst SESSION_TTL = resolvePositiveIntegerEnv('SESSION_DEFAULT_TTL', resolvePositiveIntegerEnv('SESSION_TTL', DEFAULT_SESSION_TTL));\n\nconst SESSION_COOKIE_NAME = process.env.SESSION_COOKIE_NAME || '__session_id';\nconst SESSION_SECRET = resolveSessionSecret();\nconst SESSION_COOKIE_SECURE = resolveCookieSecure();\nconst SESSION_COOKIE_PATH = '/';\nconst SESSION_COOKIE_SAME_SITE = resolveCookieSameSite();\nconst SESSION_COOKIE_DOMAIN = process.env.SESSION_COOKIE_DOMAIN?.trim();\nconst SESSION_ENCRYPTION_ENABLED = resolveSessionEncryptionEnabled();\nconst SESSION_ENCRYPTION_KEY = resolveSessionEncryptionKey(SESSION_ENCRYPTION_ENABLED);\n\nexport const AUTH_ERROR_COOKIE_PREFIX = SESSION_COOKIE_NAME.replace('_session', '').replace('__', '');\n\nfunction resolveBooleanEnv(name: string, defaultValue: boolean): boolean {\n const value = process.env[name]?.trim().toLowerCase();\n if (!value) {\n return defaultValue;\n }\n\n if (['1', 'true', 'yes', 'on'].includes(value)) {\n return true;\n }\n\n if (['0', 'false', 'no', 'off'].includes(value)) {\n return false;\n }\n\n throw new Error(`${name} must be a boolean value.`);\n}\n\nfunction resolvePositiveIntegerEnv(name: string, defaultValue: number): number {\n const value = process.env[name]?.trim();\n if (!value) {\n return defaultValue;\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed <= 0) {\n throw new Error(`${name} must be a positive integer.`);\n }\n\n return parsed;\n}\n\nfunction resolveSessionSecret(): string {\n const secret = process.env.SESSION_SECRET?.trim();\n if (!secret || secret === DEFAULT_SESSION_SECRET) {\n if (process.env.NODE_ENV === 'production') {\n throw new Error('SESSION_SECRET must be set to a unique random value in production.');\n }\n\n return DEFAULT_SESSION_SECRET;\n }\n\n if (secret.length < 32) {\n throw new Error('SESSION_SECRET must be at least 32 characters long.');\n }\n\n return secret;\n}\n\nfunction resolveCookieSecure(): boolean {\n return resolveBooleanEnv('SESSION_COOKIE_SECURE', process.env.NODE_ENV === 'production');\n}\n\nfunction resolveCookieSameSite(): 'Lax' | 'Strict' | 'None' {\n const value = process.env.SESSION_COOKIE_SAME_SITE?.trim().toLowerCase();\n const sameSite = value === 'strict' ? 'Strict' : value === 'none' ? 'None' : 'Lax';\n\n if (sameSite === 'None' && !SESSION_COOKIE_SECURE) {\n throw new Error('SESSION_COOKIE_SAME_SITE=None requires SESSION_COOKIE_SECURE=true.');\n }\n\n return sameSite;\n}\n\nfunction resolveSessionEncryptionEnabled(): boolean {\n const enabled = resolveBooleanEnv('SESSION_ENCRYPTION', process.env.NODE_ENV === 'production');\n if (process.env.NODE_ENV === 'production' && !enabled) {\n throw new Error('SESSION_ENCRYPTION cannot be disabled in production.');\n }\n\n return enabled;\n}\n\nfunction decodeBase64Url(value: string): Buffer {\n const normalized = value.replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=');\n return Buffer.from(padded, 'base64');\n}\n\nfunction resolveSessionEncryptionKey(enabled: boolean): Buffer | null {\n if (!enabled) {\n return null;\n }\n\n const value = process.env.SESSION_ENCRYPTION_KEY?.trim();\n if (!value) {\n throw new Error('SESSION_ENCRYPTION_KEY is required when session encryption is enabled.');\n }\n\n const key = /^[0-9a-f]{64}$/i.test(value)\n ? Buffer.from(value, 'hex')\n : decodeBase64Url(value);\n\n if (key.length !== 32) {\n throw new Error('SESSION_ENCRYPTION_KEY must decode to exactly 32 bytes. Generate one with: openssl rand -base64 32');\n }\n\n return key;\n}\n\ntype SessionCookieOptions = {\n maxAge: number;\n};\n\nfunction parseCookieHeader(cookieHeader: string | null): Record<string, string> {\n if (!cookieHeader) {\n return {};\n }\n\n return cookieHeader\n .split(';')\n .map((part) => part.trim())\n .filter(Boolean)\n .reduce<Record<string, string>>((cookies, part) => {\n const separatorIndex = part.indexOf('=');\n if (separatorIndex <= 0) {\n return cookies;\n }\n\n const name = part.slice(0, separatorIndex).trim();\n const value = part.slice(separatorIndex + 1).trim();\n if (!name) {\n return cookies;\n }\n\n cookies[name] = value;\n return cookies;\n }, {});\n}\n\nfunction createCookieSignature(value: string, secret: string): string {\n return createHmac('sha256', secret).update(value).digest('base64url');\n}\n\nfunction safelyCompareValues(left: string, right: string): boolean {\n const leftBuffer = Buffer.from(left);\n const rightBuffer = Buffer.from(right);\n\n if (leftBuffer.length !== rightBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(leftBuffer, rightBuffer);\n}\n\nfunction signSessionId(sessionId: string): string {\n const signature = createCookieSignature(sessionId, SESSION_SECRET);\n return `${sessionId}.${signature}`;\n}\n\nfunction unsignSessionId(cookieValue: string | undefined): string | null {\n if (!cookieValue) {\n return null;\n }\n\n let decodedValue: string;\n try {\n decodedValue = decodeURIComponent(cookieValue);\n } catch {\n return null;\n }\n\n const separatorIndex = decodedValue.lastIndexOf('.');\n if (separatorIndex <= 0 || separatorIndex === decodedValue.length - 1) {\n return null;\n }\n\n const sessionId = decodedValue.slice(0, separatorIndex);\n const signature = decodedValue.slice(separatorIndex + 1);\n const expectedSignature = createCookieSignature(sessionId, SESSION_SECRET);\n\n return safelyCompareValues(signature, expectedSignature) ? sessionId : null;\n}\n\nfunction serializeSessionCookieValue(sessionId: string, options: SessionCookieOptions): string {\n const signedValue = encodeURIComponent(signSessionId(sessionId));\n const segments = [\n `${SESSION_COOKIE_NAME}=${signedValue}`,\n `Max-Age=${options.maxAge}`,\n `Path=${SESSION_COOKIE_PATH}`,\n `SameSite=${SESSION_COOKIE_SAME_SITE}`,\n 'HttpOnly',\n ];\n\n if (SESSION_COOKIE_DOMAIN) {\n segments.push(`Domain=${SESSION_COOKIE_DOMAIN}`);\n }\n\n if (SESSION_COOKIE_SECURE) {\n segments.push('Secure');\n }\n\n return segments.join('; ');\n}\n\nfunction serializeExpiredSessionCookie(): string {\n const segments = [\n `${SESSION_COOKIE_NAME}=`,\n 'Max-Age=0',\n `Path=${SESSION_COOKIE_PATH}`,\n `SameSite=${SESSION_COOKIE_SAME_SITE}`,\n 'HttpOnly',\n ];\n\n if (SESSION_COOKIE_DOMAIN) {\n segments.push(`Domain=${SESSION_COOKIE_DOMAIN}`);\n }\n\n if (SESSION_COOKIE_SECURE) {\n segments.push('Secure');\n }\n\n return segments.join('; ');\n}\n\nasync function getSessionIdFromRequest(request: Request): Promise<string | null> {\n const cookies = parseCookieHeader(request.headers.get('Cookie'));\n return unsignSessionId(cookies[SESSION_COOKIE_NAME]);\n}\n\nfunction getSessionIndexKeys(data: SessionData): string[] {\n const keys = new Set<string>();\n\n if (data.userId) {\n keys.add(REDIS_KEYS.sessionByUser(data.userId));\n }\n\n if (data.sid) {\n keys.add(REDIS_KEYS.sessionBySid(data.sid));\n }\n\n if (data.sessionState) {\n keys.add(REDIS_KEYS.sessionBySessionState(data.sessionState));\n }\n\n return Array.from(keys);\n}\n\nasync function registerSessionIndexes(sessionId: string, data: SessionData): Promise<void> {\n const indexKeys = getSessionIndexKeys(data);\n if (indexKeys.length === 0) {\n return;\n }\n\n const redisClient = getRedis();\n const pipeline = redisClient.pipeline();\n\n for (const indexKey of indexKeys) {\n pipeline.sadd(indexKey, sessionId);\n pipeline.expire(indexKey, SESSION_TTL);\n }\n\n await pipeline.exec();\n}\n\nasync function unregisterSessionIndexes(sessionId: string, data: SessionData): Promise<void> {\n const indexKeys = getSessionIndexKeys(data);\n if (indexKeys.length === 0) {\n return;\n }\n\n const redisClient = getRedis();\n const pipeline = redisClient.pipeline();\n\n for (const indexKey of indexKeys) {\n pipeline.srem(indexKey, sessionId);\n }\n\n await pipeline.exec();\n}\n\nfunction parseSessionData(sessionData: string | null): SessionData | null {\n if (!sessionData) {\n return null;\n }\n\n try {\n return JSON.parse(decodeSessionData(sessionData)) as SessionData;\n } catch (error) {\n logger.error('Failed to parse session data', error instanceof Error ? error : undefined);\n return null;\n }\n}\n\nfunction encodeSessionData(data: SessionData): string {\n const plaintext = JSON.stringify(data);\n if (!SESSION_ENCRYPTION_KEY) {\n return plaintext;\n }\n\n const iv = randomBytes(12);\n const cipher = createCipheriv('aes-256-gcm', SESSION_ENCRYPTION_KEY, iv);\n const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n return [\n 'enc:v1',\n iv.toString('base64url'),\n tag.toString('base64url'),\n ciphertext.toString('base64url'),\n ].join(':');\n}\n\nfunction decodeSessionData(value: string): string {\n if (!value.startsWith('enc:v1:')) {\n if (SESSION_ENCRYPTION_ENABLED) {\n logger.warn('Reading legacy plaintext auth session payload while session encryption is enabled.');\n }\n\n return value;\n }\n\n if (!SESSION_ENCRYPTION_KEY) {\n throw new Error('Cannot decrypt auth session payload because SESSION_ENCRYPTION_KEY is not configured.');\n }\n\n const [, version, ivValue, tagValue, ciphertextValue] = value.split(':');\n if (version !== 'v1' || !ivValue || !tagValue || !ciphertextValue) {\n throw new Error('Invalid encrypted auth session payload format.');\n }\n\n const decipher = createDecipheriv('aes-256-gcm', SESSION_ENCRYPTION_KEY, Buffer.from(ivValue, 'base64url'));\n decipher.setAuthTag(Buffer.from(tagValue, 'base64url'));\n\n return Buffer.concat([\n decipher.update(Buffer.from(ciphertextValue, 'base64url')),\n decipher.final(),\n ]).toString('utf8');\n}\n\nasync function readSessionData(sessionId: string): Promise<SessionData | null> {\n try {\n const sessionData = await getRedis().get(REDIS_KEYS.session(sessionId));\n if (!sessionData) {\n logger.debug('Session not found in Redis', { sessionId });\n return null;\n }\n\n await getRedis().expire(REDIS_KEYS.session(sessionId), SESSION_TTL);\n return parseSessionData(sessionData);\n } catch (error) {\n logger.error('Failed to read session from Redis', error instanceof Error ? error : undefined);\n return null;\n }\n}\n\nasync function writeSessionData(sessionId: string, data: SessionData): Promise<void> {\n try {\n const existingSessionData = parseSessionData(await getRedis().get(REDIS_KEYS.session(sessionId)));\n if (existingSessionData) {\n await unregisterSessionIndexes(sessionId, existingSessionData);\n }\n\n await getRedis().setex(REDIS_KEYS.session(sessionId), SESSION_TTL, encodeSessionData(data));\n await registerSessionIndexes(sessionId, data);\n logger.debug('Updated Redis session', { sessionId });\n } catch (error) {\n logger.error('Failed to update session in Redis', error instanceof Error ? error : undefined);\n throw error;\n }\n}\n\nasync function deleteSessionData(sessionId: string): Promise<void> {\n try {\n const existingSessionData = parseSessionData(await getRedis().get(REDIS_KEYS.session(sessionId)));\n if (existingSessionData) {\n await unregisterSessionIndexes(sessionId, existingSessionData);\n }\n\n await getRedis().del(REDIS_KEYS.session(sessionId));\n logger.debug('Deleted Redis session', { sessionId });\n } catch (error) {\n logger.error('Failed to delete session from Redis', error instanceof Error ? error : undefined);\n }\n}\n\nfunction mergeSessionData(existingData: SessionData, updates: Partial<SessionData>): SessionData {\n const nextData = { ...existingData } as Record<string, unknown>;\n\n Object.entries(updates).forEach(([key, value]) => {\n if (value === undefined) {\n delete nextData[key];\n return;\n }\n\n nextData[key] = value;\n });\n\n return nextData as SessionData;\n}\n\nasync function getSessionIdsFromIndex(indexKey: string): Promise<string[]> {\n const sessionIds = await getRedis().smembers(indexKey);\n const activeSessionIds: string[] = [];\n const staleSessionIds: string[] = [];\n\n for (const sessionId of sessionIds) {\n const exists = await getRedis().exists(REDIS_KEYS.session(sessionId));\n if (exists) {\n activeSessionIds.push(sessionId);\n } else {\n staleSessionIds.push(sessionId);\n }\n }\n\n if (staleSessionIds.length > 0) {\n await getRedis().srem(indexKey, ...staleSessionIds);\n }\n\n return activeSessionIds;\n}\n\nfunction toAuthSessionSummary(\n sessionId: string,\n session: SessionData,\n currentSessionId?: string | null\n): AuthSessionSummary {\n return {\n sessionId,\n userId: session.userId,\n email: session.user?.email,\n name: session.user?.name,\n issuer: session.issuer,\n sid: session.sid,\n sessionState: session.sessionState,\n clientId: session.clientId,\n ipAddress: session.ipAddress,\n userAgent: session.userAgent,\n createdAt: session.createdAt,\n lastActivity: session.lastActivity,\n expiresAt: session.expiresAt,\n isCurrent: currentSessionId ? sessionId === currentSessionId : undefined,\n };\n}\n\nasync function listSessionDataByIds(sessionIds: string[]): Promise<Array<{ sessionId: string; data: SessionData }>> {\n const sessions: Array<{ sessionId: string; data: SessionData }> = [];\n\n for (const sessionId of sessionIds) {\n const data = await readSessionData(sessionId);\n if (data) {\n sessions.push({ sessionId, data });\n }\n }\n\n return sessions;\n}\n\nfunction getRequestIpAddress(request: Request): string | undefined {\n const forwardedFor = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim();\n return (\n forwardedFor ||\n request.headers.get('cf-connecting-ip')?.trim() ||\n request.headers.get('x-real-ip')?.trim() ||\n undefined\n );\n}\n\n// ============================================================================\n// Session Helper Functions\n// ============================================================================\n\nexport async function createAuthSession(\n request: Request,\n sessionData: Partial<SessionData>\n): Promise<Headers> {\n const currentSessionId = await getSessionIdFromRequest(request);\n const nextSessionId = crypto.randomUUID();\n const nextSessionData = mergeSessionData({}, {\n ...sessionData,\n sessionId: nextSessionId,\n ipAddress: sessionData.ipAddress ?? getRequestIpAddress(request),\n userAgent: sessionData.userAgent ?? request.headers.get('user-agent') ?? undefined,\n });\n\n logger.debug('Creating auth session', { dataKeys: Object.keys(sessionData) });\n\n if (currentSessionId) {\n await deleteSessionData(currentSessionId);\n }\n\n await writeSessionData(nextSessionId, nextSessionData);\n\n return new Headers({\n 'Set-Cookie': serializeSessionCookieValue(nextSessionId, { maxAge: SESSION_TTL }),\n });\n}\n\nexport async function getAuthSession(request: Request): Promise<SessionData> {\n const sessionId = await getSessionIdFromRequest(request);\n if (!sessionId) {\n return {};\n }\n\n const session = await readSessionData(sessionId);\n if (!session) {\n return {};\n }\n\n return {\n sessionId,\n userId: session.userId,\n accessToken: session.accessToken,\n refreshToken: session.refreshToken,\n idToken: session.idToken,\n expiresAt: session.expiresAt,\n issuer: session.issuer,\n sid: session.sid,\n sessionState: session.sessionState,\n clientId: session.clientId,\n ipAddress: session.ipAddress,\n userAgent: session.userAgent,\n user: session.user,\n createdAt: session.createdAt,\n lastActivity: session.lastActivity,\n };\n}\n\nexport async function updateAuthSession(\n request: Request,\n updates: Partial<SessionData>\n): Promise<Headers> {\n const currentSessionId = await getSessionIdFromRequest(request);\n const currentSessionData = currentSessionId\n ? (await readSessionData(currentSessionId)) ?? {}\n : {};\n const nextSessionId = currentSessionId ?? crypto.randomUUID();\n const nextSessionData = mergeSessionData(currentSessionData, {\n ...updates,\n sessionId: nextSessionId,\n });\n\n await writeSessionData(nextSessionId, nextSessionData);\n\n return new Headers({\n 'Set-Cookie': serializeSessionCookieValue(nextSessionId, { maxAge: SESSION_TTL }),\n });\n}\n\nexport async function destroyAuthSession(request: Request): Promise<Headers> {\n const sessionId = await getSessionIdFromRequest(request);\n if (sessionId) {\n await deleteSessionData(sessionId);\n }\n\n return new Headers({\n 'Set-Cookie': serializeExpiredSessionCookie(),\n });\n}\n\nexport async function getCurrentAuthSessionId(request: Request): Promise<string | null> {\n return await getSessionIdFromRequest(request);\n}\n\nexport async function getAuthSessionById(sessionId: string): Promise<SessionData | null> {\n const session = await readSessionData(sessionId);\n return session ? { ...session, sessionId } : null;\n}\n\nexport async function listAuthSessionDataForUser(\n userId: string\n): Promise<Array<{ sessionId: string; data: SessionData }>> {\n const sessionIds = await getSessionIdsFromIndex(REDIS_KEYS.sessionByUser(userId));\n return await listSessionDataByIds(sessionIds);\n}\n\nexport async function listAuthSessionsForUser(\n userId: string,\n currentSessionId?: string | null\n): Promise<AuthSessionSummary[]> {\n const sessions = await listAuthSessionDataForUser(userId);\n\n return sessions\n .map(({ sessionId, data }) => toAuthSessionSummary(sessionId, data, currentSessionId))\n .sort((left, right) => (right.lastActivity ?? right.createdAt ?? 0) - (left.lastActivity ?? left.createdAt ?? 0));\n}\n\nexport async function listCurrentUserAuthSessions(request: Request): Promise<AuthSessionSummary[]> {\n const currentSession = await getAuthSession(request);\n if (!currentSession.userId) {\n return [];\n }\n\n return await listAuthSessionsForUser(currentSession.userId, currentSession.sessionId);\n}\n\nexport async function destroyAuthSessionById(sessionId: string): Promise<boolean> {\n const session = await readSessionData(sessionId);\n if (!session) {\n return false;\n }\n\n await deleteSessionData(sessionId);\n return true;\n}\n\nexport async function destroyAuthSessionsForUser(userId: string): Promise<number> {\n const sessionIds = await getSessionIdsFromIndex(REDIS_KEYS.sessionByUser(userId));\n let destroyedSessions = 0;\n\n for (const sessionId of sessionIds) {\n if (await destroyAuthSessionById(sessionId)) {\n destroyedSessions += 1;\n }\n }\n\n return destroyedSessions;\n}\n\nexport async function destroyAuthSessionsBySid(sid: string): Promise<number> {\n const sessionIds = await getSessionIdsFromIndex(REDIS_KEYS.sessionBySid(sid));\n let destroyedSessions = 0;\n\n for (const sessionId of sessionIds) {\n if (await destroyAuthSessionById(sessionId)) {\n destroyedSessions += 1;\n }\n }\n\n return destroyedSessions;\n}\n\nexport async function destroyAuthSessionsBySessionState(sessionState: string): Promise<number> {\n const sessionIds = await getSessionIdsFromIndex(REDIS_KEYS.sessionBySessionState(sessionState));\n let destroyedSessions = 0;\n\n for (const sessionId of sessionIds) {\n if (await destroyAuthSessionById(sessionId)) {\n destroyedSessions += 1;\n }\n }\n\n return destroyedSessions;\n}\n\nexport async function destroyAuthSessionsByIdentitySession(params: {\n sid?: string;\n sessionState?: string;\n userId?: string;\n}): Promise<number> {\n if (params.sid) {\n return await destroyAuthSessionsBySid(params.sid);\n }\n\n if (params.sessionState) {\n return await destroyAuthSessionsBySessionState(params.sessionState);\n }\n\n if (params.userId) {\n return await destroyAuthSessionsForUser(params.userId);\n }\n\n return 0;\n}\n\nexport async function requireAuthSession(request: Request): Promise<SessionData> {\n const sessionData = await getAuthSession(request);\n\n if (!sessionData.userId || !sessionData.user || !sessionData.accessToken || isSessionDataExpired(sessionData)) {\n throw new Response('Unauthorized', { status: 401 });\n }\n\n return sessionData;\n}\n\nfunction getJwtExpiry(accessToken: string | undefined): number | null {\n if (!accessToken) {\n return null;\n }\n\n try {\n const [, encodedPayload] = accessToken.split('.');\n if (!encodedPayload) {\n return null;\n }\n\n const payload = JSON.parse(\n Buffer.from(encodedPayload, 'base64url').toString('utf8'),\n ) as { exp?: unknown };\n\n return typeof payload.exp === 'number' && Number.isFinite(payload.exp)\n ? payload.exp * 1000\n : null;\n } catch {\n return null;\n }\n}\n\nfunction isSessionDataExpired(sessionData: SessionData): boolean {\n const now = Date.now();\n if (\n typeof sessionData.expiresAt === 'number' &&\n (!Number.isFinite(sessionData.expiresAt) || now >= sessionData.expiresAt)\n ) {\n return true;\n }\n\n const jwtExpiry = getJwtExpiry(sessionData.accessToken);\n return jwtExpiry !== null && now >= jwtExpiry;\n}\n\nexport async function isSessionValid(request: Request): Promise<boolean> {\n try {\n const sessionData = await getAuthSession(request);\n\n if (!sessionData.user || !sessionData.accessToken || isSessionDataExpired(sessionData)) {\n return false;\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\n// ============================================================================\n// OAuth State Management\n// ============================================================================\n\nexport async function createOAuthState(state: OAuthState): Promise<string> {\n const stateId = crypto.randomUUID();\n const key = REDIS_KEYS.oauthState(stateId);\n\n await getRedis().setex(key, OAUTH_STATE_TTL_SECONDS, JSON.stringify(state));\n\n logger.debug('OAuth state created', { hasStateId: true, ttl: OAUTH_STATE_TTL_SECONDS });\n\n return stateId;\n}\n\nexport async function getOAuthState(\n stateId: string,\n options?: { readonly throwOnMalformed?: boolean },\n): Promise<OAuthState | null> {\n const key = REDIS_KEYS.oauthState(stateId);\n const data = await getRedis().get(key);\n\n if (!data) {\n logger.warn('OAuth state not found or expired', { hasStateId: true });\n return null;\n }\n\n try {\n return JSON.parse(data);\n } catch (error) {\n logger.error('Failed to parse OAuth state', undefined, {\n errorType: error instanceof Error ? error.name : 'UnknownError',\n });\n if (options?.throwOnMalformed) {\n throw new Error('OAuth state storage failure');\n }\n return null;\n }\n}\n\nexport async function deleteOAuthState(stateId: string): Promise<void> {\n const key = REDIS_KEYS.oauthState(stateId);\n await getRedis().del(key);\n logger.debug('OAuth state deleted', { hasStateId: true });\n}\n\nexport async function cleanupExpiredOAuthStates(): Promise<number> {\n const pattern = REDIS_KEYS.oauthState('*');\n const keys = await getRedis().keys(pattern);\n\n let cleaned = 0;\n for (const key of keys) {\n const ttl = await getRedis().ttl(key);\n if (ttl === -1) {\n await getRedis().del(key);\n cleaned++;\n }\n }\n\n logger.info('OAuth state cleanup completed', { cleaned, total: keys.length });\n return cleaned;\n}\n\n// ============================================================================\n// Cleanup\n// ============================================================================\n\nexport async function closeRedisConnection(): Promise<void> {\n if (redis) {\n await redis.quit();\n redis = null;\n logger.info('Redis connection closed');\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { logger } from "./logging/index.mjs";
2
- import { S as updateAuthSession, a as createOAuthState, d as destroyAuthSessionsBySid, f as destroyAuthSessionsForUser, g as getOAuthState, i as createAuthSession, l as destroyAuthSessionsByIdentitySession, o as deleteOAuthState, p as getAuthSession, s as destroyAuthSession, t as AUTH_ERROR_COOKIE_PREFIX, v as listAuthSessionDataForUser } from "./redis-session-storage.server-DGgQcJ9B.mjs";
2
+ import { C as OAUTH_STATE_TTL_MS, S as updateAuthSession, a as createOAuthState, d as destroyAuthSessionsBySid, f as destroyAuthSessionsForUser, g as getOAuthState, i as createAuthSession, l as destroyAuthSessionsByIdentitySession, o as deleteOAuthState, p as getAuthSession, s as destroyAuthSession, t as AUTH_ERROR_COOKIE_PREFIX, v as listAuthSessionDataForUser, w as OAUTH_STATE_TTL_SECONDS } from "./redis-session-storage.server-CI_IYIn8.mjs";
3
3
  import * as oidc from "openid-client";
4
4
  import { createRemoteJWKSet, errors, jwtVerify } from "jose";
5
5
  //#region src/http/response.ts
@@ -132,6 +132,75 @@ function serializeTemporaryAuthCookie(name, value, options) {
132
132
  return segments.join("; ");
133
133
  }
134
134
  //#endregion
135
+ //#region src/auth/callback-errors.ts
136
+ const OAuthCallbackFailureCodes = {
137
+ StaleTransaction: "stale_transaction",
138
+ StateMismatch: "state_mismatch",
139
+ MalformedCallback: "malformed_callback",
140
+ ApplicationActionFailed: "application_action_failed",
141
+ TokenExchangeFailed: "token_exchange_failed",
142
+ SessionCreationFailed: "session_creation_failed",
143
+ StorageError: "storage_error",
144
+ ConfigurationError: "configuration_error"
145
+ };
146
+ const OAUTH_CALLBACK_ERROR_BRAND = Symbol.for("@eminuckan/spine/OAuthCallbackError");
147
+ function isOAuthCallbackFailureCode(value) {
148
+ return typeof value === "string" && Object.values(OAuthCallbackFailureCodes).includes(value);
149
+ }
150
+ function isHeadersLike(value) {
151
+ return value !== null && typeof value === "object" && typeof value.get === "function" && typeof value.append === "function";
152
+ }
153
+ function getSetCookieValues(headers) {
154
+ const getSetCookie = headers.getSetCookie;
155
+ if (getSetCookie) return getSetCookie.call(headers);
156
+ return Array.from(headers).reduce((cookies, [name, value]) => {
157
+ if (name.toLowerCase() === "set-cookie") cookies.push(value);
158
+ return cookies;
159
+ }, []);
160
+ }
161
+ /**
162
+ * A callback failure that consumers can classify without parsing messages.
163
+ *
164
+ * The message is intentionally constant. Consumers should branch on
165
+ * `code`, not on `message`, and should merge `cleanupHeaders` (or the
166
+ * lossless `cleanupSetCookies` values) with any response they create using
167
+ * repeated `Set-Cookie` appends.
168
+ */
169
+ var OAuthCallbackError = class extends Error {
170
+ code;
171
+ cleanupHeaders;
172
+ cleanupSetCookies;
173
+ constructor(code, cleanupHeaders = {}) {
174
+ super("Authentication failed");
175
+ this.name = "OAuthCallbackError";
176
+ this.code = code;
177
+ this.cleanupHeaders = new Headers(cleanupHeaders);
178
+ this.cleanupSetCookies = getSetCookieValues(this.cleanupHeaders);
179
+ Object.defineProperty(this, OAUTH_CALLBACK_ERROR_BRAND, {
180
+ configurable: false,
181
+ enumerable: false,
182
+ value: true,
183
+ writable: false
184
+ });
185
+ }
186
+ };
187
+ /**
188
+ * Cross-package-copy safe type guard for callback failures.
189
+ */
190
+ function isOAuthCallbackError(error) {
191
+ if (error instanceof OAuthCallbackError) return true;
192
+ if (error === null || typeof error !== "object") return false;
193
+ const candidate = error;
194
+ return candidate[OAUTH_CALLBACK_ERROR_BRAND] === true && isOAuthCallbackFailureCode(candidate.code) && isHeadersLike(candidate.cleanupHeaders);
195
+ }
196
+ /**
197
+ * Only a normal authorization callback whose local transaction is absent or
198
+ * expired may be retried. All other callback failures fail closed.
199
+ */
200
+ function isRecoverableOAuthCallbackError(error) {
201
+ return isOAuthCallbackError(error) && error.code === "stale_transaction";
202
+ }
203
+ //#endregion
135
204
  //#region src/auth/auth.server.ts
136
205
  /**
137
206
  * OpenID Connect Authentication Server Module
@@ -314,11 +383,6 @@ function getOAuthErrorMessage(error) {
314
383
  if (error instanceof Error) return error.message;
315
384
  return "Unknown error";
316
385
  }
317
- function shouldLogoutAfterOAuthError(error) {
318
- if (error instanceof oidc.ResponseBodyError) return error.status === 400 || error.status === 401 || error.error === "invalid_grant" || error.error === "invalid_token";
319
- const message = error instanceof Error ? error.message.toLowerCase() : "";
320
- return message.includes("invalid_grant") || message.includes("invalid_token") || message.includes("unauthorized");
321
- }
322
386
  function getLogoutScope(url) {
323
387
  const normalized = (url.searchParams.get("logout") ?? url.searchParams.get("logout_scope") ?? url.searchParams.get("scope"))?.trim().toLowerCase();
324
388
  if (normalized === "local" || normalized === "application" || normalized === "app" || normalized === "current-device") return "local";
@@ -389,11 +453,12 @@ async function getOAuthConfig() {
389
453
  });
390
454
  return configuration;
391
455
  } catch (error) {
392
- logger.error("OIDC discovery failed", error instanceof Error ? error : void 0, {
456
+ logger.error("OIDC discovery failed", void 0, {
393
457
  authority: config.authority,
394
- clientId: config.clientId
458
+ clientId: config.clientId,
459
+ errorType: error instanceof Error ? error.name : "UnknownError"
395
460
  });
396
- throw new Error(`OIDC discovery failed: ${getOAuthErrorMessage(error)}`);
461
+ throw new Error("OIDC discovery failed");
397
462
  }
398
463
  }
399
464
  function getJwks(jwksUri) {
@@ -666,6 +731,74 @@ function clearAuthErrorHeaders() {
666
731
  function hasAuthError(request) {
667
732
  return request.headers.get("Cookie")?.includes(`${AUTH_ERROR_COOKIE}=`) ?? false;
668
733
  }
734
+ function escapeRegExp(value) {
735
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
736
+ }
737
+ function getOAuthStateIdFromRequest(request) {
738
+ const cookies = request.headers.get("Cookie");
739
+ if (!cookies) return null;
740
+ const cookieRegex = new RegExp(`(?:^|;\\s*)${escapeRegExp(OAUTH_STATE_COOKIE)}=([^;]*)`);
741
+ const cookieValue = cookies.match(cookieRegex)?.[1];
742
+ if (!cookieValue) return null;
743
+ try {
744
+ const stateId = decodeURIComponent(cookieValue);
745
+ return stateId.length > 0 ? stateId : null;
746
+ } catch {
747
+ return null;
748
+ }
749
+ }
750
+ function hasNonEmptyCallbackValue(value) {
751
+ return value !== null && value.trim().length > 0;
752
+ }
753
+ function appendHeadersPreservingSetCookie(target, source) {
754
+ const sourceHeaders = new Headers(source);
755
+ const setCookies = sourceHeaders.getSetCookie?.call(sourceHeaders) ?? Array.from(sourceHeaders).filter(([name]) => name.toLowerCase() === "set-cookie").map(([, value]) => value);
756
+ if (setCookies.length > 0) for (const cookie of setCookies) target.append("Set-Cookie", cookie);
757
+ else {
758
+ const setCookie = sourceHeaders.get("Set-Cookie");
759
+ if (setCookie) target.append("Set-Cookie", setCookie);
760
+ }
761
+ for (const [name, value] of sourceHeaders) if (name.toLowerCase() !== "set-cookie") target.set(name, value);
762
+ }
763
+ function appendOAuthStateCookieClear(headers) {
764
+ headers.append("Set-Cookie", serializeTemporaryAuthCookie(OAUTH_STATE_COOKIE, "", { maxAge: 0 }));
765
+ }
766
+ function isUsableOAuthState(state, requireNonce) {
767
+ return state !== null && typeof state.state === "string" && state.state.length > 0 && typeof state.codeVerifier === "string" && state.codeVerifier.length > 0 && (state.nonce === void 0 ? !requireNonce : typeof state.nonce === "string" && state.nonce.length > 0) && typeof state.createdAt === "number" && Number.isFinite(state.createdAt) && (state.returnUrl === void 0 || typeof state.returnUrl === "string") && (state.kcAction === void 0 || typeof state.kcAction === "string");
768
+ }
769
+ function getCallbackFailureCode(error, stage) {
770
+ if (isOAuthCallbackError(error)) return error.code;
771
+ switch (stage) {
772
+ case "configuration": return OAuthCallbackFailureCodes.ConfigurationError;
773
+ case "state":
774
+ case "storage": return OAuthCallbackFailureCodes.StorageError;
775
+ case "application_action": return OAuthCallbackFailureCodes.ApplicationActionFailed;
776
+ case "token_exchange": return OAuthCallbackFailureCodes.TokenExchangeFailed;
777
+ case "session_creation": return OAuthCallbackFailureCodes.SessionCreationFailed;
778
+ default: return OAuthCallbackFailureCodes.StorageError;
779
+ }
780
+ }
781
+ async function createOAuthCallbackFailure(request, stateId, failureCode) {
782
+ const cleanupHeaders = new Headers();
783
+ let cleanupError;
784
+ if (stateId) try {
785
+ await deleteOAuthState(stateId);
786
+ } catch (error) {
787
+ cleanupError = error;
788
+ }
789
+ try {
790
+ appendHeadersPreservingSetCookie(cleanupHeaders, await destroyAuthSession(request));
791
+ } catch (error) {
792
+ cleanupError ??= error;
793
+ }
794
+ appendOAuthStateCookieClear(cleanupHeaders);
795
+ const effectiveFailureCode = cleanupError ? OAuthCallbackFailureCodes.StorageError : failureCode;
796
+ if (cleanupError) logger.warn("OAuth callback cleanup failed", {
797
+ failureCode: effectiveFailureCode,
798
+ errorType: cleanupError instanceof Error ? cleanupError.name : "UnknownError"
799
+ });
800
+ return new OAuthCallbackError(effectiveFailureCode, cleanupHeaders);
801
+ }
669
802
  /**
670
803
  * Initiate OAuth login flow
671
804
  *
@@ -718,7 +851,7 @@ async function login(request, returnUrlOrOptions) {
718
851
  if (options.kcAction) authorizationParameters.kc_action = options.kcAction;
719
852
  const authorizationUrl = oidc.buildAuthorizationUrl(oidcConfig, authorizationParameters);
720
853
  const headers = new Headers();
721
- headers.append("Set-Cookie", serializeTemporaryAuthCookie(OAUTH_STATE_COOKIE, stateId, { maxAge: 600 }));
854
+ headers.append("Set-Cookie", serializeTemporaryAuthCookie(OAUTH_STATE_COOKIE, stateId, { maxAge: OAUTH_STATE_TTL_SECONDS }));
722
855
  logger.info("OIDC authorization flow initiated", {
723
856
  clientId: config.clientId,
724
857
  redirectUri: config.redirectUri,
@@ -747,9 +880,13 @@ async function login(request, returnUrlOrOptions) {
747
880
  * - Redirect to home to start fresh login
748
881
  */
749
882
  async function handleCallback(request) {
883
+ let stateId = null;
884
+ let callbackStage = "configuration";
750
885
  try {
886
+ stateId = getOAuthStateIdFromRequest(request);
751
887
  const config = getAuthConfig();
752
888
  const hasLandingPage = getEffectiveHasLandingPage(config);
889
+ const requiresNonce = config.scope.split(/\s+/).includes("openid");
753
890
  const url = new URL(request.url);
754
891
  const code = url.searchParams.get("code");
755
892
  const state = url.searchParams.get("state");
@@ -757,28 +894,31 @@ async function handleCallback(request) {
757
894
  const errorDescription = url.searchParams.get("error_description");
758
895
  const kcAction = url.searchParams.get("kc_action");
759
896
  const kcActionStatus = url.searchParams.get("kc_action_status");
897
+ const hasCode = hasNonEmptyCallbackValue(code);
898
+ const hasState = hasNonEmptyCallbackValue(state);
899
+ const hasApplicationAction = Boolean(hasNonEmptyCallbackValue(kcAction) || hasNonEmptyCallbackValue(kcActionStatus));
760
900
  logger.info("OIDC callback received", {
761
- hasCode: !!code,
762
- hasState: !!state,
901
+ hasCode,
902
+ hasState,
763
903
  hasError: Boolean(error),
764
904
  hasErrorDescription: Boolean(errorDescription),
765
- hasKcAction: Boolean(kcAction),
766
- hasKcActionStatus: Boolean(kcActionStatus),
905
+ hasKcAction: hasNonEmptyCallbackValue(kcAction),
906
+ hasKcActionStatus: hasNonEmptyCallbackValue(kcActionStatus),
767
907
  clientId: config.clientId,
768
908
  redirectUri: config.redirectUri
769
909
  });
770
910
  if (error) {
771
- if (kcActionStatus && state) {
772
- const cookies = request.headers.get("Cookie");
773
- const oauthStateRegex = new RegExp(`${OAUTH_STATE_COOKIE}=([^;]+)`);
774
- const stateId = (cookies?.match(oauthStateRegex))?.[1];
911
+ if (kcActionStatus && hasState) {
912
+ callbackStage = "state";
775
913
  if (stateId) {
776
- const oauthState = await getOAuthState(stateId);
914
+ const oauthState = await getOAuthState(stateId, { throwOnMalformed: true });
777
915
  const stateAge = oauthState ? Date.now() - oauthState.createdAt : Number.POSITIVE_INFINITY;
778
- if (oauthState && oauthState.state === state && stateAge <= 600 * 1e3) {
916
+ if (oauthState && isUsableOAuthState(oauthState, requiresNonce) && oauthState.state === state && stateAge <= OAUTH_STATE_TTL_MS) {
917
+ callbackStage = "storage";
779
918
  await deleteOAuthState(stateId);
919
+ callbackStage = "application_action";
780
920
  const headers = new Headers();
781
- headers.append("Set-Cookie", serializeTemporaryAuthCookie(OAUTH_STATE_COOKIE, "", { maxAge: 0 }));
921
+ appendOAuthStateCookieClear(headers);
782
922
  const redirectUrl = appendApplicationInitiatedActionResult(sanitizeOAuthReturnUrl(oauthState.returnUrl, request) ?? "/", kcAction || oauthState.kcAction, kcActionStatus);
783
923
  logger.info("Application initiated action returned with OAuth error", {
784
924
  hasError: Boolean(error),
@@ -799,9 +939,12 @@ async function handleCallback(request) {
799
939
  });
800
940
  if (error === "access_denied") {
801
941
  logger.warn("Access denied for this application", { clientId: config.clientId });
942
+ callbackStage = "storage";
943
+ if (stateId) await deleteOAuthState(stateId);
802
944
  const headers = await destroyAuthSession(request);
945
+ appendOAuthStateCookieClear(headers);
803
946
  headers.append("Set-Cookie", serializeTemporaryAuthCookie(AUTH_ERROR_COOKIE, "access_denied", { maxAge: 300 }));
804
- headers.append("Set-Cookie", serializeTemporaryAuthCookie(AUTH_ERROR_DESC_COOKIE, errorDescription || "You do not have access to this application.", { maxAge: 300 }));
947
+ headers.append("Set-Cookie", serializeTemporaryAuthCookie(AUTH_ERROR_DESC_COOKIE, "You do not have access to this application.", { maxAge: 300 }));
805
948
  const baseUrl = new URL(config.redirectUri).origin;
806
949
  logger.info("Redirecting to /auth/login to show access_denied error", {
807
950
  hasLandingPage,
@@ -812,30 +955,47 @@ async function handleCallback(request) {
812
955
  }
813
956
  if (error === "login_required") {
814
957
  logger.info("Login required, user session may have expired");
958
+ callbackStage = "storage";
959
+ if (stateId) await deleteOAuthState(stateId);
815
960
  const headers = await destroyAuthSession(request);
961
+ appendOAuthStateCookieClear(headers);
816
962
  const baseUrl = new URL(config.redirectUri).origin;
817
963
  return createRedirectResponse(baseUrl, { headers });
818
964
  }
965
+ callbackStage = "storage";
966
+ if (stateId) await deleteOAuthState(stateId);
819
967
  const headers = await destroyAuthSession(request);
968
+ appendOAuthStateCookieClear(headers);
820
969
  headers.append("Set-Cookie", serializeTemporaryAuthCookie(AUTH_ERROR_COOKIE, error, { maxAge: 300 }));
821
- if (errorDescription) headers.append("Set-Cookie", serializeTemporaryAuthCookie(AUTH_ERROR_DESC_COOKIE, errorDescription, { maxAge: 300 }));
822
970
  const baseUrl = new URL(config.redirectUri).origin;
823
971
  return createRedirectResponse(baseUrl, { headers });
824
972
  }
825
- const cookies = request.headers.get("Cookie");
826
- const oauthStateRegex = new RegExp(`${OAUTH_STATE_COOKIE}=([^;]+)`);
827
- const stateId = (cookies?.match(oauthStateRegex))?.[1];
828
- if (!stateId) throw new Error("OAuth state ID not found in cookie");
829
- const oauthState = await getOAuthState(stateId);
830
- if (!oauthState) throw new Error("No OAuth state found - state may have expired");
831
- if (!state) throw new Error("Missing required callback state parameter");
832
- if (oauthState.state !== state) throw new Error(`OAuth state mismatch`);
833
- if (Date.now() - oauthState.createdAt > 600 * 1e3) throw new Error("OAuth state expired");
834
- if (!code) {
973
+ if (!hasState) {
974
+ callbackStage = hasApplicationAction ? "application_action" : "state";
975
+ throw new OAuthCallbackError(hasApplicationAction ? OAuthCallbackFailureCodes.ApplicationActionFailed : OAuthCallbackFailureCodes.MalformedCallback);
976
+ }
977
+ if (!hasCode && !hasApplicationAction) {
978
+ callbackStage = "state";
979
+ throw new OAuthCallbackError(OAuthCallbackFailureCodes.MalformedCallback);
980
+ }
981
+ if (!stateId) {
982
+ callbackStage = hasApplicationAction ? "application_action" : "state";
983
+ throw new OAuthCallbackError(hasApplicationAction ? OAuthCallbackFailureCodes.ApplicationActionFailed : OAuthCallbackFailureCodes.StaleTransaction);
984
+ }
985
+ callbackStage = "state";
986
+ const oauthState = await getOAuthState(stateId, { throwOnMalformed: true });
987
+ if (!oauthState) throw new OAuthCallbackError(hasApplicationAction ? OAuthCallbackFailureCodes.ApplicationActionFailed : OAuthCallbackFailureCodes.StaleTransaction);
988
+ if (!isUsableOAuthState(oauthState, requiresNonce)) throw new OAuthCallbackError(OAuthCallbackFailureCodes.StorageError);
989
+ if (oauthState.state !== state) throw new OAuthCallbackError(OAuthCallbackFailureCodes.StateMismatch);
990
+ const isApplicationAction = Boolean(hasApplicationAction || oauthState.kcAction);
991
+ if (Date.now() - oauthState.createdAt > OAUTH_STATE_TTL_MS) throw new OAuthCallbackError(isApplicationAction ? OAuthCallbackFailureCodes.ApplicationActionFailed : OAuthCallbackFailureCodes.StaleTransaction);
992
+ if (!hasCode) {
835
993
  if (kcActionStatus) {
994
+ callbackStage = "storage";
836
995
  await deleteOAuthState(stateId);
996
+ callbackStage = "application_action";
837
997
  const headers = new Headers();
838
- headers.append("Set-Cookie", serializeTemporaryAuthCookie(OAUTH_STATE_COOKIE, "", { maxAge: 0 }));
998
+ appendOAuthStateCookieClear(headers);
839
999
  const redirectUrl = appendApplicationInitiatedActionResult(sanitizeOAuthReturnUrl(oauthState.returnUrl, request) ?? "/", kcAction || oauthState.kcAction, kcActionStatus);
840
1000
  logger.info("Application initiated action returned without authorization code", {
841
1001
  hasKcAction: Boolean(kcAction || oauthState.kcAction),
@@ -843,11 +1003,13 @@ async function handleCallback(request) {
843
1003
  });
844
1004
  return createRedirectResponse(redirectUrl, { headers });
845
1005
  }
846
- throw new Error("Missing required callback code parameter");
1006
+ throw new OAuthCallbackError(isApplicationAction ? OAuthCallbackFailureCodes.ApplicationActionFailed : OAuthCallbackFailureCodes.MalformedCallback);
847
1007
  }
848
1008
  logger.info("Initiating token exchange");
1009
+ callbackStage = "configuration";
849
1010
  const oidcConfig = await getOAuthConfig();
850
1011
  const authorizationResponseUrl = createAuthorizationResponseUrl(request.url, config.redirectUri);
1012
+ callbackStage = "token_exchange";
851
1013
  const tokenResult = await oidc.authorizationCodeGrant(oidcConfig, authorizationResponseUrl, {
852
1014
  pkceCodeVerifier: oauthState.codeVerifier,
853
1015
  expectedState: oauthState.state,
@@ -855,22 +1017,24 @@ async function handleCallback(request) {
855
1017
  idTokenExpected: config.scope.split(/\s+/).includes("openid")
856
1018
  });
857
1019
  logger.info("Token exchange completed successfully");
858
- const sessionHeaders = await createAuthSession(request, await createSessionData(tokenResult, getClaimsFromTokenResponse(tokenResult)));
1020
+ callbackStage = "session_creation";
1021
+ const newSessionData = await createSessionData(tokenResult, getClaimsFromTokenResponse(tokenResult));
1022
+ callbackStage = "session_creation";
1023
+ const sessionHeaders = await createAuthSession(request, newSessionData);
1024
+ callbackStage = "storage";
859
1025
  await deleteOAuthState(stateId);
860
- const headers = new Headers(sessionHeaders);
861
- headers.append("Set-Cookie", serializeTemporaryAuthCookie(OAUTH_STATE_COOKIE, "", { maxAge: 0 }));
1026
+ const headers = new Headers();
1027
+ appendHeadersPreservingSetCookie(headers, sessionHeaders);
1028
+ appendOAuthStateCookieClear(headers);
862
1029
  logger.info("OAuth callback processed successfully");
863
1030
  return createRedirectResponse(appendApplicationInitiatedActionResult(sanitizeOAuthReturnUrl(oauthState.returnUrl, request) ?? "/", kcAction || oauthState.kcAction, kcActionStatus), { headers });
864
1031
  } catch (error) {
865
- logger.error("OAuth callback failed", void 0, { errorType: error instanceof Error ? error.name : "UnknownError" });
866
- try {
867
- const cookies = request.headers.get("Cookie");
868
- const oauthStateRegex = new RegExp(`${OAUTH_STATE_COOKIE}=([^;]+)`);
869
- const stateId = (cookies?.match(oauthStateRegex))?.[1];
870
- if (stateId) await deleteOAuthState(stateId);
871
- } catch {}
872
- await destroyAuthSession(request);
873
- throw new Error("Authentication failed");
1032
+ const failureCode = getCallbackFailureCode(error, callbackStage);
1033
+ logger.error("OAuth callback failed", void 0, {
1034
+ failureCode,
1035
+ errorType: error instanceof Error ? error.name : "UnknownError"
1036
+ });
1037
+ throw await createOAuthCallbackFailure(request, stateId, failureCode);
874
1038
  }
875
1039
  }
876
1040
  /**
@@ -1019,12 +1183,31 @@ async function handleFrontChannelLogout(request) {
1019
1183
  return new Response(null, { status: 204 });
1020
1184
  }
1021
1185
  }
1186
+ function getAccessTokenExpiry(accessToken) {
1187
+ if (!accessToken) return null;
1188
+ try {
1189
+ const [, encodedPayload] = accessToken.split(".");
1190
+ if (!encodedPayload) return null;
1191
+ const payload = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
1192
+ return typeof payload.exp === "number" && Number.isFinite(payload.exp) ? payload.exp * 1e3 : null;
1193
+ } catch {
1194
+ return null;
1195
+ }
1196
+ }
1197
+ function isSessionExpired(sessionData) {
1198
+ const now = Date.now();
1199
+ if (typeof sessionData.expiresAt === "number" && (!Number.isFinite(sessionData.expiresAt) || now >= sessionData.expiresAt)) return true;
1200
+ const accessTokenExpiry = getAccessTokenExpiry(sessionData.accessToken);
1201
+ return accessTokenExpiry !== null && now >= accessTokenExpiry;
1202
+ }
1022
1203
  /**
1023
1204
  * Get current user from session
1024
1205
  */
1025
1206
  async function getUser(request) {
1026
1207
  try {
1027
- return (await getAuthSession(request)).user || null;
1208
+ const sessionData = await getAuthSession(request);
1209
+ if (!sessionData.user || !sessionData.accessToken || isSessionExpired(sessionData)) return null;
1210
+ return sessionData.user;
1028
1211
  } catch (error) {
1029
1212
  logger.error("Failed to get user", error instanceof Error ? error : void 0);
1030
1213
  return null;
@@ -1060,8 +1243,16 @@ async function getAccessToken(request) {
1060
1243
  async function refreshTokens(request, refreshToken) {
1061
1244
  try {
1062
1245
  const sessionData = await getAuthSession(request);
1063
- const tokenToRefresh = refreshToken || sessionData.refreshToken;
1064
- if (!tokenToRefresh) {
1246
+ if (!sessionData.sessionId) {
1247
+ logger.warn("Cannot refresh tokens without an active Redis session");
1248
+ return {
1249
+ success: false,
1250
+ error: "No active session",
1251
+ shouldLogout: true
1252
+ };
1253
+ }
1254
+ const sessionRefreshToken = sessionData.refreshToken;
1255
+ if (!sessionRefreshToken) {
1065
1256
  logger.warn("No refresh token available");
1066
1257
  return {
1067
1258
  success: false,
@@ -1069,10 +1260,27 @@ async function refreshTokens(request, refreshToken) {
1069
1260
  shouldLogout: true
1070
1261
  };
1071
1262
  }
1263
+ if (refreshToken && refreshToken !== sessionRefreshToken) {
1264
+ logger.warn("Rejected refresh token that does not match the active Redis session");
1265
+ return {
1266
+ success: false,
1267
+ error: "Refresh token does not match active session",
1268
+ shouldLogout: true
1269
+ };
1270
+ }
1271
+ const tokenToRefresh = sessionRefreshToken;
1072
1272
  logger.info("Initiating token refresh");
1073
1273
  const oidcConfig = await getOAuthConfig();
1074
1274
  const refreshResult = await oidc.refreshTokenGrant(oidcConfig, tokenToRefresh);
1075
1275
  logger.info("Token refresh successful");
1276
+ if (!refreshResult.access_token) {
1277
+ logger.error("Token refresh returned no access token");
1278
+ return {
1279
+ success: false,
1280
+ error: "No access token in refresh response",
1281
+ shouldLogout: true
1282
+ };
1283
+ }
1076
1284
  const newRefreshToken = refreshResult.refresh_token || tokenToRefresh;
1077
1285
  const expiresAt = refreshResult.expires_in ? Date.now() + refreshResult.expires_in * 1e3 : sessionData.expiresAt;
1078
1286
  await updateAuthSession(request, {
@@ -1096,7 +1304,7 @@ async function refreshTokens(request, refreshToken) {
1096
1304
  return {
1097
1305
  success: false,
1098
1306
  error: getOAuthErrorMessage(error),
1099
- shouldLogout: shouldLogoutAfterOAuthError(error)
1307
+ shouldLogout: true
1100
1308
  };
1101
1309
  }
1102
1310
  }
@@ -1125,15 +1333,16 @@ function resolveRefreshThreshold() {
1125
1333
  async function shouldRefreshToken(request) {
1126
1334
  try {
1127
1335
  const sessionData = await getAuthSession(request);
1336
+ const now = Date.now();
1337
+ const sessionExpired = typeof sessionData.expiresAt === "number" && now >= sessionData.expiresAt;
1128
1338
  if (!sessionData.refreshToken || sessionData.refreshToken.length === 0) {
1129
- if (sessionData.expiresAt && Date.now() >= sessionData.expiresAt) {
1339
+ if (sessionExpired) {
1130
1340
  logger.info("Token expired and no refresh token, forcing local application logout");
1131
1341
  throw await logout(await createAutomaticLogoutRequest(request, "expired-no-refresh-token"));
1132
1342
  }
1133
1343
  return false;
1134
1344
  }
1135
1345
  if (!sessionData.expiresAt || !sessionData.accessToken) return false;
1136
- const now = Date.now();
1137
1346
  let jwtExpiry = null;
1138
1347
  try {
1139
1348
  const tokenParts = sessionData.accessToken.split(".");
@@ -1144,8 +1353,9 @@ async function shouldRefreshToken(request) {
1144
1353
  } catch {}
1145
1354
  return (jwtExpiry && jwtExpiry < sessionData.expiresAt ? jwtExpiry : sessionData.expiresAt) - now <= REFRESH_THRESHOLD;
1146
1355
  } catch (error) {
1356
+ if (error instanceof Response) throw error;
1147
1357
  logger.error("Error checking token refresh", error instanceof Error ? error : void 0);
1148
- return false;
1358
+ throw error;
1149
1359
  }
1150
1360
  }
1151
1361
  /**
@@ -1156,13 +1366,11 @@ async function autoRefreshTokens(request) {
1156
1366
  if (await shouldRefreshToken(request)) {
1157
1367
  logger.info("Auto-refreshing tokens due to expiry");
1158
1368
  const result = await refreshTokens(request);
1159
- if (result.success) logger.info("Auto-refresh completed successfully");
1369
+ if (result.success && result.tokens?.access_token) logger.info("Auto-refresh completed successfully");
1160
1370
  else {
1161
- logger.error("Auto-refresh failed", void 0, { error: result.error });
1162
- if (result.shouldLogout) {
1163
- logger.error("Refresh token invalid, forcing local application logout");
1164
- throw await logout(await createAutomaticLogoutRequest(request, "refresh-failed"));
1165
- }
1371
+ logger.error("Auto-refresh failed", void 0, { error: result.error ?? "Refresh response did not include an access token" });
1372
+ logger.error("Refresh token unusable, forcing local application logout");
1373
+ throw await logout(await createAutomaticLogoutRequest(request, "refresh-failed"));
1166
1374
  }
1167
1375
  }
1168
1376
  } catch (error) {
@@ -1266,7 +1474,8 @@ async function protectRoute(request, protection, loaderFn) {
1266
1474
  case "public": try {
1267
1475
  await autoRefreshTokens(request);
1268
1476
  return loaderFn(await getUser(request) ?? void 0);
1269
- } catch {
1477
+ } catch (error) {
1478
+ if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
1270
1479
  return loaderFn(void 0);
1271
1480
  }
1272
1481
  case "auth":
@@ -1360,7 +1569,7 @@ async function attemptTokenRefresh(request) {
1360
1569
  return {
1361
1570
  success: false,
1362
1571
  error: refreshResult.error,
1363
- shouldLogout: refreshResult.shouldLogout
1572
+ shouldLogout: true
1364
1573
  };
1365
1574
  }
1366
1575
  const tokens = refreshResult.tokens;
@@ -1399,6 +1608,6 @@ function shouldAttemptTokenRefresh(response, hasAuth) {
1399
1608
  return hasAuth && isTokenExpiredResponse(response);
1400
1609
  }
1401
1610
  //#endregion
1402
- export { resetAuthClaimMapping as A, handleCallback as C, logout as D, login as E, refreshTokens as O, handleBackChannelLogout as S, hasAuthError as T, createAuthorizationResponseUrl as _, configureRouteProtection as a, getAuthError as b, protectRoute as c, resetRouteProtectionConfig as d, shouldRefreshToken as f, configureAuthClaimMapping as g, clearAuthServerCache as h, authRoute as i, requireAuth as k, protectedRoute as l, clearAuthErrorHeaders as m, isTokenExpiredResponse as n, onboardingRoute as o, subscriptionRoute as p, shouldAttemptTokenRefresh as r, policyRoute as s, attemptTokenRefresh as t, publicRoute as u, extractUserInfo as v, handleFrontChannelLogout as w, getUser as x, getAccessToken as y };
1611
+ export { resetAuthClaimMapping as A, handleCallback as C, logout as D, login as E, OAuthCallbackFailureCodes as M, isOAuthCallbackError as N, refreshTokens as O, isRecoverableOAuthCallbackError as P, handleBackChannelLogout as S, hasAuthError as T, createAuthorizationResponseUrl as _, configureRouteProtection as a, getAuthError as b, protectRoute as c, resetRouteProtectionConfig as d, shouldRefreshToken as f, configureAuthClaimMapping as g, clearAuthServerCache as h, authRoute as i, OAuthCallbackError as j, requireAuth as k, protectedRoute as l, clearAuthErrorHeaders as m, isTokenExpiredResponse as n, onboardingRoute as o, subscriptionRoute as p, shouldAttemptTokenRefresh as r, policyRoute as s, attemptTokenRefresh as t, publicRoute as u, extractUserInfo as v, handleFrontChannelLogout as w, getUser as x, getAccessToken as y };
1403
1612
 
1404
- //# sourceMappingURL=server-DPCu40ud.mjs.map
1613
+ //# sourceMappingURL=server-Cw86FdT-.mjs.map