jg-github 0.0.0-stage → 0.2.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 +19 -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,2 @@
1
+ export type { ICreateInstallationTokenOptions, IGithubPermissionLevel, IGithubPermissions, IGithubRepository, IGithubRequestOptions, IInstallationToken, IInstallationTokenRepositories, } from './types.js';
2
+ export { createInstallationToken, revokeInstallationToken } from './installation-token.js';
@@ -0,0 +1,3 @@
1
+ // installation tokens
2
+ export { createInstallationToken, revokeInstallationToken } from './installation-token.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/installation-token/index.ts"],"names":[],"mappings":"AAWA,sBAAsB;AACtB,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC","sourcesContent":["// types\nexport type {\n ICreateInstallationTokenOptions,\n IGithubPermissionLevel,\n IGithubPermissions,\n IGithubRepository,\n IGithubRequestOptions,\n IInstallationToken,\n IInstallationTokenRepositories,\n} from './types.js';\n\n// installation tokens\nexport { createInstallationToken, revokeInstallationToken } from './installation-token.js';\n"]}
@@ -0,0 +1,57 @@
1
+ import type { ICreateInstallationTokenOptions, IGithubRequestOptions, IInstallationToken } from './types.js';
2
+ /**
3
+ * Revokes an installation token before it expires, such as when the work it was for is done,
4
+ * with `DELETE /installation/token`, which GitHub answers with 204. It never retries. The error
5
+ * never holds the token.
6
+ * @param token The installation token.
7
+ * @param options The User-Agent, the API's base URL and version, and the limits.
8
+ * @returns A promise that resolves once GitHub has revoked it.
9
+ * @example
10
+ * import { revokeInstallationToken } from 'jg-github';
11
+ *
12
+ * declare const token: string;
13
+ *
14
+ * await revokeInstallationToken(token, { userAgent: 'acme-deploy-bot' });
15
+ * @throws
16
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
17
+ * - GITHUB_TOKEN_REVOKE_FAILED: The installation token couldn't be revoked.
18
+ */
19
+ export declare const revokeInstallationToken: (token: string, { userAgent, apiOrigin, apiVersion, timeoutSeconds, signal, }: IGithubRequestOptions) => Promise<void>;
20
+ /**
21
+ * Creates an installation token for exactly the repositories and the permissions asked for, with
22
+ * `POST /app/installations/{id}/access_tokens`, authenticated as the App. `permissions` is
23
+ * required, because GitHub grants every permission of the installation without it, and the
24
+ * repositories are named by name or by ID, never both. The granted token must reach exactly
25
+ * those repositories, only the selected ones, with exactly those permissions, plus
26
+ * `metadata: 'read'`, and must not have expired; otherwise it's revoked before the error is
27
+ * thrown, and the error's data says whether that worked. Nothing is retried, and no error holds
28
+ * the token or the App's JWT.
29
+ * @param options The App, the installation, the scope, and the request's limits.
30
+ * @returns A promise that resolves to the token, when it expires, and what it reaches.
31
+ * @example
32
+ * import { createInstallationToken, revokeInstallationToken } from 'jg-github';
33
+ *
34
+ * declare const privateKey: string;
35
+ *
36
+ * const { token } = await createInstallationToken({
37
+ * appId: 123456,
38
+ * privateKey,
39
+ * installationId: 12345678,
40
+ * repositories: ['app'],
41
+ * permissions: { contents: 'read' },
42
+ * userAgent: 'acme-deploy-bot',
43
+ * });
44
+ * try {
45
+ * // read the repository's contents with the token
46
+ * } finally {
47
+ * await revokeInstallationToken(token, { userAgent: 'acme-deploy-bot' });
48
+ * }
49
+ * @throws
50
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
51
+ * - GITHUB_PRIVATE_KEY_INVALID: The GitHub App's private key isn't an unencrypted RSA private key.
52
+ * - GITHUB_REQUEST_FAILED: The GitHub request failed.
53
+ * - GITHUB_RESPONSE_INVALID: GitHub's response isn't in the expected format.
54
+ * - GITHUB_TOKEN_SCOPE_MISMATCH: The installation token's scope isn't the one requested.
55
+ * - GITHUB_TOKEN_EXPIRED: The installation token had already expired.
56
+ */
57
+ export declare const createInstallationToken: (options: ICreateInstallationTokenOptions) => Promise<IInstallationToken>;
@@ -0,0 +1,152 @@
1
+ import { request } from 'jg-fetch';
2
+ import { createGithubAppJwt } from '../app-auth/index.js';
3
+ import { DEFAULT_GITHUB_MAX_RESPONSE_BYTES, DEFAULT_GITHUB_TIMEOUT_SECONDS, buildGithubApiUrl, createGithubHeaders, } from '../shared/index.js';
4
+ import { INVALID_ARGUMENT_KINDS } from './constants.js';
5
+ import { fromFetchError, responseInvalid, revokeFailed, scopeMismatch, tokenExpired, } from './errors.js';
6
+ import { findScopeMismatch, readToken, readTokenDetails } from './utilities.js';
7
+ import { readInstallationId, readPermissions, readRequestedRepositories } from './validations.js';
8
+ /**
9
+ * Revokes an installation token before it expires, such as when the work it was for is done,
10
+ * with `DELETE /installation/token`, which GitHub answers with 204. It never retries. The error
11
+ * never holds the token.
12
+ * @param token The installation token.
13
+ * @param options The User-Agent, the API's base URL and version, and the limits.
14
+ * @returns A promise that resolves once GitHub has revoked it.
15
+ * @example
16
+ * import { revokeInstallationToken } from 'jg-github';
17
+ *
18
+ * declare const token: string;
19
+ *
20
+ * await revokeInstallationToken(token, { userAgent: 'acme-deploy-bot' });
21
+ * @throws
22
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
23
+ * - GITHUB_TOKEN_REVOKE_FAILED: The installation token couldn't be revoked.
24
+ */
25
+ export const revokeInstallationToken = async (token, { userAgent, apiOrigin, apiVersion, timeoutSeconds = DEFAULT_GITHUB_TIMEOUT_SECONDS, signal, }) => {
26
+ const headers = createGithubHeaders(token, { userAgent, apiVersion });
27
+ const { error, meta } = await request(buildGithubApiUrl(['installation', 'token'], {}, { apiOrigin }), {
28
+ method: 'DELETE',
29
+ headers,
30
+ parseAs: 'none',
31
+ retryDelaysSeconds: [],
32
+ timeoutSeconds,
33
+ init: { redirect: 'error' },
34
+ ...(signal === undefined ? {} : { signal }),
35
+ });
36
+ if (error !== null) {
37
+ throw INVALID_ARGUMENT_KINDS.has(error.kind)
38
+ ? fromFetchError(error, [token])
39
+ : revokeFailed(error.kind, error.status, [token]);
40
+ }
41
+ if (meta.status !== 204) {
42
+ throw revokeFailed('UNEXPECTED_STATUS', meta.status, [token]);
43
+ }
44
+ };
45
+ /**
46
+ * Revokes a token jg-github received but won't hand over, with its own timeout and without the
47
+ * caller's signal, so a cancelled call still revokes it.
48
+ * @param token The token.
49
+ * @param options The request's options.
50
+ * @returns A promise that resolves to whether GitHub revoked it.
51
+ */
52
+ const revokeUnusedToken = async (token, { userAgent, apiOrigin, apiVersion, timeoutSeconds }) => {
53
+ try {
54
+ await revokeInstallationToken(token, { userAgent, apiOrigin, apiVersion, timeoutSeconds });
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ };
61
+ /**
62
+ * Creates an installation token for exactly the repositories and the permissions asked for, with
63
+ * `POST /app/installations/{id}/access_tokens`, authenticated as the App. `permissions` is
64
+ * required, because GitHub grants every permission of the installation without it, and the
65
+ * repositories are named by name or by ID, never both. The granted token must reach exactly
66
+ * those repositories, only the selected ones, with exactly those permissions, plus
67
+ * `metadata: 'read'`, and must not have expired; otherwise it's revoked before the error is
68
+ * thrown, and the error's data says whether that worked. Nothing is retried, and no error holds
69
+ * the token or the App's JWT.
70
+ * @param options The App, the installation, the scope, and the request's limits.
71
+ * @returns A promise that resolves to the token, when it expires, and what it reaches.
72
+ * @example
73
+ * import { createInstallationToken, revokeInstallationToken } from 'jg-github';
74
+ *
75
+ * declare const privateKey: string;
76
+ *
77
+ * const { token } = await createInstallationToken({
78
+ * appId: 123456,
79
+ * privateKey,
80
+ * installationId: 12345678,
81
+ * repositories: ['app'],
82
+ * permissions: { contents: 'read' },
83
+ * userAgent: 'acme-deploy-bot',
84
+ * });
85
+ * try {
86
+ * // read the repository's contents with the token
87
+ * } finally {
88
+ * await revokeInstallationToken(token, { userAgent: 'acme-deploy-bot' });
89
+ * }
90
+ * @throws
91
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
92
+ * - GITHUB_PRIVATE_KEY_INVALID: The GitHub App's private key isn't an unencrypted RSA private key.
93
+ * - GITHUB_REQUEST_FAILED: The GitHub request failed.
94
+ * - GITHUB_RESPONSE_INVALID: GitHub's response isn't in the expected format.
95
+ * - GITHUB_TOKEN_SCOPE_MISMATCH: The installation token's scope isn't the one requested.
96
+ * - GITHUB_TOKEN_EXPIRED: The installation token had already expired.
97
+ */
98
+ export const createInstallationToken = async (options) => {
99
+ const { appId, privateKey, installationId, permissions, repositories, repositoryIds, userAgent, apiOrigin, apiVersion, timeoutSeconds = DEFAULT_GITHUB_TIMEOUT_SECONDS, maxResponseBytes = DEFAULT_GITHUB_MAX_RESPONSE_BYTES, signal, } = options;
100
+ const id = readInstallationId(installationId);
101
+ const requestedPermissions = readPermissions(permissions);
102
+ const requested = readRequestedRepositories(repositories, repositoryIds);
103
+ const url = buildGithubApiUrl(['app', 'installations', id, 'access_tokens'], {}, { apiOrigin });
104
+ const jwt = createGithubAppJwt({ appId, privateKey });
105
+ const { data, error, meta } = await request(url, {
106
+ method: 'POST',
107
+ headers: createGithubHeaders(jwt, { userAgent, apiVersion }),
108
+ body: {
109
+ permissions: requestedPermissions,
110
+ ...(requested.kind === 'names'
111
+ ? { repositories: requested.values }
112
+ : { repository_ids: requested.values }),
113
+ },
114
+ parseAs: 'json',
115
+ retryDelaysSeconds: [],
116
+ timeoutSeconds,
117
+ maxResponseBytes,
118
+ init: { redirect: 'error' },
119
+ ...(signal === undefined ? {} : { signal }),
120
+ });
121
+ if (error !== null) {
122
+ throw fromFetchError(error, [jwt]);
123
+ }
124
+ const token = readToken(data);
125
+ if (token === undefined) {
126
+ throw responseInvalid([jwt]);
127
+ }
128
+ // from here on GitHub granted a token, which is revoked before any error is thrown
129
+ const redact = [jwt, token];
130
+ const revokeOptions = { userAgent, apiOrigin, apiVersion, timeoutSeconds };
131
+ const details = meta.status === 201 ? readTokenDetails(data) : undefined;
132
+ if (details === undefined) {
133
+ throw responseInvalid(redact, await revokeUnusedToken(token, revokeOptions));
134
+ }
135
+ const mismatch = findScopeMismatch(details, requested, requestedPermissions);
136
+ if (mismatch !== null) {
137
+ throw scopeMismatch(mismatch, await revokeUnusedToken(token, revokeOptions), redact);
138
+ }
139
+ if (details.expiresAt.getTime() <= Date.now()) {
140
+ throw tokenExpired(await revokeUnusedToken(token, revokeOptions), redact);
141
+ }
142
+ return {
143
+ token,
144
+ expiresAt: details.expiresAt,
145
+ permissions: details.permissions,
146
+ repositories: details.repositories.map(({ id: repositoryId, fullName }) => ({
147
+ id: repositoryId,
148
+ fullName,
149
+ })),
150
+ };
151
+ };
152
+ //# sourceMappingURL=installation-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installation-token.js","sourceRoot":"","sources":["../../src/installation-token/installation-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EACL,iCAAiC,EACjC,8BAA8B,EAC9B,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,cAAc,EACd,eAAe,EACf,YAAY,EACZ,aAAa,EACb,YAAY,GACb,MAAM,aAAa,CAAC;AAMrB,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAElG;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,EAC1C,KAAa,EACb,EACE,SAAS,EACT,SAAS,EACT,UAAU,EACV,cAAc,GAAG,8BAA8B,EAC/C,MAAM,GACgB,EACT,EAAE;IACjB,MAAM,OAAO,GAAG,mBAAmB,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;IACtE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,OAAO,CACnC,iBAAiB,CAAC,CAAC,cAAc,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAC/D;QACE,MAAM,EAAE,QAAQ;QAChB,OAAO;QACP,OAAO,EAAE,MAAM;QACf,kBAAkB,EAAE,EAAE;QACtB,cAAc;QACd,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE;QAC3B,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;KAC5C,CACF,CAAC;IACF,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;YAC1C,CAAC,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;YAChC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACxB,MAAM,YAAY,CAAC,mBAAmB,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAChE,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,iBAAiB,GAAG,KAAK,EAC7B,KAAa,EACb,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAyB,EACzD,EAAE;IACpB,IAAI,CAAC;QACH,MAAM,uBAAuB,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC,CAAC;QAC3F,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,EAC1C,OAAwC,EACX,EAAE;IAC/B,MAAM,EACJ,KAAK,EACL,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,aAAa,EACb,SAAS,EACT,SAAS,EACT,UAAU,EACV,cAAc,GAAG,8BAA8B,EAC/C,gBAAgB,GAAG,iCAAiC,EACpD,MAAM,GACP,GAAG,OAAO,CAAC;IACZ,MAAM,EAAE,GAAG,kBAAkB,CAAC,cAAc,CAAC,CAAC;IAC9C,MAAM,oBAAoB,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAC1D,MAAM,SAAS,GAAG,yBAAyB,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;IACzE,MAAM,GAAG,GAAG,iBAAiB,CAAC,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,EAAE,eAAe,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IAChG,MAAM,GAAG,GAAG,kBAAkB,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;IACtD,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE;QAC/C,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,mBAAmB,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;QAC5D,IAAI,EAAE;YACJ,WAAW,EAAE,oBAAoB;YACjC,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK,OAAO;gBAC5B,CAAC,CAAC,EAAE,YAAY,EAAE,SAAS,CAAC,MAAM,EAAE;gBACpC,CAAC,CAAC,EAAE,cAAc,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC;SAC1C;QACD,OAAO,EAAE,MAAM;QACf,kBAAkB,EAAE,EAAE;QACtB,cAAc;QACd,gBAAgB;QAChB,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE;QAC3B,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;KAC5C,CAAC,CAAC;IACH,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,cAAc,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrC,CAAC;IACD,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,eAAe,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,CAAC;IAED,mFAAmF;IACnF,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC5B,MAAM,aAAa,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC;IAC3E,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACzE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,eAAe,CAAC,MAAM,EAAE,MAAM,iBAAiB,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC;IAC/E,CAAC;IACD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,OAAO,EAAE,SAAS,EAAE,oBAAoB,CAAC,CAAC;IAC7E,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,MAAM,aAAa,CAAC,QAAQ,EAAE,MAAM,iBAAiB,CAAC,KAAK,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CAAC;IACvF,CAAC;IACD,IAAI,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QAC9C,MAAM,YAAY,CAAC,MAAM,iBAAiB,CAAC,KAAK,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO;QACL,KAAK;QACL,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;YAC1E,EAAE,EAAE,YAAY;YAChB,QAAQ;SACT,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { request } from 'jg-fetch';\n\nimport { createGithubAppJwt } from '../app-auth/index.js';\nimport {\n DEFAULT_GITHUB_MAX_RESPONSE_BYTES,\n DEFAULT_GITHUB_TIMEOUT_SECONDS,\n buildGithubApiUrl,\n createGithubHeaders,\n} from '../shared/index.js';\nimport { INVALID_ARGUMENT_KINDS } from './constants.js';\nimport {\n fromFetchError,\n responseInvalid,\n revokeFailed,\n scopeMismatch,\n tokenExpired,\n} from './errors.js';\nimport type {\n ICreateInstallationTokenOptions,\n IGithubRequestOptions,\n IInstallationToken,\n} from './types.js';\nimport { findScopeMismatch, readToken, readTokenDetails } from './utilities.js';\nimport { readInstallationId, readPermissions, readRequestedRepositories } from './validations.js';\n\n/**\n * Revokes an installation token before it expires, such as when the work it was for is done,\n * with `DELETE /installation/token`, which GitHub answers with 204. It never retries. The error\n * never holds the token.\n * @param token The installation token.\n * @param options The User-Agent, the API's base URL and version, and the limits.\n * @returns A promise that resolves once GitHub has revoked it.\n * @example\n * import { revokeInstallationToken } from 'jg-github';\n *\n * declare const token: string;\n *\n * await revokeInstallationToken(token, { userAgent: 'acme-deploy-bot' });\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n * - GITHUB_TOKEN_REVOKE_FAILED: The installation token couldn't be revoked.\n */\nexport const revokeInstallationToken = async (\n token: string,\n {\n userAgent,\n apiOrigin,\n apiVersion,\n timeoutSeconds = DEFAULT_GITHUB_TIMEOUT_SECONDS,\n signal,\n }: IGithubRequestOptions,\n): Promise<void> => {\n const headers = createGithubHeaders(token, { userAgent, apiVersion });\n const { error, meta } = await request(\n buildGithubApiUrl(['installation', 'token'], {}, { apiOrigin }),\n {\n method: 'DELETE',\n headers,\n parseAs: 'none',\n retryDelaysSeconds: [],\n timeoutSeconds,\n init: { redirect: 'error' },\n ...(signal === undefined ? {} : { signal }),\n },\n );\n if (error !== null) {\n throw INVALID_ARGUMENT_KINDS.has(error.kind)\n ? fromFetchError(error, [token])\n : revokeFailed(error.kind, error.status, [token]);\n }\n if (meta.status !== 204) {\n throw revokeFailed('UNEXPECTED_STATUS', meta.status, [token]);\n }\n};\n\n/**\n * Revokes a token jg-github received but won't hand over, with its own timeout and without the\n * caller's signal, so a cancelled call still revokes it.\n * @param token The token.\n * @param options The request's options.\n * @returns A promise that resolves to whether GitHub revoked it.\n */\nconst revokeUnusedToken = async (\n token: string,\n { userAgent, apiOrigin, apiVersion, timeoutSeconds }: IGithubRequestOptions,\n): Promise<boolean> => {\n try {\n await revokeInstallationToken(token, { userAgent, apiOrigin, apiVersion, timeoutSeconds });\n return true;\n } catch {\n return false;\n }\n};\n\n/**\n * Creates an installation token for exactly the repositories and the permissions asked for, with\n * `POST /app/installations/{id}/access_tokens`, authenticated as the App. `permissions` is\n * required, because GitHub grants every permission of the installation without it, and the\n * repositories are named by name or by ID, never both. The granted token must reach exactly\n * those repositories, only the selected ones, with exactly those permissions, plus\n * `metadata: 'read'`, and must not have expired; otherwise it's revoked before the error is\n * thrown, and the error's data says whether that worked. Nothing is retried, and no error holds\n * the token or the App's JWT.\n * @param options The App, the installation, the scope, and the request's limits.\n * @returns A promise that resolves to the token, when it expires, and what it reaches.\n * @example\n * import { createInstallationToken, revokeInstallationToken } from 'jg-github';\n *\n * declare const privateKey: string;\n *\n * const { token } = await createInstallationToken({\n * appId: 123456,\n * privateKey,\n * installationId: 12345678,\n * repositories: ['app'],\n * permissions: { contents: 'read' },\n * userAgent: 'acme-deploy-bot',\n * });\n * try {\n * // read the repository's contents with the token\n * } finally {\n * await revokeInstallationToken(token, { userAgent: 'acme-deploy-bot' });\n * }\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n * - GITHUB_PRIVATE_KEY_INVALID: The GitHub App's private key isn't an unencrypted RSA private key.\n * - GITHUB_REQUEST_FAILED: The GitHub request failed.\n * - GITHUB_RESPONSE_INVALID: GitHub's response isn't in the expected format.\n * - GITHUB_TOKEN_SCOPE_MISMATCH: The installation token's scope isn't the one requested.\n * - GITHUB_TOKEN_EXPIRED: The installation token had already expired.\n */\nexport const createInstallationToken = async (\n options: ICreateInstallationTokenOptions,\n): Promise<IInstallationToken> => {\n const {\n appId,\n privateKey,\n installationId,\n permissions,\n repositories,\n repositoryIds,\n userAgent,\n apiOrigin,\n apiVersion,\n timeoutSeconds = DEFAULT_GITHUB_TIMEOUT_SECONDS,\n maxResponseBytes = DEFAULT_GITHUB_MAX_RESPONSE_BYTES,\n signal,\n } = options;\n const id = readInstallationId(installationId);\n const requestedPermissions = readPermissions(permissions);\n const requested = readRequestedRepositories(repositories, repositoryIds);\n const url = buildGithubApiUrl(['app', 'installations', id, 'access_tokens'], {}, { apiOrigin });\n const jwt = createGithubAppJwt({ appId, privateKey });\n const { data, error, meta } = await request(url, {\n method: 'POST',\n headers: createGithubHeaders(jwt, { userAgent, apiVersion }),\n body: {\n permissions: requestedPermissions,\n ...(requested.kind === 'names'\n ? { repositories: requested.values }\n : { repository_ids: requested.values }),\n },\n parseAs: 'json',\n retryDelaysSeconds: [],\n timeoutSeconds,\n maxResponseBytes,\n init: { redirect: 'error' },\n ...(signal === undefined ? {} : { signal }),\n });\n if (error !== null) {\n throw fromFetchError(error, [jwt]);\n }\n const token = readToken(data);\n if (token === undefined) {\n throw responseInvalid([jwt]);\n }\n\n // from here on GitHub granted a token, which is revoked before any error is thrown\n const redact = [jwt, token];\n const revokeOptions = { userAgent, apiOrigin, apiVersion, timeoutSeconds };\n const details = meta.status === 201 ? readTokenDetails(data) : undefined;\n if (details === undefined) {\n throw responseInvalid(redact, await revokeUnusedToken(token, revokeOptions));\n }\n const mismatch = findScopeMismatch(details, requested, requestedPermissions);\n if (mismatch !== null) {\n throw scopeMismatch(mismatch, await revokeUnusedToken(token, revokeOptions), redact);\n }\n if (details.expiresAt.getTime() <= Date.now()) {\n throw tokenExpired(await revokeUnusedToken(token, revokeOptions), redact);\n }\n return {\n token,\n expiresAt: details.expiresAt,\n permissions: details.permissions,\n repositories: details.repositories.map(({ id: repositoryId, fullName }) => ({\n id: repositoryId,\n fullName,\n })),\n };\n};\n"]}
@@ -0,0 +1,33 @@
1
+ export type IGithubPermissionLevel = 'read' | 'write' | 'admin';
2
+ export type IGithubPermissions = Record<string, IGithubPermissionLevel>;
3
+ export type IGithubRepository = {
4
+ id: number;
5
+ fullName: string;
6
+ };
7
+ export type IGithubRequestOptions = {
8
+ userAgent: string;
9
+ apiOrigin?: string | undefined;
10
+ apiVersion?: string | undefined;
11
+ timeoutSeconds?: number | undefined;
12
+ signal?: AbortSignal | undefined;
13
+ };
14
+ export type IInstallationTokenRepositories = {
15
+ repositories: readonly string[];
16
+ repositoryIds?: undefined;
17
+ } | {
18
+ repositoryIds: readonly number[];
19
+ repositories?: undefined;
20
+ };
21
+ export type ICreateInstallationTokenOptions = IGithubRequestOptions & IInstallationTokenRepositories & {
22
+ appId: string | number;
23
+ privateKey: string;
24
+ installationId: string | number;
25
+ permissions: IGithubPermissions;
26
+ maxResponseBytes?: number | undefined;
27
+ };
28
+ export type IInstallationToken = {
29
+ token: string;
30
+ expiresAt: Date;
31
+ permissions: Record<string, string>;
32
+ repositories: IGithubRepository[];
33
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/installation-token/types.ts"],"names":[],"mappings":"","sourcesContent":["// the access a GitHub App permission grants\nexport type IGithubPermissionLevel = 'read' | 'write' | 'admin';\n\n// the permissions of an installation token, by GitHub's names, such as { contents: 'read' }\nexport type IGithubPermissions = Record<string, IGithubPermissionLevel>;\n\n// a repository an installation token can reach\nexport type IGithubRepository = {\n // the repository's ID\n id: number;\n\n // the repository's owner and name, such as 'acme/app'\n fullName: string;\n};\n\n// what every request to GitHub's REST API takes\nexport type IGithubRequestOptions = {\n // the User-Agent GitHub requires on every request, such as the app's name\n userAgent: string;\n\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 // the REST API version. Defaults to GITHUB_API_VERSION\n apiVersion?: string | undefined;\n\n // the seconds the request may take. Defaults to 10\n timeoutSeconds?: number | undefined;\n\n // stops the request when it aborts\n signal?: AbortSignal | undefined;\n};\n\n// the repositories an installation token is for: by name or by ID, never both\nexport type IInstallationTokenRepositories =\n | {\n // the repositories' names, without the owner, such as ['app', 'docs']\n repositories: readonly string[];\n repositoryIds?: undefined;\n }\n | {\n // the repositories' IDs\n repositoryIds: readonly number[];\n repositories?: undefined;\n };\n\n// what createInstallationToken takes\nexport type ICreateInstallationTokenOptions = IGithubRequestOptions &\n IInstallationTokenRepositories & {\n // the App's ID, such as 123456, or its client ID\n appId: string | number;\n\n // the App's private key, PEM-encoded\n privateKey: string;\n\n // the installation's ID, such as 12345678\n installationId: string | number;\n\n // the permissions the token needs, at least one; GitHub would grant every permission of the\n // installation without them\n permissions: IGithubPermissions;\n\n // the bytes GitHub's answer may take. Defaults to 128 KiB\n maxResponseBytes?: number | undefined;\n };\n\n// an installation token, scoped as requested\nexport type IInstallationToken = {\n // the token, sent as a bearer credential\n token: string;\n\n // when it expires, an hour after it was created\n expiresAt: Date;\n\n // the permissions it grants: the ones requested, and `metadata: 'read'` when GitHub adds it\n permissions: Record<string, string>;\n\n // the repositories it reaches, exactly the ones requested\n repositories: IGithubRepository[];\n};\n"]}
@@ -0,0 +1,36 @@
1
+ import type { IGithubPermissions, IGithubRepository } from './types.js';
2
+ import type { IRequestedRepositories } from './validations.js';
3
+ export type IGrantedRepository = IGithubRepository & {
4
+ name: string;
5
+ };
6
+ export type ITokenDetails = {
7
+ expiresAt: Date;
8
+ permissions: Record<string, string>;
9
+ repositorySelection: string;
10
+ repositories: IGrantedRepository[];
11
+ };
12
+ export type IScopeMismatch = 'repository_selection' | 'repositories' | 'permissions';
13
+ /**
14
+ * Reads the token from GitHub's answer to a token request.
15
+ * @param data The answer, parsed from JSON.
16
+ * @returns The token, or undefined when there's none.
17
+ */
18
+ export declare const readToken: (data: unknown) => string | undefined;
19
+ /**
20
+ * Reads the rest of GitHub's answer to a token request: when it expires, its permissions, and
21
+ * the repositories it reaches, which an answer for every repository leaves out.
22
+ * @param data The answer, parsed from JSON.
23
+ * @returns The details, or undefined when the answer isn't valid.
24
+ */
25
+ export declare const readTokenDetails: (data: unknown) => ITokenDetails | undefined;
26
+ /**
27
+ * Compares a granted token's scope with the request: it must reach only the selected
28
+ * repositories, exactly the requested ones, by name ignoring case or by ID, as they were
29
+ * requested, and grant exactly the requested permissions, plus `metadata: 'read'` when GitHub
30
+ * adds it.
31
+ * @param details The granted token's details.
32
+ * @param requested The requested repositories.
33
+ * @param permissions The requested permissions.
34
+ * @returns What differs, or null when nothing does.
35
+ */
36
+ export declare const findScopeMismatch: (details: ITokenDetails, requested: IRequestedRepositories, permissions: IGithubPermissions) => IScopeMismatch | null;
@@ -0,0 +1,93 @@
1
+ import { isHeaderValue, isRecord } from '../shared/index.js';
2
+ import { IMPLICIT_PERMISSION } from './constants.js';
3
+ /**
4
+ * Reads the token from GitHub's answer to a token request.
5
+ * @param data The answer, parsed from JSON.
6
+ * @returns The token, or undefined when there's none.
7
+ */
8
+ export const readToken = (data) => isRecord(data) && isHeaderValue(data['token']) && !data['token'].includes(' ')
9
+ ? data['token']
10
+ : undefined;
11
+ /**
12
+ * Reads a granted repository.
13
+ * @param value The repository, parsed from JSON.
14
+ * @returns The repository, or undefined when it isn't one.
15
+ */
16
+ const readRepository = (value) => isRecord(value) &&
17
+ Number.isSafeInteger(value['id']) &&
18
+ value['id'] > 0 &&
19
+ typeof value['name'] === 'string' &&
20
+ typeof value['full_name'] === 'string'
21
+ ? { id: value['id'], name: value['name'], fullName: value['full_name'] }
22
+ : undefined;
23
+ /**
24
+ * Reads the rest of GitHub's answer to a token request: when it expires, its permissions, and
25
+ * the repositories it reaches, which an answer for every repository leaves out.
26
+ * @param data The answer, parsed from JSON.
27
+ * @returns The details, or undefined when the answer isn't valid.
28
+ */
29
+ export const readTokenDetails = (data) => {
30
+ if (!isRecord(data) || typeof data['expires_at'] !== 'string') {
31
+ return undefined;
32
+ }
33
+ const expiresAt = new Date(data['expires_at']);
34
+ const permissions = data['permissions'];
35
+ const repositorySelection = data['repository_selection'];
36
+ const rawRepositories = data['repositories'] ?? [];
37
+ if (Number.isNaN(expiresAt.getTime()) ||
38
+ !isRecord(permissions) ||
39
+ !Object.values(permissions).every((level) => typeof level === 'string') ||
40
+ typeof repositorySelection !== 'string' ||
41
+ !Array.isArray(rawRepositories)) {
42
+ return undefined;
43
+ }
44
+ const repositories = rawRepositories.map(readRepository);
45
+ if (repositories.includes(undefined)) {
46
+ return undefined;
47
+ }
48
+ return {
49
+ expiresAt,
50
+ permissions: permissions,
51
+ repositorySelection,
52
+ repositories: repositories,
53
+ };
54
+ };
55
+ /**
56
+ * Checks whether two lists hold the same values, each once.
57
+ * @param granted The granted values.
58
+ * @param requested The requested values, without repeats.
59
+ * @returns Whether they're the same set.
60
+ */
61
+ const isSameSet = (granted, requested) => {
62
+ const grantedSet = new Set(granted);
63
+ return (grantedSet.size === granted.length &&
64
+ grantedSet.size === requested.length &&
65
+ requested.every((value) => grantedSet.has(value)));
66
+ };
67
+ /**
68
+ * Compares a granted token's scope with the request: it must reach only the selected
69
+ * repositories, exactly the requested ones, by name ignoring case or by ID, as they were
70
+ * requested, and grant exactly the requested permissions, plus `metadata: 'read'` when GitHub
71
+ * adds it.
72
+ * @param details The granted token's details.
73
+ * @param requested The requested repositories.
74
+ * @param permissions The requested permissions.
75
+ * @returns What differs, or null when nothing does.
76
+ */
77
+ export const findScopeMismatch = (details, requested, permissions) => {
78
+ if (details.repositorySelection !== 'selected') {
79
+ return 'repository_selection';
80
+ }
81
+ const isSameRepositories = requested.kind === 'names'
82
+ ? isSameSet(details.repositories.map(({ name }) => name.toLowerCase()), requested.values.map((name) => name.toLowerCase()))
83
+ : isSameSet(details.repositories.map(({ id }) => id), requested.values);
84
+ if (!isSameRepositories) {
85
+ return 'repositories';
86
+ }
87
+ const granted = Object.entries(details.permissions);
88
+ const isSamePermissions = Object.entries(permissions).every(([name, level]) => Object.hasOwn(details.permissions, name) && details.permissions[name] === level) &&
89
+ granted.every(([name, level]) => Object.hasOwn(permissions, name) ||
90
+ (name === IMPLICIT_PERMISSION.name && level === IMPLICIT_PERMISSION.level));
91
+ return isSamePermissions ? null : 'permissions';
92
+ };
93
+ //# sourceMappingURL=utilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utilities.js","sourceRoot":"","sources":["../../src/installation-token/utilities.ts"],"names":[],"mappings":"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","sourcesContent":["import { isHeaderValue, isRecord } from '../shared/index.js';\nimport { IMPLICIT_PERMISSION } from './constants.js';\nimport type { IGithubPermissions, IGithubRepository } from './types.js';\nimport type { IRequestedRepositories } from './validations.js';\n\n// a granted repository, as GitHub describes it\nexport type IGrantedRepository = IGithubRepository & {\n // the repository's name, without its owner\n name: string;\n};\n\n// what GitHub's answer to a token request says, once read\nexport type ITokenDetails = {\n expiresAt: Date;\n permissions: Record<string, string>;\n repositorySelection: string;\n repositories: IGrantedRepository[];\n};\n\n// what in a granted token differs from the request\nexport type IScopeMismatch = 'repository_selection' | 'repositories' | 'permissions';\n\n/**\n * Reads the token from GitHub's answer to a token request.\n * @param data The answer, parsed from JSON.\n * @returns The token, or undefined when there's none.\n */\nexport const readToken = (data: unknown): string | undefined =>\n isRecord(data) && isHeaderValue(data['token']) && !data['token'].includes(' ')\n ? data['token']\n : undefined;\n\n/**\n * Reads a granted repository.\n * @param value The repository, parsed from JSON.\n * @returns The repository, or undefined when it isn't one.\n */\nconst readRepository = (value: unknown): IGrantedRepository | undefined =>\n isRecord(value) &&\n Number.isSafeInteger(value['id']) &&\n (value['id'] as number) > 0 &&\n typeof value['name'] === 'string' &&\n typeof value['full_name'] === 'string'\n ? { id: value['id'] as number, name: value['name'], fullName: value['full_name'] }\n : undefined;\n\n/**\n * Reads the rest of GitHub's answer to a token request: when it expires, its permissions, and\n * the repositories it reaches, which an answer for every repository leaves out.\n * @param data The answer, parsed from JSON.\n * @returns The details, or undefined when the answer isn't valid.\n */\nexport const readTokenDetails = (data: unknown): ITokenDetails | undefined => {\n if (!isRecord(data) || typeof data['expires_at'] !== 'string') {\n return undefined;\n }\n const expiresAt = new Date(data['expires_at']);\n const permissions = data['permissions'];\n const repositorySelection = data['repository_selection'];\n const rawRepositories = data['repositories'] ?? [];\n if (\n Number.isNaN(expiresAt.getTime()) ||\n !isRecord(permissions) ||\n !Object.values(permissions).every((level) => typeof level === 'string') ||\n typeof repositorySelection !== 'string' ||\n !Array.isArray(rawRepositories)\n ) {\n return undefined;\n }\n const repositories = rawRepositories.map(readRepository);\n if (repositories.includes(undefined)) {\n return undefined;\n }\n return {\n expiresAt,\n permissions: permissions as Record<string, string>,\n repositorySelection,\n repositories: repositories as IGrantedRepository[],\n };\n};\n\n/**\n * Checks whether two lists hold the same values, each once.\n * @param granted The granted values.\n * @param requested The requested values, without repeats.\n * @returns Whether they're the same set.\n */\nconst isSameSet = <T>(granted: readonly T[], requested: readonly T[]): boolean => {\n const grantedSet = new Set(granted);\n return (\n grantedSet.size === granted.length &&\n grantedSet.size === requested.length &&\n requested.every((value) => grantedSet.has(value))\n );\n};\n\n/**\n * Compares a granted token's scope with the request: it must reach only the selected\n * repositories, exactly the requested ones, by name ignoring case or by ID, as they were\n * requested, and grant exactly the requested permissions, plus `metadata: 'read'` when GitHub\n * adds it.\n * @param details The granted token's details.\n * @param requested The requested repositories.\n * @param permissions The requested permissions.\n * @returns What differs, or null when nothing does.\n */\nexport const findScopeMismatch = (\n details: ITokenDetails,\n requested: IRequestedRepositories,\n permissions: IGithubPermissions,\n): IScopeMismatch | null => {\n if (details.repositorySelection !== 'selected') {\n return 'repository_selection';\n }\n const isSameRepositories =\n requested.kind === 'names'\n ? isSameSet(\n details.repositories.map(({ name }) => name.toLowerCase()),\n requested.values.map((name) => name.toLowerCase()),\n )\n : isSameSet(\n details.repositories.map(({ id }) => id),\n requested.values,\n );\n if (!isSameRepositories) {\n return 'repositories';\n }\n const granted = Object.entries(details.permissions);\n const isSamePermissions =\n Object.entries(permissions).every(\n ([name, level]) =>\n Object.hasOwn(details.permissions, name) && details.permissions[name] === level,\n ) &&\n granted.every(\n ([name, level]) =>\n Object.hasOwn(permissions, name) ||\n (name === IMPLICIT_PERMISSION.name && level === IMPLICIT_PERMISSION.level),\n );\n return isSamePermissions ? null : 'permissions';\n};\n"]}
@@ -0,0 +1,35 @@
1
+ import type { IGithubPermissions } from './types.js';
2
+ export type IRequestedRepositories = {
3
+ kind: 'names';
4
+ values: string[];
5
+ } | {
6
+ kind: 'ids';
7
+ values: number[];
8
+ };
9
+ /**
10
+ * Reads an installation's ID as the string the URL takes.
11
+ * @param installationId The ID.
12
+ * @returns The ID.
13
+ * @throws
14
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
15
+ */
16
+ export declare const readInstallationId: (installationId: unknown) => string;
17
+ /**
18
+ * Checks the permissions a token is requested with: at least one, each a known level, because
19
+ * GitHub reads no permissions as every permission of the installation.
20
+ * @param permissions The permissions.
21
+ * @returns The permissions.
22
+ * @throws
23
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
24
+ */
25
+ export declare const readPermissions: (permissions: unknown) => IGithubPermissions;
26
+ /**
27
+ * Checks the repositories a token is requested for: by name or by ID, exactly one of them, with
28
+ * at least one repository and no repeats; names are compared ignoring case, as GitHub does.
29
+ * @param repositories The names, or undefined.
30
+ * @param repositoryIds The IDs, or undefined.
31
+ * @returns The repositories.
32
+ * @throws
33
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
34
+ */
35
+ export declare const readRequestedRepositories: (repositories: unknown, repositoryIds: unknown) => IRequestedRepositories;
@@ -0,0 +1,74 @@
1
+ import { invalidArgument, isRecord } from '../shared/index.js';
2
+ import { INSTALLATION_ID_PATTERN, PERMISSION_LEVELS, PERMISSION_NAME_PATTERN, REPOSITORY_NAME_PATTERN, } from './constants.js';
3
+ /**
4
+ * Reads an installation's ID as the string the URL takes.
5
+ * @param installationId The ID.
6
+ * @returns The ID.
7
+ * @throws
8
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
9
+ */
10
+ export const readInstallationId = (installationId) => {
11
+ const value = typeof installationId === 'number' && Number.isSafeInteger(installationId)
12
+ ? String(installationId)
13
+ : installationId;
14
+ if (typeof value !== 'string' || !INSTALLATION_ID_PATTERN.test(value)) {
15
+ throw invalidArgument('installationId');
16
+ }
17
+ return value;
18
+ };
19
+ /**
20
+ * Checks the permissions a token is requested with: at least one, each a known level, because
21
+ * GitHub reads no permissions as every permission of the installation.
22
+ * @param permissions The permissions.
23
+ * @returns The permissions.
24
+ * @throws
25
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
26
+ */
27
+ export const readPermissions = (permissions) => {
28
+ if (!isRecord(permissions) ||
29
+ Object.keys(permissions).length === 0 ||
30
+ !Object.entries(permissions).every(([name, level]) => PERMISSION_NAME_PATTERN.test(name) &&
31
+ PERMISSION_LEVELS.has(level))) {
32
+ throw invalidArgument('permissions');
33
+ }
34
+ return { ...permissions };
35
+ };
36
+ /**
37
+ * Checks that a list has at least one item, each valid, and no two the same once normalized.
38
+ * @param values The list.
39
+ * @param isValid Whether an item is valid.
40
+ * @param normalize What two items are compared by.
41
+ * @returns Whether the list is valid.
42
+ */
43
+ const isUniqueList = (values, isValid, normalize) => Array.isArray(values) &&
44
+ values.length > 0 &&
45
+ values.every(isValid) &&
46
+ new Set(values.map(normalize)).size === values.length;
47
+ /**
48
+ * Checks the repositories a token is requested for: by name or by ID, exactly one of them, with
49
+ * at least one repository and no repeats; names are compared ignoring case, as GitHub does.
50
+ * @param repositories The names, or undefined.
51
+ * @param repositoryIds The IDs, or undefined.
52
+ * @returns The repositories.
53
+ * @throws
54
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
55
+ */
56
+ export const readRequestedRepositories = (repositories, repositoryIds) => {
57
+ if ((repositories === undefined) === (repositoryIds === undefined)) {
58
+ throw invalidArgument('repositories');
59
+ }
60
+ if (repositories !== undefined) {
61
+ if (!isUniqueList(repositories, (name) => typeof name === 'string' &&
62
+ REPOSITORY_NAME_PATTERN.test(name) &&
63
+ name !== '.' &&
64
+ name !== '..', (name) => name.toLowerCase())) {
65
+ throw invalidArgument('repositories');
66
+ }
67
+ return { kind: 'names', values: [...repositories] };
68
+ }
69
+ if (!isUniqueList(repositoryIds, (id) => Number.isSafeInteger(id) && id > 0, (id) => id)) {
70
+ throw invalidArgument('repositoryIds');
71
+ }
72
+ return { kind: 'ids', values: [...repositoryIds] };
73
+ };
74
+ //# sourceMappingURL=validations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validations.js","sourceRoot":"","sources":["../../src/installation-token/validations.ts"],"names":[],"mappings":"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","sourcesContent":["import { invalidArgument, isRecord } from '../shared/index.js';\nimport {\n INSTALLATION_ID_PATTERN,\n PERMISSION_LEVELS,\n PERMISSION_NAME_PATTERN,\n REPOSITORY_NAME_PATTERN,\n} from './constants.js';\nimport type { IGithubPermissionLevel, IGithubPermissions } from './types.js';\n\n// the repositories a token is requested for, checked\nexport type IRequestedRepositories =\n { kind: 'names'; values: string[] } | { kind: 'ids'; values: number[] };\n\n/**\n * Reads an installation's ID as the string the URL takes.\n * @param installationId The ID.\n * @returns The ID.\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n */\nexport const readInstallationId = (installationId: unknown): string => {\n const value =\n typeof installationId === 'number' && Number.isSafeInteger(installationId)\n ? String(installationId)\n : installationId;\n if (typeof value !== 'string' || !INSTALLATION_ID_PATTERN.test(value)) {\n throw invalidArgument('installationId');\n }\n return value;\n};\n\n/**\n * Checks the permissions a token is requested with: at least one, each a known level, because\n * GitHub reads no permissions as every permission of the installation.\n * @param permissions The permissions.\n * @returns The permissions.\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n */\nexport const readPermissions = (permissions: unknown): IGithubPermissions => {\n if (\n !isRecord(permissions) ||\n Object.keys(permissions).length === 0 ||\n !Object.entries(permissions).every(\n ([name, level]) =>\n PERMISSION_NAME_PATTERN.test(name) &&\n PERMISSION_LEVELS.has(level as IGithubPermissionLevel),\n )\n ) {\n throw invalidArgument('permissions');\n }\n return { ...(permissions as IGithubPermissions) };\n};\n\n/**\n * Checks that a list has at least one item, each valid, and no two the same once normalized.\n * @param values The list.\n * @param isValid Whether an item is valid.\n * @param normalize What two items are compared by.\n * @returns Whether the list is valid.\n */\nconst isUniqueList = (\n values: unknown,\n isValid: (value: unknown) => boolean,\n normalize: (value: unknown) => unknown,\n): boolean =>\n Array.isArray(values) &&\n values.length > 0 &&\n values.every(isValid) &&\n new Set(values.map(normalize)).size === values.length;\n\n/**\n * Checks the repositories a token is requested for: by name or by ID, exactly one of them, with\n * at least one repository and no repeats; names are compared ignoring case, as GitHub does.\n * @param repositories The names, or undefined.\n * @param repositoryIds The IDs, or undefined.\n * @returns The repositories.\n * @throws\n * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.\n */\nexport const readRequestedRepositories = (\n repositories: unknown,\n repositoryIds: unknown,\n): IRequestedRepositories => {\n if ((repositories === undefined) === (repositoryIds === undefined)) {\n throw invalidArgument('repositories');\n }\n if (repositories !== undefined) {\n if (\n !isUniqueList(\n repositories,\n (name) =>\n typeof name === 'string' &&\n REPOSITORY_NAME_PATTERN.test(name) &&\n name !== '.' &&\n name !== '..',\n (name) => (name as string).toLowerCase(),\n )\n ) {\n throw invalidArgument('repositories');\n }\n return { kind: 'names', values: [...(repositories as string[])] };\n }\n if (\n !isUniqueList(\n repositoryIds,\n (id) => Number.isSafeInteger(id) && (id as number) > 0,\n (id) => id,\n )\n ) {\n throw invalidArgument('repositoryIds');\n }\n return { kind: 'ids', values: [...(repositoryIds as number[])] };\n};\n"]}
@@ -0,0 +1,2 @@
1
+ export declare const FALLBACK_RETRY_MS = 60000;
2
+ export declare const IMF_FIXDATE_PATTERN: RegExp;
@@ -0,0 +1,5 @@
1
+ // the wait before the first retry when GitHub doesn't say how long, doubled for each later one
2
+ export const FALLBACK_RETRY_MS = 60_000;
3
+ // a Retry-After date, in the strict IMF-fixdate form, such as 'Wed, 21 Oct 2026 07:28:00 GMT'
4
+ export const IMF_FIXDATE_PATTERN = /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/;
5
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/rate-limit/constants.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAExC,8FAA8F;AAC9F,MAAM,CAAC,MAAM,mBAAmB,GAC9B,kEAAkE,CAAC","sourcesContent":["// the wait before the first retry when GitHub doesn't say how long, doubled for each later one\nexport const FALLBACK_RETRY_MS = 60_000;\n\n// a Retry-After date, in the strict IMF-fixdate form, such as 'Wed, 21 Oct 2026 07:28:00 GMT'\nexport const IMF_FIXDATE_PATTERN =\n /^[A-Z][a-z]{2}, \\d{2} [A-Z][a-z]{2} \\d{4} \\d{2}:\\d{2}:\\d{2} GMT$/;\n"]}
@@ -0,0 +1,2 @@
1
+ export type { IGithubRateLimitDeadlineOptions, IGithubResponseHeaders } from './types.js';
2
+ export { getGithubRateLimitDeadline } from './rate-limit.js';
@@ -0,0 +1,3 @@
1
+ // rate limits
2
+ export { getGithubRateLimitDeadline } from './rate-limit.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rate-limit/index.ts"],"names":[],"mappings":"AAGA,cAAc;AACd,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["// types\nexport type { IGithubRateLimitDeadlineOptions, IGithubResponseHeaders } from './types.js';\n\n// rate limits\nexport { getGithubRateLimitDeadline } from './rate-limit.js';\n"]}
@@ -0,0 +1,24 @@
1
+ import type { IGithubRateLimitDeadlineOptions } from './types.js';
2
+ /**
3
+ * Gives the time to retry a GitHub request at after a rate limit, or null when the response isn't
4
+ * rate limited. A 429 is, and so is a 403 with Retry-After, with no requests remaining, or with a
5
+ * rate-limit message, and a success that used the last remaining request. The deadline is the
6
+ * latest of Retry-After, as whole seconds or a strict IMF-fixdate, and `x-ratelimit-reset` when no
7
+ * requests remain, counting only future times; without one, it's a minute from now, doubled for
8
+ * each retry already made.
9
+ * @param options The response's status and headers, the retries made, and the time.
10
+ * @returns The time to retry at, or null.
11
+ * @example
12
+ * import { getGithubRateLimitDeadline } from 'jg-github';
13
+ *
14
+ * declare const response: Response;
15
+ *
16
+ * const deadline = getGithubRateLimitDeadline({
17
+ * status: response.status,
18
+ * headers: response.headers,
19
+ * retryCount: 0,
20
+ * });
21
+ * @throws
22
+ * - GITHUB_INVALID_ARGUMENT: An argument isn't valid.
23
+ */
24
+ export declare const getGithubRateLimitDeadline: ({ status, headers, retryCount, hasRateLimitMessage, now, }: IGithubRateLimitDeadlineOptions) => Date | null;