@dependabit/github-client 0.1.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +266 -0
- package/dist/auth/basic.d.ts +46 -0
- package/dist/auth/basic.d.ts.map +1 -0
- package/dist/auth/basic.js +88 -0
- package/dist/auth/basic.js.map +1 -0
- package/dist/auth/oauth.d.ts +48 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +139 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/token.d.ts +40 -0
- package/dist/auth/token.d.ts.map +1 -0
- package/dist/auth/token.js +67 -0
- package/dist/auth/token.js.map +1 -0
- package/dist/auth.d.ts +47 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +78 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +53 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +74 -0
- package/dist/client.js.map +1 -0
- package/dist/commits.d.ts +57 -0
- package/dist/commits.d.ts.map +1 -0
- package/dist/commits.js +113 -0
- package/dist/commits.js.map +1 -0
- package/dist/feedback.d.ts +69 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +111 -0
- package/dist/feedback.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/issues.d.ts +55 -0
- package/dist/issues.d.ts.map +1 -0
- package/dist/issues.js +123 -0
- package/dist/issues.js.map +1 -0
- package/dist/rate-limit.d.ts +71 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +145 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/releases.d.ts +50 -0
- package/dist/releases.d.ts.map +1 -0
- package/dist/releases.js +113 -0
- package/dist/releases.js.map +1 -0
- package/package.json +39 -0
- package/src/auth/basic.ts +102 -0
- package/src/auth/oauth.ts +183 -0
- package/src/auth/token.ts +81 -0
- package/src/auth.ts +100 -0
- package/src/client.test.ts +115 -0
- package/src/client.ts +109 -0
- package/src/commits.ts +184 -0
- package/src/feedback.ts +166 -0
- package/src/index.ts +15 -0
- package/src/issues.ts +185 -0
- package/src/rate-limit.ts +210 -0
- package/src/releases.ts +149 -0
- package/test/auth/basic.test.ts +122 -0
- package/test/auth/oauth.test.ts +196 -0
- package/test/auth/token.test.ts +97 -0
- package/test/commits.test.ts +169 -0
- package/test/feedback.test.ts +203 -0
- package/test/issues.test.ts +197 -0
- package/test/rate-limit.test.ts +154 -0
- package/test/releases.test.ts +187 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
package/dist/releases.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Manager
|
|
3
|
+
* Handles fetching and comparing GitHub releases
|
|
4
|
+
*/
|
|
5
|
+
import { Octokit } from 'octokit';
|
|
6
|
+
export class ReleaseManager {
|
|
7
|
+
octokit;
|
|
8
|
+
constructor(auth) {
|
|
9
|
+
this.octokit = new Octokit({
|
|
10
|
+
auth: auth || process.env['GITHUB_TOKEN']
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Fetches the latest release from a repository
|
|
15
|
+
*/
|
|
16
|
+
async getLatestRelease(params) {
|
|
17
|
+
const { owner, repo } = params;
|
|
18
|
+
try {
|
|
19
|
+
const response = await this.octokit.rest.repos.getLatestRelease({
|
|
20
|
+
owner,
|
|
21
|
+
repo
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
tagName: response.data.tag_name,
|
|
25
|
+
name: response.data.name || response.data.tag_name,
|
|
26
|
+
publishedAt: new Date(response.data.published_at || response.data.created_at),
|
|
27
|
+
body: response.data.body || undefined,
|
|
28
|
+
htmlUrl: response.data.html_url,
|
|
29
|
+
prerelease: response.data.prerelease,
|
|
30
|
+
draft: response.data.draft
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.status === 404) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Fetches all releases from a repository
|
|
42
|
+
*/
|
|
43
|
+
async getAllReleases(params) {
|
|
44
|
+
const { owner, repo, page = 1, perPage = 30 } = params;
|
|
45
|
+
try {
|
|
46
|
+
const response = await this.octokit.rest.repos.listReleases({
|
|
47
|
+
owner,
|
|
48
|
+
repo,
|
|
49
|
+
page,
|
|
50
|
+
per_page: perPage
|
|
51
|
+
});
|
|
52
|
+
return response.data.map((release) => ({
|
|
53
|
+
tagName: release.tag_name,
|
|
54
|
+
name: release.name || release.tag_name,
|
|
55
|
+
publishedAt: new Date(release.published_at || release.created_at),
|
|
56
|
+
body: release.body || undefined,
|
|
57
|
+
htmlUrl: release.html_url,
|
|
58
|
+
prerelease: release.prerelease,
|
|
59
|
+
draft: release.draft
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.status === 404) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Compares two sets of releases to find new ones
|
|
71
|
+
*/
|
|
72
|
+
compareReleases(oldReleases, newReleases) {
|
|
73
|
+
const oldTags = new Set(oldReleases.map((r) => r.tagName));
|
|
74
|
+
const newTags = new Set(newReleases.map((r) => r.tagName));
|
|
75
|
+
// Find releases in new but not in old
|
|
76
|
+
const newOnes = newReleases.filter((r) => !oldTags.has(r.tagName));
|
|
77
|
+
// Find releases in old but not in new (removed/deleted)
|
|
78
|
+
const oldOnes = oldReleases.filter((r) => !newTags.has(r.tagName));
|
|
79
|
+
return {
|
|
80
|
+
newReleases: newOnes,
|
|
81
|
+
oldReleases: oldOnes
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Fetches release notes for a specific tag
|
|
86
|
+
*/
|
|
87
|
+
async getReleaseByTag(params) {
|
|
88
|
+
const { owner, repo, tag } = params;
|
|
89
|
+
try {
|
|
90
|
+
const response = await this.octokit.rest.repos.getReleaseByTag({
|
|
91
|
+
owner,
|
|
92
|
+
repo,
|
|
93
|
+
tag
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
tagName: response.data.tag_name,
|
|
97
|
+
name: response.data.name || response.data.tag_name,
|
|
98
|
+
publishedAt: new Date(response.data.published_at || response.data.created_at),
|
|
99
|
+
body: response.data.body || undefined,
|
|
100
|
+
htmlUrl: response.data.html_url,
|
|
101
|
+
prerelease: response.data.prerelease,
|
|
102
|
+
draft: response.data.draft
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error.status === 404) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=releases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"releases.js","sourceRoot":"","sources":["../src/releases.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dependabit/github-client",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "GitHub API wrapper with rate limiting and authentication",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@actions/github": "^9.0.0",
|
|
16
|
+
"octokit": "^5.0.5",
|
|
17
|
+
"zod": "^4.3.6"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^25.2.2",
|
|
21
|
+
"tsx": "^4.21.0",
|
|
22
|
+
"typescript": "^5.9.3",
|
|
23
|
+
"vitest": "^4.0.18"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"github",
|
|
27
|
+
"api",
|
|
28
|
+
"client"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsgo -p tsconfig.json",
|
|
33
|
+
"clean": "rm -rf dist",
|
|
34
|
+
"dev": "tsx watch src/index.ts",
|
|
35
|
+
"type-check": "tsgo --noEmit -p tsconfig.json",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic authentication handler for GitHub API
|
|
3
|
+
* Supports username/password or username/personal access token
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface BasicAuth {
|
|
7
|
+
type: 'basic';
|
|
8
|
+
username: string;
|
|
9
|
+
password: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handler for HTTP Basic authentication
|
|
14
|
+
*/
|
|
15
|
+
export class BasicAuthHandler {
|
|
16
|
+
private username: string;
|
|
17
|
+
private password: string;
|
|
18
|
+
|
|
19
|
+
constructor(username: string, password: string) {
|
|
20
|
+
if (!username || username.trim() === '') {
|
|
21
|
+
throw new Error('Username cannot be empty');
|
|
22
|
+
}
|
|
23
|
+
if (!password || password.trim() === '') {
|
|
24
|
+
throw new Error('Password cannot be empty');
|
|
25
|
+
}
|
|
26
|
+
this.username = username;
|
|
27
|
+
this.password = password;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Authenticate and return auth object
|
|
32
|
+
*/
|
|
33
|
+
async authenticate(): Promise<BasicAuth> {
|
|
34
|
+
return {
|
|
35
|
+
type: 'basic',
|
|
36
|
+
username: this.username,
|
|
37
|
+
password: this.password
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get base64-encoded Basic auth header value
|
|
43
|
+
*/
|
|
44
|
+
getAuthHeader(): string {
|
|
45
|
+
const credentials = `${this.username}:${this.password}`;
|
|
46
|
+
const encoded = Buffer.from(credentials).toString('base64');
|
|
47
|
+
return `Basic ${encoded}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate credentials format
|
|
52
|
+
*/
|
|
53
|
+
validate(): boolean {
|
|
54
|
+
// Check for invalid characters (newlines, etc.)
|
|
55
|
+
if (this.username.includes('\n') || this.username.includes('\r')) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (this.password.includes('\n') || this.password.includes('\r')) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get authentication type
|
|
66
|
+
*/
|
|
67
|
+
getType(): string {
|
|
68
|
+
return 'basic';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Update credentials (for rotation)
|
|
73
|
+
*/
|
|
74
|
+
updateCredentials(username: string, password: string): void {
|
|
75
|
+
if (!username || username.trim() === '') {
|
|
76
|
+
throw new Error('Username cannot be empty');
|
|
77
|
+
}
|
|
78
|
+
if (!password || password.trim() === '') {
|
|
79
|
+
throw new Error('Password cannot be empty');
|
|
80
|
+
}
|
|
81
|
+
this.username = username;
|
|
82
|
+
this.password = password;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* String representation (masks password)
|
|
87
|
+
*/
|
|
88
|
+
toString(): string {
|
|
89
|
+
return `BasicAuth(username=${this.username}, password=***)`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* JSON representation (excludes password)
|
|
94
|
+
*/
|
|
95
|
+
toJSON(): Record<string, unknown> {
|
|
96
|
+
return {
|
|
97
|
+
type: 'basic',
|
|
98
|
+
username: this.username
|
|
99
|
+
// password intentionally excluded
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 authentication handler for GitHub
|
|
3
|
+
* Supports authorization code flow and token refresh
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface OAuthConfig {
|
|
7
|
+
clientId: string;
|
|
8
|
+
clientSecret: string;
|
|
9
|
+
redirectUri: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface OAuthAuth {
|
|
13
|
+
type: 'oauth';
|
|
14
|
+
token: string;
|
|
15
|
+
tokenType: string;
|
|
16
|
+
scope?: string | undefined;
|
|
17
|
+
expiresIn?: number | undefined;
|
|
18
|
+
refreshToken?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface TokenResponse {
|
|
22
|
+
access_token: string;
|
|
23
|
+
token_type: string;
|
|
24
|
+
scope?: string;
|
|
25
|
+
expires_in?: number;
|
|
26
|
+
refresh_token?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handler for OAuth 2.0 authentication
|
|
31
|
+
*/
|
|
32
|
+
export class OAuthHandler {
|
|
33
|
+
private config: OAuthConfig;
|
|
34
|
+
private readonly GITHUB_OAUTH_URL = 'https://github.com/login/oauth';
|
|
35
|
+
|
|
36
|
+
constructor(config: OAuthConfig) {
|
|
37
|
+
if (!config.clientId || config.clientId.trim() === '') {
|
|
38
|
+
throw new Error('clientId is required');
|
|
39
|
+
}
|
|
40
|
+
if (!config.clientSecret || config.clientSecret.trim() === '') {
|
|
41
|
+
throw new Error('clientSecret is required');
|
|
42
|
+
}
|
|
43
|
+
this.config = config;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Exchange authorization code for access token
|
|
48
|
+
*/
|
|
49
|
+
async authenticate(code: string): Promise<OAuthAuth> {
|
|
50
|
+
if (!code || code.trim() === '') {
|
|
51
|
+
throw new Error('Authorization code is required');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tokenResponse = await this.exchangeCodeForToken(code);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
type: 'oauth',
|
|
58
|
+
token: tokenResponse.access_token,
|
|
59
|
+
tokenType: tokenResponse.token_type,
|
|
60
|
+
scope: tokenResponse.scope,
|
|
61
|
+
expiresIn: tokenResponse.expires_in,
|
|
62
|
+
refreshToken: tokenResponse.refresh_token
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate authorization URL for OAuth flow
|
|
68
|
+
*/
|
|
69
|
+
getAuthorizationUrl(scopes: string[], state?: string): string {
|
|
70
|
+
const params = new URLSearchParams({
|
|
71
|
+
client_id: this.config.clientId,
|
|
72
|
+
redirect_uri: this.config.redirectUri,
|
|
73
|
+
scope: scopes.join(' ')
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (state) {
|
|
77
|
+
params.append('state', state);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return `${this.GITHUB_OAUTH_URL}/authorize?${params.toString()}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Refresh an expired access token
|
|
85
|
+
*/
|
|
86
|
+
async refreshToken(refreshToken: string): Promise<OAuthAuth> {
|
|
87
|
+
if (!refreshToken || refreshToken.trim() === '') {
|
|
88
|
+
throw new Error('Refresh token is required');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const tokenResponse = await this.performTokenRefresh(refreshToken);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
type: 'oauth',
|
|
95
|
+
token: tokenResponse.access_token,
|
|
96
|
+
tokenType: tokenResponse.token_type,
|
|
97
|
+
scope: tokenResponse.scope,
|
|
98
|
+
expiresIn: tokenResponse.expires_in
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate OAuth configuration
|
|
104
|
+
*/
|
|
105
|
+
validate(): boolean {
|
|
106
|
+
try {
|
|
107
|
+
// Validate redirect URI format
|
|
108
|
+
new URL(this.config.redirectUri);
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get authentication type
|
|
117
|
+
*/
|
|
118
|
+
getType(): string {
|
|
119
|
+
return 'oauth';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Exchange authorization code for token (internal)
|
|
124
|
+
*/
|
|
125
|
+
private async exchangeCodeForToken(code: string): Promise<TokenResponse> {
|
|
126
|
+
const response = await fetch(`${this.GITHUB_OAUTH_URL}/access_token`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: {
|
|
129
|
+
'Content-Type': 'application/json',
|
|
130
|
+
Accept: 'application/json'
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
client_id: this.config.clientId,
|
|
134
|
+
client_secret: this.config.clientSecret,
|
|
135
|
+
code: code,
|
|
136
|
+
redirect_uri: this.config.redirectUri
|
|
137
|
+
})
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(`Failed to exchange code for token: ${response.statusText}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const data = await response.json();
|
|
145
|
+
|
|
146
|
+
if (data.error) {
|
|
147
|
+
throw new Error(data.error_description || data.error);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return data;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Perform token refresh (internal)
|
|
155
|
+
*/
|
|
156
|
+
private async performTokenRefresh(refreshToken: string): Promise<TokenResponse> {
|
|
157
|
+
const response = await fetch(`${this.GITHUB_OAUTH_URL}/access_token`, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
Accept: 'application/json'
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
client_id: this.config.clientId,
|
|
165
|
+
client_secret: this.config.clientSecret,
|
|
166
|
+
grant_type: 'refresh_token',
|
|
167
|
+
refresh_token: refreshToken
|
|
168
|
+
})
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
throw new Error(`Failed to refresh token: ${response.statusText}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const data = await response.json();
|
|
176
|
+
|
|
177
|
+
if (data.error) {
|
|
178
|
+
throw new Error(data.error_description || data.error);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return data;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token authentication handler for GitHub API
|
|
3
|
+
* Supports GitHub PAT tokens, fine-grained tokens, and API keys
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface TokenAuth {
|
|
7
|
+
type: 'token';
|
|
8
|
+
token: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handler for token-based authentication (GitHub PAT, API keys)
|
|
13
|
+
*/
|
|
14
|
+
export class TokenAuthHandler {
|
|
15
|
+
private token: string;
|
|
16
|
+
private readonly GITHUB_TOKEN_PREFIXES = [
|
|
17
|
+
'ghp_', // Personal Access Token
|
|
18
|
+
'gho_', // OAuth Access Token
|
|
19
|
+
'ghu_', // User-to-Server Token
|
|
20
|
+
'ghs_', // Server-to-Server Token
|
|
21
|
+
'ghr_', // Refresh Token
|
|
22
|
+
'github_pat_' // Fine-grained PAT
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
constructor(token: string) {
|
|
26
|
+
if (!token || token.trim() === '') {
|
|
27
|
+
throw new Error('Token cannot be empty');
|
|
28
|
+
}
|
|
29
|
+
this.token = token;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Authenticate and return auth object
|
|
34
|
+
*/
|
|
35
|
+
async authenticate(): Promise<TokenAuth> {
|
|
36
|
+
return {
|
|
37
|
+
type: 'token',
|
|
38
|
+
token: this.token
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate token format
|
|
44
|
+
*/
|
|
45
|
+
validate(): boolean {
|
|
46
|
+
// Check if token starts with valid GitHub prefix or is an API key
|
|
47
|
+
const hasValidPrefix = this.GITHUB_TOKEN_PREFIXES.some((prefix) =>
|
|
48
|
+
this.token.startsWith(prefix)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Allow any token format, but prefer GitHub token prefixes
|
|
52
|
+
return hasValidPrefix || this.token.length > 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get authentication type
|
|
57
|
+
*/
|
|
58
|
+
getType(): string {
|
|
59
|
+
return 'token';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Update token (for rotation)
|
|
64
|
+
*/
|
|
65
|
+
updateToken(newToken: string): void {
|
|
66
|
+
if (!newToken || newToken.trim() === '') {
|
|
67
|
+
throw new Error('Token cannot be empty');
|
|
68
|
+
}
|
|
69
|
+
this.token = newToken;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get current token
|
|
74
|
+
*
|
|
75
|
+
* @warning This method exposes the raw token value. Use with caution and avoid
|
|
76
|
+
* logging or displaying the token. Prefer using authenticate() for auth operations.
|
|
77
|
+
*/
|
|
78
|
+
getToken(): string {
|
|
79
|
+
return this.token;
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication support for GitHub API client
|
|
3
|
+
* Provides token, OAuth, and basic authentication methods
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TokenAuthHandler, type TokenAuth } from './auth/token';
|
|
7
|
+
import { OAuthHandler, type OAuthAuth, type OAuthConfig } from './auth/oauth';
|
|
8
|
+
import { BasicAuthHandler, type BasicAuth } from './auth/basic';
|
|
9
|
+
|
|
10
|
+
export type AuthType = 'token' | 'oauth' | 'basic';
|
|
11
|
+
export type AuthResult = TokenAuth | OAuthAuth | BasicAuth;
|
|
12
|
+
|
|
13
|
+
export interface AuthConfig {
|
|
14
|
+
type: AuthType;
|
|
15
|
+
token?: string;
|
|
16
|
+
oauth?: OAuthConfig;
|
|
17
|
+
username?: string;
|
|
18
|
+
password?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Authentication manager that supports multiple auth methods
|
|
23
|
+
*/
|
|
24
|
+
export class AuthManager {
|
|
25
|
+
private handler: TokenAuthHandler | OAuthHandler | BasicAuthHandler;
|
|
26
|
+
|
|
27
|
+
constructor(config: AuthConfig) {
|
|
28
|
+
switch (config.type) {
|
|
29
|
+
case 'token':
|
|
30
|
+
if (!config.token) {
|
|
31
|
+
throw new Error('Token is required for token authentication');
|
|
32
|
+
}
|
|
33
|
+
this.handler = new TokenAuthHandler(config.token);
|
|
34
|
+
break;
|
|
35
|
+
|
|
36
|
+
case 'oauth':
|
|
37
|
+
if (!config.oauth) {
|
|
38
|
+
throw new Error('OAuth config is required for OAuth authentication');
|
|
39
|
+
}
|
|
40
|
+
this.handler = new OAuthHandler(config.oauth);
|
|
41
|
+
break;
|
|
42
|
+
|
|
43
|
+
case 'basic':
|
|
44
|
+
if (!config.username || !config.password) {
|
|
45
|
+
throw new Error('Username and password are required for basic authentication');
|
|
46
|
+
}
|
|
47
|
+
this.handler = new BasicAuthHandler(config.username, config.password);
|
|
48
|
+
break;
|
|
49
|
+
|
|
50
|
+
default:
|
|
51
|
+
throw new Error(`Unsupported authentication type: ${config.type}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Perform authentication
|
|
57
|
+
*/
|
|
58
|
+
async authenticate(code?: string): Promise<AuthResult> {
|
|
59
|
+
if (this.handler instanceof OAuthHandler && code) {
|
|
60
|
+
return this.handler.authenticate(code);
|
|
61
|
+
}
|
|
62
|
+
if (this.handler instanceof TokenAuthHandler || this.handler instanceof BasicAuthHandler) {
|
|
63
|
+
return this.handler.authenticate();
|
|
64
|
+
}
|
|
65
|
+
throw new Error('Invalid authentication flow');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate authentication configuration
|
|
70
|
+
*/
|
|
71
|
+
validate(): boolean {
|
|
72
|
+
return this.handler.validate();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get authentication type
|
|
77
|
+
*/
|
|
78
|
+
getType(): string {
|
|
79
|
+
return this.handler.getType();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get underlying handler
|
|
84
|
+
*/
|
|
85
|
+
getHandler(): TokenAuthHandler | OAuthHandler | BasicAuthHandler {
|
|
86
|
+
return this.handler;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create authentication manager from config
|
|
92
|
+
*/
|
|
93
|
+
export function createAuth(config: AuthConfig): AuthManager {
|
|
94
|
+
return new AuthManager(config);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Re-export handler classes and types
|
|
98
|
+
export { TokenAuthHandler, type TokenAuth } from './auth/token';
|
|
99
|
+
export { OAuthHandler, type OAuthAuth, type OAuthConfig } from './auth/oauth';
|
|
100
|
+
export { BasicAuthHandler, type BasicAuth } from './auth/basic';
|