@dependabit/github-client 0.1.13 → 0.1.15

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/src/rate-limit.ts DELETED
@@ -1,210 +0,0 @@
1
- /**
2
- * Rate Limit Handler
3
- * Manages GitHub API rate limits and request budgeting
4
- */
5
-
6
- import { Octokit } from 'octokit';
7
-
8
- export interface RateLimitInfo {
9
- limit: number;
10
- remaining: number;
11
- reset: Date;
12
- used: number;
13
- warning?: string;
14
- }
15
-
16
- export interface RateLimitStatus {
17
- core: RateLimitInfo & { percentageRemaining: number };
18
- search: RateLimitInfo & { percentageRemaining: number };
19
- graphql: RateLimitInfo & { percentageRemaining: number };
20
- }
21
-
22
- export interface BudgetReservation {
23
- reserved: boolean;
24
- reason?: string;
25
- waitTime?: number;
26
- }
27
-
28
- export class RateLimitHandler {
29
- private octokit: Octokit;
30
- private lastCheck?: RateLimitStatus;
31
- private lastCheckTime?: Date;
32
-
33
- constructor(auth?: string) {
34
- this.octokit = new Octokit({
35
- auth: auth || process.env['GITHUB_TOKEN']
36
- });
37
- }
38
-
39
- /**
40
- * Checks current rate limit status
41
- */
42
- async checkRateLimit(): Promise<RateLimitInfo> {
43
- const response = await this.octokit.rest.rateLimit.get();
44
- const { rate } = response.data;
45
-
46
- const info: RateLimitInfo = {
47
- limit: rate.limit,
48
- remaining: rate.remaining,
49
- reset: new Date(rate.reset * 1000),
50
- used: rate.used
51
- };
52
-
53
- // Add warning if approaching limit
54
- if (info.remaining < info.limit * 0.1) {
55
- info.warning = `Only ${info.remaining} requests remaining. Reset at ${info.reset.toISOString()}`;
56
- }
57
-
58
- return info;
59
- }
60
-
61
- /**
62
- * Waits if rate limited
63
- */
64
- async waitIfNeeded(): Promise<void> {
65
- const rateLimit = await this.checkRateLimit();
66
-
67
- if (rateLimit.remaining === 0) {
68
- const waitTime = this.calculateWaitTime(rateLimit);
69
- if (waitTime > 0) {
70
- console.log(`Rate limited. Waiting ${Math.ceil(waitTime / 1000)} seconds until reset...`);
71
- await new Promise((resolve) => setTimeout(resolve, waitTime));
72
- }
73
- }
74
- }
75
-
76
- /**
77
- * Calculates wait time until rate limit resets
78
- */
79
- calculateWaitTime(rateLimitInfo: RateLimitInfo): number {
80
- if (rateLimitInfo.remaining > 0) {
81
- return 0;
82
- }
83
-
84
- const now = Date.now();
85
- const resetTime = rateLimitInfo.reset.getTime();
86
- const waitTime = Math.max(0, resetTime - now);
87
-
88
- return waitTime;
89
- }
90
-
91
- /**
92
- * Attempts to reserve API call budget with proactive checking
93
- */
94
- async reserveBudget(
95
- callsNeeded: number,
96
- options?: {
97
- safetyMargin?: number; // Additional buffer (default: 10% of calls needed)
98
- maxWaitTime?: number; // Max time to wait in ms
99
- }
100
- ): Promise<BudgetReservation> {
101
- const safetyMargin = options?.safetyMargin ?? Math.ceil(callsNeeded * 0.1);
102
- const totalNeeded = callsNeeded + safetyMargin;
103
-
104
- const rateLimit = await this.checkRateLimit();
105
-
106
- if (rateLimit.remaining >= totalNeeded) {
107
- return {
108
- reserved: true
109
- };
110
- }
111
-
112
- const waitTime = this.calculateWaitTime(rateLimit);
113
-
114
- // Check if wait time exceeds maximum allowed
115
- if (options?.maxWaitTime && waitTime > options.maxWaitTime) {
116
- return {
117
- reserved: false,
118
- reason: `Wait time (${Math.ceil(waitTime / 1000)}s) exceeds maximum (${Math.ceil(options.maxWaitTime / 1000)}s)`,
119
- waitTime
120
- };
121
- }
122
-
123
- return {
124
- reserved: false,
125
- reason: `Insufficient API quota. Need ${callsNeeded} + ${safetyMargin} margin, have ${rateLimit.remaining}`,
126
- waitTime
127
- };
128
- }
129
-
130
- /**
131
- * Proactively check if operation can proceed without hitting rate limit
132
- */
133
- async canProceed(
134
- estimatedCalls: number,
135
- options?: {
136
- threshold?: number; // Minimum remaining calls (default: 100)
137
- safetyMargin?: number;
138
- }
139
- ): Promise<{ canProceed: boolean; reason?: string }> {
140
- const threshold = options?.threshold ?? 100;
141
- const safetyMargin = options?.safetyMargin ?? Math.ceil(estimatedCalls * 0.1);
142
- const totalNeeded = estimatedCalls + safetyMargin;
143
-
144
- const rateLimit = await this.checkRateLimit();
145
-
146
- // Check if we have enough remaining calls
147
- if (rateLimit.remaining < totalNeeded) {
148
- return {
149
- canProceed: false,
150
- reason: `Insufficient quota: need ${totalNeeded}, have ${rateLimit.remaining}`
151
- };
152
- }
153
-
154
- // Check if we'd drop below threshold
155
- if (rateLimit.remaining - totalNeeded < threshold) {
156
- return {
157
- canProceed: false,
158
- reason: `Operation would leave only ${rateLimit.remaining - totalNeeded} calls (threshold: ${threshold})`
159
- };
160
- }
161
-
162
- return { canProceed: true };
163
- }
164
-
165
- /**
166
- * Gets detailed rate limit status for all API categories
167
- */
168
- async getRateLimitStatus(): Promise<RateLimitStatus> {
169
- const response = await this.octokit.rest.rateLimit.get();
170
- const { resources } = response.data;
171
-
172
- const createInfo = (resource: {
173
- limit: number;
174
- remaining: number;
175
- reset: number;
176
- used: number;
177
- }): RateLimitInfo & { percentageRemaining: number } => ({
178
- limit: resource.limit,
179
- remaining: resource.remaining,
180
- reset: new Date(resource.reset * 1000),
181
- used: resource.used,
182
- percentageRemaining: resource.limit > 0 ? (resource.remaining / resource.limit) * 100 : 0
183
- });
184
-
185
- const status: RateLimitStatus = {
186
- core: createInfo(resources.core),
187
- search: createInfo(resources.search),
188
- graphql: createInfo(resources.graphql || { limit: 0, remaining: 0, reset: 0, used: 0 })
189
- };
190
-
191
- this.lastCheck = status;
192
- this.lastCheckTime = new Date();
193
-
194
- return status;
195
- }
196
-
197
- /**
198
- * Gets cached rate limit status (avoids API call)
199
- */
200
- getCachedStatus(): RateLimitStatus | undefined {
201
- // Return cached status if less than 60 seconds old
202
- if (this.lastCheck && this.lastCheckTime) {
203
- const age = Date.now() - this.lastCheckTime.getTime();
204
- if (age < 60000) {
205
- return this.lastCheck;
206
- }
207
- }
208
- return undefined;
209
- }
210
- }
package/src/releases.ts DELETED
@@ -1,149 +0,0 @@
1
- /**
2
- * Release Manager
3
- * Handles fetching and comparing GitHub releases
4
- */
5
-
6
- import { Octokit } from 'octokit';
7
-
8
- export interface Release {
9
- tagName: string;
10
- name: string;
11
- publishedAt: Date;
12
- body?: string | undefined;
13
- htmlUrl: string;
14
- prerelease?: boolean;
15
- draft?: boolean;
16
- }
17
-
18
- export interface ReleaseComparison {
19
- newReleases: Release[];
20
- oldReleases: Release[];
21
- }
22
-
23
- export class ReleaseManager {
24
- private octokit: Octokit;
25
-
26
- constructor(auth?: string) {
27
- this.octokit = new Octokit({
28
- auth: auth || process.env['GITHUB_TOKEN']
29
- });
30
- }
31
-
32
- /**
33
- * Fetches the latest release from a repository
34
- */
35
- async getLatestRelease(params: { owner: string; repo: string }): Promise<Release | null> {
36
- const { owner, repo } = params;
37
-
38
- try {
39
- const response = await this.octokit.rest.repos.getLatestRelease({
40
- owner,
41
- repo
42
- });
43
-
44
- return {
45
- tagName: response.data.tag_name,
46
- name: response.data.name || response.data.tag_name,
47
- publishedAt: new Date(response.data.published_at || response.data.created_at),
48
- body: response.data.body || undefined,
49
- htmlUrl: response.data.html_url,
50
- prerelease: response.data.prerelease,
51
- draft: response.data.draft
52
- };
53
- } catch (error) {
54
- if ((error as { status?: number }).status === 404) {
55
- return null;
56
- }
57
- throw error;
58
- }
59
- }
60
-
61
- /**
62
- * Fetches all releases from a repository
63
- */
64
- async getAllReleases(params: {
65
- owner: string;
66
- repo: string;
67
- page?: number;
68
- perPage?: number;
69
- }): Promise<Release[]> {
70
- const { owner, repo, page = 1, perPage = 30 } = params;
71
-
72
- try {
73
- const response = await this.octokit.rest.repos.listReleases({
74
- owner,
75
- repo,
76
- page,
77
- per_page: perPage
78
- });
79
-
80
- return response.data.map((release) => ({
81
- tagName: release.tag_name,
82
- name: release.name || release.tag_name,
83
- publishedAt: new Date(release.published_at || release.created_at),
84
- body: release.body || undefined,
85
- htmlUrl: release.html_url,
86
- prerelease: release.prerelease,
87
- draft: release.draft
88
- }));
89
- } catch (error) {
90
- if ((error as { status?: number }).status === 404) {
91
- return [];
92
- }
93
- throw error;
94
- }
95
- }
96
-
97
- /**
98
- * Compares two sets of releases to find new ones
99
- */
100
- compareReleases(oldReleases: Release[], newReleases: Release[]): ReleaseComparison {
101
- const oldTags = new Set(oldReleases.map((r) => r.tagName));
102
- const newTags = new Set(newReleases.map((r) => r.tagName));
103
-
104
- // Find releases in new but not in old
105
- const newOnes = newReleases.filter((r) => !oldTags.has(r.tagName));
106
-
107
- // Find releases in old but not in new (removed/deleted)
108
- const oldOnes = oldReleases.filter((r) => !newTags.has(r.tagName));
109
-
110
- return {
111
- newReleases: newOnes,
112
- oldReleases: oldOnes
113
- };
114
- }
115
-
116
- /**
117
- * Fetches release notes for a specific tag
118
- */
119
- async getReleaseByTag(params: {
120
- owner: string;
121
- repo: string;
122
- tag: string;
123
- }): Promise<Release | null> {
124
- const { owner, repo, tag } = params;
125
-
126
- try {
127
- const response = await this.octokit.rest.repos.getReleaseByTag({
128
- owner,
129
- repo,
130
- tag
131
- });
132
-
133
- return {
134
- tagName: response.data.tag_name,
135
- name: response.data.name || response.data.tag_name,
136
- publishedAt: new Date(response.data.published_at || response.data.created_at),
137
- body: response.data.body || undefined,
138
- htmlUrl: response.data.html_url,
139
- prerelease: response.data.prerelease,
140
- draft: response.data.draft
141
- };
142
- } catch (error) {
143
- if ((error as { status?: number }).status === 404) {
144
- return null;
145
- }
146
- throw error;
147
- }
148
- }
149
- }
@@ -1,122 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { BasicAuthHandler } from '../../src/auth/basic';
3
-
4
- describe('BasicAuthHandler', () => {
5
- beforeEach(() => {
6
- vi.clearAllMocks();
7
- });
8
-
9
- describe('constructor', () => {
10
- it('should create handler with username and password', () => {
11
- const handler = new BasicAuthHandler('user', 'pass');
12
- expect(handler).toBeInstanceOf(BasicAuthHandler);
13
- });
14
-
15
- it('should throw error for empty username', () => {
16
- expect(() => new BasicAuthHandler('', 'pass')).toThrow('Username cannot be empty');
17
- });
18
-
19
- it('should throw error for empty password', () => {
20
- expect(() => new BasicAuthHandler('user', '')).toThrow('Password cannot be empty');
21
- });
22
- });
23
-
24
- describe('authenticate', () => {
25
- it('should return auth object with credentials', async () => {
26
- const handler = new BasicAuthHandler('testuser', 'testpass');
27
- const auth = await handler.authenticate();
28
-
29
- expect(auth).toEqual({
30
- type: 'basic',
31
- username: 'testuser',
32
- password: 'testpass'
33
- });
34
- });
35
-
36
- it('should encode credentials properly', async () => {
37
- const handler = new BasicAuthHandler('user@example.com', 'p@ssw0rd!');
38
- const auth = await handler.authenticate();
39
-
40
- expect(auth.username).toBe('user@example.com');
41
- expect(auth.password).toBe('p@ssw0rd!');
42
- });
43
- });
44
-
45
- describe('getAuthHeader', () => {
46
- it('should generate base64 encoded auth header', () => {
47
- const handler = new BasicAuthHandler('user', 'pass');
48
- const header = handler.getAuthHeader();
49
-
50
- // "user:pass" in base64 is "dXNlcjpwYXNz"
51
- expect(header).toBe('Basic dXNlcjpwYXNz');
52
- });
53
-
54
- it('should handle special characters in credentials', () => {
55
- const handler = new BasicAuthHandler('user@example.com', 'p@ss:word');
56
- const header = handler.getAuthHeader();
57
-
58
- expect(header).toMatch(/^Basic [A-Za-z0-9+/=]+$/);
59
- });
60
- });
61
-
62
- describe('validate', () => {
63
- it('should validate credentials format', () => {
64
- const handler = new BasicAuthHandler('user', 'pass');
65
- expect(handler.validate()).toBe(true);
66
- });
67
-
68
- it('should reject username with invalid characters', () => {
69
- const handler = new BasicAuthHandler('user\n', 'pass');
70
- expect(handler.validate()).toBe(false);
71
- });
72
-
73
- it('should reject password with newline', () => {
74
- const handler = new BasicAuthHandler('user', 'pass\n');
75
- expect(handler.validate()).toBe(false);
76
- });
77
-
78
- it('should accept email as username', () => {
79
- const handler = new BasicAuthHandler('user@example.com', 'pass');
80
- expect(handler.validate()).toBe(true);
81
- });
82
- });
83
-
84
- describe('getType', () => {
85
- it('should return basic type', () => {
86
- const handler = new BasicAuthHandler('user', 'pass');
87
- expect(handler.getType()).toBe('basic');
88
- });
89
- });
90
-
91
- describe('credential rotation', () => {
92
- it('should allow password update', () => {
93
- const handler = new BasicAuthHandler('user', 'oldpass');
94
- handler.updateCredentials('user', 'newpass');
95
-
96
- const header = handler.getAuthHeader();
97
- expect(header).toContain(Buffer.from('user:newpass').toString('base64'));
98
- });
99
-
100
- it('should throw error on empty password update', () => {
101
- const handler = new BasicAuthHandler('user', 'pass');
102
- expect(() => handler.updateCredentials('user', '')).toThrow('Password cannot be empty');
103
- });
104
- });
105
-
106
- describe('security', () => {
107
- it('should not expose password in toString', () => {
108
- const handler = new BasicAuthHandler('user', 'secretpass');
109
- const str = handler.toString();
110
-
111
- expect(str).not.toContain('secretpass');
112
- expect(str).toContain('***');
113
- });
114
-
115
- it('should not expose password in JSON', () => {
116
- const handler = new BasicAuthHandler('user', 'secretpass');
117
- const json = JSON.stringify(handler);
118
-
119
- expect(json).not.toContain('secretpass');
120
- });
121
- });
122
- });
@@ -1,196 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { OAuthHandler } from '../../src/auth/oauth';
3
-
4
- describe('OAuthHandler', () => {
5
- beforeEach(() => {
6
- vi.clearAllMocks();
7
- });
8
-
9
- describe('constructor', () => {
10
- it('should create handler with client credentials', () => {
11
- const config = {
12
- clientId: 'test_client_id',
13
- clientSecret: 'test_client_secret',
14
- redirectUri: 'http://localhost:3000/callback'
15
- };
16
- const handler = new OAuthHandler(config);
17
- expect(handler).toBeInstanceOf(OAuthHandler);
18
- });
19
-
20
- it('should throw error for missing clientId', () => {
21
- const config = {
22
- clientId: '',
23
- clientSecret: 'secret',
24
- redirectUri: 'http://localhost:3000/callback'
25
- };
26
- expect(() => new OAuthHandler(config)).toThrow('clientId is required');
27
- });
28
-
29
- it('should throw error for missing clientSecret', () => {
30
- const config = {
31
- clientId: 'client',
32
- clientSecret: '',
33
- redirectUri: 'http://localhost:3000/callback'
34
- };
35
- expect(() => new OAuthHandler(config)).toThrow('clientSecret is required');
36
- });
37
- });
38
-
39
- describe('authenticate', () => {
40
- it('should exchange code for access token', async () => {
41
- const config = {
42
- clientId: 'test_client',
43
- clientSecret: 'test_secret',
44
- redirectUri: 'http://localhost:3000/callback'
45
- };
46
- const handler = new OAuthHandler(config);
47
-
48
- // Mock the token exchange
49
- vi.spyOn(handler as any, 'exchangeCodeForToken').mockResolvedValue({
50
- access_token: 'gho_accesstoken123',
51
- token_type: 'bearer',
52
- scope: 'repo'
53
- });
54
-
55
- const auth = await handler.authenticate('test_code');
56
-
57
- expect(auth).toEqual({
58
- type: 'oauth',
59
- token: 'gho_accesstoken123',
60
- tokenType: 'bearer',
61
- scope: 'repo'
62
- });
63
- });
64
-
65
- it('should throw error for invalid code', async () => {
66
- const config = {
67
- clientId: 'test_client',
68
- clientSecret: 'test_secret',
69
- redirectUri: 'http://localhost:3000/callback'
70
- };
71
- const handler = new OAuthHandler(config);
72
-
73
- await expect(handler.authenticate('')).rejects.toThrow('Authorization code is required');
74
- });
75
-
76
- it('should handle token exchange failure', async () => {
77
- const config = {
78
- clientId: 'test_client',
79
- clientSecret: 'test_secret',
80
- redirectUri: 'http://localhost:3000/callback'
81
- };
82
- const handler = new OAuthHandler(config);
83
-
84
- vi.spyOn(handler as any, 'exchangeCodeForToken').mockRejectedValue(
85
- new Error('Invalid authorization code')
86
- );
87
-
88
- await expect(handler.authenticate('bad_code')).rejects.toThrow('Invalid authorization code');
89
- });
90
- });
91
-
92
- describe('getAuthorizationUrl', () => {
93
- it('should generate authorization URL with scopes', () => {
94
- const config = {
95
- clientId: 'test_client',
96
- clientSecret: 'test_secret',
97
- redirectUri: 'http://localhost:3000/callback'
98
- };
99
- const handler = new OAuthHandler(config);
100
-
101
- const url = handler.getAuthorizationUrl(['repo', 'user']);
102
-
103
- expect(url).toContain('https://github.com/login/oauth/authorize');
104
- expect(url).toContain('client_id=test_client');
105
- expect(url).toContain('redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback');
106
- expect(url).toContain('scope=repo+user'); // URLSearchParams uses + for spaces
107
- });
108
-
109
- it('should include state parameter for CSRF protection', () => {
110
- const config = {
111
- clientId: 'test_client',
112
- clientSecret: 'test_secret',
113
- redirectUri: 'http://localhost:3000/callback'
114
- };
115
- const handler = new OAuthHandler(config);
116
-
117
- const url = handler.getAuthorizationUrl(['repo'], 'random_state_123');
118
-
119
- expect(url).toContain('state=random_state_123');
120
- });
121
- });
122
-
123
- describe('refreshToken', () => {
124
- it('should refresh expired access token', async () => {
125
- const config = {
126
- clientId: 'test_client',
127
- clientSecret: 'test_secret',
128
- redirectUri: 'http://localhost:3000/callback'
129
- };
130
- const handler = new OAuthHandler(config);
131
-
132
- vi.spyOn(handler as any, 'performTokenRefresh').mockResolvedValue({
133
- access_token: 'gho_newtoken456',
134
- token_type: 'bearer',
135
- scope: 'repo'
136
- });
137
-
138
- const result = await handler.refreshToken('refresh_token_123');
139
-
140
- expect(result).toEqual({
141
- type: 'oauth',
142
- token: 'gho_newtoken456',
143
- tokenType: 'bearer',
144
- scope: 'repo'
145
- });
146
- });
147
-
148
- it('should throw error for missing refresh token', async () => {
149
- const config = {
150
- clientId: 'test_client',
151
- clientSecret: 'test_secret',
152
- redirectUri: 'http://localhost:3000/callback'
153
- };
154
- const handler = new OAuthHandler(config);
155
-
156
- await expect(handler.refreshToken('')).rejects.toThrow('Refresh token is required');
157
- });
158
- });
159
-
160
- describe('validate', () => {
161
- it('should validate OAuth configuration', () => {
162
- const config = {
163
- clientId: 'test_client',
164
- clientSecret: 'test_secret',
165
- redirectUri: 'http://localhost:3000/callback'
166
- };
167
- const handler = new OAuthHandler(config);
168
-
169
- expect(handler.validate()).toBe(true);
170
- });
171
-
172
- it('should fail validation for invalid redirect URI', () => {
173
- const config = {
174
- clientId: 'test_client',
175
- clientSecret: 'test_secret',
176
- redirectUri: 'invalid-uri'
177
- };
178
- const handler = new OAuthHandler(config);
179
-
180
- expect(handler.validate()).toBe(false);
181
- });
182
- });
183
-
184
- describe('getType', () => {
185
- it('should return oauth type', () => {
186
- const config = {
187
- clientId: 'test_client',
188
- clientSecret: 'test_secret',
189
- redirectUri: 'http://localhost:3000/callback'
190
- };
191
- const handler = new OAuthHandler(config);
192
-
193
- expect(handler.getType()).toBe('oauth');
194
- });
195
- });
196
- });