← back to CelebritySignatures
server.js
1227 lines
#!/usr/bin/env node
// Zero-dependency server for the CelebritySignatures grid + murals storefront.
// Adds lightweight email/password accounts (scrypt + cookie sessions, JSON-backed)
// so a visitor can SAVE a mural placement they set in the wall studio.
import { createServer } from 'node:http';
import { readFile, writeFile, appendFile, mkdir, unlink } from 'node:fs/promises';
import { extname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { scryptSync, randomBytes, timingSafeEqual, createHash, sign } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { spawn } from 'node:child_process';
const ROOT = fileURLToPath(new URL('.', import.meta.url));
const PORT = process.env.PORT || 9920;
const DATA = join(ROOT, 'data');
const TYPES = {
'.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css',
'.json': 'application/json', '.svg': 'image/svg+xml',
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
'.webp': 'image/webp', '.gif': 'image/gif', '.ico': 'image/x-icon',
};
// ---- tiny JSON store --------------------------------------------------------
async function load(name, fallback) { try { return JSON.parse(await readFile(join(DATA, name), 'utf8')); } catch { return fallback; } }
async function store(name, val) { await writeFile(join(DATA, name), JSON.stringify(val, null, 2)); }
function readBody(req) {
return new Promise((resolve, reject) => {
let raw = '';
req.on('data', c => { raw += c; if (raw.length > 2e5) reject(new Error('too large')); });
req.on('end', () => { try { resolve(raw ? JSON.parse(raw) : {}); } catch { reject(new Error('bad json')); } });
req.on('error', reject);
});
}
function sendJSON(res, code, obj, headers = {}) { res.writeHead(code, { 'Content-Type': 'application/json', ...headers }); res.end(JSON.stringify(obj)); }
// larger body reader for signature uploads (base64 data URLs; ~4MB cap)
function readBodyBig(req) {
return new Promise((resolve, reject) => {
let raw = '';
req.on('data', c => { raw += c; if (raw.length > 4.5e6) reject(new Error('too large')); });
req.on('end', () => { try { resolve(raw ? JSON.parse(raw) : {}); } catch { reject(new Error('bad json')); } });
req.on('error', reject);
});
}
// ---- celebrity signature uploads (owned, tracked, NEVER shared raw) ---------
// Files live in data/uploads-private/ (blocked from static serving); the ONLY
// way a file leaves the server is the tracked /api/signature-file endpoint,
// which appends a commission entry to data/download-ledger.jsonl per download.
const UPLOADS_DIR = join(DATA, 'uploads-private');
const LB_HITS = new Map(); // per-IP leaderboard POST timestamps (rate limit)
// Stripe key resolver. TEST by default. LIVE (real money) is used ONLY when
// BOTH STRIPE_LIVE_ENABLED=1 AND a sk_live_ key are present — the deliberate
// Steve-gated go-live switch. Default (flag unset) = test mode, so real cards
// are never charged unless Steve explicitly turns it on.
function envVal(name) {
if (process.env[name]) return process.env[name];
try {
const m = readFileSync(new URL('.env', import.meta.url), 'utf8').match(new RegExp('^' + name + '=(.+)$', 'm'));
if (m) return m[1].trim().replace(/^["']|["']$/g, '');
} catch {}
return null;
}
const STRIPE_LIVE_ENABLED = envVal('STRIPE_LIVE_ENABLED') === '1';
const _testKey = (() => { const k = envVal('STRIPE_TEST_SECRET_KEY'); return k && k.startsWith('sk_test_') ? k : null; })();
const _liveKey = (() => { const k = envVal('STRIPE_LIVE_SECRET_KEY'); return k && /^(sk|rk)_live_/.test(k) ? k : null; })(); // rk_ = a scoped restricted live key (least-privilege)
const STRIPE_LIVE = STRIPE_LIVE_ENABLED && _liveKey; // real-money mode active?
// PER-FLOW keys (Steve 2026-08-04: "go live on murals only, keep downloads in
// test until payouts exist"). MURALS charge real cards once the live switch is
// on; DOWNLOADS stay pinned to the TEST key until a celebrity-payout mechanism
// exists — so flipping the live switch can NEVER accidentally take real money
// for a download whose owner we cannot yet pay their commission.
const STRIPE_MURAL_KEY = STRIPE_LIVE ? _liveKey : _testKey;
const STRIPE_DOWNLOAD_KEY = _testKey;
const STRIPE_MODE = STRIPE_LIVE ? 'live' : 'test';
// WEAR gets its OWN live switch, deliberately separate from STRIPE_LIVE_ENABLED
// (Steve 2026-09-09, TK-10286): murals/downloads already flipped the shared
// flag live for an unrelated feature, so if wear reused STRIPE_MURAL_KEY, the
// moment WEAR_SALES_LIVE=1 landed it would charge REAL cards with zero chance
// to place a test order first. WEAR_STRIPE_LIVE_ENABLED lets Steve turn
// everything else about /wear on (Printify mapped, checkout functional, cart
// working) while still charging the TEST key — then flip this ONE additional
// switch, independently, once a real end-to-end test order looks right.
const WEAR_STRIPE_LIVE_ENABLED = envVal('WEAR_STRIPE_LIVE_ENABLED') === '1';
const WEAR_STRIPE_LIVE = WEAR_STRIPE_LIVE_ENABLED && _liveKey;
const WEAR_STRIPE_KEY = WEAR_STRIPE_LIVE ? _liveKey : _testKey;
const WEAR_STRIPE_MODE = WEAR_STRIPE_LIVE ? 'live' : 'test';
if (STRIPE_LIVE) console.log('⚠️ STRIPE LIVE MODE — murals charge REAL cards; downloads stay TEST'); else if (_testKey) console.log('Stripe test mode active (murals + downloads)');
if (WEAR_STRIPE_LIVE) console.log('⚠️ WEAR STRIPE LIVE MODE — apparel charges REAL cards'); else if (_testKey) console.log('Wear apparel: Stripe test mode (independent of the murals live flag)');
// ---- Google AdSense (Auto ads) ---------------------------------------------
// Driven by ONE env var, ADSENSE_PUB_ID (stored as `pub-XXXXXXXXXXXXXXXX`; a
// leading `ca-` is tolerated). When set, the loader is injected into every HTML
// response and /ads.txt is served; when unset, everything no-ops so nothing
// broken renders. Auto ads means Google places units itself — no per-slot IDs.
const ADSENSE_PUB_ID = (envVal('ADSENSE_PUB_ID') || '').trim().replace(/^ca-/, '');
const ADSENSE_TAG = /^pub-\d{10,}$/.test(ADSENSE_PUB_ID)
? `<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-${ADSENSE_PUB_ID}" crossorigin="anonymous"></script>`
: '';
// Hand-placed reserved ad ZONES (better fill than Auto ads alone on a dense
// grid). Each zone stays hidden — zero layout impact — until its per-zone slot
// id is set via env (post-approval: create the units in AdSense → paste the
// data-ad-slot numbers here). assets/ads.js reads this config and activates
// only the zones that have a real slot id, reserving height to avoid CLS.
const AD_SLOTS = { gallery: (envVal('ADSENSE_SLOT_GALLERY') || '').trim(), article: (envVal('ADSENSE_SLOT_ARTICLE') || '').trim(), game: (envVal('ADSENSE_SLOT_GAME') || '').trim() };
const ADSENSE_CONFIG = ADSENSE_TAG
? `<script>window.__ADPUB=${JSON.stringify('ca-' + ADSENSE_PUB_ID)};window.__ADSLOTS=${JSON.stringify(AD_SLOTS)};</script><script src="/assets/ads.js" defer></script>`
: '';
const _injectAdTags = html => (ADSENSE_TAG && typeof html === 'string' && html.includes('</head>') && !html.includes('adsbygoogle.js'))
? html.replace('</head>', ADSENSE_TAG + ADSENSE_CONFIG + '\n</head>') : html;
// Site-wide footer — injected before </body> on every HTML response so the
// AdSense-required Privacy Policy link is discoverable on every page (Google
// rejects sites whose policy isn't linked). Kept in the ad chokepoint so all
// four page-serving call sites inherit it with no per-site edits.
const FOOTER = '<footer class="site-footer" style="margin:40px 0 0;padding:22px 16px;border-top:1px solid #e6e3dc;background:#f7f5f0;color:#6b6b6b;font:400 13px/1.7 -apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;text-align:center">'
+ '<div>© 2026 CelebritySignatures · celebsignatures.com</div>'
+ '<nav style="margin-top:6px"><a href="/" style="color:#6b6b6b;text-decoration:none">Home</a> · '
+ '<a href="/game" style="color:#6b6b6b;text-decoration:none">Game</a> · '
+ '<a href="/wallpaper" style="color:#6b6b6b;text-decoration:none">Wallpaper</a> · '
+ '<a href="/privacy" style="color:#6b6b6b;text-decoration:none">Privacy Policy</a> · '
+ '<a href="mailto:info@designerwallcoverings.com" style="color:#6b6b6b;text-decoration:none">Contact</a></nav></footer>';
const injectFooter = html => (typeof html === 'string' && html.includes('</body>') && !html.includes('site-footer'))
? html.replace('</body>', FOOTER + '\n</body>') : html;
const injectAds = html => injectFooter(_injectAdTags(html));
if (ADSENSE_TAG) { const live = Object.entries(AD_SLOTS).filter(([, v]) => v).map(([k]) => k); console.log(`AdSense active (${ADSENSE_PUB_ID}) — reserved zones live: ${live.length ? live.join(', ') : 'none yet (awaiting slot ids)'}`); }
// Atomic in-process guard against concurrent double-credit for the same paid session
// (claimed synchronously before any await; the download-ledger is the restart-surviving backstop).
const USED_SIDS = new Set();
// Merged signatures feed (used by /api/signatures, the crawlable /a/:qid pages,
// and the sitemap). The server-side occupation gate keeps the Artists category
// clean regardless of what a build/merge pipeline writes to artists.json.
const qidOf = r => (r.wikidata || '').split('/').pop();
const ART_OCC = /(paint|sculpt|printmak|draughts|illustrat|photograph|architect|designer|engrav|\bartist\b|animat|ceramic|etcher|watercolo|muralist|lithograph|graphic|calligraph|craft|goldsmith|jewel|potter|weav|textile|cartoonist)/;
const ART_OCC_EXCLUDE = /(fashion designer|costume designer|couturier|game designer|sound designer|web designer)/;
const isArtOcc = o => { const s = String(o).toLowerCase(); return ART_OCC.test(s) && !ART_OCC_EXCLUDE.test(s); };
let _sigCache = null, _sigCacheAt = 0;
// Permanently excluded from EVERY surface (feed, grid, wear, sitemap) — brand safety (Steve 2026-09-09).
const SIG_EXCLUDE_QIDS = new Set(['Q352']); // Q352 = Adolf Hitler
async function mergedSignatures() {
if (_sigCache && Date.now() - _sigCacheAt < 60000) return _sigCache; // 60s cache
const base = await load('celebrity_signatures.json', []);
const artists = await load('artists.json', []);
const authors = await load('authors.json', []);
const raw = await load('artists-raw.json', []);
const artistQids = new Set(raw.filter(r => (r.occupations || []).some(isArtOcc)).map(qidOf));
const cleanArtists = raw.length ? artists.filter(a => artistQids.has(qidOf(a))) : artists;
const seenQ = new Set([...base.map(qidOf), ...cleanArtists.map(qidOf)]);
const cleanAuthors = authors.filter(a => !seenQ.has(qidOf(a)));
_sigCache = [...base.filter(r => r.category !== 'Artists' && r.category !== 'Authors'), ...cleanArtists, ...cleanAuthors].filter(r => !SIG_EXCLUDE_QIDS.has(qidOf(r)));
_sigCacheAt = Date.now();
return _sigCache;
}
// ---- /wear: trademark-cleared signature apparel ----------------------------
// A signature may be offered on apparel ONLY if it is LEGALLY CLEAR (Steve, hard
// rule 2026-08-06): commercially usable, not high-risk, AND its name returned a
// CLEAR trademark verdict against a COMPLETE screen of the USPTO register. A
// seed-only / partial index never makes anything sellable — absence of a match is
// not trust; it must be an affirmative clear from real register data.
let _tmCache = null, _tmCacheAt = 0;
async function tmClearance() {
if (_tmCache && Date.now() - _tmCacheAt < 60000) return _tmCache;
_tmCache = await load('tm-clearance.json', { indexComplete: false, indexSource: 'none', verdicts: {}, tally: {} });
_tmCacheAt = Date.now();
return _tmCache;
}
let _wearTplCache = null, _wearTplAt = 0;
async function wearTemplates() {
if (_wearTplCache && Date.now() - _wearTplAt < 60000) return _wearTplCache;
_wearTplCache = await load('wear-templates.json', { garments: [] });
_wearTplAt = Date.now();
return _wearTplCache;
}
const WEAR_POD_PROVIDERS = new Set(['printful', 'printify']);
function wearPodProvider() {
const provider = String(envVal('WEAR_POD_PROVIDER') || 'printful').toLowerCase();
return WEAR_POD_PROVIDERS.has(provider) ? provider : null;
}
function wearPodReady(tpl, provider) {
if (!provider) return false;
const credential = provider === 'printify' ? envVal('PRINTIFY_API_TOKEN') : envVal('PRINTFUL_API_KEY');
if (!credential) return false;
// Printify's sender (scripts/submit-pod-draft-printify.mjs) refuses to POST
// unless it ALSO has a shop id + an explicit manual-approval acknowledgement
// (5 gates total). Checkout readiness must mirror that, or a paid order can
// sit forever as a DRAFT_UNSENT the sender will never touch.
if (provider === 'printify') {
if (!envVal('PRINTIFY_SHOP_ID')) return false;
if (envVal('PRINTIFY_MANUAL_APPROVAL_CONFIRMED') !== '1') return false;
}
const offered = (tpl.garments || []).filter(g => g.saleEnabled !== false);
return offered.length > 0 && offered.every(g => {
const mapping = g[provider] || {};
if (provider === 'printify' && (!mapping.blueprintId || !mapping.printProviderId)) return false;
return (g.colors || []).every(c => (g.sizes || []).every(size => mapping.variants?.[`${c.id}/${size}`]));
});
}
// v1 (2026-08-10, Steve GO — TK-10286): the USPTO live-trademark auto-screen is DEFERRED.
// A signature is sellable on apparel only if it is a clearly PUBLIC-DOMAIN HISTORICAL figure
// — deceased on/before WEAR_PD_DEATH_CUTOFF — where there is no live apparel-trademark or
// estate-licensing risk (Jefferson, Franklin, da Vinci…). Living / modern figures stay
// excluded. When the USPTO ODP key lands, swap wearEligible → wearEligibleTM below.
const WEAR_PD_DEATH_CUTOFF = 1900;
const wearDeathYear = sig => { const y = parseInt(String((sig && sig.death_date) || '').slice(0, 4), 10); return isNaN(y) ? null : y; };
// The single source of truth for "can this signature be sold on a shirt?"
function wearEligible(sig, clr) {
if (!sig || sig.usable_in_commercial_collage !== 'yes') return false;
if (sig.risk_level === 'high') return false; // safe + medium only
const dy = wearDeathYear(sig);
return dy !== null && dy <= WEAR_PD_DEATH_CUTOFF; // public-domain historical only
}
// Preserved for when the USPTO ODP key lands — the stricter live-register screen.
function wearEligibleTM(sig, clr) {
if (!sig || sig.usable_in_commercial_collage !== 'yes' || sig.risk_level === 'high') return false;
if (!clr || clr.indexComplete !== true) return false;
const v = clr.verdicts[sig.full_name];
return !!v && v.verdict === 'CLEAR';
}
async function wearSignatures() {
const [sigs, clr] = await Promise.all([mergedSignatures(), tmClearance()]);
return sigs.filter(s => s.signature_image_url && wearEligible(s, clr));
}
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const DEFAULT_PRICE_USD = 20; // download price (payments wired later — Steve-gated)
const DEFAULT_COMMISSION_PCT = 50; // owner's cut per download
const WEAR_GARMENT_DEFAULT_PRICE_USD = 34; // fallback garment price when template omits priceUsd
const WEAR_ORDER_ID_SEED = 5000; // wear orders start above 5000 to avoid colliding with mural/download IDs
const WEAR_CART_MAX_ITEMS = 20; // sane cap on a single checkout — not a real limit concern, just guards against abuse
// Orders placed before the cart feature (2026-09-08) stored one item's fields
// directly on the order (qid/garment/color/size/...); the cart shape stores
// an items[] array instead. This normalizes either shape to items[] so
// /wear-success and the reconciliation script don't need two code paths.
function wearOrderItems(order) {
if (Array.isArray(order.items)) return order.items;
if (!order.qid) return [];
return [{ qid: order.qid, signature_name: order.signature_name, garment: order.garment, garment_label: order.garment_label,
color: order.color, color_label: order.color_label, size: order.size, placement: order.placement }];
}
// Mirrors public/wear.html's client-side luminance() — decides whether a
// garment color is dark enough that the source signature art (dark ink)
// needs recoloring to a light ink before it's actually printed.
const wearLuminance = hex => {
const n = parseInt(hex.slice(1), 16), r = (n>>16)&255, g = (n>>8)&255, b = n&255;
return (0.299*r + 0.587*g + 0.114*b) / 255;
};
// ---- per-signer ink color (Node port of public/assets/signature-ink.js) -----
// MUST stay in lockstep with the client inkColorForHue: same FNV-1a hash, same
// HSL, same WCAG-3:1-on-white contrast guarantee — so the color baked into the
// PRINTED art matches what the buyer saw in the on-shirt preview and the swatch.
const _hslToRgb = (h, s, l) => {
if (s === 0) { const v = Math.round(l*255); return [v,v,v]; }
const q = l<0.5 ? l*(1+s) : l+s-l*s, p = 2*l-q;
const hk = t => { t=(t%1+1)%1;
if (t<1/6) return p+(q-p)*6*t;
if (t<1/2) return q;
if (t<2/3) return p+(q-p)*(2/3-t)*6;
return p; };
return [Math.round(hk(h+1/3)*255), Math.round(hk(h)*255), Math.round(hk(h-1/3)*255)];
};
const _relLuminance = ([r,g,b]) => {
const f = c => { c/=255; return c<=0.03928 ? c/12.92 : ((c+0.055)/1.055)**2.4; };
return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b);
};
const INK_MAX_LUMINANCE = 0.26;
const _rgbToHex = ([r,g,b]) => '#'+[r,g,b].map(v=>v.toString(16).padStart(2,'0')).join('');
function inkColorHexFor(key) {
let h = 2166136261; const s = String(key||'');
for (let i=0;i<s.length;i++){ h^=s.charCodeAt(i); h=Math.imul(h,16777619); }
const hue = (h>>>0)%360;
let l = 0.42, rgb = _hslToRgb(hue/360, 0.62, l);
while (_relLuminance(rgb) > INK_MAX_LUMINANCE && l > 0.14) { l -= 0.02; rgb = _hslToRgb(hue/360, 0.62, l); }
return _rgbToHex(rgb);
}
// Sanitize a client-supplied ink color before it can reach ImageMagick's -fill;
// only a literal #rrggbb is allowed, anything else falls back to the auto color.
const safeInkHex = v => (typeof v === 'string' && /^#[0-9a-fA-F]{6}$/.test(v)) ? v.toLowerCase() : null;
const RECOLOR_DIR = join(ROOT, 'public', 'assets', 'recolored-signatures');
const WEAR_SITE_ORIGIN = 'https://celebsignatures.com';
// Real print fix for the KNOWN ISSUE noted in wear-templates.json: Printify
// prints a design's own pixel colors as-is (no auto-recolor for dark
// garments), so a dark-ink signature on a dark colorway (e.g. the polo's
// black) would print near-invisible. For orders on a dark colorway, recolor
// the signature to a light ink — mirroring the on-site preview's own
// recolorToLight() — via ImageMagick (already on this box for other
// scripts), cached by URL hash so a re-ordered signature isn't reprocessed.
// FAILS OPEN: any error (magick missing, fetch failure, timeout) falls back
// to the original image rather than blocking order/draft creation — a
// slightly-low-contrast print is a quality issue, not a reason to lose the
// customer's order record.
async function recolorSignatureInk(imageUrl, hex) {
const ink = safeInkHex(hex) || '#f5f3ee'; // fall back to the old light ink
try {
const hash = createHash('sha256').update(imageUrl + '|' + ink).digest('hex').slice(0, 24);
const outPath = join(RECOLOR_DIR, `${hash}.png`);
// FULLY QUALIFIED — this URL gets set as design_image_url on the draft,
// which flows straight into the Printify order payload as images[0].src;
// Printify fetches it from the public internet, so a relative path would
// fail there (caught via a2a from codex-run-10286, 2026-09-09).
const outUrl = `${WEAR_SITE_ORIGIN}/assets/recolored-signatures/${hash}.png`;
try { await readFile(outPath); return outUrl; } catch {} // cache hit
const src = await fetch(imageUrl);
if (!src.ok) throw new Error(`fetch ${src.status}`);
const buf = Buffer.from(await src.arrayBuffer());
const out = await new Promise((resolve, reject) => {
// 'convert' not 'magick': the Kamatera prod box only has ImageMagick 6
// (convert), not the IMv7 'magick' unified CLI — verified via SSH.
// 'convert' also exists on Mac2 (IMv7's back-compat shim, just prints a
// deprecation warning to stderr, which this doesn't treat as failure).
const proc = spawn('convert', ['-background', 'none', '-', '-channel', 'RGB', '-fill', ink, '-colorize', '100%', 'png:-']);
const chunks = []; let errText = '';
const timer = setTimeout(() => { proc.kill(); reject(new Error('magick timeout')); }, 8000);
proc.stdout.on('data', c => chunks.push(c));
proc.stderr.on('data', c => errText += c);
proc.on('error', e => { clearTimeout(timer); reject(e); });
proc.on('close', code => { clearTimeout(timer); code === 0 ? resolve(Buffer.concat(chunks)) : reject(new Error(errText || `magick exit ${code}`)); });
proc.stdin.end(buf);
});
await mkdir(RECOLOR_DIR, { recursive: true });
await writeFile(outPath, out);
return outUrl;
} catch (e) {
console.error('recolorSignatureInk failed, using original image:', e.message);
return imageUrl;
}
}
async function adminToken() {
// Bootstrap a local admin token on first use (data/admin-token.txt, gitignored
// + deploy-protected). Steve reads it from the file to approve uploads.
try { return (await readFile(join(DATA, 'admin-token.txt'), 'utf8')).trim(); }
catch {
const t = randomBytes(18).toString('hex');
await writeFile(join(DATA, 'admin-token.txt'), t + '\n');
console.log('upload admin token created → data/admin-token.txt');
return t;
}
}
// ---- auth helpers -----------------------------------------------------------
const COOKIE = 'cs_sess';
function hashPw(pw, salt) { return scryptSync(pw, salt, 64).toString('hex'); }
function verifyPw(pw, salt, hash) {
const a = Buffer.from(hashPw(pw, salt), 'hex'), b = Buffer.from(hash, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}
function parseCookies(req) {
const out = {}; (req.headers.cookie || '').split(';').forEach(p => { const i = p.indexOf('='); if (i > 0) out[p.slice(0, i).trim()] = decodeURIComponent(p.slice(i + 1).trim()); });
return out;
}
async function currentUser(req) {
const sessions = await load('sessions.json', {});
const tok = parseCookies(req)[COOKIE];
const s = tok && sessions[tok];
if (!s) return null;
const users = await load('users.json', []);
return users.find(u => u.id === s.userId) || null;
}
async function newSession(userId) {
const sessions = await load('sessions.json', {});
const tok = randomBytes(24).toString('hex');
sessions[tok] = { userId, at: new Date().toISOString() };
await store('sessions.json', sessions);
return tok;
}
const setCookie = tok => `${COOKIE}=${tok}; HttpOnly; Path=/; SameSite=Lax; Max-Age=2592000`;
const clearCookie = `${COOKIE}=; HttpOnly; Path=/; Max-Age=0`;
const pubUser = u => ({ email: u.email, name: u.name || '' });
// ---- Sign in with Apple (web OAuth bridged into the app's WebView) ----------
// Celeb's iOS app is a hybrid WebView; Apple/Google reject OAuth INSIDE an
// embedded WebView, so the app runs sign-in in ASWebAuthenticationSession and
// replays a one-time token at /auth/handoff so the cs_sess cookie lands in the
// WebView's jar (the proven Charge & Explore pattern). Fully DORMANT until the
// Apple Services ID + sign-in key are configured — /api/auth-config reports it,
// and the site hides the button until then, so nothing shows broken.
const APPLE_TEAM_ID = process.env.APPLE_TEAM_ID || '';
const APPLE_SERVICES_ID = process.env.APPLE_SERVICES_ID || ''; // e.g. com.abrams.celebsignatures.signin
const APPLE_KEY_ID = process.env.APPLE_KEY_ID || '';
const APPLE_PRIVATE_KEY = process.env.APPLE_PRIVATE_KEY
|| (process.env.APPLE_PRIVATE_KEY_PATH
? (() => { try { return readFileSync(process.env.APPLE_PRIVATE_KEY_PATH, 'utf8'); } catch { return ''; } })()
: '');
const APPLE_REDIRECT_URI = process.env.APPLE_REDIRECT_URI || 'https://celebsignatures.com/auth/apple/callback';
const APPLE_CONFIGURED = !!(APPLE_TEAM_ID && APPLE_SERVICES_ID && APPLE_KEY_ID && APPLE_PRIVATE_KEY);
const APP_SCHEME = 'celebsignatures';
const b64u = s => Buffer.from(s).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
// Apple client_secret: a short-lived ES256 JWT signed with the .p8 sign-in key.
function appleClientSecret() {
const now = Math.floor(Date.now() / 1000);
const head = b64u(JSON.stringify({ alg: 'ES256', kid: APPLE_KEY_ID }));
const body = b64u(JSON.stringify({ iss: APPLE_TEAM_ID, iat: now, exp: now + 60 * 30, aud: 'https://appleid.apple.com', sub: APPLE_SERVICES_ID }));
const sig = sign('sha256', Buffer.from(`${head}.${body}`), { key: APPLE_PRIVATE_KEY, dsaEncoding: 'ieee-p1363' });
return `${head}.${body}.${b64u(sig)}`;
}
const appleStates = new Map(); // state -> { app, exp } — short-lived CSRF/flow state
const handoffTokens = new Map(); // token -> { setCookie, location, exp } — single-use native bridge
const HANDOFF_TTL_MS = 5 * 60 * 1000;
// Finish an Apple login: native app flow → stash the Set-Cookie under a one-time
// token + deep-link it back; web flow → set the cookie + 302.
function completeAppleLogin(res, cookieHeader, appFlow, location = '/') {
if (appFlow) {
const token = randomBytes(24).toString('hex');
handoffTokens.set(token, { setCookie: cookieHeader, location, exp: Date.now() + HANDOFF_TTL_MS });
res.writeHead(302, { location: `${APP_SCHEME}://auth?token=${token}` }); res.end(); return;
}
res.writeHead(302, { 'Set-Cookie': cookieHeader, location }); res.end();
}
function readRaw(req) {
return new Promise((resolve, reject) => {
let raw = '';
req.on('data', c => { raw += c; if (raw.length > 2e5) reject(new Error('too large')); });
req.on('end', () => resolve(raw));
req.on('error', reject);
});
}
// Find-or-create a Celeb account from a verified Apple identity (keyed by sub).
async function upsertAppleUser(sub, email, name) {
const users = await load('users.json', []);
const appleSub = `apple:${sub}`;
let u = users.find(x => x.appleSub === appleSub) || (email && users.find(x => x.email === email));
if (!u) {
u = {
id: createHash('sha256').update(appleSub + Date.now()).digest('hex').slice(0, 16),
email: email || `${sub}@privaterelay.appleid.com`,
name: name || '',
appleSub,
at: new Date().toISOString(),
};
users.push(u); await store('users.json', users);
} else if (name && !u.name) {
u.name = name; await store('users.json', users);
}
return u;
}
createServer(async (req, res) => {
try {
const url = new URL(req.url, 'http://x');
let path = decodeURIComponent(url.pathname);
const M = req.method;
// ===== sensitive-path guard =====
// data/ (accounts, sessions, ledgers, private uploads) and scripts/ are
// NEVER directly fetchable; public JSON flows only through /api/* routes.
// Block data/, scripts/, tmp_*, and dotfiles — EXCEPT /.well-known/ (Apple's
// Sign in with Apple domain-association file is served from there).
if (/^\/(data|scripts|tmp_)/.test(path)
|| (path.startsWith('/.') && !path.startsWith('/.well-known/'))
|| path.includes('..')) {
res.writeHead(403, { 'Content-Type': 'text/plain' }).end('forbidden'); return;
}
// ===== AUTH =====
if (path === '/api/auth/signup' && M === 'POST') {
const b = await readBody(req);
const email = String(b.email || '').trim().toLowerCase();
const pw = String(b.password || '');
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return sendJSON(res, 400, { ok: false, error: 'invalid email' });
if (pw.length < 6) return sendJSON(res, 400, { ok: false, error: 'password must be at least 6 characters' });
const users = await load('users.json', []);
if (users.some(u => u.email === email)) return sendJSON(res, 409, { ok: false, error: 'account already exists — sign in instead' });
const salt = randomBytes(16).toString('hex');
const user = { id: createHash('sha256').update(email + Date.now()).digest('hex').slice(0, 16), email, name: String(b.name || '').slice(0, 80), salt, hash: hashPw(pw, salt), at: new Date().toISOString() };
users.push(user); await store('users.json', users);
const tok = await newSession(user.id);
return sendJSON(res, 200, { ok: true, user: pubUser(user) }, { 'Set-Cookie': setCookie(tok) });
}
if (path === '/api/auth/login' && M === 'POST') {
const b = await readBody(req);
const email = String(b.email || '').trim().toLowerCase();
const users = await load('users.json', []);
const u = users.find(x => x.email === email);
if (!u || !verifyPw(String(b.password || ''), u.salt, u.hash)) return sendJSON(res, 401, { ok: false, error: 'wrong email or password' });
const tok = await newSession(u.id);
return sendJSON(res, 200, { ok: true, user: pubUser(u) }, { 'Set-Cookie': setCookie(tok) });
}
if (path === '/api/auth/logout' && M === 'POST') {
const sessions = await load('sessions.json', {});
const tok = parseCookies(req)[COOKIE];
if (tok) { delete sessions[tok]; await store('sessions.json', sessions); }
return sendJSON(res, 200, { ok: true }, { 'Set-Cookie': clearCookie });
}
if (path === '/api/auth/me' && M === 'GET') {
const u = await currentUser(req);
return sendJSON(res, 200, u ? { ok: true, user: pubUser(u) } : { ok: true, user: null });
}
// Account deletion — Apple App Store Guideline 5.1.1(v). Cookie-authed via the
// same currentUser(req) mechanism as the rest of /api/auth/*. Removes the user
// and ALL their data (sessions, saved placements, uploaded signature files),
// then clears the session cookie exactly like /api/auth/logout.
if (path === '/api/auth/delete' && M === 'POST') {
const u = await currentUser(req);
if (!u) return sendJSON(res, 401, { ok: false, error: 'not signed in' });
const uid = u.id;
// a. drop the user object
const users = await load('users.json', []);
await store('users.json', users.filter(x => x.id !== uid));
// b. drop every session belonging to this user
const sessions = await load('sessions.json', {});
for (const [tok, s] of Object.entries(sessions)) { if (s && s.userId === uid) delete sessions[tok]; }
await store('sessions.json', sessions);
// c. drop this user's saved placements
const saves = await load('saves.json', []);
await store('saves.json', saves.filter(s => s.userId !== uid));
// d. delete this user's private upload files (best-effort), then drop the rows
const ups = await load('celebrity-uploads.json', []);
for (const x of ups.filter(e => e.userId === uid)) {
if (x.file) { try { await unlink(join(UPLOADS_DIR, x.file)); } catch {} }
}
await store('celebrity-uploads.json', ups.filter(e => e.userId !== uid));
// e. clear the session cookie (same header the logout route uses)
return sendJSON(res, 200, { ok: true }, { 'Set-Cookie': clearCookie });
}
// ===== SIGN IN WITH APPLE (web OAuth, bridged into the app's WebView) =====
// Which sign-in methods are live — the site hides the Apple button until its
// creds are configured, so a dormant deploy never shows a broken button.
if (path === '/api/auth-config' && M === 'GET') {
return sendJSON(res, 200, { ok: true, apple: APPLE_CONFIGURED });
}
// Kick off Apple sign-in. The iOS app appends ?app=1 and runs this in
// ASWebAuthenticationSession; the web appends nothing and runs it inline.
if (path === '/auth/apple/login' && M === 'GET') {
if (!APPLE_CONFIGURED) return sendJSON(res, 503, { ok: false, error: 'Sign in with Apple is not configured yet' });
const state = randomBytes(16).toString('hex');
appleStates.set(state, { app: url.searchParams.get('app') === '1', exp: Date.now() + 10 * 60 * 1000 });
const a = new URL('https://appleid.apple.com/auth/authorize');
a.searchParams.set('response_type', 'code');
a.searchParams.set('client_id', APPLE_SERVICES_ID);
a.searchParams.set('redirect_uri', APPLE_REDIRECT_URI);
a.searchParams.set('scope', 'name email');
a.searchParams.set('response_mode', 'form_post');
a.searchParams.set('state', state);
res.writeHead(302, { location: a.toString() }); return res.end();
}
// Apple posts the code back here (form_post). Exchange it, verify the
// identity, upsert the account, start a cs_sess session, finish the login.
if (path === '/auth/apple/callback' && M === 'POST') {
const form = new URLSearchParams(await readRaw(req));
const code = form.get('code'); const state = form.get('state');
const st = state ? appleStates.get(state) : undefined;
if (state) appleStates.delete(state);
if (!code || !st || st.exp < Date.now()) return sendJSON(res, 400, { ok: false, error: 'invalid or expired apple state' });
try {
const body = new URLSearchParams({
grant_type: 'authorization_code', code,
client_id: APPLE_SERVICES_ID, client_secret: appleClientSecret(),
redirect_uri: APPLE_REDIRECT_URI,
});
const tr = await fetch('https://appleid.apple.com/auth/token', {
method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body,
});
const tok = await tr.json();
if (!tok.id_token) return sendJSON(res, 502, { ok: false, error: 'apple token exchange failed' });
const claims = JSON.parse(Buffer.from(tok.id_token.split('.')[1] || '', 'base64url').toString());
if (!claims.sub) return sendJSON(res, 502, { ok: false, error: 'apple id_token missing subject' });
const email = String(claims.email || '').toLowerCase();
// Apple sends the human name ONLY on the first authorization (a `user` field).
let name = '';
try { const uf = form.get('user'); if (uf) { const j = JSON.parse(uf); name = `${j?.name?.firstName || ''} ${j?.name?.lastName || ''}`.trim(); } } catch { /* */ }
const u = await upsertAppleUser(String(claims.sub), email, name);
const tokc = await newSession(u.id);
return completeAppleLogin(res, setCookie(tokc), !!st.app, '/');
} catch { return sendJSON(res, 502, { ok: false, error: 'apple callback failed' }); }
}
// Native-app handoff: the app replays the one-time token HERE, inside its own
// WebView, so the Set-Cookie lands in the WebView's cookie jar. Single-use.
if (path === '/auth/handoff' && M === 'GET') {
const token = url.searchParams.get('token') || '';
const rec = handoffTokens.get(token); if (rec) handoffTokens.delete(token);
if (!rec || rec.exp < Date.now()) { res.writeHead(302, { location: '/?signin=expired' }); return res.end(); }
res.writeHead(302, { 'Set-Cookie': rec.setCookie, location: rec.location }); return res.end();
}
// ===== GAME LEADERBOARD (public, no account needed) =====
// Top-20 per game+difficulty. HARDENED (Cody gate, yoloforever C2):
// - game/diff must be from a fixed WHITELIST → no arbitrary-key disk-fill
// - score capped at the real max any game can produce (250) → blocks 9999
// - per-IP rate limit (10 POST/min) → no spam-flood of leaderboard.json
// - each entry carries a submission id → deterministic rank (no name+score collision)
if (path === '/api/leaderboard') {
const board = await load('leaderboard.json', {});
const GAMES = new Set(['whose', 'art', 'match', 'early', 'century', 'lightning']);
const DIFFS = new Set(['icons', 'scholar', 'curator', 'deep']);
const MAX_SCORE = 250; // > any legit run (10-20 rounds, ~20/round incl. streak+bonus)
if (M === 'GET') {
const g = String(url.searchParams.get('game') || ''), d = String(url.searchParams.get('diff') || '');
if (!GAMES.has(g) || !DIFFS.has(d)) return sendJSON(res, 400, { ok: false, error: 'unknown game/diff' });
return sendJSON(res, 200, { ok: true, top: (board[g + '|' + d] || []).slice(0, 20).map(({ id, ...e }) => e) });
}
if (M === 'POST') {
// per-IP rate limit
const ip = (req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || req.socket.remoteAddress || '?').split(',')[0].trim();
const now = Date.now();
LB_HITS.set(ip, (LB_HITS.get(ip) || []).filter(t => now - t < 60000));
if (LB_HITS.get(ip).length >= 10) return sendJSON(res, 429, { ok: false, error: 'slow down — try again in a minute' });
LB_HITS.get(ip).push(now);
const b = await readBody(req);
const game = String(b.game || ''), diff = String(b.diff || '');
if (!GAMES.has(game) || !DIFFS.has(diff)) return sendJSON(res, 400, { ok: false, error: 'unknown game/diff' });
const score = Math.max(0, Math.min(MAX_SCORE, parseInt(b.score, 10) || 0));
const name = (String(b.name || 'Anonymous').replace(/[<>&"]/g, '').trim() || 'Anonymous').slice(0, 24);
const id = randomBytes(6).toString('hex');
const key = game + '|' + diff;
board[key] = board[key] || [];
board[key].push({ id, name, score, at: new Date().toISOString() });
board[key].sort((a, b2) => b2.score - a.score);
board[key] = board[key].slice(0, 50);
await store('leaderboard.json', board);
const rank = board[key].findIndex(e => e.id === id) + 1;
return sendJSON(res, 200, { ok: true, rank, id, top: board[key].slice(0, 20).map(({ id: _i, ...e }) => e) });
}
}
// ===== SAVED PLACEMENTS (per account) =====
if (path === '/api/saves') {
const u = await currentUser(req);
if (!u) return sendJSON(res, 401, { ok: false, error: 'sign in to save' });
const all = await load('saves.json', []);
if (M === 'GET') return sendJSON(res, 200, { ok: true, saves: all.filter(s => s.userId === u.id).sort((a, b) => b.at.localeCompare(a.at)) });
if (M === 'POST') {
const b = await readBody(req);
if (!b.mural) return sendJSON(res, 400, { ok: false, error: 'mural required' });
const id = randomBytes(8).toString('hex');
const rec = { id, userId: u.id, at: new Date().toISOString(),
mural: String(b.mural).slice(0, 60), mural_title: String(b.mural_title || '').slice(0, 120),
wall_w: +b.wall_w || null, wall_h: +b.wall_h || null,
placement: b.placement && typeof b.placement === 'object' ? { from_left_ft: +b.placement.from_left_ft || 0, off_floor_ft: +b.placement.off_floor_ft || 0 } : null,
scene: String(b.scene || '').slice(0, 40), label: String(b.label || '').slice(0, 80) };
all.push(rec); await store('saves.json', all);
return sendJSON(res, 200, { ok: true, save: rec });
}
if (M === 'DELETE') {
const id = url.searchParams.get('id');
const kept = all.filter(s => !(s.id === id && s.userId === u.id));
await store('saves.json', kept);
return sendJSON(res, 200, { ok: true, removed: all.length - kept.length });
}
}
// ===== made-to-order request (existing) =====
if (path === '/api/mural-order' && M === 'POST') {
const b = await readBody(req);
if (!b.name || !b.email) return sendJSON(res, 400, { ok: false, error: 'name and email required' });
const u = await currentUser(req);
const list = await load('mural-orders.json', []);
const id = (list.at(-1)?.id || 1000) + 1;
list.push({ id, at: new Date().toISOString(), account: u ? u.email : null, ...b });
await store('mural-orders.json', list);
return sendJSON(res, 200, { ok: true, id });
}
// ===== STRIPE CHECKOUT (mural orders) — TEST MODE ONLY =====
// Zero-dep: we call Stripe's REST API directly. Reads STRIPE_TEST_SECRET_KEY
// (sk_test_…). The LIVE key is deliberately NOT read here — going live is a
// separate Steve-gated switch (STRIPE_LIVE_ENABLED + a live key). If no test
// key is configured, the endpoint reports so gracefully (the lead-form flow
// still works as the fallback).
if (path === '/api/mural-checkout' && M === 'POST') {
if (!STRIPE_MURAL_KEY) return sendJSON(res, 503, { ok: false, error: 'payments not configured yet (awaiting Stripe key)' });
const b = await readBody(req);
const name = String(b.name || '').slice(0, 120);
const email = String(b.email || '').trim();
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return sendJSON(res, 400, { ok: false, error: 'valid email required' });
const w = Math.max(4, Math.min(40, +b.widthFt || 0));
const h = Math.max(3, Math.min(16, +b.heightFt || 0));
const perSqFt = 10; // matches murals-catalog pricePerSqFt
const amountCents = Math.round(w * h * perSqFt) * 100;
if (amountCents < 500) return sendJSON(res, 400, { ok: false, error: 'invalid mural size' });
const title = String(b.mural_title || 'Signature Mural').slice(0, 120);
const u = await currentUser(req);
// record a PENDING order first so we have it even before webhook/return
const list = await load('mural-orders.json', []);
const id = (list.at(-1)?.id || 1000) + 1;
const order = { id, at: new Date().toISOString(), account: u ? u.email : null, name, email,
mural: b.mural, mural_title: title, widthFt: w, heightFt: h, sqft: w * h,
amountUsd: amountCents / 100, placement: b.placement || null, status: 'pending_payment', mode: STRIPE_MODE };
list.push(order); await store('mural-orders.json', list);
// create the Stripe Checkout Session via REST (form-encoded)
const params = new URLSearchParams();
params.set('mode', 'payment');
params.set('success_url', 'https://celebsignatures.com/order-success?sid={CHECKOUT_SESSION_ID}');
params.set('cancel_url', 'https://celebsignatures.com/murals');
params.set('customer_email', email);
params.set('client_reference_id', String(id));
params.set('line_items[0][quantity]', '1');
params.set('line_items[0][price_data][currency]', 'usd');
params.set('line_items[0][price_data][unit_amount]', String(amountCents));
params.set('line_items[0][price_data][product_data][name]', `${title} — ${h}×${w} ft signature mural`);
params.set('line_items[0][price_data][product_data][description]', `Custom ${w * h} sq ft made-to-order signature mural`);
params.set('metadata[order_id]', String(id));
try {
const sres = await fetch('https://api.stripe.com/v1/checkout/sessions', {
method: 'POST',
headers: { Authorization: `Bearer ${STRIPE_MURAL_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});
const sj = await sres.json();
if (!sres.ok) { console.error('stripe error', sj.error?.message); return sendJSON(res, 502, { ok: false, error: sj.error?.message || 'stripe error' }); }
order.stripeSession = sj.id; await store('mural-orders.json', list);
return sendJSON(res, 200, { ok: true, url: sj.url, orderId: id });
} catch (e) { return sendJSON(res, 502, { ok: false, error: 'payment gateway unreachable' }); }
}
if (path === '/order-success' && M === 'GET') {
const sid = url.searchParams.get('sid') || '';
let paid = false, order = null;
if (STRIPE_MURAL_KEY && /^cs_(test|live)_[A-Za-z0-9]+$/.test(sid)) {
try {
const s = await (await fetch(`https://api.stripe.com/v1/checkout/sessions/${sid}`, { headers: { Authorization: `Bearer ${STRIPE_MURAL_KEY}` } })).json();
if (s.payment_status === 'paid') {
paid = true;
const list = await load('mural-orders.json', []);
order = list.find(o => String(o.id) === String(s.metadata?.order_id || s.client_reference_id));
if (order && order.status !== 'paid') { order.status = 'paid'; order.paidAt = new Date().toISOString(); await store('mural-orders.json', list); }
}
} catch {}
}
const body = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${paid ? 'Order confirmed' : 'Order'} — Celebrity Signatures</title><link rel="icon" href="/assets/favicon.png">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600&display=swap" rel="stylesheet">
<style>body{margin:0;font:16px/1.6 -apple-system,sans-serif;background:#f7f5f0;color:#1a1a1a;text-align:center}
.w{max-width:520px;margin:0 auto;padding:70px 24px}h1{font:600 30px 'Playfair Display',serif}
.ok{font-size:52px}a.btn{display:inline-block;margin-top:22px;padding:12px 26px;border-radius:999px;background:#1a1a1a;color:#fff;text-decoration:none}
.sub{color:#6b6b6b}</style></head><body><div class="w">
${paid ? `<div class="ok">✓</div><h1>Order confirmed</h1>
<p>Thank you${order ? ', ' + esc(order.name) : ''} — your ${order ? esc(order.heightFt + '×' + order.widthFt + ' ft') : ''} signature mural is ordered${order ? ' (order #' + order.id + ', $' + order.amountUsd.toLocaleString() + ')' : ''}. We'll email your 150-DPI proof and shipping details.</p>
<p class="sub">A receipt is on its way from Stripe.</p>`
: `<h1>Order not completed</h1><p class="sub">No payment was captured. You can try again from the wall studio.</p>`}
<a class="btn" href="/murals">${paid ? 'Design another wall' : 'Back to the studio'}</a>
<p style="margin-top:20px"><a href="/">← Celebrity Signatures</a></p></div></body></html>`;
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(injectAds(body)); return;
}
// ===== CELEBRITY SIGNATURE UPLOADS (owned + commission-tracked) =====
// A celebrity uploads THEIR OWN signature as a digital file. The file is
// tied to their account, stored privately, and every download is logged to
// an append-only ledger that accrues their commission. Raw files are never
// shared or statically served — the tracked endpoint is the only exit.
if (path === '/api/signature-upload' && M === 'POST') {
const u = await currentUser(req);
if (!u) return sendJSON(res, 401, { ok: false, error: 'sign in to upload your signature' });
const b = await readBodyBig(req);
const name = String(b.celebrity_name || '').trim().slice(0, 120);
if (name.length < 2) return sendJSON(res, 400, { ok: false, error: 'name required' });
if (b.attest !== true) return sendJSON(res, 400, { ok: false, error: 'you must attest this is your own signature (or you are authorized to license it)' });
const m = String(b.dataUrl || '').match(/^data:(image\/(png|jpeg|svg\+xml));base64,([A-Za-z0-9+/=]+)$/);
if (!m) return sendJSON(res, 400, { ok: false, error: 'file must be a PNG, JPEG, or SVG data URL' });
const buf = Buffer.from(m[3], 'base64');
if (buf.length < 500 || buf.length > 3e6) return sendJSON(res, 400, { ok: false, error: 'file must be 0.5KB–3MB' });
const ext = m[2] === 'svg+xml' ? 'svg' : m[2];
const id = 'up_' + randomBytes(8).toString('hex');
await mkdir(UPLOADS_DIR, { recursive: true });
await writeFile(join(UPLOADS_DIR, `${id}.${ext}`), buf);
const ups = await load('celebrity-uploads.json', []);
ups.push({
id, userId: u.id, ownerEmail: u.email, celebrity_name: name,
file: `${id}.${ext}`, mime: m[1], bytes: buf.length,
at: new Date().toISOString(), status: 'pending',
priceUsd: DEFAULT_PRICE_USD, commissionPct: DEFAULT_COMMISSION_PCT, downloads: 0,
});
await store('celebrity-uploads.json', ups);
return sendJSON(res, 200, { ok: true, id, status: 'pending', note: 'submitted for identity review — you will see it in your dashboard' });
}
if (path === '/api/my-signatures' && M === 'GET') {
const u = await currentUser(req);
if (!u) return sendJSON(res, 401, { ok: false, error: 'sign in' });
const ups = (await load('celebrity-uploads.json', [])).filter(x => x.userId === u.id);
const mine = ups.map(({ file, userId, ...pub }) => ({
...pub,
earnedUsd: +(pub.downloads * pub.priceUsd * pub.commissionPct / 100).toFixed(2),
}));
return sendJSON(res, 200, { ok: true, uploads: mine, totalEarnedUsd: +mine.reduce((s, x) => s + x.earnedUsd, 0).toFixed(2) });
}
if (path === '/api/celebrity-signatures' && M === 'GET') {
// public directory of APPROVED uploads — metadata only, no file paths
const ups = await load('celebrity-uploads.json', []);
return sendJSON(res, 200, ups.filter(x => x.status === 'approved')
.map(x => ({ id: x.id, celebrity_name: x.celebrity_name, downloads: x.downloads, priceUsd: x.priceUsd, at: x.at })));
}
// ===== PAID SIGNATURE DOWNLOAD (Stripe TEST mode) =====
// Create a Stripe TEST checkout for a single signature file. On payment,
// Stripe redirects to /api/signature-file/:id?sid=… which verifies the paid
// session before releasing the file. Inert (503) until a test key is set;
// the LIVE key is deliberately never read here — going live is a separate gate.
if (path === '/api/signature-checkout' && M === 'POST') {
if (!STRIPE_DOWNLOAD_KEY) return sendJSON(res, 503, { ok: false, error: 'downloads not purchasable yet (awaiting Stripe test key)' });
const b = await readBody(req);
const ups = await load('celebrity-uploads.json', []);
const x = ups.find(e => e.id === String(b.id || ''));
if (!x || x.status !== 'approved') return sendJSON(res, 404, { ok: false, error: 'not available' });
const params = new URLSearchParams();
params.set('mode', 'payment');
params.set('success_url', `https://celebsignatures.com/api/signature-file/${x.id}?sid={CHECKOUT_SESSION_ID}`);
params.set('cancel_url', 'https://celebsignatures.com/');
params.set('line_items[0][quantity]', '1');
params.set('line_items[0][price_data][currency]', 'usd');
params.set('line_items[0][price_data][unit_amount]', String(Math.round(x.priceUsd * 100)));
params.set('line_items[0][price_data][product_data][name]', `${x.celebrity_name} — signature file`);
params.set('metadata[upload_id]', x.id);
try {
const sres = await fetch('https://api.stripe.com/v1/checkout/sessions', {
method: 'POST',
headers: { Authorization: `Bearer ${STRIPE_DOWNLOAD_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});
const sj = await sres.json();
if (!sres.ok) return sendJSON(res, 502, { ok: false, error: sj.error?.message || 'stripe error' });
return sendJSON(res, 200, { ok: true, url: sj.url });
} catch (e) { return sendJSON(res, 502, { ok: false, error: 'payment gateway unreachable' }); }
}
const fm = path.match(/^\/api\/signature-file\/(up_[a-f0-9]+)$/);
if (fm && M === 'GET') {
const ups = await load('celebrity-uploads.json', []);
const x = ups.find(e => e.id === fm[1]);
if (!x || x.status !== 'approved') return sendJSON(res, 404, { ok: false, error: 'not available' });
// PAYMENT GATE (TEST mode): require a PAID Stripe checkout session for this file.
const sid = url.searchParams.get('sid') || '';
if (!STRIPE_DOWNLOAD_KEY) return sendJSON(res, 503, { ok: false, error: 'downloads require payment — not configured yet' });
if (!/^cs_(test|live)_[A-Za-z0-9]+$/.test(sid)) return sendJSON(res, 402, { ok: false, error: 'payment required — purchase this download first', purchase: '/api/signature-checkout' });
let paid = false, sessionCreated = 0;
try {
const s = await (await fetch(`https://api.stripe.com/v1/checkout/sessions/${sid}`, { headers: { Authorization: `Bearer ${STRIPE_DOWNLOAD_KEY}` } })).json();
// gate fulfillment on the settled session (status===complete), not just payment_status
paid = s.status === 'complete' && s.payment_status === 'paid' && String(s.metadata?.upload_id) === x.id;
sessionCreated = s.created || 0;
} catch {}
if (!paid) return sendJSON(res, 402, { ok: false, error: 'payment not verified' });
// HOLE 5 FIX: a paid Stripe session stays paid forever, so a shared ?sid= URL
// would be permanent free re-download. Gate the file to a SHORT WINDOW (1h from
// purchase) AND a small per-session use cap (3), so a leaked/shared link dies
// fast but a buyer whose download drops can retry. Beyond that → 410, contact support.
if (sessionCreated && (Date.now() / 1000 - sessionCreated) > 3600) {
return sendJSON(res, 410, { ok: false, error: 'this download link has expired (1-hour window) — contact info@designerwallcoverings.com to re-download' });
}
const sidUse = await load('download-sids.json', {});
if ((sidUse[sid] || 0) >= 3) {
return sendJSON(res, 410, { ok: false, error: 'this download link has been used its maximum times — contact info@designerwallcoverings.com to re-download' });
}
sidUse[sid] = (sidUse[sid] || 0) + 1;
await store('download-sids.json', sidUse);
// Idempotent credit: claim the sid ATOMICALLY in-process (closes the concurrent
// double-credit race — has+add are synchronous, no await between them), then confirm
// against the persistent ledger so a restart can't re-credit an already-paid sid.
let firstUse = false;
if (!USED_SIDS.has(sid)) {
USED_SIDS.add(sid);
let inLedger = false;
try { inLedger = (await readFile(join(DATA, 'download-ledger.jsonl'), 'utf8')).includes(`"sid":"${sid}"`); } catch {}
firstUse = !inLedger;
}
if (firstUse) {
const dl = await currentUser(req); // downloader identity when signed in
const commission = +(x.priceUsd * x.commissionPct / 100).toFixed(2);
x.downloads++;
await store('celebrity-uploads.json', ups);
await appendFile(join(DATA, 'download-ledger.jsonl'), JSON.stringify({
ts: new Date().toISOString(), uploadId: x.id, ownerEmail: x.ownerEmail,
downloader: dl ? dl.email : null, priceUsd: x.priceUsd, commissionUsd: commission, sid,
}) + '\n');
}
const buf = await readFile(join(UPLOADS_DIR, x.file));
res.writeHead(200, {
'Content-Type': x.mime,
'Content-Disposition': `attachment; filename="${x.celebrity_name.replace(/[^a-zA-Z0-9 _-]/g, '')} signature.${extname(x.file).slice(1)}"`,
});
return res.end(buf);
}
// admin review (token from data/admin-token.txt, never in git or rsync)
if (path === '/api/admin/signature-uploads' && M === 'GET') {
if (url.searchParams.get('token') !== await adminToken()) return sendJSON(res, 403, { ok: false });
return sendJSON(res, 200, { ok: true, uploads: await load('celebrity-uploads.json', []) });
}
if (path === '/api/admin/signature-review' && M === 'POST') {
const b = await readBody(req);
if (b.token !== await adminToken()) return sendJSON(res, 403, { ok: false });
const ups = await load('celebrity-uploads.json', []);
const x = ups.find(e => e.id === b.id);
if (!x) return sendJSON(res, 404, { ok: false, error: 'no such upload' });
if (!['approve', 'reject'].includes(b.action)) return sendJSON(res, 400, { ok: false, error: 'action approve|reject' });
x.status = b.action === 'approve' ? 'approved' : 'rejected';
x.reviewedAt = new Date().toISOString();
await store('celebrity-uploads.json', ups);
return sendJSON(res, 200, { ok: true, id: x.id, status: x.status });
}
// ===== signatures feed = hand-curated catalog + authoritative Artists set =====
// artists.json is the SINGLE AUTHORITY for the Artists category (occupation-
// filtered to real artists, famous-first — see scripts/filter-artists.mjs).
// We DROP any 'Artists' that a roster pipeline physically merged into
// celebrity_signatures.json, so non-artist museum constituents (portrait
// SUBJECTS like Shakespeare / Newton / Lincoln) can't leak into the grid and
// no artist double-lists with Politics. The base file owns only the
// hand-curated categories (Oldest Signatures / Declaration / Politics).
if (path === '/api/signatures' && M === 'GET') {
return sendJSON(res, 200, await mergedSignatures(), { 'Cache-Control': 'no-cache' });
}
// ===== /wear — legally-cleared signature apparel =====
// Returns ONLY signatures that passed the eligibility gate (commercially
// usable, not high-risk, full-register trademark screen == CLEAR), plus the
// garment templates and a disclosure describing the screen. If the screen is
// not yet complete, `signatures` is empty by design (nothing is "legally clear"
// until the register is actually screened).
if (path === '/api/wear/signatures' && M === 'GET') {
const clr = await tmClearance();
const tpl = await wearTemplates();
const sigs = (await wearSignatures()).map(s => ({
qid: qidOf(s), full_name: s.full_name, category: s.category,
signature_image_url: s.signature_image_url, image_license: s.image_license,
}));
return sendJSON(res, 200, {
garments: tpl.garments || [], placement: tpl.placement || 'left_chest',
disclosure: {
basis: 'public-domain-historical', cutoffDeathYear: WEAR_PD_DEATH_CUTOFF,
purchasable: envVal('WEAR_SALES_LIVE') === '1',
note: 'Only public-domain historical signatures (figures deceased on or before ' + WEAR_PD_DEATH_CUTOFF + ') are offered — no living or modern figures. Live USPTO trademark screening is deferred.',
},
count: sigs.length, signatures: sigs,
}, { 'Cache-Control': 'no-cache' });
}
// ===== /wear checkout (Stripe TEST mode only) — a real multi-item cart =====
// Mirrors the mural-checkout flow but uses the TEST download key — going live
// (real cards) AND actually submitting to a POD firm are both Steve-gated. On
// payment, a POD-order draft is appended to data/pod-order-drafts.jsonl per
// cart line item; NO POD API call is made here.
if (path === '/api/wear-checkout' && M === 'POST') {
// v1 soft-launch: catalog is public + browsable, but real sales stay OFF until Steve
// flips WEAR_SALES_LIVE=1 (paired with live Stripe + a wired POD). Until then, no visitor
// is sent to a test-mode checkout — they get a friendly "opening soon".
if (envVal('WEAR_SALES_LIVE') !== '1') return sendJSON(res, 200, { ok: false, comingSoon: true, error: 'Apparel sales open soon — thanks for your interest!' });
// Never charge a customer unless the selected POD provider has both a
// credential and a complete color/size mapping for every offered garment.
const podProvider = wearPodProvider();
const podTemplates = await wearTemplates();
if (!wearPodReady(podTemplates, podProvider)) return sendJSON(res, 503, { ok: false, error: 'apparel fulfillment is not configured' });
// Apparel is a physical POD sale — charge through wear's OWN live-capable
// resolver (WEAR_STRIPE_KEY: live only when WEAR_STRIPE_LIVE_ENABLED=1 AND
// a sk_live_ key is present, else the test key). Deliberately NOT
// STRIPE_MURAL_KEY — that flag is already live for an unrelated feature,
// so reusing it would let real charges start before Steve ever placed a
// test order through wear specifically.
if (!WEAR_STRIPE_KEY) return sendJSON(res, 503, { ok: false, error: 'apparel not purchasable yet (awaiting Stripe key)' });
const b = await readBody(req);
const email = String(b.email || '').trim();
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return sendJSON(res, 400, { ok: false, error: 'valid email required' });
const cartIn = Array.isArray(b.items) ? b.items : (b.qid ? [b] : []); // back-compat: a bare single item still works
if (!cartIn.length) return sendJSON(res, 400, { ok: false, error: 'cart is empty' });
if (cartIn.length > WEAR_CART_MAX_ITEMS) return sendJSON(res, 400, { ok: false, error: `cart is limited to ${WEAR_CART_MAX_ITEMS} items` });
const clr = await tmClearance();
const allSigs = await mergedSignatures();
const tpl = podTemplates;
const resolvedItems = [];
for (const it of cartIn) {
const sig = allSigs.find(s => qidOf(s) === String(it.qid || ''));
if (!sig || !wearEligible(sig, clr)) return sendJSON(res, 403, { ok: false, error: `${it.qid || 'a signature'} is not cleared for sale` });
const garment = (tpl.garments || []).find(g => g.id === String(it.garment || ''));
if (!garment) return sendJSON(res, 400, { ok: false, error: 'unknown garment' });
if (garment.saleEnabled === false) return sendJSON(res, 503, { ok: false, error: `${garment.label} is coming soon` });
const color = (garment.colors || []).find(c => c.id === String(it.color || '')) || garment.colors[0];
if (!(garment.sizes || []).includes(String(it.size))) return sendJSON(res, 400, { ok: false, error: 'unknown size' });
const size = String(it.size);
// Base price + any size upcharge (e.g. 2XL costs Printful more; keep our margin flat).
const baseUsd = garment.priceUsd || WEAR_GARMENT_DEFAULT_PRICE_USD;
const upchargeUsd = (garment.sizeUpchargeUsd && garment.sizeUpchargeUsd[size]) || 0;
const amountCents = Math.round((baseUsd + upchargeUsd) * 100);
resolvedItems.push({ qid: qidOf(sig), signature_name: sig.full_name, garment: garment.id, garment_label: garment.label,
color: color.id, color_label: color.label, size, placement: garment.placement || tpl.placement || 'left_chest',
placementLabel: garment.placementLabel || 'Signature printed at the left chest.',
inkColor: safeInkHex(it.inkColor), amountCents }); // buyer's chosen ink (validated; null = auto)
}
const u = await currentUser(req);
const list = await load('wear-orders.json', []);
const id = (list.at(-1)?.id || WEAR_ORDER_ID_SEED) + 1;
const totalCents = resolvedItems.reduce((s, i) => s + i.amountCents, 0);
const order = { id, at: new Date().toISOString(), account: u ? u.email : null, email,
items: resolvedItems, amountUsd: totalCents / 100,
// Pin the POD provider to the order at the moment of purchase. If
// WEAR_POD_PROVIDER (or its readiness) changes before the customer lands
// on /wear-success, the draft must still route to the provider that was
// actually configured — and eligible — when the charge happened.
status: 'pending_payment', mode: WEAR_STRIPE_MODE, provider: podProvider, pod_submitted: false };
list.push(order); await store('wear-orders.json', list);
const params = new URLSearchParams();
params.set('mode', 'payment');
// Physical POD product => collect a shipping address (Printful requires one).
params.set('shipping_address_collection[allowed_countries][0]', 'US');
params.set('success_url', 'https://celebsignatures.com/wear-success?sid={CHECKOUT_SESSION_ID}');
params.set('cancel_url', 'https://celebsignatures.com/wear');
params.set('customer_email', email);
params.set('client_reference_id', String(id));
resolvedItems.forEach((it, i) => {
params.set(`line_items[${i}][quantity]`, '1');
params.set(`line_items[${i}][price_data][currency]`, 'usd');
params.set(`line_items[${i}][price_data][unit_amount]`, String(it.amountCents));
params.set(`line_items[${i}][price_data][product_data][name]`, `${it.signature_name} signature — ${it.garment_label} (${it.color_label}, ${it.size})`);
params.set(`line_items[${i}][price_data][product_data][description]`, `${it.placementLabel} Made to order.${WEAR_STRIPE_MODE === 'test' ? ' TEST checkout.' : ''}`);
});
params.set('metadata[order_id]', String(id));
try {
const sres = await fetch('https://api.stripe.com/v1/checkout/sessions', {
method: 'POST',
headers: { Authorization: `Bearer ${WEAR_STRIPE_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});
const sj = await sres.json();
if (!sres.ok) { console.error('stripe error', sj.error?.message); return sendJSON(res, 502, { ok: false, error: sj.error?.message || 'stripe error' }); }
order.stripeSession = sj.id; await store('wear-orders.json', list);
return sendJSON(res, 200, { ok: true, url: sj.url, orderId: id });
} catch (e) { return sendJSON(res, 502, { ok: false, error: 'payment gateway unreachable' }); }
}
if (path === '/wear-success' && M === 'GET') {
const sid = url.searchParams.get('sid') || '';
let paid = false, order = null;
if (WEAR_STRIPE_KEY && /^cs_(test|live)_[A-Za-z0-9]+$/.test(sid)) {
try {
const s = await (await fetch(`https://api.stripe.com/v1/checkout/sessions/${sid}`, { headers: { Authorization: `Bearer ${WEAR_STRIPE_KEY}` } })).json();
if (s.payment_status === 'paid') {
paid = true;
const list = await load('wear-orders.json', []);
order = list.find(o => String(o.id) === String(s.metadata?.order_id || s.client_reference_id));
if (order && order.status !== 'paid') {
order.status = 'paid'; order.paidAt = new Date().toISOString();
// Capture the shipping address Stripe collected (Printful needs it).
const sd = s.shipping_details || s.customer_details || {};
const ad = sd.address || {};
order.recipient = { name: sd.name || null, address1: ad.line1 || null, address2: ad.line2 || null,
city: ad.city || null, state_code: ad.state || null, country_code: ad.country || null, zip: ad.postal_code || null };
// DRAFT one POD order per cart line item — do NOT submit. The selected
// provider's separately gated sender validates and transmits each one
// later. Use the provider PINNED on the order at checkout time (falls
// back to the live env for orders placed before that field existed) —
// never re-read WEAR_POD_PROVIDER fresh here, or a provider switch
// between checkout and this GET could route a paid order to a
// provider whose variant map doesn't match what the customer bought.
const provider = order.provider || wearPodProvider();
if (!order.pod_submitted) {
// Idempotency guard: if a prior run appended some draft lines but
// crashed before persisting pod_submitted=true, don't re-append the
// ones already written — check the append-only log itself, not
// just the in-memory flag. Each cart line item gets its own
// composite id ("<orderId>.<itemIndex>") so a multi-item order
// produces one draft — and one eventual Printify order — per item.
let existingLines = [];
try { existingLines = (await readFile(join(DATA, 'pod-order-drafts.jsonl'), 'utf8')).trim().split('\n').filter(Boolean); } catch {}
const alreadyDrafted = new Set(existingLines.map(l => { try { return JSON.parse(l).orderId; } catch { return null; } }));
const sigsAll = await mergedSignatures();
const draftTpl = await wearTemplates();
const items = wearOrderItems(order);
for (let i = 0; i < items.length; i++) {
const it = items[i];
const compositeId = `${order.id}.${i}`;
if (alreadyDrafted.has(compositeId)) continue;
const sig = sigsAll.find(x => qidOf(x) === it.qid);
let designUrl = sig ? sig.signature_image_url : null;
// Bake the ink color into the printed art (Printify prints the
// source pixels as-is): the buyer's chosen color if they picked
// one, else the auto rule — white on a dark garment (a color
// would vanish), the signer's own distinct color on a light one.
// Matches the on-shirt preview's inkHexFor() exactly.
if (designUrl) {
const g = (draftTpl.garments || []).find(x => x.id === it.garment);
const c = g && (g.colors || []).find(x => x.id === it.color);
const darkCloth = !!(c && wearLuminance(c.hex) < 0.4);
// Recolor ONLY when there's a reason: the buyer picked a color,
// or the cloth is dark (a dark signature would vanish → white).
// Otherwise keep the ORIGINAL art in its true source colors.
const ink = safeInkHex(it.inkColor) || (darkCloth ? '#ffffff' : null);
if (ink) designUrl = await recolorSignatureInk(designUrl, ink);
}
const draft = { draftedAt: new Date().toISOString(), orderId: compositeId, status: 'DRAFT_UNSENT', provider,
recipient_email: order.email, recipient: order.recipient, garment: it.garment, color: it.color, size: it.size,
placement: it.placement, signature_name: it.signature_name,
design_image_url: designUrl,
note: `NOT SENT — awaiting Steve approval + ${provider} credentials/mapping + WEAR_SALES_LIVE=1` };
await appendFile(join(DATA, 'pod-order-drafts.jsonl'), JSON.stringify(draft) + '\n');
}
order.pod_submitted = true;
}
await store('wear-orders.json', list);
}
}
} catch (e) { console.error('wear-success', e.message); }
}
const body = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${paid ? 'Order received' : 'Order'} — Celebrity Signatures</title><link rel="icon" href="/assets/favicon.png">
<style>body{margin:0;font:16px/1.6 -apple-system,sans-serif;background:#f7f5f0;color:#1a1a1a;text-align:center}
.w{max-width:520px;margin:0 auto;padding:70px 24px}h1{font:600 28px 'Playfair Display',serif}
.ok{font-size:52px}a.btn{display:inline-block;margin-top:22px;padding:12px 26px;border-radius:999px;background:#1a1a1a;color:#fff;text-decoration:none}
.sub{color:#6b6b6b}</style></head><body><div class="w">
${paid ? `<div class="ok">✓</div><h1>Order received</h1>
<p>${order ? wearOrderItems(order).map(it => `${esc(it.signature_name)} — ${esc(it.garment_label)} (${esc(it.color_label || it.color)}, ${esc(it.size)})`).join('<br>') : 'Your order'} ${wearOrderItems(order || {}).length > 1 ? 'are' : 'is'} queued. (TEST order #${order ? order.id : ''}.)</p>
<p class="sub">This is a test checkout; nothing was charged and no print order has been submitted yet.</p>`
: `<h1>Order not completed</h1><p class="sub">No payment was captured.</p>`}
<a class="btn" href="/wear">Back to the shop</a></div></body></html>`;
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(injectAds(body)); return;
}
// ===== static + page routes =====
if (path === '/') path = '/public/index.html';
if (path === '/murals') path = '/public/murals.html';
if (path === '/wear') path = '/public/wear.html';
if (path === '/wallpaper') path = '/public/wallpaper.html';
if (path === '/upload') path = '/public/upload-signature.html';
if (path === '/game') path = '/public/game.html';
if (path === '/signature-edit') path = '/public/signature-edit.html';
// ===== SEO: crawlable per-signature page =====
// The grid is JS-rendered (Google can't index 7,000 cards). Each signature
// also gets a real server-rendered HTML page at /a/<QID> with JSON-LD Person
// schema, canonical, OG — the indexable surface the sitemap points at.
const am = path.match(/^\/a\/(Q\d+)$/);
if (am && M === 'GET') {
const r = (await mergedSignatures()).find(x => qidOf(x) === am[1]);
if (!r) { res.writeHead(404, { 'Content-Type': 'text/plain' }).end('signature not found'); return; }
const url = `https://celebsignatures.com/a/${am[1]}`;
const life = r.deceased === 'yes' ? `† ${(r.death_date || '').slice(0, 10)}` : 'living';
const why = (r.reason_for_ranking || '').replace('Cross-wiki notability: ', '');
const museums = Array.isArray(r.museums) ? r.museums : [];
const jsonld = {
'@context': 'https://schema.org', '@type': 'Person', name: r.full_name,
sameAs: r.wikidata || undefined,
image: r.signature_image_url,
description: `Authentic signature of ${r.full_name}${r.death_date ? ', d. ' + String(r.death_date).slice(0, 4) : ''} — ${r.category}.`,
};
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(r.full_name)} — signature | Celebrity Signatures</title>
<link rel="canonical" href="${url}">
<meta name="description" content="The authentic signature of ${esc(r.full_name)} (${esc(r.category)}${r.death_date ? ', d. ' + esc(String(r.death_date).slice(0, 4)) : ''}), traced to a real archival source.">
<link rel="icon" type="image/png" href="/assets/favicon.png">
<meta property="og:type" content="profile"><meta property="og:title" content="${esc(r.full_name)} — signature">
<meta property="og:description" content="Authentic signature of ${esc(r.full_name)}, on Celebrity Signatures.">
<meta property="og:url" content="${url}"><meta property="og:image" content="${esc(r.signature_image_url)}">
<meta name="twitter:card" content="summary_large_image">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,600;1,400&display=swap" rel="stylesheet">
<script type="application/ld+json">${JSON.stringify(jsonld)}</script>
<style>body{margin:0;font:16px/1.6 -apple-system,BlinkMacSystemFont,sans-serif;color:#1a1a1a;background:#f7f5f0}
.wrap{max-width:640px;margin:0 auto;padding:30px 20px 70px}a{color:#1d7a36}
.mast{font:italic 600 22px 'Playfair Display',serif;text-decoration:none;color:#1a1a1a}
.sig{background:#fff;border:1px solid #e6e3dc;border-radius:12px;padding:26px;text-align:center;margin:20px 0}
.sig img{max-width:90%;max-height:150px;object-fit:contain}
h1{font:400 32px/1.2 'Playfair Display',serif;margin:6px 0}
.meta{color:#6b6b6b;font-size:14px}.sec{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:#6b6b6b;margin:18px 0 4px}
.cta{display:inline-block;margin-top:18px;padding:11px 24px;border-radius:999px;background:#1a1a1a;color:#fff;text-decoration:none}</style>
</head><body><div class="wrap">
<a class="mast" href="/">Celebrity Signatures</a>
<div class="sig"><img src="${esc(r.signature_image_url)}" alt="Signature of ${esc(r.full_name)}"></div>
<h1>${esc(r.full_name)}</h1>
<div class="meta">${esc(life)} · ${esc(r.category)}${why ? ' · ' + esc(why) : ''}</div>
${museums.length ? `<div class="sec">In the collections of</div><div class="meta">${esc(museums.join(' · '))}</div>` : ''}
<div class="sec">License & source</div><div class="meta">${esc(r.image_license || '')} · <a href="${esc(r.wikidata)}" rel="nofollow noopener" target="_blank">Wikidata</a></div>
<div class="ad-zone" data-ad-zone="article"></div>
<p style="margin-top:22px"><a href="/?artist=${am[1]}">See ${esc(r.full_name)} in the full gallery →</a></p>
<a class="cta" href="/wallpaper?qid=${am[1]}&name=${encodeURIComponent(r.full_name)}">Design wallpaper with this signature →</a>
<p style="margin-top:26px"><a href="/">← Browse all ${(await mergedSignatures()).length.toLocaleString()} signatures</a></p>
</div></body></html>`;
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=3600' });
res.end(injectAds(html)); return;
}
if (path === '/sitemap.xml' && M === 'GET') {
const sigs = await mergedSignatures();
const B = 'https://celebsignatures.com';
const urls = ['/', '/signature-edit', '/game', '/wallpaper', '/wear', '/murals', '/upload'].map(u => `<url><loc>${B}${u}</loc></url>`)
.concat(sigs.map(r => `<url><loc>${B}/a/${qidOf(r)}</loc></url>`).filter(u => /\/a\/Q\d+/.test(u)));
res.writeHead(200, { 'Content-Type': 'application/xml' });
res.end(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.join('\n')}\n</urlset>`);
return;
}
if (path === '/robots.txt' && M === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('User-agent: *\nAllow: /\nSitemap: https://celebsignatures.com/sitemap.xml\n');
return;
}
if (path === '/llms.txt' && M === 'GET') {
const count = (await mergedSignatures()).length;
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=3600' });
res.end(`# Celebrity Signatures\n\n> A sourced public-domain archive of ${count.toLocaleString()} notable signatures, with crawlable person pages and archival attribution.\n\n- Canonical site: https://celebsignatures.com/\n- Signature index: https://celebsignatures.com/sitemap.xml\n- Games: https://celebsignatures.com/game\n- Murals: https://celebsignatures.com/murals\n- Contributor upload: https://celebsignatures.com/upload\n\nEach /a/Q… page describes one person and includes Person JSON-LD, source/license fields, canonical metadata, and an image of the signature.\n`);
return;
}
// AdSense ads.txt — required for the account to be authorized to serve ads.
// Served only once a publisher id is configured (else 404, so no broken file).
if (path === '/ads.txt' && M === 'GET') {
if (!ADSENSE_PUB_ID) { res.writeHead(404, { 'Content-Type': 'text/plain' }).end('not found'); return; }
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`google.com, ${ADSENSE_PUB_ID}, DIRECT, f08c47fec0942fa0\n`);
return;
}
if (path.startsWith('/assets/')) path = '/public' + path;
if (path === '/favicon.ico') path = '/public/assets/favicon.png';
if (path === '/privacy' || path === '/privacy.html') path = '/public/privacy.html';
if (path === '/account.js') path = '/public/account.js';
if (path === '/api/murals-catalog') path = '/data/murals-catalog.json';
if (path === '/api/signature-evolution') path = '/data/signature-evolution.json';
if (path === '/api/portraits') path = '/data/portraits.json';
const SHORT = { d: 'declaration-of-independence', p: 'politics', s: 'sports', h: 'hollywood', c: 'movies-classic', t: 'tv', o: 'oldest-signatures', a: 'artists', w: 'authors' };
const km = path.match(/^\/k\/([a-z0-9-]+)$/);
if (km) path = `/output/collage-${SHORT[km[1]] || km[1]}-named.png`;
const file = join(ROOT, path);
if (!file.startsWith(ROOT)) { res.writeHead(403).end('forbidden'); return; }
// Inject the account/save widget into the murals page WITHOUT editing murals.html.
if (path === '/public/murals.html') {
let html = await readFile(file, 'utf8');
html = html.replace('</body>', '<script src="/account.js"></script>\n</body>');
res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
res.end(injectAds(html)); return;
}
// HTML pages: serve as text so the AdSense loader can be injected into <head>.
if (extname(file) === '.html') {
const html = await readFile(file, 'utf8');
res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
res.end(injectAds(html)); return;
}
const body = await readFile(file);
res.writeHead(200, { 'Content-Type': TYPES[extname(file)] || 'application/octet-stream', 'Cache-Control': 'no-cache' });
res.end(body);
} catch (e) {
if (e.message === 'bad json' || e.message === 'too large') { sendJSON(res, 400, { ok: false, error: e.message }); return; }
res.writeHead(404, { 'Content-Type': 'text/plain' }).end('not found');
}
}).listen(PORT, () => console.log(`CelebritySignatures → http://localhost:${PORT}`));