(codeVerifier: string)
| 137 | } |
| 138 | |
| 139 | function startCallbackServer(codeVerifier: string): Promise<ChatGptOAuthCredentials> { |
| 140 | const redirectUrl = new URL(CHATGPT_OAUTH_REDIRECT_URI) |
| 141 | const port = parseInt(redirectUrl.port, 10) |
| 142 | const callbackPath = redirectUrl.pathname |
| 143 | |
| 144 | return new Promise<ChatGptOAuthCredentials>((resolve, reject) => { |
| 145 | const timeout = setTimeout(() => { |
| 146 | stopChatGptOAuthServer() |
| 147 | reject(new Error('Timeout waiting for ChatGPT authorization')) |
| 148 | }, CALLBACK_SERVER_TIMEOUT_MS) |
| 149 | |
| 150 | const server = http.createServer(async (req, res) => { |
| 151 | const reqUrl = new URL(req.url ?? '/', `http://127.0.0.1:${port}`) |
| 152 | |
| 153 | if (reqUrl.pathname !== callbackPath) { |
| 154 | res.writeHead(404, { 'Content-Type': 'text/plain' }) |
| 155 | res.end('Not found') |
| 156 | return |
| 157 | } |
| 158 | |
| 159 | const code = reqUrl.searchParams.get('code') |
| 160 | if (!code) { |
| 161 | res.writeHead(400, { 'Content-Type': 'text/html' }) |
| 162 | res.end(callbackPageHtml(false, 'No authorization code received.')) |
| 163 | clearTimeout(timeout) |
| 164 | stopChatGptOAuthServer() |
| 165 | reject(new Error('No authorization code in callback')) |
| 166 | return |
| 167 | } |
| 168 | |
| 169 | const state = reqUrl.searchParams.get('state') |
| 170 | if (pendingState && (!state || state !== pendingState)) { |
| 171 | res.writeHead(400, { 'Content-Type': 'text/html' }) |
| 172 | res.end(callbackPageHtml(false, 'OAuth state mismatch. Please try again.')) |
| 173 | clearTimeout(timeout) |
| 174 | stopChatGptOAuthServer() |
| 175 | reject(new Error('OAuth state mismatch in callback')) |
| 176 | return |
| 177 | } |
| 178 | |
| 179 | try { |
| 180 | const fullCallbackUrl = `${CHATGPT_OAUTH_REDIRECT_URI}${reqUrl.search}` |
| 181 | const credentials = await exchangeChatGptCodeForTokens(fullCallbackUrl, codeVerifier) |
| 182 | |
| 183 | res.writeHead(200, { 'Content-Type': 'text/html' }) |
| 184 | res.end(callbackPageHtml(true)) |
| 185 | |
| 186 | clearTimeout(timeout) |
| 187 | stopChatGptOAuthServer() |
| 188 | resolve(credentials) |
| 189 | } catch (err) { |
| 190 | const message = err instanceof Error ? err.message : 'Token exchange failed' |
| 191 | res.writeHead(500, { 'Content-Type': 'text/html' }) |
| 192 | res.end(callbackPageHtml(false, message)) |
| 193 | |
| 194 | clearTimeout(timeout) |
| 195 | stopChatGptOAuthServer() |
| 196 | reject(err instanceof Error ? err : new Error(message)) |
no test coverage detected