Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jobs:
test -n "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}"
npm --version
echo "repository=${GITHUB_REPOSITORY} workflow=release.yml ref=${RELEASE_TAG} environment=npm"
- name: Exchange GitHub OIDC for a package-scoped npm token
run: bun run scripts/exchange-npm-oidc.ts
- name: Install exact dependencies
working-directory: typescript
run: bun install --frozen-lockfile
Expand All @@ -62,3 +64,6 @@ jobs:
if: ${{ !contains(env.RELEASE_TAG, '-rc.') }}
working-directory: typescript
run: npm publish --access public --tag latest --provenance
- name: Remove ephemeral npm credentials
if: always()
run: rm -f "${RUNNER_TEMP}/ignition-sdk-oidc.npmrc"
2 changes: 1 addition & 1 deletion .sdk-source.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"sourceCommit": "a6786f290e8e0da81eab2c3d8e6fd02a0b97b37d",
"sourceCommit": "763326ae6b5868711a24bb28f1afa21ae0b51b49",
"contract": "contracts/2026.07/public-sdk-contract.json",
"packages": [
"typescript"
Expand Down
81 changes: 81 additions & 0 deletions scripts/exchange-npm-oidc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { appendFile, writeFile } from "node:fs/promises";
import { join } from "node:path";

const PACKAGE_NAME = "@ignitionai/sdk";
const EXPECTED_REPOSITORY = "IgnitionAI/sdk";
const EXPECTED_AUDIENCE = "npm:registry.npmjs.org";
const EXPECTED_SUBJECT = `repo:${EXPECTED_REPOSITORY}:environment:npm`;

function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing GitHub OIDC runtime variable: ${name}`);
return value;
}

function decodeClaims(token: string): Record<string, unknown> {
const payload = token.split(".")[1];
if (!payload) throw new Error("GitHub returned a malformed OIDC token");
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
}

function assertClaim(claims: Record<string, unknown>, name: string, expected: string): void {
if (claims[name] !== expected) {
throw new Error(`Unexpected OIDC ${name}: ${String(claims[name])}`);
}
}

const requestUrl = new URL(required("ACTIONS_ID_TOKEN_REQUEST_URL"));
requestUrl.searchParams.set("audience", EXPECTED_AUDIENCE);
const oidcResponse = await fetch(requestUrl, {
headers: { Authorization: `Bearer ${required("ACTIONS_ID_TOKEN_REQUEST_TOKEN")}` },
});
if (!oidcResponse.ok) {
throw new Error(`GitHub OIDC request failed with HTTP ${oidcResponse.status}`);
}
const oidcPayload = await oidcResponse.json() as { value?: string };
if (!oidcPayload.value) throw new Error("GitHub OIDC response did not include a token");

const claims = decodeClaims(oidcPayload.value);
assertClaim(claims, "aud", EXPECTED_AUDIENCE);
assertClaim(claims, "repository", EXPECTED_REPOSITORY);
assertClaim(claims, "repository_visibility", "public");
assertClaim(claims, "runner_environment", "github-hosted");
assertClaim(claims, "sub", EXPECTED_SUBJECT);
const workflowRef = String(claims.job_workflow_ref ?? claims.workflow_ref ?? "");
if (!workflowRef.includes(`${EXPECTED_REPOSITORY}/.github/workflows/release.yml@`)) {
throw new Error(`Unexpected OIDC workflow reference: ${workflowRef}`);
}

const exchangeUrl = new URL(
`/-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(PACKAGE_NAME)}`,
"https://registry.npmjs.org",
);
const exchangeResponse = await fetch(exchangeUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${oidcPayload.value}`,
Accept: "application/json",
},
});
if (exchangeResponse.status !== 201) {
const errorBody = (await exchangeResponse.text()).slice(0, 1_000);
throw new Error(`npm OIDC exchange failed with HTTP ${exchangeResponse.status}: ${errorBody}`);
}
const exchange = await exchangeResponse.json() as { token?: string; expires?: string };
if (!exchange.token || /[\r\n]/.test(exchange.token)) {
throw new Error("npm OIDC exchange returned an invalid token");
}

console.log(`::add-mask::${exchange.token}`);
const githubEnv = required("GITHUB_ENV");
const npmrcPath = join(required("RUNNER_TEMP"), "ignition-sdk-oidc.npmrc");
await writeFile(
npmrcPath,
"registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\n",
{ mode: 0o600 },
);
await appendFile(
githubEnv,
`NODE_AUTH_TOKEN=${exchange.token}\nNPM_CONFIG_USERCONFIG=${npmrcPath}\n`,
);
console.log(`npm OIDC exchange accepted for ${PACKAGE_NAME}; expires=${exchange.expires ?? "unknown"}`);