mc8yp 2.8.3 → 2.8.4

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.
@@ -774,6 +774,12 @@ const command = defineCommand({
774
774
  cancel: "reject"
775
775
  });
776
776
  parse(pipe(string(), url()), tenantUrl);
777
+ const { protocol, hostname } = new URL(tenantUrl);
778
+ if (protocol === "http:" && ![
779
+ "localhost",
780
+ "127.0.0.1",
781
+ "[::1]"
782
+ ].includes(hostname)) consola.warn(`${tenantUrl} uses plain http — your password (and TFA code) would be sent unencrypted. Use https unless this is a trusted local setup.`);
777
783
  const user = await consola.prompt("Username:", {
778
784
  type: "text",
779
785
  cancel: "reject"
package/dist/cli.mjs CHANGED
@@ -6,12 +6,12 @@ import { join, sep } from "node:path";
6
6
  import * as tty from "node:tty";
7
7
  import { AsyncLocalStorage } from "node:async_hooks";
8
8
  import { Buffer as Buffer$1 } from "node:buffer";
9
- import { AsyncEntry, findCredentialsAsync } from "@napi-rs/keyring";
10
9
  import { lookup } from "node:dns";
11
10
  import { createSandbox } from "@iso4/sandbox";
12
11
  import { Bash } from "just-bash";
13
12
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
13
  import { homedir } from "node:os";
14
+ import { AsyncEntry, findCredentialsAsync } from "@napi-rs/keyring";
15
15
  //#region \0rolldown/runtime.js
16
16
  var __create = Object.create;
17
17
  var __defProp = Object.defineProperty;
@@ -1847,7 +1847,7 @@ const consola = createConsola();
1847
1847
  //#endregion
1848
1848
  //#region package.json
1849
1849
  var name = "mc8yp";
1850
- var version = "2.8.3";
1850
+ var version = "2.8.4";
1851
1851
  var description$1 = "Cumulocity IoT MCP Server - Model Context Protocol integration for IoT device management";
1852
1852
  //#endregion
1853
1853
  //#region \0virtual:core-openapi
@@ -127256,148 +127256,6 @@ function getBundledOnlyCapabilities() {
127256
127256
  mcpServers: {}
127257
127257
  };
127258
127258
  }
127259
- //#endregion
127260
- //#region src/utils/credentials.ts
127261
- /**
127262
- * Thrown when the platform rejects a login because a TFA code is required.
127263
- * `creds add` catches it to prompt for the code.
127264
- */
127265
- var TfaRequiredError = class extends Error {
127266
- constructor(message) {
127267
- super(message);
127268
- this.name = "TfaRequiredError";
127269
- }
127270
- };
127271
- /**
127272
- * Turn a failed platform response into an Error carrying the platform's own
127273
- * message (e.g. "Invalid credentials! : TFA TOTP code required.").
127274
- * @param res - Non-2xx response from the tenant
127275
- * @param action - What was attempted, for the message prefix
127276
- */
127277
- async function platformError(res, action) {
127278
- const body = await res.json().catch(() => void 0);
127279
- const message = `${action} failed: ${res.status} ${res.statusText}${body?.message ? ` — ${body.message}` : ""}`;
127280
- return res.status === 401 && /\bTFA\b/i.test(body?.message ?? "") ? new TfaRequiredError(message) : new Error(message);
127281
- }
127282
- async function resolveTenantId(tenantUrl, authorizationHeader) {
127283
- const res = await fetch(`${tenantUrl}/tenant/currentTenant`, { headers: {
127284
- Authorization: authorizationHeader,
127285
- Accept: "application/json"
127286
- } });
127287
- if (!res.ok) throw await platformError(res, "Login");
127288
- return (await res.json()).name;
127289
- }
127290
- /**
127291
- * Exchange user, password and a current TFA code for an OAI-Secure access
127292
- * token via `POST /tenant/oauth/token`.
127293
- * @param creds - Tenant URL, user, password, and the tenant ID when already known
127294
- * @param tfaCode - Current TFA code (TOTP)
127295
- */
127296
- async function requestTfaSession(creds, tfaCode) {
127297
- const url = new URL("/tenant/oauth/token", creds.tenantUrl);
127298
- if (creds.tenantId) url.searchParams.set("tenant_id", creds.tenantId);
127299
- const res = await fetch(url, {
127300
- method: "POST",
127301
- headers: {
127302
- "Content-Type": "application/x-www-form-urlencoded",
127303
- "Accept": "application/json"
127304
- },
127305
- body: new URLSearchParams({
127306
- grant_type: "PASSWORD",
127307
- username: creds.user,
127308
- password: creds.password,
127309
- tfa_code: tfaCode.trim()
127310
- })
127311
- });
127312
- if (!res.ok) throw await platformError(res, "TFA login");
127313
- const { access_token: token } = await res.json();
127314
- if (!token) throw new Error("TFA login failed: the platform response contained no access token.");
127315
- let exp;
127316
- try {
127317
- exp = JSON.parse(Buffer$1.from(token.split(".")[1] ?? "", "base64url").toString("utf8")).exp;
127318
- } catch {}
127319
- if (typeof exp !== "number") throw new Error("TFA login failed: could not read the expiry (exp claim) of the access token.");
127320
- return {
127321
- token,
127322
- expiresAt: exp * 1e3
127323
- };
127324
- }
127325
- async function writeStoredC8yAuth(creds) {
127326
- const jsonString = JSON.stringify(creds);
127327
- const entry = new AsyncEntry(name, creds.tenantUrl);
127328
- try {
127329
- await entry.setPassword(jsonString);
127330
- } catch (err) {
127331
- throw new Error("Failed to store credentials", { cause: err });
127332
- }
127333
- }
127334
- function parseStoredUserC8yAuth(jsonString, tenantUrl) {
127335
- const cred = JSON.parse(jsonString);
127336
- if (!cred.tenantId) throw new Error(`Stored credentials for tenant URL ${tenantUrl} are outdated. Remove and add them again with tenantId.`);
127337
- return {
127338
- tenantUrl,
127339
- user: cred.user,
127340
- password: cred.password,
127341
- tenantId: cred.tenantId,
127342
- ...cred.tfaSession ? { tfaSession: cred.tfaSession } : {}
127343
- };
127344
- }
127345
- async function getStoredC8yAuth() {
127346
- const found = await findCredentialsAsync(name);
127347
- const creds = [];
127348
- for (const entry of found) {
127349
- const { account, password: jsonString } = entry;
127350
- creds.push(parseStoredUserC8yAuth(jsonString, cleanTenantUrl(account)));
127351
- }
127352
- return creds;
127353
- }
127354
- async function getCredentialsByTenantUrl(tenantUrl) {
127355
- const cleanedUrl = cleanTenantUrl(tenantUrl);
127356
- const entry = (await findCredentialsAsync(name)).find((e) => cleanTenantUrl(e.account) === cleanedUrl);
127357
- if (!entry) throw new Error(`No stored credentials found for tenant URL: ${cleanedUrl}`);
127358
- return parseStoredUserC8yAuth(entry.password, cleanedUrl);
127359
- }
127360
- async function setStoredC8yAuth(creds) {
127361
- const cleanedTenantUrl = cleanTenantUrl(creds.tenantUrl);
127362
- await writeStoredC8yAuth({
127363
- ...creds,
127364
- tenantUrl: cleanedTenantUrl,
127365
- tenantId: creds.tenantId ?? await resolveTenantId(cleanedTenantUrl, creds.tfaSession ? `Bearer ${creds.tfaSession.token}` : `Basic ${Buffer$1.from(`${creds.user}:${creds.password}`).toString("base64")}`)
127366
- });
127367
- }
127368
- /**
127369
- * Replace the stored TFA session for an existing credential entry.
127370
- * @param creds - The stored credentials the new session belongs to
127371
- * @param tfaSession - Freshly obtained session token
127372
- */
127373
- async function updateStoredTfaSession(creds, tfaSession) {
127374
- const updated = {
127375
- ...creds,
127376
- tfaSession
127377
- };
127378
- await writeStoredC8yAuth(updated);
127379
- return updated;
127380
- }
127381
- function cleanTenantUrl(url) {
127382
- let cleaned = url.trim();
127383
- const slashIndex = cleaned.indexOf("/", cleaned.indexOf("://") + 3);
127384
- if (slashIndex !== -1) cleaned = cleaned.slice(0, slashIndex);
127385
- if (cleaned.endsWith("/")) cleaned = cleaned.slice(0, -1);
127386
- return cleaned;
127387
- }
127388
- async function deleteStoredC8yAuth(tenantUrl) {
127389
- const cleanedUrl = cleanTenantUrl(tenantUrl);
127390
- if (!(await findCredentialsAsync(name)).some((entry) => cleanTenantUrl(entry.account) === cleanedUrl)) return false;
127391
- const entry = new AsyncEntry(name, cleanedUrl);
127392
- let deleteError;
127393
- try {
127394
- await entry.deletePassword();
127395
- } catch (err) {
127396
- deleteError = err;
127397
- }
127398
- if (!(await findCredentialsAsync(name)).some((e) => cleanTenantUrl(e.account) === cleanedUrl)) return true;
127399
- throw new Error(`Keyring refused to delete the credentials for ${cleanedUrl}. The entry is present when listing but cannot be removed by target — this is typically a libsecret / WSL2 locked-collection issue. Unlock the login keyring (e.g. \`gnome-keyring-daemon --unlock --components=secrets\`) and try again, or remove the entry manually with \`secret-tool clear service ${name} account ${cleanedUrl}\`.`, deleteError instanceof Error ? { cause: deleteError } : void 0);
127400
- }
127401
127259
  const inFlight = /* @__PURE__ */ new Map();
127402
127260
  /**
127403
127261
  * Build a \@c8y/client for stored CLI credentials: Bearer with the TFA
@@ -127444,18 +127302,20 @@ async function renewTfaSession(creds) {
127444
127302
  if (!c8yMcpServer.ctx.sessionInfo?.clientCapabilities?.elicitation) throw new Error(`${state} This MCP client cannot prompt for a new TFA code (no elicitation support). ${shellHint}`);
127445
127303
  let code;
127446
127304
  try {
127447
- const answer = await c8yMcpServer.elicitation(`${state} Enter the current code from your authenticator app to continue.`, /* @__PURE__ */ object({ code: /* @__PURE__ */ pipe(/* @__PURE__ */ string(), /* @__PURE__ */ regex(/^\s*\d{6,8}\s*$/, "Enter the 6-digit code from your authenticator app."), /* @__PURE__ */ description("TFA code")) }));
127305
+ const answer = await c8yMcpServer.elicitation(`mc8yp — the Cumulocity MCP server your AI assistant is using — needs a new TFA code to keep acting as ${creds.user} on ${creds.tenantUrl}. The current session ${expiresAt <= Date.now() ? "expired" : "expires"} at ${new Date(expiresAt).toLocaleString()}. Entering the code from your authenticator app renews the assistant's access to this tenant for the full token lifetime set by your tenant. Decline if you did not expect this prompt.`, /* @__PURE__ */ object({ code: /* @__PURE__ */ pipe(/* @__PURE__ */ string(), /* @__PURE__ */ regex(/^\s*\d{6,8}\s*$/, "Enter the 6-digit code from your authenticator app."), /* @__PURE__ */ description("TFA code")) }));
127448
127306
  code = answer.action === "accept" ? answer.content?.code : void 0;
127449
127307
  } catch (error) {
127450
127308
  if (isInputRequired(error)) throw error;
127451
127309
  throw new Error(`${state} Prompting for a new TFA code failed (${error instanceof Error ? error.message : String(error)}). ${shellHint}`);
127452
127310
  }
127453
127311
  if (!code) throw new Error(`${state} No TFA code was entered. ${shellHint}`);
127454
- return updateStoredTfaSession(creds, await requestTfaSession(creds, code));
127312
+ const session = await globalThis._requestTfaSession(creds, code);
127313
+ return globalThis._updateStoredTfaSession(creds, session);
127455
127314
  }
127456
127315
  //#endregion
127457
127316
  //#region src/cli/tenant-context.ts
127458
127317
  let _context = null;
127318
+ let _pendingTenantUrl = null;
127459
127319
  /**
127460
127320
  * Return the current CLI tenant context, or null if none has been set.
127461
127321
  */
@@ -127463,6 +127323,13 @@ function getCliTenantContext() {
127463
127323
  return _context;
127464
127324
  }
127465
127325
  /**
127326
+ * Return the tenant that is selected but waits for a new TFA code before it
127327
+ * can be activated, or null.
127328
+ */
127329
+ function getPendingCliTenantUrl() {
127330
+ return _pendingTenantUrl;
127331
+ }
127332
+ /**
127466
127333
  * Drop the in-memory tenant context. Used by drift-recovery (credentials
127467
127334
  * disappeared for the active tenant) and by the explicit reset path on
127468
127335
  * set-active-tenant. Does not touch persistence — the caller is responsible
@@ -127470,6 +127337,7 @@ function getCliTenantContext() {
127470
127337
  */
127471
127338
  function clearCliTenantContext() {
127472
127339
  _context = null;
127340
+ _pendingTenantUrl = null;
127473
127341
  }
127474
127342
  /**
127475
127343
  * Set (or update) the active tenant context.
@@ -127486,34 +127354,74 @@ function clearCliTenantContext() {
127486
127354
  * @param tenantUrl - Base URL of the Cumulocity tenant to activate
127487
127355
  */
127488
127356
  async function setCliTenantContext(tenantUrl) {
127357
+ _context = await resolveCliTenantContext(tenantUrl);
127358
+ _pendingTenantUrl = null;
127359
+ return _context;
127360
+ }
127361
+ /**
127362
+ * Restore the persisted tenant at CLI startup. A TFA session that has run
127363
+ * out (or is about to) cannot be renewed yet — no client is connected to
127364
+ * ask for a code — so the tenant is only marked pending and activated on
127365
+ * the first call instead.
127366
+ * @param tenantUrl - Persisted active tenant URL
127367
+ * @returns The activated context, or null when activation waits for a TFA code
127368
+ */
127369
+ async function restoreCliTenantContext(tenantUrl) {
127370
+ const creds = await globalThis._getCredentialsByTenantUrl(tenantUrl);
127371
+ if (creds.tfaSession && creds.tfaSession.expiresAt - Date.now() <= 3e5) {
127372
+ _pendingTenantUrl = tenantUrl;
127373
+ return null;
127374
+ }
127375
+ return setCliTenantContext(tenantUrl);
127376
+ }
127377
+ async function resolveCliTenantContext(tenantUrl) {
127489
127378
  const creds = await getFreshCliCredentials(tenantUrl);
127490
127379
  const authHeaders = createC8yAuthHeaders(creds);
127491
127380
  const cliClient = createCliClient(creds);
127492
127381
  const { specs: discovered, installedContextPaths, mcpServers } = await startDiscovery(creds.tenantId, cliClient);
127493
- _context = {
127382
+ return {
127494
127383
  tenantUrl,
127495
127384
  authorizationHeader: authHeaders.Authorization,
127496
127385
  specs: resolveCapabilities(discovered, installedContextPaths, mcpServers),
127497
127386
  tfaExpiresAt: creds.tfaSession?.expiresAt
127498
127387
  };
127499
- return _context;
127500
127388
  }
127501
127389
  /**
127502
- * Renew the active tenant's TFA session when it is about to expire, and
127503
- * push the new Authorization header into the shared MCP context. Noop for
127504
- * Basic-auth tenants and while the token is still fresh, so the keyring is
127505
- * only read near expiry. Called by the codemode tool before each run.
127390
+ * Make the selected tenant usable before a codemode run:
127391
+ * - a tenant left pending at startup is activated now (asks for the TFA
127392
+ * code, then runs discovery);
127393
+ * - an active tenant whose TFA session is about to expire is renewed.
127394
+ *
127395
+ * The new auth (and, on activation, specs) is pushed into the shared MCP
127396
+ * context. Noop for Basic-auth tenants and while the token is still fresh,
127397
+ * so the keyring is only read near expiry.
127506
127398
  *
127507
- * Throws (via `getFreshCliCredentials`) when the session cannot be renewed
127508
- * here; the codemode tool surfaces that message to the agent.
127399
+ * Throws (via `getFreshCliCredentials`) when the code cannot be obtained;
127400
+ * the codemode tool then fails the call without running the code.
127509
127401
  */
127510
- async function renewCliTenantAuthIfExpiring() {
127402
+ async function ensureCliTenantReady() {
127403
+ const custom = c8yMcpServer.ctx.custom;
127404
+ const pending = _pendingTenantUrl;
127405
+ if (!_context && pending) {
127406
+ const ctx = await resolveCliTenantContext(pending);
127407
+ if (_pendingTenantUrl !== pending || _context) return;
127408
+ _context = ctx;
127409
+ _pendingTenantUrl = null;
127410
+ if (custom) {
127411
+ custom.auth = {
127412
+ tenantUrl: ctx.tenantUrl,
127413
+ authorizationHeader: ctx.authorizationHeader
127414
+ };
127415
+ custom.specs = ctx.specs;
127416
+ }
127417
+ return;
127418
+ }
127511
127419
  const ctx = _context;
127512
127420
  if (!ctx?.tfaExpiresAt || ctx.tfaExpiresAt - Date.now() > 3e5) return;
127513
127421
  const creds = await getFreshCliCredentials(ctx.tenantUrl);
127422
+ if (_context !== ctx) return;
127514
127423
  ctx.authorizationHeader = createC8yAuthHeaders(creds).Authorization;
127515
127424
  ctx.tfaExpiresAt = creds.tfaSession?.expiresAt;
127516
- const custom = c8yMcpServer.ctx.custom;
127517
127425
  if (custom) custom.auth = {
127518
127426
  tenantUrl: ctx.tenantUrl,
127519
127427
  authorizationHeader: ctx.authorizationHeader
@@ -147433,7 +147341,7 @@ async () => {
147433
147341
  replayable: true
147434
147342
  }, async (input) => {
147435
147343
  try {
147436
- if (env === "cli") await renewCliTenantAuthIfExpiring();
147344
+ if (env === "cli") await ensureCliTenantReady();
147437
147345
  return tool.text(await execute(input.code));
147438
147346
  } catch (error) {
147439
147347
  if (isInputRequired(error)) throw error;
@@ -147526,8 +147434,8 @@ function createSetActiveTenantTool() {
147526
147434
  const available = storedCreds.map((c) => c.tenantUrl).join(", ");
147527
147435
  return tool.error(`Tenant ${input.tenantUrl} not found in stored credentials.${available ? ` Known tenants: ${available}` : " No tenants stored — use creds add first."}`);
147528
147436
  }
147529
- writeActiveTenant(input.tenantUrl);
147530
147437
  const ctx = await setCliTenantContext(input.tenantUrl);
147438
+ writeActiveTenant(input.tenantUrl);
147531
147439
  const custom = c8yMcpServer.ctx.custom;
147532
147440
  if (!custom) throw new Error("MCP server context not initialized");
147533
147441
  custom.auth = {
@@ -147560,6 +147468,7 @@ function createStatusTool() {
147560
147468
  async function buildCliStatus(refresh) {
147561
147469
  const creds = await globalThis._getStoredC8yAuth();
147562
147470
  let active = getCliTenantContext();
147471
+ let pending = getPendingCliTenantUrl();
147563
147472
  const sections = [];
147564
147473
  if (active && !creds.some((c) => c.tenantUrl === active.tenantUrl)) {
147565
147474
  const cleared = active.tenantUrl;
@@ -147567,14 +147476,21 @@ async function buildCliStatus(refresh) {
147567
147476
  active = null;
147568
147477
  sections.push(`Active tenant ${cleared} was cleared automatically because no credentials are stored for it. Codemode discovery now falls back to all bundled OpenAPI snapshots; live API calls are unavailable until you set a tenant.`);
147569
147478
  }
147479
+ if (pending && !creds.some((c) => c.tenantUrl === pending)) {
147480
+ sections.push(`Selected tenant ${pending} was cleared automatically because no credentials are stored for it.`);
147481
+ resetActiveTenant();
147482
+ pending = null;
147483
+ }
147570
147484
  if (refresh) {
147571
- if (!active) sections.push("Refresh requested but no tenant is active — nothing to refresh. Call set-active-tenant first.");
147485
+ if (pending) sections.push(`Refresh skipped: ${pending} is waiting for a new TFA code. The next codemode or set-active-tenant call asks for it and runs discovery.`);
147486
+ else if (!active) sections.push("Refresh requested but no tenant is active — nothing to refresh. Call set-active-tenant first.");
147572
147487
  else {
147573
147488
  sections.push(await refreshCliActiveTenant(active.tenantUrl));
147574
147489
  active = getCliTenantContext();
147575
147490
  }
147576
147491
  }
147577
147492
  if (active) sections.push(`Active tenant: ${active.tenantUrl}`);
147493
+ else if (pending) sections.push(`Active tenant: ${pending} — waiting for a new TFA code because the stored TFA session expired. The next codemode or set-active-tenant call asks the user for it; until then codemode calls fail without running.`);
147578
147494
  else sections.push("Active tenant: (none) — codemode discovery falls back to all bundled OpenAPI snapshots; live API calls are unavailable until set-active-tenant is called. Visibility in the bundled-only mode does NOT guarantee any service is installed on any tenant.");
147579
147495
  if (creds.length === 0) sections.push("Stored credentials: (none). Use `creds add` from the shell to register a tenant before calling set-active-tenant.");
147580
147496
  else {
@@ -147584,7 +147500,7 @@ async function buildCliStatus(refresh) {
147584
147500
  }).join("\n");
147585
147501
  sections.push(`Stored credentials:\n${lines}`);
147586
147502
  }
147587
- if (!active && creds.length > 0) sections.push("Next step: call set-active-tenant with one of the tenant URLs above before making live API calls through codemode.");
147503
+ if (!active && !pending && creds.length > 0) sections.push("Next step: call set-active-tenant with one of the tenant URLs above before making live API calls through codemode.");
147588
147504
  return sections.join("\n\n");
147589
147505
  }
147590
147506
  /**
@@ -147600,9 +147516,19 @@ async function refreshCliActiveTenant(tenantUrl) {
147600
147516
  const result = await refreshCapabilities(creds.tenantId, client);
147601
147517
  const resolved = resolveCapabilities(result.specs, result.installedContextPaths, result.mcpServers);
147602
147518
  const cliCtx = getCliTenantContext();
147603
- if (cliCtx) cliCtx.specs = resolved;
147604
- const custom = c8yMcpServer.ctx.custom;
147605
- if (custom) custom.specs = resolved;
147519
+ if (cliCtx?.tenantUrl === tenantUrl) {
147520
+ cliCtx.specs = resolved;
147521
+ cliCtx.authorizationHeader = createC8yAuthHeaders(creds).Authorization;
147522
+ cliCtx.tfaExpiresAt = creds.tfaSession?.expiresAt;
147523
+ const custom = c8yMcpServer.ctx.custom;
147524
+ if (custom) {
147525
+ custom.specs = resolved;
147526
+ custom.auth = {
147527
+ tenantUrl,
147528
+ authorizationHeader: cliCtx.authorizationHeader
147529
+ };
147530
+ }
147531
+ }
147606
147532
  return `Refreshed API discovery for ${tenantUrl}: ${result.specs.length} spec(s) downloaded, ${result.mcpServers.length} MCP server(s) connected, ${result.installedContextPaths.size} subscribed application(s).`;
147607
147533
  } catch (err) {
147608
147534
  if (isInputRequired(err)) throw err;
@@ -147624,6 +147550,159 @@ function setupMcpServer(env) {
147624
147550
  c8yMcpServer.tool(createSetActiveTenantTool());
147625
147551
  }
147626
147552
  }
147553
+ //#endregion
147554
+ //#region src/utils/credentials.ts
147555
+ /**
147556
+ * Upper bound for a stored TFA session's expiry. Keeps a malformed `exp`
147557
+ * claim from producing an out-of-range Date.
147558
+ */
147559
+ const MAX_TFA_SESSION_MS = 31536e6;
147560
+ /**
147561
+ * Thrown when the platform rejects a login because a TFA code is required.
147562
+ * `creds add` catches it to prompt for the code.
147563
+ */
147564
+ var TfaRequiredError = class extends Error {
147565
+ constructor(message) {
147566
+ super(message);
147567
+ this.name = "TfaRequiredError";
147568
+ }
147569
+ };
147570
+ /**
147571
+ * Turn a failed platform response into an Error carrying the platform's own
147572
+ * message (e.g. "Invalid credentials! : TFA TOTP code required.").
147573
+ * @param res - Non-2xx response from the tenant
147574
+ * @param action - What was attempted, for the message prefix
147575
+ */
147576
+ async function platformError(res, action) {
147577
+ const body = await res.json().catch(() => void 0);
147578
+ const message = `${action} failed: ${res.status} ${res.statusText}${body?.message ? ` — ${body.message}` : ""}`;
147579
+ return res.status === 401 && /\bTFA\b/i.test(body?.message ?? "") ? new TfaRequiredError(message) : new Error(message);
147580
+ }
147581
+ async function resolveTenantId(tenantUrl, authorizationHeader) {
147582
+ const res = await fetch(`${tenantUrl}/tenant/currentTenant`, {
147583
+ headers: {
147584
+ Authorization: authorizationHeader,
147585
+ Accept: "application/json"
147586
+ },
147587
+ redirect: "error"
147588
+ });
147589
+ if (!res.ok) throw await platformError(res, "Login");
147590
+ return (await res.json()).name;
147591
+ }
147592
+ /**
147593
+ * Exchange user, password and a current TFA code for an OAI-Secure access
147594
+ * token via `POST /tenant/oauth/token`.
147595
+ * @param creds - Tenant URL, user, password, and the tenant ID when already known
147596
+ * @param tfaCode - Current TFA code (TOTP)
147597
+ */
147598
+ async function requestTfaSession(creds, tfaCode) {
147599
+ const url = new URL("/tenant/oauth/token", creds.tenantUrl);
147600
+ if (creds.tenantId) url.searchParams.set("tenant_id", creds.tenantId);
147601
+ const res = await fetch(url, {
147602
+ method: "POST",
147603
+ headers: {
147604
+ "Content-Type": "application/x-www-form-urlencoded",
147605
+ "Accept": "application/json"
147606
+ },
147607
+ body: new URLSearchParams({
147608
+ grant_type: "PASSWORD",
147609
+ username: creds.user,
147610
+ password: creds.password,
147611
+ tfa_code: tfaCode.trim()
147612
+ }),
147613
+ redirect: "error"
147614
+ });
147615
+ if (!res.ok) throw await platformError(res, "TFA login");
147616
+ const { access_token: token } = await res.json();
147617
+ if (!token) throw new Error("TFA login failed: the platform response contained no access token.");
147618
+ let exp;
147619
+ try {
147620
+ exp = JSON.parse(Buffer$1.from(token.split(".")[1] ?? "", "base64url").toString("utf8")).exp;
147621
+ } catch {}
147622
+ if (typeof exp !== "number" || !Number.isFinite(exp)) throw new Error("TFA login failed: could not read the expiry (exp claim) of the access token.");
147623
+ const expiresAt = exp * 1e3;
147624
+ if (expiresAt <= Date.now()) throw new Error(`TFA login failed: the platform issued an access token that already expired (${new Date(expiresAt).toISOString()}).`);
147625
+ return {
147626
+ token,
147627
+ expiresAt: Math.min(expiresAt, Date.now() + MAX_TFA_SESSION_MS)
147628
+ };
147629
+ }
147630
+ async function writeStoredC8yAuth(creds) {
147631
+ const jsonString = JSON.stringify(creds);
147632
+ const entry = new AsyncEntry(name, creds.tenantUrl);
147633
+ try {
147634
+ await entry.setPassword(jsonString);
147635
+ } catch (err) {
147636
+ throw new Error("Failed to store credentials", { cause: err });
147637
+ }
147638
+ }
147639
+ function parseStoredUserC8yAuth(jsonString, tenantUrl) {
147640
+ const cred = JSON.parse(jsonString);
147641
+ if (!cred.tenantId) throw new Error(`Stored credentials for tenant URL ${tenantUrl} are outdated. Remove and add them again with tenantId.`);
147642
+ return {
147643
+ tenantUrl,
147644
+ user: cred.user,
147645
+ password: cred.password,
147646
+ tenantId: cred.tenantId,
147647
+ ...cred.tfaSession ? { tfaSession: cred.tfaSession } : {}
147648
+ };
147649
+ }
147650
+ async function getStoredC8yAuth() {
147651
+ const found = await findCredentialsAsync(name);
147652
+ const creds = [];
147653
+ for (const entry of found) {
147654
+ const { account, password: jsonString } = entry;
147655
+ creds.push(parseStoredUserC8yAuth(jsonString, cleanTenantUrl(account)));
147656
+ }
147657
+ return creds;
147658
+ }
147659
+ async function getCredentialsByTenantUrl(tenantUrl) {
147660
+ const cleanedUrl = cleanTenantUrl(tenantUrl);
147661
+ const entry = (await findCredentialsAsync(name)).find((e) => cleanTenantUrl(e.account) === cleanedUrl);
147662
+ if (!entry) throw new Error(`No stored credentials found for tenant URL: ${cleanedUrl}`);
147663
+ return parseStoredUserC8yAuth(entry.password, cleanedUrl);
147664
+ }
147665
+ async function setStoredC8yAuth(creds) {
147666
+ const cleanedTenantUrl = cleanTenantUrl(creds.tenantUrl);
147667
+ await writeStoredC8yAuth({
147668
+ ...creds,
147669
+ tenantUrl: cleanedTenantUrl,
147670
+ tenantId: creds.tenantId ?? await resolveTenantId(cleanedTenantUrl, creds.tfaSession ? `Bearer ${creds.tfaSession.token}` : `Basic ${Buffer$1.from(`${creds.user}:${creds.password}`).toString("base64")}`)
147671
+ });
147672
+ }
147673
+ /**
147674
+ * Replace the stored TFA session for an existing credential entry.
147675
+ * @param creds - The stored credentials the new session belongs to
147676
+ * @param tfaSession - Freshly obtained session token
147677
+ */
147678
+ async function updateStoredTfaSession(creds, tfaSession) {
147679
+ const updated = {
147680
+ ...creds,
147681
+ tfaSession
147682
+ };
147683
+ await writeStoredC8yAuth(updated);
147684
+ return updated;
147685
+ }
147686
+ function cleanTenantUrl(url) {
147687
+ let cleaned = url.trim();
147688
+ const slashIndex = cleaned.indexOf("/", cleaned.indexOf("://") + 3);
147689
+ if (slashIndex !== -1) cleaned = cleaned.slice(0, slashIndex);
147690
+ if (cleaned.endsWith("/")) cleaned = cleaned.slice(0, -1);
147691
+ return cleaned;
147692
+ }
147693
+ async function deleteStoredC8yAuth(tenantUrl) {
147694
+ const cleanedUrl = cleanTenantUrl(tenantUrl);
147695
+ if (!(await findCredentialsAsync(name)).some((entry) => cleanTenantUrl(entry.account) === cleanedUrl)) return false;
147696
+ const entry = new AsyncEntry(name, cleanedUrl);
147697
+ let deleteError;
147698
+ try {
147699
+ await entry.deletePassword();
147700
+ } catch (err) {
147701
+ deleteError = err;
147702
+ }
147703
+ if (!(await findCredentialsAsync(name)).some((e) => cleanTenantUrl(e.account) === cleanedUrl)) return true;
147704
+ throw new Error(`Keyring refused to delete the credentials for ${cleanedUrl}. The entry is present when listing but cannot be removed by target — this is typically a libsecret / WSL2 locked-collection issue. Unlock the login keyring (e.g. \`gnome-keyring-daemon --unlock --components=secrets\`) and try again, or remove the entry manually with \`secret-tool clear service ${name} account ${cleanedUrl}\`.`, deleteError instanceof Error ? { cause: deleteError } : void 0);
147705
+ }
147627
147706
  runMain(defineCommand({
147628
147707
  meta: {
147629
147708
  name: `${name}-cli`,
@@ -147659,8 +147738,10 @@ runMain(defineCommand({
147659
147738
  setup: () => {
147660
147739
  globalThis._getCredentialsByTenantUrl = getCredentialsByTenantUrl;
147661
147740
  globalThis._getStoredC8yAuth = getStoredC8yAuth;
147741
+ globalThis._requestTfaSession = requestTfaSession;
147742
+ globalThis._updateStoredTfaSession = updateStoredTfaSession;
147662
147743
  },
147663
- subCommands: { creds: () => import("./creds-BQypJsxt.mjs").then((m) => m.default) },
147744
+ subCommands: { creds: () => import("./creds-DXlyJw6X.mjs").then((m) => m.default) },
147664
147745
  run: async ({ args }) => {
147665
147746
  const requested = (Array.isArray(args.spec) ? args.spec.at(-1) : args.spec) ?? getCoreOpenApiVersion();
147666
147747
  const selected = specs.find((e) => e.version === requested);
@@ -147684,11 +147765,13 @@ runMain(defineCommand({
147684
147765
  if (externalMcpServers.length > 0) consola.info(`External MCP namespaces: ${externalMcpServers.map((s) => `${s.name} (${s.url})`).join(", ")}`);
147685
147766
  const activeTenant = readActiveTenantUrl();
147686
147767
  if (activeTenant) try {
147687
- const tenantCtx = await setCliTenantContext(activeTenant);
147688
- const specKeys = Object.keys(tenantCtx.specs.specs);
147689
- const mcpKeys = Object.keys(tenantCtx.specs.mcpServers);
147690
- consola.info(`Active tenant: ${activeTenant}`);
147691
- consola.info(`Startup discovery complete — OpenAPI specs: ${specKeys.length > 0 ? specKeys.join(", ") : "none"}; MCP servers: ${mcpKeys.length > 0 ? mcpKeys.join(", ") : "none"}`);
147768
+ const tenantCtx = await restoreCliTenantContext(activeTenant);
147769
+ if (tenantCtx) {
147770
+ const specKeys = Object.keys(tenantCtx.specs.specs);
147771
+ const mcpKeys = Object.keys(tenantCtx.specs.mcpServers);
147772
+ consola.info(`Active tenant: ${activeTenant}`);
147773
+ consola.info(`Startup discovery complete — OpenAPI specs: ${specKeys.length > 0 ? specKeys.join(", ") : "none"}; MCP servers: ${mcpKeys.length > 0 ? mcpKeys.join(", ") : "none"}`);
147774
+ }
147692
147775
  } catch (err) {
147693
147776
  const message = err instanceof Error ? err.message : String(err);
147694
147777
  if (message.includes("No stored credentials found for tenant URL")) {
@@ -6,12 +6,12 @@ const command = defineCommand({
6
6
  description: "Manage your Cumulocity credentials"
7
7
  },
8
8
  subCommands: {
9
- add: () => import("./add-Bt0fMQY3.mjs").then((m) => m.default),
9
+ add: () => import("./add-Sf15gY0D.mjs").then((m) => m.default),
10
10
  remove: () => import("./remove-DeJzuKJK.mjs").then((m) => m.default),
11
11
  list: () => import("./list-Cr6LlrRt.mjs").then((m) => m.default)
12
12
  },
13
13
  run: async ({ rawArgs }) => {
14
- const command = await import("./add-Bt0fMQY3.mjs").then((m) => m.default);
14
+ const command = await import("./add-Sf15gY0D.mjs").then((m) => m.default);
15
15
  await runCommand(command, { rawArgs });
16
16
  }
17
17
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mc8yp",
3
- "version": "2.8.3",
3
+ "version": "2.8.4",
4
4
  "type": "module",
5
5
  "description": "Cumulocity IoT MCP Server - Model Context Protocol integration for IoT device management",
6
6
  "keywords": [