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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
127503
|
-
*
|
|
127504
|
-
*
|
|
127505
|
-
*
|
|
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
|
|
127508
|
-
*
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
147604
|
-
|
|
147605
|
-
|
|
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-
|
|
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
|
|
147688
|
-
|
|
147689
|
-
|
|
147690
|
-
|
|
147691
|
-
|
|
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-
|
|
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-
|
|
14
|
+
const command = await import("./add-Sf15gY0D.mjs").then((m) => m.default);
|
|
15
15
|
await runCommand(command, { rawArgs });
|
|
16
16
|
}
|
|
17
17
|
});
|