// "You're joining the line of X" must only greet someone actually about to join. // // The last-touch sponsor cookie lives 30 days, so an EXISTING member who once clicked a // teammate's invite link was being told on the sign-in page that logging in would place // them under that person (Marty, 2026-09-17). Untrue and alarming: their sponsor locked // at their first purchase and nothing on that screen can move it. // // Boots its own throwaway server so the account row it needs is real, not assumed. // node qa/sponsor-note.mjs import { spawn } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; const PORT = 8799; const B = 'http://127.0.0.1:' + PORT; const DATA = path.join(os.tmpdir(), 'iap-sponsor-note-' + Date.now()); fs.mkdirSync(DATA, { recursive: true }); const ok = [], bad = []; const t = (n, c, extra) => { (c ? ok : bad).push(n + (c || !extra ? '' : ' -> ' + extra)); }; const srv = spawn(process.execPath, ['server.js'], { env: { ...process.env, PORT: String(PORT), DATA_DIR: DATA, ADMIN_PASSWORD: 'localtest', ADMIN_EMAIL: 'admin@example.com', NODE_ENV: 'test' }, stdio: ['ignore', 'pipe', 'pipe'] }); const bye = code => { try { srv.kill(); } catch (e) {} process.exit(code); }; for (let i = 0; i < 60; i++) { try { const r = await fetch(B + '/api/stats'); if (r.ok) break; } catch (e) {} await new Promise(r => setTimeout(r, 500)); } const j = async (p, opts) => { const r = await fetch(B + p, opts); return { status: r.status, body: await r.json().catch(() => ({})), headers: r.headers }; }; const ask = (cookie, qs) => fetch(B + '/api/sponsor' + (qs || ''), { headers: cookie ? { Cookie: cookie } : {} }).then(r => r.json()); // make a real member on device DEV_A, the way a person would const DEV_A = 'a1b2c3d4e5f60718293a4b5c6d7e8f90'; const DEV_B = '0f9e8d7c6b5a49382716f5e4d3c2b1a0'; const EMAIL = 'returning@example.com'; const start = await j('/api/auth/email/start', { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: 'iap.dev=' + DEV_A }, body: JSON.stringify({ email: EMAIL, fts: Date.now() - 20000 }) }); const code = start.body && (start.body.devCode || start.body.code); if (!code) { console.log('could not start sign-up (no devCode in test mode):', JSON.stringify(start.body).slice(0, 200)); bye(2); } const fin = await j('/api/auth/email/verify', { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: 'iap.dev=' + DEV_A }, body: JSON.stringify({ email: EMAIL, code }) }); t('a real account was created for the test', fin.status === 200 && !fin.body.error, JSON.stringify(fin.body).slice(0, 160)); const REF = 'house'; // 1. arriving THROUGH a link: greeting is correct and wanted, even on that device const arriving = await ask('iap.dev=' + DEV_A, '?ref=' + REF); t('someone arriving with ?ref= is still greeted', arriving.invited === true, JSON.stringify(arriving).slice(0, 140)); // 2. a browser with NO account, carrying only the stored invite cookie: still greeted, // because they genuinely came back to finish joining const fresh = await ask('iap.sponsor=' + REF + '; iap.dev=' + DEV_B); t('a browser with no account is still greeted from the cookie', fresh.invited === true, JSON.stringify(fresh).slice(0, 140)); // 3. THE BUG: a browser that already has an account, carrying a stale invite cookie const existing = await ask('iap.sponsor=' + REF + '; iap.dev=' + DEV_A); t('a browser that already has an account is NOT told it is joining anyone', existing.invited === false, JSON.stringify(existing).slice(0, 180)); // 4. attribution itself must be untouched: the ref is still resolved and returned t('the sponsor reference is still resolved for attribution', existing.ref === REF || existing.sponsorId > 0, JSON.stringify(existing).slice(0, 180)); console.log('PASS ' + ok.length); for (const b of bad) console.log('FAIL ' + b); try { fs.rmSync(DATA, { recursive: true, force: true }); } catch (e) {} bye(bad.length ? 1 : 0);