@thinremote/thinr-cli 1.3.4 → 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.
- package/CHANGELOG.md +86 -0
- package/README.md +117 -123
- package/bin/thinr.js +43 -105
- package/commands/_shared.js +10 -90
- package/commands/device/_shared.js +2 -2
- package/commands/device/exec.js +2 -2
- package/commands/device/fs.js +2 -2
- package/commands/device/index.js +5 -19
- package/commands/device/logs.js +4 -4
- package/commands/device/status.js +1 -1
- package/commands/device/transfer.js +2 -2
- package/commands/device/update.js +8 -2
- package/commands/fleet/upgrade.js +3 -3
- package/commands/playbook/run.js +2 -2
- package/commands/playbook/validate.js +2 -2
- package/commands/product/_shared.js +1 -1
- package/commands/product/exec.js +2 -2
- package/commands/product/fs.js +2 -2
- package/commands/product/index.js +4 -11
- package/commands/product/logs.js +2 -2
- package/commands/product/metric.js +2 -2
- package/commands/product/playbook.js +5 -5
- package/commands/product/property.js +4 -4
- package/commands/product/push.js +2 -2
- package/commands/product/resource.js +4 -4
- package/commands/product/status.js +3 -3
- package/dist/dashboard.js +12 -12
- package/docs/guides/thinremote-metrics-logs.md +59 -0
- package/docs/guides/thinremote-playbooks.md +133 -0
- package/docs/guides/thinremote-scripts.md +115 -0
- package/docs/guides/thinremote.md +116 -0
- package/lib/command-stream.js +3 -2
- package/lib/console.js +6 -6
- package/lib/constants.js +1 -19
- package/lib/dashboard/event-stream.js +5 -5
- package/lib/device-api.js +4 -4
- package/lib/guides.js +41 -0
- package/lib/identity.js +75 -0
- package/lib/local-files.js +54 -0
- package/lib/mcp/{tools-install.js → agent-install.js} +12 -61
- package/lib/mcp/helpers.js +5 -25
- package/lib/mcp/playbook-ops.js +393 -0
- package/lib/mcp/registry.js +9 -43
- package/lib/mcp/resource-hooks.js +53 -0
- package/lib/mcp/resource-types.js +857 -0
- package/lib/mcp/tools-exec.js +217 -177
- package/lib/monitoring.js +3 -3
- package/lib/playbook/actions.js +30 -18
- package/lib/playbook/expression.js +1 -4
- package/lib/playbook/fleet.js +4 -1
- package/lib/playbook/loader.js +16 -8
- package/lib/playbook/runner.js +40 -8
- package/lib/playbook/schema.js +108 -34
- package/lib/product/log-presets.js +1 -1
- package/lib/product/logs.js +2 -2
- package/lib/product-orchestrator.js +14 -9
- package/lib/product.js +29 -227
- package/lib/proxy.js +4 -4
- package/lib/status.js +13 -5
- package/package.json +4 -2
- package/commands/device/delete.js +0 -53
- package/commands/device/list.js +0 -55
- package/commands/device/property.js +0 -52
- package/commands/device/resource.js +0 -52
- package/commands/logout.js +0 -47
- package/commands/product/list.js +0 -86
- package/commands/product/props.js +0 -125
- package/commands/profile.js +0 -173
- package/lib/alarms.js +0 -223
- package/lib/api.js +0 -89
- package/lib/auth.js +0 -428
- package/lib/bucket.js +0 -82
- package/lib/concurrency.js +0 -52
- package/lib/config.js +0 -230
- package/lib/devices.js +0 -60
- package/lib/errors.js +0 -65
- package/lib/format.js +0 -114
- package/lib/mcp/server.js +0 -67
- package/lib/mcp/tools-alarms.js +0 -507
- package/lib/mcp/tools-buckets.js +0 -99
- package/lib/mcp/tools-devices.js +0 -235
- package/lib/mcp/tools-filesystem.js +0 -281
- package/lib/mcp/tools-logs.js +0 -225
- package/lib/mcp/tools-playbook.js +0 -210
- package/lib/mcp/tools-products.js +0 -1557
- package/lib/mcp/tools-profile.js +0 -635
- package/lib/mcp/tools-profiles.js +0 -30
- package/lib/mcp/tools-properties.js +0 -79
- package/lib/mcp/tools-resources.js +0 -78
- package/lib/mcp/tools-scripts.js +0 -212
- package/lib/mcp/tools-tokens.js +0 -369
- package/lib/output.js +0 -114
- package/lib/paths.js +0 -32
- package/lib/profile.js +0 -657
- package/lib/property.js +0 -75
- package/lib/resource.js +0 -151
- package/lib/storage.js +0 -117
- package/lib/tokens.js +0 -366
package/lib/auth.js
DELETED
|
@@ -1,428 +0,0 @@
|
|
|
1
|
-
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { input, select } from '@inquirer/prompts';
|
|
3
|
-
import ora from 'ora';
|
|
4
|
-
import open, { apps } from 'open';
|
|
5
|
-
import { writeConfig, readConfig, mergeConfig } from './config.js';
|
|
6
|
-
import { label, success, warning, error as errorStyle, info } from './format.js';
|
|
7
|
-
|
|
8
|
-
import api, { setBaseURL } from './api.js';
|
|
9
|
-
import { OAUTH, TIMEOUTS } from './constants.js';
|
|
10
|
-
import { v1 } from './paths.js';
|
|
11
|
-
|
|
12
|
-
const OAUTH_CLIENT_ID = OAUTH.CLIENT_ID;
|
|
13
|
-
const OAUTH_DEVICE_CODE_TIMEOUT_MS = TIMEOUTS.OAUTH_DEVICE_CODE_MS;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Authenticate user
|
|
17
|
-
* @returns {Promise<void>}
|
|
18
|
-
*/
|
|
19
|
-
export async function authenticate() {
|
|
20
|
-
// When re-authenticating an existing profile, default the server prompt
|
|
21
|
-
// to the one already stored so `thinr login` is a couple of keystrokes.
|
|
22
|
-
const existing = readConfig();
|
|
23
|
-
|
|
24
|
-
// Ask for authentication method
|
|
25
|
-
const method = await select({
|
|
26
|
-
message: 'How would you like to authenticate?',
|
|
27
|
-
choices: [
|
|
28
|
-
{ name: 'OAuth', value: 'oauth' },
|
|
29
|
-
{ name: 'Token', value: 'token' },
|
|
30
|
-
{ name: 'Username and password', value: 'userpass' },
|
|
31
|
-
],
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// Ask for server URL
|
|
35
|
-
const server = await input({
|
|
36
|
-
message: 'Enter server URL:',
|
|
37
|
-
default: existing?.server || 'console.thinr.io',
|
|
38
|
-
validate: (input) => {
|
|
39
|
-
return input ? true : 'Server URL is required';
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
setBaseURL(`https://${server}`);
|
|
44
|
-
|
|
45
|
-
writeConfig({
|
|
46
|
-
server,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (method === 'userpass') {
|
|
50
|
-
// Authenticate with username and password
|
|
51
|
-
await authenticateWithUserPass();
|
|
52
|
-
} else if (method === 'oauth') {
|
|
53
|
-
await authenticateWithOAuth();
|
|
54
|
-
} else {
|
|
55
|
-
// Authenticate with token
|
|
56
|
-
await authenticateWithToken();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Authenticate with username and password using OAuth2 password flow
|
|
62
|
-
* @returns {Promise<void>}
|
|
63
|
-
*/
|
|
64
|
-
async function authenticateWithUserPass() {
|
|
65
|
-
// Ask for credentials
|
|
66
|
-
const username = await input({
|
|
67
|
-
message: 'Enter username:',
|
|
68
|
-
validate: (input) => (input ? true : 'Username is required'),
|
|
69
|
-
});
|
|
70
|
-
const password = await input({
|
|
71
|
-
message: 'Enter password:',
|
|
72
|
-
type: 'password',
|
|
73
|
-
mask: '*',
|
|
74
|
-
validate: (input) => (input ? true : 'Password is required'),
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const spinner = ora('Authenticating...').start();
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
// OAuth2 password grant
|
|
81
|
-
const formData = new URLSearchParams();
|
|
82
|
-
formData.append('grant_type', 'password');
|
|
83
|
-
formData.append('username', username);
|
|
84
|
-
formData.append('password', password);
|
|
85
|
-
|
|
86
|
-
await api.post(`/oauth/token`, formData, {
|
|
87
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Replace any existing token with a fresh permanent one
|
|
91
|
-
spinner.text = 'Preparing user token...';
|
|
92
|
-
try {
|
|
93
|
-
await api.delete(`${v1(username)}/tokens/ThinRemote`);
|
|
94
|
-
} catch (error) {
|
|
95
|
-
if (error.response && error.response.status !== 404) throw error;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
spinner.text = 'Creating new user token...';
|
|
99
|
-
const createTokenResponse = await api.post(
|
|
100
|
-
`${v1(username)}/tokens`,
|
|
101
|
-
{
|
|
102
|
-
enabled: true,
|
|
103
|
-
allow: { '*': { '*': '*' } },
|
|
104
|
-
deny: {},
|
|
105
|
-
token: 'ThinRemote',
|
|
106
|
-
name: 'ThinRemote',
|
|
107
|
-
description: '',
|
|
108
|
-
},
|
|
109
|
-
{ headers: { 'Content-Type': 'application/json' } },
|
|
110
|
-
);
|
|
111
|
-
const permanentToken = createTokenResponse.data.access_token;
|
|
112
|
-
|
|
113
|
-
// server was already persisted by the outer authenticate(); only
|
|
114
|
-
// username + token need to be merged here.
|
|
115
|
-
mergeConfig({ username, token: permanentToken });
|
|
116
|
-
spinner.succeed('Authentication successful');
|
|
117
|
-
} catch (error) {
|
|
118
|
-
spinner.fail('Authentication failed');
|
|
119
|
-
if (error.response && error.response.status === 401) {
|
|
120
|
-
throw new Error('Invalid username or password', { cause: error });
|
|
121
|
-
}
|
|
122
|
-
if (error.response && error.response.status === 404) {
|
|
123
|
-
throw new Error('Authentication endpoint not found. Please check the server URL.', {
|
|
124
|
-
cause: error,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
throw error;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Exchange the saved refresh_token for a fresh access_token. Throws a
|
|
133
|
-
* tagged `code: 'unauthorized'` Error when no refresh_token is on
|
|
134
|
-
* record (e.g. logins made with username/password don't get one) or
|
|
135
|
-
* when the OAuth server rejects the refresh — both cases mean the
|
|
136
|
-
* caller has to re-authenticate.
|
|
137
|
-
*
|
|
138
|
-
* Concurrent callers share a single in-flight refresh: refresh tokens
|
|
139
|
-
* are rotated on use, so issuing two refreshes in parallel makes the
|
|
140
|
-
* second one fail against an already-consumed token. The shared
|
|
141
|
-
* promise lets every awaiter see the same result and lets the next
|
|
142
|
-
* caller (after completion) start a new refresh against the freshly
|
|
143
|
-
* persisted token.
|
|
144
|
-
*
|
|
145
|
-
* @returns {Promise<string>} The new access token.
|
|
146
|
-
*/
|
|
147
|
-
let inFlightRefresh = null;
|
|
148
|
-
|
|
149
|
-
export async function refreshToken() {
|
|
150
|
-
if (inFlightRefresh) return inFlightRefresh;
|
|
151
|
-
inFlightRefresh = doRefresh().finally(() => {
|
|
152
|
-
inFlightRefresh = null;
|
|
153
|
-
});
|
|
154
|
-
return inFlightRefresh;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function doRefresh() {
|
|
158
|
-
const config = readConfig();
|
|
159
|
-
if (!config.refresh_token) {
|
|
160
|
-
const err = new Error('Session expired. Run `thinr login` to sign in again.');
|
|
161
|
-
err.code = 'unauthorized';
|
|
162
|
-
throw err;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const formData = new URLSearchParams();
|
|
166
|
-
formData.append('grant_type', 'refresh_token');
|
|
167
|
-
formData.append('client_id', OAUTH_CLIENT_ID);
|
|
168
|
-
formData.append('refresh_token', config.refresh_token);
|
|
169
|
-
|
|
170
|
-
let response;
|
|
171
|
-
try {
|
|
172
|
-
// `_isRefresh` flags this request so the response interceptor
|
|
173
|
-
// skips its own refresh-on-401 logic — otherwise a refresh that
|
|
174
|
-
// returned 401 would recurse back here forever.
|
|
175
|
-
response = await api.post('/oauth/token', formData, {
|
|
176
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
177
|
-
_isRefresh: true,
|
|
178
|
-
});
|
|
179
|
-
} catch (_error) {
|
|
180
|
-
const err = new Error('Session expired. Run `thinr login` to sign in again.');
|
|
181
|
-
err.code = 'unauthorized';
|
|
182
|
-
throw err;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
mergeConfig({
|
|
186
|
-
token: response.data.access_token,
|
|
187
|
-
refresh_token: response.data.refresh_token,
|
|
188
|
-
});
|
|
189
|
-
return response.data.access_token;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Milliseconds-since-epoch expiry of a JWT access token, or null when
|
|
194
|
-
* the token is not a decodable JWT (permanent API tokens have no exp).
|
|
195
|
-
*/
|
|
196
|
-
function jwtExpiryMs(token) {
|
|
197
|
-
try {
|
|
198
|
-
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
|
199
|
-
return typeof payload.exp === 'number' ? payload.exp * 1000 : null;
|
|
200
|
-
} catch {
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Return a currently-valid access token, refreshing first when the
|
|
207
|
-
* stored one is an expired (or about-to-expire) JWT. HTTP requests
|
|
208
|
-
* recover from expiry through the api.js response interceptor, but
|
|
209
|
-
* WebSocket handshakes have no retry layer: callers opening a WS use
|
|
210
|
-
* this to start from a valid token. Tokens without a decodable `exp`
|
|
211
|
-
* and profiles without a refresh_token pass through untouched.
|
|
212
|
-
*/
|
|
213
|
-
export async function ensureFreshToken() {
|
|
214
|
-
const config = readConfig();
|
|
215
|
-
if (!config?.token || !config.refresh_token) return config?.token ?? null;
|
|
216
|
-
const expiresAt = jwtExpiryMs(config.token);
|
|
217
|
-
if (expiresAt === null || expiresAt - Date.now() > TIMEOUTS.TOKEN_EXPIRY_SLACK_MS) {
|
|
218
|
-
return config.token;
|
|
219
|
-
}
|
|
220
|
-
return refreshToken();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Delete user token from server
|
|
225
|
-
* @returns {Promise<void>}
|
|
226
|
-
*/
|
|
227
|
-
export async function deleteToken() {
|
|
228
|
-
try {
|
|
229
|
-
const config = readConfig();
|
|
230
|
-
if (!config || !config.username || !config.server || !config.token) {
|
|
231
|
-
console.log(warning('No configuration found'));
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const spinner = ora('Deleting user token...').start();
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
await api.delete(`${v1(config.username)}/tokens/ThinRemote`);
|
|
239
|
-
spinner.succeed('Token deleted successfully');
|
|
240
|
-
} catch (error) {
|
|
241
|
-
if (error.response && error.response.status === 404) {
|
|
242
|
-
spinner.succeed('Token was already deleted');
|
|
243
|
-
} else {
|
|
244
|
-
spinner.fail('Failed to delete token');
|
|
245
|
-
console.error(errorStyle(`Error: ${error.message}`));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
} catch (error) {
|
|
249
|
-
console.error(errorStyle(`Error: ${error.message}`));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Authenticate with token
|
|
255
|
-
* @returns {Promise<void>}
|
|
256
|
-
*/
|
|
257
|
-
async function authenticateWithToken() {
|
|
258
|
-
const username = await input({
|
|
259
|
-
message: 'Enter username:',
|
|
260
|
-
validate: (input) => (input ? true : 'Username is required'),
|
|
261
|
-
});
|
|
262
|
-
const token = await input({
|
|
263
|
-
message: 'Enter token:',
|
|
264
|
-
type: 'password',
|
|
265
|
-
mask: '*',
|
|
266
|
-
validate: (input) => (input ? true : 'Token is required'),
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const spinner = ora('Validating token...').start();
|
|
270
|
-
try {
|
|
271
|
-
// The api interceptor attaches Authorization from the saved
|
|
272
|
-
// config; at this point we haven't persisted the token yet, so
|
|
273
|
-
// the header has to be passed explicitly for the probe request.
|
|
274
|
-
await api.get(`/v1/proxies`, {
|
|
275
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
276
|
-
});
|
|
277
|
-
} catch (error) {
|
|
278
|
-
spinner.fail('Token validation failed');
|
|
279
|
-
if (error.response && error.response.status === 401) {
|
|
280
|
-
throw new Error('Invalid token', { cause: error });
|
|
281
|
-
}
|
|
282
|
-
throw error;
|
|
283
|
-
}
|
|
284
|
-
mergeConfig({ username, token });
|
|
285
|
-
spinner.succeed('Token validation successful');
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Authenticate with OAuth token
|
|
290
|
-
* @returns {Promise<void>}
|
|
291
|
-
*/
|
|
292
|
-
async function authenticateWithOAuth() {
|
|
293
|
-
const client_id = OAUTH_CLIENT_ID;
|
|
294
|
-
let oauth_tokens = {};
|
|
295
|
-
|
|
296
|
-
const spinner = ora('Requesting OAuth User Code...').start();
|
|
297
|
-
let data;
|
|
298
|
-
try {
|
|
299
|
-
({ data } = await api.post(
|
|
300
|
-
`/oauth/device/authorize`,
|
|
301
|
-
{ client_id },
|
|
302
|
-
{ headers: { 'content-type': 'application/x-www-form-urlencoded' } },
|
|
303
|
-
));
|
|
304
|
-
spinner.succeed('OAuth User Code received');
|
|
305
|
-
} catch (error) {
|
|
306
|
-
spinner.fail('OAuth device-code request failed');
|
|
307
|
-
throw error;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
console.log('\nYour one time device code is: ' + label(data.user_code));
|
|
311
|
-
console.log(
|
|
312
|
-
info('Press ' + label('ENTER')) +
|
|
313
|
-
' to open your browser or submit your device code here: ' +
|
|
314
|
-
data.verification_uri +
|
|
315
|
-
'\n',
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
const device_code = data.device_code;
|
|
319
|
-
let pollDeviceIntervalId;
|
|
320
|
-
let interval = 5000;
|
|
321
|
-
let pollError = null; // captured by polling, surfaced after the await
|
|
322
|
-
const controller = new AbortController();
|
|
323
|
-
|
|
324
|
-
function setPollingInterval(ms) {
|
|
325
|
-
if (pollDeviceIntervalId) clearInterval(pollDeviceIntervalId);
|
|
326
|
-
pollDeviceIntervalId = setInterval(pollDevice, ms);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async function pollDevice() {
|
|
330
|
-
try {
|
|
331
|
-
const response = await api.post(
|
|
332
|
-
`/oauth/token`,
|
|
333
|
-
{
|
|
334
|
-
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
335
|
-
client_id,
|
|
336
|
-
device_code,
|
|
337
|
-
},
|
|
338
|
-
{ headers: { 'content-type': 'application/x-www-form-urlencoded' } },
|
|
339
|
-
);
|
|
340
|
-
if (response.status === 200) {
|
|
341
|
-
oauth_tokens = response.data;
|
|
342
|
-
controller.abort('validated');
|
|
343
|
-
}
|
|
344
|
-
} catch (e) {
|
|
345
|
-
if (e.response && e.response.status === 400) {
|
|
346
|
-
const err = e.response.data && e.response.data.error;
|
|
347
|
-
if (err === 'authorization_pending') return;
|
|
348
|
-
if (err === 'slow_down') {
|
|
349
|
-
interval += 1000;
|
|
350
|
-
setPollingInterval(interval);
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (err === 'access_denied') {
|
|
354
|
-
pollError = new Error('User denied the authorization request');
|
|
355
|
-
controller.abort('error');
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
if (err === 'expired_token') {
|
|
359
|
-
pollError = new Error('Device code expired');
|
|
360
|
-
controller.abort('error');
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
pollError = new Error(`OAuth error: ${err}`);
|
|
364
|
-
controller.abort('error');
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
setPollingInterval(interval);
|
|
370
|
-
|
|
371
|
-
// 10-minute hard ceiling on the user finishing in the browser
|
|
372
|
-
const timeoutId = setTimeout(() => {
|
|
373
|
-
clearInterval(pollDeviceIntervalId);
|
|
374
|
-
controller.abort('timeout');
|
|
375
|
-
}, OAUTH_DEVICE_CODE_TIMEOUT_MS);
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
await input(
|
|
379
|
-
{
|
|
380
|
-
message: 'Press ENTER to continue or wait for authentication...',
|
|
381
|
-
waitUserInput: true,
|
|
382
|
-
validate: () => {
|
|
383
|
-
open(data.verification_uri_complete, {
|
|
384
|
-
app: [{ name: apps.browser }, 'firefox-developer-edition'],
|
|
385
|
-
});
|
|
386
|
-
return '';
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
signal: controller.signal,
|
|
391
|
-
},
|
|
392
|
-
).catch((error) => {
|
|
393
|
-
if (error.cause === 'timeout') {
|
|
394
|
-
console.log(warning('Polling aborted due to timeout'));
|
|
395
|
-
} else if (error.cause === 'validated') {
|
|
396
|
-
console.log(success('Device authenticated successfully'));
|
|
397
|
-
}
|
|
398
|
-
// 'error' cause falls through; we surface pollError below.
|
|
399
|
-
});
|
|
400
|
-
} finally {
|
|
401
|
-
clearInterval(pollDeviceIntervalId);
|
|
402
|
-
clearTimeout(timeoutId);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (pollError) throw pollError;
|
|
406
|
-
if (!oauth_tokens.access_token) throw new Error('OAuth flow ended without a token');
|
|
407
|
-
|
|
408
|
-
const token = oauth_tokens.access_token;
|
|
409
|
-
const refresh_token = oauth_tokens.refresh_token;
|
|
410
|
-
const decodedJWT = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
|
411
|
-
const username = decodedJWT.usr;
|
|
412
|
-
|
|
413
|
-
mergeConfig({ username, token, refresh_token });
|
|
414
|
-
console.log(success('OAuth token validation successful\n'));
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Get access token from configuration
|
|
419
|
-
* @returns {string|axios.CancelToken|*|null}
|
|
420
|
-
*/
|
|
421
|
-
export function getAccessToken() {
|
|
422
|
-
const config = readConfig();
|
|
423
|
-
if (!config || !config.token) {
|
|
424
|
-
console.log(warning('No token found in configuration'));
|
|
425
|
-
return null;
|
|
426
|
-
}
|
|
427
|
-
return config.token;
|
|
428
|
-
}
|
package/lib/bucket.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import api from './api.js';
|
|
2
|
-
import { apiError } from './errors.js';
|
|
3
|
-
import { v2, v3 } from './paths.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Query an arbitrary time-series bucket declared in a product profile.
|
|
7
|
-
* When `device` is provided, scopes to that device's bucket; otherwise
|
|
8
|
-
* queries the user-level bucket across every device of the account.
|
|
9
|
-
*
|
|
10
|
-
* Buckets are the mechanism products use to persist structured samples
|
|
11
|
-
* over time — the platform ships a `monitoring` bucket by default, and
|
|
12
|
-
* users can declare custom ones for their own metrics.
|
|
13
|
-
*
|
|
14
|
-
* Time range:
|
|
15
|
-
* - `minutes`: shortcut for "last N minutes" (sets min_ts to
|
|
16
|
-
* `Date.now() - minutes * 60_000`, max_ts to 0).
|
|
17
|
-
* - `min_ts` / `max_ts`: explicit unix-millis range when `minutes`
|
|
18
|
-
* is not supplied; either may be omitted for an open bound.
|
|
19
|
-
*
|
|
20
|
-
* Aggregation: `agg` is the time-window size (e.g. "5m"), `agg_type`
|
|
21
|
-
* the reducer (e.g. "mean"), and `fields` a comma-separated allowlist
|
|
22
|
-
* to trim the response down.
|
|
23
|
-
*
|
|
24
|
-
* `group_by` is passed through verbatim — no default. Custom buckets
|
|
25
|
-
* may not even expose a `device` tag, so the caller decides whether
|
|
26
|
-
* to group (e.g. `group_by=device` for a fleet view) or read raw
|
|
27
|
-
* interleaved samples.
|
|
28
|
-
*
|
|
29
|
-
* @param {{
|
|
30
|
-
* bucket: string,
|
|
31
|
-
* device?: string | null,
|
|
32
|
-
* user?: string | null,
|
|
33
|
-
* items?: number,
|
|
34
|
-
* sort?: string,
|
|
35
|
-
* minutes?: number,
|
|
36
|
-
* min_ts?: number,
|
|
37
|
-
* max_ts?: number,
|
|
38
|
-
* agg?: string,
|
|
39
|
-
* agg_type?: string,
|
|
40
|
-
* fields?: string,
|
|
41
|
-
* group_by?: string | null,
|
|
42
|
-
* }} opts
|
|
43
|
-
*/
|
|
44
|
-
export async function getBucketData({
|
|
45
|
-
bucket,
|
|
46
|
-
device = null,
|
|
47
|
-
user = null,
|
|
48
|
-
items = 10,
|
|
49
|
-
sort = 'desc',
|
|
50
|
-
minutes,
|
|
51
|
-
min_ts,
|
|
52
|
-
max_ts,
|
|
53
|
-
agg,
|
|
54
|
-
agg_type,
|
|
55
|
-
fields,
|
|
56
|
-
group_by,
|
|
57
|
-
} = {}) {
|
|
58
|
-
if (!bucket) throw new Error('bucket is required');
|
|
59
|
-
const params = { items, sort };
|
|
60
|
-
if (minutes) {
|
|
61
|
-
params.min_ts = Date.now() - minutes * 60 * 1000;
|
|
62
|
-
params.max_ts = 0;
|
|
63
|
-
} else {
|
|
64
|
-
if (min_ts) params.min_ts = min_ts;
|
|
65
|
-
if (max_ts !== undefined) params.max_ts = max_ts;
|
|
66
|
-
}
|
|
67
|
-
if (agg) params.agg = agg;
|
|
68
|
-
if (agg_type) params.agg_type = agg_type;
|
|
69
|
-
if (group_by) params.group_by = group_by;
|
|
70
|
-
if (fields) params.fields = fields;
|
|
71
|
-
|
|
72
|
-
const url = device
|
|
73
|
-
? `${v3(user)}/devices/${device}/buckets/${bucket}/data`
|
|
74
|
-
: `${v2(user)}/buckets/${bucket}/data`;
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const res = await api.get(url, { params });
|
|
78
|
-
return res.data;
|
|
79
|
-
} catch (e) {
|
|
80
|
-
throw apiError(e);
|
|
81
|
-
}
|
|
82
|
-
}
|
package/lib/concurrency.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Run `worker(item, index)` over `items` with at most `limit` invocations
|
|
5
|
-
* in flight at any time. Returns an array parallel to `items` describing
|
|
6
|
-
* the outcome of each task as `{ ok: true, value }` or `{ ok: false, error }`.
|
|
7
|
-
*
|
|
8
|
-
* By default, a rejection from one worker does not stop the others — the
|
|
9
|
-
* caller inspects the results array. Set `failFast: true` to stop dequeueing
|
|
10
|
-
* new work as soon as any worker rejects; in-flight tasks still finish and
|
|
11
|
-
* their results are included. The first rejection is rethrown after the
|
|
12
|
-
* in-flight tasks settle.
|
|
13
|
-
*
|
|
14
|
-
* @template T, R
|
|
15
|
-
* @param {T[]} items
|
|
16
|
-
* @param {number} limit
|
|
17
|
-
* @param {(item: T, index: number) => Promise<R>} worker
|
|
18
|
-
* @param {{ failFast?: boolean }} [opts]
|
|
19
|
-
* @returns {Promise<Array<{ ok: true, value: R } | { ok: false, error: unknown }>>}
|
|
20
|
-
*/
|
|
21
|
-
export async function runPool(items, limit, worker, { failFast = false } = {}) {
|
|
22
|
-
const results = new Array(items.length);
|
|
23
|
-
let nextIndex = 0;
|
|
24
|
-
let aborted = false;
|
|
25
|
-
/** @type {unknown} */
|
|
26
|
-
let abortError = null;
|
|
27
|
-
|
|
28
|
-
const effectiveLimit = Math.max(1, Math.min(limit, items.length));
|
|
29
|
-
|
|
30
|
-
async function runner() {
|
|
31
|
-
while (!aborted) {
|
|
32
|
-
const i = nextIndex++;
|
|
33
|
-
if (i >= items.length) return;
|
|
34
|
-
try {
|
|
35
|
-
results[i] = { ok: true, value: await worker(items[i], i) };
|
|
36
|
-
} catch (error) {
|
|
37
|
-
results[i] = { ok: false, error };
|
|
38
|
-
if (failFast && !aborted) {
|
|
39
|
-
aborted = true;
|
|
40
|
-
abortError = error;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const runners = [];
|
|
47
|
-
for (let i = 0; i < effectiveLimit; i++) runners.push(runner());
|
|
48
|
-
await Promise.all(runners);
|
|
49
|
-
|
|
50
|
-
if (aborted && abortError) throw abortError;
|
|
51
|
-
return results;
|
|
52
|
-
}
|