/** * Core types for @emdash-cms/auth */ // ============================================================================ // Roles & Permissions // ============================================================================ export const Role = { SUBSCRIBER: 10, CONTRIBUTOR: 20, AUTHOR: 30, EDITOR: 40, ADMIN: 50, } as const; export type RoleLevel = (typeof Role)[keyof typeof Role]; export type RoleName = keyof typeof Role; export function roleFromLevel(level: number): RoleName | undefined { const entry = Object.entries(Role).find(([, v]) => v === level); if (!entry) return undefined; const name = entry[0]; if (isRoleName(name)) return name; return undefined; } function isRoleName(value: string): value is RoleName { return value in Role; } const ROLE_LEVEL_MAP = new Map(Object.values(Role).map((v) => [v, v])); export function toRoleLevel(value: number): RoleLevel { const level = ROLE_LEVEL_MAP.get(value); if (level !== undefined) return level; throw new Error(`Invalid role level: ${value}`); } const DEVICE_TYPE_MAP: Record = { singleDevice: "singleDevice", multiDevice: "multiDevice", }; export function toDeviceType(value: string): DeviceType { const dt = DEVICE_TYPE_MAP[value]; if (dt !== undefined) return dt; throw new Error(`Invalid device type: ${value}`); } const TOKEN_TYPE_MAP: Record = { magic_link: "magic_link", email_verify: "email_verify", invite: "invite", recovery: "recovery", }; export function toTokenType(value: string): TokenType { const tt = TOKEN_TYPE_MAP[value]; if (tt !== undefined) return tt; throw new Error(`Invalid token type: ${value}`); } export function roleToLevel(name: RoleName): RoleLevel { return Role[name]; } // ============================================================================ // User // ============================================================================ export interface User { id: string; email: string; name: string | null; avatarUrl: string | null; role: RoleLevel; emailVerified: boolean; disabled: boolean; data: Record | null; createdAt: Date; updatedAt: Date; } export interface NewUser { email: string; name?: string | null; avatarUrl?: string | null; role?: RoleLevel; emailVerified?: boolean; data?: Record | null; } export interface UpdateUser { email?: string; name?: string | null; avatarUrl?: string | null; role?: RoleLevel; emailVerified?: boolean; disabled?: boolean; data?: Record | null; } // ============================================================================ // Credentials (Passkeys) // ============================================================================ export type AuthenticatorTransport = "usb" | "nfc" | "ble" | "internal" | "hybrid"; export type DeviceType = "singleDevice" | "multiDevice"; export interface Credential { id: string; // Base64url credential ID userId: string; publicKey: Uint8Array; // Encoded public key (SEC1 for ECDSA, PKIX for RSA) algorithm: number; // COSE algorithm identifier (e.g., -7 for ES256, -257 for RS256) counter: number; deviceType: DeviceType; backedUp: boolean; transports: AuthenticatorTransport[]; name: string | null; createdAt: Date; lastUsedAt: Date; } export interface NewCredential { id: string; userId: string; publicKey: Uint8Array; algorithm: number; counter: number; deviceType: DeviceType; backedUp: boolean; transports: AuthenticatorTransport[]; name?: string | null; } // ============================================================================ // Sessions // ============================================================================ export interface Session { id: string; userId: string; expiresAt: Date; ipAddress: string | null; userAgent: string | null; createdAt: Date; } export interface SessionData { userId: string; expiresAt: number; // Unix timestamp } // ============================================================================ // Auth Tokens (magic links, invites, etc.) // ============================================================================ export type TokenType = "magic_link" | "email_verify" | "invite" | "recovery"; export interface AuthToken { hash: string; // SHA-256 hash of the raw token userId: string | null; // null for pre-user tokens (invite/signup) email: string | null; // For pre-user tokens type: TokenType; role: RoleLevel | null; // For invites invitedBy: string | null; expiresAt: Date; createdAt: Date; } export interface NewAuthToken { hash: string; userId?: string | null; email?: string | null; type: TokenType; role?: RoleLevel | null; invitedBy?: string | null; expiresAt: Date; } // ============================================================================ // OAuth Accounts // ============================================================================ export interface OAuthAccount { provider: string; providerAccountId: string; userId: string; createdAt: Date; } export interface NewOAuthAccount { provider: string; providerAccountId: string; userId: string; } // ============================================================================ // OAuth Connections (SSO config) // ============================================================================ export interface OAuthConnection { id: string; name: string; provider: "oidc" | "github" | "google"; clientId: string; clientSecretEnc: string; // Encrypted issuerUrl: string | null; config: Record | null; enabled: boolean; createdAt: Date; } // ============================================================================ // OAuth Clients (when EmDash is provider) // ============================================================================ export interface OAuthClient { id: string; name: string; secretHash: string; redirectUris: string[]; scopes: string[]; createdAt: Date; } // ============================================================================ // Allowed Domains (self-signup) // ============================================================================ export interface AllowedDomain { domain: string; defaultRole: RoleLevel; enabled: boolean; createdAt: Date; } // ============================================================================ // User Listing Types (for admin UI) // ============================================================================ /** Extended user with list view computed fields */ export interface UserListItem extends User { lastLogin: Date | null; credentialCount: number; oauthProviders: string[]; } /** User with full details including related data */ export interface UserWithDetails { user: User; credentials: Credential[]; oauthAccounts: OAuthAccount[]; lastLogin: Date | null; } // ============================================================================ // Auth Adapter Interface // ============================================================================ export interface AuthAdapter { // Users getUserById(id: string): Promise; getUserByEmail(email: string): Promise; createUser(user: NewUser): Promise; updateUser(id: string, data: UpdateUser): Promise; deleteUser(id: string): Promise; countUsers(): Promise; // User listing and details (for admin) getUsers(options?: { search?: string; role?: number; cursor?: string; limit?: number; }): Promise<{ items: UserListItem[]; nextCursor?: string }>; getUserWithDetails(id: string): Promise; countAdmins(): Promise; // Credentials getCredentialById(id: string): Promise; getCredentialsByUserId(userId: string): Promise; createCredential(credential: NewCredential): Promise; updateCredentialCounter(id: string, counter: number): Promise; updateCredentialName(id: string, name: string | null): Promise; deleteCredential(id: string): Promise; countCredentialsByUserId(userId: string): Promise; // Auth Tokens createToken(token: NewAuthToken): Promise; getToken(hash: string, type: TokenType): Promise; deleteToken(hash: string): Promise; deleteExpiredTokens(): Promise; // OAuth Accounts getOAuthAccount(provider: string, providerAccountId: string): Promise; getOAuthAccountsByUserId(userId: string): Promise; createOAuthAccount(account: NewOAuthAccount): Promise; deleteOAuthAccount(provider: string, providerAccountId: string): Promise; // Allowed Domains getAllowedDomain(domain: string): Promise; getAllowedDomains(): Promise; createAllowedDomain(domain: string, defaultRole: RoleLevel): Promise; updateAllowedDomain(domain: string, enabled: boolean, defaultRole?: RoleLevel): Promise; deleteAllowedDomain(domain: string): Promise; } // ============================================================================ // Email Adapter Interface // ============================================================================ export interface EmailMessage { to: string; subject: string; text: string; html?: string; } export interface EmailAdapter { send(message: EmailMessage): Promise; } // ============================================================================ // Auth Errors // ============================================================================ export class AuthError extends Error { constructor( public code: AuthErrorCode, message?: string, ) { super(message ?? code); this.name = "AuthError"; } } export type AuthErrorCode = | "invalid_credentials" | "invalid_token" | "token_expired" | "user_not_found" | "user_exists" | "credential_exists" | "max_credentials" | "email_not_verified" | "signup_not_allowed" | "domain_not_allowed" | "forbidden" | "unauthorized" | "rate_limited" | "invalid_request" | "internal_error";