@stats-forge/github-stats-forge-core 0.5.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 (49) 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/common/config.d.ts +9 -3
  29. package/build/common/config.d.ts.map +1 -1
  30. package/build/common/config.js +21 -7
  31. package/build/common/error.d.ts +10 -1
  32. package/build/common/error.d.ts.map +1 -1
  33. package/build/common/error.js +18 -2
  34. package/build/common/log.d.ts +4 -1
  35. package/build/common/log.d.ts.map +1 -1
  36. package/build/common/log.js +10 -3
  37. package/package.json +1 -1
  38. package/src/api/contributed-to.ts +1 -0
  39. package/src/api/gist.ts +1 -0
  40. package/src/api/handler.ts +38 -13
  41. package/src/api/organization.ts +1 -0
  42. package/src/api/params.ts +10 -10
  43. package/src/api/pin.ts +1 -0
  44. package/src/api/stats.ts +1 -0
  45. package/src/api/top-langs.ts +1 -0
  46. package/src/api/wakatime.ts +2 -0
  47. package/src/common/config.ts +26 -10
  48. package/src/common/error.ts +24 -4
  49. package/src/common/log.ts +10 -3
@@ -4,7 +4,7 @@ import * as z from 'zod/mini';
4
4
  *
5
5
  * @returns The rendered card, or a rendered error.
6
6
  */
7
- export declare const contributedTo: (query: import("./params.ts").ApiQuery<z.ZodMiniObject<{
7
+ export declare const contributedTo: ((query: import("./params.ts").ApiQuery<z.ZodMiniObject<{
8
8
  username: z.ZodMiniType<string | undefined, string | undefined, z.core.$ZodTypeInternals<string | undefined, string | undefined>>;
9
9
  locale: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<string | undefined, string | undefined>>;
10
10
  repos_count: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<number | undefined, string | undefined>>;
@@ -19,5 +19,7 @@ export declare const contributedTo: (query: import("./params.ts").ApiQuery<z.Zod
19
19
  custom_title: z.ZodMiniOptional<z.ZodMiniString<string>>;
20
20
  border_radius: z.ZodMiniType<number | undefined, string | undefined, z.core.$ZodTypeInternals<number | undefined, string | undefined>>;
21
21
  disable_animations: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<boolean | undefined, string | undefined>>;
22
- }, z.core.$strip>>, config: import("./index.ts").CardConfig) => Promise<import("./api-result.ts").ApiResult>;
22
+ }, z.core.$strip>>, config: import("./index.ts").CardConfig) => Promise<import("./api-result.ts").ApiResult>) & {
23
+ IDENTITIES: Partial<Record<"border_radius" | "card_width" | "custom_title" | "disable_animations" | "exclude_repo" | "from" | "hide_border" | "hide_title" | "hide_years" | "include_own_repos" | "locale" | "repos_count" | "to" | "username", import("../common/config.ts").AllowlistKind>>;
24
+ };
23
25
  //# sourceMappingURL=contributed-to.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contributed-to.d.ts","sourceRoot":"","sources":["../../src/api/contributed-to.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAuC9B;;;;GAIG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;4GAuCzB,CAAC"}
1
+ {"version":3,"file":"contributed-to.d.ts","sourceRoot":"","sources":["../../src/api/contributed-to.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAuC9B;;;;GAIG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;CAwCzB,CAAC"}
@@ -27,7 +27,7 @@ const contributedToQuery = z
27
27
  *
28
28
  * @returns The rendered card, or a rendered error.
29
29
  */
30
- export const contributedTo = cardHandler(contributedToQuery, async ({ username, locale, repos_count, include_own_repos, exclude_repo, from, to, hide_years, hide_title, hide_border, card_width, custom_title, border_radius, disable_animations, }, colors, config) => {
30
+ export const contributedTo = cardHandler(contributedToQuery, { username: 'username' }, async ({ username, locale, repos_count, include_own_repos, exclude_repo, from, to, hide_years, hide_title, hide_border, card_width, custom_title, border_radius, disable_animations, }, colors, config) => {
31
31
  const data = await fetchContributedTo({ username, include_own_repos, repos_count, exclude_repo, from, to }, config);
32
32
  return renderContributedToCard(data, {
33
33
  ...colors,
@@ -4,12 +4,14 @@ import * as z from 'zod/mini';
4
4
  *
5
5
  * @returns The rendered card, or a rendered error.
6
6
  */
7
- export declare const gist: (query: import("./params.ts").ApiQuery<z.ZodMiniObject<{
7
+ export declare const gist: ((query: import("./params.ts").ApiQuery<z.ZodMiniObject<{
8
8
  id: z.ZodMiniType<string | undefined, string | undefined, z.core.$ZodTypeInternals<string | undefined, string | undefined>>;
9
9
  locale: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<string | undefined, string | undefined>>;
10
10
  border_radius: z.ZodMiniType<number | undefined, string | undefined, z.core.$ZodTypeInternals<number | undefined, string | undefined>>;
11
11
  show_owner: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<boolean | undefined, string | undefined>>;
12
12
  browser_rendering: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<boolean | undefined, string | undefined>>;
13
13
  hide_border: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<boolean | undefined, string | undefined>>;
14
- }, z.core.$strip>>, config: import("./index.ts").CardConfig) => Promise<import("./api-result.ts").ApiResult>;
14
+ }, z.core.$strip>>, config: import("./index.ts").CardConfig) => Promise<import("./api-result.ts").ApiResult>) & {
15
+ IDENTITIES: Partial<Record<"border_radius" | "browser_rendering" | "hide_border" | "id" | "locale" | "show_owner", import("../common/config.ts").AllowlistKind>>;
16
+ };
15
17
  //# sourceMappingURL=gist.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gist.d.ts","sourceRoot":"","sources":["../../src/api/gist.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAkB9B;;;;GAIG;AACH,eAAO,MAAM,IAAI;;;;;;;4GAkBhB,CAAC"}
1
+ {"version":3,"file":"gist.d.ts","sourceRoot":"","sources":["../../src/api/gist.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAkB9B;;;;GAIG;AACH,eAAO,MAAM,IAAI;;;;;;;;;CAmBhB,CAAC"}
package/build/api/gist.js CHANGED
@@ -17,7 +17,7 @@ const gistQuery = z.object({
17
17
  *
18
18
  * @returns The rendered card, or a rendered error.
19
19
  */
20
- export const gist = cardHandler(gistQuery, async ({ id, locale, border_radius, show_owner, browser_rendering, hide_border }, colors, config) => {
20
+ export const gist = cardHandler(gistQuery, { id: 'gist' }, async ({ id, locale, border_radius, show_owner, browser_rendering, hide_border }, colors, config) => {
21
21
  const gistData = await fetchGist({ id }, config);
22
22
  return renderGistCard(gistData, {
23
23
  ...colors,
@@ -1,16 +1,23 @@
1
1
  import type * as z from 'zod/mini';
2
2
  import type { ColorParams } from '../common/color.ts';
3
- import type { CardConfig } from '../common/config.ts';
3
+ import type { AllowlistKind, CardConfig } from '../common/config.ts';
4
4
  import type { ApiResult } from './api-result.ts';
5
5
  import type { ApiQuery } from './params.ts';
6
+ /** Which of an endpoint's params name an identity, and which list guards each. */
7
+ type Identities<TSchema extends z.ZodMiniType> = Partial<Record<Extract<keyof z.output<TSchema>, string>, AllowlistKind>>;
8
+ /** An endpoint, and the identities a pinned deployment may refuse it. */
9
+ type CardEndpoint<TSchema extends z.ZodMiniType> = ((query: ApiQuery<TSchema>, config: CardConfig) => Promise<ApiResult>) & {
10
+ IDENTITIES: Identities<TSchema>;
11
+ };
6
12
  /**
7
- * An endpoint: the schema it accepts, and what it draws from the parsed params.
13
+ * An endpoint: the schema it accepts, the identities a pinned deployment may refuse,
14
+ * and what it draws from the parsed params.
8
15
  *
9
16
  * Colors parse first and on their own, because a rejected color cannot be used to draw its own error card.
10
- * Parsing throws, fetching throws, and one `catch` turns whatever was thrown into the answer.
17
+ * Parsing throws, the allowlist throws, fetching throws, and one `catch` turns whatever was thrown into the answer.
11
18
  *
12
19
  * @returns The handler, taking the query the schema describes.
13
20
  */
14
- declare const cardHandler: <TSchema extends z.ZodMiniType>(schema: TSchema, render: (params: z.output<TSchema>, colors: ColorParams, config: CardConfig) => Promise<string>) => (query: ApiQuery<TSchema>, config: CardConfig) => Promise<ApiResult>;
21
+ declare const cardHandler: <TSchema extends z.ZodMiniType>(schema: TSchema, identities: Identities<TSchema>, render: (params: z.output<TSchema>, colors: ColorParams, config: CardConfig) => Promise<string>) => CardEndpoint<TSchema>;
15
22
  export { cardHandler };
16
23
  //# sourceMappingURL=handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,CAAC,MAAM,UAAU,CAAC;AAEnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C;;;;;;;GAOG;AACH,QAAA,MAAM,WAAW,GACd,OAAO,SAAS,CAAC,CAAC,WAAW,UACpB,OAAO,UACP,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,MAAM,CAAC,aAEnF,QAAQ,CAAC,OAAO,CAAC,UAAU,UAAU,KAAG,OAAO,CAAC,SAAS,CAgBtE,CAAC;AAEJ,OAAO,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,CAAC,MAAM,UAAU,CAAC;AAEnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,kFAAkF;AAClF,KAAK,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,WAAW,IAAI,OAAO,CACtD,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,CAChE,CAAC;AAEF,yEAAyE;AACzE,KAAK,YAAY,CAAC,OAAO,SAAS,CAAC,CAAC,WAAW,IAAI,CAAC,CAClD,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,EACxB,MAAM,EAAE,UAAU,KACf,OAAO,CAAC,SAAS,CAAC,CAAC,GAAG;IAAE,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC;AAE/D;;;;;;;;GAQG;AACH,QAAA,MAAM,WAAW,GAAI,OAAO,SAAS,CAAC,CAAC,WAAW,UACxC,OAAO,cACH,UAAU,CAAC,OAAO,CAAC,UACvB,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,MAAM,CAAC,KAC9F,YAAY,CAAC,OAAO,CA6BtB,CAAC;AAEF,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -1,29 +1,41 @@
1
+ import { CardError } from '../common/error.js';
1
2
  import { errorResult } from './api-result.js';
2
3
  import { parseColorParams, parseParams } from './params.js';
3
4
  /**
4
- * An endpoint: the schema it accepts, and what it draws from the parsed params.
5
+ * An endpoint: the schema it accepts, the identities a pinned deployment may refuse,
6
+ * and what it draws from the parsed params.
5
7
  *
6
8
  * Colors parse first and on their own, because a rejected color cannot be used to draw its own error card.
7
- * Parsing throws, fetching throws, and one `catch` turns whatever was thrown into the answer.
9
+ * Parsing throws, the allowlist throws, fetching throws, and one `catch` turns whatever was thrown into the answer.
8
10
  *
9
11
  * @returns The handler, taking the query the schema describes.
10
12
  */
11
- const cardHandler = (schema, render) => async (query, config) => {
12
- let colors;
13
- try {
14
- colors = parseColorParams(query);
15
- }
16
- catch (error) {
17
- return errorResult(error);
18
- }
19
- try {
20
- return {
21
- status: 'success',
22
- content: await render(parseParams(schema, query), colors, config),
23
- };
24
- }
25
- catch (error) {
26
- return errorResult(error, colors);
27
- }
13
+ const cardHandler = (schema, identities, render) => {
14
+ const handler = async (query, config) => {
15
+ let colors;
16
+ try {
17
+ colors = parseColorParams(query);
18
+ }
19
+ catch (error) {
20
+ return errorResult(error);
21
+ }
22
+ try {
23
+ const params = parseParams(schema, query);
24
+ // before the render, so a refusal costs no rate-limit point; an absent identity is the
25
+ // fetcher's `missing_param` to report
26
+ for (const [param, kind] of Object.entries(identities)) {
27
+ const value = params[param];
28
+ if (typeof value === 'string' && !config.isAllowed(value, kind)) {
29
+ throw CardError.notAllowed(param);
30
+ }
31
+ }
32
+ return { status: 'success', content: await render(params, colors, config) };
33
+ }
34
+ catch (error) {
35
+ return errorResult(error, colors);
36
+ }
37
+ };
38
+ // carried like `OPTIONS`, so `tests/allowlist.test.ts` can assert every card declares one
39
+ return Object.assign(handler, { IDENTITIES: identities });
28
40
  };
29
41
  export { cardHandler };
@@ -20,6 +20,8 @@ export declare const org: ((query: import("./params.ts").ApiQuery<z.ZodMiniObjec
20
20
  locale: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<string | undefined, string | undefined>>;
21
21
  border_radius: z.ZodMiniType<number | undefined, string | undefined, z.core.$ZodTypeInternals<number | undefined, string | undefined>>;
22
22
  }, z.core.$strip>>, config: import("./index.ts").CardConfig) => Promise<import("./api-result.ts").ApiResult>) & {
23
+ IDENTITIES: Partial<Record<"border_radius" | "card_width" | "custom_title" | "disable_animations" | "hide" | "hide_border" | "hide_description" | "hide_title" | "line_height" | "locale" | "number_format" | "org" | "show" | "show_icons" | "text_bold", import("../common/config.ts").AllowlistKind>>;
24
+ } & {
23
25
  OPTIONS: {
24
26
  show: readonly ["releases", "commits", "members", "top_language", "created"];
25
27
  hide: readonly ["repos", "stars", "forks", "watchers", "open_issues", "open_prs"];
@@ -1 +1 @@
1
- {"version":3,"file":"organization.d.ts","sourceRoot":"","sources":["../../src/api/organization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAqF9B;;;GAGG;AACH,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;CAAwE,CAAC"}
1
+ {"version":3,"file":"organization.d.ts","sourceRoot":"","sources":["../../src/api/organization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAsF9B;;;GAGG;AACH,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;CAAwE,CAAC"}
@@ -26,7 +26,7 @@ const orgQuery = z.object({
26
26
  *
27
27
  * @returns The rendered card, or a rendered error.
28
28
  */
29
- const renderOrg = cardHandler(orgQuery, async ({ org, hide, show, show_icons, hide_title, hide_border, hide_description, card_width, line_height, custom_title, disable_animations, number_format, text_bold, locale, border_radius, }, colors, config) => {
29
+ const renderOrg = cardHandler(orgQuery, { org: 'username' }, async ({ org, hide, show, show_icons, hide_title, hide_border, hide_description, card_width, line_height, custom_title, disable_animations, number_format, text_bold, locale, border_radius, }, colors, config) => {
30
30
  const organizationData = await fetchOrganization({ org }, config);
31
31
  return renderOrganizationCard(organizationData, {
32
32
  ...colors,
@@ -1,5 +1,3 @@
1
- import * as z from 'zod/mini';
2
- import type { ColorParams } from '../common/color.ts';
3
1
  /**
4
2
  * @file The api layer is the trust boundary:
5
3
  * a query string arrives as strings, and each endpoint declares what it accepts as a schema over them.
@@ -9,6 +7,8 @@ import type { ColorParams } from '../common/color.ts';
9
7
  * `zod/mini` rather than `zod`:
10
8
  * this package ships to the browser, and the functional API tree-shakes down to the checks used below.
11
9
  */
10
+ import * as z from 'zod/mini';
11
+ import type { ColorParams } from '../common/color.ts';
12
12
  /** Every param arrives as a string, or not at all. */
13
13
  declare const rawParam: z.ZodMiniOptional<z.ZodMiniString<string>>;
14
14
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../../src/api/params.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAE9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAQtD;;;;;;;;GAQG;AAEH,sDAAsD;AACtD,QAAA,MAAM,QAAQ,4CAAyB,CAAC;AA4CxC;;;GAGG;AACH,QAAA,MAAM,YAAY,wHAIjB,CAAC;AAEF,8DAA8D;AAC9D,QAAA,MAAM,SAAS,6GAA4C,CAAC;AAE5D;;;;;;GAMG;AACH,QAAA,MAAM,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CAGtE,CAAC;AAEF;;;GAGG;AACH,QAAA,MAAM,aAAa,uHAGlB,CAAC;AAsBF,2DAA2D;AAC3D,QAAA,MAAM,SAAS,qHAAqB,CAAC;AAErC,yDAAyD;AACzD,QAAA,MAAM,OAAO,qHAAmB,CAAC;AAEjC;;;GAGG;AACH,QAAA,MAAM,aAAa;WAAqB,IAAI,GAAG,SAAS;SAAO,IAAI,GAAG,SAAS;EAG9E,CAAC;AAEF;;;GAGG;AACH,QAAA,MAAM,SAAS,EAAE,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CAEpE,CAAC;AAEF;;;GAGG;AACH,QAAA,MAAM,aAAa,EAAE,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CAExE,CAAC;AAEF;;;;GAIG;AACH,QAAA,MAAM,aAAa,EAAE,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAGnE,CAAC;AAEF;;;;GAIG;AACH,QAAA,MAAM,WAAW,uHAGhB,CAAC;AAEF;;;;GAIG;AACH,QAAA,MAAM,SAAS,GAAI,KAAK,CAAC,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,UAC9C,CAAC,KACR,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CACqC,CAAC;AAmBhG;;;;GAIG;AACH,KAAK,QAAQ,CAAC,OAAO,SAAS,CAAC,CAAC,WAAW,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,WAAW,CAAC;AAcvF;;;;;;;;;GASG;AACH,QAAA,MAAM,WAAW,GAAI,OAAO,SAAS,CAAC,CAAC,WAAW,UACxC,OAAO,SACR,OAAO,KACb,CAAC,CAAC,MAAM,CAAC,OAAO,CAMlB,CAAC;AAEF;;;;;;;GAOG;AACH,QAAA,MAAM,gBAAgB,UAAW,OAAO,KAAG,WAAoD,CAAC;AAEhG,YAAY,EAAE,QAAQ,EAAE,CAAC;AAEzB,OAAO,EACL,YAAY,EACZ,SAAS,EACT,SAAS,EACT,SAAS,EACT,aAAa,EACb,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,QAAQ,EACR,aAAa,EACb,SAAS,EACT,OAAO,EACP,aAAa,GACd,CAAC"}
1
+ {"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../../src/api/params.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAE9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAQtD,sDAAsD;AACtD,QAAA,MAAM,QAAQ,4CAAyB,CAAC;AA4CxC;;;GAGG;AACH,QAAA,MAAM,YAAY,wHAIjB,CAAC;AAEF,8DAA8D;AAC9D,QAAA,MAAM,SAAS,6GAA4C,CAAC;AAE5D;;;;;;GAMG;AACH,QAAA,MAAM,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CAGtE,CAAC;AAEF;;;GAGG;AACH,QAAA,MAAM,aAAa,uHAGlB,CAAC;AAsBF,2DAA2D;AAC3D,QAAA,MAAM,SAAS,qHAAqB,CAAC;AAErC,yDAAyD;AACzD,QAAA,MAAM,OAAO,qHAAmB,CAAC;AAEjC;;;GAGG;AACH,QAAA,MAAM,aAAa;WAAqB,IAAI,GAAG,SAAS;SAAO,IAAI,GAAG,SAAS;EAG9E,CAAC;AAEF;;;GAGG;AACH,QAAA,MAAM,SAAS,EAAE,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CAEpE,CAAC;AAEF;;;GAGG;AACH,QAAA,MAAM,aAAa,EAAE,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CAExE,CAAC;AAEF;;;;GAIG;AACH,QAAA,MAAM,aAAa,EAAE,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAGnE,CAAC;AAEF;;;;GAIG;AACH,QAAA,MAAM,WAAW,uHAGhB,CAAC;AAEF;;;;GAIG;AACH,QAAA,MAAM,SAAS,GAAI,KAAK,CAAC,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,UAC9C,CAAC,KACR,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CACqC,CAAC;AAmBhG;;;;GAIG;AACH,KAAK,QAAQ,CAAC,OAAO,SAAS,CAAC,CAAC,WAAW,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,WAAW,CAAC;AAcvF;;;;;;;;;GASG;AACH,QAAA,MAAM,WAAW,GAAI,OAAO,SAAS,CAAC,CAAC,WAAW,UACxC,OAAO,SACR,OAAO,KACb,CAAC,CAAC,MAAM,CAAC,OAAO,CAMlB,CAAC;AAEF;;;;;;;GAOG;AACH,QAAA,MAAM,gBAAgB,UAAW,OAAO,KAAG,WAAoD,CAAC;AAEhG,YAAY,EAAE,QAAQ,EAAE,CAAC;AAEzB,OAAO,EACL,YAAY,EACZ,SAAS,EACT,SAAS,EACT,SAAS,EACT,aAAa,EACb,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,QAAQ,EACR,aAAa,EACb,SAAS,EACT,OAAO,EACP,aAAa,GACd,CAAC"}
@@ -1,10 +1,3 @@
1
- import * as z from 'zod/mini';
2
- import { COLOR_PARAM_KEYS, THEME_PARAM_KEYS, isValidColorInput } from '../common/color.js';
3
- import { GITHUB_USERNAME_PATTERN } from '../common/constants.js';
4
- import { getWidestRange, parseRangeDate } from '../common/date.js';
5
- import { CardError } from '../common/error.js';
6
- import { isLocaleAvailable } from '../common/localize.js';
7
- import { parseArray, parseBoolean } from '../common/ops.js';
8
1
  /**
9
2
  * @file The api layer is the trust boundary:
10
3
  * a query string arrives as strings, and each endpoint declares what it accepts as a schema over them.
@@ -14,6 +7,13 @@ import { parseArray, parseBoolean } from '../common/ops.js';
14
7
  * `zod/mini` rather than `zod`:
15
8
  * this package ships to the browser, and the functional API tree-shakes down to the checks used below.
16
9
  */
10
+ import * as z from 'zod/mini';
11
+ import { COLOR_PARAM_KEYS, THEME_PARAM_KEYS, isValidColorInput } from '../common/color.js';
12
+ import { GITHUB_USERNAME_PATTERN } from '../common/constants.js';
13
+ import { getWidestRange, parseRangeDate } from '../common/date.js';
14
+ import { CardError } from '../common/error.js';
15
+ import { isLocaleAvailable } from '../common/localize.js';
16
+ import { parseArray, parseBoolean } from '../common/ops.js';
17
17
  /** Every param arrives as a string, or not at all. */
18
18
  const rawParam = z.optional(z.string());
19
19
  /** Characters a repository, owner or gist id may contain. */
@@ -19,6 +19,8 @@ export declare const pin: ((query: import("./params.ts").ApiQuery<z.ZodMiniObjec
19
19
  border_radius: z.ZodMiniType<number | undefined, string | undefined, z.core.$ZodTypeInternals<number | undefined, string | undefined>>;
20
20
  description_lines_count: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<number | undefined, string | undefined>>;
21
21
  }, z.core.$strip>>, config: import("./index.ts").CardConfig) => Promise<import("./api-result.ts").ApiResult>) & {
22
+ IDENTITIES: Partial<Record<"border_radius" | "browser_rendering" | "card_width" | "description_lines_count" | "hide_border" | "line_height" | "locale" | "number_format" | "repo" | "show" | "show_icons" | "show_owner" | "text_bold" | "username", import("../common/config.ts").AllowlistKind>>;
23
+ } & {
22
24
  OPTIONS: {
23
25
  show: readonly ["prs_authored", "prs_commented", "prs_reviewed", "issues_authored", "issues_commented"];
24
26
  number_format: readonly ["short", "long"];
@@ -1 +1 @@
1
- {"version":3,"file":"pin.d.ts","sourceRoot":"","sources":["../../src/api/pin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AA8F9B;;;GAGG;AACH,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;CAAgE,CAAC"}
1
+ {"version":3,"file":"pin.d.ts","sourceRoot":"","sources":["../../src/api/pin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AA+F9B;;;GAGG;AACH,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;CAAgE,CAAC"}
package/build/api/pin.js CHANGED
@@ -25,7 +25,7 @@ const pinQuery = z.object({
25
25
  *
26
26
  * @returns The rendered card, or a rendered error.
27
27
  */
28
- const renderPin = cardHandler(pinQuery, async ({ username, repo, hide_border, card_width, show_owner, browser_rendering, show, show_icons, number_format, text_bold, line_height, locale, border_radius, description_lines_count, }, colors, config) => {
28
+ const renderPin = cardHandler(pinQuery, { username: 'username' }, async ({ username, repo, hide_border, card_width, show_owner, browser_rendering, show, show_icons, number_format, text_bold, line_height, locale, border_radius, description_lines_count, }, colors, config) => {
29
29
  const repoData = await fetchRepo({
30
30
  username,
31
31
  reponame: repo,
@@ -30,6 +30,8 @@ export declare const stats: ((query: import("./params.ts").ApiQuery<z.ZodMiniObj
30
30
  show: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<string[], string | undefined>>;
31
31
  contribs_include_own_repos: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<boolean | undefined, string | undefined>>;
32
32
  }, z.core.$strip>>, config: import("./index.ts").CardConfig) => Promise<import("./api-result.ts").ApiResult>) & {
33
+ IDENTITIES: Partial<Record<"border_radius" | "card_width" | "contribs_include_own_repos" | "custom_title" | "disable_animations" | "exclude_repo" | "from" | "hide" | "hide_border" | "hide_rank" | "hide_title" | "include_all_commits" | "line_height" | "locale" | "number_format" | "number_precision" | "owner" | "rank_icon" | "repo" | "role" | "show" | "show_icons" | "text_bold" | "to" | "username", import("../common/config.ts").AllowlistKind>>;
34
+ } & {
33
35
  OPTIONS: {
34
36
  rank_icon: readonly ["default", "github", "percentile"];
35
37
  show: readonly ["contributions", "prs_merged", "prs_merged_percentage", "reviews", "discussions_started", "discussions_answered", "prs_authored", "prs_commented", "prs_reviewed", "issues_authored", "issues_commented", "all_time_contribs"];
@@ -1 +1 @@
1
- {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/api/stats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAuJ9B;;;GAGG;AACH,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAEhB,CAAC"}
1
+ {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/api/stats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAwJ9B;;;GAGG;AACH,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAEhB,CAAC"}
@@ -39,7 +39,7 @@ const statsQuery = z
39
39
  *
40
40
  * @returns The rendered card, or a rendered error.
41
41
  */
42
- const renderStats = cardHandler(statsQuery, async ({ username, repo, owner, hide, hide_title, hide_border, card_width, hide_rank, show_icons, include_all_commits, from, to, line_height, text_bold, exclude_repo, custom_title, locale, disable_animations, border_radius, number_format, role, number_precision, rank_icon, show, contribs_include_own_repos, }, colors, config) => {
42
+ const renderStats = cardHandler(statsQuery, { username: 'username' }, async ({ username, repo, owner, hide, hide_title, hide_border, card_width, hide_rank, show_icons, include_all_commits, from, to, line_height, text_bold, exclude_repo, custom_title, locale, disable_animations, border_radius, number_format, role, number_precision, rank_icon, show, contribs_include_own_repos, }, colors, config) => {
43
43
  // A bare repo name is scoped to the user whose card this is.
44
44
  const repository = repo.map((name) => name.includes('/') ? name : `${username ?? ''}/${name}`);
45
45
  const stats = await fetchStats({
@@ -23,6 +23,8 @@ export declare const topLangs: ((query: import("./params.ts").ApiQuery<z.ZodMini
23
23
  hide_values: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<boolean | undefined, string | undefined>>;
24
24
  stats_format: z.ZodMiniType<"bytes" | "percentages" | undefined, string | undefined, z.core.$ZodTypeInternals<"bytes" | "percentages" | undefined, string | undefined>>;
25
25
  }, z.core.$strip>>, config: import("./index.ts").CardConfig) => Promise<import("./api-result.ts").ApiResult>) & {
26
+ IDENTITIES: Partial<Record<"border_radius" | "card_width" | "count_weight" | "custom_title" | "disable_animations" | "exclude_repo" | "hide" | "hide_border" | "hide_progress" | "hide_title" | "hide_values" | "langs_count" | "layout" | "locale" | "role" | "size_weight" | "stats_format" | "username", import("../common/config.ts").AllowlistKind>>;
27
+ } & {
26
28
  OPTIONS: {
27
29
  layout: readonly ["compact", "normal", "donut", "donut-vertical", "pie"];
28
30
  stats_format: readonly ["percentages", "bytes"];
@@ -1 +1 @@
1
- {"version":3,"file":"top-langs.d.ts","sourceRoot":"","sources":["../../src/api/top-langs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAqG9B;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;CAEnB,CAAC"}
1
+ {"version":3,"file":"top-langs.d.ts","sourceRoot":"","sources":["../../src/api/top-langs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAsG9B;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;CAEnB,CAAC"}
@@ -30,7 +30,7 @@ const topLangsQuery = z.object({
30
30
  *
31
31
  * @returns The rendered card, or a rendered error.
32
32
  */
33
- const renderTopLangs = cardHandler(topLangsQuery, async ({ username, hide, hide_title, hide_border, card_width, layout, langs_count, exclude_repo, size_weight, count_weight, custom_title, locale, border_radius, role, disable_animations, hide_progress, hide_values, stats_format, }, colors, config) => {
33
+ const renderTopLangs = cardHandler(topLangsQuery, { username: 'username' }, async ({ username, hide, hide_title, hide_border, card_width, layout, langs_count, exclude_repo, size_weight, count_weight, custom_title, locale, border_radius, role, disable_animations, hide_progress, hide_values, stats_format, }, colors, config) => {
34
34
  const topLangs = await fetchTopLanguages({
35
35
  username,
36
36
  exclude_repo,
@@ -20,6 +20,8 @@ export declare const wakatime: ((query: import("./params.ts").ApiQuery<z.ZodMini
20
20
  display_format: z.ZodMiniType<"percent" | "time" | undefined, string | undefined, z.core.$ZodTypeInternals<"percent" | "time" | undefined, string | undefined>>;
21
21
  disable_animations: z.ZodMiniPipe<z.ZodMiniOptional<z.ZodMiniString<string>>, z.ZodMiniTransform<boolean | undefined, string | undefined>>;
22
22
  }, z.core.$strip>>, config: import("./index.ts").CardConfig) => Promise<import("./api-result.ts").ApiResult>) & {
23
+ IDENTITIES: Partial<Record<"api_domain" | "border_radius" | "card_width" | "custom_title" | "disable_animations" | "display_format" | "hide" | "hide_border" | "hide_progress" | "hide_title" | "langs_count" | "layout" | "line_height" | "locale" | "username", import("../common/config.ts").AllowlistKind>>;
24
+ } & {
23
25
  OPTIONS: {
24
26
  layout: readonly ["compact", "normal"];
25
27
  display_format: readonly ["time", "percent"];
@@ -1 +1 @@
1
- {"version":3,"file":"wakatime.d.ts","sourceRoot":"","sources":["../../src/api/wakatime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAuF9B;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;CAAyE,CAAC"}
1
+ {"version":3,"file":"wakatime.d.ts","sourceRoot":"","sources":["../../src/api/wakatime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAyF9B;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;CAAyE,CAAC"}
@@ -28,7 +28,9 @@ const wakatimeQuery = z.object({
28
28
  *
29
29
  * @returns The rendered card, or a rendered error.
30
30
  */
31
- const renderWakatime = cardHandler(wakatimeQuery, async ({ username, hide_border, card_width, line_height, hide_title, hide_progress, custom_title, locale, layout, langs_count, hide, api_domain, border_radius, display_format, disable_animations, }, colors, config) => {
31
+ const renderWakatime = cardHandler(wakatimeQuery,
32
+ // no allowlist: `username` is a WakaTime profile, not a GitHub login (see `apps/server/README.md`)
33
+ {}, async ({ username, hide_border, card_width, line_height, hide_title, hide_progress, custom_title, locale, layout, langs_count, hide, api_domain, border_radius, display_format, disable_animations, }, colors, config) => {
32
34
  const stats = await fetchWakatimeStats({ username, api_domain }, config);
33
35
  return renderWakatimeCard(stats, {
34
36
  ...colors,
@@ -1,5 +1,7 @@
1
1
  import type { FetchLike } from './http.ts';
2
2
  type Env = Record<string, string | undefined>;
3
+ /** Which list guards an identity. `username` is any GitHub login, an organization's included. */
4
+ type AllowlistKind = 'username' | 'gist';
3
5
  interface PersonalAccessToken {
4
6
  /** Env variable the token came from — the retryer logs this name, never the value. */
5
7
  name: string;
@@ -34,10 +36,14 @@ export declare class CardConfig {
34
36
  constructor(init?: CardConfigInit);
35
37
  /** @returns Config for this deployment. */
36
38
  static fromEnv(env: Env): CardConfig;
37
- /** @returns Whether this deployment serves the id. */
38
- isAllowed(id: string, kind: 'username' | 'gist'): boolean;
39
+ /**
40
+ * Matched case-insensitively, as a GitHub login is; an absent list serves anyone.
41
+ *
42
+ * @returns Whether this deployment serves the id.
43
+ */
44
+ isAllowed(id: string, kind: AllowlistKind): boolean;
39
45
  /** @returns A copy with `overrides` applied — how a host swaps in a user's PAT per request. */
40
46
  with(overrides: CardConfigInit): CardConfig;
41
47
  }
42
- export type { PersonalAccessToken, CardConfigInit };
48
+ export type { AllowlistKind, PersonalAccessToken, CardConfigInit };
43
49
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/common/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE9C,UAAU,mBAAmB;IAC3B,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qFAAqF;AACrF,UAAU,cAAc;IACtB,IAAI,CAAC,EAAE,aAAa,CAAC,mBAAmB,CAAC,CAAC;IAC1C,iBAAiB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IACtD,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAClD,mBAAmB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AA2BD;;;;;GAKG;AACH,qBAAa,UAAU;IACrB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,mBAAmB,CAAC,CAAC;IAClD,oEAAoE;IACpE,QAAQ,CAAC,iBAAiB,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAC9D,mEAAmE;IACnE,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1D,QAAQ,CAAC,mBAAmB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpD,mFAAmF;IACnF,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAE1B,YAAY,IAAI,GAAE,cAAmB,EAOpC;IAED,2CAA2C;IAC3C,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,UAAU,CAQnC;IAED,sDAAsD;IACtD,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAGxD;IAED,+FAA+F;IAC/F,IAAI,CAAC,SAAS,EAAE,cAAc,GAAG,UAAU,CAU1C;CACF;AAED,YAAY,EAAE,mBAAmB,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/common/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE9C,iGAAiG;AACjG,KAAK,aAAa,GAAG,UAAU,GAAG,MAAM,CAAC;AAEzC,UAAU,mBAAmB;IAC3B,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qFAAqF;AACrF,UAAU,cAAc;IACtB,IAAI,CAAC,EAAE,aAAa,CAAC,mBAAmB,CAAC,CAAC;IAC1C,iBAAiB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IACtD,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAClD,mBAAmB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAmCD;;;;;GAKG;AACH,qBAAa,UAAU;IACrB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,mBAAmB,CAAC,CAAC;IAClD,oEAAoE;IACpE,QAAQ,CAAC,iBAAiB,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAC9D,mEAAmE;IACnE,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1D,QAAQ,CAAC,mBAAmB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpD,mFAAmF;IACnF,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAE1B,YAAY,IAAI,GAAE,cAAmB,EAOpC;IAED,2CAA2C;IAC3C,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,UAAU,CAQnC;IAED;;;;OAIG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAIlD;IAED,+FAA+F;IAC/F,IAAI,CAAC,SAAS,EAAE,cAAc,GAAG,UAAU,CAU1C;CACF;AAED,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,cAAc,EAAE,CAAC"}
@@ -1,8 +1,14 @@
1
1
  import { defaultFetch } from './http.js';
2
2
  /**
3
- * @returns Parsed string values.
3
+ * @returns The comma-separated values, trimmed, or `undefined` when the variable is unset or empty.
4
4
  */
5
- const parseCsv = (value) => value ? value.split(',') : undefined;
5
+ const parseCsv = (value) => {
6
+ const values = (value ?? '')
7
+ .split(',')
8
+ .map((entry) => entry.trim())
9
+ .filter((entry) => entry !== '');
10
+ return values.length > 0 ? values : undefined;
11
+ };
6
12
  /**
7
13
  * @returns Page limit: `"true"` means every page, a positive number caps the pages, anything else means one.
8
14
  */
@@ -14,11 +20,14 @@ const parseFetchMultiPageStars = (value) => {
14
20
  return limit > 0 ? limit : 1;
15
21
  };
16
22
  /**
23
+ * An empty variable is an unset one: `docker compose` writes `PAT_2=` for a token left blank.
24
+ *
17
25
  * @returns Personal access tokens found in the environment.
18
26
  */
19
27
  const parsePATsFromEnv = (env) => Object.keys(env)
20
28
  .filter((key) => /PAT_\d*$/.exec(key))
21
- .map((name) => ({ name, value: env[name] ?? '' }));
29
+ .map((name) => ({ name, value: env[name] ?? '' }))
30
+ .filter((pat) => pat.value !== '');
22
31
  /**
23
32
  * Deployment-wide configuration for the card renderers.
24
33
  *
@@ -48,16 +57,21 @@ export class CardConfig {
48
57
  static fromEnv(env) {
49
58
  return new CardConfig({
50
59
  pats: parsePATsFromEnv(env),
51
- usernameAllowlist: parseCsv(env['WHITELIST']),
52
- gistAllowlist: parseCsv(env['GIST_WHITELIST']),
60
+ usernameAllowlist: parseCsv(env['ALLOWLIST']),
61
+ gistAllowlist: parseCsv(env['GIST_ALLOWLIST']),
53
62
  excludeRepositories: parseCsv(env['EXCLUDE_REPO']) ?? [],
54
63
  fetchMultiPageStars: parseFetchMultiPageStars(env['FETCH_MULTI_PAGE_STARS']),
55
64
  });
56
65
  }
57
- /** @returns Whether this deployment serves the id. */
66
+ /**
67
+ * Matched case-insensitively, as a GitHub login is; an absent list serves anyone.
68
+ *
69
+ * @returns Whether this deployment serves the id.
70
+ */
58
71
  isAllowed(id, kind) {
59
72
  const list = kind === 'gist' ? this.gistAllowlist : this.usernameAllowlist;
60
- return list === undefined || list.includes(id);
73
+ const wanted = id.toLowerCase();
74
+ return list === undefined || list.some((allowed) => allowed.toLowerCase() === wanted);
61
75
  }
62
76
  /** @returns A copy with `overrides` applied — how a host swaps in a user's PAT per request. */
63
77
  with(overrides) {
@@ -15,9 +15,11 @@ type ErrorCode =
15
15
  'invalid_param'
16
16
  /** The query is missing a param the endpoint cannot render without. */
17
17
  | 'missing_param'
18
+ /** The query is well-formed, but this deployment is pinned to identities it does not name. */
19
+ | 'not_allowed'
18
20
  /** The user, repository or gist does not exist. */
19
21
  | 'not_found'
20
- /** The deployment has no usable GitHub token. */
22
+ /** The deployment has no usable GitHub token — retryable, the fix being a token on the next start. */
21
23
  | 'no_tokens'
22
24
  /** Every token is rate limited. */
23
25
  | 'rate_limited'
@@ -55,6 +57,13 @@ declare class CardError extends Error {
55
57
  * @returns The error.
56
58
  */
57
59
  static missingParam(params: Array<string>, secondaryMessage?: string): CardError;
60
+ /**
61
+ * An identity this deployment is not configured to draw.
62
+ * Names neither the value nor the list: one is the caller's, the other the operator's.
63
+ *
64
+ * @returns The error.
65
+ */
66
+ static notAllowed(param: string): CardError;
58
67
  /**
59
68
  * Anything thrown that is not already a `CardError`:
60
69
  * an upstream failure, since the query itself got this far.
@@ -1 +1 @@
1
- {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/common/error.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,wDAAwD;AACxD,QAAA,MAAM,eAAe,2BAA2B,CAAC;AAEjD,4BAA4B;AAC5B,KAAK,SAAS;AACZ,2EAA2E;AACzE,eAAe;AACjB,uEAAuE;GACrE,eAAe;AACjB,mDAAmD;GACjD,WAAW;AACb,iDAAiD;GAC/C,WAAW;AACb,mCAAmC;GACjC,cAAc;AAChB,2DAA2D;GACzD,UAAU,CAAC;AAef,sDAAsD;AACtD,QAAA,MAAM,wBAAwB,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAKhE,CAAC;AAEF,wDAAwD;AACxD,UAAU,aAAa;IACrB,qBAAqB;IACrB,IAAI,EAAE,SAAS,CAAC;IAChB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,sCAAsC;AACtC,cAAM,SAAU,SAAQ,KAAK;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAEnC,YAAY,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAM/C;IAED,sEAAsE;IACtE,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;OAIG;IACH,MAAM,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAMtE;IAED;;;;;OAKG;IACH,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAO/E;IAED;;;;;OAKG;IACH,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAQnC;CACF;AAED,qEAAqE;AACrE,QAAA,MAAM,mBAAmB,QAEtB,CAAC;AAEJ,0DAA0D;AAC1D,QAAA,MAAM,uBAAuB,iDAAiD,CAAC;AAE/E,yEAAyE;AACzE,QAAA,MAAM,cAAc,2DAA2D,CAAC;AAEhF,kFAAkF;AAClF,QAAA,MAAM,sBAAsB,iEAAiE,CAAC;AAE9F,2EAA2E;AAC3E,QAAA,MAAM,cAAc,+DAA+D,CAAC;AAEpF,0CAA0C;AAC1C,QAAA,MAAM,cAAc,8CAA8C,CAAC;AAEnE,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B,OAAO,EACL,SAAS,EACT,cAAc,EACd,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,uBAAuB,GACxB,CAAC"}
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/common/error.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,wDAAwD;AACxD,QAAA,MAAM,eAAe,2BAA2B,CAAC;AAKjD,4BAA4B;AAC5B,KAAK,SAAS;AACZ,2EAA2E;AACzE,eAAe;AACjB,uEAAuE;GACrE,eAAe;AACjB,8FAA8F;GAC5F,aAAa;AACf,mDAAmD;GACjD,WAAW;AACb,sGAAsG;GACpG,WAAW;AACb,mCAAmC;GACjC,cAAc;AAChB,2DAA2D;GACzD,UAAU,CAAC;AAgBf,sDAAsD;AACtD,QAAA,MAAM,wBAAwB,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAKhE,CAAC;AAEF,wDAAwD;AACxD,UAAU,aAAa;IACrB,qBAAqB;IACrB,IAAI,EAAE,SAAS,CAAC;IAChB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,sCAAsC;AACtC,cAAM,SAAU,SAAQ,KAAK;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAEnC,YAAY,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAM/C;IAED,sEAAsE;IACtE,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;OAIG;IACH,MAAM,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAMtE;IAED;;;;;OAKG;IACH,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAO/E;IAED;;;;;OAKG;IACH,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAM1C;IAED;;;;;OAKG;IACH,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAQnC;CACF;AAED,qEAAqE;AACrE,QAAA,MAAM,mBAAmB,QAEtB,CAAC;AAEJ,0DAA0D;AAC1D,QAAA,MAAM,uBAAuB,iDAAiD,CAAC;AAE/E,yEAAyE;AACzE,QAAA,MAAM,cAAc,2DAA2D,CAAC;AAEhF,kFAAkF;AAClF,QAAA,MAAM,sBAAsB,iEAAiE,CAAC;AAE9F,2EAA2E;AAC3E,QAAA,MAAM,cAAc,+DAA+D,CAAC;AAEpF,0CAA0C;AAC1C,QAAA,MAAM,cAAc,8CAA8C,CAAC;AAEnE,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B,OAAO,EACL,SAAS,EACT,cAAc,EACd,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,uBAAuB,GACxB,CAAC"}
@@ -1,4 +1,3 @@
1
- import { OWNER_AFFILIATIONS } from './constants.js';
2
1
  /**
3
2
  * @file One error type for everything a card render can fail on.
4
3
  *
@@ -8,8 +7,11 @@ import { OWNER_AFFILIATIONS } from './constants.js';
8
7
  * Anything else that escapes is wrapped as `upstream` by `from`, which is retryable —
9
8
  * so a permanent failure has to throw a `CardError` to be reported as one.
10
9
  */
10
+ import { OWNER_AFFILIATIONS } from './constants.js';
11
11
  /** A general message to ask user to try again later. */
12
12
  const TRY_AGAIN_LATER = 'Please try again later';
13
+ /** The second line for an identity a pinned deployment does not serve. */
14
+ const NOT_ALLOWED = 'This deployment does not serve that account';
13
15
  /**
14
16
  * Whether repeating the request could produce a different answer.
15
17
  * A host reads this to decide between caching the failure and retrying it.
@@ -17,8 +19,9 @@ const TRY_AGAIN_LATER = 'Please try again later';
17
19
  const RETRYABLE = {
18
20
  invalid_param: false,
19
21
  missing_param: false,
22
+ not_allowed: false,
20
23
  not_found: false,
21
- no_tokens: false,
24
+ no_tokens: true,
22
25
  rate_limited: true,
23
26
  upstream: true,
24
27
  };
@@ -70,6 +73,19 @@ class CardError extends Error {
70
73
  param: params[0],
71
74
  });
72
75
  }
76
+ /**
77
+ * An identity this deployment is not configured to draw.
78
+ * Names neither the value nor the list: one is the caller's, the other the operator's.
79
+ *
80
+ * @returns The error.
81
+ */
82
+ static notAllowed(param) {
83
+ return new CardError('Not allowed', {
84
+ code: 'not_allowed',
85
+ secondaryMessage: NOT_ALLOWED,
86
+ param,
87
+ });
88
+ }
73
89
  /**
74
90
  * Anything thrown that is not already a `CardError`:
75
91
  * an upstream failure, since the query itself got this far.
@@ -1,5 +1,8 @@
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
  declare const logger: {
5
8
  log: (...args: Array<unknown>) => void;
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stats-forge/github-stats-forge-core",
3
- "version": "0.5.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,
@@ -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.
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 };