@mondaydotcomorg/atp-providers 0.17.14

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 (58) hide show
  1. package/README.md +430 -0
  2. package/__tests__/oauth-integration.test.ts +457 -0
  3. package/__tests__/scope-checkers.test.ts +560 -0
  4. package/__tests__/token-expiration.test.ts +308 -0
  5. package/dist/audit/jsonl.d.ts +29 -0
  6. package/dist/audit/jsonl.d.ts.map +1 -0
  7. package/dist/audit/jsonl.js +163 -0
  8. package/dist/audit/jsonl.js.map +1 -0
  9. package/dist/audit/opentelemetry.d.ts +23 -0
  10. package/dist/audit/opentelemetry.d.ts.map +1 -0
  11. package/dist/audit/opentelemetry.js +240 -0
  12. package/dist/audit/opentelemetry.js.map +1 -0
  13. package/dist/audit/otel-metrics.d.ts +111 -0
  14. package/dist/audit/otel-metrics.d.ts.map +1 -0
  15. package/dist/audit/otel-metrics.js +115 -0
  16. package/dist/audit/otel-metrics.js.map +1 -0
  17. package/dist/auth/env.d.ts +21 -0
  18. package/dist/auth/env.d.ts.map +1 -0
  19. package/dist/auth/env.js +48 -0
  20. package/dist/auth/env.js.map +1 -0
  21. package/dist/cache/file.d.ts +47 -0
  22. package/dist/cache/file.d.ts.map +1 -0
  23. package/dist/cache/file.js +262 -0
  24. package/dist/cache/file.js.map +1 -0
  25. package/dist/cache/memory.d.ts +30 -0
  26. package/dist/cache/memory.d.ts.map +1 -0
  27. package/dist/cache/memory.js +81 -0
  28. package/dist/cache/memory.js.map +1 -0
  29. package/dist/cache/redis.d.ts +28 -0
  30. package/dist/cache/redis.d.ts.map +1 -0
  31. package/dist/cache/redis.js +124 -0
  32. package/dist/cache/redis.js.map +1 -0
  33. package/dist/index.d.ts +10 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +9 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/oauth/index.d.ts +2 -0
  38. package/dist/oauth/index.d.ts.map +1 -0
  39. package/dist/oauth/index.js +2 -0
  40. package/dist/oauth/index.js.map +1 -0
  41. package/dist/oauth/scope-checkers.d.ts +61 -0
  42. package/dist/oauth/scope-checkers.d.ts.map +1 -0
  43. package/dist/oauth/scope-checkers.js +166 -0
  44. package/dist/oauth/scope-checkers.js.map +1 -0
  45. package/package.json +26 -0
  46. package/project.json +31 -0
  47. package/src/audit/jsonl.ts +189 -0
  48. package/src/audit/opentelemetry.ts +286 -0
  49. package/src/audit/otel-metrics.ts +122 -0
  50. package/src/auth/env.ts +65 -0
  51. package/src/cache/file.ts +330 -0
  52. package/src/cache/memory.ts +105 -0
  53. package/src/cache/redis.ts +160 -0
  54. package/src/index.ts +32 -0
  55. package/src/oauth/index.ts +1 -0
  56. package/src/oauth/scope-checkers.ts +196 -0
  57. package/tsconfig.json +14 -0
  58. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,457 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { ScopeCheckerRegistry } from '../src/oauth/scope-checkers.js';
3
+ import type { AuthProvider, UserCredentialData } from '@mondaydotcomorg/atp-protocol';
4
+
5
+ // Mock fetch globally
6
+ global.fetch = vi.fn();
7
+
8
+ // Mock Auth Provider
9
+ class MockAuthProvider implements AuthProvider {
10
+ name = 'mock';
11
+ private userCredentials = new Map<string, Map<string, UserCredentialData>>();
12
+
13
+ async getCredential(key: string): Promise<string | null> {
14
+ return process.env[key] || null;
15
+ }
16
+
17
+ async setCredential(key: string, value: string): Promise<void> {}
18
+
19
+ async deleteCredential(key: string): Promise<void> {}
20
+
21
+ async getUserCredential(userId: string, provider: string): Promise<UserCredentialData | null> {
22
+ const userMap = this.userCredentials.get(userId);
23
+ const creds = userMap?.get(provider);
24
+
25
+ if (!creds) {
26
+ return null;
27
+ }
28
+
29
+ // Check expiration
30
+ if (creds.expiresAt && creds.expiresAt < Date.now()) {
31
+ return null;
32
+ }
33
+
34
+ return creds;
35
+ }
36
+
37
+ async setUserCredential(
38
+ userId: string,
39
+ provider: string,
40
+ data: UserCredentialData
41
+ ): Promise<void> {
42
+ if (!this.userCredentials.has(userId)) {
43
+ this.userCredentials.set(userId, new Map());
44
+ }
45
+ this.userCredentials.get(userId)!.set(provider, data);
46
+ }
47
+
48
+ async deleteUserCredential(userId: string, provider: string): Promise<void> {
49
+ this.userCredentials.get(userId)?.delete(provider);
50
+ }
51
+
52
+ async listUserProviders(userId: string): Promise<string[]> {
53
+ const userMap = this.userCredentials.get(userId);
54
+ return userMap ? Array.from(userMap.keys()) : [];
55
+ }
56
+ }
57
+
58
+ // Mock scope checkers for testing
59
+ class TestGitHubChecker {
60
+ provider = 'github';
61
+ async check(token: string) {
62
+ const response = await fetch('https://api.github.com/user', {
63
+ headers: { Authorization: `Bearer ${token}` },
64
+ });
65
+ if (!response.ok) {
66
+ throw new Error('Invalid or expired GitHub token');
67
+ }
68
+ const scopesHeader = response.headers.get('X-OAuth-Scopes');
69
+ return scopesHeader ? scopesHeader.split(',').map((s) => s.trim()) : [];
70
+ }
71
+ async validate(token: string) {
72
+ try {
73
+ const response = await fetch('https://api.github.com/user', {
74
+ headers: { Authorization: `Bearer ${token}` },
75
+ });
76
+ return response.ok;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+ }
82
+
83
+ class TestGoogleChecker {
84
+ provider = 'google';
85
+ async check(token: string) {
86
+ const response = await fetch(
87
+ `https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(token)}`
88
+ );
89
+ if (!response.ok) {
90
+ throw new Error('Invalid or expired Google token');
91
+ }
92
+ const data = (await response.json()) as { scope?: string; exp?: number };
93
+ return data.scope ? data.scope.split(' ') : [];
94
+ }
95
+ async validate(token: string) {
96
+ try {
97
+ const response = await fetch(
98
+ `https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(token)}`
99
+ );
100
+ return response.ok;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+ }
106
+
107
+ describe('OAuth Integration Tests', () => {
108
+ let scopeChecker: ScopeCheckerRegistry;
109
+ let authProvider: MockAuthProvider;
110
+
111
+ beforeEach(() => {
112
+ scopeChecker = new ScopeCheckerRegistry();
113
+ // Register test checkers
114
+ scopeChecker.register(new TestGitHubChecker() as any);
115
+ scopeChecker.register(new TestGoogleChecker() as any);
116
+
117
+ authProvider = new MockAuthProvider();
118
+ vi.clearAllMocks();
119
+ });
120
+
121
+ afterEach(() => {
122
+ scopeChecker.stopCleanup();
123
+ });
124
+
125
+ describe('End-to-end OAuth connection flow', () => {
126
+ it('should validate and store GitHub credentials', async () => {
127
+ const userId = 'user123';
128
+ const provider = 'github';
129
+ const accessToken = 'gho_test_token';
130
+
131
+ // Mock GitHub API responses
132
+ (global.fetch as any)
133
+ .mockResolvedValueOnce({
134
+ // check() call
135
+ ok: true,
136
+ headers: {
137
+ get: (name: string) => (name === 'X-OAuth-Scopes' ? 'repo, read:user' : null),
138
+ },
139
+ })
140
+ .mockResolvedValueOnce({
141
+ // validate() call
142
+ ok: true,
143
+ });
144
+
145
+ // Validate token
146
+ const tokenInfo = await scopeChecker.getTokenInfo(provider, accessToken);
147
+
148
+ expect(tokenInfo.valid).toBe(true);
149
+ expect(tokenInfo.scopes).toEqual(['repo', 'read:user']);
150
+
151
+ // Store credentials
152
+ await authProvider.setUserCredential(userId, provider, {
153
+ token: accessToken,
154
+ scopes: tokenInfo.scopes,
155
+ expiresAt: Date.now() + 3600000,
156
+ });
157
+
158
+ // Retrieve credentials
159
+ const storedCreds = await authProvider.getUserCredential(userId, provider);
160
+
161
+ expect(storedCreds).not.toBeNull();
162
+ expect(storedCreds?.token).toBe(accessToken);
163
+ expect(storedCreds?.scopes).toEqual(['repo', 'read:user']);
164
+ });
165
+
166
+ it('should reject invalid tokens', async () => {
167
+ const provider = 'github';
168
+ const invalidToken = 'invalid_token';
169
+
170
+ // Mock GitHub API error
171
+ (global.fetch as any).mockResolvedValueOnce({
172
+ ok: false,
173
+ status: 401,
174
+ statusText: 'Unauthorized',
175
+ });
176
+
177
+ await expect(scopeChecker.getTokenInfo(provider, invalidToken)).rejects.toThrow(
178
+ 'Invalid or expired GitHub token'
179
+ );
180
+ });
181
+
182
+ it('should handle Google OAuth flow with expiration', async () => {
183
+ const userId = 'user123';
184
+ const provider = 'google';
185
+ const accessToken = 'ya29.test_token';
186
+
187
+ const now = Math.floor(Date.now() / 1000);
188
+ const expiresIn = 3600; // 1 hour
189
+
190
+ // Mock Google tokeninfo
191
+ (global.fetch as any)
192
+ .mockResolvedValueOnce({
193
+ ok: true,
194
+ json: async () => ({
195
+ scope: 'https://www.googleapis.com/auth/calendar',
196
+ exp: now + expiresIn,
197
+ }),
198
+ })
199
+ .mockResolvedValueOnce({
200
+ ok: true,
201
+ json: async () => ({
202
+ exp: now + expiresIn,
203
+ }),
204
+ });
205
+
206
+ const tokenInfo = await scopeChecker.getTokenInfo(provider, accessToken);
207
+
208
+ expect(tokenInfo.valid).toBe(true);
209
+ expect(tokenInfo.scopes).toEqual(['https://www.googleapis.com/auth/calendar']);
210
+
211
+ // Store with expiration
212
+ await authProvider.setUserCredential(userId, provider, {
213
+ token: accessToken,
214
+ scopes: tokenInfo.scopes,
215
+ expiresAt: (now + expiresIn) * 1000, // Convert to milliseconds
216
+ });
217
+
218
+ // Should be retrievable immediately
219
+ const creds = await authProvider.getUserCredential(userId, provider);
220
+ expect(creds).not.toBeNull();
221
+ });
222
+ });
223
+
224
+ describe('Multi-user scenarios', () => {
225
+ it('should handle multiple users with same provider', async () => {
226
+ const provider = 'github';
227
+
228
+ // Mock API responses for both users
229
+ (global.fetch as any)
230
+ .mockResolvedValueOnce({
231
+ ok: true,
232
+ headers: {
233
+ get: (name: string) => (name === 'X-OAuth-Scopes' ? 'repo' : null),
234
+ },
235
+ })
236
+ .mockResolvedValueOnce({
237
+ ok: true,
238
+ })
239
+ .mockResolvedValueOnce({
240
+ ok: true,
241
+ headers: {
242
+ get: (name: string) => (name === 'X-OAuth-Scopes' ? 'read:user' : null),
243
+ },
244
+ })
245
+ .mockResolvedValueOnce({
246
+ ok: true,
247
+ });
248
+
249
+ // User 1 connects
250
+ const token1Info = await scopeChecker.getTokenInfo(provider, 'token1');
251
+ await authProvider.setUserCredential('user1', provider, {
252
+ token: 'token1',
253
+ scopes: token1Info.scopes,
254
+ });
255
+
256
+ // User 2 connects
257
+ const token2Info = await scopeChecker.getTokenInfo(provider, 'token2');
258
+ await authProvider.setUserCredential('user2', provider, {
259
+ token: 'token2',
260
+ scopes: token2Info.scopes,
261
+ });
262
+
263
+ // Verify both users have different credentials
264
+ const user1Creds = await authProvider.getUserCredential('user1', provider);
265
+ const user2Creds = await authProvider.getUserCredential('user2', provider);
266
+
267
+ expect(user1Creds?.token).toBe('token1');
268
+ expect(user1Creds?.scopes).toEqual(['repo']);
269
+
270
+ expect(user2Creds?.token).toBe('token2');
271
+ expect(user2Creds?.scopes).toEqual(['read:user']);
272
+ });
273
+
274
+ it('should handle user with multiple providers', async () => {
275
+ const userId = 'user123';
276
+
277
+ // Mock GitHub
278
+ (global.fetch as any)
279
+ .mockResolvedValueOnce({
280
+ ok: true,
281
+ headers: {
282
+ get: (name: string) => (name === 'X-OAuth-Scopes' ? 'repo' : null),
283
+ },
284
+ })
285
+ .mockResolvedValueOnce({
286
+ ok: true,
287
+ });
288
+
289
+ const githubInfo = await scopeChecker.getTokenInfo('github', 'github_token');
290
+ await authProvider.setUserCredential(userId, 'github', {
291
+ token: 'github_token',
292
+ scopes: githubInfo.scopes,
293
+ });
294
+
295
+ // Mock Google
296
+ (global.fetch as any)
297
+ .mockResolvedValueOnce({
298
+ ok: true,
299
+ json: async () => ({
300
+ scope: 'https://www.googleapis.com/auth/calendar',
301
+ exp: Math.floor(Date.now() / 1000) + 3600,
302
+ }),
303
+ })
304
+ .mockResolvedValueOnce({
305
+ ok: true,
306
+ json: async () => ({
307
+ exp: Math.floor(Date.now() / 1000) + 3600,
308
+ }),
309
+ });
310
+
311
+ const googleInfo = await scopeChecker.getTokenInfo('google', 'google_token');
312
+ await authProvider.setUserCredential(userId, 'google', {
313
+ token: 'google_token',
314
+ scopes: googleInfo.scopes,
315
+ });
316
+
317
+ // List providers
318
+ const providers = await authProvider.listUserProviders(userId);
319
+
320
+ expect(providers).toEqual(expect.arrayContaining(['github', 'google']));
321
+ expect(providers.length).toBe(2);
322
+ });
323
+ });
324
+
325
+ describe('Token disconnection', () => {
326
+ it('should remove provider credentials', async () => {
327
+ const userId = 'user123';
328
+ const provider = 'github';
329
+
330
+ // Connect
331
+ await authProvider.setUserCredential(userId, provider, {
332
+ token: 'token',
333
+ scopes: ['repo'],
334
+ });
335
+
336
+ // Verify connected
337
+ const beforeDisconnect = await authProvider.getUserCredential(userId, provider);
338
+ expect(beforeDisconnect).not.toBeNull();
339
+
340
+ // Disconnect
341
+ await authProvider.deleteUserCredential(userId, provider);
342
+
343
+ // Verify disconnected
344
+ const afterDisconnect = await authProvider.getUserCredential(userId, provider);
345
+ expect(afterDisconnect).toBeNull();
346
+ });
347
+
348
+ it('should only remove specified provider', async () => {
349
+ const userId = 'user123';
350
+
351
+ // Connect multiple providers
352
+ await authProvider.setUserCredential(userId, 'github', {
353
+ token: 'github_token',
354
+ scopes: ['repo'],
355
+ });
356
+
357
+ await authProvider.setUserCredential(userId, 'google', {
358
+ token: 'google_token',
359
+ scopes: ['calendar'],
360
+ });
361
+
362
+ // Disconnect only GitHub
363
+ await authProvider.deleteUserCredential(userId, 'github');
364
+
365
+ // Verify GitHub is gone but Google remains
366
+ const githubCreds = await authProvider.getUserCredential(userId, 'github');
367
+ const googleCreds = await authProvider.getUserCredential(userId, 'google');
368
+
369
+ expect(githubCreds).toBeNull();
370
+ expect(googleCreds).not.toBeNull();
371
+ expect(googleCreds?.token).toBe('google_token');
372
+ });
373
+ });
374
+
375
+ describe('Scope caching behavior', () => {
376
+ it('should cache scope checks across requests', async () => {
377
+ const provider = 'github';
378
+ const token = 'gho_test_token';
379
+
380
+ // Mock single API call
381
+ (global.fetch as any).mockResolvedValueOnce({
382
+ ok: true,
383
+ headers: {
384
+ get: (name: string) => (name === 'X-OAuth-Scopes' ? 'repo' : null),
385
+ },
386
+ });
387
+
388
+ // Make multiple requests
389
+ await scopeChecker.checkScopes(provider, token);
390
+ await scopeChecker.checkScopes(provider, token);
391
+ await scopeChecker.checkScopes(provider, token);
392
+
393
+ // Should only call API once
394
+ expect(global.fetch).toHaveBeenCalledTimes(1);
395
+ });
396
+
397
+ it('should respect cache TTL', async () => {
398
+ vi.useFakeTimers();
399
+
400
+ const provider = 'github';
401
+ const token = 'gho_test_token';
402
+
403
+ // Mock API responses
404
+ (global.fetch as any).mockResolvedValue({
405
+ ok: true,
406
+ headers: {
407
+ get: (name: string) => (name === 'X-OAuth-Scopes' ? 'repo' : null),
408
+ },
409
+ });
410
+
411
+ // First call
412
+ await scopeChecker.checkScopes(provider, token, 1); // 1 second TTL
413
+ expect(global.fetch).toHaveBeenCalledTimes(1);
414
+
415
+ // Within TTL - should use cache
416
+ await scopeChecker.checkScopes(provider, token, 1);
417
+ expect(global.fetch).toHaveBeenCalledTimes(1);
418
+
419
+ // Advance time past TTL
420
+ vi.advanceTimersByTime(2000);
421
+
422
+ // Should make new API call
423
+ await scopeChecker.checkScopes(provider, token, 1);
424
+ expect(global.fetch).toHaveBeenCalledTimes(2);
425
+
426
+ vi.useRealTimers();
427
+ });
428
+ });
429
+
430
+ describe('Error handling', () => {
431
+ it('should handle network errors gracefully', async () => {
432
+ (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
433
+
434
+ await expect(scopeChecker.checkScopes('github', 'token')).rejects.toThrow('Network error');
435
+ });
436
+
437
+ it('should handle malformed API responses', async () => {
438
+ (global.fetch as any).mockResolvedValueOnce({
439
+ ok: true,
440
+ json: async () => {
441
+ throw new Error('Invalid JSON');
442
+ },
443
+ });
444
+
445
+ await expect(scopeChecker.checkScopes('google', 'token')).rejects.toThrow();
446
+ });
447
+
448
+ it('should handle missing provider gracefully', async () => {
449
+ const userId = 'user123';
450
+ const nonexistentProvider = 'nonexistent';
451
+
452
+ const creds = await authProvider.getUserCredential(userId, nonexistentProvider);
453
+
454
+ expect(creds).toBeNull();
455
+ });
456
+ });
457
+ });