jg-github 0.0.0-stage → 0.1.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 (84) hide show
  1. package/API.md +648 -0
  2. package/CHANGELOG.md +13 -0
  3. package/LICENSE +21 -0
  4. package/README.md +100 -2
  5. package/RECIPES.md +101 -0
  6. package/dist/app-auth/app-auth.d.ts +33 -0
  7. package/dist/app-auth/app-auth.js +95 -0
  8. package/dist/app-auth/app-auth.js.map +1 -0
  9. package/dist/app-auth/constants.d.ts +4 -0
  10. package/dist/app-auth/constants.js +10 -0
  11. package/dist/app-auth/constants.js.map +1 -0
  12. package/dist/app-auth/index.d.ts +2 -0
  13. package/dist/app-auth/index.js +3 -0
  14. package/dist/app-auth/index.js.map +1 -0
  15. package/dist/app-auth/types.d.ts +5 -0
  16. package/dist/app-auth/types.js +2 -0
  17. package/dist/app-auth/types.js.map +1 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.js +13 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/installation-token/constants.d.ts +12 -0
  22. package/dist/installation-token/constants.js +30 -0
  23. package/dist/installation-token/constants.js.map +1 -0
  24. package/dist/installation-token/errors.d.ts +41 -0
  25. package/dist/installation-token/errors.js +94 -0
  26. package/dist/installation-token/errors.js.map +1 -0
  27. package/dist/installation-token/index.d.ts +2 -0
  28. package/dist/installation-token/index.js +3 -0
  29. package/dist/installation-token/index.js.map +1 -0
  30. package/dist/installation-token/installation-token.d.ts +57 -0
  31. package/dist/installation-token/installation-token.js +152 -0
  32. package/dist/installation-token/installation-token.js.map +1 -0
  33. package/dist/installation-token/types.d.ts +33 -0
  34. package/dist/installation-token/types.js +2 -0
  35. package/dist/installation-token/types.js.map +1 -0
  36. package/dist/installation-token/utilities.d.ts +36 -0
  37. package/dist/installation-token/utilities.js +93 -0
  38. package/dist/installation-token/utilities.js.map +1 -0
  39. package/dist/installation-token/validations.d.ts +35 -0
  40. package/dist/installation-token/validations.js +74 -0
  41. package/dist/installation-token/validations.js.map +1 -0
  42. package/dist/rate-limit/constants.d.ts +2 -0
  43. package/dist/rate-limit/constants.js +5 -0
  44. package/dist/rate-limit/constants.js.map +1 -0
  45. package/dist/rate-limit/index.d.ts +2 -0
  46. package/dist/rate-limit/index.js +3 -0
  47. package/dist/rate-limit/index.js.map +1 -0
  48. package/dist/rate-limit/rate-limit.d.ts +24 -0
  49. package/dist/rate-limit/rate-limit.js +88 -0
  50. package/dist/rate-limit/rate-limit.js.map +1 -0
  51. package/dist/rate-limit/types.d.ts +8 -0
  52. package/dist/rate-limit/types.js +2 -0
  53. package/dist/rate-limit/types.js.map +1 -0
  54. package/dist/shared/constants.d.ts +7 -0
  55. package/dist/shared/constants.js +15 -0
  56. package/dist/shared/constants.js.map +1 -0
  57. package/dist/shared/errors.d.ts +9 -0
  58. package/dist/shared/errors.js +11 -0
  59. package/dist/shared/errors.js.map +1 -0
  60. package/dist/shared/headers.d.ts +19 -0
  61. package/dist/shared/headers.js +37 -0
  62. package/dist/shared/headers.js.map +1 -0
  63. package/dist/shared/index.d.ts +6 -0
  64. package/dist/shared/index.js +11 -0
  65. package/dist/shared/index.js.map +1 -0
  66. package/dist/shared/types.d.ts +17 -0
  67. package/dist/shared/types.js +2 -0
  68. package/dist/shared/types.js.map +1 -0
  69. package/dist/shared/urls.d.ts +32 -0
  70. package/dist/shared/urls.js +110 -0
  71. package/dist/shared/urls.js.map +1 -0
  72. package/dist/shared/utilities.d.ts +20 -0
  73. package/dist/shared/utilities.js +25 -0
  74. package/dist/shared/utilities.js.map +1 -0
  75. package/dist/webhook/constants.d.ts +2 -0
  76. package/dist/webhook/constants.js +5 -0
  77. package/dist/webhook/constants.js.map +1 -0
  78. package/dist/webhook/index.d.ts +1 -0
  79. package/dist/webhook/index.js +3 -0
  80. package/dist/webhook/index.js.map +1 -0
  81. package/dist/webhook/webhook.d.ts +23 -0
  82. package/dist/webhook/webhook.js +35 -0
  83. package/dist/webhook/webhook.js.map +1 -0
  84. package/package.json +68 -3
@@ -0,0 +1,88 @@
1
+ import { invalidArgument } from '../shared/index.js';
2
+ import { FALLBACK_RETRY_MS, IMF_FIXDATE_PATTERN } from './constants.js';
3
+ /**
4
+ * Reads one header, by its lowercase name, as a single string.
5
+ * @param headers The headers.
6
+ * @param name The header's lowercase name.
7
+ * @returns Its value, or null when it's missing or repeated.
8
+ */
9
+ const readHeader = (headers, name) => {
10
+ const value = headers instanceof Headers ? headers.get(name) : headers[name];
11
+ return typeof value === 'string' ? value : null;
12
+ };
13
+ /**
14
+ * Reads Retry-After as a deadline: whole seconds from now, or a strict IMF-fixdate.
15
+ * @param retryAfter The header's value.
16
+ * @param now The current time, in milliseconds.
17
+ * @returns The deadline in milliseconds, or NaN when the value isn't one.
18
+ */
19
+ const readRetryAfter = (retryAfter, now) => {
20
+ const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : NaN;
21
+ if (Number.isSafeInteger(seconds)) {
22
+ return now + seconds * 1000;
23
+ }
24
+ return IMF_FIXDATE_PATTERN.test(retryAfter) ? Date.parse(retryAfter) : NaN;
25
+ };
26
+ /**
27
+ * Gives the time to retry a GitHub request at after a rate limit, or null when the response isn't
28
+ * rate limited. A 429 is, and so is a 403 with Retry-After, with no requests remaining, or with a
29
+ * rate-limit message, and a success that used the last remaining request. The deadline is the
30
+ * latest of Retry-After, as whole seconds or a strict IMF-fixdate, and `x-ratelimit-reset` when no
31
+ * requests remain, counting only future times; without one, it's a minute from now, doubled for
32
+ * each retry already made.
33
+ * @param options The response's status and headers, the retries made, and the time.
34
+ * @returns The time to retry at, or null.
35
+ * @example
36
+ * import { getGithubRateLimitDeadline } from 'jg-github';
37
+ *
38
+ * declare const response: Response;
39
+ *
40
+ * const deadline = getGithubRateLimitDeadline({
41
+ * status: response.status,
42
+ * headers: response.headers,
43
+ * retryCount: 0,
44
+ * });
45
+ * @throws
46
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
47
+ */
48
+ export const getGithubRateLimitDeadline = ({ status, headers, retryCount, hasRateLimitMessage = false, now = new Date(), }) => {
49
+ if (!Number.isSafeInteger(status) || status < 100 || status > 599) {
50
+ throw invalidArgument('status');
51
+ }
52
+ if (!(headers instanceof Headers) && (typeof headers !== 'object' || headers === null)) {
53
+ throw invalidArgument('headers');
54
+ }
55
+ if (!Number.isSafeInteger(retryCount) || retryCount < 0) {
56
+ throw invalidArgument('retryCount');
57
+ }
58
+ if (!(now instanceof Date) || Number.isNaN(now.getTime())) {
59
+ throw invalidArgument('now');
60
+ }
61
+ const nowMs = now.getTime();
62
+ const retryAfter = readHeader(headers, 'retry-after');
63
+ const remaining = readHeader(headers, 'x-ratelimit-remaining');
64
+ const reset = readHeader(headers, 'x-ratelimit-reset');
65
+ // the last successful request can use up the primary limit before GitHub answers with an error
66
+ const hasExhaustedSuccess = status >= 200 && status < 300 && remaining === '0';
67
+ if (status !== 403 && status !== 429 && !hasExhaustedSuccess) {
68
+ return null;
69
+ }
70
+ if (status === 403 && retryAfter === null && remaining !== '0' && !hasRateLimitMessage) {
71
+ return null;
72
+ }
73
+ const deadlines = [];
74
+ if (retryAfter !== null) {
75
+ const deadline = readRetryAfter(retryAfter, nowMs);
76
+ if (Number.isSafeInteger(deadline) && deadline > nowMs) {
77
+ deadlines.push(deadline);
78
+ }
79
+ }
80
+ if (remaining === '0' && reset !== null && /^\d+$/.test(reset)) {
81
+ const deadline = Number(reset) * 1000;
82
+ if (Number.isSafeInteger(deadline) && deadline > nowMs) {
83
+ deadlines.push(deadline);
84
+ }
85
+ }
86
+ return new Date(deadlines.length === 0 ? nowMs + FALLBACK_RETRY_MS * 2 ** retryCount : Math.max(...deadlines));
87
+ };
88
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/rate-limit/rate-limit.ts"],"names":[],"mappings":"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","sourcesContent":["import { invalidArgument } from '../shared/index.js';\nimport { FALLBACK_RETRY_MS, IMF_FIXDATE_PATTERN } from './constants.js';\nimport type { IGithubRateLimitDeadlineOptions, IGithubResponseHeaders } from './types.js';\n\n/**\n * Reads one header, by its lowercase name, as a single string.\n * @param headers The headers.\n * @param name The header's lowercase name.\n * @returns Its value, or null when it's missing or repeated.\n */\nconst readHeader = (headers: IGithubResponseHeaders, name: string): string | null => {\n const value = headers instanceof Headers ? headers.get(name) : headers[name];\n return typeof value === 'string' ? value : null;\n};\n\n/**\n * Reads Retry-After as a deadline: whole seconds from now, or a strict IMF-fixdate.\n * @param retryAfter The header's value.\n * @param now The current time, in milliseconds.\n * @returns The deadline in milliseconds, or NaN when the value isn't one.\n */\nconst readRetryAfter = (retryAfter: string, now: number): number => {\n const seconds = /^\\d+$/.test(retryAfter) ? Number(retryAfter) : NaN;\n if (Number.isSafeInteger(seconds)) {\n return now + seconds * 1000;\n }\n return IMF_FIXDATE_PATTERN.test(retryAfter) ? Date.parse(retryAfter) : NaN;\n};\n\n/**\n * Gives the time to retry a GitHub request at after a rate limit, or null when the response isn't\n * rate limited. A 429 is, and so is a 403 with Retry-After, with no requests remaining, or with a\n * rate-limit message, and a success that used the last remaining request. The deadline is the\n * latest of Retry-After, as whole seconds or a strict IMF-fixdate, and `x-ratelimit-reset` when no\n * requests remain, counting only future times; without one, it's a minute from now, doubled for\n * each retry already made.\n * @param options The response's status and headers, the retries made, and the time.\n * @returns The time to retry at, or null.\n * @example\n * import { getGithubRateLimitDeadline } from 'jg-github';\n *\n * declare const response: Response;\n *\n * const deadline = getGithubRateLimitDeadline({\n * status: response.status,\n * headers: response.headers,\n * retryCount: 0,\n * });\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n */\nexport const getGithubRateLimitDeadline = ({\n status,\n headers,\n retryCount,\n hasRateLimitMessage = false,\n now = new Date(),\n}: IGithubRateLimitDeadlineOptions): Date | null => {\n if (!Number.isSafeInteger(status) || status < 100 || status > 599) {\n throw invalidArgument('status');\n }\n if (!(headers instanceof Headers) && (typeof headers !== 'object' || headers === null)) {\n throw invalidArgument('headers');\n }\n if (!Number.isSafeInteger(retryCount) || retryCount < 0) {\n throw invalidArgument('retryCount');\n }\n if (!(now instanceof Date) || Number.isNaN(now.getTime())) {\n throw invalidArgument('now');\n }\n const nowMs = now.getTime();\n const retryAfter = readHeader(headers, 'retry-after');\n const remaining = readHeader(headers, 'x-ratelimit-remaining');\n const reset = readHeader(headers, 'x-ratelimit-reset');\n\n // the last successful request can use up the primary limit before GitHub answers with an error\n const hasExhaustedSuccess = status >= 200 && status < 300 && remaining === '0';\n if (status !== 403 && status !== 429 && !hasExhaustedSuccess) {\n return null;\n }\n if (status === 403 && retryAfter === null && remaining !== '0' && !hasRateLimitMessage) {\n return null;\n }\n\n const deadlines: number[] = [];\n if (retryAfter !== null) {\n const deadline = readRetryAfter(retryAfter, nowMs);\n if (Number.isSafeInteger(deadline) && deadline > nowMs) {\n deadlines.push(deadline);\n }\n }\n if (remaining === '0' && reset !== null && /^\\d+$/.test(reset)) {\n const deadline = Number(reset) * 1000;\n if (Number.isSafeInteger(deadline) && deadline > nowMs) {\n deadlines.push(deadline);\n }\n }\n return new Date(\n deadlines.length === 0 ? nowMs + FALLBACK_RETRY_MS * 2 ** retryCount : Math.max(...deadlines),\n );\n};\n"]}
@@ -0,0 +1,8 @@
1
+ export type IGithubResponseHeaders = Headers | Readonly<Record<string, string | readonly string[] | undefined>>;
2
+ export type IGithubRateLimitDeadlineOptions = {
3
+ status: number;
4
+ headers: IGithubResponseHeaders;
5
+ retryCount: number;
6
+ hasRateLimitMessage?: boolean | undefined;
7
+ now?: Date | undefined;
8
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/rate-limit/types.ts"],"names":[],"mappings":"","sourcesContent":["// a response's headers: a Headers object, or a record with lowercase names, as Node.js's\n// IncomingHttpHeaders has\nexport type IGithubResponseHeaders =\n Headers | Readonly<Record<string, string | readonly string[] | undefined>>;\n\n// what getGithubRateLimitDeadline takes\nexport type IGithubRateLimitDeadlineOptions = {\n // the response's HTTP status\n status: number;\n\n // the response's headers\n headers: IGithubResponseHeaders;\n\n // the retries already made for this request, from 0, which doubles the fallback wait\n retryCount: number;\n\n // whether the response's message says a rate limit was hit, which makes a bare 403 count.\n // Defaults to false\n hasRateLimitMessage?: boolean | undefined;\n\n // the current time. Defaults to now\n now?: Date | undefined;\n};\n"]}
@@ -0,0 +1,7 @@
1
+ export declare const GITHUB_API_ORIGIN = "https://api.github.com";
2
+ export declare const GITHUB_RAW_CONTENT_ORIGIN = "https://raw.githubusercontent.com";
3
+ export declare const GITHUB_API_VERSION = "2026-03-10";
4
+ export declare const GITHUB_MEDIA_TYPE = "application/vnd.github+json";
5
+ export declare const DEFAULT_GITHUB_TIMEOUT_SECONDS = 10;
6
+ export declare const DEFAULT_GITHUB_MAX_RESPONSE_BYTES = 131072;
7
+ export declare const LOOPBACK_HOSTNAMES: ReadonlySet<string>;
@@ -0,0 +1,15 @@
1
+ // the origin of GitHub's REST API on github.com
2
+ export const GITHUB_API_ORIGIN = 'https://api.github.com';
3
+ // the origin GitHub serves a repository's raw files from
4
+ export const GITHUB_RAW_CONTENT_ORIGIN = 'https://raw.githubusercontent.com';
5
+ // the REST API version every request asks for, in the X-GitHub-Api-Version header
6
+ export const GITHUB_API_VERSION = '2026-03-10';
7
+ // the media type GitHub's REST API answers with
8
+ export const GITHUB_MEDIA_TYPE = 'application/vnd.github+json';
9
+ // the seconds a request to GitHub may take when the options don't say
10
+ export const DEFAULT_GITHUB_TIMEOUT_SECONDS = 10;
11
+ // the bytes a response from GitHub may take when the options don't say (128 KiB)
12
+ export const DEFAULT_GITHUB_MAX_RESPONSE_BYTES = 131_072;
13
+ // the host names that only this machine can reach, where plain http is allowed for test servers
14
+ export const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']);
15
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/shared/constants.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,MAAM,CAAC,MAAM,iBAAiB,GAAG,wBAAwB,CAAC;AAE1D,yDAAyD;AACzD,MAAM,CAAC,MAAM,yBAAyB,GAAG,mCAAmC,CAAC;AAE7E,kFAAkF;AAClF,MAAM,CAAC,MAAM,kBAAkB,GAAG,YAAY,CAAC;AAE/C,gDAAgD;AAChD,MAAM,CAAC,MAAM,iBAAiB,GAAG,6BAA6B,CAAC;AAE/D,sEAAsE;AACtE,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,CAAC;AAEjD,iFAAiF;AACjF,MAAM,CAAC,MAAM,iCAAiC,GAAG,OAAO,CAAC;AAEzD,gGAAgG;AAChG,MAAM,CAAC,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC","sourcesContent":["// the origin of GitHub's REST API on github.com\nexport const GITHUB_API_ORIGIN = 'https://api.github.com';\n\n// the origin GitHub serves a repository's raw files from\nexport const GITHUB_RAW_CONTENT_ORIGIN = 'https://raw.githubusercontent.com';\n\n// the REST API version every request asks for, in the X-GitHub-Api-Version header\nexport const GITHUB_API_VERSION = '2026-03-10';\n\n// the media type GitHub's REST API answers with\nexport const GITHUB_MEDIA_TYPE = 'application/vnd.github+json';\n\n// the seconds a request to GitHub may take when the options don't say\nexport const DEFAULT_GITHUB_TIMEOUT_SECONDS = 10;\n\n// the bytes a response from GitHub may take when the options don't say (128 KiB)\nexport const DEFAULT_GITHUB_MAX_RESPONSE_BYTES = 131_072;\n\n// the host names that only this machine can reach, where plain http is allowed for test servers\nexport const LOOPBACK_HOSTNAMES: ReadonlySet<string> = new Set(['localhost', '127.0.0.1', '[::1]']);\n"]}
@@ -0,0 +1,9 @@
1
+ export declare const ERRORS: {
2
+ readonly GITHUB_PRIVATE_KEY_INVALID: "GITHUB_PRIVATE_KEY_INVALID";
3
+ readonly GITHUB_INVALID_ARGUMENT: "GITHUB_INVALID_ARGUMENT";
4
+ readonly GITHUB_REQUEST_FAILED: "GITHUB_REQUEST_FAILED";
5
+ readonly GITHUB_RESPONSE_INVALID: "GITHUB_RESPONSE_INVALID";
6
+ readonly GITHUB_TOKEN_SCOPE_MISMATCH: "GITHUB_TOKEN_SCOPE_MISMATCH";
7
+ readonly GITHUB_TOKEN_EXPIRED: "GITHUB_TOKEN_EXPIRED";
8
+ readonly GITHUB_TOKEN_REVOKE_FAILED: "GITHUB_TOKEN_REVOKE_FAILED";
9
+ };
@@ -0,0 +1,11 @@
1
+ // the codes for every error thrown by the package
2
+ export const ERRORS = {
3
+ GITHUB_PRIVATE_KEY_INVALID: 'GITHUB_PRIVATE_KEY_INVALID',
4
+ GITHUB_INVALID_ARGUMENT: 'GITHUB_INVALID_ARGUMENT',
5
+ GITHUB_REQUEST_FAILED: 'GITHUB_REQUEST_FAILED',
6
+ GITHUB_RESPONSE_INVALID: 'GITHUB_RESPONSE_INVALID',
7
+ GITHUB_TOKEN_SCOPE_MISMATCH: 'GITHUB_TOKEN_SCOPE_MISMATCH',
8
+ GITHUB_TOKEN_EXPIRED: 'GITHUB_TOKEN_EXPIRED',
9
+ GITHUB_TOKEN_REVOKE_FAILED: 'GITHUB_TOKEN_REVOKE_FAILED',
10
+ };
11
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/shared/errors.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,0BAA0B,EAAE,4BAA4B;IACxD,uBAAuB,EAAE,yBAAyB;IAClD,qBAAqB,EAAE,uBAAuB;IAC9C,uBAAuB,EAAE,yBAAyB;IAClD,2BAA2B,EAAE,6BAA6B;IAC1D,oBAAoB,EAAE,sBAAsB;IAC5C,0BAA0B,EAAE,4BAA4B;CAChD,CAAC","sourcesContent":["// the codes for every error thrown by the package\nexport const ERRORS = {\n GITHUB_PRIVATE_KEY_INVALID: 'GITHUB_PRIVATE_KEY_INVALID',\n GITHUB_INVALID_ARGUMENT: 'GITHUB_INVALID_ARGUMENT',\n GITHUB_REQUEST_FAILED: 'GITHUB_REQUEST_FAILED',\n GITHUB_RESPONSE_INVALID: 'GITHUB_RESPONSE_INVALID',\n GITHUB_TOKEN_SCOPE_MISMATCH: 'GITHUB_TOKEN_SCOPE_MISMATCH',\n GITHUB_TOKEN_EXPIRED: 'GITHUB_TOKEN_EXPIRED',\n GITHUB_TOKEN_REVOKE_FAILED: 'GITHUB_TOKEN_REVOKE_FAILED',\n} as const;\n"]}
@@ -0,0 +1,19 @@
1
+ import type { IGithubHeadersOptions } from './types.js';
2
+ /**
3
+ * Builds the headers a request to GitHub's REST API sends: the media type, the token as a bearer
4
+ * credential, the User-Agent GitHub requires, and the API version.
5
+ * @param token The token, such as an installation token or an App JWT.
6
+ * @param options The User-Agent and the API version.
7
+ * @returns The headers, with lowercase names.
8
+ * @example
9
+ * import { createGithubHeaders } from 'jg-github';
10
+ *
11
+ * declare const token: string;
12
+ *
13
+ * const response = await fetch('https://api.github.com/installation/repositories', {
14
+ * headers: createGithubHeaders(token, { userAgent: 'acme-deploy-bot' }),
15
+ * });
16
+ * @throws
17
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
18
+ */
19
+ export declare const createGithubHeaders: (token: string, { userAgent, apiVersion }: IGithubHeadersOptions) => Record<string, string>;
@@ -0,0 +1,37 @@
1
+ import { GITHUB_API_VERSION, GITHUB_MEDIA_TYPE } from './constants.js';
2
+ import { invalidArgument, isHeaderValue } from './utilities.js';
3
+ /**
4
+ * Builds the headers a request to GitHub's REST API sends: the media type, the token as a bearer
5
+ * credential, the User-Agent GitHub requires, and the API version.
6
+ * @param token The token, such as an installation token or an App JWT.
7
+ * @param options The User-Agent and the API version.
8
+ * @returns The headers, with lowercase names.
9
+ * @example
10
+ * import { createGithubHeaders } from 'jg-github';
11
+ *
12
+ * declare const token: string;
13
+ *
14
+ * const response = await fetch('https://api.github.com/installation/repositories', {
15
+ * headers: createGithubHeaders(token, { userAgent: 'acme-deploy-bot' }),
16
+ * });
17
+ * @throws
18
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
19
+ */
20
+ export const createGithubHeaders = (token, { userAgent, apiVersion = GITHUB_API_VERSION }) => {
21
+ if (!isHeaderValue(token) || token.includes(' ')) {
22
+ throw invalidArgument('token');
23
+ }
24
+ if (!isHeaderValue(userAgent)) {
25
+ throw invalidArgument('userAgent');
26
+ }
27
+ if (!isHeaderValue(apiVersion)) {
28
+ throw invalidArgument('apiVersion');
29
+ }
30
+ return {
31
+ accept: GITHUB_MEDIA_TYPE,
32
+ authorization: `Bearer ${token}`,
33
+ 'user-agent': userAgent,
34
+ 'x-github-api-version': apiVersion,
35
+ };
36
+ };
37
+ //# sourceMappingURL=headers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/shared/headers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEvE,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEhE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,KAAa,EACb,EAAE,SAAS,EAAE,UAAU,GAAG,kBAAkB,EAAyB,EAC7C,EAAE;IAC1B,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IACjC,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,MAAM,eAAe,CAAC,WAAW,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,MAAM,eAAe,CAAC,YAAY,CAAC,CAAC;IACtC,CAAC;IACD,OAAO;QACL,MAAM,EAAE,iBAAiB;QACzB,aAAa,EAAE,UAAU,KAAK,EAAE;QAChC,YAAY,EAAE,SAAS;QACvB,sBAAsB,EAAE,UAAU;KACnC,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { GITHUB_API_VERSION, GITHUB_MEDIA_TYPE } from './constants.js';\nimport type { IGithubHeadersOptions } from './types.js';\nimport { invalidArgument, isHeaderValue } from './utilities.js';\n\n/**\n * Builds the headers a request to GitHub's REST API sends: the media type, the token as a bearer\n * credential, the User-Agent GitHub requires, and the API version.\n * @param token The token, such as an installation token or an App JWT.\n * @param options The User-Agent and the API version.\n * @returns The headers, with lowercase names.\n * @example\n * import { createGithubHeaders } from 'jg-github';\n *\n * declare const token: string;\n *\n * const response = await fetch('https://api.github.com/installation/repositories', {\n * headers: createGithubHeaders(token, { userAgent: 'acme-deploy-bot' }),\n * });\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n */\nexport const createGithubHeaders = (\n token: string,\n { userAgent, apiVersion = GITHUB_API_VERSION }: IGithubHeadersOptions,\n): Record<string, string> => {\n if (!isHeaderValue(token) || token.includes(' ')) {\n throw invalidArgument('token');\n }\n if (!isHeaderValue(userAgent)) {\n throw invalidArgument('userAgent');\n }\n if (!isHeaderValue(apiVersion)) {\n throw invalidArgument('apiVersion');\n }\n return {\n accept: GITHUB_MEDIA_TYPE,\n authorization: `Bearer ${token}`,\n 'user-agent': userAgent,\n 'x-github-api-version': apiVersion,\n };\n};\n"]}
@@ -0,0 +1,6 @@
1
+ export type { IGithubApiUrlOptions, IGithubHeadersOptions, IGithubQuery, IGithubRawContentLocation, IGithubRawContentUrlOptions, } from './types.js';
2
+ export { DEFAULT_GITHUB_MAX_RESPONSE_BYTES, DEFAULT_GITHUB_TIMEOUT_SECONDS, GITHUB_API_ORIGIN, GITHUB_API_VERSION, GITHUB_RAW_CONTENT_ORIGIN, } from './constants.js';
3
+ export { ERRORS } from './errors.js';
4
+ export { invalidArgument, isHeaderValue, isRecord } from './utilities.js';
5
+ export { buildGithubApiUrl, buildGithubRawContentUrl } from './urls.js';
6
+ export { createGithubHeaders } from './headers.js';
@@ -0,0 +1,11 @@
1
+ // constants
2
+ export { DEFAULT_GITHUB_MAX_RESPONSE_BYTES, DEFAULT_GITHUB_TIMEOUT_SECONDS, GITHUB_API_ORIGIN, GITHUB_API_VERSION, GITHUB_RAW_CONTENT_ORIGIN, } from './constants.js';
3
+ // error codes
4
+ export { ERRORS } from './errors.js';
5
+ // errors and checks
6
+ export { invalidArgument, isHeaderValue, isRecord } from './utilities.js';
7
+ // URLs
8
+ export { buildGithubApiUrl, buildGithubRawContentUrl } from './urls.js';
9
+ // headers
10
+ export { createGithubHeaders } from './headers.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/shared/index.ts"],"names":[],"mappings":"AASA,YAAY;AACZ,OAAO,EACL,iCAAiC,EACjC,8BAA8B,EAC9B,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,gBAAgB,CAAC;AAExB,cAAc;AACd,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,oBAAoB;AACpB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1E,OAAO;AACP,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAExE,UAAU;AACV,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC","sourcesContent":["// types\nexport type {\n IGithubApiUrlOptions,\n IGithubHeadersOptions,\n IGithubQuery,\n IGithubRawContentLocation,\n IGithubRawContentUrlOptions,\n} from './types.js';\n\n// constants\nexport {\n DEFAULT_GITHUB_MAX_RESPONSE_BYTES,\n DEFAULT_GITHUB_TIMEOUT_SECONDS,\n GITHUB_API_ORIGIN,\n GITHUB_API_VERSION,\n GITHUB_RAW_CONTENT_ORIGIN,\n} from './constants.js';\n\n// error codes\nexport { ERRORS } from './errors.js';\n\n// errors and checks\nexport { invalidArgument, isHeaderValue, isRecord } from './utilities.js';\n\n// URLs\nexport { buildGithubApiUrl, buildGithubRawContentUrl } from './urls.js';\n\n// headers\nexport { createGithubHeaders } from './headers.js';\n"]}
@@ -0,0 +1,17 @@
1
+ export type IGithubHeadersOptions = {
2
+ userAgent: string;
3
+ apiVersion?: string | undefined;
4
+ };
5
+ export type IGithubApiUrlOptions = {
6
+ apiOrigin?: string | undefined;
7
+ };
8
+ export type IGithubQuery = Record<string, string | number | boolean | undefined>;
9
+ export type IGithubRawContentLocation = {
10
+ owner: string;
11
+ repository: string;
12
+ ref: string;
13
+ path: string;
14
+ };
15
+ export type IGithubRawContentUrlOptions = {
16
+ rawContentOrigin?: string | undefined;
17
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"","sourcesContent":["// what createGithubHeaders takes\nexport type IGithubHeadersOptions = {\n // the User-Agent GitHub requires on every request, such as the app's name\n userAgent: string;\n\n // the REST API version. Defaults to GITHUB_API_VERSION\n apiVersion?: string | undefined;\n};\n\n// what buildGithubApiUrl takes\nexport type IGithubApiUrlOptions = {\n // the API's base URL, such as `https://ghe.example.com/api/v3` for GitHub Enterprise Server.\n // Defaults to GITHUB_API_ORIGIN\n apiOrigin?: string | undefined;\n};\n\n// the query parameters a URL builder appends; undefined ones are left out\nexport type IGithubQuery = Record<string, string | number | boolean | undefined>;\n\n// a file in a repository, at a ref\nexport type IGithubRawContentLocation = {\n // the account that owns the repository\n owner: string;\n\n // the repository's name\n repository: string;\n\n // the branch, tag, or commit, such as `main` or `feature/login`\n ref: string;\n\n // the file's path from the repository's root, such as `docs/setup.md`\n path: string;\n};\n\n// what buildGithubRawContentUrl takes\nexport type IGithubRawContentUrlOptions = {\n // the raw content's base URL. Defaults to GITHUB_RAW_CONTENT_ORIGIN\n rawContentOrigin?: string | undefined;\n};\n"]}
@@ -0,0 +1,32 @@
1
+ import type { IGithubApiUrlOptions, IGithubQuery, IGithubRawContentLocation, IGithubRawContentUrlOptions } from './types.js';
2
+ /**
3
+ * Builds a REST API URL from its path segments, each encoded on its own, so a branch's `/`
4
+ * becomes `%2F` and no value can climb out of the path, plus the query parameters.
5
+ * @param segments The path's segments, such as `['repos', owner, repository, 'branches', branch]`.
6
+ * @param query The query parameters; undefined ones are left out.
7
+ * @param options The API's base URL, for GitHub Enterprise Server or a test server.
8
+ * @returns The URL.
9
+ * @example
10
+ * import { buildGithubApiUrl } from 'jg-github';
11
+ *
12
+ * buildGithubApiUrl(['repos', 'acme', 'app', 'branches', 'feature/login'], { per_page: 100 });
13
+ * // 'https://api.github.com/repos/acme/app/branches/feature%2Flogin?per_page=100'
14
+ * @throws
15
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
16
+ */
17
+ export declare const buildGithubApiUrl: (segments: readonly (string | number)[], query?: IGithubQuery, { apiOrigin }?: IGithubApiUrlOptions) => string;
18
+ /**
19
+ * Builds the URL of a file's raw content at a ref, with the owner and the repository encoded as
20
+ * one segment each, and the ref's and the path's `/`s kept.
21
+ * @param location The repository, the ref, and the file's path.
22
+ * @param options The raw content's base URL, for a test server.
23
+ * @returns The URL.
24
+ * @example
25
+ * import { buildGithubRawContentUrl } from 'jg-github';
26
+ *
27
+ * buildGithubRawContentUrl({ owner: 'acme', repository: 'app', ref: 'main', path: 'docs/setup.md' });
28
+ * // 'https://raw.githubusercontent.com/acme/app/main/docs/setup.md'
29
+ * @throws
30
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
31
+ */
32
+ export declare const buildGithubRawContentUrl: ({ owner, repository, ref, path }: IGithubRawContentLocation, { rawContentOrigin }?: IGithubRawContentUrlOptions) => string;
@@ -0,0 +1,110 @@
1
+ import { GITHUB_API_ORIGIN, GITHUB_RAW_CONTENT_ORIGIN, LOOPBACK_HOSTNAMES } from './constants.js';
2
+ import { invalidArgument } from './utilities.js';
3
+ /**
4
+ * Reads a base URL: https, or http on a loopback host for a local test server, without
5
+ * credentials, a query, or a fragment, and without its trailing slash.
6
+ * @param value The base URL.
7
+ * @param name The argument it came from.
8
+ * @returns The base URL.
9
+ * @throws
10
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
11
+ */
12
+ const readBaseUrl = (value, name) => {
13
+ let url;
14
+ try {
15
+ url = new URL(String(value));
16
+ }
17
+ catch {
18
+ throw invalidArgument(name);
19
+ }
20
+ const isAllowedProtocol = url.protocol === 'https:' || (url.protocol === 'http:' && LOOPBACK_HOSTNAMES.has(url.hostname));
21
+ if (typeof value !== 'string' ||
22
+ !isAllowedProtocol ||
23
+ url.username !== '' ||
24
+ url.password !== '' ||
25
+ url.search !== '' ||
26
+ url.hash !== '') {
27
+ throw invalidArgument(name);
28
+ }
29
+ return url.href.replace(/\/+$/, '');
30
+ };
31
+ /**
32
+ * Encodes one path segment, refusing one that's empty or a dot segment, which a URL would
33
+ * resolve away.
34
+ * @param segment The segment.
35
+ * @param name The argument it came from.
36
+ * @returns The encoded segment.
37
+ * @throws
38
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
39
+ */
40
+ const encodeSegment = (segment, name) => {
41
+ if (typeof segment === 'number' && Number.isSafeInteger(segment) && segment >= 0) {
42
+ return String(segment);
43
+ }
44
+ if (typeof segment !== 'string' || segment === '' || segment === '.' || segment === '..') {
45
+ throw invalidArgument(name);
46
+ }
47
+ return encodeURIComponent(segment);
48
+ };
49
+ /**
50
+ * Encodes a value whose `/`s separate segments, such as a file's path or a branch like
51
+ * `feature/login`, keeping the `/`s.
52
+ * @param value The value.
53
+ * @param name The argument it came from.
54
+ * @returns The encoded value.
55
+ * @throws
56
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
57
+ */
58
+ const encodePath = (value, name) => {
59
+ if (typeof value !== 'string') {
60
+ throw invalidArgument(name);
61
+ }
62
+ return value
63
+ .split('/')
64
+ .map((segment) => encodeSegment(segment, name))
65
+ .join('/');
66
+ };
67
+ /**
68
+ * Builds a REST API URL from its path segments, each encoded on its own, so a branch's `/`
69
+ * becomes `%2F` and no value can climb out of the path, plus the query parameters.
70
+ * @param segments The path's segments, such as `['repos', owner, repository, 'branches', branch]`.
71
+ * @param query The query parameters; undefined ones are left out.
72
+ * @param options The API's base URL, for GitHub Enterprise Server or a test server.
73
+ * @returns The URL.
74
+ * @example
75
+ * import { buildGithubApiUrl } from 'jg-github';
76
+ *
77
+ * buildGithubApiUrl(['repos', 'acme', 'app', 'branches', 'feature/login'], { per_page: 100 });
78
+ * // 'https://api.github.com/repos/acme/app/branches/feature%2Flogin?per_page=100'
79
+ * @throws
80
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
81
+ */
82
+ export const buildGithubApiUrl = (segments, query = {}, { apiOrigin = GITHUB_API_ORIGIN } = {}) => {
83
+ const base = readBaseUrl(apiOrigin, 'apiOrigin');
84
+ if (!Array.isArray(segments) || segments.length === 0) {
85
+ throw invalidArgument('segments');
86
+ }
87
+ const url = new URL(`${base}/${segments.map((segment) => encodeSegment(segment, 'segments')).join('/')}`);
88
+ for (const [key, value] of Object.entries(query)) {
89
+ if (value !== undefined) {
90
+ url.searchParams.append(key, String(value));
91
+ }
92
+ }
93
+ return url.href;
94
+ };
95
+ /**
96
+ * Builds the URL of a file's raw content at a ref, with the owner and the repository encoded as
97
+ * one segment each, and the ref's and the path's `/`s kept.
98
+ * @param location The repository, the ref, and the file's path.
99
+ * @param options The raw content's base URL, for a test server.
100
+ * @returns The URL.
101
+ * @example
102
+ * import { buildGithubRawContentUrl } from 'jg-github';
103
+ *
104
+ * buildGithubRawContentUrl({ owner: 'acme', repository: 'app', ref: 'main', path: 'docs/setup.md' });
105
+ * // 'https://raw.githubusercontent.com/acme/app/main/docs/setup.md'
106
+ * @throws
107
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
108
+ */
109
+ export const buildGithubRawContentUrl = ({ owner, repository, ref, path }, { rawContentOrigin = GITHUB_RAW_CONTENT_ORIGIN } = {}) => `${readBaseUrl(rawContentOrigin, 'rawContentOrigin')}/${encodeSegment(owner, 'owner')}/${encodeSegment(repository, 'repository')}/${encodePath(ref, 'ref')}/${encodePath(path, 'path')}`;
110
+ //# sourceMappingURL=urls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"urls.js","sourceRoot":"","sources":["../../src/shared/urls.ts"],"names":[],"mappings":"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","sourcesContent":["import { GITHUB_API_ORIGIN, GITHUB_RAW_CONTENT_ORIGIN, LOOPBACK_HOSTNAMES } from './constants.js';\nimport type {\n IGithubApiUrlOptions,\n IGithubQuery,\n IGithubRawContentLocation,\n IGithubRawContentUrlOptions,\n} from './types.js';\nimport { invalidArgument } from './utilities.js';\n\n/**\n * Reads a base URL: https, or http on a loopback host for a local test server, without\n * credentials, a query, or a fragment, and without its trailing slash.\n * @param value The base URL.\n * @param name The argument it came from.\n * @returns The base URL.\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n */\nconst readBaseUrl = (value: unknown, name: string): string => {\n let url: URL;\n try {\n url = new URL(String(value));\n } catch {\n throw invalidArgument(name);\n }\n const isAllowedProtocol =\n url.protocol === 'https:' || (url.protocol === 'http:' && LOOPBACK_HOSTNAMES.has(url.hostname));\n if (\n typeof value !== 'string' ||\n !isAllowedProtocol ||\n url.username !== '' ||\n url.password !== '' ||\n url.search !== '' ||\n url.hash !== ''\n ) {\n throw invalidArgument(name);\n }\n return url.href.replace(/\\/+$/, '');\n};\n\n/**\n * Encodes one path segment, refusing one that's empty or a dot segment, which a URL would\n * resolve away.\n * @param segment The segment.\n * @param name The argument it came from.\n * @returns The encoded segment.\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n */\nconst encodeSegment = (segment: unknown, name: string): string => {\n if (typeof segment === 'number' && Number.isSafeInteger(segment) && segment >= 0) {\n return String(segment);\n }\n if (typeof segment !== 'string' || segment === '' || segment === '.' || segment === '..') {\n throw invalidArgument(name);\n }\n return encodeURIComponent(segment);\n};\n\n/**\n * Encodes a value whose `/`s separate segments, such as a file's path or a branch like\n * `feature/login`, keeping the `/`s.\n * @param value The value.\n * @param name The argument it came from.\n * @returns The encoded value.\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n */\nconst encodePath = (value: unknown, name: string): string => {\n if (typeof value !== 'string') {\n throw invalidArgument(name);\n }\n return value\n .split('/')\n .map((segment) => encodeSegment(segment, name))\n .join('/');\n};\n\n/**\n * Builds a REST API URL from its path segments, each encoded on its own, so a branch's `/`\n * becomes `%2F` and no value can climb out of the path, plus the query parameters.\n * @param segments The path's segments, such as `['repos', owner, repository, 'branches', branch]`.\n * @param query The query parameters; undefined ones are left out.\n * @param options The API's base URL, for GitHub Enterprise Server or a test server.\n * @returns The URL.\n * @example\n * import { buildGithubApiUrl } from 'jg-github';\n *\n * buildGithubApiUrl(['repos', 'acme', 'app', 'branches', 'feature/login'], { per_page: 100 });\n * // 'https://api.github.com/repos/acme/app/branches/feature%2Flogin?per_page=100'\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n */\nexport const buildGithubApiUrl = (\n segments: readonly (string | number)[],\n query: IGithubQuery = {},\n { apiOrigin = GITHUB_API_ORIGIN }: IGithubApiUrlOptions = {},\n): string => {\n const base = readBaseUrl(apiOrigin, 'apiOrigin');\n if (!Array.isArray(segments) || segments.length === 0) {\n throw invalidArgument('segments');\n }\n const url = new URL(\n `${base}/${segments.map((segment: unknown) => encodeSegment(segment, 'segments')).join('/')}`,\n );\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) {\n url.searchParams.append(key, String(value));\n }\n }\n return url.href;\n};\n\n/**\n * Builds the URL of a file's raw content at a ref, with the owner and the repository encoded as\n * one segment each, and the ref's and the path's `/`s kept.\n * @param location The repository, the ref, and the file's path.\n * @param options The raw content's base URL, for a test server.\n * @returns The URL.\n * @example\n * import { buildGithubRawContentUrl } from 'jg-github';\n *\n * buildGithubRawContentUrl({ owner: 'acme', repository: 'app', ref: 'main', path: 'docs/setup.md' });\n * // 'https://raw.githubusercontent.com/acme/app/main/docs/setup.md'\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n */\nexport const buildGithubRawContentUrl = (\n { owner, repository, ref, path }: IGithubRawContentLocation,\n { rawContentOrigin = GITHUB_RAW_CONTENT_ORIGIN }: IGithubRawContentUrlOptions = {},\n): string =>\n `${readBaseUrl(rawContentOrigin, 'rawContentOrigin')}/${encodeSegment(owner, 'owner')}/${encodeSegment(\n repository,\n 'repository',\n )}/${encodePath(ref, 'ref')}/${encodePath(path, 'path')}`;\n"]}
@@ -0,0 +1,20 @@
1
+ import { Exception } from 'jg-exception';
2
+ /**
3
+ * Builds the exception for an argument that isn't valid.
4
+ * @param name The argument, such as `installationId`.
5
+ * @returns The GITHUB_INVALID_ARGUMENT exception.
6
+ */
7
+ export declare const invalidArgument: (name: string) => Exception;
8
+ /**
9
+ * Checks whether a value is a plain object, such as one parsed from JSON.
10
+ * @param value The value.
11
+ * @returns Whether it is.
12
+ */
13
+ export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
14
+ /**
15
+ * Checks whether a value can go in an HTTP header: a non-empty string of visible ASCII characters
16
+ * and spaces, without line breaks.
17
+ * @param value The value.
18
+ * @returns Whether it can.
19
+ */
20
+ export declare const isHeaderValue: (value: unknown) => value is string;
@@ -0,0 +1,25 @@
1
+ import { Exception } from 'jg-exception';
2
+ import { ERRORS } from './errors.js';
3
+ /**
4
+ * Builds the exception for an argument that isn't valid.
5
+ * @param name The argument, such as `installationId`.
6
+ * @returns The GITHUB_INVALID_ARGUMENT exception.
7
+ */
8
+ export const invalidArgument = (name) => new Exception("An argument isn't valid.", {
9
+ code: ERRORS.GITHUB_INVALID_ARGUMENT,
10
+ data: { argument: name },
11
+ });
12
+ /**
13
+ * Checks whether a value is a plain object, such as one parsed from JSON.
14
+ * @param value The value.
15
+ * @returns Whether it is.
16
+ */
17
+ export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
18
+ /**
19
+ * Checks whether a value can go in an HTTP header: a non-empty string of visible ASCII characters
20
+ * and spaces, without line breaks.
21
+ * @param value The value.
22
+ * @returns Whether it can.
23
+ */
24
+ export const isHeaderValue = (value) => typeof value === 'string' && /^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?$/.test(value);
25
+ //# sourceMappingURL=utilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utilities.js","sourceRoot":"","sources":["../../src/shared/utilities.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAa,EAAE,CACzD,IAAI,SAAS,CAAC,0BAA0B,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC,uBAAuB;IACpC,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;CACzB,CAAC,CAAC;AAEL;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAoC,EAAE,CAC3E,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAc,EAAmB,EAAE,CAC/D,OAAO,KAAK,KAAK,QAAQ,IAAI,2CAA2C,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC","sourcesContent":["import { Exception } from 'jg-exception';\n\nimport { ERRORS } from './errors.js';\n\n/**\n * Builds the exception for an argument that isn't valid.\n * @param name The argument, such as `installationId`.\n * @returns The GITHUB_INVALID_ARGUMENT exception.\n */\nexport const invalidArgument = (name: string): Exception =>\n new Exception(\"An argument isn't valid.\", {\n code: ERRORS.GITHUB_INVALID_ARGUMENT,\n data: { argument: name },\n });\n\n/**\n * Checks whether a value is a plain object, such as one parsed from JSON.\n * @param value The value.\n * @returns Whether it is.\n */\nexport const isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * Checks whether a value can go in an HTTP header: a non-empty string of visible ASCII characters\n * and spaces, without line breaks.\n * @param value The value.\n * @returns Whether it can.\n */\nexport const isHeaderValue = (value: unknown): value is string =>\n typeof value === 'string' && /^[\\x21-\\x7e](?:[\\x20-\\x7e]*[\\x21-\\x7e])?$/.test(value);\n"]}
@@ -0,0 +1,2 @@
1
+ export declare const MAX_SIGNATURE_HEADER_LENGTH = 71;
2
+ export declare const SIGNATURE_HEADER_PATTERN: RegExp;
@@ -0,0 +1,5 @@
1
+ // the longest X-Hub-Signature-256 header: 'sha256=' and 64 hexadecimal characters
2
+ export const MAX_SIGNATURE_HEADER_LENGTH = 71;
3
+ // an X-Hub-Signature-256 header
4
+ export const SIGNATURE_HEADER_PATTERN = /^sha256=[0-9a-f]{64}$/;
5
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/webhook/constants.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAE9C,gCAAgC;AAChC,MAAM,CAAC,MAAM,wBAAwB,GAAG,uBAAuB,CAAC","sourcesContent":["// the longest X-Hub-Signature-256 header: 'sha256=' and 64 hexadecimal characters\nexport const MAX_SIGNATURE_HEADER_LENGTH = 71;\n\n// an X-Hub-Signature-256 header\nexport const SIGNATURE_HEADER_PATTERN = /^sha256=[0-9a-f]{64}$/;\n"]}
@@ -0,0 +1 @@
1
+ export { verifyGithubWebhookSignature } from './webhook.js';
@@ -0,0 +1,3 @@
1
+ // webhook signatures
2
+ export { verifyGithubWebhookSignature } from './webhook.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webhook/index.ts"],"names":[],"mappings":"AAAA,qBAAqB;AACrB,OAAO,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAC","sourcesContent":["// webhook signatures\nexport { verifyGithubWebhookSignature } from './webhook.js';\n"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Verifies a GitHub webhook delivery's X-Hub-Signature-256 header against the body's exact
3
+ * bytes, in constant time. A header that's missing, repeated, longer than 71 characters, or not
4
+ * `sha256=` and 64 lowercase hexadecimal characters is false, like a wrong signature. Verify the
5
+ * raw body before parsing it, because parsing and serializing can change its bytes.
6
+ * @param rawBody The request's body, as received.
7
+ * @param header The X-Hub-Signature-256 header, as the framework gives it.
8
+ * @param secret The webhook's secret.
9
+ * @returns Whether the signature is the body's.
10
+ * @example
11
+ * import { verifyGithubWebhookSignature } from 'jg-github';
12
+ *
13
+ * declare const rawBody: Buffer;
14
+ * declare const headers: Record<string, string | string[] | undefined>;
15
+ * declare const webhookSecret: string;
16
+ *
17
+ * if (!verifyGithubWebhookSignature(rawBody, headers['x-hub-signature-256'], webhookSecret)) {
18
+ * // answer 401
19
+ * }
20
+ * @throws
21
+ * - INVALID_HMAC_SECRET: The HMAC secret must be a non-empty string or byte array.
22
+ */
23
+ export declare const verifyGithubWebhookSignature: (rawBody: string | Uint8Array, header: unknown, secret: string | Uint8Array) => boolean;
@@ -0,0 +1,35 @@
1
+ import { verifyHmac } from 'jg-crypto';
2
+ import { MAX_SIGNATURE_HEADER_LENGTH, SIGNATURE_HEADER_PATTERN } from './constants.js';
3
+ /**
4
+ * Verifies a GitHub webhook delivery's X-Hub-Signature-256 header against the body's exact
5
+ * bytes, in constant time. A header that's missing, repeated, longer than 71 characters, or not
6
+ * `sha256=` and 64 lowercase hexadecimal characters is false, like a wrong signature. Verify the
7
+ * raw body before parsing it, because parsing and serializing can change its bytes.
8
+ * @param rawBody The request's body, as received.
9
+ * @param header The X-Hub-Signature-256 header, as the framework gives it.
10
+ * @param secret The webhook's secret.
11
+ * @returns Whether the signature is the body's.
12
+ * @example
13
+ * import { verifyGithubWebhookSignature } from 'jg-github';
14
+ *
15
+ * declare const rawBody: Buffer;
16
+ * declare const headers: Record<string, string | string[] | undefined>;
17
+ * declare const webhookSecret: string;
18
+ *
19
+ * if (!verifyGithubWebhookSignature(rawBody, headers['x-hub-signature-256'], webhookSecret)) {
20
+ * // answer 401
21
+ * }
22
+ * @throws
23
+ * - INVALID_HMAC_SECRET: The HMAC secret must be a non-empty string or byte array.
24
+ */
25
+ export const verifyGithubWebhookSignature = (rawBody, header, secret) => {
26
+ if (typeof header !== 'string' ||
27
+ header.length > MAX_SIGNATURE_HEADER_LENGTH ||
28
+ !SIGNATURE_HEADER_PATTERN.test(header)) {
29
+ // still checks the secret, so an empty one fails loudly whatever the header
30
+ verifyHmac(rawBody, '', secret);
31
+ return false;
32
+ }
33
+ return verifyHmac(rawBody, header.slice('sha256='.length), secret);
34
+ };
35
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/webhook/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,OAAO,EAAE,2BAA2B,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAEvF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAC1C,OAA4B,EAC5B,MAAe,EACf,MAA2B,EAClB,EAAE;IACX,IACE,OAAO,MAAM,KAAK,QAAQ;QAC1B,MAAM,CAAC,MAAM,GAAG,2BAA2B;QAC3C,CAAC,wBAAwB,CAAC,IAAI,CAAC,MAAM,CAAC,EACtC,CAAC;QACD,4EAA4E;QAC5E,UAAU,CAAC,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;AACrE,CAAC,CAAC","sourcesContent":["import { verifyHmac } from 'jg-crypto';\n\nimport { MAX_SIGNATURE_HEADER_LENGTH, SIGNATURE_HEADER_PATTERN } from './constants.js';\n\n/**\n * Verifies a GitHub webhook delivery's X-Hub-Signature-256 header against the body's exact\n * bytes, in constant time. A header that's missing, repeated, longer than 71 characters, or not\n * `sha256=` and 64 lowercase hexadecimal characters is false, like a wrong signature. Verify the\n * raw body before parsing it, because parsing and serializing can change its bytes.\n * @param rawBody The request's body, as received.\n * @param header The X-Hub-Signature-256 header, as the framework gives it.\n * @param secret The webhook's secret.\n * @returns Whether the signature is the body's.\n * @example\n * import { verifyGithubWebhookSignature } from 'jg-github';\n *\n * declare const rawBody: Buffer;\n * declare const headers: Record<string, string | string[] | undefined>;\n * declare const webhookSecret: string;\n *\n * if (!verifyGithubWebhookSignature(rawBody, headers['x-hub-signature-256'], webhookSecret)) {\n * // answer 401\n * }\n * @throws\n * - INVALID_HMAC_SECRET: The HMAC secret must be a non-empty string or byte array.\n */\nexport const verifyGithubWebhookSignature = (\n rawBody: string | Uint8Array,\n header: unknown,\n secret: string | Uint8Array,\n): boolean => {\n if (\n typeof header !== 'string' ||\n header.length > MAX_SIGNATURE_HEADER_LENGTH ||\n !SIGNATURE_HEADER_PATTERN.test(header)\n ) {\n // still checks the secret, so an empty one fails loudly whatever the header\n verifyHmac(rawBody, '', secret);\n return false;\n }\n return verifyHmac(rawBody, header.slice('sha256='.length), secret);\n};\n"]}