← Files OpsTruthARCHIVED FILE
test/github-app.test.js
14.1 KB · Oct 2, 2026 · 00:31 UTC
import assert from "node:assert/strict";
import { generateKeyPairSync, verify } from "node:crypto";
import test from "node:test";
import {
createGithubAppClient,
createGithubAppJwt,
GITHUB_APP_REQUESTED_PERMISSIONS,
githubAppHealth,
} from "../src/github-app.js";
const NOW = Date.parse("2026-08-31T12:00:00.000Z");
const REPOSITORY = "Example/project";
const INSTALLATION_TOKEN = "installation-token-sentinel";
const { publicKey, privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
function appEnv(overrides = {}) {
return {
OPSTRUTH_GITHUB_APP_ID: "123456",
OPSTRUTH_GITHUB_APP_INSTALLATION_ID: "987654",
OPSTRUTH_GITHUB_APP_PRIVATE_KEY_PEM: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
OPSTRUTH_GITHUB_APP_ALLOWED_REPOSITORY: REPOSITORY,
OPSTRUTH_GITHUB_APP_ALLOWED_REPOSITORY_ID: "424242",
...overrides,
};
}
function tokenResponse(overrides = {}) {
return {
token: INSTALLATION_TOKEN,
expires_at: new Date(NOW + 60 * 60 * 1000).toISOString(),
permissions: { checks: "read", contents: "read", metadata: "read", statuses: "read" },
repository_selection: "selected",
repositories: [{ id: 424242, full_name: REPOSITORY, private: false, visibility: "public" }],
...overrides,
};
}
function decodeBase64Url(value) {
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
return Buffer.from(padded, "base64");
}
function clientOptions(fetch, extra = {}) {
return { fetch, now: () => NOW, sleep: async () => {}, ...extra };
}
test("GitHub App JWT is an RS256 credential bounded to ten minutes", async () => {
for (const type of ["pkcs8", "pkcs1"]) {
const env = appEnv({
OPSTRUTH_GITHUB_APP_PRIVATE_KEY_PEM: privateKey.export({ type, format: "pem" }).toString(),
});
const jwt = await createGithubAppJwt(env, REPOSITORY, NOW);
const [encodedHeader, encodedPayload, encodedSignature] = jwt.split(".");
assert.deepEqual(JSON.parse(decodeBase64Url(encodedHeader)), { alg: "RS256", typ: "JWT" });
const payload = JSON.parse(decodeBase64Url(encodedPayload));
assert.equal(payload.iss, "123456");
assert.equal(payload.iat, Math.floor(NOW / 1000) - 60);
assert.equal(payload.exp - payload.iat, 600);
assert.equal(payload.exp <= Math.floor(NOW / 1000) + 540, true);
assert.equal(verify("RSA-SHA256", Buffer.from(`${encodedHeader}.${encodedPayload}`), publicKey, decodeBase64Url(encodedSignature)), true);
}
});
test("GitHub App health exposes configuration state without identity or secret values", () => {
const configured = githubAppHealth(appEnv());
assert.deepEqual(configured, {
mode: "github_app_installation",
configured: true,
scope: "selected_public_repository",
});
assert.equal(JSON.stringify(configured).includes("123456"), false);
assert.equal(JSON.stringify(configured).includes(REPOSITORY), false);
assert.equal(JSON.stringify(configured).includes("424242"), false);
const partial = appEnv({ OPSTRUTH_GITHUB_APP_PRIVATE_KEY_PEM: "" });
assert.equal(githubAppHealth(partial).configured, false);
const malformed = appEnv({ OPSTRUTH_GITHUB_APP_PRIVATE_KEY_PEM: "-----BEGIN PRIVATE KEY-----\nnot-base64!\n-----END PRIVATE KEY-----" });
assert.equal(githubAppHealth(malformed).configured, false);
assert.equal(githubAppHealth({ GITHUB_READ_TOKEN: "legacy-static-token" }).configured, false);
});
test("GitHub App rejects a non-selected repository before token minting", async () => {
let requests = 0;
assert.throws(
() => createGithubAppClient(appEnv(), "Example/another", clientOptions(async () => { requests += 1; })),
(error) => error.code === "GITHUB_APP_REPOSITORY_NOT_ALLOWED" && !error.message.includes(REPOSITORY),
);
assert.equal(requests, 0);
await assert.rejects(createGithubAppJwt(appEnv(), "Example/another", NOW), { code: "GITHUB_APP_REPOSITORY_NOT_ALLOWED" });
});
test("installation token minting is restricted to the exact repository and read permissions", async () => {
const requests = [];
const fetch = async (request) => {
requests.push(request);
const url = new URL(request.url);
if (url.pathname === "/app/installations/987654/access_tokens") {
assert.equal(request.method, "POST");
assert.match(request.headers.get("authorization"), /^Bearer [^.]+\.[^.]+\.[^.]+$/);
assert.deepEqual(await request.json(), {
repositories: ["project"],
permissions: { checks: "read", contents: "read", statuses: "read" },
});
return Response.json(tokenResponse());
}
assert.equal(request.method, "GET");
assert.equal(request.headers.get("authorization"), `Bearer ${INSTALLATION_TOKEN}`);
return Response.json({ full_name: REPOSITORY, private: false, visibility: "public" });
};
const client = createGithubAppClient(appEnv(), REPOSITORY, clientOptions(fetch));
const metadata = await client.json("/repos/Example/project");
assert.equal(metadata.full_name, REPOSITORY);
assert.deepEqual(client.authority, { mode: "github_app_installation", scope: "selected_public_repository" });
assert.equal(requests.length, 2);
assert.deepEqual(GITHUB_APP_REQUESTED_PERMISSIONS, { checks: "read", contents: "read", statuses: "read" });
});
test("installation token response must be short-lived and least privilege", async (t) => {
const invalidResponses = [
["expired", { expires_at: new Date(NOW - 1).toISOString() }, "GITHUB_APP_TOKEN_INVALID"],
["near expiry", { expires_at: new Date(NOW + 15_000).toISOString() }, "GITHUB_APP_TOKEN_INVALID"],
["overlong", { expires_at: new Date(NOW + 2 * 60 * 60 * 1000).toISOString() }, "GITHUB_APP_TOKEN_INVALID"],
["all repositories", { repository_selection: "all" }, "GITHUB_APP_SCOPE_INVALID"],
["wrong repository", { repositories: [{ id: 7, full_name: "Example/another", private: false, visibility: "public" }] }, "GITHUB_APP_SCOPE_INVALID"],
["wrong immutable repository ID", { repositories: [{ id: 7, full_name: REPOSITORY, private: false, visibility: "public" }] }, "GITHUB_APP_SCOPE_INVALID"],
["missing repository identity", { repositories: [{ full_name: REPOSITORY, private: false, visibility: "public" }] }, "GITHUB_APP_SCOPE_INVALID"],
["multiple repositories", { repositories: [
{ id: 424242, full_name: REPOSITORY, private: false, visibility: "public" },
{ id: 7, full_name: "Example/another", private: false, visibility: "public" },
] }, "GITHUB_APP_SCOPE_INVALID"],
["missing checks", { permissions: { contents: "read", metadata: "read", statuses: "read" } }, "GITHUB_APP_SCOPE_INVALID"],
["write permission", { permissions: { checks: "read", contents: "write", metadata: "read", statuses: "read" } }, "GITHUB_APP_SCOPE_INVALID"],
["extra permission", { permissions: { actions: "read", checks: "read", contents: "read", metadata: "read", statuses: "read" } }, "GITHUB_APP_SCOPE_INVALID"],
];
for (const [name, overrides, code] of invalidResponses) {
await t.test(name, async () => {
const fetch = async (request) => new URL(request.url).pathname.startsWith("/app/installations/")
? Response.json(tokenResponse(overrides))
: Response.json({ ok: true });
const client = createGithubAppClient(appEnv(), REPOSITORY, clientOptions(fetch));
await assert.rejects(client.json("/repos/Example/project"), { code });
});
}
});
test("GitHub App refreshes once after 401 and never loops on revoked credentials", async () => {
let tokenMints = 0;
let evidenceReads = 0;
const fetch = async (request) => {
const url = new URL(request.url);
if (url.pathname.startsWith("/app/installations/")) {
tokenMints += 1;
return Response.json(tokenResponse({ token: `${INSTALLATION_TOKEN}-${tokenMints}` }));
}
evidenceReads += 1;
if (evidenceReads === 1) return Response.json({ message: "expired" }, { status: 401 });
if (evidenceReads === 2) return Response.json({ ok: true });
return Response.json({ token: `${INSTALLATION_TOKEN}-${tokenMints}` }, { status: 401 });
};
const client = createGithubAppClient(appEnv(), REPOSITORY, clientOptions(fetch));
assert.deepEqual(await client.json("/repos/Example/project"), { ok: true });
assert.equal(tokenMints, 2);
assert.equal(evidenceReads, 2);
await assert.rejects(client.json("/repos/Example/project/commits/head"), { code: "GITHUB_APP_AUTH_FAILED" });
assert.equal(tokenMints, 2);
assert.equal(evidenceReads, 3);
});
test("GitHub App replaces a token before its validated expiry window", async () => {
let currentTime = NOW;
let tokenMints = 0;
const fetch = async (request) => {
const url = new URL(request.url);
if (url.pathname.startsWith("/app/installations/")) {
tokenMints += 1;
return Response.json(tokenResponse({
token: `${INSTALLATION_TOKEN}-${tokenMints}`,
expires_at: new Date(currentTime + 60 * 60 * 1000).toISOString(),
}));
}
return Response.json({ ok: true });
};
const client = createGithubAppClient(appEnv(), REPOSITORY, {
fetch,
now: () => currentTime,
sleep: async () => {},
});
await client.json("/repos/Example/project");
currentTime += 60 * 60 * 1000 - 20_000;
await client.json("/repos/Example/project/commits/head");
assert.equal(tokenMints, 2);
});
test("GitHub App rejects repository identity drift during credential refresh", async () => {
let tokenMints = 0;
let evidenceReads = 0;
const client = createGithubAppClient(appEnv(), REPOSITORY, clientOptions(async (request) => {
const url = new URL(request.url);
if (url.pathname.startsWith("/app/installations/")) {
tokenMints += 1;
return Response.json(tokenResponse({
token: `${INSTALLATION_TOKEN}-${tokenMints}`,
repositories: [{ id: tokenMints === 1 ? 424242 : 777777, full_name: REPOSITORY, private: false, visibility: "public" }],
}));
}
evidenceReads += 1;
if (evidenceReads === 1) return Response.json({ id: 424242, full_name: REPOSITORY, private: false, visibility: "public" });
return Response.json({ message: "revoked" }, { status: 401 });
}));
const metadata = await client.json("/repos/Example/project");
client.assertSelectedRepository(metadata);
await assert.rejects(client.json("/repos/Example/project/commits/head"), { code: "GITHUB_APP_SCOPE_INVALID" });
assert.equal(tokenMints, 2);
});
test("GitHub App rate-limit handling retries only within fixed attempt and delay bounds", async () => {
let evidenceReads = 0;
const delays = [];
const fetch = async (request) => {
const url = new URL(request.url);
if (url.pathname.startsWith("/app/installations/")) return Response.json(tokenResponse());
evidenceReads += 1;
if (evidenceReads === 1) return Response.json({ message: "secondary rate limit" }, { status: 403, headers: { "retry-after": "0" } });
if (evidenceReads === 2) return Response.json({ message: "secondary rate limit" }, { status: 429, headers: { "retry-after": "0" } });
return Response.json({ ok: true });
};
const client = createGithubAppClient(appEnv(), REPOSITORY, clientOptions(fetch, { sleep: async (delay) => delays.push(delay) }));
assert.deepEqual(await client.json("/repos/Example/project"), { ok: true });
assert.equal(evidenceReads, 3);
assert.deepEqual(delays, [0, 0]);
let resetReads = 0;
const resetDelays = [];
const resetClient = createGithubAppClient(appEnv(), REPOSITORY, clientOptions(async (request) => {
const url = new URL(request.url);
if (url.pathname.startsWith("/app/installations/")) return Response.json(tokenResponse());
resetReads += 1;
if (resetReads === 1) {
return Response.json({ message: "primary rate limit" }, {
status: 403,
headers: { "x-ratelimit-remaining": "0", "x-ratelimit-reset": String(NOW / 1000 + 1) },
});
}
return Response.json({ ok: true });
}, { sleep: async (delay) => resetDelays.push(delay) }));
assert.deepEqual(await resetClient.json("/repos/Example/project"), { ok: true });
assert.equal(resetReads, 2);
assert.deepEqual(resetDelays, [1_000]);
let boundedReads = 0;
const boundedClient = createGithubAppClient(appEnv(), REPOSITORY, clientOptions(async (request) => {
const url = new URL(request.url);
if (url.pathname.startsWith("/app/installations/")) return Response.json(tokenResponse());
boundedReads += 1;
return Response.json({ message: "rate limit" }, { status: 403, headers: { "x-ratelimit-remaining": "0", "retry-after": "0" } });
}));
await assert.rejects(boundedClient.json("/repos/Example/project"), (error) => (
error.code === "GITHUB_APP_RATE_LIMIT" && error.limitation === "rate_limited" && error.retryAfterSeconds === 0
));
assert.equal(boundedReads, 3);
let longWindowReads = 0;
const longWindowClient = createGithubAppClient(appEnv(), REPOSITORY, clientOptions(async (request) => {
const url = new URL(request.url);
if (url.pathname.startsWith("/app/installations/")) return Response.json(tokenResponse());
longWindowReads += 1;
return Response.json({ message: "rate limit" }, { status: 429, headers: { "retry-after": "60" } });
}));
await assert.rejects(longWindowClient.json("/repos/Example/project"), { code: "GITHUB_APP_RATE_LIMIT" });
assert.equal(longWindowReads, 1);
});
test("GitHub App failures never expose private keys, JWTs, tokens, IDs, or provider bodies", async () => {
const env = appEnv();
let jwt = "";
const client = createGithubAppClient(env, REPOSITORY, clientOptions(async (request) => {
const url = new URL(request.url);
if (url.pathname.startsWith("/app/installations/")) {
jwt = request.headers.get("authorization").slice("Bearer ".length);
return Response.json(tokenResponse());
}
return new Response(`${INSTALLATION_TOKEN} ${jwt} ${env.OPSTRUTH_GITHUB_APP_PRIVATE_KEY_PEM}`, { status: 500 });
}));
let failure;
try {
await client.json("/repos/Example/project");
} catch (error) {
failure = error;
}
const rendered = JSON.stringify({ code: failure?.code, message: failure?.message });
for (const sentinel of [INSTALLATION_TOKEN, jwt, "123456", "987654", "PRIVATE KEY"]) {
assert.equal(rendered.includes(sentinel), false);
}
assert.equal(failure?.code, "GITHUB_APP_REQUEST_FAILED");
});
SHA-256: 18500718ed786449daa4a08929b14e9112c5702f650e71716577a4083ad83467