Use the account's stored sponsor at buy time, not just the cookie

/api/sponsor (which the buy flow reads to set sponsorIdIfNew) resolved ONLY the
iap.sponsor cookie. A member whose join cookie was absent at buy time (different
device, cleared cookies, return visit) resolved to 0 and activated on-chain as
their own root instead of under their real sponsor — irreversible. Prefer the
logged-in account's stored sponsorRef, cookie only as anonymous fallback,
matching /api/me.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-08 11:59:48 -05:00
parent 8be30bef13
commit b7e2cf56b9
+7 -1
View File
@@ -418,7 +418,13 @@ const server = http.createServer(async (req, res) => {
} catch (e) { return json(res, 200, { found: false, rpcError: true }); } } catch (e) { return json(res, 200, { found: false, rpcError: true }); }
} }
if (p === '/api/sponsor' && req.method === 'GET') { if (p === '/api/sponsor' && req.method === 'GET') {
const tok = parseCookies(req)['iap.sponsor'] || ''; // The account's stored sponsor is authoritative — it persists across
// devices, cleared cookies, and return visits. Fall back to the first-touch
// cookie only for anonymous visitors with no account sponsor yet. (Reading
// the cookie alone was orphaning buyers to root when the cookie was absent.)
const s = await auth.fromRequest(req);
const acct = s && s.email ? await accounts.byEmail(s.email) : null;
const tok = (acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor'] || '';
const sponsorId = await resolveSponsorToken(tok); const sponsorId = await resolveSponsorToken(tok);
let name = null, avatarUrl = null; let name = null, avatarUrl = null;
if (tok) { if (tok) {