mcp-oauth-provider 0.0.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 (44) hide show
  1. package/README.md +668 -0
  2. package/dist/__tests__/config.test.js +56 -0
  3. package/dist/__tests__/config.test.js.map +1 -0
  4. package/dist/__tests__/integration.test.js +341 -0
  5. package/dist/__tests__/integration.test.js.map +1 -0
  6. package/dist/__tests__/oauth-flow.test.js +201 -0
  7. package/dist/__tests__/oauth-flow.test.js.map +1 -0
  8. package/dist/__tests__/server.test.js +271 -0
  9. package/dist/__tests__/server.test.js.map +1 -0
  10. package/dist/__tests__/storage.test.js +256 -0
  11. package/dist/__tests__/storage.test.js.map +1 -0
  12. package/dist/client/config.js +30 -0
  13. package/dist/client/config.js.map +1 -0
  14. package/dist/client/factory.js +16 -0
  15. package/dist/client/factory.js.map +1 -0
  16. package/dist/client/index.js +237 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client/oauth-flow.js +73 -0
  19. package/dist/client/oauth-flow.js.map +1 -0
  20. package/dist/client/storage.js +237 -0
  21. package/dist/client/storage.js.map +1 -0
  22. package/dist/index.js +12 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/server/callback.js +164 -0
  25. package/dist/server/callback.js.map +1 -0
  26. package/dist/server/index.js +8 -0
  27. package/dist/server/index.js.map +1 -0
  28. package/dist/server/templates.js +245 -0
  29. package/dist/server/templates.js.map +1 -0
  30. package/package.json +66 -0
  31. package/src/__tests__/config.test.ts +78 -0
  32. package/src/__tests__/integration.test.ts +398 -0
  33. package/src/__tests__/oauth-flow.test.ts +276 -0
  34. package/src/__tests__/server.test.ts +391 -0
  35. package/src/__tests__/storage.test.ts +329 -0
  36. package/src/client/config.ts +134 -0
  37. package/src/client/factory.ts +19 -0
  38. package/src/client/index.ts +361 -0
  39. package/src/client/oauth-flow.ts +115 -0
  40. package/src/client/storage.ts +335 -0
  41. package/src/index.ts +31 -0
  42. package/src/server/callback.ts +257 -0
  43. package/src/server/index.ts +21 -0
  44. package/src/server/templates.ts +271 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/__tests__/oauth-flow.test.ts"],"sourcesContent":["import { describe, expect, mock, test } from 'bun:test';\nimport type { OAuthTokens } from '../client/config.js';\nimport {\n areTokensExpired,\n calculateTokenExpiry,\n refreshTokensWithRetry,\n} from '../client/oauth-flow.js';\n\ndescribe('oauth-flow utilities', () => {\n describe('areTokensExpired', () => {\n test('should return true for undefined tokens', () => {\n expect(areTokensExpired(undefined)).toBe(true);\n });\n\n test('should return true when tokenExpiryTime is in the past', () => {\n const tokens: OAuthTokens = {\n access_token: 'test-token',\n token_type: 'Bearer',\n expires_in: 3600,\n };\n const pastExpiryTime = Date.now() - 1000;\n\n expect(areTokensExpired(tokens, pastExpiryTime)).toBe(true);\n });\n\n test('should return true when tokenExpiryTime is within buffer period', () => {\n const tokens: OAuthTokens = {\n access_token: 'test-token',\n token_type: 'Bearer',\n expires_in: 3600,\n };\n // Expiry time 2 minutes in future (less than 5 minute buffer)\n const soonExpiryTime = Date.now() + 120 * 1000;\n\n expect(areTokensExpired(tokens, soonExpiryTime, 300)).toBe(true);\n });\n\n test('should return false when tokenExpiryTime is beyond buffer period', () => {\n const tokens: OAuthTokens = {\n access_token: 'test-token',\n token_type: 'Bearer',\n expires_in: 3600,\n };\n // Expiry time 10 minutes in future (beyond 5 minute buffer)\n const futureExpiryTime = Date.now() + 600 * 1000;\n\n expect(areTokensExpired(tokens, futureExpiryTime, 300)).toBe(false);\n });\n\n test('should return false when no expiry info and no tokenExpiryTime', () => {\n const tokens: OAuthTokens = {\n access_token: 'test-token',\n token_type: 'Bearer',\n };\n\n expect(areTokensExpired(tokens)).toBe(false);\n });\n\n test('should return true when expires_in is less than buffer', () => {\n const tokens: OAuthTokens = {\n access_token: 'test-token',\n token_type: 'Bearer',\n expires_in: 100, // 100 seconds\n };\n\n expect(areTokensExpired(tokens, undefined, 300)).toBe(true);\n });\n\n test('should return false when expires_in is greater than buffer', () => {\n const tokens: OAuthTokens = {\n access_token: 'test-token',\n token_type: 'Bearer',\n expires_in: 600, // 600 seconds\n };\n\n expect(areTokensExpired(tokens, undefined, 300)).toBe(false);\n });\n\n test('should use custom buffer period', () => {\n const tokens: OAuthTokens = {\n access_token: 'test-token',\n token_type: 'Bearer',\n expires_in: 150,\n };\n\n expect(areTokensExpired(tokens, undefined, 100)).toBe(false);\n expect(areTokensExpired(tokens, undefined, 200)).toBe(true);\n });\n });\n\n describe('calculateTokenExpiry', () => {\n test('should calculate correct expiry timestamp', () => {\n const expiresIn = 3600; // 1 hour\n const before = Date.now();\n const expiry = calculateTokenExpiry(expiresIn);\n const after = Date.now();\n const expectedMin = before + expiresIn * 1000;\n const expectedMax = after + expiresIn * 1000;\n\n expect(expiry).toBeGreaterThanOrEqual(expectedMin);\n expect(expiry).toBeLessThanOrEqual(expectedMax);\n });\n\n test('should handle different expiry values', () => {\n const testCases = [60, 300, 3600, 7200];\n\n for (const expiresIn of testCases) {\n const expiry = calculateTokenExpiry(expiresIn);\n const expectedApprox = Date.now() + expiresIn * 1000;\n // Allow 100ms tolerance\n\n expect(Math.abs(expiry - expectedApprox)).toBeLessThan(100);\n }\n });\n });\n\n describe('refreshTokensWithRetry', () => {\n test('should successfully refresh tokens on first attempt', async () => {\n const mockTokens: OAuthTokens = {\n access_token: 'new-access-token',\n token_type: 'Bearer',\n expires_in: 3600,\n refresh_token: 'new-refresh-token',\n };\n const mockFetch = mock(() =>\n Promise.resolve(\n new Response(JSON.stringify(mockTokens), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n )\n );\n const clientInfo = {\n client_id: 'test-client-id',\n client_secret: 'test-client-secret',\n };\n\n const result = await refreshTokensWithRetry(\n 'https://auth.example.com',\n clientInfo,\n 'old-refresh-token',\n undefined,\n 3,\n 100,\n mockFetch as unknown as typeof fetch\n );\n\n expect(result).toEqual(mockTokens);\n expect(mockFetch).toHaveBeenCalledTimes(1);\n });\n\n test('should retry on failure and eventually succeed', async () => {\n const mockTokens: OAuthTokens = {\n access_token: 'new-access-token',\n token_type: 'Bearer',\n expires_in: 3600,\n refresh_token: 'refresh-token', // Include the refresh token that was sent\n };\n let callCount = 0;\n const mockFetch = mock(() => {\n callCount++;\n if (callCount < 2) {\n return Promise.reject(new Error('Network error'));\n }\n\n return Promise.resolve(\n new Response(JSON.stringify(mockTokens), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n );\n });\n const clientInfo = { client_id: 'test-client-id' };\n\n const result = await refreshTokensWithRetry(\n 'https://auth.example.com',\n clientInfo,\n 'refresh-token',\n undefined,\n 3,\n 10, // Short delay for testing\n mockFetch as unknown as typeof fetch\n );\n\n expect(result).toEqual(mockTokens);\n expect(callCount).toBe(2);\n });\n\n test('should throw error after max retries', async () => {\n const mockFetch = mock(() => Promise.reject(new Error('Network error')));\n const clientInfo = { client_id: 'test-client-id' };\n\n await expect(\n refreshTokensWithRetry(\n 'https://auth.example.com',\n clientInfo,\n 'refresh-token',\n undefined,\n 3,\n 10,\n mockFetch as unknown as typeof fetch\n )\n ).rejects.toThrow('Token refresh failed after 3 attempts');\n\n expect(mockFetch).toHaveBeenCalledTimes(3);\n });\n\n test('should use exponential backoff for retries', async () => {\n const callTimes: number[] = [];\n const mockFetch = mock(() => {\n callTimes.push(Date.now());\n\n return Promise.reject(new Error('Network error'));\n });\n const clientInfo = { client_id: 'test-client-id' };\n const baseDelay = 50;\n\n try {\n await refreshTokensWithRetry(\n 'https://auth.example.com',\n clientInfo,\n 'refresh-token',\n undefined,\n 3,\n baseDelay,\n mockFetch as unknown as typeof fetch\n );\n } catch {\n // Expected to fail\n }\n\n // Should have made 3 attempts\n expect(callTimes.length).toBe(3);\n\n // Calculate delays between attempts\n const delay1 = callTimes[1]! - callTimes[0]!;\n const delay2 = callTimes[2]! - callTimes[1]!;\n\n // Verify exponential backoff (delays should increase)\n // First retry should be approximately baseDelay * 1\n expect(delay1).toBeGreaterThanOrEqual(baseDelay);\n // Second retry should be approximately baseDelay * 2 and greater than first\n expect(delay2).toBeGreaterThan(delay1);\n expect(delay2).toBeGreaterThanOrEqual(baseDelay * 2);\n });\n\n test('should pass custom addClientAuth function', async () => {\n const mockTokens: OAuthTokens = {\n access_token: 'new-token',\n token_type: 'Bearer',\n };\n const mockFetch = mock(() =>\n Promise.resolve(\n new Response(JSON.stringify(mockTokens), { status: 200 })\n )\n );\n const mockAddClientAuth = mock(() => {\n // Mock implementation\n });\n const clientInfo = { client_id: 'test-client-id' };\n\n await refreshTokensWithRetry(\n 'https://auth.example.com',\n clientInfo,\n 'refresh-token',\n mockAddClientAuth,\n 3,\n 10,\n mockFetch as unknown as typeof fetch\n );\n\n // The SDK's refreshAuthorization should have been called with addClientAuth\n expect(mockFetch).toHaveBeenCalled();\n });\n });\n});\n"],"names":["describe","expect","mock","test","areTokensExpired","calculateTokenExpiry","refreshTokensWithRetry","undefined","toBe","tokens","access_token","token_type","expires_in","pastExpiryTime","Date","now","soonExpiryTime","futureExpiryTime","expiresIn","before","expiry","after","expectedMin","expectedMax","toBeGreaterThanOrEqual","toBeLessThanOrEqual","testCases","expectedApprox","Math","abs","toBeLessThan","mockTokens","refresh_token","mockFetch","Promise","resolve","Response","JSON","stringify","status","headers","clientInfo","client_id","client_secret","result","toEqual","toHaveBeenCalledTimes","callCount","reject","Error","rejects","toThrow","callTimes","push","baseDelay","length","delay1","delay2","toBeGreaterThan","mockAddClientAuth","toHaveBeenCalled"],"mappings":"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"}
@@ -0,0 +1,271 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { OAuthCallbackServer } from '../server/callback.js';
3
+ describe('OAuthCallbackServer', ()=>{
4
+ let server;
5
+ let testPort = 9876;
6
+ const testHostname = 'localhost';
7
+ const startServer = async ()=>{
8
+ if (server) {
9
+ await server.stop();
10
+ }
11
+ // Use a new port for each test to avoid port conflicts
12
+ testPort++;
13
+ server = new OAuthCallbackServer();
14
+ return server.start({
15
+ port: testPort,
16
+ hostname: testHostname
17
+ });
18
+ };
19
+ afterEach(async ()=>{
20
+ if (server) {
21
+ try {
22
+ await server.stop();
23
+ } catch (error) {
24
+ // Ignore "Server stopped" errors from rejected promises
25
+ if (error instanceof Error && !error.message.includes('Server stopped')) {
26
+ throw error;
27
+ }
28
+ }
29
+ }
30
+ });
31
+ describe('server lifecycle', ()=>{
32
+ test('should start server successfully', async ()=>{
33
+ await startServer();
34
+ // Verify server is running by making a request
35
+ const response = await fetch(`http://${testHostname}:${testPort}/unknown`);
36
+ expect(response.status).toBe(404);
37
+ });
38
+ test('should stop server successfully', async ()=>{
39
+ await startServer();
40
+ expect(server.isRunning()).toBe(true);
41
+ await server.stop();
42
+ expect(server.isRunning()).toBe(false);
43
+ });
44
+ test('should throw error when starting already running server', async ()=>{
45
+ await startServer();
46
+ await expect(server.start({
47
+ port: testPort + 1,
48
+ hostname: testHostname
49
+ })).rejects.toThrow('Server is already running');
50
+ });
51
+ test('should handle abort signal during start', async ()=>{
52
+ const abortController = new AbortController();
53
+ abortController.abort();
54
+ await expect(server.start({
55
+ port: testPort,
56
+ hostname: testHostname,
57
+ signal: abortController.signal
58
+ })).rejects.toThrow('Operation aborted');
59
+ });
60
+ test('should stop server when abort signal is triggered', async ()=>{
61
+ const abortController = new AbortController();
62
+ await server.start({
63
+ port: testPort,
64
+ hostname: testHostname,
65
+ signal: abortController.signal
66
+ });
67
+ expect(server.isRunning()).toBe(true);
68
+ // Trigger abort
69
+ abortController.abort();
70
+ // Give it a moment to cleanup
71
+ await new Promise((resolve)=>setTimeout(resolve, 100));
72
+ expect(server.isRunning()).toBe(false);
73
+ });
74
+ });
75
+ describe('callback handling', ()=>{
76
+ test('should handle successful OAuth callback', async ()=>{
77
+ await startServer();
78
+ const callbackPath = '/callback/success';
79
+ const expectedCode = 'test-auth-code-12345';
80
+ const expectedState = 'test-state-67890';
81
+ // Start waiting for callback first
82
+ const callbackPromise = server.waitForCallback(callbackPath, 5000);
83
+ // Delay to ensure listener registration completes
84
+ await new Promise((resolve)=>setTimeout(resolve, 10));
85
+ // Make the HTTP request
86
+ const fetchPromise = fetch(`http://${testHostname}:${testPort}${callbackPath}?code=${expectedCode}&state=${expectedState}`);
87
+ const [result, response] = await Promise.all([
88
+ callbackPromise,
89
+ fetchPromise
90
+ ]);
91
+ // Should get success page
92
+ expect(response.status).toBe(200);
93
+ const html = await response.text();
94
+ expect(html).toContain('Authorization Successful');
95
+ // Should resolve with callback data
96
+ expect(result).toMatchObject({
97
+ code: expectedCode,
98
+ state: expectedState
99
+ });
100
+ });
101
+ test('should handle OAuth error callback', async ()=>{
102
+ await startServer();
103
+ const callbackPath = '/callback/error';
104
+ const expectedError = 'access_denied';
105
+ const expectedDescription = 'User denied authorization';
106
+ const callbackPromise = server.waitForCallback(callbackPath, 5000);
107
+ await new Promise((resolve)=>setTimeout(resolve, 10));
108
+ const fetchPromise = fetch(`http://${testHostname}:${testPort}${callbackPath}?error=${expectedError}&error_description=${encodeURIComponent(expectedDescription)}`);
109
+ const [result, response] = await Promise.all([
110
+ callbackPromise,
111
+ fetchPromise
112
+ ]);
113
+ expect(response.status).toBe(400);
114
+ const html = await response.text();
115
+ expect(html).toContain('Authorization Failed');
116
+ expect(result).toMatchObject({
117
+ error: expectedError,
118
+ error_description: expectedDescription
119
+ });
120
+ });
121
+ test('should timeout when callback takes too long', async ()=>{
122
+ await startServer();
123
+ const callbackPath = '/callback/timeout';
124
+ // Wait with very short timeout
125
+ await expect(server.waitForCallback(callbackPath, 100)).rejects.toThrow('OAuth callback timeout');
126
+ });
127
+ test('should handle multiple callback parameters', async ()=>{
128
+ await startServer();
129
+ const callbackPath = '/oauth/callback';
130
+ const params = {
131
+ code: 'auth-code',
132
+ state: 'state-value',
133
+ scope: 'openid profile email',
134
+ session_state: 'session-123'
135
+ };
136
+ const queryString = new URLSearchParams(params).toString();
137
+ const callbackPromise = server.waitForCallback(callbackPath, 5000);
138
+ await new Promise((resolve)=>setTimeout(resolve, 10));
139
+ const fetchPromise = fetch(`http://${testHostname}:${testPort}${callbackPath}?${queryString}`);
140
+ const [result] = await Promise.all([
141
+ callbackPromise,
142
+ fetchPromise
143
+ ]);
144
+ expect(result).toMatchObject(params);
145
+ });
146
+ test('should return 404 for unknown paths', async ()=>{
147
+ await startServer();
148
+ const response = await fetch(`http://${testHostname}:${testPort}/unknown-path`);
149
+ expect(response.status).toBe(404);
150
+ const text = await response.text();
151
+ expect(text).toBe('Not Found');
152
+ });
153
+ test('should cleanup listeners after successful callback', async ()=>{
154
+ await startServer();
155
+ const callbackPath = '/callback/cleanup';
156
+ // First callback
157
+ await Promise.all([
158
+ server.waitForCallback(callbackPath, 5000),
159
+ fetch(`http://${testHostname}:${testPort}${callbackPath}?code=code1&state=state1`)
160
+ ]);
161
+ // Second callback to same path should work
162
+ const [result2] = await Promise.all([
163
+ server.waitForCallback(callbackPath, 5000),
164
+ fetch(`http://${testHostname}:${testPort}${callbackPath}?code=code2&state=state2`)
165
+ ]);
166
+ expect(result2.code).toBe('code2');
167
+ });
168
+ test('should use custom success HTML template', async ()=>{
169
+ await startServer();
170
+ const customServer = new OAuthCallbackServer();
171
+ await customServer.start({
172
+ hostname: testHostname,
173
+ port: testPort + 1,
174
+ successHtml: '<html><body>Custom Success!</body></html>'
175
+ });
176
+ const callbackPath = '/callback/custom-success';
177
+ const [, response] = await Promise.all([
178
+ customServer.waitForCallback(callbackPath, 5000),
179
+ fetch(`http://${testHostname}:${testPort + 1}${callbackPath}?code=test`)
180
+ ]);
181
+ const html = await response.text();
182
+ expect(html).toContain('Custom Success!');
183
+ await customServer.stop();
184
+ });
185
+ test('should use custom error HTML template', async ()=>{
186
+ await startServer();
187
+ const customServer = new OAuthCallbackServer();
188
+ await customServer.start({
189
+ hostname: testHostname,
190
+ port: testPort + 2,
191
+ errorHtml: '<html><body>Custom Error!</body></html>'
192
+ });
193
+ const callbackPath = '/callback/custom-error';
194
+ const [, response] = await Promise.all([
195
+ customServer.waitForCallback(callbackPath, 5000),
196
+ fetch(`http://${testHostname}:${testPort + 2}${callbackPath}?error=test`)
197
+ ]);
198
+ const html = await response.text();
199
+ expect(html).toContain('Custom Error!');
200
+ await customServer.stop();
201
+ });
202
+ test('should call onRequest callback', async ()=>{
203
+ await startServer();
204
+ await server.stop();
205
+ const requests = [];
206
+ // Use a new port for this second server instance
207
+ testPort++;
208
+ await server.start({
209
+ port: testPort,
210
+ hostname: testHostname,
211
+ onRequest: (req)=>requests.push(req)
212
+ });
213
+ await fetch(`http://${testHostname}:${testPort}/test`);
214
+ expect(requests).toHaveLength(1);
215
+ expect(requests[0]).toBeInstanceOf(Request);
216
+ });
217
+ });
218
+ describe('edge cases', ()=>{
219
+ test('should handle callback with no query parameters', async ()=>{
220
+ await startServer();
221
+ const callbackPath = '/callback/no-params';
222
+ // Start waiting for callback first to ensure listener is registered
223
+ const callbackPromise = server.waitForCallback(callbackPath, 5000);
224
+ // Small delay to ensure listener registration completes
225
+ await new Promise((resolve)=>setImmediate(resolve));
226
+ // Now make the HTTP request
227
+ const fetchPromise = fetch(`http://${testHostname}:${testPort}${callbackPath}`);
228
+ const [result] = await Promise.all([
229
+ callbackPromise,
230
+ fetchPromise
231
+ ]);
232
+ expect(result).toEqual({});
233
+ });
234
+ test('should handle special characters in parameters', async ()=>{
235
+ await startServer();
236
+ const callbackPath = '/callback/special-chars';
237
+ const specialState = 'state-with-special-chars_123';
238
+ const callbackPromise = server.waitForCallback(callbackPath, 5000);
239
+ await new Promise((resolve)=>setTimeout(resolve, 10));
240
+ const fetchPromise = fetch(`http://${testHostname}:${testPort}${callbackPath}?code=test&state=${encodeURIComponent(specialState)}`);
241
+ const [result] = await Promise.all([
242
+ callbackPromise,
243
+ fetchPromise
244
+ ]);
245
+ expect(result.state).toBe(specialState);
246
+ });
247
+ test('should handle concurrent callbacks to different paths', async ()=>{
248
+ await startServer();
249
+ const path1 = '/callback1';
250
+ const path2 = '/callback2';
251
+ // Register both listeners first
252
+ const promise1 = server.waitForCallback(path1, 5000);
253
+ const promise2 = server.waitForCallback(path2, 5000);
254
+ // Use a longer delay to ensure both listeners are registered
255
+ await new Promise((resolve)=>setTimeout(resolve, 10));
256
+ // Now make both HTTP requests
257
+ const fetch1 = fetch(`http://${testHostname}:${testPort}${path1}?code=code1&state=state1`);
258
+ const fetch2 = fetch(`http://${testHostname}:${testPort}${path2}?code=code2&state=state2`);
259
+ const [result1, result2] = await Promise.all([
260
+ promise1,
261
+ promise2,
262
+ fetch1,
263
+ fetch2
264
+ ]);
265
+ expect(result1.code).toBe('code1');
266
+ expect(result2.code).toBe('code2');
267
+ });
268
+ });
269
+ });
270
+
271
+ //# sourceMappingURL=server.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/__tests__/server.test.ts"],"sourcesContent":["import { afterEach, describe, expect, test } from 'bun:test';\nimport { OAuthCallbackServer } from '../server/callback.js';\n\ndescribe('OAuthCallbackServer', () => {\n let server: OAuthCallbackServer;\n let testPort = 9876;\n const testHostname = 'localhost';\n\n const startServer = async () => {\n if (server) {\n await server.stop();\n }\n\n // Use a new port for each test to avoid port conflicts\n testPort++;\n\n server = new OAuthCallbackServer();\n\n return server.start({\n port: testPort,\n hostname: testHostname,\n });\n };\n\n afterEach(async () => {\n if (server) {\n try {\n await server.stop();\n } catch (error) {\n // Ignore \"Server stopped\" errors from rejected promises\n if (\n error instanceof Error &&\n !error.message.includes('Server stopped')\n ) {\n throw error;\n }\n }\n }\n });\n\n describe('server lifecycle', () => {\n test('should start server successfully', async () => {\n await startServer();\n\n // Verify server is running by making a request\n const response = await fetch(\n `http://${testHostname}:${testPort}/unknown`\n );\n\n expect(response.status).toBe(404);\n });\n\n test('should stop server successfully', async () => {\n await startServer();\n\n expect(server.isRunning()).toBe(true);\n\n await server.stop();\n\n expect(server.isRunning()).toBe(false);\n });\n\n test('should throw error when starting already running server', async () => {\n await startServer();\n\n await expect(\n server.start({\n port: testPort + 1,\n hostname: testHostname,\n })\n ).rejects.toThrow('Server is already running');\n });\n\n test('should handle abort signal during start', async () => {\n const abortController = new AbortController();\n\n abortController.abort();\n\n await expect(\n server.start({\n port: testPort,\n hostname: testHostname,\n signal: abortController.signal,\n })\n ).rejects.toThrow('Operation aborted');\n });\n\n test('should stop server when abort signal is triggered', async () => {\n const abortController = new AbortController();\n\n await server.start({\n port: testPort,\n hostname: testHostname,\n signal: abortController.signal,\n });\n\n expect(server.isRunning()).toBe(true);\n\n // Trigger abort\n abortController.abort();\n\n // Give it a moment to cleanup\n await new Promise(resolve => setTimeout(resolve, 100));\n\n expect(server.isRunning()).toBe(false);\n });\n });\n\n describe('callback handling', () => {\n test('should handle successful OAuth callback', async () => {\n await startServer();\n const callbackPath = '/callback/success';\n const expectedCode = 'test-auth-code-12345';\n const expectedState = 'test-state-67890';\n\n // Start waiting for callback first\n const callbackPromise = server.waitForCallback(callbackPath, 5000);\n\n // Delay to ensure listener registration completes\n await new Promise(resolve => setTimeout(resolve, 10));\n\n // Make the HTTP request\n const fetchPromise = fetch(\n `http://${testHostname}:${testPort}${callbackPath}?code=${expectedCode}&state=${expectedState}`\n );\n\n const [result, response] = await Promise.all([\n callbackPromise,\n fetchPromise,\n ]);\n\n // Should get success page\n expect(response.status).toBe(200);\n\n const html = await response.text();\n\n expect(html).toContain('Authorization Successful');\n\n // Should resolve with callback data\n expect(result).toMatchObject({\n code: expectedCode,\n state: expectedState,\n });\n });\n\n test('should handle OAuth error callback', async () => {\n await startServer();\n const callbackPath = '/callback/error';\n const expectedError = 'access_denied';\n const expectedDescription = 'User denied authorization';\n\n const callbackPromise = server.waitForCallback(callbackPath, 5000);\n\n await new Promise(resolve => setTimeout(resolve, 10));\n\n const fetchPromise = fetch(\n `http://${testHostname}:${testPort}${callbackPath}?error=${expectedError}&error_description=${encodeURIComponent(expectedDescription)}`\n );\n\n const [result, response] = await Promise.all([\n callbackPromise,\n fetchPromise,\n ]);\n\n expect(response.status).toBe(400);\n\n const html = await response.text();\n\n expect(html).toContain('Authorization Failed');\n\n expect(result).toMatchObject({\n error: expectedError,\n error_description: expectedDescription,\n });\n });\n\n test('should timeout when callback takes too long', async () => {\n await startServer();\n const callbackPath = '/callback/timeout';\n\n // Wait with very short timeout\n await expect(server.waitForCallback(callbackPath, 100)).rejects.toThrow(\n 'OAuth callback timeout'\n );\n });\n\n test('should handle multiple callback parameters', async () => {\n await startServer();\n const callbackPath = '/oauth/callback';\n const params = {\n code: 'auth-code',\n state: 'state-value',\n scope: 'openid profile email',\n session_state: 'session-123',\n };\n const queryString = new URLSearchParams(params).toString();\n\n const callbackPromise = server.waitForCallback(callbackPath, 5000);\n\n await new Promise(resolve => setTimeout(resolve, 10));\n\n const fetchPromise = fetch(\n `http://${testHostname}:${testPort}${callbackPath}?${queryString}`\n );\n\n const [result] = await Promise.all([callbackPromise, fetchPromise]);\n\n expect(result).toMatchObject(params);\n });\n\n test('should return 404 for unknown paths', async () => {\n await startServer();\n const response = await fetch(\n `http://${testHostname}:${testPort}/unknown-path`\n );\n\n expect(response.status).toBe(404);\n\n const text = await response.text();\n\n expect(text).toBe('Not Found');\n });\n\n test('should cleanup listeners after successful callback', async () => {\n await startServer();\n const callbackPath = '/callback/cleanup';\n\n // First callback\n await Promise.all([\n server.waitForCallback(callbackPath, 5000),\n fetch(\n `http://${testHostname}:${testPort}${callbackPath}?code=code1&state=state1`\n ),\n ]);\n\n // Second callback to same path should work\n const [result2] = await Promise.all([\n server.waitForCallback(callbackPath, 5000),\n fetch(\n `http://${testHostname}:${testPort}${callbackPath}?code=code2&state=state2`\n ),\n ]);\n\n expect(result2.code).toBe('code2');\n });\n\n test('should use custom success HTML template', async () => {\n await startServer();\n const customServer = new OAuthCallbackServer();\n\n await customServer.start({\n hostname: testHostname,\n port: testPort + 1,\n successHtml: '<html><body>Custom Success!</body></html>',\n });\n\n const callbackPath = '/callback/custom-success';\n\n const [, response] = await Promise.all([\n customServer.waitForCallback(callbackPath, 5000),\n fetch(\n `http://${testHostname}:${testPort + 1}${callbackPath}?code=test`\n ),\n ]);\n\n const html = await response.text();\n\n expect(html).toContain('Custom Success!');\n\n await customServer.stop();\n });\n\n test('should use custom error HTML template', async () => {\n await startServer();\n const customServer = new OAuthCallbackServer();\n\n await customServer.start({\n hostname: testHostname,\n port: testPort + 2,\n errorHtml: '<html><body>Custom Error!</body></html>',\n });\n\n const callbackPath = '/callback/custom-error';\n\n const [, response] = await Promise.all([\n customServer.waitForCallback(callbackPath, 5000),\n fetch(\n `http://${testHostname}:${testPort + 2}${callbackPath}?error=test`\n ),\n ]);\n\n const html = await response.text();\n\n expect(html).toContain('Custom Error!');\n\n await customServer.stop();\n });\n\n test('should call onRequest callback', async () => {\n await startServer();\n await server.stop();\n\n const requests: Request[] = [];\n\n // Use a new port for this second server instance\n testPort++;\n\n await server.start({\n port: testPort,\n hostname: testHostname,\n onRequest: req => requests.push(req),\n });\n\n await fetch(`http://${testHostname}:${testPort}/test`);\n\n expect(requests).toHaveLength(1);\n expect(requests[0]).toBeInstanceOf(Request);\n });\n });\n\n describe('edge cases', () => {\n test('should handle callback with no query parameters', async () => {\n await startServer();\n const callbackPath = '/callback/no-params';\n\n // Start waiting for callback first to ensure listener is registered\n const callbackPromise = server.waitForCallback(callbackPath, 5000);\n\n // Small delay to ensure listener registration completes\n await new Promise(resolve => setImmediate(resolve));\n\n // Now make the HTTP request\n const fetchPromise = fetch(\n `http://${testHostname}:${testPort}${callbackPath}`\n );\n\n const [result] = await Promise.all([callbackPromise, fetchPromise]);\n\n expect(result).toEqual({});\n });\n\n test('should handle special characters in parameters', async () => {\n await startServer();\n const callbackPath = '/callback/special-chars';\n const specialState = 'state-with-special-chars_123';\n\n const callbackPromise = server.waitForCallback(callbackPath, 5000);\n\n await new Promise(resolve => setTimeout(resolve, 10));\n\n const fetchPromise = fetch(\n `http://${testHostname}:${testPort}${callbackPath}?code=test&state=${encodeURIComponent(specialState)}`\n );\n\n const [result] = await Promise.all([callbackPromise, fetchPromise]);\n\n expect(result.state).toBe(specialState);\n });\n\n test('should handle concurrent callbacks to different paths', async () => {\n await startServer();\n const path1 = '/callback1';\n const path2 = '/callback2';\n\n // Register both listeners first\n const promise1 = server.waitForCallback(path1, 5000);\n const promise2 = server.waitForCallback(path2, 5000);\n\n // Use a longer delay to ensure both listeners are registered\n await new Promise(resolve => setTimeout(resolve, 10));\n\n // Now make both HTTP requests\n const fetch1 = fetch(\n `http://${testHostname}:${testPort}${path1}?code=code1&state=state1`\n );\n const fetch2 = fetch(\n `http://${testHostname}:${testPort}${path2}?code=code2&state=state2`\n );\n\n const [result1, result2] = await Promise.all([\n promise1,\n promise2,\n fetch1,\n fetch2,\n ]);\n\n expect(result1.code).toBe('code1');\n expect(result2.code).toBe('code2');\n });\n });\n});\n"],"names":["afterEach","describe","expect","test","OAuthCallbackServer","server","testPort","testHostname","startServer","stop","start","port","hostname","error","Error","message","includes","response","fetch","status","toBe","isRunning","rejects","toThrow","abortController","AbortController","abort","signal","Promise","resolve","setTimeout","callbackPath","expectedCode","expectedState","callbackPromise","waitForCallback","fetchPromise","result","all","html","text","toContain","toMatchObject","code","state","expectedError","expectedDescription","encodeURIComponent","error_description","params","scope","session_state","queryString","URLSearchParams","toString","result2","customServer","successHtml","errorHtml","requests","onRequest","req","push","toHaveLength","toBeInstanceOf","Request","setImmediate","toEqual","specialState","path1","path2","promise1","promise2","fetch1","fetch2","result1"],"mappings":"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"}
@@ -0,0 +1,256 @@
1
+ import { beforeEach, describe, expect, test } from 'bun:test';
2
+ import { FileStorage, MemoryStorage, OAuthStorage, createStorageAdapter } from '../client/storage.js';
3
+ describe('MemoryStorage', ()=>{
4
+ let storage;
5
+ beforeEach(()=>{
6
+ storage = new MemoryStorage();
7
+ });
8
+ test('should store and retrieve values', async ()=>{
9
+ await storage.set('test-key', 'test-value');
10
+ const value = await storage.get('test-key');
11
+ expect(value).toBe('test-value');
12
+ });
13
+ test('should return undefined for missing keys', async ()=>{
14
+ const value = await storage.get('non-existent');
15
+ expect(value).toBeUndefined();
16
+ });
17
+ test('should delete values', async ()=>{
18
+ await storage.set('test-key', 'test-value');
19
+ await storage.delete('test-key');
20
+ const value = await storage.get('test-key');
21
+ expect(value).toBeUndefined();
22
+ });
23
+ test('should clear all data', async ()=>{
24
+ await storage.set('key1', 'value1');
25
+ await storage.set('key2', 'value2');
26
+ storage.clear();
27
+ const value1 = await storage.get('key1');
28
+ const value2 = await storage.get('key2');
29
+ expect(value1).toBeUndefined();
30
+ expect(value2).toBeUndefined();
31
+ });
32
+ test('should handle multiple values', async ()=>{
33
+ await storage.set('key1', 'value1');
34
+ await storage.set('key2', 'value2');
35
+ await storage.set('key3', 'value3');
36
+ const value1 = await storage.get('key1');
37
+ const value2 = await storage.get('key2');
38
+ const value3 = await storage.get('key3');
39
+ expect(value1).toBe('value1');
40
+ expect(value2).toBe('value2');
41
+ expect(value3).toBe('value3');
42
+ });
43
+ });
44
+ describe('FileStorage', ()=>{
45
+ let storage;
46
+ const testPath = './test-oauth-data';
47
+ beforeEach(async ()=>{
48
+ storage = new FileStorage(testPath);
49
+ await storage.clear(); // Clean up before each test
50
+ });
51
+ test('should store and retrieve values', async ()=>{
52
+ await storage.set('test-key', 'test-value');
53
+ const value = await storage.get('test-key');
54
+ expect(value).toBe('test-value');
55
+ });
56
+ test('should return undefined for missing keys', async ()=>{
57
+ const value = await storage.get('non-existent');
58
+ expect(value).toBeUndefined();
59
+ });
60
+ test('should delete values', async ()=>{
61
+ await storage.set('test-key', 'test-value');
62
+ await storage.delete('test-key');
63
+ const value = await storage.get('test-key');
64
+ expect(value).toBeUndefined();
65
+ });
66
+ test('should sanitize keys for filenames', async ()=>{
67
+ const unsafeKey = 'test/key:with@special#chars';
68
+ await storage.set(unsafeKey, 'test-value');
69
+ const value = await storage.get(unsafeKey);
70
+ expect(value).toBe('test-value');
71
+ });
72
+ test('should handle multiple values', async ()=>{
73
+ await storage.set('key1', 'value1');
74
+ await storage.set('key2', 'value2');
75
+ const value1 = await storage.get('key1');
76
+ const value2 = await storage.get('key2');
77
+ expect(value1).toBe('value1');
78
+ expect(value2).toBe('value2');
79
+ });
80
+ test('should clear all data', async ()=>{
81
+ await storage.set('key1', 'value1');
82
+ await storage.set('key2', 'value2');
83
+ await storage.clear();
84
+ const value1 = await storage.get('key1');
85
+ const value2 = await storage.get('key2');
86
+ expect(value1).toBeUndefined();
87
+ expect(value2).toBeUndefined();
88
+ });
89
+ });
90
+ describe('createStorageAdapter', ()=>{
91
+ test('should create MemoryStorage by default', ()=>{
92
+ const storage = createStorageAdapter();
93
+ expect(storage).toBeInstanceOf(MemoryStorage);
94
+ });
95
+ test('should create MemoryStorage when type is "memory"', ()=>{
96
+ const storage = createStorageAdapter('memory');
97
+ expect(storage).toBeInstanceOf(MemoryStorage);
98
+ });
99
+ test('should create FileStorage when type is "file"', ()=>{
100
+ const storage = createStorageAdapter('file');
101
+ expect(storage).toBeInstanceOf(FileStorage);
102
+ });
103
+ test('should use custom path for FileStorage', async ()=>{
104
+ const customPath = './custom-oauth-path';
105
+ const storage = createStorageAdapter('file', {
106
+ path: customPath
107
+ });
108
+ expect(storage).toBeInstanceOf(FileStorage);
109
+ // Clean up
110
+ if (storage instanceof FileStorage) {
111
+ await storage.clear();
112
+ }
113
+ });
114
+ });
115
+ describe('OAuthStorage', ()=>{
116
+ let storage;
117
+ let oauthStorage;
118
+ const sessionId = 'test-session-id';
119
+ beforeEach(()=>{
120
+ storage = new MemoryStorage();
121
+ oauthStorage = new OAuthStorage(storage, sessionId);
122
+ });
123
+ describe('token management', ()=>{
124
+ test('should save and retrieve tokens', async ()=>{
125
+ const tokens = {
126
+ access_token: 'test-access-token',
127
+ token_type: 'Bearer',
128
+ expires_in: 3600,
129
+ refresh_token: 'test-refresh-token'
130
+ };
131
+ await oauthStorage.saveTokens(tokens);
132
+ const retrieved = await oauthStorage.getTokens();
133
+ expect(retrieved).toBeDefined();
134
+ expect(retrieved?.access_token).toBe(tokens.access_token);
135
+ expect(retrieved?.token_type).toBe(tokens.token_type);
136
+ expect(retrieved?.refresh_token).toBe(tokens.refresh_token);
137
+ // expires_in should be close to original value (within 5 seconds due to time passing)
138
+ expect(retrieved?.expires_in).toBeGreaterThanOrEqual(3595);
139
+ expect(retrieved?.expires_in).toBeLessThanOrEqual(3600);
140
+ });
141
+ test('should return undefined when no tokens exist', async ()=>{
142
+ const tokens = await oauthStorage.getTokens();
143
+ expect(tokens).toBeUndefined();
144
+ });
145
+ test('should clear tokens', async ()=>{
146
+ const tokens = {
147
+ access_token: 'test-access-token',
148
+ token_type: 'Bearer'
149
+ };
150
+ await oauthStorage.saveTokens(tokens);
151
+ await oauthStorage.clearTokens();
152
+ const retrieved = await oauthStorage.getTokens();
153
+ expect(retrieved).toBeUndefined();
154
+ });
155
+ test('should handle tokens without optional fields', async ()=>{
156
+ const tokens = {
157
+ access_token: 'test-access-token',
158
+ token_type: 'Bearer'
159
+ };
160
+ await oauthStorage.saveTokens(tokens);
161
+ const retrieved = await oauthStorage.getTokens();
162
+ expect(retrieved).toEqual(tokens);
163
+ });
164
+ });
165
+ describe('client info management', ()=>{
166
+ test('should save and retrieve client info', async ()=>{
167
+ const clientInfo = {
168
+ client_id: 'test-client-id',
169
+ client_secret: 'test-client-secret',
170
+ redirect_uris: [
171
+ 'http://localhost:8080/callback'
172
+ ]
173
+ };
174
+ await oauthStorage.saveClientInfo(clientInfo);
175
+ const retrieved = await oauthStorage.getClientInfo();
176
+ expect(retrieved).toEqual(clientInfo);
177
+ });
178
+ test('should return undefined when no client info exists', async ()=>{
179
+ const clientInfo = await oauthStorage.getClientInfo();
180
+ expect(clientInfo).toBeUndefined();
181
+ });
182
+ test('should handle client info without secret', async ()=>{
183
+ const clientInfo = {
184
+ client_id: 'test-client-id',
185
+ redirect_uris: [
186
+ 'http://localhost:8080/callback'
187
+ ]
188
+ };
189
+ await oauthStorage.saveClientInfo(clientInfo);
190
+ const retrieved = await oauthStorage.getClientInfo();
191
+ expect(retrieved).toEqual(clientInfo);
192
+ });
193
+ });
194
+ describe('code verifier management', ()=>{
195
+ test('should save and retrieve code verifier', async ()=>{
196
+ const verifier = 'test-code-verifier-12345';
197
+ await oauthStorage.saveCodeVerifier(verifier);
198
+ const retrieved = await oauthStorage.getCodeVerifier();
199
+ expect(retrieved).toBe(verifier);
200
+ });
201
+ test('should return undefined when no code verifier exists', async ()=>{
202
+ const verifier = await oauthStorage.getCodeVerifier();
203
+ expect(verifier).toBeUndefined();
204
+ });
205
+ test('should clear code verifier', async ()=>{
206
+ const verifier = 'test-code-verifier-12345';
207
+ await oauthStorage.saveCodeVerifier(verifier);
208
+ await oauthStorage.clearCodeVerifier();
209
+ const retrieved = await oauthStorage.getCodeVerifier();
210
+ expect(retrieved).toBeUndefined();
211
+ });
212
+ });
213
+ describe('session isolation', ()=>{
214
+ test('should isolate tokens between sessions', async ()=>{
215
+ const session1 = new OAuthStorage(storage, 'session-1');
216
+ const session2 = new OAuthStorage(storage, 'session-2');
217
+ const tokens1 = {
218
+ access_token: 'token-1',
219
+ token_type: 'Bearer'
220
+ };
221
+ const tokens2 = {
222
+ access_token: 'token-2',
223
+ token_type: 'Bearer'
224
+ };
225
+ await session1.saveTokens(tokens1);
226
+ await session2.saveTokens(tokens2);
227
+ const retrieved1 = await session1.getTokens();
228
+ const retrieved2 = await session2.getTokens();
229
+ expect(retrieved1).toEqual(tokens1);
230
+ expect(retrieved2).toEqual(tokens2);
231
+ });
232
+ test('should correctly calculate expires_in over time', async ()=>{
233
+ const tokens = {
234
+ access_token: 'test-access-token',
235
+ token_type: 'Bearer',
236
+ expires_in: 3600,
237
+ refresh_token: 'test-refresh-token'
238
+ };
239
+ // Save tokens
240
+ await oauthStorage.saveTokens(tokens);
241
+ // Retrieve immediately - should be close to 3600
242
+ const retrieved1 = await oauthStorage.getTokens();
243
+ expect(retrieved1?.expires_in).toBeGreaterThanOrEqual(3595);
244
+ expect(retrieved1?.expires_in).toBeLessThanOrEqual(3600);
245
+ // Wait 2 seconds
246
+ await new Promise((resolve)=>setTimeout(resolve, 2000));
247
+ // Retrieve again - should be approximately 2 seconds less
248
+ const retrieved2 = await oauthStorage.getTokens();
249
+ expect(retrieved2?.expires_in).toBeGreaterThanOrEqual(3593);
250
+ expect(retrieved2?.expires_in).toBeLessThanOrEqual(3598);
251
+ expect(retrieved2?.expires_in).toBeLessThan(retrieved1.expires_in);
252
+ });
253
+ });
254
+ });
255
+
256
+ //# sourceMappingURL=storage.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/__tests__/storage.test.ts"],"sourcesContent":["import { beforeEach, describe, expect, test } from 'bun:test';\nimport type { OAuthTokens } from '../client/config.js';\nimport {\n FileStorage,\n MemoryStorage,\n OAuthStorage,\n createStorageAdapter,\n} from '../client/storage.js';\n\ndescribe('MemoryStorage', () => {\n let storage: MemoryStorage;\n\n beforeEach(() => {\n storage = new MemoryStorage();\n });\n\n test('should store and retrieve values', async () => {\n await storage.set('test-key', 'test-value');\n const value = await storage.get('test-key');\n\n expect(value).toBe('test-value');\n });\n\n test('should return undefined for missing keys', async () => {\n const value = await storage.get('non-existent');\n\n expect(value).toBeUndefined();\n });\n\n test('should delete values', async () => {\n await storage.set('test-key', 'test-value');\n await storage.delete('test-key');\n const value = await storage.get('test-key');\n\n expect(value).toBeUndefined();\n });\n\n test('should clear all data', async () => {\n await storage.set('key1', 'value1');\n await storage.set('key2', 'value2');\n storage.clear();\n const value1 = await storage.get('key1');\n const value2 = await storage.get('key2');\n\n expect(value1).toBeUndefined();\n expect(value2).toBeUndefined();\n });\n\n test('should handle multiple values', async () => {\n await storage.set('key1', 'value1');\n await storage.set('key2', 'value2');\n await storage.set('key3', 'value3');\n const value1 = await storage.get('key1');\n const value2 = await storage.get('key2');\n const value3 = await storage.get('key3');\n\n expect(value1).toBe('value1');\n expect(value2).toBe('value2');\n expect(value3).toBe('value3');\n });\n});\n\ndescribe('FileStorage', () => {\n let storage: FileStorage;\n const testPath = './test-oauth-data';\n\n beforeEach(async () => {\n storage = new FileStorage(testPath);\n await storage.clear(); // Clean up before each test\n });\n\n test('should store and retrieve values', async () => {\n await storage.set('test-key', 'test-value');\n const value = await storage.get('test-key');\n\n expect(value).toBe('test-value');\n });\n\n test('should return undefined for missing keys', async () => {\n const value = await storage.get('non-existent');\n\n expect(value).toBeUndefined();\n });\n\n test('should delete values', async () => {\n await storage.set('test-key', 'test-value');\n await storage.delete('test-key');\n const value = await storage.get('test-key');\n\n expect(value).toBeUndefined();\n });\n\n test('should sanitize keys for filenames', async () => {\n const unsafeKey = 'test/key:with@special#chars';\n\n await storage.set(unsafeKey, 'test-value');\n const value = await storage.get(unsafeKey);\n\n expect(value).toBe('test-value');\n });\n\n test('should handle multiple values', async () => {\n await storage.set('key1', 'value1');\n await storage.set('key2', 'value2');\n const value1 = await storage.get('key1');\n const value2 = await storage.get('key2');\n\n expect(value1).toBe('value1');\n expect(value2).toBe('value2');\n });\n\n test('should clear all data', async () => {\n await storage.set('key1', 'value1');\n await storage.set('key2', 'value2');\n await storage.clear();\n const value1 = await storage.get('key1');\n const value2 = await storage.get('key2');\n\n expect(value1).toBeUndefined();\n expect(value2).toBeUndefined();\n });\n});\n\ndescribe('createStorageAdapter', () => {\n test('should create MemoryStorage by default', () => {\n const storage = createStorageAdapter();\n\n expect(storage).toBeInstanceOf(MemoryStorage);\n });\n\n test('should create MemoryStorage when type is \"memory\"', () => {\n const storage = createStorageAdapter('memory');\n\n expect(storage).toBeInstanceOf(MemoryStorage);\n });\n\n test('should create FileStorage when type is \"file\"', () => {\n const storage = createStorageAdapter('file');\n\n expect(storage).toBeInstanceOf(FileStorage);\n });\n\n test('should use custom path for FileStorage', async () => {\n const customPath = './custom-oauth-path';\n const storage = createStorageAdapter('file', { path: customPath });\n\n expect(storage).toBeInstanceOf(FileStorage);\n\n // Clean up\n if (storage instanceof FileStorage) {\n await storage.clear();\n }\n });\n});\n\ndescribe('OAuthStorage', () => {\n let storage: MemoryStorage;\n let oauthStorage: OAuthStorage;\n const sessionId = 'test-session-id';\n\n beforeEach(() => {\n storage = new MemoryStorage();\n oauthStorage = new OAuthStorage(storage, sessionId);\n });\n\n describe('token management', () => {\n test('should save and retrieve tokens', async () => {\n const tokens: OAuthTokens = {\n access_token: 'test-access-token',\n token_type: 'Bearer',\n expires_in: 3600,\n refresh_token: 'test-refresh-token',\n };\n\n await oauthStorage.saveTokens(tokens);\n const retrieved = await oauthStorage.getTokens();\n\n expect(retrieved).toBeDefined();\n expect(retrieved?.access_token).toBe(tokens.access_token);\n expect(retrieved?.token_type).toBe(tokens.token_type);\n expect(retrieved?.refresh_token).toBe(tokens.refresh_token);\n // expires_in should be close to original value (within 5 seconds due to time passing)\n expect(retrieved?.expires_in).toBeGreaterThanOrEqual(3595);\n expect(retrieved?.expires_in).toBeLessThanOrEqual(3600);\n });\n\n test('should return undefined when no tokens exist', async () => {\n const tokens = await oauthStorage.getTokens();\n\n expect(tokens).toBeUndefined();\n });\n\n test('should clear tokens', async () => {\n const tokens: OAuthTokens = {\n access_token: 'test-access-token',\n token_type: 'Bearer',\n };\n\n await oauthStorage.saveTokens(tokens);\n await oauthStorage.clearTokens();\n const retrieved = await oauthStorage.getTokens();\n\n expect(retrieved).toBeUndefined();\n });\n\n test('should handle tokens without optional fields', async () => {\n const tokens: OAuthTokens = {\n access_token: 'test-access-token',\n token_type: 'Bearer',\n };\n\n await oauthStorage.saveTokens(tokens);\n const retrieved = await oauthStorage.getTokens();\n\n expect(retrieved).toEqual(tokens);\n });\n });\n\n describe('client info management', () => {\n test('should save and retrieve client info', async () => {\n const clientInfo = {\n client_id: 'test-client-id',\n client_secret: 'test-client-secret',\n redirect_uris: ['http://localhost:8080/callback'],\n };\n\n await oauthStorage.saveClientInfo(clientInfo);\n const retrieved = await oauthStorage.getClientInfo();\n\n expect(retrieved).toEqual(clientInfo);\n });\n\n test('should return undefined when no client info exists', async () => {\n const clientInfo = await oauthStorage.getClientInfo();\n\n expect(clientInfo).toBeUndefined();\n });\n\n test('should handle client info without secret', async () => {\n const clientInfo = {\n client_id: 'test-client-id',\n redirect_uris: ['http://localhost:8080/callback'],\n };\n\n await oauthStorage.saveClientInfo(clientInfo);\n const retrieved = await oauthStorage.getClientInfo();\n\n expect(retrieved).toEqual(clientInfo);\n });\n });\n\n describe('code verifier management', () => {\n test('should save and retrieve code verifier', async () => {\n const verifier = 'test-code-verifier-12345';\n\n await oauthStorage.saveCodeVerifier(verifier);\n const retrieved = await oauthStorage.getCodeVerifier();\n\n expect(retrieved).toBe(verifier);\n });\n\n test('should return undefined when no code verifier exists', async () => {\n const verifier = await oauthStorage.getCodeVerifier();\n\n expect(verifier).toBeUndefined();\n });\n\n test('should clear code verifier', async () => {\n const verifier = 'test-code-verifier-12345';\n\n await oauthStorage.saveCodeVerifier(verifier);\n await oauthStorage.clearCodeVerifier();\n const retrieved = await oauthStorage.getCodeVerifier();\n\n expect(retrieved).toBeUndefined();\n });\n });\n\n describe('session isolation', () => {\n test('should isolate tokens between sessions', async () => {\n const session1 = new OAuthStorage(storage, 'session-1');\n const session2 = new OAuthStorage(storage, 'session-2');\n const tokens1: OAuthTokens = {\n access_token: 'token-1',\n token_type: 'Bearer',\n };\n const tokens2: OAuthTokens = {\n access_token: 'token-2',\n token_type: 'Bearer',\n };\n\n await session1.saveTokens(tokens1);\n await session2.saveTokens(tokens2);\n const retrieved1 = await session1.getTokens();\n const retrieved2 = await session2.getTokens();\n\n expect(retrieved1).toEqual(tokens1);\n expect(retrieved2).toEqual(tokens2);\n });\n\n test('should correctly calculate expires_in over time', async () => {\n const tokens: OAuthTokens = {\n access_token: 'test-access-token',\n token_type: 'Bearer',\n expires_in: 3600, // 1 hour\n refresh_token: 'test-refresh-token',\n };\n\n // Save tokens\n await oauthStorage.saveTokens(tokens);\n\n // Retrieve immediately - should be close to 3600\n const retrieved1 = await oauthStorage.getTokens();\n\n expect(retrieved1?.expires_in).toBeGreaterThanOrEqual(3595);\n expect(retrieved1?.expires_in).toBeLessThanOrEqual(3600);\n\n // Wait 2 seconds\n await new Promise(resolve => setTimeout(resolve, 2000));\n\n // Retrieve again - should be approximately 2 seconds less\n const retrieved2 = await oauthStorage.getTokens();\n\n expect(retrieved2?.expires_in).toBeGreaterThanOrEqual(3593);\n expect(retrieved2?.expires_in).toBeLessThanOrEqual(3598);\n expect(retrieved2?.expires_in).toBeLessThan(retrieved1!.expires_in!);\n });\n });\n});\n"],"names":["beforeEach","describe","expect","test","FileStorage","MemoryStorage","OAuthStorage","createStorageAdapter","storage","set","value","get","toBe","toBeUndefined","delete","clear","value1","value2","value3","testPath","unsafeKey","toBeInstanceOf","customPath","path","oauthStorage","sessionId","tokens","access_token","token_type","expires_in","refresh_token","saveTokens","retrieved","getTokens","toBeDefined","toBeGreaterThanOrEqual","toBeLessThanOrEqual","clearTokens","toEqual","clientInfo","client_id","client_secret","redirect_uris","saveClientInfo","getClientInfo","verifier","saveCodeVerifier","getCodeVerifier","clearCodeVerifier","session1","session2","tokens1","tokens2","retrieved1","retrieved2","Promise","resolve","setTimeout","toBeLessThan"],"mappings":"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"}