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.
- package/README.md +668 -0
- package/dist/__tests__/config.test.js +56 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/integration.test.js +341 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/oauth-flow.test.js +201 -0
- package/dist/__tests__/oauth-flow.test.js.map +1 -0
- package/dist/__tests__/server.test.js +271 -0
- package/dist/__tests__/server.test.js.map +1 -0
- package/dist/__tests__/storage.test.js +256 -0
- package/dist/__tests__/storage.test.js.map +1 -0
- package/dist/client/config.js +30 -0
- package/dist/client/config.js.map +1 -0
- package/dist/client/factory.js +16 -0
- package/dist/client/factory.js.map +1 -0
- package/dist/client/index.js +237 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/oauth-flow.js +73 -0
- package/dist/client/oauth-flow.js.map +1 -0
- package/dist/client/storage.js +237 -0
- package/dist/client/storage.js.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/server/callback.js +164 -0
- package/dist/server/callback.js.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/templates.js +245 -0
- package/dist/server/templates.js.map +1 -0
- package/package.json +66 -0
- package/src/__tests__/config.test.ts +78 -0
- package/src/__tests__/integration.test.ts +398 -0
- package/src/__tests__/oauth-flow.test.ts +276 -0
- package/src/__tests__/server.test.ts +391 -0
- package/src/__tests__/storage.test.ts +329 -0
- package/src/client/config.ts +134 -0
- package/src/client/factory.ts +19 -0
- package/src/client/index.ts +361 -0
- package/src/client/oauth-flow.ts +115 -0
- package/src/client/storage.ts +335 -0
- package/src/index.ts +31 -0
- package/src/server/callback.ts +257 -0
- package/src/server/index.ts +21 -0
- package/src/server/templates.ts +271 -0
@@ -0,0 +1,335 @@
|
|
1
|
+
import type {
|
2
|
+
OAuthClientInformation,
|
3
|
+
OAuthClientInformationFull,
|
4
|
+
OAuthTokens,
|
5
|
+
StorageAdapter,
|
6
|
+
} from './config.js';
|
7
|
+
|
8
|
+
export type { StorageAdapter };
|
9
|
+
|
10
|
+
/**
|
11
|
+
* Internal token storage format with absolute expiry timestamp
|
12
|
+
*/
|
13
|
+
interface StoredTokens extends Omit<OAuthTokens, 'expires_in'> {
|
14
|
+
/**
|
15
|
+
* Absolute timestamp (milliseconds) when the token expires
|
16
|
+
* This is derived from expires_in when tokens are saved
|
17
|
+
*/
|
18
|
+
expires_at?: number;
|
19
|
+
}
|
20
|
+
|
21
|
+
/**
|
22
|
+
* Calculate expires_in from expires_at timestamp
|
23
|
+
*/
|
24
|
+
function calculateExpiresIn(expiresAt: number | undefined): number | undefined {
|
25
|
+
if (!expiresAt) {
|
26
|
+
return undefined;
|
27
|
+
}
|
28
|
+
|
29
|
+
const now = Date.now();
|
30
|
+
const expiresInMs = expiresAt - now;
|
31
|
+
const expiresInSeconds = Math.floor(expiresInMs / 1000);
|
32
|
+
|
33
|
+
// Return 0 if already expired, otherwise return remaining seconds
|
34
|
+
return Math.max(0, expiresInSeconds);
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Calculate expires_at from expires_in
|
39
|
+
*/
|
40
|
+
function calculateExpiresAt(expiresIn: number | undefined): number {
|
41
|
+
return Date.now() + (expiresIn || 0) * 1000;
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* In-memory storage adapter for OAuth data
|
46
|
+
* Suitable for development and testing, but data is lost when process exits
|
47
|
+
*/
|
48
|
+
export class MemoryStorage implements StorageAdapter {
|
49
|
+
private data = new Map<string, string>();
|
50
|
+
|
51
|
+
async get(key: string): Promise<string | undefined> {
|
52
|
+
return this.data.get(key);
|
53
|
+
}
|
54
|
+
|
55
|
+
async set(key: string, value: string): Promise<void> {
|
56
|
+
this.data.set(key, value);
|
57
|
+
}
|
58
|
+
|
59
|
+
async delete(key: string): Promise<void> {
|
60
|
+
this.data.delete(key);
|
61
|
+
}
|
62
|
+
|
63
|
+
/**
|
64
|
+
* Clear all data (useful for testing)
|
65
|
+
*/
|
66
|
+
clear(): void {
|
67
|
+
this.data.clear();
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
/**
|
72
|
+
* File-based storage adapter for OAuth data
|
73
|
+
* Persists data to the filesystem for longer-term storage
|
74
|
+
*/
|
75
|
+
export class FileStorage implements StorageAdapter {
|
76
|
+
private basePath: string;
|
77
|
+
|
78
|
+
constructor(basePath = './oauth-data') {
|
79
|
+
this.basePath = basePath;
|
80
|
+
}
|
81
|
+
|
82
|
+
private getFilePath(key: string): string {
|
83
|
+
// Sanitize key for filename
|
84
|
+
const sanitized = key.replace(/[^a-zA-Z0-9-_]/g, '_');
|
85
|
+
|
86
|
+
return `${this.basePath}/${sanitized}.json`;
|
87
|
+
}
|
88
|
+
|
89
|
+
private async ensureDirectory(): Promise<void> {
|
90
|
+
try {
|
91
|
+
await Bun.write(`${this.basePath}/.gitkeep`, '');
|
92
|
+
} catch {
|
93
|
+
// Directory creation will happen automatically with Bun.write
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
async get(key: string): Promise<string | undefined> {
|
98
|
+
try {
|
99
|
+
const file = Bun.file(this.getFilePath(key));
|
100
|
+
const exists = await file.exists();
|
101
|
+
|
102
|
+
if (!exists) {
|
103
|
+
return undefined;
|
104
|
+
}
|
105
|
+
|
106
|
+
return await file.text();
|
107
|
+
} catch {
|
108
|
+
return undefined;
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
async set(key: string, value: string): Promise<void> {
|
113
|
+
await this.ensureDirectory();
|
114
|
+
await Bun.write(this.getFilePath(key), value);
|
115
|
+
}
|
116
|
+
|
117
|
+
async delete(key: string): Promise<void> {
|
118
|
+
try {
|
119
|
+
await Bun.$`rm -f ${this.getFilePath(key)}`;
|
120
|
+
} catch {
|
121
|
+
// File might not exist, ignore error
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
/**
|
126
|
+
* Clear all data (useful for testing)
|
127
|
+
*/
|
128
|
+
async clear(): Promise<void> {
|
129
|
+
try {
|
130
|
+
await Bun.$`rm -rf ${this.basePath}`;
|
131
|
+
} catch {
|
132
|
+
// Directory might not exist, ignore error
|
133
|
+
}
|
134
|
+
}
|
135
|
+
}
|
136
|
+
|
137
|
+
/**
|
138
|
+
* Create a storage adapter based on the provided configuration
|
139
|
+
*/
|
140
|
+
export function createStorageAdapter(
|
141
|
+
type?: 'memory' | 'file',
|
142
|
+
options?: { path?: string }
|
143
|
+
): StorageAdapter {
|
144
|
+
switch (type) {
|
145
|
+
case 'file':
|
146
|
+
return new FileStorage(options?.path);
|
147
|
+
case 'memory':
|
148
|
+
default:
|
149
|
+
return new MemoryStorage();
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
/**
|
154
|
+
* Options for initializing OAuthStorage
|
155
|
+
*/
|
156
|
+
export interface OAuthStorageOptions {
|
157
|
+
/**
|
158
|
+
* Static client information (from config)
|
159
|
+
* If provided, will ALWAYS be returned (takes precedence over storage)
|
160
|
+
* This is because client credentials are like API keys - they don't change
|
161
|
+
*/
|
162
|
+
staticClientInfo?: OAuthClientInformation;
|
163
|
+
|
164
|
+
/**
|
165
|
+
* Initial tokens (from config)
|
166
|
+
* If provided, will be stored ONCE on first access if storage is empty
|
167
|
+
* After that, storage takes precedence (tokens change over time)
|
168
|
+
*/
|
169
|
+
initialTokens?: OAuthTokens;
|
170
|
+
}
|
171
|
+
|
172
|
+
/**
|
173
|
+
* Helper class to manage OAuth data with the simplified storage adapter
|
174
|
+
* Handles initialization from config and provides a unified storage interface
|
175
|
+
*/
|
176
|
+
export class OAuthStorage {
|
177
|
+
private initialized = false;
|
178
|
+
|
179
|
+
constructor(
|
180
|
+
private storage: StorageAdapter,
|
181
|
+
private sessionId: string,
|
182
|
+
private options?: OAuthStorageOptions
|
183
|
+
) {}
|
184
|
+
|
185
|
+
/**
|
186
|
+
* Initialize storage with config values if provided
|
187
|
+
* This ensures config tokens are written to storage on first use
|
188
|
+
*/
|
189
|
+
private async initialize(): Promise<void> {
|
190
|
+
if (this.initialized) {
|
191
|
+
return;
|
192
|
+
}
|
193
|
+
|
194
|
+
this.initialized = true;
|
195
|
+
|
196
|
+
// Initialize tokens if provided and not already in storage
|
197
|
+
// (tokens are one-time initialization, storage takes over after that)
|
198
|
+
if (this.options?.initialTokens) {
|
199
|
+
const existing = await this.storage.get(`tokens:${this.sessionId}`);
|
200
|
+
|
201
|
+
if (!existing) {
|
202
|
+
// Convert expires_in to expires_at before storing
|
203
|
+
const storedTokens: StoredTokens = {
|
204
|
+
access_token: this.options.initialTokens.access_token,
|
205
|
+
token_type: this.options.initialTokens.token_type,
|
206
|
+
refresh_token: this.options.initialTokens.refresh_token,
|
207
|
+
scope: this.options.initialTokens.scope,
|
208
|
+
};
|
209
|
+
|
210
|
+
// Only set expires_at if expires_in is provided
|
211
|
+
if (this.options.initialTokens.expires_in !== undefined) {
|
212
|
+
storedTokens.expires_at = calculateExpiresAt(
|
213
|
+
this.options.initialTokens.expires_in
|
214
|
+
);
|
215
|
+
}
|
216
|
+
|
217
|
+
await this.storage.set(
|
218
|
+
`tokens:${this.sessionId}`,
|
219
|
+
JSON.stringify(storedTokens)
|
220
|
+
);
|
221
|
+
}
|
222
|
+
}
|
223
|
+
}
|
224
|
+
|
225
|
+
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
226
|
+
await this.initialize();
|
227
|
+
|
228
|
+
// Convert expires_in to expires_at for storage
|
229
|
+
const storedTokens: StoredTokens = {
|
230
|
+
access_token: tokens.access_token,
|
231
|
+
token_type: tokens.token_type,
|
232
|
+
refresh_token: tokens.refresh_token,
|
233
|
+
scope: tokens.scope,
|
234
|
+
};
|
235
|
+
|
236
|
+
// Only set expires_at if expires_in is provided
|
237
|
+
if (tokens.expires_in !== undefined) {
|
238
|
+
storedTokens.expires_at = calculateExpiresAt(tokens.expires_in);
|
239
|
+
}
|
240
|
+
|
241
|
+
await this.storage.set(
|
242
|
+
`tokens:${this.sessionId}`,
|
243
|
+
JSON.stringify(storedTokens)
|
244
|
+
);
|
245
|
+
}
|
246
|
+
|
247
|
+
async getTokens(): Promise<OAuthTokens | undefined> {
|
248
|
+
await this.initialize();
|
249
|
+
const data = await this.storage.get(`tokens:${this.sessionId}`);
|
250
|
+
|
251
|
+
if (!data) {
|
252
|
+
return undefined;
|
253
|
+
}
|
254
|
+
|
255
|
+
const storedTokens: StoredTokens = JSON.parse(data);
|
256
|
+
|
257
|
+
// Convert expires_at back to expires_in for the OAuthTokens interface
|
258
|
+
const tokens: OAuthTokens = {
|
259
|
+
access_token: storedTokens.access_token,
|
260
|
+
token_type: storedTokens.token_type,
|
261
|
+
};
|
262
|
+
|
263
|
+
// Only include optional fields if they exist
|
264
|
+
if (storedTokens.refresh_token !== undefined) {
|
265
|
+
tokens.refresh_token = storedTokens.refresh_token;
|
266
|
+
}
|
267
|
+
if (storedTokens.scope !== undefined) {
|
268
|
+
tokens.scope = storedTokens.scope;
|
269
|
+
}
|
270
|
+
if (storedTokens.expires_at !== undefined) {
|
271
|
+
tokens.expires_in = calculateExpiresIn(storedTokens.expires_at);
|
272
|
+
}
|
273
|
+
|
274
|
+
return tokens;
|
275
|
+
}
|
276
|
+
|
277
|
+
async clearTokens(): Promise<void> {
|
278
|
+
await this.initialize();
|
279
|
+
await this.storage.delete(`tokens:${this.sessionId}`);
|
280
|
+
}
|
281
|
+
|
282
|
+
async saveClientInfo(clientInfo: OAuthClientInformationFull): Promise<void> {
|
283
|
+
await this.initialize();
|
284
|
+
await this.storage.set('client_info', JSON.stringify(clientInfo));
|
285
|
+
}
|
286
|
+
|
287
|
+
async getClientInfo(): Promise<OAuthClientInformation | undefined> {
|
288
|
+
await this.initialize();
|
289
|
+
|
290
|
+
// Static client info from config ALWAYS takes precedence
|
291
|
+
// (client credentials are like API keys - they don't change)
|
292
|
+
if (this.options?.staticClientInfo?.client_id) {
|
293
|
+
return this.options.staticClientInfo;
|
294
|
+
}
|
295
|
+
|
296
|
+
const data = await this.storage.get('client_info');
|
297
|
+
|
298
|
+
return data ? JSON.parse(data) : undefined;
|
299
|
+
}
|
300
|
+
|
301
|
+
async clearClientInfo(): Promise<void> {
|
302
|
+
await this.initialize();
|
303
|
+
await this.storage.delete('client_info');
|
304
|
+
}
|
305
|
+
|
306
|
+
async saveCodeVerifier(verifier: string): Promise<void> {
|
307
|
+
await this.initialize();
|
308
|
+
await this.storage.set(`verifier:${this.sessionId}`, verifier);
|
309
|
+
}
|
310
|
+
|
311
|
+
async getCodeVerifier(): Promise<string | undefined> {
|
312
|
+
await this.initialize();
|
313
|
+
|
314
|
+
return this.storage.get(`verifier:${this.sessionId}`);
|
315
|
+
}
|
316
|
+
|
317
|
+
async clearCodeVerifier(): Promise<void> {
|
318
|
+
await this.initialize();
|
319
|
+
await this.storage.delete(`verifier:${this.sessionId}`);
|
320
|
+
}
|
321
|
+
|
322
|
+
async clearSession(): Promise<void> {
|
323
|
+
await this.initialize();
|
324
|
+
await Promise.all([this.clearTokens(), this.clearCodeVerifier()]);
|
325
|
+
}
|
326
|
+
|
327
|
+
async clearAll(): Promise<void> {
|
328
|
+
await this.initialize();
|
329
|
+
await Promise.all([
|
330
|
+
this.clearTokens(),
|
331
|
+
this.clearCodeVerifier(),
|
332
|
+
this.clearClientInfo(),
|
333
|
+
]);
|
334
|
+
}
|
335
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
/**
|
2
|
+
* MCP OAuth Provider
|
3
|
+
*
|
4
|
+
* OAuth client provider implementation for the Model Context Protocol (MCP)
|
5
|
+
*/
|
6
|
+
|
7
|
+
export { MCPOAuthClientProvider } from './client/index.js';
|
8
|
+
|
9
|
+
export {
|
10
|
+
createStorageAdapter,
|
11
|
+
FileStorage,
|
12
|
+
MemoryStorage,
|
13
|
+
OAuthStorage,
|
14
|
+
type StorageAdapter,
|
15
|
+
} from './client/storage.js';
|
16
|
+
|
17
|
+
export {
|
18
|
+
DEFAULT_CLIENT_METADATA,
|
19
|
+
generateSessionId,
|
20
|
+
generateState,
|
21
|
+
type OAuthClientInformation,
|
22
|
+
type OAuthClientInformationFull,
|
23
|
+
type OAuthClientMetadata,
|
24
|
+
type OAuthConfig,
|
25
|
+
type OAuthTokens,
|
26
|
+
} from './client/config.js';
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Factory function to create an OAuth client provider
|
30
|
+
*/
|
31
|
+
export { createOAuthProvider } from './client/factory';
|
@@ -0,0 +1,257 @@
|
|
1
|
+
import { renderErrorPage, renderSuccessPage } from './templates.js';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* OAuth callback result interface
|
5
|
+
*/
|
6
|
+
export interface CallbackResult {
|
7
|
+
/** Authorization code returned by OAuth provider */
|
8
|
+
code?: string;
|
9
|
+
/** State parameter for CSRF protection */
|
10
|
+
state?: string;
|
11
|
+
/** OAuth error code (e.g., 'access_denied', 'invalid_request') */
|
12
|
+
error?: string;
|
13
|
+
/** Human-readable error description */
|
14
|
+
error_description?: string;
|
15
|
+
/** URI with additional error information */
|
16
|
+
error_uri?: string;
|
17
|
+
/** Additional query parameters from OAuth provider */
|
18
|
+
[key: string]: string | undefined;
|
19
|
+
}
|
20
|
+
|
21
|
+
/**
|
22
|
+
* Configuration options for the OAuth callback server
|
23
|
+
*/
|
24
|
+
export interface CallbackServerOptions {
|
25
|
+
/** Port number to bind the server to */
|
26
|
+
port: number;
|
27
|
+
/** Hostname to bind the server to (default: "localhost") */
|
28
|
+
hostname?: string;
|
29
|
+
/** Custom HTML content for successful authorization */
|
30
|
+
successHtml?: string;
|
31
|
+
/** Custom HTML template for error pages */
|
32
|
+
errorHtml?: string;
|
33
|
+
/** AbortSignal for cancelling the server operation */
|
34
|
+
signal?: AbortSignal;
|
35
|
+
/** Callback function called for each HTTP request */
|
36
|
+
onRequest?: (req: Request) => void;
|
37
|
+
}
|
38
|
+
|
39
|
+
/**
|
40
|
+
* OAuth callback server implementation using Bun
|
41
|
+
*/
|
42
|
+
export class OAuthCallbackServer {
|
43
|
+
private server?: { stop: () => void };
|
44
|
+
private callbackListeners = new Map<
|
45
|
+
string,
|
46
|
+
{
|
47
|
+
resolve: (result: CallbackResult) => void;
|
48
|
+
reject: (error: Error) => void;
|
49
|
+
}
|
50
|
+
>();
|
51
|
+
private options: CallbackServerOptions | undefined;
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Start the HTTP server
|
55
|
+
*/
|
56
|
+
async start(options: CallbackServerOptions): Promise<void> {
|
57
|
+
this.options = options;
|
58
|
+
|
59
|
+
if (this.server) {
|
60
|
+
throw new Error('Server is already running');
|
61
|
+
}
|
62
|
+
|
63
|
+
// Handle abort signal
|
64
|
+
if (options.signal?.aborted) {
|
65
|
+
throw new Error('Operation aborted');
|
66
|
+
}
|
67
|
+
|
68
|
+
const abortHandler = () => {
|
69
|
+
this.stop().catch(() => {
|
70
|
+
// Ignore errors during cleanup
|
71
|
+
});
|
72
|
+
};
|
73
|
+
|
74
|
+
if (options.signal) {
|
75
|
+
options.signal.addEventListener('abort', abortHandler);
|
76
|
+
}
|
77
|
+
|
78
|
+
try {
|
79
|
+
this.server = Bun.serve({
|
80
|
+
port: options.port,
|
81
|
+
hostname: options.hostname || 'localhost',
|
82
|
+
fetch: request => this.handleRequest(request),
|
83
|
+
error: error => {
|
84
|
+
return new Response(`Server error: ${error.message}`, {
|
85
|
+
status: 500,
|
86
|
+
});
|
87
|
+
},
|
88
|
+
});
|
89
|
+
|
90
|
+
// eslint-disable-next-line no-console
|
91
|
+
console.log(
|
92
|
+
`OAuth callback server running on http://${options.hostname || 'localhost'}:${options.port}`
|
93
|
+
);
|
94
|
+
} catch (error) {
|
95
|
+
if (options.signal) {
|
96
|
+
options.signal.removeEventListener('abort', abortHandler);
|
97
|
+
}
|
98
|
+
throw error;
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* Handle incoming HTTP requests
|
104
|
+
*/
|
105
|
+
private handleRequest(request: Request): Response {
|
106
|
+
this.options?.onRequest?.(request);
|
107
|
+
|
108
|
+
const url = new URL(request.url);
|
109
|
+
const listener = this.callbackListeners.get(url.pathname);
|
110
|
+
|
111
|
+
if (!listener) {
|
112
|
+
return new Response('Not Found', { status: 404 });
|
113
|
+
}
|
114
|
+
|
115
|
+
// Extract OAuth callback parameters from URL
|
116
|
+
const params: CallbackResult = {};
|
117
|
+
|
118
|
+
for (const [key, value] of url.searchParams.entries()) {
|
119
|
+
params[key] = value;
|
120
|
+
}
|
121
|
+
|
122
|
+
// Generate appropriate response
|
123
|
+
const hasError = Boolean(params.error);
|
124
|
+
const html = hasError
|
125
|
+
? renderErrorPage(
|
126
|
+
params.error,
|
127
|
+
params.error_description,
|
128
|
+
params.error_uri,
|
129
|
+
this.options?.errorHtml
|
130
|
+
)
|
131
|
+
: renderSuccessPage(this.options?.successHtml);
|
132
|
+
|
133
|
+
// Resolve the waiting promise with params (including error params)
|
134
|
+
// The caller can check for params.error to determine if it was an OAuth error
|
135
|
+
listener.resolve(params);
|
136
|
+
|
137
|
+
// Return HTML response with appropriate status code
|
138
|
+
return new Response(html, {
|
139
|
+
status: hasError ? 400 : 200,
|
140
|
+
headers: {
|
141
|
+
'Content-Type': 'text/html; charset=utf-8',
|
142
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
143
|
+
Pragma: 'no-cache',
|
144
|
+
Expires: '0',
|
145
|
+
},
|
146
|
+
});
|
147
|
+
}
|
148
|
+
|
149
|
+
/**
|
150
|
+
* Wait for OAuth callback on the specified path
|
151
|
+
*/
|
152
|
+
async waitForCallback(
|
153
|
+
path: string,
|
154
|
+
timeout: number
|
155
|
+
): Promise<CallbackResult> {
|
156
|
+
if (!this.server) {
|
157
|
+
throw new Error('Server is not running');
|
158
|
+
}
|
159
|
+
|
160
|
+
try {
|
161
|
+
return await Promise.race([
|
162
|
+
// Promise resolved by handleRequest method
|
163
|
+
new Promise<CallbackResult>((resolve, reject) => {
|
164
|
+
this.callbackListeners.set(path, { resolve, reject });
|
165
|
+
}),
|
166
|
+
// Timeout promise
|
167
|
+
new Promise<CallbackResult>((_, reject) => {
|
168
|
+
setTimeout(() => {
|
169
|
+
reject(
|
170
|
+
new Error(
|
171
|
+
`OAuth callback timeout after ${timeout}ms waiting for ${path}`
|
172
|
+
)
|
173
|
+
);
|
174
|
+
}, timeout);
|
175
|
+
}),
|
176
|
+
]);
|
177
|
+
} finally {
|
178
|
+
// Always clean up the listener
|
179
|
+
this.callbackListeners.delete(path);
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
/**
|
184
|
+
* Stop the server and clean up resources
|
185
|
+
*/
|
186
|
+
async stop(): Promise<void> {
|
187
|
+
if (this.server) {
|
188
|
+
// Reject any pending promises
|
189
|
+
const error = new Error('Server stopped');
|
190
|
+
|
191
|
+
for (const listener of this.callbackListeners.values()) {
|
192
|
+
// Reject in a try-catch to prevent unhandled promise rejections
|
193
|
+
try {
|
194
|
+
listener.reject(error);
|
195
|
+
} catch {
|
196
|
+
// Ignore - the caller may have already handled or abandoned this promise
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
this.callbackListeners.clear();
|
201
|
+
|
202
|
+
// Stop the server
|
203
|
+
await this.server.stop();
|
204
|
+
this.server = undefined;
|
205
|
+
}
|
206
|
+
}
|
207
|
+
|
208
|
+
/**
|
209
|
+
* Check if server is running
|
210
|
+
*/
|
211
|
+
isRunning(): boolean {
|
212
|
+
return Boolean(this.server);
|
213
|
+
}
|
214
|
+
|
215
|
+
/**
|
216
|
+
* Get server URL if running
|
217
|
+
*/
|
218
|
+
getServerUrl(): string | undefined {
|
219
|
+
if (!this.server || !this.options) {
|
220
|
+
return undefined;
|
221
|
+
}
|
222
|
+
|
223
|
+
const { hostname = 'localhost', port } = this.options;
|
224
|
+
|
225
|
+
return `http://${hostname}:${port}`;
|
226
|
+
}
|
227
|
+
}
|
228
|
+
|
229
|
+
/**
|
230
|
+
* Create and start a temporary OAuth callback server
|
231
|
+
*/
|
232
|
+
export async function createCallbackServer(
|
233
|
+
options: CallbackServerOptions
|
234
|
+
): Promise<OAuthCallbackServer> {
|
235
|
+
const server = new OAuthCallbackServer();
|
236
|
+
|
237
|
+
await server.start(options);
|
238
|
+
|
239
|
+
return server;
|
240
|
+
}
|
241
|
+
|
242
|
+
/**
|
243
|
+
* Convenience function to start server, wait for callback, and stop server
|
244
|
+
*/
|
245
|
+
export async function waitForOAuthCallback(
|
246
|
+
path: string,
|
247
|
+
options: CallbackServerOptions & { timeout?: number }
|
248
|
+
): Promise<CallbackResult> {
|
249
|
+
const server = await createCallbackServer(options);
|
250
|
+
const timeout = options.timeout ?? 30000; // 30 second default
|
251
|
+
|
252
|
+
try {
|
253
|
+
return await server.waitForCallback(path, timeout);
|
254
|
+
} finally {
|
255
|
+
await server.stop();
|
256
|
+
}
|
257
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
/**
|
2
|
+
* OAuth Callback Server Module
|
3
|
+
*
|
4
|
+
* Provides HTTP server functionality for handling OAuth authorization callbacks
|
5
|
+
*/
|
6
|
+
|
7
|
+
export {
|
8
|
+
createCallbackServer,
|
9
|
+
OAuthCallbackServer,
|
10
|
+
waitForOAuthCallback,
|
11
|
+
type CallbackResult,
|
12
|
+
type CallbackServerOptions,
|
13
|
+
} from './callback.js';
|
14
|
+
|
15
|
+
export {
|
16
|
+
ERROR_TEMPLATE,
|
17
|
+
renderErrorPage,
|
18
|
+
renderSuccessPage,
|
19
|
+
renderTemplate,
|
20
|
+
SUCCESS_TEMPLATE,
|
21
|
+
} from './templates.js';
|