c8y-nitro 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,212 @@
1
+ import { a as findMicroserviceByName, d as updateMicroservice, l as subscribeToApplication, n as createBasicAuthHeader, o as getBootstrapCredentials, p as createC8yManifest, r as createMicroservice } from "./c8y-api-DscUs-Kd.mjs";
2
+ import { t as writeBootstrapCredentials } from "./env-file-BrOTmVjA.mjs";
3
+ import { join } from "node:path";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { createHash } from "node:crypto";
6
+ import process from "node:process";
7
+ //#region src/module/manifestCache.ts
8
+ /**
9
+ * Directory (inside the project's `node_modules`) where c8y-nitro persists
10
+ * local dev state such as the last-synced manifest hash. Lives in
11
+ * `node_modules` so it is naturally gitignored and scoped per-checkout.
12
+ */
13
+ const CACHE_DIR_SEGMENTS = ["node_modules", ".c8y-nitro"];
14
+ const CACHE_FILE_NAME = "bootstrap-state.json";
15
+ const CACHE_SCHEMA_VERSION = 1;
16
+ function getCacheDir(rootDir) {
17
+ return join(rootDir, ...CACHE_DIR_SEGMENTS);
18
+ }
19
+ function getCacheFile(rootDir) {
20
+ return join(getCacheDir(rootDir), CACHE_FILE_NAME);
21
+ }
22
+ /**
23
+ * Compute a hash over the manifest and the deployment target it will be synced
24
+ * to. Folding the target in means switching tenant/baseURL invalidates the
25
+ * cached hash even if the manifest itself is unchanged.
26
+ *
27
+ * @param manifest - The generated Cumulocity manifest
28
+ * @param target - The deployment target identity
29
+ */
30
+ function hashManifest(manifest, target) {
31
+ const payload = JSON.stringify({
32
+ manifest,
33
+ target
34
+ });
35
+ return createHash("sha256").update(payload).digest("hex");
36
+ }
37
+ /**
38
+ * Read the persisted bootstrap state, or `null` when absent, unreadable, or
39
+ * from an incompatible schema version.
40
+ * @param rootDir - Project root directory (containing `node_modules`)
41
+ */
42
+ async function readManifestCache(rootDir) {
43
+ try {
44
+ const content = await readFile(getCacheFile(rootDir), "utf-8");
45
+ const state = JSON.parse(content);
46
+ if (state.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
47
+ return state;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * Persist the bootstrap state for future dev restarts.
54
+ * @param rootDir - Project root directory (containing `node_modules`)
55
+ * @param state - The state to persist (schema version is filled in automatically)
56
+ */
57
+ async function writeManifestCache(rootDir, state) {
58
+ await mkdir(getCacheDir(rootDir), { recursive: true });
59
+ const payload = {
60
+ schemaVersion: CACHE_SCHEMA_VERSION,
61
+ ...state
62
+ };
63
+ await writeFile(getCacheFile(rootDir), JSON.stringify(payload, null, 2), "utf-8");
64
+ }
65
+ //#endregion
66
+ //#region src/module/autoBootstrap.ts
67
+ const BOOTSTRAP_ENV_VARS = [
68
+ "C8Y_BOOTSTRAP_TENANT",
69
+ "C8Y_BOOTSTRAP_USER",
70
+ "C8Y_BOOTSTRAP_PASSWORD"
71
+ ];
72
+ const DEV_ENV_VARS = [
73
+ "C8Y_BASEURL",
74
+ "C8Y_DEVELOPMENT_TENANT",
75
+ "C8Y_DEVELOPMENT_USER",
76
+ "C8Y_DEVELOPMENT_PASSWORD"
77
+ ];
78
+ /**
79
+ * Reads bootstrap credentials from an env object (e.g. a loaded `.env` file or
80
+ * `process.env`). Returns `undefined` unless all three variables are present.
81
+ * @param env - Environment variables object
82
+ */
83
+ function readBootstrapCredentialsFromEnv(env) {
84
+ const tenant = env.C8Y_BOOTSTRAP_TENANT;
85
+ const name = env.C8Y_BOOTSTRAP_USER;
86
+ const password = env.C8Y_BOOTSTRAP_PASSWORD;
87
+ if (!tenant || !name || !password) return;
88
+ return {
89
+ tenant,
90
+ name,
91
+ password
92
+ };
93
+ }
94
+ /**
95
+ * Verifies the microservice on the development tenant and keeps it in sync with
96
+ * the local manifest, bootstrapping (or re-bootstrapping) when needed.
97
+ *
98
+ * Unlike a purely env-var-based check, this always verifies against the tenant,
99
+ * so it also handles:
100
+ * - the manifest changing between runs (auto-updates a placeholder), and
101
+ * - the application being deleted on the tenant while stale bootstrap env vars
102
+ * still linger locally (re-creates it).
103
+ *
104
+ * A previously *deployed* microservice (one with an active version) is never
105
+ * overwritten automatically - we only warn about the drift.
106
+ *
107
+ * @param options - See {@link SyncBootstrappedMicroserviceOptions}
108
+ * @returns The bootstrap credentials for the microservice (freshly fetched when
109
+ * they were refreshed, otherwise the ones already present in `env`)
110
+ */
111
+ async function syncBootstrappedMicroservice(options) {
112
+ const { rootDir, baseUrl, developmentTenant, authHeader, manifest, logger } = options;
113
+ const env = options.env ?? process.env;
114
+ const target = {
115
+ baseUrl,
116
+ developmentTenant,
117
+ name: manifest.name
118
+ };
119
+ const currentHash = hashManifest(manifest, target);
120
+ const existingApp = await findMicroserviceByName(baseUrl, manifest.name, authHeader);
121
+ if (!existingApp) {
122
+ const createdApp = await createMicroservice(baseUrl, manifest, authHeader);
123
+ logger.debug(`Microservice "${manifest.name}" created (ID: ${createdApp.id})`);
124
+ const credentials = await finalizeBootstrap({
125
+ rootDir,
126
+ logger,
127
+ baseUrl,
128
+ developmentTenant,
129
+ appId: createdApp.id,
130
+ authHeader,
131
+ target,
132
+ manifestHash: currentHash,
133
+ forceCredentials: true
134
+ });
135
+ logger.success(`Microservice "${manifest.name}" created on the development tenant.`);
136
+ return credentials;
137
+ }
138
+ const appId = existingApp.id;
139
+ const isRealService = Boolean(existingApp.activeVersionId);
140
+ const cache = await readManifestCache(rootDir);
141
+ if (!cache || cache.manifestHash !== currentHash || cache.appId !== appId) if (isRealService) 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.`);
142
+ else {
143
+ await updateMicroservice(baseUrl, appId, manifest, authHeader);
144
+ logger.success(`Updated microservice "${manifest.name}" with changed manifest.`);
145
+ }
146
+ const hasBootstrapCreds = BOOTSTRAP_ENV_VARS.every((v) => env[v]);
147
+ const appChanged = cache?.appId !== appId;
148
+ return await finalizeBootstrap({
149
+ rootDir,
150
+ logger,
151
+ baseUrl,
152
+ developmentTenant,
153
+ appId,
154
+ authHeader,
155
+ target,
156
+ manifestHash: currentHash,
157
+ forceCredentials: !hasBootstrapCreds || appChanged
158
+ }) ?? readBootstrapCredentialsFromEnv(env);
159
+ }
160
+ /**
161
+ * Verifies the microservice on the development tenant during dev startup.
162
+ * Runs silently unless a bootstrap/sync was performed or an error occurs.
163
+ * @param nitro - Nitro instance
164
+ */
165
+ async function autoBootstrap(nitro) {
166
+ try {
167
+ if (DEV_ENV_VARS.filter((v) => !process.env[v]).length > 0) return;
168
+ const baseUrl = process.env.C8Y_BASEURL.endsWith("/") ? process.env.C8Y_BASEURL.slice(0, -1) : process.env.C8Y_BASEURL;
169
+ const authHeader = createBasicAuthHeader(process.env.C8Y_DEVELOPMENT_TENANT, process.env.C8Y_DEVELOPMENT_USER, process.env.C8Y_DEVELOPMENT_PASSWORD);
170
+ const manifest = await createC8yManifest(nitro.options.rootDir, nitro.options, nitro.logger);
171
+ await syncBootstrappedMicroservice({
172
+ rootDir: nitro.options.rootDir,
173
+ baseUrl,
174
+ developmentTenant: process.env.C8Y_DEVELOPMENT_TENANT,
175
+ authHeader,
176
+ manifest,
177
+ logger: nitro.logger
178
+ });
179
+ } catch (error) {
180
+ nitro.logger.warn("Auto-bootstrap failed:", error instanceof Error ? error.message : String(error));
181
+ }
182
+ }
183
+ /**
184
+ * Subscribe the tenant, optionally (re)write bootstrap credentials, and persist
185
+ * the synced state to the local cache.
186
+ * @param opts - Bootstrap finalization inputs
187
+ * @returns The freshly fetched credentials when `forceCredentials` was set
188
+ */
189
+ async function finalizeBootstrap(opts) {
190
+ await subscribeToApplication(opts.baseUrl, opts.developmentTenant, opts.appId, opts.authHeader);
191
+ let credentials;
192
+ if (opts.forceCredentials) {
193
+ credentials = await getBootstrapCredentials(opts.baseUrl, opts.appId, opts.authHeader);
194
+ const envFileName = await writeBootstrapCredentials(opts.rootDir, {
195
+ C8Y_BOOTSTRAP_TENANT: credentials.tenant,
196
+ C8Y_BOOTSTRAP_USER: credentials.name,
197
+ C8Y_BOOTSTRAP_PASSWORD: credentials.password
198
+ });
199
+ process.env.C8Y_BOOTSTRAP_TENANT = credentials.tenant;
200
+ process.env.C8Y_BOOTSTRAP_USER = credentials.name;
201
+ process.env.C8Y_BOOTSTRAP_PASSWORD = credentials.password;
202
+ opts.logger.debug(`Bootstrap credentials written to ${envFileName}`);
203
+ }
204
+ await writeManifestCache(opts.rootDir, {
205
+ manifestHash: opts.manifestHash,
206
+ target: opts.target,
207
+ appId: opts.appId
208
+ });
209
+ return credentials;
210
+ }
211
+ //#endregion
212
+ export { readBootstrapCredentialsFromEnv as n, syncBootstrappedMicroservice as r, autoBootstrap as t };
@@ -1,4 +1,4 @@
1
- import { a as findMicroserviceByName, d as updateMicroservice, l as subscribeToApplication, n as createBasicAuthHeader, o as getBootstrapCredentials, p as createC8yManifest, r as createMicroservice } from "./c8y-api-BFjK6AYb.mjs";
1
+ import { a as findMicroserviceByName, d as updateMicroservice, l as subscribeToApplication, n as createBasicAuthHeader, o as getBootstrapCredentials, p as createC8yManifest, r as createMicroservice } from "./c8y-api-DscUs-Kd.mjs";
2
2
  import { t as writeBootstrapCredentials } from "./env-file-BrOTmVjA.mjs";
3
3
  import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
4
4
  import { defineCommand, runCommand } from "citty";
@@ -52,7 +52,7 @@ var bootstrap_default = defineCommand({
52
52
  });
53
53
  consola.success(`Bootstrap credentials written to ${envFileName}`);
54
54
  if (manifest.roles && manifest.roles.length > 0) {
55
- if (await consola.prompt("Do you want to manage microservice roles for your development user?", { type: "confirm" })) await runCommand(await import("./roles-B58Fguop.mjs").then((r) => r.default), { rawArgs: [] });
55
+ if (await consola.prompt("Do you want to manage microservice roles for your development user?", { type: "confirm" })) await runCommand(await import("./roles-B-DNuScZ.mjs").then((r) => r.default), { rawArgs: [] });
56
56
  }
57
57
  consola.success("Bootstrap complete!");
58
58
  }
@@ -283,14 +283,20 @@ async function unassignUserRole(baseUrl, tenantId, userId, roleId, authHeader) {
283
283
  }
284
284
  }
285
285
  /**
286
- * Gets all tenant options for a specific category.
286
+ * Fetches the current application's settings (the tenant options of the
287
+ * microservice's own settings category) via
288
+ * `GET /application/currentApplication/settings`.
289
+ *
290
+ * Must be called with microservice credentials (bootstrap or service user).
291
+ * Unlike the Options API — which returns `credentials.*` values encrypted for
292
+ * regular users and rejects the bootstrap user with `403` — this endpoint
293
+ * returns encrypted `credentials.*` options **decrypted**.
287
294
  * @param baseUrl - The Cumulocity base URL
288
- * @param category - The category to fetch options for
289
- * @param authHeader - The Basic Auth header
290
- * @returns Record of key-value pairs for the category
295
+ * @param authHeader - Basic Auth header of the bootstrap (or service) user
296
+ * @returns Flat key-value map of the microservice's settings
291
297
  */
292
- async function getTenantOptionsByCategory(baseUrl, category, authHeader) {
293
- const url = `${baseUrl}/tenant/options/${encodeURIComponent(category)}`;
298
+ async function getCurrentApplicationSettings(baseUrl, authHeader) {
299
+ const url = `${baseUrl}/application/currentApplication/settings`;
294
300
  const response = await fetch(url, {
295
301
  method: "GET",
296
302
  headers: {
@@ -298,23 +304,21 @@ async function getTenantOptionsByCategory(baseUrl, category, authHeader) {
298
304
  Accept: "application/json"
299
305
  }
300
306
  });
301
- if (response.status === 404) return {};
302
307
  if (!response.ok) {
303
308
  const errorText = await response.text();
304
- throw new Error(`Failed to get tenant options for category ${category}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
309
+ throw new Error(`Failed to get current application settings: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
305
310
  }
306
311
  return await response.json();
307
312
  }
308
313
  /**
309
- * Gets a specific tenant option value.
314
+ * Gets all tenant options for a specific category.
310
315
  * @param baseUrl - The Cumulocity base URL
311
- * @param category - The category of the option
312
- * @param key - The option key
316
+ * @param category - The category to fetch options for
313
317
  * @param authHeader - The Basic Auth header
314
- * @returns The option value, or undefined if not set
318
+ * @returns Record of key-value pairs for the category
315
319
  */
316
- async function getTenantOption(baseUrl, category, key, authHeader) {
317
- const url = `${baseUrl}/tenant/options/${encodeURIComponent(category)}/${encodeURIComponent(key)}`;
320
+ async function getTenantOptionsByCategory(baseUrl, category, authHeader) {
321
+ const url = `${baseUrl}/tenant/options/${encodeURIComponent(category)}`;
318
322
  const response = await fetch(url, {
319
323
  method: "GET",
320
324
  headers: {
@@ -322,12 +326,12 @@ async function getTenantOption(baseUrl, category, key, authHeader) {
322
326
  Accept: "application/json"
323
327
  }
324
328
  });
325
- if (response.status === 404) return;
329
+ if (response.status === 404) return {};
326
330
  if (!response.ok) {
327
331
  const errorText = await response.text();
328
- throw new Error(`Failed to get tenant option ${category}/${key}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
332
+ throw new Error(`Failed to get tenant options for category ${category}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
329
333
  }
330
- return (await response.json()).value;
334
+ return await response.json();
331
335
  }
332
336
  /**
333
337
  * Creates a tenant option.
@@ -420,4 +424,4 @@ async function deleteTenantOption(baseUrl, category, key, authHeader) {
420
424
  }
421
425
  }
422
426
  //#endregion
423
- export { GENERATED_READINESS_ROUTE as _, findMicroserviceByName as a, getTenantOptionsByCategory as c, updateMicroservice as d, upsertTenantOption as f, GENERATED_LIVENESS_ROUTE as g, GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE as h, deleteTenantOption as i, subscribeToApplication as l, createC8yManifestFromNitro as m, createBasicAuthHeader as n, getBootstrapCredentials as o, createC8yManifest as p, createMicroservice as r, getTenantOption as s, assignUserRole as t, unassignUserRole as u };
427
+ export { GENERATED_READINESS_ROUTE as _, findMicroserviceByName as a, getTenantOptionsByCategory as c, updateMicroservice as d, upsertTenantOption as f, GENERATED_LIVENESS_ROUTE as g, GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE as h, deleteTenantOption as i, subscribeToApplication as l, createC8yManifestFromNitro as m, createBasicAuthHeader as n, getBootstrapCredentials as o, createC8yManifest as p, createMicroservice as r, getCurrentApplicationSettings as s, assignUserRole as t, unassignUserRole as u };
@@ -1,4 +1,4 @@
1
- import { n as name, r as version, t as description } from "../package-D5xRshsx.mjs";
1
+ import { n as name, r as version, t as description } from "../package-CNWz76PV.mjs";
2
2
  import { defineCommand, runMain } from "citty";
3
3
  //#region src/cli/index.ts
4
4
  runMain(defineCommand({
@@ -8,9 +8,9 @@ runMain(defineCommand({
8
8
  description
9
9
  },
10
10
  subCommands: {
11
- bootstrap: () => import("../bootstrap-ObaEF7rN.mjs").then((r) => r.default),
12
- roles: () => import("../roles-B58Fguop.mjs").then((r) => r.default),
13
- options: () => import("../options-B7_yb_wr.mjs").then((r) => r.default)
11
+ bootstrap: () => import("../bootstrap-C1Qly0J_.mjs").then((r) => r.default),
12
+ roles: () => import("../roles-B-DNuScZ.mjs").then((r) => r.default),
13
+ options: () => import("../options-CmLpZ3Qf.mjs").then((r) => r.default)
14
14
  }
15
15
  }));
16
16
  //#endregion
@@ -96,6 +96,50 @@ async function getUserTenantDomain(requestOrEvent) {
96
96
  const tenantId = await getCurrentUserTenantId(requestOrEvent);
97
97
  return cachedTenantDomain(requestOrEvent, tenantId);
98
98
  }
99
+ const FAST_DOMAIN_SUCCESS_TTL = 6e4;
100
+ const FAST_DOMAIN_FAILURE_TTL = 1e4;
101
+ const FAST_DOMAIN_MAX_ENTRIES = 1e3;
102
+ const fastDomainCache = /* @__PURE__ */ new Map();
103
+ /**
104
+ * Best-effort variant of {@link getUserTenantDomain} for per-request hot paths:
105
+ * never throws (returns `undefined` for unauthenticated requests or failed
106
+ * lookups) and memoizes results in-process per auth material. Failures are
107
+ * cached briefly so requests with unresolvable credentials don't trigger a
108
+ * platform call each time.
109
+ * @param requestOrEvent - The H3Event or ServerRequest from the current request
110
+ * @param resolveDomain - Domain resolver, only overridden in tests
111
+ */
112
+ async function tryGetUserTenantDomainFast(requestOrEvent, resolveDomain = getUserTenantDomain) {
113
+ if (!getCurrentUserTenantCacheKeyMaterial(requestOrEvent)) return;
114
+ const key = createCurrentUserTenantCacheKey(requestOrEvent);
115
+ const now = Date.now();
116
+ const cached = fastDomainCache.get(key);
117
+ if (cached && cached.expiresAt > now) return cached.domain;
118
+ let domain;
119
+ try {
120
+ domain = await resolveDomain(requestOrEvent);
121
+ } catch {
122
+ domain = void 0;
123
+ }
124
+ if (!fastDomainCache.has(key) && fastDomainCache.size >= FAST_DOMAIN_MAX_ENTRIES) fastDomainCache.delete(fastDomainCache.keys().next().value);
125
+ fastDomainCache.set(key, {
126
+ domain,
127
+ expiresAt: now + (domain ? FAST_DOMAIN_SUCCESS_TTL : FAST_DOMAIN_FAILURE_TTL)
128
+ });
129
+ return domain;
130
+ }
131
+ /**
132
+ * Builds the public URL a request is reachable at through the platform proxy:
133
+ * `https://<tenant domain>/service/<contextPath><pathname><search>`.
134
+ * The proxy strips the `/service/<contextPath>` prefix before the request
135
+ * reaches the microservice, so it is re-added here.
136
+ * @param tenantDomain - Public domain of the tenant (e.g. `tenant.cumulocity.com`)
137
+ * @param contextPath - Context path of the microservice
138
+ * @param url - URL the request was received with (only pathname and search are used)
139
+ */
140
+ function buildPublicRequestUrl(tenantDomain, contextPath, url) {
141
+ return new URL(`https://${tenantDomain}/service/${contextPath}${url.pathname}${url.search}`);
142
+ }
99
143
  //#endregion
100
144
  //#region src/utils/credentials.ts
101
145
  let prevCredentials = null;
@@ -233,4 +277,4 @@ async function useDeployedTenantClient() {
233
277
  return client;
234
278
  }
235
279
  //#endregion
236
- export { getUserTenantDomain as n, useDeployedTenantClient as t };
280
+ export { buildPublicRequestUrl as n, tryGetUserTenantDomainFast as r, useDeployedTenantClient as t };
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { _ as GENERATED_READINESS_ROUTE, a as findMicroserviceByName, d as updateMicroservice, g as GENERATED_LIVENESS_ROUTE, h as GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE, l as subscribeToApplication, m as createC8yManifestFromNitro, n as createBasicAuthHeader, o as getBootstrapCredentials, p as createC8yManifest, r as createMicroservice } from "./c8y-api-BFjK6AYb.mjs";
2
- import { t as writeBootstrapCredentials } from "./env-file-BrOTmVjA.mjs";
3
- import { n as name } from "./package-D5xRshsx.mjs";
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-CNWz76PV.mjs";
4
4
  import { basename, dirname, join, relative } from "node:path";
5
- import { mkdir, readFile, writeFile } from "node:fs/promises";
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
  /**
@@ -465,6 +463,22 @@ function registerRuntime(nitro, options = {}) {
465
463
  handler,
466
464
  middleware: true
467
465
  })));
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
+ }
468
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.");
469
483
  else {
470
484
  nitro.options.handlers.push({
@@ -535,173 +549,6 @@ function checkProbeEndpoint(nitro, probe, probeType) {
535
549
  }
536
550
  }
537
551
  //#endregion
538
- //#region src/module/manifestCache.ts
539
- /**
540
- * Directory (inside the project's `node_modules`) where c8y-nitro persists
541
- * local dev state such as the last-synced manifest hash. Lives in
542
- * `node_modules` so it is naturally gitignored and scoped per-checkout.
543
- */
544
- const CACHE_DIR_SEGMENTS = ["node_modules", ".c8y-nitro"];
545
- const CACHE_FILE_NAME = "bootstrap-state.json";
546
- const CACHE_SCHEMA_VERSION = 1;
547
- function getCacheDir(rootDir) {
548
- return join(rootDir, ...CACHE_DIR_SEGMENTS);
549
- }
550
- function getCacheFile(rootDir) {
551
- return join(getCacheDir(rootDir), CACHE_FILE_NAME);
552
- }
553
- /**
554
- * Compute a hash over the manifest and the deployment target it will be synced
555
- * to. Folding the target in means switching tenant/baseURL invalidates the
556
- * cached hash even if the manifest itself is unchanged.
557
- *
558
- * @param manifest - The generated Cumulocity manifest
559
- * @param target - The deployment target identity
560
- */
561
- function hashManifest(manifest, target) {
562
- const payload = JSON.stringify({
563
- manifest,
564
- target
565
- });
566
- return createHash("sha256").update(payload).digest("hex");
567
- }
568
- /**
569
- * Read the persisted bootstrap state, or `null` when absent, unreadable, or
570
- * from an incompatible schema version.
571
- * @param rootDir - Project root directory (containing `node_modules`)
572
- */
573
- async function readManifestCache(rootDir) {
574
- try {
575
- const content = await readFile(getCacheFile(rootDir), "utf-8");
576
- const state = JSON.parse(content);
577
- if (state.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
578
- return state;
579
- } catch {
580
- return null;
581
- }
582
- }
583
- /**
584
- * Persist the bootstrap state for future dev restarts.
585
- * @param rootDir - Project root directory (containing `node_modules`)
586
- * @param state - The state to persist (schema version is filled in automatically)
587
- */
588
- async function writeManifestCache(rootDir, state) {
589
- await mkdir(getCacheDir(rootDir), { recursive: true });
590
- const payload = {
591
- schemaVersion: CACHE_SCHEMA_VERSION,
592
- ...state
593
- };
594
- await writeFile(getCacheFile(rootDir), JSON.stringify(payload, null, 2), "utf-8");
595
- }
596
- //#endregion
597
- //#region src/module/autoBootstrap.ts
598
- const BOOTSTRAP_ENV_VARS = [
599
- "C8Y_BOOTSTRAP_TENANT",
600
- "C8Y_BOOTSTRAP_USER",
601
- "C8Y_BOOTSTRAP_PASSWORD"
602
- ];
603
- const DEV_ENV_VARS = [
604
- "C8Y_BASEURL",
605
- "C8Y_DEVELOPMENT_TENANT",
606
- "C8Y_DEVELOPMENT_USER",
607
- "C8Y_DEVELOPMENT_PASSWORD"
608
- ];
609
- /**
610
- * Verifies the microservice on the development tenant and keeps it in sync with
611
- * the local manifest, bootstrapping (or re-bootstrapping) when needed.
612
- *
613
- * Unlike a purely env-var-based check, this always verifies against the tenant
614
- * when development credentials are available, so it also handles:
615
- * - the manifest changing between dev restarts (auto-updates a placeholder), and
616
- * - the application being deleted on the tenant while stale bootstrap env vars
617
- * still linger locally (re-creates it).
618
- *
619
- * A previously *deployed* microservice (one with an active version) is never
620
- * overwritten automatically - we only warn about the drift.
621
- *
622
- * Runs silently unless a bootstrap/sync was performed or an error occurs.
623
- * @param nitro - Nitro instance
624
- */
625
- async function autoBootstrap(nitro) {
626
- try {
627
- const rootDir = nitro.options.rootDir;
628
- if (DEV_ENV_VARS.filter((v) => !process.env[v]).length > 0) return;
629
- const baseUrl = process.env.C8Y_BASEURL.endsWith("/") ? process.env.C8Y_BASEURL.slice(0, -1) : process.env.C8Y_BASEURL;
630
- const developmentTenant = process.env.C8Y_DEVELOPMENT_TENANT;
631
- const authHeader = createBasicAuthHeader(developmentTenant, process.env.C8Y_DEVELOPMENT_USER, process.env.C8Y_DEVELOPMENT_PASSWORD);
632
- const manifest = await createC8yManifest(rootDir, nitro.options, nitro.logger);
633
- const target = {
634
- baseUrl,
635
- developmentTenant,
636
- name: manifest.name
637
- };
638
- const currentHash = hashManifest(manifest, target);
639
- const existingApp = await findMicroserviceByName(baseUrl, manifest.name, authHeader);
640
- if (!existingApp) {
641
- const createdApp = await createMicroservice(baseUrl, manifest, authHeader);
642
- nitro.logger.debug(`Microservice "${manifest.name}" created (ID: ${createdApp.id})`);
643
- await finalizeBootstrap(nitro, {
644
- baseUrl,
645
- developmentTenant,
646
- appId: createdApp.id,
647
- authHeader,
648
- target,
649
- manifestHash: currentHash,
650
- forceCredentials: true
651
- });
652
- nitro.logger.success(`Auto-bootstrap complete! Microservice "${manifest.name}" created on tenant.`);
653
- return;
654
- }
655
- const appId = existingApp.id;
656
- const isRealService = Boolean(existingApp.activeVersionId);
657
- const cache = await readManifestCache(rootDir);
658
- 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.`);
659
- else {
660
- await updateMicroservice(baseUrl, appId, manifest, authHeader);
661
- nitro.logger.success(`Auto-bootstrap: updated microservice "${manifest.name}" with changed manifest.`);
662
- }
663
- const hasBootstrapCreds = BOOTSTRAP_ENV_VARS.every((v) => process.env[v]);
664
- const appChanged = cache?.appId !== appId;
665
- await finalizeBootstrap(nitro, {
666
- baseUrl,
667
- developmentTenant,
668
- appId,
669
- authHeader,
670
- target,
671
- manifestHash: currentHash,
672
- forceCredentials: !hasBootstrapCreds || appChanged
673
- });
674
- } catch (error) {
675
- nitro.logger.warn("Auto-bootstrap failed:", error instanceof Error ? error.message : String(error));
676
- }
677
- }
678
- /**
679
- * Subscribe the tenant, optionally (re)write bootstrap credentials, and persist
680
- * the synced state to the local cache.
681
- * @param nitro
682
- * @param opts
683
- */
684
- async function finalizeBootstrap(nitro, opts) {
685
- await subscribeToApplication(opts.baseUrl, opts.developmentTenant, opts.appId, opts.authHeader);
686
- if (opts.forceCredentials) {
687
- const credentials = await getBootstrapCredentials(opts.baseUrl, opts.appId, opts.authHeader);
688
- const envFileName = await writeBootstrapCredentials(nitro.options.rootDir, {
689
- C8Y_BOOTSTRAP_TENANT: credentials.tenant,
690
- C8Y_BOOTSTRAP_USER: credentials.name,
691
- C8Y_BOOTSTRAP_PASSWORD: credentials.password
692
- });
693
- process.env.C8Y_BOOTSTRAP_TENANT = credentials.tenant;
694
- process.env.C8Y_BOOTSTRAP_USER = credentials.name;
695
- process.env.C8Y_BOOTSTRAP_PASSWORD = credentials.password;
696
- nitro.logger.debug(`Bootstrap credentials written to ${envFileName}`);
697
- }
698
- await writeManifestCache(nitro.options.rootDir, {
699
- manifestHash: opts.manifestHash,
700
- target: opts.target,
701
- appId: opts.appId
702
- });
703
- }
704
- //#endregion
705
552
  //#region src/index.ts
706
553
  function c8y() {
707
554
  return {