/* ============================================================ Shared server library. Runs on Cloudflare Pages Functions (V8 isolate, WebCrypto available). No external/AI APIs. Security-critical helpers live here. ============================================================ */ /* ---- server-side source of truth for pricing (never trust client) ---- */ /* prices in CENTS. Keep in sync with assets/js/data.js display values. */ export const PRODUCTS = { "stack-2k": { name: "$20 Stack — 2K Movie Roll", price: 5000, face: 200000 }, "stack-4k": { name: "$20 Stack — 4K Bundle", price: 10000, face: 400000 }, "stack-8k": { name: "$20 Stack — 8K Case", price: 20000, face: 800000 }, "stack-10k": { name: "$20 Stack — 10K Duffel", price: 30000, face: 1000000 }, "stack-16k": { name: "$20 Stack — 16K Pallet Pack", price: 40000, face: 1600000 }, "stack-20k": { name: "$20 Stack — 20K Vault Crate", price: 50000, face: 2000000 } }; export const SHIPPING = { "usps-standard": { label: "USPS 2–7 Day", price: 1200 }, "usps-2day": { label: "USPS 2-Day", price: 4000 }, "usps-overnight": { label: "USPS Overnight", price: 6500 } }; /* ---- security headers applied to every response ---- */ export function securityHeaders() { return { "Content-Security-Policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " + "img-src 'self' data:; font-src 'self'; connect-src 'self'; " + "frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'", "X-Frame-Options": "DENY", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "strict-origin-when-cross-origin", "Permissions-Policy": "geolocation=(), microphone=(), camera=(), payment=(), usb=()", "Strict-Transport-Security": "max-age=31536000; includeSubDomains", "Cross-Origin-Opener-Policy": "same-origin", "X-Robots-Tag": "noindex" // overridden per-page for public pages via _headers }; } export function json(data, status = 200, extraHeaders = {}) { return new Response(JSON.stringify(data), { status, headers: Object.assign( { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }, extraHeaders ) }); } /* ---- request helpers ---- */ export function clientIP(request) { return request.headers.get("CF-Connecting-IP") || request.headers.get("X-Forwarded-For") || "0.0.0.0"; } export function country(request) { return (request.cf && request.cf.country) || request.headers.get("CF-IPCountry") || ""; } /* ---- input hygiene ---- */ export function str(v, max = 200) { if (v == null) return ""; return String(v).replace(/[\u0000-\u001F\u007F]/g, "").trim().slice(0, max); } export function isEmail(v) { return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v); } export function clampInt(v, lo, hi) { v = parseInt(v, 10); if (isNaN(v)) v = lo; return Math.max(lo, Math.min(hi, v)); } /* ---- crypto ---- */ const enc = new TextEncoder(); export async function sha256Hex(text) { const buf = await crypto.subtle.digest("SHA-256", enc.encode(text)); return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, "0")).join(""); } export function timingSafeEqual(a, b) { if (typeof a !== "string" || typeof b !== "string" || a.length !== b.length) return false; let out = 0; for (let i = 0; i < a.length; i++) out |= a.charCodeAt(i) ^ b.charCodeAt(i); return out === 0; } /* base64url helpers */ function b64url(bytes) { let s = btoa(String.fromCharCode(...new Uint8Array(bytes))); return s.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function b64urlStr(text) { return b64url(enc.encode(text)); } async function hmac(secret, msg) { const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); const sig = await crypto.subtle.sign("HMAC", key, enc.encode(msg)); return b64url(sig); } /* ---- signed sessions (HMAC-signed, delivered as HttpOnly cookies) ---- Payload carries the username (u) and role (r). The browser can NEVER read these (HttpOnly); it only replays the cookie, and the server re-derives the role from it. Admin and ops ride in SEPARATE cookies (bb_a1 / bb_a2) so one staff role's session is a different variable the other never touches. */ /* The signing secret MUST exist and be strong. If it is missing or weak, we refuse to issue OR accept any session — the whole auth system fails CLOSED instead of signing tokens with a guessable key an attacker could forge. */ export function sessionSecretOk(env) { const s = env && env.SESSION_SECRET; return typeof s === "string" && s.length >= 24; } export async function makeSession(env, user, role = "customer", ttlSec = 60 * 60 * 8) { if (!sessionSecretOk(env)) return null; // caller must treat null as "cannot sign in" const payload = JSON.stringify({ u: user, r: role, exp: Math.floor(Date.now() / 1000) + ttlSec }); const body = b64urlStr(payload); const sig = await hmac(env.SESSION_SECRET, body); return body + "." + sig; } export async function verifySession(env, token) { if (!sessionSecretOk(env)) return null; // no valid secret → no valid session if (!token || token.indexOf(".") === -1) return null; const [body, sig] = token.split("."); const expect = await hmac(env.SESSION_SECRET, body); if (!timingSafeEqual(sig, expect)) return null; let data; try { data = JSON.parse(atob(body.replace(/-/g, "+").replace(/_/g, "/"))); } catch (e) { return null; } if (!data || !data.exp || data.exp < Math.floor(Date.now() / 1000)) return null; return data; } /* Cookie names — intentionally distinct per role so they stay isolated. bb_a1 = admin (full dashboard), bb_a2 = ops (operations), bb_user = customer */ export const COOKIE_ADMIN = "bb_a1"; export const COOKIE_OPS = "bb_a2"; export const COOKIE_USER = "bb_user"; function cookieFor(name, token, ttlSec) { return `${name}=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${ttlSec}`; } function clearNamed(name) { return `${name}=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0`; } export function roleCookie(role, token, ttlSec = 60 * 60 * 8) { if (role === "admin") return cookieFor(COOKIE_ADMIN, token, ttlSec); if (role === "ops") return cookieFor(COOKIE_OPS, token, ttlSec); return cookieFor(COOKIE_USER, token, 60 * 60 * 24 * 30); } export function clearAllSessionCookies() { return [clearNamed(COOKIE_ADMIN), clearNamed(COOKIE_OPS), clearNamed(COOKIE_USER)]; } export function readCookie(request, name) { const c = request.headers.get("Cookie") || ""; const m = c.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]+)")); return m ? m[1] : null; } /* CSRF defense: a cross-site