c8y-nitro 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autoBootstrap-Drk7ltXK.mjs +212 -0
- package/dist/{bootstrap-ObaEF7rN.mjs → bootstrap-C1Qly0J_.mjs} +2 -2
- package/dist/{c8y-api-BFjK6AYb.mjs → c8y-api-DscUs-Kd.mjs} +22 -18
- package/dist/cli/index.mjs +4 -4
- package/dist/client-D0Tdq7H1.mjs +280 -0
- package/dist/{index-ByDSMEvU.d.mts → index-vu4xaFkz.d.mts} +51 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +44 -183
- package/dist/logging-BUYT8Z2V.mjs +57 -0
- package/dist/options-CmLpZ3Qf.mjs +179 -0
- package/dist/{package-BhFLRPOb.mjs → package-BHOzdhHp.mjs} +1 -1
- package/dist/{roles-B58Fguop.mjs → roles-B-DNuScZ.mjs} +1 -1
- package/dist/runtime/handlers/invalidateTenantOptions.mjs +2 -215
- package/dist/runtime/handlers/openapi.mjs +57 -0
- package/dist/runtime/handlers/rewriteRequestUrl.mjs +38 -0
- package/dist/runtime/middlewares/c8y-client.mjs +1 -1
- package/dist/types.d.mts +2 -2
- package/dist/utils.d.mts +80 -3
- package/dist/utils.mjs +91 -2
- package/package.json +1 -1
- package/dist/logging-DYnYICoX.mjs +0 -3
- package/dist/options-B7_yb_wr.mjs +0 -147
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as C8yNitroModuleOptions } from "./index-
|
|
1
|
+
import { n as C8yNitroModuleOptions } from "./index-vu4xaFkz.mjs";
|
|
2
2
|
import { t as TenantCredentials } from "./credentials-eKnJ7m9Y.mjs";
|
|
3
3
|
import { NitroModule } from "nitro/types";
|
|
4
4
|
//#region src/index.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { _ as GENERATED_READINESS_ROUTE,
|
|
2
|
-
import { t as
|
|
3
|
-
import { n as name } from "./package-
|
|
1
|
+
import { _ as GENERATED_READINESS_ROUTE, g as GENERATED_LIVENESS_ROUTE, h as GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE, m as createC8yManifestFromNitro, p as createC8yManifest } from "./c8y-api-DscUs-Kd.mjs";
|
|
2
|
+
import { t as autoBootstrap } from "./autoBootstrap-Drk7ltXK.mjs";
|
|
3
|
+
import { n as name } from "./package-BHOzdhHp.mjs";
|
|
4
4
|
import { basename, dirname, join, relative } from "node:path";
|
|
5
|
-
import { mkdir,
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
6
|
import { x } from "tinyexec";
|
|
7
7
|
import { join as join$1 } from "pathe";
|
|
8
8
|
import { mkdir as mkdir$1, readFile as readFile$1, stat, writeFile as writeFile$1 } from "fs/promises";
|
|
@@ -11,8 +11,6 @@ import Spinnies from "spinnies";
|
|
|
11
11
|
import { colors } from "consola/utils";
|
|
12
12
|
import { mkdirSync, writeFileSync } from "fs";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { createHash } from "node:crypto";
|
|
15
|
-
import process from "node:process";
|
|
16
14
|
import evlog from "evlog/nitro/v3";
|
|
17
15
|
//#region src/module/apiClient.ts
|
|
18
16
|
/**
|
|
@@ -220,14 +218,15 @@ async function writeAPIClient(nitro, options) {
|
|
|
220
218
|
/**
|
|
221
219
|
* Generate the Dockerfile content for a Nitro build
|
|
222
220
|
* @param outputDirName - Name of the output directory (e.g., '.output')
|
|
221
|
+
* @param options - Dockerfile customization (base image, extra instructions)
|
|
223
222
|
* @returns Dockerfile content as a string
|
|
224
223
|
*/
|
|
225
|
-
function getDockerfileContent(outputDirName) {
|
|
226
|
-
return `FROM node:24-slim AS runtime
|
|
224
|
+
function getDockerfileContent(outputDirName, options = {}) {
|
|
225
|
+
return `FROM ${options.baseImage ?? "node:24-slim"} AS runtime
|
|
227
226
|
|
|
228
227
|
WORKDIR /app
|
|
229
228
|
|
|
230
|
-
# Copy the Nitro build output
|
|
229
|
+
${options.extraInstructions?.length ? `${options.extraInstructions.join("\n")}\n\n` : ""}# Copy the Nitro build output
|
|
231
230
|
COPY ${outputDirName}/ ${outputDirName}/
|
|
232
231
|
|
|
233
232
|
ENV NODE_ENV=production
|
|
@@ -248,12 +247,12 @@ async function checkDockerInstalled() {
|
|
|
248
247
|
return false;
|
|
249
248
|
}
|
|
250
249
|
}
|
|
251
|
-
async function writeDockerfile(outputDir) {
|
|
250
|
+
async function writeDockerfile(outputDir, options) {
|
|
252
251
|
const outputDirName = basename(outputDir);
|
|
253
252
|
const c8yDir = join(outputDir, "../.c8y");
|
|
254
253
|
const dockerfilePath = join(c8yDir, "Dockerfile");
|
|
255
254
|
await mkdir(c8yDir, { recursive: true });
|
|
256
|
-
await writeFile(dockerfilePath, getDockerfileContent(outputDirName), "utf-8");
|
|
255
|
+
await writeFile(dockerfilePath, getDockerfileContent(outputDirName, options), "utf-8");
|
|
257
256
|
return c8yDir;
|
|
258
257
|
}
|
|
259
258
|
async function buildDockerImage(nitro, c8yDir) {
|
|
@@ -302,12 +301,13 @@ async function saveDockerImageToTar(nitro, c8yDir, imageName) {
|
|
|
302
301
|
/**
|
|
303
302
|
* Create a Docker image from the Nitro build output
|
|
304
303
|
* @param nitro Nitro instance
|
|
304
|
+
* @param options Dockerfile customization (base image, extra instructions)
|
|
305
305
|
* @returns Path to the saved Docker image tar file
|
|
306
306
|
*/
|
|
307
|
-
async function createDockerImage(nitro) {
|
|
307
|
+
async function createDockerImage(nitro, options = {}) {
|
|
308
308
|
if (!await checkDockerInstalled()) throw new Error("Docker is not installed or not available in PATH. Please install Docker to build images.");
|
|
309
309
|
nitro.logger.debug("Creating Docker image...");
|
|
310
|
-
const c8yDir = await writeDockerfile(nitro.options.output.dir);
|
|
310
|
+
const c8yDir = await writeDockerfile(nitro.options.output.dir, options);
|
|
311
311
|
const imageTarPath = await saveDockerImageToTar(nitro, c8yDir, await buildDockerImage(nitro, c8yDir));
|
|
312
312
|
nitro.logger.debug("Docker image creation complete.");
|
|
313
313
|
return imageTarPath;
|
|
@@ -337,12 +337,12 @@ function formatBytes(bytes) {
|
|
|
337
337
|
function resolveZipOutputPath(rootDir, options, manifest) {
|
|
338
338
|
return join$1(join$1(rootDir, options.outputDir ?? "./"), typeof options.name === "function" ? options.name(manifest.name, manifest.version) : options.name ?? `${manifest.name}-${manifest.version}.zip`);
|
|
339
339
|
}
|
|
340
|
-
async function createC8yZip(nitro, options = {}) {
|
|
340
|
+
async function createC8yZip(nitro, options = {}, dockerOptions = {}) {
|
|
341
341
|
const startTime = Date.now();
|
|
342
342
|
const spinnerName = "c8y-zip";
|
|
343
343
|
spinnies.add(spinnerName, { text: "Creating Dockerfile..." });
|
|
344
344
|
spinnies.update(spinnerName, { text: "Building Docker image..." });
|
|
345
|
-
const imageTarPath = await createDockerImage(nitro);
|
|
345
|
+
const imageTarPath = await createDockerImage(nitro, dockerOptions);
|
|
346
346
|
spinnies.update(spinnerName, { text: "Creating manifest..." });
|
|
347
347
|
const manifest = await createC8yManifestFromNitro(nitro);
|
|
348
348
|
spinnies.update(spinnerName, { text: "Reading image.tar..." });
|
|
@@ -421,6 +421,9 @@ async function setupRuntimeConfig(nitro, options) {
|
|
|
421
421
|
nitro.options.runtimeConfig.c8yTenantOptionsPerKeyTTL = options.cache?.tenantOptions ?? {};
|
|
422
422
|
const manifest = await createC8yManifest(nitro.options.rootDir, nitro.options, nitro.logger);
|
|
423
423
|
nitro.options.runtimeConfig.c8ySettingsCategory = options.manifest?.settingsCategory ?? manifest.contextPath ?? manifest.name;
|
|
424
|
+
const excludeInternalRoutes = options.openapi?.excludeInternalRoutes ?? true;
|
|
425
|
+
const probePaths = [options.manifest?.livenessProbe?.httpGet?.path, options.manifest?.readinessProbe?.httpGet?.path].filter((path) => Boolean(path));
|
|
426
|
+
nitro.options.runtimeConfig.c8yOpenApiExcludeRoutes = [...excludeInternalRoutes ? ["/_", ...probePaths] : [], ...options.openapi?.excludeRoutes ?? []];
|
|
424
427
|
}
|
|
425
428
|
//#endregion
|
|
426
429
|
//#region src/module/registerRuntime.ts
|
|
@@ -461,6 +464,31 @@ function registerRuntime(nitro, options = {}) {
|
|
|
461
464
|
middleware: true
|
|
462
465
|
})));
|
|
463
466
|
/**
|
|
467
|
+
* Request URL rewrite middleware (always on in production builds).
|
|
468
|
+
* Rewrites `event.url` to the public tenant endpoint so handlers that build
|
|
469
|
+
* absolute URLs from the request don't leak internal cluster URLs.
|
|
470
|
+
* Registered after the scanned middlewares (which observe the original
|
|
471
|
+
* cluster-local URL) and before the OpenAPI transform, which reuses the
|
|
472
|
+
* resolved tenant domain from its cache.
|
|
473
|
+
*/
|
|
474
|
+
if (!isNitroDev) {
|
|
475
|
+
nitro.options.handlers.push({
|
|
476
|
+
route: "/**",
|
|
477
|
+
handler: join$1(thisFilePath, "./runtime/handlers/rewriteRequestUrl"),
|
|
478
|
+
middleware: true
|
|
479
|
+
});
|
|
480
|
+
nitro.logger.debug("Registered public request URL rewrite middleware");
|
|
481
|
+
}
|
|
482
|
+
if (Boolean(nitro.options.experimental?.openAPI && (isNitroDev || nitro.options.openAPI?.production))) if (!isNitroDev && nitro.options.openAPI?.production === "prerender") nitro.logger.warn("c8y-nitro cannot transform a prerendered OpenAPI document (internal routes stay listed and the server URL is baked in at build time). Use `openAPI: { production: true }` to serve the document at runtime instead.");
|
|
483
|
+
else {
|
|
484
|
+
nitro.options.handlers.push({
|
|
485
|
+
route: "/**",
|
|
486
|
+
handler: join$1(thisFilePath, "./runtime/handlers/openapi"),
|
|
487
|
+
middleware: true
|
|
488
|
+
});
|
|
489
|
+
nitro.logger.debug(`Registered OpenAPI transform middleware for ${nitro.options.openAPI?.route || "/_openapi.json"}`);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
464
492
|
* Handlers (can't be auto scanned as they need methods etc)
|
|
465
493
|
*/
|
|
466
494
|
const handlers = [];
|
|
@@ -521,173 +549,6 @@ function checkProbeEndpoint(nitro, probe, probeType) {
|
|
|
521
549
|
}
|
|
522
550
|
}
|
|
523
551
|
//#endregion
|
|
524
|
-
//#region src/module/manifestCache.ts
|
|
525
|
-
/**
|
|
526
|
-
* Directory (inside the project's `node_modules`) where c8y-nitro persists
|
|
527
|
-
* local dev state such as the last-synced manifest hash. Lives in
|
|
528
|
-
* `node_modules` so it is naturally gitignored and scoped per-checkout.
|
|
529
|
-
*/
|
|
530
|
-
const CACHE_DIR_SEGMENTS = ["node_modules", ".c8y-nitro"];
|
|
531
|
-
const CACHE_FILE_NAME = "bootstrap-state.json";
|
|
532
|
-
const CACHE_SCHEMA_VERSION = 1;
|
|
533
|
-
function getCacheDir(rootDir) {
|
|
534
|
-
return join(rootDir, ...CACHE_DIR_SEGMENTS);
|
|
535
|
-
}
|
|
536
|
-
function getCacheFile(rootDir) {
|
|
537
|
-
return join(getCacheDir(rootDir), CACHE_FILE_NAME);
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Compute a hash over the manifest and the deployment target it will be synced
|
|
541
|
-
* to. Folding the target in means switching tenant/baseURL invalidates the
|
|
542
|
-
* cached hash even if the manifest itself is unchanged.
|
|
543
|
-
*
|
|
544
|
-
* @param manifest - The generated Cumulocity manifest
|
|
545
|
-
* @param target - The deployment target identity
|
|
546
|
-
*/
|
|
547
|
-
function hashManifest(manifest, target) {
|
|
548
|
-
const payload = JSON.stringify({
|
|
549
|
-
manifest,
|
|
550
|
-
target
|
|
551
|
-
});
|
|
552
|
-
return createHash("sha256").update(payload).digest("hex");
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* Read the persisted bootstrap state, or `null` when absent, unreadable, or
|
|
556
|
-
* from an incompatible schema version.
|
|
557
|
-
* @param rootDir - Project root directory (containing `node_modules`)
|
|
558
|
-
*/
|
|
559
|
-
async function readManifestCache(rootDir) {
|
|
560
|
-
try {
|
|
561
|
-
const content = await readFile(getCacheFile(rootDir), "utf-8");
|
|
562
|
-
const state = JSON.parse(content);
|
|
563
|
-
if (state.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
|
|
564
|
-
return state;
|
|
565
|
-
} catch {
|
|
566
|
-
return null;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Persist the bootstrap state for future dev restarts.
|
|
571
|
-
* @param rootDir - Project root directory (containing `node_modules`)
|
|
572
|
-
* @param state - The state to persist (schema version is filled in automatically)
|
|
573
|
-
*/
|
|
574
|
-
async function writeManifestCache(rootDir, state) {
|
|
575
|
-
await mkdir(getCacheDir(rootDir), { recursive: true });
|
|
576
|
-
const payload = {
|
|
577
|
-
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
578
|
-
...state
|
|
579
|
-
};
|
|
580
|
-
await writeFile(getCacheFile(rootDir), JSON.stringify(payload, null, 2), "utf-8");
|
|
581
|
-
}
|
|
582
|
-
//#endregion
|
|
583
|
-
//#region src/module/autoBootstrap.ts
|
|
584
|
-
const BOOTSTRAP_ENV_VARS = [
|
|
585
|
-
"C8Y_BOOTSTRAP_TENANT",
|
|
586
|
-
"C8Y_BOOTSTRAP_USER",
|
|
587
|
-
"C8Y_BOOTSTRAP_PASSWORD"
|
|
588
|
-
];
|
|
589
|
-
const DEV_ENV_VARS = [
|
|
590
|
-
"C8Y_BASEURL",
|
|
591
|
-
"C8Y_DEVELOPMENT_TENANT",
|
|
592
|
-
"C8Y_DEVELOPMENT_USER",
|
|
593
|
-
"C8Y_DEVELOPMENT_PASSWORD"
|
|
594
|
-
];
|
|
595
|
-
/**
|
|
596
|
-
* Verifies the microservice on the development tenant and keeps it in sync with
|
|
597
|
-
* the local manifest, bootstrapping (or re-bootstrapping) when needed.
|
|
598
|
-
*
|
|
599
|
-
* Unlike a purely env-var-based check, this always verifies against the tenant
|
|
600
|
-
* when development credentials are available, so it also handles:
|
|
601
|
-
* - the manifest changing between dev restarts (auto-updates a placeholder), and
|
|
602
|
-
* - the application being deleted on the tenant while stale bootstrap env vars
|
|
603
|
-
* still linger locally (re-creates it).
|
|
604
|
-
*
|
|
605
|
-
* A previously *deployed* microservice (one with an active version) is never
|
|
606
|
-
* overwritten automatically - we only warn about the drift.
|
|
607
|
-
*
|
|
608
|
-
* Runs silently unless a bootstrap/sync was performed or an error occurs.
|
|
609
|
-
* @param nitro - Nitro instance
|
|
610
|
-
*/
|
|
611
|
-
async function autoBootstrap(nitro) {
|
|
612
|
-
try {
|
|
613
|
-
const rootDir = nitro.options.rootDir;
|
|
614
|
-
if (DEV_ENV_VARS.filter((v) => !process.env[v]).length > 0) return;
|
|
615
|
-
const baseUrl = process.env.C8Y_BASEURL.endsWith("/") ? process.env.C8Y_BASEURL.slice(0, -1) : process.env.C8Y_BASEURL;
|
|
616
|
-
const developmentTenant = process.env.C8Y_DEVELOPMENT_TENANT;
|
|
617
|
-
const authHeader = createBasicAuthHeader(developmentTenant, process.env.C8Y_DEVELOPMENT_USER, process.env.C8Y_DEVELOPMENT_PASSWORD);
|
|
618
|
-
const manifest = await createC8yManifest(rootDir, nitro.options, nitro.logger);
|
|
619
|
-
const target = {
|
|
620
|
-
baseUrl,
|
|
621
|
-
developmentTenant,
|
|
622
|
-
name: manifest.name
|
|
623
|
-
};
|
|
624
|
-
const currentHash = hashManifest(manifest, target);
|
|
625
|
-
const existingApp = await findMicroserviceByName(baseUrl, manifest.name, authHeader);
|
|
626
|
-
if (!existingApp) {
|
|
627
|
-
const createdApp = await createMicroservice(baseUrl, manifest, authHeader);
|
|
628
|
-
nitro.logger.debug(`Microservice "${manifest.name}" created (ID: ${createdApp.id})`);
|
|
629
|
-
await finalizeBootstrap(nitro, {
|
|
630
|
-
baseUrl,
|
|
631
|
-
developmentTenant,
|
|
632
|
-
appId: createdApp.id,
|
|
633
|
-
authHeader,
|
|
634
|
-
target,
|
|
635
|
-
manifestHash: currentHash,
|
|
636
|
-
forceCredentials: true
|
|
637
|
-
});
|
|
638
|
-
nitro.logger.success(`Auto-bootstrap complete! Microservice "${manifest.name}" created on tenant.`);
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
const appId = existingApp.id;
|
|
642
|
-
const isRealService = Boolean(existingApp.activeVersionId);
|
|
643
|
-
const cache = await readManifestCache(rootDir);
|
|
644
|
-
if (!cache || cache.manifestHash !== currentHash || cache.appId !== appId) if (isRealService) nitro.logger.warn(`Local manifest for "${manifest.name}" differs from the deployed microservice on the development tenant. The running service was left untouched. Redeploy or run \`npx c8y-nitro bootstrap\` to update it explicitly.`);
|
|
645
|
-
else {
|
|
646
|
-
await updateMicroservice(baseUrl, appId, manifest, authHeader);
|
|
647
|
-
nitro.logger.success(`Auto-bootstrap: updated microservice "${manifest.name}" with changed manifest.`);
|
|
648
|
-
}
|
|
649
|
-
const hasBootstrapCreds = BOOTSTRAP_ENV_VARS.every((v) => process.env[v]);
|
|
650
|
-
const appChanged = cache?.appId !== appId;
|
|
651
|
-
await finalizeBootstrap(nitro, {
|
|
652
|
-
baseUrl,
|
|
653
|
-
developmentTenant,
|
|
654
|
-
appId,
|
|
655
|
-
authHeader,
|
|
656
|
-
target,
|
|
657
|
-
manifestHash: currentHash,
|
|
658
|
-
forceCredentials: !hasBootstrapCreds || appChanged
|
|
659
|
-
});
|
|
660
|
-
} catch (error) {
|
|
661
|
-
nitro.logger.warn("Auto-bootstrap failed:", error instanceof Error ? error.message : String(error));
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
/**
|
|
665
|
-
* Subscribe the tenant, optionally (re)write bootstrap credentials, and persist
|
|
666
|
-
* the synced state to the local cache.
|
|
667
|
-
* @param nitro
|
|
668
|
-
* @param opts
|
|
669
|
-
*/
|
|
670
|
-
async function finalizeBootstrap(nitro, opts) {
|
|
671
|
-
await subscribeToApplication(opts.baseUrl, opts.developmentTenant, opts.appId, opts.authHeader);
|
|
672
|
-
if (opts.forceCredentials) {
|
|
673
|
-
const credentials = await getBootstrapCredentials(opts.baseUrl, opts.appId, opts.authHeader);
|
|
674
|
-
const envFileName = await writeBootstrapCredentials(nitro.options.rootDir, {
|
|
675
|
-
C8Y_BOOTSTRAP_TENANT: credentials.tenant,
|
|
676
|
-
C8Y_BOOTSTRAP_USER: credentials.name,
|
|
677
|
-
C8Y_BOOTSTRAP_PASSWORD: credentials.password
|
|
678
|
-
});
|
|
679
|
-
process.env.C8Y_BOOTSTRAP_TENANT = credentials.tenant;
|
|
680
|
-
process.env.C8Y_BOOTSTRAP_USER = credentials.name;
|
|
681
|
-
process.env.C8Y_BOOTSTRAP_PASSWORD = credentials.password;
|
|
682
|
-
nitro.logger.debug(`Bootstrap credentials written to ${envFileName}`);
|
|
683
|
-
}
|
|
684
|
-
await writeManifestCache(nitro.options.rootDir, {
|
|
685
|
-
manifestHash: opts.manifestHash,
|
|
686
|
-
target: opts.target,
|
|
687
|
-
appId: opts.appId
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
//#endregion
|
|
691
552
|
//#region src/index.ts
|
|
692
553
|
function c8y() {
|
|
693
554
|
return {
|
|
@@ -747,7 +608,7 @@ function c8y() {
|
|
|
747
608
|
}
|
|
748
609
|
});
|
|
749
610
|
nitro.hooks.hook("close", async () => {
|
|
750
|
-
if (nitro.options.preset !== "nitro-dev") await createC8yZip(nitro, options.zip);
|
|
611
|
+
if (nitro.options.preset !== "nitro-dev") await createC8yZip(nitro, options.zip, options.docker);
|
|
751
612
|
});
|
|
752
613
|
}
|
|
753
614
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createError } from "evlog";
|
|
2
|
+
import "evlog/nitro/v3";
|
|
3
|
+
//#region src/utils/logging.ts
|
|
4
|
+
/**
|
|
5
|
+
* Create a structured error for a Cumulocity microservice. Always prefer this
|
|
6
|
+
* over Nitro/h3's built-in `createError`: the returned error is captured in the
|
|
7
|
+
* wide log event **and** serialized into the JSON response under a `data` key.
|
|
8
|
+
*
|
|
9
|
+
* ## What the caller sees vs what stays on the server
|
|
10
|
+
*
|
|
11
|
+
* Every field **except `internal`** is sent to the client in the HTTP response.
|
|
12
|
+
* `internal` is stripped from the response and only ever appears in your logs.
|
|
13
|
+
*
|
|
14
|
+
* | Field | Sent to client? | Purpose |
|
|
15
|
+
* | ---------- | :-------------: | ------------------------------------------------------------- |
|
|
16
|
+
* | `message` | yes | Short, safe summary of what went wrong. |
|
|
17
|
+
* | `status` | yes | HTTP status code (default `500`). |
|
|
18
|
+
* | `code` | yes | Stable machine-readable id (e.g. `'PAYMENT_DECLINED'`). |
|
|
19
|
+
* | `why` | yes | Human-readable cause — safe to expose, no secrets. |
|
|
20
|
+
* | `fix` | yes | What the caller can do about it. |
|
|
21
|
+
* | `link` | yes | Docs URL with more detail. |
|
|
22
|
+
* | `cause` | no (logged) | The original `Error` you caught. |
|
|
23
|
+
* | `internal` | no (logged) | Backend-only diagnostics — never reaches the client. |
|
|
24
|
+
*
|
|
25
|
+
* ## Do NOT leak internal system errors
|
|
26
|
+
*
|
|
27
|
+
* Put anything sensitive or diagnostic (raw Cumulocity core responses, DB
|
|
28
|
+
* errors, tokens, tenant internals) in `internal`, **not** in `message`/`why`.
|
|
29
|
+
* `internal` is logged for debugging but never serialized to the response.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // User-facing error — every field here is visible to the caller:
|
|
33
|
+
* throw createError({
|
|
34
|
+
* code: 'PAYMENT_DECLINED',
|
|
35
|
+
* message: 'Payment failed',
|
|
36
|
+
* status: 402,
|
|
37
|
+
* why: 'Card declined by issuer',
|
|
38
|
+
* fix: 'Try a different payment method',
|
|
39
|
+
* link: 'https://docs.example.com/payments/declined',
|
|
40
|
+
* })
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // Wrapping an upstream failure — keep the raw payload server-side only:
|
|
44
|
+
* catch (cause) {
|
|
45
|
+
* throw createError({
|
|
46
|
+
* status: 502,
|
|
47
|
+
* message: 'Upstream request failed', // safe, generic — what the caller sees
|
|
48
|
+
* cause: cause as Error,
|
|
49
|
+
* internal: { upstreamStatus: cause?.res?.status, upstreamBody: cause?.data },
|
|
50
|
+
* })
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* @see https://www.evlog.dev
|
|
54
|
+
*/
|
|
55
|
+
const createError$1 = createError;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { createError$1 as t };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { c as getTenantOptionsByCategory, f as upsertTenantOption, i as deleteTenantOption, n as createBasicAuthHeader, p as createC8yManifest, s as getCurrentApplicationSettings } from "./c8y-api-DscUs-Kd.mjs";
|
|
2
|
+
import { n as readBootstrapCredentialsFromEnv, r as syncBootstrappedMicroservice } from "./autoBootstrap-Drk7ltXK.mjs";
|
|
3
|
+
import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
import { consola } from "consola";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
//#region src/cli/commands/options.ts
|
|
9
|
+
var options_default = defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: "options",
|
|
12
|
+
description: "Manage tenant options on the development tenant"
|
|
13
|
+
},
|
|
14
|
+
args: {},
|
|
15
|
+
async run() {
|
|
16
|
+
consola.info("Loading configuration...");
|
|
17
|
+
const { env, nitroConfig, configDir } = await loadC8yConfig();
|
|
18
|
+
consola.info("Validating environment variables...");
|
|
19
|
+
const envVars = validateBootstrapEnv(env);
|
|
20
|
+
consola.info("Loading manifest...");
|
|
21
|
+
const manifest = await createC8yManifest(configDir, nitroConfig);
|
|
22
|
+
const category = manifest.settingsCategory || manifest.contextPath || manifest.name;
|
|
23
|
+
if (!manifest.settings || manifest.settings.length === 0) throw new Error("No settings defined in manifest. Add settings to your c8y.manifest configuration.");
|
|
24
|
+
consola.success(`Using category: ${category}`);
|
|
25
|
+
const devAuthHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
|
|
26
|
+
let bootstrapCredentials;
|
|
27
|
+
try {
|
|
28
|
+
consola.info("Checking microservice on development tenant...");
|
|
29
|
+
bootstrapCredentials = await syncBootstrappedMicroservice({
|
|
30
|
+
rootDir: configDir,
|
|
31
|
+
baseUrl: envVars.C8Y_BASEURL,
|
|
32
|
+
developmentTenant: envVars.C8Y_DEVELOPMENT_TENANT,
|
|
33
|
+
authHeader: devAuthHeader,
|
|
34
|
+
manifest,
|
|
35
|
+
logger: consola,
|
|
36
|
+
env
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
consola.warn(`Could not sync microservice with the development tenant: ${error instanceof Error ? error.message : String(error)}`);
|
|
40
|
+
bootstrapCredentials = readBootstrapCredentialsFromEnv(env);
|
|
41
|
+
}
|
|
42
|
+
const bootstrapAuthHeader = bootstrapCredentials ? createBasicAuthHeader(bootstrapCredentials.tenant, bootstrapCredentials.name, bootstrapCredentials.password) : void 0;
|
|
43
|
+
if (!bootstrapAuthHeader) consola.warn("No bootstrap credentials available - reading with the development user. Encrypted credentials.* options cannot be decrypted.");
|
|
44
|
+
consola.info("Fetching current tenant options...");
|
|
45
|
+
const currentOptions = await getTenantOptionsByCategory(envVars.C8Y_BASEURL, category, devAuthHeader);
|
|
46
|
+
if (bootstrapAuthHeader) try {
|
|
47
|
+
const decryptedSettings = await getCurrentApplicationSettings(envVars.C8Y_BASEURL, bootstrapAuthHeader);
|
|
48
|
+
for (const [key, value] of Object.entries(decryptedSettings)) if (currentOptions[key] !== void 0 || key.startsWith("credentials.")) currentOptions[key] = value;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
consola.warn(`Could not read decrypted settings with the bootstrap user: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
}
|
|
52
|
+
const availableKeys = manifest.settings.map((s) => s.key);
|
|
53
|
+
const setManifestKeys = availableKeys.filter((k) => currentOptions[k] !== void 0);
|
|
54
|
+
consola.success(`Found ${Object.keys(currentOptions).length} options set on tenant (${setManifestKeys.length} of ${availableKeys.length} manifest settings)`);
|
|
55
|
+
const ctx = {
|
|
56
|
+
baseUrl: envVars.C8Y_BASEURL,
|
|
57
|
+
category,
|
|
58
|
+
devAuthHeader,
|
|
59
|
+
bootstrapAuthHeader,
|
|
60
|
+
availableKeys,
|
|
61
|
+
currentOptions
|
|
62
|
+
};
|
|
63
|
+
const action = await consola.prompt("What do you want to do?", {
|
|
64
|
+
type: "select",
|
|
65
|
+
options: [
|
|
66
|
+
{
|
|
67
|
+
label: "Read option value",
|
|
68
|
+
value: "read"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: "Update/Create option",
|
|
72
|
+
value: "update"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "Delete option(s)",
|
|
76
|
+
value: "delete"
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
});
|
|
80
|
+
if (action === "read") await handleRead(ctx);
|
|
81
|
+
else if (action === "update") await handleUpdate(ctx);
|
|
82
|
+
else if (action === "delete") await handleDelete(ctx);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Handle reading options: prints all options at once (decrypted where
|
|
87
|
+
* possible), then - after the user confirms with Enter - rewrites the output
|
|
88
|
+
* in place with `credentials.*` values masked, so secrets don't linger in the
|
|
89
|
+
* terminal scrollback.
|
|
90
|
+
* @param ctx - The shared options context
|
|
91
|
+
*/
|
|
92
|
+
async function handleRead(ctx) {
|
|
93
|
+
const { bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
|
|
94
|
+
if (!bootstrapAuthHeader && availableKeys.some((k) => k.startsWith("credentials."))) consola.warn("Encrypted credentials.* options cannot be read in decrypted form without bootstrap credentials. Run `npx c8y-nitro bootstrap` first.");
|
|
95
|
+
const keys = [.../* @__PURE__ */ new Set([...availableKeys, ...Object.keys(currentOptions)])];
|
|
96
|
+
const padWidth = Math.max(...keys.map((k) => k.length));
|
|
97
|
+
const renderLine = (key, maskCredentials) => {
|
|
98
|
+
const value = currentOptions[key];
|
|
99
|
+
const shownValue = value === void 0 ? "(not set)" : maskCredentials && key.startsWith("credentials.") ? "****" : value;
|
|
100
|
+
const suffix = availableKeys.includes(key) ? "" : " (not in manifest)";
|
|
101
|
+
return ` ${key.padEnd(padWidth)} = ${shownValue}${suffix}`;
|
|
102
|
+
};
|
|
103
|
+
const revealedLines = keys.map((key) => renderLine(key, false));
|
|
104
|
+
process.stdout.write(`${revealedLines.join("\n")}\n`);
|
|
105
|
+
const rl = createInterface({
|
|
106
|
+
input: process.stdin,
|
|
107
|
+
output: process.stdout
|
|
108
|
+
});
|
|
109
|
+
await rl.question("Press <Enter> to hide credential values...");
|
|
110
|
+
rl.close();
|
|
111
|
+
if (process.stdout.isTTY) {
|
|
112
|
+
const columns = process.stdout.columns || 80;
|
|
113
|
+
const rowsFor = (line) => Math.max(1, Math.ceil(line.length / columns));
|
|
114
|
+
const rowsToErase = revealedLines.reduce((rows, line) => rows + rowsFor(line), 1);
|
|
115
|
+
process.stdout.write(`\x1B[${rowsToErase}A\x1B[0J`);
|
|
116
|
+
process.stdout.write(`${keys.map((key) => renderLine(key, true)).join("\n")}\n`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Handle updating options (with loop for multiple updates)
|
|
121
|
+
* @param ctx - The shared options context
|
|
122
|
+
*/
|
|
123
|
+
async function handleUpdate(ctx) {
|
|
124
|
+
const { baseUrl, category, devAuthHeader, bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
|
|
125
|
+
let continueUpdating = true;
|
|
126
|
+
while (continueUpdating) {
|
|
127
|
+
const key = await consola.prompt("Select option to update:", {
|
|
128
|
+
type: "select",
|
|
129
|
+
options: availableKeys.map((k) => ({
|
|
130
|
+
label: currentOptions[k] !== void 0 ? `${k} (current: ${currentOptions[k]})` : k.startsWith("credentials.") && !bootstrapAuthHeader ? `${k} (current: unknown)` : `${k} (not set)`,
|
|
131
|
+
value: k
|
|
132
|
+
})),
|
|
133
|
+
cancel: "reject"
|
|
134
|
+
});
|
|
135
|
+
const currentValue = currentOptions[key];
|
|
136
|
+
const newValue = await consola.prompt("Enter new value:", {
|
|
137
|
+
type: "text",
|
|
138
|
+
default: currentValue,
|
|
139
|
+
cancel: "reject"
|
|
140
|
+
});
|
|
141
|
+
consola.info(`Updating option: ${key}`);
|
|
142
|
+
await upsertTenantOption(baseUrl, category, key, newValue, devAuthHeader);
|
|
143
|
+
currentOptions[key] = newValue;
|
|
144
|
+
consola.success(`Option '${key}' updated successfully`);
|
|
145
|
+
if (!await consola.prompt("Update another option?", {
|
|
146
|
+
type: "confirm",
|
|
147
|
+
initial: false,
|
|
148
|
+
cancel: "reject"
|
|
149
|
+
})) continueUpdating = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Handle deleting multiple options
|
|
154
|
+
* @param ctx - The shared options context
|
|
155
|
+
*/
|
|
156
|
+
async function handleDelete(ctx) {
|
|
157
|
+
const { baseUrl, category, devAuthHeader, bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
|
|
158
|
+
const allKeys = bootstrapAuthHeader ? availableKeys.filter((k) => currentOptions[k] !== void 0) : [...availableKeys.filter((k) => !k.startsWith("credentials.") && currentOptions[k] !== void 0), ...availableKeys.filter((k) => k.startsWith("credentials."))];
|
|
159
|
+
if (allKeys.length === 0) {
|
|
160
|
+
consola.warn("No options are currently set");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const keysToDelete = await consola.prompt("Select option(s) to delete:", {
|
|
164
|
+
type: "multiselect",
|
|
165
|
+
options: allKeys,
|
|
166
|
+
required: true,
|
|
167
|
+
cancel: "reject"
|
|
168
|
+
});
|
|
169
|
+
consola.info(`Deleting ${keysToDelete.length} option(s)...`);
|
|
170
|
+
for (const key of keysToDelete) {
|
|
171
|
+
consola.info(`Deleting option: ${key}`);
|
|
172
|
+
await deleteTenantOption(baseUrl, category, key, devAuthHeader);
|
|
173
|
+
delete currentOptions[key];
|
|
174
|
+
consola.success(`✓ Deleted: ${key}`);
|
|
175
|
+
}
|
|
176
|
+
consola.success("Delete operation completed");
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
export { options_default as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as createBasicAuthHeader, p as createC8yManifest, t as assignUserRole, u as unassignUserRole } from "./c8y-api-
|
|
1
|
+
import { n as createBasicAuthHeader, p as createC8yManifest, t as assignUserRole, u as unassignUserRole } from "./c8y-api-DscUs-Kd.mjs";
|
|
2
2
|
import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { consola } from "consola";
|