-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathpost.js
More file actions
136 lines (123 loc) · 4.16 KB
/
Copy pathpost.js
File metadata and controls
136 lines (123 loc) · 4.16 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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
const https = require("node:https");
const { host } = require("./host.js");
const { isTransientStatus, retry } = require("./retry.js");
function request(url, options = {}) {
return new Promise((resolve, reject) => {
const request = https.request(url, options, (response) => {
response.resume();
response.on("end", () => resolve(response.statusCode));
});
request.on("error", reject);
request.end();
});
}
function revocationRequestOptions(token) {
return {
method: "DELETE",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"User-Agent": "tempoxyz-gh-actions-github-sts",
"X-GitHub-Api-Version": "2022-11-28",
},
};
}
function stsRevocationRequestOptions(token) {
return {
method: "DELETE",
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
"User-Agent": "tempoxyz-gh-actions-github-sts",
},
};
}
async function revokeToken(token, stsHost, dependencies = {}) {
const send = dependencies.request || request;
const withRetry = dependencies.retry || retry;
const log = dependencies.console || console;
const stsUrl = (() => {
try {
return `https://${host(stsHost)}/sts/exchange`;
} catch {
return null;
}
})();
let stsStatus = null;
if (stsUrl) {
try {
stsStatus = await withRetry(
() => send(stsUrl, stsRevocationRequestOptions(token)),
{ label: "STS token revocation", isTransient: isTransientStatus },
);
} catch {
log.warn("STS token revocation could not reach STS; falling back to GitHub.");
}
if (stsStatus === 204) {
log.log("GitHub App token revoked and STS ledger updated.");
return;
}
if (stsStatus !== null) {
log.warn(`STS token revocation returned HTTP ${stsStatus}; falling back to GitHub.`);
}
} else {
log.warn("STS host state is unavailable; falling back to GitHub token revocation.");
}
const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
const providerStatus = await withRetry(
() => send(`${apiUrl}/installation/token`, revocationRequestOptions(token)),
{ label: "GitHub token revocation", isTransient: isTransientStatus },
);
if (providerStatus !== 204 && providerStatus !== 401) {
throw new Error(`Failed to revoke GitHub App token (HTTP ${providerStatus}).`);
}
// A provider fallback can still reconcile an existing ledger row: GitHub
// returns 401 for the now-invalid credential, which STS treats as an
// idempotent revocation success and uses to clear its stored copy.
if (stsUrl) {
try {
if ((await send(stsUrl, stsRevocationRequestOptions(token))) === 204) {
log.log("GitHub App token revoked and STS ledger updated.");
return;
}
} catch {
log.warn("STS ledger reconciliation could not reach STS.");
}
}
const outcome = providerStatus === 204 ? "revoked" : "was already invalid or expired";
log.warn(
`GitHub App token ${outcome}; its STS ledger row is already clear or reconciliation remains pending.`,
);
}
function escapeAnnotation(value) {
return value
.replaceAll("%", "%25")
.replaceAll("\r", "%0D")
.replaceAll("\n", "%0A");
}
// Revocation is best-effort. The token expires at its requested TTL, so a
// revocation that neither the STS nor GitHub can serve after retries must not
// turn a finished job red; it is reported as a warning instead.
async function main({ env = process.env, dependencies = {} } = {}) {
const token = env.STATE_token;
if (!token) {
console.log("No GitHub App token was minted; skipping revocation.");
return;
}
try {
await revokeToken(token, env.STATE_sts_host, dependencies);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
console.log(
"::warning title=GitHub App token revocation failed::" +
escapeAnnotation(`${reason} The token expires at its requested TTL.`),
);
}
}
if (require.main === module) {
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
}
module.exports = { main, revocationRequestOptions, revokeToken, stsRevocationRequestOptions };