@stats-forge/github-stats-forge-core 0.4.0 → 0.6.0
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/build/api/contributed-to.d.ts +4 -2
- package/build/api/contributed-to.d.ts.map +1 -1
- package/build/api/contributed-to.js +1 -1
- package/build/api/gist.d.ts +4 -2
- package/build/api/gist.d.ts.map +1 -1
- package/build/api/gist.js +1 -1
- package/build/api/handler.d.ts +11 -4
- package/build/api/handler.d.ts.map +1 -1
- package/build/api/handler.js +31 -19
- package/build/api/organization.d.ts +2 -0
- package/build/api/organization.d.ts.map +1 -1
- package/build/api/organization.js +1 -1
- package/build/api/params.d.ts +2 -2
- package/build/api/params.d.ts.map +1 -1
- package/build/api/params.js +7 -7
- package/build/api/pin.d.ts +2 -0
- package/build/api/pin.d.ts.map +1 -1
- package/build/api/pin.js +1 -1
- package/build/api/stats.d.ts +2 -0
- package/build/api/stats.d.ts.map +1 -1
- package/build/api/stats.js +1 -1
- package/build/api/top-langs.d.ts +2 -0
- package/build/api/top-langs.d.ts.map +1 -1
- package/build/api/top-langs.js +1 -1
- package/build/api/wakatime.d.ts +2 -0
- package/build/api/wakatime.d.ts.map +1 -1
- package/build/api/wakatime.js +3 -1
- package/build/cards/organization/index.d.ts.map +1 -1
- package/build/cards/organization/index.js +2 -1
- package/build/common/config.d.ts +9 -3
- package/build/common/config.d.ts.map +1 -1
- package/build/common/config.js +21 -7
- package/build/common/error.d.ts +10 -1
- package/build/common/error.d.ts.map +1 -1
- package/build/common/error.js +18 -2
- package/build/common/http.d.ts +2 -0
- package/build/common/http.d.ts.map +1 -1
- package/build/common/languageColors.json +2 -0
- package/build/common/log.d.ts +4 -1
- package/build/common/log.d.ts.map +1 -1
- package/build/common/log.js +10 -3
- package/build/fetchers/organization.d.ts.map +1 -1
- package/build/fetchers/organization.js +17 -2
- package/build/fetchers/types.d.ts +2 -1
- package/build/fetchers/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/contributed-to.ts +1 -0
- package/src/api/gist.ts +1 -0
- package/src/api/handler.ts +38 -13
- package/src/api/organization.ts +1 -0
- package/src/api/params.ts +10 -10
- package/src/api/pin.ts +1 -0
- package/src/api/stats.ts +1 -0
- package/src/api/top-langs.ts +1 -0
- package/src/api/wakatime.ts +2 -0
- package/src/cards/organization/index.ts +2 -1
- package/src/common/config.ts +26 -10
- package/src/common/error.ts +24 -4
- package/src/common/http.ts +6 -1
- package/src/common/languageColors.json +2 -0
- package/src/common/log.ts +10 -3
- package/src/fetchers/organization.ts +21 -2
- package/src/fetchers/types.ts +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/common/log.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/common/log.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,QAAA,MAAM,MAAM,EAAE;IACZ,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IACvC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CAQ1C,CAAC;AAEF,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
package/build/common/log.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Where this package's diagnostics go.
|
|
3
|
+
*
|
|
4
|
+
* Dispatches to `console` at call time rather than import time,
|
|
5
|
+
* so a host or a test that replaces `console.log` is obeyed.
|
|
3
6
|
*/
|
|
4
7
|
const logger = {
|
|
5
|
-
log:
|
|
6
|
-
|
|
8
|
+
log: (...args) => {
|
|
9
|
+
console.log(...args);
|
|
10
|
+
},
|
|
11
|
+
error: (...args) => {
|
|
12
|
+
console.error(...args);
|
|
13
|
+
},
|
|
7
14
|
};
|
|
8
15
|
export { logger };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"organization.d.ts","sourceRoot":"","sources":["../../src/fetchers/organization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"organization.d.ts","sourceRoot":"","sources":["../../src/fetchers/organization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AActD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA+DnD;;;;;;;GAOG;AACH,QAAA,MAAM,iBAAiB,YACZ;IAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,UAC5B,UAAU,KACjB,OAAO,CAAC,gBAAgB,CAiF1B,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GITHUB_USERNAME_PATTERN } from '../common/constants.js';
|
|
2
2
|
import { CardError, ORGANIZATION_NOT_FOUND } from '../common/error.js';
|
|
3
3
|
import { createGraphQLFetcher } from '../common/http.js';
|
|
4
|
+
import { logger } from '../common/log.js';
|
|
4
5
|
import { retryer } from '../common/retryer.js';
|
|
5
6
|
import { GetOrganizationDocument } from '../graphql/generated/organization.js';
|
|
6
7
|
import { graphqlError } from './graphql-error.js';
|
|
@@ -38,6 +39,15 @@ const topLanguage = (repos) => {
|
|
|
38
39
|
}
|
|
39
40
|
return top;
|
|
40
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* The member count is the one field here a token can be refused:
|
|
44
|
+
* it needs the organization `Members` permission, which an app installation token rarely carries.
|
|
45
|
+
* GitHub answers `FORBIDDEN` on that field alone and still returns the organization,
|
|
46
|
+
* so the card drops the stat instead of failing whole.
|
|
47
|
+
*
|
|
48
|
+
* @returns Whether the member count is all the response was refused.
|
|
49
|
+
*/
|
|
50
|
+
const onlyMembersForbidden = (errors) => errors.every((error) => error.type === 'FORBIDDEN' && error.path?.at(-1) === 'membersWithRole');
|
|
41
51
|
/**
|
|
42
52
|
* Fetch an organization and the totals of its public repositories.
|
|
43
53
|
*
|
|
@@ -61,6 +71,7 @@ const fetchOrganization = async ({ org }, config) => {
|
|
|
61
71
|
let organization;
|
|
62
72
|
let after = null;
|
|
63
73
|
let pages = 0;
|
|
74
|
+
let membersForbidden = false;
|
|
64
75
|
do {
|
|
65
76
|
// Annotated because the walk feeds its own cursor back in, which TypeScript reads as circular.
|
|
66
77
|
const res = await retryer(fetcher, { login: org, after }, config);
|
|
@@ -73,7 +84,11 @@ const fetchOrganization = async ({ org }, config) => {
|
|
|
73
84
|
secondaryMessage: ORGANIZATION_NOT_FOUND,
|
|
74
85
|
});
|
|
75
86
|
}
|
|
76
|
-
|
|
87
|
+
if (!onlyMembersForbidden(res.data.errors)) {
|
|
88
|
+
throw graphqlError(res.data.errors, res.statusText, ORGANIZATION_ERROR);
|
|
89
|
+
}
|
|
90
|
+
logger.log(res.data.errors[0]?.message);
|
|
91
|
+
membersForbidden = true;
|
|
77
92
|
}
|
|
78
93
|
organization = res.data.data.organization ?? undefined;
|
|
79
94
|
if (!organization) {
|
|
@@ -107,7 +122,7 @@ const fetchOrganization = async ({ org }, config) => {
|
|
|
107
122
|
? target.history.totalCount
|
|
108
123
|
: 0;
|
|
109
124
|
}),
|
|
110
|
-
publicMembers: organization.membersWithRole.totalCount,
|
|
125
|
+
publicMembers: membersForbidden ? null : organization.membersWithRole.totalCount,
|
|
111
126
|
topLanguage: topLanguage(repos),
|
|
112
127
|
truncated: after !== null,
|
|
113
128
|
};
|
|
@@ -55,8 +55,9 @@ export interface OrganizationData {
|
|
|
55
55
|
/**
|
|
56
56
|
* Members GitHub shows publicly.
|
|
57
57
|
* Membership is private by default, so this counts fewer people than the organization has.
|
|
58
|
+
* `null` when the token may not read them, which is the card's cue to leave the stat out.
|
|
58
59
|
*/
|
|
59
|
-
publicMembers: number;
|
|
60
|
+
publicMembers: number | null;
|
|
60
61
|
/** The language most of those repositories name as their primary one, `null` when none do. */
|
|
61
62
|
topLanguage: {
|
|
62
63
|
name: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fetchers/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAErE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,KAAK,QAAQ,GAAG,IAAI,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,GAAG;IAC1D,eAAe,EAAE;QACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KACrB,GAAG,IAAI,CAAC;CACV,CAAC;AAEF,MAAM,WAAW,aAAa;IAE5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,aAAa,CAAC;AAEtD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,4FAA4F;IAC5F,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fetchers/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/api/gist.ts
CHANGED
package/src/api/handler.ts
CHANGED
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
import type * as z from 'zod/mini';
|
|
2
2
|
|
|
3
3
|
import type { ColorParams } from '../common/color.ts';
|
|
4
|
-
import type { CardConfig } from '../common/config.ts';
|
|
4
|
+
import type { AllowlistKind, CardConfig } from '../common/config.ts';
|
|
5
|
+
import { CardError } from '../common/error.ts';
|
|
5
6
|
|
|
6
7
|
import type { ApiResult } from './api-result.ts';
|
|
7
8
|
import { errorResult } from './api-result.ts';
|
|
8
9
|
import type { ApiQuery } from './params.ts';
|
|
9
10
|
import { parseColorParams, parseParams } from './params.ts';
|
|
10
11
|
|
|
12
|
+
/** Which of an endpoint's params name an identity, and which list guards each. */
|
|
13
|
+
type Identities<TSchema extends z.ZodMiniType> = Partial<
|
|
14
|
+
Record<Extract<keyof z.output<TSchema>, string>, AllowlistKind>
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
/** An endpoint, and the identities a pinned deployment may refuse it. */
|
|
18
|
+
type CardEndpoint<TSchema extends z.ZodMiniType> = ((
|
|
19
|
+
query: ApiQuery<TSchema>,
|
|
20
|
+
config: CardConfig,
|
|
21
|
+
) => Promise<ApiResult>) & { IDENTITIES: Identities<TSchema> };
|
|
22
|
+
|
|
11
23
|
/**
|
|
12
|
-
* An endpoint: the schema it accepts,
|
|
24
|
+
* An endpoint: the schema it accepts, the identities a pinned deployment may refuse,
|
|
25
|
+
* and what it draws from the parsed params.
|
|
13
26
|
*
|
|
14
27
|
* Colors parse first and on their own, because a rejected color cannot be used to draw its own error card.
|
|
15
|
-
* Parsing throws, fetching throws, and one `catch` turns whatever was thrown into the answer.
|
|
28
|
+
* Parsing throws, the allowlist throws, fetching throws, and one `catch` turns whatever was thrown into the answer.
|
|
16
29
|
*
|
|
17
30
|
* @returns The handler, taking the query the schema describes.
|
|
18
31
|
*/
|
|
19
|
-
const cardHandler =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
async (query: ApiQuery<TSchema>, config: CardConfig): Promise<ApiResult> => {
|
|
32
|
+
const cardHandler = <TSchema extends z.ZodMiniType>(
|
|
33
|
+
schema: TSchema,
|
|
34
|
+
identities: Identities<TSchema>,
|
|
35
|
+
render: (params: z.output<TSchema>, colors: ColorParams, config: CardConfig) => Promise<string>,
|
|
36
|
+
): CardEndpoint<TSchema> => {
|
|
37
|
+
const handler = async (query: ApiQuery<TSchema>, config: CardConfig): Promise<ApiResult> => {
|
|
25
38
|
let colors: ColorParams;
|
|
26
39
|
try {
|
|
27
40
|
colors = parseColorParams(query);
|
|
@@ -30,13 +43,25 @@ const cardHandler =
|
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
const params = parseParams(schema, query);
|
|
47
|
+
|
|
48
|
+
// before the render, so a refusal costs no rate-limit point; an absent identity is the
|
|
49
|
+
// fetcher's `missing_param` to report
|
|
50
|
+
for (const [param, kind] of Object.entries(identities) as Array<[string, AllowlistKind]>) {
|
|
51
|
+
const value: unknown = (params as Record<string, unknown>)[param];
|
|
52
|
+
if (typeof value === 'string' && !config.isAllowed(value, kind)) {
|
|
53
|
+
throw CardError.notAllowed(param);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { status: 'success', content: await render(params, colors, config) };
|
|
37
58
|
} catch (error) {
|
|
38
59
|
return errorResult(error, colors);
|
|
39
60
|
}
|
|
40
61
|
};
|
|
41
62
|
|
|
63
|
+
// carried like `OPTIONS`, so `tests/allowlist.test.ts` can assert every card declares one
|
|
64
|
+
return Object.assign(handler, { IDENTITIES: identities });
|
|
65
|
+
};
|
|
66
|
+
|
|
42
67
|
export { cardHandler };
|
package/src/api/organization.ts
CHANGED
package/src/api/params.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
import * as z from 'zod/mini';
|
|
2
|
-
|
|
3
|
-
import type { ColorParams } from '../common/color.ts';
|
|
4
|
-
import { COLOR_PARAM_KEYS, THEME_PARAM_KEYS, isValidColorInput } from '../common/color.ts';
|
|
5
|
-
import { GITHUB_USERNAME_PATTERN } from '../common/constants.ts';
|
|
6
|
-
import { getWidestRange, parseRangeDate } from '../common/date.ts';
|
|
7
|
-
import { CardError } from '../common/error.ts';
|
|
8
|
-
import { isLocaleAvailable } from '../common/localize.ts';
|
|
9
|
-
import { parseArray, parseBoolean } from '../common/ops.ts';
|
|
10
|
-
|
|
11
1
|
/**
|
|
12
2
|
* @file The api layer is the trust boundary:
|
|
13
3
|
* a query string arrives as strings, and each endpoint declares what it accepts as a schema over them.
|
|
@@ -18,6 +8,16 @@ import { parseArray, parseBoolean } from '../common/ops.ts';
|
|
|
18
8
|
* this package ships to the browser, and the functional API tree-shakes down to the checks used below.
|
|
19
9
|
*/
|
|
20
10
|
|
|
11
|
+
import * as z from 'zod/mini';
|
|
12
|
+
|
|
13
|
+
import type { ColorParams } from '../common/color.ts';
|
|
14
|
+
import { COLOR_PARAM_KEYS, THEME_PARAM_KEYS, isValidColorInput } from '../common/color.ts';
|
|
15
|
+
import { GITHUB_USERNAME_PATTERN } from '../common/constants.ts';
|
|
16
|
+
import { getWidestRange, parseRangeDate } from '../common/date.ts';
|
|
17
|
+
import { CardError } from '../common/error.ts';
|
|
18
|
+
import { isLocaleAvailable } from '../common/localize.ts';
|
|
19
|
+
import { parseArray, parseBoolean } from '../common/ops.ts';
|
|
20
|
+
|
|
21
21
|
/** Every param arrives as a string, or not at all. */
|
|
22
22
|
const rawParam = z.optional(z.string());
|
|
23
23
|
|
package/src/api/pin.ts
CHANGED
package/src/api/stats.ts
CHANGED
package/src/api/top-langs.ts
CHANGED
package/src/api/wakatime.ts
CHANGED
|
@@ -211,7 +211,8 @@ const renderCard = (
|
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
|
|
214
|
+
// A refused member count is `null`, and no row is better than a row reading "0".
|
|
215
|
+
if (shows('members') && publicMembers !== null) {
|
|
215
216
|
STATS['members'] = {
|
|
216
217
|
icon: icons.people,
|
|
217
218
|
label: t.members(),
|
package/src/common/config.ts
CHANGED
|
@@ -3,6 +3,9 @@ import type { FetchLike } from './http.ts';
|
|
|
3
3
|
|
|
4
4
|
type Env = Record<string, string | undefined>;
|
|
5
5
|
|
|
6
|
+
/** Which list guards an identity. `username` is any GitHub login, an organization's included. */
|
|
7
|
+
type AllowlistKind = 'username' | 'gist';
|
|
8
|
+
|
|
6
9
|
interface PersonalAccessToken {
|
|
7
10
|
/** Env variable the token came from — the retryer logs this name, never the value. */
|
|
8
11
|
name: string;
|
|
@@ -20,10 +23,15 @@ interface CardConfigInit {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
|
-
* @returns
|
|
26
|
+
* @returns The comma-separated values, trimmed, or `undefined` when the variable is unset or empty.
|
|
24
27
|
*/
|
|
25
|
-
const parseCsv = (value: string | undefined): Array<string> | undefined =>
|
|
26
|
-
value
|
|
28
|
+
const parseCsv = (value: string | undefined): Array<string> | undefined => {
|
|
29
|
+
const values = (value ?? '')
|
|
30
|
+
.split(',')
|
|
31
|
+
.map((entry) => entry.trim())
|
|
32
|
+
.filter((entry) => entry !== '');
|
|
33
|
+
return values.length > 0 ? values : undefined;
|
|
34
|
+
};
|
|
27
35
|
|
|
28
36
|
/**
|
|
29
37
|
* @returns Page limit: `"true"` means every page, a positive number caps the pages, anything else means one.
|
|
@@ -37,12 +45,15 @@ const parseFetchMultiPageStars = (value: string | undefined): number => {
|
|
|
37
45
|
};
|
|
38
46
|
|
|
39
47
|
/**
|
|
48
|
+
* An empty variable is an unset one: `docker compose` writes `PAT_2=` for a token left blank.
|
|
49
|
+
*
|
|
40
50
|
* @returns Personal access tokens found in the environment.
|
|
41
51
|
*/
|
|
42
52
|
const parsePATsFromEnv = (env: Env): Array<PersonalAccessToken> =>
|
|
43
53
|
Object.keys(env)
|
|
44
54
|
.filter((key) => /PAT_\d*$/.exec(key))
|
|
45
|
-
.map((name) => ({ name, value: env[name] ?? '' }))
|
|
55
|
+
.map((name) => ({ name, value: env[name] ?? '' }))
|
|
56
|
+
.filter((pat) => pat.value !== '');
|
|
46
57
|
|
|
47
58
|
/**
|
|
48
59
|
* Deployment-wide configuration for the card renderers.
|
|
@@ -75,17 +86,22 @@ export class CardConfig {
|
|
|
75
86
|
static fromEnv(env: Env): CardConfig {
|
|
76
87
|
return new CardConfig({
|
|
77
88
|
pats: parsePATsFromEnv(env),
|
|
78
|
-
usernameAllowlist: parseCsv(env['
|
|
79
|
-
gistAllowlist: parseCsv(env['
|
|
89
|
+
usernameAllowlist: parseCsv(env['ALLOWLIST']),
|
|
90
|
+
gistAllowlist: parseCsv(env['GIST_ALLOWLIST']),
|
|
80
91
|
excludeRepositories: parseCsv(env['EXCLUDE_REPO']) ?? [],
|
|
81
92
|
fetchMultiPageStars: parseFetchMultiPageStars(env['FETCH_MULTI_PAGE_STARS']),
|
|
82
93
|
});
|
|
83
94
|
}
|
|
84
95
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Matched case-insensitively, as a GitHub login is; an absent list serves anyone.
|
|
98
|
+
*
|
|
99
|
+
* @returns Whether this deployment serves the id.
|
|
100
|
+
*/
|
|
101
|
+
isAllowed(id: string, kind: AllowlistKind): boolean {
|
|
87
102
|
const list = kind === 'gist' ? this.gistAllowlist : this.usernameAllowlist;
|
|
88
|
-
|
|
103
|
+
const wanted = id.toLowerCase();
|
|
104
|
+
return list === undefined || list.some((allowed) => allowed.toLowerCase() === wanted);
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
/** @returns A copy with `overrides` applied — how a host swaps in a user's PAT per request. */
|
|
@@ -102,4 +118,4 @@ export class CardConfig {
|
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
export type { PersonalAccessToken, CardConfigInit };
|
|
121
|
+
export type { AllowlistKind, PersonalAccessToken, CardConfigInit };
|
package/src/common/error.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { OWNER_AFFILIATIONS } from './constants.ts';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @file One error type for everything a card render can fail on.
|
|
5
3
|
*
|
|
@@ -10,18 +8,25 @@ import { OWNER_AFFILIATIONS } from './constants.ts';
|
|
|
10
8
|
* so a permanent failure has to throw a `CardError` to be reported as one.
|
|
11
9
|
*/
|
|
12
10
|
|
|
11
|
+
import { OWNER_AFFILIATIONS } from './constants.ts';
|
|
12
|
+
|
|
13
13
|
/** A general message to ask user to try again later. */
|
|
14
14
|
const TRY_AGAIN_LATER = 'Please try again later';
|
|
15
15
|
|
|
16
|
+
/** The second line for an identity a pinned deployment does not serve. */
|
|
17
|
+
const NOT_ALLOWED = 'This deployment does not serve that account';
|
|
18
|
+
|
|
16
19
|
/** Why a request failed. */
|
|
17
20
|
type ErrorCode =
|
|
18
21
|
/** The query is wrong: a param is malformed, unsafe, or not renderable. */
|
|
19
22
|
| 'invalid_param'
|
|
20
23
|
/** The query is missing a param the endpoint cannot render without. */
|
|
21
24
|
| 'missing_param'
|
|
25
|
+
/** The query is well-formed, but this deployment is pinned to identities it does not name. */
|
|
26
|
+
| 'not_allowed'
|
|
22
27
|
/** The user, repository or gist does not exist. */
|
|
23
28
|
| 'not_found'
|
|
24
|
-
/** The deployment has no usable GitHub token. */
|
|
29
|
+
/** The deployment has no usable GitHub token — retryable, the fix being a token on the next start. */
|
|
25
30
|
| 'no_tokens'
|
|
26
31
|
/** Every token is rate limited. */
|
|
27
32
|
| 'rate_limited'
|
|
@@ -35,8 +40,9 @@ type ErrorCode =
|
|
|
35
40
|
const RETRYABLE: Record<ErrorCode, boolean> = {
|
|
36
41
|
invalid_param: false,
|
|
37
42
|
missing_param: false,
|
|
43
|
+
not_allowed: false,
|
|
38
44
|
not_found: false,
|
|
39
|
-
no_tokens:
|
|
45
|
+
no_tokens: true,
|
|
40
46
|
rate_limited: true,
|
|
41
47
|
upstream: true,
|
|
42
48
|
};
|
|
@@ -106,6 +112,20 @@ class CardError extends Error {
|
|
|
106
112
|
});
|
|
107
113
|
}
|
|
108
114
|
|
|
115
|
+
/**
|
|
116
|
+
* An identity this deployment is not configured to draw.
|
|
117
|
+
* Names neither the value nor the list: one is the caller's, the other the operator's.
|
|
118
|
+
*
|
|
119
|
+
* @returns The error.
|
|
120
|
+
*/
|
|
121
|
+
static notAllowed(param: string): CardError {
|
|
122
|
+
return new CardError('Not allowed', {
|
|
123
|
+
code: 'not_allowed',
|
|
124
|
+
secondaryMessage: NOT_ALLOWED,
|
|
125
|
+
param,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
/**
|
|
110
130
|
* Anything thrown that is not already a `CardError`:
|
|
111
131
|
* an upstream failure, since the query itself got this far.
|
package/src/common/http.ts
CHANGED
|
@@ -63,7 +63,12 @@ const httpRequest = async <TData>(
|
|
|
63
63
|
/** Response of a GraphQL call: the envelope the GitHub API wraps results in. */
|
|
64
64
|
type GraphQLResponse<TResult> = HttpResponse<{
|
|
65
65
|
data: TResult;
|
|
66
|
-
errors?: Array<{
|
|
66
|
+
errors?: Array<{
|
|
67
|
+
type?: string;
|
|
68
|
+
message?: string;
|
|
69
|
+
/** The field the error is about, from the operation root: `['organization', 'membersWithRole']`. */
|
|
70
|
+
path?: Array<string | number>;
|
|
71
|
+
}>;
|
|
67
72
|
}>;
|
|
68
73
|
|
|
69
74
|
/**
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"Apollo Guidance Computer": "#0B3D91",
|
|
34
34
|
"AppleScript": "#101F1F",
|
|
35
35
|
"Arc": "#aa2afe",
|
|
36
|
+
"ArkTS": "#0080ff",
|
|
36
37
|
"AsciiDoc": "#73a0c5",
|
|
37
38
|
"AspectJ": "#a957b0",
|
|
38
39
|
"Assembly": "#6E4C13",
|
|
@@ -322,6 +323,7 @@
|
|
|
322
323
|
"Kotlin": "#A97BFF",
|
|
323
324
|
"LFE": "#4C3023",
|
|
324
325
|
"LLVM": "#185619",
|
|
326
|
+
"LLVM TableGen": "#6E8B3D",
|
|
325
327
|
"LOLCODE": "#cc9900",
|
|
326
328
|
"LSL": "#3d9970",
|
|
327
329
|
"LabVIEW": "#fede06",
|
package/src/common/log.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Where this package's diagnostics go.
|
|
3
|
+
*
|
|
4
|
+
* Dispatches to `console` at call time rather than import time,
|
|
5
|
+
* so a host or a test that replaces `console.log` is obeyed.
|
|
3
6
|
*/
|
|
4
7
|
const logger: {
|
|
5
8
|
log: (...args: Array<unknown>) => void;
|
|
6
9
|
error: (...args: Array<unknown>) => void;
|
|
7
10
|
} = {
|
|
8
|
-
log:
|
|
9
|
-
|
|
11
|
+
log: (...args) => {
|
|
12
|
+
console.log(...args);
|
|
13
|
+
},
|
|
14
|
+
error: (...args) => {
|
|
15
|
+
console.error(...args);
|
|
16
|
+
},
|
|
10
17
|
};
|
|
11
18
|
|
|
12
19
|
export { logger };
|
|
@@ -3,6 +3,7 @@ import { GITHUB_USERNAME_PATTERN } from '../common/constants.ts';
|
|
|
3
3
|
import { CardError, ORGANIZATION_NOT_FOUND } from '../common/error.ts';
|
|
4
4
|
import { createGraphQLFetcher } from '../common/http.ts';
|
|
5
5
|
import type { GraphQLResponse } from '../common/http.ts';
|
|
6
|
+
import { logger } from '../common/log.ts';
|
|
6
7
|
import { retryer } from '../common/retryer.ts';
|
|
7
8
|
import { GetOrganizationDocument } from '../graphql/generated/organization.ts';
|
|
8
9
|
import type {
|
|
@@ -61,6 +62,19 @@ const topLanguage = (
|
|
|
61
62
|
return top;
|
|
62
63
|
};
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The member count is the one field here a token can be refused:
|
|
67
|
+
* it needs the organization `Members` permission, which an app installation token rarely carries.
|
|
68
|
+
* GitHub answers `FORBIDDEN` on that field alone and still returns the organization,
|
|
69
|
+
* so the card drops the stat instead of failing whole.
|
|
70
|
+
*
|
|
71
|
+
* @returns Whether the member count is all the response was refused.
|
|
72
|
+
*/
|
|
73
|
+
const onlyMembersForbidden = (
|
|
74
|
+
errors: NonNullable<GraphQLResponse<unknown>['data']['errors']>,
|
|
75
|
+
): boolean =>
|
|
76
|
+
errors.every((error) => error.type === 'FORBIDDEN' && error.path?.at(-1) === 'membersWithRole');
|
|
77
|
+
|
|
64
78
|
/**
|
|
65
79
|
* Fetch an organization and the totals of its public repositories.
|
|
66
80
|
*
|
|
@@ -88,6 +102,7 @@ const fetchOrganization = async (
|
|
|
88
102
|
let organization: Organization | undefined;
|
|
89
103
|
let after: string | null = null;
|
|
90
104
|
let pages = 0;
|
|
105
|
+
let membersForbidden = false;
|
|
91
106
|
|
|
92
107
|
do {
|
|
93
108
|
// Annotated because the walk feeds its own cursor back in, which TypeScript reads as circular.
|
|
@@ -105,7 +120,11 @@ const fetchOrganization = async (
|
|
|
105
120
|
secondaryMessage: ORGANIZATION_NOT_FOUND,
|
|
106
121
|
});
|
|
107
122
|
}
|
|
108
|
-
|
|
123
|
+
if (!onlyMembersForbidden(res.data.errors)) {
|
|
124
|
+
throw graphqlError(res.data.errors, res.statusText, ORGANIZATION_ERROR);
|
|
125
|
+
}
|
|
126
|
+
logger.log(res.data.errors[0]?.message);
|
|
127
|
+
membersForbidden = true;
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
organization = res.data.data.organization ?? undefined;
|
|
@@ -144,7 +163,7 @@ const fetchOrganization = async (
|
|
|
144
163
|
? target.history.totalCount
|
|
145
164
|
: 0;
|
|
146
165
|
}),
|
|
147
|
-
publicMembers: organization.membersWithRole.totalCount,
|
|
166
|
+
publicMembers: membersForbidden ? null : organization.membersWithRole.totalCount,
|
|
148
167
|
topLanguage: topLanguage(repos),
|
|
149
168
|
truncated: after !== null,
|
|
150
169
|
};
|
package/src/fetchers/types.ts
CHANGED
|
@@ -61,8 +61,9 @@ export interface OrganizationData {
|
|
|
61
61
|
/**
|
|
62
62
|
* Members GitHub shows publicly.
|
|
63
63
|
* Membership is private by default, so this counts fewer people than the organization has.
|
|
64
|
+
* `null` when the token may not read them, which is the card's cue to leave the stat out.
|
|
64
65
|
*/
|
|
65
|
-
publicMembers: number;
|
|
66
|
+
publicMembers: number | null;
|
|
66
67
|
/** The language most of those repositories name as their primary one, `null` when none do. */
|
|
67
68
|
topLanguage: { name: string; color: string | null } | null;
|
|
68
69
|
/** Whether the repository walk stopped at its page cap, leaving the totals short. */
|