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
package/API.md ADDED
@@ -0,0 +1,648 @@
1
+ # <a id="api-reference"></a>jg-github API reference
2
+
3
+ Every export of `jg-github@0.2.0`, 30 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.
4
+
5
+ - Functions: [`assertGithubAppPrivateKey`](#api-assertGithubAppPrivateKey), [`buildGithubApiUrl`](#api-buildGithubApiUrl), [`buildGithubRawContentUrl`](#api-buildGithubRawContentUrl), [`createGithubAppJwt`](#api-createGithubAppJwt), [`createGithubHeaders`](#api-createGithubHeaders), [`createInstallationToken`](#api-createInstallationToken), [`getGithubRateLimitDeadline`](#api-getGithubRateLimitDeadline), [`revokeInstallationToken`](#api-revokeInstallationToken), [`verifyGithubWebhookSignature`](#api-verifyGithubWebhookSignature)
6
+ - Constants: [`DEFAULT_GITHUB_MAX_RESPONSE_BYTES`](#api-DEFAULT_GITHUB_MAX_RESPONSE_BYTES), [`DEFAULT_GITHUB_TIMEOUT_SECONDS`](#api-DEFAULT_GITHUB_TIMEOUT_SECONDS), [`ERRORS`](#api-ERRORS), [`GITHUB_API_ORIGIN`](#api-GITHUB_API_ORIGIN), [`GITHUB_API_VERSION`](#api-GITHUB_API_VERSION), [`GITHUB_RAW_CONTENT_ORIGIN`](#api-GITHUB_RAW_CONTENT_ORIGIN)
7
+ - Types: [`ICreateGithubAppJwtOptions`](#api-ICreateGithubAppJwtOptions), [`ICreateInstallationTokenOptions`](#api-ICreateInstallationTokenOptions), [`IGithubApiUrlOptions`](#api-IGithubApiUrlOptions), [`IGithubHeadersOptions`](#api-IGithubHeadersOptions), [`IGithubPermissionLevel`](#api-IGithubPermissionLevel), [`IGithubPermissions`](#api-IGithubPermissions), [`IGithubQuery`](#api-IGithubQuery), [`IGithubRateLimitDeadlineOptions`](#api-IGithubRateLimitDeadlineOptions), [`IGithubRawContentLocation`](#api-IGithubRawContentLocation), [`IGithubRawContentUrlOptions`](#api-IGithubRawContentUrlOptions), [`IGithubRepository`](#api-IGithubRepository), [`IGithubRequestOptions`](#api-IGithubRequestOptions), [`IGithubResponseHeaders`](#api-IGithubResponseHeaders), [`IInstallationToken`](#api-IInstallationToken), [`IInstallationTokenRepositories`](#api-IInstallationTokenRepositories)
8
+
9
+ ## <a id="api-functions"></a>Functions
10
+
11
+ ### <a id="api-assertGithubAppPrivateKey"></a>`assertGithubAppPrivateKey`
12
+
13
+ *Function*
14
+
15
+ Checks that a GitHub App's private key can sign its JWTs: an unencrypted RSA key, PEM-encoded as PKCS#1, the `RSA PRIVATE KEY` GitHub downloads, or PKCS#8, the `PRIVATE KEY` it converts to. Run it at startup, so a bad key fails before the first request. The error never holds the key.
16
+
17
+ ```ts
18
+ assertGithubAppPrivateKey(pem: string): void
19
+ ```
20
+
21
+ **Parameters**
22
+
23
+ - `pem` `string`: The key.
24
+
25
+ **Throws**
26
+
27
+ - `GITHUB_PRIVATE_KEY_INVALID`: The GitHub App's private key isn't an unencrypted RSA private key.
28
+
29
+ **Example**
30
+
31
+ ```ts
32
+ import { assertGithubAppPrivateKey } from 'jg-github';
33
+
34
+ declare const privateKey: string;
35
+
36
+ assertGithubAppPrivateKey(privateKey);
37
+ ```
38
+
39
+ ### <a id="api-buildGithubApiUrl"></a>`buildGithubApiUrl`
40
+
41
+ *Function*
42
+
43
+ Builds a REST API URL from its path segments, each encoded on its own, so a branch's `/` becomes `%2F` and no value can climb out of the path, plus the query parameters.
44
+
45
+ ```ts
46
+ buildGithubApiUrl(
47
+ segments: readonly (string | number)[],
48
+ query?: IGithubQuery,
49
+ options?: IGithubApiUrlOptions,
50
+ ): string
51
+ ```
52
+
53
+ **Parameters**
54
+
55
+ - `segments` `readonly (string | number)[]`: The path's segments, such as `['repos', owner, repository, 'branches', branch]`.
56
+ - `query?` `IGithubQuery`: The query parameters; undefined ones are left out. Defaults to `{}`.
57
+ - `options?` `IGithubApiUrlOptions`: The API's base URL, for GitHub Enterprise Server or a test server. Defaults to `{}`.
58
+
59
+ **Returns** `string`: The URL.
60
+
61
+ **Throws**
62
+
63
+ - `GITHUB_INVALID_ARGUMENT`: An argument isn't valid.
64
+
65
+ **Example**
66
+
67
+ ```ts
68
+ import { buildGithubApiUrl } from 'jg-github';
69
+
70
+ buildGithubApiUrl(['repos', 'acme', 'app', 'branches', 'feature/login'], { per_page: 100 });
71
+ // 'https://api.github.com/repos/acme/app/branches/feature%2Flogin?per_page=100'
72
+ ```
73
+
74
+ ### <a id="api-buildGithubRawContentUrl"></a>`buildGithubRawContentUrl`
75
+
76
+ *Function*
77
+
78
+ Builds the URL of a file's raw content at a ref, with the owner and the repository encoded as one segment each, and the ref's and the path's `/`s kept.
79
+
80
+ ```ts
81
+ buildGithubRawContentUrl(
82
+ location: IGithubRawContentLocation,
83
+ options?: IGithubRawContentUrlOptions,
84
+ ): string
85
+ ```
86
+
87
+ **Parameters**
88
+
89
+ - `location` `IGithubRawContentLocation`: The repository, the ref, and the file's path.
90
+ - `options?` `IGithubRawContentUrlOptions`: The raw content's base URL, for a test server. Defaults to `{}`.
91
+
92
+ **Returns** `string`: The URL.
93
+
94
+ **Throws**
95
+
96
+ - `GITHUB_INVALID_ARGUMENT`: An argument isn't valid.
97
+
98
+ **Example**
99
+
100
+ ```ts
101
+ import { buildGithubRawContentUrl } from 'jg-github';
102
+
103
+ buildGithubRawContentUrl({ owner: 'acme', repository: 'app', ref: 'main', path: 'docs/setup.md' });
104
+ // 'https://raw.githubusercontent.com/acme/app/main/docs/setup.md'
105
+ ```
106
+
107
+ ### <a id="api-createGithubAppJwt"></a>`createGithubAppJwt`
108
+
109
+ *Function*
110
+
111
+ Creates the JWT a GitHub App authenticates as itself with, signed with RS256: issued 60 seconds back, for a GitHub clock behind this one, and expiring 540 seconds ahead, inside GitHub's ten-minute limit even with this clock a minute ahead.
112
+
113
+ ```ts
114
+ createGithubAppJwt(options: ICreateGithubAppJwtOptions): string
115
+ ```
116
+
117
+ **Parameters**
118
+
119
+ - `options` `ICreateGithubAppJwtOptions`: The App's ID, its private key, and the time.
120
+
121
+ **Returns** `string`: The JWT.
122
+
123
+ **Throws**
124
+
125
+ - `GITHUB_INVALID_ARGUMENT`: An argument isn't valid.
126
+ - `GITHUB_PRIVATE_KEY_INVALID`: The GitHub App's private key isn't an unencrypted RSA private key.
127
+
128
+ **Example**
129
+
130
+ ```ts
131
+ import { createGithubAppJwt } from 'jg-github';
132
+
133
+ declare const privateKey: string;
134
+
135
+ const jwt = createGithubAppJwt({ appId: 123456, privateKey });
136
+ ```
137
+
138
+ ### <a id="api-createGithubHeaders"></a>`createGithubHeaders`
139
+
140
+ *Function*
141
+
142
+ Builds the headers a request to GitHub's REST API sends: the media type, the token as a bearer credential, the User-Agent GitHub requires, and the API version.
143
+
144
+ ```ts
145
+ createGithubHeaders(
146
+ token: string,
147
+ options: IGithubHeadersOptions,
148
+ ): Record<string, string>
149
+ ```
150
+
151
+ **Parameters**
152
+
153
+ - `token` `string`: The token, such as an installation token or an App JWT.
154
+ - `options` `IGithubHeadersOptions`: The User-Agent and the API version.
155
+
156
+ **Returns** `Record<string, string>`: The headers, with lowercase names.
157
+
158
+ **Throws**
159
+
160
+ - `GITHUB_INVALID_ARGUMENT`: An argument isn't valid.
161
+
162
+ **Example**
163
+
164
+ ```ts
165
+ import { createGithubHeaders } from 'jg-github';
166
+
167
+ declare const token: string;
168
+
169
+ const response = await fetch('https://api.github.com/installation/repositories', {
170
+ headers: createGithubHeaders(token, { userAgent: 'acme-deploy-bot' }),
171
+ });
172
+ ```
173
+
174
+ ### <a id="api-createInstallationToken"></a>`createInstallationToken`
175
+
176
+ *Function*
177
+
178
+ Creates an installation token for exactly the repositories and the permissions asked for, with `POST /app/installations/{id}/access_tokens`, authenticated as the App. `permissions` is required, because GitHub grants every permission of the installation without it, and the repositories are named by name or by ID, never both. The granted token must reach exactly those repositories, only the selected ones, with exactly those permissions, plus `metadata: 'read'`, and must not have expired; otherwise it's revoked before the error is thrown, and the error's data says whether that worked. Nothing is retried, and no error holds the token or the App's JWT.
179
+
180
+ ```ts
181
+ createInstallationToken(
182
+ options: ICreateInstallationTokenOptions,
183
+ ): Promise<IInstallationToken>
184
+ ```
185
+
186
+ **Parameters**
187
+
188
+ - `options` `ICreateInstallationTokenOptions`: The App, the installation, the scope, and the request's limits.
189
+
190
+ **Returns** `Promise<IInstallationToken>`: A promise that resolves to the token, when it expires, and what it reaches.
191
+
192
+ **Throws**
193
+
194
+ - `GITHUB_INVALID_ARGUMENT`: An argument isn't valid.
195
+ - `GITHUB_PRIVATE_KEY_INVALID`: The GitHub App's private key isn't an unencrypted RSA private key.
196
+ - `GITHUB_REQUEST_FAILED`: The GitHub request failed.
197
+ - `GITHUB_RESPONSE_INVALID`: GitHub's response isn't in the expected format.
198
+ - `GITHUB_TOKEN_SCOPE_MISMATCH`: The installation token's scope isn't the one requested.
199
+ - `GITHUB_TOKEN_EXPIRED`: The installation token had already expired.
200
+
201
+ **Example**
202
+
203
+ ```ts
204
+ import { createInstallationToken, revokeInstallationToken } from 'jg-github';
205
+
206
+ declare const privateKey: string;
207
+
208
+ const { token } = await createInstallationToken({
209
+ appId: 123456,
210
+ privateKey,
211
+ installationId: 12345678,
212
+ repositories: ['app'],
213
+ permissions: { contents: 'read' },
214
+ userAgent: 'acme-deploy-bot',
215
+ });
216
+ try {
217
+ // read the repository's contents with the token
218
+ } finally {
219
+ await revokeInstallationToken(token, { userAgent: 'acme-deploy-bot' });
220
+ }
221
+ ```
222
+
223
+ ### <a id="api-getGithubRateLimitDeadline"></a>`getGithubRateLimitDeadline`
224
+
225
+ *Function*
226
+
227
+ Gives the time to retry a GitHub request at after a rate limit, or null when the response isn't rate limited. A 429 is, and so is a 403 with Retry-After, with no requests remaining, or with a rate-limit message, and a success that used the last remaining request. The deadline is the latest of Retry-After, as whole seconds or a strict IMF-fixdate, and `x-ratelimit-reset` when no requests remain, counting only future times; without one, it's a minute from now, doubled for each retry already made.
228
+
229
+ ```ts
230
+ getGithubRateLimitDeadline(
231
+ options: IGithubRateLimitDeadlineOptions,
232
+ ): Date | null
233
+ ```
234
+
235
+ **Parameters**
236
+
237
+ - `options` `IGithubRateLimitDeadlineOptions`: The response's status and headers, the retries made, and the time.
238
+
239
+ **Returns** `Date | null`: The time to retry at, or null.
240
+
241
+ **Throws**
242
+
243
+ - `GITHUB_INVALID_ARGUMENT`: An argument isn't valid.
244
+
245
+ **Example**
246
+
247
+ ```ts
248
+ import { getGithubRateLimitDeadline } from 'jg-github';
249
+
250
+ declare const response: Response;
251
+
252
+ const deadline = getGithubRateLimitDeadline({
253
+ status: response.status,
254
+ headers: response.headers,
255
+ retryCount: 0,
256
+ });
257
+ ```
258
+
259
+ ### <a id="api-revokeInstallationToken"></a>`revokeInstallationToken`
260
+
261
+ *Function*
262
+
263
+ Revokes an installation token before it expires, such as when the work it was for is done, with `DELETE /installation/token`, which GitHub answers with 204. It never retries. The error never holds the token.
264
+
265
+ ```ts
266
+ revokeInstallationToken(
267
+ token: string,
268
+ options: IGithubRequestOptions,
269
+ ): Promise<void>
270
+ ```
271
+
272
+ **Parameters**
273
+
274
+ - `token` `string`: The installation token.
275
+ - `options` `IGithubRequestOptions`: The User-Agent, the API's base URL and version, and the limits.
276
+
277
+ **Returns** `Promise<void>`: A promise that resolves once GitHub has revoked it.
278
+
279
+ **Throws**
280
+
281
+ - `GITHUB_INVALID_ARGUMENT`: An argument isn't valid.
282
+ - `GITHUB_TOKEN_REVOKE_FAILED`: The installation token couldn't be revoked.
283
+
284
+ **Example**
285
+
286
+ ```ts
287
+ import { revokeInstallationToken } from 'jg-github';
288
+
289
+ declare const token: string;
290
+
291
+ await revokeInstallationToken(token, { userAgent: 'acme-deploy-bot' });
292
+ ```
293
+
294
+ ### <a id="api-verifyGithubWebhookSignature"></a>`verifyGithubWebhookSignature`
295
+
296
+ *Function*
297
+
298
+ Verifies a GitHub webhook delivery's X-Hub-Signature-256 header against the body's exact bytes, in constant time. A header that's missing, repeated, longer than 71 characters, or not `sha256=` and 64 lowercase hexadecimal characters is false, like a wrong signature. Verify the raw body before parsing it, because parsing and serializing can change its bytes.
299
+
300
+ ```ts
301
+ verifyGithubWebhookSignature(
302
+ rawBody: string | Uint8Array<ArrayBufferLike>,
303
+ header: unknown,
304
+ secret: string | Uint8Array<ArrayBufferLike>,
305
+ ): boolean
306
+ ```
307
+
308
+ **Parameters**
309
+
310
+ - `rawBody` `string | Uint8Array<ArrayBufferLike>`: The request's body, as received.
311
+ - `header` `unknown`: The X-Hub-Signature-256 header, as the framework gives it.
312
+ - `secret` `string | Uint8Array<ArrayBufferLike>`: The webhook's secret.
313
+
314
+ **Returns** `boolean`: Whether the signature is the body's.
315
+
316
+ **Throws**
317
+
318
+ - `INVALID_HMAC_SECRET`: The HMAC secret must be a non-empty string or byte array.
319
+
320
+ **Example**
321
+
322
+ ```ts
323
+ import { verifyGithubWebhookSignature } from 'jg-github';
324
+
325
+ declare const rawBody: Buffer;
326
+ declare const headers: Record<string, string | string[] | undefined>;
327
+ declare const webhookSecret: string;
328
+
329
+ if (!verifyGithubWebhookSignature(rawBody, headers['x-hub-signature-256'], webhookSecret)) {
330
+ // answer 401
331
+ }
332
+ ```
333
+
334
+ ## <a id="api-constants"></a>Constants
335
+
336
+ ### <a id="api-DEFAULT_GITHUB_MAX_RESPONSE_BYTES"></a>`DEFAULT_GITHUB_MAX_RESPONSE_BYTES`
337
+
338
+ *Constant*
339
+
340
+ the bytes a response from GitHub may take when the options don't say (128 KiB)
341
+
342
+ ```ts
343
+ const DEFAULT_GITHUB_MAX_RESPONSE_BYTES: 131072
344
+ ```
345
+
346
+ ### <a id="api-DEFAULT_GITHUB_TIMEOUT_SECONDS"></a>`DEFAULT_GITHUB_TIMEOUT_SECONDS`
347
+
348
+ *Constant*
349
+
350
+ the seconds a request to GitHub may take when the options don't say
351
+
352
+ ```ts
353
+ const DEFAULT_GITHUB_TIMEOUT_SECONDS: 10
354
+ ```
355
+
356
+ ### <a id="api-ERRORS"></a>`ERRORS`
357
+
358
+ *Constant*
359
+
360
+ the codes for every error thrown by the package
361
+
362
+ ```ts
363
+ const ERRORS: { GITHUB_PRIVATE_KEY_INVALID: "GITHUB_PRIVATE_KEY_INVALID"; GITHUB_INVALID_ARGUMENT: "GITHUB_INVALID_ARGUMENT"; GITHUB_REQUEST_FAILED: "GITHUB_REQUEST_FAILED"; GITHUB_RESPONSE_INVALID: "GITHUB_RESPONSE_INVALID"; GITHUB_TOKEN_SCOPE_MISMATCH: "GITHUB_TOKEN_SCOPE_MISMATCH"; GITHUB_TOKEN_EXPIRED: "GITHUB_TOKEN_EXPIRED"; GITHUB_TOKEN_REVOKE_FAILED: "GITHUB_TOKEN_REVOKE_FAILED" }
364
+ ```
365
+
366
+ ### <a id="api-GITHUB_API_ORIGIN"></a>`GITHUB_API_ORIGIN`
367
+
368
+ *Constant*
369
+
370
+ the origin of GitHub's REST API on github.com
371
+
372
+ ```ts
373
+ const GITHUB_API_ORIGIN: "https://api.github.com"
374
+ ```
375
+
376
+ ### <a id="api-GITHUB_API_VERSION"></a>`GITHUB_API_VERSION`
377
+
378
+ *Constant*
379
+
380
+ the REST API version every request asks for, in the X-GitHub-Api-Version header
381
+
382
+ ```ts
383
+ const GITHUB_API_VERSION: "2026-03-10"
384
+ ```
385
+
386
+ ### <a id="api-GITHUB_RAW_CONTENT_ORIGIN"></a>`GITHUB_RAW_CONTENT_ORIGIN`
387
+
388
+ *Constant*
389
+
390
+ the origin GitHub serves a repository's raw files from
391
+
392
+ ```ts
393
+ const GITHUB_RAW_CONTENT_ORIGIN: "https://raw.githubusercontent.com"
394
+ ```
395
+
396
+ ## <a id="api-types"></a>Types
397
+
398
+ ### <a id="api-ICreateGithubAppJwtOptions"></a>`ICreateGithubAppJwtOptions`
399
+
400
+ *Type*
401
+
402
+ what createGithubAppJwt takes
403
+
404
+ ```ts
405
+ type ICreateGithubAppJwtOptions = {
406
+ appId: string | number;
407
+ privateKey: string;
408
+ now?: Date | undefined;
409
+ };
410
+ ```
411
+
412
+ **Members**
413
+
414
+ - `appId: string | number` (property): the App's ID, such as 123456, or its client ID
415
+ - `privateKey: string` (property): the App's private key, PEM-encoded, as GitHub downloads it
416
+ - `now: Date` (property): the time the JWT is issued at. Defaults to now
417
+
418
+ ### <a id="api-ICreateInstallationTokenOptions"></a>`ICreateInstallationTokenOptions`
419
+
420
+ *Type*
421
+
422
+ what createInstallationToken takes
423
+
424
+ ```ts
425
+ type ICreateInstallationTokenOptions = IGithubRequestOptions & IInstallationTokenRepositories & {
426
+ appId: string | number;
427
+ privateKey: string;
428
+ installationId: string | number;
429
+ permissions: IGithubPermissions;
430
+ maxResponseBytes?: number | undefined;
431
+ };
432
+ ```
433
+
434
+ ### <a id="api-IGithubApiUrlOptions"></a>`IGithubApiUrlOptions`
435
+
436
+ *Type*
437
+
438
+ what buildGithubApiUrl takes
439
+
440
+ ```ts
441
+ type IGithubApiUrlOptions = {
442
+ apiOrigin?: string | undefined;
443
+ };
444
+ ```
445
+
446
+ **Members**
447
+
448
+ - `apiOrigin: string` (property): the API's base URL, such as `https://ghe.example.com/api/v3` for GitHub Enterprise Server. Defaults to GITHUB_API_ORIGIN
449
+
450
+ ### <a id="api-IGithubHeadersOptions"></a>`IGithubHeadersOptions`
451
+
452
+ *Type*
453
+
454
+ what createGithubHeaders takes
455
+
456
+ ```ts
457
+ type IGithubHeadersOptions = {
458
+ userAgent: string;
459
+ apiVersion?: string | undefined;
460
+ };
461
+ ```
462
+
463
+ **Members**
464
+
465
+ - `userAgent: string` (property): the User-Agent GitHub requires on every request, such as the app's name
466
+ - `apiVersion: string` (property): the REST API version. Defaults to GITHUB_API_VERSION
467
+
468
+ ### <a id="api-IGithubPermissionLevel"></a>`IGithubPermissionLevel`
469
+
470
+ *Type*
471
+
472
+ the access a GitHub App permission grants
473
+
474
+ ```ts
475
+ type IGithubPermissionLevel = 'read' | 'write' | 'admin';
476
+ ```
477
+
478
+ ### <a id="api-IGithubPermissions"></a>`IGithubPermissions`
479
+
480
+ *Type*
481
+
482
+ the permissions of an installation token, by GitHub's names, such as { contents: 'read' }
483
+
484
+ ```ts
485
+ type IGithubPermissions = Record<string, IGithubPermissionLevel>;
486
+ ```
487
+
488
+ ### <a id="api-IGithubQuery"></a>`IGithubQuery`
489
+
490
+ *Type*
491
+
492
+ the query parameters a URL builder appends; undefined ones are left out
493
+
494
+ ```ts
495
+ type IGithubQuery = Record<string, string | number | boolean | undefined>;
496
+ ```
497
+
498
+ ### <a id="api-IGithubRateLimitDeadlineOptions"></a>`IGithubRateLimitDeadlineOptions`
499
+
500
+ *Type*
501
+
502
+ what getGithubRateLimitDeadline takes
503
+
504
+ ```ts
505
+ type IGithubRateLimitDeadlineOptions = {
506
+ status: number;
507
+ headers: IGithubResponseHeaders;
508
+ retryCount: number;
509
+ hasRateLimitMessage?: boolean | undefined;
510
+ now?: Date | undefined;
511
+ };
512
+ ```
513
+
514
+ **Members**
515
+
516
+ - `status: number` (property): the response's HTTP status
517
+ - `headers: IGithubResponseHeaders` (property): the response's headers
518
+ - `retryCount: number` (property): the retries already made for this request, from 0, which doubles the fallback wait
519
+ - `hasRateLimitMessage: boolean` (property): whether the response's message says a rate limit was hit, which makes a bare 403 count. Defaults to false
520
+ - `now: Date` (property): the current time. Defaults to now
521
+
522
+ ### <a id="api-IGithubRawContentLocation"></a>`IGithubRawContentLocation`
523
+
524
+ *Type*
525
+
526
+ a file in a repository, at a ref
527
+
528
+ ```ts
529
+ type IGithubRawContentLocation = {
530
+ owner: string;
531
+ repository: string;
532
+ ref: string;
533
+ path: string;
534
+ };
535
+ ```
536
+
537
+ **Members**
538
+
539
+ - `owner: string` (property): the account that owns the repository
540
+ - `repository: string` (property): the repository's name
541
+ - `ref: string` (property): the branch, tag, or commit, such as `main` or `feature/login`
542
+ - `path: string` (property): the file's path from the repository's root, such as `docs/setup.md`
543
+
544
+ ### <a id="api-IGithubRawContentUrlOptions"></a>`IGithubRawContentUrlOptions`
545
+
546
+ *Type*
547
+
548
+ what buildGithubRawContentUrl takes
549
+
550
+ ```ts
551
+ type IGithubRawContentUrlOptions = {
552
+ rawContentOrigin?: string | undefined;
553
+ };
554
+ ```
555
+
556
+ **Members**
557
+
558
+ - `rawContentOrigin: string` (property): the raw content's base URL. Defaults to GITHUB_RAW_CONTENT_ORIGIN
559
+
560
+ ### <a id="api-IGithubRepository"></a>`IGithubRepository`
561
+
562
+ *Type*
563
+
564
+ a repository an installation token can reach
565
+
566
+ ```ts
567
+ type IGithubRepository = {
568
+ id: number;
569
+ fullName: string;
570
+ };
571
+ ```
572
+
573
+ **Members**
574
+
575
+ - `id: number` (property): the repository's ID
576
+ - `fullName: string` (property): the repository's owner and name, such as 'acme/app'
577
+
578
+ ### <a id="api-IGithubRequestOptions"></a>`IGithubRequestOptions`
579
+
580
+ *Type*
581
+
582
+ what every request to GitHub's REST API takes
583
+
584
+ ```ts
585
+ type IGithubRequestOptions = {
586
+ userAgent: string;
587
+ apiOrigin?: string | undefined;
588
+ apiVersion?: string | undefined;
589
+ timeoutSeconds?: number | undefined;
590
+ signal?: AbortSignal | undefined;
591
+ };
592
+ ```
593
+
594
+ **Members**
595
+
596
+ - `userAgent: string` (property): the User-Agent GitHub requires on every request, such as the app's name
597
+ - `apiOrigin: string` (property): the API's base URL, such as `https://ghe.example.com/api/v3` for GitHub Enterprise Server. Defaults to GITHUB_API_ORIGIN
598
+ - `apiVersion: string` (property): the REST API version. Defaults to GITHUB_API_VERSION
599
+ - `timeoutSeconds: number` (property): the seconds the request may take. Defaults to 10
600
+ - `signal: AbortSignal` (property): stops the request when it aborts
601
+
602
+ ### <a id="api-IGithubResponseHeaders"></a>`IGithubResponseHeaders`
603
+
604
+ *Type*
605
+
606
+ a response's headers: a Headers object, or a record with lowercase names, as Node.js's IncomingHttpHeaders has
607
+
608
+ ```ts
609
+ type IGithubResponseHeaders = Headers | Readonly<Record<string, string | readonly string[] | undefined>>;
610
+ ```
611
+
612
+ ### <a id="api-IInstallationToken"></a>`IInstallationToken`
613
+
614
+ *Type*
615
+
616
+ an installation token, scoped as requested
617
+
618
+ ```ts
619
+ type IInstallationToken = {
620
+ token: string;
621
+ expiresAt: Date;
622
+ permissions: Record<string, string>;
623
+ repositories: IGithubRepository[];
624
+ };
625
+ ```
626
+
627
+ **Members**
628
+
629
+ - `token: string` (property): the token, sent as a bearer credential
630
+ - `expiresAt: Date` (property): when it expires, an hour after it was created
631
+ - `permissions: Record<string, string>` (property): the permissions it grants: the ones requested, and `metadata: 'read'` when GitHub adds it
632
+ - `repositories: IGithubRepository[]` (property): the repositories it reaches, exactly the ones requested
633
+
634
+ ### <a id="api-IInstallationTokenRepositories"></a>`IInstallationTokenRepositories`
635
+
636
+ *Type*
637
+
638
+ the repositories an installation token is for: by name or by ID, never both
639
+
640
+ ```ts
641
+ type IInstallationTokenRepositories = {
642
+ repositories: readonly string[];
643
+ repositoryIds?: undefined;
644
+ } | {
645
+ repositoryIds: readonly number[];
646
+ repositories?: undefined;
647
+ };
648
+ ```
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Changed
6
+
7
+ - Supports Node.js 22.17.0 and later, down from 24.
8
+
9
+ ## 0.1.0
10
+
11
+ ### Added
12
+
13
+ - `createInstallationToken(options)`, an installation token for exactly the repositories, by name or by ID, and the permissions asked for. `permissions` is required and non-empty, and a granted token whose repositories, permissions, or repository selection differ, or that has expired, is revoked before `GITHUB_TOKEN_SCOPE_MISMATCH` or `GITHUB_TOKEN_EXPIRED` is thrown. `revokeInstallationToken(token, options)` revokes one, expecting 204.
14
+ - `createGithubAppJwt({ appId, privateKey, now })`, the App's RS256 JWT, issued 60 seconds back and expiring 540 seconds ahead, and `assertGithubAppPrivateKey(pem)`, which accepts an unencrypted RSA key as PKCS#1 or PKCS#8.
15
+ - `verifyGithubWebhookSignature(rawBody, header, secret)`, which checks `X-Hub-Signature-256` in constant time through jg-crypto, refusing a malformed or repeated header.
16
+ - `getGithubRateLimitDeadline(options)`, when to retry a rate limited request, from `Retry-After` and `x-ratelimit-reset`, with an exponential fallback.
17
+ - `createGithubHeaders`, `buildGithubApiUrl`, and `buildGithubRawContentUrl`, with `GITHUB_API_ORIGIN`, `GITHUB_RAW_CONTENT_ORIGIN`, and `GITHUB_API_VERSION`.
18
+ - Requests through jg-fetch with no redirects, no retries, a 10-second timeout, and a 128 KiB response limit, whose errors never hold a token or a response body.
19
+ - Recipes for a scoped installation token revoked when done, and a webhook route.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jesus Graterol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.