fix(sdk): render CLI OAuth success page on localhost (twenty dev) (#22131)

This commit is contained in:
martmull
2026-06-25 06:11:49 +02:00
committed by GitHub
parent 7a23a3d851
commit 4277f8f04f
2 changed files with 75 additions and 18 deletions
@@ -2,14 +2,26 @@ import http from 'node:http';
import { startCallbackServer } from '../callback-server';
const httpGet = (url: string): Promise<{ status: number; body: string }> =>
const httpGet = (
url: string,
): Promise<{
status: number;
body: string;
headers: http.IncomingHttpHeaders;
}> =>
new Promise((resolve, reject) => {
http
.get(url, (res) => {
let body = '';
res.on('data', (chunk: string) => (body += chunk));
res.on('end', () => resolve({ status: res.statusCode ?? 0, body }));
res.on('end', () =>
resolve({
status: res.statusCode ?? 0,
body,
headers: res.headers,
}),
);
})
.on('error', reject);
});
@@ -44,6 +56,46 @@ describe('startCallbackServer', () => {
}
});
it('should send the success page with an explicit Content-Length (not chunked)', async () => {
const server = await startCallbackServer();
try {
const waitPromise = server.waitForCallback();
const response = await httpGet(
`${server.callbackUrl}?code=test-auth-code`,
);
await waitPromise;
expect(response.status).toBe(200);
expect(response.headers['transfer-encoding']).toBeUndefined();
expect(response.headers['content-length']).toBe(
String(Buffer.byteLength(response.body)),
);
expect(response.body).toContain('Authentication successful');
} finally {
server.close();
}
});
it('should deliver the full page even when the server is closed right after the callback resolves', async () => {
const server = await startCallbackServer();
const responsePromise = httpGet(
`${server.callbackUrl}?code=test-auth-code`,
);
await server.waitForCallback();
server.close();
const response = await responsePromise;
expect(response.status).toBe(200);
expect(response.body).toContain('Authentication successful');
expect(response.body).toContain('</html>');
});
it('should resolve with error when callback contains an error', async () => {
const server = await startCallbackServer();
@@ -195,7 +195,7 @@ export const startCallbackServer = (options?: {
const url = new URL(req.url ?? '/', `http://127.0.0.1`);
if (url.pathname !== '/callback') {
res.writeHead(404);
res.writeHead(404, { Connection: 'close' });
res.end('Not found');
return;
@@ -205,23 +205,28 @@ export const startCallbackServer = (options?: {
const error = url.searchParams.get('error');
const isDarkMode = url.searchParams.get('theme') === 'dark';
const headers = {
const result: CallbackResult = code
? { success: true, code }
: {
success: false,
error:
error ??
url.searchParams.get('error_description') ??
'Unknown error',
};
const body = result.success
? successHtml(isDarkMode)
: errorHtml(result.error, isDarkMode);
res.writeHead(200, {
'Content-Type': 'text/html',
'Content-Length': Buffer.byteLength(body),
Connection: 'close',
};
});
if (code) {
res.writeHead(200, headers);
res.end(successHtml(isDarkMode));
callbackResolve({ success: true, code });
} else {
const errorMessage =
error ?? url.searchParams.get('error_description') ?? 'Unknown error';
res.writeHead(200, headers);
res.end(errorHtml(errorMessage, isDarkMode));
callbackResolve({ success: false, error: errorMessage });
}
res.on('close', () => callbackResolve(result));
res.end(body);
});
server.listen(0, '127.0.0.1', () => {
@@ -252,8 +257,8 @@ export const startCallbackServer = (options?: {
},
close: () => {
clearTimeout(timeoutHandle);
server.closeAllConnections();
server.close();
server.closeIdleConnections();
},
});
});