callkaro-cli-unofficial 0.5.1 → 0.6.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,388 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.expiryLabel = void 0;
7
+ exports.cookieJar = cookieJar;
8
+ exports.cookieHeader = cookieHeader;
9
+ exports.adminLogin = adminLogin;
10
+ exports.saveAdmin = saveAdmin;
11
+ exports.adminSession = adminSession;
12
+ exports.adminSessions = adminSessions;
13
+ exports.hasAdmin = hasAdmin;
14
+ exports.forgetAdmin = forgetAdmin;
15
+ exports.requireAdmin = requireAdmin;
16
+ exports.requireAdminFor = requireAdminFor;
17
+ exports.impersonateClient = impersonateClient;
18
+ exports.reImpersonate = reImpersonate;
19
+ exports.adminLoginCommand = adminLoginCommand;
20
+ exports.adminWhoamiCommand = adminWhoamiCommand;
21
+ exports.adminLogoutCommand = adminLogoutCommand;
22
+ const prompts_1 = __importDefault(require("prompts"));
23
+ const ui_1 = require("./ui");
24
+ const config_1 = require("./config");
25
+ const dashboardAuth_1 = require("./dashboardAuth");
26
+ Object.defineProperty(exports, "expiryLabel", { enumerable: true, get: function () { return dashboardAuth_1.expiryLabel; } });
27
+ /**
28
+ * Sign-in to the internal admin console — `https://intadmin.callkaro.ai`.
29
+ *
30
+ * ## The credential
31
+ *
32
+ * `POST /monitor/auth/login {email, password}` returns a cookie named
33
+ * `access_token_monitor`: a JWT with a 24h lifetime and **no refresh endpoint**.
34
+ * It reaches the `/monitor/*` routes and nothing else — the `ck_` token and the
35
+ * dashboard JWT are both rejected there, and this one is rejected by theirs.
36
+ *
37
+ * So the session is stored like the dashboard session (0600, password optional)
38
+ * and renews by signing in again, silently, when a password was kept.
39
+ *
40
+ * ## The cookie jar
41
+ *
42
+ * The login response sets `access_token_monitor` **four times**: three headers
43
+ * that clear it (empty value, epoch expiry) and then the real one. Taking the
44
+ * first, or joining the headers naively, produces a cookie header the server
45
+ * rejects. So the jar keeps the last non-empty value per name.
46
+ */
47
+ /**
48
+ * Collect Set-Cookie headers into name→value, keeping the last non-empty value
49
+ * for each name.
50
+ *
51
+ * A response can repeat a cookie to clear it (empty value, past expiry) and
52
+ * then set it for real. Taking the last non-empty value handles both orders.
53
+ */
54
+ function cookieJar(headers) {
55
+ const jar = new Map();
56
+ const list = typeof headers.getSetCookie === 'function'
57
+ ? headers.getSetCookie()
58
+ : [headers.get('set-cookie') || ''];
59
+ for (const raw of list) {
60
+ const [pair] = raw.split(';');
61
+ const i = pair.indexOf('=');
62
+ if (i < 0)
63
+ continue;
64
+ const name = pair.slice(0, i).trim();
65
+ const value = pair.slice(i + 1).trim();
66
+ if (value)
67
+ jar.set(name, value);
68
+ }
69
+ return jar;
70
+ }
71
+ /** `name=value; name=value` for a Cookie header. */
72
+ function cookieHeader(jar) {
73
+ return [...jar].map(([k, v]) => `${k}=${v}`).join('; ');
74
+ }
75
+ /**
76
+ * Sign in to the admin console with email + password.
77
+ *
78
+ * Returns the session without the password; the caller decides whether to keep
79
+ * it. The admin's own id comes from the same response, because
80
+ * `GET /monitor/users/:id` — the route that lists the client accounts — is
81
+ * keyed by it.
82
+ */
83
+ async function adminLogin(email, password) {
84
+ const res = await fetch(`${(0, config_1.backendUrl)()}/monitor/auth/login`, {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({ email, password }),
88
+ });
89
+ const text = await res.text();
90
+ let data;
91
+ try {
92
+ data = text ? JSON.parse(text) : {};
93
+ }
94
+ catch {
95
+ throw new Error(`Admin login returned a non-JSON response (HTTP ${res.status}).`);
96
+ }
97
+ if (!res.ok) {
98
+ throw new Error(data.msg || `Admin login failed (HTTP ${res.status})`);
99
+ }
100
+ const jar = cookieJar(res.headers);
101
+ const token = jar.get('access_token_monitor');
102
+ if (!token) {
103
+ throw new Error('Admin login succeeded but no access_token_monitor cookie came back, so the session is incomplete.\n' +
104
+ ' Without it, every `cku admin` command will 401.\n' +
105
+ ' Usually a proxy or browser extension stripping Set-Cookie.');
106
+ }
107
+ const payload = (0, dashboardAuth_1.decodeJwt)(token) || {};
108
+ const user = data.user || payload.user || {};
109
+ return {
110
+ email: user.email || data.user?.email || email,
111
+ token,
112
+ userId: user._id || data.user?._id || payload.id || '',
113
+ role: user.role || data.user?.role || payload.role,
114
+ expiresAt: typeof payload.exp === 'number' ? new Date(payload.exp * 1000).toISOString() : undefined,
115
+ savedAt: new Date().toISOString(),
116
+ };
117
+ }
118
+ // --- account binding -------------------------------------------------------
119
+ /** Store on the account, keeping whatever else it holds. */
120
+ function saveAdmin(email, session) {
121
+ return (0, config_1.saveAdminSession)(email, session);
122
+ }
123
+ function adminSession(email) {
124
+ if (!email)
125
+ return undefined;
126
+ return (0, config_1.loadAccount)((0, config_1.slugFor)(email)).admin;
127
+ }
128
+ /**
129
+ * Every stored account that holds an admin console session, newest first.
130
+ *
131
+ * The admin console session is a *person's* credential, not an account's: one
132
+ * admin, one session, whichever client account happens to be active. So the
133
+ * gate looks past the active account when the active account has none.
134
+ */
135
+ function adminSessions() {
136
+ return (0, config_1.listAccounts)()
137
+ .filter((a) => a.admin?.token)
138
+ .map((a) => ({ email: a.email || '', session: a.admin }));
139
+ }
140
+ /** Does this account hold an admin console session locally? A hint, not a guarantee. */
141
+ function hasAdmin(email) {
142
+ return Boolean(email && (0, config_1.loadAccount)((0, config_1.slugFor)(email)).admin?.token);
143
+ }
144
+ function forgetAdmin(email) {
145
+ return (0, config_1.clearAdminSession)(email);
146
+ }
147
+ // --- the auth gate ---------------------------------------------------------
148
+ /**
149
+ * The restricted-endpoint gate for the `/monitor/*` surface, the counterpart to
150
+ * `session.ts`'s `requireApi()` and `dashboardAuth.ts`'s `requireDashboard()`.
151
+ *
152
+ * Deliberately separate: no `/cli/*` command may depend on this credential, and
153
+ * no admin command on the `ck_` token.
154
+ *
155
+ * The console session has no refresh endpoint, so a lapsed one is re-issued by
156
+ * signing in again. With a stored password that happens here, silently;
157
+ * without one this fails with the concrete next command.
158
+ */
159
+ async function requireAdmin() {
160
+ const slug = (0, config_1.activeAccount)();
161
+ const email = slug ? (0, config_1.loadAccount)(slug).email : undefined;
162
+ // The console session belongs to a *person*, not to a client account: one
163
+ // admin holds one session, filed on whichever account they signed in with.
164
+ // So the active account's session is used when it has one, and otherwise the
165
+ // admin's session is found wherever it was filed — the admin signs in with
166
+ // their own login, but acts on client accounts they are assigned.
167
+ if (email && adminSession(email)?.token)
168
+ return requireAdminFor(email);
169
+ const others = adminSessions();
170
+ if (others.length)
171
+ return requireAdminFor(others[0].email);
172
+ throw new Error(`No admin console session for ${email || 'the account in effect'}.\n` +
173
+ ' The /monitor/* routes need an admin console sign-in, not a ck_ token.\n' +
174
+ ' Run: cku admin login');
175
+ }
176
+ /**
177
+ * The same gate, for a named account rather than the active one.
178
+ *
179
+ * Needed because an impersonated client account renews by impersonating again,
180
+ * and the admin session that allows that lives on the **admin's** account — not
181
+ * on the client account that happens to be active while the renewal runs.
182
+ */
183
+ async function requireAdminFor(email) {
184
+ const session = adminSession(email);
185
+ if (!session?.token) {
186
+ throw new Error(`No admin console session for ${email || 'the account in effect'}.\n` +
187
+ ' The /monitor/* routes need an admin console sign-in, not a ck_ token.\n' +
188
+ ' Run: cku admin login');
189
+ }
190
+ // Expiry is read off the cookie token. A session stored before the role
191
+ // fields existed still works; only the token matters here.
192
+ const left = (0, dashboardAuth_1.msUntilExpiry)(session.token);
193
+ if (left !== undefined && left <= (session.password ? 60_000 : 0)) {
194
+ if (!session.password) {
195
+ const why = `Admin console session for ${email} expired ${new Date(Date.now() + left).toLocaleString('en-US')}.\n` +
196
+ ' There is no refresh endpoint — the token lives 24h and must be re-issued.';
197
+ throw new Error(`${why}\n Run: cku admin login`);
198
+ }
199
+ try {
200
+ const renewed = await adminLogin(session.email || email, session.password);
201
+ saveAdmin(email, { ...renewed, password: session.password });
202
+ return { token: renewed.token, email: renewed.email, role: renewed.role, userId: renewed.userId };
203
+ }
204
+ catch (err) {
205
+ throw new Error(`Admin console session for ${email} expired and re-login with the stored password failed: ${err.message}\n` +
206
+ ' If the password changed, run: cku admin login');
207
+ }
208
+ }
209
+ return { token: session.token, email: session.email, role: session.role, userId: session.userId };
210
+ }
211
+ // --- impersonation ---------------------------------------------------------
212
+ /**
213
+ * Mint a real `ck_` token for a client account, through the admin console.
214
+ *
215
+ * ## Why this is not one call
216
+ *
217
+ * The console's "Login as this user" button returns a **dashboard JWT** with an
218
+ * `actAs` claim — not a `ck_` token. Probed: that JWT gets 401 on
219
+ * `/cli/auth/me`, and so does the admin cookie. The missing link is
220
+ * `POST /cli/auth/browser/approve`, which is what the web app's own login page
221
+ * calls after the browser signs in. The console's browser flow therefore
222
+ * produces the same thing a normal sign-in does: a `ck_` token.
223
+ *
224
+ * The chain, all four hops confirmed live:
225
+ *
226
+ * 1. GET /monitor/clients/:id/impersonate -> { "auth-token": <JWT> }
227
+ * 2. POST /auth/apply-admin-session {token} -> cookie `access_token`
228
+ * 3. POST /cli/auth/browser/start -> { code, pollSecret }
229
+ * POST /cli/auth/browser/approve {code} -> 200
230
+ * 4. GET /cli/auth/browser/poll?code&secret -> { status:'approved', token:'ck_…' }
231
+ *
232
+ * The resulting token carries the *admin's* identity, so on its own it reaches
233
+ * no agents; `X-CallKaro-Account-Id` scopes it to the client. That is exactly
234
+ * the `cku monitor` mechanism, so the caller stores it with `monitoring` set.
235
+ */
236
+ async function impersonateClient(clientId, opts = {}) {
237
+ const base = (0, config_1.backendUrl)();
238
+ // The stored admin console session is the credential for hop 1. It is looked
239
+ // up on the *admin's* account, which is not the active one when a client
240
+ // account is renewing itself.
241
+ const { token: monitorToken, email: adminEmail } = await requireAdminFor(opts.adminEmail);
242
+ const imp = await fetch(`${base}/monitor/clients/${encodeURIComponent(clientId)}/impersonate`, {
243
+ headers: { Cookie: `access_token_monitor=${monitorToken}` },
244
+ });
245
+ const impText = await imp.text();
246
+ if (!imp.ok) {
247
+ const msg = (() => {
248
+ try {
249
+ return JSON.parse(impText).msg;
250
+ }
251
+ catch {
252
+ return impText.slice(0, 160);
253
+ }
254
+ })();
255
+ throw new Error(`Could not impersonate ${clientId} (HTTP ${imp.status}): ${msg}\n` +
256
+ ' The client account must be assigned to your admin user.');
257
+ }
258
+ const impersonationToken = (JSON.parse(impText)['auth-token'] || '');
259
+ if (!impersonationToken)
260
+ throw new Error('Impersonation returned no auth-token.');
261
+ // 2. Trade it for a session cookie. This is the dashboard's `access_token`,
262
+ // not the console's — a different cookie, for the impersonated identity.
263
+ const apply = await fetch(`${base}/auth/apply-admin-session`, {
264
+ method: 'POST',
265
+ headers: { 'Content-Type': 'application/json' },
266
+ body: JSON.stringify({ token: impersonationToken }),
267
+ });
268
+ if (!apply.ok) {
269
+ throw new Error(`Could not apply the admin session (HTTP ${apply.status}).`);
270
+ }
271
+ const sessionJar = cookieJar(apply.headers);
272
+ const sessionCookie = cookieHeader(sessionJar);
273
+ if (!sessionJar.has('access_token')) {
274
+ throw new Error('The admin session was applied but no access_token cookie came back.\n' +
275
+ ' Without it, the browser-approve exchange cannot be signed.');
276
+ }
277
+ const sessionHeaders = { Cookie: sessionCookie };
278
+ // 3. Start the browser-approval handshake and approve it ourselves.
279
+ const start = await fetch(`${base}/cli/auth/browser/start`, {
280
+ method: 'POST',
281
+ headers: { 'Content-Type': 'application/json', ...sessionHeaders },
282
+ });
283
+ const startText = await start.text();
284
+ if (!start.ok)
285
+ throw new Error(`Could not start the approval session (HTTP ${start.status}).`);
286
+ const { code, pollSecret } = JSON.parse(startText);
287
+ if (!code || !pollSecret)
288
+ throw new Error('The approval session came back without a code.');
289
+ const approve = await fetch(`${base}/cli/auth/browser/approve`, {
290
+ method: 'POST',
291
+ headers: { 'Content-Type': 'application/json', ...sessionHeaders },
292
+ body: JSON.stringify({ code }),
293
+ });
294
+ if (!approve.ok) {
295
+ throw new Error(`Could not approve the session (HTTP ${approve.status}).`);
296
+ }
297
+ // 4. Poll for the token.
298
+ const poll = await fetch(`${base}/cli/auth/browser/poll?code=${encodeURIComponent(code)}&secret=${encodeURIComponent(pollSecret)}`, { headers: sessionHeaders });
299
+ const pollText = await poll.text();
300
+ if (!poll.ok) {
301
+ throw new Error(`Could not collect the token (HTTP ${poll.status}): ${pollText.slice(0, 160)}`);
302
+ }
303
+ const polled = JSON.parse(pollText);
304
+ if (polled.status !== 'approved' || !polled.token) {
305
+ throw new Error(`The approval session ended with status "${polled.status}" instead of an approved token.`);
306
+ }
307
+ return { token: polled.token, email: adminEmail };
308
+ }
309
+ /**
310
+ * Re-mint an impersonated token. Used by the renewal path: an impersonated
311
+ * account has no password, so it renews by impersonating again.
312
+ */
313
+ async function reImpersonate(impersonated) {
314
+ try {
315
+ const { token } = await impersonateClient(impersonated.clientId, { adminEmail: impersonated.adminEmail });
316
+ return token;
317
+ }
318
+ catch {
319
+ return undefined;
320
+ }
321
+ }
322
+ // --- commands --------------------------------------------------------------
323
+ /** `cku admin login` — sign in to the internal admin console. */
324
+ async function adminLoginCommand(opts) {
325
+ try {
326
+ (0, ui_1.needTty)('Admin login is interactive; sign in from a terminal.');
327
+ const answers = await (0, prompts_1.default)([
328
+ { type: opts.email ? null : 'text', name: 'email', message: 'Admin email' },
329
+ { type: 'password', name: 'password', message: 'Password' },
330
+ ], { onCancel: () => process.exit(1) });
331
+ const email = opts.email || answers.email;
332
+ const password = answers.password;
333
+ if (!email || !password)
334
+ return (0, ui_1.handleError)(new Error('Email and password are required'));
335
+ const session = await adminLogin(email, password);
336
+ // Kept by default so the session renews itself every 24h; --no-save-password
337
+ // opts out and brings back a daily re-login.
338
+ const keep = opts.savePassword !== false;
339
+ saveAdmin(email, keep ? { ...session, password } : session);
340
+ (0, ui_1.success)(`Admin console session stored for ${session.email} (role ${session.role || 'unknown'}).`);
341
+ if (keep) {
342
+ (0, ui_1.info)(' Password saved (plain text, mode 0600) so the session renews itself when it lapses.');
343
+ (0, ui_1.info)(' Remove it with `cku admin logout`, or sign in with --no-save-password.');
344
+ }
345
+ else {
346
+ (0, ui_1.info)(' Password not saved. The session cannot be renewed, so re-run this once it expires.');
347
+ }
348
+ }
349
+ catch (err) {
350
+ (0, ui_1.handleError)(err);
351
+ }
352
+ }
353
+ /** `cku admin whoami` — prove the stored admin session still works. */
354
+ async function adminWhoamiCommand(opts) {
355
+ try {
356
+ const { adminApi } = require('./adminApi');
357
+ const res = await adminApi().getAdmin();
358
+ if (opts.json)
359
+ return void console.log(JSON.stringify(res, null, 2));
360
+ const user = res?.user || res;
361
+ (0, ui_1.success)(`Signed in to the admin console as ${user?.email || 'unknown'} (role ${user?.role || 'unknown'}).`);
362
+ if (user?.isSuperAdmin)
363
+ (0, ui_1.info)(' Super admin — every route is available.');
364
+ }
365
+ catch (err) {
366
+ (0, ui_1.handleError)(err);
367
+ }
368
+ }
369
+ /** `cku admin logout` — forget the admin session, keep the ck_ token. */
370
+ async function adminLogoutCommand(opts) {
371
+ try {
372
+ // The session may be filed on any account, so find where it actually is.
373
+ const slug = (0, config_1.activeAccount)();
374
+ const active = slug ? (0, config_1.loadAccount)(slug).email : undefined;
375
+ const held = active && hasAdmin(active) ? active : adminSessions()[0]?.email;
376
+ if (!held)
377
+ return void (0, ui_1.info)('No admin console session stored.');
378
+ if (!(await (0, ui_1.confirm)(`Forget the admin console session for ${held}? (your ck_ token is kept)`, opts.yes)))
379
+ return;
380
+ if (forgetAdmin(held))
381
+ (0, ui_1.success)(`Admin console session removed for ${held}.`);
382
+ else
383
+ (0, ui_1.fail)('Nothing to remove.');
384
+ }
385
+ catch (err) {
386
+ (0, ui_1.handleError)(err);
387
+ }
388
+ }
@@ -23,6 +23,8 @@ exports.loadAccount = loadAccount;
23
23
  exports.saveAccount = saveAccount;
24
24
  exports.saveDashboardSession = saveDashboardSession;
25
25
  exports.clearDashboardSession = clearDashboardSession;
26
+ exports.saveAdminSession = saveAdminSession;
27
+ exports.clearAdminSession = clearAdminSession;
26
28
  exports.removeAccount = removeAccount;
27
29
  exports.loadConfig = loadConfig;
28
30
  exports.clearSession = clearSession;
@@ -232,13 +234,21 @@ function loadAccount(slug) {
232
234
  return {};
233
235
  return readJson(accountPath(slug), {});
234
236
  }
235
- /** Store a session for `email` and make it the active account. */
236
- function saveAccount(email, session) {
237
+ /**
238
+ * Store a session for `email` and make it the active account.
239
+ *
240
+ * `activate: false` stores it without moving the global pointer — what
241
+ * `cku admin impersonate` needs, since minting a client account's token must
242
+ * not silently redirect the admin's own commands.
243
+ */
244
+ function saveAccount(email, session, opts = {}) {
237
245
  const slug = slugFor(email);
238
246
  // Carry the dashboard session across. `saveLogin` passes only { token, user },
239
247
  // so without this a later `cku login` would silently drop a dashboard session
240
248
  // the account already holds.
241
249
  const dashboard = session.dashboard ?? loadAccount(slug).dashboard;
250
+ // The admin console session is a third credential, carried the same way.
251
+ const admin = session.admin ?? loadAccount(slug).admin;
242
252
  // The password is the one field carried across a re-login: it is what lets a
243
253
  // rejected token be re-issued without the user signing in again.
244
254
  const password = session.password ?? loadAccount(slug).password;
@@ -247,9 +257,11 @@ function saveAccount(email, session) {
247
257
  email,
248
258
  savedAt: new Date().toISOString(),
249
259
  ...(dashboard ? { dashboard } : {}),
260
+ ...(admin ? { admin } : {}),
250
261
  ...(password ? { password } : {}),
251
262
  });
252
- setActiveAccount(email);
263
+ if (opts.activate !== false)
264
+ setActiveAccount(email);
253
265
  return slug;
254
266
  }
255
267
  /**
@@ -285,6 +297,38 @@ function clearDashboardSession(email) {
285
297
  writeJson(accountPath(slug), rest);
286
298
  return true;
287
299
  }
300
+ /**
301
+ * Store the admin console session on an account, keeping whatever else it holds.
302
+ * Merging matters: signing in to the console must not disturb a `ck_` token or a
303
+ * dashboard session the account already holds.
304
+ */
305
+ function saveAdminSession(email, admin) {
306
+ const slug = slugFor(email);
307
+ const current = loadAccount(slug);
308
+ // Built field by field, so stray fields a caller passes are not written to disk.
309
+ // `password` is the one deliberate exception.
310
+ const stored = {
311
+ email: admin.email || email,
312
+ token: admin.token,
313
+ savedAt: admin.savedAt || new Date().toISOString(),
314
+ ...(admin.userId ? { userId: admin.userId } : {}),
315
+ ...(admin.role ? { role: admin.role } : {}),
316
+ ...(admin.expiresAt ? { expiresAt: admin.expiresAt } : {}),
317
+ ...(admin.password ? { password: admin.password } : {}),
318
+ };
319
+ writeJson(accountPath(slug), { ...current, email, admin: stored, savedAt: new Date().toISOString() });
320
+ return slug;
321
+ }
322
+ /** Forget only the admin console session; the `ck_` token is left alone. */
323
+ function clearAdminSession(email) {
324
+ const slug = slugFor(email);
325
+ const current = loadAccount(slug);
326
+ if (!current.admin)
327
+ return false;
328
+ const { admin, ...rest } = current;
329
+ writeJson(accountPath(slug), rest);
330
+ return true;
331
+ }
288
332
  function removeAccount(slug) {
289
333
  const path = accountPath(slug);
290
334
  if (!(0, fs_1.existsSync)(path))
@@ -332,12 +376,18 @@ function clearSession() {
332
376
  const path = accountPath(slug);
333
377
  if (!(0, fs_1.existsSync)(path))
334
378
  return undefined;
335
- const { email, savedAt, dashboard } = loadAccount(slug);
379
+ const { email, savedAt, dashboard, admin } = loadAccount(slug);
336
380
  // Dropping the ck_ token is what logout means. The password goes with it: it
337
381
  // exists only to re-issue that token, so keeping it would let a logged-out
338
382
  // account sign itself back in. A dashboard session is a separate credential
339
- // and is left in place — `cku dashboard logout` removes it.
340
- writeJson(path, { email, savedAt, ...(dashboard ? { dashboard } : {}) });
383
+ // and is left in place — `cku dashboard logout` removes it. Same for the
384
+ // admin console session — `cku admin logout` removes that.
385
+ writeJson(path, {
386
+ email,
387
+ savedAt,
388
+ ...(dashboard ? { dashboard } : {}),
389
+ ...(admin ? { admin } : {}),
390
+ });
341
391
  return email;
342
392
  }
343
393
  /**
@@ -391,6 +441,10 @@ function getClient() {
391
441
  * stored its password renews itself; one that did not cannot, and the caller
392
442
  * gets undefined so the error can name `cku login`.
393
443
  *
444
+ * An account that was *impersonated* has no password: its token was minted by
445
+ * the admin console. It renews by impersonating again, which needs the admin
446
+ * console session rather than a password.
447
+ *
394
448
  * Under CK_TOKEN there is no stored account in effect, so nothing is renewed:
395
449
  * the environment owns the credential.
396
450
  */
@@ -399,7 +453,33 @@ function renewTokenFor(cfg = loadConfig()) {
399
453
  return undefined;
400
454
  const email = cfg.email;
401
455
  const password = cfg.password;
402
- if (!email || !password)
456
+ const impersonated = cfg.impersonated;
457
+ if (!email)
458
+ return undefined;
459
+ // Persist to the slot the session was read from, not to one derived from the
460
+ // email: the account file is what every later command resolves. Writing the
461
+ // new token somewhere else renews the command but leaves the slot dead, and
462
+ // the next invocation fails exactly as before.
463
+ const slug = activeAccount() || slugFor(email);
464
+ if (impersonated) {
465
+ return async () => {
466
+ try {
467
+ // Lazy require: adminAuth imports this module, so a top-level import
468
+ // would be a cycle. Same pattern as refreshModelCatalog in dashboardAuth.
469
+ const { reImpersonate } = require('./adminAuth');
470
+ const fresh = await reImpersonate(impersonated);
471
+ if (!fresh)
472
+ return undefined;
473
+ const current = loadAccount(slug);
474
+ writeJson(accountPath(slug), { ...current, email, token: fresh, savedAt: new Date().toISOString() });
475
+ return fresh;
476
+ }
477
+ catch {
478
+ return undefined;
479
+ }
480
+ };
481
+ }
482
+ if (!password)
403
483
  return undefined;
404
484
  return async () => {
405
485
  try {
@@ -412,7 +492,6 @@ function renewTokenFor(cfg = loadConfig()) {
412
492
  if (!res.ok || !data.token)
413
493
  return undefined;
414
494
  // Persist the new token, keeping everything else the account holds.
415
- const slug = slugFor(email);
416
495
  const current = loadAccount(slug);
417
496
  writeJson(accountPath(slug), { ...current, email, token: data.token, savedAt: new Date().toISOString() });
418
497
  return data.token;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readJsonFile = readJsonFile;
4
4
  exports.stripBom = stripBom;
5
+ exports.repairPosixPath = repairPosixPath;
5
6
  exports.parseLooseJson = parseLooseJson;
6
7
  const fs_1 = require("fs");
7
8
  /**
@@ -25,6 +26,48 @@ const NUMBER = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/;
25
26
  /** Characters that end a bare value / a bare key at the current nesting level. */
26
27
  const VALUE_STOP = ',}]';
27
28
  const KEY_STOP = ':,}';
29
+ /**
30
+ * Repair a POSIX path argument that Git Bash turned into a Windows path.
31
+ *
32
+ * MSYS2 (Git Bash on Windows) rewrites an argument that looks like an absolute
33
+ * POSIX path into a Windows one, so
34
+ *
35
+ * ck dashboard api GET /v1/call-analyzers
36
+ *
37
+ * arrives in `process.argv` as `C:/Program Files/Git/v1/call-analyzers` — the
38
+ * leading slash is replaced by the shell's own root and the original path
39
+ * follows it. The argument is then refused with "path must start with /",
40
+ * which reads like a CLI bug rather than a shell quirk.
41
+ *
42
+ * The repair finds the known route segment as a whole path segment and takes
43
+ * the path from there. Boundaries matter: `monitoring` must not match the
44
+ * segment `monitor`, and a segment must not match in the middle of a name.
45
+ *
46
+ * Only the listed segments are repaired: a path that contains none of them is
47
+ * left alone, and the caller's own "must start with /" check then reports it.
48
+ * Guessing a split point would turn a wrong path into a different wrong path,
49
+ * silently.
50
+ */
51
+ function repairPosixPath(raw, segments) {
52
+ if (raw.startsWith('/'))
53
+ return raw;
54
+ for (const segment of segments) {
55
+ let from = 0;
56
+ for (;;) {
57
+ const i = raw.indexOf(segment, from);
58
+ if (i < 0)
59
+ break;
60
+ const before = raw[i - 1];
61
+ const after = raw[i + segment.length];
62
+ const leftOk = i === 0 || before === '/' || before === '\\' || before === ':';
63
+ const rightOk = after === undefined || after === '/' || after === '?' || after === '&';
64
+ if (leftOk && rightOk)
65
+ return '/' + raw.slice(i);
66
+ from = i + 1;
67
+ }
68
+ }
69
+ return raw;
70
+ }
28
71
  /**
29
72
  * Parse `raw` as JSON, repairing shell-stripped quotes if the strict parse fails.
30
73
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "callkaro-cli-unofficial",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
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": {