/** * Issue and send a magic link. ALWAYS resolves void - unknown emails are a * silent no-op (unless `createUser`), so callers can answer a uniform * "check your email" without leaking which addresses exist. Callers gate * with their route's `.rateLimit()`; this adds a per-email limiter on top. */ export declare function sendMagicLink(email: string, options?: SendMagicLinkOptions): Promise; /** * Atomically consume a raw token. Exactly one caller can win a given token: * the claim is a conditional UPDATE on (unconsumed AND unexpired), so two * concurrent consumes resolve to one `ok` and one `used`. */ export declare function consumeMagicLink(raw: string): Promise; /** Drop expired and long-consumed rows. For a daily janitor job. */ export declare function pruneMagicLinkTokens(olderThanDays?: number): Promise; /** * Magic-link (passwordless) sign-in. * * Mirrors `password/reset.ts` where the concerns match (anti-enumeration, * single outstanding token per email, template-with-plaintext-fallback * delivery) and deliberately differs where they don't: * * - `token` at rest is SHA-256, not bcrypt: consume is one indexed * lookup, and the raw token carries 256 bits of entropy, so a fast * hash gives an offline attacker nothing. * - Consume is ATOMIC single-use: one conditional UPDATE claims the row; * concurrent requests race for one winner. GET never consumes - the * interstitial page POSTs, because mail scanners prefetch GETs and * would burn single-use links. */ export declare interface SendMagicLinkOptions { redirectTo?: string siteId?: number ttlMinutes?: number createUser?: boolean } export type ConsumeMagicLinkResult = | { ok: true, userId: number, email: string, redirectTo: string } | { ok: false, reason: 'invalid' | 'expired' | 'used' | 'no-user' }