callkaro-cli-unofficial 0.5.0 → 0.5.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/dist/commands/accounts.js +30 -3
- package/dist/index.js +1 -0
- package/dist/lib/api.js +41 -14
- package/dist/lib/config.js +59 -4
- package/package.json +1 -1
|
@@ -44,7 +44,19 @@ async function loginCommand(opts) {
|
|
|
44
44
|
if (!email || !password)
|
|
45
45
|
return (0, ui_1.handleError)(new Error('Email and password are required'));
|
|
46
46
|
const res = await api.login(email, password);
|
|
47
|
-
|
|
47
|
+
// The password is kept by default so a token the server stops accepting can
|
|
48
|
+
// be re-issued on its own — the `ck_` token is opaque and has no refresh
|
|
49
|
+
// route, so a dead token is otherwise a dead end. `--no-save-password` opts
|
|
50
|
+
// out and brings back a manual re-login.
|
|
51
|
+
const keep = opts.savePassword !== false;
|
|
52
|
+
(0, ui_1.success)((0, config_1.saveLogin)(res.user?.email || email, { token: res.token, user: res.user, ...(keep ? { password } : {}) }));
|
|
53
|
+
if (keep) {
|
|
54
|
+
(0, ui_1.info)(' Password saved (plain text, mode 0600) so the CLI can renew this token by itself.');
|
|
55
|
+
(0, ui_1.info)(' Drop it with `cku accounts remove <email>`, or sign in with --no-save-password.');
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
(0, ui_1.info)(' Password not saved. The token cannot be renewed, so re-run this if it stops working.');
|
|
59
|
+
}
|
|
48
60
|
}
|
|
49
61
|
catch (err) {
|
|
50
62
|
(0, ui_1.handleError)(err);
|
|
@@ -160,9 +172,24 @@ async function configCommand() {
|
|
|
160
172
|
async function accountsCommand(opts) {
|
|
161
173
|
const accounts = (0, config_1.listAccounts)();
|
|
162
174
|
const active = (0, config_1.activeAccount)();
|
|
163
|
-
// Never print tokens: this output ends up in agent transcripts
|
|
175
|
+
// Never print tokens or passwords: this output ends up in agent transcripts
|
|
176
|
+
// and CI logs. `signedIn` says whether a credential is held, not what it is.
|
|
164
177
|
if (opts.json) {
|
|
165
|
-
return void console.log(JSON.stringify(accounts.map(({ token, ...a }) => ({
|
|
178
|
+
return void console.log(JSON.stringify(accounts.map(({ token, password, dashboard, ...a }) => ({
|
|
179
|
+
...a,
|
|
180
|
+
signedIn: Boolean(token),
|
|
181
|
+
...(dashboard
|
|
182
|
+
? {
|
|
183
|
+
dashboard: {
|
|
184
|
+
email: dashboard.email,
|
|
185
|
+
userId: dashboard.userId,
|
|
186
|
+
expiresAt: dashboard.expiresAt,
|
|
187
|
+
savedAt: dashboard.savedAt,
|
|
188
|
+
signedIn: Boolean(dashboard.token),
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
: {}),
|
|
192
|
+
})), null, 2));
|
|
166
193
|
}
|
|
167
194
|
if (!accounts.length) {
|
|
168
195
|
return void (0, ui_1.info)('No accounts saved. Run `cku login` to add one.');
|
package/dist/index.js
CHANGED
|
@@ -71,6 +71,7 @@ program
|
|
|
71
71
|
.description('sign in through your browser (no password in the terminal)')
|
|
72
72
|
.option('--password', 'use the classic email + password prompt instead')
|
|
73
73
|
.option('--email <email>', 'email address (with --password)')
|
|
74
|
+
.option('--no-save-password', "don't keep the password (the token then cannot be renewed)")
|
|
74
75
|
.action(accounts_1.loginCommand);
|
|
75
76
|
program
|
|
76
77
|
.command('logout')
|
package/dist/lib/api.js
CHANGED
|
@@ -12,30 +12,64 @@ const types_1 = require("./types");
|
|
|
12
12
|
* logic. Public methods map 1:1 to the `/cli/*` endpoints.
|
|
13
13
|
*/
|
|
14
14
|
class Api {
|
|
15
|
-
token;
|
|
16
15
|
monitoringAccountId;
|
|
16
|
+
renewToken;
|
|
17
17
|
baseUrl;
|
|
18
18
|
callBaseUrl;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/** Mutable: a renewed token replaces the rejected one for later requests. */
|
|
20
|
+
token;
|
|
21
|
+
constructor(baseUrl, callBaseUrl, token, monitoringAccountId,
|
|
22
|
+
/**
|
|
23
|
+
* Re-issue a rejected token. The `ck_` token is opaque and has no refresh
|
|
24
|
+
* route, so the only way back in is to sign in again — which needs the
|
|
25
|
+
* account's password. Returns the new token, or undefined when the account
|
|
26
|
+
* cannot renew itself.
|
|
27
|
+
*/
|
|
28
|
+
renewToken) {
|
|
21
29
|
this.monitoringAccountId = monitoringAccountId;
|
|
30
|
+
this.renewToken = renewToken;
|
|
22
31
|
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
23
32
|
this.callBaseUrl = (callBaseUrl || baseUrl).replace(/\/+$/, '');
|
|
33
|
+
this.token = token;
|
|
24
34
|
}
|
|
25
35
|
request(method, path, body, includeAccount = true) {
|
|
26
36
|
return this.requestTo(this.baseUrl, method, path, body, includeAccount);
|
|
27
37
|
}
|
|
28
38
|
/** Same as request(), but against a specific base URL (e.g. the call cloud). */
|
|
29
39
|
async requestTo(base, method, path, body, includeAccount = true) {
|
|
40
|
+
let res = await this.send(base, method, path, body, includeAccount, this.token);
|
|
41
|
+
// A rejected token is not a failure the user can act on by retrying: the
|
|
42
|
+
// same token gets the same answer. Renew once, silently, when the account
|
|
43
|
+
// has the means to renew itself. The renewed token then serves the rest of
|
|
44
|
+
// the command, so one renewal covers every request in it.
|
|
45
|
+
if (res.status === 401 && this.renewToken) {
|
|
46
|
+
const fresh = await this.renewToken();
|
|
47
|
+
if (fresh) {
|
|
48
|
+
this.token = fresh;
|
|
49
|
+
res = await this.send(base, method, path, body, includeAccount, fresh);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const text = await res.text();
|
|
53
|
+
const data = text ? safeJson(text) : undefined;
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw new types_1.ApiError(res.status, res.status === 401
|
|
56
|
+
? `${data?.msg || 'Your session is no longer valid'} — the stored token was rejected and could not be renewed.\n` +
|
|
57
|
+
' Run `cku login` to sign in again.'
|
|
58
|
+
: data?.msg || data?.message || (typeof data?.error === 'string' ? data.error : undefined) ||
|
|
59
|
+
`Request failed (${res.status})`, parseErrorDetails(data?.errors ?? data?.data?.errors ?? invalidDetails(data?.invalid)));
|
|
60
|
+
}
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
/** One HTTP round trip, with the given bearer token. */
|
|
64
|
+
async send(base, method, path, body, includeAccount, token) {
|
|
30
65
|
const headers = { 'Content-Type': 'application/json' };
|
|
31
|
-
if (
|
|
32
|
-
headers.Authorization = `Bearer ${
|
|
66
|
+
if (token)
|
|
67
|
+
headers.Authorization = `Bearer ${token}`;
|
|
33
68
|
// Monitoring mode: every request, writes included, acts on the client account.
|
|
34
69
|
if (includeAccount && this.monitoringAccountId)
|
|
35
70
|
headers['X-CallKaro-Account-Id'] = this.monitoringAccountId;
|
|
36
|
-
let res;
|
|
37
71
|
try {
|
|
38
|
-
|
|
72
|
+
return await fetch(`${base}${path}`, {
|
|
39
73
|
method,
|
|
40
74
|
headers,
|
|
41
75
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
@@ -44,13 +78,6 @@ class Api {
|
|
|
44
78
|
catch (err) {
|
|
45
79
|
throw new types_1.ApiError(0, `Cannot reach ${base} (${err.message})`);
|
|
46
80
|
}
|
|
47
|
-
const text = await res.text();
|
|
48
|
-
const data = text ? safeJson(text) : undefined;
|
|
49
|
-
if (!res.ok) {
|
|
50
|
-
throw new types_1.ApiError(res.status, data?.msg || data?.message || (typeof data?.error === 'string' ? data.error : undefined) ||
|
|
51
|
-
`Request failed (${res.status})`, parseErrorDetails(data?.errors ?? data?.data?.errors ?? invalidDetails(data?.invalid)));
|
|
52
|
-
}
|
|
53
|
-
return data;
|
|
54
81
|
}
|
|
55
82
|
// --- auth endpoints -------------------------------------------------------
|
|
56
83
|
registerStart(email) { return this.request('POST', '/cli/auth/register/start', { email }); }
|
package/dist/lib/config.js
CHANGED
|
@@ -30,6 +30,7 @@ exports.saveLogin = saveLogin;
|
|
|
30
30
|
exports.monitoringAccountId = monitoringAccountId;
|
|
31
31
|
exports.setMonitoring = setMonitoring;
|
|
32
32
|
exports.getClient = getClient;
|
|
33
|
+
exports.renewTokenFor = renewTokenFor;
|
|
33
34
|
const fs_1 = require("fs");
|
|
34
35
|
const os_1 = require("os");
|
|
35
36
|
const path_1 = require("path");
|
|
@@ -238,7 +239,16 @@ function saveAccount(email, session) {
|
|
|
238
239
|
// so without this a later `cku login` would silently drop a dashboard session
|
|
239
240
|
// the account already holds.
|
|
240
241
|
const dashboard = session.dashboard ?? loadAccount(slug).dashboard;
|
|
241
|
-
|
|
242
|
+
// The password is the one field carried across a re-login: it is what lets a
|
|
243
|
+
// rejected token be re-issued without the user signing in again.
|
|
244
|
+
const password = session.password ?? loadAccount(slug).password;
|
|
245
|
+
writeJson(accountPath(slug), {
|
|
246
|
+
...session,
|
|
247
|
+
email,
|
|
248
|
+
savedAt: new Date().toISOString(),
|
|
249
|
+
...(dashboard ? { dashboard } : {}),
|
|
250
|
+
...(password ? { password } : {}),
|
|
251
|
+
});
|
|
242
252
|
setActiveAccount(email);
|
|
243
253
|
return slug;
|
|
244
254
|
}
|
|
@@ -323,8 +333,10 @@ function clearSession() {
|
|
|
323
333
|
if (!(0, fs_1.existsSync)(path))
|
|
324
334
|
return undefined;
|
|
325
335
|
const { email, savedAt, dashboard } = loadAccount(slug);
|
|
326
|
-
// Dropping the ck_ token is what logout means.
|
|
327
|
-
//
|
|
336
|
+
// Dropping the ck_ token is what logout means. The password goes with it: it
|
|
337
|
+
// exists only to re-issue that token, so keeping it would let a logged-out
|
|
338
|
+
// account sign itself back in. A dashboard session is a separate credential
|
|
339
|
+
// and is left in place — `cku dashboard logout` removes it.
|
|
328
340
|
writeJson(path, { email, savedAt, ...(dashboard ? { dashboard } : {}) });
|
|
329
341
|
return email;
|
|
330
342
|
}
|
|
@@ -365,5 +377,48 @@ function setMonitoring(monitoring) {
|
|
|
365
377
|
/** Build the API client from the endpoint URLs + resolved token. */
|
|
366
378
|
function getClient() {
|
|
367
379
|
const cfg = loadConfig();
|
|
368
|
-
return {
|
|
380
|
+
return {
|
|
381
|
+
api: new api_1.Api(backendUrl(), callUrl(), cfg.token, monitoringAccountId(cfg), renewTokenFor(cfg)),
|
|
382
|
+
cfg,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Sign in again for an account that stored its password, and persist the new
|
|
387
|
+
* token.
|
|
388
|
+
*
|
|
389
|
+
* The `ck_` token is opaque and has no refresh route, so a token the server
|
|
390
|
+
* stops accepting leaves no way back in except a fresh sign-in. An account that
|
|
391
|
+
* stored its password renews itself; one that did not cannot, and the caller
|
|
392
|
+
* gets undefined so the error can name `cku login`.
|
|
393
|
+
*
|
|
394
|
+
* Under CK_TOKEN there is no stored account in effect, so nothing is renewed:
|
|
395
|
+
* the environment owns the credential.
|
|
396
|
+
*/
|
|
397
|
+
function renewTokenFor(cfg = loadConfig()) {
|
|
398
|
+
if (process.env.CK_TOKEN)
|
|
399
|
+
return undefined;
|
|
400
|
+
const email = cfg.email;
|
|
401
|
+
const password = cfg.password;
|
|
402
|
+
if (!email || !password)
|
|
403
|
+
return undefined;
|
|
404
|
+
return async () => {
|
|
405
|
+
try {
|
|
406
|
+
const res = await fetch(`${backendUrl()}/cli/auth/login`, {
|
|
407
|
+
method: 'POST',
|
|
408
|
+
headers: { 'Content-Type': 'application/json' },
|
|
409
|
+
body: JSON.stringify({ email, password }),
|
|
410
|
+
});
|
|
411
|
+
const data = (await res.json().catch(() => ({})));
|
|
412
|
+
if (!res.ok || !data.token)
|
|
413
|
+
return undefined;
|
|
414
|
+
// Persist the new token, keeping everything else the account holds.
|
|
415
|
+
const slug = slugFor(email);
|
|
416
|
+
const current = loadAccount(slug);
|
|
417
|
+
writeJson(accountPath(slug), { ...current, email, token: data.token, savedAt: new Date().toISOString() });
|
|
418
|
+
return data.token;
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
369
424
|
}
|