diff --git a/scripts/pre-commit/pnpm-audit-prod.mjs b/scripts/pre-commit/pnpm-audit-prod.mjs index b274be4c3ab..919627ad96c 100644 --- a/scripts/pre-commit/pnpm-audit-prod.mjs +++ b/scripts/pre-commit/pnpm-audit-prod.mjs @@ -14,6 +14,7 @@ const MIN_SEVERITY = "high"; export const BULK_ADVISORY_ERROR_BODY_MAX_CHARS = 4096; export const BULK_ADVISORY_RESPONSE_BODY_MAX_BYTES = 8 * 1024 * 1024; export const BULK_ADVISORY_REQUEST_TIMEOUT_MS = 60_000; +const MAX_TIMER_TIMEOUT_MS = 2_147_000_000; const SEVERITY_RANK = { info: 0, low: 1, @@ -695,9 +696,11 @@ function parsePositiveIntegerEnv(name, fallback) { } function resolveBulkAdvisoryRequestTimeoutMs() { - return parsePositiveIntegerEnv( - "OPENCLAW_PNPM_AUDIT_BULK_TIMEOUT_MS", - BULK_ADVISORY_REQUEST_TIMEOUT_MS, + return clampTimerTimeoutMs( + parsePositiveIntegerEnv( + "OPENCLAW_PNPM_AUDIT_BULK_TIMEOUT_MS", + BULK_ADVISORY_REQUEST_TIMEOUT_MS, + ), ); } @@ -708,15 +711,21 @@ function resolveBulkAdvisoryResponseBodyMaxBytes() { ); } +function clampTimerTimeoutMs(valueMs) { + const value = Number.isFinite(valueMs) ? valueMs : BULK_ADVISORY_REQUEST_TIMEOUT_MS; + return Math.min(Math.max(Math.floor(value), 1), MAX_TIMER_TIMEOUT_MS); +} + async function withBulkAdvisoryTimeout({ label, timeoutMs, run }) { + const resolvedTimeoutMs = clampTimerTimeoutMs(timeoutMs); const controller = new AbortController(); let timeout; const timeoutPromise = new Promise((_resolve, reject) => { timeout = setTimeout(() => { - const error = new Error(`${label} exceeded timeout of ${timeoutMs}ms`); + const error = new Error(`${label} exceeded timeout of ${resolvedTimeoutMs}ms`); controller.abort(error); reject(error); - }, timeoutMs); + }, resolvedTimeoutMs); }); try { return await Promise.race([run({ signal: controller.signal, timeoutPromise }), timeoutPromise]); diff --git a/test/scripts/pnpm-audit-prod.test.ts b/test/scripts/pnpm-audit-prod.test.ts index aab2b57258f..c90b41928e8 100644 --- a/test/scripts/pnpm-audit-prod.test.ts +++ b/test/scripts/pnpm-audit-prod.test.ts @@ -2,6 +2,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { describe, expect, it } from "vitest"; import { collectProdResolvedPackagesFromLockfile, @@ -274,6 +275,32 @@ snapshots: expect(signal?.aborted).toBe(true); }); + it("clamps oversized bulk advisory request timers before scheduling", async () => { + let signal: AbortSignal | undefined; + const request = fetchBulkAdvisories({ + payload: { axios: ["1.0.0"] }, + timeoutMs: MAX_TIMER_TIMEOUT_MS + 1, + fetchImpl: (async (_url, init) => { + signal = init?.signal ?? undefined; + await new Promise((resolve, reject) => { + const timer = setTimeout(resolve, 25); + signal?.addEventListener( + "abort", + () => { + clearTimeout(timer); + reject(new Error("aborted")); + }, + { once: true }, + ); + }); + return new Response("{}", { status: 200 }); + }) as typeof fetch, + }); + + await expect(request).resolves.toEqual({}); + expect(signal?.aborted).toBe(false); + }); + it("cancels stalled successful bulk advisory response bodies on request timeout", async () => { let cancelled = false; const body = new ReadableStream({