/** * Shared helpers for newsletter API routes (Cloudflare Worker / Astro). */ import { env as workerEnv } from 'cloudflare:workers'; export type RuntimeEnv = Record; export type SubscriberStatus = 'pending' | 'active' | 'unsubscribed'; export type NewsletterSubscriber = { id: number; email: string; status: SubscriberStatus; locale: string; confirm_token: string | null; unsubscribe_token: string; source: string; created_at: string; confirmed_at: string | null; unsubscribed_at: string | null; }; /** Astro v6+: use `cloudflare:workers` env (locals.runtime.env was removed). */ export function getRuntimeEnv(_locals?: unknown): RuntimeEnv { return workerEnv as unknown as RuntimeEnv; } export function readEnv(name: string, runtimeEnv: RuntimeEnv): string { const runtimeValue = runtimeEnv[name]; if (typeof runtimeValue === 'string' && runtimeValue.trim().length > 0) { return runtimeValue.trim(); } const staticValue = import.meta.env[name]; return typeof staticValue === 'string' ? staticValue.trim() : ''; } export function getNewsletterDb(runtimeEnv: RuntimeEnv): D1Database | null { const db = runtimeEnv.NEWSLETTER_DB; if (db && typeof db === 'object' && 'prepare' in db) { return db as D1Database; } return null; } export function sanitizeRedirect(candidate: string, requestUrl: URL): string { if (!candidate) return ''; try { const u = new URL(candidate, requestUrl); if (u.origin !== requestUrl.origin) return ''; return u.pathname + u.search + u.hash; } catch { return ''; } } export function required(value: FormDataEntryValue | null): string { return typeof value === 'string' ? value.trim() : ''; } export function isValidEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && email.length <= 254; } export function normalizeEmail(email: string): string { return email.trim().toLowerCase(); } export function createToken(): string { const bytes = new Uint8Array(32); crypto.getRandomValues(bytes); return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join(''); } export async function verifyTurnstile( token: string, secret: string, ip?: string | null, ): Promise { const body = new URLSearchParams(); body.set('secret', secret); body.set('response', token); if (ip) body.set('remoteip', ip); const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body, }); if (!response.ok) return false; const payload = (await response.json()) as { success?: boolean }; return payload.success === true; } export { getEmailBinding, sendCloudflareEmail } from './email'; export function siteOrigin(requestUrl: URL, runtimeEnv: RuntimeEnv): string { const configured = readEnv('PUBLIC_SITE_URL', runtimeEnv) || readEnv('SITE', runtimeEnv); if (configured) { try { return new URL(configured).origin; } catch { /* fall through */ } } return requestUrl.origin; } export function absoluteUrl(origin: string, path: string): string { return new URL(path, origin).toString(); }