/* ============================================================ Partner ref capture (A1 Session 3). A partner shares vyndr.app/?ref=CODE. On first visit we set a FIRST-PARTY cookie `vyndr_ref` (90 days). Signup forwards the code into Supabase user metadata as `partner_ref`, which is what the internal /api/partners/report/:code attribution reads. Attribution model = FIRST TOUCH: an existing vyndr_ref cookie is never overwritten by a later ?ref= visit. Codes are sanitized (A-Z 0-9 - _, max 32) and stored UPPERCASE — the same convention as Stripe promo codes (see docs/PARTNERS.md). CommonJS so the plain-JS Jest suite exercises it directly. ============================================================ */ const REF_COOKIE = 'vyndr_ref'; const REF_TTL_DAYS = 90; /** Sanitize a raw code → canonical UPPERCASE code, or null. */ function sanitizeRefCode(raw) { const s = String(raw == null ? '' : raw).trim().toUpperCase(); if (!s || s.length > 32) return null; return /^[A-Z0-9_-]+$/.test(s) ? s : null; } /** Pull a sanitized ?ref= code out of a location.search string. */ function parseRefFromSearch(search) { try { const params = new URLSearchParams(search || ''); return sanitizeRefCode(params.get('ref')); } catch { return null; } } /** Read the vyndr_ref value out of a document.cookie string. */ function readRefCookie(cookieStr) { const parts = String(cookieStr == null ? '' : cookieStr).split(';'); for (const part of parts) { const [name, ...rest] = part.split('='); if (name && name.trim() === REF_COOKIE) { return sanitizeRefCode(decodeURIComponent(rest.join('=').trim())); } } return null; } /** Build the Set-Cookie string for a code (first-party, 90d, lax). */ function buildRefCookie(code, { days = REF_TTL_DAYS, secure = false } = {}) { const clean = sanitizeRefCode(code); if (!clean) return null; const maxAge = Math.round(days * 24 * 60 * 60); return `${REF_COOKIE}=${encodeURIComponent(clean)}; Max-Age=${maxAge}; Path=/; SameSite=Lax${secure ? '; Secure' : ''}`; } /** * Capture ?ref=CODE from the current visit into the cookie. * First-touch: no-op when the cookie already exists. `win` is * injectable for tests (defaults to the browser window). * Returns the code that was captured, or null. */ function captureRef(win) { const w = win || (typeof window !== 'undefined' ? window : null); if (!w || !w.document || !w.location) return null; const code = parseRefFromSearch(w.location.search); if (!code) return null; if (readRefCookie(w.document.cookie)) return null; // first touch wins const cookie = buildRefCookie(code, { secure: w.location.protocol === 'https:' }); if (!cookie) return null; w.document.cookie = cookie; return code; } module.exports = { REF_COOKIE, REF_TTL_DAYS, sanitizeRefCode, parseRefFromSearch, readRefCookie, buildRefCookie, captureRef, };