@spfn/auth 0.3.0-beta.7 → 0.3.0-beta.8
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 +378 -0
- package/dist/client-proof.d.ts +23 -12
- package/dist/client-proof.js +143 -4
- package/dist/client-proof.js.map +1 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +76 -3
- package/dist/errors.js +45 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +46 -6
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -1
- package/dist/{authenticate-Mg9D7Nys.d.ts → machine-principals-Bd5hp76H.d.ts} +343 -6
- package/dist/nextjs/api.js +190 -9
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.js +59 -8
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +719 -89
- package/dist/server.js +1383 -440
- package/dist/server.js.map +1 -1
- package/migrations/20260901091716_fine_arclight/migration.sql +21 -0
- package/migrations/20260901091716_fine_arclight/snapshot.json +3849 -0
- package/package.json +2 -2
package/dist/nextjs/server.js
CHANGED
|
@@ -22,7 +22,8 @@ var authLogger = {
|
|
|
22
22
|
general: rootLogger.child("@spfn/auth:interceptor:general"),
|
|
23
23
|
login: rootLogger.child("@spfn/auth:interceptor:login"),
|
|
24
24
|
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
|
|
25
|
-
oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
|
|
25
|
+
oauth: rootLogger.child("@spfn/auth:interceptor:oauth"),
|
|
26
|
+
csrf: rootLogger.child("@spfn/auth:interceptor:csrf")
|
|
26
27
|
},
|
|
27
28
|
session: rootLogger.child("@spfn/auth:session"),
|
|
28
29
|
service: rootLogger.child("@spfn/auth:service"),
|
|
@@ -89,8 +90,39 @@ async function unsealSession(jwt) {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// src/server/lib/
|
|
93
|
+
// src/server/lib/csrf.ts
|
|
93
94
|
import { env as env2 } from "@spfn/auth/config";
|
|
95
|
+
var CSRF_SUBKEY_LABEL = "spfn-auth-csrf-token-v1";
|
|
96
|
+
function sessionSecret() {
|
|
97
|
+
const secret = env2.SPFN_AUTH_SESSION_SECRET;
|
|
98
|
+
if (!secret) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"SPFN_AUTH_SESSION_SECRET is required for CSRF protection. Set it (sessions need it anyway), or set SPFN_AUTH_CSRF=off."
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return secret;
|
|
104
|
+
}
|
|
105
|
+
async function hmacSha256(key, message) {
|
|
106
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
107
|
+
"raw",
|
|
108
|
+
key.buffer,
|
|
109
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
110
|
+
false,
|
|
111
|
+
["sign"]
|
|
112
|
+
);
|
|
113
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(message));
|
|
114
|
+
return new Uint8Array(signature);
|
|
115
|
+
}
|
|
116
|
+
function toHex(bytes) {
|
|
117
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
118
|
+
}
|
|
119
|
+
async function deriveCsrfToken(keyId) {
|
|
120
|
+
const subkey = await hmacSha256(new TextEncoder().encode(sessionSecret()), CSRF_SUBKEY_LABEL);
|
|
121
|
+
return toHex(await hmacSha256(subkey, keyId));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/server/lib/config.ts
|
|
125
|
+
import { env as env3 } from "@spfn/auth/config";
|
|
94
126
|
function getCookieSuffix() {
|
|
95
127
|
const port = process.env.SPFN_PORT;
|
|
96
128
|
return port ? `_${port}` : "";
|
|
@@ -115,6 +147,10 @@ var COOKIE_NAMES = {
|
|
|
115
147
|
/** Password-setup session for verified-email signup — temporary, single-purpose */
|
|
116
148
|
get SIGNUP_SETUP() {
|
|
117
149
|
return `spfn_signup_setup${getCookieSuffix()}`;
|
|
150
|
+
},
|
|
151
|
+
/** CSRF token — the only cookie here the browser can read */
|
|
152
|
+
get CSRF() {
|
|
153
|
+
return `spfn_csrf${getCookieSuffix()}`;
|
|
118
154
|
}
|
|
119
155
|
};
|
|
120
156
|
function parseDuration(duration) {
|
|
@@ -151,7 +187,7 @@ function getSessionTtl(override) {
|
|
|
151
187
|
if (globalConfig.sessionTtl !== void 0) {
|
|
152
188
|
return parseDuration(globalConfig.sessionTtl);
|
|
153
189
|
}
|
|
154
|
-
const envTtl =
|
|
190
|
+
const envTtl = env3.SPFN_AUTH_SESSION_TTL;
|
|
155
191
|
if (envTtl) {
|
|
156
192
|
return parseDuration(envTtl);
|
|
157
193
|
}
|
|
@@ -159,7 +195,7 @@ function getSessionTtl(override) {
|
|
|
159
195
|
}
|
|
160
196
|
|
|
161
197
|
// src/nextjs/session-helpers.ts
|
|
162
|
-
import { env as
|
|
198
|
+
import { env as env4 } from "@spfn/auth/config";
|
|
163
199
|
import { logger } from "@spfn/core/logger";
|
|
164
200
|
async function saveSession(data, options) {
|
|
165
201
|
let maxAge;
|
|
@@ -177,6 +213,13 @@ async function saveSession(data, options) {
|
|
|
177
213
|
path: "/",
|
|
178
214
|
maxAge
|
|
179
215
|
});
|
|
216
|
+
cookieStore.set(COOKIE_NAMES.CSRF, await deriveCsrfToken(data.keyId), {
|
|
217
|
+
httpOnly: false,
|
|
218
|
+
secure: process.env.NODE_ENV === "production",
|
|
219
|
+
sameSite: "lax",
|
|
220
|
+
path: "/",
|
|
221
|
+
maxAge
|
|
222
|
+
});
|
|
180
223
|
}
|
|
181
224
|
async function getSession() {
|
|
182
225
|
const cookieStore = await cookies();
|
|
@@ -201,9 +244,10 @@ async function clearSession() {
|
|
|
201
244
|
const cookieStore = await cookies();
|
|
202
245
|
cookieStore.delete(COOKIE_NAMES.SESSION);
|
|
203
246
|
cookieStore.delete(COOKIE_NAMES.SESSION_KEY_ID);
|
|
247
|
+
cookieStore.delete(COOKIE_NAMES.CSRF);
|
|
204
248
|
}
|
|
205
249
|
async function getPendingSessionKey() {
|
|
206
|
-
const secret =
|
|
250
|
+
const secret = env4.SPFN_AUTH_SESSION_SECRET;
|
|
207
251
|
const encoder = new TextEncoder();
|
|
208
252
|
const data = encoder.encode(`oauth-pending:${secret}`);
|
|
209
253
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -358,7 +402,7 @@ async function RequirePermission({
|
|
|
358
402
|
// src/nextjs/oauth-handlers.ts
|
|
359
403
|
import { NextResponse } from "next/server";
|
|
360
404
|
import { cookies as cookies2 } from "next/headers.js";
|
|
361
|
-
import { env as
|
|
405
|
+
import { env as env5 } from "@spfn/core/config";
|
|
362
406
|
import { logger as logger2 } from "@spfn/core/logger";
|
|
363
407
|
function createOAuthCallbackHandler(options) {
|
|
364
408
|
const defaultRedirect = options?.defaultRedirectUrl || "/";
|
|
@@ -401,14 +445,21 @@ function createOAuthCallbackHandler(options) {
|
|
|
401
445
|
const response = NextResponse.redirect(redirectUrl);
|
|
402
446
|
response.cookies.set(COOKIE_NAMES.SESSION, sessionToken, {
|
|
403
447
|
httpOnly: true,
|
|
404
|
-
secure:
|
|
448
|
+
secure: env5.NODE_ENV === "production",
|
|
405
449
|
sameSite: "lax",
|
|
406
450
|
maxAge: ttl,
|
|
407
451
|
path: "/"
|
|
408
452
|
});
|
|
409
453
|
response.cookies.set(COOKIE_NAMES.SESSION_KEY_ID, keyId, {
|
|
410
454
|
httpOnly: true,
|
|
411
|
-
secure:
|
|
455
|
+
secure: env5.NODE_ENV === "production",
|
|
456
|
+
sameSite: "lax",
|
|
457
|
+
maxAge: ttl,
|
|
458
|
+
path: "/"
|
|
459
|
+
});
|
|
460
|
+
response.cookies.set(COOKIE_NAMES.CSRF, await deriveCsrfToken(keyId), {
|
|
461
|
+
httpOnly: false,
|
|
462
|
+
secure: env5.NODE_ENV === "production",
|
|
412
463
|
sameSite: "lax",
|
|
413
464
|
maxAge: ttl,
|
|
414
465
|
path: "/"
|
|
@@ -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';\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 /** Password-setup session for verified-email signup — temporary, single-purpose */\n get SIGNUP_SETUP()\n {\n return `spfn_signup_setup${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"]}
|
|
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/csrf.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 { deriveCsrfToken } from '../server/lib/csrf';\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 // Readable companion: the client mirrors it into x-spfn-csrf, and the proxy\n // refuses cookie-session mutations that arrive without it. A session saved\n // here without one would be a session that cannot mutate anything.\n cookieStore.set(COOKIE_NAMES.CSRF, await deriveCsrfToken(data.keyId), {\n httpOnly: false,\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 cookieStore.delete(COOKIE_NAMES.CSRF);\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 csrf: rootLogger.child('@spfn/auth:interceptor:csrf'),\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 - CSRF token derivation\n *\n * The token is an HMAC of the session's key id under a subkey derived from the\n * session secret. Two properties follow from that shape:\n *\n * - The proxy recomputes it from the session it just unsealed, so a value an\n * attacker planted in the readable cookie (sibling-subdomain cookie tossing)\n * never verifies. Nothing here compares a cookie against a header.\n * - It is bound to the key id, so rotating the session key invalidates it.\n *\n * No new secret: the subkey is a labelled HMAC of SPFN_AUTH_SESSION_SECRET, so\n * the key that encrypts sessions is never used verbatim as the token key.\n */\n\nimport { env } from '@spfn/auth/config';\n\n/** Header the readable CSRF cookie is mirrored into by the client. */\nexport const CSRF_HEADER = 'x-spfn-csrf';\n\n/** Domain-separation label for the CSRF subkey. */\nconst CSRF_SUBKEY_LABEL = 'spfn-auth-csrf-token-v1';\n\n/**\n * Upper bound on candidate values accepted in one header.\n *\n * Deliberately generous, and must stay in step with MAX_CANDIDATES in\n * @spfn/core's client: verification recomputes the expected value once and then\n * only compares fixed-length strings, so an extra candidate costs a few hundred\n * nanoseconds, while a candidate dropped below this line is a user locked out of\n * every mutation. A low cap is not a security control either — accepting a match\n * among many is no weaker than accepting one, because every candidate still has\n * to equal a value recomputed from the session.\n */\nconst MAX_CANDIDATES = 32;\n\n/**\n * Resolve the session secret, refusing to derive anything without one.\n *\n * `env` throws on a missing required variable, but returns undefined when\n * SKIP_ENV_VALIDATION is set — and hashing `undefined` would yield a token every\n * deployment could compute. Fail closed instead.\n */\nfunction sessionSecret(): string\n{\n const secret = env.SPFN_AUTH_SESSION_SECRET;\n\n if (!secret)\n {\n throw new Error(\n 'SPFN_AUTH_SESSION_SECRET is required for CSRF protection. '\n + 'Set it (sessions need it anyway), or set SPFN_AUTH_CSRF=off.',\n );\n }\n\n return secret;\n}\n\n/**\n * HMAC-SHA256 over Web Crypto, so this works in the Edge runtime too.\n */\nasync function hmacSha256(key: Uint8Array, message: string): Promise<Uint8Array>\n{\n const cryptoKey = await crypto.subtle.importKey(\n 'raw',\n key.buffer as ArrayBuffer,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const signature = await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(message));\n\n return new Uint8Array(signature);\n}\n\nfunction toHex(bytes: Uint8Array): string\n{\n return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * Derive the CSRF token for a session key id.\n *\n * @param keyId - Session key id (`SessionData.keyId`)\n * @returns 64-char hex token — safe to put in a readable cookie, it reveals\n * neither the secret nor the key id\n */\nexport async function deriveCsrfToken(keyId: string): Promise<string>\n{\n const subkey = await hmacSha256(new TextEncoder().encode(sessionSecret()), CSRF_SUBKEY_LABEL);\n\n return toHex(await hmacSha256(subkey, keyId));\n}\n\n/**\n * Constant-time string comparison.\n *\n * Named for the string it takes, so it does not collide with node's Buffer-based\n * `timingSafeEqual` — which this package also uses, in the OAuth providers. It is\n * not re-exported from the package barrel for the same reason.\n *\n * Length is compared first and leaks only the length, which is fixed and public.\n */\nexport function timingSafeEqualString(a: string, b: string): boolean\n{\n if (a.length !== b.length)\n {\n return false;\n }\n\n let difference = 0;\n\n for (let i = 0; i < a.length; i++)\n {\n difference |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n\n return difference === 0;\n}\n\n/**\n * Whether a presented header value carries the expected token.\n *\n * The header may carry several comma-separated candidates — a browser sees every\n * `spfn_csrf*` cookie set on the host and cannot tell which dev instance owns\n * which, nor which of two same-named cookies a sibling subdomain tossed in.\n *\n * Every candidate the client is allowed to send is checked — the cap here is the\n * one it selects against — so a genuine value is never evicted by tossed ones\n * that happen to sort ahead of it. A header longer than that can only come from a\n * client that ignored the shared bound, and its surplus is dropped. Accepting any\n * match is no weaker than accepting one: a candidate the attacker chose still has\n * to equal a value recomputed from the session.\n */\nexport function matchesCsrfToken(expected: string, presented: string | null | undefined): boolean\n{\n if (!presented)\n {\n return false;\n }\n\n return presented\n .split(',', MAX_CANDIDATES)\n .some((candidate) => timingSafeEqualString(expected, candidate.trim()));\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';\nimport { authLogger } from '../logger';\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 /** Password-setup session for verified-email signup — temporary, single-purpose */\n get SIGNUP_SETUP()\n {\n return `spfn_signup_setup${getCookieSuffix()}`;\n },\n /** CSRF token — the only cookie here the browser can read */\n get CSRF()\n {\n return `spfn_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 * How the Next.js proxy treats a cookie-authenticated mutation that arrives\n * without a valid CSRF header.\n *\n * - `off`: no check\n * - `warn`: allow it through, log one line per request that would be refused\n * - `enforce`: refuse it with 403\n */\nexport type CsrfMode = 'off' | 'warn' | 'enforce';\n\n/**\n * CSRF configuration for the Next.js proxy\n */\nexport interface AuthCsrfConfig\n{\n /**\n * @default 'warn' — an existing app gets signal before it gets breakage.\n * `SPFN_AUTH_CSRF` sets it when this is not; new apps scaffolded by\n * `spfn init` are given `enforce`.\n */\n mode?: CsrfMode;\n\n /**\n * Backend paths that skip the check, matched exactly.\n *\n * These are route paths as the backend sees them (`/webhooks/stripe`), not\n * `/api/rpc/...` URLs, with route params already substituted. Intended for\n * endpoints a browser session never calls — webhook receivers and the like.\n * A path listed here is unprotected for cookie callers too, so list only\n * endpoints that carry their own authentication.\n */\n exemptPaths?: string[];\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 * CSRF protection for cookie-session mutations, enforced in the Next.js proxy.\n *\n * @example\n * ```typescript\n * configureAuth({\n * csrf: { mode: 'enforce', exemptPaths: ['/webhooks/stripe'] },\n * });\n * ```\n */\n csrf?: AuthCsrfConfig;\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\nconst CSRF_MODES: CsrfMode[] = ['off', 'warn', 'enforce'];\n\n/** The typo notice is a property of the process, not of a request */\nlet unrecognizedCsrfModeReported = false;\n\n/**\n * Get the CSRF mode\n *\n * Priority:\n * 1. Global config (configureAuth)\n * 2. Environment variable (SPFN_AUTH_CSRF)\n * 3. Default ('warn')\n *\n * An unrecognized value is a typo in the one setting that turns the check on;\n * it resolves to `enforce` and says so, rather than quietly leaving mutations\n * unprotected. It says so once per process: this runs on every mutation, so a\n * per-call error would be pure repetition burying the rest of the log.\n */\nexport function getCsrfMode(): CsrfMode\n{\n const configured = globalConfig.csrf?.mode ?? env.SPFN_AUTH_CSRF;\n\n if (!configured)\n {\n return 'warn';\n }\n\n const normalized = String(configured).trim().toLowerCase() as CsrfMode;\n\n if (!CSRF_MODES.includes(normalized))\n {\n if (!unrecognizedCsrfModeReported)\n {\n unrecognizedCsrfModeReported = true;\n authLogger.interceptor.csrf.error(\n `Unrecognized CSRF mode \"${configured}\" — expected off | warn | enforce. Enforcing.`,\n );\n }\n\n return 'enforce';\n }\n\n return normalized;\n}\n\n/**\n * Get the paths exempted from the CSRF check (exact match, backend route paths)\n */\nexport function getCsrfExemptPaths(): string[]\n{\n return globalConfig.csrf?.exemptPaths ?? [];\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 { deriveCsrfToken } from '../server/lib/csrf';\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 // Readable CSRF cookie — the client mirrors it into x-spfn-csrf\n response.cookies.set(COOKIE_NAMES.CSRF, await deriveCsrfToken(keyId), {\n httpOnly: false,\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","env","env","redirect","Fragment","jsx","redirect","redirect","Fragment","jsx","redirect","cookies","env","logger","logger","cookies","env","error"]}
|