c8y-nitro 0.9.1 → 0.10.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,206 @@
1
+ import { n as createC8yManifestFromNitro } from "./manifest-DV3UW9Dl.mjs";
2
+ import { dirname, join, relative } from "node:path";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ //#region src/module/apiClient.ts
5
+ /**
6
+ * Converts route path to PascalCase function name.
7
+ * Examples:
8
+ * - /health + get -> GetHealth
9
+ * - /health + post -> PostHealth
10
+ * - /someRoute -> SomeRoute (default)
11
+ * - /[id] or /:id + get -> GetById
12
+ * - /api/[multiple]/[params] -> GetApiByMultipleByParams
13
+ * @param path - The route path (e.g., "/api/[id]" or "/api/:id")
14
+ * @param method - HTTP method (get, post, put, delete, etc.)
15
+ */
16
+ function generateFunctionName(path, method) {
17
+ return `${method}${path.replace(/^\//, "").replaceAll(".", "_").replaceAll("*", "").split("/").map((seg) => {
18
+ if (seg.startsWith("[") && seg.endsWith("]")) return `By${capitalize(seg.slice(1, -1))}`;
19
+ if (seg.startsWith(":")) return `By${capitalize(seg.slice(1))}`;
20
+ return capitalize(seg);
21
+ }).join("") || "Index"}`;
22
+ }
23
+ /**
24
+ * Extracts route parameters from path.
25
+ * Example: "/api/[id]/items/[itemId]" or "/api/:id/items/:itemId" -> [{name: "id", type: "string"}, {name: "itemId", type: "string"}]
26
+ * @param path - The route path to extract parameters from
27
+ */
28
+ function extractParams(path) {
29
+ const params = [];
30
+ const segments = path.split("/");
31
+ for (const seg of segments) if (seg.startsWith("[") && seg.endsWith("]")) {
32
+ const name = seg.slice(1, -1);
33
+ params.push(name);
34
+ } else if (seg.startsWith(":")) {
35
+ const name = seg.slice(1);
36
+ params.push(name);
37
+ }
38
+ return params;
39
+ }
40
+ /**
41
+ * Capitalizes first letter of a string.
42
+ * @param str - The string to capitalize
43
+ */
44
+ function capitalize(str) {
45
+ return str.charAt(0).toUpperCase() + str.slice(1);
46
+ }
47
+ /**
48
+ * Converts a file name to a valid JavaScript class name.
49
+ * Only allows letters, numbers (not at start), $ and _.
50
+ * Removes or replaces invalid characters and ensures PascalCase.
51
+ * Always prefixed with "Generated" for clarity.
52
+ * @param fileName - The file name to convert
53
+ * @example "my-service-api" -> "GeneratedMyServiceApi"
54
+ * @example "playgroundAPIClient" -> "GeneratedPlaygroundAPIClient"
55
+ * @example "special@chars!" -> "GeneratedSpecialchars"
56
+ */
57
+ function toValidClassName(fileName) {
58
+ let cleaned = fileName.replace(/\.(ts|js|mjs|cjs|tsx|jsx)$/, "").replace(/[-_.]/g, " ");
59
+ cleaned = cleaned.replace(/[^a-zA-Z0-9\s$]/g, "");
60
+ return `Generated${cleaned.split(/\s+/).filter((word) => word.length > 0).map((word) => capitalize(word)).join("")}`;
61
+ }
62
+ /**
63
+ * Generates TypeScript method code for a route in an Angular service.
64
+ * @param route - Parsed route information including path, method, params, and return type
65
+ */
66
+ function generateMethod(route) {
67
+ const hasParams = route.params.length > 0;
68
+ let inlineParamsType = "";
69
+ if (hasParams) inlineParamsType = `{ ${route.params.map((name) => `${name}: string | number`).join("; ")} }`;
70
+ const methodParam = hasParams ? `params: ${inlineParamsType}` : "";
71
+ const returnTypeAnnotation = `Promise<${route.returnType}>`;
72
+ let pathExpression = `\`\${this.BASE_PATH}${route.path}\``;
73
+ if (hasParams) pathExpression = `\`\${this.BASE_PATH}${route.path.replace(/\[([^\]]+)\]/g, (_, paramName) => `\${params.${paramName}}`)}\``;
74
+ const fetchOptions = `{ method: '${route.method}', headers: { 'Content-Type': 'application/json' } }`;
75
+ return ` async ${route.functionName}(${methodParam}): ${returnTypeAnnotation} {
76
+ const response = await this.fetchClient.fetch(${pathExpression}, ${fetchOptions});
77
+ return this.serialize(response);
78
+ }`;
79
+ }
80
+ /**
81
+ * Helper types for proper type serialization from Nitro.
82
+ * https://github.com/nitrojs/nitro/blob/67b43f2692a41728a2759462b6982c6872ed3a81/src/types/fetch/_serialize.ts
83
+ */
84
+ const serializationTypes = `type JsonPrimitive = string | number | boolean | string | number | boolean | null;
85
+ type NonJsonPrimitive = undefined | Function | symbol;
86
+ type IsAny<T> = 0 extends 1 & T ? true : false;
87
+ type FilterKeys<TObj extends object, TFilter> = { [TKey in keyof TObj]: TObj[TKey] extends TFilter ? TKey : never }[keyof TObj];
88
+ type Serialize<T> = IsAny<T> extends true ? any : T extends JsonPrimitive | undefined ? T : T extends Map<any, any> | Set<any> ? Record<string, never> : T extends NonJsonPrimitive ? never : T extends {
89
+ toJSON: () => infer U;
90
+ } ? U : T extends [] ? [] : T extends [unknown, ...unknown[]] ? SerializeTuple<T> : T extends ReadonlyArray<infer U> ? (U extends NonJsonPrimitive ? null : Serialize<U>)[] : T extends object ? SerializeObject<T> : never;
91
+ type SerializeTuple<T extends [unknown, ...unknown[]]> = { [k in keyof T]: T[k] extends NonJsonPrimitive ? null : Serialize<T[k]> };
92
+ type SerializeObject<T extends object> = { [k in keyof Omit<T, FilterKeys<T, NonJsonPrimitive>>]: Serialize<T[k]> };
93
+ type Simplify<TType> = TType extends any[] | Date ? TType : { [K in keyof TType]: Simplify<TType[K]> };
94
+ type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
95
+ type C8ySerialize<T> = T extends void | undefined | null ? undefined : T extends string ? string : Simplify<Serialize<T>>;
96
+ `;
97
+ /**
98
+ * Generates complete Angular API client service.
99
+ * @param routes - Array of parsed routes to generate methods for
100
+ * @param contextPath - The microservice context path (e.g., "my-service")
101
+ * @param className - The class name for the generated service (e.g., "PlaygroundAPIClient")
102
+ */
103
+ function generateAPIClient(routes, contextPath, className) {
104
+ const methods = routes.map((route) => generateMethod(route)).join("\n\n");
105
+ return `/* eslint-disable eslint-comments/no-unlimited-disable */
106
+ /* eslint-disable */
107
+ /**
108
+ * Auto-generated Cumulocity API Client
109
+ * Generated by c8y-nitro
110
+ *
111
+ * This Angular service provides typed methods for all Nitro routes.
112
+ * Each method corresponds to a route handler and returns properly typed responses.
113
+ */
114
+ import { Injectable, inject } from '@angular/core'
115
+ import { FetchClient } from '@c8y/client'
116
+
117
+ // Type helpers for proper serialization
118
+ ${serializationTypes}
119
+
120
+ @Injectable({ providedIn: 'root' })
121
+ export class ${className} {
122
+ private readonly BASE_PATH = '/service/${contextPath}';
123
+ private readonly fetchClient: FetchClient = inject(FetchClient)
124
+
125
+ /**
126
+ * Serializes the response based on content type and body.
127
+ * - Empty body (null, undefined, void) -> undefined
128
+ * - text/plain (string) -> raw text
129
+ * - application/json (number, boolean, object, array) -> JSON parsed
130
+ */
131
+ private async serialize(response: Response): Promise<any> {
132
+ const contentType = response.headers.get('content-type');
133
+ const text = await response.text();
134
+
135
+ // Handle empty responses (null, undefined, empty return)
136
+ if (!text || text.length === 0) {
137
+ return undefined;
138
+ }
139
+
140
+ // Handle plain text responses (strings)
141
+ if (contentType?.includes('text/plain')) {
142
+ return text;
143
+ }
144
+
145
+ // Handle JSON responses (numbers, booleans, objects, arrays)
146
+ return JSON.parse(text);
147
+ }
148
+
149
+ ${methods}
150
+ }`;
151
+ }
152
+ /**
153
+ * Generates the TypeScript return type for a route handler.
154
+ * @param handlerPath - Absolute path to the route handler file
155
+ * @param outputFile - Absolute path to the output API client file
156
+ * @returns TypeScript return type string with proper serialization wrappers
157
+ */
158
+ function getReturnType(handlerPath, outputFile) {
159
+ const relativeHandlerPath = relative(dirname(outputFile), handlerPath);
160
+ return `C8ySerialize<Awaited<ReturnType<typeof import('${(relativeHandlerPath.startsWith(".") ? relativeHandlerPath : `./${relativeHandlerPath}`).replace(/\.ts$/, "")}').default>>>`;
161
+ }
162
+ /**
163
+ * Writes the generated API client to disk.
164
+ * @param nitro - Nitro instance
165
+ * @param options - Complete module options including apiClient and manifest
166
+ */
167
+ async function writeAPIClient(nitro, options) {
168
+ const { apiClient: apiClientOptions } = options;
169
+ if (!apiClientOptions) {
170
+ nitro.logger.debug("API client generation skipped: no apiClient options provided");
171
+ return;
172
+ }
173
+ const manifest = await createC8yManifestFromNitro(nitro);
174
+ const serviceName = manifest.name;
175
+ const serviceContextPath = apiClientOptions.contextPath ?? manifest.contextPath ?? serviceName;
176
+ const name = `${serviceName}APIClient`;
177
+ const rootDir = nitro.options.rootDir;
178
+ const outputDir = join(rootDir, apiClientOptions.dir);
179
+ const outputFile = join(outputDir, `${name}.ts`);
180
+ const routes = (nitro.routing.routes._routes ?? []).filter((route) => {
181
+ return !route.handler.includes("nitro/dist/runtime/internal");
182
+ }).map((route) => {
183
+ const path = route.route;
184
+ const method = (!route.method ? "GET" : route.method).toUpperCase();
185
+ const params = extractParams(path);
186
+ const functionName = generateFunctionName(path, method);
187
+ const returnType = getReturnType(route.handler, outputFile);
188
+ return {
189
+ path: path.replace(/:([^/]+)/g, "[$1]"),
190
+ method,
191
+ functionName,
192
+ params,
193
+ returnType
194
+ };
195
+ });
196
+ if (routes.length === 0) {
197
+ nitro.logger.warn("No routes found to generate API client");
198
+ return;
199
+ }
200
+ const code = generateAPIClient(routes, serviceContextPath, toValidClassName(name));
201
+ await mkdir(outputDir, { recursive: true });
202
+ await writeFile(outputFile, code, "utf-8");
203
+ nitro.logger.success(`Generated API client with ${routes.length} routes at: ${relative(rootDir, outputFile)}`);
204
+ }
205
+ //#endregion
206
+ export { writeAPIClient as t };
@@ -1,4 +1,5 @@
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-DpCWhaJU.mjs";
1
+ import { t as createC8yManifest } from "./manifest-DV3UW9Dl.mjs";
2
+ import { a as findMicroserviceByName, d as updateMicroservice, l as subscribeToApplication, n as createBasicAuthHeader, o as getBootstrapCredentials, r as createMicroservice } from "./c8y-api-DSs1xIIh.mjs";
2
3
  import { t as writeBootstrapCredentials } from "./env-file-BrOTmVjA.mjs";
3
4
  import { join } from "node:path";
4
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -55,7 +56,8 @@ async function readManifestCache(rootDir) {
55
56
  * @param state - The state to persist (schema version is filled in automatically)
56
57
  */
57
58
  async function writeManifestCache(rootDir, state) {
58
- await mkdir(getCacheDir(rootDir), { recursive: true });
59
+ const dir = getCacheDir(rootDir);
60
+ await mkdir(dir, { recursive: true });
59
61
  const payload = {
60
62
  schemaVersion: CACHE_SCHEMA_VERSION,
61
63
  ...state
@@ -138,10 +140,12 @@ async function syncBootstrappedMicroservice(options) {
138
140
  const appId = existingApp.id;
139
141
  const isRealService = Boolean(existingApp.activeVersionId);
140
142
  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.`);
143
+ if (!cache || cache.manifestHash !== currentHash || cache.appId !== appId) {
144
+ 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.`);
145
+ else {
146
+ await updateMicroservice(baseUrl, appId, manifest, authHeader);
147
+ logger.success(`Updated microservice "${manifest.name}" with changed manifest.`);
148
+ }
145
149
  }
146
150
  const hasBootstrapCreds = BOOTSTRAP_ENV_VARS.every((v) => env[v]);
147
151
  const appChanged = cache?.appId !== appId;
@@ -1,4 +1,5 @@
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-DpCWhaJU.mjs";
1
+ import { t as createC8yManifest } from "./manifest-DV3UW9Dl.mjs";
2
+ import { a as findMicroserviceByName, d as updateMicroservice, l as subscribeToApplication, n as createBasicAuthHeader, o as getBootstrapCredentials, r as createMicroservice } from "./c8y-api-DSs1xIIh.mjs";
2
3
  import { t as writeBootstrapCredentials } from "./env-file-BrOTmVjA.mjs";
3
4
  import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
4
5
  import { defineCommand, runCommand } from "citty";
@@ -52,7 +53,10 @@ var bootstrap_default = defineCommand({
52
53
  });
53
54
  consola.success(`Bootstrap credentials written to ${envFileName}`);
54
55
  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-CkUvL64s.mjs").then((r) => r.default), { rawArgs: [] });
56
+ if (await consola.prompt("Do you want to manage microservice roles for your development user?", { type: "confirm" })) {
57
+ const rolesCommand = await import("./roles-CNCObfa3.mjs").then((r) => r.default);
58
+ await runCommand(rolesCommand, { rawArgs: [] });
59
+ }
56
60
  }
57
61
  consola.success("Bootstrap complete!");
58
62
  }
@@ -1,128 +1,4 @@
1
- import { readPackage } from "pkg-types";
2
1
  import { Buffer } from "node:buffer";
3
- //#region src/module/constants.ts
4
- const GENERATED_LIVENESS_ROUTE = "/_c8y_nitro/liveness";
5
- const GENERATED_READINESS_ROUTE = "/_c8y_nitro/readiness";
6
- const GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE = "/_c8y_nitro/invalidate-tenant-options";
7
- //#endregion
8
- //#region src/module/manifest.ts
9
- const DEFAULT_OPENAPI_ROUTE = "/_openapi.json";
10
- const ROLE_OPTION_MANAGEMENT_READ = "ROLE_OPTION_MANAGEMENT_READ";
11
- /**
12
- * Cumulocity microservice name constraints: lowercase letters, digits and
13
- * hyphens only, 1-23 characters. Enforced by the platform on upload, so we
14
- * fail early with a clear message instead of a cryptic API rejection.
15
- * @see https://github.com/schplitt/c8y-nitro/issues/75
16
- */
17
- const MICROSERVICE_NAME_REGEX = /^[a-z0-9-]{1,23}$/;
18
- /**
19
- * Decide whether Nitro will expose an OpenAPI spec in the deployed build,
20
- * and if so return the configured route (with leading slash).
21
- * Returns null when OpenAPI is not enabled for production.
22
- * @param config - Nitro config (or options) to inspect
23
- */
24
- function resolveNitroOpenApiRoute(config) {
25
- if (!config.experimental?.openAPI) return null;
26
- if (!config.openAPI?.production) return null;
27
- return config.openAPI?.route || DEFAULT_OPENAPI_ROUTE;
28
- }
29
- /**
30
- * Pull the parts of a Nitro config that feed manifest generation.
31
- * @param config - Nitro config (or options) loaded from disk or from a Nitro instance
32
- */
33
- function extractManifestInputs(config) {
34
- return {
35
- options: config.c8y?.manifest ?? {},
36
- openApiSpec: resolveNitroOpenApiRoute(config) ?? void 0
37
- };
38
- }
39
- function validateManifestSettings(options) {
40
- const invalidSettings = options.settings?.filter((setting) => typeof setting.defaultValue !== "string" || setting.defaultValue.length === 0) ?? [];
41
- if (invalidSettings.length === 0) return;
42
- const invalidKeys = invalidSettings.map((setting) => `"${setting.key}"`).join(", ");
43
- throw new Error(`manifest.settings entries must define a non-empty defaultValue. Invalid keys: ${invalidKeys}`);
44
- }
45
- async function readPackageJsonFieldsForManifest(rootDir, logger) {
46
- logger?.debug(`Reading package file from ${rootDir}`);
47
- const pkg = await readPackage(rootDir);
48
- const name = pkg.name?.replace(/^@[^/]+\//, "");
49
- const version = pkg.version;
50
- const author = pkg.author;
51
- const authorName = typeof author === "string" ? author : author?.name;
52
- const authorEmail = typeof author === "string" ? void 0 : author?.email;
53
- const authorUrl = typeof author === "string" ? void 0 : author?.url;
54
- if (!name || !version || !authorName) throw new Error("package.json must contain name, version, and author name fields");
55
- if (!MICROSERVICE_NAME_REGEX.test(name)) throw new Error(`Invalid microservice name "${name}". Cumulocity requires the name to match ${MICROSERVICE_NAME_REGEX.source} (lowercase letters, digits and hyphens only, 1-23 characters). Adjust the "name" field in package.json accordingly (scope like "@org/" is stripped and does not count).`);
56
- const support = typeof pkg.bugs === "string" ? pkg.bugs : pkg.bugs?.url ?? pkg.bugs?.email;
57
- const provider = {
58
- name: authorName ?? name,
59
- domain: authorUrl ?? pkg.homepage,
60
- support: support ?? authorEmail
61
- };
62
- logger?.debug(`Found package.json fields for manifest: name=${name}, version=${version}, provider=${JSON.stringify(provider)}`);
63
- return {
64
- name,
65
- version,
66
- provider,
67
- contextPath: name
68
- };
69
- }
70
- /**
71
- * Create a Cumulocity manifest from a project root and its Nitro config.
72
- * Reads `package.json` for identity fields, takes user manifest options from
73
- * `nitroConfig.c8y.manifest`, and auto-injects `openApiSpec` when Nitro
74
- * OpenAPI is enabled for production.
75
- *
76
- * Used by both the Nitro module and the CLI commands.
77
- * @param rootDir - Directory containing the project's `package.json`
78
- * @param nitroConfig - Nitro config (or options) for this project
79
- * @param logger - Optional logger for debug output
80
- */
81
- async function createC8yManifest(rootDir, nitroConfig = {}, logger) {
82
- const { options, openApiSpec } = extractManifestInputs(nitroConfig);
83
- validateManifestSettings(options);
84
- const { name, version, provider, ...restManifestFields } = await readPackageJsonFieldsForManifest(rootDir, logger);
85
- const probeFields = {};
86
- if (!options.livenessProbe?.httpGet) probeFields.livenessProbe = {
87
- ...options.livenessProbe,
88
- httpGet: { path: GENERATED_LIVENESS_ROUTE }
89
- };
90
- if (!options.readinessProbe?.httpGet) probeFields.readinessProbe = {
91
- ...options.readinessProbe,
92
- httpGet: { path: GENERATED_READINESS_ROUTE }
93
- };
94
- const key = `${name}-key`;
95
- let requiredRoles = options.requiredRoles ? [...options.requiredRoles] : void 0;
96
- if (options.settings && options.settings.length > 0 && !requiredRoles?.includes(ROLE_OPTION_MANAGEMENT_READ)) {
97
- requiredRoles = [...requiredRoles ?? [], ROLE_OPTION_MANAGEMENT_READ];
98
- logger?.debug(`Auto-added ${ROLE_OPTION_MANAGEMENT_READ} to requiredRoles because manifest.settings are defined`);
99
- }
100
- const manifest = {
101
- ...restManifestFields,
102
- ...probeFields,
103
- ...options,
104
- ...requiredRoles !== void 0 ? { requiredRoles } : {},
105
- ...openApiSpec ? { openApiSpec } : {},
106
- provider,
107
- name,
108
- version,
109
- apiVersion: "v2",
110
- key,
111
- type: "MICROSERVICE"
112
- };
113
- if (openApiSpec) logger?.debug(`Auto-added openApiSpec="${openApiSpec}" to manifest (Nitro OpenAPI is enabled)`);
114
- logger?.debug(`Created Cumulocity manifest: ${JSON.stringify(manifest, null, 2)}`);
115
- return manifest;
116
- }
117
- /**
118
- * Creates a Cumulocity manifest from a Nitro instance.
119
- * Convenience wrapper for use in the Nitro module.
120
- * @param nitro - The Nitro instance
121
- */
122
- async function createC8yManifestFromNitro(nitro) {
123
- return createC8yManifest(nitro.options.rootDir, nitro.options, nitro.logger);
124
- }
125
- //#endregion
126
2
  //#region src/cli/utils/c8y-api.ts
127
3
  /**
128
4
  * Creates a Basic Auth header for Cumulocity API requests.
@@ -432,4 +308,4 @@ async function deleteTenantOption(baseUrl, category, key, authHeader) {
432
308
  }
433
309
  }
434
310
  //#endregion
435
- 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 };
311
+ export { findMicroserviceByName as a, getTenantOptionsByCategory as c, updateMicroservice as d, upsertTenantOption as f, deleteTenantOption as i, subscribeToApplication as l, createBasicAuthHeader as n, getBootstrapCredentials as o, createMicroservice as r, getCurrentApplicationSettings as s, assignUserRole as t, unassignUserRole as u };
@@ -1 +1 @@
1
- export { };
1
+ export {}
@@ -1,17 +1,19 @@
1
- import { n as name, r as version, t as description } from "../package-CUuWGZoQ.mjs";
1
+ import { n as name, r as version, t as description } from "../package-CX0mgzB-.mjs";
2
2
  import { defineCommand, runMain } from "citty";
3
3
  //#region src/cli/index.ts
4
- runMain(defineCommand({
4
+ const main = defineCommand({
5
5
  meta: {
6
6
  name,
7
7
  version,
8
8
  description
9
9
  },
10
10
  subCommands: {
11
- bootstrap: () => import("../bootstrap-BcTCUiqG.mjs").then((r) => r.default),
12
- roles: () => import("../roles-CkUvL64s.mjs").then((r) => r.default),
13
- options: () => import("../options-BBnR-y_W.mjs").then((r) => r.default)
11
+ bootstrap: () => import("../bootstrap-DrMjty-k.mjs").then((r) => r.default),
12
+ roles: () => import("../roles-CNCObfa3.mjs").then((r) => r.default),
13
+ options: () => import("../options-54uJmuUV.mjs").then((r) => r.default),
14
+ typegen: () => import("../typegen-BO_AkyNc.mjs").then((r) => r.default)
14
15
  }
15
- }));
16
+ });
17
+ runMain(main);
16
18
  //#endregion
17
19
  export {};
@@ -262,7 +262,9 @@ const useDeployedTenantCredentials = Object.assign(async () => {
262
262
  function useUserClient(requestOrEvent) {
263
263
  const request = "req" in requestOrEvent ? requestOrEvent.req : requestOrEvent;
264
264
  if (request.context?.["c8y_user_client"]) return request.context["c8y_user_client"];
265
- const client = new Client(new MicroserviceClientRequestAuth(convertRequestHeadersToC8yFormat(request)), process.env.C8Y_BASEURL);
265
+ const headers = convertRequestHeadersToC8yFormat(request);
266
+ const auth = new MicroserviceClientRequestAuth(headers);
267
+ const client = new Client(auth, process.env.C8Y_BASEURL);
266
268
  request.context ??= {};
267
269
  request.context["c8y_user_client"] = client;
268
270
  return client;
@@ -414,6 +414,18 @@ interface C8yCacheOptions {
414
414
  //#endregion
415
415
  //#region src/types/docker.d.ts
416
416
  interface C8yDockerOptions {
417
+ /**
418
+ * Path to a custom Dockerfile, relative to the Nitro config file location
419
+ * (`rootDir`).
420
+ *
421
+ * When set, the built-in Dockerfile template is **not** generated. The file
422
+ * at this path is copied into the build context and used as-is for the Docker
423
+ * build. The `baseImage` and `extraInstructions` options are ignored when a
424
+ * custom Dockerfile is provided.
425
+ *
426
+ * @example 'docker/Dockerfile'
427
+ */
428
+ dockerfile?: string;
417
429
  /**
418
430
  * Base image for the generated Dockerfile.
419
431
  *
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as C8yNitroModuleOptions } from "./index-Dil1PcDr.mjs";
1
+ import { n as C8yNitroModuleOptions } from "./index-DHd7xpBX.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,9 @@
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-DpCWhaJU.mjs";
2
- import { t as autoBootstrap } from "./autoBootstrap-qCcCgset.mjs";
3
- import { n as name } from "./package-CUuWGZoQ.mjs";
4
- import { basename, dirname, join, relative } from "node:path";
5
- import { mkdir, writeFile } from "node:fs/promises";
1
+ import { a as GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE, i as C8Y_NITRO_TYPES_FILE, n as createC8yManifestFromNitro, o as GENERATED_LIVENESS_ROUTE, r as C8Y_NITRO_TYPES_DIR, s as GENERATED_READINESS_ROUTE, t as createC8yManifest } from "./manifest-DV3UW9Dl.mjs";
2
+ import { t as writeAPIClient } from "./apiClient-kxv5KHYJ.mjs";
3
+ import { t as autoBootstrap } from "./autoBootstrap-DUjHuHe_.mjs";
4
+ import { n as name } from "./package-CX0mgzB-.mjs";
5
+ import { basename, join, resolve } from "node:path";
6
+ import { copyFile, mkdir, writeFile } from "node:fs/promises";
6
7
  import { x } from "tinyexec";
7
8
  import { join as join$1 } from "pathe";
8
9
  import { mkdir as mkdir$1, readFile as readFile$1, stat, writeFile as writeFile$1 } from "fs/promises";
@@ -14,208 +15,6 @@ import { isValidRealtimeName, toRealtimeName } from "c8y-realtime";
14
15
  import { createHash } from "node:crypto";
15
16
  import { fileURLToPath } from "node:url";
16
17
  import evlog from "evlog/nitro/v3";
17
- //#region src/module/apiClient.ts
18
- /**
19
- * Converts route path to PascalCase function name.
20
- * Examples:
21
- * - /health + get -> GetHealth
22
- * - /health + post -> PostHealth
23
- * - /someRoute -> SomeRoute (default)
24
- * - /[id] or /:id + get -> GetById
25
- * - /api/[multiple]/[params] -> GetApiByMultipleByParams
26
- * @param path - The route path (e.g., "/api/[id]" or "/api/:id")
27
- * @param method - HTTP method (get, post, put, delete, etc.)
28
- */
29
- function generateFunctionName(path, method) {
30
- return `${method}${path.replace(/^\//, "").replaceAll(".", "_").replaceAll("*", "").split("/").map((seg) => {
31
- if (seg.startsWith("[") && seg.endsWith("]")) return `By${capitalize(seg.slice(1, -1))}`;
32
- if (seg.startsWith(":")) return `By${capitalize(seg.slice(1))}`;
33
- return capitalize(seg);
34
- }).join("") || "Index"}`;
35
- }
36
- /**
37
- * Extracts route parameters from path.
38
- * Example: "/api/[id]/items/[itemId]" or "/api/:id/items/:itemId" -> [{name: "id", type: "string"}, {name: "itemId", type: "string"}]
39
- * @param path - The route path to extract parameters from
40
- */
41
- function extractParams(path) {
42
- const params = [];
43
- const segments = path.split("/");
44
- for (const seg of segments) if (seg.startsWith("[") && seg.endsWith("]")) {
45
- const name = seg.slice(1, -1);
46
- params.push(name);
47
- } else if (seg.startsWith(":")) {
48
- const name = seg.slice(1);
49
- params.push(name);
50
- }
51
- return params;
52
- }
53
- /**
54
- * Capitalizes first letter of a string.
55
- * @param str - The string to capitalize
56
- */
57
- function capitalize(str) {
58
- return str.charAt(0).toUpperCase() + str.slice(1);
59
- }
60
- /**
61
- * Converts a file name to a valid JavaScript class name.
62
- * Only allows letters, numbers (not at start), $ and _.
63
- * Removes or replaces invalid characters and ensures PascalCase.
64
- * Always prefixed with "Generated" for clarity.
65
- * @param fileName - The file name to convert
66
- * @example "my-service-api" -> "GeneratedMyServiceApi"
67
- * @example "playgroundAPIClient" -> "GeneratedPlaygroundAPIClient"
68
- * @example "special@chars!" -> "GeneratedSpecialchars"
69
- */
70
- function toValidClassName(fileName) {
71
- let cleaned = fileName.replace(/\.(ts|js|mjs|cjs|tsx|jsx)$/, "").replace(/[-_.]/g, " ");
72
- cleaned = cleaned.replace(/[^a-zA-Z0-9\s$]/g, "");
73
- return `Generated${cleaned.split(/\s+/).filter((word) => word.length > 0).map((word) => capitalize(word)).join("")}`;
74
- }
75
- /**
76
- * Generates TypeScript method code for a route in an Angular service.
77
- * @param route - Parsed route information including path, method, params, and return type
78
- */
79
- function generateMethod(route) {
80
- const hasParams = route.params.length > 0;
81
- let inlineParamsType = "";
82
- if (hasParams) inlineParamsType = `{ ${route.params.map((name) => `${name}: string | number`).join("; ")} }`;
83
- const methodParam = hasParams ? `params: ${inlineParamsType}` : "";
84
- const returnTypeAnnotation = `Promise<${route.returnType}>`;
85
- let pathExpression = `\`\${this.BASE_PATH}${route.path}\``;
86
- if (hasParams) pathExpression = `\`\${this.BASE_PATH}${route.path.replace(/\[([^\]]+)\]/g, (_, paramName) => `\${params.${paramName}}`)}\``;
87
- const fetchOptions = `{ method: '${route.method}', headers: { 'Content-Type': 'application/json' } }`;
88
- return ` async ${route.functionName}(${methodParam}): ${returnTypeAnnotation} {
89
- const response = await this.fetchClient.fetch(${pathExpression}, ${fetchOptions});
90
- return this.serialize(response);
91
- }`;
92
- }
93
- /**
94
- * Helper types for proper type serialization from Nitro.
95
- * https://github.com/nitrojs/nitro/blob/67b43f2692a41728a2759462b6982c6872ed3a81/src/types/fetch/_serialize.ts
96
- */
97
- const serializationTypes = `type JsonPrimitive = string | number | boolean | string | number | boolean | null;
98
- type NonJsonPrimitive = undefined | Function | symbol;
99
- type IsAny<T> = 0 extends 1 & T ? true : false;
100
- type FilterKeys<TObj extends object, TFilter> = { [TKey in keyof TObj]: TObj[TKey] extends TFilter ? TKey : never }[keyof TObj];
101
- type Serialize<T> = IsAny<T> extends true ? any : T extends JsonPrimitive | undefined ? T : T extends Map<any, any> | Set<any> ? Record<string, never> : T extends NonJsonPrimitive ? never : T extends {
102
- toJSON: () => infer U;
103
- } ? U : T extends [] ? [] : T extends [unknown, ...unknown[]] ? SerializeTuple<T> : T extends ReadonlyArray<infer U> ? (U extends NonJsonPrimitive ? null : Serialize<U>)[] : T extends object ? SerializeObject<T> : never;
104
- type SerializeTuple<T extends [unknown, ...unknown[]]> = { [k in keyof T]: T[k] extends NonJsonPrimitive ? null : Serialize<T[k]> };
105
- type SerializeObject<T extends object> = { [k in keyof Omit<T, FilterKeys<T, NonJsonPrimitive>>]: Serialize<T[k]> };
106
- type Simplify<TType> = TType extends any[] | Date ? TType : { [K in keyof TType]: Simplify<TType[K]> };
107
- type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
108
- type C8ySerialize<T> = T extends void | undefined | null ? undefined : T extends string ? string : Simplify<Serialize<T>>;
109
- `;
110
- /**
111
- * Generates complete Angular API client service.
112
- * @param routes - Array of parsed routes to generate methods for
113
- * @param contextPath - The microservice context path (e.g., "my-service")
114
- * @param className - The class name for the generated service (e.g., "PlaygroundAPIClient")
115
- */
116
- function generateAPIClient(routes, contextPath, className) {
117
- const methods = routes.map((route) => generateMethod(route)).join("\n\n");
118
- return `/* eslint-disable eslint-comments/no-unlimited-disable */
119
- /* eslint-disable */
120
- /**
121
- * Auto-generated Cumulocity API Client
122
- * Generated by c8y-nitro
123
- *
124
- * This Angular service provides typed methods for all Nitro routes.
125
- * Each method corresponds to a route handler and returns properly typed responses.
126
- */
127
- import { Injectable, inject } from '@angular/core'
128
- import { FetchClient } from '@c8y/client'
129
-
130
- // Type helpers for proper serialization
131
- ${serializationTypes}
132
-
133
- @Injectable({ providedIn: 'root' })
134
- export class ${className} {
135
- private readonly BASE_PATH = '/service/${contextPath}';
136
- private readonly fetchClient: FetchClient = inject(FetchClient)
137
-
138
- /**
139
- * Serializes the response based on content type and body.
140
- * - Empty body (null, undefined, void) -> undefined
141
- * - text/plain (string) -> raw text
142
- * - application/json (number, boolean, object, array) -> JSON parsed
143
- */
144
- private async serialize(response: Response): Promise<any> {
145
- const contentType = response.headers.get('content-type');
146
- const text = await response.text();
147
-
148
- // Handle empty responses (null, undefined, empty return)
149
- if (!text || text.length === 0) {
150
- return undefined;
151
- }
152
-
153
- // Handle plain text responses (strings)
154
- if (contentType?.includes('text/plain')) {
155
- return text;
156
- }
157
-
158
- // Handle JSON responses (numbers, booleans, objects, arrays)
159
- return JSON.parse(text);
160
- }
161
-
162
- ${methods}
163
- }`;
164
- }
165
- /**
166
- * Generates the TypeScript return type for a route handler.
167
- * @param handlerPath - Absolute path to the route handler file
168
- * @param outputFile - Absolute path to the output API client file
169
- * @returns TypeScript return type string with proper serialization wrappers
170
- */
171
- function getReturnType(handlerPath, outputFile) {
172
- const relativeHandlerPath = relative(dirname(outputFile), handlerPath);
173
- return `C8ySerialize<Awaited<ReturnType<typeof import('${(relativeHandlerPath.startsWith(".") ? relativeHandlerPath : `./${relativeHandlerPath}`).replace(/\.ts$/, "")}').default>>>`;
174
- }
175
- /**
176
- * Writes the generated API client to disk.
177
- * @param nitro - Nitro instance
178
- * @param options - Complete module options including apiClient and manifest
179
- */
180
- async function writeAPIClient(nitro, options) {
181
- const { apiClient: apiClientOptions } = options;
182
- if (!apiClientOptions) {
183
- nitro.logger.debug("API client generation skipped: no apiClient options provided");
184
- return;
185
- }
186
- const manifest = await createC8yManifestFromNitro(nitro);
187
- const serviceName = manifest.name;
188
- const serviceContextPath = apiClientOptions.contextPath ?? manifest.contextPath ?? serviceName;
189
- const name = `${serviceName}APIClient`;
190
- const rootDir = nitro.options.rootDir;
191
- const outputDir = join(rootDir, apiClientOptions.dir);
192
- const outputFile = join(outputDir, `${name}.ts`);
193
- const routes = (nitro.routing.routes._routes ?? []).filter((route) => {
194
- return !route.handler.includes("nitro/dist/runtime/internal");
195
- }).map((route) => {
196
- const path = route.route;
197
- const method = (!route.method ? "GET" : route.method).toUpperCase();
198
- const params = extractParams(path);
199
- const functionName = generateFunctionName(path, method);
200
- const returnType = getReturnType(route.handler, outputFile);
201
- return {
202
- path: path.replace(/:([^/]+)/g, "[$1]"),
203
- method,
204
- functionName,
205
- params,
206
- returnType
207
- };
208
- });
209
- if (routes.length === 0) {
210
- nitro.logger.warn("No routes found to generate API client");
211
- return;
212
- }
213
- const code = generateAPIClient(routes, serviceContextPath, toValidClassName(name));
214
- await mkdir(outputDir, { recursive: true });
215
- await writeFile(outputFile, code, "utf-8");
216
- nitro.logger.success(`Generated API client with ${routes.length} routes at: ${relative(rootDir, outputFile)}`);
217
- }
218
- //#endregion
219
18
  //#region src/module/docker.ts
220
19
  /**
221
20
  * Generate the Dockerfile content for a Nitro build
@@ -249,12 +48,17 @@ async function checkDockerInstalled() {
249
48
  return false;
250
49
  }
251
50
  }
252
- async function writeDockerfile(outputDir, options) {
253
- const outputDirName = basename(outputDir);
51
+ async function writeDockerfile(outputDir, rootDir, options) {
254
52
  const c8yDir = join(outputDir, "../.c8y");
255
53
  const dockerfilePath = join(c8yDir, "Dockerfile");
256
54
  await mkdir(c8yDir, { recursive: true });
257
- await writeFile(dockerfilePath, getDockerfileContent(outputDirName, options), "utf-8");
55
+ if (options.dockerfile) {
56
+ const customDockerfilePath = resolve(rootDir, options.dockerfile);
57
+ await copyFile(customDockerfilePath, dockerfilePath);
58
+ } else {
59
+ const dockerfileContent = getDockerfileContent(basename(outputDir), options);
60
+ await writeFile(dockerfilePath, dockerfileContent, "utf-8");
61
+ }
258
62
  return c8yDir;
259
63
  }
260
64
  async function buildDockerImage(nitro, c8yDir) {
@@ -309,7 +113,7 @@ async function saveDockerImageToTar(nitro, c8yDir, imageName) {
309
113
  async function createDockerImage(nitro, options = {}) {
310
114
  if (!await checkDockerInstalled()) throw new Error("Docker is not installed or not available in PATH. Please install Docker to build images.");
311
115
  nitro.logger.debug("Creating Docker image...");
312
- const c8yDir = await writeDockerfile(nitro.options.output.dir, options);
116
+ const c8yDir = await writeDockerfile(nitro.options.output.dir, nitro.options.rootDir, options);
313
117
  const imageTarPath = await saveDockerImageToTar(nitro, c8yDir, await buildDockerImage(nitro, c8yDir));
314
118
  nitro.logger.debug("Docker image creation complete.");
315
119
  return imageTarPath;
@@ -337,7 +141,9 @@ function formatBytes(bytes) {
337
141
  * @returns Absolute path to the output zip file
338
142
  */
339
143
  function resolveZipOutputPath(rootDir, options, manifest) {
340
- 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`);
144
+ const outputDir = join$1(rootDir, options.outputDir ?? "./");
145
+ const fileName = typeof options.name === "function" ? options.name(manifest.name, manifest.version) : options.name ?? `${manifest.name}-${manifest.version}.zip`;
146
+ return join$1(outputDir, fileName);
341
147
  }
342
148
  async function createC8yZip(nitro, options = {}, dockerOptions = {}) {
343
149
  const startTime = Date.now();
@@ -377,8 +183,8 @@ function setupRuntime(nitro, manifest) {
377
183
  const roles = manifest.roles ?? [];
378
184
  const settingKeys = (manifest.settings ?? []).map((s) => s.key);
379
185
  const settingsCategory = manifest.settingsCategory ?? manifest.contextPath ?? manifest.name;
380
- const completeTypesDir = join$1(nitro.options.rootDir, nitro.options.typescript.generatedTypesDir ?? "node_modules/.nitro/types");
381
- const typesFile = join$1(completeTypesDir, "c8y-nitro.d.ts");
186
+ const completeTypesDir = join$1(nitro.options.rootDir, C8Y_NITRO_TYPES_DIR);
187
+ const typesFile = join$1(completeTypesDir, C8Y_NITRO_TYPES_FILE);
382
188
  const typesContent = `// generated by c8y-nitro
383
189
  declare module 'c8y-nitro/types' {
384
190
  interface C8YRoles {
@@ -512,14 +318,16 @@ function registerRuntime(nitro, options = {}) {
512
318
  });
513
319
  nitro.logger.debug("Registered public request URL rewrite middleware");
514
320
  }
515
- 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.");
516
- else {
517
- nitro.options.handlers.push({
518
- route: "/**",
519
- handler: join$1(thisFilePath, "./runtime/handlers/openapi"),
520
- middleware: true
521
- });
522
- nitro.logger.debug(`Registered OpenAPI transform middleware for ${nitro.options.openAPI?.route || "/_openapi.json"}`);
321
+ if (Boolean(nitro.options.experimental?.openAPI && (isNitroDev || nitro.options.openAPI?.production))) {
322
+ 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.");
323
+ else {
324
+ nitro.options.handlers.push({
325
+ route: "/**",
326
+ handler: join$1(thisFilePath, "./runtime/handlers/openapi"),
327
+ middleware: true
328
+ });
329
+ nitro.logger.debug(`Registered OpenAPI transform middleware for ${nitro.options.openAPI?.route || "/_openapi.json"}`);
330
+ }
523
331
  }
524
332
  /**
525
333
  * Handlers (can't be auto scanned as they need methods etc)
@@ -588,10 +396,6 @@ function c8y() {
588
396
  name: "c8y-nitro",
589
397
  setup: async (nitro) => {
590
398
  const options = nitro.options.c8y ?? {};
591
- nitro.options.typescript.generateTsConfig = true;
592
- nitro.options.typescript.tsConfig = {};
593
- nitro.options.typescript.tsConfig.include = ["./**/*.d.ts"];
594
- nitro.options.typescript.tsConfig.exclude = [];
595
399
  nitro.options.noExternals = nitro.options.noExternals === true ? true : [
596
400
  ...Array.isArray(nitro.options.noExternals) ? nitro.options.noExternals : [],
597
401
  name,
@@ -634,12 +438,6 @@ function c8y() {
634
438
  registerRuntime(nitro, options);
635
439
  if (options.manifest) checkProbes(nitro, options.manifest);
636
440
  });
637
- nitro.hooks.hook("types:extend", async () => {
638
- if (options.apiClient) {
639
- nitro.logger.debug("Generating C8Y API client");
640
- await writeAPIClient(nitro, options);
641
- }
642
- });
643
441
  nitro.hooks.hook("close", async () => {
644
442
  if (nitro.options.preset !== "nitro-dev") await createC8yZip(nitro, options.zip, options.docker);
645
443
  });
@@ -0,0 +1,135 @@
1
+ import { readPackage } from "pkg-types";
2
+ //#region src/module/constants.ts
3
+ const GENERATED_LIVENESS_ROUTE = "/_c8y_nitro/liveness";
4
+ const GENERATED_READINESS_ROUTE = "/_c8y_nitro/readiness";
5
+ const GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE = "/_c8y_nitro/invalidate-tenant-options";
6
+ /**
7
+ * Directory the module writes its generated type declarations into, relative to
8
+ * the project root. Consumers add `node_modules/.c8y-nitro/*.d.ts` to their
9
+ * tsconfig `include` — nitro no longer generates a tsconfig to inherit from.
10
+ */
11
+ const C8Y_NITRO_TYPES_DIR = "node_modules/.c8y-nitro";
12
+ /**
13
+ * File name of the generated declarations inside {@link C8Y_NITRO_TYPES_DIR}.
14
+ */
15
+ const C8Y_NITRO_TYPES_FILE = "types.d.ts";
16
+ //#endregion
17
+ //#region src/module/manifest.ts
18
+ const DEFAULT_OPENAPI_ROUTE = "/_openapi.json";
19
+ const ROLE_OPTION_MANAGEMENT_READ = "ROLE_OPTION_MANAGEMENT_READ";
20
+ /**
21
+ * Cumulocity microservice name constraints: lowercase letters, digits and
22
+ * hyphens only, 1-23 characters. Enforced by the platform on upload, so we
23
+ * fail early with a clear message instead of a cryptic API rejection.
24
+ * @see https://github.com/schplitt/c8y-nitro/issues/75
25
+ */
26
+ const MICROSERVICE_NAME_REGEX = /^[a-z0-9-]{1,23}$/;
27
+ /**
28
+ * Decide whether Nitro will expose an OpenAPI spec in the deployed build,
29
+ * and if so return the configured route (with leading slash).
30
+ * Returns null when OpenAPI is not enabled for production.
31
+ * @param config - Nitro config (or options) to inspect
32
+ */
33
+ function resolveNitroOpenApiRoute(config) {
34
+ if (!config.experimental?.openAPI) return null;
35
+ if (!config.openAPI?.production) return null;
36
+ return config.openAPI?.route || DEFAULT_OPENAPI_ROUTE;
37
+ }
38
+ /**
39
+ * Pull the parts of a Nitro config that feed manifest generation.
40
+ * @param config - Nitro config (or options) loaded from disk or from a Nitro instance
41
+ */
42
+ function extractManifestInputs(config) {
43
+ return {
44
+ options: config.c8y?.manifest ?? {},
45
+ openApiSpec: resolveNitroOpenApiRoute(config) ?? void 0
46
+ };
47
+ }
48
+ function validateManifestSettings(options) {
49
+ const invalidSettings = options.settings?.filter((setting) => typeof setting.defaultValue !== "string" || setting.defaultValue.length === 0) ?? [];
50
+ if (invalidSettings.length === 0) return;
51
+ const invalidKeys = invalidSettings.map((setting) => `"${setting.key}"`).join(", ");
52
+ throw new Error(`manifest.settings entries must define a non-empty defaultValue. Invalid keys: ${invalidKeys}`);
53
+ }
54
+ async function readPackageJsonFieldsForManifest(rootDir, logger) {
55
+ logger?.debug(`Reading package file from ${rootDir}`);
56
+ const pkg = await readPackage(rootDir);
57
+ const name = pkg.name?.replace(/^@[^/]+\//, "");
58
+ const version = pkg.version;
59
+ const author = pkg.author;
60
+ const authorName = typeof author === "string" ? author : author?.name;
61
+ const authorEmail = typeof author === "string" ? void 0 : author?.email;
62
+ const authorUrl = typeof author === "string" ? void 0 : author?.url;
63
+ if (!name || !version || !authorName) throw new Error("package.json must contain name, version, and author name fields");
64
+ if (!MICROSERVICE_NAME_REGEX.test(name)) throw new Error(`Invalid microservice name "${name}". Cumulocity requires the name to match ${MICROSERVICE_NAME_REGEX.source} (lowercase letters, digits and hyphens only, 1-23 characters). Adjust the "name" field in package.json accordingly (scope like "@org/" is stripped and does not count).`);
65
+ const support = typeof pkg.bugs === "string" ? pkg.bugs : pkg.bugs?.url ?? pkg.bugs?.email;
66
+ const provider = {
67
+ name: authorName ?? name,
68
+ domain: authorUrl ?? pkg.homepage,
69
+ support: support ?? authorEmail
70
+ };
71
+ logger?.debug(`Found package.json fields for manifest: name=${name}, version=${version}, provider=${JSON.stringify(provider)}`);
72
+ return {
73
+ name,
74
+ version,
75
+ provider,
76
+ contextPath: name
77
+ };
78
+ }
79
+ /**
80
+ * Create a Cumulocity manifest from a project root and its Nitro config.
81
+ * Reads `package.json` for identity fields, takes user manifest options from
82
+ * `nitroConfig.c8y.manifest`, and auto-injects `openApiSpec` when Nitro
83
+ * OpenAPI is enabled for production.
84
+ *
85
+ * Used by both the Nitro module and the CLI commands.
86
+ * @param rootDir - Directory containing the project's `package.json`
87
+ * @param nitroConfig - Nitro config (or options) for this project
88
+ * @param logger - Optional logger for debug output
89
+ */
90
+ async function createC8yManifest(rootDir, nitroConfig = {}, logger) {
91
+ const { options, openApiSpec } = extractManifestInputs(nitroConfig);
92
+ validateManifestSettings(options);
93
+ const { name, version, provider, ...restManifestFields } = await readPackageJsonFieldsForManifest(rootDir, logger);
94
+ const probeFields = {};
95
+ if (!options.livenessProbe?.httpGet) probeFields.livenessProbe = {
96
+ ...options.livenessProbe,
97
+ httpGet: { path: GENERATED_LIVENESS_ROUTE }
98
+ };
99
+ if (!options.readinessProbe?.httpGet) probeFields.readinessProbe = {
100
+ ...options.readinessProbe,
101
+ httpGet: { path: GENERATED_READINESS_ROUTE }
102
+ };
103
+ const key = `${name}-key`;
104
+ let requiredRoles = options.requiredRoles ? [...options.requiredRoles] : void 0;
105
+ if (options.settings && options.settings.length > 0 && !requiredRoles?.includes(ROLE_OPTION_MANAGEMENT_READ)) {
106
+ requiredRoles = [...requiredRoles ?? [], ROLE_OPTION_MANAGEMENT_READ];
107
+ logger?.debug(`Auto-added ${ROLE_OPTION_MANAGEMENT_READ} to requiredRoles because manifest.settings are defined`);
108
+ }
109
+ const manifest = {
110
+ ...restManifestFields,
111
+ ...probeFields,
112
+ ...options,
113
+ ...requiredRoles !== void 0 ? { requiredRoles } : {},
114
+ ...openApiSpec ? { openApiSpec } : {},
115
+ provider,
116
+ name,
117
+ version,
118
+ apiVersion: "v2",
119
+ key,
120
+ type: "MICROSERVICE"
121
+ };
122
+ if (openApiSpec) logger?.debug(`Auto-added openApiSpec="${openApiSpec}" to manifest (Nitro OpenAPI is enabled)`);
123
+ logger?.debug(`Created Cumulocity manifest: ${JSON.stringify(manifest, null, 2)}`);
124
+ return manifest;
125
+ }
126
+ /**
127
+ * Creates a Cumulocity manifest from a Nitro instance.
128
+ * Convenience wrapper for use in the Nitro module.
129
+ * @param nitro - The Nitro instance
130
+ */
131
+ async function createC8yManifestFromNitro(nitro) {
132
+ return createC8yManifest(nitro.options.rootDir, nitro.options, nitro.logger);
133
+ }
134
+ //#endregion
135
+ export { GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE as a, C8Y_NITRO_TYPES_FILE as i, createC8yManifestFromNitro as n, GENERATED_LIVENESS_ROUTE as o, C8Y_NITRO_TYPES_DIR as r, GENERATED_READINESS_ROUTE as s, createC8yManifest as t };
@@ -1,5 +1,6 @@
1
- import { c as getTenantOptionsByCategory, f as upsertTenantOption, i as deleteTenantOption, n as createBasicAuthHeader, p as createC8yManifest, s as getCurrentApplicationSettings } from "./c8y-api-DpCWhaJU.mjs";
2
- import { n as readBootstrapCredentialsFromEnv, r as syncBootstrappedMicroservice } from "./autoBootstrap-qCcCgset.mjs";
1
+ import { t as createC8yManifest } from "./manifest-DV3UW9Dl.mjs";
2
+ import { c as getTenantOptionsByCategory, f as upsertTenantOption, i as deleteTenantOption, n as createBasicAuthHeader, s as getCurrentApplicationSettings } from "./c8y-api-DSs1xIIh.mjs";
3
+ import { n as readBootstrapCredentialsFromEnv, r as syncBootstrappedMicroservice } from "./autoBootstrap-DUjHuHe_.mjs";
3
4
  import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
4
5
  import process from "node:process";
5
6
  import { defineCommand } from "citty";
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "c8y-nitro";
3
- var version = "0.9.1";
3
+ var version = "0.10.0";
4
4
  var description = "Lightning fast Cumulocity IoT microservice development powered by Nitro";
5
5
  //#endregion
6
6
  export { name as n, version as r, description as t };
@@ -1,4 +1,5 @@
1
- import { n as createBasicAuthHeader, p as createC8yManifest, t as assignUserRole, u as unassignUserRole } from "./c8y-api-DpCWhaJU.mjs";
1
+ import { t as createC8yManifest } from "./manifest-DV3UW9Dl.mjs";
2
+ import { n as createBasicAuthHeader, t as assignUserRole, u as unassignUserRole } from "./c8y-api-DSs1xIIh.mjs";
2
3
  import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
3
4
  import { defineCommand } from "citty";
4
5
  import { consola } from "consola";
@@ -1,5 +1,5 @@
1
1
  import { t as createError } from "../../logging-BUYT8Z2V.mjs";
2
- import { t as useDeployedTenantClient } from "../../client-DCov9jV6.mjs";
2
+ import { t as useDeployedTenantClient } from "../../client-BjA5bBxj.mjs";
3
3
  import { c8yTenantOptionKeys } from "c8y-nitro/runtime";
4
4
  import { defineCachedFunction } from "nitro/cache";
5
5
  import { useRuntimeConfig } from "nitro/runtime-config";
@@ -1,4 +1,4 @@
1
- import { r as tryGetUserTenantDomainFast } from "../../client-DCov9jV6.mjs";
1
+ import { r as tryGetUserTenantDomainFast } from "../../client-BjA5bBxj.mjs";
2
2
  import { c8yManifest } from "c8y-nitro/runtime";
3
3
  import { useRuntimeConfig } from "nitro/runtime-config";
4
4
  import { getRequestURL } from "nitro/h3";
@@ -1,4 +1,4 @@
1
- import { n as buildPublicRequestUrl, r as tryGetUserTenantDomainFast } from "../../client-DCov9jV6.mjs";
1
+ import { n as buildPublicRequestUrl, r as tryGetUserTenantDomainFast } from "../../client-BjA5bBxj.mjs";
2
2
  import { c8yManifest } from "c8y-nitro/runtime";
3
3
  import { defineMiddleware } from "nitro";
4
4
  //#region src/module/runtime/handlers/rewriteRequestUrl.ts
@@ -0,0 +1,30 @@
1
+ import { i as C8Y_NITRO_TYPES_FILE, r as C8Y_NITRO_TYPES_DIR } from "./manifest-DV3UW9Dl.mjs";
2
+ import { t as writeAPIClient } from "./apiClient-kxv5KHYJ.mjs";
3
+ import { resolve } from "pathe";
4
+ import process from "node:process";
5
+ import { defineCommand } from "citty";
6
+ import { consola } from "consola";
7
+ import { createNitro } from "nitro/builder";
8
+ //#region src/cli/commands/typegen.ts
9
+ var typegen_default = defineCommand({
10
+ meta: {
11
+ name: "typegen",
12
+ description: "Generate the c8y-nitro type declarations (replaces the removed `nitro prepare`)"
13
+ },
14
+ args: { dir: {
15
+ type: "positional",
16
+ description: "Project root directory",
17
+ required: false,
18
+ default: "."
19
+ } },
20
+ async run({ args }) {
21
+ const rootDir = resolve(process.cwd(), args.dir);
22
+ const nitro = await createNitro({ rootDir });
23
+ consola.success(`Generated types at ${C8Y_NITRO_TYPES_DIR}/${C8Y_NITRO_TYPES_FILE}`);
24
+ const c8yOptions = nitro.options.c8y ?? {};
25
+ if (c8yOptions.apiClient) await writeAPIClient(nitro, c8yOptions);
26
+ consola.info(`Add "${C8Y_NITRO_TYPES_DIR}/*.d.ts" to your tsconfig \`include\` if you have not already.`);
27
+ }
28
+ });
29
+ //#endregion
30
+ export { typegen_default as default };
package/dist/types.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { a as C8YTenantOptionKey, c as C8yDockerOptions, d as C8YManifest, f as C8YManifestOptions, g as C8YAPIClientOptions, h as C8YKnownRole, i as C8YSettingsCategory, l as C8yCacheOptions, m as C8YRoles, n as C8yNitroModuleOptions, o as C8YTenantOptionKeysCacheConfig, p as C8YRole, r as C8yOpenAPIOptions, s as C8yRealtimeOptions, t as C8yDevOptions, u as C8YZipOptions } from "./index-Dil1PcDr.mjs";
1
+ import { a as C8YTenantOptionKey, c as C8yDockerOptions, d as C8YManifest, f as C8YManifestOptions, g as C8YAPIClientOptions, h as C8YKnownRole, i as C8YSettingsCategory, l as C8yCacheOptions, m as C8YRoles, n as C8yNitroModuleOptions, o as C8YTenantOptionKeysCacheConfig, p as C8YRole, r as C8yOpenAPIOptions, s as C8yRealtimeOptions, t as C8yDevOptions, u as C8YZipOptions } from "./index-DHd7xpBX.mjs";
2
2
  import { t as TenantCredentials } from "./credentials-eKnJ7m9Y.mjs";
3
3
  export { C8YAPIClientOptions, type C8YKnownRole, type C8YManifest, type C8YManifestOptions, C8YRole, C8YRoles, C8YSettingsCategory, C8YTenantOptionKey, C8YTenantOptionKeysCacheConfig, type C8YZipOptions, type C8yCacheOptions, C8yDevOptions, type C8yDockerOptions, C8yNitroModuleOptions, C8yOpenAPIOptions, type C8yRealtimeOptions, type TenantCredentials };
package/dist/utils.mjs CHANGED
@@ -132,7 +132,8 @@ defineCachedFunction(async (requestOrEvent, _tenantId) => {
132
132
  * const log = useLogger(useRequest())
133
133
  */
134
134
  function useLogger(eventOrRequest, service) {
135
- return useLogger$1("req" in eventOrRequest ? eventOrRequest : { req: eventOrRequest }, service);
135
+ const event = "req" in eventOrRequest ? eventOrRequest : { req: eventOrRequest };
136
+ return useLogger$1(event, service);
136
137
  }
137
138
  /**
138
139
  * Create a structured error for a Cumulocity microservice. Always prefer this
@@ -324,7 +325,9 @@ async function useUserTenantCredentials(requestOrEvent) {
324
325
  function useUserClient(requestOrEvent) {
325
326
  const request = "req" in requestOrEvent ? requestOrEvent.req : requestOrEvent;
326
327
  if (request.context?.["c8y_user_client"]) return request.context["c8y_user_client"];
327
- const client = new Client(new MicroserviceClientRequestAuth(convertRequestHeadersToC8yFormat(request)), process.env.C8Y_BASEURL);
328
+ const headers = convertRequestHeadersToC8yFormat(request);
329
+ const auth = new MicroserviceClientRequestAuth(headers);
330
+ const client = new Client(auth, process.env.C8Y_BASEURL);
328
331
  request.context ??= {};
329
332
  request.context["c8y_user_client"] = client;
330
333
  return client;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8y-nitro",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Lightning fast Cumulocity IoT microservice development powered by Nitro",
6
6
  "keywords": [
@@ -38,18 +38,18 @@
38
38
  "dist"
39
39
  ],
40
40
  "dependencies": {
41
- "c12": "^4.0.0-beta.5",
41
+ "c12": "^4.0.0-rc.1",
42
42
  "c8y-realtime": "^0.2.2",
43
43
  "citty": "^0.2.2",
44
44
  "consola": "^3.4.2",
45
45
  "croner": "^10.0.1",
46
- "evlog": "^2.21.0",
46
+ "evlog": "^2.28.1",
47
47
  "itty-time": "^2.0.2",
48
48
  "jszip": "^3.10.1",
49
49
  "pathe": "^2.0.3",
50
- "pkg-types": "^2.3.1",
50
+ "pkg-types": "^2.3.2",
51
51
  "spinnies": "^0.5.1",
52
- "tinyexec": "^1.2.4",
52
+ "tinyexec": "^1.3.1",
53
53
  "ws": "^8.21.3"
54
54
  },
55
55
  "devDependencies": {
@@ -58,20 +58,20 @@
58
58
  "@types/node": "^24.12.3",
59
59
  "@types/spinnies": "^0.5.3",
60
60
  "@types/ws": "^8.18.1",
61
- "bumpp": "^11.1.0",
62
- "changelogithub": "^14.0.0",
63
- "eslint": "^10.7.0",
64
- "memfs": "^4.64.0",
65
- "tsdown": "^0.22.8",
66
- "tsnapi": "^1.0.0",
61
+ "bumpp": "^12.3.0",
62
+ "changelogithub": "^15.0.5",
63
+ "eslint": "^10.9.1",
64
+ "memfs": "^4.69.1",
65
+ "tsdown": "^0.22.14",
66
+ "tsnapi": "^1.4.2",
67
67
  "typescript": "^6.0.3",
68
68
  "vitepress": "2.0.0-alpha.17",
69
69
  "vitepress-plugin-llms": "1.13.3",
70
- "vitest": "^4.1.10"
70
+ "vitest": "^4.1.11"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "@c8y/client": ">=1021",
74
- "nitro": "3.0.260429-beta"
74
+ "nitro": "3.0.260903-beta"
75
75
  },
76
76
  "engines": {
77
77
  "node": ">=24.0.0"