import crypto from 'node:crypto'; import { DEFAULT_USER } from '../constants.js'; import { getConfigValue } from '../util.js'; /** * Sets the Clear-Site-Data header to bust the browser cache. */ class CacheBuster { /** * Handles/User-Agents that have already been busted. * @type {Set} */ #keys = new Set(); /** * User agent regex to match against requests. * @type {RegExp | null} */ #userAgentRegex = null; /** * Whether the cache buster is enabled. * @type {boolean | null} */ #isEnabled = null; constructor() { this.#isEnabled = !!getConfigValue('cacheBuster.enabled', false, 'boolean'); const userAgentPattern = getConfigValue('cacheBuster.userAgentPattern', ''); if (userAgentPattern) { try { this.#userAgentRegex = new RegExp(userAgentPattern, 'i'); } catch { console.error('[Cache Buster] Invalid user agent pattern:', userAgentPattern); } } } /** * Check if the cache should be busted for the given request. * @param {import('express').Request} request Express request object. * @param {import('express').Response} response Express response object. * @returns {boolean} Whether the cache should be busted. */ shouldBust(request, response) { // If disabled with config, don't do anything if (!this.#isEnabled) { return false; } // If response headers are already sent or response is ended if (response.headersSent || response.writableEnded) { console.warn('[Cache Buster] Response ended or headers already sent'); return false; } // Check if the user agent matches the configured pattern const userAgent = request.headers['user-agent'] || ''; // Bust cache for all requests if no pattern is set if (!this.#userAgentRegex) { return true; } return this.#userAgentRegex.test(userAgent); } /** * Middleware to bust the browser cache for the current user. * @type {import('express').RequestHandler} */ #middleware(request, response, next) { const handle = request.user?.profile?.handle || DEFAULT_USER.handle; const userAgent = request.headers['user-agent'] || ''; const hash = crypto.createHash('sha256').update(userAgent).digest('hex'); const key = `${handle}-${hash}`; if (this.#keys.has(key)) { return next(); } this.#keys.add(key); this.bust(request, response); next(); } /** * Middleware to bust the browser cache for the current user. * @returns {import('express').RequestHandler} The middleware function. */ get middleware() { return this.#middleware.bind(this); } /** * Bust the cache for the given response. * @param {import('express').Request} request Express request object. * @param {import('express').Response} response Express response object. * @returns {void} */ bust(request, response) { if (this.shouldBust(request, response)) { response.setHeader('Clear-Site-Data', '"cache"'); } } } // Export a single instance for the entire application const instance = new CacheBuster(); export default instance;