/** * Create login request procedure * First step in the login flow - validates email and returns available auth methods */ import { COOKIE_DURATIONS, COOKIE_NAMES, COOKIE_OPTIONS, getCookie, setCookie, } from "../../utils/cookies.js"; import { generateDeviceFingerprint, generateExpiry, generateSecureBase58Token, } from "../../utils/crypto.js"; import { getGeoInfo, getUserAgent } from "../../utils/geo.js"; import { isDeviceTrusted } from "../../utils/session.js"; import { os } from "../base.js"; /** * Create login request handler * - Normalizes email to lowercase * - Reads/generates device fingerprint cookie * - Looks up user by email * - If user exists: checks device trust, passkey, password; creates login request * - If user doesn't exist: generates fake token for anti-enumeration * - Returns auth method availability and device trust status */ export const createLoginRequest = os.auth.createLoginRequest.handler( async ({ input, context }) => { const { email: rawEmail } = input; // Normalize email to lowercase const email = rawEmail.toLowerCase(); // Read or generate device fingerprint let deviceFingerprint = getCookie( context.reqHeaders, COOKIE_NAMES.DEVICE_FINGERPRINT, ); if (!deviceFingerprint) { deviceFingerprint = generateDeviceFingerprint(); setCookie( context.resHeaders, COOKIE_NAMES.DEVICE_FINGERPRINT, deviceFingerprint, COOKIE_OPTIONS.device, ); } // Look up user by email const user = await context.db .selectFrom("users") .select(["id", "password_hash"]) .where("email", "=", email) .executeTakeFirst(); // User doesn't exist - return fake response for anti-enumeration if (!user) { // Generate placeholder token (base58) for anti-enumeration // This prevents attackers from knowing if an email exists based on response const placeholderToken = generateSecureBase58Token("login_"); // Set placeholder login request token cookie setCookie( context.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN, placeholderToken, COOKIE_OPTIONS.loginRequest, ); return { hasPasskey: false, hasPassword: false, isTrustedDevice: false, email, }; } // User exists - gather real auth information const userId = user.id; // Check if device is trusted const isTrustedDevice = await isDeviceTrusted( context.db, userId, deviceFingerprint, ); // Check if user has passkey const passkey = await context.db .selectFrom("passkeys") .select(["id"]) .where("user_id", "=", userId) .executeTakeFirst(); const hasPasskey = !!passkey; // Check if user has password const hasPassword = user.password_hash !== null; // Get geo info and user agent const geo = getGeoInfo(context.reqHeaders); const userAgent = getUserAgent(context.reqHeaders); // Create login request with secure token const expiresAt = generateExpiry(COOKIE_DURATIONS.LOGIN_REQUEST); const token = generateSecureBase58Token("login_"); await context.db .insertInto("login_requests") .values({ user_id: userId, email, token, device_fingerprint: deviceFingerprint, ip_address: geo.ip, city: geo.city, region: geo.region, country: geo.country, user_agent: userAgent, expires_at: expiresAt, }) .execute(); // Set login request token cookie with the secure token setCookie( context.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN, token, COOKIE_OPTIONS.loginRequest, ); return { hasPasskey, hasPassword, isTrustedDevice, email, }; }, );