github-app-auth-kit 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emeka Orji
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.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # github-app-auth-kit
2
+
3
+ A minimal, dependency-free TypeScript toolkit for GitHub App authentication.
4
+
5
+ Use it to:
6
+
7
+ - mint short-lived GitHub App JWTs
8
+ - resolve installation IDs from `owner/repo`
9
+ - create installation access tokens
10
+ - work in Node, edge runtimes, and GitHub Enterprise
11
+
12
+ ## Why this library
13
+
14
+ - Small surface area with strong types
15
+ - Works in three styles: one-shot, client instance, or low-level calls
16
+ - Runtime-agnostic (bring your own `fetch` if needed)
17
+ - Designed for clear errors and predictable behavior
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pnpm add github-app-auth-kit
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ ```ts
28
+ import { GitHubAppAuth } from 'github-app-auth-kit';
29
+
30
+ const auth = new GitHubAppAuth({
31
+ appId: process.env.GITHUB_APP_ID!,
32
+ privateKey: process.env.GITHUB_PRIVATE_KEY!,
33
+ });
34
+
35
+ const jwt = auth.createJwt();
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### Client instance (recommended for multiple calls)
41
+
42
+ ```ts
43
+ import { GitHubAppAuth } from 'github-app-auth-kit';
44
+
45
+ const auth = new GitHubAppAuth({
46
+ appId: process.env.GITHUB_APP_ID!,
47
+ privateKey: process.env.GITHUB_PRIVATE_KEY!,
48
+ owner: 'acme',
49
+ repo: 'platform',
50
+ });
51
+
52
+ const installationId = await auth.resolveInstallationId();
53
+ const token = await auth.createAccessToken();
54
+ ```
55
+
56
+ ### Resolve installation id for any repo
57
+
58
+ ```ts
59
+ import { GitHubAppAuth } from 'github-app-auth-kit';
60
+
61
+ const auth = new GitHubAppAuth({
62
+ appId: process.env.GITHUB_APP_ID!,
63
+ privateKey: process.env.GITHUB_PRIVATE_KEY!,
64
+ });
65
+
66
+ const installationId = await auth.resolveInstallationId({
67
+ owner: 'acme',
68
+ repo: 'platform',
69
+ });
70
+ ```
71
+
72
+ ### Create access token with a known installation id
73
+
74
+ ```ts
75
+ import { GitHubAppAuth } from 'github-app-auth-kit';
76
+
77
+ const auth = new GitHubAppAuth({
78
+ appId: process.env.GITHUB_APP_ID!,
79
+ privateKey: process.env.GITHUB_PRIVATE_KEY!,
80
+ installationId: 12345678,
81
+ });
82
+
83
+ const token = await auth.createAccessToken();
84
+ ```
85
+
86
+ ### One-shot access token
87
+
88
+ ```ts
89
+ import { GitHubAppAuth } from 'github-app-auth-kit';
90
+
91
+ const token = await GitHubAppAuth.createAccessToken({
92
+ appId: process.env.GITHUB_APP_ID!,
93
+ privateKey: process.env.GITHUB_PRIVATE_KEY!,
94
+ installationId: 12345678,
95
+ permissions: {
96
+ contents: 'read',
97
+ },
98
+ });
99
+ ```
100
+
101
+ ### GitHub Enterprise
102
+
103
+ ```ts
104
+ import { GitHubAppAuth } from 'github-app-auth-kit';
105
+
106
+ const auth = new GitHubAppAuth({
107
+ appId: process.env.GITHUB_APP_ID!,
108
+ privateKey: process.env.GITHUB_PRIVATE_KEY!,
109
+ apiBaseUrl: 'https://github.example.com/api/v3',
110
+ installationId: 12345678,
111
+ });
112
+
113
+ const token = await auth.createAccessToken();
114
+ ```
115
+
116
+ ### Custom fetch (edge runtimes or Node <18)
117
+
118
+ ```ts
119
+ import { GitHubAppAuth } from 'github-app-auth-kit';
120
+ import { fetch as undiciFetch } from 'undici';
121
+
122
+ const auth = new GitHubAppAuth({
123
+ appId: process.env.GITHUB_APP_ID!,
124
+ privateKey: process.env.GITHUB_PRIVATE_KEY!,
125
+ installationId: 12345678,
126
+ fetch: undiciFetch,
127
+ });
128
+
129
+ const token = await auth.createAccessToken();
130
+ ```
131
+
132
+ ## API
133
+
134
+ ### `new GitHubAppAuth(options)`
135
+
136
+ | Option | Type | Required | Description |
137
+ | ---------------- | ------------------ | -------- | ------------------------------------------------- |
138
+ | `appId` | `number \| string` | Yes | GitHub App id. |
139
+ | `privateKey` | `string` | Yes | PEM private key (supports `\n` escaped newlines). |
140
+ | `owner` | `string` | No | Default repository owner. |
141
+ | `repo` | `string` | No | Default repository name. |
142
+ | `installationId` | `number \| string` | No | Default installation id for tokens. |
143
+ | `apiBaseUrl` | `string` | No | Override REST API base URL (GitHub Enterprise). |
144
+ | `fetch` | `typeof fetch` | No | Provide `fetch` for non-standard runtimes. |
145
+
146
+ ### `auth.createJwt()`
147
+
148
+ Returns a short-lived GitHub App JWT. JWTs are valid for 10 minutes maximum; this library issues 9-minute tokens with a 60-second clock skew.
149
+
150
+ ### `auth.resolveInstallationId({ owner, repo })`
151
+
152
+ Resolves the installation id for the given repository. Uses the default `owner`/`repo` if set on the constructor.
153
+
154
+ ### `auth.createAccessToken(options)`
155
+
156
+ Creates an installation access token. You can provide:
157
+
158
+ - `installationId` to skip repository lookup
159
+ - `owner` and `repo` to look up installation id on demand
160
+ - optional `permissions`, `repositories`, and `repositoryIds` for fine-grained tokens
161
+
162
+ ### `GitHubAppAuth.createAccessToken(options)`
163
+
164
+ Convenience one-call helper that creates a client and returns an access token.
165
+
166
+ ## Error handling
167
+
168
+ All network errors and non-2xx GitHub API responses throw descriptive errors that include the HTTP status and response body.
169
+
170
+ ## Security notes
171
+
172
+ - Treat `privateKey`, JWTs, and access tokens as secrets.
173
+ - Do not log tokens or private keys.
174
+ - Issue JWTs only when needed; they expire quickly.
175
+
176
+ ## Runtime support
177
+
178
+ - Node.js 18+ (native `fetch`)
179
+ - Other runtimes: pass a `fetch` implementation in the constructor
180
+
181
+ ## Troubleshooting
182
+
183
+ - If you see "`fetch` is not available in this runtime", pass a `fetch` implementation.
184
+ - If you see "Missing repository target", provide both `owner` and `repo`, or use `installationId`.
185
+
186
+ ## Related links
187
+
188
+ - [GitHub Apps documentation](https://docs.github.com/en/apps/creating-github-apps)
189
+ - [GitHub REST API](https://docs.github.com/en/rest)
190
+
191
+ ## License
192
+
193
+ MIT
@@ -0,0 +1,42 @@
1
+ type FetchLike = typeof fetch;
2
+ type PermissionLevel = 'read' | 'write';
3
+ type InstallationTokenPermissions = Record<string, PermissionLevel>;
4
+ type GitHubRepoRef = {
5
+ owner: string;
6
+ repo: string;
7
+ };
8
+ type ResolveInstallationIdOptions = Partial<GitHubRepoRef>;
9
+ type CreateAccessTokenOptions = ResolveInstallationIdOptions & {
10
+ installationId?: number | undefined;
11
+ permissions?: InstallationTokenPermissions | undefined;
12
+ repositories?: string[] | undefined;
13
+ repositoryIds?: number[] | undefined;
14
+ };
15
+ type GitHubAppAuthOptions = {
16
+ appId: number | string;
17
+ privateKey?: string | undefined;
18
+ privateKeyRaw?: string | undefined;
19
+ owner?: string | undefined;
20
+ repo?: string | undefined;
21
+ installationId?: number | undefined;
22
+ apiBaseUrl?: string | undefined;
23
+ fetch?: FetchLike | undefined;
24
+ };
25
+ declare function generateGitHubAppJwt(appId: number | string, privateKeyRaw: string): string;
26
+ declare class GitHubAppAuth {
27
+ private readonly appId;
28
+ private readonly privateKey;
29
+ private readonly owner;
30
+ private readonly repo;
31
+ private readonly installationId;
32
+ private readonly apiBaseUrl;
33
+ private readonly fetchImpl;
34
+ constructor({ appId, privateKey, privateKeyRaw, owner, repo, installationId, apiBaseUrl, fetch, }: GitHubAppAuthOptions);
35
+ createJwt(): string;
36
+ resolveInstallationId(options?: ResolveInstallationIdOptions): Promise<number>;
37
+ createAccessToken(options?: CreateAccessTokenOptions): Promise<string>;
38
+ withContext(context: Partial<Pick<GitHubAppAuthOptions, 'owner' | 'repo' | 'installationId'>>): GitHubAppAuth;
39
+ static createAccessToken(options: GitHubAppAuthOptions & CreateAccessTokenOptions): Promise<string>;
40
+ }
41
+
42
+ export { type CreateAccessTokenOptions, GitHubAppAuth, type GitHubAppAuthOptions, type GitHubRepoRef, type InstallationTokenPermissions, type ResolveInstallationIdOptions, generateGitHubAppJwt };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,MAAM,CAyBR;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,MAAM,CAAC,CAuBjB"}
package/dist/index.js ADDED
@@ -0,0 +1,290 @@
1
+ // src/index.ts
2
+ import crypto from "crypto";
3
+
4
+ // src/utils.ts
5
+ function toBase64UrlJson(value) {
6
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
7
+ }
8
+
9
+ // src/index.ts
10
+ var DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
11
+ function normalizePrivateKey(privateKeyRaw) {
12
+ return privateKeyRaw.replace(/\\n/g, "\n");
13
+ }
14
+ function requireNonEmptyString(value, fieldName) {
15
+ const trimmed = value.trim();
16
+ if (!trimmed) {
17
+ throw new Error(`\`${fieldName}\` must be a non-empty string.`);
18
+ }
19
+ return trimmed;
20
+ }
21
+ function normalizeOptionalNonEmptyString(value, fieldName) {
22
+ if (value === void 0) {
23
+ return void 0;
24
+ }
25
+ return requireNonEmptyString(value, fieldName);
26
+ }
27
+ function resolvePrivateKey({
28
+ privateKey,
29
+ privateKeyRaw
30
+ }) {
31
+ const key = privateKey ?? privateKeyRaw;
32
+ if (key === void 0) {
33
+ throw new Error("Either `privateKey` or `privateKeyRaw` is required.");
34
+ }
35
+ return requireNonEmptyString(key, "privateKey");
36
+ }
37
+ function resolveFetch(fetchOverride) {
38
+ const fetchImpl = fetchOverride ?? globalThis.fetch;
39
+ if (typeof fetchImpl !== "function") {
40
+ throw new Error(
41
+ "A fetch implementation is required. Pass `fetch` in options when global fetch is unavailable."
42
+ );
43
+ }
44
+ return fetchImpl;
45
+ }
46
+ function resolveApiBaseUrl(apiBaseUrl) {
47
+ return apiBaseUrl ? requireNonEmptyString(apiBaseUrl, "apiBaseUrl") : DEFAULT_GITHUB_API_BASE_URL;
48
+ }
49
+ function parsePositiveInteger(value, fieldName) {
50
+ const parsed = typeof value === "number" ? value : Number(value);
51
+ if (!Number.isInteger(parsed) || parsed <= 0) {
52
+ throw new Error(`\`${fieldName}\` must be a positive integer.`);
53
+ }
54
+ return parsed;
55
+ }
56
+ function parseOptionalInstallationId(value) {
57
+ if (value === void 0) {
58
+ return void 0;
59
+ }
60
+ return parsePositiveInteger(value, "installationId");
61
+ }
62
+ function resolveRepoRef({
63
+ defaultOwner,
64
+ defaultRepo,
65
+ owner,
66
+ repo
67
+ }) {
68
+ const resolvedOwner = owner ?? defaultOwner;
69
+ const resolvedRepo = repo ?? defaultRepo;
70
+ if (!resolvedOwner || !resolvedRepo) {
71
+ throw new Error(
72
+ "Missing repository target. Provide both `owner` and `repo` in the constructor or method call."
73
+ );
74
+ }
75
+ return {
76
+ owner: resolvedOwner,
77
+ repo: resolvedRepo
78
+ };
79
+ }
80
+ function buildAccessTokenRequestBody({
81
+ permissions,
82
+ repositories,
83
+ repositoryIds
84
+ }) {
85
+ const body = {};
86
+ if (permissions) {
87
+ body.permissions = permissions;
88
+ }
89
+ if (repositories?.length) {
90
+ body.repositories = repositories;
91
+ }
92
+ if (repositoryIds?.length) {
93
+ body.repository_ids = repositoryIds;
94
+ }
95
+ if (Object.keys(body).length === 0) {
96
+ return void 0;
97
+ }
98
+ return JSON.stringify(body);
99
+ }
100
+ async function throwGitHubApiError(response, context) {
101
+ const responseBodyRaw = await response.text();
102
+ const responseBody = responseBodyRaw.trim().length > 0 ? responseBodyRaw : "(empty response body)";
103
+ throw new Error(
104
+ `${context} (${response.status} ${response.statusText}): ${responseBody}`
105
+ );
106
+ }
107
+ async function requestInstallationId({
108
+ owner,
109
+ repo,
110
+ appJwt,
111
+ apiBaseUrl,
112
+ fetchImpl
113
+ }) {
114
+ const encodedOwner = encodeURIComponent(owner);
115
+ const encodedRepo = encodeURIComponent(repo);
116
+ const response = await fetchImpl(
117
+ `${apiBaseUrl}/repos/${encodedOwner}/${encodedRepo}/installation`,
118
+ {
119
+ headers: {
120
+ Accept: "application/vnd.github+json",
121
+ Authorization: `Bearer ${appJwt}`
122
+ }
123
+ }
124
+ );
125
+ if (!response.ok) {
126
+ return throwGitHubApiError(
127
+ response,
128
+ `Failed to read installation for ${owner}/${repo}`
129
+ );
130
+ }
131
+ const payload = await response.json();
132
+ if (!payload.id) {
133
+ throw new Error("GitHub response did not include an installation id.");
134
+ }
135
+ return payload.id;
136
+ }
137
+ async function requestAccessToken({
138
+ installationId,
139
+ appJwt,
140
+ apiBaseUrl,
141
+ fetchImpl,
142
+ tokenOptions
143
+ }) {
144
+ const body = buildAccessTokenRequestBody(tokenOptions);
145
+ const headers = {
146
+ Accept: "application/vnd.github+json",
147
+ Authorization: `Bearer ${appJwt}`
148
+ };
149
+ if (body) {
150
+ headers["Content-Type"] = "application/json";
151
+ }
152
+ const response = await fetchImpl(
153
+ `${apiBaseUrl}/app/installations/${installationId}/access_tokens`,
154
+ body ? {
155
+ method: "POST",
156
+ headers,
157
+ body
158
+ } : {
159
+ method: "POST",
160
+ headers
161
+ }
162
+ );
163
+ if (!response.ok) {
164
+ return throwGitHubApiError(
165
+ response,
166
+ `Failed to create installation access token for installation ${installationId}`
167
+ );
168
+ }
169
+ const payload = await response.json();
170
+ if (!payload.token) {
171
+ throw new Error("GitHub response did not include an installation token.");
172
+ }
173
+ return payload.token;
174
+ }
175
+ function generateGitHubAppJwt(appId, privateKeyRaw) {
176
+ const normalizedAppId = parsePositiveInteger(appId, "appId");
177
+ const privateKey = normalizePrivateKey(privateKeyRaw);
178
+ const now = Math.floor(Date.now() / 1e3);
179
+ const payload = {
180
+ iat: now - 60,
181
+ exp: now + 9 * 60,
182
+ iss: normalizedAppId
183
+ };
184
+ const header = {
185
+ alg: "RS256",
186
+ typ: "JWT"
187
+ };
188
+ const encodedHeader = toBase64UrlJson(header);
189
+ const encodedPayload = toBase64UrlJson(payload);
190
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
191
+ const signer = crypto.createSign("RSA-SHA256");
192
+ signer.update(signingInput);
193
+ signer.end();
194
+ const signature = signer.sign(privateKey, "base64url");
195
+ return `${signingInput}.${signature}`;
196
+ }
197
+ var GitHubAppAuth = class _GitHubAppAuth {
198
+ appId;
199
+ privateKey;
200
+ owner;
201
+ repo;
202
+ installationId;
203
+ apiBaseUrl;
204
+ fetchImpl;
205
+ constructor({
206
+ appId,
207
+ privateKey,
208
+ privateKeyRaw,
209
+ owner,
210
+ repo,
211
+ installationId,
212
+ apiBaseUrl,
213
+ fetch
214
+ }) {
215
+ this.appId = parsePositiveInteger(appId, "appId");
216
+ this.privateKey = normalizePrivateKey(
217
+ resolvePrivateKey({
218
+ privateKey,
219
+ privateKeyRaw
220
+ })
221
+ );
222
+ this.owner = normalizeOptionalNonEmptyString(owner, "owner");
223
+ this.repo = normalizeOptionalNonEmptyString(repo, "repo");
224
+ this.installationId = parseOptionalInstallationId(installationId);
225
+ this.apiBaseUrl = resolveApiBaseUrl(apiBaseUrl);
226
+ this.fetchImpl = resolveFetch(fetch);
227
+ if (this.owner && !this.repo || !this.owner && this.repo) {
228
+ throw new Error(
229
+ "Both `owner` and `repo` must be provided together when setting a default repository target."
230
+ );
231
+ }
232
+ }
233
+ createJwt() {
234
+ return generateGitHubAppJwt(this.appId, this.privateKey);
235
+ }
236
+ async resolveInstallationId(options = {}) {
237
+ const repoRef = resolveRepoRef({
238
+ defaultOwner: this.owner,
239
+ defaultRepo: this.repo,
240
+ owner: normalizeOptionalNonEmptyString(options.owner, "owner"),
241
+ repo: normalizeOptionalNonEmptyString(options.repo, "repo")
242
+ });
243
+ return requestInstallationId({
244
+ ...repoRef,
245
+ appJwt: this.createJwt(),
246
+ apiBaseUrl: this.apiBaseUrl,
247
+ fetchImpl: this.fetchImpl
248
+ });
249
+ }
250
+ async createAccessToken(options = {}) {
251
+ const installationId = parseOptionalInstallationId(
252
+ options.installationId ?? this.installationId
253
+ );
254
+ const resolveOptions = {};
255
+ if (options.owner !== void 0) {
256
+ resolveOptions.owner = options.owner;
257
+ }
258
+ if (options.repo !== void 0) {
259
+ resolveOptions.repo = options.repo;
260
+ }
261
+ const resolvedInstallationId = installationId ?? await this.resolveInstallationId(resolveOptions);
262
+ return requestAccessToken({
263
+ installationId: resolvedInstallationId,
264
+ appJwt: this.createJwt(),
265
+ apiBaseUrl: this.apiBaseUrl,
266
+ fetchImpl: this.fetchImpl,
267
+ tokenOptions: options
268
+ });
269
+ }
270
+ withContext(context) {
271
+ return new _GitHubAppAuth({
272
+ appId: this.appId,
273
+ privateKey: this.privateKey,
274
+ owner: context.owner ?? this.owner,
275
+ repo: context.repo ?? this.repo,
276
+ installationId: context.installationId ?? this.installationId,
277
+ apiBaseUrl: this.apiBaseUrl,
278
+ fetch: this.fetchImpl
279
+ });
280
+ }
281
+ static async createAccessToken(options) {
282
+ const auth = new _GitHubAppAuth(options);
283
+ return auth.createAccessToken(options);
284
+ }
285
+ };
286
+ export {
287
+ GitHubAppAuth,
288
+ generateGitHubAppJwt
289
+ };
290
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/utils.ts"],"sourcesContent":["import crypto from 'node:crypto';\n\nimport { toBase64UrlJson } from './utils';\n\nconst DEFAULT_GITHUB_API_BASE_URL = 'https://api.github.com';\n\ntype FetchLike = typeof fetch;\n\ntype PermissionLevel = 'read' | 'write';\n\nexport type InstallationTokenPermissions = Record<string, PermissionLevel>;\n\nexport type GitHubRepoRef = {\n owner: string;\n repo: string;\n};\n\nexport type ResolveInstallationIdOptions = Partial<GitHubRepoRef>;\n\nexport type CreateAccessTokenOptions = ResolveInstallationIdOptions & {\n installationId?: number | undefined;\n permissions?: InstallationTokenPermissions | undefined;\n repositories?: string[] | undefined;\n repositoryIds?: number[] | undefined;\n};\n\nexport type GitHubAppAuthOptions = {\n appId: number | string;\n privateKey?: string | undefined;\n privateKeyRaw?: string | undefined;\n owner?: string | undefined;\n repo?: string | undefined;\n installationId?: number | undefined;\n apiBaseUrl?: string | undefined;\n fetch?: FetchLike | undefined;\n};\n\ntype InstallationLookupResponse = {\n id?: number;\n};\n\ntype AccessTokenResponse = {\n token?: string;\n};\n\ntype AccessTokenRequestBody = {\n permissions?: InstallationTokenPermissions;\n repositories?: string[];\n repository_ids?: number[];\n};\n\nfunction normalizePrivateKey(privateKeyRaw: string): string {\n return privateKeyRaw.replace(/\\\\n/g, '\\n');\n}\n\nfunction requireNonEmptyString(value: string, fieldName: string): string {\n const trimmed = value.trim();\n if (!trimmed) {\n throw new Error(`\\`${fieldName}\\` must be a non-empty string.`);\n }\n\n return trimmed;\n}\n\nfunction normalizeOptionalNonEmptyString(\n value: string | undefined,\n fieldName: string\n): string | undefined {\n if (value === undefined) {\n return undefined;\n }\n\n return requireNonEmptyString(value, fieldName);\n}\n\nfunction resolvePrivateKey({\n privateKey,\n privateKeyRaw,\n}: {\n privateKey?: string | undefined;\n privateKeyRaw?: string | undefined;\n}): string {\n const key = privateKey ?? privateKeyRaw;\n if (key === undefined) {\n throw new Error('Either `privateKey` or `privateKeyRaw` is required.');\n }\n\n return requireNonEmptyString(key, 'privateKey');\n}\n\nfunction resolveFetch(fetchOverride?: FetchLike): FetchLike {\n const fetchImpl = fetchOverride ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new Error(\n 'A fetch implementation is required. Pass `fetch` in options when global fetch is unavailable.'\n );\n }\n\n return fetchImpl;\n}\n\nfunction resolveApiBaseUrl(apiBaseUrl?: string): string {\n return apiBaseUrl\n ? requireNonEmptyString(apiBaseUrl, 'apiBaseUrl')\n : DEFAULT_GITHUB_API_BASE_URL;\n}\n\nfunction parsePositiveInteger(\n value: number | string,\n fieldName: string\n): number {\n const parsed = typeof value === 'number' ? value : Number(value);\n if (!Number.isInteger(parsed) || parsed <= 0) {\n throw new Error(`\\`${fieldName}\\` must be a positive integer.`);\n }\n\n return parsed;\n}\n\nfunction parseOptionalInstallationId(\n value: number | undefined\n): number | undefined {\n if (value === undefined) {\n return undefined;\n }\n\n return parsePositiveInteger(value, 'installationId');\n}\n\nfunction resolveRepoRef({\n defaultOwner,\n defaultRepo,\n owner,\n repo,\n}: {\n defaultOwner: string | undefined;\n defaultRepo: string | undefined;\n owner: string | undefined;\n repo: string | undefined;\n}): GitHubRepoRef {\n const resolvedOwner = owner ?? defaultOwner;\n const resolvedRepo = repo ?? defaultRepo;\n\n if (!resolvedOwner || !resolvedRepo) {\n throw new Error(\n 'Missing repository target. Provide both `owner` and `repo` in the constructor or method call.'\n );\n }\n\n return {\n owner: resolvedOwner,\n repo: resolvedRepo,\n };\n}\n\nfunction buildAccessTokenRequestBody({\n permissions,\n repositories,\n repositoryIds,\n}: CreateAccessTokenOptions): string | undefined {\n const body: AccessTokenRequestBody = {};\n\n if (permissions) {\n body.permissions = permissions;\n }\n\n if (repositories?.length) {\n body.repositories = repositories;\n }\n\n if (repositoryIds?.length) {\n body.repository_ids = repositoryIds;\n }\n\n if (Object.keys(body).length === 0) {\n return undefined;\n }\n\n return JSON.stringify(body);\n}\n\nasync function throwGitHubApiError(\n response: Response,\n context: string\n): Promise<never> {\n const responseBodyRaw = await response.text();\n const responseBody =\n responseBodyRaw.trim().length > 0 ? responseBodyRaw : '(empty response body)';\n\n throw new Error(\n `${context} (${response.status} ${response.statusText}): ${responseBody}`\n );\n}\n\nasync function requestInstallationId({\n owner,\n repo,\n appJwt,\n apiBaseUrl,\n fetchImpl,\n}: {\n owner: string;\n repo: string;\n appJwt: string;\n apiBaseUrl: string;\n fetchImpl: FetchLike;\n}): Promise<number> {\n const encodedOwner = encodeURIComponent(owner);\n const encodedRepo = encodeURIComponent(repo);\n const response = await fetchImpl(\n `${apiBaseUrl}/repos/${encodedOwner}/${encodedRepo}/installation`,\n {\n headers: {\n Accept: 'application/vnd.github+json',\n Authorization: `Bearer ${appJwt}`,\n },\n }\n );\n\n if (!response.ok) {\n return throwGitHubApiError(\n response,\n `Failed to read installation for ${owner}/${repo}`\n );\n }\n\n const payload = (await response.json()) as InstallationLookupResponse;\n if (!payload.id) {\n throw new Error('GitHub response did not include an installation id.');\n }\n\n return payload.id;\n}\n\nasync function requestAccessToken({\n installationId,\n appJwt,\n apiBaseUrl,\n fetchImpl,\n tokenOptions,\n}: {\n installationId: number;\n appJwt: string;\n apiBaseUrl: string;\n fetchImpl: FetchLike;\n tokenOptions: CreateAccessTokenOptions;\n}): Promise<string> {\n const body = buildAccessTokenRequestBody(tokenOptions);\n const headers: Record<string, string> = {\n Accept: 'application/vnd.github+json',\n Authorization: `Bearer ${appJwt}`,\n };\n\n if (body) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await fetchImpl(\n `${apiBaseUrl}/app/installations/${installationId}/access_tokens`,\n body\n ? {\n method: 'POST',\n headers,\n body,\n }\n : {\n method: 'POST',\n headers,\n }\n );\n\n if (!response.ok) {\n return throwGitHubApiError(\n response,\n `Failed to create installation access token for installation ${installationId}`\n );\n }\n\n const payload = (await response.json()) as AccessTokenResponse;\n if (!payload.token) {\n throw new Error('GitHub response did not include an installation token.');\n }\n\n return payload.token;\n}\n\nexport function generateGitHubAppJwt(\n appId: number | string,\n privateKeyRaw: string\n): string {\n const normalizedAppId = parsePositiveInteger(appId, 'appId');\n const privateKey = normalizePrivateKey(privateKeyRaw);\n\n const now = Math.floor(Date.now() / 1000);\n const payload = {\n iat: now - 60,\n exp: now + 9 * 60,\n iss: normalizedAppId,\n };\n\n const header = {\n alg: 'RS256',\n typ: 'JWT',\n };\n\n const encodedHeader = toBase64UrlJson(header);\n const encodedPayload = toBase64UrlJson(payload);\n const signingInput = `${encodedHeader}.${encodedPayload}`;\n\n const signer = crypto.createSign('RSA-SHA256');\n signer.update(signingInput);\n signer.end();\n\n const signature = signer.sign(privateKey, 'base64url');\n return `${signingInput}.${signature}`;\n}\n\nexport class GitHubAppAuth {\n private readonly appId: number;\n private readonly privateKey: string;\n private readonly owner: string | undefined;\n private readonly repo: string | undefined;\n private readonly installationId: number | undefined;\n private readonly apiBaseUrl: string;\n private readonly fetchImpl: FetchLike;\n\n constructor({\n appId,\n privateKey,\n privateKeyRaw,\n owner,\n repo,\n installationId,\n apiBaseUrl,\n fetch,\n }: GitHubAppAuthOptions) {\n this.appId = parsePositiveInteger(appId, 'appId');\n this.privateKey = normalizePrivateKey(\n resolvePrivateKey({\n privateKey,\n privateKeyRaw,\n })\n );\n this.owner = normalizeOptionalNonEmptyString(owner, 'owner');\n this.repo = normalizeOptionalNonEmptyString(repo, 'repo');\n this.installationId = parseOptionalInstallationId(installationId);\n this.apiBaseUrl = resolveApiBaseUrl(apiBaseUrl);\n this.fetchImpl = resolveFetch(fetch);\n\n if ((this.owner && !this.repo) || (!this.owner && this.repo)) {\n throw new Error(\n 'Both `owner` and `repo` must be provided together when setting a default repository target.'\n );\n }\n }\n\n createJwt(): string {\n return generateGitHubAppJwt(this.appId, this.privateKey);\n }\n\n async resolveInstallationId(\n options: ResolveInstallationIdOptions = {}\n ): Promise<number> {\n const repoRef = resolveRepoRef({\n defaultOwner: this.owner,\n defaultRepo: this.repo,\n owner: normalizeOptionalNonEmptyString(options.owner, 'owner'),\n repo: normalizeOptionalNonEmptyString(options.repo, 'repo'),\n });\n\n return requestInstallationId({\n ...repoRef,\n appJwt: this.createJwt(),\n apiBaseUrl: this.apiBaseUrl,\n fetchImpl: this.fetchImpl,\n });\n }\n\n async createAccessToken(options: CreateAccessTokenOptions = {}): Promise<string> {\n const installationId = parseOptionalInstallationId(\n options.installationId ?? this.installationId\n );\n\n const resolveOptions: ResolveInstallationIdOptions = {};\n if (options.owner !== undefined) {\n resolveOptions.owner = options.owner;\n }\n\n if (options.repo !== undefined) {\n resolveOptions.repo = options.repo;\n }\n\n const resolvedInstallationId =\n installationId ?? (await this.resolveInstallationId(resolveOptions));\n\n return requestAccessToken({\n installationId: resolvedInstallationId,\n appJwt: this.createJwt(),\n apiBaseUrl: this.apiBaseUrl,\n fetchImpl: this.fetchImpl,\n tokenOptions: options,\n });\n }\n\n withContext(\n context: Partial<Pick<GitHubAppAuthOptions, 'owner' | 'repo' | 'installationId'>>\n ): GitHubAppAuth {\n return new GitHubAppAuth({\n appId: this.appId,\n privateKey: this.privateKey,\n owner: context.owner ?? this.owner,\n repo: context.repo ?? this.repo,\n installationId: context.installationId ?? this.installationId,\n apiBaseUrl: this.apiBaseUrl,\n fetch: this.fetchImpl,\n });\n }\n\n static async createAccessToken(\n options: GitHubAppAuthOptions & CreateAccessTokenOptions\n ): Promise<string> {\n const auth = new GitHubAppAuth(options);\n return auth.createAccessToken(options);\n }\n}\n","export function toBase64UrlJson(value: Record<string, unknown>): string {\n return Buffer.from(JSON.stringify(value)).toString('base64url');\n}\n"],"mappings":";AAAA,OAAO,YAAY;;;ACAZ,SAAS,gBAAgB,OAAwC;AACtE,SAAO,OAAO,KAAK,KAAK,UAAU,KAAK,CAAC,EAAE,SAAS,WAAW;AAChE;;;ADEA,IAAM,8BAA8B;AA+CpC,SAAS,oBAAoB,eAA+B;AAC1D,SAAO,cAAc,QAAQ,QAAQ,IAAI;AAC3C;AAEA,SAAS,sBAAsB,OAAe,WAA2B;AACvE,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,KAAK,SAAS,gCAAgC;AAAA,EAChE;AAEA,SAAO;AACT;AAEA,SAAS,gCACP,OACA,WACoB;AACpB,MAAI,UAAU,QAAW;AACvB,WAAO;AAAA,EACT;AAEA,SAAO,sBAAsB,OAAO,SAAS;AAC/C;AAEA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AACF,GAGW;AACT,QAAM,MAAM,cAAc;AAC1B,MAAI,QAAQ,QAAW;AACrB,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AAEA,SAAO,sBAAsB,KAAK,YAAY;AAChD;AAEA,SAAS,aAAa,eAAsC;AAC1D,QAAM,YAAY,iBAAiB,WAAW;AAC9C,MAAI,OAAO,cAAc,YAAY;AACnC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,kBAAkB,YAA6B;AACtD,SAAO,aACH,sBAAsB,YAAY,YAAY,IAC9C;AACN;AAEA,SAAS,qBACP,OACA,WACQ;AACR,QAAM,SAAS,OAAO,UAAU,WAAW,QAAQ,OAAO,KAAK;AAC/D,MAAI,CAAC,OAAO,UAAU,MAAM,KAAK,UAAU,GAAG;AAC5C,UAAM,IAAI,MAAM,KAAK,SAAS,gCAAgC;AAAA,EAChE;AAEA,SAAO;AACT;AAEA,SAAS,4BACP,OACoB;AACpB,MAAI,UAAU,QAAW;AACvB,WAAO;AAAA,EACT;AAEA,SAAO,qBAAqB,OAAO,gBAAgB;AACrD;AAEA,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKkB;AAChB,QAAM,gBAAgB,SAAS;AAC/B,QAAM,eAAe,QAAQ;AAE7B,MAAI,CAAC,iBAAiB,CAAC,cAAc;AACnC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AACF;AAEA,SAAS,4BAA4B;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF,GAAiD;AAC/C,QAAM,OAA+B,CAAC;AAEtC,MAAI,aAAa;AACf,SAAK,cAAc;AAAA,EACrB;AAEA,MAAI,cAAc,QAAQ;AACxB,SAAK,eAAe;AAAA,EACtB;AAEA,MAAI,eAAe,QAAQ;AACzB,SAAK,iBAAiB;AAAA,EACxB;AAEA,MAAI,OAAO,KAAK,IAAI,EAAE,WAAW,GAAG;AAClC,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,UAAU,IAAI;AAC5B;AAEA,eAAe,oBACb,UACA,SACgB;AAChB,QAAM,kBAAkB,MAAM,SAAS,KAAK;AAC5C,QAAM,eACJ,gBAAgB,KAAK,EAAE,SAAS,IAAI,kBAAkB;AAExD,QAAM,IAAI;AAAA,IACR,GAAG,OAAO,KAAK,SAAS,MAAM,IAAI,SAAS,UAAU,MAAM,YAAY;AAAA,EACzE;AACF;AAEA,eAAe,sBAAsB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMoB;AAClB,QAAM,eAAe,mBAAmB,KAAK;AAC7C,QAAM,cAAc,mBAAmB,IAAI;AAC3C,QAAM,WAAW,MAAM;AAAA,IACrB,GAAG,UAAU,UAAU,YAAY,IAAI,WAAW;AAAA,IAClD;AAAA,MACE,SAAS;AAAA,QACP,QAAQ;AAAA,QACR,eAAe,UAAU,MAAM;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,SAAS,IAAI;AAChB,WAAO;AAAA,MACL;AAAA,MACA,mCAAmC,KAAK,IAAI,IAAI;AAAA,IAClD;AAAA,EACF;AAEA,QAAM,UAAW,MAAM,SAAS,KAAK;AACrC,MAAI,CAAC,QAAQ,IAAI;AACf,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AAEA,SAAO,QAAQ;AACjB;AAEA,eAAe,mBAAmB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMoB;AAClB,QAAM,OAAO,4BAA4B,YAAY;AACrD,QAAM,UAAkC;AAAA,IACtC,QAAQ;AAAA,IACR,eAAe,UAAU,MAAM;AAAA,EACjC;AAEA,MAAI,MAAM;AACR,YAAQ,cAAc,IAAI;AAAA,EAC5B;AAEA,QAAM,WAAW,MAAM;AAAA,IACrB,GAAG,UAAU,sBAAsB,cAAc;AAAA,IACjD,OACI;AAAA,MACE,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,IACF,IACA;AAAA,MACE,QAAQ;AAAA,MACR;AAAA,IACF;AAAA,EACN;AAEA,MAAI,CAAC,SAAS,IAAI;AAChB,WAAO;AAAA,MACL;AAAA,MACA,+DAA+D,cAAc;AAAA,IAC/E;AAAA,EACF;AAEA,QAAM,UAAW,MAAM,SAAS,KAAK;AACrC,MAAI,CAAC,QAAQ,OAAO;AAClB,UAAM,IAAI,MAAM,wDAAwD;AAAA,EAC1E;AAEA,SAAO,QAAQ;AACjB;AAEO,SAAS,qBACd,OACA,eACQ;AACR,QAAM,kBAAkB,qBAAqB,OAAO,OAAO;AAC3D,QAAM,aAAa,oBAAoB,aAAa;AAEpD,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,UAAU;AAAA,IACd,KAAK,MAAM;AAAA,IACX,KAAK,MAAM,IAAI;AAAA,IACf,KAAK;AAAA,EACP;AAEA,QAAM,SAAS;AAAA,IACb,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AAEA,QAAM,gBAAgB,gBAAgB,MAAM;AAC5C,QAAM,iBAAiB,gBAAgB,OAAO;AAC9C,QAAM,eAAe,GAAG,aAAa,IAAI,cAAc;AAEvD,QAAM,SAAS,OAAO,WAAW,YAAY;AAC7C,SAAO,OAAO,YAAY;AAC1B,SAAO,IAAI;AAEX,QAAM,YAAY,OAAO,KAAK,YAAY,WAAW;AACrD,SAAO,GAAG,YAAY,IAAI,SAAS;AACrC;AAEO,IAAM,gBAAN,MAAM,eAAc;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAAyB;AACvB,SAAK,QAAQ,qBAAqB,OAAO,OAAO;AAChD,SAAK,aAAa;AAAA,MAChB,kBAAkB;AAAA,QAChB;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AACA,SAAK,QAAQ,gCAAgC,OAAO,OAAO;AAC3D,SAAK,OAAO,gCAAgC,MAAM,MAAM;AACxD,SAAK,iBAAiB,4BAA4B,cAAc;AAChE,SAAK,aAAa,kBAAkB,UAAU;AAC9C,SAAK,YAAY,aAAa,KAAK;AAEnC,QAAK,KAAK,SAAS,CAAC,KAAK,QAAU,CAAC,KAAK,SAAS,KAAK,MAAO;AAC5D,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,YAAoB;AAClB,WAAO,qBAAqB,KAAK,OAAO,KAAK,UAAU;AAAA,EACzD;AAAA,EAEA,MAAM,sBACJ,UAAwC,CAAC,GACxB;AACjB,UAAM,UAAU,eAAe;AAAA,MAC7B,cAAc,KAAK;AAAA,MACnB,aAAa,KAAK;AAAA,MAClB,OAAO,gCAAgC,QAAQ,OAAO,OAAO;AAAA,MAC7D,MAAM,gCAAgC,QAAQ,MAAM,MAAM;AAAA,IAC5D,CAAC;AAED,WAAO,sBAAsB;AAAA,MAC3B,GAAG;AAAA,MACH,QAAQ,KAAK,UAAU;AAAA,MACvB,YAAY,KAAK;AAAA,MACjB,WAAW,KAAK;AAAA,IAClB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,kBAAkB,UAAoC,CAAC,GAAoB;AAC/E,UAAM,iBAAiB;AAAA,MACrB,QAAQ,kBAAkB,KAAK;AAAA,IACjC;AAEA,UAAM,iBAA+C,CAAC;AACtD,QAAI,QAAQ,UAAU,QAAW;AAC/B,qBAAe,QAAQ,QAAQ;AAAA,IACjC;AAEA,QAAI,QAAQ,SAAS,QAAW;AAC9B,qBAAe,OAAO,QAAQ;AAAA,IAChC;AAEA,UAAM,yBACJ,kBAAmB,MAAM,KAAK,sBAAsB,cAAc;AAEpE,WAAO,mBAAmB;AAAA,MACxB,gBAAgB;AAAA,MAChB,QAAQ,KAAK,UAAU;AAAA,MACvB,YAAY,KAAK;AAAA,MACjB,WAAW,KAAK;AAAA,MAChB,cAAc;AAAA,IAChB,CAAC;AAAA,EACH;AAAA,EAEA,YACE,SACe;AACf,WAAO,IAAI,eAAc;AAAA,MACvB,OAAO,KAAK;AAAA,MACZ,YAAY,KAAK;AAAA,MACjB,OAAO,QAAQ,SAAS,KAAK;AAAA,MAC7B,MAAM,QAAQ,QAAQ,KAAK;AAAA,MAC3B,gBAAgB,QAAQ,kBAAkB,KAAK;AAAA,MAC/C,YAAY,KAAK;AAAA,MACjB,OAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH;AAAA,EAEA,aAAa,kBACX,SACiB;AACjB,UAAM,OAAO,IAAI,eAAc,OAAO;AACtC,WAAO,KAAK,kBAAkB,OAAO;AAAA,EACvC;AACF;","names":[]}
@@ -0,0 +1,2 @@
1
+ export declare function toBase64UrlJson(value: Record<string, unknown>): string;
2
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEtE"}
package/dist/utils.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toBase64UrlJson = toBase64UrlJson;
4
+ function toBase64UrlJson(value) {
5
+ return Buffer.from(JSON.stringify(value)).toString('base64url');
6
+ }
7
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";;AAAA,0CAEC;AAFD,SAAgB,eAAe,CAAC,KAA8B;IAC5D,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAClE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "github-app-auth-kit",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "TypeScript helpers for GitHub App JWTs and installation access tokens.",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "keywords": [
18
+ "github-app",
19
+ "installation-token",
20
+ "jwt",
21
+ "typescript"
22
+ ],
23
+ "author": "Emeka Orji",
24
+ "license": "ISC",
25
+ "devDependencies": {
26
+ "@types/node": "^22.13.10",
27
+ "tsup": "^8.5.1",
28
+ "typescript": "^5.8.2",
29
+ "vitest": "^2.1.9"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "vitest run"
35
+ }
36
+ }