mirror of
https://github.com/anuraghazra/github-readme-stats.git
synced 2024-12-27 06:25:47 +08:00
159 lines
4.5 KiB
JavaScript
159 lines
4.5 KiB
JavaScript
/**
|
|
* @file Contains a simple cloud function that can be used to check which PATs are no
|
|
* longer working. It returns a list of valid PATs, expired PATs and PATs with errors.
|
|
*
|
|
* @description This function is currently rate limited to 1 request per 5 minutes.
|
|
*/
|
|
|
|
import { logger, request, dateDiff } from "../../src/common/utils.js";
|
|
export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 5 minutes
|
|
|
|
/**
|
|
* @typedef {import('axios').AxiosRequestHeaders} AxiosRequestHeaders Axios request headers.
|
|
* @typedef {import('axios').AxiosResponse} AxiosResponse Axios response.
|
|
*/
|
|
|
|
/**
|
|
* Simple uptime check fetcher for the PATs.
|
|
*
|
|
* @param {AxiosRequestHeaders} variables Fetcher variables.
|
|
* @param {string} token GitHub token.
|
|
* @returns {Promise<AxiosResponse>} The response.
|
|
*/
|
|
const uptimeFetcher = (variables, token) => {
|
|
return request(
|
|
{
|
|
query: `
|
|
query {
|
|
rateLimit {
|
|
remaining
|
|
resetAt
|
|
},
|
|
}`,
|
|
variables,
|
|
},
|
|
{
|
|
Authorization: `bearer ${token}`,
|
|
},
|
|
);
|
|
};
|
|
|
|
const getAllPATs = () => {
|
|
return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key));
|
|
};
|
|
|
|
/**
|
|
* @typedef {(variables: AxiosRequestHeaders, token: string) => Promise<AxiosResponse>} Fetcher The fetcher function.
|
|
* @typedef {{validPATs: string[], expiredPATs: string[], exhaustedPATs: string[], suspendedPATs: string[], errorPATs: string[], details: any}} PATInfo The PAT info.
|
|
*/
|
|
|
|
/**
|
|
* Check whether any of the PATs is expired.
|
|
*
|
|
* @param {Fetcher} fetcher The fetcher function.
|
|
* @param {AxiosRequestHeaders} variables Fetcher variables.
|
|
* @returns {Promise<PATInfo>} The response.
|
|
*/
|
|
const getPATInfo = async (fetcher, variables) => {
|
|
const details = {};
|
|
const PATs = getAllPATs();
|
|
|
|
for (const pat of PATs) {
|
|
try {
|
|
const response = await fetcher(variables, process.env[pat]);
|
|
const errors = response.data.errors;
|
|
const hasErrors = Boolean(errors);
|
|
const errorType = errors?.[0]?.type;
|
|
const isRateLimited =
|
|
(hasErrors && errorType === "RATE_LIMITED") ||
|
|
response.data.data?.rateLimit?.remaining === 0;
|
|
|
|
// Store PATs with errors.
|
|
if (hasErrors && errorType !== "RATE_LIMITED") {
|
|
details[pat] = {
|
|
status: "error",
|
|
error: {
|
|
type: errors[0].type,
|
|
message: errors[0].message,
|
|
},
|
|
};
|
|
continue;
|
|
} else if (isRateLimited) {
|
|
const date1 = new Date();
|
|
const date2 = new Date(response.data?.data?.rateLimit?.resetAt);
|
|
details[pat] = {
|
|
status: "exhausted",
|
|
remaining: 0,
|
|
resetIn: dateDiff(date2, date1) + " minutes",
|
|
};
|
|
} else {
|
|
details[pat] = {
|
|
status: "valid",
|
|
remaining: response.data.data.rateLimit.remaining,
|
|
};
|
|
}
|
|
} catch (err) {
|
|
// Store the PAT if it is expired.
|
|
const errorMessage = err.response?.data?.message?.toLowerCase();
|
|
if (errorMessage === "bad credentials") {
|
|
details[pat] = {
|
|
status: "expired",
|
|
};
|
|
} else if (errorMessage === "sorry. your account was suspended.") {
|
|
details[pat] = {
|
|
status: "suspended",
|
|
};
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
const filterPATsByStatus = (status) => {
|
|
return Object.keys(details).filter((pat) => details[pat].status === status);
|
|
};
|
|
|
|
const sortedDetails = Object.keys(details)
|
|
.sort()
|
|
.reduce((obj, key) => {
|
|
obj[key] = details[key];
|
|
return obj;
|
|
}, {});
|
|
|
|
return {
|
|
validPATs: filterPATsByStatus("valid"),
|
|
expiredPATs: filterPATsByStatus("expired"),
|
|
exhaustedPATs: filterPATsByStatus("exhausted"),
|
|
suspendedPATs: filterPATsByStatus("suspended"),
|
|
errorPATs: filterPATsByStatus("error"),
|
|
details: sortedDetails,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Cloud function that returns information about the used PATs.
|
|
*
|
|
* @param {any} _ The request.
|
|
* @param {any} res The response.
|
|
* @returns {Promise<void>} The response.
|
|
*/
|
|
export default async (_, res) => {
|
|
res.setHeader("Content-Type", "application/json");
|
|
try {
|
|
// Add header to prevent abuse.
|
|
const PATsInfo = await getPATInfo(uptimeFetcher, {});
|
|
if (PATsInfo) {
|
|
res.setHeader(
|
|
"Cache-Control",
|
|
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
|
|
);
|
|
}
|
|
res.send(JSON.stringify(PATsInfo, null, 2));
|
|
} catch (err) {
|
|
// Throw error if something went wrong.
|
|
logger.error(err);
|
|
res.setHeader("Cache-Control", "no-store");
|
|
res.send("Something went wrong: " + err.message);
|
|
}
|
|
};
|