30 lines
1.8 KiB
TypeScript
30 lines
1.8 KiB
TypeScript
import { randomBytes, scrypt as derive, timingSafeEqual, createHash } from 'node:crypto';
|
|
import { promisify } from 'node:util';
|
|
const scrypt = promisify(derive);
|
|
export const ACCOUNT_COOKIE = 'wiki_account';
|
|
export const SESSION_TTL = 7 * 86400;
|
|
export function normalizeEmail(value: unknown) {
|
|
if (typeof value !== 'string') return null;
|
|
const email = value.trim().toLowerCase();
|
|
return email.length <= 254 && /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\.[a-z]{2,}$/i.test(email) ? email : null;
|
|
}
|
|
export function validPassword(value: unknown): value is string { return typeof value === 'string' && value.length >= 12 && Buffer.byteLength(value) <= 256; }
|
|
export async function hashPassword(password: string) {
|
|
const salt = randomBytes(16).toString('hex');
|
|
const hash = await scrypt(password, salt, 64) as Buffer;
|
|
return `scrypt:${salt}:${hash.toString('hex')}`;
|
|
}
|
|
export async function checkPassword(password: string, stored: string) {
|
|
const [kind,salt,hex] = stored.split(':');
|
|
if (kind !== 'scrypt' || !/^[a-f0-9]{32}$/.test(salt) || !/^[a-f0-9]{128}$/.test(hex)) return false;
|
|
return timingSafeEqual(await scrypt(password,salt,64) as Buffer,Buffer.from(hex,'hex'));
|
|
}
|
|
export function newToken() { return randomBytes(32).toString('hex'); }
|
|
export function tokenDigest(token: string) { return createHash('sha256').update(token).digest('hex'); }
|
|
export function accountCookie(token: string, clear = false) {
|
|
return `${ACCOUNT_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${clear ? 0 : SESSION_TTL}${process.env.AUTH_COOKIE_SECURE === 'false' ? '' : '; Secure'}`;
|
|
}
|
|
export function trustedOrigin(request: Request) {
|
|
try { return request.headers.get('sec-fetch-site') !== 'cross-site' && new URL(request.headers.get('origin') || '').origin === new URL(process.env.ACCOUNT_PUBLIC_ORIGIN || request.url).origin; } catch { return false; }
|
|
}
|