@hazeljs/oauth 0.2.0-alpha.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.
Files changed (66) hide show
  1. package/LICENSE +192 -0
  2. package/README.md +200 -0
  3. package/dist/guards/oauth-state.guard.d.ts +15 -0
  4. package/dist/guards/oauth-state.guard.d.ts.map +1 -0
  5. package/dist/guards/oauth-state.guard.js +43 -0
  6. package/dist/guards/oauth-state.guard.test.d.ts +2 -0
  7. package/dist/guards/oauth-state.guard.test.d.ts.map +1 -0
  8. package/dist/guards/oauth-state.guard.test.js +60 -0
  9. package/dist/index.d.ts +12 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +17 -0
  12. package/dist/oauth.controller.d.ts +29 -0
  13. package/dist/oauth.controller.d.ts.map +1 -0
  14. package/dist/oauth.controller.js +137 -0
  15. package/dist/oauth.module.d.ts +5 -0
  16. package/dist/oauth.module.d.ts.map +1 -0
  17. package/dist/oauth.module.js +27 -0
  18. package/dist/oauth.module.test.d.ts +2 -0
  19. package/dist/oauth.module.test.d.ts.map +1 -0
  20. package/dist/oauth.module.test.js +40 -0
  21. package/dist/oauth.service.d.ts +47 -0
  22. package/dist/oauth.service.d.ts.map +1 -0
  23. package/dist/oauth.service.js +256 -0
  24. package/dist/oauth.service.test.d.ts +2 -0
  25. package/dist/oauth.service.test.d.ts.map +1 -0
  26. package/dist/oauth.service.test.js +248 -0
  27. package/dist/providers/facebook.provider.d.ts +11 -0
  28. package/dist/providers/facebook.provider.d.ts.map +1 -0
  29. package/dist/providers/facebook.provider.js +63 -0
  30. package/dist/providers/facebook.provider.test.d.ts +2 -0
  31. package/dist/providers/facebook.provider.test.d.ts.map +1 -0
  32. package/dist/providers/facebook.provider.test.js +66 -0
  33. package/dist/providers/github.provider.d.ts +11 -0
  34. package/dist/providers/github.provider.d.ts.map +1 -0
  35. package/dist/providers/github.provider.js +79 -0
  36. package/dist/providers/github.provider.test.d.ts +2 -0
  37. package/dist/providers/github.provider.test.d.ts.map +1 -0
  38. package/dist/providers/github.provider.test.js +73 -0
  39. package/dist/providers/google.provider.d.ts +11 -0
  40. package/dist/providers/google.provider.d.ts.map +1 -0
  41. package/dist/providers/google.provider.js +61 -0
  42. package/dist/providers/google.provider.test.d.ts +2 -0
  43. package/dist/providers/google.provider.test.d.ts.map +1 -0
  44. package/dist/providers/google.provider.test.js +69 -0
  45. package/dist/providers/index.d.ts +7 -0
  46. package/dist/providers/index.d.ts.map +1 -0
  47. package/dist/providers/index.js +22 -0
  48. package/dist/providers/microsoft.provider.d.ts +11 -0
  49. package/dist/providers/microsoft.provider.d.ts.map +1 -0
  50. package/dist/providers/microsoft.provider.js +62 -0
  51. package/dist/providers/microsoft.provider.test.d.ts +2 -0
  52. package/dist/providers/microsoft.provider.test.d.ts.map +1 -0
  53. package/dist/providers/microsoft.provider.test.js +62 -0
  54. package/dist/providers/provider.types.d.ts +94 -0
  55. package/dist/providers/provider.types.d.ts.map +1 -0
  56. package/dist/providers/provider.types.js +5 -0
  57. package/dist/providers/provider.types.test.d.ts +2 -0
  58. package/dist/providers/provider.types.test.d.ts.map +1 -0
  59. package/dist/providers/provider.types.test.js +57 -0
  60. package/dist/providers/twitter.provider.d.ts +11 -0
  61. package/dist/providers/twitter.provider.d.ts.map +1 -0
  62. package/dist/providers/twitter.provider.js +67 -0
  63. package/dist/providers/twitter.provider.test.d.ts +2 -0
  64. package/dist/providers/twitter.provider.test.d.ts.map +1 -0
  65. package/dist/providers/twitter.provider.test.js +71 -0
  66. package/package.json +56 -0
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const oauth_service_1 = require("./oauth.service");
37
+ const arctic = __importStar(require("arctic"));
38
+ jest.mock('arctic', () => ({
39
+ generateState: jest.fn(() => 'mock-state-123'),
40
+ generateCodeVerifier: jest.fn(() => 'mock-code-verifier-456'),
41
+ Google: jest.fn().mockImplementation(() => ({
42
+ createAuthorizationURL: (state, _cv, scopes) => new URL(`https://accounts.google.com/o/oauth2/v2/auth?state=${state}&scope=${scopes.join(' ')}`),
43
+ validateAuthorizationCode: jest.fn().mockResolvedValue({
44
+ accessToken: () => 'mock-access-token',
45
+ hasRefreshToken: () => true,
46
+ refreshToken: () => 'mock-refresh-token',
47
+ accessTokenExpiresAt: () => new Date(Date.now() + 3600 * 1000),
48
+ }),
49
+ })),
50
+ MicrosoftEntraId: jest.fn().mockImplementation(() => ({
51
+ createAuthorizationURL: (state, _cv, scopes) => new URL(`https://login.microsoftonline.com/oauth2/v2.0/authorize?state=${state}&scope=${scopes.join(' ')}`),
52
+ validateAuthorizationCode: jest.fn().mockResolvedValue({
53
+ accessToken: () => 'mock-access-token',
54
+ hasRefreshToken: () => true,
55
+ refreshToken: () => 'mock-refresh-token',
56
+ accessTokenExpiresAt: () => new Date(Date.now() + 3600 * 1000),
57
+ }),
58
+ })),
59
+ Twitter: jest.fn().mockImplementation(() => ({
60
+ createAuthorizationURL: (state, _cv, scopes) => new URL(`https://twitter.com/i/oauth2/authorize?state=${state}&scope=${scopes.join(' ')}`),
61
+ validateAuthorizationCode: jest.fn().mockResolvedValue({
62
+ accessToken: () => 'mock-access-token',
63
+ hasRefreshToken: () => true,
64
+ refreshToken: () => 'mock-refresh-token',
65
+ accessTokenExpiresAt: () => new Date(Date.now() + 3600 * 1000),
66
+ }),
67
+ })),
68
+ GitHub: jest.fn().mockImplementation(() => ({
69
+ createAuthorizationURL: (state, scopes) => new URL(`https://github.com/login/oauth/authorize?state=${state}&scope=${scopes.join(' ')}`),
70
+ validateAuthorizationCode: jest.fn().mockResolvedValue({
71
+ accessToken: () => 'mock-access-token',
72
+ hasRefreshToken: () => true,
73
+ refreshToken: () => 'mock-refresh-token',
74
+ accessTokenExpiresAt: () => new Date(Date.now() + 3600 * 1000),
75
+ }),
76
+ })),
77
+ Facebook: jest.fn().mockImplementation(() => ({
78
+ createAuthorizationURL: (state, scopes) => new URL(`https://facebook.com/v18.0/dialog/oauth?state=${state}&scope=${scopes.join(',')}`),
79
+ validateAuthorizationCode: jest.fn().mockResolvedValue({
80
+ accessToken: () => 'mock-access-token',
81
+ hasRefreshToken: () => true,
82
+ refreshToken: () => 'mock-refresh-token',
83
+ accessTokenExpiresAt: () => new Date(Date.now() + 3600 * 1000),
84
+ }),
85
+ })),
86
+ }));
87
+ const mockFetch = jest.fn();
88
+ global.fetch = mockFetch;
89
+ describe('OAuthService', () => {
90
+ const baseConfig = {
91
+ clientId: 'test-client-id',
92
+ clientSecret: 'test-client-secret',
93
+ redirectUri: 'https://app.com/callback',
94
+ };
95
+ beforeEach(() => {
96
+ jest.clearAllMocks();
97
+ oauth_service_1.OAuthService.configure({
98
+ providers: {
99
+ google: { ...baseConfig },
100
+ github: { ...baseConfig },
101
+ facebook: { ...baseConfig },
102
+ microsoft: { ...baseConfig },
103
+ twitter: { ...baseConfig },
104
+ },
105
+ });
106
+ mockFetch.mockResolvedValue({
107
+ ok: true,
108
+ json: () => Promise.resolve({
109
+ sub: 'user-123',
110
+ email: 'test@example.com',
111
+ name: 'Test User',
112
+ picture: 'https://example.com/avatar.png',
113
+ }),
114
+ });
115
+ });
116
+ describe('configure and getOptions', () => {
117
+ it('should throw when not configured', () => {
118
+ oauth_service_1.OAuthService.options = null;
119
+ expect(() => new oauth_service_1.OAuthService()).toThrow('OAuthModule not configured');
120
+ oauth_service_1.OAuthService.configure({ providers: { google: baseConfig } });
121
+ });
122
+ });
123
+ describe('getAuthorizationUrl', () => {
124
+ it('should return URL with state for GitHub (no PKCE)', () => {
125
+ const service = new oauth_service_1.OAuthService();
126
+ const result = service.getAuthorizationUrl('github');
127
+ expect(result.url).toContain('github.com');
128
+ expect(result.state).toBe('mock-state-123');
129
+ expect(result.codeVerifier).toBeUndefined();
130
+ expect(arctic.generateState).toHaveBeenCalled();
131
+ });
132
+ it('should return URL with state and codeVerifier for Google (PKCE)', () => {
133
+ const service = new oauth_service_1.OAuthService();
134
+ const result = service.getAuthorizationUrl('google');
135
+ expect(result.url).toContain('accounts.google.com');
136
+ expect(result.state).toBe('mock-state-123');
137
+ expect(result.codeVerifier).toBe('mock-code-verifier-456');
138
+ expect(arctic.generateCodeVerifier).toHaveBeenCalled();
139
+ });
140
+ it('should use provided state when given', () => {
141
+ const service = new oauth_service_1.OAuthService();
142
+ const result = service.getAuthorizationUrl('github', 'custom-state');
143
+ expect(result.state).toBe('custom-state');
144
+ expect(result.url).toContain('custom-state');
145
+ });
146
+ it('should use custom scopes when provided', () => {
147
+ const service = new oauth_service_1.OAuthService();
148
+ const result = service.getAuthorizationUrl('github', undefined, ['user:email', 'repo']);
149
+ expect(result.url).toMatch(/user:email|user%3Aemail/);
150
+ expect(result.url).toContain('repo');
151
+ });
152
+ it('should throw for unconfigured provider', () => {
153
+ oauth_service_1.OAuthService.configure({ providers: { google: baseConfig } });
154
+ const service = new oauth_service_1.OAuthService();
155
+ expect(() => service.getAuthorizationUrl('github')).toThrow('GitHub OAuth is not configured');
156
+ });
157
+ });
158
+ describe('validateState', () => {
159
+ it('should return true when states match', () => {
160
+ const service = new oauth_service_1.OAuthService();
161
+ expect(service.validateState('abc123', 'abc123')).toBe(true);
162
+ });
163
+ it('should return false when states differ', () => {
164
+ const service = new oauth_service_1.OAuthService();
165
+ expect(service.validateState('abc123', 'xyz789')).toBe(false);
166
+ });
167
+ it('should return false when received state is empty', () => {
168
+ const service = new oauth_service_1.OAuthService();
169
+ expect(service.validateState('', 'stored')).toBe(false);
170
+ });
171
+ });
172
+ describe('generateState', () => {
173
+ it('should return generated state', () => {
174
+ const service = new oauth_service_1.OAuthService();
175
+ const state = service.generateState();
176
+ expect(state).toBe('mock-state-123');
177
+ expect(arctic.generateState).toHaveBeenCalled();
178
+ });
179
+ });
180
+ describe('handleCallback', () => {
181
+ it('should exchange code and fetch user for GitHub', async () => {
182
+ const service = new oauth_service_1.OAuthService();
183
+ mockFetch.mockResolvedValueOnce({
184
+ ok: true,
185
+ json: () => Promise.resolve({
186
+ id: 12345,
187
+ email: 'github@example.com',
188
+ name: 'GitHub User',
189
+ avatar_url: 'https://github.com/avatar.png',
190
+ }),
191
+ });
192
+ const result = await service.handleCallback('github', 'auth-code', 'mock-state-123');
193
+ expect(result.accessToken).toBe('mock-access-token');
194
+ expect(result.user.id).toBe('12345');
195
+ expect(result.user.email).toBe('github@example.com');
196
+ expect(result.user.name).toBe('GitHub User');
197
+ expect(result.user.picture).toBe('https://github.com/avatar.png');
198
+ });
199
+ it('should require codeVerifier for Google (PKCE)', async () => {
200
+ const service = new oauth_service_1.OAuthService();
201
+ await expect(service.handleCallback('google', 'code', 'state')).rejects.toThrow('requires codeVerifier');
202
+ });
203
+ it('should exchange code with codeVerifier for Google', async () => {
204
+ const service = new oauth_service_1.OAuthService();
205
+ const result = await service.handleCallback('google', 'auth-code', 'mock-state-123', 'mock-code-verifier-456');
206
+ expect(result.accessToken).toBe('mock-access-token');
207
+ expect(result.user.email).toBe('test@example.com');
208
+ });
209
+ it('should fetch Facebook user with correct API', async () => {
210
+ const service = new oauth_service_1.OAuthService();
211
+ mockFetch.mockResolvedValue({
212
+ ok: true,
213
+ json: () => Promise.resolve({
214
+ id: 'fb-123',
215
+ email: 'fb@example.com',
216
+ name: 'FB User',
217
+ picture: { data: { url: 'https://fb.com/pic.png' } },
218
+ }),
219
+ });
220
+ const result = await service.handleCallback('facebook', 'code', 'state');
221
+ expect(result.user.id).toBe('fb-123');
222
+ expect(result.user.picture).toBe('https://fb.com/pic.png');
223
+ });
224
+ it('should fetch Twitter user (no email from API v2)', async () => {
225
+ const service = new oauth_service_1.OAuthService();
226
+ mockFetch.mockResolvedValue({
227
+ ok: true,
228
+ json: () => Promise.resolve({
229
+ data: {
230
+ id: 'tw-123',
231
+ name: 'Twitter User',
232
+ username: 'twuser',
233
+ profile_image_url: 'https://twitter.com/pic.png',
234
+ },
235
+ }),
236
+ });
237
+ const result = await service.handleCallback('twitter', 'code', 'state', 'code-verifier');
238
+ expect(result.user.id).toBe('tw-123');
239
+ expect(result.user.email).toBe('');
240
+ expect(result.user.name).toBe('Twitter User');
241
+ });
242
+ it('should throw for unconfigured provider', async () => {
243
+ oauth_service_1.OAuthService.configure({ providers: { google: baseConfig } });
244
+ const service = new oauth_service_1.OAuthService();
245
+ await expect(service.handleCallback('facebook', 'code', 'state')).rejects.toThrow('Facebook OAuth is not configured');
246
+ });
247
+ });
248
+ });
@@ -0,0 +1,11 @@
1
+ import * as arctic from 'arctic';
2
+ import type { FacebookProviderConfig } from './provider.types';
3
+ export declare function createFacebookProvider(config: FacebookProviderConfig): arctic.Facebook;
4
+ export declare function getFacebookDefaultScopes(): string[];
5
+ export declare function fetchFacebookUser(accessToken: string): Promise<{
6
+ id: string;
7
+ email: string;
8
+ name: string | null;
9
+ picture?: string | null;
10
+ }>;
11
+ //# sourceMappingURL=facebook.provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facebook.provider.d.ts","sourceRoot":"","sources":["../../src/providers/facebook.provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAI/D,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAAC,QAAQ,CAEtF;AAED,wBAAgB,wBAAwB,IAAI,MAAM,EAAE,CAEnD;AAED,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IACpE,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC,CAqBD"}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createFacebookProvider = createFacebookProvider;
37
+ exports.getFacebookDefaultScopes = getFacebookDefaultScopes;
38
+ exports.fetchFacebookUser = fetchFacebookUser;
39
+ const arctic = __importStar(require("arctic"));
40
+ const DEFAULT_SCOPES = ['email', 'public_profile'];
41
+ function createFacebookProvider(config) {
42
+ return new arctic.Facebook(config.clientId, config.clientSecret, config.redirectUri);
43
+ }
44
+ function getFacebookDefaultScopes() {
45
+ return [...DEFAULT_SCOPES];
46
+ }
47
+ async function fetchFacebookUser(accessToken) {
48
+ const params = new URLSearchParams();
49
+ params.set('access_token', accessToken);
50
+ params.set('fields', ['id', 'name', 'picture', 'email'].join(','));
51
+ const res = await fetch(`https://graph.facebook.com/me?${params.toString()}`);
52
+ if (!res.ok) {
53
+ throw new Error(`Failed to fetch Facebook user: ${res.status}`);
54
+ }
55
+ const data = (await res.json());
56
+ const pictureUrl = data.picture?.data?.url ?? null;
57
+ return {
58
+ id: data.id,
59
+ email: data.email || '',
60
+ name: data.name || null,
61
+ picture: pictureUrl,
62
+ };
63
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=facebook.provider.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facebook.provider.test.d.ts","sourceRoot":"","sources":["../../src/providers/facebook.provider.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const facebook_provider_1 = require("./facebook.provider");
4
+ jest.mock('arctic', () => ({
5
+ Facebook: jest.fn().mockImplementation(() => ({})),
6
+ }));
7
+ describe('Facebook Provider', () => {
8
+ const config = {
9
+ clientId: 'test-id',
10
+ clientSecret: 'test-secret',
11
+ redirectUri: 'https://app.com/callback',
12
+ };
13
+ describe('createFacebookProvider', () => {
14
+ it('should create provider instance', () => {
15
+ const provider = (0, facebook_provider_1.createFacebookProvider)(config);
16
+ expect(provider).toBeDefined();
17
+ });
18
+ });
19
+ describe('getFacebookDefaultScopes', () => {
20
+ it('should return default scopes', () => {
21
+ const scopes = (0, facebook_provider_1.getFacebookDefaultScopes)();
22
+ expect(scopes).toContain('email');
23
+ expect(scopes).toContain('public_profile');
24
+ });
25
+ });
26
+ describe('fetchFacebookUser', () => {
27
+ const originalFetch = global.fetch;
28
+ beforeEach(() => {
29
+ global.fetch = jest.fn();
30
+ });
31
+ afterEach(() => {
32
+ global.fetch = originalFetch;
33
+ });
34
+ it('should fetch and map user data with nested picture', async () => {
35
+ global.fetch.mockResolvedValue({
36
+ ok: true,
37
+ json: () => Promise.resolve({
38
+ id: 'fb-123',
39
+ email: 'user@fb.com',
40
+ name: 'FB User',
41
+ picture: { data: { url: 'https://fb.com/pic.jpg' } },
42
+ }),
43
+ });
44
+ const user = await (0, facebook_provider_1.fetchFacebookUser)('access-token');
45
+ expect(user.id).toBe('fb-123');
46
+ expect(user.email).toBe('user@fb.com');
47
+ expect(user.name).toBe('FB User');
48
+ expect(user.picture).toBe('https://fb.com/pic.jpg');
49
+ });
50
+ it('should handle missing picture', async () => {
51
+ global.fetch.mockResolvedValue({
52
+ ok: true,
53
+ json: () => Promise.resolve({
54
+ id: 'fb-456',
55
+ name: 'No Pic User',
56
+ }),
57
+ });
58
+ const user = await (0, facebook_provider_1.fetchFacebookUser)('access-token');
59
+ expect(user.picture).toBeNull();
60
+ });
61
+ it('should throw on failed request', async () => {
62
+ global.fetch.mockResolvedValue({ ok: false });
63
+ await expect((0, facebook_provider_1.fetchFacebookUser)('bad-token')).rejects.toThrow('Failed to fetch Facebook user');
64
+ });
65
+ });
66
+ });
@@ -0,0 +1,11 @@
1
+ import * as arctic from 'arctic';
2
+ import type { GitHubProviderConfig } from './provider.types';
3
+ export declare function createGitHubProvider(config: GitHubProviderConfig): arctic.GitHub;
4
+ export declare function getGitHubDefaultScopes(): string[];
5
+ export declare function fetchGitHubUser(accessToken: string): Promise<{
6
+ id: string;
7
+ email: string;
8
+ name: string | null;
9
+ picture?: string | null;
10
+ }>;
11
+ //# sourceMappingURL=github.provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.provider.d.ts","sourceRoot":"","sources":["../../src/providers/github.provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAEhF;AAED,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC,CAuCD"}
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createGitHubProvider = createGitHubProvider;
37
+ exports.getGitHubDefaultScopes = getGitHubDefaultScopes;
38
+ exports.fetchGitHubUser = fetchGitHubUser;
39
+ const arctic = __importStar(require("arctic"));
40
+ const DEFAULT_SCOPES = ['user:email'];
41
+ function createGitHubProvider(config) {
42
+ return new arctic.GitHub(config.clientId, config.clientSecret, config.redirectUri);
43
+ }
44
+ function getGitHubDefaultScopes() {
45
+ return [...DEFAULT_SCOPES];
46
+ }
47
+ async function fetchGitHubUser(accessToken) {
48
+ const res = await fetch('https://api.github.com/user', {
49
+ headers: {
50
+ Authorization: `Bearer ${accessToken}`,
51
+ Accept: 'application/vnd.github+json',
52
+ 'X-GitHub-Api-Version': '2022-11-28',
53
+ },
54
+ });
55
+ if (!res.ok) {
56
+ throw new Error(`Failed to fetch GitHub user: ${res.status}`);
57
+ }
58
+ const data = (await res.json());
59
+ let email = data.email || '';
60
+ if (!email) {
61
+ const emailsRes = await fetch('https://api.github.com/user/emails', {
62
+ headers: {
63
+ Authorization: `Bearer ${accessToken}`,
64
+ Accept: 'application/vnd.github+json',
65
+ },
66
+ });
67
+ if (emailsRes.ok) {
68
+ const emails = (await emailsRes.json());
69
+ const primary = emails.find((e) => e.primary) || emails[0];
70
+ email = primary?.email || '';
71
+ }
72
+ }
73
+ return {
74
+ id: String(data.id),
75
+ email,
76
+ name: data.name || null,
77
+ picture: data.avatar_url ?? null,
78
+ };
79
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=github.provider.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.provider.test.d.ts","sourceRoot":"","sources":["../../src/providers/github.provider.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const github_provider_1 = require("./github.provider");
4
+ jest.mock('arctic', () => ({
5
+ GitHub: jest.fn().mockImplementation(() => ({})),
6
+ }));
7
+ describe('GitHub Provider', () => {
8
+ const config = {
9
+ clientId: 'test-id',
10
+ clientSecret: 'test-secret',
11
+ redirectUri: 'https://app.com/callback',
12
+ };
13
+ describe('createGitHubProvider', () => {
14
+ it('should create provider instance', () => {
15
+ const provider = (0, github_provider_1.createGitHubProvider)(config);
16
+ expect(provider).toBeDefined();
17
+ });
18
+ });
19
+ describe('getGitHubDefaultScopes', () => {
20
+ it('should return default scopes', () => {
21
+ const scopes = (0, github_provider_1.getGitHubDefaultScopes)();
22
+ expect(scopes).toContain('user:email');
23
+ });
24
+ });
25
+ describe('fetchGitHubUser', () => {
26
+ const originalFetch = global.fetch;
27
+ beforeEach(() => {
28
+ global.fetch = jest.fn();
29
+ });
30
+ afterEach(() => {
31
+ global.fetch = originalFetch;
32
+ });
33
+ it('should fetch and map user data', async () => {
34
+ global.fetch.mockResolvedValueOnce({
35
+ ok: true,
36
+ json: () => Promise.resolve({
37
+ id: 999,
38
+ email: 'dev@github.com',
39
+ name: 'Dev User',
40
+ avatar_url: 'https://github.com/avatar.png',
41
+ }),
42
+ });
43
+ const user = await (0, github_provider_1.fetchGitHubUser)('access-token');
44
+ expect(user.id).toBe('999');
45
+ expect(user.email).toBe('dev@github.com');
46
+ expect(user.name).toBe('Dev User');
47
+ expect(user.picture).toBe('https://github.com/avatar.png');
48
+ });
49
+ it('should fetch email from emails endpoint when not in user', async () => {
50
+ global.fetch
51
+ .mockResolvedValueOnce({
52
+ ok: true,
53
+ json: () => Promise.resolve({
54
+ id: 888,
55
+ name: 'No Email User',
56
+ }),
57
+ })
58
+ .mockResolvedValueOnce({
59
+ ok: true,
60
+ json: () => Promise.resolve([
61
+ { email: 'primary@example.com', primary: true },
62
+ { email: 'other@example.com', primary: false },
63
+ ]),
64
+ });
65
+ const user = await (0, github_provider_1.fetchGitHubUser)('access-token');
66
+ expect(user.email).toBe('primary@example.com');
67
+ });
68
+ it('should throw on failed request', async () => {
69
+ global.fetch.mockResolvedValue({ ok: false });
70
+ await expect((0, github_provider_1.fetchGitHubUser)('bad-token')).rejects.toThrow('Failed to fetch GitHub user');
71
+ });
72
+ });
73
+ });
@@ -0,0 +1,11 @@
1
+ import * as arctic from 'arctic';
2
+ import type { GoogleProviderConfig } from './provider.types';
3
+ export declare function createGoogleProvider(config: GoogleProviderConfig): arctic.Google;
4
+ export declare function getGoogleDefaultScopes(): string[];
5
+ export declare function fetchGoogleUser(accessToken: string): Promise<{
6
+ id: string;
7
+ email: string;
8
+ name: string | null;
9
+ picture?: string | null;
10
+ }>;
11
+ //# sourceMappingURL=google.provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google.provider.d.ts","sourceRoot":"","sources":["../../src/providers/google.provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAEhF;AAED,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC,CAmBD"}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createGoogleProvider = createGoogleProvider;
37
+ exports.getGoogleDefaultScopes = getGoogleDefaultScopes;
38
+ exports.fetchGoogleUser = fetchGoogleUser;
39
+ const arctic = __importStar(require("arctic"));
40
+ const DEFAULT_SCOPES = ['openid', 'profile', 'email'];
41
+ function createGoogleProvider(config) {
42
+ return new arctic.Google(config.clientId, config.clientSecret, config.redirectUri);
43
+ }
44
+ function getGoogleDefaultScopes() {
45
+ return [...DEFAULT_SCOPES];
46
+ }
47
+ async function fetchGoogleUser(accessToken) {
48
+ const res = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
49
+ headers: { Authorization: `Bearer ${accessToken}` },
50
+ });
51
+ if (!res.ok) {
52
+ throw new Error(`Failed to fetch Google user: ${res.status}`);
53
+ }
54
+ const data = (await res.json());
55
+ return {
56
+ id: data.sub,
57
+ email: data.email || '',
58
+ name: data.name || null,
59
+ picture: data.picture ?? null,
60
+ };
61
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=google.provider.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google.provider.test.d.ts","sourceRoot":"","sources":["../../src/providers/google.provider.test.ts"],"names":[],"mappings":""}