import { base64url } from "jose"; import { Logger } from "winston"; import { CosmeticsSchema } from "../core/CosmeticSchemas"; import { startPolling } from "./PollingLoop"; import { FailOpenPrivilegeChecker, PrivilegeChecker, PrivilegeCheckerImpl, } from "./Privilege"; // Refreshes the privilege checker every 5 minutes. // WARNING: This fails open if cosmetics.json is not available. export class PrivilegeRefresher { private privilegeChecker: PrivilegeChecker | null = null; private failOpenPrivilegeChecker: PrivilegeChecker = new FailOpenPrivilegeChecker(); private log: Logger; constructor( private endpoint: string, parentLog: Logger, private refreshInterval: number = 1000 * 60 * 3, ) { this.log = parentLog.child({ comp: "privilege-refresher" }); } public async start() { this.log.info( `Starting privilege refresher with interval ${this.refreshInterval}`, ); startPolling(() => this.loadPrivilegeChecker(), this.refreshInterval); } public get(): PrivilegeChecker { return this.privilegeChecker ?? this.failOpenPrivilegeChecker; } private async loadPrivilegeChecker(): Promise { this.log.info(`Loading privilege checker from ${this.endpoint}`); try { const response = await fetch(this.endpoint); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const cosmeticsData = await response.json(); const result = CosmeticsSchema.safeParse(cosmeticsData); if (!result.success) { throw new Error(`Invalid cosmetics data: ${result.error.message}`); } this.privilegeChecker = new PrivilegeCheckerImpl( result.data, base64url.decode, ); this.log.info(`Privilege checker loaded successfully`); } catch (error) { this.log.error(`Failed to fetch cosmetics from ${this.endpoint}:`, error); throw error; } } }