@korajs/auth 0.1.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/types.ts","../src/tokens/jwt.ts","../src/tokens/token-manager.ts","../src/provider/built-in/password-hash.ts","../src/provider/built-in/user-store.ts","../src/provider/built-in/auth-routes.ts","../src/provider/adapter.ts","../src/device/device-identity.ts"],"sourcesContent":["// @korajs/auth/server — Server-side public API\n// Every export here is a public API commitment. Be explicit.\n\n// === Auth Routes (built-in email/password provider) ===\nexport { BuiltInAuthRoutes } from './provider/built-in/auth-routes'\nexport type { AuthRoutesConfig, AuthRouteResponse } from './provider/built-in/auth-routes'\n\n// === Token Manager ===\nexport { TokenManager } from './tokens/token-manager'\nexport type { TokenManagerConfig } from './tokens/token-manager'\n\n// === JWT Utilities ===\nexport { encodeJwt, decodeJwt, verifyJwt, isExpired } from './tokens/jwt'\n\n// === Password Hashing ===\nexport { hashPassword, verifyPassword } from './provider/built-in/password-hash'\n\n// === User Store ===\nexport { InMemoryUserStore, DuplicateEmailError } from './provider/built-in/user-store'\nexport type { AuthUser, StoredUser, AuthDevice } from './provider/built-in/user-store'\n\n// === Provider Adapter ===\nexport { BuiltInProvider, AuthProviderError } from './provider/adapter'\nexport type { AuthProviderAdapter, SignUpParams, SignInParams } from './provider/adapter'\n\n// === Device Identity (verification on server) ===\nexport { verifyChallenge, computePublicKeyThumbprint } from './device/device-identity'\n","import { KoraError } from '@korajs/core'\n\n// ============================================================================\n// User types\n// ============================================================================\n\n/**\n * Authenticated user record.\n * Represents the public-facing user identity returned by sign-in and sign-up flows.\n */\nexport interface AuthUser {\n\t/** Unique user identifier (UUID v7) */\n\tid: string\n\t/** User's email address, used as the primary login credential */\n\temail: string\n\t/** Display name */\n\tname: string\n\t/** Timestamp of user creation (milliseconds since epoch) */\n\tcreatedAt: number\n\t/** Timestamp of last profile update (milliseconds since epoch) */\n\tupdatedAt: number\n}\n\n/**\n * A registered device that can operate on behalf of a user.\n * Each device has its own keypair for offline credential verification.\n */\nexport interface AuthDevice {\n\t/** Device identifier, same as the Kora nodeId for this device */\n\tid: string\n\t/** The user this device belongs to */\n\tuserId: string\n\t/** JWK-encoded public key for this device's keypair */\n\tpublicKey: string\n\t/** Human-readable device name (e.g., \"Chrome on MacBook\") */\n\tname: string\n\t/** Timestamp when the device was first registered (milliseconds since epoch) */\n\tregisteredAt: number\n\t/** Timestamp of last activity from this device (milliseconds since epoch) */\n\tlastSeenAt: number\n\t/** Whether this device is allowed to sync */\n\tstatus: DeviceStatus\n}\n\n/** Device status values */\nexport type DeviceStatus = 'active' | 'revoked'\n\n// ============================================================================\n// Token types\n// ============================================================================\n\n/** The three kinds of tokens issued by the auth system */\nexport type TokenType = 'access' | 'refresh' | 'device_credential'\n\n/**\n * Base payload present in all JWT tokens.\n * Fields follow standard JWT claim names.\n */\nexport interface TokenPayload {\n\t/** Subject: the user ID */\n\tsub: string\n\t/** Device ID that this token was issued to */\n\tdev: string\n\t/** Which kind of token this is */\n\ttype: TokenType\n\t/** Issued-at time (seconds since epoch, per JWT spec) */\n\tiat: number\n\t/** Expiration time (seconds since epoch, per JWT spec) */\n\texp: number\n}\n\n/**\n * Short-lived token used to authenticate API requests.\n * Typically lives for 15 minutes.\n */\nexport interface AccessTokenPayload extends TokenPayload {\n\ttype: 'access'\n}\n\n/**\n * Longer-lived token used to obtain new access tokens.\n * Typically lives for 90 days.\n */\nexport interface RefreshTokenPayload extends TokenPayload {\n\ttype: 'refresh'\n}\n\n/**\n * Offline credential stored on the device.\n * Allows the device to continue operating offline, with a mandatory\n * check-in deadline by which it must reconnect to remain authorized.\n */\nexport interface DeviceCredentialPayload extends TokenPayload {\n\ttype: 'device_credential'\n\t/** Device public key thumbprint, binds this credential to a specific device keypair */\n\tdpk: string\n\t/** Timestamp (seconds since epoch) by which the device must check in with the server */\n\tmustCheckinBy: number\n}\n\n/**\n * Bundle of tokens returned after successful authentication.\n */\nexport interface AuthTokens {\n\t/** Short-lived access token */\n\taccessToken: string\n\t/** Long-lived refresh token */\n\trefreshToken: string\n\t/** Optional device credential for offline operation */\n\tdeviceCredential?: string\n}\n\n// ============================================================================\n// Auth configuration\n// ============================================================================\n\n/** Supported auth provider types */\nexport type AuthProviderType = 'built-in' | 'custom'\n\n/** Unlock mechanism for encrypted local storage */\nexport type UnlockMethod = 'biometric' | 'passphrase' | 'both'\n\n/**\n * Encryption settings for locally stored auth credentials.\n */\nexport interface AuthEncryptionConfig {\n\t/** Whether local credential encryption is enabled */\n\tenabled: boolean\n\t/** How the user unlocks encrypted credentials */\n\tunlock: UnlockMethod\n\t/** Milliseconds of inactivity before the app auto-locks. Defaults to 15 minutes. */\n\tautoLockTimeout?: number\n}\n\n/**\n * Custom lifetimes for each token type.\n * All values are in milliseconds.\n */\nexport interface TokenLifetimeConfig {\n\t/** Access token lifetime in ms. Default: 15 minutes. */\n\taccess?: number\n\t/** Refresh token lifetime in ms. Default: 90 days. */\n\trefresh?: number\n\t/** Device credential lifetime in ms. Default: 90 days. */\n\tdeviceCredential?: number\n}\n\n/**\n * Configuration for the Kora auth system.\n * Passed to the auth initializer to control provider, encryption, and token behavior.\n */\nexport interface AuthConfig {\n\t/** Which auth provider to use */\n\tprovider: AuthProviderType\n\t/** Local encryption settings for stored credentials */\n\tencryption?: AuthEncryptionConfig\n\t/** Maximum time a device can operate offline without checking in (ms). Default: 30 days. */\n\tmaxOfflineDuration?: number\n\t/** Custom token lifetimes */\n\ttokenLifetimes?: TokenLifetimeConfig\n}\n\n// ============================================================================\n// Auth state\n// ============================================================================\n\n/** Possible authentication states */\nexport type AuthStatus = 'authenticated' | 'unauthenticated' | 'locked'\n\n/**\n * Current state of the auth system.\n * This is the value exposed to the UI layer for rendering auth-dependent views.\n */\nexport interface AuthState {\n\t/** Current authentication status */\n\tstatus: AuthStatus\n\t/** The authenticated user, or null if unauthenticated/locked */\n\tuser: AuthUser | null\n\t/** The current device's ID, or null if not yet registered */\n\tdeviceId: string | null\n}\n\n// ============================================================================\n// Auth events\n// ============================================================================\n\n/**\n * Events emitted by the auth system.\n * These integrate with Kora's event system for DevTools observability.\n */\nexport type AuthEvent =\n\t| { type: 'auth:signed-in'; user: AuthUser }\n\t| { type: 'auth:signed-out' }\n\t| { type: 'auth:locked' }\n\t| { type: 'auth:unlocked'; user: AuthUser }\n\t| { type: 'auth:token-refreshed' }\n\t| { type: 'auth:device-revoked'; deviceId: string }\n\t| { type: 'auth:permission-changed' }\n\n/** Extract the event type string union from AuthEvent */\nexport type AuthEventType = AuthEvent['type']\n\n/** Extract a specific auth event by its type */\nexport type AuthEventByType<T extends AuthEventType> = Extract<AuthEvent, { type: T }>\n\n// ============================================================================\n// Auth errors\n// ============================================================================\n\n/**\n * Base error class for authentication-related failures.\n * Extends KoraError to integrate with the framework's error handling patterns.\n */\nexport class AuthError extends KoraError {\n\tconstructor(message: string, code: string, context?: Record<string, unknown>) {\n\t\tsuper(message, code, context)\n\t\tthis.name = 'AuthError'\n\t}\n}\n\n/**\n * Thrown when sign-in credentials are invalid (wrong email or password).\n */\nexport class InvalidCredentialsError extends AuthError {\n\tconstructor() {\n\t\tsuper('Invalid email or password.', 'AUTH_INVALID_CREDENTIALS')\n\t\tthis.name = 'InvalidCredentialsError'\n\t}\n}\n\n/**\n * Thrown when a user tries to sign up with an email that already exists.\n */\nexport class EmailAlreadyExistsError extends AuthError {\n\tconstructor(email: string) {\n\t\tsuper(\n\t\t\t`An account with email \"${email}\" already exists.`,\n\t\t\t'AUTH_EMAIL_EXISTS',\n\t\t\t{ email },\n\t\t)\n\t\tthis.name = 'EmailAlreadyExistsError'\n\t}\n}\n\n/**\n * Thrown when a token is expired, malformed, or has an invalid signature.\n */\nexport class TokenError extends AuthError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'AUTH_TOKEN_ERROR', context)\n\t\tthis.name = 'TokenError'\n\t}\n}\n\n/**\n * Thrown when a device's credential has expired or the device has been revoked.\n */\nexport class DeviceRevokedError extends AuthError {\n\tconstructor(deviceId: string) {\n\t\tsuper(\n\t\t\t`Device \"${deviceId}\" has been revoked and can no longer sync.`,\n\t\t\t'AUTH_DEVICE_REVOKED',\n\t\t\t{ deviceId },\n\t\t)\n\t\tthis.name = 'DeviceRevokedError'\n\t}\n}\n\n/**\n * Thrown when the maximum offline duration has been exceeded\n * and the device must reconnect to continue operating.\n */\nexport class OfflineExpiredError extends AuthError {\n\tconstructor(maxDuration: number, lastCheckin: number) {\n\t\tconst daysSinceCheckin = Math.round((Date.now() - lastCheckin) / (24 * 60 * 60 * 1000))\n\t\tsuper(\n\t\t\t`Device has been offline for ${daysSinceCheckin} days, exceeding the maximum offline duration. Reconnect to re-authenticate.`,\n\t\t\t'AUTH_OFFLINE_EXPIRED',\n\t\t\t{ maxDuration, lastCheckin, daysSinceCheckin },\n\t\t)\n\t\tthis.name = 'OfflineExpiredError'\n\t}\n}\n\n// ============================================================================\n// Sign up / sign in params\n// ============================================================================\n\n/**\n * Parameters for creating a new user account.\n */\nexport interface SignUpParams {\n\t/** Email address (used as login credential) */\n\temail: string\n\t/** Password (will be hashed before storage) */\n\tpassword: string\n\t/** Optional display name. Defaults to the local part of the email if omitted. */\n\tname?: string\n}\n\n/**\n * Parameters for signing in to an existing account.\n */\nexport interface SignInParams {\n\t/** Email address */\n\temail: string\n\t/** Password */\n\tpassword: string\n}\n\n// ============================================================================\n// Server-side types for the built-in provider\n// ============================================================================\n\n/**\n * Parameters for creating a user record in the server-side store.\n * The password has already been hashed by the time this is used.\n */\nexport interface CreateUserParams {\n\t/** Email address */\n\temail: string\n\t/** Argon2id or bcrypt hash of the password */\n\tpasswordHash: string\n\t/** Random salt used during hashing */\n\tsalt: string\n\t/** Display name */\n\tname: string\n}\n\n/**\n * Full user record as stored on the server, including sensitive credential fields.\n * This type must NEVER be returned to the client; strip passwordHash and salt first.\n */\nexport interface StoredUser extends AuthUser {\n\t/** Hashed password */\n\tpasswordHash: string\n\t/** Salt used for hashing */\n\tsalt: string\n}\n\n// ============================================================================\n// Auth provider adapter interface\n// ============================================================================\n\n/**\n * Adapter interface for pluggable auth providers.\n * Implement this to integrate Kora auth with a custom identity provider\n * (e.g., Firebase Auth, Auth0, Supabase Auth).\n *\n * The built-in provider implements this interface internally.\n */\nexport interface AuthProviderAdapter {\n\t/** Register a new user and return the user with initial tokens */\n\tsignUp(params: SignUpParams): Promise<{ user: AuthUser; tokens: AuthTokens }>\n\t/** Authenticate an existing user and return the user with tokens */\n\tsignIn(params: SignInParams): Promise<{ user: AuthUser; tokens: AuthTokens }>\n\t/** Exchange a refresh token for a new token set */\n\trefreshToken(refreshToken: string): Promise<AuthTokens>\n\t/** Validate an access token and return its payload, or null if invalid */\n\tvalidateAccessToken(token: string): Promise<TokenPayload | null>\n\t/** Revoke a device, preventing it from syncing */\n\trevokeDevice(deviceId: string): Promise<void>\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Default access token lifetime: 15 minutes */\nexport const DEFAULT_ACCESS_TOKEN_LIFETIME = 15 * 60 * 1000\n\n/** Default refresh token lifetime: 90 days */\nexport const DEFAULT_REFRESH_TOKEN_LIFETIME = 90 * 24 * 60 * 60 * 1000\n\n/** Default device credential lifetime: 90 days */\nexport const DEFAULT_DEVICE_CREDENTIAL_LIFETIME = 90 * 24 * 60 * 60 * 1000\n\n/** Default maximum offline duration: 30 days */\nexport const DEFAULT_MAX_OFFLINE_DURATION = 30 * 24 * 60 * 60 * 1000\n\n/** Default auto-lock timeout: 15 minutes */\nexport const DEFAULT_AUTO_LOCK_TIMEOUT = 15 * 60 * 1000\n","import { createHmac } from 'node:crypto'\n\n// ============================================================================\n// Base64url helpers\n// ============================================================================\n\n/**\n * Encodes a UTF-8 string to base64url format (RFC 7515).\n * Base64url uses the URL-safe alphabet (- instead of +, _ instead of /)\n * and strips trailing padding characters.\n *\n * @param input - The UTF-8 string to encode\n * @returns Base64url-encoded string\n */\nexport function base64urlEncode(input: string): string {\n\treturn Buffer.from(input, 'utf-8').toString('base64url')\n}\n\n/**\n * Decodes a base64url-encoded string back to UTF-8.\n *\n * @param input - The base64url-encoded string to decode\n * @returns Decoded UTF-8 string\n */\nexport function base64urlDecode(input: string): string {\n\treturn Buffer.from(input, 'base64url').toString('utf-8')\n}\n\n// ============================================================================\n// Internal signing\n// ============================================================================\n\n/**\n * Computes an HMAC-SHA256 signature and returns it as a base64url string.\n * Uses Node.js crypto module for synchronous signing.\n */\nfunction hmacSha256Base64url(data: string, secret: string): string {\n\treturn createHmac('sha256', secret).update(data).digest('base64url')\n}\n\n// ============================================================================\n// JWT operations\n// ============================================================================\n\n/** Pre-encoded JWT header. Only HS256 is supported; the header never changes. */\nconst ENCODED_HEADER = base64urlEncode(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))\n\n/**\n * Creates a signed JWT (HS256) from a payload and secret.\n *\n * The token is structured as `header.payload.signature` per RFC 7519.\n * Only HMAC-SHA256 is supported. The header is always `{\"alg\":\"HS256\",\"typ\":\"JWT\"}`.\n *\n * @param payload - The claims to include in the token. Must be JSON-serializable.\n * @param secret - The HMAC-SHA256 secret key used for signing.\n * @returns A signed JWT string in the format `header.payload.signature`\n *\n * @example\n * ```typescript\n * const token = encodeJwt(\n * { sub: 'user-123', exp: Math.floor(Date.now() / 1000) + 900 },\n * 'my-secret'\n * )\n * ```\n */\nexport function encodeJwt(payload: Record<string, unknown>, secret: string): string {\n\tconst encodedPayload = base64urlEncode(JSON.stringify(payload))\n\tconst signingInput = `${ENCODED_HEADER}.${encodedPayload}`\n\tconst signature = hmacSha256Base64url(signingInput, secret)\n\treturn `${signingInput}.${signature}`\n}\n\n/**\n * Decodes a JWT without verifying its signature.\n *\n * Use this only when you need to inspect claims (e.g., reading `exp` to decide\n * whether to attempt a refresh) and do NOT need to trust the token's authenticity.\n * For trusted reads, use {@link verifyJwt} instead.\n *\n * @param token - The JWT string to decode\n * @returns The decoded payload as a record, or null if the token is malformed\n *\n * @example\n * ```typescript\n * const claims = decodeJwt(token)\n * if (claims && typeof claims.sub === 'string') {\n * console.log('User:', claims.sub)\n * }\n * ```\n */\nexport function decodeJwt(token: string): Record<string, unknown> | null {\n\tconst parts = token.split('.')\n\tif (parts.length !== 3) {\n\t\treturn null\n\t}\n\n\tconst payloadSegment = parts[1]\n\tif (payloadSegment === undefined) {\n\t\treturn null\n\t}\n\n\ttry {\n\t\tconst decoded = base64urlDecode(payloadSegment)\n\t\tconst parsed: unknown = JSON.parse(decoded)\n\t\tif (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n\t\t\treturn null\n\t\t}\n\t\treturn parsed as Record<string, unknown>\n\t} catch {\n\t\treturn null\n\t}\n}\n\n/**\n * Decodes a JWT and verifies its HMAC-SHA256 signature.\n *\n * Returns the payload only if the signature is valid. Returns null if the token\n * is malformed, has an invalid signature, or cannot be parsed.\n *\n * This function does NOT check expiration. Use {@link isExpired} separately\n * to check the `exp` claim, allowing callers to distinguish between\n * \"invalid signature\" (security issue) and \"expired\" (normal lifecycle).\n *\n * @param token - The JWT string to verify\n * @param secret - The HMAC-SHA256 secret key that was used to sign the token\n * @returns The decoded payload if the signature is valid, or null otherwise\n *\n * @example\n * ```typescript\n * const claims = verifyJwt(token, 'my-secret')\n * if (claims === null) {\n * throw new Error('Invalid token signature')\n * }\n * if (isExpired(claims)) {\n * throw new Error('Token has expired')\n * }\n * ```\n */\nexport function verifyJwt(token: string, secret: string): Record<string, unknown> | null {\n\tconst parts = token.split('.')\n\tif (parts.length !== 3) {\n\t\treturn null\n\t}\n\n\tconst headerSegment = parts[0]\n\tconst payloadSegment = parts[1]\n\tconst signatureSegment = parts[2]\n\n\tif (\n\t\theaderSegment === undefined\n\t\t|| payloadSegment === undefined\n\t\t|| signatureSegment === undefined\n\t) {\n\t\treturn null\n\t}\n\n\t// Recompute signature and compare\n\tconst signingInput = `${headerSegment}.${payloadSegment}`\n\tconst expectedSignature = hmacSha256Base64url(signingInput, secret)\n\n\t// Constant-time comparison to prevent timing attacks.\n\t// Both strings are base64url-encoded HMAC outputs, so they are ASCII-safe\n\t// and we can compare byte-by-byte without early exit.\n\tif (expectedSignature.length !== signatureSegment.length) {\n\t\treturn null\n\t}\n\tlet mismatch = 0\n\tfor (let i = 0; i < expectedSignature.length; i++) {\n\t\t// Bitwise OR accumulates differences without short-circuiting\n\t\tmismatch |= expectedSignature.charCodeAt(i) ^ signatureSegment.charCodeAt(i)\n\t}\n\tif (mismatch !== 0) {\n\t\treturn null\n\t}\n\n\t// Decode the payload\n\ttry {\n\t\tconst decoded = base64urlDecode(payloadSegment)\n\t\tconst parsed: unknown = JSON.parse(decoded)\n\t\tif (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n\t\t\treturn null\n\t\t}\n\t\treturn parsed as Record<string, unknown>\n\t} catch {\n\t\treturn null\n\t}\n}\n\n/**\n * Checks whether a token payload has expired based on its `exp` claim.\n *\n * The `exp` claim is expected to be in seconds since the Unix epoch (per JWT spec).\n * If the `exp` claim is missing or is not a number, the token is considered\n * non-expiring and this function returns false.\n *\n * @param payload - An object with an optional `exp` field (seconds since epoch)\n * @returns true if the token has expired, false otherwise\n *\n * @example\n * ```typescript\n * const claims = verifyJwt(token, secret)\n * if (claims && isExpired(claims)) {\n * // Token signature is valid but it has expired -- attempt refresh\n * }\n * ```\n */\nexport function isExpired(payload: { exp?: number }): boolean {\n\tif (typeof payload.exp !== 'number') {\n\t\treturn false\n\t}\n\tconst nowSeconds = Math.floor(Date.now() / 1000)\n\treturn nowSeconds >= payload.exp\n}\n","import type {\n\tAuthTokens,\n\tDeviceCredentialPayload,\n\tTokenPayload,\n} from '../types'\nimport {\n\tDEFAULT_ACCESS_TOKEN_LIFETIME,\n\tDEFAULT_DEVICE_CREDENTIAL_LIFETIME,\n\tDEFAULT_REFRESH_TOKEN_LIFETIME,\n} from '../types'\nimport { encodeJwt, isExpired, verifyJwt } from './jwt'\n\n/**\n * Configuration for the server-side TokenManager.\n */\nexport interface TokenManagerConfig {\n\t/** Secret key for signing JWTs (HMAC-SHA256) */\n\tsecret: string\n\n\t/** Access token lifetime in milliseconds (default: 15 minutes) */\n\taccessTokenLifetime?: number\n\n\t/** Refresh token lifetime in milliseconds (default: 90 days) */\n\trefreshTokenLifetime?: number\n\n\t/** Device credential lifetime in milliseconds (default: 90 days) */\n\tdeviceCredentialLifetime?: number\n}\n\n/**\n * Server-side token manager responsible for issuing, refreshing, and validating\n * Kora authentication tokens.\n *\n * Uses HMAC-SHA256 signed JWTs. Supports three token types:\n * - **Access tokens**: Short-lived tokens for API authorization (default: 15 min)\n * - **Refresh tokens**: Longer-lived tokens for obtaining new access tokens (default: 90 days)\n * - **Device credentials**: Long-lived credentials bound to a device key pair (default: 90 days)\n *\n * @example\n * ```typescript\n * const tokenManager = new TokenManager({ secret: 'my-signing-secret' })\n *\n * // Issue all tokens at once\n * const tokens = tokenManager.issueTokens('user-123', 'device-456')\n *\n * // Validate an access token\n * const payload = tokenManager.validateToken(tokens.accessToken)\n *\n * // Refresh when the access token expires\n * const newTokens = tokenManager.refreshAccessToken(tokens.refreshToken)\n * ```\n */\nexport class TokenManager {\n\tprivate readonly secret: string\n\tprivate readonly accessTokenLifetime: number\n\tprivate readonly refreshTokenLifetime: number\n\tprivate readonly deviceCredentialLifetime: number\n\n\tconstructor(config: TokenManagerConfig) {\n\t\tthis.secret = config.secret\n\t\tthis.accessTokenLifetime = config.accessTokenLifetime ?? DEFAULT_ACCESS_TOKEN_LIFETIME\n\t\tthis.refreshTokenLifetime = config.refreshTokenLifetime ?? DEFAULT_REFRESH_TOKEN_LIFETIME\n\t\tthis.deviceCredentialLifetime =\n\t\t\tconfig.deviceCredentialLifetime ?? DEFAULT_DEVICE_CREDENTIAL_LIFETIME\n\t}\n\n\t/**\n\t * Issue a signed JWT access token.\n\t *\n\t * Access tokens are short-lived (default 15 minutes) and used to authorize\n\t * API requests. When expired, use {@link refreshAccessToken} with a valid\n\t * refresh token to obtain a new one.\n\t *\n\t * @param userId - The subject (user ID) to encode in the token\n\t * @param deviceId - The device ID of the requesting device\n\t * @returns A signed JWT string with type 'access'\n\t */\n\tissueAccessToken(userId: string, deviceId: string): string {\n\t\tconst nowSeconds = Math.floor(Date.now() / 1000)\n\t\tconst payload: TokenPayload = {\n\t\t\tsub: userId,\n\t\t\tdev: deviceId,\n\t\t\ttype: 'access',\n\t\t\tiat: nowSeconds,\n\t\t\texp: nowSeconds + Math.floor(this.accessTokenLifetime / 1000),\n\t\t}\n\t\treturn encodeJwt(payload as unknown as Record<string, unknown>, this.secret)\n\t}\n\n\t/**\n\t * Issue a signed JWT refresh token.\n\t *\n\t * Refresh tokens are longer-lived (default 90 days) and used exclusively\n\t * to obtain new access tokens via {@link refreshAccessToken}. They should\n\t * be stored securely and never sent to resource APIs.\n\t *\n\t * @param userId - The subject (user ID) to encode in the token\n\t * @param deviceId - The device ID of the requesting device\n\t * @returns A signed JWT string with type 'refresh'\n\t */\n\tissueRefreshToken(userId: string, deviceId: string): string {\n\t\tconst nowSeconds = Math.floor(Date.now() / 1000)\n\t\tconst payload: TokenPayload = {\n\t\t\tsub: userId,\n\t\t\tdev: deviceId,\n\t\t\ttype: 'refresh',\n\t\t\tiat: nowSeconds,\n\t\t\texp: nowSeconds + Math.floor(this.refreshTokenLifetime / 1000),\n\t\t}\n\t\treturn encodeJwt(payload as unknown as Record<string, unknown>, this.secret)\n\t}\n\n\t/**\n\t * Issue a signed device credential token.\n\t *\n\t * Device credentials are long-lived tokens bound to a device's public key.\n\t * They include a `mustCheckinBy` deadline; if the device does not check in\n\t * before this deadline, the credential should be treated as revoked.\n\t *\n\t * @param userId - The subject (user ID) to encode in the token\n\t * @param deviceId - The device ID of the requesting device\n\t * @param publicKeyThumbprint - SHA-256 thumbprint of the device's public key\n\t * @returns A signed JWT string with type 'device_credential'\n\t */\n\tissueDeviceCredential(\n\t\tuserId: string,\n\t\tdeviceId: string,\n\t\tpublicKeyThumbprint: string,\n\t): string {\n\t\tconst nowSeconds = Math.floor(Date.now() / 1000)\n\t\tconst lifetimeSeconds = Math.floor(this.deviceCredentialLifetime / 1000)\n\t\tconst payload: DeviceCredentialPayload = {\n\t\t\tsub: userId,\n\t\t\tdev: deviceId,\n\t\t\ttype: 'device_credential',\n\t\t\tiat: nowSeconds,\n\t\t\texp: nowSeconds + lifetimeSeconds,\n\t\t\tdpk: publicKeyThumbprint,\n\t\t\tmustCheckinBy: nowSeconds + lifetimeSeconds,\n\t\t}\n\t\treturn encodeJwt(payload as unknown as Record<string, unknown>, this.secret)\n\t}\n\n\t/**\n\t * Issue a complete set of authentication tokens.\n\t *\n\t * Always issues an access token and refresh token. If a `publicKeyThumbprint`\n\t * is provided, also issues a device credential.\n\t *\n\t * @param userId - The subject (user ID) to encode in the tokens\n\t * @param deviceId - The device ID of the requesting device\n\t * @param publicKeyThumbprint - Optional SHA-256 thumbprint of the device's public key.\n\t * When provided, a device credential is included in the returned tokens.\n\t * @returns An {@link AuthTokens} object containing the issued tokens\n\t */\n\tissueTokens(\n\t\tuserId: string,\n\t\tdeviceId: string,\n\t\tpublicKeyThumbprint?: string,\n\t): AuthTokens {\n\t\tconst tokens: AuthTokens = {\n\t\t\taccessToken: this.issueAccessToken(userId, deviceId),\n\t\t\trefreshToken: this.issueRefreshToken(userId, deviceId),\n\t\t}\n\n\t\tif (publicKeyThumbprint !== undefined) {\n\t\t\ttokens.deviceCredential = this.issueDeviceCredential(\n\t\t\t\tuserId,\n\t\t\t\tdeviceId,\n\t\t\t\tpublicKeyThumbprint,\n\t\t\t)\n\t\t}\n\n\t\treturn tokens\n\t}\n\n\t/**\n\t * Validate and decode a token.\n\t *\n\t * Verifies the HMAC-SHA256 signature and checks that the token has not expired.\n\t * Returns null (rather than throwing) for invalid or expired tokens, so callers\n\t * can handle authentication failure without try/catch.\n\t *\n\t * @param token - The JWT string to validate\n\t * @returns The decoded {@link TokenPayload} if valid, or null if the token is\n\t * invalid, expired, or missing required claims\n\t */\n\tvalidateToken(token: string): TokenPayload | null {\n\t\tconst decoded = verifyJwt(token, this.secret)\n\t\tif (decoded === null) {\n\t\t\treturn null\n\t\t}\n\n\t\t// verifyJwt validates the signature but not expiration;\n\t\t// check the exp claim separately\n\t\tif (isExpired(decoded as { exp?: number })) {\n\t\t\treturn null\n\t\t}\n\n\t\t// Validate that all required base claims are present and correctly typed\n\t\tif (\n\t\t\ttypeof decoded['sub'] !== 'string' ||\n\t\t\ttypeof decoded['dev'] !== 'string' ||\n\t\t\ttypeof decoded['type'] !== 'string' ||\n\t\t\ttypeof decoded['iat'] !== 'number' ||\n\t\t\ttypeof decoded['exp'] !== 'number'\n\t\t) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst type = decoded['type']\n\t\tif (type !== 'access' && type !== 'refresh' && type !== 'device_credential') {\n\t\t\treturn null\n\t\t}\n\n\t\treturn {\n\t\t\tsub: decoded['sub'] as string,\n\t\t\tdev: decoded['dev'] as string,\n\t\t\ttype,\n\t\t\tiat: decoded['iat'] as number,\n\t\t\texp: decoded['exp'] as number,\n\t\t}\n\t}\n\n\t/**\n\t * Refresh an access token using a valid refresh token.\n\t *\n\t * Implements **refresh token rotation**: a new refresh token is issued alongside\n\t * the new access token, and the old refresh token should be considered consumed.\n\t * This limits the window of vulnerability if a refresh token is compromised.\n\t *\n\t * Returns null if the provided token is invalid, expired, or not a refresh token.\n\t *\n\t * @param refreshToken - The refresh token JWT string\n\t * @returns A new access/refresh token pair, or null if the refresh token is invalid\n\t */\n\trefreshAccessToken(\n\t\trefreshToken: string,\n\t): { accessToken: string; refreshToken: string } | null {\n\t\tconst payload = this.validateToken(refreshToken)\n\n\t\tif (payload === null) {\n\t\t\treturn null\n\t\t}\n\n\t\t// Only refresh tokens can be used for token refresh.\n\t\t// Accepting access tokens or device credentials here would be a security hole.\n\t\tif (payload.type !== 'refresh') {\n\t\t\treturn null\n\t\t}\n\n\t\treturn {\n\t\t\taccessToken: this.issueAccessToken(payload.sub, payload.dev),\n\t\t\trefreshToken: this.issueRefreshToken(payload.sub, payload.dev),\n\t\t}\n\t}\n}\n","import { randomBytes, pbkdf2, timingSafeEqual } from 'node:crypto'\n\n/** Number of PBKDF2 iterations. 600,000 per OWASP 2023 recommendations for SHA-512. */\nconst PBKDF2_ITERATIONS = 600_000\n\n/** Digest algorithm used by PBKDF2. */\nconst PBKDF2_DIGEST = 'sha512'\n\n/** Length of the derived key in bytes. */\nconst KEY_LENGTH = 64\n\n/** Length of the random salt in bytes. */\nconst SALT_LENGTH = 32\n\ninterface HashResult {\n\t/** Hex-encoded PBKDF2 derived key. */\n\thash: string\n\t/** Hex-encoded random salt used during hashing. */\n\tsalt: string\n}\n\n/**\n * Hashes a password using PBKDF2 with a cryptographically random salt.\n *\n * Uses SHA-512 with 600,000 iterations and a 32-byte random salt,\n * producing a 64-byte derived key. Both the hash and salt are returned\n * as hex-encoded strings for storage.\n *\n * @param password - The plaintext password to hash\n * @returns The hex-encoded hash and salt\n *\n * @example\n * ```typescript\n * const { hash, salt } = await hashPassword('my-secret-password')\n * // Store hash and salt in the database\n * ```\n */\nexport async function hashPassword(password: string): Promise<HashResult> {\n\tconst salt = randomBytes(SALT_LENGTH)\n\n\tconst derivedKey = await pbkdf2Async(\n\t\tpassword,\n\t\tsalt,\n\t\tPBKDF2_ITERATIONS,\n\t\tKEY_LENGTH,\n\t\tPBKDF2_DIGEST,\n\t)\n\n\treturn {\n\t\thash: derivedKey.toString('hex'),\n\t\tsalt: salt.toString('hex'),\n\t}\n}\n\n/**\n * Verifies a plaintext password against a stored hash and salt using\n * timing-safe comparison to prevent timing attacks.\n *\n * Re-derives the key from the password and salt using the same PBKDF2\n * parameters, then compares the result against the stored hash using\n * `crypto.timingSafeEqual` to avoid leaking information through\n * response timing.\n *\n * @param password - The plaintext password to verify\n * @param hash - The hex-encoded hash to compare against\n * @param salt - The hex-encoded salt that was used to produce the hash\n * @returns `true` if the password matches, `false` otherwise\n *\n * @example\n * ```typescript\n * const isValid = await verifyPassword('my-secret-password', storedHash, storedSalt)\n * if (isValid) {\n * // Grant access\n * }\n * ```\n */\nexport async function verifyPassword(\n\tpassword: string,\n\thash: string,\n\tsalt: string,\n): Promise<boolean> {\n\tconst saltBuffer = Buffer.from(salt, 'hex')\n\n\tconst derivedKey = await pbkdf2Async(\n\t\tpassword,\n\t\tsaltBuffer,\n\t\tPBKDF2_ITERATIONS,\n\t\tKEY_LENGTH,\n\t\tPBKDF2_DIGEST,\n\t)\n\n\tconst hashBuffer = Buffer.from(hash, 'hex')\n\n\t// Both buffers must be the same length for timingSafeEqual.\n\t// If the stored hash has an unexpected length, reject rather than throw.\n\tif (derivedKey.length !== hashBuffer.length) {\n\t\treturn false\n\t}\n\n\treturn timingSafeEqual(derivedKey, hashBuffer)\n}\n\n/**\n * Promisified wrapper around Node.js `crypto.pbkdf2`.\n * The callback-based API delegates hashing to libuv's thread pool,\n * avoiding blocking the event loop during the 600,000 iterations.\n */\nfunction pbkdf2Async(\n\tpassword: string,\n\tsalt: Buffer,\n\titerations: number,\n\tkeyLength: number,\n\tdigest: string,\n): Promise<Buffer> {\n\treturn new Promise((resolve, reject) => {\n\t\tpbkdf2(password, salt, iterations, keyLength, digest, (err, derivedKey) => {\n\t\t\tif (err) {\n\t\t\t\treject(err)\n\t\t\t} else {\n\t\t\t\tresolve(derivedKey)\n\t\t\t}\n\t\t})\n\t})\n}\n","import { KoraError } from '@korajs/core'\nimport { randomUUID } from 'node:crypto'\n\n/**\n * A user as visible to the application layer.\n * Does not include sensitive fields like password hash or salt.\n */\nexport interface AuthUser {\n\t/** Unique user identifier (UUID v7 or crypto.randomUUID) */\n\tid: string\n\t/** User's email address */\n\temail: string\n\t/** User's display name */\n\tname: string\n\t/** Timestamp when the user was created (milliseconds since epoch) */\n\tcreatedAt: number\n}\n\n/**\n * Internal user record that includes credentials.\n * Extends AuthUser with password hash and salt for verification.\n */\nexport interface StoredUser extends AuthUser {\n\t/** Hex-encoded PBKDF2 derived key */\n\tpasswordHash: string\n\t/** Hex-encoded random salt used during hashing */\n\tsalt: string\n}\n\n/**\n * A device registered to a user.\n */\nexport interface AuthDevice {\n\t/** Unique device identifier */\n\tid: string\n\t/** ID of the user who owns this device */\n\tuserId: string\n\t/** Base64url-encoded public key (or thumbprint) for the device */\n\tpublicKey: string\n\t/** Human-readable device name */\n\tname: string\n\t/** Whether the device has been revoked */\n\trevoked: boolean\n\t/** Timestamp when the device was first registered (milliseconds since epoch) */\n\tcreatedAt: number\n\t/** Timestamp when the device was last seen (milliseconds since epoch) */\n\tlastSeenAt: number\n}\n\n/**\n * Thrown when a user account already exists with the given email.\n */\nexport class DuplicateEmailError extends KoraError {\n\tconstructor(email: string) {\n\t\tsuper(\n\t\t\t`A user with email \"${email}\" already exists. Use a different email address or sign in to the existing account.`,\n\t\t\t'DUPLICATE_EMAIL',\n\t\t\t{ email },\n\t\t)\n\t\tthis.name = 'DuplicateEmailError'\n\t}\n}\n\n/**\n * In-memory user and device store for the built-in auth provider.\n *\n * This is a simple implementation suitable for development and testing.\n * Production applications should replace this with a database-backed store\n * implementing the same interface.\n *\n * All methods are async to match the interface that a real database store\n * would expose, even though the in-memory operations are synchronous.\n *\n * @example\n * ```typescript\n * const store = new InMemoryUserStore()\n * const user = await store.createUser({\n * email: 'alice@example.com',\n * passwordHash: 'abc123...',\n * salt: 'def456...',\n * name: 'Alice',\n * })\n * ```\n */\nexport class InMemoryUserStore {\n\t/** Users indexed by ID */\n\tprivate readonly usersById = new Map<string, StoredUser>()\n\n\t/** Users indexed by email (lowercase) for fast lookup */\n\tprivate readonly usersByEmail = new Map<string, StoredUser>()\n\n\t/** Devices indexed by device ID */\n\tprivate readonly devicesById = new Map<string, AuthDevice>()\n\n\t/** Device IDs indexed by user ID for fast listing */\n\tprivate readonly devicesByUserId = new Map<string, Set<string>>()\n\n\t/**\n\t * Create a new user account.\n\t *\n\t * @param params - User creation parameters\n\t * @param params.email - The user's email address (must be unique, case-insensitive)\n\t * @param params.passwordHash - Hex-encoded PBKDF2 derived key\n\t * @param params.salt - Hex-encoded salt used during hashing\n\t * @param params.name - The user's display name\n\t * @returns The created user (without sensitive credential fields)\n\t * @throws {DuplicateEmailError} If a user with the same email already exists\n\t */\n\tasync createUser(params: {\n\t\temail: string\n\t\tpasswordHash: string\n\t\tsalt: string\n\t\tname: string\n\t}): Promise<AuthUser> {\n\t\tconst normalizedEmail = params.email.toLowerCase()\n\n\t\tif (this.usersByEmail.has(normalizedEmail)) {\n\t\t\tthrow new DuplicateEmailError(params.email)\n\t\t}\n\n\t\tconst now = Date.now()\n\t\tconst id = randomUUID()\n\n\t\tconst storedUser: StoredUser = {\n\t\t\tid,\n\t\t\temail: normalizedEmail,\n\t\t\tname: params.name,\n\t\t\tcreatedAt: now,\n\t\t\tpasswordHash: params.passwordHash,\n\t\t\tsalt: params.salt,\n\t\t}\n\n\t\tthis.usersById.set(id, storedUser)\n\t\tthis.usersByEmail.set(normalizedEmail, storedUser)\n\n\t\treturn toAuthUser(storedUser)\n\t}\n\n\t/**\n\t * Find a user by email address.\n\t *\n\t * @param email - The email to search for (case-insensitive)\n\t * @returns The stored user record including credentials, or null if not found\n\t */\n\tasync findByEmail(email: string): Promise<StoredUser | null> {\n\t\treturn this.usersByEmail.get(email.toLowerCase()) ?? null\n\t}\n\n\t/**\n\t * Find a user by ID.\n\t *\n\t * @param id - The user ID to search for\n\t * @returns The stored user record including credentials, or null if not found\n\t */\n\tasync findById(id: string): Promise<StoredUser | null> {\n\t\treturn this.usersById.get(id) ?? null\n\t}\n\n\t/**\n\t * Register a device for a user.\n\t *\n\t * If a device with the same ID already exists and is not revoked, it is\n\t * returned as-is (idempotent registration). If it was previously revoked,\n\t * it is re-activated with updated details.\n\t *\n\t * @param params - Device registration parameters\n\t * @param params.id - Unique device identifier\n\t * @param params.userId - ID of the user who owns the device\n\t * @param params.publicKey - Base64url-encoded device public key or thumbprint\n\t * @param params.name - Human-readable device name\n\t * @returns The registered device record\n\t */\n\tasync registerDevice(params: {\n\t\tid: string\n\t\tuserId: string\n\t\tpublicKey: string\n\t\tname: string\n\t}): Promise<AuthDevice> {\n\t\tconst existing = this.devicesById.get(params.id)\n\t\tif (existing !== undefined && !existing.revoked) {\n\t\t\treturn existing\n\t\t}\n\n\t\tconst now = Date.now()\n\t\tconst device: AuthDevice = {\n\t\t\tid: params.id,\n\t\t\tuserId: params.userId,\n\t\t\tpublicKey: params.publicKey,\n\t\t\tname: params.name,\n\t\t\trevoked: false,\n\t\t\tcreatedAt: now,\n\t\t\tlastSeenAt: now,\n\t\t}\n\n\t\tthis.devicesById.set(params.id, device)\n\n\t\tlet userDevices = this.devicesByUserId.get(params.userId)\n\t\tif (userDevices === undefined) {\n\t\t\tuserDevices = new Set()\n\t\t\tthis.devicesByUserId.set(params.userId, userDevices)\n\t\t}\n\t\tuserDevices.add(params.id)\n\n\t\treturn device\n\t}\n\n\t/**\n\t * Find a device by its ID.\n\t *\n\t * @param deviceId - The device ID to search for\n\t * @returns The device record, or null if not found\n\t */\n\tasync findDevice(deviceId: string): Promise<AuthDevice | null> {\n\t\treturn this.devicesById.get(deviceId) ?? null\n\t}\n\n\t/**\n\t * List all devices registered for a user.\n\t *\n\t * @param userId - The user ID whose devices to list\n\t * @returns Array of device records (includes revoked devices)\n\t */\n\tasync listDevices(userId: string): Promise<AuthDevice[]> {\n\t\tconst deviceIds = this.devicesByUserId.get(userId)\n\t\tif (deviceIds === undefined) {\n\t\t\treturn []\n\t\t}\n\n\t\tconst devices: AuthDevice[] = []\n\t\tfor (const deviceId of deviceIds) {\n\t\t\tconst device = this.devicesById.get(deviceId)\n\t\t\tif (device !== undefined) {\n\t\t\t\tdevices.push(device)\n\t\t\t}\n\t\t}\n\n\t\treturn devices\n\t}\n\n\t/**\n\t * Revoke a device, preventing it from being used for authentication.\n\t *\n\t * This is a soft revoke — the device record remains but is marked as revoked.\n\t * If the device does not exist, this is a no-op.\n\t *\n\t * @param deviceId - The ID of the device to revoke\n\t */\n\tasync revokeDevice(deviceId: string): Promise<void> {\n\t\tconst device = this.devicesById.get(deviceId)\n\t\tif (device !== undefined) {\n\t\t\tdevice.revoked = true\n\t\t}\n\t}\n\n\t/**\n\t * Update the last-seen timestamp for a device.\n\t *\n\t * Called when a device authenticates or syncs to track activity.\n\t * If the device does not exist, this is a no-op.\n\t *\n\t * @param deviceId - The ID of the device to update\n\t */\n\tasync touchDevice(deviceId: string): Promise<void> {\n\t\tconst device = this.devicesById.get(deviceId)\n\t\tif (device !== undefined) {\n\t\t\tdevice.lastSeenAt = Date.now()\n\t\t}\n\t}\n}\n\n/**\n * Strip sensitive fields from a StoredUser to produce an AuthUser.\n * Ensures password hash and salt are never leaked to the application layer.\n */\nfunction toAuthUser(stored: StoredUser): AuthUser {\n\treturn {\n\t\tid: stored.id,\n\t\temail: stored.email,\n\t\tname: stored.name,\n\t\tcreatedAt: stored.createdAt,\n\t}\n}\n","import type { AuthTokens } from '../../types'\nimport { TokenManager } from '../../tokens/token-manager'\nimport { hashPassword, verifyPassword } from './password-hash'\nimport {\n\tInMemoryUserStore,\n\ttype AuthUser,\n\ttype AuthDevice,\n} from './user-store'\n\n/**\n * Configuration for building the built-in auth routes.\n */\nexport interface AuthRoutesConfig {\n\t/** The user/device store backing the auth routes */\n\tuserStore: InMemoryUserStore\n\t/** The token manager for issuing and validating JWTs */\n\ttokenManager: TokenManager\n}\n\n/**\n * Response envelope returned by all auth route handlers.\n *\n * Successful responses include a `data` field; failures include an `error` string.\n * The `status` field maps directly to an HTTP status code.\n */\nexport interface AuthRouteResponse<T> {\n\t/** HTTP status code */\n\tstatus: number\n\t/** Either the success payload or an error message */\n\tbody: { data: T } | { error: string }\n}\n\n/** Minimum password length enforced at sign-up. */\nconst MIN_PASSWORD_LENGTH = 8\n\n/**\n * Simple email format validation.\n * Checks for the presence of exactly one @ with non-empty local and domain parts,\n * and at least one dot in the domain. This is intentionally lenient — real email\n * validation happens by sending a confirmation email, not by regex.\n */\nfunction isValidEmail(email: string): boolean {\n\tif (email.length === 0 || email.length > 254) {\n\t\treturn false\n\t}\n\tconst atIndex = email.indexOf('@')\n\tif (atIndex < 1) {\n\t\treturn false\n\t}\n\tconst domain = email.slice(atIndex + 1)\n\tif (domain.length === 0 || !domain.includes('.')) {\n\t\treturn false\n\t}\n\t// No double-@ or spaces\n\tif (email.indexOf('@', atIndex + 1) !== -1) {\n\t\treturn false\n\t}\n\tif (email.includes(' ')) {\n\t\treturn false\n\t}\n\treturn true\n}\n\n/**\n * HTTP route handlers for the built-in Kora auth provider.\n *\n * These are framework-agnostic functions that accept parsed request bodies and return\n * structured responses. The server package is responsible for wiring them into its\n * HTTP server (e.g., mapping `POST /auth/signup` to `handleSignUp`).\n *\n * All handlers follow the same pattern:\n * - Validate input\n * - Perform the operation\n * - Return `{ status, body: { data } }` on success\n * - Return `{ status, body: { error } }` on failure\n *\n * @example\n * ```typescript\n * const routes = new BuiltInAuthRoutes({\n * userStore: new InMemoryUserStore(),\n * tokenManager: new TokenManager({ secret: 'my-secret' }),\n * })\n *\n * // Wire into an HTTP server:\n * app.post('/auth/signup', async (req, res) => {\n * const result = await routes.handleSignUp(req.body)\n * res.status(result.status).json(result.body)\n * })\n * ```\n */\nexport class BuiltInAuthRoutes {\n\tprivate readonly userStore: InMemoryUserStore\n\tprivate readonly tokenManager: TokenManager\n\n\tconstructor(config: AuthRoutesConfig) {\n\t\tthis.userStore = config.userStore\n\t\tthis.tokenManager = config.tokenManager\n\t}\n\n\t/**\n\t * Handle user sign-up (POST /auth/signup).\n\t *\n\t * Validates email format and password length, hashes the password,\n\t * creates the user, optionally registers a device, and issues tokens.\n\t *\n\t * @param body - Sign-up request body\n\t * @param body.email - The user's email address\n\t * @param body.password - The plaintext password (min 8 characters)\n\t * @param body.name - Optional display name (defaults to email local part)\n\t * @param body.deviceId - Optional device ID to register\n\t * @param body.devicePublicKey - Optional device public key (base64url)\n\t * @returns Auth response with the created user and tokens, or an error\n\t */\n\tasync handleSignUp(body: {\n\t\temail: string\n\t\tpassword: string\n\t\tname?: string\n\t\tdeviceId?: string\n\t\tdevicePublicKey?: string\n\t}): Promise<AuthRouteResponse<{ user: AuthUser; tokens: AuthTokens }>> {\n\t\t// Validate email format\n\t\tif (!isValidEmail(body.email)) {\n\t\t\treturn {\n\t\t\t\tstatus: 400,\n\t\t\t\tbody: {\n\t\t\t\t\terror: 'Invalid email address. Please provide a valid email in the format user@domain.com.',\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\n\t\t// Validate password length\n\t\tif (body.password.length < MIN_PASSWORD_LENGTH) {\n\t\t\treturn {\n\t\t\t\tstatus: 400,\n\t\t\t\tbody: {\n\t\t\t\t\terror: `Password must be at least ${MIN_PASSWORD_LENGTH} characters long.`,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\n\t\t// Hash the password\n\t\tconst { hash, salt } = await hashPassword(body.password)\n\n\t\t// Create the user — may throw DuplicateEmailError\n\t\tlet user: AuthUser\n\t\ttry {\n\t\t\tuser = await this.userStore.createUser({\n\t\t\t\temail: body.email,\n\t\t\t\tpasswordHash: hash,\n\t\t\t\tsalt,\n\t\t\t\tname: body.name ?? body.email.split('@')[0] ?? body.email,\n\t\t\t})\n\t\t} catch (err: unknown) {\n\t\t\tif (err instanceof Error && err.name === 'DuplicateEmailError') {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: 409,\n\t\t\t\t\tbody: { error: 'An account with this email already exists.' },\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow err\n\t\t}\n\n\t\t// Use provided deviceId or generate a placeholder\n\t\tconst deviceId = body.deviceId ?? `device-${user.id}`\n\n\t\t// Register device if device info was provided\n\t\tif (body.deviceId !== undefined && body.devicePublicKey !== undefined) {\n\t\t\tawait this.userStore.registerDevice({\n\t\t\t\tid: body.deviceId,\n\t\t\t\tuserId: user.id,\n\t\t\t\tpublicKey: body.devicePublicKey,\n\t\t\t\tname: 'Primary Device',\n\t\t\t})\n\t\t}\n\n\t\t// Issue tokens\n\t\tconst tokens = this.tokenManager.issueTokens(user.id, deviceId)\n\n\t\treturn {\n\t\t\tstatus: 201,\n\t\t\tbody: { data: { user, tokens } },\n\t\t}\n\t}\n\n\t/**\n\t * Handle user sign-in (POST /auth/signin).\n\t *\n\t * Looks up the user by email, verifies the password, optionally registers\n\t * a new device, and issues tokens.\n\t *\n\t * @param body - Sign-in request body\n\t * @param body.email - The user's email address\n\t * @param body.password - The plaintext password\n\t * @param body.deviceId - Optional device ID to register\n\t * @param body.devicePublicKey - Optional device public key (base64url)\n\t * @returns Auth response with the user and tokens, or an error\n\t */\n\tasync handleSignIn(body: {\n\t\temail: string\n\t\tpassword: string\n\t\tdeviceId?: string\n\t\tdevicePublicKey?: string\n\t}): Promise<AuthRouteResponse<{ user: AuthUser; tokens: AuthTokens }>> {\n\t\tconst storedUser = await this.userStore.findByEmail(body.email)\n\t\tif (storedUser === null) {\n\t\t\treturn {\n\t\t\t\tstatus: 401,\n\t\t\t\tbody: { error: 'Invalid email or password.' },\n\t\t\t}\n\t\t}\n\n\t\tconst passwordValid = await verifyPassword(\n\t\t\tbody.password,\n\t\t\tstoredUser.passwordHash,\n\t\t\tstoredUser.salt,\n\t\t)\n\t\tif (!passwordValid) {\n\t\t\treturn {\n\t\t\t\tstatus: 401,\n\t\t\t\tbody: { error: 'Invalid email or password.' },\n\t\t\t}\n\t\t}\n\n\t\t// Use provided deviceId or generate a placeholder\n\t\tconst deviceId = body.deviceId ?? `device-${storedUser.id}`\n\n\t\t// Register device if device info was provided\n\t\tif (body.deviceId !== undefined && body.devicePublicKey !== undefined) {\n\t\t\tawait this.userStore.registerDevice({\n\t\t\t\tid: body.deviceId,\n\t\t\t\tuserId: storedUser.id,\n\t\t\t\tpublicKey: body.devicePublicKey,\n\t\t\t\tname: 'Device',\n\t\t\t})\n\t\t}\n\n\t\tconst tokens = this.tokenManager.issueTokens(storedUser.id, deviceId)\n\n\t\tconst user: AuthUser = {\n\t\t\tid: storedUser.id,\n\t\t\temail: storedUser.email,\n\t\t\tname: storedUser.name,\n\t\t\tcreatedAt: storedUser.createdAt,\n\t\t}\n\n\t\treturn {\n\t\t\tstatus: 200,\n\t\t\tbody: { data: { user, tokens } },\n\t\t}\n\t}\n\n\t/**\n\t * Handle token refresh (POST /auth/refresh).\n\t *\n\t * Validates the provided refresh token and issues a new token pair\n\t * (refresh token rotation). The old refresh token should be considered\n\t * consumed after this call.\n\t *\n\t * @param body - Refresh request body\n\t * @param body.refreshToken - The current refresh token\n\t * @returns Auth response with new tokens, or an error\n\t */\n\tasync handleRefresh(body: {\n\t\trefreshToken: string\n\t}): Promise<AuthRouteResponse<AuthTokens>> {\n\t\tconst result = this.tokenManager.refreshAccessToken(body.refreshToken)\n\n\t\tif (result === null) {\n\t\t\treturn {\n\t\t\t\tstatus: 401,\n\t\t\t\tbody: { error: 'Invalid or expired refresh token.' },\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tstatus: 200,\n\t\t\tbody: { data: result },\n\t\t}\n\t}\n\n\t/**\n\t * Handle get-current-user (GET /auth/me).\n\t *\n\t * Validates the access token and returns the authenticated user's profile.\n\t *\n\t * @param accessToken - The JWT access token (without \"Bearer \" prefix)\n\t * @returns Auth response with the user profile, or an error\n\t */\n\tasync handleGetMe(accessToken: string): Promise<AuthRouteResponse<AuthUser>> {\n\t\tconst payload = this.tokenManager.validateToken(accessToken)\n\t\tif (payload === null || payload.type !== 'access') {\n\t\t\treturn {\n\t\t\t\tstatus: 401,\n\t\t\t\tbody: { error: 'Invalid or expired access token.' },\n\t\t\t}\n\t\t}\n\n\t\tconst storedUser = await this.userStore.findById(payload.sub)\n\t\tif (storedUser === null) {\n\t\t\treturn {\n\t\t\t\tstatus: 404,\n\t\t\t\tbody: { error: 'User not found.' },\n\t\t\t}\n\t\t}\n\n\t\tconst user: AuthUser = {\n\t\t\tid: storedUser.id,\n\t\t\temail: storedUser.email,\n\t\t\tname: storedUser.name,\n\t\t\tcreatedAt: storedUser.createdAt,\n\t\t}\n\n\t\treturn {\n\t\t\tstatus: 200,\n\t\t\tbody: { data: user },\n\t\t}\n\t}\n\n\t/**\n\t * Handle list-devices (GET /auth/devices).\n\t *\n\t * Validates the access token and returns all devices registered for the user.\n\t *\n\t * @param accessToken - The JWT access token (without \"Bearer \" prefix)\n\t * @returns Auth response with the device list, or an error\n\t */\n\tasync handleListDevices(\n\t\taccessToken: string,\n\t): Promise<AuthRouteResponse<AuthDevice[]>> {\n\t\tconst payload = this.tokenManager.validateToken(accessToken)\n\t\tif (payload === null || payload.type !== 'access') {\n\t\t\treturn {\n\t\t\t\tstatus: 401,\n\t\t\t\tbody: { error: 'Invalid or expired access token.' },\n\t\t\t}\n\t\t}\n\n\t\tconst devices = await this.userStore.listDevices(payload.sub)\n\n\t\treturn {\n\t\t\tstatus: 200,\n\t\t\tbody: { data: devices },\n\t\t}\n\t}\n\n\t/**\n\t * Handle device revocation (DELETE /auth/device/:id).\n\t *\n\t * Validates the access token and revokes the specified device.\n\t * Only the device's owner can revoke it.\n\t *\n\t * @param accessToken - The JWT access token (without \"Bearer \" prefix)\n\t * @param deviceId - The ID of the device to revoke\n\t * @returns Auth response with success flag, or an error\n\t */\n\tasync handleRevokeDevice(\n\t\taccessToken: string,\n\t\tdeviceId: string,\n\t): Promise<AuthRouteResponse<{ success: boolean }>> {\n\t\tconst payload = this.tokenManager.validateToken(accessToken)\n\t\tif (payload === null || payload.type !== 'access') {\n\t\t\treturn {\n\t\t\t\tstatus: 401,\n\t\t\t\tbody: { error: 'Invalid or expired access token.' },\n\t\t\t}\n\t\t}\n\n\t\t// Verify the device belongs to the authenticated user\n\t\tconst device = await this.userStore.findDevice(deviceId)\n\t\tif (device === null) {\n\t\t\treturn {\n\t\t\t\tstatus: 404,\n\t\t\t\tbody: { error: 'Device not found.' },\n\t\t\t}\n\t\t}\n\n\t\tif (device.userId !== payload.sub) {\n\t\t\treturn {\n\t\t\t\tstatus: 403,\n\t\t\t\tbody: { error: 'You can only revoke your own devices.' },\n\t\t\t}\n\t\t}\n\n\t\tawait this.userStore.revokeDevice(deviceId)\n\n\t\treturn {\n\t\t\tstatus: 200,\n\t\t\tbody: { data: { success: true } },\n\t\t}\n\t}\n\n\t/**\n\t * Creates a sync server auth provider compatible with `@korajs/server`.\n\t *\n\t * The returned object implements the `AuthProvider` interface from\n\t * `@korajs/server`, validating access tokens and returning an auth\n\t * context containing the user ID and device metadata. This bridges\n\t * the built-in auth system with the sync server's authentication layer.\n\t *\n\t * @returns An object with an `authenticate` method suitable for KoraSyncServer's `auth` config\n\t *\n\t * @example\n\t * ```typescript\n\t * const routes = new BuiltInAuthRoutes({ userStore, tokenManager })\n\t * const syncServer = new KoraSyncServer({\n\t * store,\n\t * auth: routes.toSyncAuthProvider(),\n\t * })\n\t * ```\n\t */\n\ttoSyncAuthProvider(): {\n\t\tauthenticate(token: string): Promise<{\n\t\t\tuserId: string\n\t\t\tscopes?: Record<string, Record<string, unknown>>\n\t\t\tmetadata?: Record<string, unknown>\n\t\t} | null>\n\t} {\n\t\tconst tokenManager = this.tokenManager\n\t\tconst userStore = this.userStore\n\n\t\treturn {\n\t\t\tasync authenticate(token: string) {\n\t\t\t\tconst payload = tokenManager.validateToken(token)\n\t\t\t\tif (payload === null || payload.type !== 'access') {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\n\t\t\t\t// Verify the user still exists\n\t\t\t\tconst user = await userStore.findById(payload.sub)\n\t\t\t\tif (user === null) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\n\t\t\t\t// Touch the device to update last-seen timestamp\n\t\t\t\tawait userStore.touchDevice(payload.dev)\n\n\t\t\t\treturn {\n\t\t\t\t\tuserId: payload.sub,\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\tdeviceId: payload.dev,\n\t\t\t\t\t\temail: user.email,\n\t\t\t\t\t\tname: user.name,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t},\n\t\t}\n\t}\n}\n","import type { AuthTokens } from '../types'\nimport type { TokenManager } from '../tokens/token-manager'\nimport type { AuthUser, AuthDevice } from './built-in/user-store'\nimport type { InMemoryUserStore } from './built-in/user-store'\nimport { BuiltInAuthRoutes, type AuthRoutesConfig } from './built-in/auth-routes'\n\n/**\n * Parameters for signing up a new user.\n */\nexport interface SignUpParams {\n\t/** The user's email address */\n\temail: string\n\t/** The plaintext password */\n\tpassword: string\n\t/** Optional display name */\n\tname?: string\n\t/** Optional device ID to register with the account */\n\tdeviceId?: string\n\t/** Optional device public key (base64url) */\n\tdevicePublicKey?: string\n}\n\n/**\n * Parameters for signing in an existing user.\n */\nexport interface SignInParams {\n\t/** The user's email address */\n\temail: string\n\t/** The plaintext password */\n\tpassword: string\n\t/** Optional device ID to register or associate */\n\tdeviceId?: string\n\t/** Optional device public key (base64url) */\n\tdevicePublicKey?: string\n}\n\n/**\n * Abstraction for authentication providers.\n *\n * This interface allows swapping between the built-in email/password provider\n * and external providers (OAuth, SAML, custom) without changing application code.\n * Every provider must support the same core operations: sign up, sign in,\n * token refresh, token validation, user lookup, and device management.\n *\n * @example\n * ```typescript\n * // Use the built-in provider\n * const provider: AuthProviderAdapter = new BuiltInProvider({\n * userStore: new InMemoryUserStore(),\n * tokenManager: new TokenManager({ secret: 'my-secret' }),\n * })\n *\n * const { user, tokens } = await provider.signUp({\n * email: 'alice@example.com',\n * password: 'secure-password-123',\n * })\n * ```\n */\nexport interface AuthProviderAdapter {\n\t/**\n\t * Create a new user account and issue authentication tokens.\n\t *\n\t * @param params - Sign-up parameters including email, password, and optional device info\n\t * @returns The created user and tokens\n\t * @throws If the email is already registered or input validation fails\n\t */\n\tsignUp(params: SignUpParams): Promise<{ user: AuthUser; tokens: AuthTokens }>\n\n\t/**\n\t * Authenticate an existing user and issue tokens.\n\t *\n\t * @param params - Sign-in parameters including email and password\n\t * @returns The authenticated user and tokens\n\t * @throws If the credentials are invalid\n\t */\n\tsignIn(params: SignInParams): Promise<{ user: AuthUser; tokens: AuthTokens }>\n\n\t/**\n\t * Exchange a refresh token for new tokens (rotation).\n\t *\n\t * @param refreshToken - The current refresh token\n\t * @returns A new token pair\n\t * @throws If the refresh token is invalid or expired\n\t */\n\trefreshTokens(refreshToken: string): Promise<AuthTokens>\n\n\t/**\n\t * Validate an access token and extract identity claims.\n\t *\n\t * @param token - The JWT access token\n\t * @returns User ID and device ID if valid, or null if invalid/expired\n\t */\n\tvalidateAccessToken(token: string): Promise<{ userId: string; deviceId: string } | null>\n\n\t/**\n\t * Look up a user by ID.\n\t *\n\t * @param userId - The user's ID\n\t * @returns The user profile, or null if not found\n\t */\n\tgetUser(userId: string): Promise<AuthUser | null>\n\n\t/**\n\t * Revoke a device. Requires a valid access token for authorization.\n\t *\n\t * @param accessToken - The caller's access token\n\t * @param deviceId - The ID of the device to revoke\n\t * @throws If the token is invalid or the device does not belong to the caller\n\t */\n\trevokeDevice(accessToken: string, deviceId: string): Promise<void>\n\n\t/**\n\t * List all devices for the authenticated user.\n\t *\n\t * @param accessToken - The caller's access token\n\t * @returns Array of device records\n\t * @throws If the token is invalid\n\t */\n\tlistDevices(accessToken: string): Promise<AuthDevice[]>\n}\n\n/**\n * Error thrown by provider adapter methods when an operation fails.\n * Wraps the HTTP-style status and error message from route handlers\n * into an exception for use in the adapter pattern.\n */\nexport class AuthProviderError extends Error {\n\t/** HTTP-style status code */\n\treadonly status: number\n\n\tconstructor(message: string, status: number) {\n\t\tsuper(message)\n\t\tthis.name = 'AuthProviderError'\n\t\tthis.status = status\n\t}\n}\n\n/**\n * Built-in authentication provider implementing email/password authentication.\n *\n * Wraps {@link BuiltInAuthRoutes} in the {@link AuthProviderAdapter} interface,\n * converting HTTP-style responses into direct return values and exceptions.\n * This is the default provider shipped with Kora and is suitable for\n * applications that want simple email/password auth without external services.\n *\n * @example\n * ```typescript\n * import { BuiltInProvider } from '@korajs/auth'\n * import { InMemoryUserStore } from '@korajs/auth'\n * import { TokenManager } from '@korajs/auth'\n *\n * const provider = new BuiltInProvider({\n * userStore: new InMemoryUserStore(),\n * tokenManager: new TokenManager({ secret: process.env.AUTH_SECRET }),\n * })\n *\n * const { user, tokens } = await provider.signUp({\n * email: 'alice@example.com',\n * password: 'strong-password-123',\n * name: 'Alice',\n * })\n * ```\n */\nexport class BuiltInProvider implements AuthProviderAdapter {\n\tprivate readonly routes: BuiltInAuthRoutes\n\tprivate readonly tokenManager: TokenManager\n\tprivate readonly userStore: InMemoryUserStore\n\n\tconstructor(config: AuthRoutesConfig) {\n\t\tthis.routes = new BuiltInAuthRoutes(config)\n\t\tthis.tokenManager = config.tokenManager\n\t\tthis.userStore = config.userStore\n\t}\n\n\t/** @inheritdoc */\n\tasync signUp(params: SignUpParams): Promise<{ user: AuthUser; tokens: AuthTokens }> {\n\t\tconst result = await this.routes.handleSignUp(params)\n\t\tif ('error' in result.body) {\n\t\t\tthrow new AuthProviderError(result.body.error, result.status)\n\t\t}\n\t\treturn result.body.data\n\t}\n\n\t/** @inheritdoc */\n\tasync signIn(params: SignInParams): Promise<{ user: AuthUser; tokens: AuthTokens }> {\n\t\tconst result = await this.routes.handleSignIn(params)\n\t\tif ('error' in result.body) {\n\t\t\tthrow new AuthProviderError(result.body.error, result.status)\n\t\t}\n\t\treturn result.body.data\n\t}\n\n\t/** @inheritdoc */\n\tasync refreshTokens(refreshToken: string): Promise<AuthTokens> {\n\t\tconst result = await this.routes.handleRefresh({ refreshToken })\n\t\tif ('error' in result.body) {\n\t\t\tthrow new AuthProviderError(result.body.error, result.status)\n\t\t}\n\t\treturn result.body.data\n\t}\n\n\t/** @inheritdoc */\n\tasync validateAccessToken(\n\t\ttoken: string,\n\t): Promise<{ userId: string; deviceId: string } | null> {\n\t\tconst payload = this.tokenManager.validateToken(token)\n\t\tif (payload === null || payload.type !== 'access') {\n\t\t\treturn null\n\t\t}\n\t\treturn { userId: payload.sub, deviceId: payload.dev }\n\t}\n\n\t/** @inheritdoc */\n\tasync getUser(userId: string): Promise<AuthUser | null> {\n\t\tconst stored = await this.userStore.findById(userId)\n\t\tif (stored === null) {\n\t\t\treturn null\n\t\t}\n\t\treturn {\n\t\t\tid: stored.id,\n\t\t\temail: stored.email,\n\t\t\tname: stored.name,\n\t\t\tcreatedAt: stored.createdAt,\n\t\t}\n\t}\n\n\t/** @inheritdoc */\n\tasync revokeDevice(accessToken: string, deviceId: string): Promise<void> {\n\t\tconst result = await this.routes.handleRevokeDevice(accessToken, deviceId)\n\t\tif ('error' in result.body) {\n\t\t\tthrow new AuthProviderError(result.body.error, result.status)\n\t\t}\n\t}\n\n\t/** @inheritdoc */\n\tasync listDevices(accessToken: string): Promise<AuthDevice[]> {\n\t\tconst result = await this.routes.handleListDevices(accessToken)\n\t\tif ('error' in result.body) {\n\t\t\tthrow new AuthProviderError(result.body.error, result.status)\n\t\t}\n\t\treturn result.body.data\n\t}\n}\n","import { KoraError } from '@korajs/core'\n\n// --- Auth-specific errors ---\n\n/**\n * Thrown when the Web Crypto API is not available in the current environment.\n * This can happen in older Node.js versions or SSR environments without crypto support.\n */\nexport class CryptoUnavailableError extends KoraError {\n\tconstructor() {\n\t\tsuper(\n\t\t\t'Web Crypto API (crypto.subtle) is not available in this environment. ' +\n\t\t\t\t'Device identity requires crypto.subtle, which is available in modern browsers and Node.js 20+. ' +\n\t\t\t\t'If running in SSR, ensure your runtime provides the Web Crypto API.',\n\t\t\t'CRYPTO_UNAVAILABLE',\n\t\t)\n\t\tthis.name = 'CryptoUnavailableError'\n\t}\n}\n\n/**\n * Thrown when a device identity operation fails (key generation, signing, verification).\n */\nexport class DeviceIdentityError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'DEVICE_IDENTITY_ERROR', context)\n\t\tthis.name = 'DeviceIdentityError'\n\t}\n}\n\n// --- Encoding helpers ---\n\n/**\n * Encodes an ArrayBuffer as a base64url string (no padding).\n *\n * @param buffer - The binary data to encode\n * @returns A base64url-encoded string without padding characters\n */\nexport function toBase64Url(buffer: ArrayBuffer): string {\n\tconst bytes = new Uint8Array(buffer)\n\tlet binary = ''\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\tbinary += String.fromCharCode(bytes[i] as number)\n\t}\n\t// Standard base64, then convert to base64url (no padding)\n\treturn btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/**\n * Decodes a base64url string (no padding) into a Uint8Array.\n *\n * @param str - A base64url-encoded string (with or without padding)\n * @returns The decoded binary data as a Uint8Array\n */\nexport function fromBase64Url(str: string): Uint8Array {\n\t// Convert base64url back to standard base64\n\tlet base64 = str.replace(/-/g, '+').replace(/_/g, '/')\n\t// Add padding if necessary\n\tconst paddingNeeded = (4 - (base64.length % 4)) % 4\n\tbase64 += '='.repeat(paddingNeeded)\n\n\tconst binary = atob(base64)\n\tconst bytes = new Uint8Array(binary.length)\n\tfor (let i = 0; i < binary.length; i++) {\n\t\tbytes[i] = binary.charCodeAt(i)\n\t}\n\treturn bytes\n}\n\n// --- Internal helpers ---\n\n/**\n * Asserts that `crypto.subtle` is available, throwing a clear error if not.\n */\nfunction assertCryptoAvailable(): void {\n\tif (\n\t\ttypeof globalThis.crypto === 'undefined' ||\n\t\ttypeof globalThis.crypto.subtle === 'undefined'\n\t) {\n\t\tthrow new CryptoUnavailableError()\n\t}\n}\n\n/** ECDSA algorithm parameters used throughout the module. */\nconst ECDSA_ALGORITHM: EcKeyGenParams = {\n\tname: 'ECDSA',\n\tnamedCurve: 'P-256',\n}\n\n/** Signing algorithm parameters: ECDSA with SHA-256. */\nconst ECDSA_SIGN_ALGORITHM: EcdsaParams = {\n\tname: 'ECDSA',\n\thash: { name: 'SHA-256' },\n}\n\n// --- Public API ---\n\n/**\n * Generates an ECDSA P-256 key pair for device identity.\n *\n * The private key is marked as non-extractable, ensuring it cannot be\n * exported from the browser's crypto subsystem. This provides\n * proof-of-possession: only code running on this device can sign with the key.\n *\n * @returns A CryptoKeyPair containing the public and private ECDSA P-256 keys\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {DeviceIdentityError} If key generation fails\n *\n * @example\n * ```typescript\n * const keyPair = await generateDeviceKeyPair()\n * // keyPair.publicKey can be exported; keyPair.privateKey stays on device\n * ```\n */\nexport async function generateDeviceKeyPair(): Promise<CryptoKeyPair> {\n\tassertCryptoAvailable()\n\n\ttry {\n\t\tconst keyPair = await globalThis.crypto.subtle.generateKey(\n\t\t\tECDSA_ALGORITHM,\n\t\t\t// extractable: false makes the private key non-extractable.\n\t\t\t// The public key is always extractable regardless of this flag.\n\t\t\tfalse,\n\t\t\t['sign', 'verify'],\n\t\t)\n\t\treturn keyPair\n\t} catch (cause) {\n\t\tthrow new DeviceIdentityError(\n\t\t\t'Failed to generate ECDSA P-256 device key pair. ' +\n\t\t\t\t'Ensure the runtime supports the ECDSA algorithm with the P-256 curve.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n\n/**\n * Exports the public key from a key pair as a JSON Web Key (JWK).\n *\n * The JWK can be safely transmitted to a server or other devices to identify\n * this device. It contains only the public component of the key pair.\n *\n * @param keyPair - The CryptoKeyPair whose public key should be exported\n * @returns The public key in JWK format\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {DeviceIdentityError} If the export operation fails\n *\n * @example\n * ```typescript\n * const keyPair = await generateDeviceKeyPair()\n * const jwk = await exportPublicKeyJwk(keyPair)\n * // jwk contains { kty: 'EC', crv: 'P-256', x: '...', y: '...' }\n * ```\n */\nexport async function exportPublicKeyJwk(keyPair: CryptoKeyPair): Promise<JsonWebKey> {\n\tassertCryptoAvailable()\n\n\ttry {\n\t\tconst jwk = await globalThis.crypto.subtle.exportKey('jwk', keyPair.publicKey)\n\t\treturn jwk\n\t} catch (cause) {\n\t\tthrow new DeviceIdentityError(\n\t\t\t'Failed to export public key as JWK. ' +\n\t\t\t\t'The key pair may be invalid or the public key may not support JWK export.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n\n/**\n * Signs a challenge string with the device's private key.\n *\n * Used for proof-of-possession during authentication: the server sends a\n * random challenge, and the device proves it holds the private key by signing it.\n *\n * @param privateKey - The device's private CryptoKey (ECDSA P-256)\n * @param challenge - The challenge string to sign (typically a random nonce from the server)\n * @returns A base64url-encoded ECDSA signature (no padding)\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {DeviceIdentityError} If the signing operation fails\n *\n * @example\n * ```typescript\n * const keyPair = await generateDeviceKeyPair()\n * const signature = await signChallenge(keyPair.privateKey, 'server-nonce-abc123')\n * // signature is a base64url string like 'MEUCIQDx...'\n * ```\n */\nexport async function signChallenge(privateKey: CryptoKey, challenge: string): Promise<string> {\n\tassertCryptoAvailable()\n\n\ttry {\n\t\tconst encoded = new TextEncoder().encode(challenge)\n\t\tconst signatureBuffer = await globalThis.crypto.subtle.sign(\n\t\t\tECDSA_SIGN_ALGORITHM,\n\t\t\tprivateKey,\n\t\t\tencoded,\n\t\t)\n\t\treturn toBase64Url(signatureBuffer)\n\t} catch (cause) {\n\t\tthrow new DeviceIdentityError(\n\t\t\t'Failed to sign challenge. ' +\n\t\t\t\t'Ensure the key is a valid ECDSA P-256 private key with \"sign\" usage.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n\n/**\n * Verifies a challenge signature against a public key.\n *\n * Used server-side (or on any verifying party) to confirm that a device\n * holds the private key corresponding to the given public key.\n *\n * @param publicKeyJwk - The device's public key in JWK format\n * @param challenge - The original challenge string that was signed\n * @param signature - The base64url-encoded signature to verify\n * @returns `true` if the signature is valid, `false` otherwise\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {DeviceIdentityError} If the verification operation fails due to an invalid key or format\n *\n * @example\n * ```typescript\n * const isValid = await verifyChallenge(publicKeyJwk, 'server-nonce-abc123', signature)\n * if (isValid) {\n * // Device proved possession of the private key\n * }\n * ```\n */\nexport async function verifyChallenge(\n\tpublicKeyJwk: JsonWebKey,\n\tchallenge: string,\n\tsignature: string,\n): Promise<boolean> {\n\tassertCryptoAvailable()\n\n\ttry {\n\t\tconst publicKey = await globalThis.crypto.subtle.importKey(\n\t\t\t'jwk',\n\t\t\tpublicKeyJwk,\n\t\t\tECDSA_ALGORITHM,\n\t\t\ttrue,\n\t\t\t['verify'],\n\t\t)\n\n\t\tconst encoded = new TextEncoder().encode(challenge)\n\t\tconst signatureBytes = fromBase64Url(signature)\n\n\t\tconst isValid = await globalThis.crypto.subtle.verify(\n\t\t\tECDSA_SIGN_ALGORITHM,\n\t\t\tpublicKey,\n\t\t\tsignatureBytes as unknown as ArrayBuffer,\n\t\t\tencoded,\n\t\t)\n\t\treturn isValid\n\t} catch (cause) {\n\t\tthrow new DeviceIdentityError(\n\t\t\t'Failed to verify challenge signature. ' +\n\t\t\t\t'The public key JWK or signature format may be invalid.',\n\t\t\t{\n\t\t\t\tcause: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\tpublicKeyKty: publicKeyJwk.kty,\n\t\t\t\tpublicKeyCrv: publicKeyJwk.crv,\n\t\t\t},\n\t\t)\n\t}\n}\n\n/**\n * Computes a SHA-256 thumbprint of a JWK public key.\n *\n * The thumbprint is computed per RFC 7638: the JWK members required for the key\n * type are serialized in lexicographic order, then hashed with SHA-256. For EC keys\n * (kty: \"EC\"), the required members are `crv`, `kty`, `x`, and `y`.\n *\n * This thumbprint serves as a compact, stable identifier for the device's public key\n * (used as the `dpk` claim in device credentials).\n *\n * @param publicKeyJwk - The public key in JWK format (must be an EC P-256 key)\n * @returns A base64url-encoded SHA-256 thumbprint (no padding)\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {DeviceIdentityError} If the thumbprint computation fails or the JWK is missing required fields\n *\n * @example\n * ```typescript\n * const keyPair = await generateDeviceKeyPair()\n * const jwk = await exportPublicKeyJwk(keyPair)\n * const thumbprint = await computePublicKeyThumbprint(jwk)\n * // thumbprint is a base64url string, e.g., 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs'\n * ```\n */\nexport async function computePublicKeyThumbprint(publicKeyJwk: JsonWebKey): Promise<string> {\n\tassertCryptoAvailable()\n\n\t// RFC 7638 requires specific members in lexicographic order for each key type.\n\t// For EC (kty: \"EC\"), the required members are: crv, kty, x, y.\n\tif (publicKeyJwk.kty !== 'EC') {\n\t\tthrow new DeviceIdentityError(\n\t\t\t`Expected JWK key type \"EC\" but received \"${publicKeyJwk.kty ?? 'undefined'}\". ` +\n\t\t\t\t'Only ECDSA public keys are supported for device identity.',\n\t\t\t{ kty: publicKeyJwk.kty },\n\t\t)\n\t}\n\n\tif (!publicKeyJwk.crv || !publicKeyJwk.x || !publicKeyJwk.y) {\n\t\tthrow new DeviceIdentityError(\n\t\t\t'JWK is missing required EC fields. ' +\n\t\t\t\t'An EC public key JWK must include \"crv\", \"x\", and \"y\" members.',\n\t\t\t{\n\t\t\t\thasCrv: Boolean(publicKeyJwk.crv),\n\t\t\t\thasX: Boolean(publicKeyJwk.x),\n\t\t\t\thasY: Boolean(publicKeyJwk.y),\n\t\t\t},\n\t\t)\n\t}\n\n\t// Build the canonical JSON with only the required members in lexicographic order.\n\t// Per RFC 7638, no whitespace, keys in sorted order.\n\tconst canonicalJson = JSON.stringify({\n\t\tcrv: publicKeyJwk.crv,\n\t\tkty: publicKeyJwk.kty,\n\t\tx: publicKeyJwk.x,\n\t\ty: publicKeyJwk.y,\n\t})\n\n\ttry {\n\t\tconst encoded = new TextEncoder().encode(canonicalJson)\n\t\tconst hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', encoded)\n\t\treturn toBase64Url(hashBuffer)\n\t} catch (cause) {\n\t\tthrow new DeviceIdentityError(\n\t\t\t'Failed to compute SHA-256 thumbprint of the public key JWK.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n"],"mappings":"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","names":["import_node_crypto","import_core","import_node_crypto","import_core"]}