From b7e2cf56b972871d5c486f2abefb52ce5d0f16ef Mon Sep 17 00:00:00 2001 From: martbost Date: Tue, 8 Sep 2026 11:59:48 -0500 Subject: [PATCH] Use the account's stored sponsor at buy time, not just the cookie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /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 --- server.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index e82b1ba..abbf431 100644 --- a/server.js +++ b/server.js @@ -418,7 +418,13 @@ const server = http.createServer(async (req, res) => { } catch (e) { return json(res, 200, { found: false, rpcError: true }); } } 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); let name = null, avatarUrl = null; if (tok) {