fix: security hardening and Safari stability

- Remove hardcoded JWT_SECRET fallback (crash on boot if unset)
- Make LiveKit config optional with 503 guard on token endpoint
- Add REST rate limiting via @fastify/rate-limit (auth 10/15m, messages 5/5s, uploads 10/1m, global 60/1m)
- Add WebSocket token bucket rate limiter (30 burst, 2/sec refill)
- Add DM channel ownership (ownerId) with migration, enforce on add-member
- Require friendship to add users to group DMs
- Add silent 20Hz oscillator to prevent Safari AudioContext suspension
- Move WebSocket heartbeat to Web Worker to bypass Safari background throttling
This commit is contained in:
Jannis Braun
2026-02-24 04:34:36 +01:00
parent 36e27121da
commit 024833c470
16 changed files with 205 additions and 36 deletions
+37
View File
@@ -473,6 +473,35 @@ class ConnectionManager {
export const connectionManager = new ConnectionManager();
// ─── WebSocket Rate Limiter (Token Bucket) ─────────────────────────────────
class WsRateLimiter {
private tokens: number;
private readonly maxTokens: number;
private readonly refillRate: number; // tokens per second
private lastRefill: number;
constructor(maxTokens = 30, refillRate = 2) {
this.maxTokens = maxTokens;
this.tokens = maxTokens;
this.refillRate = refillRate;
this.lastRefill = Date.now();
}
consume(): boolean {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
this.lastRefill = now;
if (this.tokens >= 1) {
this.tokens -= 1;
return true;
}
return false;
}
}
function buildReadyPayload(userId: string): {
user: User;
servers: ServerWithChannelsAndMembers[];
@@ -685,6 +714,7 @@ function buildReadyPayload(userId: string): {
dmChannels.push({
id: dmChannel.id,
ownerId: dmChannel.ownerId ?? null,
createdAt: dmChannel.createdAt,
members,
lastMessage: last ? {
@@ -791,6 +821,7 @@ export async function registerWebSocket(app: FastifyInstance): Promise<void> {
let authenticated = false;
let userId: string | undefined;
let username: string | undefined;
const rateLimiter = new WsRateLimiter();
// Set auth timeout - must authenticate within 10 seconds
const authTimeout = setTimeout(() => {
@@ -861,6 +892,12 @@ export async function registerWebSocket(app: FastifyInstance): Promise<void> {
return;
}
// Rate limit all post-auth, non-ping messages
if (!rateLimiter.consume()) {
ws.send(JSON.stringify({ type: 'error', message: 'Rate limited' }));
return;
}
// Handle authenticated events
if (userId && username) {
handleClientEvent(parsed, userId, username);