@spfn/auth 0.3.0-beta.4 → 0.3.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +162 -1
- package/dist/server.js +156 -21
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -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: 'lax',\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\nimport type { SocialProvider } from '../types';\n\n/**\n * Cookie name suffix derived from the server port, so several local dev\n * instances on the same domain do not overwrite each other's sessions.\n *\n * BREAKING: this read `PORT`, which no longer exists — the framework's port is\n * `SPFN_PORT`, because `PORT` is Next.js's own variable and two processes are\n * started. An app that had `PORT` set gets different cookie names than before\n * and its existing sessions stop resolving; one sign-in fixes it.\n */\nfunction getCookieSuffix(): string\n{\n const port = process.env.SPFN_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 * OAuth CSRF 쿠키를 PORT 접미사와 무관하게 전부 수집한다.\n *\n * 쿠키를 심는 쪽은 Next.js 프로세스, 읽는 쪽은 API 프로세스라 분리 배포에서는\n * 두 프로세스의 PORT가 달라 COOKIE_NAMES.OAUTH_CSRF 정확 일치 조회가 빗나간다.\n * nonce 자체가 랜덤값이고 암호화된 state의 nonce와 대조되므로, 접미사가 다른\n * spfn_oauth_csrf* 후보를 모두 대조 대상으로 넘겨도 안전하다.\n */\nexport function matchOAuthCsrfCookies(\n cookies: Record<string, string>,\n): { name: string; value: string }[]\n{\n return Object.entries(cookies)\n .filter(([name]) => /^spfn_oauth_csrf(_\\d+)?$/.test(name))\n .map(([name, value]) => ({ name, value }));\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 * Registration channel passed to the beforeRegister hook\n *\n * - credentials: email/phone + password registration\n * - oauth: new-user signup through a social provider (web or native flow)\n * - invitation: invitation acceptance\n */\nexport type RegisterChannel = 'credentials' | 'oauth' | 'invitation';\n\n/**\n * Context passed to the beforeRegister hook\n *\n * Credentials (password, keys) are intentionally excluded — the hook is a\n * policy gate, not a credential handler.\n */\nexport interface BeforeRegisterContext\n{\n channel: RegisterChannel;\n /** Social provider — only set when channel is 'oauth' */\n provider?: SocialProvider;\n email?: string;\n /**\n * Whether the email is verified — only set when channel is 'oauth'.\n * OAuth providers may report an unverified (spoofable) email; the created\n * account stores it as null in that case, so email-based policies must\n * check this flag. credentials/invitation emails are already verified.\n */\n emailVerified?: boolean;\n phone?: string;\n /** App-supplied registration metadata (register params / OAuth start params / invitation) */\n metadata?: Record<string, unknown>;\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 * App-injected validator that runs before a new user row is created,\n * on every registration channel (credentials, oauth, invitation).\n *\n * Throw to reject the registration — RegistrationRejectedError (403) is\n * the recommended error; any HttpError subclass keeps its own status.\n * Runs after built-in checks (verification token, duplicate account),\n * so existing error precedence is unchanged. Not called for admin\n * seeding (initializeAuth) or when linking a social account to an\n * existing user.\n *\n * Runs inside the registration DB transaction on every channel — keep it\n * fast. A slow call (e.g. an external policy API) holds a pooled DB\n * connection open for its full duration on every signup.\n *\n * @example\n * ```typescript\n * configureAuth({\n * beforeRegister: async ({ channel, metadata }) =>\n * {\n * if (channel === 'credentials' && !isOldEnough(metadata?.birthDate))\n * {\n * throw new RegistrationRejectedError({ message: 'Age requirement not met' });\n * }\n * },\n * });\n * ```\n */\n beforeRegister?: (context: BeforeRegisterContext) => void | Promise<void>;\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 * Run the app-injected beforeRegister hook if configured — throws to reject.\n *\n * Single entry point for every registration channel so a new channel cannot\n * forget the configured-check. Callers invoke this right before creating the\n * user row.\n */\nexport async function runBeforeRegister(context: BeforeRegisterContext): Promise<void>\n{\n const { beforeRegister } = globalConfig;\n\n if (beforeRegister)\n {\n await beforeRegister(context);\n }\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: 'lax',\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: 'lax',\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: 'lax',\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\nimport type { SocialProvider } from '../types';\nimport { normalizeOptionalEmail } from '../helpers/email';\n\n/**\n * Cookie name suffix derived from the server port, so several local dev\n * instances on the same domain do not overwrite each other's sessions.\n *\n * BREAKING: this read `PORT`, which no longer exists — the framework's port is\n * `SPFN_PORT`, because `PORT` is Next.js's own variable and two processes are\n * started. An app that had `PORT` set gets different cookie names than before\n * and its existing sessions stop resolving; one sign-in fixes it.\n */\nfunction getCookieSuffix(): string\n{\n const port = process.env.SPFN_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 * OAuth CSRF 쿠키를 PORT 접미사와 무관하게 전부 수집한다.\n *\n * 쿠키를 심는 쪽은 Next.js 프로세스, 읽는 쪽은 API 프로세스라 분리 배포에서는\n * 두 프로세스의 PORT가 달라 COOKIE_NAMES.OAUTH_CSRF 정확 일치 조회가 빗나간다.\n * nonce 자체가 랜덤값이고 암호화된 state의 nonce와 대조되므로, 접미사가 다른\n * spfn_oauth_csrf* 후보를 모두 대조 대상으로 넘겨도 안전하다.\n */\nexport function matchOAuthCsrfCookies(\n cookies: Record<string, string>,\n): { name: string; value: string }[]\n{\n return Object.entries(cookies)\n .filter(([name]) => /^spfn_oauth_csrf(_\\d+)?$/.test(name))\n .map(([name, value]) => ({ name, value }));\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 * Registration channel passed to the beforeRegister hook\n *\n * - credentials: email/phone + password registration\n * - oauth: new-user signup through a social provider (web or native flow)\n * - invitation: invitation acceptance\n */\nexport type RegisterChannel = 'credentials' | 'oauth' | 'invitation';\n\n/**\n * Context passed to the beforeRegister hook\n *\n * Credentials (password, keys) are intentionally excluded — the hook is a\n * policy gate, not a credential handler.\n */\nexport interface BeforeRegisterContext\n{\n channel: RegisterChannel;\n /** Social provider — only set when channel is 'oauth' */\n provider?: SocialProvider;\n /**\n * Canonical form of the address — trimmed and lower-cased, the same form\n * the account is stored under. A policy keyed on the address (a denylist, a\n * domain allowlist) therefore matches whatever capitalization the person\n * typed, instead of being walked past by `Blocked@Example.com`.\n */\n email?: string;\n /**\n * Whether the email is verified — only set when channel is 'oauth'.\n * OAuth providers may report an unverified (spoofable) email; the created\n * account stores it as null in that case, so email-based policies must\n * check this flag. credentials/invitation emails are already verified.\n */\n emailVerified?: boolean;\n phone?: string;\n /** App-supplied registration metadata (register params / OAuth start params / invitation) */\n metadata?: Record<string, unknown>;\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 * App-injected validator that runs before a new user row is created,\n * on every registration channel (credentials, oauth, invitation).\n *\n * Throw to reject the registration — RegistrationRejectedError (403) is\n * the recommended error; any HttpError subclass keeps its own status.\n * Runs after built-in checks (verification token, duplicate account),\n * so existing error precedence is unchanged. Not called for admin\n * seeding (initializeAuth) or when linking a social account to an\n * existing user.\n *\n * Runs inside the registration DB transaction on every channel — keep it\n * fast. A slow call (e.g. an external policy API) holds a pooled DB\n * connection open for its full duration on every signup.\n *\n * @example\n * ```typescript\n * configureAuth({\n * beforeRegister: async ({ channel, metadata }) =>\n * {\n * if (channel === 'credentials' && !isOldEnough(metadata?.birthDate))\n * {\n * throw new RegistrationRejectedError({ message: 'Age requirement not met' });\n * }\n * },\n * });\n * ```\n */\n beforeRegister?: (context: BeforeRegisterContext) => void | Promise<void>;\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 * Run the app-injected beforeRegister hook if configured — throws to reject.\n *\n * Single entry point for every registration channel so a new channel cannot\n * forget the configured-check. Callers invoke this right before creating the\n * user row.\n *\n * The address is folded here rather than at each call site, for the same reason\n * the check itself lives here: three channels supply it, and a policy that sees\n * a different spelling depending on which one the person came through is a\n * policy that can be walked past.\n */\nexport async function runBeforeRegister(context: BeforeRegisterContext): Promise<void>\n{\n const { beforeRegister } = globalConfig;\n\n if (beforeRegister)\n {\n await beforeRegister({ ...context, email: normalizeOptionalEmail(context.email) });\n }\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: 'lax',\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: 'lax',\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
|
@@ -431,6 +431,42 @@ declare function updateUsernameService(userId: string | number | bigint, usernam
|
|
|
431
431
|
*/
|
|
432
432
|
declare function initializeAuth(options?: AuthInitOptions): Promise<void>;
|
|
433
433
|
|
|
434
|
+
/**
|
|
435
|
+
* @spfn/auth - Stored email normalization
|
|
436
|
+
*
|
|
437
|
+
* Brings existing rows into the canonical form the repository now writes and
|
|
438
|
+
* reads. Without it, normalizing the code alone would lock out every account
|
|
439
|
+
* whose address was stored with any capitalization: the lookup folds the typed
|
|
440
|
+
* address to lower case, the stored one stays mixed, and they stop matching.
|
|
441
|
+
*
|
|
442
|
+
* Runs once at startup and records that it ran, so a healthy install pays one
|
|
443
|
+
* small metadata read per boot rather than a scan of the users table.
|
|
444
|
+
*
|
|
445
|
+
* The work itself stays in the database — one query to find the addresses two
|
|
446
|
+
* rows would share, one statement to fold the rest. Nothing proportional to the
|
|
447
|
+
* table is carried through the application, which is what keeps a legacy
|
|
448
|
+
* install's first boot from turning into a row-at-a-time migration.
|
|
449
|
+
*/
|
|
450
|
+
interface EmailNormalizationResult {
|
|
451
|
+
/** Rows rewritten to canonical form. */
|
|
452
|
+
normalized: number;
|
|
453
|
+
/** User id groups that share an address once folded, left untouched. */
|
|
454
|
+
conflicts: number[][];
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Rewrite stored addresses to canonical form, skipping anything that would
|
|
458
|
+
* collide.
|
|
459
|
+
*
|
|
460
|
+
* A collision means two accounts differ only by capitalization. Which of them is
|
|
461
|
+
* the real one, and what happens to the other's data, is not a question this can
|
|
462
|
+
* answer — so every row in the group is left exactly as it is and all of their
|
|
463
|
+
* ids are reported. The alternative, merging or deleting one, destroys an
|
|
464
|
+
* account on a guess.
|
|
465
|
+
*
|
|
466
|
+
* @returns what was rewritten and what was left for an operator
|
|
467
|
+
*/
|
|
468
|
+
declare function normalizeStoredEmails(): Promise<EmailNormalizationResult>;
|
|
469
|
+
|
|
434
470
|
/**
|
|
435
471
|
* @spfn/auth - Permission Service
|
|
436
472
|
*
|
|
@@ -3629,6 +3665,38 @@ declare class UsersRepository extends BaseRepository {
|
|
|
3629
3665
|
phoneVerifiedAt: Date | null;
|
|
3630
3666
|
lastLoginAt: Date | null;
|
|
3631
3667
|
}>;
|
|
3668
|
+
/**
|
|
3669
|
+
* 이메일로 사용자 조회 — 저장된 형태와 무관하게 찾는다.
|
|
3670
|
+
*
|
|
3671
|
+
* `findByEmail` asks whether a row holds this exact address; this asks
|
|
3672
|
+
* whether any row *is* this address, whatever form it was written in. The
|
|
3673
|
+
* difference matters to a caller that answers "no" by creating an account:
|
|
3674
|
+
* a lookup that misses a row stored in another form would make a second
|
|
3675
|
+
* account for a person who already has one, and the unique constraint
|
|
3676
|
+
* cannot object because the two stored strings differ.
|
|
3677
|
+
*
|
|
3678
|
+
* Folding in the predicate means no index on `email` applies, so this is for
|
|
3679
|
+
* the few addresses a caller decides about — admin seeding — not for a
|
|
3680
|
+
* request path. Write primary: the answer decides whether to insert.
|
|
3681
|
+
*/
|
|
3682
|
+
findByEmailInAnyStoredForm(email: string): Promise<{
|
|
3683
|
+
deletedAt: Date | null;
|
|
3684
|
+
deletedBy: string | null;
|
|
3685
|
+
createdAt: Date;
|
|
3686
|
+
updatedAt: Date;
|
|
3687
|
+
id: number;
|
|
3688
|
+
publicId: string;
|
|
3689
|
+
email: string | null;
|
|
3690
|
+
phone: string | null;
|
|
3691
|
+
username: string | null;
|
|
3692
|
+
passwordHash: string | null;
|
|
3693
|
+
passwordChangeRequired: boolean;
|
|
3694
|
+
roleId: number;
|
|
3695
|
+
status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
|
|
3696
|
+
emailVerifiedAt: Date | null;
|
|
3697
|
+
phoneVerifiedAt: Date | null;
|
|
3698
|
+
lastLoginAt: Date | null;
|
|
3699
|
+
}>;
|
|
3632
3700
|
/**
|
|
3633
3701
|
* 전화번호로 사용자 조회
|
|
3634
3702
|
* Read replica 사용
|
|
@@ -3770,6 +3838,51 @@ declare class UsersRepository extends BaseRepository {
|
|
|
3770
3838
|
deletedAt: Date | null;
|
|
3771
3839
|
deletedBy: string | null;
|
|
3772
3840
|
}>;
|
|
3841
|
+
/**
|
|
3842
|
+
* User ids grouped by an address two or more rows share once folded.
|
|
3843
|
+
*
|
|
3844
|
+
* The whole comparison happens in the database and only the colliding groups
|
|
3845
|
+
* come back, so the size of the answer is the size of the problem rather
|
|
3846
|
+
* than the size of the table. `users.email` is unique, so a group of more
|
|
3847
|
+
* than one can only be rows that differ by capitalization or padding —
|
|
3848
|
+
* exactly the ones a rewrite cannot decide between.
|
|
3849
|
+
*
|
|
3850
|
+
* Every member id is returned, including a row already holding the
|
|
3851
|
+
* canonical form, because the operator has to compare the accounts against
|
|
3852
|
+
* each other to settle which is real.
|
|
3853
|
+
*
|
|
3854
|
+
* Write primary: the caller is about to rewrite rows and a replica could
|
|
3855
|
+
* still be showing the pre-fix state.
|
|
3856
|
+
*/
|
|
3857
|
+
findEmailConflictGroups(): Promise<number[][]>;
|
|
3858
|
+
/**
|
|
3859
|
+
* Fold every stored address to canonical form, leaving the given ids alone.
|
|
3860
|
+
*
|
|
3861
|
+
* One statement rather than a row at a time: the rewrite is the same
|
|
3862
|
+
* expression the detection uses, so the database can do it in place. A
|
|
3863
|
+
* legacy install with a large users table therefore pays one update instead
|
|
3864
|
+
* of a round trip per row on the boot path, and no list of addresses is ever
|
|
3865
|
+
* carried through the application.
|
|
3866
|
+
*
|
|
3867
|
+
* The excluded ids travel as a single array parameter, so the count of
|
|
3868
|
+
* conflicts cannot run into the protocol's limit on bind parameters.
|
|
3869
|
+
*
|
|
3870
|
+
* One statement also means all or nothing. `users.email` is unique, so if an
|
|
3871
|
+
* instance still running the old code registers a canonical address in the
|
|
3872
|
+
* moment between the conflict query and this update, the update aborts and
|
|
3873
|
+
* nothing is folded on this boot. The next boot sees that pair as a conflict
|
|
3874
|
+
* and folds everything else, so the repair is deferred rather than lost.
|
|
3875
|
+
*
|
|
3876
|
+
* `lower(btrim(...))` is the SQL spelling of `normalizeEmail`. The two agree
|
|
3877
|
+
* on every address this package's validation accepts (ASCII, no interior
|
|
3878
|
+
* whitespace); an address outside that set — reachable only by an app
|
|
3879
|
+
* writing to the repository directly — may fold differently in a database
|
|
3880
|
+
* whose collation lower-cases non-ASCII letters.
|
|
3881
|
+
*
|
|
3882
|
+
* @param excludedIds - Rows to leave untouched, normally the conflict groups
|
|
3883
|
+
* @returns How many rows were rewritten
|
|
3884
|
+
*/
|
|
3885
|
+
normalizeEmailsExcept(excludedIds: number[]): Promise<number>;
|
|
3773
3886
|
/**
|
|
3774
3887
|
* 사용자 정보 업데이트
|
|
3775
3888
|
* Write primary 사용
|
|
@@ -5669,6 +5782,43 @@ declare class OpsTokensRepository extends BaseRepository {
|
|
|
5669
5782
|
}
|
|
5670
5783
|
declare const opsTokensRepository: OpsTokensRepository;
|
|
5671
5784
|
|
|
5785
|
+
/**
|
|
5786
|
+
* @spfn/auth - Email address normalization
|
|
5787
|
+
*
|
|
5788
|
+
* One canonical form for an address, applied on the way in AND on the way out,
|
|
5789
|
+
* so `Foo@Example.com` and `foo@example.com` are the same account everywhere.
|
|
5790
|
+
*
|
|
5791
|
+
* Both sides have to agree or normalization makes things worse rather than
|
|
5792
|
+
* better: normalizing only the lookup means an address stored in mixed case can
|
|
5793
|
+
* no longer be found, and normalizing only the write means an old row stays
|
|
5794
|
+
* unreachable. `UsersRepository` therefore applies this to every read and every
|
|
5795
|
+
* write of `users.email`, which is what keeps a call site from forgetting.
|
|
5796
|
+
*/
|
|
5797
|
+
/**
|
|
5798
|
+
* Trim and lower-case an address.
|
|
5799
|
+
*
|
|
5800
|
+
* Nothing else. Two normalizations that look tempting are deliberately absent:
|
|
5801
|
+
* stripping dots and cutting at `+` are Gmail delivery rules, not internet ones,
|
|
5802
|
+
* and applying them would merge addresses that other providers treat as
|
|
5803
|
+
* genuinely different people.
|
|
5804
|
+
*
|
|
5805
|
+
* The local part is technically case-sensitive per RFC 5321, but no mail
|
|
5806
|
+
* provider in practice treats it that way, while users routinely retype their
|
|
5807
|
+
* address with different capitalization. Matching the practice avoids duplicate
|
|
5808
|
+
* accounts; honouring the RFC creates them.
|
|
5809
|
+
*
|
|
5810
|
+
* @param email - Address as supplied
|
|
5811
|
+
* @returns The canonical form used for storage and lookup
|
|
5812
|
+
*/
|
|
5813
|
+
declare function normalizeEmail(email: string): string;
|
|
5814
|
+
/**
|
|
5815
|
+
* Normalize when there is an address, pass null/undefined through untouched.
|
|
5816
|
+
*
|
|
5817
|
+
* Accounts registered by phone have no email, and `null` is meaningfully
|
|
5818
|
+
* different from `''` in that column.
|
|
5819
|
+
*/
|
|
5820
|
+
declare function normalizeOptionalEmail<T extends string | null | undefined>(email: T): T;
|
|
5821
|
+
|
|
5672
5822
|
/**
|
|
5673
5823
|
* @spfn/auth - Password Helpers
|
|
5674
5824
|
*
|
|
@@ -6270,6 +6420,12 @@ interface BeforeRegisterContext {
|
|
|
6270
6420
|
channel: RegisterChannel;
|
|
6271
6421
|
/** Social provider — only set when channel is 'oauth' */
|
|
6272
6422
|
provider?: SocialProvider;
|
|
6423
|
+
/**
|
|
6424
|
+
* Canonical form of the address — trimmed and lower-cased, the same form
|
|
6425
|
+
* the account is stored under. A policy keyed on the address (a denylist, a
|
|
6426
|
+
* domain allowlist) therefore matches whatever capitalization the person
|
|
6427
|
+
* typed, instead of being walked past by `Blocked@Example.com`.
|
|
6428
|
+
*/
|
|
6273
6429
|
email?: string;
|
|
6274
6430
|
/**
|
|
6275
6431
|
* Whether the email is verified — only set when channel is 'oauth'.
|
|
@@ -6349,6 +6505,11 @@ declare function getAuthConfig(): AuthConfig;
|
|
|
6349
6505
|
* Single entry point for every registration channel so a new channel cannot
|
|
6350
6506
|
* forget the configured-check. Callers invoke this right before creating the
|
|
6351
6507
|
* user row.
|
|
6508
|
+
*
|
|
6509
|
+
* The address is folded here rather than at each call site, for the same reason
|
|
6510
|
+
* the check itself lives here: three channels supply it, and a policy that sees
|
|
6511
|
+
* a different spelling depending on which one the person came through is a
|
|
6512
|
+
* policy that can be walked past.
|
|
6352
6513
|
*/
|
|
6353
6514
|
declare function runBeforeRegister(context: BeforeRegisterContext): Promise<void>;
|
|
6354
6515
|
/**
|
|
@@ -6915,4 +7076,4 @@ type AuthDeletionCancelledPayload = typeof authDeletionCancelledEvent._payload;
|
|
|
6915
7076
|
type AuthDeletionCompletedPayload = typeof authDeletionCompletedEvent._payload;
|
|
6916
7077
|
type OAuthUnlinkedPayload = typeof oauthUnlinkedEvent._payload;
|
|
6917
7078
|
|
|
6918
|
-
export { type AccountDeletionPurgeUser, type AccountDeletionRequest, AccountDeletionRequestedBy, AccountDeletionRequestsRepository, type AuthConfig, AuthContext, type AuthDeletionCancelledPayload, type AuthDeletionCompletedPayload, type AuthDeletionConfig, type AuthDeletionRequestedPayload, type AuthLifecycleConfig, type AuthLifecycleOptions, type AuthLoginPayload, type AuthMetadataEntity, AuthMetadataRepository, AuthProviderSchema, type AuthRegisterPayload, type BeforeRegisterContext, COOKIE_NAMES, type CancelAccountDeletionParams, type CancelAccountDeletionResult, type CreateOAuthStateParams, DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE, DEFAULT_DELETION_GRACE_PERIOD_DAYS, DEFAULT_DELETION_PURGE_CRON, DEFAULT_DELETION_PURGE_STRATEGY, DEFAULT_DELETION_SEND_NOTIFICATIONS, type DecryptedOAuthToken, EnvironmentKeyringTokenCipher, type GoogleTokenResponse, type GoogleUserInfo, type Invitation, type InvitationAcceptedPayload, type InvitationCreatedPayload, InvitationStatus, InvitationsRepository, type IssuedOpsToken, KeyAlgorithmType, KeysRepository, type NewAccountDeletionRequest, type NewAuthMetadataEntity, type NewInvitation, type NewOpsToken, 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 OAuthTokenCipher, type OAuthTokenContext, type OAuthTokenType, type OAuthUnlinkedPayload, type OpsToken, OpsTokensRepository, type PendingDeletionInfo, type Permission, type PermissionEntity, PermissionsRepository, PurgeStrategy, type PurgeUserResult, type RegisterChannel, type RequestAccountDeletionParams, type RequestAccountDeletionResult, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, type SessionPayload, SocialAccountsRepository, SocialProvider, type SweepDuePurgesResult, type TokenPayload, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, type UserPublicKey, type UserSocialAccount, UsersRepository, type VerificationCode, VerificationCodesRepository, VerificationPurpose, type VerifiedOpsToken, type VerifyIdTokenParams, acceptInvitation, accountDeletionRequests, accountDeletionRequestsRepository, addPermissionToRole, appleProvider, assertCanAssignRole, authDeletionCancelledEvent, authDeletionCompletedEvent, authDeletionRequestedEvent, authJobRouter, authLogger, authLoginEvent, authMetadata, authMetadataRepository, authRegisterEvent, authSchema, cancelAccountDeletionService, cancelInvitation, checkUsernameAvailableService, configureAuth, configureDeletion, configureOAuthTokenCipher, createAuthDeletionJobRouter, createAuthDeletionPurgeJob, createAuthLifecycle, createInvitation, createOAuthState, createRole, decodeToken, decryptToken, deleteInvitation, deleteRole, encryptToken, exchangeCodeForTokens, expireOldInvitations, generateOAuthNonce, generateToken, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getDeletionConfig, getDummyPasswordHash, getGoogleAuthUrl, getGoogleOAuthConfig, getGoogleUserInfo, getInvitationByToken, getInvitationWithDetails, getKeyId, getLocale, getOneTimeTokenManager, getOpsToken, getOptionalAuth, getPendingDeletionInfo, getRole, getRoleByName, getRolePermissions, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, githubProvider, googleProvider, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashPassword, initOneTimeTokenManager, initializeAuth, invitationAcceptedEvent, invitationCreatedEvent, invitationsRepository, isEncrypted, isGoogleOAuthEnabled, issueOpsTokenService, kakaoProvider, keysRepository, listInvitations, listOpsTokensService, matchOAuthCsrfCookies, naverProvider, oauthUnlinkedEvent, oneTimeTokenAuth, opsTokenAuth, opsTokens, opsTokensRepository, parseDuration, permissions, permissionsRepository, purgeUserService, refreshAccessToken, removePermissionFromRole, requestAccountDeletionService, requireAnyPermission, requireOpsScope, requirePermissions, requireRole, resendInvitation, revokeOpsTokenService, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, runBeforeRegister, setRolePermissions, socialAccountsRepository, sweepDuePurges, updateLastLoginService, updateLocaleService, updateRole, updateUserProfileService, updateUserService, updateUsernameService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userPublicKeys, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyClientToken, verifyIdToken, verifyKeyFingerprint, verifyOAuthState, verifyOpsTokenService, verifyPassword, verifyToken };
|
|
7079
|
+
export { type AccountDeletionPurgeUser, type AccountDeletionRequest, AccountDeletionRequestedBy, AccountDeletionRequestsRepository, type AuthConfig, AuthContext, type AuthDeletionCancelledPayload, type AuthDeletionCompletedPayload, type AuthDeletionConfig, type AuthDeletionRequestedPayload, type AuthLifecycleConfig, type AuthLifecycleOptions, type AuthLoginPayload, type AuthMetadataEntity, AuthMetadataRepository, AuthProviderSchema, type AuthRegisterPayload, type BeforeRegisterContext, COOKIE_NAMES, type CancelAccountDeletionParams, type CancelAccountDeletionResult, type CreateOAuthStateParams, DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE, DEFAULT_DELETION_GRACE_PERIOD_DAYS, DEFAULT_DELETION_PURGE_CRON, DEFAULT_DELETION_PURGE_STRATEGY, DEFAULT_DELETION_SEND_NOTIFICATIONS, type DecryptedOAuthToken, type EmailNormalizationResult, EnvironmentKeyringTokenCipher, type GoogleTokenResponse, type GoogleUserInfo, type Invitation, type InvitationAcceptedPayload, type InvitationCreatedPayload, InvitationStatus, InvitationsRepository, type IssuedOpsToken, KeyAlgorithmType, KeysRepository, type NewAccountDeletionRequest, type NewAuthMetadataEntity, type NewInvitation, type NewOpsToken, 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 OAuthTokenCipher, type OAuthTokenContext, type OAuthTokenType, type OAuthUnlinkedPayload, type OpsToken, OpsTokensRepository, type PendingDeletionInfo, type Permission, type PermissionEntity, PermissionsRepository, PurgeStrategy, type PurgeUserResult, type RegisterChannel, type RequestAccountDeletionParams, type RequestAccountDeletionResult, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, type SessionPayload, SocialAccountsRepository, SocialProvider, type SweepDuePurgesResult, type TokenPayload, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, type UserPublicKey, type UserSocialAccount, UsersRepository, type VerificationCode, VerificationCodesRepository, VerificationPurpose, type VerifiedOpsToken, type VerifyIdTokenParams, acceptInvitation, accountDeletionRequests, accountDeletionRequestsRepository, addPermissionToRole, appleProvider, assertCanAssignRole, authDeletionCancelledEvent, authDeletionCompletedEvent, authDeletionRequestedEvent, authJobRouter, authLogger, authLoginEvent, authMetadata, authMetadataRepository, authRegisterEvent, authSchema, cancelAccountDeletionService, cancelInvitation, checkUsernameAvailableService, configureAuth, configureDeletion, configureOAuthTokenCipher, createAuthDeletionJobRouter, createAuthDeletionPurgeJob, createAuthLifecycle, createInvitation, createOAuthState, createRole, decodeToken, decryptToken, deleteInvitation, deleteRole, encryptToken, exchangeCodeForTokens, expireOldInvitations, generateOAuthNonce, generateToken, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getDeletionConfig, getDummyPasswordHash, getGoogleAuthUrl, getGoogleOAuthConfig, getGoogleUserInfo, getInvitationByToken, getInvitationWithDetails, getKeyId, getLocale, getOneTimeTokenManager, getOpsToken, getOptionalAuth, getPendingDeletionInfo, getRole, getRoleByName, getRolePermissions, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, githubProvider, googleProvider, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashPassword, initOneTimeTokenManager, initializeAuth, invitationAcceptedEvent, invitationCreatedEvent, invitationsRepository, isEncrypted, isGoogleOAuthEnabled, issueOpsTokenService, kakaoProvider, keysRepository, listInvitations, listOpsTokensService, matchOAuthCsrfCookies, naverProvider, normalizeEmail, normalizeOptionalEmail, normalizeStoredEmails, oauthUnlinkedEvent, oneTimeTokenAuth, opsTokenAuth, opsTokens, opsTokensRepository, parseDuration, permissions, permissionsRepository, purgeUserService, refreshAccessToken, removePermissionFromRole, requestAccountDeletionService, requireAnyPermission, requireOpsScope, requirePermissions, requireRole, resendInvitation, revokeOpsTokenService, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, runBeforeRegister, setRolePermissions, socialAccountsRepository, sweepDuePurges, updateLastLoginService, updateLocaleService, updateRole, updateUserProfileService, updateUserService, updateUsernameService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userPublicKeys, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyClientToken, verifyIdToken, verifyKeyFingerprint, verifyOAuthState, verifyOpsTokenService, verifyPassword, verifyToken };
|