From 2d1ccadfd9fcf3c408f08281aa0536f2e2b3c78a Mon Sep 17 00:00:00 2001 From: martbost Date: Mon, 31 Aug 2026 15:07:29 -0500 Subject: [PATCH] Suite: let members switch between positions they hold A member holding #2 and #3 connected #3's wallet, closed the browser, came back, and still saw #2. Not a wallet problem: the sign-in cookie lasts 30 days and there was no sign-out anywhere on the site, so the first position he authenticated as was pinned to that browser and the wallet was never consulted again. This hits precisely the people we tell to buy several positions - the Triple Play is on /how-pay-works and in the chatbot - so it will keep happening. Adds POST /api/public/signout (drops the server session and expires the cookie) and a "not this position? switch" link beside the identity line on /suite. Co-Authored-By: Claude Fable 5 --- messages.js | 17 ++++++++++++++++- public/suite.js | 8 ++++++++ server.js | 8 ++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/messages.js b/messages.js index b95d8d9..0f28c5c 100644 --- a/messages.js +++ b/messages.js @@ -181,4 +181,19 @@ function adminList() { return getMessages().slice(-300).reverse().map(m => ({ mid: m.mid, fromId: m.fromId, toId: m.toId || null, org: !!m.org, body: m.body, ts: m.ts, readCount: Object.keys(m.read || {}).length })); } -module.exports = { init, makeChallenge, verifyChallenge, mintSession, authFromCookie, sessionCookie, send, inbox, markRead, unreadCount, adminList, ADDR_RE }; +// Drop a session. The sign-in cookie is persistent (30 days), so closing the +// browser does NOT end it - a member holding several positions who connects a +// different wallet keeps getting served the position they first signed in as. +function clearSession(req) { + // authFromCookie returns the session VALUE, not its key - read the token + // straight off the cookie so the server-side entry really goes away. + try { + const m = /(?:^|;\s*)ctb\.msid=([^;]+)/.exec(req.headers.cookie || ''); + if (m) sessions.delete(decodeURIComponent(m[1])); + } catch (e) {} +} +function clearCookie() { + return `ctb.msid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${IS_PROD ? '; Secure' : ''}`; +} + +module.exports = { init, makeChallenge, verifyChallenge, mintSession, authFromCookie, sessionCookie, clearSession, clearCookie, send, inbox, markRead, unreadCount, adminList, ADDR_RE }; diff --git a/public/suite.js b/public/suite.js index e98c4ee..31d9f91 100644 --- a/public/suite.js +++ b/public/suite.js @@ -83,12 +83,20 @@ $('suMe').style.display = 'block'; $('suMe').innerHTML = '#' + me.id + ' — you’re early! The Circle Suite is in team beta right now. Your license is already reserved by your position, and every tool below activates for you the moment we open the doors. Nothing to do — watch the team channel.'; $('suConnect').style.display = 'none'; + var sw = document.getElementById('suSwitch'); + if (sw) sw.addEventListener('click', async function (ev) { + ev.preventDefault(); + sw.textContent = 'signing out…'; + try { await fetch('/api/public/signout', { method: 'POST' }); } catch (e) {} + location.reload(); + }); me = null; pips(); render(); return true; } $('suMe').style.display = 'block'; $('suMe').innerHTML = me.inOrg ? '#' + me.id + ' · ' + me.tierName + ' · Level ' + me.level + ' (' + me.levelName + ')' + + ' not this position? switch' + (me.directCount >= 2 ? ' · ✓ qualified' : ' · ' + me.directCount + '/2 directs') + '
Your license covers Level ' + me.level + ' and everything below it' + (me.level < 8 ? ' — your next upgrade extends it to ' + LEVELS[me.level] + '.' : ' — full Suite.') diff --git a/server.js b/server.js index 6eb7234..50e0e82 100644 --- a/server.js +++ b/server.js @@ -703,6 +703,14 @@ async function handleApi(req,res,pathname){ return out; } + // Sign out / switch position. Needed because one person can hold several + // positions (the Triple Play we actively recommend), and without this the + // 30-day cookie pins them to whichever one they signed in as first. + if(req.method==='POST'&&pathname==='/api/public/signout'){ + try{ messages.clearSession(req); }catch(e){} + return json(res,200,{ok:true},{'Set-Cookie':messages.clearCookie()}); + } + // ── Circle Suite entitlement helper (shared by suite-me and the tools) ──── async function suiteEntitlement(req){ const s=messages.authFromCookie(req);