File size: 3,417 Bytes
c120a1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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<string>}
     */
    #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;