mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
2c0de878c6
* fix(release): preflight npm trusted publishers * fix(release): fully escape npm package path
119 lines
3.6 KiB
JavaScript
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;
|
|
}
|
|
}
|