|
|
import crypto from 'node:crypto'; |
|
|
import { DEFAULT_USER } from '../constants.js'; |
|
|
import { getConfigValue } from '../util.js'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CacheBuster { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#keys = new Set(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#userAgentRegex = 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); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
shouldBust(request, response) { |
|
|
|
|
|
if (!this.#isEnabled) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
if (response.headersSent || response.writableEnded) { |
|
|
console.warn('[Cache Buster] Response ended or headers already sent'); |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
const userAgent = request.headers['user-agent'] || ''; |
|
|
|
|
|
|
|
|
if (!this.#userAgentRegex) { |
|
|
return true; |
|
|
} |
|
|
|
|
|
return this.#userAgentRegex.test(userAgent); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#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(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get middleware() { |
|
|
return this.#middleware.bind(this); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bust(request, response) { |
|
|
if (this.shouldBust(request, response)) { |
|
|
response.setHeader('Clear-Site-Data', '"cache"'); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const instance = new CacheBuster(); |
|
|
export default instance; |
|
|
|