Files
OmniRoute/open-sse/services/autoRefreshDaemon.ts
Praveen K Palaniswamy 65e81158ab fix(ollama): route models by advertised capability (#11088)
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host.

Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean.

Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
2026-08-23 11:45:01 -03:00

220 lines
7.2 KiB
TypeScript

/**
* AutoRefreshDaemon — Background cookie validity checker for web-cookie providers
*
* Periodically checks stored credentials for web-cookie providers by making
* lightweight requests to their home pages. If a credential is expired, it
* logs a warning and marks the credential for re-authentication.
*
* The daemon does NOT automatically re-login (that requires user interaction
* for security). It alerts the system so higher-level components can decide
* what to do (e.g., fallback to another provider, prompt user to re-login).
*/
import { TOKEN_EXTRACTION_CONFIGS } from "./tokenExtractionConfig";
// ─── Types ──────────────────────────────────────────────────────────────────
export interface DaemonStatus {
running: boolean;
checkedProviderCount: number;
expiredCredentials: string[];
lastRun: number | null;
}
interface StoredCredentialEntry {
providerId: string;
value: string;
storedAt: number;
}
// ─── Constants ──────────────────────────────────────────────────────────────
const DEFAULT_CHECK_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
const MIN_CHECK_INTERVAL_MS = 60 * 1000; // 1 minute minimum
// ─── Daemon ─────────────────────────────────────────────────────────────────
class AutoRefreshDaemon {
private timerId: ReturnType<typeof setInterval> | null = null;
private running = false;
private checkIntervalMs: number;
private expiredCredentials: string[] = [];
private lastRun: number | null = null;
/** In-memory store of web-cookie credentials (real persistence uses SQLite) */
private credentialStore = new Map<string, StoredCredentialEntry>();
constructor(checkIntervalMs = DEFAULT_CHECK_INTERVAL_MS) {
this.checkIntervalMs = Math.max(checkIntervalMs, MIN_CHECK_INTERVAL_MS);
}
/**
* Register a credential for auto-refresh monitoring.
* Called when credentials are extracted/updated.
*/
registerCredential(providerId: string, value: string): void {
this.credentialStore.set(providerId, {
providerId,
value,
storedAt: Date.now(),
});
}
/**
* Remove a credential from monitoring (e.g., provider deleted)
*/
unregisterCredential(providerId: string): void {
this.credentialStore.delete(providerId);
}
/**
* Start the daemon — begins periodic credential checks
*/
start(): void {
if (this.running) return;
this.running = true;
// Run an initial check immediately
this.check().catch(() => {});
this.timerId = setInterval(() => {
this.check().catch(() => {});
}, this.checkIntervalMs);
// Don't keep the process alive solely for this periodic daemon.
(this.timerId as { unref?: () => void })?.unref?.();
console.log(
`[AutoRefreshDaemon] Started — checking ${this.credentialStore.size} credentials every ${this.checkIntervalMs / 1000}s`
);
}
/**
* Stop the daemon
*/
stop(): void {
if (!this.running) return;
this.running = false;
if (this.timerId) {
clearInterval(this.timerId);
this.timerId = null;
}
console.log("[AutoRefreshDaemon] Stopped");
}
/**
* Check all stored credentials for validity.
* Makes a lightweight HEAD/GET request to the provider's home page.
*/
async check(): Promise<void> {
this.lastRun = Date.now();
const newlyExpired: string[] = [];
const entries = [...this.credentialStore.entries()];
for (const [providerId] of entries) {
const config = TOKEN_EXTRACTION_CONFIGS.get(providerId);
if (!config) {
this.credentialStore.delete(providerId);
continue;
}
try {
const isValid = await this.validateCredential(providerId, config.homeUrl);
if (!isValid) {
newlyExpired.push(providerId);
console.warn(
`[AutoRefreshDaemon] Credential expired for "${providerId}" (${config.displayName})`
);
}
} catch (err) {
// Network errors are non-fatal — retry next cycle. G8: log which
// provider failed so credential problems are not silently masked.
console.warn(
`[AutoRefreshDaemon] Network error validating credential for "${providerId}" — retry next cycle`,
err instanceof Error ? err.message : err
);
}
}
// Update expired list
for (const id of newlyExpired) {
if (!this.expiredCredentials.includes(id)) {
this.expiredCredentials.push(id);
}
}
}
/**
* Validate a credential by making a request to the provider's home page.
* Returns true if the response suggests the credential is still valid.
*/
private async validateCredential(providerId: string, homeUrl: string): Promise<boolean> {
const entry = this.credentialStore.get(providerId);
if (!entry) return false;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(homeUrl, {
method: "HEAD",
signal: controller.signal,
headers: {
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
},
});
// A valid credential typically returns 200 (occasionally 301/302)
// 401/403 strongly suggest expired credential
if (response.status === 401 || response.status === 403) {
return false;
}
return true;
} catch (err) {
// Network errors (timeout, DNS failure) don't mean the credential is bad.
// G8 (silent-stop fix): the previous bare `catch { return true; }` swallowed
// the error entirely — operators could never tell a credential was failing
// to validate due to network trouble. Log it (provider + reason) before
// returning the fail-open result.
console.warn(
`[AutoRefreshDaemon] Network error validating credential for "${providerId}" — treated as valid (fail-open), will retry next cycle`,
err instanceof Error ? err.message : err
);
return true;
} finally {
clearTimeout(timeout);
}
}
/**
* Get the current daemon status
*/
getStatus(): DaemonStatus {
return {
running: this.running,
checkedProviderCount: this.credentialStore.size,
expiredCredentials: [...this.expiredCredentials],
lastRun: this.lastRun,
};
}
/**
* Clear expired credentials list (e.g., after re-authentication)
*/
clearExpired(): void {
this.expiredCredentials = [];
}
/**
* Restart the daemon (useful when config changes)
*/
restart(): void {
this.stop();
this.start();
}
}
// ─── Singleton ──────────────────────────────────────────────────────────────
export const autoRefreshDaemon = new AutoRefreshDaemon();