Passwordless sign-in: 6-digit email codes, feature-flagged on SendGrid key

/api/auth/email/start issues a 15-min code (60s resend guard, 6 tries);
verify creates the account passwordless (sponsor cookie first-touch) and
mints the session. UI swaps the password cards for the code flow when
config.emailAuth is on; dev mode returns the code inline. Password flow
remains until the key lands in the volume (data/sendgrid.key) or
SENDGRID_KEY env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-04 13:38:11 -05:00
parent 7bb1a78ca3
commit 08f7a408e3
5 changed files with 143 additions and 3 deletions
+13 -1
View File
@@ -68,6 +68,18 @@ function login(email, password) {
acct.lastSeen = Date.now(); save();
return { ok: true, account: publicView(acct) };
}
// Passwordless path: a verified email code proves ownership, so the account
// may exist with no password at all.
function ensure(email, sponsorId) {
const e = normEmail(email);
if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' };
if (!db.byEmail[e]) {
db.byEmail[e] = { email: e, pass: null, sponsorId: Number(sponsorId) || 0, address: null, created: Date.now() };
db.joins += 1;
save();
}
return { ok: true, account: publicView(db.byEmail[e]) };
}
function byEmail(email) { const a = db.byEmail[normEmail(email)]; return a ? publicView(a) : null; }
function byAddress(address) {
const e = db.byAddress[normAddr(address)];
@@ -96,4 +108,4 @@ function publicView(a) {
}
function count() { return Object.keys(db.byEmail).length; }
module.exports = { init, signup, login, byEmail, byAddress, linkWallet, count };
module.exports = { init, signup, login, ensure, byEmail, byAddress, linkWallet, count };