v2.3.5: externalize donor list, fix password redirect loop, fix plugin loader scope

This commit is contained in:
ancsemi 2026-02-26 20:24:02 -05:00
parent 674fc26671
commit 0f74fd6ec3
11 changed files with 107 additions and 49 deletions

View file

@ -11,6 +11,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). Haven uses [Sema
---
## [2.3.5] — 2026-02-26
### Added
- **Donor list externalized** — sponsors and donors are now loaded from `donors.json` at the server root, so the list can be updated without editing HTML. The Thank You modal fetches `/api/donors` on open.
### Fixed
- **Password change redirect loop** — changing your password no longer kicks your own session into an infinite redirect. The server now sends the fresh token before disconnecting sockets, and the client guards against self-eviction during password changes.
- **Plugin loader scope** — the plugin loader now passes `globalThis` into the plugin sandbox as `_win`, so plugins can register classes that the loader can discover. Previously `new Function()` ran in a strict scope where `window` was inaccessible, breaking all plugins including the built-in MessageTimestamps.
- **MessageTimestamps plugin** — updated to register via `_win` so it loads correctly with the fixed plugin loader.
---
## [2.3.4] — 2026-02-26
### Added

View file

@ -810,13 +810,13 @@
<span class="discord-feat">🖥️ Windows &amp; Linux</span>
</div>
<div style="margin-top: 28px; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap;">
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.3/Haven-Setup-1.0.3.exe" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.4/Haven-Setup-1.0.4.exe" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<span class="icon"></span> Windows Installer
</a>
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.3/Haven-1.0.3.AppImage" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.4/Haven-1.0.4.AppImage" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<span class="icon"></span> Linux AppImage
</a>
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.3/haven-desktop_1.0.3_amd64.deb" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.4/haven-desktop_1.0.4_amd64.deb" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<span class="icon"></span> Linux .deb
</a>
</div>
@ -1246,12 +1246,12 @@
</div>
<div class="download-card fade-in">
<h2>⬡ Haven Server &mdash; v2.3.4</h2>
<h2>⬡ Haven Server &mdash; v2.3.5</h2>
<p class="download-version">Latest stable release &middot; Windows, macOS &amp; Linux &middot; ~5 MB</p>
<div class="download-btn-group">
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.3.4.zip" class="btn btn-primary download-main">
<span class="icon"></span> Download v2.3.4 (.zip)
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.3.5.zip" class="btn btn-primary download-main">
<span class="icon"></span> Download v2.3.5 (.zip)
</a>
<div class="download-alt-links">
<a href="https://github.com/ancsemi/Haven" target="_blank">&#9965; View on GitHub</a>
@ -1268,7 +1268,11 @@
<div class="version-list">
<div class="version-list-inner">
<div class="version-item">
<div><span class="v-name">v2.3.4</span><span class="v-tag latest">Latest</span></div>
<div><span class="v-name">v2.3.5</span><span class="v-tag latest">Latest</span></div>
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.3.5.zip">Download &rarr;</a>
</div>
<div class="version-item">
<div><span class="v-name">v2.3.4</span> &mdash; Right-click voice users, donor tier styling</div>
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.3.4.zip">Download &rarr;</a>
</div>
<div class="version-item">
@ -1407,12 +1411,12 @@
<!-- Desktop App Download Card -->
<div class="download-card fade-in" style="margin-top: 32px; border-color: rgba(250, 166, 26, 0.25);">
<div style="position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, var(--accent-dim), var(--warning), var(--green)); box-shadow: 0 0 12px rgba(250, 166, 26, 0.3);"></div>
<h2>🖥️ Haven Desktop <span class="beta-badge-inline">BETA</span> &mdash; v1.0.3</h2>
<h2>🖥️ Haven Desktop <span class="beta-badge-inline">BETA</span> &mdash; v1.0.4</h2>
<p class="download-version">Latest beta release &middot; Windows &amp; Linux &middot; Standalone installer</p>
<div class="download-btn-group">
<a href="https://github.com/ancsemi/Haven-Desktop/releases/latest" class="btn btn-primary download-main" style="background: var(--warning); box-shadow: 0 4px 24px rgba(250, 166, 26, 0.25), 0 0 0 1px rgba(250, 166, 26, 0.3);">
<span class="icon"></span> Download Desktop v1.0.3
<span class="icon"></span> Download Desktop v1.0.4
</a>
<div class="download-alt-links">
<a href="https://github.com/ancsemi/Haven-Desktop" target="_blank">&#9965; View on GitHub</a>

11
donors.json Normal file
View file

@ -0,0 +1,11 @@
{
"sponsors": [
"BillyAlt",
"nexitem"
],
"donors": [
"wreckedcarzz",
"JollyOrc",
"ArtyDaSmarty"
]
}

View file

@ -1,6 +1,6 @@
{
"name": "haven",
"version": "2.3.4",
"version": "2.3.5",
"description": "Self-hosted private chat — your server, your rules",
"license": "MIT-NC",
"main": "server.js",

View file

@ -56,3 +56,6 @@ class MessageTimestamps {
return `${days}d ago`;
}
}
// Register with the plugin loader's _win scope
if (typeof _win !== 'undefined') _win.MessageTimestamps = MessageTimestamps;

View file

@ -1950,24 +1950,27 @@
<h2 class="donors-title">Thank You</h2>
<p class="donors-subtitle">Haven is built with love and kept alive by the generosity of people like you. Every donation — big or small — means the world.</p>
<div class="donors-tiers">
<div class="donors-tiers" id="donors-tiers">
<div class="donors-tier">
<h3 class="donors-tier-title donors-sponsor-title">✨ Sponsors</h3>
<div class="donors-grid">
<span class="donor-chip donor-sponsor">BillyAlt</span>
</div>
<div class="donors-grid" id="sponsors-grid"></div>
</div>
<div class="donors-tier">
<h3 class="donors-tier-title donors-donor-title">💛 Donors</h3>
<div class="donors-grid">
<span class="donor-chip">wreckedcarzz</span>
<span class="donor-chip">JollyOrc</span>
<span class="donor-chip">nexitem</span>
<span class="donor-chip">ArtyDaSmarty</span>
</div>
<div class="donors-grid" id="donors-grid"></div>
</div>
</div>
<script>
(function(){
fetch('/api/donors').then(r=>r.json()).then(d=>{
const sg=document.getElementById('sponsors-grid');
const dg=document.getElementById('donors-grid');
(d.sponsors||[]).forEach(n=>{ const s=document.createElement('span'); s.className='donor-chip donor-sponsor'; s.textContent=n; sg.appendChild(s); });
(d.donors||[]).forEach(n=>{ const s=document.createElement('span'); s.className='donor-chip'; s.textContent=n; dg.appendChild(s); });
}).catch(()=>{});
})();
</script>
<p class="donors-note">New donors will be added each release. If you've donated and don't see your name, it'll be in the next update!</p>

View file

@ -367,6 +367,8 @@ class HavenApp {
});
this.socket.on('connect_error', (err) => {
// Don't kick during password change — socket will reconnect with fresh token
if (this._justChangedPassword) return;
if (err.message === 'Invalid token' || err.message === 'Authentication required' || err.message === 'Session expired') {
localStorage.removeItem('haven_token');
localStorage.removeItem('haven_user');
@ -380,15 +382,11 @@ class HavenApp {
// Password was changed on this or another session — force re-login
this.socket.on('force-logout', (data) => {
if (data && data.reason === 'password_changed') {
// If WE just changed the password, we already have the fresh token
const freshToken = localStorage.getItem('haven_token');
if (freshToken && freshToken !== this.token) {
// Another tab/device changed it but we somehow got a new token — use it
this.token = freshToken;
this.socket.auth.token = freshToken;
// If WE just changed the password, skip the kick — we already have the fresh token
if (this._justChangedPassword) {
this._justChangedPassword = false;
return;
}
// Otherwise this is a different session — kick to login
localStorage.removeItem('haven_token');
localStorage.removeItem('haven_user');
window.location.href = '/';
@ -2348,6 +2346,9 @@ class HavenApp {
if (np.length < 8) return hint.textContent = 'New password must be 8+ characters';
if (np !== conf) return hint.textContent = 'Passwords do not match';
// Flag to prevent force-logout from kicking us out
this._justChangedPassword = true;
try {
const res = await fetch('/api/auth/change-password', {
method: 'POST',
@ -2385,7 +2386,10 @@ class HavenApp {
document.getElementById('current-password').value = '';
document.getElementById('new-password').value = '';
document.getElementById('confirm-password').value = '';
// Clear the flag after a delay so socket reconnects go through
setTimeout(() => { this._justChangedPassword = false; }, 5000);
} catch {
this._justChangedPassword = false;
hint.textContent = 'Network error';
hint.classList.add('error');
}

View file

@ -184,11 +184,12 @@ window.HavenPluginLoader = (function () {
const code = await resp.text();
// Execute in a Function scope so plugins can define classes
const factory = new Function('HavenApi', code + '\n;return (typeof module !== "undefined" && module.exports) || (typeof exports !== "undefined" ? exports : null);');
const exported = factory(HavenApi);
// Pass globalThis as _win so plugins can register classes via _win.ClassName = ...
const factory = new Function('HavenApi', '_win', code + '\n;return (typeof module !== "undefined" && module.exports) || (typeof exports !== "undefined" ? exports : null);');
const exported = factory(HavenApi, globalThis);
// The plugin should place its class on window, or we find the last class defined
// Convention: plugin sets module.exports = ClassName or window.PluginName = class { ... }
// Convention: plugin sets module.exports = ClassName or _win.PluginName = class { ... }
// We'll look for any new class on window that has start()/stop()
let PluginClass = null;
@ -201,10 +202,10 @@ window.HavenPluginLoader = (function () {
PluginClass = window[baseName];
} else {
// Fallback: look for any class defined via the code — we wrap it
// The code itself may call window.XYZ = class { ... }
// The code itself may call _win.XYZ = class { ... }
// Just re-execute looking for the return value
const fn2 = new Function('HavenApi', code + '\n;return typeof start === "function" ? { start, stop: typeof stop === "function" ? stop : () => {} } : null;');
const obj = fn2(HavenApi);
const fn2 = new Function('HavenApi', '_win', code + '\n;return typeof start === "function" ? { start, stop: typeof stop === "function" ? stop : () => {} } : null;');
const obj = fn2(HavenApi, globalThis);
if (obj) PluginClass = function() { this.start = obj.start; this.stop = obj.stop || (() => {}); };
}
}

View file

@ -462,6 +462,17 @@ app.get('/games/flappy', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'games', 'flappy.html'));
});
// ── Donors / sponsors list (loaded from donors.json) ──
app.get('/api/donors', (req, res) => {
try {
const donorsPath = path.join(__dirname, 'donors.json');
const data = JSON.parse(fs.readFileSync(donorsPath, 'utf-8'));
res.json(data);
} catch {
res.json({ sponsors: [], donors: [] });
}
});
// ── Health check (CORS allowed for multi-server status pings) ──
app.get('/api/health', (req, res) => {
res.set('Access-Control-Allow-Origin', '*');

View file

@ -263,18 +263,23 @@ router.post('/change-password', async (req, res) => {
{ expiresIn: '7d' }
);
// Send the response FIRST so the client can store the fresh token
// before we disconnect sockets (prevents redirect loop)
res.json({ message: 'Password changed successfully', token: freshToken });
// Disconnect all existing sockets for this user (forces re-login on other sessions)
const io = req.app.get('io');
if (io) {
for (const [, s] of io.sockets.sockets) {
if (s.user && s.user.id === user.id) {
s.emit('force-logout', { reason: 'password_changed' });
s.disconnect(true);
// Small delay to let the HTTP response reach the client first
setTimeout(() => {
for (const [, s] of io.sockets.sockets) {
if (s.user && s.user.id === user.id) {
s.emit('force-logout', { reason: 'password_changed' });
s.disconnect(true);
}
}
}
}, 500);
}
res.json({ message: 'Password changed successfully', token: freshToken });
} catch (err) {
console.error('Change password error:', err);
res.status(500).json({ error: 'Server error' });

View file

@ -810,13 +810,13 @@
<span class="discord-feat">🖥️ Windows &amp; Linux</span>
</div>
<div style="margin-top: 28px; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap;">
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.3/Haven-Setup-1.0.3.exe" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.4/Haven-Setup-1.0.4.exe" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<span class="icon"></span> Windows Installer
</a>
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.3/Haven-1.0.3.AppImage" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.4/Haven-1.0.4.AppImage" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<span class="icon"></span> Linux AppImage
</a>
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.3/haven-desktop_1.0.3_amd64.deb" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<a href="https://github.com/ancsemi/Haven-Desktop/releases/download/v1.0.4/haven-desktop_1.0.4_amd64.deb" class="btn btn-primary" style="padding: 12px 24px; font-size: 1rem;">
<span class="icon"></span> Linux .deb
</a>
</div>
@ -1246,12 +1246,12 @@
</div>
<div class="download-card fade-in">
<h2>⬡ Haven Server &mdash; v2.3.4</h2>
<h2>⬡ Haven Server &mdash; v2.3.5</h2>
<p class="download-version">Latest stable release &middot; Windows, macOS &amp; Linux &middot; ~5 MB</p>
<div class="download-btn-group">
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.3.4.zip" class="btn btn-primary download-main">
<span class="icon"></span> Download v2.3.4 (.zip)
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.3.5.zip" class="btn btn-primary download-main">
<span class="icon"></span> Download v2.3.5 (.zip)
</a>
<div class="download-alt-links">
<a href="https://github.com/ancsemi/Haven" target="_blank">&#9965; View on GitHub</a>
@ -1268,7 +1268,11 @@
<div class="version-list">
<div class="version-list-inner">
<div class="version-item">
<div><span class="v-name">v2.3.4</span><span class="v-tag latest">Latest</span></div>
<div><span class="v-name">v2.3.5</span><span class="v-tag latest">Latest</span></div>
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.3.5.zip">Download &rarr;</a>
</div>
<div class="version-item">
<div><span class="v-name">v2.3.4</span> &mdash; Right-click voice users, donor tier styling</div>
<a href="https://github.com/ancsemi/Haven/archive/refs/tags/v2.3.4.zip">Download &rarr;</a>
</div>
<div class="version-item">
@ -1407,12 +1411,12 @@
<!-- Desktop App Download Card -->
<div class="download-card fade-in" style="margin-top: 32px; border-color: rgba(250, 166, 26, 0.25);">
<div style="position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, var(--accent-dim), var(--warning), var(--green)); box-shadow: 0 0 12px rgba(250, 166, 26, 0.3);"></div>
<h2>🖥️ Haven Desktop <span class="beta-badge-inline">BETA</span> &mdash; v1.0.3</h2>
<h2>🖥️ Haven Desktop <span class="beta-badge-inline">BETA</span> &mdash; v1.0.4</h2>
<p class="download-version">Latest beta release &middot; Windows &amp; Linux &middot; Standalone installer</p>
<div class="download-btn-group">
<a href="https://github.com/ancsemi/Haven-Desktop/releases/latest" class="btn btn-primary download-main" style="background: var(--warning); box-shadow: 0 4px 24px rgba(250, 166, 26, 0.25), 0 0 0 1px rgba(250, 166, 26, 0.3);">
<span class="icon"></span> Download Desktop v1.0.3
<span class="icon"></span> Download Desktop v1.0.4
</a>
<div class="download-alt-links">
<a href="https://github.com/ancsemi/Haven-Desktop" target="_blank">&#9965; View on GitHub</a>