@thinremote/thinr-cli 1.0.2-alpha.2 → 1.1.0-alpha.1
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/CHANGELOG.md +90 -0
- package/README.md +362 -87
- package/bin/thinr.js +51 -11
- package/commands/device/_shared.js +48 -0
- package/commands/device/console.js +15 -0
- package/commands/device/exec.js +76 -0
- package/commands/device/index.js +31 -0
- package/commands/device/list.js +43 -0
- package/commands/device/property.js +50 -0
- package/commands/device/proxy.js +42 -0
- package/commands/device/resource.js +51 -0
- package/commands/device/status.js +26 -0
- package/commands/device/update.js +53 -0
- package/commands/logout.js +12 -5
- package/commands/product.js +146 -177
- package/commands/profile.js +105 -0
- package/lib/api.js +54 -31
- package/lib/auth.js +225 -309
- package/lib/config.js +176 -45
- package/lib/console.js +87 -108
- package/lib/device-api.js +257 -0
- package/lib/devices.js +69 -0
- package/lib/errors.js +53 -0
- package/lib/mcp/helpers.js +34 -0
- package/lib/mcp/registry.js +32 -0
- package/lib/mcp/server.js +63 -0
- package/lib/mcp/tools-devices.js +194 -0
- package/lib/mcp/tools-exec.js +213 -0
- package/lib/mcp/tools-filesystem.js +163 -0
- package/lib/mcp/tools-products.js +232 -0
- package/lib/mcp/tools-profiles.js +30 -0
- package/lib/mcp/tools-properties.js +79 -0
- package/lib/mcp/tools-resources.js +76 -0
- package/lib/mcp/tools-scripts.js +172 -0
- package/lib/monitoring.js +72 -0
- package/lib/output.js +114 -0
- package/lib/product.js +373 -0
- package/lib/property.js +41 -57
- package/lib/proxy.js +58 -90
- package/lib/resource.js +106 -72
- package/lib/status.js +7 -33
- package/lib/storage.js +128 -0
- package/package.json +68 -37
- package/.github/workflows/publish.yml +0 -79
- package/commands/device.js +0 -268
- package/lib/device.js +0 -76
package/lib/auth.js
CHANGED
|
@@ -7,7 +7,13 @@ import { writeConfig, readConfig, mergeConfig } from './config.js';
|
|
|
7
7
|
|
|
8
8
|
import api, { setBaseURL } from './api.js';
|
|
9
9
|
|
|
10
|
+
// OAuth device-flow client identifier registered for this CLI on the
|
|
11
|
+
// ThinRemote OAuth provider. Stable across versions.
|
|
12
|
+
const OAUTH_CLIENT_ID = '3b40164d28730e416cbd';
|
|
10
13
|
|
|
14
|
+
// Hard ceiling on how long to keep polling for the user to finish the
|
|
15
|
+
// device-code flow in the browser before we give up (10 minutes).
|
|
16
|
+
const OAUTH_DEVICE_CODE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Authenticate user
|
|
@@ -21,7 +27,7 @@ export async function authenticate() {
|
|
|
21
27
|
{ name: 'OAuth', value: 'oauth' },
|
|
22
28
|
{ name: 'Token', value: 'token' },
|
|
23
29
|
{ name: 'Username and password', value: 'userpass' },
|
|
24
|
-
]
|
|
30
|
+
],
|
|
25
31
|
});
|
|
26
32
|
|
|
27
33
|
// Ask for server URL
|
|
@@ -30,7 +36,7 @@ export async function authenticate() {
|
|
|
30
36
|
default: 'console.thinr.io',
|
|
31
37
|
validate: (input) => {
|
|
32
38
|
return input ? true : 'Server URL is required';
|
|
33
|
-
}
|
|
39
|
+
},
|
|
34
40
|
});
|
|
35
41
|
|
|
36
42
|
setBaseURL(`https://${server}`);
|
|
@@ -48,7 +54,6 @@ export async function authenticate() {
|
|
|
48
54
|
// Authenticate with token
|
|
49
55
|
await authenticateWithToken();
|
|
50
56
|
}
|
|
51
|
-
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
/**
|
|
@@ -56,150 +61,114 @@ export async function authenticate() {
|
|
|
56
61
|
* @returns {Promise<void>}
|
|
57
62
|
*/
|
|
58
63
|
async function authenticateWithUserPass() {
|
|
64
|
+
// Ask for credentials
|
|
65
|
+
const username = await input({
|
|
66
|
+
message: 'Enter username:',
|
|
67
|
+
validate: (input) => (input ? true : 'Username is required'),
|
|
68
|
+
});
|
|
69
|
+
const password = await input({
|
|
70
|
+
message: 'Enter password:',
|
|
71
|
+
type: 'password',
|
|
72
|
+
mask: '*',
|
|
73
|
+
validate: (input) => (input ? true : 'Password is required'),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const spinner = ora('Authenticating...').start();
|
|
77
|
+
|
|
59
78
|
try {
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
message: 'Enter password:',
|
|
69
|
-
type: 'password',
|
|
70
|
-
mask: '*',
|
|
71
|
-
validate: (input) => {
|
|
72
|
-
return input ? true : 'Password is required';
|
|
73
|
-
}
|
|
79
|
+
// OAuth2 password grant
|
|
80
|
+
const formData = new URLSearchParams();
|
|
81
|
+
formData.append('grant_type', 'password');
|
|
82
|
+
formData.append('username', username);
|
|
83
|
+
formData.append('password', password);
|
|
84
|
+
|
|
85
|
+
await api.post(`/oauth/token`, formData, {
|
|
86
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
74
87
|
});
|
|
75
88
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
// Replace any existing token with a fresh permanent one
|
|
90
|
+
spinner.text = 'Preparing user token...';
|
|
79
91
|
try {
|
|
80
|
-
|
|
81
|
-
const formData = new URLSearchParams();
|
|
82
|
-
formData.append('grant_type', 'password');
|
|
83
|
-
formData.append('username', username);
|
|
84
|
-
formData.append('password', password);
|
|
85
|
-
|
|
86
|
-
// Authenticate with server using OAuth2 password flow
|
|
87
|
-
const response = await api.post(
|
|
88
|
-
`/oauth/token`,
|
|
89
|
-
formData,
|
|
90
|
-
{
|
|
91
|
-
headers: {
|
|
92
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
// Update spinner message
|
|
98
|
-
spinner.text = 'Preparing user token...';
|
|
99
|
-
|
|
100
|
-
// Delete existing token if it exists
|
|
101
|
-
try {
|
|
102
|
-
await api.delete(
|
|
103
|
-
`/v1/users/${username}/tokens/ThinRemote`,
|
|
104
|
-
);
|
|
105
|
-
} catch (error) {
|
|
106
|
-
// Ignore 404 errors (token doesn't exist)
|
|
107
|
-
if (error.response && error.response.status !== 404) {
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Create new token
|
|
113
|
-
spinner.text = 'Creating new user token...';
|
|
114
|
-
const createTokenResponse = await api.post(
|
|
115
|
-
`/v1/users/${username}/tokens`,
|
|
116
|
-
{
|
|
117
|
-
"enabled": true,
|
|
118
|
-
"allow": {"*": {"*": "*"}},
|
|
119
|
-
"deny": {},
|
|
120
|
-
"token": "ThinRemote",
|
|
121
|
-
"name": "ThinRemote",
|
|
122
|
-
"description": ""
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
headers: {
|
|
126
|
-
'Content-Type': 'application/json'
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
);
|
|
130
|
-
const permanentToken = createTokenResponse.data.access_token;
|
|
131
|
-
|
|
132
|
-
// Save configuration with permanent token
|
|
133
|
-
writeConfig({
|
|
134
|
-
username,
|
|
135
|
-
server,
|
|
136
|
-
token: permanentToken
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
spinner.succeed('Authentication successful');
|
|
92
|
+
await api.delete(`/v1/users/${username}/tokens/ThinRemote`);
|
|
140
93
|
} catch (error) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (error.response && error.response.status === 401) {
|
|
144
|
-
console.error(chalk.red('Invalid username or password'));
|
|
145
|
-
} else if (error.response && error.response.status === 404) {
|
|
146
|
-
console.error(chalk.red('Authentication endpoint not found. Please check the server URL.'));
|
|
147
|
-
} else {
|
|
148
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
process.exit(1);
|
|
94
|
+
if (error.response && error.response.status !== 404) throw error;
|
|
152
95
|
}
|
|
96
|
+
|
|
97
|
+
spinner.text = 'Creating new user token...';
|
|
98
|
+
const createTokenResponse = await api.post(
|
|
99
|
+
`/v1/users/${username}/tokens`,
|
|
100
|
+
{
|
|
101
|
+
enabled: true,
|
|
102
|
+
allow: { '*': { '*': '*' } },
|
|
103
|
+
deny: {},
|
|
104
|
+
token: 'ThinRemote',
|
|
105
|
+
name: 'ThinRemote',
|
|
106
|
+
description: '',
|
|
107
|
+
},
|
|
108
|
+
{ headers: { 'Content-Type': 'application/json' } },
|
|
109
|
+
);
|
|
110
|
+
const permanentToken = createTokenResponse.data.access_token;
|
|
111
|
+
|
|
112
|
+
// server was already persisted by the outer authenticate(); only
|
|
113
|
+
// username + token need to be merged here.
|
|
114
|
+
mergeConfig({ username, token: permanentToken });
|
|
115
|
+
spinner.succeed('Authentication successful');
|
|
153
116
|
} catch (error) {
|
|
154
|
-
|
|
155
|
-
|
|
117
|
+
spinner.fail('Authentication failed');
|
|
118
|
+
if (error.response && error.response.status === 401) {
|
|
119
|
+
throw new Error('Invalid username or password', { cause: error });
|
|
120
|
+
}
|
|
121
|
+
if (error.response && error.response.status === 404) {
|
|
122
|
+
throw new Error('Authentication endpoint not found. Please check the server URL.', {
|
|
123
|
+
cause: error,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
156
127
|
}
|
|
157
128
|
}
|
|
158
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Exchange the saved refresh_token for a fresh access_token. Throws a
|
|
132
|
+
* tagged `code: 'unauthorized'` Error when no refresh_token is on
|
|
133
|
+
* record (e.g. logins made with username/password don't get one) or
|
|
134
|
+
* when the OAuth server rejects the refresh — both cases mean the
|
|
135
|
+
* caller has to re-authenticate.
|
|
136
|
+
*
|
|
137
|
+
* @returns {Promise<string>} The new access token.
|
|
138
|
+
*/
|
|
159
139
|
export async function refreshToken() {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
//const spinner = ora('Refreshing user token...').start();
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
// Prepare OAuth2 refresh token form data
|
|
171
|
-
const formData = new URLSearchParams();
|
|
172
|
-
formData.append('grant_type', 'refresh_token');
|
|
173
|
-
formData.append('client_id', '3b40164d28730e416cbd');
|
|
174
|
-
formData.append('refresh_token', config.refresh_token);
|
|
175
|
-
|
|
176
|
-
// Refresh token using OAuth2 refresh flow
|
|
177
|
-
const response = await api.post(
|
|
178
|
-
`/oauth/token`,
|
|
179
|
-
formData,
|
|
180
|
-
{
|
|
181
|
-
headers: {
|
|
182
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
// Save new token in configuration
|
|
188
|
-
mergeConfig({
|
|
189
|
-
token: response.data.access_token,
|
|
190
|
-
refresh_token: response.data.refresh_token
|
|
191
|
-
});
|
|
140
|
+
const config = readConfig();
|
|
141
|
+
if (!config.refresh_token) {
|
|
142
|
+
const err = new Error('No refresh token available; re-authenticate with `thinr`.');
|
|
143
|
+
err.code = 'unauthorized';
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
192
146
|
|
|
193
|
-
|
|
194
|
-
|
|
147
|
+
const formData = new URLSearchParams();
|
|
148
|
+
formData.append('grant_type', 'refresh_token');
|
|
149
|
+
formData.append('client_id', OAUTH_CLIENT_ID);
|
|
150
|
+
formData.append('refresh_token', config.refresh_token);
|
|
195
151
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
152
|
+
let response;
|
|
153
|
+
try {
|
|
154
|
+
// `_isRefresh` flags this request so the response interceptor
|
|
155
|
+
// skips its own refresh-on-401 logic — otherwise a refresh that
|
|
156
|
+
// returned 401 would recurse back here forever.
|
|
157
|
+
response = await api.post('/oauth/token', formData, {
|
|
158
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
159
|
+
_isRefresh: true,
|
|
160
|
+
});
|
|
161
|
+
} catch (_error) {
|
|
162
|
+
const err = new Error('Token refresh failed; re-authenticate with `thinr`.');
|
|
163
|
+
err.code = 'unauthorized';
|
|
164
|
+
throw err;
|
|
202
165
|
}
|
|
166
|
+
|
|
167
|
+
mergeConfig({
|
|
168
|
+
token: response.data.access_token,
|
|
169
|
+
refresh_token: response.data.refresh_token,
|
|
170
|
+
});
|
|
171
|
+
return response.data.access_token;
|
|
203
172
|
}
|
|
204
173
|
|
|
205
174
|
/**
|
|
@@ -217,9 +186,7 @@ export async function deleteToken() {
|
|
|
217
186
|
const spinner = ora('Deleting user token...').start();
|
|
218
187
|
|
|
219
188
|
try {
|
|
220
|
-
await api.delete(
|
|
221
|
-
`/v1/users/${config.username}/tokens/ThinRemote`,
|
|
222
|
-
);
|
|
189
|
+
await api.delete(`/v1/users/${config.username}/tokens/ThinRemote`);
|
|
223
190
|
spinner.succeed('Token deleted successfully');
|
|
224
191
|
} catch (error) {
|
|
225
192
|
if (error.response && error.response.status === 404) {
|
|
@@ -239,54 +206,34 @@ export async function deleteToken() {
|
|
|
239
206
|
* @returns {Promise<void>}
|
|
240
207
|
*/
|
|
241
208
|
async function authenticateWithToken() {
|
|
209
|
+
const username = await input({
|
|
210
|
+
message: 'Enter username:',
|
|
211
|
+
validate: (input) => (input ? true : 'Username is required'),
|
|
212
|
+
});
|
|
213
|
+
const token = await input({
|
|
214
|
+
message: 'Enter token:',
|
|
215
|
+
type: 'password',
|
|
216
|
+
mask: '*',
|
|
217
|
+
validate: (input) => (input ? true : 'Token is required'),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const spinner = ora('Validating token...').start();
|
|
242
221
|
try {
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
const token = await input({
|
|
251
|
-
message: 'Enter token:',
|
|
252
|
-
type: 'password',
|
|
253
|
-
mask: '*',
|
|
254
|
-
validate: (input) => {
|
|
255
|
-
return input ? true : 'Token is required';
|
|
256
|
-
}
|
|
222
|
+
// The api interceptor attaches Authorization from the saved
|
|
223
|
+
// config; at this point we haven't persisted the token yet, so
|
|
224
|
+
// the header has to be passed explicitly for the probe request.
|
|
225
|
+
await api.get(`/v1/proxies`, {
|
|
226
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
257
227
|
});
|
|
258
|
-
|
|
259
|
-
// Show spinner
|
|
260
|
-
const spinner = ora('Validating token...').start();
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
// Validate token by making a request to the proxies API
|
|
264
|
-
await api.get(
|
|
265
|
-
`/v1/proxies`,
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
// Save configuration
|
|
269
|
-
mergeConfig({
|
|
270
|
-
username,
|
|
271
|
-
token
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
spinner.succeed('Token validation successful');
|
|
275
|
-
} catch (error) {
|
|
276
|
-
spinner.fail('Token validation failed');
|
|
277
|
-
|
|
278
|
-
if (error.response && error.response.status === 401) {
|
|
279
|
-
console.error(chalk.red('Invalid token'));
|
|
280
|
-
} else {
|
|
281
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
process.exit(1);
|
|
285
|
-
}
|
|
286
228
|
} catch (error) {
|
|
287
|
-
|
|
288
|
-
|
|
229
|
+
spinner.fail('Token validation failed');
|
|
230
|
+
if (error.response && error.response.status === 401) {
|
|
231
|
+
throw new Error('Invalid token', { cause: error });
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
289
234
|
}
|
|
235
|
+
mergeConfig({ username, token });
|
|
236
|
+
spinner.succeed('Token validation successful');
|
|
290
237
|
}
|
|
291
238
|
|
|
292
239
|
/**
|
|
@@ -294,159 +241,128 @@ async function authenticateWithToken() {
|
|
|
294
241
|
* @returns {Promise<void>}
|
|
295
242
|
*/
|
|
296
243
|
async function authenticateWithOAuth() {
|
|
297
|
-
|
|
298
|
-
const client_id = '3b40164d28730e416cbd';
|
|
299
|
-
let device_code = '';
|
|
244
|
+
const client_id = OAUTH_CLIENT_ID;
|
|
300
245
|
let oauth_tokens = {};
|
|
301
246
|
|
|
247
|
+
const spinner = ora('Requesting OAuth User Code...').start();
|
|
248
|
+
let data;
|
|
302
249
|
try {
|
|
250
|
+
({ data } = await api.post(
|
|
251
|
+
`/oauth/device/authorize`,
|
|
252
|
+
{ client_id },
|
|
253
|
+
{ headers: { 'content-type': 'application/x-www-form-urlencoded' } },
|
|
254
|
+
));
|
|
255
|
+
spinner.succeed('OAuth User Code received');
|
|
256
|
+
} catch (error) {
|
|
257
|
+
spinner.fail('OAuth device-code request failed');
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
303
260
|
|
|
304
|
-
|
|
305
|
-
|
|
261
|
+
console.log('\nYour one time device code is: ' + chalk.bold(data.user_code));
|
|
262
|
+
console.log(
|
|
263
|
+
chalk.blue('Press ' + chalk.bold('ENTER')) +
|
|
264
|
+
' to open your browser or submit your device code here: ' +
|
|
265
|
+
data.verification_uri +
|
|
266
|
+
'\n',
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const device_code = data.device_code;
|
|
270
|
+
let pollDeviceIntervalId;
|
|
271
|
+
let interval = 5000;
|
|
272
|
+
let pollError = null; // captured by polling, surfaced after the await
|
|
273
|
+
const controller = new AbortController();
|
|
274
|
+
|
|
275
|
+
function setPollingInterval(ms) {
|
|
276
|
+
if (pollDeviceIntervalId) clearInterval(pollDeviceIntervalId);
|
|
277
|
+
pollDeviceIntervalId = setInterval(pollDevice, ms);
|
|
278
|
+
}
|
|
306
279
|
|
|
280
|
+
async function pollDevice() {
|
|
307
281
|
try {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
`/oauth/device/authorize`,
|
|
282
|
+
const response = await api.post(
|
|
283
|
+
`/oauth/token`,
|
|
311
284
|
{
|
|
312
|
-
|
|
285
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
286
|
+
client_id,
|
|
287
|
+
device_code,
|
|
313
288
|
},
|
|
314
|
-
{
|
|
315
|
-
headers: {
|
|
316
|
-
'content-type': 'application/x-www-form-urlencoded'
|
|
317
|
-
}
|
|
318
|
-
}
|
|
289
|
+
{ headers: { 'content-type': 'application/x-www-form-urlencoded' } },
|
|
319
290
|
);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
console.log('\nYour one time device code is: ' + chalk.bold(data.user_code));
|
|
324
|
-
|
|
325
|
-
console.log(chalk.blue('Press ' + chalk.bold('ENTER')) + ' to open your browser or submit your device code here: ' + data.verification_uri + '\n');
|
|
326
|
-
|
|
327
|
-
device_code = data.device_code;
|
|
328
|
-
|
|
329
|
-
let validated = false;
|
|
330
|
-
let pollDeviceIntervalId;
|
|
331
|
-
let interval = 5000; // Default polling interval
|
|
332
|
-
const controller = new AbortController();
|
|
333
|
-
|
|
334
|
-
function setPollingInterval(interval) {
|
|
335
|
-
if (pollDeviceIntervalId) {
|
|
336
|
-
clearInterval(pollDeviceIntervalId);
|
|
337
|
-
}
|
|
338
|
-
pollDeviceIntervalId = setInterval(pollDevice, interval);
|
|
291
|
+
if (response.status === 200) {
|
|
292
|
+
oauth_tokens = response.data;
|
|
293
|
+
controller.abort('validated');
|
|
339
294
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
}).catch((e) => {
|
|
363
|
-
if (e.status === 400) {
|
|
364
|
-
// Check for specific OAuth error messages
|
|
365
|
-
if ( e.response.data.error === 'authorization_pending' ) {
|
|
366
|
-
//console.debug("Authorization pending, continuing to poll...");
|
|
367
|
-
} else if ( e.response.data.error === 'slow_down' ) {
|
|
368
|
-
//console.debug("Received slow_down, increasing polling interval");
|
|
369
|
-
// Increase polling interval to avoid hitting rate limits
|
|
370
|
-
clearInterval(pollDeviceIntervalId);
|
|
371
|
-
interval = 5000+1000;
|
|
372
|
-
setPollingInterval(interval); // Increase to 10 seconds
|
|
373
|
-
} else if ( e.response.data.error === 'access_denied' ) {
|
|
374
|
-
console.error(chalk.red('User denied the authorization request'));
|
|
375
|
-
process.exit(1);
|
|
376
|
-
} else if ( e.response.data.error === 'expired_token' ) {
|
|
377
|
-
console.error(chalk.red('Device code expired'));
|
|
378
|
-
process.exit(1);
|
|
379
|
-
} else {
|
|
380
|
-
console.error(chalk.red(`OAuth error: ${e.response.data.error}`));
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
|
|
295
|
+
} catch (e) {
|
|
296
|
+
if (e.response && e.response.status === 400) {
|
|
297
|
+
const err = e.response.data && e.response.data.error;
|
|
298
|
+
if (err === 'authorization_pending') return;
|
|
299
|
+
if (err === 'slow_down') {
|
|
300
|
+
interval += 1000;
|
|
301
|
+
setPollingInterval(interval);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (err === 'access_denied') {
|
|
305
|
+
pollError = new Error('User denied the authorization request');
|
|
306
|
+
controller.abort('error');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (err === 'expired_token') {
|
|
310
|
+
pollError = new Error('Device code expired');
|
|
311
|
+
controller.abort('error');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
pollError = new Error(`OAuth error: ${err}`);
|
|
315
|
+
controller.abort('error');
|
|
385
316
|
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
386
319
|
|
|
387
|
-
|
|
388
|
-
setPollingInterval(interval);
|
|
389
|
-
//pollDeviceIntervalId = setInterval(pollDevice, 5000, client_id, data.device_code);
|
|
390
|
-
|
|
391
|
-
// Timeout after 10 minutes
|
|
392
|
-
setTimeout(() => {
|
|
393
|
-
clearInterval(pollDeviceIntervalId)
|
|
394
|
-
controller.abort("timeout");
|
|
395
|
-
}, 600000);
|
|
320
|
+
setPollingInterval(interval);
|
|
396
321
|
|
|
322
|
+
// 10-minute hard ceiling on the user finishing in the browser
|
|
323
|
+
const timeoutId = setTimeout(() => {
|
|
324
|
+
clearInterval(pollDeviceIntervalId);
|
|
325
|
+
controller.abort('timeout');
|
|
326
|
+
}, OAUTH_DEVICE_CODE_TIMEOUT_MS);
|
|
397
327
|
|
|
398
|
-
|
|
399
|
-
|
|
328
|
+
try {
|
|
329
|
+
await input(
|
|
330
|
+
{
|
|
400
331
|
message: 'Press ENTER to continue or wait for authentication...',
|
|
401
332
|
waitUserInput: true,
|
|
402
|
-
validate:
|
|
403
|
-
open(data.verification_uri_complete, {
|
|
333
|
+
validate: () => {
|
|
334
|
+
open(data.verification_uri_complete, {
|
|
335
|
+
app: [{ name: apps.browser }, 'firefox-developer-edition'],
|
|
336
|
+
});
|
|
404
337
|
return '';
|
|
405
338
|
},
|
|
406
339
|
},
|
|
407
340
|
{
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
clearInterval(pollDeviceIntervalId);
|
|
419
|
-
|
|
420
|
-
const token = oauth_tokens.access_token;
|
|
421
|
-
const refresh_token = oauth_tokens.refresh_token;
|
|
422
|
-
const decodedJWT = JSON.parse(Buffer.from(oauth_tokens.access_token.split('.')[1], 'base64').toString());
|
|
423
|
-
const username = decodedJWT.usr;
|
|
424
|
-
|
|
425
|
-
// Save configuration
|
|
426
|
-
mergeConfig({
|
|
427
|
-
username,
|
|
428
|
-
token,
|
|
429
|
-
refresh_token,
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
spinner.succeed('OAuth token validation successful\n');
|
|
433
|
-
} catch (error) {
|
|
434
|
-
spinner.fail('OAuth token validation failed');
|
|
435
|
-
|
|
436
|
-
if (error.response && error.response.status === 401) {
|
|
437
|
-
console.error(chalk.red('Invalid OAuth token'));
|
|
438
|
-
} else {
|
|
439
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
341
|
+
signal: controller.signal,
|
|
342
|
+
},
|
|
343
|
+
).catch((error) => {
|
|
344
|
+
if (error.cause === 'timeout') {
|
|
345
|
+
console.log(chalk.yellow('Polling aborted due to timeout'));
|
|
346
|
+
} else if (error.cause === 'validated') {
|
|
347
|
+
console.log(chalk.green('Device authenticated successfully'));
|
|
440
348
|
}
|
|
349
|
+
// 'error' cause falls through; we surface pollError below.
|
|
350
|
+
});
|
|
351
|
+
} finally {
|
|
352
|
+
clearInterval(pollDeviceIntervalId);
|
|
353
|
+
clearTimeout(timeoutId);
|
|
354
|
+
}
|
|
441
355
|
|
|
442
|
-
|
|
443
|
-
|
|
356
|
+
if (pollError) throw pollError;
|
|
357
|
+
if (!oauth_tokens.access_token) throw new Error('OAuth flow ended without a token');
|
|
444
358
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
359
|
+
const token = oauth_tokens.access_token;
|
|
360
|
+
const refresh_token = oauth_tokens.refresh_token;
|
|
361
|
+
const decodedJWT = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
|
362
|
+
const username = decodedJWT.usr;
|
|
449
363
|
|
|
364
|
+
mergeConfig({ username, token, refresh_token });
|
|
365
|
+
console.log(chalk.green('OAuth token validation successful\n'));
|
|
450
366
|
}
|
|
451
367
|
|
|
452
368
|
/**
|