import type { Env } from './types'; const encoder = new TextEncoder(); const decoder = new TextDecoder(); function toArrayBuffer(bytes: Uint8Array): ArrayBuffer { return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; } function base64Url(bytes: Uint8Array): string { let binary = ''; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); } function fromBase64(value: string): Uint8Array { const normalized = value.replace(/-/g, '+').replace(/_/g, '/'); const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '='); const binary = atob(padded); return Uint8Array.from(binary, (char) => char.charCodeAt(0)); } async function hmacKey(secret: string): Promise { return crypto.subtle.importKey( 'raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify'], ); } export async function signSession(env: Env, email: string): Promise { const payload = base64Url(encoder.encode(JSON.stringify({ email, exp: Date.now() + 7 * 86400_000 }))); const signature = new Uint8Array( await crypto.subtle.sign('HMAC', await hmacKey(env.SESSION_SECRET), encoder.encode(payload)), ); return `${payload}.${base64Url(signature)}`; } export async function verifySession(env: Env, token: string | null): Promise { if (!token) return null; const [payload, signature] = token.split('.'); if (!payload || !signature) return null; const ok = await crypto.subtle.verify( 'HMAC', await hmacKey(env.SESSION_SECRET), toArrayBuffer(fromBase64(signature)), encoder.encode(payload), ); if (!ok) return null; try { const parsed = JSON.parse(decoder.decode(fromBase64(payload))) as { email?: string; exp?: number }; if (!parsed.email || !parsed.exp || parsed.exp < Date.now()) return null; return parsed.email; } catch { return null; } } async function encryptionKey(env: Env): Promise { const raw = fromBase64(env.TOKEN_ENCRYPTION_KEY); if (raw.byteLength !== 32) throw new Error('TOKEN_ENCRYPTION_KEY must decode to 32 bytes'); return crypto.subtle.importKey('raw', toArrayBuffer(raw), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']); } export async function encryptToken(env: Env, value: string): Promise { const iv = crypto.getRandomValues(new Uint8Array(12)); const ciphertext = new Uint8Array( await crypto.subtle.encrypt({ name: 'AES-GCM', iv: toArrayBuffer(iv) }, await encryptionKey(env), encoder.encode(value)), ); return `${base64Url(iv)}.${base64Url(ciphertext)}`; } export async function decryptToken(env: Env, value: string): Promise { const [ivText, ciphertextText] = value.split('.'); if (!ivText || !ciphertextText) throw new Error('Invalid encrypted token'); const plaintext = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: toArrayBuffer(fromBase64(ivText)) }, await encryptionKey(env), toArrayBuffer(fromBase64(ciphertextText)), ); return decoder.decode(plaintext); } export function randomUrlToken(bytes = 32): string { return base64Url(crypto.getRandomValues(new Uint8Array(bytes))); } export async function sha256Base64Url(value: string): Promise { const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(value))); return base64Url(digest); } export function getCookie(request: Request, key: string): string | null { const cookie = request.headers.get('Cookie') ?? ''; for (const part of cookie.split(';')) { const [name, ...rest] = part.trim().split('='); if (name === key) return decodeURIComponent(rest.join('=')); } return null; } export function sessionCookie(value: string, maxAge = 7 * 86400, secure = true): string { const securePart = secure ? '; Secure' : ''; return `meet_session=${encodeURIComponent(value)}; Path=/; HttpOnly${securePart}; SameSite=Lax; Max-Age=${maxAge}`; } export function clearSessionCookie(secure = true): string { const securePart = secure ? '; Secure' : ''; return `meet_session=; Path=/; HttpOnly${securePart}; SameSite=Lax; Max-Age=0`; }