@startanaicompany/cli 1.3.0 → 1.4.0

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.
@@ -0,0 +1,205 @@
1
+ /**
2
+ * OAuth Helper Module
3
+ * Handles Git OAuth authentication flow for SAAC CLI
4
+ */
5
+
6
+ const axios = require('axios');
7
+ const open = require('open');
8
+ const crypto = require('crypto');
9
+ const logger = require('./logger');
10
+ const { getApiUrl } = require('./config');
11
+
12
+ /**
13
+ * Extract git_host from repository URL
14
+ * @param {string} gitUrl - Git repository URL (SSH or HTTPS)
15
+ * @returns {string} - Git host domain
16
+ */
17
+ function extractGitHost(gitUrl) {
18
+ // SSH format: git@git.startanaicompany.com:user/repo.git
19
+ const sshMatch = gitUrl.match(/git@([^:]+):/);
20
+ if (sshMatch) {
21
+ return sshMatch[1];
22
+ }
23
+
24
+ // HTTPS format: https://git.startanaicompany.com/user/repo.git
25
+ const httpsMatch = gitUrl.match(/https?:\/\/([^/]+)/);
26
+ if (httpsMatch) {
27
+ return httpsMatch[1];
28
+ }
29
+
30
+ throw new Error('Invalid Git repository URL format. Expected SSH (git@host:user/repo.git) or HTTPS (https://host/user/repo.git)');
31
+ }
32
+
33
+ /**
34
+ * Initiate OAuth flow for a git_host
35
+ * @param {string} gitHost - Git host domain
36
+ * @param {string} apiKey - User's API key
37
+ * @returns {Promise<object>} - { gitUsername, gitHost }
38
+ */
39
+ async function connectGitAccount(gitHost, apiKey) {
40
+ const sessionId = crypto.randomBytes(16).toString('hex');
41
+
42
+ logger.newline();
43
+ logger.section(`Connecting to ${gitHost}`);
44
+ logger.newline();
45
+ logger.field('Session ID', sessionId);
46
+ logger.newline();
47
+
48
+ // Build authorization URL
49
+ const baseUrl = getApiUrl().replace('/api/v1', ''); // Remove /api/v1 suffix
50
+ const authUrl = `${baseUrl}/oauth/authorize?git_host=${encodeURIComponent(gitHost)}&session_id=${sessionId}`;
51
+
52
+ logger.info('Opening browser for authentication...');
53
+ logger.newline();
54
+ logger.warn('If browser doesn\'t open, visit:');
55
+ logger.log(` ${authUrl}`);
56
+ logger.newline();
57
+
58
+ // Open browser
59
+ try {
60
+ await open(authUrl);
61
+ } catch (error) {
62
+ logger.warn('Could not open browser automatically');
63
+ logger.info('Please open the URL above manually');
64
+ }
65
+
66
+ const spin = logger.spinner('Waiting for authorization...').start();
67
+
68
+ try {
69
+ // Poll for completion
70
+ const result = await pollForCompletion(sessionId, apiKey);
71
+
72
+ spin.succeed(`Connected to ${gitHost} as ${result.gitUsername}`);
73
+
74
+ return result;
75
+ } catch (error) {
76
+ spin.fail('Authorization failed');
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Poll OAuth session until completed
83
+ * @param {string} sessionId - Session ID
84
+ * @param {string} apiKey - User's API key
85
+ * @returns {Promise<object>} - { gitUsername, gitHost }
86
+ */
87
+ async function pollForCompletion(sessionId, apiKey) {
88
+ const pollInterval = 2000; // 2 seconds
89
+ const maxAttempts = 150; // 5 minutes total (150 * 2s)
90
+
91
+ const baseUrl = getApiUrl().replace('/api/v1', ''); // Remove /api/v1 suffix
92
+
93
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
94
+ await sleep(pollInterval);
95
+
96
+ try {
97
+ const response = await axios.get(
98
+ `${baseUrl}/oauth/poll/${sessionId}`,
99
+ {
100
+ headers: {
101
+ 'X-API-Key': apiKey,
102
+ },
103
+ }
104
+ );
105
+
106
+ const { status, gitUsername, gitHost } = response.data;
107
+
108
+ if (status === 'completed') {
109
+ return { gitUsername, gitHost };
110
+ }
111
+
112
+ if (status === 'failed') {
113
+ throw new Error('OAuth authorization failed');
114
+ }
115
+
116
+ // Still pending, continue polling
117
+ // Silent polling - spinner shows progress
118
+ } catch (error) {
119
+ if (error.response?.status === 404) {
120
+ throw new Error('OAuth session not found or expired');
121
+ }
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ throw new Error('OAuth authorization timed out (5 minutes). Please try again.');
127
+ }
128
+
129
+ /**
130
+ * Check if user has OAuth connection for git_host
131
+ * @param {string} gitHost - Git host domain
132
+ * @param {string} apiKey - User's API key
133
+ * @returns {Promise<object|null>} - Connection object or null
134
+ */
135
+ async function getConnection(gitHost, apiKey) {
136
+ try {
137
+ const response = await axios.get(
138
+ `${getApiUrl()}/users/me/oauth`,
139
+ {
140
+ headers: {
141
+ 'X-API-Key': apiKey,
142
+ },
143
+ }
144
+ );
145
+
146
+ const connection = response.data.connections.find(
147
+ (conn) => conn.gitHost === gitHost
148
+ );
149
+
150
+ return connection || null;
151
+ } catch (error) {
152
+ return null;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * List all OAuth connections
158
+ * @param {string} apiKey - User's API key
159
+ * @returns {Promise<array>} - Array of connection objects
160
+ */
161
+ async function listConnections(apiKey) {
162
+ const response = await axios.get(
163
+ `${getApiUrl()}/users/me/oauth`,
164
+ {
165
+ headers: {
166
+ 'X-API-Key': apiKey,
167
+ },
168
+ }
169
+ );
170
+
171
+ return response.data.connections || [];
172
+ }
173
+
174
+ /**
175
+ * Revoke OAuth connection for git_host
176
+ * @param {string} gitHost - Git host domain
177
+ * @param {string} apiKey - User's API key
178
+ */
179
+ async function revokeConnection(gitHost, apiKey) {
180
+ await axios.delete(
181
+ `${getApiUrl()}/users/me/oauth/${encodeURIComponent(gitHost)}`,
182
+ {
183
+ headers: {
184
+ 'X-API-Key': apiKey,
185
+ },
186
+ }
187
+ );
188
+ }
189
+
190
+ /**
191
+ * Sleep utility
192
+ * @param {number} ms - Milliseconds to sleep
193
+ * @returns {Promise<void>}
194
+ */
195
+ function sleep(ms) {
196
+ return new Promise((resolve) => setTimeout(resolve, ms));
197
+ }
198
+
199
+ module.exports = {
200
+ extractGitHost,
201
+ connectGitAccount,
202
+ getConnection,
203
+ listConnections,
204
+ revokeConnection,
205
+ };