@plosson/agentio 0.3.1 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plosson/agentio",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "CLI for LLM agents to interact with communication and tracking services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,30 +1,9 @@
1
- import { createServer, type Server } from 'http';
2
1
  import { URL } from 'url';
3
2
  import { GITHUB_OAUTH_CONFIG } from '../config/credentials';
3
+ import { findAvailablePort, startOAuthCallbackServer, launchBrowser } from './oauth-server';
4
4
 
5
5
  const GITHUB_SCOPES = ['repo'];
6
6
 
7
- const PORT_RANGE_START = 3000;
8
- const PORT_RANGE_END = 3010;
9
-
10
- async function findAvailablePort(): Promise<number> {
11
- for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
12
- try {
13
- await new Promise<void>((resolve, reject) => {
14
- const server = createServer();
15
- server.listen(port, () => {
16
- server.close(() => resolve());
17
- });
18
- server.on('error', reject);
19
- });
20
- return port;
21
- } catch {
22
- continue;
23
- }
24
- }
25
- throw new Error(`No available port found in range ${PORT_RANGE_START}-${PORT_RANGE_END}`);
26
- }
27
-
28
7
  export interface GitHubOAuthResult {
29
8
  accessToken: string;
30
9
  }
@@ -32,110 +11,54 @@ export interface GitHubOAuthResult {
32
11
  export async function performGitHubOAuthFlow(): Promise<GitHubOAuthResult> {
33
12
  const port = await findAvailablePort();
34
13
  const redirectUri = `http://localhost:${port}/callback`;
14
+ const state = Math.random().toString(36).substring(7);
35
15
 
36
16
  const authUrl = new URL('https://github.com/login/oauth/authorize');
37
17
  authUrl.searchParams.set('client_id', GITHUB_OAUTH_CONFIG.clientId);
38
18
  authUrl.searchParams.set('redirect_uri', redirectUri);
39
19
  authUrl.searchParams.set('scope', GITHUB_SCOPES.join(' '));
40
- authUrl.searchParams.set('state', Math.random().toString(36).substring(7));
41
-
42
- return new Promise((resolve, reject) => {
43
- let server: Server;
44
-
45
- const timeout = setTimeout(() => {
46
- server?.close();
47
- reject(new Error('OAuth flow timed out after 5 minutes'));
48
- }, 5 * 60 * 1000);
49
-
50
- server = createServer(async (req, res) => {
51
- const url = new URL(req.url || '', `http://localhost:${port}`);
52
-
53
- if (url.pathname !== '/callback') {
54
- res.writeHead(404);
55
- res.end('Not found');
56
- return;
57
- }
58
-
59
- const code = url.searchParams.get('code');
60
- const error = url.searchParams.get('error');
61
- const errorDescription = url.searchParams.get('error_description');
62
-
63
- if (error) {
64
- res.writeHead(200, { 'Content-Type': 'text/html' });
65
- res.end('<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>');
66
- clearTimeout(timeout);
67
- server.close();
68
- reject(new Error(`GitHub OAuth error: ${error} - ${errorDescription || 'Unknown error'}`));
69
- return;
70
- }
20
+ authUrl.searchParams.set('state', state);
71
21
 
72
- if (!code) {
73
- res.writeHead(400, { 'Content-Type': 'text/html' });
74
- res.end('<html><body><h1>Missing Authorization Code</h1><p>You can close this window.</p></body></html>');
75
- clearTimeout(timeout);
76
- server.close();
77
- reject(new Error('Missing authorization code in OAuth callback'));
78
- return;
79
- }
80
-
81
- try {
82
- // Exchange code for access token
83
- const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
84
- method: 'POST',
85
- headers: {
86
- Accept: 'application/json',
87
- 'Content-Type': 'application/json',
88
- },
89
- body: JSON.stringify({
90
- client_id: GITHUB_OAUTH_CONFIG.clientId,
91
- client_secret: GITHUB_OAUTH_CONFIG.clientSecret,
92
- code,
93
- redirect_uri: redirectUri,
94
- }),
95
- });
96
-
97
- const tokenData = await tokenResponse.json() as {
98
- access_token?: string;
99
- error?: string;
100
- error_description?: string;
101
- };
102
-
103
- if (tokenData.error || !tokenData.access_token) {
104
- throw new Error(tokenData.error_description || tokenData.error || 'Failed to get access token');
105
- }
106
-
107
- res.writeHead(200, { 'Content-Type': 'text/html' });
108
- res.end('<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>');
109
-
110
- clearTimeout(timeout);
111
- server.close();
22
+ // Start callback server and browser in parallel
23
+ const callbackPromise = startOAuthCallbackServer({
24
+ port,
25
+ serviceName: 'GitHub',
26
+ expectedState: state,
27
+ });
112
28
 
113
- resolve({
114
- accessToken: tokenData.access_token,
115
- });
116
- } catch (err) {
117
- res.writeHead(500);
118
- res.end('Failed to exchange authorization code');
119
- clearTimeout(timeout);
120
- server.close();
121
- reject(err);
122
- }
123
- });
29
+ console.error(`\nOpening browser for GitHub authorization...`);
30
+ console.error(`If browser doesn't open, visit:\n${authUrl.toString()}\n`);
31
+ launchBrowser(authUrl.toString());
32
+
33
+ // Wait for the callback with the authorization code
34
+ const { code } = await callbackPromise;
35
+
36
+ // Exchange code for access token
37
+ const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
38
+ method: 'POST',
39
+ headers: {
40
+ Accept: 'application/json',
41
+ 'Content-Type': 'application/json',
42
+ },
43
+ body: JSON.stringify({
44
+ client_id: GITHUB_OAUTH_CONFIG.clientId,
45
+ client_secret: GITHUB_OAUTH_CONFIG.clientSecret,
46
+ code,
47
+ redirect_uri: redirectUri,
48
+ }),
49
+ });
124
50
 
125
- server.listen(port, () => {
126
- console.error(`\nOpening browser for GitHub authorization...`);
127
- console.error(`If browser doesn't open, visit:\n${authUrl.toString()}\n`);
51
+ const tokenData = await tokenResponse.json() as {
52
+ access_token?: string;
53
+ error?: string;
54
+ error_description?: string;
55
+ };
128
56
 
129
- // Open browser
130
- const open = process.platform === 'darwin' ? 'open' :
131
- process.platform === 'win32' ? 'start' : 'xdg-open';
132
- Bun.spawn([open, authUrl.toString()], { stdout: 'ignore', stderr: 'ignore' });
133
- });
57
+ if (tokenData.error || !tokenData.access_token) {
58
+ throw new Error(tokenData.error_description || tokenData.error || 'Failed to get access token');
59
+ }
134
60
 
135
- server.on('error', (err) => {
136
- clearTimeout(timeout);
137
- server?.close();
138
- reject(err);
139
- });
140
- });
61
+ return {
62
+ accessToken: tokenData.access_token,
63
+ };
141
64
  }
@@ -1,6 +1,6 @@
1
- import { createServer, type Server } from 'http';
2
1
  import { URL } from 'url';
3
2
  import { JIRA_OAUTH_CONFIG } from '../config/credentials';
3
+ import { startOAuthCallbackServer, launchBrowser } from './oauth-server';
4
4
 
5
5
  const ATLASSIAN_AUTH_URL = 'https://auth.atlassian.com/authorize';
6
6
  const ATLASSIAN_TOKEN_URL = 'https://auth.atlassian.com/oauth/token';
@@ -114,8 +114,8 @@ export async function performJiraOAuthFlow(
114
114
  selectSite?: (sites: AtlassianSite[]) => Promise<AtlassianSite>
115
115
  ): Promise<JiraOAuthResult> {
116
116
  const redirectUri = `http://localhost:${OAUTH_PORT}/callback`;
117
-
118
117
  const state = Math.random().toString(36).substring(2);
118
+
119
119
  const authUrl = new URL(ATLASSIAN_AUTH_URL);
120
120
  authUrl.searchParams.set('audience', 'api.atlassian.com');
121
121
  authUrl.searchParams.set('client_id', JIRA_OAUTH_CONFIG.clientId);
@@ -125,107 +125,45 @@ export async function performJiraOAuthFlow(
125
125
  authUrl.searchParams.set('response_type', 'code');
126
126
  authUrl.searchParams.set('prompt', 'consent');
127
127
 
128
- return new Promise((resolve, reject) => {
129
- let server: Server;
130
-
131
- const timeout = setTimeout(() => {
132
- server?.close();
133
- reject(new Error('OAuth flow timed out after 5 minutes'));
134
- }, 5 * 60 * 1000);
135
-
136
- server = createServer(async (req, res) => {
137
- const url = new URL(req.url || '', `http://localhost:${OAUTH_PORT}`);
138
-
139
- if (url.pathname !== '/callback') {
140
- res.writeHead(404);
141
- res.end('Not found');
142
- return;
143
- }
144
-
145
- const code = url.searchParams.get('code');
146
- const error = url.searchParams.get('error');
147
- const returnedState = url.searchParams.get('state');
148
-
149
- if (error) {
150
- res.writeHead(200, { 'Content-Type': 'text/html' });
151
- res.end('<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>');
152
- clearTimeout(timeout);
153
- server.close();
154
- reject(new Error(`OAuth error: ${error}`));
155
- return;
156
- }
157
-
158
- if (returnedState !== state) {
159
- res.writeHead(400, { 'Content-Type': 'text/html' });
160
- res.end('<html><body><h1>State Mismatch</h1><p>You can close this window.</p></body></html>');
161
- clearTimeout(timeout);
162
- server.close();
163
- reject(new Error('OAuth state mismatch - possible CSRF attack'));
164
- return;
165
- }
166
-
167
- if (!code) {
168
- res.writeHead(400, { 'Content-Type': 'text/html' });
169
- res.end('<html><body><h1>Missing Authorization Code</h1><p>You can close this window.</p></body></html>');
170
- clearTimeout(timeout);
171
- server.close();
172
- reject(new Error('Missing authorization code in OAuth callback'));
173
- return;
174
- }
175
-
176
- try {
177
- res.writeHead(200, { 'Content-Type': 'text/html' });
178
- res.end('<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>');
179
-
180
- clearTimeout(timeout);
181
- server.close();
182
-
183
- // Exchange code for tokens
184
- const tokens = await exchangeCodeForTokens(code, JIRA_OAUTH_CONFIG.clientId, JIRA_OAUTH_CONFIG.clientSecret, redirectUri);
185
-
186
- // Get accessible resources to find cloud ID
187
- const sites = await getAccessibleResources(tokens.accessToken);
188
-
189
- if (sites.length === 0) {
190
- throw new Error('No accessible Jira sites found. Make sure your app has the correct permissions.');
191
- }
192
-
193
- // Let user select site if multiple, otherwise use the first one
194
- let selectedSite: AtlassianSite;
195
- if (sites.length === 1) {
196
- selectedSite = sites[0];
197
- } else if (selectSite) {
198
- selectedSite = await selectSite(sites);
199
- } else {
200
- selectedSite = sites[0];
201
- }
202
-
203
- resolve({
204
- accessToken: tokens.accessToken,
205
- refreshToken: tokens.refreshToken,
206
- expiryDate: Date.now() + tokens.expiresIn * 1000,
207
- cloudId: selectedSite.id,
208
- siteUrl: selectedSite.url,
209
- });
210
- } catch (err) {
211
- reject(err);
212
- }
213
- });
214
-
215
- server.listen(OAUTH_PORT, () => {
216
- console.error(`\nOpening browser for Atlassian authorization...`);
217
- console.error(`If browser doesn't open, visit:\n${authUrl.toString()}\n`);
218
-
219
- // Open browser
220
- const open = process.platform === 'darwin' ? 'open' :
221
- process.platform === 'win32' ? 'start' : 'xdg-open';
222
- Bun.spawn([open, authUrl.toString()], { stdout: 'ignore', stderr: 'ignore' });
223
- });
224
-
225
- server.on('error', (err) => {
226
- clearTimeout(timeout);
227
- server?.close();
228
- reject(err);
229
- });
128
+ // Start callback server and browser in parallel
129
+ const callbackPromise = startOAuthCallbackServer({
130
+ port: OAUTH_PORT,
131
+ serviceName: 'Atlassian',
132
+ expectedState: state,
230
133
  });
134
+
135
+ console.error(`\nOpening browser for Atlassian authorization...`);
136
+ console.error(`If browser doesn't open, visit:\n${authUrl.toString()}\n`);
137
+ launchBrowser(authUrl.toString());
138
+
139
+ // Wait for the callback with the authorization code
140
+ const { code } = await callbackPromise;
141
+
142
+ // Exchange code for tokens
143
+ const tokens = await exchangeCodeForTokens(code, JIRA_OAUTH_CONFIG.clientId, JIRA_OAUTH_CONFIG.clientSecret, redirectUri);
144
+
145
+ // Get accessible resources to find cloud ID
146
+ const sites = await getAccessibleResources(tokens.accessToken);
147
+
148
+ if (sites.length === 0) {
149
+ throw new Error('No accessible Jira sites found. Make sure your app has the correct permissions.');
150
+ }
151
+
152
+ // Let user select site if multiple, otherwise use the first one
153
+ let selectedSite: AtlassianSite;
154
+ if (sites.length === 1) {
155
+ selectedSite = sites[0];
156
+ } else if (selectSite) {
157
+ selectedSite = await selectSite(sites);
158
+ } else {
159
+ selectedSite = sites[0];
160
+ }
161
+
162
+ return {
163
+ accessToken: tokens.accessToken,
164
+ refreshToken: tokens.refreshToken,
165
+ expiryDate: Date.now() + tokens.expiresIn * 1000,
166
+ cloudId: selectedSite.id,
167
+ siteUrl: selectedSite.url,
168
+ };
231
169
  }
@@ -0,0 +1,149 @@
1
+ import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'http';
2
+ import { URL } from 'url';
3
+
4
+ const PORT_RANGE_START = 3000;
5
+ const PORT_RANGE_END = 3010;
6
+ const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
7
+
8
+ /**
9
+ * Find an available port in the range 3000-3010.
10
+ */
11
+ export async function findAvailablePort(): Promise<number> {
12
+ for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
13
+ try {
14
+ await new Promise<void>((resolve, reject) => {
15
+ const server = createServer();
16
+ server.listen(port, () => {
17
+ server.close(() => resolve());
18
+ });
19
+ server.on('error', reject);
20
+ });
21
+ return port;
22
+ } catch {
23
+ continue;
24
+ }
25
+ }
26
+ throw new Error(`No available port found in range ${PORT_RANGE_START}-${PORT_RANGE_END}`);
27
+ }
28
+
29
+ /**
30
+ * Launch the default browser to open a URL.
31
+ */
32
+ export function launchBrowser(url: string): void {
33
+ const open = process.platform === 'darwin' ? 'open' :
34
+ process.platform === 'win32' ? 'start' : 'xdg-open';
35
+ Bun.spawn([open, url], { stdout: 'ignore', stderr: 'ignore' });
36
+ }
37
+
38
+ /**
39
+ * HTML templates for OAuth callback responses.
40
+ */
41
+ export const OAuthHtml = {
42
+ success: '<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>',
43
+ failed: '<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>',
44
+ missingCode: '<html><body><h1>Missing Authorization Code</h1><p>You can close this window.</p></body></html>',
45
+ stateMismatch: '<html><body><h1>State Mismatch</h1><p>You can close this window.</p></body></html>',
46
+ };
47
+
48
+ export interface OAuthCallbackResult {
49
+ code: string;
50
+ state?: string;
51
+ }
52
+
53
+ export interface OAuthServerConfig {
54
+ port: number;
55
+ serviceName: string;
56
+ expectedState?: string;
57
+ }
58
+
59
+ /**
60
+ * Start an OAuth callback server that listens for the authorization code.
61
+ *
62
+ * @param config Configuration for the server
63
+ * @returns Promise that resolves with the authorization code
64
+ */
65
+ export function startOAuthCallbackServer(
66
+ config: OAuthServerConfig
67
+ ): Promise<OAuthCallbackResult> {
68
+ const { port, serviceName, expectedState } = config;
69
+
70
+ return new Promise((resolve, reject) => {
71
+ let server: Server;
72
+
73
+ const timeout = setTimeout(() => {
74
+ server?.close();
75
+ reject(new Error('OAuth flow timed out after 5 minutes'));
76
+ }, TIMEOUT_MS);
77
+
78
+ const handleCallback = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
79
+ const url = new URL(req.url || '', `http://localhost:${port}`);
80
+
81
+ if (url.pathname !== '/callback') {
82
+ res.writeHead(404);
83
+ res.end('Not found');
84
+ return;
85
+ }
86
+
87
+ const code = url.searchParams.get('code');
88
+ const error = url.searchParams.get('error');
89
+ const errorDescription = url.searchParams.get('error_description');
90
+ const returnedState = url.searchParams.get('state');
91
+
92
+ if (error) {
93
+ res.writeHead(200, { 'Content-Type': 'text/html' });
94
+ res.end(OAuthHtml.failed);
95
+ clearTimeout(timeout);
96
+ server.close();
97
+ const errorMsg = errorDescription ? `${error} - ${errorDescription}` : error;
98
+ reject(new Error(`${serviceName} OAuth error: ${errorMsg}`));
99
+ return;
100
+ }
101
+
102
+ if (expectedState && returnedState !== expectedState) {
103
+ res.writeHead(400, { 'Content-Type': 'text/html' });
104
+ res.end(OAuthHtml.stateMismatch);
105
+ clearTimeout(timeout);
106
+ server.close();
107
+ reject(new Error('OAuth state mismatch - possible CSRF attack'));
108
+ return;
109
+ }
110
+
111
+ if (!code) {
112
+ res.writeHead(400, { 'Content-Type': 'text/html' });
113
+ res.end(OAuthHtml.missingCode);
114
+ clearTimeout(timeout);
115
+ server.close();
116
+ reject(new Error('Missing authorization code in OAuth callback'));
117
+ return;
118
+ }
119
+
120
+ res.writeHead(200, { 'Content-Type': 'text/html' });
121
+ res.end(OAuthHtml.success);
122
+ clearTimeout(timeout);
123
+ server.close();
124
+ resolve({ code, state: returnedState || undefined });
125
+ };
126
+
127
+ server = createServer((req, res) => {
128
+ handleCallback(req, res).catch((err) => {
129
+ if (!res.headersSent) {
130
+ res.writeHead(500);
131
+ res.end('Internal server error');
132
+ }
133
+ clearTimeout(timeout);
134
+ server?.close();
135
+ reject(err);
136
+ });
137
+ });
138
+
139
+ server.listen(port, () => {
140
+ // Server is ready for callback
141
+ });
142
+
143
+ server.on('error', (err) => {
144
+ clearTimeout(timeout);
145
+ server?.close();
146
+ reject(err);
147
+ });
148
+ });
149
+ }
package/src/auth/oauth.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { createServer, type Server } from 'http';
2
- import { URL } from 'url';
3
1
  import { google } from 'googleapis';
4
2
  import { GOOGLE_OAUTH_CONFIG } from '../config/credentials';
3
+ import { findAvailablePort, startOAuthCallbackServer, launchBrowser } from './oauth-server';
5
4
  import type { OAuthTokens } from '../types/tokens';
6
5
 
7
6
  const GMAIL_SCOPES = [
@@ -18,26 +17,10 @@ const GCHAT_SCOPES = [
18
17
  'https://www.googleapis.com/auth/userinfo.email', // get user email for profile naming
19
18
  ];
20
19
 
21
- const PORT_RANGE_START = 3000;
22
- const PORT_RANGE_END = 3010;
23
-
24
- async function findAvailablePort(): Promise<number> {
25
- for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
26
- try {
27
- await new Promise<void>((resolve, reject) => {
28
- const server = createServer();
29
- server.listen(port, () => {
30
- server.close(() => resolve());
31
- });
32
- server.on('error', reject);
33
- });
34
- return port;
35
- } catch {
36
- continue;
37
- }
38
- }
39
- throw new Error(`No available port found in range ${PORT_RANGE_START}-${PORT_RANGE_END}`);
40
- }
20
+ const SCOPES: Record<'gmail' | 'gchat', string[]> = {
21
+ gmail: GMAIL_SCOPES,
22
+ gchat: GCHAT_SCOPES,
23
+ };
41
24
 
42
25
  export async function performOAuthFlow(
43
26
  service: 'gmail' | 'gchat'
@@ -51,7 +34,7 @@ export async function performOAuthFlow(
51
34
  redirectUri
52
35
  );
53
36
 
54
- const scopes = service === 'gmail' ? GMAIL_SCOPES : service === 'gchat' ? GCHAT_SCOPES : [];
37
+ const scopes = SCOPES[service];
55
38
 
56
39
  const authUrl = oauth2Client.generateAuthUrl({
57
40
  access_type: 'offline',
@@ -59,83 +42,27 @@ export async function performOAuthFlow(
59
42
  prompt: 'consent',
60
43
  });
61
44
 
62
- return new Promise((resolve, reject) => {
63
- let server: Server;
64
-
65
- const timeout = setTimeout(() => {
66
- server?.close();
67
- reject(new Error('OAuth flow timed out after 5 minutes'));
68
- }, 5 * 60 * 1000);
69
-
70
- server = createServer(async (req, res) => {
71
- const url = new URL(req.url || '', `http://localhost:${port}`);
72
-
73
- if (url.pathname !== '/callback') {
74
- res.writeHead(404);
75
- res.end('Not found');
76
- return;
77
- }
78
-
79
- const code = url.searchParams.get('code');
80
- const error = url.searchParams.get('error');
81
-
82
- if (error) {
83
- res.writeHead(200, { 'Content-Type': 'text/html' });
84
- res.end('<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>');
85
- clearTimeout(timeout);
86
- server.close();
87
- reject(new Error(`OAuth error: ${error}`));
88
- return;
89
- }
90
-
91
- if (!code) {
92
- res.writeHead(400, { 'Content-Type': 'text/html' });
93
- res.end('<html><body><h1>Missing Authorization Code</h1><p>You can close this window.</p></body></html>');
94
- clearTimeout(timeout);
95
- server.close();
96
- reject(new Error('Missing authorization code in OAuth callback'));
97
- return;
98
- }
99
-
100
- try {
101
- const { tokens } = await oauth2Client.getToken(code);
102
-
103
- res.writeHead(200, { 'Content-Type': 'text/html' });
104
- res.end('<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>');
105
-
106
- clearTimeout(timeout);
107
- server.close();
45
+ // Start callback server and browser in parallel
46
+ const callbackPromise = startOAuthCallbackServer({
47
+ port,
48
+ serviceName: 'Google',
49
+ });
108
50
 
109
- resolve({
110
- access_token: tokens.access_token!,
111
- refresh_token: tokens.refresh_token || undefined,
112
- expiry_date: tokens.expiry_date || undefined,
113
- token_type: tokens.token_type || 'Bearer',
114
- scope: tokens.scope || undefined,
115
- });
116
- } catch (err) {
117
- res.writeHead(500);
118
- res.end('Failed to exchange authorization code');
119
- clearTimeout(timeout);
120
- server.close();
121
- reject(err);
122
- }
123
- });
51
+ console.error(`\nOpening browser for authorization...`);
52
+ console.error(`If browser doesn't open, visit:\n${authUrl}\n`);
53
+ launchBrowser(authUrl);
124
54
 
125
- server.listen(port, () => {
126
- console.error(`\nOpening browser for authorization...`);
127
- console.error(`If browser doesn't open, visit:\n${authUrl}\n`);
55
+ // Wait for the callback with the authorization code
56
+ const { code } = await callbackPromise;
128
57
 
129
- // Open browser
130
- const open = process.platform === 'darwin' ? 'open' :
131
- process.platform === 'win32' ? 'start' : 'xdg-open';
132
- Bun.spawn([open, authUrl], { stdout: 'ignore', stderr: 'ignore' });
133
- });
58
+ // Exchange code for tokens using Google's OAuth client
59
+ const { tokens } = await oauth2Client.getToken(code);
134
60
 
135
- server.on('error', (err) => {
136
- clearTimeout(timeout);
137
- server?.close();
138
- reject(err);
139
- });
140
- });
61
+ return {
62
+ access_token: tokens.access_token!,
63
+ refresh_token: tokens.refresh_token || undefined,
64
+ expiry_date: tokens.expiry_date || undefined,
65
+ token_type: tokens.token_type || 'Bearer',
66
+ scope: tokens.scope || undefined,
67
+ };
141
68
  }
@@ -3,27 +3,13 @@ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypt
3
3
  import { readFile, writeFile } from 'fs/promises';
4
4
  import { existsSync } from 'fs';
5
5
  import { join } from 'path';
6
- import { createInterface } from 'readline';
7
6
  import { loadConfig, saveConfig, setEnv, unsetEnv, listEnv } from '../config/config-manager';
8
7
  import { getAllCredentials, setAllCredentials } from '../auth/token-store';
9
8
  import { CliError, handleError } from '../utils/errors';
9
+ import { confirm } from '../utils/stdin';
10
10
  import type { Config } from '../types/config';
11
11
  import type { StoredCredentials } from '../types/tokens';
12
12
 
13
- async function confirm(message: string): Promise<boolean> {
14
- const rl = createInterface({
15
- input: process.stdin,
16
- output: process.stderr,
17
- });
18
-
19
- return new Promise((resolve) => {
20
- rl.question(`${message} [y/N] `, (answer) => {
21
- rl.close();
22
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
23
- });
24
- });
25
- }
26
-
27
13
  const ALGORITHM = 'aes-256-gcm';
28
14
 
29
15
  interface ExportedData {