@spfn/auth 0.2.0-beta.59 → 0.2.0-beta.60

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/nextjs/api.ts","../../src/nextjs/interceptors/login-register.ts","../../src/nextjs/interceptors/cookie-options.ts","../../src/nextjs/interceptors/general-auth.ts","../../src/nextjs/interceptors/key-rotation.ts","../../src/nextjs/interceptors/oauth.ts","../../src/nextjs/session-helpers.ts","../../src/nextjs/interceptors/index.ts"],"sourcesContent":["/**\n * @spfn/auth/adapters/nextjs/api\n *\n * Next.js Adapter for SPFN Auth\n *\n * Provides automatic interceptor registration for seamless auth flow:\n * - Session management (HttpOnly cookies)\n * - JWT generation and signing\n * - Public key encryption\n *\n * @requires next >= 13.0.0\n *\n * @example\n * ```typescript\n * // Just import to auto-register interceptors\n * import '@spfn/auth/nextjs/api';\n * ```\n */\n\n// Re-export interceptors for advanced usage\nimport { registerInterceptors } from \"@spfn/core/nextjs/server\";\nimport { authInterceptors } from './interceptors';\n\n// Auto-register interceptors on import\nregisterInterceptors('auth', authInterceptors);","/**\n * Login/Register Interceptor\n *\n * Automatically handles key generation and session management\n * for login and register endpoints\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs/server';\nimport { generateKeyPair, sealSession, getSessionTtl, COOKIE_NAMES, authLogger } from '@spfn/auth/server';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * Login and Register Interceptor\n *\n * Request: Generates key pair and adds publicKey to request body\n * Response: Saves privateKey to HttpOnly cookie\n */\nexport const loginRegisterInterceptor: InterceptorRule =\n {\n pathPattern: /^\\/_auth\\/(login|register)$/,\n method: 'POST',\n\n request: async (ctx, next) =>\n {\n // Get old session if exists (for key rotation on login)\n const oldKeyId = ctx.cookies.get(COOKIE_NAMES.SESSION_KEY_ID);\n\n // Extract remember option from request body (if provided)\n const remember = ctx.body?.remember;\n\n // Generate new key pair\n const keyPair = generateKeyPair('ES256');\n\n // Add publicKey data to request body\n if (!ctx.body)\n {\n ctx.body = {};\n }\n\n ctx.body.publicKey = keyPair.publicKey;\n ctx.body.keyId = keyPair.keyId;\n ctx.body.fingerprint = keyPair.fingerprint;\n ctx.body.algorithm = keyPair.algorithm;\n ctx.body.keySize = Buffer.from(keyPair.publicKey, 'base64').length;\n\n // Add oldKeyId for login (key rotation)\n if (ctx.path === '/_auth/login' && oldKeyId)\n {\n ctx.body.oldKeyId = oldKeyId;\n }\n\n // Remove remember from body (not part of contract)\n delete ctx.body.remember;\n\n // Store privateKey and remember in metadata for response interceptor\n ctx.metadata.privateKey = keyPair.privateKey;\n ctx.metadata.keyId = keyPair.keyId;\n ctx.metadata.algorithm = keyPair.algorithm;\n ctx.metadata.remember = remember;\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Only process successful responses\n if (ctx.response.status !== 200)\n {\n await next();\n return;\n }\n\n // Handle both wrapped ({ data: { userId } }) and direct ({ userId }) responses\n const userData = ctx.response.body?.data || ctx.response.body;\n if (!userData?.userId)\n {\n authLogger.interceptor.login.error('No userId in response');\n await next();\n return;\n }\n\n try\n {\n // Get session TTL (priority: runtime > global > env > default)\n const ttl = getSessionTtl(ctx.metadata.remember);\n\n // Encrypt session data\n const sessionData =\n {\n userId: userData.userId,\n privateKey: ctx.metadata.privateKey,\n keyId: ctx.metadata.keyId,\n algorithm: ctx.metadata.algorithm,\n };\n\n const sealed = await sealSession(sessionData, ttl);\n\n // Set HttpOnly session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Set keyId cookie (for oldKeyId lookup)\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: ctx.metadata.keyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.login.error('Failed to save session', err);\n }\n\n await next();\n },\n };","/**\n * Shared cookie option helpers for auth interceptors\n *\n * SPFN_AUTH_COOKIE_SECURE env var allows overriding the Secure flag.\n * - unset: defaults to NODE_ENV === 'production'\n * - \"true\" / \"false\": explicit override\n *\n * Useful for HTTP-only staging environments (e.g. bastion over plain HTTP).\n */\n\n/**\n * Resolve whether cookies should have the Secure flag.\n *\n * Priority:\n * 1. SPFN_AUTH_COOKIE_SECURE (explicit override)\n * 2. NODE_ENV === 'production'\n */\nfunction resolveSecure(): boolean\n{\n const override = process.env.SPFN_AUTH_COOKIE_SECURE;\n\n if (override !== undefined)\n {\n return override === 'true';\n }\n\n return process.env.NODE_ENV === 'production';\n}\n\n/**\n * Whether cookies should have the Secure flag.\n * Evaluated once at module load time.\n */\nexport const cookieSecure = resolveSecure();\n","/**\n * General Authentication Interceptor\n *\n * Handles authentication for all API requests except login/register\n * - Session validation and renewal\n * - JWT generation and signing\n * - Expired session cleanup\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs/server';\nimport { unsealSession, sealSession, shouldRefreshSession, generateClientToken, getSessionTtl, COOKIE_NAMES, authLogger } from '@spfn/auth/server';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * Check if path requires authentication\n */\nfunction requiresAuth(path: string): boolean\n{\n // Paths that don't require auth\n const publicPaths = [\n /^\\/_auth\\/login$/,\n /^\\/_auth\\/register$/,\n /^\\/_auth\\/codes$/, // Send verification code\n /^\\/_auth\\/codes\\/verify$/, // Verify code\n /^\\/_auth\\/exists$/, // Check account exists\n ];\n\n return !publicPaths.some((pattern) => pattern.test(path));\n}\n\n/**\n * General Authentication Interceptor\n *\n * Applies to all paths except login/register/codes\n * - Validates session\n * - Generates JWT token\n * - Refreshes session if needed\n * - Clears expired sessions\n */\nexport const generalAuthInterceptor: InterceptorRule =\n{\n pathPattern: '*', // Match all paths, filter by requiresAuth()\n method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],\n\n request: async (ctx, next) =>\n {\n // Skip if path doesn't require auth\n if (!requiresAuth(ctx.path))\n {\n authLogger.interceptor.general.debug(`Public path, skipping auth: ${ctx.path}`);\n await next();\n return;\n }\n\n // Log available cookies\n const cookieNames = Array.from(ctx.cookies.keys());\n authLogger.interceptor.general.debug('Available cookies:', {\n cookieNames,\n totalCount: cookieNames.length,\n lookingFor: COOKIE_NAMES.SESSION,\n });\n\n const sessionCookie = ctx.cookies.get(COOKIE_NAMES.SESSION);\n\n authLogger.interceptor.general.debug('Request', {\n method: ctx.method,\n path: ctx.path,\n hasSession: !!sessionCookie,\n sessionLength: sessionCookie?.length ?? 0,\n sessionPrefix: sessionCookie?.slice(0, 20) ?? '',\n sessionSuffix: sessionCookie?.slice(-10) ?? '',\n });\n\n // No session cookie\n if (!sessionCookie)\n {\n authLogger.interceptor.general.debug('No session cookie, proceeding without auth');\n // Let request proceed - server will return 401\n await next();\n return;\n }\n\n try\n {\n // Decrypt and validate session\n const session = await unsealSession(sessionCookie);\n\n authLogger.interceptor.general.debug('Session valid', {\n userId: session.userId,\n keyId: session.keyId,\n });\n\n // Check if session should be refreshed (within 24h of expiry)\n const needsRefresh = await shouldRefreshSession(sessionCookie, 24);\n\n if (needsRefresh)\n {\n authLogger.interceptor.general.debug('Session needs refresh (within 24h of expiry)');\n // Mark for session renewal in response interceptor\n ctx.metadata.refreshSession = true;\n ctx.metadata.sessionData = session;\n }\n\n // Generate JWT token\n const token = generateClientToken(\n {\n userId: session.userId,\n keyId: session.keyId,\n timestamp: Date.now(),\n },\n session.privateKey,\n session.algorithm,\n { expiresIn: '15m' }\n );\n\n authLogger.interceptor.general.debug('Generated JWT token (expires in 15m)');\n\n // Add authentication headers\n ctx.headers['Authorization'] = `Bearer ${token}`;\n ctx.headers['X-Key-Id'] = session.keyId;\n\n // Store session info in metadata\n ctx.metadata.userId = session.userId;\n ctx.metadata.sessionValid = true;\n }\n catch (error)\n {\n const err = error as Error;\n const msg = err.message.toLowerCase();\n\n // Session expired or invalid\n if (msg.includes('expired') || msg.includes('invalid'))\n {\n authLogger.interceptor.general.warn('Session expired or invalid', {\n message: err.message,\n cookieLength: sessionCookie.length,\n cookiePrefix: sessionCookie.slice(0, 20),\n cookieSuffix: sessionCookie.slice(-10),\n });\n authLogger.interceptor.general.debug('Marking session for cleanup');\n\n // Mark for cleanup in response interceptor\n ctx.metadata.clearSession = true;\n ctx.metadata.sessionValid = false;\n }\n else\n {\n authLogger.interceptor.general.error('Failed to process session', err);\n }\n }\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Backend returned 401 with a valid session — server rejected it\n if (ctx.response.status === 401 && ctx.metadata.sessionValid)\n {\n authLogger.interceptor.general.warn('Backend returned 401, clearing session');\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: '',\n options: { maxAge: 0, path: '/' },\n });\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: '',\n options: { maxAge: 0, path: '/' },\n });\n\n await next();\n return;\n }\n\n // Clear expired/invalid session\n if (ctx.metadata.clearSession)\n {\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n }\n // Refresh session if needed and request was successful\n else if (ctx.metadata.refreshSession && ctx.response.status === 200)\n {\n try\n {\n const sessionData = ctx.metadata.sessionData;\n const ttl = getSessionTtl();\n\n // Re-encrypt session with new TTL\n const sealed = await sealSession(sessionData, ttl);\n\n // Update session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Update keyId cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: sessionData.keyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n authLogger.interceptor.general.info('Session refreshed', {\n userId: sessionData.userId,\n sealedLength: sealed.length,\n sealedPrefix: sealed.slice(0, 20),\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.general.error('Failed to refresh session', err);\n }\n }\n // Handle logout (clear session)\n else if (ctx.path === '/_auth/logout' && ctx.response.ok)\n {\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n }\n\n await next();\n },\n};","/**\n * Key Rotation Interceptor\n *\n * Handles key rotation with new key generation and session update\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs/server';\nimport { generateKeyPair, unsealSession, sealSession, generateClientToken, getSessionTtl, COOKIE_NAMES, authLogger } from '@spfn/auth/server';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * Key Rotation Interceptor\n *\n * Request: Generates new key pair and adds to body, authenticates with current key\n * Response: Updates session with new privateKey\n */\nexport const keyRotationInterceptor: InterceptorRule =\n{\n pathPattern: '/_auth/keys/rotate',\n method: 'POST',\n\n request: async (ctx, next) =>\n {\n const sessionCookie = ctx.cookies.get(COOKIE_NAMES.SESSION);\n\n if (!sessionCookie)\n {\n await next();\n return;\n }\n\n try\n {\n // Get current session\n const currentSession = await unsealSession(sessionCookie);\n\n // Generate new key pair\n const newKeyPair = generateKeyPair('ES256');\n\n // Add new publicKey to request body\n if (!ctx.body)\n {\n ctx.body = {};\n }\n\n ctx.body.publicKey = newKeyPair.publicKey;\n ctx.body.keyId = newKeyPair.keyId;\n ctx.body.fingerprint = newKeyPair.fingerprint;\n ctx.body.algorithm = newKeyPair.algorithm;\n ctx.body.keySize = Buffer.from(newKeyPair.publicKey, 'base64').length;\n\n console.log('New key generated:', newKeyPair);\n console.log('publicKey:', newKeyPair.publicKey);\n console.log('keyId:', newKeyPair.keyId);\n console.log('fingerprint:', newKeyPair.fingerprint);\n\n // Authenticate with CURRENT key\n const token = generateClientToken(\n {\n userId: currentSession.userId,\n keyId: currentSession.keyId,\n action: 'rotate_key',\n timestamp: Date.now(),\n },\n currentSession.privateKey,\n currentSession.algorithm,\n {expiresIn: '15m'}\n );\n\n ctx.headers['Authorization'] = `Bearer ${token}`;\n ctx.headers['X-Key-Id'] = currentSession.keyId;\n\n // Store new key and userId in metadata\n ctx.metadata.newPrivateKey = newKeyPair.privateKey;\n ctx.metadata.newKeyId = newKeyPair.keyId;\n ctx.metadata.newAlgorithm = newKeyPair.algorithm;\n ctx.metadata.userId = currentSession.userId;\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.keyRotation.error('Failed to prepare key rotation', err);\n }\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Only update session on successful rotation\n if (ctx.response.status !== 200)\n {\n await next();\n return;\n }\n\n if (!ctx.metadata.newPrivateKey || !ctx.metadata.userId)\n {\n authLogger.interceptor.keyRotation.error('Missing key rotation metadata');\n await next();\n return;\n }\n\n try\n {\n // Get session TTL\n const ttl = getSessionTtl();\n\n // Create new session with rotated key\n const newSessionData =\n {\n userId: ctx.metadata.userId,\n privateKey: ctx.metadata.newPrivateKey,\n keyId: ctx.metadata.newKeyId,\n algorithm: ctx.metadata.newAlgorithm,\n };\n\n const sealed = await sealSession(newSessionData, ttl);\n\n // Update session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Update keyId cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: ctx.metadata.newKeyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.keyRotation.error('Failed to update session after rotation', err);\n }\n\n await next();\n },\n};","/**\n * OAuth Interceptors\n *\n * 1. oauthUrlInterceptor: OAuth URL 요청 시 키쌍 생성 및 state 주입\n * 2. oauthFinalizeInterceptor: OAuth 완료 시 pending session에서 세션 저장\n */\n\nimport type { InterceptorRule, ResponseInterceptorContext } from '@spfn/core/nextjs/server';\nimport {\n generateKeyPair,\n createOAuthState,\n sealSession,\n COOKIE_NAMES,\n getSessionTtl,\n authLogger,\n} from '@spfn/auth/server';\nimport { sealPendingSession, unsealPendingSession } from '../session-helpers';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * OAuth URL Interceptor\n *\n * POST /_auth/oauth/:provider/url 요청을 가로채서\n * 키쌍 생성 및 state 주입 처리\n */\nexport const oauthUrlInterceptor: InterceptorRule = {\n pathPattern: /^\\/_auth\\/oauth\\/\\w+\\/url$/,\n method: 'POST',\n\n request: async (ctx, next) =>\n {\n const provider = ctx.path.split('/')[3]; // google, github, etc.\n const returnUrl = ctx.body?.returnUrl || '/';\n\n // 키쌍 생성\n const keyPair = generateKeyPair('ES256');\n\n // state 생성 (publicKey 포함)\n const state = await createOAuthState({\n provider,\n returnUrl,\n publicKey: keyPair.publicKey,\n keyId: keyPair.keyId,\n fingerprint: keyPair.fingerprint,\n algorithm: keyPair.algorithm,\n });\n\n // body에 state 주입\n if (!ctx.body)\n {\n ctx.body = {};\n }\n ctx.body.state = state;\n\n // pending session 저장용 metadata\n ctx.metadata.pendingSession = {\n privateKey: keyPair.privateKey,\n keyId: keyPair.keyId,\n algorithm: keyPair.algorithm,\n };\n\n authLogger.interceptor.oauth?.debug?.('OAuth state created', {\n provider,\n keyId: keyPair.keyId,\n });\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // 성공 응답이고 pending session이 있으면 쿠키 설정\n if (ctx.response.ok && ctx.metadata.pendingSession)\n {\n try\n {\n const sealed = await sealPendingSession(ctx.metadata.pendingSession);\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.OAUTH_PENDING,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'lax', // OAuth 리다이렉트 허용\n maxAge: 600, // 10분\n path: '/',\n },\n });\n\n authLogger.interceptor.oauth?.debug?.('Pending session cookie set', {\n keyId: ctx.metadata.pendingSession.keyId,\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.oauth?.error?.('Failed to set pending session', err);\n }\n }\n\n await next();\n },\n};\n\n/**\n * Finalize 실패 시 에러 응답 설정 + pending 쿠키 정리\n */\nfunction setFinalizeError(ctx: ResponseInterceptorContext, message: string): void\n{\n ctx.response.ok = false;\n ctx.response.status = 401;\n ctx.response.statusText = 'Unauthorized';\n ctx.response.body = { success: false, message };\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.OAUTH_PENDING,\n value: '',\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'lax',\n maxAge: 0,\n path: '/',\n },\n });\n}\n\n/**\n * OAuth Finalize Interceptor\n *\n * POST /_auth/oauth/finalize 요청을 가로채서\n * pending session에서 세션 저장\n */\nexport const oauthFinalizeInterceptor: InterceptorRule = {\n pathPattern: /^\\/_auth\\/oauth\\/finalize$/,\n method: 'POST',\n\n response: async (ctx, next) =>\n {\n // 성공 응답일 때만 처리\n if (!ctx.response.ok)\n {\n await next();\n return;\n }\n\n const pendingCookie = ctx.cookies.get(COOKIE_NAMES.OAUTH_PENDING);\n if (!pendingCookie)\n {\n authLogger.interceptor.oauth?.warn?.('No pending session cookie found');\n setFinalizeError(ctx, 'OAuth session expired. Please try again.');\n await next();\n return;\n }\n\n try\n {\n // pending session에서 privateKey 복원\n const pendingSession = await unsealPendingSession(pendingCookie);\n\n // body에서 userId, keyId 추출\n const { userId, keyId } = ctx.response.body || {};\n\n if (!userId || !keyId)\n {\n authLogger.interceptor.oauth?.error?.('Missing userId or keyId in response');\n setFinalizeError(ctx, 'OAuth finalize failed: missing credentials');\n await next();\n return;\n }\n\n // keyId 일치 확인\n if (pendingSession.keyId !== keyId)\n {\n authLogger.interceptor.oauth?.error?.('KeyId mismatch', {\n expected: pendingSession.keyId,\n received: keyId,\n });\n setFinalizeError(ctx, 'OAuth session mismatch. Please try again.');\n await next();\n return;\n }\n\n // 세션 생성\n const ttl = getSessionTtl();\n const sessionToken = await sealSession({\n userId,\n privateKey: pendingSession.privateKey,\n keyId: pendingSession.keyId,\n algorithm: pendingSession.algorithm,\n }, ttl);\n\n // 세션 쿠키 설정\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sessionToken,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // keyId 쿠키 설정\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: keyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // pending session 쿠키 삭제 (maxAge: 0)\n ctx.setCookies.push({\n name: COOKIE_NAMES.OAUTH_PENDING,\n value: '',\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'lax',\n maxAge: 0,\n path: '/',\n },\n });\n\n authLogger.interceptor.oauth?.debug?.('OAuth session finalized', {\n userId,\n keyId,\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.oauth?.error?.('Failed to finalize OAuth session', err);\n setFinalizeError(ctx, err.message);\n }\n\n await next();\n },\n};\n","/**\n * Session helpers for Next.js\n *\n * Server-side only (uses next/headers)\n */\n\nimport * as jose from 'jose';\nimport { cookies } from 'next/headers.js';\nimport {\n sealSession,\n unsealSession,\n COOKIE_NAMES,\n getSessionTtl,\n parseDuration,\n type SessionData,\n type KeyAlgorithmType,\n} from '@spfn/auth/server';\nimport { env } from '@spfn/auth/config';\nimport { logger } from '@spfn/core/logger';\n\nexport type { SessionData };\n\n/**\n * Pending OAuth session data (before user ID is known)\n */\nexport interface PendingSessionData\n{\n privateKey: string;\n keyId: string;\n algorithm: KeyAlgorithmType;\n}\n\n/**\n * Public session information (excludes sensitive data)\n */\nexport interface PublicSession\n{\n /** User ID */\n userId: string;\n}\n\n/**\n * Options for saveSession\n */\nexport interface SaveSessionOptions\n{\n /**\n * Session TTL (time to live)\n *\n * Supports:\n * - Number: seconds (e.g., 2592000)\n * - String: duration format ('30d', '12h', '45m', '3600s')\n *\n * If not provided, uses global configuration:\n * 1. Global config (configureAuth)\n * 2. Environment variable (SPFN_AUTH_SESSION_TTL)\n * 3. Default (7d)\n */\n maxAge?: number | string;\n\n /**\n * Remember me option\n *\n * When true, uses extended session duration (if configured)\n */\n remember?: boolean;\n}\n\n/**\n * Save session to HttpOnly cookie\n *\n * @param data - Session data to save\n * @param options - Session options (maxAge, remember)\n *\n * @example\n * ```typescript\n * // Use global configuration\n * await saveSession(sessionData);\n *\n * // Custom TTL with duration string\n * await saveSession(sessionData, { maxAge: '30d' });\n *\n * // Custom TTL in seconds\n * await saveSession(sessionData, { maxAge: 2592000 });\n *\n * // Remember me\n * await saveSession(sessionData, { remember: true });\n * ```\n */\nexport async function saveSession(\n data: SessionData,\n options?: SaveSessionOptions\n): Promise<void>\n{\n // Calculate maxAge\n let maxAge: number;\n\n if (options?.maxAge !== undefined)\n {\n // Custom maxAge provided\n maxAge = typeof options.maxAge === 'number'\n ? options.maxAge\n : parseDuration(options.maxAge);\n }\n else\n {\n // Use getSessionTtl for consistent configuration\n maxAge = getSessionTtl();\n }\n\n const token = await sealSession(data, maxAge);\n const cookieStore = await cookies();\n\n cookieStore.set(COOKIE_NAMES.SESSION, token, {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'strict',\n path: '/',\n maxAge\n });\n}\n\n/**\n * Get session from HttpOnly cookie\n *\n * Returns public session info only (excludes privateKey, algorithm, keyId)\n */\nexport async function getSession(): Promise<PublicSession | null>\n{\n const cookieStore = await cookies();\n const sessionCookie = cookieStore.get(COOKIE_NAMES.SESSION);\n\n if (!sessionCookie)\n {\n return null;\n }\n\n try\n {\n logger.debug('Validating session cookie', { cookie: sessionCookie.value });\n const session = await unsealSession(sessionCookie.value);\n // Return only public information\n return {\n userId: session.userId,\n };\n }\n catch (error)\n {\n // Session expired or invalid\n // Note: Cannot delete cookies in Server Components (read-only)\n // Use validateSessionMiddleware() in Next.js middleware for automatic cleanup\n logger.debug('Session validation failed', {\n error: error instanceof Error ? error.message : String(error)\n });\n\n return null;\n }\n}\n\n/**\n * Clear session cookie\n */\nexport async function clearSession(): Promise<void>\n{\n const cookieStore = await cookies();\n cookieStore.delete(COOKIE_NAMES.SESSION);\n cookieStore.delete(COOKIE_NAMES.SESSION_KEY_ID);\n}\n\n// ============================================================================\n// Pending OAuth Session (for OAuth flow)\n// ============================================================================\n\n/**\n * Get encryption key for pending session\n */\nasync function getPendingSessionKey(): Promise<Uint8Array>\n{\n const secret = env.SPFN_AUTH_SESSION_SECRET;\n const encoder = new TextEncoder();\n const data = encoder.encode(`oauth-pending:${secret}`);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(hashBuffer);\n}\n\n/**\n * Seal pending session data (for OAuth flow)\n *\n * @param data - Pending session data (privateKey, keyId, algorithm)\n * @param ttl - Time to live in seconds (default: 10 minutes)\n */\nexport async function sealPendingSession(\n data: PendingSessionData,\n ttl: number = 600\n): Promise<string>\n{\n const key = await getPendingSessionKey();\n\n return await new jose.EncryptJWT({ data })\n .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })\n .setIssuedAt()\n .setExpirationTime(`${ttl}s`)\n .setIssuer('spfn-auth')\n .setAudience('spfn-oauth')\n .encrypt(key);\n}\n\n/**\n * Unseal pending session data\n *\n * @param jwt - Encrypted pending session token\n */\nexport async function unsealPendingSession(jwt: string): Promise<PendingSessionData>\n{\n const key = await getPendingSessionKey();\n\n const { payload } = await jose.jwtDecrypt(jwt, key, {\n issuer: 'spfn-auth',\n audience: 'spfn-oauth',\n });\n\n return payload.data as PendingSessionData;\n}\n\n/**\n * Get pending session from cookie\n */\nexport async function getPendingSession(): Promise<PendingSessionData | null>\n{\n const cookieStore = await cookies();\n const pendingCookie = cookieStore.get(COOKIE_NAMES.OAUTH_PENDING);\n\n if (!pendingCookie)\n {\n return null;\n }\n\n try\n {\n return await unsealPendingSession(pendingCookie.value);\n }\n catch (error)\n {\n logger.debug('Pending session validation failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n return null;\n }\n}\n\n/**\n * Clear pending session cookie\n */\nexport async function clearPendingSession(): Promise<void>\n{\n const cookieStore = await cookies();\n cookieStore.delete(COOKIE_NAMES.OAUTH_PENDING);\n}\n","/**\n * Auth Interceptors for Next.js Proxy\n *\n * Automatically registers interceptors for authentication flow\n *\n * Order matters - more specific interceptors first:\n * 1. loginRegisterInterceptor - Most specific (login/register only)\n * 2. keyRotationInterceptor - Specific (key rotation only)\n * 3. oauthUrlInterceptor - OAuth URL generation (key generation + state injection)\n * 4. generalAuthInterceptor - General (all authenticated requests)\n */\n\nimport { loginRegisterInterceptor } from './login-register';\nimport { generalAuthInterceptor } from './general-auth';\nimport { keyRotationInterceptor } from './key-rotation';\nimport { oauthUrlInterceptor, oauthFinalizeInterceptor } from './oauth';\n\n/**\n * All auth interceptors\n *\n * Execution order:\n * 1. loginRegisterInterceptor - Handles login/register (key generation + session save)\n * 2. keyRotationInterceptor - Handles key rotation (new key generation + session update)\n * 3. oauthUrlInterceptor - Handles OAuth URL requests (key generation + state injection + pending session)\n * 4. oauthFinalizeInterceptor - Handles OAuth finalize (pending session → full session)\n * 5. generalAuthInterceptor - Handles all authenticated requests (session validation + JWT injection + session renewal)\n */\nexport const authInterceptors = [\n loginRegisterInterceptor,\n keyRotationInterceptor,\n oauthUrlInterceptor,\n oauthFinalizeInterceptor,\n generalAuthInterceptor,\n];\n\nexport { loginRegisterInterceptor } from './login-register';\nexport { generalAuthInterceptor } from './general-auth';\nexport { keyRotationInterceptor } from './key-rotation';\nexport { oauthUrlInterceptor, oauthFinalizeInterceptor } from './oauth';\n\n// Deprecated: use generalAuthInterceptor instead\nexport { generalAuthInterceptor as authenticationInterceptor };"],"mappings":"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","names":["sealSession","getSessionTtl","COOKIE_NAMES","authLogger","authLogger","COOKIE_NAMES","getSessionTtl","sealSession","generateKeyPair","unsealSession","sealSession","generateClientToken","getSessionTtl","COOKIE_NAMES","authLogger","COOKIE_NAMES","unsealSession","generateKeyPair","generateClientToken","authLogger","getSessionTtl","sealSession","generateKeyPair","sealSession","COOKIE_NAMES","getSessionTtl","authLogger","sealSession","unsealSession","COOKIE_NAMES","getSessionTtl","generateKeyPair","authLogger","COOKIE_NAMES","getSessionTtl","sealSession"]}
1
+ {"version":3,"sources":["../../src/nextjs/api.ts","../../src/server/lib/crypto.ts","../../src/server/lib/session.ts","../../src/server/logger.ts","../../src/server/lib/config.ts","../../src/nextjs/interceptors/cookie-options.ts","../../src/nextjs/interceptors/login-register.ts","../../src/nextjs/interceptors/general-auth.ts","../../src/nextjs/interceptors/key-rotation.ts","../../src/server/lib/oauth/state.ts","../../src/nextjs/session-helpers.ts","../../src/nextjs/interceptors/oauth.ts","../../src/nextjs/interceptors/index.ts"],"sourcesContent":["/**\n * @spfn/auth/adapters/nextjs/api\n *\n * Next.js Adapter for SPFN Auth\n *\n * Provides automatic interceptor registration for seamless auth flow:\n * - Session management (HttpOnly cookies)\n * - JWT generation and signing\n * - Public key encryption\n *\n * @requires next >= 13.0.0\n *\n * @example\n * ```typescript\n * // Just import to auto-register interceptors\n * import '@spfn/auth/nextjs/api';\n * ```\n */\n\n// Re-export interceptors for advanced usage\nimport { registerInterceptors } from \"@spfn/core/nextjs/server\";\nimport { authInterceptors } from './interceptors';\n\n// Auto-register interceptors on import\nregisterInterceptors('auth', authInterceptors);","/**\n * @spfn/auth - Client Crypto Helpers\n *\n * ES256 (ECDSA P-256) key generation and JWT signing for Next.js\n * Keys are stored in DER format (Base64 encoded) for efficiency\n *\n * Key Sizes:\n * - ES256 (ECDSA P-256): ~91 bytes (Base64: ~120 chars)\n * - RS256 (RSA 2048): ~294 bytes (Base64: ~392 chars)\n */\n\nimport { type KeyAlgorithmType } from '../types';\nimport crypto from 'crypto';\nimport jwt, { type Algorithm, type SignOptions } from 'jsonwebtoken';\n\ntype Unit =\n | \"Years\"\n | \"Year\"\n | \"Yrs\"\n | \"Yr\"\n | \"Y\"\n | \"Weeks\"\n | \"Week\"\n | \"W\"\n | \"Days\"\n | \"Day\"\n | \"D\"\n | \"Hours\"\n | \"Hour\"\n | \"Hrs\"\n | \"Hr\"\n | \"H\"\n | \"Minutes\"\n | \"Minute\"\n | \"Mins\"\n | \"Min\"\n | \"M\"\n | \"Seconds\"\n | \"Second\"\n | \"Secs\"\n | \"Sec\"\n | \"s\"\n | \"Milliseconds\"\n | \"Millisecond\"\n | \"Msecs\"\n | \"Msec\"\n | \"Ms\";\n\ntype UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>;\n\ntype StringValue =\n | `${number}`\n | `${number}${UnitAnyCase}`\n | `${number} ${UnitAnyCase}`;\n\nexport interface KeyPair\n{\n privateKey: string; // Base64 encoded DER\n publicKey: string; // Base64 encoded DER\n keyId: string; // UUID\n fingerprint: string; // SHA-256 hash\n algorithm: KeyAlgorithmType;\n}\n\n/**\n * Generate ECDSA P-256 key pair (ES256)\n * Recommended for optimal size and performance\n */\nexport function generateKeyPairES256(): KeyPair\n{\n const keyId = crypto.randomUUID();\n\n const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {\n namedCurve: 'P-256', // ES256\n publicKeyEncoding: {\n type: 'spki',\n format: 'der',\n },\n privateKeyEncoding: {\n type: 'pkcs8',\n format: 'der',\n },\n });\n\n // Convert Buffer to Base64\n const privateKeyB64 = privateKey.toString('base64');\n const publicKeyB64 = publicKey.toString('base64');\n\n // Generate fingerprint (SHA-256 of public key)\n const fingerprint = crypto\n .createHash('sha256')\n .update(publicKey)\n .digest('hex');\n\n return {\n privateKey: privateKeyB64,\n publicKey: publicKeyB64,\n keyId,\n fingerprint,\n algorithm: 'ES256',\n };\n}\n\n/**\n * Generate RSA 2048 key pair (RS256)\n * Fallback option, larger size but wider compatibility\n */\nexport function generateKeyPairRS256(): KeyPair\n{\n const keyId = crypto.randomUUID();\n\n const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {\n modulusLength: 2048,\n publicKeyEncoding: {\n type: 'spki',\n format: 'der',\n },\n privateKeyEncoding: {\n type: 'pkcs8',\n format: 'der',\n },\n });\n\n const privateKeyB64 = privateKey.toString('base64');\n const publicKeyB64 = publicKey.toString('base64');\n\n const fingerprint = crypto\n .createHash('sha256')\n .update(publicKey)\n .digest('hex');\n\n return {\n privateKey: privateKeyB64,\n publicKey: publicKeyB64,\n keyId,\n fingerprint,\n algorithm: 'RS256',\n };\n}\n\n/**\n * Generate key pair (defaults to ES256)\n */\nexport function generateKeyPair(\n algorithm: KeyAlgorithmType = 'ES256'\n): KeyPair\n{\n return algorithm === 'ES256'\n ? generateKeyPairES256()\n : generateKeyPairRS256();\n}\n\n/**\n * Generate JWT signed with client private key (DER format)\n */\nexport function generateClientToken(\n payload: Record<string, any>,\n privateKeyB64: string,\n algorithm: Algorithm,\n options?: {\n expiresIn?: StringValue | number;\n issuer?: string;\n }\n): string\n{\n try\n {\n // Convert Base64 back to Buffer\n const privateKeyDER = Buffer.from(privateKeyB64, 'base64');\n\n // Create key object for signing\n const privateKeyObject = crypto.createPrivateKey({\n key: privateKeyDER,\n format: 'der',\n type: 'pkcs8',\n });\n\n // Export as PEM for jwt.sign\n const privateKeyPEM = privateKeyObject.export({\n type: 'pkcs8',\n format: 'pem',\n });\n\n const signOptions: SignOptions = {\n algorithm,\n issuer: options?.issuer || 'spfn-client',\n expiresIn: options?.expiresIn ?? '15m', // Default to 15 minutes\n }\n\n return jwt.sign(payload, privateKeyPEM, signOptions);\n }\n catch (error)\n {\n throw new Error(\n `Failed to generate client token: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n}\n\n/**\n * Get key size information\n */\nexport function getKeySize(publicKeyB64: string): {\n bytes: number;\n base64Length: number;\n}\n{\n const keyDER = Buffer.from(publicKeyB64, 'base64');\n\n return {\n bytes: keyDER.length,\n base64Length: publicKeyB64.length,\n };\n}\n\n/**\n * Check if key should be rotated based on creation date\n */\nexport function shouldRotateKey(\n createdAt: Date,\n rotationDays: number = 90\n): {\n shouldRotate: boolean;\n daysRemaining: number;\n}\n{\n const now = new Date();\n const ageInDays = Math.floor(\n (now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24)\n );\n const daysRemaining = Math.max(0, rotationDays - ageInDays);\n\n return {\n shouldRotate: daysRemaining <= 7, // Warn 7 days before expiry\n daysRemaining,\n };\n}","/**\n * @spfn/auth - Client Session Management\n *\n * Uses Jose JWE (JSON Web Encryption) to securely store session data in cookies\n * More efficient than Iron Session with better Edge Runtime support\n */\n\nimport * as jose from 'jose';\nimport { env } from '@spfn/auth/config';\nimport { env as coreEnv } from '@spfn/core/config';\nimport { authLogger } from '../logger';\n\nimport { type KeyAlgorithmType } from '../types';\n\nexport interface SessionData\n{\n userId: string;\n privateKey: string; // Base64 encoded DER\n keyId: string;\n algorithm: KeyAlgorithmType;\n}\n\n/**\n * Get session secret key derived from environment\n * Must be at least 32 characters (256-bit)\n *\n * Derives a 32-byte key using SHA-256 to ensure compatibility with Jose A256GCM\n */\nasync function getSessionSecretKey(): Promise<Uint8Array>\n{\n const secret = env.SPFN_AUTH_SESSION_SECRET;\n\n // Derive a 32-byte key using SHA-256 for A256GCM compatibility\n // Use Web Crypto API for universal compatibility (browser + Node.js)\n const encoder = new TextEncoder();\n const data = encoder.encode(secret);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(hashBuffer);\n}\n\n/**\n * Get a short fingerprint of the current secret key for debugging\n * Logs only the first 8 hex chars of the SHA-256 hash — safe to expose\n */\nasync function getSecretFingerprint(): Promise<string>\n{\n const key = await getSessionSecretKey();\n const hash = await crypto.subtle.digest('SHA-256', key.buffer as ArrayBuffer);\n const hex = Array.from(new Uint8Array(hash))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n return hex.slice(0, 8);\n}\n\n/**\n * Seal session data into encrypted JWT (JWE)\n *\n * @param data - Session data to encrypt\n * @param ttl - Time to live in seconds (default: 7 days)\n * @returns Encrypted JWT string\n */\nexport async function sealSession(\n data: SessionData,\n ttl: number = 60 * 60 * 24 * 7 // 7 days\n): Promise<string>\n{\n const secret = await getSessionSecretKey();\n\n const result = await new jose.EncryptJWT({ data })\n .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })\n .setIssuedAt()\n .setExpirationTime(`${ttl}s`)\n .setIssuer('spfn-auth')\n .setAudience('spfn-client')\n .encrypt(secret);\n\n if (coreEnv.NODE_ENV !== 'production')\n {\n const fingerprint = await getSecretFingerprint();\n authLogger.session.debug(`Sealed session`, {\n secretFingerprint: fingerprint,\n resultLength: result.length,\n resultPrefix: result.slice(0, 20),\n });\n }\n\n return result;\n}\n\n/**\n * Unseal encrypted JWT (JWE) to session data\n *\n * @param jwt - Encrypted JWT string\n * @returns Session data\n * @throws Error if session is invalid or expired\n */\nexport async function unsealSession(jwt: string): Promise<SessionData>\n{\n try\n {\n const secret = await getSessionSecretKey();\n\n const { payload } = await jose.jwtDecrypt(jwt, secret, {\n issuer: 'spfn-auth',\n audience: 'spfn-client',\n });\n\n return payload.data as SessionData;\n }\n catch (err)\n {\n if (err instanceof jose.errors.JWTExpired)\n {\n throw new Error('Session expired');\n }\n\n if (err instanceof jose.errors.JWEDecryptionFailed)\n {\n // Log secret fingerprint for debugging cross-process key mismatch\n if (coreEnv.NODE_ENV !== 'production')\n {\n const fingerprint = await getSecretFingerprint();\n authLogger.session.warn(`JWE decryption failed`, {\n secretFingerprint: fingerprint,\n jwtLength: jwt.length,\n jwtPrefix: jwt.slice(0, 20),\n jwtSuffix: jwt.slice(-10),\n });\n }\n\n throw new Error('Invalid session');\n }\n\n if (err instanceof jose.errors.JWTClaimValidationFailed)\n {\n throw new Error('Session validation failed');\n }\n\n throw new Error('Failed to unseal session');\n }\n}\n\n/**\n * Get session metadata without decrypting\n *\n * @param jwt - Encrypted JWT string\n * @returns Session metadata or null if invalid\n */\nexport async function getSessionInfo(jwt: string): Promise<{\n issuedAt: Date;\n expiresAt: Date;\n issuer: string;\n audience: string;\n} | null>\n{\n const secret = await getSessionSecretKey();\n\n try\n {\n const { payload } = await jose.jwtDecrypt(jwt, secret);\n\n return {\n issuedAt: new Date(payload.iat! * 1000),\n expiresAt: new Date(payload.exp! * 1000),\n issuer: payload.iss || '',\n audience: Array.isArray(payload.aud) ? payload.aud[0] : payload.aud || '',\n };\n }\n catch (err)\n {\n // Log error for debugging but return null for graceful handling\n if (coreEnv.NODE_ENV !== 'production')\n {\n authLogger.session.warn('Failed to get session info:', err instanceof Error ? err.message : 'Unknown error');\n }\n\n return null;\n }\n}\n\n/**\n * Check if session is about to expire (within threshold)\n *\n * @param jwt - Encrypted JWT string\n * @param thresholdHours - Hours before expiry to trigger refresh (default: 24)\n * @returns True if session should be refreshed\n */\nexport async function shouldRefreshSession(\n jwt: string,\n thresholdHours: number = 24\n): Promise<boolean>\n{\n const info = await getSessionInfo(jwt);\n\n if (!info)\n {\n return true;\n }\n\n const hoursRemaining = (info.expiresAt.getTime() - Date.now()) / (1000 * 60 * 60);\n\n return hoursRemaining < thresholdHours;\n}","/**\n * @spfn/auth - Centralized Logger\n *\n * All auth package loggers with consistent naming\n */\n\nimport { logger as rootLogger } from '@spfn/core/logger';\n\nexport const authLogger = {\n plugin: rootLogger.child('@spfn/auth:plugin'),\n middleware: rootLogger.child('@spfn/auth:middleware'),\n interceptor: {\n general: rootLogger.child('@spfn/auth:interceptor:general'),\n login: rootLogger.child('@spfn/auth:interceptor:login'),\n keyRotation: rootLogger.child('@spfn/auth:interceptor:key-rotation'),\n oauth: rootLogger.child('@spfn/auth:interceptor:oauth'),\n },\n session: rootLogger.child('@spfn/auth:session'),\n service: rootLogger.child('@spfn/auth:service'),\n setup: rootLogger.child('@spfn/auth:setup'),\n email: rootLogger.child('@spfn/auth:email'),\n sms: rootLogger.child('@spfn/auth:sms'),\n};","/**\n * @spfn/auth - Global Configuration\n *\n * Manages global auth configuration including session TTL\n */\n\nimport { env } from '@spfn/auth/config';\n\n/**\n * Cookie name suffix derived from PORT to isolate sessions across\n * multiple local dev instances running on the same domain (localhost).\n */\nfunction getCookieSuffix(): string\n{\n const port = process.env.PORT;\n return port ? `_${port}` : '';\n}\n\n/**\n * Cookie names used by SPFN Auth\n *\n * Names include a port-based suffix so that multiple dev instances\n * on different ports don't overwrite each other's cookies.\n */\nexport const COOKIE_NAMES = {\n /** Encrypted session data (userId, privateKey, keyId, algorithm) */\n get SESSION() { return `spfn_session${getCookieSuffix()}`; },\n /** Current key ID (for key rotation) */\n get SESSION_KEY_ID() { return `spfn_session_key_id${getCookieSuffix()}`; },\n /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */\n get OAUTH_PENDING() { return `spfn_oauth_pending${getCookieSuffix()}`; },\n};\n\n/**\n * Parse duration string to seconds\n *\n * Supports: '30d', '12h', '45m', '3600s', or plain number\n *\n * @example\n * parseDuration('30d') // 2592000 (30 days in seconds)\n * parseDuration('12h') // 43200\n * parseDuration('45m') // 2700\n * parseDuration('3600') // 3600\n */\nexport function parseDuration(duration: string | number): number\n{\n if (typeof duration === 'number')\n {\n return duration;\n }\n\n const match = duration.match(/^(\\d+)([dhms]?)$/);\n if (!match)\n {\n throw new Error(`Invalid duration format: ${duration}. Use format like '30d', '12h', '45m', '3600s', or plain number.`);\n }\n\n const value = parseInt(match[1], 10);\n const unit = match[2] || 's';\n\n switch (unit)\n {\n case 'd':\n return value * 24 * 60 * 60;\n case 'h':\n return value * 60 * 60;\n case 'm':\n return value * 60;\n case 's':\n return value;\n default:\n throw new Error(`Unknown duration unit: ${unit}`);\n }\n}\n\n/**\n * Auth configuration\n */\nexport interface AuthConfig\n{\n /**\n * Default session TTL in seconds or duration string\n *\n * Supports:\n * - Number: seconds (e.g., 2592000)\n * - String: '30d', '12h', '45m', '3600s'\n *\n * @default 7d (7 days)\n */\n sessionTtl?: string | number;\n}\n\n/**\n * Global auth configuration state\n */\nlet globalConfig: AuthConfig = {\n sessionTtl: '7d', // Default: 7 days\n};\n\n/**\n * Configure global auth settings\n *\n * @param config - Auth configuration\n *\n * @example\n * ```typescript\n * configureAuth({\n * sessionTtl: '30d', // 30 days\n * });\n * ```\n */\nexport function configureAuth(config: AuthConfig): void\n{\n globalConfig = {\n ...globalConfig,\n ...config,\n };\n}\n\n/**\n * Get current auth configuration\n */\nexport function getAuthConfig(): AuthConfig\n{\n return { ...globalConfig };\n}\n\n/**\n * Get session TTL in seconds\n *\n * Priority:\n * 1. Runtime override (remember parameter)\n * 2. Global config (configureAuth)\n * 3. Environment variable (SPFN_AUTH_SESSION_TTL) - via config module\n * 4. Default (7 days)\n */\nexport function getSessionTtl(override?: string | number): number\n{\n // 1. Runtime override\n if (override !== undefined)\n {\n return parseDuration(override);\n }\n\n // 2. Global config\n if (globalConfig.sessionTtl !== undefined)\n {\n return parseDuration(globalConfig.sessionTtl);\n }\n\n // 3. Environment variable (from config module)\n const envTtl = env.SPFN_AUTH_SESSION_TTL;\n if (envTtl)\n {\n return parseDuration(envTtl);\n }\n\n // 4. Default: 7 days\n return 7 * 24 * 60 * 60;\n}","/**\n * Shared cookie option helpers for auth interceptors\n *\n * SPFN_AUTH_COOKIE_SECURE env var allows overriding the Secure flag.\n * - unset: defaults to NODE_ENV === 'production'\n * - \"true\" / \"false\": explicit override\n *\n * Useful for HTTP-only staging environments (e.g. bastion over plain HTTP).\n */\n\n/**\n * Resolve whether cookies should have the Secure flag.\n *\n * Priority:\n * 1. SPFN_AUTH_COOKIE_SECURE (explicit override)\n * 2. NODE_ENV === 'production'\n */\nfunction resolveSecure(): boolean\n{\n const override = process.env.SPFN_AUTH_COOKIE_SECURE;\n\n if (override !== undefined)\n {\n return override === 'true';\n }\n\n return process.env.NODE_ENV === 'production';\n}\n\n/**\n * Whether cookies should have the Secure flag.\n * Evaluated once at module load time.\n */\nexport const cookieSecure = resolveSecure();\n","/**\n * Login/Register Interceptor\n *\n * Automatically handles key generation and session management\n * for login and register endpoints\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs/server';\nimport { generateKeyPair } from '../../server/lib/crypto';\nimport { sealSession } from '../../server/lib/session';\nimport { getSessionTtl, COOKIE_NAMES } from '../../server/lib/config';\nimport { authLogger } from '../../server/logger';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * Login and Register Interceptor\n *\n * Request: Generates key pair and adds publicKey to request body\n * Response: Saves privateKey to HttpOnly cookie\n */\nexport const loginRegisterInterceptor: InterceptorRule =\n {\n pathPattern: /^\\/_auth\\/(login|register)$/,\n method: 'POST',\n\n request: async (ctx, next) =>\n {\n // Get old session if exists (for key rotation on login)\n const oldKeyId = ctx.cookies.get(COOKIE_NAMES.SESSION_KEY_ID);\n\n // Extract remember option from request body (if provided)\n const remember = ctx.body?.remember;\n\n // Generate new key pair\n const keyPair = generateKeyPair('ES256');\n\n // Add publicKey data to request body\n if (!ctx.body)\n {\n ctx.body = {};\n }\n\n ctx.body.publicKey = keyPair.publicKey;\n ctx.body.keyId = keyPair.keyId;\n ctx.body.fingerprint = keyPair.fingerprint;\n ctx.body.algorithm = keyPair.algorithm;\n ctx.body.keySize = Buffer.from(keyPair.publicKey, 'base64').length;\n\n // Add oldKeyId for login (key rotation)\n if (ctx.path === '/_auth/login' && oldKeyId)\n {\n ctx.body.oldKeyId = oldKeyId;\n }\n\n // Remove remember from body (not part of contract)\n delete ctx.body.remember;\n\n // Store privateKey and remember in metadata for response interceptor\n ctx.metadata.privateKey = keyPair.privateKey;\n ctx.metadata.keyId = keyPair.keyId;\n ctx.metadata.algorithm = keyPair.algorithm;\n ctx.metadata.remember = remember;\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Only process successful responses\n if (ctx.response.status !== 200)\n {\n await next();\n return;\n }\n\n // Handle both wrapped ({ data: { userId } }) and direct ({ userId }) responses\n const userData = ctx.response.body?.data || ctx.response.body;\n if (!userData?.userId)\n {\n authLogger.interceptor.login.error('No userId in response');\n await next();\n return;\n }\n\n try\n {\n // Get session TTL (priority: runtime > global > env > default)\n const ttl = getSessionTtl(ctx.metadata.remember);\n\n // Encrypt session data\n const sessionData =\n {\n userId: userData.userId,\n privateKey: ctx.metadata.privateKey,\n keyId: ctx.metadata.keyId,\n algorithm: ctx.metadata.algorithm,\n };\n\n const sealed = await sealSession(sessionData, ttl);\n\n // Set HttpOnly session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Set keyId cookie (for oldKeyId lookup)\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: ctx.metadata.keyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.login.error('Failed to save session', err);\n }\n\n await next();\n },\n };","/**\n * General Authentication Interceptor\n *\n * Handles authentication for all API requests except login/register\n * - Session validation and renewal\n * - JWT generation and signing\n * - Expired session cleanup\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs/server';\nimport { unsealSession, sealSession, shouldRefreshSession } from '../../server/lib/session';\nimport { generateClientToken } from '../../server/lib/crypto';\nimport { getSessionTtl, COOKIE_NAMES } from '../../server/lib/config';\nimport { authLogger } from '../../server/logger';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * Check if path requires authentication\n */\nfunction requiresAuth(path: string): boolean\n{\n // Paths that don't require auth\n const publicPaths = [\n /^\\/_auth\\/login$/,\n /^\\/_auth\\/register$/,\n /^\\/_auth\\/codes$/, // Send verification code\n /^\\/_auth\\/codes\\/verify$/, // Verify code\n /^\\/_auth\\/exists$/, // Check account exists\n ];\n\n return !publicPaths.some((pattern) => pattern.test(path));\n}\n\n/**\n * General Authentication Interceptor\n *\n * Applies to all paths except login/register/codes\n * - Validates session\n * - Generates JWT token\n * - Refreshes session if needed\n * - Clears expired sessions\n */\nexport const generalAuthInterceptor: InterceptorRule =\n{\n pathPattern: '*', // Match all paths, filter by requiresAuth()\n method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],\n\n request: async (ctx, next) =>\n {\n // Skip if path doesn't require auth\n if (!requiresAuth(ctx.path))\n {\n authLogger.interceptor.general.debug(`Public path, skipping auth: ${ctx.path}`);\n await next();\n return;\n }\n\n // Log available cookies\n const cookieNames = Array.from(ctx.cookies.keys());\n authLogger.interceptor.general.debug('Available cookies:', {\n cookieNames,\n totalCount: cookieNames.length,\n lookingFor: COOKIE_NAMES.SESSION,\n });\n\n const sessionCookie = ctx.cookies.get(COOKIE_NAMES.SESSION);\n\n authLogger.interceptor.general.debug('Request', {\n method: ctx.method,\n path: ctx.path,\n hasSession: !!sessionCookie,\n sessionLength: sessionCookie?.length ?? 0,\n sessionPrefix: sessionCookie?.slice(0, 20) ?? '',\n sessionSuffix: sessionCookie?.slice(-10) ?? '',\n });\n\n // No session cookie\n if (!sessionCookie)\n {\n authLogger.interceptor.general.debug('No session cookie, proceeding without auth');\n // Let request proceed - server will return 401\n await next();\n return;\n }\n\n try\n {\n // Decrypt and validate session\n const session = await unsealSession(sessionCookie);\n\n authLogger.interceptor.general.debug('Session valid', {\n userId: session.userId,\n keyId: session.keyId,\n });\n\n // Check if session should be refreshed (within 24h of expiry)\n const needsRefresh = await shouldRefreshSession(sessionCookie, 24);\n\n if (needsRefresh)\n {\n authLogger.interceptor.general.debug('Session needs refresh (within 24h of expiry)');\n // Mark for session renewal in response interceptor\n ctx.metadata.refreshSession = true;\n ctx.metadata.sessionData = session;\n }\n\n // Generate JWT token\n const token = generateClientToken(\n {\n userId: session.userId,\n keyId: session.keyId,\n timestamp: Date.now(),\n },\n session.privateKey,\n session.algorithm,\n { expiresIn: '15m' }\n );\n\n authLogger.interceptor.general.debug('Generated JWT token (expires in 15m)');\n\n // Add authentication headers\n ctx.headers['Authorization'] = `Bearer ${token}`;\n ctx.headers['X-Key-Id'] = session.keyId;\n\n // Store session info in metadata\n ctx.metadata.userId = session.userId;\n ctx.metadata.sessionValid = true;\n }\n catch (error)\n {\n const err = error as Error;\n const msg = err.message.toLowerCase();\n\n // Session expired or invalid\n if (msg.includes('expired') || msg.includes('invalid'))\n {\n authLogger.interceptor.general.warn('Session expired or invalid', {\n message: err.message,\n cookieLength: sessionCookie.length,\n cookiePrefix: sessionCookie.slice(0, 20),\n cookieSuffix: sessionCookie.slice(-10),\n });\n authLogger.interceptor.general.debug('Marking session for cleanup');\n\n // Mark for cleanup in response interceptor\n ctx.metadata.clearSession = true;\n ctx.metadata.sessionValid = false;\n }\n else\n {\n authLogger.interceptor.general.error('Failed to process session', err);\n }\n }\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Backend returned 401 with a valid session — server rejected it\n if (ctx.response.status === 401 && ctx.metadata.sessionValid)\n {\n authLogger.interceptor.general.warn('Backend returned 401, clearing session');\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: '',\n options: { maxAge: 0, path: '/' },\n });\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: '',\n options: { maxAge: 0, path: '/' },\n });\n\n await next();\n return;\n }\n\n // Clear expired/invalid session\n if (ctx.metadata.clearSession)\n {\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n }\n // Refresh session if needed and request was successful\n else if (ctx.metadata.refreshSession && ctx.response.status === 200)\n {\n try\n {\n const sessionData = ctx.metadata.sessionData;\n const ttl = getSessionTtl();\n\n // Re-encrypt session with new TTL\n const sealed = await sealSession(sessionData, ttl);\n\n // Update session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Update keyId cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: sessionData.keyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n authLogger.interceptor.general.info('Session refreshed', {\n userId: sessionData.userId,\n sealedLength: sealed.length,\n sealedPrefix: sealed.slice(0, 20),\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.general.error('Failed to refresh session', err);\n }\n }\n // Handle logout (clear session)\n else if (ctx.path === '/_auth/logout' && ctx.response.ok)\n {\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: '',\n options: {\n maxAge: 0,\n path: '/',\n },\n });\n }\n\n await next();\n },\n};","/**\n * Key Rotation Interceptor\n *\n * Handles key rotation with new key generation and session update\n */\n\nimport type { InterceptorRule } from '@spfn/core/nextjs/server';\nimport { generateKeyPair, generateClientToken } from '../../server/lib/crypto';\nimport { unsealSession, sealSession } from '../../server/lib/session';\nimport { getSessionTtl, COOKIE_NAMES } from '../../server/lib/config';\nimport { authLogger } from '../../server/logger';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * Key Rotation Interceptor\n *\n * Request: Generates new key pair and adds to body, authenticates with current key\n * Response: Updates session with new privateKey\n */\nexport const keyRotationInterceptor: InterceptorRule =\n{\n pathPattern: '/_auth/keys/rotate',\n method: 'POST',\n\n request: async (ctx, next) =>\n {\n const sessionCookie = ctx.cookies.get(COOKIE_NAMES.SESSION);\n\n if (!sessionCookie)\n {\n await next();\n return;\n }\n\n try\n {\n // Get current session\n const currentSession = await unsealSession(sessionCookie);\n\n // Generate new key pair\n const newKeyPair = generateKeyPair('ES256');\n\n // Add new publicKey to request body\n if (!ctx.body)\n {\n ctx.body = {};\n }\n\n ctx.body.publicKey = newKeyPair.publicKey;\n ctx.body.keyId = newKeyPair.keyId;\n ctx.body.fingerprint = newKeyPair.fingerprint;\n ctx.body.algorithm = newKeyPair.algorithm;\n ctx.body.keySize = Buffer.from(newKeyPair.publicKey, 'base64').length;\n\n console.log('New key generated:', newKeyPair);\n console.log('publicKey:', newKeyPair.publicKey);\n console.log('keyId:', newKeyPair.keyId);\n console.log('fingerprint:', newKeyPair.fingerprint);\n\n // Authenticate with CURRENT key\n const token = generateClientToken(\n {\n userId: currentSession.userId,\n keyId: currentSession.keyId,\n action: 'rotate_key',\n timestamp: Date.now(),\n },\n currentSession.privateKey,\n currentSession.algorithm,\n {expiresIn: '15m'}\n );\n\n ctx.headers['Authorization'] = `Bearer ${token}`;\n ctx.headers['X-Key-Id'] = currentSession.keyId;\n\n // Store new key and userId in metadata\n ctx.metadata.newPrivateKey = newKeyPair.privateKey;\n ctx.metadata.newKeyId = newKeyPair.keyId;\n ctx.metadata.newAlgorithm = newKeyPair.algorithm;\n ctx.metadata.userId = currentSession.userId;\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.keyRotation.error('Failed to prepare key rotation', err);\n }\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // Only update session on successful rotation\n if (ctx.response.status !== 200)\n {\n await next();\n return;\n }\n\n if (!ctx.metadata.newPrivateKey || !ctx.metadata.userId)\n {\n authLogger.interceptor.keyRotation.error('Missing key rotation metadata');\n await next();\n return;\n }\n\n try\n {\n // Get session TTL\n const ttl = getSessionTtl();\n\n // Create new session with rotated key\n const newSessionData =\n {\n userId: ctx.metadata.userId,\n privateKey: ctx.metadata.newPrivateKey,\n keyId: ctx.metadata.newKeyId,\n algorithm: ctx.metadata.newAlgorithm,\n };\n\n const sealed = await sealSession(newSessionData, ttl);\n\n // Update session cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // Update keyId cookie\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: ctx.metadata.newKeyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.keyRotation.error('Failed to update session after rotation', err);\n }\n\n await next();\n },\n};","/**\n * OAuth State Management\n *\n * CSRF 방지를 위한 state 파라미터 암호화/복호화\n * - returnUrl: OAuth 성공 후 리다이렉트할 URL\n * - nonce: CSRF 방지용 일회용 토큰\n * - provider: OAuth provider (google, github 등)\n * - publicKey, keyId, fingerprint, algorithm: 클라이언트 키 정보\n * - expiresAt: state 만료 시간\n */\n\nimport * as jose from 'jose';\nimport { env } from '@spfn/auth/config';\nimport { type KeyAlgorithmType } from '../../types';\n\nexport interface OAuthState\n{\n returnUrl: string;\n nonce: string;\n provider: string;\n publicKey: string;\n keyId: string;\n fingerprint: string;\n algorithm: KeyAlgorithmType;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Get encryption key derived from session secret\n */\nasync function getStateKey(): Promise<Uint8Array>\n{\n const secret = env.SPFN_AUTH_SESSION_SECRET;\n const encoder = new TextEncoder();\n const data = encoder.encode(`oauth-state:${secret}`);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(hashBuffer);\n}\n\n/**\n * Generate random nonce\n */\nfunction generateNonce(): string\n{\n const array = new Uint8Array(16);\n crypto.getRandomValues(array);\n return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');\n}\n\nexport interface CreateOAuthStateParams\n{\n provider: string;\n returnUrl: string;\n publicKey: string;\n keyId: string;\n fingerprint: string;\n algorithm: KeyAlgorithmType;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * OAuth state 생성 및 암호화\n *\n * @param params - state 생성에 필요한 파라미터\n * @returns 암호화된 state 문자열\n */\nexport async function createOAuthState(params: CreateOAuthStateParams): Promise<string>\n{\n const key = await getStateKey();\n\n const state: OAuthState = {\n returnUrl: params.returnUrl,\n nonce: generateNonce(),\n provider: params.provider,\n publicKey: params.publicKey,\n keyId: params.keyId,\n fingerprint: params.fingerprint,\n algorithm: params.algorithm,\n metadata: params.metadata,\n };\n\n const jwe = await new jose.EncryptJWT({ state })\n .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })\n .setIssuedAt()\n .setExpirationTime('10m')\n .encrypt(key);\n\n // URL-safe base64 encoding\n return encodeURIComponent(jwe);\n}\n\n/**\n * OAuth state 복호화 및 검증\n *\n * @param encryptedState - 암호화된 state 문자열\n * @returns 복호화된 state 객체\n * @throws Error if state is invalid or expired (JWE exp claim으로 자동 검증)\n */\nexport async function verifyOAuthState(encryptedState: string): Promise<OAuthState>\n{\n const key = await getStateKey();\n\n const jwe = decodeURIComponent(encryptedState);\n const { payload } = await jose.jwtDecrypt(jwe, key);\n\n return payload.state as OAuthState;\n}\n","/**\n * Session helpers for Next.js\n *\n * Server-side only (uses next/headers)\n */\n\nimport * as jose from 'jose';\nimport { cookies } from 'next/headers.js';\nimport { sealSession, unsealSession, type SessionData } from '../server/lib/session';\nimport { COOKIE_NAMES, getSessionTtl, parseDuration } from '../server/lib/config';\nimport { type KeyAlgorithmType } from '../server/types';\nimport { env } from '@spfn/auth/config';\nimport { logger } from '@spfn/core/logger';\n\nexport type { SessionData };\n\n/**\n * Pending OAuth session data (before user ID is known)\n */\nexport interface PendingSessionData\n{\n privateKey: string;\n keyId: string;\n algorithm: KeyAlgorithmType;\n}\n\n/**\n * Public session information (excludes sensitive data)\n */\nexport interface PublicSession\n{\n /** User ID */\n userId: string;\n}\n\n/**\n * Options for saveSession\n */\nexport interface SaveSessionOptions\n{\n /**\n * Session TTL (time to live)\n *\n * Supports:\n * - Number: seconds (e.g., 2592000)\n * - String: duration format ('30d', '12h', '45m', '3600s')\n *\n * If not provided, uses global configuration:\n * 1. Global config (configureAuth)\n * 2. Environment variable (SPFN_AUTH_SESSION_TTL)\n * 3. Default (7d)\n */\n maxAge?: number | string;\n\n /**\n * Remember me option\n *\n * When true, uses extended session duration (if configured)\n */\n remember?: boolean;\n}\n\n/**\n * Save session to HttpOnly cookie\n *\n * @param data - Session data to save\n * @param options - Session options (maxAge, remember)\n *\n * @example\n * ```typescript\n * // Use global configuration\n * await saveSession(sessionData);\n *\n * // Custom TTL with duration string\n * await saveSession(sessionData, { maxAge: '30d' });\n *\n * // Custom TTL in seconds\n * await saveSession(sessionData, { maxAge: 2592000 });\n *\n * // Remember me\n * await saveSession(sessionData, { remember: true });\n * ```\n */\nexport async function saveSession(\n data: SessionData,\n options?: SaveSessionOptions\n): Promise<void>\n{\n // Calculate maxAge\n let maxAge: number;\n\n if (options?.maxAge !== undefined)\n {\n // Custom maxAge provided\n maxAge = typeof options.maxAge === 'number'\n ? options.maxAge\n : parseDuration(options.maxAge);\n }\n else\n {\n // Use getSessionTtl for consistent configuration\n maxAge = getSessionTtl();\n }\n\n const token = await sealSession(data, maxAge);\n const cookieStore = await cookies();\n\n cookieStore.set(COOKIE_NAMES.SESSION, token, {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'strict',\n path: '/',\n maxAge\n });\n}\n\n/**\n * Get session from HttpOnly cookie\n *\n * Returns public session info only (excludes privateKey, algorithm, keyId)\n */\nexport async function getSession(): Promise<PublicSession | null>\n{\n const cookieStore = await cookies();\n const sessionCookie = cookieStore.get(COOKIE_NAMES.SESSION);\n\n if (!sessionCookie)\n {\n return null;\n }\n\n try\n {\n logger.debug('Validating session cookie', { cookie: sessionCookie.value });\n const session = await unsealSession(sessionCookie.value);\n // Return only public information\n return {\n userId: session.userId,\n };\n }\n catch (error)\n {\n // Session expired or invalid\n // Note: Cannot delete cookies in Server Components (read-only)\n // Use validateSessionMiddleware() in Next.js middleware for automatic cleanup\n logger.debug('Session validation failed', {\n error: error instanceof Error ? error.message : String(error)\n });\n\n return null;\n }\n}\n\n/**\n * Clear session cookie\n */\nexport async function clearSession(): Promise<void>\n{\n const cookieStore = await cookies();\n cookieStore.delete(COOKIE_NAMES.SESSION);\n cookieStore.delete(COOKIE_NAMES.SESSION_KEY_ID);\n}\n\n// ============================================================================\n// Pending OAuth Session (for OAuth flow)\n// ============================================================================\n\n/**\n * Get encryption key for pending session\n */\nasync function getPendingSessionKey(): Promise<Uint8Array>\n{\n const secret = env.SPFN_AUTH_SESSION_SECRET;\n const encoder = new TextEncoder();\n const data = encoder.encode(`oauth-pending:${secret}`);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(hashBuffer);\n}\n\n/**\n * Seal pending session data (for OAuth flow)\n *\n * @param data - Pending session data (privateKey, keyId, algorithm)\n * @param ttl - Time to live in seconds (default: 10 minutes)\n */\nexport async function sealPendingSession(\n data: PendingSessionData,\n ttl: number = 600\n): Promise<string>\n{\n const key = await getPendingSessionKey();\n\n return await new jose.EncryptJWT({ data })\n .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })\n .setIssuedAt()\n .setExpirationTime(`${ttl}s`)\n .setIssuer('spfn-auth')\n .setAudience('spfn-oauth')\n .encrypt(key);\n}\n\n/**\n * Unseal pending session data\n *\n * @param jwt - Encrypted pending session token\n */\nexport async function unsealPendingSession(jwt: string): Promise<PendingSessionData>\n{\n const key = await getPendingSessionKey();\n\n const { payload } = await jose.jwtDecrypt(jwt, key, {\n issuer: 'spfn-auth',\n audience: 'spfn-oauth',\n });\n\n return payload.data as PendingSessionData;\n}\n\n/**\n * Get pending session from cookie\n */\nexport async function getPendingSession(): Promise<PendingSessionData | null>\n{\n const cookieStore = await cookies();\n const pendingCookie = cookieStore.get(COOKIE_NAMES.OAUTH_PENDING);\n\n if (!pendingCookie)\n {\n return null;\n }\n\n try\n {\n return await unsealPendingSession(pendingCookie.value);\n }\n catch (error)\n {\n logger.debug('Pending session validation failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n return null;\n }\n}\n\n/**\n * Clear pending session cookie\n */\nexport async function clearPendingSession(): Promise<void>\n{\n const cookieStore = await cookies();\n cookieStore.delete(COOKIE_NAMES.OAUTH_PENDING);\n}\n","/**\n * OAuth Interceptors\n *\n * 1. oauthUrlInterceptor: OAuth URL 요청 시 키쌍 생성 및 state 주입\n * 2. oauthFinalizeInterceptor: OAuth 완료 시 pending session에서 세션 저장\n */\n\nimport type { InterceptorRule, ResponseInterceptorContext } from '@spfn/core/nextjs/server';\nimport { generateKeyPair } from '../../server/lib/crypto';\nimport { createOAuthState } from '../../server/lib/oauth/state';\nimport { sealSession } from '../../server/lib/session';\nimport { COOKIE_NAMES, getSessionTtl } from '../../server/lib/config';\nimport { authLogger } from '../../server/logger';\nimport { sealPendingSession, unsealPendingSession } from '../session-helpers';\nimport { cookieSecure } from './cookie-options';\n\n/**\n * OAuth URL Interceptor\n *\n * POST /_auth/oauth/:provider/url 요청을 가로채서\n * 키쌍 생성 및 state 주입 처리\n */\nexport const oauthUrlInterceptor: InterceptorRule = {\n pathPattern: /^\\/_auth\\/oauth\\/\\w+\\/url$/,\n method: 'POST',\n\n request: async (ctx, next) =>\n {\n const provider = ctx.path.split('/')[3]; // google, github, etc.\n const returnUrl = ctx.body?.returnUrl || '/';\n\n // 키쌍 생성\n const keyPair = generateKeyPair('ES256');\n\n // state 생성 (publicKey 포함)\n const state = await createOAuthState({\n provider,\n returnUrl,\n publicKey: keyPair.publicKey,\n keyId: keyPair.keyId,\n fingerprint: keyPair.fingerprint,\n algorithm: keyPair.algorithm,\n });\n\n // body에 state 주입\n if (!ctx.body)\n {\n ctx.body = {};\n }\n ctx.body.state = state;\n\n // pending session 저장용 metadata\n ctx.metadata.pendingSession = {\n privateKey: keyPair.privateKey,\n keyId: keyPair.keyId,\n algorithm: keyPair.algorithm,\n };\n\n authLogger.interceptor.oauth?.debug?.('OAuth state created', {\n provider,\n keyId: keyPair.keyId,\n });\n\n await next();\n },\n\n response: async (ctx, next) =>\n {\n // 성공 응답이고 pending session이 있으면 쿠키 설정\n if (ctx.response.ok && ctx.metadata.pendingSession)\n {\n try\n {\n const sealed = await sealPendingSession(ctx.metadata.pendingSession);\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.OAUTH_PENDING,\n value: sealed,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'lax', // OAuth 리다이렉트 허용\n maxAge: 600, // 10분\n path: '/',\n },\n });\n\n authLogger.interceptor.oauth?.debug?.('Pending session cookie set', {\n keyId: ctx.metadata.pendingSession.keyId,\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.oauth?.error?.('Failed to set pending session', err);\n }\n }\n\n await next();\n },\n};\n\n/**\n * Finalize 실패 시 에러 응답 설정 + pending 쿠키 정리\n */\nfunction setFinalizeError(ctx: ResponseInterceptorContext, message: string): void\n{\n ctx.response.ok = false;\n ctx.response.status = 401;\n ctx.response.statusText = 'Unauthorized';\n ctx.response.body = { success: false, message };\n\n ctx.setCookies.push({\n name: COOKIE_NAMES.OAUTH_PENDING,\n value: '',\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'lax',\n maxAge: 0,\n path: '/',\n },\n });\n}\n\n/**\n * OAuth Finalize Interceptor\n *\n * POST /_auth/oauth/finalize 요청을 가로채서\n * pending session에서 세션 저장\n */\nexport const oauthFinalizeInterceptor: InterceptorRule = {\n pathPattern: /^\\/_auth\\/oauth\\/finalize$/,\n method: 'POST',\n\n response: async (ctx, next) =>\n {\n // 성공 응답일 때만 처리\n if (!ctx.response.ok)\n {\n await next();\n return;\n }\n\n const pendingCookie = ctx.cookies.get(COOKIE_NAMES.OAUTH_PENDING);\n if (!pendingCookie)\n {\n authLogger.interceptor.oauth?.warn?.('No pending session cookie found');\n setFinalizeError(ctx, 'OAuth session expired. Please try again.');\n await next();\n return;\n }\n\n try\n {\n // pending session에서 privateKey 복원\n const pendingSession = await unsealPendingSession(pendingCookie);\n\n // body에서 userId, keyId 추출\n const { userId, keyId } = ctx.response.body || {};\n\n if (!userId || !keyId)\n {\n authLogger.interceptor.oauth?.error?.('Missing userId or keyId in response');\n setFinalizeError(ctx, 'OAuth finalize failed: missing credentials');\n await next();\n return;\n }\n\n // keyId 일치 확인\n if (pendingSession.keyId !== keyId)\n {\n authLogger.interceptor.oauth?.error?.('KeyId mismatch', {\n expected: pendingSession.keyId,\n received: keyId,\n });\n setFinalizeError(ctx, 'OAuth session mismatch. Please try again.');\n await next();\n return;\n }\n\n // 세션 생성\n const ttl = getSessionTtl();\n const sessionToken = await sealSession({\n userId,\n privateKey: pendingSession.privateKey,\n keyId: pendingSession.keyId,\n algorithm: pendingSession.algorithm,\n }, ttl);\n\n // 세션 쿠키 설정\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION,\n value: sessionToken,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // keyId 쿠키 설정\n ctx.setCookies.push({\n name: COOKIE_NAMES.SESSION_KEY_ID,\n value: keyId,\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n },\n });\n\n // pending session 쿠키 삭제 (maxAge: 0)\n ctx.setCookies.push({\n name: COOKIE_NAMES.OAUTH_PENDING,\n value: '',\n options: {\n httpOnly: true,\n secure: cookieSecure,\n sameSite: 'lax',\n maxAge: 0,\n path: '/',\n },\n });\n\n authLogger.interceptor.oauth?.debug?.('OAuth session finalized', {\n userId,\n keyId,\n });\n }\n catch (error)\n {\n const err = error as Error;\n authLogger.interceptor.oauth?.error?.('Failed to finalize OAuth session', err);\n setFinalizeError(ctx, err.message);\n }\n\n await next();\n },\n};\n","/**\n * Auth Interceptors for Next.js Proxy\n *\n * Automatically registers interceptors for authentication flow\n *\n * Order matters - more specific interceptors first:\n * 1. loginRegisterInterceptor - Most specific (login/register only)\n * 2. keyRotationInterceptor - Specific (key rotation only)\n * 3. oauthUrlInterceptor - OAuth URL generation (key generation + state injection)\n * 4. generalAuthInterceptor - General (all authenticated requests)\n */\n\nimport { loginRegisterInterceptor } from './login-register';\nimport { generalAuthInterceptor } from './general-auth';\nimport { keyRotationInterceptor } from './key-rotation';\nimport { oauthUrlInterceptor, oauthFinalizeInterceptor } from './oauth';\n\n/**\n * All auth interceptors\n *\n * Execution order:\n * 1. loginRegisterInterceptor - Handles login/register (key generation + session save)\n * 2. keyRotationInterceptor - Handles key rotation (new key generation + session update)\n * 3. oauthUrlInterceptor - Handles OAuth URL requests (key generation + state injection + pending session)\n * 4. oauthFinalizeInterceptor - Handles OAuth finalize (pending session → full session)\n * 5. generalAuthInterceptor - Handles all authenticated requests (session validation + JWT injection + session renewal)\n */\nexport const authInterceptors = [\n loginRegisterInterceptor,\n keyRotationInterceptor,\n oauthUrlInterceptor,\n oauthFinalizeInterceptor,\n generalAuthInterceptor,\n];\n\nexport { loginRegisterInterceptor } from './login-register';\nexport { generalAuthInterceptor } from './general-auth';\nexport { keyRotationInterceptor } from './key-rotation';\nexport { oauthUrlInterceptor, oauthFinalizeInterceptor } from './oauth';\n\n// Deprecated: use generalAuthInterceptor instead\nexport { generalAuthInterceptor as authenticationInterceptor };"],"mappings":";AAoBA,SAAS,4BAA4B;;;ACRrC,OAAOA,aAAY;AACnB,OAAO,SAA+C;AAuD/C,SAAS,uBAChB;AACI,QAAM,QAAQA,QAAO,WAAW;AAEhC,QAAM,EAAE,YAAY,UAAU,IAAIA,QAAO,oBAAoB,MAAM;AAAA,IAC/D,YAAY;AAAA;AAAA,IACZ,mBAAmB;AAAA,MACf,MAAM;AAAA,MACN,QAAQ;AAAA,IACZ;AAAA,IACA,oBAAoB;AAAA,MAChB,MAAM;AAAA,MACN,QAAQ;AAAA,IACZ;AAAA,EACJ,CAAC;AAGD,QAAM,gBAAgB,WAAW,SAAS,QAAQ;AAClD,QAAM,eAAe,UAAU,SAAS,QAAQ;AAGhD,QAAM,cAAcA,QACf,WAAW,QAAQ,EACnB,OAAO,SAAS,EAChB,OAAO,KAAK;AAEjB,SAAO;AAAA,IACH,YAAY;AAAA,IACZ,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACf;AACJ;AAMO,SAAS,uBAChB;AACI,QAAM,QAAQA,QAAO,WAAW;AAEhC,QAAM,EAAE,YAAY,UAAU,IAAIA,QAAO,oBAAoB,OAAO;AAAA,IAChE,eAAe;AAAA,IACf,mBAAmB;AAAA,MACf,MAAM;AAAA,MACN,QAAQ;AAAA,IACZ;AAAA,IACA,oBAAoB;AAAA,MAChB,MAAM;AAAA,MACN,QAAQ;AAAA,IACZ;AAAA,EACJ,CAAC;AAED,QAAM,gBAAgB,WAAW,SAAS,QAAQ;AAClD,QAAM,eAAe,UAAU,SAAS,QAAQ;AAEhD,QAAM,cAAcA,QACf,WAAW,QAAQ,EACnB,OAAO,SAAS,EAChB,OAAO,KAAK;AAEjB,SAAO;AAAA,IACH,YAAY;AAAA,IACZ,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACf;AACJ;AAKO,SAAS,gBACZ,YAA8B,SAElC;AACI,SAAO,cAAc,UACf,qBAAqB,IACrB,qBAAqB;AAC/B;AAKO,SAAS,oBACZ,SACA,eACA,WACA,SAKJ;AACI,MACA;AAEI,UAAM,gBAAgB,OAAO,KAAK,eAAe,QAAQ;AAGzD,UAAM,mBAAmBA,QAAO,iBAAiB;AAAA,MAC7C,KAAK;AAAA,MACL,QAAQ;AAAA,MACR,MAAM;AAAA,IACV,CAAC;AAGD,UAAM,gBAAgB,iBAAiB,OAAO;AAAA,MAC1C,MAAM;AAAA,MACN,QAAQ;AAAA,IACZ,CAAC;AAED,UAAM,cAA2B;AAAA,MAC7B;AAAA,MACA,QAAQ,SAAS,UAAU;AAAA,MAC3B,WAAW,SAAS,aAAa;AAAA;AAAA,IACrC;AAEA,WAAO,IAAI,KAAK,SAAS,eAAe,WAAW;AAAA,EACvD,SACO,OACP;AACI,UAAM,IAAI;AAAA,MACN,oCAAoC,iBAAiB,QAAQ,MAAM,UAAU,eAAe;AAAA,IAChG;AAAA,EACJ;AACJ;;;AC9LA,YAAY,UAAU;AACtB,SAAS,WAAW;AACpB,SAAS,OAAO,eAAe;;;ACH/B,SAAS,UAAU,kBAAkB;AAE9B,IAAM,aAAa;AAAA,EACtB,QAAQ,WAAW,MAAM,mBAAmB;AAAA,EAC5C,YAAY,WAAW,MAAM,uBAAuB;AAAA,EACpD,aAAa;AAAA,IACT,SAAS,WAAW,MAAM,gCAAgC;AAAA,IAC1D,OAAO,WAAW,MAAM,8BAA8B;AAAA,IACtD,aAAa,WAAW,MAAM,qCAAqC;AAAA,IACnE,OAAO,WAAW,MAAM,8BAA8B;AAAA,EAC1D;AAAA,EACA,SAAS,WAAW,MAAM,oBAAoB;AAAA,EAC9C,SAAS,WAAW,MAAM,oBAAoB;AAAA,EAC9C,OAAO,WAAW,MAAM,kBAAkB;AAAA,EAC1C,OAAO,WAAW,MAAM,kBAAkB;AAAA,EAC1C,KAAK,WAAW,MAAM,gBAAgB;AAC1C;;;ADMA,eAAe,sBACf;AACI,QAAM,SAAS,IAAI;AAInB,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,MAAM;AAClC,QAAM,aAAa,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AAC7D,SAAO,IAAI,WAAW,UAAU;AACpC;AAMA,eAAe,uBACf;AACI,QAAM,MAAM,MAAM,oBAAoB;AACtC,QAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI,MAAqB;AAC5E,QAAM,MAAM,MAAM,KAAK,IAAI,WAAW,IAAI,CAAC,EACtC,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAC1C,KAAK,EAAE;AACZ,SAAO,IAAI,MAAM,GAAG,CAAC;AACzB;AASA,eAAsB,YAClB,MACA,MAAc,KAAK,KAAK,KAAK,GAEjC;AACI,QAAM,SAAS,MAAM,oBAAoB;AAEzC,QAAM,SAAS,MAAM,IAAS,gBAAW,EAAE,KAAK,CAAC,EAC5C,mBAAmB,EAAE,KAAK,OAAO,KAAK,UAAU,CAAC,EACjD,YAAY,EACZ,kBAAkB,GAAG,GAAG,GAAG,EAC3B,UAAU,WAAW,EACrB,YAAY,aAAa,EACzB,QAAQ,MAAM;AAEnB,MAAI,QAAQ,aAAa,cACzB;AACI,UAAM,cAAc,MAAM,qBAAqB;AAC/C,eAAW,QAAQ,MAAM,kBAAkB;AAAA,MACvC,mBAAmB;AAAA,MACnB,cAAc,OAAO;AAAA,MACrB,cAAc,OAAO,MAAM,GAAG,EAAE;AAAA,IACpC,CAAC;AAAA,EACL;AAEA,SAAO;AACX;AASA,eAAsB,cAAcC,MACpC;AACI,MACA;AACI,UAAM,SAAS,MAAM,oBAAoB;AAEzC,UAAM,EAAE,QAAQ,IAAI,MAAW,gBAAWA,MAAK,QAAQ;AAAA,MACnD,QAAQ;AAAA,MACR,UAAU;AAAA,IACd,CAAC;AAED,WAAO,QAAQ;AAAA,EACnB,SACO,KACP;AACI,QAAI,eAAoB,YAAO,YAC/B;AACI,YAAM,IAAI,MAAM,iBAAiB;AAAA,IACrC;AAEA,QAAI,eAAoB,YAAO,qBAC/B;AAEI,UAAI,QAAQ,aAAa,cACzB;AACI,cAAM,cAAc,MAAM,qBAAqB;AAC/C,mBAAW,QAAQ,KAAK,yBAAyB;AAAA,UAC7C,mBAAmB;AAAA,UACnB,WAAWA,KAAI;AAAA,UACf,WAAWA,KAAI,MAAM,GAAG,EAAE;AAAA,UAC1B,WAAWA,KAAI,MAAM,GAAG;AAAA,QAC5B,CAAC;AAAA,MACL;AAEA,YAAM,IAAI,MAAM,iBAAiB;AAAA,IACrC;AAEA,QAAI,eAAoB,YAAO,0BAC/B;AACI,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC/C;AAEA,UAAM,IAAI,MAAM,0BAA0B;AAAA,EAC9C;AACJ;AAQA,eAAsB,eAAeA,MAMrC;AACI,QAAM,SAAS,MAAM,oBAAoB;AAEzC,MACA;AACI,UAAM,EAAE,QAAQ,IAAI,MAAW,gBAAWA,MAAK,MAAM;AAErD,WAAO;AAAA,MACH,UAAU,IAAI,KAAK,QAAQ,MAAO,GAAI;AAAA,MACtC,WAAW,IAAI,KAAK,QAAQ,MAAO,GAAI;AAAA,MACvC,QAAQ,QAAQ,OAAO;AAAA,MACvB,UAAU,MAAM,QAAQ,QAAQ,GAAG,IAAI,QAAQ,IAAI,CAAC,IAAI,QAAQ,OAAO;AAAA,IAC3E;AAAA,EACJ,SACO,KACP;AAEI,QAAI,QAAQ,aAAa,cACzB;AACI,iBAAW,QAAQ,KAAK,+BAA+B,eAAe,QAAQ,IAAI,UAAU,eAAe;AAAA,IAC/G;AAEA,WAAO;AAAA,EACX;AACJ;AASA,eAAsB,qBAClBA,MACA,iBAAyB,IAE7B;AACI,QAAM,OAAO,MAAM,eAAeA,IAAG;AAErC,MAAI,CAAC,MACL;AACI,WAAO;AAAA,EACX;AAEA,QAAM,kBAAkB,KAAK,UAAU,QAAQ,IAAI,KAAK,IAAI,MAAM,MAAO,KAAK;AAE9E,SAAO,iBAAiB;AAC5B;;;AEpMA,SAAS,OAAAC,YAAW;AAMpB,SAAS,kBACT;AACI,QAAM,OAAO,QAAQ,IAAI;AACzB,SAAO,OAAO,IAAI,IAAI,KAAK;AAC/B;AAQO,IAAM,eAAe;AAAA;AAAA,EAExB,IAAI,UAAU;AAAE,WAAO,eAAe,gBAAgB,CAAC;AAAA,EAAI;AAAA;AAAA,EAE3D,IAAI,iBAAiB;AAAE,WAAO,sBAAsB,gBAAgB,CAAC;AAAA,EAAI;AAAA;AAAA,EAEzE,IAAI,gBAAgB;AAAE,WAAO,qBAAqB,gBAAgB,CAAC;AAAA,EAAI;AAC3E;AAaO,SAAS,cAAc,UAC9B;AACI,MAAI,OAAO,aAAa,UACxB;AACI,WAAO;AAAA,EACX;AAEA,QAAM,QAAQ,SAAS,MAAM,kBAAkB;AAC/C,MAAI,CAAC,OACL;AACI,UAAM,IAAI,MAAM,4BAA4B,QAAQ,kEAAkE;AAAA,EAC1H;AAEA,QAAM,QAAQ,SAAS,MAAM,CAAC,GAAG,EAAE;AACnC,QAAM,OAAO,MAAM,CAAC,KAAK;AAEzB,UAAQ,MACR;AAAA,IACI,KAAK;AACD,aAAO,QAAQ,KAAK,KAAK;AAAA,IAC7B,KAAK;AACD,aAAO,QAAQ,KAAK;AAAA,IACxB,KAAK;AACD,aAAO,QAAQ;AAAA,IACnB,KAAK;AACD,aAAO;AAAA,IACX;AACI,YAAM,IAAI,MAAM,0BAA0B,IAAI,EAAE;AAAA,EACxD;AACJ;AAsBA,IAAI,eAA2B;AAAA,EAC3B,YAAY;AAAA;AAChB;AAuCO,SAAS,cAAc,UAC9B;AAEI,MAAI,aAAa,QACjB;AACI,WAAO,cAAc,QAAQ;AAAA,EACjC;AAGA,MAAI,aAAa,eAAe,QAChC;AACI,WAAO,cAAc,aAAa,UAAU;AAAA,EAChD;AAGA,QAAM,SAASC,KAAI;AACnB,MAAI,QACJ;AACI,WAAO,cAAc,MAAM;AAAA,EAC/B;AAGA,SAAO,IAAI,KAAK,KAAK;AACzB;;;AC9IA,SAAS,gBACT;AACI,QAAM,WAAW,QAAQ,IAAI;AAE7B,MAAI,aAAa,QACjB;AACI,WAAO,aAAa;AAAA,EACxB;AAEA,SAAO,QAAQ,IAAI,aAAa;AACpC;AAMO,IAAM,eAAe,cAAc;;;ACbnC,IAAM,2BACT;AAAA,EACI,aAAa;AAAA,EACb,QAAQ;AAAA,EAER,SAAS,OAAO,KAAK,SACrB;AAEI,UAAM,WAAW,IAAI,QAAQ,IAAI,aAAa,cAAc;AAG5D,UAAM,WAAW,IAAI,MAAM;AAG3B,UAAM,UAAU,gBAAgB,OAAO;AAGvC,QAAI,CAAC,IAAI,MACT;AACI,UAAI,OAAO,CAAC;AAAA,IAChB;AAEA,QAAI,KAAK,YAAY,QAAQ;AAC7B,QAAI,KAAK,QAAQ,QAAQ;AACzB,QAAI,KAAK,cAAc,QAAQ;AAC/B,QAAI,KAAK,YAAY,QAAQ;AAC7B,QAAI,KAAK,UAAU,OAAO,KAAK,QAAQ,WAAW,QAAQ,EAAE;AAG5D,QAAI,IAAI,SAAS,kBAAkB,UACnC;AACI,UAAI,KAAK,WAAW;AAAA,IACxB;AAGA,WAAO,IAAI,KAAK;AAGhB,QAAI,SAAS,aAAa,QAAQ;AAClC,QAAI,SAAS,QAAQ,QAAQ;AAC7B,QAAI,SAAS,YAAY,QAAQ;AACjC,QAAI,SAAS,WAAW;AAExB,UAAM,KAAK;AAAA,EACf;AAAA,EAEA,UAAU,OAAO,KAAK,SACtB;AAEI,QAAI,IAAI,SAAS,WAAW,KAC5B;AACI,YAAM,KAAK;AACX;AAAA,IACJ;AAGA,UAAM,WAAW,IAAI,SAAS,MAAM,QAAQ,IAAI,SAAS;AACzD,QAAI,CAAC,UAAU,QACf;AACI,iBAAW,YAAY,MAAM,MAAM,uBAAuB;AAC1D,YAAM,KAAK;AACX;AAAA,IACJ;AAEA,QACA;AAEI,YAAM,MAAM,cAAc,IAAI,SAAS,QAAQ;AAG/C,YAAM,cACF;AAAA,QACI,QAAQ,SAAS;AAAA,QACjB,YAAY,IAAI,SAAS;AAAA,QACzB,OAAO,IAAI,SAAS;AAAA,QACpB,WAAW,IAAI,SAAS;AAAA,MAC5B;AAEJ,YAAM,SAAS,MAAM,YAAY,aAAa,GAAG;AAGjD,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO;AAAA,QACP,SAAS;AAAA,UACL,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,MAAM;AAAA,QACV;AAAA,MACJ,CAAC;AAGD,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO,IAAI,SAAS;AAAA,QACpB,SAAS;AAAA,UACL,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,MAAM;AAAA,QACV;AAAA,MACJ,CAAC;AAAA,IACL,SACO,OACP;AACI,YAAM,MAAM;AACZ,iBAAW,YAAY,MAAM,MAAM,0BAA0B,GAAG;AAAA,IACpE;AAEA,UAAM,KAAK;AAAA,EACf;AACJ;;;ACnHJ,SAAS,aAAa,MACtB;AAEI,QAAM,cAAc;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,EACJ;AAEA,SAAO,CAAC,YAAY,KAAK,CAAC,YAAY,QAAQ,KAAK,IAAI,CAAC;AAC5D;AAWO,IAAM,yBACb;AAAA,EACI,aAAa;AAAA;AAAA,EACb,QAAQ,CAAC,OAAO,QAAQ,OAAO,SAAS,QAAQ;AAAA,EAEhD,SAAS,OAAO,KAAK,SACrB;AAEI,QAAI,CAAC,aAAa,IAAI,IAAI,GAC1B;AACI,iBAAW,YAAY,QAAQ,MAAM,+BAA+B,IAAI,IAAI,EAAE;AAC9E,YAAM,KAAK;AACX;AAAA,IACJ;AAGA,UAAM,cAAc,MAAM,KAAK,IAAI,QAAQ,KAAK,CAAC;AACjD,eAAW,YAAY,QAAQ,MAAM,sBAAsB;AAAA,MACvD;AAAA,MACA,YAAY,YAAY;AAAA,MACxB,YAAY,aAAa;AAAA,IAC7B,CAAC;AAED,UAAM,gBAAgB,IAAI,QAAQ,IAAI,aAAa,OAAO;AAE1D,eAAW,YAAY,QAAQ,MAAM,WAAW;AAAA,MAC5C,QAAQ,IAAI;AAAA,MACZ,MAAM,IAAI;AAAA,MACV,YAAY,CAAC,CAAC;AAAA,MACd,eAAe,eAAe,UAAU;AAAA,MACxC,eAAe,eAAe,MAAM,GAAG,EAAE,KAAK;AAAA,MAC9C,eAAe,eAAe,MAAM,GAAG,KAAK;AAAA,IAChD,CAAC;AAGD,QAAI,CAAC,eACL;AACI,iBAAW,YAAY,QAAQ,MAAM,4CAA4C;AAEjF,YAAM,KAAK;AACX;AAAA,IACJ;AAEA,QACA;AAEI,YAAM,UAAU,MAAM,cAAc,aAAa;AAEjD,iBAAW,YAAY,QAAQ,MAAM,iBAAiB;AAAA,QAClD,QAAQ,QAAQ;AAAA,QAChB,OAAO,QAAQ;AAAA,MACnB,CAAC;AAGD,YAAM,eAAe,MAAM,qBAAqB,eAAe,EAAE;AAEjE,UAAI,cACJ;AACI,mBAAW,YAAY,QAAQ,MAAM,8CAA8C;AAEnF,YAAI,SAAS,iBAAiB;AAC9B,YAAI,SAAS,cAAc;AAAA,MAC/B;AAGA,YAAM,QAAQ;AAAA,QACV;AAAA,UACI,QAAQ,QAAQ;AAAA,UAChB,OAAO,QAAQ;AAAA,UACf,WAAW,KAAK,IAAI;AAAA,QACxB;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR,EAAE,WAAW,MAAM;AAAA,MACvB;AAEA,iBAAW,YAAY,QAAQ,MAAM,sCAAsC;AAG3E,UAAI,QAAQ,eAAe,IAAI,UAAU,KAAK;AAC9C,UAAI,QAAQ,UAAU,IAAI,QAAQ;AAGlC,UAAI,SAAS,SAAS,QAAQ;AAC9B,UAAI,SAAS,eAAe;AAAA,IAChC,SACO,OACP;AACI,YAAM,MAAM;AACZ,YAAM,MAAM,IAAI,QAAQ,YAAY;AAGpC,UAAI,IAAI,SAAS,SAAS,KAAK,IAAI,SAAS,SAAS,GACrD;AACI,mBAAW,YAAY,QAAQ,KAAK,8BAA8B;AAAA,UAC9D,SAAS,IAAI;AAAA,UACb,cAAc,cAAc;AAAA,UAC5B,cAAc,cAAc,MAAM,GAAG,EAAE;AAAA,UACvC,cAAc,cAAc,MAAM,GAAG;AAAA,QACzC,CAAC;AACD,mBAAW,YAAY,QAAQ,MAAM,6BAA6B;AAGlE,YAAI,SAAS,eAAe;AAC5B,YAAI,SAAS,eAAe;AAAA,MAChC,OAEA;AACI,mBAAW,YAAY,QAAQ,MAAM,6BAA6B,GAAG;AAAA,MACzE;AAAA,IACJ;AAEA,UAAM,KAAK;AAAA,EACf;AAAA,EAEA,UAAU,OAAO,KAAK,SACtB;AAEI,QAAI,IAAI,SAAS,WAAW,OAAO,IAAI,SAAS,cAChD;AACI,iBAAW,YAAY,QAAQ,KAAK,wCAAwC;AAE5E,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO;AAAA,QACP,SAAS,EAAE,QAAQ,GAAG,MAAM,IAAI;AAAA,MACpC,CAAC;AAED,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO;AAAA,QACP,SAAS,EAAE,QAAQ,GAAG,MAAM,IAAI;AAAA,MACpC,CAAC;AAED,YAAM,KAAK;AACX;AAAA,IACJ;AAGA,QAAI,IAAI,SAAS,cACjB;AACI,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO;AAAA,QACP,SAAS;AAAA,UACL,QAAQ;AAAA,UACR,MAAM;AAAA,QACV;AAAA,MACJ,CAAC;AAED,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO;AAAA,QACP,SAAS;AAAA,UACL,QAAQ;AAAA,UACR,MAAM;AAAA,QACV;AAAA,MACJ,CAAC;AAAA,IACL,WAES,IAAI,SAAS,kBAAkB,IAAI,SAAS,WAAW,KAChE;AACI,UACA;AACI,cAAM,cAAc,IAAI,SAAS;AACjC,cAAM,MAAM,cAAc;AAG1B,cAAM,SAAS,MAAM,YAAY,aAAa,GAAG;AAGjD,YAAI,WAAW,KAAK;AAAA,UAChB,MAAM,aAAa;AAAA,UACnB,OAAO;AAAA,UACP,SAAS;AAAA,YACL,UAAU;AAAA,YACV,QAAQ;AAAA,YACR,UAAU;AAAA,YACV,QAAQ;AAAA,YACR,MAAM;AAAA,UACV;AAAA,QACJ,CAAC;AAGD,YAAI,WAAW,KAAK;AAAA,UAChB,MAAM,aAAa;AAAA,UACnB,OAAO,YAAY;AAAA,UACnB,SAAS;AAAA,YACL,UAAU;AAAA,YACV,QAAQ;AAAA,YACR,UAAU;AAAA,YACV,QAAQ;AAAA,YACR,MAAM;AAAA,UACV;AAAA,QACJ,CAAC;AAED,mBAAW,YAAY,QAAQ,KAAK,qBAAqB;AAAA,UACrD,QAAQ,YAAY;AAAA,UACpB,cAAc,OAAO;AAAA,UACrB,cAAc,OAAO,MAAM,GAAG,EAAE;AAAA,QACpC,CAAC;AAAA,MACL,SACO,OACP;AACI,cAAM,MAAM;AACZ,mBAAW,YAAY,QAAQ,MAAM,6BAA6B,GAAG;AAAA,MACzE;AAAA,IACJ,WAES,IAAI,SAAS,mBAAmB,IAAI,SAAS,IACtD;AACI,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO;AAAA,QACP,SAAS;AAAA,UACL,QAAQ;AAAA,UACR,MAAM;AAAA,QACV;AAAA,MACJ,CAAC;AAED,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO;AAAA,QACP,SAAS;AAAA,UACL,QAAQ;AAAA,UACR,MAAM;AAAA,QACV;AAAA,MACJ,CAAC;AAAA,IACL;AAEA,UAAM,KAAK;AAAA,EACf;AACJ;;;AC/PO,IAAM,yBACb;AAAA,EACI,aAAa;AAAA,EACb,QAAQ;AAAA,EAER,SAAS,OAAO,KAAK,SACrB;AACI,UAAM,gBAAgB,IAAI,QAAQ,IAAI,aAAa,OAAO;AAE1D,QAAI,CAAC,eACL;AACI,YAAM,KAAK;AACX;AAAA,IACJ;AAEA,QACA;AAEI,YAAM,iBAAiB,MAAM,cAAc,aAAa;AAGxD,YAAM,aAAa,gBAAgB,OAAO;AAG1C,UAAI,CAAC,IAAI,MACT;AACI,YAAI,OAAO,CAAC;AAAA,MAChB;AAEA,UAAI,KAAK,YAAY,WAAW;AAChC,UAAI,KAAK,QAAQ,WAAW;AAC5B,UAAI,KAAK,cAAc,WAAW;AAClC,UAAI,KAAK,YAAY,WAAW;AAChC,UAAI,KAAK,UAAU,OAAO,KAAK,WAAW,WAAW,QAAQ,EAAE;AAE/D,cAAQ,IAAI,sBAAsB,UAAU;AAC5C,cAAQ,IAAI,cAAc,WAAW,SAAS;AAC9C,cAAQ,IAAI,UAAU,WAAW,KAAK;AACtC,cAAQ,IAAI,gBAAgB,WAAW,WAAW;AAGlD,YAAM,QAAQ;AAAA,QACV;AAAA,UACI,QAAQ,eAAe;AAAA,UACvB,OAAO,eAAe;AAAA,UACtB,QAAQ;AAAA,UACR,WAAW,KAAK,IAAI;AAAA,QACxB;AAAA,QACA,eAAe;AAAA,QACf,eAAe;AAAA,QACf,EAAC,WAAW,MAAK;AAAA,MACrB;AAEA,UAAI,QAAQ,eAAe,IAAI,UAAU,KAAK;AAC9C,UAAI,QAAQ,UAAU,IAAI,eAAe;AAGzC,UAAI,SAAS,gBAAgB,WAAW;AACxC,UAAI,SAAS,WAAW,WAAW;AACnC,UAAI,SAAS,eAAe,WAAW;AACvC,UAAI,SAAS,SAAS,eAAe;AAAA,IACzC,SACO,OACP;AACI,YAAM,MAAM;AACZ,iBAAW,YAAY,YAAY,MAAM,kCAAkC,GAAG;AAAA,IAClF;AAEA,UAAM,KAAK;AAAA,EACf;AAAA,EAEA,UAAU,OAAO,KAAK,SACtB;AAEI,QAAI,IAAI,SAAS,WAAW,KAC5B;AACI,YAAM,KAAK;AACX;AAAA,IACJ;AAEA,QAAI,CAAC,IAAI,SAAS,iBAAiB,CAAC,IAAI,SAAS,QACjD;AACI,iBAAW,YAAY,YAAY,MAAM,+BAA+B;AACxE,YAAM,KAAK;AACX;AAAA,IACJ;AAEA,QACA;AAEI,YAAM,MAAM,cAAc;AAG1B,YAAM,iBACN;AAAA,QACI,QAAQ,IAAI,SAAS;AAAA,QACrB,YAAY,IAAI,SAAS;AAAA,QACzB,OAAO,IAAI,SAAS;AAAA,QACpB,WAAW,IAAI,SAAS;AAAA,MAC5B;AAEA,YAAM,SAAS,MAAM,YAAY,gBAAgB,GAAG;AAGpD,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO;AAAA,QACP,SAAS;AAAA,UACL,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,MAAM;AAAA,QACV;AAAA,MACJ,CAAC;AAGD,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO,IAAI,SAAS;AAAA,QACpB,SAAS;AAAA,UACL,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,MAAM;AAAA,QACV;AAAA,MACJ,CAAC;AAAA,IACL,SACO,OACP;AACI,YAAM,MAAM;AACZ,iBAAW,YAAY,YAAY,MAAM,2CAA2C,GAAG;AAAA,IAC3F;AAEA,UAAM,KAAK;AAAA,EACf;AACJ;;;ACjJA,YAAYC,WAAU;AACtB,SAAS,OAAAC,YAAW;AAkBpB,eAAe,cACf;AACI,QAAM,SAASA,KAAI;AACnB,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,eAAe,MAAM,EAAE;AACnD,QAAM,aAAa,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AAC7D,SAAO,IAAI,WAAW,UAAU;AACpC;AAKA,SAAS,gBACT;AACI,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAC5B,SAAO,MAAM,KAAK,OAAO,OAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC1E;AAmBA,eAAsB,iBAAiB,QACvC;AACI,QAAM,MAAM,MAAM,YAAY;AAE9B,QAAM,QAAoB;AAAA,IACtB,WAAW,OAAO;AAAA,IAClB,OAAO,cAAc;AAAA,IACrB,UAAU,OAAO;AAAA,IACjB,WAAW,OAAO;AAAA,IAClB,OAAO,OAAO;AAAA,IACd,aAAa,OAAO;AAAA,IACpB,WAAW,OAAO;AAAA,IAClB,UAAU,OAAO;AAAA,EACrB;AAEA,QAAM,MAAM,MAAM,IAAS,iBAAW,EAAE,MAAM,CAAC,EAC1C,mBAAmB,EAAE,KAAK,OAAO,KAAK,UAAU,CAAC,EACjD,YAAY,EACZ,kBAAkB,KAAK,EACvB,QAAQ,GAAG;AAGhB,SAAO,mBAAmB,GAAG;AACjC;;;ACnFA,YAAYC,WAAU;AACtB,SAAS,eAAe;AAIxB,SAAS,OAAAC,YAAW;AACpB,SAAS,cAAc;AA8JvB,eAAe,uBACf;AACI,QAAM,SAASC,KAAI;AACnB,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,iBAAiB,MAAM,EAAE;AACrD,QAAM,aAAa,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AAC7D,SAAO,IAAI,WAAW,UAAU;AACpC;AAQA,eAAsB,mBAClB,MACA,MAAc,KAElB;AACI,QAAM,MAAM,MAAM,qBAAqB;AAEvC,SAAO,MAAM,IAAS,iBAAW,EAAE,KAAK,CAAC,EACpC,mBAAmB,EAAE,KAAK,OAAO,KAAK,UAAU,CAAC,EACjD,YAAY,EACZ,kBAAkB,GAAG,GAAG,GAAG,EAC3B,UAAU,WAAW,EACrB,YAAY,YAAY,EACxB,QAAQ,GAAG;AACpB;AAOA,eAAsB,qBAAqBC,MAC3C;AACI,QAAM,MAAM,MAAM,qBAAqB;AAEvC,QAAM,EAAE,QAAQ,IAAI,MAAW,iBAAWA,MAAK,KAAK;AAAA,IAChD,QAAQ;AAAA,IACR,UAAU;AAAA,EACd,CAAC;AAED,SAAO,QAAQ;AACnB;;;AClMO,IAAM,sBAAuC;AAAA,EAChD,aAAa;AAAA,EACb,QAAQ;AAAA,EAER,SAAS,OAAO,KAAK,SACrB;AACI,UAAM,WAAW,IAAI,KAAK,MAAM,GAAG,EAAE,CAAC;AACtC,UAAM,YAAY,IAAI,MAAM,aAAa;AAGzC,UAAM,UAAU,gBAAgB,OAAO;AAGvC,UAAM,QAAQ,MAAM,iBAAiB;AAAA,MACjC;AAAA,MACA;AAAA,MACA,WAAW,QAAQ;AAAA,MACnB,OAAO,QAAQ;AAAA,MACf,aAAa,QAAQ;AAAA,MACrB,WAAW,QAAQ;AAAA,IACvB,CAAC;AAGD,QAAI,CAAC,IAAI,MACT;AACI,UAAI,OAAO,CAAC;AAAA,IAChB;AACA,QAAI,KAAK,QAAQ;AAGjB,QAAI,SAAS,iBAAiB;AAAA,MAC1B,YAAY,QAAQ;AAAA,MACpB,OAAO,QAAQ;AAAA,MACf,WAAW,QAAQ;AAAA,IACvB;AAEA,eAAW,YAAY,OAAO,QAAQ,uBAAuB;AAAA,MACzD;AAAA,MACA,OAAO,QAAQ;AAAA,IACnB,CAAC;AAED,UAAM,KAAK;AAAA,EACf;AAAA,EAEA,UAAU,OAAO,KAAK,SACtB;AAEI,QAAI,IAAI,SAAS,MAAM,IAAI,SAAS,gBACpC;AACI,UACA;AACI,cAAM,SAAS,MAAM,mBAAmB,IAAI,SAAS,cAAc;AAEnE,YAAI,WAAW,KAAK;AAAA,UAChB,MAAM,aAAa;AAAA,UACnB,OAAO;AAAA,UACP,SAAS;AAAA,YACL,UAAU;AAAA,YACV,QAAQ;AAAA,YACR,UAAU;AAAA;AAAA,YACV,QAAQ;AAAA;AAAA,YACR,MAAM;AAAA,UACV;AAAA,QACJ,CAAC;AAED,mBAAW,YAAY,OAAO,QAAQ,8BAA8B;AAAA,UAChE,OAAO,IAAI,SAAS,eAAe;AAAA,QACvC,CAAC;AAAA,MACL,SACO,OACP;AACI,cAAM,MAAM;AACZ,mBAAW,YAAY,OAAO,QAAQ,iCAAiC,GAAG;AAAA,MAC9E;AAAA,IACJ;AAEA,UAAM,KAAK;AAAA,EACf;AACJ;AAKA,SAAS,iBAAiB,KAAiC,SAC3D;AACI,MAAI,SAAS,KAAK;AAClB,MAAI,SAAS,SAAS;AACtB,MAAI,SAAS,aAAa;AAC1B,MAAI,SAAS,OAAO,EAAE,SAAS,OAAO,QAAQ;AAE9C,MAAI,WAAW,KAAK;AAAA,IAChB,MAAM,aAAa;AAAA,IACnB,OAAO;AAAA,IACP,SAAS;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,MAAM;AAAA,IACV;AAAA,EACJ,CAAC;AACL;AAQO,IAAM,2BAA4C;AAAA,EACrD,aAAa;AAAA,EACb,QAAQ;AAAA,EAER,UAAU,OAAO,KAAK,SACtB;AAEI,QAAI,CAAC,IAAI,SAAS,IAClB;AACI,YAAM,KAAK;AACX;AAAA,IACJ;AAEA,UAAM,gBAAgB,IAAI,QAAQ,IAAI,aAAa,aAAa;AAChE,QAAI,CAAC,eACL;AACI,iBAAW,YAAY,OAAO,OAAO,iCAAiC;AACtE,uBAAiB,KAAK,0CAA0C;AAChE,YAAM,KAAK;AACX;AAAA,IACJ;AAEA,QACA;AAEI,YAAM,iBAAiB,MAAM,qBAAqB,aAAa;AAG/D,YAAM,EAAE,QAAQ,MAAM,IAAI,IAAI,SAAS,QAAQ,CAAC;AAEhD,UAAI,CAAC,UAAU,CAAC,OAChB;AACI,mBAAW,YAAY,OAAO,QAAQ,qCAAqC;AAC3E,yBAAiB,KAAK,4CAA4C;AAClE,cAAM,KAAK;AACX;AAAA,MACJ;AAGA,UAAI,eAAe,UAAU,OAC7B;AACI,mBAAW,YAAY,OAAO,QAAQ,kBAAkB;AAAA,UACpD,UAAU,eAAe;AAAA,UACzB,UAAU;AAAA,QACd,CAAC;AACD,yBAAiB,KAAK,2CAA2C;AACjE,cAAM,KAAK;AACX;AAAA,MACJ;AAGA,YAAM,MAAM,cAAc;AAC1B,YAAM,eAAe,MAAM,YAAY;AAAA,QACnC;AAAA,QACA,YAAY,eAAe;AAAA,QAC3B,OAAO,eAAe;AAAA,QACtB,WAAW,eAAe;AAAA,MAC9B,GAAG,GAAG;AAGN,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO;AAAA,QACP,SAAS;AAAA,UACL,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,MAAM;AAAA,QACV;AAAA,MACJ,CAAC;AAGD,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO;AAAA,QACP,SAAS;AAAA,UACL,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,MAAM;AAAA,QACV;AAAA,MACJ,CAAC;AAGD,UAAI,WAAW,KAAK;AAAA,QAChB,MAAM,aAAa;AAAA,QACnB,OAAO;AAAA,QACP,SAAS;AAAA,UACL,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,MAAM;AAAA,QACV;AAAA,MACJ,CAAC;AAED,iBAAW,YAAY,OAAO,QAAQ,2BAA2B;AAAA,QAC7D;AAAA,QACA;AAAA,MACJ,CAAC;AAAA,IACL,SACO,OACP;AACI,YAAM,MAAM;AACZ,iBAAW,YAAY,OAAO,QAAQ,oCAAoC,GAAG;AAC7E,uBAAiB,KAAK,IAAI,OAAO;AAAA,IACrC;AAEA,UAAM,KAAK;AAAA,EACf;AACJ;;;ACxNO,IAAM,mBAAmB;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;;;AZTA,qBAAqB,QAAQ,gBAAgB;","names":["crypto","jwt","env","env","jose","env","jose","env","env","jwt"]}
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { SessionData, KeyAlgorithmType } from '@spfn/auth/server';
4
- export { SessionData } from '@spfn/auth/server';
3
+ import { S as SessionData } from '../session-Dbvz9Sdp.js';
4
+ import { K as KeyAlgorithmType } from '../types-B1CzVZkU.js';
5
5
  import { NextRequest, NextResponse } from 'next/server';
6
6
 
7
7
  interface RequireAuthProps {
@@ -153,7 +153,7 @@ declare function getAuthSessionData(): Promise<{
153
153
  id: number;
154
154
  name: string;
155
155
  displayName: string;
156
- category: "auth" | "user" | "rbac" | "system" | "custom" | undefined;
156
+ category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
157
157
  }[];
158
158
  userId: number;
159
159
  publicId: string;
@@ -314,4 +314,4 @@ interface OAuthCallbackOptions {
314
314
  */
315
315
  declare function createOAuthCallbackHandler(options?: OAuthCallbackOptions): (request: NextRequest) => Promise<NextResponse>;
316
316
 
317
- export { type OAuthCallbackOptions, type PendingSessionData, type PublicSession, RequireAuth, type RequireAuthProps, RequirePermission, type RequirePermissionProps, RequireRole, type RequireRoleProps, type SaveSessionOptions, clearPendingSession, clearSession, createOAuthCallbackHandler, getAuthSessionData, getPendingSession, getSession, getUserPermissions, getUserRole, hasAnyPermission, hasAnyRole, saveSession, sealPendingSession, unsealPendingSession };
317
+ export { type OAuthCallbackOptions, type PendingSessionData, type PublicSession, RequireAuth, type RequireAuthProps, RequirePermission, type RequirePermissionProps, RequireRole, type RequireRoleProps, type SaveSessionOptions, SessionData, clearPendingSession, clearSession, createOAuthCallbackHandler, getAuthSessionData, getPendingSession, getSession, getUserPermissions, getUserRole, hasAnyPermission, hasAnyRole, saveSession, sealPendingSession, unsealPendingSession };
@@ -5,16 +5,153 @@ import "server-only";
5
5
  import { redirect } from "next/navigation";
6
6
 
7
7
  // src/nextjs/session-helpers.ts
8
- import * as jose from "jose";
8
+ import * as jose2 from "jose";
9
9
  import { cookies } from "next/headers.js";
10
- import {
11
- sealSession,
12
- unsealSession,
13
- COOKIE_NAMES,
14
- getSessionTtl,
15
- parseDuration
16
- } from "@spfn/auth/server";
10
+
11
+ // src/server/lib/session.ts
12
+ import * as jose from "jose";
17
13
  import { env } from "@spfn/auth/config";
14
+ import { env as coreEnv } from "@spfn/core/config";
15
+
16
+ // src/server/logger.ts
17
+ import { logger as rootLogger } from "@spfn/core/logger";
18
+ var authLogger = {
19
+ plugin: rootLogger.child("@spfn/auth:plugin"),
20
+ middleware: rootLogger.child("@spfn/auth:middleware"),
21
+ interceptor: {
22
+ general: rootLogger.child("@spfn/auth:interceptor:general"),
23
+ login: rootLogger.child("@spfn/auth:interceptor:login"),
24
+ keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
25
+ oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
26
+ },
27
+ session: rootLogger.child("@spfn/auth:session"),
28
+ service: rootLogger.child("@spfn/auth:service"),
29
+ setup: rootLogger.child("@spfn/auth:setup"),
30
+ email: rootLogger.child("@spfn/auth:email"),
31
+ sms: rootLogger.child("@spfn/auth:sms")
32
+ };
33
+
34
+ // src/server/lib/session.ts
35
+ async function getSessionSecretKey() {
36
+ const secret = env.SPFN_AUTH_SESSION_SECRET;
37
+ const encoder = new TextEncoder();
38
+ const data = encoder.encode(secret);
39
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
40
+ return new Uint8Array(hashBuffer);
41
+ }
42
+ async function getSecretFingerprint() {
43
+ const key = await getSessionSecretKey();
44
+ const hash = await crypto.subtle.digest("SHA-256", key.buffer);
45
+ const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
46
+ return hex.slice(0, 8);
47
+ }
48
+ async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
49
+ const secret = await getSessionSecretKey();
50
+ const result = await new jose.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
51
+ if (coreEnv.NODE_ENV !== "production") {
52
+ const fingerprint = await getSecretFingerprint();
53
+ authLogger.session.debug(`Sealed session`, {
54
+ secretFingerprint: fingerprint,
55
+ resultLength: result.length,
56
+ resultPrefix: result.slice(0, 20)
57
+ });
58
+ }
59
+ return result;
60
+ }
61
+ async function unsealSession(jwt) {
62
+ try {
63
+ const secret = await getSessionSecretKey();
64
+ const { payload } = await jose.jwtDecrypt(jwt, secret, {
65
+ issuer: "spfn-auth",
66
+ audience: "spfn-client"
67
+ });
68
+ return payload.data;
69
+ } catch (err) {
70
+ if (err instanceof jose.errors.JWTExpired) {
71
+ throw new Error("Session expired");
72
+ }
73
+ if (err instanceof jose.errors.JWEDecryptionFailed) {
74
+ if (coreEnv.NODE_ENV !== "production") {
75
+ const fingerprint = await getSecretFingerprint();
76
+ authLogger.session.warn(`JWE decryption failed`, {
77
+ secretFingerprint: fingerprint,
78
+ jwtLength: jwt.length,
79
+ jwtPrefix: jwt.slice(0, 20),
80
+ jwtSuffix: jwt.slice(-10)
81
+ });
82
+ }
83
+ throw new Error("Invalid session");
84
+ }
85
+ if (err instanceof jose.errors.JWTClaimValidationFailed) {
86
+ throw new Error("Session validation failed");
87
+ }
88
+ throw new Error("Failed to unseal session");
89
+ }
90
+ }
91
+
92
+ // src/server/lib/config.ts
93
+ import { env as env2 } from "@spfn/auth/config";
94
+ function getCookieSuffix() {
95
+ const port = process.env.PORT;
96
+ return port ? `_${port}` : "";
97
+ }
98
+ var COOKIE_NAMES = {
99
+ /** Encrypted session data (userId, privateKey, keyId, algorithm) */
100
+ get SESSION() {
101
+ return `spfn_session${getCookieSuffix()}`;
102
+ },
103
+ /** Current key ID (for key rotation) */
104
+ get SESSION_KEY_ID() {
105
+ return `spfn_session_key_id${getCookieSuffix()}`;
106
+ },
107
+ /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
108
+ get OAUTH_PENDING() {
109
+ return `spfn_oauth_pending${getCookieSuffix()}`;
110
+ }
111
+ };
112
+ function parseDuration(duration) {
113
+ if (typeof duration === "number") {
114
+ return duration;
115
+ }
116
+ const match = duration.match(/^(\d+)([dhms]?)$/);
117
+ if (!match) {
118
+ throw new Error(`Invalid duration format: ${duration}. Use format like '30d', '12h', '45m', '3600s', or plain number.`);
119
+ }
120
+ const value = parseInt(match[1], 10);
121
+ const unit = match[2] || "s";
122
+ switch (unit) {
123
+ case "d":
124
+ return value * 24 * 60 * 60;
125
+ case "h":
126
+ return value * 60 * 60;
127
+ case "m":
128
+ return value * 60;
129
+ case "s":
130
+ return value;
131
+ default:
132
+ throw new Error(`Unknown duration unit: ${unit}`);
133
+ }
134
+ }
135
+ var globalConfig = {
136
+ sessionTtl: "7d"
137
+ // Default: 7 days
138
+ };
139
+ function getSessionTtl(override) {
140
+ if (override !== void 0) {
141
+ return parseDuration(override);
142
+ }
143
+ if (globalConfig.sessionTtl !== void 0) {
144
+ return parseDuration(globalConfig.sessionTtl);
145
+ }
146
+ const envTtl = env2.SPFN_AUTH_SESSION_TTL;
147
+ if (envTtl) {
148
+ return parseDuration(envTtl);
149
+ }
150
+ return 7 * 24 * 60 * 60;
151
+ }
152
+
153
+ // src/nextjs/session-helpers.ts
154
+ import { env as env3 } from "@spfn/auth/config";
18
155
  import { logger } from "@spfn/core/logger";
19
156
  async function saveSession(data, options) {
20
157
  let maxAge;
@@ -58,7 +195,7 @@ async function clearSession() {
58
195
  cookieStore.delete(COOKIE_NAMES.SESSION_KEY_ID);
59
196
  }
60
197
  async function getPendingSessionKey() {
61
- const secret = env.SPFN_AUTH_SESSION_SECRET;
198
+ const secret = env3.SPFN_AUTH_SESSION_SECRET;
62
199
  const encoder = new TextEncoder();
63
200
  const data = encoder.encode(`oauth-pending:${secret}`);
64
201
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
@@ -66,11 +203,11 @@ async function getPendingSessionKey() {
66
203
  }
67
204
  async function sealPendingSession(data, ttl = 600) {
68
205
  const key = await getPendingSessionKey();
69
- return await new jose.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-oauth").encrypt(key);
206
+ return await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-oauth").encrypt(key);
70
207
  }
71
208
  async function unsealPendingSession(jwt) {
72
209
  const key = await getPendingSessionKey();
73
- const { payload } = await jose.jwtDecrypt(jwt, key, {
210
+ const { payload } = await jose2.jwtDecrypt(jwt, key, {
74
211
  issuer: "spfn-auth",
75
212
  audience: "spfn-oauth"
76
213
  });
@@ -98,7 +235,6 @@ async function clearPendingSession() {
98
235
 
99
236
  // src/nextjs/guards/auth-utils.ts
100
237
  import { authApi } from "@spfn/auth";
101
- import { authLogger } from "@spfn/auth/server";
102
238
  async function getAuthSessionData() {
103
239
  try {
104
240
  const session = await authApi.getAuthSession.call();
@@ -214,8 +350,7 @@ async function RequirePermission({
214
350
  // src/nextjs/oauth-handlers.ts
215
351
  import { NextResponse } from "next/server";
216
352
  import { cookies as cookies2 } from "next/headers.js";
217
- import { sealSession as sealSession2, COOKIE_NAMES as COOKIE_NAMES2, getSessionTtl as getSessionTtl2 } from "@spfn/auth/server";
218
- import { env as env2 } from "@spfn/core/config";
353
+ import { env as env4 } from "@spfn/core/config";
219
354
  import { logger as logger2 } from "@spfn/core/logger";
220
355
  function createOAuthCallbackHandler(options) {
221
356
  const defaultRedirect = options?.defaultRedirectUrl || "/";
@@ -239,7 +374,7 @@ function createOAuthCallbackHandler(options) {
239
374
  }
240
375
  try {
241
376
  const cookieStore = await cookies2();
242
- const pendingCookie = cookieStore.get(COOKIE_NAMES2.OAUTH_PENDING);
377
+ const pendingCookie = cookieStore.get(COOKIE_NAMES.OAUTH_PENDING);
243
378
  if (!pendingCookie) {
244
379
  throw new Error("OAuth session expired. Please try again.");
245
380
  }
@@ -247,8 +382,8 @@ function createOAuthCallbackHandler(options) {
247
382
  if (pendingSession.keyId !== keyId) {
248
383
  throw new Error("Session mismatch. Please try again.");
249
384
  }
250
- const ttl = getSessionTtl2();
251
- const sessionToken = await sealSession2({
385
+ const ttl = getSessionTtl();
386
+ const sessionToken = await sealSession({
252
387
  userId,
253
388
  privateKey: pendingSession.privateKey,
254
389
  keyId: pendingSession.keyId,
@@ -256,21 +391,21 @@ function createOAuthCallbackHandler(options) {
256
391
  }, ttl);
257
392
  const redirectUrl = new URL(returnUrl, request.url);
258
393
  const response = NextResponse.redirect(redirectUrl);
259
- response.cookies.set(COOKIE_NAMES2.SESSION, sessionToken, {
394
+ response.cookies.set(COOKIE_NAMES.SESSION, sessionToken, {
260
395
  httpOnly: true,
261
- secure: env2.NODE_ENV === "production",
396
+ secure: env4.NODE_ENV === "production",
262
397
  sameSite: "strict",
263
398
  maxAge: ttl,
264
399
  path: "/"
265
400
  });
266
- response.cookies.set(COOKIE_NAMES2.SESSION_KEY_ID, keyId, {
401
+ response.cookies.set(COOKIE_NAMES.SESSION_KEY_ID, keyId, {
267
402
  httpOnly: true,
268
- secure: env2.NODE_ENV === "production",
403
+ secure: env4.NODE_ENV === "production",
269
404
  sameSite: "strict",
270
405
  maxAge: ttl,
271
406
  path: "/"
272
407
  });
273
- response.cookies.delete(COOKIE_NAMES2.OAUTH_PENDING);
408
+ response.cookies.delete(COOKIE_NAMES.OAUTH_PENDING);
274
409
  logger2.debug("OAuth callback completed", { userId, keyId });
275
410
  return response;
276
411
  } catch (error2) {