@spfn/auth 0.2.0-beta.71 → 0.2.0-beta.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{authenticate-NhEb_j27.d.ts → authenticate-DXVtYh8X.d.ts} +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/nextjs/api.js +25 -2
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.js +4 -0
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +16 -3
- package/dist/server.js +222 -7
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/nextjs/server.ts","../../src/nextjs/guards/require-auth.tsx","../../src/nextjs/session-helpers.ts","../../src/server/lib/session.ts","../../src/server/logger.ts","../../src/server/lib/config.ts","../../src/nextjs/guards/auth-utils.ts","../../src/nextjs/guards/require-role.tsx","../../src/nextjs/guards/require-permission.tsx","../../src/nextjs/oauth-handlers.ts"],"sourcesContent":["import 'server-only';\n\nexport { RequireAuth } from './guards/require-auth';\nexport type { RequireAuthProps } from './guards/require-auth';\n\nexport { RequireRole } from './guards/require-role';\nexport type { RequireRoleProps } from './guards/require-role';\n\nexport { RequirePermission } from './guards/require-permission';\nexport type { RequirePermissionProps } from './guards/require-permission';\n\nexport { getAuthSessionData, getUserRole, getUserPermissions, hasAnyRole, hasAnyPermission } from './guards/auth-utils';\n\n// Session helpers\nexport {\n saveSession,\n getSession,\n clearSession,\n // Pending session (OAuth)\n sealPendingSession,\n unsealPendingSession,\n getPendingSession,\n clearPendingSession,\n type SessionData,\n type PublicSession,\n type SaveSessionOptions,\n type PendingSessionData,\n} from './session-helpers';\n\n// OAuth handlers\nexport {\n createOAuthCallbackHandler,\n type OAuthCallbackOptions,\n} from './oauth-handlers';\n","/**\n * RequireAuth Guard Component\n *\n * Requires user to be authenticated\n */\n\nimport { redirect } from 'next/navigation';\nimport { getSession } from '../session-helpers';\nimport { getAuthSessionData } from './auth-utils';\nimport type { ReactNode } from 'react';\n\nexport interface RequireAuthProps\n{\n /**\n * Children to render if authenticated\n */\n children: ReactNode;\n\n /**\n * Path to redirect to if not authenticated\n * @default '/login'\n */\n redirectTo?: string;\n\n /**\n * Fallback UI to show instead of redirecting\n */\n fallback?: ReactNode;\n}\n\n/**\n * Require Authentication Guard\n *\n * Ensures user is logged in before rendering children\n *\n * @example\n * ```tsx\n * <RequireAuth redirectTo=\"/login\">\n * <DashboardContent />\n * </RequireAuth>\n * ```\n *\n * @example With fallback\n * ```tsx\n * <RequireAuth fallback={<LoginPrompt />}>\n * <PrivateContent />\n * </RequireAuth>\n * ```\n */\nexport async function RequireAuth({\n children,\n redirectTo = '/auth/login',\n fallback,\n}: RequireAuthProps)\n{\n const session = await getSession();\n\n if (!session)\n {\n if (fallback)\n {\n return <>{fallback}</>;\n }\n\n redirect(redirectTo);\n }\n\n // Validate server-side session (key expiry, user status, etc.)\n const serverSession = await getAuthSessionData();\n\n if (!serverSession)\n {\n // Note: clearSession() cannot be called in Server Components (Next.js 16+)\n // The RPC proxy interceptor handles session cleanup on 401 responses\n redirect(redirectTo);\n }\n\n return <>{children}</>;\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 // Never log the cookie value — it's the sealed session token.\n logger.debug('Validating session cookie', { present: true });\n const session = await unsealSession(sessionCookie.value);\n\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\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\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 * @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\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\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","/**\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","/**\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\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() \n {\n return `spfn_session${getCookieSuffix()}`; \n },\n /** Current key ID (for key rotation) */\n get SESSION_KEY_ID() \n {\n return `spfn_session_key_id${getCookieSuffix()}`; \n },\n /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */\n get OAUTH_PENDING() \n {\n return `spfn_oauth_pending${getCookieSuffix()}`; \n },\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","/**\n * Server-side auth utilities for guards\n *\n * Uses authApi to check permissions in real-time\n */\n\nimport { authApi } from '@spfn/auth';\nimport { authLogger } from '../../server/logger';\n\n/**\n * Get current auth session with roles and permissions via API\n */\nexport async function getAuthSessionData()\n{\n try\n {\n const session = await authApi.getAuthSession.call();\n authLogger.middleware.debug('Auth session retrieved', { name: session.role?.name });\n\n return session;\n }\n catch (error)\n {\n authLogger.middleware.error('Failed to get auth session', { error });\n\n return null;\n }\n}\n\n/**\n * Get user role\n */\nexport async function getUserRole(): Promise<string | null>\n{\n const session = await getAuthSessionData();\n\n return session?.role?.name || null;\n}\n\n/**\n * Get user permissions\n */\nexport async function getUserPermissions(): Promise<string[]>\n{\n const session = await getAuthSessionData();\n\n if (!session)\n {\n return [];\n }\n\n return session.permissions?.map((p: any) => p.name) || [];\n}\n\n/**\n * Check if user has any of the specified roles\n */\nexport async function hasAnyRole(requiredRoles: string[]): Promise<boolean>\n{\n const session = await getAuthSessionData();\n if (!session)\n {\n return false;\n }\n\n return requiredRoles.includes(session.role?.name);\n}\n\n/**\n * Check if user has any of the specified permissions\n */\nexport async function hasAnyPermission(requiredPermissions: string[]): Promise<boolean>\n{\n const session = await getAuthSessionData();\n\n if (!session)\n {\n return false;\n }\n\n const userPermissionNames = session.permissions?.map((p: any) => p.name) || [];\n\n return requiredPermissions.some(permission => userPermissionNames.includes(permission));\n}\n","/**\n * RequireRole Guard Component\n *\n * Requires user to have at least one of the specified roles\n */\n\nimport { redirect } from 'next/navigation';\nimport { getSession } from '../session-helpers';\nimport { hasAnyRole } from './auth-utils';\nimport type { ReactNode } from 'react';\n\nexport interface RequireRoleProps\n{\n /**\n * Required role(s) - user must have at least one\n */\n roles: string | string[];\n\n /**\n * Children to render if user has required role\n */\n children: ReactNode;\n\n /**\n * Path to redirect to if user doesn't have role\n * @default '/unauthorized'\n */\n redirectTo?: string;\n\n /**\n * Fallback UI to show instead of redirecting\n */\n fallback?: ReactNode;\n}\n\n/**\n * Require Role Guard\n *\n * Ensures user has at least one of the specified roles\n *\n * @example Single role\n * ```tsx\n * <RequireRole roles=\"admin\">\n * <AdminPanel />\n * </RequireRole>\n * ```\n *\n * @example Multiple roles (OR condition)\n * ```tsx\n * <RequireRole roles={['admin', 'manager']}>\n * <ManagementDashboard />\n * </RequireRole>\n * ```\n *\n * @example With fallback\n * ```tsx\n * <RequireRole roles=\"admin\" fallback={<AccessDenied />}>\n * <AdminContent />\n * </RequireRole>\n * ```\n */\nexport async function RequireRole({\n roles,\n children,\n redirectTo = '/unauthorized',\n fallback,\n}: RequireRoleProps)\n{\n const session = await getSession();\n\n // Not authenticated\n if (!session)\n {\n if (fallback)\n {\n return <>{fallback}</>;\n }\n\n redirect('/login');\n }\n\n // Normalize to array\n const requiredRoles = Array.isArray(roles) ? roles : [roles];\n\n // Check if user has any of the required roles\n const hasRole = await hasAnyRole(requiredRoles);\n\n if (!hasRole)\n {\n if (fallback)\n {\n return <>{fallback}</>;\n }\n\n redirect(redirectTo);\n }\n\n return <>{children}</>;\n}\n","/**\n * RequirePermission Guard Component\n *\n * Requires user to have at least one of the specified permissions\n */\n\nimport { redirect } from 'next/navigation';\nimport { getSession } from '../session-helpers';\nimport { hasAnyPermission } from './auth-utils';\nimport type { ReactNode } from 'react';\n\nexport interface RequirePermissionProps\n{\n /**\n * Required permission(s) - user must have at least one\n */\n permissions: string | string[];\n\n /**\n * Children to render if user has required permission\n */\n children: ReactNode;\n\n /**\n * Path to redirect to if user doesn't have permission\n * @default '/unauthorized'\n */\n redirectTo?: string;\n\n /**\n * Fallback UI to show instead of redirecting\n */\n fallback?: ReactNode;\n}\n\n/**\n * Require Permission Guard\n *\n * Ensures user has at least one of the specified permissions\n *\n * @example Single permission\n * ```tsx\n * <RequirePermission permissions=\"user:delete\">\n * <DeleteUserButton />\n * </RequirePermission>\n * ```\n *\n * @example Multiple permissions (OR condition)\n * ```tsx\n * <RequirePermission permissions={['user:delete', 'user:update']}>\n * <UserManagement />\n * </RequirePermission>\n * ```\n *\n * @example With fallback\n * ```tsx\n * <RequirePermission permissions=\"project:create\" fallback={<UpgradePrompt />}>\n * <CreateProject />\n * </RequirePermission>\n * ```\n */\nexport async function RequirePermission({\n permissions,\n children,\n redirectTo = '/unauthorized',\n fallback,\n}: RequirePermissionProps)\n{\n const session = await getSession();\n\n // Not authenticated\n if (!session)\n {\n if (fallback)\n {\n return <>{fallback}</>;\n }\n\n redirect('/login');\n }\n\n // Normalize to array\n const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions];\n\n // Check if user has any of the required permissions\n const hasPermission = await hasAnyPermission(requiredPermissions);\n\n if (!hasPermission)\n {\n if (fallback)\n {\n return <>{fallback}</>;\n }\n\n redirect(redirectTo);\n }\n\n return <>{children}</>;\n}\n","/**\n * OAuth Handlers for Next.js\n *\n * Helper functions to create OAuth callback route handlers\n */\n\nimport { NextRequest, NextResponse } from 'next/server';\nimport { cookies } from 'next/headers.js';\nimport { sealSession } from '../server/lib/session';\nimport { COOKIE_NAMES, getSessionTtl } from '../server/lib/config';\nimport { env } from '@spfn/core/config';\nimport { logger } from '@spfn/core/logger';\nimport { unsealPendingSession } from './session-helpers';\n\nexport interface OAuthCallbackOptions\n{\n /**\n * Default redirect URL if returnUrl is not provided\n * @default '/'\n */\n defaultRedirectUrl?: string;\n\n /**\n * Error redirect URL\n * @default '/auth/error'\n */\n errorRedirectUrl?: string;\n}\n\n/**\n * Create OAuth callback handler for Next.js API Route\n *\n * Handles the final step of OAuth flow:\n * 1. Gets userId, keyId from query params (set by backend)\n * 2. Gets privateKey from pending session cookie\n * 3. Creates full session and saves to cookie\n * 4. Redirects to returnUrl\n *\n * @example\n * ```typescript\n * // /api/auth/callback/route.ts\n * import { createOAuthCallbackHandler } from '@spfn/auth/nextjs/server';\n * export const GET = createOAuthCallbackHandler();\n * ```\n */\nexport function createOAuthCallbackHandler(options?: OAuthCallbackOptions)\n{\n const defaultRedirect = options?.defaultRedirectUrl || '/';\n const errorRedirect = options?.errorRedirectUrl || '/auth/error';\n\n return async (request: NextRequest): Promise<NextResponse> =>\n {\n const searchParams = request.nextUrl.searchParams;\n const userId = searchParams.get('userId');\n const keyId = searchParams.get('keyId');\n const returnUrl = searchParams.get('returnUrl') || defaultRedirect;\n const error = searchParams.get('error');\n\n // Handle error from backend\n if (error)\n {\n const errorUrl = new URL(errorRedirect, request.url);\n errorUrl.searchParams.set('error', error);\n\n return NextResponse.redirect(errorUrl);\n }\n\n // Validate required params\n if (!userId || !keyId)\n {\n logger.error('OAuth callback missing required params', { userId: !!userId, keyId: !!keyId });\n const errorUrl = new URL(errorRedirect, request.url);\n errorUrl.searchParams.set('error', 'Missing required parameters');\n\n return NextResponse.redirect(errorUrl);\n }\n\n try\n {\n // Get pending session from cookie\n const cookieStore = await cookies();\n const pendingCookie = cookieStore.get(COOKIE_NAMES.OAUTH_PENDING);\n\n if (!pendingCookie)\n {\n throw new Error('OAuth session expired. Please try again.');\n }\n\n const pendingSession = await unsealPendingSession(pendingCookie.value);\n\n // Verify keyId matches\n if (pendingSession.keyId !== keyId)\n {\n throw new Error('Session mismatch. Please try again.');\n }\n\n // Create full session\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 // Build redirect response\n const redirectUrl = new URL(returnUrl, request.url);\n const response = NextResponse.redirect(redirectUrl);\n\n // Set session cookie\n response.cookies.set(COOKIE_NAMES.SESSION, sessionToken, {\n httpOnly: true,\n secure: env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n });\n\n // Set keyId cookie\n response.cookies.set(COOKIE_NAMES.SESSION_KEY_ID, keyId, {\n httpOnly: true,\n secure: env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n });\n\n // Clear pending session cookie\n response.cookies.delete(COOKIE_NAMES.OAUTH_PENDING);\n\n logger.debug('OAuth callback completed', { userId, keyId });\n\n return response;\n }\n catch (error)\n {\n const err = error as Error;\n logger.error('OAuth callback failed', { error: err.message });\n\n const errorUrl = new URL(errorRedirect, request.url);\n errorUrl.searchParams.set('error', err.message);\n\n return NextResponse.redirect(errorUrl);\n }\n };\n}\n"],"mappings":"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","names":["jose","env","env","env","redirect","Fragment","jsx","redirect","redirect","Fragment","jsx","redirect","cookies","env","logger","logger","cookies","env","error"]}
|
|
1
|
+
{"version":3,"sources":["../../src/nextjs/server.ts","../../src/nextjs/guards/require-auth.tsx","../../src/nextjs/session-helpers.ts","../../src/server/lib/session.ts","../../src/server/logger.ts","../../src/server/lib/config.ts","../../src/nextjs/guards/auth-utils.ts","../../src/nextjs/guards/require-role.tsx","../../src/nextjs/guards/require-permission.tsx","../../src/nextjs/oauth-handlers.ts"],"sourcesContent":["import 'server-only';\n\nexport { RequireAuth } from './guards/require-auth';\nexport type { RequireAuthProps } from './guards/require-auth';\n\nexport { RequireRole } from './guards/require-role';\nexport type { RequireRoleProps } from './guards/require-role';\n\nexport { RequirePermission } from './guards/require-permission';\nexport type { RequirePermissionProps } from './guards/require-permission';\n\nexport { getAuthSessionData, getUserRole, getUserPermissions, hasAnyRole, hasAnyPermission } from './guards/auth-utils';\n\n// Session helpers\nexport {\n saveSession,\n getSession,\n clearSession,\n // Pending session (OAuth)\n sealPendingSession,\n unsealPendingSession,\n getPendingSession,\n clearPendingSession,\n type SessionData,\n type PublicSession,\n type SaveSessionOptions,\n type PendingSessionData,\n} from './session-helpers';\n\n// OAuth handlers\nexport {\n createOAuthCallbackHandler,\n type OAuthCallbackOptions,\n} from './oauth-handlers';\n","/**\n * RequireAuth Guard Component\n *\n * Requires user to be authenticated\n */\n\nimport { redirect } from 'next/navigation';\nimport { getSession } from '../session-helpers';\nimport { getAuthSessionData } from './auth-utils';\nimport type { ReactNode } from 'react';\n\nexport interface RequireAuthProps\n{\n /**\n * Children to render if authenticated\n */\n children: ReactNode;\n\n /**\n * Path to redirect to if not authenticated\n * @default '/login'\n */\n redirectTo?: string;\n\n /**\n * Fallback UI to show instead of redirecting\n */\n fallback?: ReactNode;\n}\n\n/**\n * Require Authentication Guard\n *\n * Ensures user is logged in before rendering children\n *\n * @example\n * ```tsx\n * <RequireAuth redirectTo=\"/login\">\n * <DashboardContent />\n * </RequireAuth>\n * ```\n *\n * @example With fallback\n * ```tsx\n * <RequireAuth fallback={<LoginPrompt />}>\n * <PrivateContent />\n * </RequireAuth>\n * ```\n */\nexport async function RequireAuth({\n children,\n redirectTo = '/auth/login',\n fallback,\n}: RequireAuthProps)\n{\n const session = await getSession();\n\n if (!session)\n {\n if (fallback)\n {\n return <>{fallback}</>;\n }\n\n redirect(redirectTo);\n }\n\n // Validate server-side session (key expiry, user status, etc.)\n const serverSession = await getAuthSessionData();\n\n if (!serverSession)\n {\n // Note: clearSession() cannot be called in Server Components (Next.js 16+)\n // The RPC proxy interceptor handles session cleanup on 401 responses\n redirect(redirectTo);\n }\n\n return <>{children}</>;\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 // Never log the cookie value — it's the sealed session token.\n logger.debug('Validating session cookie', { present: true });\n const session = await unsealSession(sessionCookie.value);\n\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\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\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 * @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\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\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","/**\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","/**\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\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() \n {\n return `spfn_session${getCookieSuffix()}`; \n },\n /** Current key ID (for key rotation) */\n get SESSION_KEY_ID() \n {\n return `spfn_session_key_id${getCookieSuffix()}`; \n },\n /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */\n get OAUTH_PENDING()\n {\n return `spfn_oauth_pending${getCookieSuffix()}`;\n },\n /** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */\n get OAUTH_CSRF()\n {\n return `spfn_oauth_csrf${getCookieSuffix()}`;\n },\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","/**\n * Server-side auth utilities for guards\n *\n * Uses authApi to check permissions in real-time\n */\n\nimport { authApi } from '@spfn/auth';\nimport { authLogger } from '../../server/logger';\n\n/**\n * Get current auth session with roles and permissions via API\n */\nexport async function getAuthSessionData()\n{\n try\n {\n const session = await authApi.getAuthSession.call();\n authLogger.middleware.debug('Auth session retrieved', { name: session.role?.name });\n\n return session;\n }\n catch (error)\n {\n authLogger.middleware.error('Failed to get auth session', { error });\n\n return null;\n }\n}\n\n/**\n * Get user role\n */\nexport async function getUserRole(): Promise<string | null>\n{\n const session = await getAuthSessionData();\n\n return session?.role?.name || null;\n}\n\n/**\n * Get user permissions\n */\nexport async function getUserPermissions(): Promise<string[]>\n{\n const session = await getAuthSessionData();\n\n if (!session)\n {\n return [];\n }\n\n return session.permissions?.map((p: any) => p.name) || [];\n}\n\n/**\n * Check if user has any of the specified roles\n */\nexport async function hasAnyRole(requiredRoles: string[]): Promise<boolean>\n{\n const session = await getAuthSessionData();\n if (!session)\n {\n return false;\n }\n\n return requiredRoles.includes(session.role?.name);\n}\n\n/**\n * Check if user has any of the specified permissions\n */\nexport async function hasAnyPermission(requiredPermissions: string[]): Promise<boolean>\n{\n const session = await getAuthSessionData();\n\n if (!session)\n {\n return false;\n }\n\n const userPermissionNames = session.permissions?.map((p: any) => p.name) || [];\n\n return requiredPermissions.some(permission => userPermissionNames.includes(permission));\n}\n","/**\n * RequireRole Guard Component\n *\n * Requires user to have at least one of the specified roles\n */\n\nimport { redirect } from 'next/navigation';\nimport { getSession } from '../session-helpers';\nimport { hasAnyRole } from './auth-utils';\nimport type { ReactNode } from 'react';\n\nexport interface RequireRoleProps\n{\n /**\n * Required role(s) - user must have at least one\n */\n roles: string | string[];\n\n /**\n * Children to render if user has required role\n */\n children: ReactNode;\n\n /**\n * Path to redirect to if user doesn't have role\n * @default '/unauthorized'\n */\n redirectTo?: string;\n\n /**\n * Fallback UI to show instead of redirecting\n */\n fallback?: ReactNode;\n}\n\n/**\n * Require Role Guard\n *\n * Ensures user has at least one of the specified roles\n *\n * @example Single role\n * ```tsx\n * <RequireRole roles=\"admin\">\n * <AdminPanel />\n * </RequireRole>\n * ```\n *\n * @example Multiple roles (OR condition)\n * ```tsx\n * <RequireRole roles={['admin', 'manager']}>\n * <ManagementDashboard />\n * </RequireRole>\n * ```\n *\n * @example With fallback\n * ```tsx\n * <RequireRole roles=\"admin\" fallback={<AccessDenied />}>\n * <AdminContent />\n * </RequireRole>\n * ```\n */\nexport async function RequireRole({\n roles,\n children,\n redirectTo = '/unauthorized',\n fallback,\n}: RequireRoleProps)\n{\n const session = await getSession();\n\n // Not authenticated\n if (!session)\n {\n if (fallback)\n {\n return <>{fallback}</>;\n }\n\n redirect('/login');\n }\n\n // Normalize to array\n const requiredRoles = Array.isArray(roles) ? roles : [roles];\n\n // Check if user has any of the required roles\n const hasRole = await hasAnyRole(requiredRoles);\n\n if (!hasRole)\n {\n if (fallback)\n {\n return <>{fallback}</>;\n }\n\n redirect(redirectTo);\n }\n\n return <>{children}</>;\n}\n","/**\n * RequirePermission Guard Component\n *\n * Requires user to have at least one of the specified permissions\n */\n\nimport { redirect } from 'next/navigation';\nimport { getSession } from '../session-helpers';\nimport { hasAnyPermission } from './auth-utils';\nimport type { ReactNode } from 'react';\n\nexport interface RequirePermissionProps\n{\n /**\n * Required permission(s) - user must have at least one\n */\n permissions: string | string[];\n\n /**\n * Children to render if user has required permission\n */\n children: ReactNode;\n\n /**\n * Path to redirect to if user doesn't have permission\n * @default '/unauthorized'\n */\n redirectTo?: string;\n\n /**\n * Fallback UI to show instead of redirecting\n */\n fallback?: ReactNode;\n}\n\n/**\n * Require Permission Guard\n *\n * Ensures user has at least one of the specified permissions\n *\n * @example Single permission\n * ```tsx\n * <RequirePermission permissions=\"user:delete\">\n * <DeleteUserButton />\n * </RequirePermission>\n * ```\n *\n * @example Multiple permissions (OR condition)\n * ```tsx\n * <RequirePermission permissions={['user:delete', 'user:update']}>\n * <UserManagement />\n * </RequirePermission>\n * ```\n *\n * @example With fallback\n * ```tsx\n * <RequirePermission permissions=\"project:create\" fallback={<UpgradePrompt />}>\n * <CreateProject />\n * </RequirePermission>\n * ```\n */\nexport async function RequirePermission({\n permissions,\n children,\n redirectTo = '/unauthorized',\n fallback,\n}: RequirePermissionProps)\n{\n const session = await getSession();\n\n // Not authenticated\n if (!session)\n {\n if (fallback)\n {\n return <>{fallback}</>;\n }\n\n redirect('/login');\n }\n\n // Normalize to array\n const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions];\n\n // Check if user has any of the required permissions\n const hasPermission = await hasAnyPermission(requiredPermissions);\n\n if (!hasPermission)\n {\n if (fallback)\n {\n return <>{fallback}</>;\n }\n\n redirect(redirectTo);\n }\n\n return <>{children}</>;\n}\n","/**\n * OAuth Handlers for Next.js\n *\n * Helper functions to create OAuth callback route handlers\n */\n\nimport { NextRequest, NextResponse } from 'next/server';\nimport { cookies } from 'next/headers.js';\nimport { sealSession } from '../server/lib/session';\nimport { COOKIE_NAMES, getSessionTtl } from '../server/lib/config';\nimport { env } from '@spfn/core/config';\nimport { logger } from '@spfn/core/logger';\nimport { unsealPendingSession } from './session-helpers';\n\nexport interface OAuthCallbackOptions\n{\n /**\n * Default redirect URL if returnUrl is not provided\n * @default '/'\n */\n defaultRedirectUrl?: string;\n\n /**\n * Error redirect URL\n * @default '/auth/error'\n */\n errorRedirectUrl?: string;\n}\n\n/**\n * Create OAuth callback handler for Next.js API Route\n *\n * Handles the final step of OAuth flow:\n * 1. Gets userId, keyId from query params (set by backend)\n * 2. Gets privateKey from pending session cookie\n * 3. Creates full session and saves to cookie\n * 4. Redirects to returnUrl\n *\n * @example\n * ```typescript\n * // /api/auth/callback/route.ts\n * import { createOAuthCallbackHandler } from '@spfn/auth/nextjs/server';\n * export const GET = createOAuthCallbackHandler();\n * ```\n */\nexport function createOAuthCallbackHandler(options?: OAuthCallbackOptions)\n{\n const defaultRedirect = options?.defaultRedirectUrl || '/';\n const errorRedirect = options?.errorRedirectUrl || '/auth/error';\n\n return async (request: NextRequest): Promise<NextResponse> =>\n {\n const searchParams = request.nextUrl.searchParams;\n const userId = searchParams.get('userId');\n const keyId = searchParams.get('keyId');\n const returnUrl = searchParams.get('returnUrl') || defaultRedirect;\n const error = searchParams.get('error');\n\n // Handle error from backend\n if (error)\n {\n const errorUrl = new URL(errorRedirect, request.url);\n errorUrl.searchParams.set('error', error);\n\n return NextResponse.redirect(errorUrl);\n }\n\n // Validate required params\n if (!userId || !keyId)\n {\n logger.error('OAuth callback missing required params', { userId: !!userId, keyId: !!keyId });\n const errorUrl = new URL(errorRedirect, request.url);\n errorUrl.searchParams.set('error', 'Missing required parameters');\n\n return NextResponse.redirect(errorUrl);\n }\n\n try\n {\n // Get pending session from cookie\n const cookieStore = await cookies();\n const pendingCookie = cookieStore.get(COOKIE_NAMES.OAUTH_PENDING);\n\n if (!pendingCookie)\n {\n throw new Error('OAuth session expired. Please try again.');\n }\n\n const pendingSession = await unsealPendingSession(pendingCookie.value);\n\n // Verify keyId matches\n if (pendingSession.keyId !== keyId)\n {\n throw new Error('Session mismatch. Please try again.');\n }\n\n // Create full session\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 // Build redirect response\n const redirectUrl = new URL(returnUrl, request.url);\n const response = NextResponse.redirect(redirectUrl);\n\n // Set session cookie\n response.cookies.set(COOKIE_NAMES.SESSION, sessionToken, {\n httpOnly: true,\n secure: env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n });\n\n // Set keyId cookie\n response.cookies.set(COOKIE_NAMES.SESSION_KEY_ID, keyId, {\n httpOnly: true,\n secure: env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: ttl,\n path: '/',\n });\n\n // Clear pending session cookie\n response.cookies.delete(COOKIE_NAMES.OAUTH_PENDING);\n\n logger.debug('OAuth callback completed', { userId, keyId });\n\n return response;\n }\n catch (error)\n {\n const err = error as Error;\n logger.error('OAuth callback failed', { error: err.message });\n\n const errorUrl = new URL(errorRedirect, request.url);\n errorUrl.searchParams.set('error', err.message);\n\n return NextResponse.redirect(errorUrl);\n }\n };\n}\n"],"mappings":"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","names":["jose","env","env","env","redirect","Fragment","jsx","redirect","redirect","Fragment","jsx","redirect","cookies","env","logger","logger","cookies","env","error"]}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { j as AuthInitOptions, k as OAuthProvider, e as VerificationPurpose, i as PermissionCategory, l as AuthContext } from './authenticate-
|
|
2
|
-
export { C as ChangePasswordParams, a3 as EmailSchema, I as IssueOneTimeTokenResult, s as LoginParams, L as LoginResult, t as LogoutParams, aa as NativeVerifyOptions, a8 as NormalizedIdentity, Z as OAuthCallbackParams, _ as OAuthCallbackResult, a0 as OAuthNativeParams, c as OAuthNativeResult, Y as OAuthStartParams, O as OAuthStartResult, a9 as OAuthTokens, a5 as PasswordSchema, a4 as PhoneSchema, q as RegisterParams, E as RegisterPublicKeyParams, a as RegisterResult, G as RevokeKeyParams, F as RotateKeyParams, b as RotateKeyResult, w as SendVerificationCodeParams, S as SendVerificationCodeResult, a6 as TargetTypeSchema, g as VERIFICATION_PURPOSES, f as VERIFICATION_TARGET_TYPES, a7 as VerificationPurposeSchema, V as VerificationTargetType, x as VerifyCodeParams, y as VerifyCodeResult, m as authRouter, a1 as authenticate, N as buildOAuthErrorUrl, p as changePasswordService, W as getEnabledOAuthProviders, X as getGoogleAccessToken, ac as getOAuthProvider, ad as getRegisteredProviders, Q as isOAuthProviderEnabled, H as issueOneTimeTokenService, n as loginService, o as logoutService, M as oauthCallbackService, $ as oauthNativeService, K as oauthStartService, a2 as optionalAuth, ab as registerOAuthProvider, z as registerPublicKeyService, r as registerService, T as requireEnabledProvider, D as revokeKeyService, B as rotateKeyService, u as sendVerificationCodeService, v as verifyCodeService, J as verifyOneTimeTokenService } from './authenticate-
|
|
1
|
+
import { j as AuthInitOptions, k as OAuthProvider, e as VerificationPurpose, i as PermissionCategory, l as AuthContext } from './authenticate-DXVtYh8X.js';
|
|
2
|
+
export { C as ChangePasswordParams, a3 as EmailSchema, I as IssueOneTimeTokenResult, s as LoginParams, L as LoginResult, t as LogoutParams, aa as NativeVerifyOptions, a8 as NormalizedIdentity, Z as OAuthCallbackParams, _ as OAuthCallbackResult, a0 as OAuthNativeParams, c as OAuthNativeResult, Y as OAuthStartParams, O as OAuthStartResult, a9 as OAuthTokens, a5 as PasswordSchema, a4 as PhoneSchema, q as RegisterParams, E as RegisterPublicKeyParams, a as RegisterResult, G as RevokeKeyParams, F as RotateKeyParams, b as RotateKeyResult, w as SendVerificationCodeParams, S as SendVerificationCodeResult, a6 as TargetTypeSchema, g as VERIFICATION_PURPOSES, f as VERIFICATION_TARGET_TYPES, a7 as VerificationPurposeSchema, V as VerificationTargetType, x as VerifyCodeParams, y as VerifyCodeResult, m as authRouter, a1 as authenticate, N as buildOAuthErrorUrl, p as changePasswordService, W as getEnabledOAuthProviders, X as getGoogleAccessToken, ac as getOAuthProvider, ad as getRegisteredProviders, Q as isOAuthProviderEnabled, H as issueOneTimeTokenService, n as loginService, o as logoutService, M as oauthCallbackService, $ as oauthNativeService, K as oauthStartService, a2 as optionalAuth, ab as registerOAuthProvider, z as registerPublicKeyService, r as registerService, T as requireEnabledProvider, D as revokeKeyService, B as rotateKeyService, u as sendVerificationCodeService, v as verifyCodeService, J as verifyOneTimeTokenService } from './authenticate-DXVtYh8X.js';
|
|
3
3
|
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
4
4
|
import { K as KeyAlgorithmType, b as InvitationStatus, d as SocialProvider } from './types-BtksCI9X.js';
|
|
5
5
|
export { I as INVITATION_STATUSES, a as KEY_ALGORITHM, S as SOCIAL_PROVIDERS, U as USER_STATUSES, c as UserStatus } from './types-BtksCI9X.js';
|
|
@@ -1520,6 +1520,12 @@ interface OAuthState {
|
|
|
1520
1520
|
algorithm: KeyAlgorithmType;
|
|
1521
1521
|
metadata?: Record<string, unknown>;
|
|
1522
1522
|
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Generate a CSRF nonce for the OAuth flow. The caller passes it to
|
|
1525
|
+
* createOAuthState AND sets it as the oauth_csrf cookie, so the callback can
|
|
1526
|
+
* double-submit-verify the flow was initiated by this same browser.
|
|
1527
|
+
*/
|
|
1528
|
+
declare function generateOAuthNonce(): string;
|
|
1523
1529
|
interface CreateOAuthStateParams {
|
|
1524
1530
|
provider: string;
|
|
1525
1531
|
returnUrl: string;
|
|
@@ -1528,6 +1534,11 @@ interface CreateOAuthStateParams {
|
|
|
1528
1534
|
fingerprint: string;
|
|
1529
1535
|
algorithm: KeyAlgorithmType;
|
|
1530
1536
|
metadata?: Record<string, unknown>;
|
|
1537
|
+
/**
|
|
1538
|
+
* CSRF nonce bound into the state. Pass the same value as the oauth_csrf
|
|
1539
|
+
* cookie. Defaults to a fresh nonce (unbound — legacy/no-CSRF callers).
|
|
1540
|
+
*/
|
|
1541
|
+
nonce?: string;
|
|
1531
1542
|
}
|
|
1532
1543
|
/**
|
|
1533
1544
|
* OAuth state 생성 및 암호화
|
|
@@ -5332,6 +5343,8 @@ declare const COOKIE_NAMES: {
|
|
|
5332
5343
|
readonly SESSION_KEY_ID: string;
|
|
5333
5344
|
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
5334
5345
|
readonly OAUTH_PENDING: string;
|
|
5346
|
+
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
5347
|
+
readonly OAUTH_CSRF: string;
|
|
5335
5348
|
};
|
|
5336
5349
|
/**
|
|
5337
5350
|
* Parse duration string to seconds
|
|
@@ -5690,4 +5703,4 @@ type AuthRegisterPayload = typeof authRegisterEvent._payload;
|
|
|
5690
5703
|
type InvitationCreatedPayload = typeof invitationCreatedEvent._payload;
|
|
5691
5704
|
type InvitationAcceptedPayload = typeof invitationAcceptedEvent._payload;
|
|
5692
5705
|
|
|
5693
|
-
export { type AuthConfig, AuthContext, type AuthLifecycleConfig, type AuthLifecycleOptions, type AuthLoginPayload, type AuthMetadataEntity, AuthMetadataRepository, AuthProviderSchema, type AuthRegisterPayload, COOKIE_NAMES, type CreateOAuthStateParams, type GoogleTokenResponse, type GoogleUserInfo, type Invitation, type InvitationAcceptedPayload, type InvitationCreatedPayload, InvitationStatus, InvitationsRepository, KeyAlgorithmType, type KeyPair, KeysRepository, type NewAuthMetadataEntity, type NewInvitation, type NewPermission, type NewPermissionEntity, type NewRole, type NewRoleEntity, type NewRolePermission, type NewUser, type NewUserPermission, type NewUserProfile, type NewUserPublicKey, type NewUserSocialAccount, type NewVerificationCode, OAuthProvider, type OAuthState, type Permission, type PermissionEntity, PermissionsRepository, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, type SessionPayload, SocialAccountsRepository, SocialProvider, type TokenPayload, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, type UserPublicKey, type UserSocialAccount, UsersRepository, type VerificationCode, VerificationCodesRepository, VerificationPurpose, type VerifyIdTokenParams, acceptInvitation, addPermissionToRole, appleProvider, assertCanAssignRole, authLogger, authLoginEvent, authMetadata, authMetadataRepository, authRegisterEvent, authSchema, cancelInvitation, checkUsernameAvailableService, configureAuth, createAuthLifecycle, createInvitation, createOAuthState, createRole, decodeToken, deleteInvitation, deleteRole, exchangeCodeForTokens, expireOldInvitations, generateClientToken, generateKeyPair, generateKeyPairES256, generateKeyPairRS256, generateToken, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getGoogleAuthUrl, getGoogleOAuthConfig, getGoogleUserInfo, getInvitationByToken, getInvitationWithDetails, getKeyId, getKeySize, getLocale, getOneTimeTokenManager, getOptionalAuth, getRole, getRoleByName, getRolePermissions, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, googleProvider, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashPassword, initOneTimeTokenManager, initializeAuth, invitationAcceptedEvent, invitationCreatedEvent, invitationsRepository, isGoogleOAuthEnabled, keysRepository, listInvitations, oneTimeTokenAuth, parseDuration, permissions, permissionsRepository, refreshAccessToken, removePermissionFromRole, requireAnyPermission, requirePermissions, requireRole, resendInvitation, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, setRolePermissions, shouldRotateKey, socialAccountsRepository, updateLastLoginService, updateLocaleService, updateRole, updateUserProfileService, updateUserService, updateUsernameService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userPublicKeys, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyClientToken, verifyIdToken, verifyKeyFingerprint, verifyOAuthState, verifyPassword, verifyToken };
|
|
5706
|
+
export { type AuthConfig, AuthContext, type AuthLifecycleConfig, type AuthLifecycleOptions, type AuthLoginPayload, type AuthMetadataEntity, AuthMetadataRepository, AuthProviderSchema, type AuthRegisterPayload, COOKIE_NAMES, type CreateOAuthStateParams, type GoogleTokenResponse, type GoogleUserInfo, type Invitation, type InvitationAcceptedPayload, type InvitationCreatedPayload, InvitationStatus, InvitationsRepository, KeyAlgorithmType, type KeyPair, KeysRepository, type NewAuthMetadataEntity, type NewInvitation, type NewPermission, type NewPermissionEntity, type NewRole, type NewRoleEntity, type NewRolePermission, type NewUser, type NewUserPermission, type NewUserProfile, type NewUserPublicKey, type NewUserSocialAccount, type NewVerificationCode, OAuthProvider, type OAuthState, type Permission, type PermissionEntity, PermissionsRepository, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, type SessionPayload, SocialAccountsRepository, SocialProvider, type TokenPayload, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, type UserPublicKey, type UserSocialAccount, UsersRepository, type VerificationCode, VerificationCodesRepository, VerificationPurpose, type VerifyIdTokenParams, acceptInvitation, addPermissionToRole, appleProvider, assertCanAssignRole, authLogger, authLoginEvent, authMetadata, authMetadataRepository, authRegisterEvent, authSchema, cancelInvitation, checkUsernameAvailableService, configureAuth, createAuthLifecycle, createInvitation, createOAuthState, createRole, decodeToken, deleteInvitation, deleteRole, exchangeCodeForTokens, expireOldInvitations, generateClientToken, generateKeyPair, generateKeyPairES256, generateKeyPairRS256, generateOAuthNonce, generateToken, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getGoogleAuthUrl, getGoogleOAuthConfig, getGoogleUserInfo, getInvitationByToken, getInvitationWithDetails, getKeyId, getKeySize, getLocale, getOneTimeTokenManager, getOptionalAuth, getRole, getRoleByName, getRolePermissions, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, googleProvider, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashPassword, initOneTimeTokenManager, initializeAuth, invitationAcceptedEvent, invitationCreatedEvent, invitationsRepository, isGoogleOAuthEnabled, keysRepository, listInvitations, oneTimeTokenAuth, parseDuration, permissions, permissionsRepository, refreshAccessToken, removePermissionFromRole, requireAnyPermission, requirePermissions, requireRole, resendInvitation, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, setRolePermissions, shouldRotateKey, socialAccountsRepository, updateLastLoginService, updateLocaleService, updateRole, updateUserProfileService, updateUserService, updateUsernameService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userPublicKeys, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyClientToken, verifyIdToken, verifyKeyFingerprint, verifyOAuthState, verifyPassword, verifyToken };
|
package/dist/server.js
CHANGED
|
@@ -7604,6 +7604,10 @@ var COOKIE_NAMES = {
|
|
|
7604
7604
|
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
7605
7605
|
get OAUTH_PENDING() {
|
|
7606
7606
|
return `spfn_oauth_pending${getCookieSuffix()}`;
|
|
7607
|
+
},
|
|
7608
|
+
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
7609
|
+
get OAUTH_CSRF() {
|
|
7610
|
+
return `spfn_oauth_csrf${getCookieSuffix()}`;
|
|
7607
7611
|
}
|
|
7608
7612
|
};
|
|
7609
7613
|
function parseDuration(duration) {
|
|
@@ -8317,11 +8321,14 @@ function generateNonce() {
|
|
|
8317
8321
|
crypto.getRandomValues(array);
|
|
8318
8322
|
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
8319
8323
|
}
|
|
8324
|
+
function generateOAuthNonce() {
|
|
8325
|
+
return generateNonce();
|
|
8326
|
+
}
|
|
8320
8327
|
async function createOAuthState(params) {
|
|
8321
8328
|
const key = await getStateKey();
|
|
8322
8329
|
const state = {
|
|
8323
8330
|
returnUrl: params.returnUrl,
|
|
8324
|
-
nonce: generateNonce(),
|
|
8331
|
+
nonce: params.nonce ?? generateNonce(),
|
|
8325
8332
|
provider: params.provider,
|
|
8326
8333
|
publicKey: params.publicKey,
|
|
8327
8334
|
keyId: params.keyId,
|
|
@@ -8539,7 +8546,7 @@ function tokenExpiryDate(expiresIn) {
|
|
|
8539
8546
|
return new Date(Date.now() + expiresIn * 1e3);
|
|
8540
8547
|
}
|
|
8541
8548
|
async function oauthStartService(params) {
|
|
8542
|
-
const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata } = params;
|
|
8549
|
+
const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata, nonce } = params;
|
|
8543
8550
|
const oauthProvider = requireEnabledProvider(provider);
|
|
8544
8551
|
const state = await createOAuthState({
|
|
8545
8552
|
provider,
|
|
@@ -8548,13 +8555,19 @@ async function oauthStartService(params) {
|
|
|
8548
8555
|
keyId,
|
|
8549
8556
|
fingerprint,
|
|
8550
8557
|
algorithm,
|
|
8551
|
-
metadata
|
|
8558
|
+
metadata,
|
|
8559
|
+
nonce
|
|
8552
8560
|
});
|
|
8553
8561
|
return { authUrl: oauthProvider.getAuthUrl(state) };
|
|
8554
8562
|
}
|
|
8555
8563
|
async function oauthCallbackService(params) {
|
|
8556
|
-
const { provider, code, state } = params;
|
|
8564
|
+
const { provider, code, state, expectedNonce } = params;
|
|
8557
8565
|
const stateData = await verifyOAuthState(state);
|
|
8566
|
+
if (!expectedNonce || stateData.nonce !== expectedNonce) {
|
|
8567
|
+
throw new ValidationError5({
|
|
8568
|
+
message: "OAuth state validation failed"
|
|
8569
|
+
});
|
|
8570
|
+
}
|
|
8558
8571
|
if (stateData.provider !== provider) {
|
|
8559
8572
|
throw new ValidationError5({
|
|
8560
8573
|
message: "OAuth state provider mismatch"
|
|
@@ -9508,6 +9521,193 @@ var userRouter = defineRouter3({
|
|
|
9508
9521
|
|
|
9509
9522
|
// src/server/routes/oauth/index.ts
|
|
9510
9523
|
init_esm();
|
|
9524
|
+
|
|
9525
|
+
// ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/url.js
|
|
9526
|
+
var tryDecode = (str, decoder) => {
|
|
9527
|
+
try {
|
|
9528
|
+
return decoder(str);
|
|
9529
|
+
} catch {
|
|
9530
|
+
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
|
|
9531
|
+
try {
|
|
9532
|
+
return decoder(match);
|
|
9533
|
+
} catch {
|
|
9534
|
+
return match;
|
|
9535
|
+
}
|
|
9536
|
+
});
|
|
9537
|
+
}
|
|
9538
|
+
};
|
|
9539
|
+
var decodeURIComponent_ = decodeURIComponent;
|
|
9540
|
+
|
|
9541
|
+
// ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/cookie.js
|
|
9542
|
+
var validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/;
|
|
9543
|
+
var validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/;
|
|
9544
|
+
var trimCookieWhitespace = (value) => {
|
|
9545
|
+
let start = 0;
|
|
9546
|
+
let end = value.length;
|
|
9547
|
+
while (start < end) {
|
|
9548
|
+
const charCode = value.charCodeAt(start);
|
|
9549
|
+
if (charCode !== 32 && charCode !== 9) {
|
|
9550
|
+
break;
|
|
9551
|
+
}
|
|
9552
|
+
start++;
|
|
9553
|
+
}
|
|
9554
|
+
while (end > start) {
|
|
9555
|
+
const charCode = value.charCodeAt(end - 1);
|
|
9556
|
+
if (charCode !== 32 && charCode !== 9) {
|
|
9557
|
+
break;
|
|
9558
|
+
}
|
|
9559
|
+
end--;
|
|
9560
|
+
}
|
|
9561
|
+
return start === 0 && end === value.length ? value : value.slice(start, end);
|
|
9562
|
+
};
|
|
9563
|
+
var parse = (cookie, name) => {
|
|
9564
|
+
if (name && cookie.indexOf(name) === -1) {
|
|
9565
|
+
return {};
|
|
9566
|
+
}
|
|
9567
|
+
const pairs = cookie.split(";");
|
|
9568
|
+
const parsedCookie = /* @__PURE__ */ Object.create(null);
|
|
9569
|
+
for (const pairStr of pairs) {
|
|
9570
|
+
const valueStartPos = pairStr.indexOf("=");
|
|
9571
|
+
if (valueStartPos === -1) {
|
|
9572
|
+
continue;
|
|
9573
|
+
}
|
|
9574
|
+
const cookieName = trimCookieWhitespace(pairStr.substring(0, valueStartPos));
|
|
9575
|
+
if (name && name !== cookieName || !validCookieNameRegEx.test(cookieName) || cookieName in parsedCookie) {
|
|
9576
|
+
continue;
|
|
9577
|
+
}
|
|
9578
|
+
let cookieValue = trimCookieWhitespace(pairStr.substring(valueStartPos + 1));
|
|
9579
|
+
if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) {
|
|
9580
|
+
cookieValue = cookieValue.slice(1, -1);
|
|
9581
|
+
}
|
|
9582
|
+
if (validCookieValueRegEx.test(cookieValue)) {
|
|
9583
|
+
parsedCookie[cookieName] = cookieValue.indexOf("%") !== -1 ? tryDecode(cookieValue, decodeURIComponent_) : cookieValue;
|
|
9584
|
+
if (name) {
|
|
9585
|
+
break;
|
|
9586
|
+
}
|
|
9587
|
+
}
|
|
9588
|
+
}
|
|
9589
|
+
return parsedCookie;
|
|
9590
|
+
};
|
|
9591
|
+
var _serialize = (name, value, opt = {}) => {
|
|
9592
|
+
if (!validCookieNameRegEx.test(name)) {
|
|
9593
|
+
throw new Error("Invalid cookie name");
|
|
9594
|
+
}
|
|
9595
|
+
let cookie = `${name}=${value}`;
|
|
9596
|
+
if (name.startsWith("__Secure-") && !opt.secure) {
|
|
9597
|
+
throw new Error("__Secure- Cookie must have Secure attributes");
|
|
9598
|
+
}
|
|
9599
|
+
if (name.startsWith("__Host-")) {
|
|
9600
|
+
if (!opt.secure) {
|
|
9601
|
+
throw new Error("__Host- Cookie must have Secure attributes");
|
|
9602
|
+
}
|
|
9603
|
+
if (opt.path !== "/") {
|
|
9604
|
+
throw new Error('__Host- Cookie must have Path attributes with "/"');
|
|
9605
|
+
}
|
|
9606
|
+
if (opt.domain) {
|
|
9607
|
+
throw new Error("__Host- Cookie must not have Domain attributes");
|
|
9608
|
+
}
|
|
9609
|
+
}
|
|
9610
|
+
for (const key of ["domain", "path", "sameSite", "priority"]) {
|
|
9611
|
+
if (opt[key] && /[;\r\n]/.test(opt[key])) {
|
|
9612
|
+
throw new Error(`${key} must not contain ";", "\\r", or "\\n"`);
|
|
9613
|
+
}
|
|
9614
|
+
}
|
|
9615
|
+
if (opt && typeof opt.maxAge === "number" && opt.maxAge >= 0) {
|
|
9616
|
+
if (opt.maxAge > 3456e4) {
|
|
9617
|
+
throw new Error(
|
|
9618
|
+
"Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration."
|
|
9619
|
+
);
|
|
9620
|
+
}
|
|
9621
|
+
cookie += `; Max-Age=${opt.maxAge | 0}`;
|
|
9622
|
+
}
|
|
9623
|
+
if (opt.domain && opt.prefix !== "host") {
|
|
9624
|
+
cookie += `; Domain=${opt.domain}`;
|
|
9625
|
+
}
|
|
9626
|
+
if (opt.path) {
|
|
9627
|
+
cookie += `; Path=${opt.path}`;
|
|
9628
|
+
}
|
|
9629
|
+
if (opt.expires) {
|
|
9630
|
+
if (opt.expires.getTime() - Date.now() > 3456e7) {
|
|
9631
|
+
throw new Error(
|
|
9632
|
+
"Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future."
|
|
9633
|
+
);
|
|
9634
|
+
}
|
|
9635
|
+
cookie += `; Expires=${opt.expires.toUTCString()}`;
|
|
9636
|
+
}
|
|
9637
|
+
if (opt.httpOnly) {
|
|
9638
|
+
cookie += "; HttpOnly";
|
|
9639
|
+
}
|
|
9640
|
+
if (opt.secure) {
|
|
9641
|
+
cookie += "; Secure";
|
|
9642
|
+
}
|
|
9643
|
+
if (opt.sameSite) {
|
|
9644
|
+
cookie += `; SameSite=${opt.sameSite.charAt(0).toUpperCase() + opt.sameSite.slice(1)}`;
|
|
9645
|
+
}
|
|
9646
|
+
if (opt.priority) {
|
|
9647
|
+
cookie += `; Priority=${opt.priority.charAt(0).toUpperCase() + opt.priority.slice(1)}`;
|
|
9648
|
+
}
|
|
9649
|
+
if (opt.partitioned) {
|
|
9650
|
+
if (!opt.secure) {
|
|
9651
|
+
throw new Error("Partitioned Cookie must have Secure attributes");
|
|
9652
|
+
}
|
|
9653
|
+
cookie += "; Partitioned";
|
|
9654
|
+
}
|
|
9655
|
+
return cookie;
|
|
9656
|
+
};
|
|
9657
|
+
var serialize = (name, value, opt) => {
|
|
9658
|
+
value = encodeURIComponent(value);
|
|
9659
|
+
return _serialize(name, value, opt);
|
|
9660
|
+
};
|
|
9661
|
+
|
|
9662
|
+
// ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/helper/cookie/index.js
|
|
9663
|
+
var getCookie = (c, key, prefix) => {
|
|
9664
|
+
const cookie = c.req.raw.headers.get("Cookie");
|
|
9665
|
+
if (typeof key === "string") {
|
|
9666
|
+
if (!cookie) {
|
|
9667
|
+
return void 0;
|
|
9668
|
+
}
|
|
9669
|
+
let finalKey = key;
|
|
9670
|
+
if (prefix === "secure") {
|
|
9671
|
+
finalKey = "__Secure-" + key;
|
|
9672
|
+
} else if (prefix === "host") {
|
|
9673
|
+
finalKey = "__Host-" + key;
|
|
9674
|
+
}
|
|
9675
|
+
const obj2 = parse(cookie, finalKey);
|
|
9676
|
+
return obj2[finalKey];
|
|
9677
|
+
}
|
|
9678
|
+
if (!cookie) {
|
|
9679
|
+
return {};
|
|
9680
|
+
}
|
|
9681
|
+
const obj = parse(cookie);
|
|
9682
|
+
return obj;
|
|
9683
|
+
};
|
|
9684
|
+
var generateCookie = (name, value, opt) => {
|
|
9685
|
+
let cookie;
|
|
9686
|
+
if (opt?.prefix === "secure") {
|
|
9687
|
+
cookie = serialize("__Secure-" + name, value, { path: "/", ...opt, secure: true });
|
|
9688
|
+
} else if (opt?.prefix === "host") {
|
|
9689
|
+
cookie = serialize("__Host-" + name, value, {
|
|
9690
|
+
...opt,
|
|
9691
|
+
path: "/",
|
|
9692
|
+
secure: true,
|
|
9693
|
+
domain: void 0
|
|
9694
|
+
});
|
|
9695
|
+
} else {
|
|
9696
|
+
cookie = serialize(name, value, { path: "/", ...opt });
|
|
9697
|
+
}
|
|
9698
|
+
return cookie;
|
|
9699
|
+
};
|
|
9700
|
+
var setCookie = (c, name, value, opt) => {
|
|
9701
|
+
const cookie = generateCookie(name, value, opt);
|
|
9702
|
+
c.header("Set-Cookie", cookie, { append: true });
|
|
9703
|
+
};
|
|
9704
|
+
var deleteCookie = (c, name, opt) => {
|
|
9705
|
+
const deletedCookie = getCookie(c, name, opt?.prefix);
|
|
9706
|
+
setCookie(c, name, "", { ...opt, maxAge: 0 });
|
|
9707
|
+
return deletedCookie;
|
|
9708
|
+
};
|
|
9709
|
+
|
|
9710
|
+
// src/server/routes/oauth/index.ts
|
|
9511
9711
|
init_types();
|
|
9512
9712
|
import { Transactional as Transactional3 } from "@spfn/core/db";
|
|
9513
9713
|
import { ValidationError as ValidationError7 } from "@spfn/core/errors";
|
|
@@ -9556,11 +9756,14 @@ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
|
|
|
9556
9756
|
if (!query.code || !query.state) {
|
|
9557
9757
|
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
9558
9758
|
}
|
|
9759
|
+
const expectedNonce = getCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF);
|
|
9760
|
+
deleteCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, { path: "/" });
|
|
9559
9761
|
try {
|
|
9560
9762
|
const result = await oauthCallbackService({
|
|
9561
9763
|
provider: "google",
|
|
9562
9764
|
code: query.code,
|
|
9563
|
-
state: query.state
|
|
9765
|
+
state: query.state,
|
|
9766
|
+
expectedNonce
|
|
9564
9767
|
});
|
|
9565
9768
|
return c.redirect(result.redirectUrl);
|
|
9566
9769
|
} catch (err) {
|
|
@@ -9594,7 +9797,15 @@ var oauthStart = route4.post("/_auth/oauth/start").input({
|
|
|
9594
9797
|
})
|
|
9595
9798
|
}).use([rateLimit2({ limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9596
9799
|
const { body } = await c.data();
|
|
9597
|
-
const
|
|
9800
|
+
const nonce = generateOAuthNonce();
|
|
9801
|
+
setCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, nonce, {
|
|
9802
|
+
httpOnly: true,
|
|
9803
|
+
secure: process.env.NODE_ENV === "production",
|
|
9804
|
+
sameSite: "Lax",
|
|
9805
|
+
maxAge: 600,
|
|
9806
|
+
path: "/"
|
|
9807
|
+
});
|
|
9808
|
+
const result = await oauthStartService({ ...body, nonce });
|
|
9598
9809
|
return result;
|
|
9599
9810
|
});
|
|
9600
9811
|
var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
|
|
@@ -9678,11 +9889,14 @@ var oauthProviderCallback = route4.get("/_auth/oauth/:provider/callback").input(
|
|
|
9678
9889
|
if (!query.code || !query.state) {
|
|
9679
9890
|
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
9680
9891
|
}
|
|
9892
|
+
const expectedNonce = getCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF);
|
|
9893
|
+
deleteCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, { path: "/" });
|
|
9681
9894
|
try {
|
|
9682
9895
|
const result = await oauthCallbackService({
|
|
9683
9896
|
provider: params.provider,
|
|
9684
9897
|
code: query.code,
|
|
9685
|
-
state: query.state
|
|
9898
|
+
state: query.state,
|
|
9899
|
+
expectedNonce
|
|
9686
9900
|
});
|
|
9687
9901
|
return c.redirect(result.redirectUrl);
|
|
9688
9902
|
} catch (err) {
|
|
@@ -10264,6 +10478,7 @@ export {
|
|
|
10264
10478
|
generateKeyPair,
|
|
10265
10479
|
generateKeyPairES256,
|
|
10266
10480
|
generateKeyPairRS256,
|
|
10481
|
+
generateOAuthNonce,
|
|
10267
10482
|
generateToken,
|
|
10268
10483
|
getAllRoles,
|
|
10269
10484
|
getAuth,
|