-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub-app-token.mjs
More file actions
96 lines (86 loc) · 3.04 KB
/
github-app-token.mjs
File metadata and controls
96 lines (86 loc) · 3.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#!/usr/bin/env node
/**
* Generate a GitHub App installation access token.
*
* Reads credentials from:
* ~/.config/github-app/app_id — numeric App ID
* ~/.config/github-app/installation_id — numeric Installation ID
* ~/.config/github-app/private_key.pem — RSA private key (PEM)
*
* Usage:
* node scripts/github-app-token.mjs
* # prints the token to stdout
*
* # use directly with gh CLI:
* GH_TOKEN=$(node scripts/github-app-token.mjs) gh pr list --repo owner/repo
*
* # or export for the session:
* export GH_TOKEN=$(node scripts/github-app-token.mjs)
*/
import { createSign } from "node:crypto";
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
const CONFIG_DIR = join(homedir(), ".config", "github-app");
function readCredential(filename) {
const filePath = join(CONFIG_DIR, filename);
if (!existsSync(filePath)) {
console.error(`Missing: ${filePath}`);
console.error(`\nSetup instructions:`);
console.error(` mkdir -p ~/.config/github-app`);
console.error(` echo "YOUR_APP_ID" > ~/.config/github-app/app_id`);
console.error(` echo "YOUR_INSTALLATION_ID" > ~/.config/github-app/installation_id`);
console.error(` cp /path/to/private-key.pem ~/.config/github-app/private_key.pem`);
console.error(` chmod 600 ~/.config/github-app/private_key.pem`);
process.exit(1);
}
return readFileSync(filePath, "utf-8").trim();
}
function base64url(buffer) {
return Buffer.from(buffer)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function buildJwt(appId, privateKey) {
const now = Math.floor(Date.now() / 1000);
const header = base64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
const payload = base64url(
JSON.stringify({
iat: now - 60, // issued 60s ago to allow clock drift
exp: now + 540, // expires in 9 minutes (max 10m)
iss: appId,
}),
);
const data = `${header}.${payload}`;
const sign = createSign("RSA-SHA256");
sign.update(data);
const signature = base64url(sign.sign(privateKey));
return `${data}.${signature}`;
}
async function getInstallationToken(jwt, installationId) {
const url = `https://api.github.com/app/installations/${installationId}/access_tokens`;
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${jwt}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "foxfang-github-app",
},
});
if (!response.ok) {
const body = await response.text();
console.error(`GitHub API error ${response.status}: ${body}`);
process.exit(1);
}
const data = await response.json();
return data.token;
}
const appId = readCredential("app_id");
const installationId = readCredential("installation_id");
const privateKey = readCredential("private_key.pem");
const jwt = buildJwt(appId, privateKey);
const token = await getInstallationToken(jwt, installationId);
process.stdout.write(token);