Files
openclaw/scripts/npm-trusted-publisher-preflight.mjs
Peter Steinberger 2c0de878c6 fix(release): preflight plugin npm trusted publishers (#124095)
* fix(release): preflight npm trusted publishers

* fix(release): fully escape npm package path
2026-08-15 01:05:54 -07:00

119 lines
3.6 KiB
JavaScript

#!/usr/bin/env node
import { readBoundedResponseText } from "./lib/bounded-response.mjs";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
const AUDIENCE = "npm:registry.npmjs.org";
const NPM_REGISTRY = "https://registry.npmjs.org";
const REQUEST_TIMEOUT_MS = 20_000;
const RESPONSE_BODY_MAX_BYTES = 16 * 1024;
const PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/u;
async function requestToken(url, init, label, tokenField) {
const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
let response;
try {
response = await fetch(url, {
...init,
signal,
});
} catch (error) {
const detail =
error instanceof Error && error.name === "TimeoutError" ? " timed out" : " failed";
throw new Error(`${label}${detail}.`, { cause: error });
}
if (!response.ok) {
await response.body?.cancel().catch(() => undefined);
throw new Error(`${label} failed (HTTP ${response.status}).`);
}
let text;
try {
text = await readBoundedResponseText(response, label, RESPONSE_BODY_MAX_BYTES, { signal });
} catch (error) {
const detail =
error instanceof Error && error.name === "TimeoutError"
? " timed out"
: " returned an invalid response body";
throw new Error(`${label}${detail}.`, { cause: error });
}
let body;
try {
body = JSON.parse(text);
} catch (error) {
throw new Error(`${label} returned invalid JSON.`, { cause: error });
}
if (!body || typeof body !== "object" || Array.isArray(body)) {
throw new Error(`${label} returned an invalid response shape.`);
}
const token = body[tokenField];
if (typeof token !== "string" || token.length === 0) {
throw new Error(`${label} response is missing ${tokenField}.`);
}
return token;
}
export async function preflightNpmTrustedPublisher(packageName) {
if (
typeof packageName !== "string" ||
packageName.length > 214 ||
!PACKAGE_NAME_PATTERN.test(packageName)
) {
throw new Error("A canonical npm package name is required.");
}
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
const githubRequestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
if (!requestUrl || !githubRequestToken) {
throw new Error("GitHub OIDC request credentials are unavailable; grant id-token: write.");
}
let githubOidcUrl;
try {
githubOidcUrl = new URL(requestUrl);
} catch {
throw new Error("GitHub OIDC request URL is invalid.");
}
githubOidcUrl.searchParams.append("audience", AUDIENCE);
const githubOidcToken = await requestToken(
githubOidcUrl.href,
{
headers: {
Accept: "application/json",
Authorization: `Bearer ${githubRequestToken}`,
},
},
"GitHub OIDC token request",
"value",
);
// npm-package-arg's escapedName preserves the scope and escapes only its slash.
const escapedPackageName = packageName.replaceAll("/", "%2f");
await requestToken(
`${NPM_REGISTRY}/-/npm/v1/oidc/token/exchange/package/${escapedPackageName}`,
{
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${githubOidcToken}`,
},
},
`npm trusted-publisher exchange for ${packageName}`,
"token",
);
console.log(`npm trusted-publisher OIDC exchange verified for ${packageName}.`);
}
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
try {
await preflightNpmTrustedPublisher(process.argv[2]);
} catch (error) {
console.error(
error instanceof Error ? error.message : "npm trusted-publisher preflight failed.",
);
process.exitCode = 1;
}
}