1import openBrowserAsync from 'better-opn';
2import http from 'http';
3import { Socket } from 'node:net';
4import querystring from 'querystring';
5
6import * as Log from '../../log';
7
8export async function getSessionUsingBrowserAuthFlowAsync(options: {
9  expoWebsiteUrl: string;
10  serverPort: number;
11}): Promise<string> {
12  const { expoWebsiteUrl, serverPort } = options;
13
14  const scheme = 'http';
15  const hostname = 'localhost';
16  const path = '/auth/callback';
17  const redirectUri = `${scheme}://${hostname}:${serverPort}${path}`;
18
19  const buildExpoSsoLoginUrl = (): string => {
20    const data = {
21      app_redirect_uri: redirectUri,
22    };
23    const params = querystring.stringify(data);
24    return `${expoWebsiteUrl}/sso-login?${params}`;
25  };
26
27  // Start server and begin auth flow
28  const executeAuthFlow = (): Promise<string> => {
29    return new Promise<string>(async (resolve, reject) => {
30      const connections = new Set<Socket>();
31
32      const server = http.createServer(
33        (request: http.IncomingMessage, response: http.ServerResponse) => {
34          try {
35            if (!(request.method === 'GET' && request.url?.includes('/auth/callback'))) {
36              throw new Error('Unexpected SSO login response.');
37            }
38            const url = new URL(request.url, `http:${request.headers.host}`);
39            const sessionSecret = url.searchParams.get('session_secret');
40
41            if (!sessionSecret) {
42              throw new Error('Request missing session_secret search parameter.');
43            }
44            resolve(sessionSecret);
45            response.writeHead(200, { 'Content-Type': 'text/plain' });
46            response.write(`Website login has completed. You can now close this tab.`);
47            response.end();
48          } catch (error) {
49            reject(error);
50          } finally {
51            server.close();
52            // Ensure that the server shuts down
53            for (const connection of connections) {
54              connection.destroy();
55            }
56          }
57        }
58      );
59
60      server.listen(serverPort, hostname, () => {
61        Log.log('Waiting for browser login...');
62      });
63
64      server.on('connection', (connection) => {
65        connections.add(connection);
66
67        connection.on('close', () => {
68          connections.delete(connection);
69        });
70      });
71
72      const authorizeUrl = buildExpoSsoLoginUrl();
73      openBrowserAsync(authorizeUrl);
74    });
75  };
76
77  return await executeAuthFlow();
78}
79