

























@@ -9,6 +9,8 @@ const DEFAULT_REGISTRY = "https://registry.npmjs.org";
99const BULK_ADVISORY_PATH = "/-/npm/v1/security/advisories/bulk";
1010const MIN_SEVERITY = "high";
1111export const BULK_ADVISORY_ERROR_BODY_MAX_CHARS = 4096;
12+export const BULK_ADVISORY_RESPONSE_BODY_MAX_BYTES = 8 * 1024 * 1024;
13+export const BULK_ADVISORY_REQUEST_TIMEOUT_MS = 60_000;
1214const SEVERITY_RANK = {
1315info: 0,
1416low: 1,
@@ -677,6 +679,92 @@ function resolveRegistryBaseUrl() {
677679return configured.replace(/\/+$/u, "");
678680}
679681682+function parsePositiveIntegerEnv(name, fallback) {
683+const raw = process.env[name]?.trim();
684+if (!raw) {
685+return fallback;
686+}
687+const parsed = Number.parseInt(raw, 10);
688+if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
689+throw new Error(`${name} must be a positive integer`);
690+}
691+return parsed;
692+}
693+694+function resolveBulkAdvisoryRequestTimeoutMs() {
695+return parsePositiveIntegerEnv(
696+"OPENCLAW_PNPM_AUDIT_BULK_TIMEOUT_MS",
697+BULK_ADVISORY_REQUEST_TIMEOUT_MS,
698+);
699+}
700+701+function resolveBulkAdvisoryResponseBodyMaxBytes() {
702+return parsePositiveIntegerEnv(
703+"OPENCLAW_PNPM_AUDIT_BULK_RESPONSE_MAX_BYTES",
704+BULK_ADVISORY_RESPONSE_BODY_MAX_BYTES,
705+);
706+}
707+708+async function withBulkAdvisoryTimeout({ label, timeoutMs, run }) {
709+const controller = new AbortController();
710+let timeout;
711+try {
712+return await Promise.race([
713+run(controller.signal),
714+new Promise((_resolve, reject) => {
715+timeout = setTimeout(() => {
716+const error = new Error(`${label} exceeded timeout of ${timeoutMs}ms`);
717+controller.abort(error);
718+reject(error);
719+}, timeoutMs);
720+}),
721+]);
722+} finally {
723+if (timeout) {
724+clearTimeout(timeout);
725+}
726+}
727+}
728+729+async function readBoundedResponseText(response, maxBytes, label) {
730+const contentLength = Number.parseInt(response.headers?.get?.("content-length") ?? "", 10);
731+if (Number.isFinite(contentLength) && contentLength > maxBytes) {
732+throw Object.assign(new Error(`${label} exceeded ${maxBytes} bytes`), { code: "ETOOBIG" });
733+}
734+735+if (!response.body) {
736+return "";
737+}
738+739+const reader = response.body.getReader();
740+const decoder = new TextDecoder();
741+const chunks = [];
742+let totalBytes = 0;
743+try {
744+for (;;) {
745+const { done, value } = await reader.read();
746+if (done) {
747+const tail = decoder.decode();
748+if (tail) {
749+chunks.push(tail);
750+}
751+break;
752+}
753+754+totalBytes += value.byteLength;
755+if (totalBytes > maxBytes) {
756+await reader.cancel().catch(() => undefined);
757+throw Object.assign(new Error(`${label} exceeded ${maxBytes} bytes`), { code: "ETOOBIG" });
758+}
759+chunks.push(decoder.decode(value, { stream: true }));
760+}
761+} finally {
762+reader.releaseLock();
763+}
764+765+return chunks.join("");
766+}
767+680768export async function readBoundedBulkAdvisoryErrorText(
681769response,
682770maxChars = BULK_ADVISORY_ERROR_BODY_MAX_CHARS,
@@ -716,29 +804,46 @@ export async function readBoundedBulkAdvisoryErrorText(
716804return truncated ? `${text}\n[truncated]` : text;
717805}
718806807+async function readBulkAdvisoryJson(response, maxBytes) {
808+const text = await readBoundedResponseText(response, maxBytes, "Bulk advisory response body");
809+if (!text.trim()) {
810+throw new Error("Bulk advisory response body was empty");
811+}
812+return JSON.parse(text);
813+}
814+719815export async function fetchBulkAdvisories({
720816 payload,
721817 fetchImpl = fetch,
722818 registryBaseUrl = resolveRegistryBaseUrl(),
819+ responseBodyMaxBytes = resolveBulkAdvisoryResponseBodyMaxBytes(),
820+ timeoutMs = resolveBulkAdvisoryRequestTimeoutMs(),
723821}) {
724822const url = `${registryBaseUrl}${BULK_ADVISORY_PATH}`;
725-const response = await fetchImpl(url, {
726-method: "POST",
727-headers: {
728-accept: "application/json",
729-"content-type": "application/json",
730-},
731-body: JSON.stringify(payload),
732-});
823+return await withBulkAdvisoryTimeout({
824+label: "Bulk advisory request",
825+ timeoutMs,
826+run: async (signal) => {
827+const response = await fetchImpl(url, {
828+method: "POST",
829+headers: {
830+accept: "application/json",
831+"content-type": "application/json",
832+},
833+body: JSON.stringify(payload),
834+ signal,
835+});
733836734-if (!response.ok) {
735-const bodyText = await readBoundedBulkAdvisoryErrorText(response);
736-throw new Error(
737-`Bulk advisory request failed (${response.status} ${response.statusText}): ${bodyText}`,
738-);
739-}
837+ if (!response.ok) {
838+ const bodyText = await readBoundedBulkAdvisoryErrorText(response);
839+ throw new Error(
840+ `Bulk advisory request failed (${response.status} ${response.statusText}): ${bodyText}`,
841+ );
842+ }
740843741-return response.json();
844+return await readBulkAdvisoryJson(response, responseBodyMaxBytes);
845+},
846+});
742847}
743848744849export async function runPnpmAuditProd({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。