@dytsou/github-readme-stats 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/api/gist.js +98 -0
- package/api/index.js +146 -0
- package/api/pin.js +114 -0
- package/api/status/pat-info.js +193 -0
- package/api/status/up.js +129 -0
- package/api/top-langs.js +163 -0
- package/api/wakatime.js +129 -0
- package/package.json +102 -0
- package/readme.md +412 -0
- package/src/calculateRank.js +87 -0
- package/src/cards/gist.js +151 -0
- package/src/cards/index.js +4 -0
- package/src/cards/repo.js +199 -0
- package/src/cards/stats.js +607 -0
- package/src/cards/top-languages.js +968 -0
- package/src/cards/types.d.ts +67 -0
- package/src/cards/wakatime.js +482 -0
- package/src/common/Card.js +294 -0
- package/src/common/I18n.js +41 -0
- package/src/common/access.js +69 -0
- package/src/common/api-utils.js +221 -0
- package/src/common/blacklist.js +10 -0
- package/src/common/cache.js +153 -0
- package/src/common/color.js +194 -0
- package/src/common/envs.js +15 -0
- package/src/common/error.js +84 -0
- package/src/common/fmt.js +90 -0
- package/src/common/html.js +43 -0
- package/src/common/http.js +24 -0
- package/src/common/icons.js +63 -0
- package/src/common/index.js +13 -0
- package/src/common/languageColors.json +651 -0
- package/src/common/log.js +14 -0
- package/src/common/ops.js +124 -0
- package/src/common/render.js +261 -0
- package/src/common/retryer.js +97 -0
- package/src/common/worker-adapter.js +148 -0
- package/src/common/worker-env.js +48 -0
- package/src/fetchers/gist.js +114 -0
- package/src/fetchers/repo.js +118 -0
- package/src/fetchers/stats.js +350 -0
- package/src/fetchers/top-languages.js +192 -0
- package/src/fetchers/types.d.ts +118 -0
- package/src/fetchers/wakatime.js +109 -0
- package/src/index.js +2 -0
- package/src/translations.js +1105 -0
- package/src/worker.ts +116 -0
- package/themes/README.md +229 -0
- package/themes/index.js +467 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Anurag Hazra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/api/gist.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { renderGistCard } from "../src/cards/gist.js";
|
|
4
|
+
import { guardAccess } from "../src/common/access.js";
|
|
5
|
+
import {
|
|
6
|
+
createValidatedColorOptions,
|
|
7
|
+
handleApiError,
|
|
8
|
+
setSvgContentType,
|
|
9
|
+
} from "../src/common/api-utils.js";
|
|
10
|
+
import {
|
|
11
|
+
CACHE_TTL,
|
|
12
|
+
resolveCacheSeconds,
|
|
13
|
+
setCacheHeaders,
|
|
14
|
+
} from "../src/common/cache.js";
|
|
15
|
+
import { parseBoolean } from "../src/common/ops.js";
|
|
16
|
+
import { fetchGist } from "../src/fetchers/gist.js";
|
|
17
|
+
import { isLocaleAvailable } from "../src/translations.js";
|
|
18
|
+
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
export default async (req, res) => {
|
|
21
|
+
const {
|
|
22
|
+
id,
|
|
23
|
+
title_color,
|
|
24
|
+
icon_color,
|
|
25
|
+
text_color,
|
|
26
|
+
bg_color,
|
|
27
|
+
theme,
|
|
28
|
+
cache_seconds,
|
|
29
|
+
locale: rawLocale,
|
|
30
|
+
border_radius,
|
|
31
|
+
border_color,
|
|
32
|
+
show_owner,
|
|
33
|
+
hide_border,
|
|
34
|
+
} = req.query;
|
|
35
|
+
|
|
36
|
+
// Only allow supported locales - validate and sanitize to prevent XSS
|
|
37
|
+
const locale =
|
|
38
|
+
typeof rawLocale === "string" && isLocaleAvailable(rawLocale)
|
|
39
|
+
? rawLocale.toLowerCase()
|
|
40
|
+
: undefined;
|
|
41
|
+
|
|
42
|
+
setSvgContentType(res);
|
|
43
|
+
|
|
44
|
+
// Create validated color options once for reuse
|
|
45
|
+
const colorOptions = createValidatedColorOptions({
|
|
46
|
+
title_color,
|
|
47
|
+
text_color,
|
|
48
|
+
bg_color,
|
|
49
|
+
border_color,
|
|
50
|
+
theme,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const access = guardAccess({
|
|
54
|
+
res,
|
|
55
|
+
id,
|
|
56
|
+
type: "gist",
|
|
57
|
+
colors: colorOptions,
|
|
58
|
+
});
|
|
59
|
+
if (!access.isPassed) {
|
|
60
|
+
return access.result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const gistData = await fetchGist(id);
|
|
65
|
+
const cacheSeconds = resolveCacheSeconds({
|
|
66
|
+
requested: parseInt(cache_seconds, 10),
|
|
67
|
+
def: CACHE_TTL.GIST_CARD.DEFAULT,
|
|
68
|
+
min: CACHE_TTL.GIST_CARD.MIN,
|
|
69
|
+
max: CACHE_TTL.GIST_CARD.MAX,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
setCacheHeaders(res, cacheSeconds);
|
|
73
|
+
|
|
74
|
+
return res.send(
|
|
75
|
+
renderGistCard(gistData, {
|
|
76
|
+
title_color,
|
|
77
|
+
icon_color,
|
|
78
|
+
text_color,
|
|
79
|
+
bg_color,
|
|
80
|
+
theme,
|
|
81
|
+
border_radius: (() => {
|
|
82
|
+
// Validate border_radius: must be a finite number between 0 and 50
|
|
83
|
+
const num = parseFloat(border_radius);
|
|
84
|
+
if (isNaN(num) || !isFinite(num) || num < 0 || num > 50) {
|
|
85
|
+
return undefined; // Let card use its default
|
|
86
|
+
}
|
|
87
|
+
return num;
|
|
88
|
+
})(),
|
|
89
|
+
border_color,
|
|
90
|
+
locale,
|
|
91
|
+
show_owner: parseBoolean(show_owner),
|
|
92
|
+
hide_border: parseBoolean(hide_border),
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return handleApiError({ res, error: err, colorOptions });
|
|
97
|
+
}
|
|
98
|
+
};
|
package/api/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { renderStatsCard } from "../src/cards/stats.js";
|
|
4
|
+
import { guardAccess } from "../src/common/access.js";
|
|
5
|
+
import {
|
|
6
|
+
createValidatedColorOptions,
|
|
7
|
+
handleApiError,
|
|
8
|
+
setSvgContentType,
|
|
9
|
+
} from "../src/common/api-utils.js";
|
|
10
|
+
import {
|
|
11
|
+
CACHE_TTL,
|
|
12
|
+
resolveCacheSeconds,
|
|
13
|
+
setCacheHeaders,
|
|
14
|
+
} from "../src/common/cache.js";
|
|
15
|
+
import { parseArray, parseBoolean } from "../src/common/ops.js";
|
|
16
|
+
import { clampValue } from "../src/common/ops.js";
|
|
17
|
+
import { fetchStats } from "../src/fetchers/stats.js";
|
|
18
|
+
import { isLocaleAvailable } from "../src/translations.js";
|
|
19
|
+
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
export default async (req, res) => {
|
|
22
|
+
const {
|
|
23
|
+
username,
|
|
24
|
+
hide,
|
|
25
|
+
hide_title,
|
|
26
|
+
hide_border,
|
|
27
|
+
card_width,
|
|
28
|
+
hide_rank,
|
|
29
|
+
show_icons,
|
|
30
|
+
include_all_commits,
|
|
31
|
+
commits_year,
|
|
32
|
+
line_height,
|
|
33
|
+
title_color,
|
|
34
|
+
ring_color,
|
|
35
|
+
icon_color,
|
|
36
|
+
text_color,
|
|
37
|
+
text_bold,
|
|
38
|
+
bg_color,
|
|
39
|
+
theme,
|
|
40
|
+
cache_seconds,
|
|
41
|
+
exclude_repo,
|
|
42
|
+
custom_title,
|
|
43
|
+
locale: rawLocale,
|
|
44
|
+
disable_animations,
|
|
45
|
+
border_radius,
|
|
46
|
+
number_format,
|
|
47
|
+
number_precision,
|
|
48
|
+
border_color,
|
|
49
|
+
rank_icon,
|
|
50
|
+
show,
|
|
51
|
+
} = req.query;
|
|
52
|
+
|
|
53
|
+
// Only allow supported locales - validate and sanitize to prevent XSS
|
|
54
|
+
const locale =
|
|
55
|
+
typeof rawLocale === "string" && isLocaleAvailable(rawLocale)
|
|
56
|
+
? rawLocale.toLowerCase()
|
|
57
|
+
: undefined;
|
|
58
|
+
|
|
59
|
+
setSvgContentType(res);
|
|
60
|
+
|
|
61
|
+
// Create validated color options once for reuse
|
|
62
|
+
const colorOptions = createValidatedColorOptions({
|
|
63
|
+
title_color,
|
|
64
|
+
text_color,
|
|
65
|
+
bg_color,
|
|
66
|
+
border_color,
|
|
67
|
+
theme,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const access = guardAccess({
|
|
71
|
+
res,
|
|
72
|
+
id: username,
|
|
73
|
+
type: "username",
|
|
74
|
+
colors: colorOptions,
|
|
75
|
+
});
|
|
76
|
+
if (!access.isPassed) {
|
|
77
|
+
return access.result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const showStats = parseArray(show);
|
|
82
|
+
const stats = await fetchStats(
|
|
83
|
+
username,
|
|
84
|
+
parseBoolean(include_all_commits),
|
|
85
|
+
parseArray(exclude_repo),
|
|
86
|
+
showStats.includes("prs_merged") ||
|
|
87
|
+
showStats.includes("prs_merged_percentage"),
|
|
88
|
+
showStats.includes("discussions_started"),
|
|
89
|
+
showStats.includes("discussions_answered"),
|
|
90
|
+
parseInt(commits_year, 10),
|
|
91
|
+
);
|
|
92
|
+
const cacheSeconds = resolveCacheSeconds({
|
|
93
|
+
requested: parseInt(cache_seconds, 10),
|
|
94
|
+
def: CACHE_TTL.STATS_CARD.DEFAULT,
|
|
95
|
+
min: CACHE_TTL.STATS_CARD.MIN,
|
|
96
|
+
max: CACHE_TTL.STATS_CARD.MAX,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
setCacheHeaders(res, cacheSeconds);
|
|
100
|
+
|
|
101
|
+
// Sanitize border_radius: parse, clamp, only include if valid
|
|
102
|
+
const borderRadiusNum = Number(border_radius);
|
|
103
|
+
const sanitizedBorderRadius =
|
|
104
|
+
Number.isFinite(borderRadiusNum) && border_radius !== undefined
|
|
105
|
+
? clampValue(borderRadiusNum, 0, 50)
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
const renderOptions = {
|
|
109
|
+
hide: parseArray(hide),
|
|
110
|
+
show_icons: parseBoolean(show_icons),
|
|
111
|
+
hide_title: parseBoolean(hide_title),
|
|
112
|
+
hide_border: parseBoolean(hide_border),
|
|
113
|
+
card_width: parseInt(card_width, 10),
|
|
114
|
+
hide_rank: parseBoolean(hide_rank),
|
|
115
|
+
include_all_commits: parseBoolean(include_all_commits),
|
|
116
|
+
commits_year: parseInt(commits_year, 10),
|
|
117
|
+
line_height,
|
|
118
|
+
title_color,
|
|
119
|
+
ring_color,
|
|
120
|
+
icon_color,
|
|
121
|
+
text_color,
|
|
122
|
+
text_bold: parseBoolean(text_bold),
|
|
123
|
+
bg_color,
|
|
124
|
+
theme,
|
|
125
|
+
// Validate custom_title is a string (prevents array from duplicate query params)
|
|
126
|
+
// Card.js handles HTML encoding internally
|
|
127
|
+
custom_title: typeof custom_title === "string" ? custom_title : undefined,
|
|
128
|
+
border_color,
|
|
129
|
+
number_format,
|
|
130
|
+
number_precision: parseInt(number_precision, 10),
|
|
131
|
+
locale,
|
|
132
|
+
disable_animations: parseBoolean(disable_animations),
|
|
133
|
+
rank_icon,
|
|
134
|
+
show: showStats,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Only include border_radius if it's valid, otherwise let Card use default
|
|
138
|
+
if (sanitizedBorderRadius !== undefined) {
|
|
139
|
+
renderOptions.border_radius = sanitizedBorderRadius;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return res.send(renderStatsCard(stats, renderOptions));
|
|
143
|
+
} catch (err) {
|
|
144
|
+
return handleApiError({ res, error: err, colorOptions });
|
|
145
|
+
}
|
|
146
|
+
};
|
package/api/pin.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { renderRepoCard } from "../src/cards/repo.js";
|
|
4
|
+
import { guardAccess } from "../src/common/access.js";
|
|
5
|
+
import {
|
|
6
|
+
createValidatedColorOptions,
|
|
7
|
+
handleApiError,
|
|
8
|
+
setSvgContentType,
|
|
9
|
+
} from "../src/common/api-utils.js";
|
|
10
|
+
import {
|
|
11
|
+
CACHE_TTL,
|
|
12
|
+
resolveCacheSeconds,
|
|
13
|
+
setCacheHeaders,
|
|
14
|
+
} from "../src/common/cache.js";
|
|
15
|
+
import { clampValue, parseBoolean } from "../src/common/ops.js";
|
|
16
|
+
import { fetchRepo } from "../src/fetchers/repo.js";
|
|
17
|
+
import { isLocaleAvailable } from "../src/translations.js";
|
|
18
|
+
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
export default async (req, res) => {
|
|
21
|
+
const {
|
|
22
|
+
username,
|
|
23
|
+
repo,
|
|
24
|
+
hide_border,
|
|
25
|
+
title_color,
|
|
26
|
+
icon_color,
|
|
27
|
+
text_color,
|
|
28
|
+
bg_color,
|
|
29
|
+
theme,
|
|
30
|
+
show_owner,
|
|
31
|
+
cache_seconds,
|
|
32
|
+
locale: rawLocale,
|
|
33
|
+
border_radius: rawBorderRadius,
|
|
34
|
+
border_color,
|
|
35
|
+
description_lines_count,
|
|
36
|
+
} = req.query;
|
|
37
|
+
|
|
38
|
+
// Only allow supported locales - validate and sanitize to prevent XSS
|
|
39
|
+
// Validate and sanitize border_radius to prevent XSS
|
|
40
|
+
const border_radius = (() => {
|
|
41
|
+
const br = parseFloat(rawBorderRadius);
|
|
42
|
+
if (isNaN(br)) {
|
|
43
|
+
return 4.5;
|
|
44
|
+
}
|
|
45
|
+
// Clamp to reasonable range; SVG border radius shouldn't exceed half width/height.
|
|
46
|
+
return Math.max(0, Math.min(br, 50));
|
|
47
|
+
})();
|
|
48
|
+
const locale =
|
|
49
|
+
typeof rawLocale === "string" && isLocaleAvailable(rawLocale)
|
|
50
|
+
? rawLocale.toLowerCase()
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
53
|
+
setSvgContentType(res);
|
|
54
|
+
|
|
55
|
+
// Create validated color options once for reuse
|
|
56
|
+
const colorOptions = createValidatedColorOptions({
|
|
57
|
+
title_color,
|
|
58
|
+
text_color,
|
|
59
|
+
bg_color,
|
|
60
|
+
border_color,
|
|
61
|
+
theme,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const access = guardAccess({
|
|
65
|
+
res,
|
|
66
|
+
id: username,
|
|
67
|
+
type: "username",
|
|
68
|
+
colors: colorOptions,
|
|
69
|
+
});
|
|
70
|
+
if (!access.isPassed) {
|
|
71
|
+
return access.result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const repoData = await fetchRepo(username, repo);
|
|
76
|
+
const cacheSeconds = resolveCacheSeconds({
|
|
77
|
+
requested: parseInt(cache_seconds, 10),
|
|
78
|
+
def: CACHE_TTL.PIN_CARD.DEFAULT,
|
|
79
|
+
min: CACHE_TTL.PIN_CARD.MIN,
|
|
80
|
+
max: CACHE_TTL.PIN_CARD.MAX,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
setCacheHeaders(res, cacheSeconds);
|
|
84
|
+
|
|
85
|
+
// Sanitize border_radius: parse, clamp, only include if valid
|
|
86
|
+
const borderRadiusNum = Number(border_radius);
|
|
87
|
+
const sanitizedBorderRadius =
|
|
88
|
+
Number.isFinite(borderRadiusNum) && border_radius !== undefined
|
|
89
|
+
? clampValue(borderRadiusNum, 0, 50)
|
|
90
|
+
: undefined;
|
|
91
|
+
|
|
92
|
+
const renderOptions = {
|
|
93
|
+
hide_border: parseBoolean(hide_border),
|
|
94
|
+
title_color,
|
|
95
|
+
icon_color,
|
|
96
|
+
text_color,
|
|
97
|
+
bg_color,
|
|
98
|
+
theme,
|
|
99
|
+
border_color,
|
|
100
|
+
show_owner: parseBoolean(show_owner),
|
|
101
|
+
locale,
|
|
102
|
+
description_lines_count,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Only include border_radius if it's valid, otherwise let Card use default
|
|
106
|
+
if (sanitizedBorderRadius !== undefined) {
|
|
107
|
+
renderOptions.border_radius = sanitizedBorderRadius;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return res.send(renderRepoCard(repoData, renderOptions));
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return handleApiError({ res, error: err, colorOptions });
|
|
113
|
+
}
|
|
114
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Contains a simple cloud function that can be used to check which PATs are no
|
|
5
|
+
* longer working. It returns a list of valid PATs, expired PATs and PATs with errors.
|
|
6
|
+
*
|
|
7
|
+
* @description This function is currently rate limited to 1 request per 5 minutes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { request } from "../../src/common/http.js";
|
|
11
|
+
import { logger } from "../../src/common/log.js";
|
|
12
|
+
import { dateDiff } from "../../src/common/ops.js";
|
|
13
|
+
import { encodeHTML } from "../../src/common/html.js";
|
|
14
|
+
import { setJsonContentType } from "../../src/common/api-utils.js";
|
|
15
|
+
|
|
16
|
+
export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 5 minutes
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Simple uptime check fetcher for the PATs.
|
|
20
|
+
*
|
|
21
|
+
* @param {Record<string, unknown>} variables Fetcher variables.
|
|
22
|
+
* @param {string} token GitHub token.
|
|
23
|
+
* @returns {Promise<import('axios').AxiosResponse>} The response.
|
|
24
|
+
*/
|
|
25
|
+
const uptimeFetcher = (variables, token) => {
|
|
26
|
+
return request(
|
|
27
|
+
{
|
|
28
|
+
query: `
|
|
29
|
+
query {
|
|
30
|
+
rateLimit {
|
|
31
|
+
remaining
|
|
32
|
+
resetAt
|
|
33
|
+
},
|
|
34
|
+
}`,
|
|
35
|
+
variables,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
Authorization: `bearer ${token}`,
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Retrieves all PAT environment variable keys.
|
|
45
|
+
*
|
|
46
|
+
* @returns {string[]} Array of PAT environment variable names.
|
|
47
|
+
*/
|
|
48
|
+
const getAllPATs = () => {
|
|
49
|
+
return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {(variables: Record<string, unknown>, token: string) => Promise<import('axios').AxiosResponse>} Fetcher
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} PATDetails
|
|
58
|
+
* @property {string} status - The PAT status.
|
|
59
|
+
* @property {number} [remaining] - Remaining API calls.
|
|
60
|
+
* @property {string} [resetIn] - Time until rate limit reset.
|
|
61
|
+
* @property {{ type: string, message: string }} [error] - Error details.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {Object} PATInfo
|
|
66
|
+
* @property {string[]} validPATs - List of valid PATs.
|
|
67
|
+
* @property {string[]} expiredPATs - List of expired PATs.
|
|
68
|
+
* @property {string[]} exhaustedPATs - List of rate-limited PATs.
|
|
69
|
+
* @property {string[]} suspendedPATs - List of suspended PATs.
|
|
70
|
+
* @property {string[]} errorPATs - List of PATs with errors.
|
|
71
|
+
* @property {Record<string, PATDetails>} details - Detailed status of each PAT.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check whether any of the PATs is expired.
|
|
76
|
+
*
|
|
77
|
+
* @param {Fetcher} fetcher The fetcher function.
|
|
78
|
+
* @param {Record<string, unknown>} variables Fetcher variables.
|
|
79
|
+
* @returns {Promise<PATInfo>} The response.
|
|
80
|
+
*/
|
|
81
|
+
const getPATInfo = async (fetcher, variables) => {
|
|
82
|
+
/** @type {Record<string, PATDetails>} */
|
|
83
|
+
const details = {};
|
|
84
|
+
const PATs = getAllPATs();
|
|
85
|
+
|
|
86
|
+
for (const pat of PATs) {
|
|
87
|
+
try {
|
|
88
|
+
const token = process.env[pat];
|
|
89
|
+
if (!token) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const response = await fetcher(variables, token);
|
|
94
|
+
const errors = response.data.errors;
|
|
95
|
+
const hasErrors = Boolean(errors);
|
|
96
|
+
const errorType = errors?.[0]?.type;
|
|
97
|
+
const isRateLimited =
|
|
98
|
+
(hasErrors && errorType === "RATE_LIMITED") ||
|
|
99
|
+
response.data.data?.rateLimit?.remaining === 0;
|
|
100
|
+
|
|
101
|
+
// Store PATs with errors
|
|
102
|
+
if (hasErrors && errorType !== "RATE_LIMITED") {
|
|
103
|
+
details[pat] = {
|
|
104
|
+
status: "error",
|
|
105
|
+
error: {
|
|
106
|
+
type: errors[0].type,
|
|
107
|
+
message: errors[0].message,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (isRateLimited) {
|
|
114
|
+
const now = new Date();
|
|
115
|
+
const resetAt = new Date(response.data?.data?.rateLimit?.resetAt);
|
|
116
|
+
details[pat] = {
|
|
117
|
+
status: "exhausted",
|
|
118
|
+
remaining: 0,
|
|
119
|
+
resetIn: dateDiff(resetAt, now) + " minutes",
|
|
120
|
+
};
|
|
121
|
+
} else {
|
|
122
|
+
details[pat] = {
|
|
123
|
+
status: "valid",
|
|
124
|
+
remaining: response.data.data.rateLimit.remaining,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// Handle known error responses
|
|
129
|
+
const errorMessage = err?.response?.data?.message?.toLowerCase();
|
|
130
|
+
if (errorMessage === "bad credentials") {
|
|
131
|
+
details[pat] = { status: "expired" };
|
|
132
|
+
} else if (errorMessage === "sorry. your account was suspended.") {
|
|
133
|
+
details[pat] = { status: "suspended" };
|
|
134
|
+
} else {
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Filters PATs by status.
|
|
142
|
+
* @param {string} status - The status to filter by.
|
|
143
|
+
* @returns {string[]} PATs with the specified status.
|
|
144
|
+
*/
|
|
145
|
+
const filterPATsByStatus = (status) => {
|
|
146
|
+
return Object.keys(details).filter((pat) => details[pat].status === status);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Sort details by key for consistent output
|
|
150
|
+
const sortedDetails = Object.keys(details)
|
|
151
|
+
.sort()
|
|
152
|
+
.reduce((obj, key) => {
|
|
153
|
+
obj[key] = details[key];
|
|
154
|
+
return obj;
|
|
155
|
+
}, /** @type {Record<string, PATDetails>} */ ({}));
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
validPATs: filterPATsByStatus("valid"),
|
|
159
|
+
expiredPATs: filterPATsByStatus("expired"),
|
|
160
|
+
exhaustedPATs: filterPATsByStatus("exhausted"),
|
|
161
|
+
suspendedPATs: filterPATsByStatus("suspended"),
|
|
162
|
+
errorPATs: filterPATsByStatus("error"),
|
|
163
|
+
details: sortedDetails,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Cloud function that returns information about the used PATs.
|
|
169
|
+
*
|
|
170
|
+
* @param {unknown} _req The request (unused).
|
|
171
|
+
* @param {import('express').Response} res The response.
|
|
172
|
+
* @returns {Promise<void>} The response.
|
|
173
|
+
*/
|
|
174
|
+
export default async (_req, res) => {
|
|
175
|
+
setJsonContentType(res);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const PATsInfo = await getPATInfo(uptimeFetcher, {});
|
|
179
|
+
if (PATsInfo) {
|
|
180
|
+
res.setHeader(
|
|
181
|
+
"Cache-Control",
|
|
182
|
+
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
res.send(JSON.stringify(PATsInfo, null, 2));
|
|
186
|
+
} catch (err) {
|
|
187
|
+
logger.error(err);
|
|
188
|
+
res.setHeader("Cache-Control", "no-store");
|
|
189
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown error";
|
|
190
|
+
const safeMessage = encodeHTML(errorMessage);
|
|
191
|
+
res.send("Something went wrong: " + safeMessage);
|
|
192
|
+
}
|
|
193
|
+
};
|
package/api/status/up.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Contains a simple cloud function that can be used to check if the PATs are still
|
|
5
|
+
* functional.
|
|
6
|
+
*
|
|
7
|
+
* @description This function is currently rate limited to 1 request per 5 minutes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { request } from "../../src/common/http.js";
|
|
11
|
+
import retryer from "../../src/common/retryer.js";
|
|
12
|
+
import { logger } from "../../src/common/log.js";
|
|
13
|
+
import { encodeHTML } from "../../src/common/html.js";
|
|
14
|
+
import { setJsonContentType } from "../../src/common/api-utils.js";
|
|
15
|
+
|
|
16
|
+
export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 5 minutes
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Simple uptime check fetcher for the PATs.
|
|
20
|
+
*
|
|
21
|
+
* @param {Record<string, unknown>} variables Fetcher variables.
|
|
22
|
+
* @param {string} token GitHub token.
|
|
23
|
+
* @returns {Promise<import('axios').AxiosResponse>} The response.
|
|
24
|
+
*/
|
|
25
|
+
const uptimeFetcher = (variables, token) => {
|
|
26
|
+
return request(
|
|
27
|
+
{
|
|
28
|
+
query: `
|
|
29
|
+
query {
|
|
30
|
+
rateLimit {
|
|
31
|
+
remaining
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
`,
|
|
35
|
+
variables,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
Authorization: `bearer ${token}`,
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} ShieldsResponse
|
|
45
|
+
* @property {number} schemaVersion - Shields.io schema version.
|
|
46
|
+
* @property {string} label - Badge label.
|
|
47
|
+
* @property {"up" | "down"} message - Status message.
|
|
48
|
+
* @property {"brightgreen" | "red"} color - Badge color.
|
|
49
|
+
* @property {boolean} isError - Whether this is an error state.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates JSON response for shields.io dynamic card generation.
|
|
54
|
+
*
|
|
55
|
+
* @param {boolean} up Whether the PATs are up or not.
|
|
56
|
+
* @returns {ShieldsResponse} Dynamic shields.io JSON response object.
|
|
57
|
+
* @see https://shields.io/endpoint
|
|
58
|
+
*/
|
|
59
|
+
const shieldsUptimeBadge = (up) => ({
|
|
60
|
+
schemaVersion: 1,
|
|
61
|
+
label: "Public Instance",
|
|
62
|
+
message: up ? "up" : "down",
|
|
63
|
+
color: up ? "brightgreen" : "red",
|
|
64
|
+
isError: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validates and normalizes the response type parameter.
|
|
69
|
+
*
|
|
70
|
+
* @param {string|undefined} type - The type parameter from query.
|
|
71
|
+
* @returns {"shields" | "json" | "boolean"} Normalized type value.
|
|
72
|
+
*/
|
|
73
|
+
const normalizeResponseType = (type) => {
|
|
74
|
+
const normalized = typeof type === "string" ? type.toLowerCase() : "boolean";
|
|
75
|
+
if (normalized === "shields" || normalized === "json") {
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
return "boolean";
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Cloud function that returns whether the PATs are still functional.
|
|
83
|
+
*
|
|
84
|
+
* @param {import('express').Request} req The request.
|
|
85
|
+
* @param {import('express').Response} res The response.
|
|
86
|
+
* @returns {Promise<void>} Nothing.
|
|
87
|
+
*/
|
|
88
|
+
export default async (req, res) => {
|
|
89
|
+
const responseType = normalizeResponseType(req.query.type);
|
|
90
|
+
|
|
91
|
+
setJsonContentType(res);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
let PATsValid = true;
|
|
95
|
+
try {
|
|
96
|
+
await retryer(uptimeFetcher, {});
|
|
97
|
+
} catch {
|
|
98
|
+
// PAT validation failed - mark as invalid
|
|
99
|
+
PATsValid = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (PATsValid) {
|
|
103
|
+
res.setHeader(
|
|
104
|
+
"Cache-Control",
|
|
105
|
+
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
res.setHeader("Cache-Control", "no-store");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
switch (responseType) {
|
|
112
|
+
case "shields":
|
|
113
|
+
res.send(shieldsUptimeBadge(PATsValid));
|
|
114
|
+
break;
|
|
115
|
+
case "json":
|
|
116
|
+
res.send({ up: PATsValid });
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
res.send(PATsValid);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
logger.error(err);
|
|
124
|
+
res.setHeader("Cache-Control", "no-store");
|
|
125
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown error";
|
|
126
|
+
const safeMessage = encodeHTML(errorMessage);
|
|
127
|
+
res.send("Something went wrong: " + safeMessage);
|
|
128
|
+
}
|
|
129
|
+
};
|