Blame Raw
· · · 110 lines (3.3 KB)
0 contributors
1import crypto from 'node:crypto';
2import { DEFAULT_USER } from '../constants.js';
3import { getConfigValue } from '../util.js';
4
5/**
6 * Sets the Clear-Site-Data header to bust the browser cache.
7 */
8class CacheBuster {
9 /**
10 * Handles/User-Agents that have already been busted.
11 * @type {Set<string>}
12 */
13 #keys = new Set();
14
15 /**
16 * User agent regex to match against requests.
17 * @type {RegExp | null}
18 */
19 #userAgentRegex = null;
20
21 /**
22 * Whether the cache buster is enabled.
23 * @type {boolean | null}
24 */
25 #isEnabled = null;
26
27 constructor() {
28 this.#isEnabled = !!getConfigValue('cacheBuster.enabled', false, 'boolean');
29 const userAgentPattern = getConfigValue('cacheBuster.userAgentPattern', '');
30 if (userAgentPattern) {
31 try {
32 this.#userAgentRegex = new RegExp(userAgentPattern, 'i');
33 } catch {
34 console.error('[Cache Buster] Invalid user agent pattern:', userAgentPattern);
35 }
36 }
37 }
38
39 /**
40 * Check if the cache should be busted for the given request.
41 * @param {import('express').Request} request Express request object.
42 * @param {import('express').Response} response Express response object.
43 * @returns {boolean} Whether the cache should be busted.
44 */
45 shouldBust(request, response) {
46 // If disabled with config, don't do anything
47 if (!this.#isEnabled) {
48 return false;
49 }
50
51 // If response headers are already sent or response is ended
52 if (response.headersSent || response.writableEnded) {
53 console.warn('[Cache Buster] Response ended or headers already sent');
54 return false;
55 }
56
57 // Check if the user agent matches the configured pattern
58 const userAgent = request.headers['user-agent'] || '';
59
60 // Bust cache for all requests if no pattern is set
61 if (!this.#userAgentRegex) {
62 return true;
63 }
64
65 return this.#userAgentRegex.test(userAgent);
66 }
67
68 /**
69 * Middleware to bust the browser cache for the current user.
70 * @type {import('express').RequestHandler}
71 */
72 #middleware(request, response, next) {
73 const handle = request.user?.profile?.handle || DEFAULT_USER.handle;
74 const userAgent = request.headers['user-agent'] || '';
75 const hash = crypto.createHash('sha256').update(userAgent).digest('hex');
76 const key = `${handle}-${hash}`;
77
78 if (this.#keys.has(key)) {
79 return next();
80 }
81
82 this.#keys.add(key);
83 this.bust(request, response);
84 next();
85 }
86
87 /**
88 * Middleware to bust the browser cache for the current user.
89 * @returns {import('express').RequestHandler} The middleware function.
90 */
91 get middleware() {
92 return this.#middleware.bind(this);
93 }
94
95 /**
96 * Bust the cache for the given response.
97 * @param {import('express').Request} request Express request object.
98 * @param {import('express').Response} response Express response object.
99 * @returns {void}
100 */
101 bust(request, response) {
102 if (this.shouldBust(request, response)) {
103 response.setHeader('Clear-Site-Data', '"cache"');
104 }
105 }
106}
107
108// Export a single instance for the entire application
109const instance = new CacheBuster();
110export default instance;