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);