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.
@@ -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
- (0, ui_1.success)((0, config_1.saveLogin)(res.user?.email || email, { token: res.token, user: res.user }));
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 and CI logs.
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 }) => ({ ...a, signedIn: Boolean(token) })), null, 2));
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
- constructor(baseUrl, callBaseUrl, token, monitoringAccountId) {
20
- this.token = token;
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 (this.token)
32
- headers.Authorization = `Bearer ${this.token}`;
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
- res = await fetch(`${base}${path}`, {
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 }); }
@@ -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
- writeJson(accountPath(slug), { ...session, email, savedAt: new Date().toISOString(), ...(dashboard ? { dashboard } : {}) });
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. A dashboard session is a
327
- // separate credential and is left in place — `cku firebase logout` removes it.
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 { api: new api_1.Api(backendUrl(), callUrl(), cfg.token, monitoringAccountId(cfg)), cfg };
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "callkaro-cli-unofficial",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Unofficial fork of the CallKaro CLI — manage your voice AI agents from the terminal",
6
6
  "bin": {