/** * Center-scoped admin cache. * * Holds the at-most-one admin user (email + userId) fetched from Center. * Used to decide who to deliver triage messages to as a silent observer * (wake=false), regardless of on-duty / mention status. * * Refresh policy (per spec, 2026-05-22): * • TTL = 1 day. Center admin changes are rare; agents tolerate a * day's stale cache without surprises * • on first lookup the cache lazy-fetches * • cli `admin refresh` forces an out-of-band refresh without waiting * for TTL expiry * * Failure mode: a Center fetch error is treated identically to "no * admin" — guild keeps operating without an observer. The cache holds * the failed-fetch decision for the same TTL so we don't hammer Center. */ import { Injectable, Logger } from '@nestjs/common'; import { fetchAdminEmail } from './center-auth.js'; const ADMIN_CACHE_TTL_MS = 24 * 60 * 60 * 1000; export interface CachedAdmin { email: string; userId: string; } @Injectable() export class AdminCacheService { private readonly logger = new Logger(AdminCacheService.name); private cached: CachedAdmin | null = null; private cachedAt = 0; private inflight: Promise | null = null; /** * Return the cached admin, fetching from Center if the cache is empty * or older than the TTL. Returns null if no admin is set. * * `force=true` bypasses the cache and refreshes immediately — used by * the cli refresh command. */ async get(force = false): Promise { const fresh = Date.now() - this.cachedAt < ADMIN_CACHE_TTL_MS; if (!force && this.cachedAt > 0 && fresh) { return this.cached; } if (this.inflight) return this.inflight; this.inflight = (async () => { try { const result = await fetchAdminEmail(); this.cached = result; this.cachedAt = Date.now(); this.logger.log( `admin cache refreshed: ${result ? `${result.email} (${result.userId})` : 'no admin set'}`, ); return result; } finally { this.inflight = null; } })(); return this.inflight; } /** Snapshot of the cached admin (no fetch). Returns null if not yet * populated. Used by the hot delivery path which doesn't want to * block on a Center round-trip. */ snapshot(): CachedAdmin | null { return this.cached; } }