@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.
Files changed (63) hide show
  1. package/build/api/contributed-to.d.ts +4 -2
  2. package/build/api/contributed-to.d.ts.map +1 -1
  3. package/build/api/contributed-to.js +1 -1
  4. package/build/api/gist.d.ts +4 -2
  5. package/build/api/gist.d.ts.map +1 -1
  6. package/build/api/gist.js +1 -1
  7. package/build/api/handler.d.ts +11 -4
  8. package/build/api/handler.d.ts.map +1 -1
  9. package/build/api/handler.js +31 -19
  10. package/build/api/organization.d.ts +2 -0
  11. package/build/api/organization.d.ts.map +1 -1
  12. package/build/api/organization.js +1 -1
  13. package/build/api/params.d.ts +2 -2
  14. package/build/api/params.d.ts.map +1 -1
  15. package/build/api/params.js +7 -7
  16. package/build/api/pin.d.ts +2 -0
  17. package/build/api/pin.d.ts.map +1 -1
  18. package/build/api/pin.js +1 -1
  19. package/build/api/stats.d.ts +2 -0
  20. package/build/api/stats.d.ts.map +1 -1
  21. package/build/api/stats.js +1 -1
  22. package/build/api/top-langs.d.ts +2 -0
  23. package/build/api/top-langs.d.ts.map +1 -1
  24. package/build/api/top-langs.js +1 -1
  25. package/build/api/wakatime.d.ts +2 -0
  26. package/build/api/wakatime.d.ts.map +1 -1
  27. package/build/api/wakatime.js +3 -1
  28. package/build/cards/organization/index.d.ts.map +1 -1
  29. package/build/cards/organization/index.js +2 -1
  30. package/build/common/config.d.ts +9 -3
  31. package/build/common/config.d.ts.map +1 -1
  32. package/build/common/config.js +21 -7
  33. package/build/common/error.d.ts +10 -1
  34. package/build/common/error.d.ts.map +1 -1
  35. package/build/common/error.js +18 -2
  36. package/build/common/http.d.ts +2 -0
  37. package/build/common/http.d.ts.map +1 -1
  38. package/build/common/languageColors.json +2 -0
  39. package/build/common/log.d.ts +4 -1
  40. package/build/common/log.d.ts.map +1 -1
  41. package/build/common/log.js +10 -3
  42. package/build/fetchers/organization.d.ts.map +1 -1
  43. package/build/fetchers/organization.js +17 -2
  44. package/build/fetchers/types.d.ts +2 -1
  45. package/build/fetchers/types.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/src/api/contributed-to.ts +1 -0
  48. package/src/api/gist.ts +1 -0
  49. package/src/api/handler.ts +38 -13
  50. package/src/api/organization.ts +1 -0
  51. package/src/api/params.ts +10 -10
  52. package/src/api/pin.ts +1 -0
  53. package/src/api/stats.ts +1 -0
  54. package/src/api/top-langs.ts +1 -0
  55. package/src/api/wakatime.ts +2 -0
  56. package/src/cards/organization/index.ts +2 -1
  57. package/src/common/config.ts +26 -10
  58. package/src/common/error.ts +24 -4
  59. package/src/common/http.ts +6 -1
  60. package/src/common/languageColors.json +2 -0
  61. package/src/common/log.ts +10 -3
  62. package/src/fetchers/organization.ts +21 -2
  63. 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;;GAEG;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;CAI1C,CAAC;AAEF,OAAO,EAAE,MAAM,EAAE,CAAC"}
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"}
@@ -1,8 +1,15 @@
1
1
  /**
2
- * Return console instance based on the environment.
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: console.log,
6
- error: console.error,
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;AAatD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAkDnD;;;;;;;GAOG;AACH,QAAA,MAAM,iBAAiB,YACZ;IAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,UAC5B,UAAU,KACjB,OAAO,CAAC,gBAAgB,CA4E1B,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,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
- throw graphqlError(res.data.errors, res.statusText, ORGANIZATION_ERROR);
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":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fetchers/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stats-forge/github-stats-forge-core",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Dynamically generate stats for your GitHub readme",
5
5
  "keywords": [
6
6
  "card-generator",
@@ -44,6 +44,7 @@ const contributedToQuery = z
44
44
  */
45
45
  export const contributedTo = cardHandler(
46
46
  contributedToQuery,
47
+ { username: 'username' },
47
48
  async (
48
49
  {
49
50
  username,
package/src/api/gist.ts CHANGED
@@ -23,6 +23,7 @@ const gistQuery = z.object({
23
23
  */
24
24
  export const gist = cardHandler(
25
25
  gistQuery,
26
+ { id: 'gist' },
26
27
  async (
27
28
  { id, locale, border_radius, show_owner, browser_rendering, hide_border },
28
29
  colors,
@@ -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, and what it draws from the parsed params.
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
- <TSchema extends z.ZodMiniType>(
21
- schema: TSchema,
22
- render: (params: z.output<TSchema>, colors: ColorParams, config: CardConfig) => Promise<string>,
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
- return {
34
- status: 'success',
35
- content: await render(parseParams(schema, query), colors, config),
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 };
@@ -40,6 +40,7 @@ const orgQuery = z.object({
40
40
  */
41
41
  const renderOrg = cardHandler(
42
42
  orgQuery,
43
+ { org: 'username' },
43
44
  async (
44
45
  {
45
46
  org,
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
@@ -40,6 +40,7 @@ const pinQuery = z.object({
40
40
  */
41
41
  const renderPin = cardHandler(
42
42
  pinQuery,
43
+ { username: 'username' },
43
44
  async (
44
45
  {
45
46
  username,
package/src/api/stats.ts CHANGED
@@ -58,6 +58,7 @@ const statsQuery = z
58
58
  */
59
59
  const renderStats = cardHandler(
60
60
  statsQuery,
61
+ { username: 'username' },
61
62
  async (
62
63
  {
63
64
  username,
@@ -45,6 +45,7 @@ const topLangsQuery = z.object({
45
45
  */
46
46
  const renderTopLangs = cardHandler(
47
47
  topLangsQuery,
48
+ { username: 'username' },
48
49
  async (
49
50
  {
50
51
  username,
@@ -43,6 +43,8 @@ const wakatimeQuery = z.object({
43
43
  */
44
44
  const renderWakatime = cardHandler(
45
45
  wakatimeQuery,
46
+ // no allowlist: `username` is a WakaTime profile, not a GitHub login (see `apps/server/README.md`)
47
+ {},
46
48
  async (
47
49
  {
48
50
  username,
@@ -211,7 +211,8 @@ const renderCard = (
211
211
  };
212
212
  }
213
213
 
214
- if (shows('members')) {
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(),
@@ -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 Parsed string values.
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 ? value.split(',') : undefined;
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['WHITELIST']),
79
- gistAllowlist: parseCsv(env['GIST_WHITELIST']),
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
- /** @returns Whether this deployment serves the id. */
86
- isAllowed(id: string, kind: 'username' | 'gist'): boolean {
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
- return list === undefined || list.includes(id);
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 };
@@ -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: false,
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.
@@ -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<{ type?: string; message?: string }>;
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
- * Return console instance based on the environment.
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: console.log,
9
- error: console.error,
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
- throw graphqlError(res.data.errors, res.statusText, ORGANIZATION_ERROR);
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
  };
@@ -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. */