oauth-callback 1.2.0 → 1.2.2
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 +26 -38
- package/dist/error.test.d.ts +2 -0
- package/dist/error.test.d.ts.map +1 -0
- package/dist/index.js +310 -389
- package/dist/mcp.js +310 -389
- package/dist/server.d.ts +6 -6
- package/dist/server.d.ts.map +1 -1
- package/package.json +21 -10
- package/src/error.test.ts +204 -0
- package/src/server.ts +190 -450
package/src/server.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* SPDX-License-Identifier: MIT
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { Server as HttpServer } from "node:http";
|
|
9
|
+
import type { IncomingMessage } from "node:http";
|
|
8
10
|
import { successTemplate, renderError } from "./templates";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* Result object returned from OAuth callback containing authorization code or error details
|
|
13
|
+
* Result object returned from OAuth callback containing authorization code or error details.
|
|
12
14
|
*/
|
|
13
15
|
export interface CallbackResult {
|
|
14
16
|
/** Authorization code returned by OAuth provider */
|
|
@@ -26,7 +28,7 @@ export interface CallbackResult {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
|
-
* Configuration options for the OAuth callback server
|
|
31
|
+
* Configuration options for the OAuth callback server.
|
|
30
32
|
*/
|
|
31
33
|
export interface ServerOptions {
|
|
32
34
|
/** Port number to bind the server to */
|
|
@@ -44,7 +46,7 @@ export interface ServerOptions {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
|
-
* Interface for OAuth callback server implementations across different runtimes
|
|
49
|
+
* Interface for OAuth callback server implementations across different runtimes.
|
|
48
50
|
*/
|
|
49
51
|
export interface CallbackServer {
|
|
50
52
|
/** Start the HTTP server with the given options */
|
|
@@ -56,7 +58,7 @@ export interface CallbackServer {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/**
|
|
59
|
-
* Generate HTML response for OAuth callback
|
|
61
|
+
* Generate HTML response for OAuth callback.
|
|
60
62
|
* @param params - OAuth callback parameters (code, error, etc.)
|
|
61
63
|
* @param successHtml - Custom success HTML template
|
|
62
64
|
* @param errorHtml - Custom error HTML template with placeholder support
|
|
@@ -67,383 +69,217 @@ function generateCallbackHTML(
|
|
|
67
69
|
successHtml?: string,
|
|
68
70
|
errorHtml?: string,
|
|
69
71
|
): string {
|
|
70
|
-
if (params.error)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
// Use custom success HTML if provided
|
|
85
|
-
return successHtml || successTemplate;
|
|
72
|
+
if (!params.error) return successHtml || successTemplate;
|
|
73
|
+
|
|
74
|
+
if (errorHtml)
|
|
75
|
+
return errorHtml
|
|
76
|
+
.replace(/{{error}}/g, params.error || "")
|
|
77
|
+
.replace(/{{error_description}}/g, params.error_description || "")
|
|
78
|
+
.replace(/{{error_uri}}/g, params.error_uri || "");
|
|
79
|
+
|
|
80
|
+
return renderError({
|
|
81
|
+
error: params.error,
|
|
82
|
+
error_description: params.error_description,
|
|
83
|
+
error_uri: params.error_uri,
|
|
84
|
+
});
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
/**
|
|
89
|
-
*
|
|
88
|
+
* Base class with shared logic for all runtime implementations.
|
|
90
89
|
*/
|
|
91
|
-
class
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
abstract class BaseCallbackServer implements CallbackServer {
|
|
91
|
+
// Use a Map to safely handle listeners for different paths.
|
|
92
|
+
// This is more robust than a single property, preventing potential race conditions.
|
|
93
|
+
protected callbackListeners = new Map<
|
|
94
|
+
string,
|
|
95
|
+
{
|
|
96
|
+
resolve: (result: CallbackResult) => void;
|
|
97
|
+
reject: (error: Error) => void;
|
|
98
|
+
}
|
|
99
|
+
>();
|
|
100
|
+
protected successHtml?: string;
|
|
101
|
+
protected errorHtml?: string;
|
|
102
|
+
protected onRequest?: (req: Request) => void;
|
|
101
103
|
private abortHandler?: () => void;
|
|
104
|
+
private signal?: AbortSignal;
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
hostname = "localhost",
|
|
107
|
-
successHtml,
|
|
108
|
-
errorHtml,
|
|
109
|
-
signal,
|
|
110
|
-
onRequest,
|
|
111
|
-
} = options;
|
|
106
|
+
// Abstract methods to be implemented by subclasses for runtime-specific logic.
|
|
107
|
+
public abstract start(options: ServerOptions): Promise<void>;
|
|
108
|
+
protected abstract stopServer(): Promise<void>;
|
|
112
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Sets up common properties and handles the abort signal.
|
|
112
|
+
*/
|
|
113
|
+
protected setup(options: ServerOptions): void {
|
|
114
|
+
const { successHtml, errorHtml, signal, onRequest } = options;
|
|
113
115
|
this.successHtml = successHtml;
|
|
114
116
|
this.errorHtml = errorHtml;
|
|
115
117
|
this.onRequest = onRequest;
|
|
118
|
+
this.signal = signal;
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
if (signal)
|
|
119
|
-
if (signal.aborted) {
|
|
120
|
-
throw new Error("Operation aborted");
|
|
121
|
-
}
|
|
122
|
-
this.abortHandler = () => {
|
|
123
|
-
this.stop();
|
|
124
|
-
if (this.callbackPromise) {
|
|
125
|
-
this.callbackPromise.reject(new Error("Operation aborted"));
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
signal.addEventListener("abort", this.abortHandler);
|
|
129
|
-
}
|
|
120
|
+
if (!signal) return;
|
|
121
|
+
if (signal.aborted) throw new Error("Operation aborted");
|
|
130
122
|
|
|
131
|
-
//
|
|
132
|
-
this.
|
|
133
|
-
|
|
134
|
-
hostname,
|
|
135
|
-
fetch: (request: Request) => this.handleRequest(request),
|
|
136
|
-
});
|
|
123
|
+
// The abort handler now just calls stop(), which handles cleanup.
|
|
124
|
+
this.abortHandler = () => this.stop();
|
|
125
|
+
signal.addEventListener("abort", this.abortHandler);
|
|
137
126
|
}
|
|
138
127
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Handles incoming HTTP requests using Web Standards APIs.
|
|
130
|
+
* This logic is the same for all runtimes.
|
|
131
|
+
*/
|
|
132
|
+
protected handleRequest(request: Request): Response {
|
|
133
|
+
this.onRequest?.(request);
|
|
144
134
|
|
|
145
135
|
const url = new URL(request.url);
|
|
136
|
+
const listener = this.callbackListeners.get(url.pathname);
|
|
146
137
|
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Resolve the callback promise
|
|
156
|
-
if (this.callbackPromise) {
|
|
157
|
-
this.callbackPromise.resolve(params);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Return success or error HTML page
|
|
161
|
-
return new Response(
|
|
162
|
-
generateCallbackHTML(params, this.successHtml, this.errorHtml),
|
|
163
|
-
{
|
|
164
|
-
status: 200,
|
|
165
|
-
headers: { "Content-Type": "text/html" },
|
|
166
|
-
},
|
|
167
|
-
);
|
|
168
|
-
}
|
|
138
|
+
if (!listener) return new Response("Not Found", { status: 404 });
|
|
139
|
+
|
|
140
|
+
const params: CallbackResult = {};
|
|
141
|
+
for (const [key, value] of url.searchParams) params[key] = value;
|
|
142
|
+
|
|
143
|
+
// Resolve the promise for the waiting listener.
|
|
144
|
+
listener.resolve(params);
|
|
169
145
|
|
|
170
|
-
return new Response(
|
|
146
|
+
return new Response(
|
|
147
|
+
generateCallbackHTML(params, this.successHtml, this.errorHtml),
|
|
148
|
+
{
|
|
149
|
+
status: 200,
|
|
150
|
+
headers: { "Content-Type": "text/html" },
|
|
151
|
+
},
|
|
152
|
+
);
|
|
171
153
|
}
|
|
172
154
|
|
|
173
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Waits for the OAuth callback on a specific path.
|
|
157
|
+
*/
|
|
158
|
+
public async waitForCallback(
|
|
174
159
|
path: string,
|
|
175
160
|
timeout: number,
|
|
176
161
|
): Promise<CallbackResult> {
|
|
177
|
-
this.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const timer = setTimeout(() => {
|
|
183
|
-
if (!isResolved) {
|
|
184
|
-
isResolved = true;
|
|
185
|
-
this.callbackPromise = undefined;
|
|
186
|
-
reject(
|
|
187
|
-
new Error(
|
|
188
|
-
`OAuth callback timeout after ${timeout}ms waiting for ${path}`,
|
|
189
|
-
),
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
}, timeout);
|
|
193
|
-
|
|
194
|
-
const wrappedResolve = (result: CallbackResult) => {
|
|
195
|
-
if (!isResolved) {
|
|
196
|
-
isResolved = true;
|
|
197
|
-
clearTimeout(timer);
|
|
198
|
-
this.callbackPromise = undefined;
|
|
199
|
-
resolve(result);
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const wrappedReject = (error: Error) => {
|
|
204
|
-
if (!isResolved) {
|
|
205
|
-
isResolved = true;
|
|
206
|
-
clearTimeout(timer);
|
|
207
|
-
this.callbackPromise = undefined;
|
|
208
|
-
reject(error);
|
|
209
|
-
}
|
|
210
|
-
};
|
|
162
|
+
if (this.callbackListeners.has(path))
|
|
163
|
+
return Promise.reject(
|
|
164
|
+
new Error(`A listener for the path "${path}" is already active.`),
|
|
165
|
+
);
|
|
211
166
|
|
|
212
|
-
|
|
213
|
-
|
|
167
|
+
try {
|
|
168
|
+
// Race a promise that waits for the callback against a promise that rejects on timeout.
|
|
169
|
+
return await Promise.race([
|
|
170
|
+
// This promise is resolved or rejected by the handleRequest method.
|
|
171
|
+
new Promise<CallbackResult>((resolve, reject) => {
|
|
172
|
+
this.callbackListeners.set(path, { resolve, reject });
|
|
173
|
+
}),
|
|
174
|
+
// This promise rejects after the specified timeout.
|
|
175
|
+
new Promise<CallbackResult>((_, reject) => {
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
reject(
|
|
178
|
+
new Error(
|
|
179
|
+
`OAuth callback timeout after ${timeout}ms waiting for ${path}`,
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}, timeout);
|
|
183
|
+
}),
|
|
184
|
+
]);
|
|
185
|
+
} finally {
|
|
186
|
+
// CRITICAL: Always clean up the listener to prevent memory leaks,
|
|
187
|
+
// regardless of whether the promise resolved or rejected.
|
|
188
|
+
this.callbackListeners.delete(path);
|
|
189
|
+
}
|
|
214
190
|
}
|
|
215
191
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
192
|
+
/**
|
|
193
|
+
* Stops the server and cleans up resources.
|
|
194
|
+
*/
|
|
195
|
+
public async stop(): Promise<void> {
|
|
196
|
+
if (this.abortHandler && this.signal) {
|
|
197
|
+
this.signal.removeEventListener("abort", this.abortHandler);
|
|
223
198
|
this.abortHandler = undefined;
|
|
224
199
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (this.server) {
|
|
232
|
-
this.server.stop();
|
|
233
|
-
this.server = undefined;
|
|
234
|
-
}
|
|
200
|
+
// Reject any pending promises before stopping the server.
|
|
201
|
+
for (const listener of this.callbackListeners.values())
|
|
202
|
+
listener.reject(new Error("Server stopped before callback received"));
|
|
203
|
+
|
|
204
|
+
this.callbackListeners.clear();
|
|
205
|
+
await this.stopServer();
|
|
235
206
|
}
|
|
236
207
|
}
|
|
237
208
|
|
|
238
209
|
/**
|
|
239
|
-
*
|
|
210
|
+
* Bun runtime implementation using Bun.serve().
|
|
240
211
|
*/
|
|
241
|
-
class
|
|
242
|
-
private server
|
|
243
|
-
private callbackPromise?: {
|
|
244
|
-
resolve: (result: CallbackResult) => void;
|
|
245
|
-
reject: (error: Error) => void;
|
|
246
|
-
};
|
|
247
|
-
private callbackPath: string = "/callback";
|
|
248
|
-
private abortController?: AbortController;
|
|
249
|
-
private successHtml?: string;
|
|
250
|
-
private errorHtml?: string;
|
|
251
|
-
private onRequest?: (req: Request) => void;
|
|
252
|
-
private abortHandler?: () => void;
|
|
212
|
+
class BunCallbackServer extends BaseCallbackServer {
|
|
213
|
+
private server?: Bun.Server<unknown>;
|
|
253
214
|
|
|
254
|
-
async start(options: ServerOptions): Promise<void> {
|
|
255
|
-
|
|
215
|
+
public async start(options: ServerOptions): Promise<void> {
|
|
216
|
+
this.setup(options);
|
|
217
|
+
const { port, hostname = "localhost" } = options;
|
|
218
|
+
|
|
219
|
+
this.server = Bun.serve({
|
|
256
220
|
port,
|
|
257
|
-
hostname
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
onRequest,
|
|
262
|
-
} = options;
|
|
221
|
+
hostname,
|
|
222
|
+
fetch: (request: Request) => this.handleRequest(request),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
263
225
|
|
|
264
|
-
|
|
265
|
-
this.
|
|
266
|
-
|
|
226
|
+
protected async stopServer(): Promise<void> {
|
|
227
|
+
if (!this.server) return;
|
|
228
|
+
// Wait for pending requests to complete before stopping
|
|
229
|
+
while (this.server.pendingRequests > 0) {
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
231
|
+
}
|
|
232
|
+
this.server.stop();
|
|
233
|
+
this.server = undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
267
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Deno runtime implementation using Deno.serve().
|
|
239
|
+
*/
|
|
240
|
+
class DenoCallbackServer extends BaseCallbackServer {
|
|
241
|
+
private abortController?: AbortController;
|
|
242
|
+
|
|
243
|
+
public async start(options: ServerOptions): Promise<void> {
|
|
244
|
+
this.setup(options);
|
|
245
|
+
const { port, hostname = "localhost" } = options;
|
|
268
246
|
this.abortController = new AbortController();
|
|
269
247
|
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
this.abortHandler = () => {
|
|
276
|
-
this.abortController?.abort();
|
|
277
|
-
if (this.callbackPromise) {
|
|
278
|
-
this.callbackPromise.reject(new Error("Operation aborted"));
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
signal.addEventListener("abort", this.abortHandler);
|
|
282
|
-
}
|
|
248
|
+
// The user's signal will abort our internal controller.
|
|
249
|
+
options.signal?.addEventListener("abort", () =>
|
|
250
|
+
this.abortController?.abort(),
|
|
251
|
+
);
|
|
283
252
|
|
|
284
|
-
|
|
285
|
-
this.server = Deno.serve(
|
|
253
|
+
Deno.serve(
|
|
286
254
|
{ port, hostname, signal: this.abortController.signal },
|
|
287
255
|
(request: Request) => this.handleRequest(request),
|
|
288
256
|
);
|
|
289
257
|
}
|
|
290
258
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const url = new URL(request.url);
|
|
298
|
-
|
|
299
|
-
if (url.pathname === this.callbackPath) {
|
|
300
|
-
const params: CallbackResult = {};
|
|
301
|
-
|
|
302
|
-
// Parse all query parameters
|
|
303
|
-
for (const [key, value] of url.searchParams) {
|
|
304
|
-
params[key] = value;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Resolve the callback promise
|
|
308
|
-
if (this.callbackPromise) {
|
|
309
|
-
this.callbackPromise.resolve(params);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Return success or error HTML page
|
|
313
|
-
return new Response(
|
|
314
|
-
generateCallbackHTML(params, this.successHtml, this.errorHtml),
|
|
315
|
-
{
|
|
316
|
-
status: 200,
|
|
317
|
-
headers: { "Content-Type": "text/html" },
|
|
318
|
-
},
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return new Response("Not Found", { status: 404 });
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async waitForCallback(
|
|
326
|
-
path: string,
|
|
327
|
-
timeout: number,
|
|
328
|
-
): Promise<CallbackResult> {
|
|
329
|
-
this.callbackPath = path;
|
|
330
|
-
|
|
331
|
-
return new Promise((resolve, reject) => {
|
|
332
|
-
let isResolved = false;
|
|
333
|
-
|
|
334
|
-
const timer = setTimeout(() => {
|
|
335
|
-
if (!isResolved) {
|
|
336
|
-
isResolved = true;
|
|
337
|
-
this.callbackPromise = undefined;
|
|
338
|
-
reject(
|
|
339
|
-
new Error(
|
|
340
|
-
`OAuth callback timeout after ${timeout}ms waiting for ${path}`,
|
|
341
|
-
),
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
}, timeout);
|
|
345
|
-
|
|
346
|
-
const wrappedResolve = (result: CallbackResult) => {
|
|
347
|
-
if (!isResolved) {
|
|
348
|
-
isResolved = true;
|
|
349
|
-
clearTimeout(timer);
|
|
350
|
-
this.callbackPromise = undefined;
|
|
351
|
-
resolve(result);
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
const wrappedReject = (error: Error) => {
|
|
356
|
-
if (!isResolved) {
|
|
357
|
-
isResolved = true;
|
|
358
|
-
clearTimeout(timer);
|
|
359
|
-
this.callbackPromise = undefined;
|
|
360
|
-
reject(error);
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
this.callbackPromise = { resolve: wrappedResolve, reject: wrappedReject };
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async stop(): Promise<void> {
|
|
369
|
-
if (this.abortHandler) {
|
|
370
|
-
// Remove abort handler if it was set
|
|
371
|
-
const signal = this.server?.signal;
|
|
372
|
-
if (signal) {
|
|
373
|
-
signal.removeEventListener("abort", this.abortHandler);
|
|
374
|
-
}
|
|
375
|
-
this.abortHandler = undefined;
|
|
376
|
-
}
|
|
377
|
-
if (this.callbackPromise) {
|
|
378
|
-
this.callbackPromise.reject(
|
|
379
|
-
new Error("Server stopped before callback received"),
|
|
380
|
-
);
|
|
381
|
-
this.callbackPromise = undefined;
|
|
382
|
-
}
|
|
383
|
-
if (this.abortController) {
|
|
384
|
-
this.abortController.abort();
|
|
385
|
-
this.abortController = undefined;
|
|
386
|
-
}
|
|
387
|
-
this.server = undefined;
|
|
259
|
+
protected async stopServer(): Promise<void> {
|
|
260
|
+
if (!this.abortController) return;
|
|
261
|
+
this.abortController.abort();
|
|
262
|
+
this.abortController = undefined;
|
|
388
263
|
}
|
|
389
264
|
}
|
|
390
265
|
|
|
391
266
|
/**
|
|
392
|
-
* Node.js implementation using node:http with Web Standards APIs
|
|
393
|
-
* Works with Node.js 18+ which has native Request/Response support
|
|
267
|
+
* Node.js implementation using node:http with Web Standards APIs.
|
|
394
268
|
*/
|
|
395
|
-
class NodeCallbackServer
|
|
396
|
-
private server?:
|
|
397
|
-
private callbackPromise?: {
|
|
398
|
-
resolve: (result: CallbackResult) => void;
|
|
399
|
-
reject: (error: Error) => void;
|
|
400
|
-
};
|
|
401
|
-
private callbackPath: string = "/callback";
|
|
402
|
-
private successHtml?: string;
|
|
403
|
-
private errorHtml?: string;
|
|
404
|
-
private onRequest?: (req: Request) => void;
|
|
405
|
-
private abortHandler?: () => void;
|
|
406
|
-
|
|
407
|
-
async start(options: ServerOptions): Promise<void> {
|
|
408
|
-
const {
|
|
409
|
-
port,
|
|
410
|
-
hostname = "localhost",
|
|
411
|
-
successHtml,
|
|
412
|
-
errorHtml,
|
|
413
|
-
signal,
|
|
414
|
-
onRequest,
|
|
415
|
-
} = options;
|
|
416
|
-
|
|
417
|
-
this.successHtml = successHtml;
|
|
418
|
-
this.errorHtml = errorHtml;
|
|
419
|
-
this.onRequest = onRequest;
|
|
420
|
-
|
|
421
|
-
// Handle abort signal
|
|
422
|
-
if (signal) {
|
|
423
|
-
if (signal.aborted) {
|
|
424
|
-
throw new Error("Operation aborted");
|
|
425
|
-
}
|
|
426
|
-
this.abortHandler = () => {
|
|
427
|
-
this.stop();
|
|
428
|
-
if (this.callbackPromise) {
|
|
429
|
-
this.callbackPromise.reject(new Error("Operation aborted"));
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
signal.addEventListener("abort", this.abortHandler);
|
|
433
|
-
}
|
|
269
|
+
class NodeCallbackServer extends BaseCallbackServer {
|
|
270
|
+
private server?: HttpServer;
|
|
434
271
|
|
|
272
|
+
public async start(options: ServerOptions): Promise<void> {
|
|
273
|
+
this.setup(options);
|
|
274
|
+
const { port, hostname = "localhost" } = options;
|
|
435
275
|
const { createServer } = await import("node:http");
|
|
436
276
|
|
|
437
277
|
return new Promise((resolve, reject) => {
|
|
438
278
|
this.server = createServer(async (req, res) => {
|
|
439
279
|
try {
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
// Handle request using Web Standards
|
|
444
|
-
const response = await this.handleRequest(request);
|
|
280
|
+
const request = this.nodeToWebRequest(req, port, hostname);
|
|
281
|
+
const response = this.handleRequest(request);
|
|
445
282
|
|
|
446
|
-
// Write Web Standards Response back to Node.js ServerResponse
|
|
447
283
|
res.writeHead(
|
|
448
284
|
response.status,
|
|
449
285
|
Object.fromEntries(response.headers.entries()),
|
|
@@ -456,154 +292,58 @@ class NodeCallbackServer implements CallbackServer {
|
|
|
456
292
|
}
|
|
457
293
|
});
|
|
458
294
|
|
|
295
|
+
// Tie server closing to the abort signal if provided.
|
|
296
|
+
if (options.signal)
|
|
297
|
+
options.signal.addEventListener("abort", () => this.server?.close());
|
|
298
|
+
|
|
459
299
|
this.server.listen(port, hostname, () => resolve());
|
|
460
300
|
this.server.on("error", reject);
|
|
461
301
|
});
|
|
462
302
|
}
|
|
463
303
|
|
|
304
|
+
protected async stopServer(): Promise<void> {
|
|
305
|
+
if (!this.server) return;
|
|
306
|
+
this.server.closeAllConnections();
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
this.server?.close(() => {
|
|
309
|
+
this.server = undefined;
|
|
310
|
+
resolve();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
464
315
|
/**
|
|
465
|
-
*
|
|
316
|
+
* Converts a Node.js IncomingMessage to a Web Standards Request.
|
|
466
317
|
*/
|
|
467
|
-
private nodeToWebRequest(
|
|
468
|
-
|
|
318
|
+
private nodeToWebRequest(
|
|
319
|
+
req: IncomingMessage,
|
|
320
|
+
port: number,
|
|
321
|
+
hostname?: string,
|
|
322
|
+
): Request {
|
|
323
|
+
const host = req.headers.host || `${hostname}:${port}`;
|
|
324
|
+
const url = new URL(req.url!, `http://${host}`);
|
|
469
325
|
|
|
470
|
-
// Convert Node.js headers to Headers object
|
|
471
326
|
const headers = new Headers();
|
|
472
327
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
473
|
-
if (typeof value === "string")
|
|
474
|
-
|
|
475
|
-
} else if (Array.isArray(value)) {
|
|
476
|
-
headers.set(key, value.join(", "));
|
|
477
|
-
}
|
|
328
|
+
if (typeof value === "string") headers.set(key, value);
|
|
329
|
+
else if (Array.isArray(value)) headers.set(key, value.join(", "));
|
|
478
330
|
}
|
|
479
331
|
|
|
480
|
-
// OAuth callbacks use GET requests without body
|
|
481
332
|
return new Request(url.toString(), {
|
|
482
333
|
method: req.method,
|
|
483
334
|
headers,
|
|
484
335
|
});
|
|
485
336
|
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Handle request using Web Standards APIs (same as Bun/Deno implementations)
|
|
489
|
-
*/
|
|
490
|
-
private async handleRequest(request: Request): Promise<Response> {
|
|
491
|
-
// Call onRequest callback if provided
|
|
492
|
-
if (this.onRequest) {
|
|
493
|
-
this.onRequest(request);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const url = new URL(request.url);
|
|
497
|
-
|
|
498
|
-
if (url.pathname === this.callbackPath) {
|
|
499
|
-
const params: CallbackResult = {};
|
|
500
|
-
|
|
501
|
-
// Parse all query parameters
|
|
502
|
-
for (const [key, value] of url.searchParams) {
|
|
503
|
-
params[key] = value;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Resolve the callback promise
|
|
507
|
-
if (this.callbackPromise) {
|
|
508
|
-
this.callbackPromise.resolve(params);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Return success or error HTML page
|
|
512
|
-
return new Response(
|
|
513
|
-
generateCallbackHTML(params, this.successHtml, this.errorHtml),
|
|
514
|
-
{
|
|
515
|
-
status: 200,
|
|
516
|
-
headers: { "Content-Type": "text/html" },
|
|
517
|
-
},
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return new Response("Not Found", { status: 404 });
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
async waitForCallback(
|
|
525
|
-
path: string,
|
|
526
|
-
timeout: number,
|
|
527
|
-
): Promise<CallbackResult> {
|
|
528
|
-
this.callbackPath = path;
|
|
529
|
-
|
|
530
|
-
return new Promise((resolve, reject) => {
|
|
531
|
-
let isResolved = false;
|
|
532
|
-
|
|
533
|
-
const timer = setTimeout(() => {
|
|
534
|
-
if (!isResolved) {
|
|
535
|
-
isResolved = true;
|
|
536
|
-
this.callbackPromise = undefined;
|
|
537
|
-
reject(
|
|
538
|
-
new Error(
|
|
539
|
-
`OAuth callback timeout after ${timeout}ms waiting for ${path}`,
|
|
540
|
-
),
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
}, timeout);
|
|
544
|
-
|
|
545
|
-
const wrappedResolve = (result: CallbackResult) => {
|
|
546
|
-
if (!isResolved) {
|
|
547
|
-
isResolved = true;
|
|
548
|
-
clearTimeout(timer);
|
|
549
|
-
this.callbackPromise = undefined;
|
|
550
|
-
resolve(result);
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
const wrappedReject = (error: Error) => {
|
|
555
|
-
if (!isResolved) {
|
|
556
|
-
isResolved = true;
|
|
557
|
-
clearTimeout(timer);
|
|
558
|
-
this.callbackPromise = undefined;
|
|
559
|
-
reject(error);
|
|
560
|
-
}
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
this.callbackPromise = { resolve: wrappedResolve, reject: wrappedReject };
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
async stop(): Promise<void> {
|
|
568
|
-
if (this.abortHandler) {
|
|
569
|
-
// Remove abort handler if it was set
|
|
570
|
-
const signal = this.server?.signal;
|
|
571
|
-
if (signal) {
|
|
572
|
-
signal.removeEventListener("abort", this.abortHandler);
|
|
573
|
-
}
|
|
574
|
-
this.abortHandler = undefined;
|
|
575
|
-
}
|
|
576
|
-
if (this.callbackPromise) {
|
|
577
|
-
this.callbackPromise.reject(
|
|
578
|
-
new Error("Server stopped before callback received"),
|
|
579
|
-
);
|
|
580
|
-
this.callbackPromise = undefined;
|
|
581
|
-
}
|
|
582
|
-
if (this.server) {
|
|
583
|
-
return new Promise((resolve) => {
|
|
584
|
-
this.server.close(() => resolve());
|
|
585
|
-
this.server = undefined;
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
337
|
}
|
|
590
338
|
|
|
591
339
|
/**
|
|
592
|
-
* Create a callback server for the current runtime (Bun, Deno, or Node.js)
|
|
593
|
-
* Automatically detects the runtime and returns appropriate server implementation
|
|
594
|
-
* @returns CallbackServer instance optimized for the current runtime
|
|
340
|
+
* Create a callback server for the current runtime (Bun, Deno, or Node.js).
|
|
341
|
+
* Automatically detects the runtime and returns the appropriate server implementation.
|
|
342
|
+
* @returns CallbackServer instance optimized for the current runtime.
|
|
595
343
|
*/
|
|
596
344
|
export function createCallbackServer(): CallbackServer {
|
|
597
|
-
|
|
598
|
-
if (typeof
|
|
599
|
-
return new BunCallbackServer();
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// @ts-ignore - Deno global not available in TypeScript definitions
|
|
603
|
-
if (typeof Deno !== "undefined") {
|
|
604
|
-
return new DenoCallbackServer();
|
|
605
|
-
}
|
|
345
|
+
if (typeof Bun !== "undefined") return new BunCallbackServer();
|
|
346
|
+
if (typeof Deno !== "undefined") return new DenoCallbackServer();
|
|
606
347
|
|
|
607
|
-
// Default to Node.js
|
|
608
348
|
return new NodeCallbackServer();
|
|
609
349
|
}
|