c8y-nitro 0.4.2 → 0.6.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # c8y-nitro
2
2
 
3
- Lightning fast Cumulocity IoT microservice development powered by [Nitro](https://v3.nitro.build).
3
+ Nitro-powered tooling for building Cumulocity IoT microservices without manually stitching together bootstrap, manifest generation, packaging, and tenant-aware runtime helpers.
4
4
 
5
5
  ## Features
6
6
 
@@ -14,514 +14,37 @@ Lightning fast Cumulocity IoT microservice development powered by [Nitro](https:
14
14
  - 🛠️ **TypeScript First** - Full type safety with excellent DX
15
15
  - 🔄 **Auto-Bootstrap** - Automatically registers and configures your microservice in development
16
16
 
17
- ## Quick Start
17
+ ## Quickstart
18
18
 
19
- The fastest way to get started is using the [c8y-nitro-starter](https://github.com/schplitt/c8y-nitro-starter) template:
19
+ Start with the [Quickstart docs](https://schplitt.github.io/c8y-nitro/quickstart).
20
20
 
21
- ```sh
22
- pnpm dlx giget@latest gh:schplitt/c8y-nitro-starter my-microservice
23
- cd my-microservice
24
- pnpm install
25
- ```
26
-
27
- Configure your development tenant in `.env`:
28
-
29
- ```sh
30
- C8Y_BASEURL=https://your-tenant.cumulocity.com
31
- C8Y_DEVELOPMENT_TENANT=t12345
32
- C8Y_DEVELOPMENT_USER=your-username
33
- C8Y_DEVELOPMENT_PASSWORD=your-password
34
- ```
35
-
36
- Then start developing:
37
-
38
- ```sh
39
- pnpm dev
40
- ```
41
-
42
- ## Installation
43
-
44
- ```sh
45
- pnpm add c8y-nitro nitro@latest
46
- ```
47
-
48
- ## Usage
49
-
50
- Configure your Cumulocity microservice in `nitro.config.ts`:
51
-
52
- ```ts
53
- import c8y from 'c8y-nitro'
54
-
55
- export default defineNitroConfig({
56
- preset: 'node-server', // or "node-cluster", Required!
57
- c8y: {
58
- // c8y-nitro configuration options go here
59
- },
60
- modules: [c8y()],
61
- })
62
- ```
63
-
64
- ### Prerequisites
65
-
66
- `c8y-nitro` requires:
67
-
68
- - `preset` - must be a node preset (`node-server` or `node-cluster`)
69
-
70
- **Recommended:**
71
-
72
- - `builder: 'rolldown'` - for faster build times
73
-
74
- **Optional:**
75
-
76
- - `experimental.asyncContext: true` - enables using utility functions without passing event/request parameters
77
-
78
- ## Getting Started
79
-
80
- Create a `.env` or `.env.local` file with your development tenant credentials:
81
-
82
- ```sh
83
- C8Y_BASEURL=https://your-tenant.cumulocity.com
84
- C8Y_DEVELOPMENT_TENANT=t12345
85
- C8Y_DEVELOPMENT_USER=your-username
86
- C8Y_DEVELOPMENT_PASSWORD=your-password
87
- ```
88
-
89
- Then simply run `pnpm dev` - that's it! The module will automatically:
90
-
91
- 1. Check if the microservice exists on the tenant
92
- 2. Create it if needed (or use existing one without overwriting)
93
- 3. Subscribe your tenant to the microservice
94
- 4. Retrieve and save bootstrap credentials to your env file
95
-
96
- After auto-bootstrap, your env file will contain:
97
-
98
- ```sh
99
- C8Y_BOOTSTRAP_TENANT=<bootstrap-tenant-id>
100
- C8Y_BOOTSTRAP_USER=<bootstrap-username>
101
- C8Y_BOOTSTRAP_PASSWORD=<generated-password>
102
- ```
103
-
104
- > **Manual Bootstrap**: For more control or troubleshooting, you can use the [CLI bootstrap command](#cli-commands) to manually register your microservice.
105
-
106
- > **Disable Auto-Bootstrap**: Set `skipBootstrap: true` in your c8y config to disable auto-bootstrap entirely. This is useful in CI/CD pipelines or when you want to manage bootstrap manually.
107
-
108
- ## Automatic Zip Creation
109
-
110
- `c8y-nitro` automatically generates a ready-to-deploy microservice zip package after each build. The process includes:
111
-
112
- 1. **Dockerfile Generation** - Creates an optimized Dockerfile using Node.js 22-slim
113
- 2. **Docker Image Build** - Builds and saves the Docker image to `image.tar`
114
- 3. **Manifest Generation** - Creates `cumulocity.json` from your package.json and configuration
115
- 4. **Zip Package** - Combines `image.tar` and `cumulocity.json` into a deployable zip file
116
-
117
- > **Note**: Docker must be installed and available in your PATH.
118
-
119
- The generated zip file (default: `<package-name>-<version>.zip` in root directory) is ready to upload directly to Cumulocity.
120
-
121
- ## Manifest Configuration
122
-
123
- The `cumulocity.json` manifest is automatically generated from your `package.json` and can be customized via the `manifest` option.
124
-
125
- **Auto-generated from package.json:**
126
-
127
- - `name` (scope stripped), `version` - from package fields
128
- - `provider.name` - from `author` field
129
- - `provider.domain` - from `author.url` or `homepage`
130
- - `provider.support` - from `bugs` or `author.email`
131
- - `contextPath` - defaults to package name
132
-
133
- For all available manifest options, see the [Cumulocity Microservice Manifest documentation](https://cumulocity.com/docs/microservice-sdk/general-aspects/#microservice-manifest).
134
-
135
- > **Note**: Custom roles defined in the manifest are automatically available as TypeScript types for use in middleware and runtime code during development.
136
-
137
- > **Note**: Health probe endpoints (`/_c8y_nitro/liveness` and `/_c8y_nitro/readiness`) are automatically injected if not manually defined.
138
-
139
- ## Cache Configuration
140
-
141
- Credential caching can be configured to optimize performance. By default, subscribed tenant credentials are cached for 10 minutes.
142
-
143
- ```ts
144
- export default defineNitroConfig({
145
- c8y: {
146
- cache: {
147
- credentialsTTL: 300, // Cache credentials for 5 minutes (in seconds)
148
- defaultTenantOptionsTTL: 600, // Default cache for tenant options (in seconds)
149
- tenantOptions: {
150
- 'myOption': 300, // Per-key override: 5 minutes
151
- 'credentials.secret': 60, // Per-key override: 1 minute
152
- },
153
- }
154
- },
155
- modules: [c8y()],
156
- })
157
- ```
158
-
159
- You can also override these at runtime using environment variables:
160
-
161
- ```sh
162
- NITRO_C8Y_CREDENTIALS_CACHE_TTL=300
163
- NITRO_C8Y_DEFAULT_TENANT_OPTIONS_TTL=300
164
- ```
165
-
166
- > **Note**: The credentials cache is used by `useSubscribedTenantCredentials()` and `useDeployedTenantCredentials()` utilities. Both share the same cache.
167
-
168
- ## Development User Injection
169
-
170
- During development, `c8y-nitro` automatically injects your development user credentials into all requests. This allows you to test authentication and authorization middlewares locally.
171
-
172
- The module uses the development credentials from your `.env` file:
173
-
174
- ```sh
175
- C8Y_DEVELOPMENT_TENANT=t12345
176
- C8Y_DEVELOPMENT_USER=your-username
177
- C8Y_DEVELOPMENT_PASSWORD=your-password
178
- ```
179
-
180
- This enables testing of access control middlewares like `hasUserRequiredRole()` and `isUserFromAllowedTenant()` without needing to manually set authorization headers.
181
-
182
- ### Managing Development User Roles
183
-
184
- Use the [CLI roles command](#cli-commands) to assign or remove your microservice's custom roles to your development user:
185
-
186
- ```sh
187
- pnpm dlx c8y-nitro roles
188
- ```
189
-
190
- This interactive command lets you select which roles from your manifest to assign to your development user for testing.
191
-
192
- ## API Client Generation
193
-
194
- For monorepo architectures, `c8y-nitro` can generate TypeScript Angular services that provide fully typed access to your microservice routes.
195
-
196
- ### Configuration
197
-
198
- ```ts
199
- export default defineNitroConfig({
200
- c8y: {
201
- apiClient: {
202
- dir: '../ui/src/app/services', // Output directory for generated client
203
- contextPath: 'my-service' // Optional: override context path
204
- }
205
- },
206
- modules: [c8y()],
207
- })
208
- ```
209
-
210
- ### Generated Client
211
-
212
- The generated service creates one method per route with automatic type inference:
213
-
214
- ```ts
215
- // Generated: my-serviceAPIClient.ts
216
- @Injectable({ providedIn: 'root' })
217
- export class GeneratedMyServiceAPIClient {
218
- async GETHealth(): Promise<{ status: string }> { }
219
- async GETUsersById(params: { id: string | number }): Promise<User> { }
220
- async POSTUsers(body: CreateUserDto): Promise<User> { }
221
- }
222
- ```
223
-
224
- ### Usage in Angular
225
-
226
- ```ts
227
- import { GeneratedMyServiceAPIClient } from './services/my-serviceAPIClient'
228
-
229
- @Component({
230
- /**
231
- * ...
232
- */
233
- })
234
- export class MyComponent {
235
- private api = inject(GeneratedMyServiceAPIClient)
236
-
237
- async ngOnInit() {
238
- const health = await this.api.GETHealth()
239
- const user = await this.api.GETUsersById({ id: 123 })
240
- }
241
- }
242
- ```
243
-
244
- > **Note**: The client regenerates automatically when routes change during development.
245
-
246
- ## Logging
247
-
248
- `c8y-nitro` builds on [evlog](https://www.evlog.dev) to provide structured **wide-event logging**, one comprehensive log per request that accumulates all relevant context rather than scattering individual log lines throughout your code.
249
-
250
- evlog is automatically configured, no extra setup required. The service name is derived from your package name.
251
-
252
- ### useLogger
253
-
254
- Use `useLogger(event)` in your route handlers to get a request-scoped logger. The logger accumulates context throughout the request lifetime and emits a single wide event when the response is sent.
255
-
256
- ```ts
257
- import { defineHandler } from 'nitro/h3'
258
- import { useLogger } from 'c8y-nitro/utils'
259
-
260
- export default defineHandler(async (event) => {
261
- const log = useLogger(event)
262
-
263
- const user = await useUser(event)
264
- log.set({ action: 'process-order', user: { id: user.userName } })
265
-
266
- // Add more context as it becomes available
267
- log.set({ order: { id: '42', total: 9999 } })
268
-
269
- return { success: true }
270
- })
271
- ```
272
-
273
- > **Note**: `useLogger` requires the `event` parameter. If you enable `experimental.asyncContext: true` in your Nitro config, you can access the logger anywhere in the call stack via `useRequest()` from `nitro/context` — see the [evlog Nitro v3 setup](https://www.evlog.dev/getting-started/installation#nitro-v3) for details.
274
-
275
- ### createError
276
-
277
- Use `createError` from `c8y-nitro/utils` instead of Nitro's built-in error helper to get richer, structured error responses. This adds `why`, `fix`, and `link` fields that are:
278
-
279
- - Logged as part of the wide event so you can see exactly what went wrong without guessing
280
- - Returned in the JSON response body so clients can display actionable context
281
-
282
- ```ts
283
- import { defineHandler } from 'nitro/h3'
284
- import { useLogger, createError } from 'c8y-nitro/utils'
285
-
286
- export default defineHandler(async (event) => {
287
- const log = useLogger(event)
288
- log.set({ action: 'payment', userId: 'user_123' })
289
-
290
- throw createError({
291
- message: 'Payment failed',
292
- status: 402,
293
- why: 'Card declined by issuer (insufficient funds)',
294
- fix: 'Try a different payment method or contact your bank',
295
- link: 'https://docs.example.com/payments/declined',
296
- })
297
- })
298
- ```
299
-
300
- The error response returned to the client:
301
-
302
- ```json
303
- {
304
- "message": "Payment failed",
305
- "status": 402,
306
- "data": {
307
- "why": "Card declined by issuer (insufficient funds)",
308
- "fix": "Try a different payment method or contact your bank",
309
- "link": "https://docs.example.com/payments/declined"
310
- }
311
- }
312
- ```
313
-
314
- > **Tip**: Always prefer `createError` from `c8y-nitro/utils`. It ensures the error is captured in the wide log event with full context, making investigation straightforward.
315
-
316
- ### createLogger (standalone)
317
-
318
- For code that runs **outside a request handler** — background jobs, queue workers, event-driven workflows, scheduled tasks — use `createLogger()` to get the same wide-event logger interface without needing an HTTP event.
319
-
320
- ```ts
321
- import { createLogger } from 'c8y-nitro/utils'
322
-
323
- export async function processSubscriptionRenewal(tenantId: string) {
324
- const log = createLogger({ job: 'subscription-renewal', tenantId })
325
-
326
- log.set({ subscription: { id: 'sub_123', plan: 'pro' } })
327
-
328
- // ... do work ...
329
-
330
- log.set({ result: 'renewed' })
331
- log.emit() // Must call emit() manually outside request context
332
- }
333
- ```
334
-
335
- This is useful for Cumulocity notification workflows where your microservice reacts to platform events (device management, alarms, etc.) outside of the standard request/response cycle.
336
-
337
- > **Note**: Unlike `useLogger`, `createLogger` does **not** auto-emit at request end. You must call `log.emit()` manually when the work is complete.
338
-
339
- For more on wide events, structured errors, and advanced configuration (sampling, draining to Axiom/Loki, enrichers), see the [evlog documentation](https://www.evlog.dev/core-concepts/wide-events).
340
-
341
- ### Logging Utilities
342
-
343
- | Function | Description | Request Context |
344
- | ---------------------- | ------------------------------------------------------------- | :-------------: |
345
- | `useLogger(event)` | Get the request-scoped wide-event logger | ✅ |
346
- | `createLogger(ctx?)` | Create a standalone wide-event logger; call `emit()` manually | ❌ |
347
- | `createError(options)` | Create a structured error with `why`, `fix`, `link` | ❌ |
348
-
349
- ## Utilities
350
-
351
- `c8y-nitro` provides several utility functions to simplify common tasks in Cumulocity microservices.
352
-
353
- To use these utilities, simply import them from `c8y-nitro/utils`:
354
-
355
- ```ts
356
- import { useUser, useUserClient } from 'c8y-nitro/utils'
357
- ```
358
-
359
- ### Usage
21
+ ## Documentation
360
22
 
361
- All utility functions that require request context accept either an `H3Event` or `ServerRequest` parameter:
23
+ Full documentation is available at [schplitt.github.io/c8y-nitro](https://schplitt.github.io/c8y-nitro/).
362
24
 
363
- ```ts
364
- // Pass the event/request parameter
365
- export default defineHandler(async (event) => {
366
- const user = await useUser(event)
367
- const client = useUserClient(event)
368
- return { user }
369
- })
370
- ```
371
-
372
- **Optional: Using with `asyncContext`**
373
-
374
- If you enable `experimental.asyncContext: true` in your Nitro config, you can use Nitro's `useRequest()` to avoid passing the event through deeply nested function calls:
375
-
376
- ```ts
377
- import { useRequest } from 'nitro/context'
378
-
379
- export default defineHandler(async (event) => {
380
- // Deep nested function - no need to pass event down
381
- return await someDeepFunction()
382
- })
383
-
384
- async function someDeepFunction() {
385
- return await anotherFunction()
386
- }
387
-
388
- async function anotherFunction() {
389
- // Use useRequest() to get the request in any nested function
390
- const request = useRequest()
391
- const user = await useUser(request)
392
- return { user }
393
- }
394
- ```
395
-
396
- ### Credentials
397
-
398
- | Function | Description | Request Context |
399
- | ---------------------------------- | ------------------------------------------------------------------ | :-------------: |
400
- | `useSubscribedTenantCredentials()` | Get credentials for all subscribed tenants (cached, default 10min) | ❌ |
401
- | `useDeployedTenantCredentials()` | Get credentials for the deployed tenant (cached, default 10min) | ❌ |
402
- | `useUserTenantCredentials()` | Get credentials for the current user's tenant | ✅ |
403
-
404
- > **Note**: `useDeployedTenantCredentials()` shares its cache with `useSubscribedTenantCredentials()`. Both functions support `.invalidate()` and `.refresh()` methods. Invalidating or refreshing one will affect the other.
405
- >
406
- > **Cache Duration**: The cache TTL is configurable via the `cache.credentialsTTL` option or `NITRO_C8Y_CREDENTIALS_CACHE_TTL` environment variable. See [Cache Configuration](#cache-configuration) for details.
407
-
408
- ### Tenant Options
409
-
410
- | Function | Description | Request Context |
411
- | ------------------- | -------------------------------------------------------- | :-------------: |
412
- | `useTenantOption()` | Get a tenant option value by key (cached, default 10min) | ❌ |
413
-
414
- Fetch tenant options (settings) configured for your microservice:
415
-
416
- ```ts
417
- import { useTenantOption } from 'c8y-nitro/utils'
418
-
419
- export default defineHandler(async (event) => {
420
- // Fetch a tenant option
421
- const value = await useTenantOption('myOption')
422
-
423
- // Fetch an encrypted secret
424
- const secret = await useTenantOption('credentials.apiKey')
425
-
426
- // Cache management
427
- await useTenantOption.invalidate('myOption') // Invalidate specific key
428
- const fresh = await useTenantOption.refresh('myOption') // Force refresh
429
- await useTenantOption.invalidateAll() // Invalidate all accessed keys
430
- await useTenantOption.refreshAll() // Refresh all accessed keys
431
- return { value, secret, fresh }
432
- })
433
- ```
434
-
435
- Define your settings in the manifest to get type-safe keys:
436
-
437
- ```ts
438
- export default defineNitroConfig({
439
- c8y: {
440
- manifest: {
441
- settings: [
442
- { key: 'myOption', defaultValue: 'default' },
443
- { key: 'credentials.secret' }, // Encrypted option
444
- ],
445
- settingsCategory: 'my-service', // Optional, defaults to contextPath/name
446
- requiredRoles: ['ROLE_OPTION_MANAGEMENT_READ'], // Required for reading tenant options
447
- },
448
- },
449
- modules: [c8y()],
450
- })
451
- ```
25
+ Useful entry points:
452
26
 
453
- > **Important**: To read tenant options, your microservice **must** have the `ROLE_OPTION_MANAGEMENT_READ` role in `manifest.requiredRoles`. Without this role, API calls will fail with a 403 Forbidden error.
454
-
455
- > **Note on Encrypted Options**: Keys prefixed with `credentials.` are stored encrypted by Cumulocity. The value is automatically decrypted when fetched if your microservice is the owner of the option (the option's category matches your microservice's `settingsCategory`, `contextPath`, or name). The `credentials.` prefix is automatically stripped when calling the API.
456
-
457
- > **Note on Missing Options**: If a tenant option is not set (404 Not Found), `useTenantOption()` returns `undefined` instead of throwing an error. Other errors (e.g., 403 Forbidden) are thrown normally.
458
-
459
- ### Resources
460
-
461
- | Function | Description | Request Context |
462
- | ---------------- | ---------------------------------- | :-------------: |
463
- | `useUser()` | Fetch current user from Cumulocity | ✅ |
464
- | `useUserRoles()` | Get roles of the current user | ✅ |
465
-
466
- ### Client
467
-
468
- | Function | Description | Request Context |
469
- | ------------------------------ | --------------------------------------------------- | :-------------: |
470
- | `useUserClient()` | Create client authenticated with user's credentials | ✅ |
471
- | `useUserTenantClient()` | Create client for user's tenant (microservice user) | ✅ |
472
- | `useSubscribedTenantClients()` | Create clients for all subscribed tenants | ❌ |
473
- | `useDeployedTenantClient()` | Create client for the deployed tenant | ❌ |
474
-
475
- ### Middleware
476
-
477
- | Function | Description | Request Context |
478
- | ------------------------------------------ | ----------------------------------------- | :-------------: |
479
- | `hasUserRequiredRole(role\|roles)` | Check if user has required role(s) | ✅ |
480
- | `isUserFromAllowedTenant(tenant\|tenants)` | Check if user is from allowed tenant(s) | ✅ |
481
- | `isUserFromDeployedTenant()` | Check if user is from the deployed tenant | ✅ |
482
-
483
- ## CLI Commands
484
-
485
- | Command | Description |
486
- | ----------- | ------------------------------------------------------- |
487
- | `bootstrap` | Manually register microservice and retrieve credentials |
488
- | `roles` | Manage development user roles |
489
- | `options` | Manage tenant options on development tenant |
490
-
491
- For more information, run:
492
-
493
- ```sh
494
- pnpm dlx c8y-nitro -h
495
- ```
27
+ - [What is c8y-nitro?](https://schplitt.github.io/c8y-nitro/guide/what-is-c8y-nitro)
28
+ - [Configuration](https://schplitt.github.io/c8y-nitro/guide/configuration)
29
+ - [Module Options](https://schplitt.github.io/c8y-nitro/reference/module-options)
30
+ - [Utilities](https://schplitt.github.io/c8y-nitro/reference/utilities)
496
31
 
497
32
  ## Development
498
33
 
34
+ For contributors working on this repository:
35
+
499
36
  ```sh
500
- # Install dependencies
501
37
  pnpm install
502
-
503
- # Run dev watcher
504
38
  pnpm dev
505
-
506
- # Build for production
507
39
  pnpm build
508
-
509
- # Run tests (watch mode)
510
- pnpm test
511
-
512
- # Run tests once
513
40
  pnpm test:run
41
+ pnpm lint
42
+ pnpm typecheck
43
+ pnpm docs:build
514
44
  ```
515
45
 
516
- ### Testing
517
-
518
- Tests are organized in two categories:
519
-
520
- - **Unit tests** (`tests/unit/`) — Test individual functions in isolation
521
- - **Server tests** (`tests/server/`) — Integration tests that spin up a Nitro dev server with the c8y-nitro module
522
-
523
- Server tests use Nitro's virtual modules to mock `@c8y/client` at build time, allowing full integration testing without real Cumulocity API calls. See [AGENTS.md](AGENTS.md#server-integration-tests) for implementation details.
46
+ Use `pnpm dev` for the package watcher, `pnpm docs:dev` for the VitePress site, and check the full contributor-facing behavior in the docs plus <AGENTS.md>.
524
47
 
525
48
  ## License
526
49
 
527
- MIT
50
+ Use `pnpm dev` for the package watcher, `pnpm docs:dev` for the VitePress site, and check the full contributor-facing behavior in the docs plus <AGENTS.md>.
@@ -1,6 +1,6 @@
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-BBSKRwKs.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-BbRS1-Ls.mjs";
2
2
  import { t as writeBootstrapCredentials } from "./env-file-B0BK-uZW.mjs";
3
- import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-Dqi-ttQi.mjs";
3
+ import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BRnvtthI.mjs";
4
4
  import { defineCommand, runCommand } from "citty";
5
5
  import { consola } from "consola";
6
6
  //#region src/cli/commands/bootstrap.ts
@@ -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-DJxp2d8p.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-D7rpbxPp.mjs").then((r) => r.default), { rawArgs: [] });
56
56
  }
57
57
  consola.success("Bootstrap complete!");
58
58
  }
@@ -3,8 +3,17 @@ import { Buffer } from "node:buffer";
3
3
  //#region src/module/constants.ts
4
4
  const GENERATED_LIVENESS_ROUTE = "/_c8y_nitro/liveness";
5
5
  const GENERATED_READINESS_ROUTE = "/_c8y_nitro/readiness";
6
+ const GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE = "/_c8y_nitro/invalidate-tenant-options";
6
7
  //#endregion
7
8
  //#region src/module/manifest.ts
9
+ const ROLE_OPTION_MANAGEMENT_READ = "ROLE_OPTION_MANAGEMENT_READ";
10
+ const ROLE_OPTION_MANAGEMENT_ADMIN = "ROLE_OPTION_MANAGEMENT_ADMIN";
11
+ function validateManifestSettings(options) {
12
+ const invalidSettings = options.settings?.filter((setting) => typeof setting.defaultValue !== "string" || setting.defaultValue.length === 0) ?? [];
13
+ if (invalidSettings.length === 0) return;
14
+ const invalidKeys = invalidSettings.map((setting) => `"${setting.key}"`).join(", ");
15
+ throw new Error(`manifest.settings entries must define a non-empty defaultValue. Invalid keys: ${invalidKeys}`);
16
+ }
8
17
  async function readPackageJsonFieldsForManifest(rootDir, logger) {
9
18
  logger?.debug(`Reading package file from ${rootDir}`);
10
19
  const pkg = await readPackage(rootDir);
@@ -37,6 +46,7 @@ async function readPackageJsonFieldsForManifest(rootDir, logger) {
37
46
  * @param logger - Optional logger for debug output
38
47
  */
39
48
  async function createC8yManifest(rootDir, options = {}, logger) {
49
+ validateManifestSettings(options);
40
50
  const { name, version, provider, ...restManifestFields } = await readPackageJsonFieldsForManifest(rootDir, logger);
41
51
  const probeFields = {};
42
52
  if (!options.livenessProbe?.httpGet) probeFields.livenessProbe = {
@@ -48,10 +58,16 @@ async function createC8yManifest(rootDir, options = {}, logger) {
48
58
  httpGet: { path: GENERATED_READINESS_ROUTE }
49
59
  };
50
60
  const key = `${name}-key`;
61
+ let requiredRoles = options.requiredRoles ? [...options.requiredRoles] : void 0;
62
+ if (options.settings && options.settings.length > 0 && !requiredRoles?.includes(ROLE_OPTION_MANAGEMENT_READ) && !requiredRoles?.includes(ROLE_OPTION_MANAGEMENT_ADMIN)) {
63
+ requiredRoles = [...requiredRoles ?? [], ROLE_OPTION_MANAGEMENT_READ];
64
+ logger?.debug(`Auto-added ${ROLE_OPTION_MANAGEMENT_READ} to requiredRoles because manifest.settings are defined`);
65
+ }
51
66
  const manifest = {
52
67
  ...restManifestFields,
53
68
  ...probeFields,
54
69
  ...options,
70
+ ...requiredRoles !== void 0 ? { requiredRoles } : {},
55
71
  provider,
56
72
  name,
57
73
  version,
@@ -286,7 +302,35 @@ async function getTenantOption(baseUrl, category, key, authHeader) {
286
302
  return (await response.json()).value;
287
303
  }
288
304
  /**
289
- * Updates or creates a tenant option.
305
+ * Creates a tenant option.
306
+ * @param baseUrl - The Cumulocity base URL
307
+ * @param category - The category of the option
308
+ * @param key - The option key
309
+ * @param value - The value to set
310
+ * @param authHeader - The Basic Auth header
311
+ */
312
+ async function createTenantOption(baseUrl, category, key, value, authHeader) {
313
+ const url = `${baseUrl}/tenant/options`;
314
+ const response = await fetch(url, {
315
+ method: "POST",
316
+ headers: {
317
+ "Authorization": authHeader,
318
+ "Content-Type": "application/vnd.com.nsn.cumulocity.option+json",
319
+ "Accept": "application/json"
320
+ },
321
+ body: JSON.stringify({
322
+ category,
323
+ key,
324
+ value
325
+ })
326
+ });
327
+ if (!response.ok) {
328
+ const errorText = await response.text();
329
+ throw new Error(`Failed to create tenant option ${category}/${key}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
330
+ }
331
+ }
332
+ /**
333
+ * Updates an existing tenant option.
290
334
  * @param baseUrl - The Cumulocity base URL
291
335
  * @param category - The category of the option
292
336
  * @param key - The option key
@@ -310,6 +354,25 @@ async function updateTenantOption(baseUrl, category, key, value, authHeader) {
310
354
  }
311
355
  }
312
356
  /**
357
+ * Updates a tenant option if it exists, otherwise creates it.
358
+ * @param baseUrl - The Cumulocity base URL
359
+ * @param category - The category of the option
360
+ * @param key - The option key
361
+ * @param value - The new value to set
362
+ * @param authHeader - The Basic Auth header
363
+ */
364
+ async function upsertTenantOption(baseUrl, category, key, value, authHeader) {
365
+ try {
366
+ await updateTenantOption(baseUrl, category, key, value, authHeader);
367
+ } catch (error) {
368
+ if (error.cause?.status === 404) {
369
+ await createTenantOption(baseUrl, category, key, value, authHeader);
370
+ return;
371
+ }
372
+ throw error;
373
+ }
374
+ }
375
+ /**
313
376
  * Deletes a tenant option.
314
377
  * @param baseUrl - The Cumulocity base URL
315
378
  * @param category - The category of the option
@@ -329,4 +392,4 @@ async function deleteTenantOption(baseUrl, category, key, authHeader) {
329
392
  }
330
393
  }
331
394
  //#endregion
332
- export { findMicroserviceByName as a, getTenantOptionsByCategory as c, updateMicroservice as d, updateTenantOption as f, GENERATED_READINESS_ROUTE as g, GENERATED_LIVENESS_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 };
395
+ 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 };
@@ -1,4 +1,4 @@
1
- import { n as name, r as version, t as description } from "../package-C4HtuVu_.mjs";
1
+ import { n as name, r as version, t as description } from "../package-DsLC9Mo3.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-BqWPkH8q.mjs").then((r) => r.default),
12
- roles: () => import("../roles-DJxp2d8p.mjs").then((r) => r.default),
13
- options: () => import("../options-BDDJWdph.mjs").then((r) => r.default)
11
+ bootstrap: () => import("../bootstrap-DUDpmXcU.mjs").then((r) => r.default),
12
+ roles: () => import("../roles-D7rpbxPp.mjs").then((r) => r.default),
13
+ options: () => import("../options-Web5UyIU.mjs").then((r) => r.default)
14
14
  }
15
15
  }));
16
16
  //#endregion