@alliance-droid/svelte-auth-core 1.0.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 (178) hide show
  1. package/dist/adapter-context.d.ts +19 -0
  2. package/dist/adapter-context.d.ts.map +1 -0
  3. package/dist/adapter-context.js +68 -0
  4. package/dist/adapter-context.js.map +1 -0
  5. package/dist/adapters/__tests__/adapter-tests.d.ts +7 -0
  6. package/dist/adapters/__tests__/adapter-tests.d.ts.map +1 -0
  7. package/dist/adapters/__tests__/adapter-tests.js +206 -0
  8. package/dist/adapters/__tests__/adapter-tests.js.map +1 -0
  9. package/dist/adapters/adapter.d.ts +60 -0
  10. package/dist/adapters/adapter.d.ts.map +1 -0
  11. package/dist/adapters/adapter.js +2 -0
  12. package/dist/adapters/adapter.js.map +1 -0
  13. package/dist/adapters/filesystem-adapter.d.ts +26 -0
  14. package/dist/adapters/filesystem-adapter.d.ts.map +1 -0
  15. package/dist/adapters/filesystem-adapter.js +148 -0
  16. package/dist/adapters/filesystem-adapter.js.map +1 -0
  17. package/dist/adapters/index.d.ts +6 -0
  18. package/dist/adapters/index.d.ts.map +1 -0
  19. package/dist/adapters/index.js +5 -0
  20. package/dist/adapters/index.js.map +1 -0
  21. package/dist/adapters/mongodb-adapter.d.ts +27 -0
  22. package/dist/adapters/mongodb-adapter.d.ts.map +1 -0
  23. package/dist/adapters/mongodb-adapter.js +213 -0
  24. package/dist/adapters/mongodb-adapter.js.map +1 -0
  25. package/dist/adapters/postgres-adapter.d.ts +30 -0
  26. package/dist/adapters/postgres-adapter.d.ts.map +1 -0
  27. package/dist/adapters/postgres-adapter.js +237 -0
  28. package/dist/adapters/postgres-adapter.js.map +1 -0
  29. package/dist/adapters/sqlite-adapter.d.ts +26 -0
  30. package/dist/adapters/sqlite-adapter.d.ts.map +1 -0
  31. package/dist/adapters/sqlite-adapter.js +261 -0
  32. package/dist/adapters/sqlite-adapter.js.map +1 -0
  33. package/dist/auth.d.ts +48 -0
  34. package/dist/auth.d.ts.map +1 -0
  35. package/dist/auth.js +205 -0
  36. package/dist/auth.js.map +1 -0
  37. package/dist/client-jwt.d.ts +30 -0
  38. package/dist/client-jwt.d.ts.map +1 -0
  39. package/dist/client-jwt.js +57 -0
  40. package/dist/client-jwt.js.map +1 -0
  41. package/dist/client-store.d.ts +31 -0
  42. package/dist/client-store.d.ts.map +1 -0
  43. package/dist/client-store.js +122 -0
  44. package/dist/client-store.js.map +1 -0
  45. package/dist/cors.d.ts +48 -0
  46. package/dist/cors.d.ts.map +1 -0
  47. package/dist/cors.js +88 -0
  48. package/dist/cors.js.map +1 -0
  49. package/dist/csrf.d.ts +57 -0
  50. package/dist/csrf.d.ts.map +1 -0
  51. package/dist/csrf.js +95 -0
  52. package/dist/csrf.js.map +1 -0
  53. package/dist/db.d.ts +22 -0
  54. package/dist/db.d.ts.map +1 -0
  55. package/dist/db.js +43 -0
  56. package/dist/db.js.map +1 -0
  57. package/dist/index.d.ts +35 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +36 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/input-validation.d.ts +78 -0
  62. package/dist/input-validation.d.ts.map +1 -0
  63. package/dist/input-validation.js +238 -0
  64. package/dist/input-validation.js.map +1 -0
  65. package/dist/oauth-callback.d.ts +31 -0
  66. package/dist/oauth-callback.d.ts.map +1 -0
  67. package/dist/oauth-callback.js +254 -0
  68. package/dist/oauth-callback.js.map +1 -0
  69. package/dist/oauth-providers.d.ts +92 -0
  70. package/dist/oauth-providers.d.ts.map +1 -0
  71. package/dist/oauth-providers.js +213 -0
  72. package/dist/oauth-providers.js.map +1 -0
  73. package/dist/oauth-types.d.ts +77 -0
  74. package/dist/oauth-types.d.ts.map +1 -0
  75. package/dist/oauth-types.js +2 -0
  76. package/dist/oauth-types.js.map +1 -0
  77. package/dist/password.d.ts +31 -0
  78. package/dist/password.d.ts.map +1 -0
  79. package/dist/password.js +54 -0
  80. package/dist/password.js.map +1 -0
  81. package/dist/providers/github-oauth.d.ts +58 -0
  82. package/dist/providers/github-oauth.d.ts.map +1 -0
  83. package/dist/providers/github-oauth.js +230 -0
  84. package/dist/providers/github-oauth.js.map +1 -0
  85. package/dist/providers/google-oauth.d.ts +46 -0
  86. package/dist/providers/google-oauth.d.ts.map +1 -0
  87. package/dist/providers/google-oauth.js +177 -0
  88. package/dist/providers/google-oauth.js.map +1 -0
  89. package/dist/providers/oidc-oauth.d.ts +85 -0
  90. package/dist/providers/oidc-oauth.d.ts.map +1 -0
  91. package/dist/providers/oidc-oauth.js +301 -0
  92. package/dist/providers/oidc-oauth.js.map +1 -0
  93. package/dist/rate-limit.d.ts +36 -0
  94. package/dist/rate-limit.d.ts.map +1 -0
  95. package/dist/rate-limit.js +88 -0
  96. package/dist/rate-limit.js.map +1 -0
  97. package/dist/rate-limiting.d.ts +113 -0
  98. package/dist/rate-limiting.d.ts.map +1 -0
  99. package/dist/rate-limiting.js +221 -0
  100. package/dist/rate-limiting.js.map +1 -0
  101. package/dist/security-headers.d.ts +54 -0
  102. package/dist/security-headers.d.ts.map +1 -0
  103. package/dist/security-headers.js +123 -0
  104. package/dist/security-headers.js.map +1 -0
  105. package/dist/session.d.ts +13 -0
  106. package/dist/session.d.ts.map +1 -0
  107. package/dist/session.js +33 -0
  108. package/dist/session.js.map +1 -0
  109. package/dist/sql-injection-prevention.d.ts +94 -0
  110. package/dist/sql-injection-prevention.d.ts.map +1 -0
  111. package/dist/sql-injection-prevention.js +222 -0
  112. package/dist/sql-injection-prevention.js.map +1 -0
  113. package/dist/token.d.ts +22 -0
  114. package/dist/token.d.ts.map +1 -0
  115. package/dist/token.js +31 -0
  116. package/dist/token.js.map +1 -0
  117. package/dist/types.d.ts +81 -0
  118. package/dist/types.d.ts.map +1 -0
  119. package/dist/types.js +2 -0
  120. package/dist/types.js.map +1 -0
  121. package/dist/user.d.ts +33 -0
  122. package/dist/user.d.ts.map +1 -0
  123. package/dist/user.js +144 -0
  124. package/dist/user.js.map +1 -0
  125. package/package.json +48 -0
  126. package/src/adapter-context.ts +72 -0
  127. package/src/adapters/__tests__/adapter-tests.ts +254 -0
  128. package/src/adapters/__tests__/filesystem-adapter.test.ts +48 -0
  129. package/src/adapters/__tests__/mongodb-adapter.test.ts +64 -0
  130. package/src/adapters/__tests__/postgres-adapter.test.ts +62 -0
  131. package/src/adapters/__tests__/sqlite-adapter.test.ts +103 -0
  132. package/src/adapters/__tests__/test-fs-adapter.json +4 -0
  133. package/src/adapters/adapter.ts +72 -0
  134. package/src/adapters/filesystem-adapter.ts +153 -0
  135. package/src/adapters/index.ts +5 -0
  136. package/src/adapters/mongodb-adapter.ts +208 -0
  137. package/src/adapters/postgres-adapter.ts +261 -0
  138. package/src/adapters/sqlite-adapter.ts +284 -0
  139. package/src/auth.ts +239 -0
  140. package/src/client-jwt.test.ts +137 -0
  141. package/src/client-jwt.ts +67 -0
  142. package/src/client-store.test.ts +149 -0
  143. package/src/client-store.ts +144 -0
  144. package/src/cors.test.ts +175 -0
  145. package/src/cors.ts +115 -0
  146. package/src/csrf.test.ts +226 -0
  147. package/src/csrf.ts +126 -0
  148. package/src/db.ts +57 -0
  149. package/src/index.ts +143 -0
  150. package/src/input-validation.test.ts +347 -0
  151. package/src/input-validation.ts +307 -0
  152. package/src/integration.test.ts +322 -0
  153. package/src/oauth-callback.test.ts +282 -0
  154. package/src/oauth-callback.ts +323 -0
  155. package/src/oauth-providers.ts +232 -0
  156. package/src/oauth-types.ts +82 -0
  157. package/src/password.test.ts +89 -0
  158. package/src/password.ts +62 -0
  159. package/src/providers/github-oauth.test.ts +290 -0
  160. package/src/providers/github-oauth.ts +226 -0
  161. package/src/providers/google-oauth.test.ts +240 -0
  162. package/src/providers/google-oauth.ts +166 -0
  163. package/src/providers/oidc-oauth.test.ts +367 -0
  164. package/src/providers/oidc-oauth.ts +302 -0
  165. package/src/rate-limit.test.ts +308 -0
  166. package/src/rate-limit.ts +118 -0
  167. package/src/rate-limiting.test.ts +390 -0
  168. package/src/rate-limiting.ts +275 -0
  169. package/src/security-headers.test.ts +242 -0
  170. package/src/security-headers.ts +160 -0
  171. package/src/security-penetration.test.ts +705 -0
  172. package/src/session.ts +42 -0
  173. package/src/sql-injection-prevention.test.ts +337 -0
  174. package/src/sql-injection-prevention.ts +272 -0
  175. package/src/token.test.ts +67 -0
  176. package/src/token.ts +34 -0
  177. package/src/types.ts +87 -0
  178. package/src/user.ts +165 -0
@@ -0,0 +1,62 @@
1
+ import * as bcrypt from 'bcryptjs';
2
+
3
+ const BCRYPT_ROUNDS = 10;
4
+
5
+ /**
6
+ * Hash a password using bcrypt
7
+ * @param password - Plain text password
8
+ * @returns Hashed password
9
+ */
10
+ export async function hashPassword(password: string): Promise<string> {
11
+ return bcrypt.hash(password, BCRYPT_ROUNDS);
12
+ }
13
+
14
+ /**
15
+ * Verify a password against a hash
16
+ * @param password - Plain text password
17
+ * @param hash - Hashed password
18
+ * @returns True if password matches, false otherwise
19
+ */
20
+ export async function verifyPassword(password: string, hash: string): Promise<boolean> {
21
+ return bcrypt.compare(password, hash);
22
+ }
23
+
24
+ /**
25
+ * Validate password strength
26
+ * Requirements:
27
+ * - At least 8 characters
28
+ * - At least one uppercase letter
29
+ * - At least one lowercase letter
30
+ * - At least one number
31
+ * @param password - Password to validate
32
+ * @returns Error message if invalid, undefined if valid
33
+ */
34
+ export function validatePasswordStrength(password: string): string | undefined {
35
+ if (password.length < 8) {
36
+ return 'Password must be at least 8 characters long';
37
+ }
38
+
39
+ if (!/[A-Z]/.test(password)) {
40
+ return 'Password must contain at least one uppercase letter';
41
+ }
42
+
43
+ if (!/[a-z]/.test(password)) {
44
+ return 'Password must contain at least one lowercase letter';
45
+ }
46
+
47
+ if (!/[0-9]/.test(password)) {
48
+ return 'Password must contain at least one number';
49
+ }
50
+
51
+ return undefined;
52
+ }
53
+
54
+ /**
55
+ * Validate email format
56
+ * @param email - Email to validate
57
+ * @returns True if valid, false otherwise
58
+ */
59
+ export function isValidEmail(email: string): boolean {
60
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
61
+ return emailRegex.test(email) && email.length <= 254;
62
+ }
@@ -0,0 +1,290 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { GitHubOAuthProvider } from './github-oauth';
3
+
4
+ describe('GitHubOAuthProvider', () => {
5
+ let provider: GitHubOAuthProvider;
6
+ const mockConfig = {
7
+ clientId: 'test-client-id',
8
+ clientSecret: 'test-client-secret',
9
+ redirectUri: 'http://localhost:3000/auth/github/callback'
10
+ };
11
+
12
+ beforeEach(() => {
13
+ provider = new GitHubOAuthProvider(mockConfig);
14
+ });
15
+
16
+ describe('constructor', () => {
17
+ it('should create provider with valid config', () => {
18
+ expect(provider).toBeDefined();
19
+ });
20
+
21
+ it('should set default scopes', () => {
22
+ const url = provider.generateAuthorizationUrl('state');
23
+ expect(url).toContain('scope=');
24
+ });
25
+
26
+ it('should support custom scopes', () => {
27
+ const customProvider = new GitHubOAuthProvider({
28
+ ...mockConfig,
29
+ scope: ['read:user', 'user:email', 'repo']
30
+ });
31
+
32
+ const url = customProvider.generateAuthorizationUrl('state');
33
+ expect(url).toContain('read%3Auser');
34
+ expect(url).toContain('user%3Aemail');
35
+ expect(url).toContain('repo');
36
+ });
37
+
38
+ it('should throw error when clientId is missing', () => {
39
+ expect(() => {
40
+ new GitHubOAuthProvider({
41
+ clientId: '',
42
+ clientSecret: 'secret',
43
+ redirectUri: 'http://localhost:3000/callback'
44
+ });
45
+ }).toThrow('GitHub OAuth configuration is missing required fields');
46
+ });
47
+
48
+ it('should throw error when clientSecret is missing', () => {
49
+ expect(() => {
50
+ new GitHubOAuthProvider({
51
+ clientId: 'id',
52
+ clientSecret: '',
53
+ redirectUri: 'http://localhost:3000/callback'
54
+ });
55
+ }).toThrow('GitHub OAuth configuration is missing required fields');
56
+ });
57
+
58
+ it('should throw error when redirectUri is missing', () => {
59
+ expect(() => {
60
+ new GitHubOAuthProvider({
61
+ clientId: 'id',
62
+ clientSecret: 'secret',
63
+ redirectUri: ''
64
+ });
65
+ }).toThrow('GitHub OAuth configuration is missing required fields');
66
+ });
67
+ });
68
+
69
+ describe('generateAuthorizationUrl', () => {
70
+ it('should generate valid authorization URL', () => {
71
+ const state = 'test-state';
72
+ const url = provider.generateAuthorizationUrl(state);
73
+
74
+ expect(url).toContain('https://github.com/login/oauth/authorize');
75
+ expect(url).toContain('client_id=test-client-id');
76
+ expect(url).toContain('redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fgithub%2Fcallback');
77
+ expect(url).toContain('state=test-state');
78
+ });
79
+
80
+ it('should allow signup by default', () => {
81
+ const url = provider.generateAuthorizationUrl('state');
82
+ expect(url).toContain('allow_signup=true');
83
+ });
84
+
85
+ it('should disable signup when configured', () => {
86
+ const customProvider = new GitHubOAuthProvider({
87
+ ...mockConfig,
88
+ allowSignup: false
89
+ });
90
+
91
+ const url = customProvider.generateAuthorizationUrl('state');
92
+ expect(url).toContain('allow_signup=false');
93
+ });
94
+ });
95
+
96
+ describe('validateAuthorizationCode', () => {
97
+ it('should validate valid authorization code', () => {
98
+ const code = 'valid-code-abc123';
99
+ expect(provider.validateAuthorizationCode(code)).toBe(true);
100
+ });
101
+
102
+ it('should reject empty code', () => {
103
+ expect(provider.validateAuthorizationCode('')).toBe(false);
104
+ });
105
+
106
+ it('should reject null/undefined code', () => {
107
+ expect(provider.validateAuthorizationCode(null as any)).toBe(false);
108
+ expect(provider.validateAuthorizationCode(undefined as any)).toBe(false);
109
+ });
110
+ });
111
+
112
+ describe('validateAccessToken', () => {
113
+ it('should validate valid access token', () => {
114
+ const token = 'ghu_valid-token-123';
115
+ expect(provider.validateAccessToken(token)).toBe(true);
116
+ });
117
+
118
+ it('should reject empty token', () => {
119
+ expect(provider.validateAccessToken('')).toBe(false);
120
+ });
121
+
122
+ it('should reject null/undefined token', () => {
123
+ expect(provider.validateAccessToken(null as any)).toBe(false);
124
+ expect(provider.validateAccessToken(undefined as any)).toBe(false);
125
+ });
126
+ });
127
+
128
+ describe('exchangeCode', () => {
129
+ it('should exchange code for access token', async () => {
130
+ const mockResponse = {
131
+ ok: true,
132
+ json: vi.fn().mockResolvedValue({
133
+ access_token: 'ghu_test-token-123',
134
+ token_type: 'bearer',
135
+ scope: 'read:user,user:email',
136
+ expires_in: 28800
137
+ })
138
+ };
139
+
140
+ global.fetch = vi.fn().mockResolvedValue(mockResponse);
141
+
142
+ const result = await provider.exchangeCode('test-code');
143
+
144
+ expect(result.accessToken).toBe('ghu_test-token-123');
145
+ expect(result.tokenType).toBe('bearer');
146
+ expect(result.expiresIn).toBe(28800);
147
+ });
148
+
149
+ it('should handle GitHub error response', async () => {
150
+ global.fetch = vi.fn().mockResolvedValue({
151
+ ok: true,
152
+ json: vi.fn().mockResolvedValue({
153
+ error: 'bad_verification_code',
154
+ error_description: 'The code passed is incorrect or expired.'
155
+ })
156
+ });
157
+
158
+ await expect(provider.exchangeCode('invalid-code')).rejects.toThrow('GitHub OAuth error');
159
+ });
160
+
161
+ it('should throw error on failed exchange', async () => {
162
+ global.fetch = vi.fn().mockResolvedValue({
163
+ ok: false,
164
+ statusText: 'Unauthorized'
165
+ });
166
+
167
+ await expect(provider.exchangeCode('test-code')).rejects.toThrow(
168
+ 'GitHub token exchange failed'
169
+ );
170
+ });
171
+ });
172
+
173
+ describe('fetchUserProfile', () => {
174
+ it('should fetch user profile with public email', async () => {
175
+ global.fetch = vi.fn().mockResolvedValue({
176
+ ok: true,
177
+ json: vi.fn().mockResolvedValue({
178
+ id: 123456,
179
+ login: 'testuser',
180
+ email: 'user@example.com',
181
+ name: 'Test User',
182
+ avatar_url: 'https://avatars.githubusercontent.com/u/123456'
183
+ })
184
+ });
185
+
186
+ const result = await provider.fetchUserProfile('test-token');
187
+
188
+ expect(result.id).toBe('123456');
189
+ expect(result.email).toBe('user@example.com');
190
+ expect(result.name).toBe('Test User');
191
+ expect(result.avatar).toBe('https://avatars.githubusercontent.com/u/123456');
192
+ expect(result.provider).toBe('github');
193
+ });
194
+
195
+ it('should fetch email from emails endpoint when not in profile', async () => {
196
+ const fetchMock = vi.fn();
197
+ fetchMock.mockResolvedValueOnce({
198
+ ok: true,
199
+ json: vi.fn().mockResolvedValue({
200
+ id: 123456,
201
+ login: 'testuser',
202
+ email: null,
203
+ name: 'Test User',
204
+ avatar_url: 'https://avatars.githubusercontent.com/u/123456'
205
+ })
206
+ });
207
+
208
+ fetchMock.mockResolvedValueOnce({
209
+ ok: true,
210
+ json: vi.fn().mockResolvedValue([
211
+ {
212
+ email: 'primary@example.com',
213
+ primary: true,
214
+ verified: true,
215
+ visibility: 'public'
216
+ }
217
+ ])
218
+ });
219
+
220
+ global.fetch = fetchMock;
221
+
222
+ const result = await provider.fetchUserProfile('test-token');
223
+
224
+ expect(result.email).toBe('primary@example.com');
225
+ });
226
+
227
+ it('should throw error when email cannot be retrieved', async () => {
228
+ global.fetch = vi.fn();
229
+
230
+ (global.fetch as any).mockResolvedValueOnce({
231
+ ok: true,
232
+ json: vi.fn().mockResolvedValue({
233
+ id: 123456,
234
+ login: 'testuser',
235
+ email: null,
236
+ name: 'Test User'
237
+ })
238
+ });
239
+
240
+ (global.fetch as any).mockResolvedValueOnce({
241
+ ok: false,
242
+ statusText: 'Unauthorized'
243
+ });
244
+
245
+ await expect(provider.fetchUserProfile('invalid-token')).rejects.toThrow(
246
+ 'Could not retrieve email from GitHub'
247
+ );
248
+ });
249
+
250
+ it('should throw error on network failure', async () => {
251
+ global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
252
+
253
+ await expect(provider.fetchUserProfile('test-token')).rejects.toThrow(
254
+ 'Failed to fetch GitHub user profile'
255
+ );
256
+ });
257
+ });
258
+
259
+ describe('refreshAccessToken', () => {
260
+ it('should return same token (GitHub tokens do not expire)', async () => {
261
+ const token = 'ghu_test-token-123';
262
+ const result = await provider.refreshAccessToken(token);
263
+
264
+ expect(result.accessToken).toBe(token);
265
+ expect(result.expiresIn).toBe(0); // GitHub tokens don't expire
266
+ });
267
+ });
268
+
269
+ describe('revokeAccessToken', () => {
270
+ it('should attempt to revoke token', async () => {
271
+ global.fetch = vi.fn().mockResolvedValue({
272
+ ok: true
273
+ });
274
+
275
+ const result = await provider.revokeAccessToken('test-token');
276
+
277
+ expect(result).toBe(true);
278
+ });
279
+
280
+ it('should return false on revocation failure', async () => {
281
+ global.fetch = vi.fn().mockResolvedValue({
282
+ ok: false
283
+ });
284
+
285
+ const result = await provider.revokeAccessToken('test-token');
286
+
287
+ expect(result).toBe(false);
288
+ });
289
+ });
290
+ });
@@ -0,0 +1,226 @@
1
+ import type { OAuthProviderConfig, OAuthUserProfile, OAuthTokenResponse } from '../oauth-types';
2
+
3
+ /**
4
+ * GitHub OAuth Configuration
5
+ */
6
+ export interface GitHubOAuthConfig extends OAuthProviderConfig {
7
+ scope?: string[];
8
+ allowSignup?: boolean;
9
+ }
10
+
11
+ /**
12
+ * GitHub OAuth Provider
13
+ * Handles GitHub OAuth 2.0 flow
14
+ */
15
+ export class GitHubOAuthProvider {
16
+ private clientId: string;
17
+ private clientSecret: string;
18
+ private redirectUri: string;
19
+ private scope: string[];
20
+ private allowSignup: boolean;
21
+
22
+ private readonly authorizationEndpoint = 'https://github.com/login/oauth/authorize';
23
+ private readonly tokenEndpoint = 'https://github.com/login/oauth/access_token';
24
+ private readonly userinfoEndpoint = 'https://api.github.com/user';
25
+
26
+ constructor(config: GitHubOAuthConfig) {
27
+ if (!config.clientId || !config.clientSecret || !config.redirectUri) {
28
+ throw new Error('GitHub OAuth configuration is missing required fields');
29
+ }
30
+
31
+ this.clientId = config.clientId;
32
+ this.clientSecret = config.clientSecret;
33
+ this.redirectUri = config.redirectUri;
34
+ this.scope = config.scope || ['read:user', 'user:email'];
35
+ this.allowSignup = config.allowSignup !== false;
36
+ }
37
+
38
+ /**
39
+ * Generate authorization URL
40
+ */
41
+ generateAuthorizationUrl(state: string): string {
42
+ const params = new URLSearchParams({
43
+ client_id: this.clientId,
44
+ redirect_uri: this.redirectUri,
45
+ scope: this.scope.join(' '),
46
+ state,
47
+ allow_signup: this.allowSignup.toString()
48
+ });
49
+
50
+ return `${this.authorizationEndpoint}?${params.toString()}`;
51
+ }
52
+
53
+ /**
54
+ * Exchange authorization code for tokens
55
+ */
56
+ async exchangeCode(code: string): Promise<OAuthTokenResponse> {
57
+ try {
58
+ const response = await fetch(this.tokenEndpoint, {
59
+ method: 'POST',
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ Accept: 'application/json'
63
+ },
64
+ body: JSON.stringify({
65
+ code,
66
+ client_id: this.clientId,
67
+ client_secret: this.clientSecret,
68
+ redirect_uri: this.redirectUri
69
+ })
70
+ });
71
+
72
+ if (!response.ok) {
73
+ throw new Error(`GitHub token exchange failed: ${response.statusText}`);
74
+ }
75
+
76
+ const data = await response.json();
77
+
78
+ if (data.error) {
79
+ throw new Error(`GitHub OAuth error: ${data.error}`);
80
+ }
81
+
82
+ return {
83
+ accessToken: data.access_token,
84
+ expiresIn: data.expires_in || 28800, // 8 hours default
85
+ tokenType: data.token_type || 'Bearer'
86
+ };
87
+ } catch (error) {
88
+ throw new Error(`Failed to exchange GitHub authorization code: ${error}`);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Fetch user profile from GitHub
94
+ * Also fetches email if not in primary profile
95
+ */
96
+ async fetchUserProfile(accessToken: string): Promise<OAuthUserProfile> {
97
+ try {
98
+ // Fetch user profile
99
+ const profileResponse = await fetch(this.userinfoEndpoint, {
100
+ headers: {
101
+ Authorization: `Bearer ${accessToken}`,
102
+ 'User-Agent': 'svelte-auth-system'
103
+ }
104
+ });
105
+
106
+ if (!profileResponse.ok) {
107
+ throw new Error(`Failed to fetch GitHub user info: ${profileResponse.statusText}`);
108
+ }
109
+
110
+ const profileData = await profileResponse.json();
111
+
112
+ // If email is not public, fetch from emails endpoint
113
+ let email = profileData.email;
114
+ if (!email) {
115
+ email = await this.fetchUserEmail(accessToken);
116
+ }
117
+
118
+ if (!email) {
119
+ throw new Error('Could not retrieve email from GitHub');
120
+ }
121
+
122
+ return {
123
+ id: profileData.id.toString(),
124
+ email,
125
+ name: profileData.name,
126
+ avatar: profileData.avatar_url,
127
+ provider: 'github'
128
+ };
129
+ } catch (error) {
130
+ throw new Error(`Failed to fetch GitHub user profile: ${error}`);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Fetch user email from GitHub emails endpoint
136
+ */
137
+ private async fetchUserEmail(accessToken: string): Promise<string | null> {
138
+ try {
139
+ const response = await fetch('https://api.github.com/user/emails', {
140
+ headers: {
141
+ Authorization: `Bearer ${accessToken}`,
142
+ 'User-Agent': 'svelte-auth-system'
143
+ }
144
+ });
145
+
146
+ if (!response.ok) {
147
+ return null;
148
+ }
149
+
150
+ const emails = await response.json();
151
+
152
+ // Find primary email
153
+ const primaryEmail = emails.find((e: any) => e.primary);
154
+ if (primaryEmail) {
155
+ return primaryEmail.email;
156
+ }
157
+
158
+ // Find verified email
159
+ const verifiedEmail = emails.find((e: any) => e.verified);
160
+ if (verifiedEmail) {
161
+ return verifiedEmail.email;
162
+ }
163
+
164
+ // Return first email if available
165
+ return emails[0]?.email || null;
166
+ } catch (error) {
167
+ console.error('Failed to fetch GitHub user email:', error);
168
+ return null;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * GitHub does not support refresh tokens (token doesn't expire)
174
+ * This method is a no-op but included for API consistency
175
+ */
176
+ async refreshAccessToken(accessToken: string): Promise<OAuthTokenResponse> {
177
+ // GitHub tokens don't expire, so we just return the same token
178
+ return {
179
+ accessToken,
180
+ expiresIn: 0,
181
+ tokenType: 'Bearer'
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Validate authorization code format
187
+ */
188
+ validateAuthorizationCode(code: string): boolean {
189
+ return !!(code && typeof code === 'string' && code.length > 0);
190
+ }
191
+
192
+ /**
193
+ * Validate access token format
194
+ */
195
+ validateAccessToken(token: string): boolean {
196
+ return !!(token && typeof token === 'string' && token.length > 0);
197
+ }
198
+
199
+ /**
200
+ * Revoke access token
201
+ */
202
+ async revokeAccessToken(accessToken: string): Promise<boolean> {
203
+ try {
204
+ // GitHub requires basic auth with clientId:clientSecret
205
+ const auth = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');
206
+
207
+ const response = await fetch(
208
+ `https://api.github.com/applications/${this.clientId}/token`,
209
+ {
210
+ method: 'DELETE',
211
+ headers: {
212
+ Authorization: `Basic ${auth}`,
213
+ 'User-Agent': 'svelte-auth-system',
214
+ 'Content-Type': 'application/json'
215
+ },
216
+ body: JSON.stringify({ access_token: accessToken })
217
+ }
218
+ );
219
+
220
+ return response.ok;
221
+ } catch (error) {
222
+ console.error('Failed to revoke GitHub access token:', error);
223
+ return false;
224
+ }
225
+ }
226
+ }