diff --git a/src/instrumentation-node.ts b/src/instrumentation-node.ts index d971716f1e..0e3fdac35b 100755 --- a/src/instrumentation-node.ts +++ b/src/instrumentation-node.ts @@ -379,6 +379,22 @@ export async function registerNodejs(): Promise { console.warn("[STARTUP] Could not restore runtime settings:", msg); } + // Proactively start the credential-health sweep at boot so stale web-session + // connections (cookies that expired overnight) get re-probed and recovered on + // startup — instead of staying red until the first real request lazily imports + // the on-demand credentialGate. Idempotent; self-disables via + // OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK and its cadence is tunable via + // CREDENTIAL_HEALTH_CHECK_INTERVAL. NOTE: this MUST live here (the real Next.js + // instrumentation startup), NOT in the unused src/server-init.ts. + try { + const { initCredentialHealthCheck } = await import("@/lib/credentialHealth/scheduler"); + initCredentialHealthCheck(); + console.log("[STARTUP] Credential health scheduler started"); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + console.warn("[STARTUP] Could not start credential health scheduler:", msg); + } + try { const { initAuditLog, cleanupExpiredLogs } = await import("@/lib/compliance/index"); initAuditLog(); diff --git a/tests/unit/credential-health-boot-wiring.test.ts b/tests/unit/credential-health-boot-wiring.test.ts new file mode 100644 index 0000000000..b2b884731a --- /dev/null +++ b/tests/unit/credential-health-boot-wiring.test.ts @@ -0,0 +1,45 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync, existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +// The credential-health sweep re-probes web-session connections and recovers ones +// whose cookies expired (the "*-web providers go red on restart" bug). scheduler.ts +// only auto-inits when something imports it; nothing did at boot, so the sweep never +// ran proactively at startup. This guards that the wiring lives in the REAL startup +// (src/instrumentation-node.ts) and NOT in the unused src/server-init.ts — the exact +// mistake that made the earlier attempt (closed PR #7432) a no-op. + +const read = (rel: string) => + readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8"); + +const instrumentation = read("../../src/instrumentation-node.ts"); + +test("credential health scheduler is started from the real Next.js instrumentation startup", () => { + assert.match( + instrumentation, + /import\(["']@\/lib\/credentialHealth\/scheduler["']\)/, + "instrumentation-node.ts must import the scheduler at boot" + ); + assert.match( + instrumentation, + /initCredentialHealthCheck\(\)/, + "instrumentation-node.ts must call initCredentialHealthCheck() at boot" + ); + assert.match( + instrumentation, + /\[STARTUP\] Credential health scheduler started/, + "a [STARTUP] log line proves the boot wiring ran (grep-able in app.log)" + ); +}); + +test("the wiring is NOT placed in the dead src/server-init.ts (the #7432 no-op)", () => { + const url = new URL("../../src/server-init.ts", import.meta.url); + const p = fileURLToPath(url); + if (!existsSync(p)) return; // file removed upstream → nothing to guard + assert.doesNotMatch( + readFileSync(p, "utf8"), + /initCredentialHealthCheck/, + "server-init.ts is unused in production; wiring there never runs" + ); +});