Email-first membership: signup/login, wallet linked at purchase time

Normal people join with email + password (sponsor attribution via cookie at
signup); the wallet only appears when buying or activating payouts, and gets
linked to the account then. Wallet-only sign-in remains for crypto-native
users. Sessions carry {email, address, memberId}.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-04 13:00:56 -05:00
parent a87a63d75d
commit a90d5eee78
6 changed files with 271 additions and 114 deletions
+19 -9
View File
@@ -84,12 +84,22 @@ async function verifyChallenge(address, signature) {
if (rec !== a) return { error: 'Your wallet signed with a different account than the page is using ('
+ rec.slice(0, 6) + '…' + rec.slice(-4) + '). Switch accounts and tap sign-in again.' };
challenges.delete(a);
let memberId = 0;
try { memberId = await chain.memberIdByAccount(a); } catch (e) { /* chain read down: session still valid */ }
return { ok: true, address: a };
}
// Sessions carry {email, address, memberId} — email accounts are the normal
// join path; the wallet fields fill in when one is linked at purchase time.
function mintSession(fields) {
const token = crypto.randomBytes(32).toString('hex');
sessions.set(token, { address: a, memberId, expires: Date.now() + SESSION_TTL });
sessions.set(token, Object.assign({ email: null, address: null, memberId: 0 }, fields,
{ expires: Date.now() + SESSION_TTL }));
saveSessions();
return token;
}
function updateSession(token, fields) {
const s = sessions.get(token);
if (!s) return;
sessions.set(token, Object.assign({}, s, fields));
saveSessions();
return { token, address: a, memberId };
}
function sessionCookie(token) {
return `iap.sid=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL / 1000}${IS_PROD ? '; Secure' : ''}`;
@@ -105,16 +115,16 @@ function fromRequest(req) {
}
async function refreshMemberId(sess) {
// called after an on-chain action so the session learns its new member id
if (!sess.address) return sess.memberId || 0;
try {
const id = await chain.memberIdByAccount(sess.address);
if (id && id !== sess.memberId) { sess.memberId = id; sessions.set(sess.token, {
address: sess.address, memberId: id, expires: sess.expires }); saveSessions(); }
return id;
} catch (e) { return sess.memberId; }
if (id && id !== sess.memberId) updateSession(sess.token, { memberId: id });
return id || sess.memberId || 0;
} catch (e) { return sess.memberId || 0; }
}
function logout(req) {
const s = fromRequest(req);
if (s) { sessions.delete(s.token); saveSessions(); }
}
module.exports = { init, makeChallenge, verifyChallenge, sessionCookie, clearCookie, fromRequest, refreshMemberId, logout };
module.exports = { init, makeChallenge, verifyChallenge, mintSession, updateSession, sessionCookie, clearCookie, fromRequest, refreshMemberId, logout };