export type BannableUser = Record & { banned?: boolean | null; banExpires?: Date | string | number | null; }; // Defense-in-depth: tessera's `handleBanUser` flips `users.banned` and // drops sessions + OAuth tokens atomically, so a banned user normally // has no live session or token row. This predicate covers the case // where `banned` is flipped outside that path — manual D1 fixes, admin // tooling that misses the wrapper, or Better Auth's plugin // /admin/ban-user route. An expired temporary ban (`banExpires` in the // past) still allows the request, matching Better Auth's auto-clear // behavior for expired bans. // // Single source of truth so the predicate stays consistent across the // session-cookie middleware (`worker/middleware/auth.ts`) and the // Better Auth hook surfaces (`worker/auth/index.ts`: `hooks.before`, // `customTokenResponseFields`, `customAccessTokenClaims`, // `customUserInfoClaims`). export const isActiveBan = (user: BannableUser): boolean => { if (user.banned !== true) return false; if (user.banExpires == null) return true; const expiresMs = user.banExpires instanceof Date ? user.banExpires.getTime() : new Date(user.banExpires).getTime(); if (!Number.isFinite(expiresMs)) return true; return expiresMs > Date.now(); };