Fallback unattributed joins under #1 instead of root

Members were activating with sponsorId 0 (their own root) whenever their sponsor
couldn't be resolved — dead pre-wipe links (dkain) or no link at all — so their
purchases rolled to admin instead of building the tree. Default an unresolvable
sponsor to the configured catch position (siteConfig.defaultSponsorId, default
1) in both the buy path (/api/sponsor) and the free-activation path (/api/me),
guarded so #1 itself is never self-sponsored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-08 15:28:28 -05:00
parent 4872fbbfd0
commit e58de01209
+7 -2
View File
@@ -428,7 +428,10 @@ const server = http.createServer(async (req, res) => {
const s = await auth.fromRequest(req); const s = await auth.fromRequest(req);
const acct = s && s.email ? await accounts.byEmail(s.email) : null; const acct = s && s.email ? await accounts.byEmail(s.email) : null;
const tok = (acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor'] || ''; const tok = (acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor'] || '';
const sponsorId = await resolveSponsorToken(tok); let sponsorId = await resolveSponsorToken(tok);
// orphan fallback: an unresolvable/absent sponsor (dead link, no link) lands
// the new member under the configured catch position (#1) instead of root
if (!sponsorId && (!acct || acct.memberId !== (Number(siteConfig().defaultSponsorId) || 1))) sponsorId = Number(siteConfig().defaultSponsorId) || 1;
let name = null, avatarUrl = null; let name = null, avatarUrl = null;
if (tok) { if (tok) {
const t = tok.toLowerCase(); const t = tok.toLowerCase();
@@ -552,7 +555,9 @@ const server = http.createServer(async (req, res) => {
if (!s) return json(res, 200, { signedIn: false }); if (!s) return json(res, 200, { signedIn: false });
const memberId = await auth.refreshMemberId(s); const memberId = await auth.refreshMemberId(s);
const acct = (s.email && await accounts.byEmail(s.email)) || (s.address && await accounts.byAddress(s.address)) || null; const acct = (s.email && await accounts.byEmail(s.email)) || (s.address && await accounts.byAddress(s.address)) || null;
const sponsorId = await resolveSponsorToken((acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor']); let sponsorId = await resolveSponsorToken((acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor']);
const _defSpon = Number(siteConfig().defaultSponsorId) || 1;
if (!sponsorId && memberId !== _defSpon) sponsorId = _defSpon; // orphan fallback → #1 (never self-sponsor)
if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {}); if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {});
const out = { signedIn: true, email: s.email || (acct && acct.email) || null, const out = { signedIn: true, email: s.email || (acct && acct.email) || null,
address: s.address || (acct && acct.address) || null, memberId, address: s.address || (acct && acct.address) || null, memberId,