(options: LoopbackServerOptions)
| 126 | * after token exchange). |
| 127 | */ |
| 128 | export async function startLoopbackServer(options: LoopbackServerOptions): Promise<LoopbackServer> { |
| 129 | const port = options.port ?? 0; |
| 130 | const host = options.host ?? "127.0.0.1"; |
| 131 | // `server.listen()` expects IPv6 hosts without brackets, but callers may pass |
| 132 | // "[::1]" since that's what URLs require. |
| 133 | const listenHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host; |
| 134 | const callbackPath = options.callbackPath ?? "/callback"; |
| 135 | const validateLoopback = options.validateLoopback ?? false; |
| 136 | const deferSuccessResponse = options.deferSuccessResponse ?? false; |
| 137 | |
| 138 | const deferred = createDeferred<Result<LoopbackCallbackResult, string>>(); |
| 139 | let deferredSuccessResponse: http.ServerResponse | null = null; |
| 140 | |
| 141 | const render = |
| 142 | options.renderHtml ?? |
| 143 | ((r: { success: boolean; error?: string }) => |
| 144 | renderOAuthCallbackHtml({ |
| 145 | title: r.success ? "Login complete" : "Login failed", |
| 146 | message: r.success |
| 147 | ? "You can return to Mux. You may now close this tab." |
| 148 | : (r.error ?? "Unknown error"), |
| 149 | success: r.success, |
| 150 | })); |
| 151 | |
| 152 | const clearDeferredSuccessResponse = (response: http.ServerResponse) => { |
| 153 | if (deferredSuccessResponse === response) { |
| 154 | deferredSuccessResponse = null; |
| 155 | } |
| 156 | }; |
| 157 | |
| 158 | const sendDeferredResponse = (result: { success: boolean; error?: string }) => { |
| 159 | const pendingResponse = deferredSuccessResponse; |
| 160 | if (!pendingResponse || pendingResponse.writableEnded || pendingResponse.destroyed) { |
| 161 | deferredSuccessResponse = null; |
| 162 | return; |
| 163 | } |
| 164 | |
| 165 | deferredSuccessResponse = null; |
| 166 | pendingResponse.statusCode = result.success ? 200 : 400; |
| 167 | pendingResponse.setHeader("Content-Type", "text/html"); |
| 168 | pendingResponse.end(render(result)); |
| 169 | }; |
| 170 | |
| 171 | const sendSuccessResponse = () => { |
| 172 | sendDeferredResponse({ success: true }); |
| 173 | }; |
| 174 | |
| 175 | const sendFailureResponse = (error: string) => { |
| 176 | sendDeferredResponse({ success: false, error }); |
| 177 | }; |
| 178 | |
| 179 | const sendCancelledResponseIfPending = () => { |
| 180 | sendFailureResponse("OAuth flow cancelled"); |
| 181 | }; |
| 182 | |
| 183 | const server = http.createServer((req, res) => { |
| 184 | // Optionally reject non-loopback connections (Codex sets validateLoopback: true). |
| 185 | if (validateLoopback && !isLoopbackAddress(req.socket.remoteAddress)) { |
no test coverage detected