c8y-nitro 0.5.0 → 0.7.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 +17 -514
- package/dist/{bootstrap-BSdVwFWO.mjs → bootstrap-ObaEF7rN.mjs} +6 -6
- package/dist/{c8y-api-BgTNTqHd.mjs → c8y-api-BFjK6AYb.mjs} +42 -6
- package/dist/cli/index.mjs +4 -4
- package/dist/credentials-eKnJ7m9Y.d.mts +5 -0
- package/dist/{index-Bvu7DqDt.d.mts → index-ByDSMEvU.d.mts} +21 -5
- package/dist/index.d.mts +7 -2
- package/dist/index.mjs +163 -35
- package/dist/{options-BkWL1FOa.mjs → options-B7_yb_wr.mjs} +4 -4
- package/dist/{package-bfMasPPg.mjs → package-BhFLRPOb.mjs} +1 -1
- package/dist/{roles-CoBPqvDv.mjs → roles-B58Fguop.mjs} +4 -4
- package/dist/runtime/handlers/invalidateTenantOptions.mjs +616 -5
- package/dist/runtime/middlewares/c8y-client.mjs +2 -2
- package/dist/runtime/middlewares/dev-user.dev.mjs +2 -2
- package/dist/runtime/plugins/c8y-variables.dev.mjs +1 -1
- package/dist/types.d.mts +3 -2
- package/dist/utils.d.mts +309 -63
- package/dist/utils.mjs +577 -114
- package/package.json +23 -15
- /package/dist/{config-BRnvtthI.mjs → config-BVQHqlgV.mjs} +0 -0
- /package/dist/{env-file-B0BK-uZW.mjs → env-file-BrOTmVjA.mjs} +0 -0
- /package/dist/{logging-CVlaRS4O.mjs → logging-DYnYICoX.mjs} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# c8y-nitro
|
|
2
2
|
|
|
3
|
-
|
|
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,534 +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
|
-
##
|
|
17
|
+
## Quickstart
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Start with the [Quickstart docs](https://schplitt.github.io/c8y-nitro/quickstart).
|
|
20
20
|
|
|
21
|
-
|
|
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
|
-
enableTenantOptionsInvalidationRoute: false,
|
|
147
|
-
cache: {
|
|
148
|
-
credentialsTTL: 300, // Cache credentials for 5 minutes (in seconds)
|
|
149
|
-
defaultTenantOptionsTTL: 600, // Default cache for tenant options (in seconds)
|
|
150
|
-
tenantOptions: {
|
|
151
|
-
'myOption': 300, // Per-key override: 5 minutes
|
|
152
|
-
'credentials.secret': 60, // Per-key override: 1 minute
|
|
153
|
-
},
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
modules: [c8y()],
|
|
157
|
-
})
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
You can also override these at runtime using environment variables:
|
|
161
|
-
|
|
162
|
-
```sh
|
|
163
|
-
NITRO_C8Y_CREDENTIALS_CACHE_TTL=300
|
|
164
|
-
NITRO_C8Y_DEFAULT_TENANT_OPTIONS_TTL=300
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
> **Note**: The credentials cache is used by `useSubscribedTenantCredentials()` and `useDeployedTenantCredentials()` utilities. Both share the same cache.
|
|
168
|
-
|
|
169
|
-
## Development User Injection
|
|
170
|
-
|
|
171
|
-
During development, `c8y-nitro` automatically injects your development user credentials into all requests. This allows you to test authentication and authorization middlewares locally.
|
|
172
|
-
|
|
173
|
-
The module uses the development credentials from your `.env` file:
|
|
174
|
-
|
|
175
|
-
```sh
|
|
176
|
-
C8Y_DEVELOPMENT_TENANT=t12345
|
|
177
|
-
C8Y_DEVELOPMENT_USER=your-username
|
|
178
|
-
C8Y_DEVELOPMENT_PASSWORD=your-password
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
This enables testing of access control middlewares like `hasUserRequiredRole()` and `isUserFromAllowedTenant()` without needing to manually set authorization headers.
|
|
182
|
-
|
|
183
|
-
If you run a local proxy that already forwards a user session or authorization header, disable this middleware:
|
|
184
|
-
|
|
185
|
-
```ts
|
|
186
|
-
export default defineNitroConfig({
|
|
187
|
-
c8y: {
|
|
188
|
-
dev: {
|
|
189
|
-
injectUser: false,
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
modules: [c8y()],
|
|
193
|
-
})
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
When disabled, `c8y-nitro` does not register the development user injection middleware, so incoming auth headers stay untouched.
|
|
197
|
-
|
|
198
|
-
### Managing Development User Roles
|
|
199
|
-
|
|
200
|
-
Use the [CLI roles command](#cli-commands) to assign or remove your microservice's custom roles to your development user:
|
|
201
|
-
|
|
202
|
-
```sh
|
|
203
|
-
pnpm dlx c8y-nitro roles
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
This interactive command lets you select which roles from your manifest to assign to your development user for testing.
|
|
207
|
-
|
|
208
|
-
## API Client Generation
|
|
209
|
-
|
|
210
|
-
For monorepo architectures, `c8y-nitro` can generate TypeScript Angular services that provide fully typed access to your microservice routes.
|
|
211
|
-
|
|
212
|
-
### Configuration
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
export default defineNitroConfig({
|
|
216
|
-
c8y: {
|
|
217
|
-
apiClient: {
|
|
218
|
-
dir: '../ui/src/app/services', // Output directory for generated client
|
|
219
|
-
contextPath: 'my-service' // Optional: override context path
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
modules: [c8y()],
|
|
223
|
-
})
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### Generated Client
|
|
227
|
-
|
|
228
|
-
The generated service creates one method per route with automatic type inference:
|
|
229
|
-
|
|
230
|
-
```ts
|
|
231
|
-
// Generated: my-serviceAPIClient.ts
|
|
232
|
-
@Injectable({ providedIn: 'root' })
|
|
233
|
-
export class GeneratedMyServiceAPIClient {
|
|
234
|
-
async GETHealth(): Promise<{ status: string }> { }
|
|
235
|
-
async GETUsersById(params: { id: string | number }): Promise<User> { }
|
|
236
|
-
async POSTUsers(body: CreateUserDto): Promise<User> { }
|
|
237
|
-
}
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### Usage in Angular
|
|
241
|
-
|
|
242
|
-
```ts
|
|
243
|
-
import { GeneratedMyServiceAPIClient } from './services/my-serviceAPIClient'
|
|
244
|
-
|
|
245
|
-
@Component({
|
|
246
|
-
/**
|
|
247
|
-
* ...
|
|
248
|
-
*/
|
|
249
|
-
})
|
|
250
|
-
export class MyComponent {
|
|
251
|
-
private api = inject(GeneratedMyServiceAPIClient)
|
|
21
|
+
## Documentation
|
|
252
22
|
|
|
253
|
-
|
|
254
|
-
const health = await this.api.GETHealth()
|
|
255
|
-
const user = await this.api.GETUsersById({ id: 123 })
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
> **Note**: The client regenerates automatically when routes change during development.
|
|
261
|
-
|
|
262
|
-
## Logging
|
|
263
|
-
|
|
264
|
-
`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.
|
|
265
|
-
|
|
266
|
-
evlog is automatically configured, no extra setup required. The service name is derived from your package name.
|
|
267
|
-
|
|
268
|
-
### useLogger
|
|
269
|
-
|
|
270
|
-
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.
|
|
271
|
-
|
|
272
|
-
```ts
|
|
273
|
-
import { defineHandler } from 'nitro/h3'
|
|
274
|
-
import { useLogger } from 'c8y-nitro/utils'
|
|
275
|
-
|
|
276
|
-
export default defineHandler(async (event) => {
|
|
277
|
-
const log = useLogger(event)
|
|
278
|
-
|
|
279
|
-
const user = await useUser(event)
|
|
280
|
-
log.set({ action: 'process-order', user: { id: user.userName } })
|
|
281
|
-
|
|
282
|
-
// Add more context as it becomes available
|
|
283
|
-
log.set({ order: { id: '42', total: 9999 } })
|
|
284
|
-
|
|
285
|
-
return { success: true }
|
|
286
|
-
})
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
> **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.
|
|
290
|
-
|
|
291
|
-
### createError
|
|
292
|
-
|
|
293
|
-
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:
|
|
294
|
-
|
|
295
|
-
- Logged as part of the wide event so you can see exactly what went wrong without guessing
|
|
296
|
-
- Returned in the JSON response body so clients can display actionable context
|
|
297
|
-
|
|
298
|
-
```ts
|
|
299
|
-
import { defineHandler } from 'nitro/h3'
|
|
300
|
-
import { useLogger, createError } from 'c8y-nitro/utils'
|
|
301
|
-
|
|
302
|
-
export default defineHandler(async (event) => {
|
|
303
|
-
const log = useLogger(event)
|
|
304
|
-
log.set({ action: 'payment', userId: 'user_123' })
|
|
305
|
-
|
|
306
|
-
throw createError({
|
|
307
|
-
message: 'Payment failed',
|
|
308
|
-
status: 402,
|
|
309
|
-
why: 'Card declined by issuer (insufficient funds)',
|
|
310
|
-
fix: 'Try a different payment method or contact your bank',
|
|
311
|
-
link: 'https://docs.example.com/payments/declined',
|
|
312
|
-
})
|
|
313
|
-
})
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
The error response returned to the client:
|
|
317
|
-
|
|
318
|
-
```json
|
|
319
|
-
{
|
|
320
|
-
"message": "Payment failed",
|
|
321
|
-
"status": 402,
|
|
322
|
-
"data": {
|
|
323
|
-
"why": "Card declined by issuer (insufficient funds)",
|
|
324
|
-
"fix": "Try a different payment method or contact your bank",
|
|
325
|
-
"link": "https://docs.example.com/payments/declined"
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
> **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.
|
|
331
|
-
|
|
332
|
-
### createLogger (standalone)
|
|
333
|
-
|
|
334
|
-
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.
|
|
335
|
-
|
|
336
|
-
```ts
|
|
337
|
-
import { createLogger } from 'c8y-nitro/utils'
|
|
338
|
-
|
|
339
|
-
export async function processSubscriptionRenewal(tenantId: string) {
|
|
340
|
-
const log = createLogger({ job: 'subscription-renewal', tenantId })
|
|
341
|
-
|
|
342
|
-
log.set({ subscription: { id: 'sub_123', plan: 'pro' } })
|
|
343
|
-
|
|
344
|
-
// ... do work ...
|
|
345
|
-
|
|
346
|
-
log.set({ result: 'renewed' })
|
|
347
|
-
log.emit() // Must call emit() manually outside request context
|
|
348
|
-
}
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
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.
|
|
352
|
-
|
|
353
|
-
> **Note**: Unlike `useLogger`, `createLogger` does **not** auto-emit at request end. You must call `log.emit()` manually when the work is complete.
|
|
23
|
+
Full documentation is available at [schplitt.github.io/c8y-nitro](https://schplitt.github.io/c8y-nitro/).
|
|
354
24
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
### Logging Utilities
|
|
358
|
-
|
|
359
|
-
| Function | Description | Request Context |
|
|
360
|
-
| ---------------------- | ------------------------------------------------------------- | :-------------: |
|
|
361
|
-
| `useLogger(event)` | Get the request-scoped wide-event logger | ✅ |
|
|
362
|
-
| `createLogger(ctx?)` | Create a standalone wide-event logger; call `emit()` manually | ❌ |
|
|
363
|
-
| `createError(options)` | Create a structured error with `why`, `fix`, `link` | ❌ |
|
|
364
|
-
|
|
365
|
-
## Utilities
|
|
366
|
-
|
|
367
|
-
`c8y-nitro` provides several utility functions to simplify common tasks in Cumulocity microservices.
|
|
368
|
-
|
|
369
|
-
To use these utilities, simply import them from `c8y-nitro/utils`:
|
|
370
|
-
|
|
371
|
-
```ts
|
|
372
|
-
import { useUser, useUserClient } from 'c8y-nitro/utils'
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
### Usage
|
|
376
|
-
|
|
377
|
-
All utility functions that require request context accept either an `H3Event` or `ServerRequest` parameter:
|
|
378
|
-
|
|
379
|
-
```ts
|
|
380
|
-
// Pass the event/request parameter
|
|
381
|
-
export default defineHandler(async (event) => {
|
|
382
|
-
const user = await useUser(event)
|
|
383
|
-
const client = useUserClient(event)
|
|
384
|
-
return { user }
|
|
385
|
-
})
|
|
386
|
-
```
|
|
25
|
+
Useful entry points:
|
|
387
26
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
```ts
|
|
393
|
-
import { useRequest } from 'nitro/context'
|
|
394
|
-
|
|
395
|
-
export default defineHandler(async (event) => {
|
|
396
|
-
// Deep nested function - no need to pass event down
|
|
397
|
-
return await someDeepFunction()
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
async function someDeepFunction() {
|
|
401
|
-
return await anotherFunction()
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
async function anotherFunction() {
|
|
405
|
-
// Use useRequest() to get the request in any nested function
|
|
406
|
-
const request = useRequest()
|
|
407
|
-
const user = await useUser(request)
|
|
408
|
-
return { user }
|
|
409
|
-
}
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### Credentials
|
|
413
|
-
|
|
414
|
-
| Function | Description | Request Context |
|
|
415
|
-
| ---------------------------------- | ------------------------------------------------------------------ | :-------------: |
|
|
416
|
-
| `useSubscribedTenantCredentials()` | Get credentials for all subscribed tenants (cached, default 10min) | ❌ |
|
|
417
|
-
| `useDeployedTenantCredentials()` | Get credentials for the deployed tenant (cached, default 10min) | ❌ |
|
|
418
|
-
| `useUserTenantCredentials()` | Get credentials for the current user's tenant | ✅ |
|
|
419
|
-
|
|
420
|
-
> **Note**: `useDeployedTenantCredentials()` shares its cache with `useSubscribedTenantCredentials()`. Both functions support `.invalidate()` and `.refresh()` methods. Invalidating or refreshing one will affect the other.
|
|
421
|
-
>
|
|
422
|
-
> **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.
|
|
423
|
-
|
|
424
|
-
### Tenant Options
|
|
425
|
-
|
|
426
|
-
| Function | Description | Request Context |
|
|
427
|
-
| ------------------- | -------------------------------------------------------- | :-------------: |
|
|
428
|
-
| `useTenantOption()` | Get a tenant option value by key (cached, default 10min) | ❌ |
|
|
429
|
-
|
|
430
|
-
Fetch tenant options (settings) configured for your microservice:
|
|
431
|
-
|
|
432
|
-
```ts
|
|
433
|
-
import { useTenantOption } from 'c8y-nitro/utils'
|
|
434
|
-
|
|
435
|
-
export default defineHandler(async (event) => {
|
|
436
|
-
// Fetch a tenant option
|
|
437
|
-
const value = await useTenantOption('myOption')
|
|
438
|
-
|
|
439
|
-
// Fetch an encrypted secret
|
|
440
|
-
const secret = await useTenantOption('credentials.apiKey')
|
|
441
|
-
|
|
442
|
-
// Cache management
|
|
443
|
-
await useTenantOption.invalidate('myOption') // Invalidate specific key
|
|
444
|
-
const fresh = await useTenantOption.refresh('myOption') // Force refresh
|
|
445
|
-
await useTenantOption.invalidateAll() // Invalidate all accessed keys
|
|
446
|
-
await useTenantOption.refreshAll() // Refresh all accessed keys
|
|
447
|
-
return { value, secret, fresh }
|
|
448
|
-
})
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
Define your settings in the manifest to get type-safe keys:
|
|
452
|
-
|
|
453
|
-
```ts
|
|
454
|
-
export default defineNitroConfig({
|
|
455
|
-
c8y: {
|
|
456
|
-
manifest: {
|
|
457
|
-
settings: [
|
|
458
|
-
{ key: 'myOption', defaultValue: 'default' },
|
|
459
|
-
{ key: 'credentials.secret', defaultValue: 'change-me' }, // Encrypted option
|
|
460
|
-
],
|
|
461
|
-
settingsCategory: 'my-service', // Optional, defaults to contextPath/name
|
|
462
|
-
requiredRoles: ['ROLE_OPTION_MANAGEMENT_READ'], // Required for reading tenant options
|
|
463
|
-
},
|
|
464
|
-
},
|
|
465
|
-
modules: [c8y()],
|
|
466
|
-
})
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
`manifest.settings[].defaultValue` is required and must be a non-empty string. `''` is rejected during manifest generation so invalid settings fail early during development/build.
|
|
470
|
-
|
|
471
|
-
> **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.
|
|
472
|
-
|
|
473
|
-
> **Note on Encrypted Options**: Keys prefixed with `credentials.` are stored encrypted by Cumulocity. See more details [here](https://cumulocity.com/api/core/#operation/postOptionCollectionResource).
|
|
474
|
-
|
|
475
|
-
> **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.
|
|
476
|
-
|
|
477
|
-
### Resources
|
|
478
|
-
|
|
479
|
-
| Function | Description | Request Context |
|
|
480
|
-
| ---------------- | ---------------------------------- | :-------------: |
|
|
481
|
-
| `useUser()` | Fetch current user from Cumulocity | ✅ |
|
|
482
|
-
| `useUserRoles()` | Get roles of the current user | ✅ |
|
|
483
|
-
|
|
484
|
-
### Client
|
|
485
|
-
|
|
486
|
-
| Function | Description | Request Context |
|
|
487
|
-
| ------------------------------ | --------------------------------------------------- | :-------------: |
|
|
488
|
-
| `useUserClient()` | Create client authenticated with user's credentials | ✅ |
|
|
489
|
-
| `useUserTenantClient()` | Create client for user's tenant (microservice user) | ✅ |
|
|
490
|
-
| `useSubscribedTenantClients()` | Create clients for all subscribed tenants | ❌ |
|
|
491
|
-
| `useDeployedTenantClient()` | Create client for the deployed tenant | ❌ |
|
|
492
|
-
|
|
493
|
-
### Middleware
|
|
494
|
-
|
|
495
|
-
| Function | Description | Request Context |
|
|
496
|
-
| ------------------------------------------ | ----------------------------------------- | :-------------: |
|
|
497
|
-
| `hasUserRequiredRole(role\|roles)` | Check if user has required role(s) | ✅ |
|
|
498
|
-
| `isUserFromAllowedTenant(tenant\|tenants)` | Check if user is from allowed tenant(s) | ✅ |
|
|
499
|
-
| `isUserFromDeployedTenant()` | Check if user is from the deployed tenant | ✅ |
|
|
500
|
-
|
|
501
|
-
Probe requests targeting the manifest-defined `livenessProbe.httpGet.path` or `readinessProbe.httpGet.path` bypass these auth helpers so orchestration health checks are not blocked by route-wide access control.
|
|
502
|
-
|
|
503
|
-
## CLI Commands
|
|
504
|
-
|
|
505
|
-
| Command | Description |
|
|
506
|
-
| ----------- | ------------------------------------------------------- |
|
|
507
|
-
| `bootstrap` | Manually register microservice and retrieve credentials |
|
|
508
|
-
| `roles` | Manage development user roles |
|
|
509
|
-
| `options` | Manage tenant options on development tenant |
|
|
510
|
-
|
|
511
|
-
For more information, run:
|
|
512
|
-
|
|
513
|
-
```sh
|
|
514
|
-
pnpm dlx c8y-nitro -h
|
|
515
|
-
```
|
|
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)
|
|
516
31
|
|
|
517
32
|
## Development
|
|
518
33
|
|
|
34
|
+
For contributors working on this repository:
|
|
35
|
+
|
|
519
36
|
```sh
|
|
520
|
-
# Install dependencies
|
|
521
37
|
pnpm install
|
|
522
|
-
|
|
523
|
-
# Run dev watcher
|
|
524
38
|
pnpm dev
|
|
525
|
-
|
|
526
|
-
# Build for production
|
|
527
39
|
pnpm build
|
|
528
|
-
|
|
529
|
-
# Run tests (watch mode)
|
|
530
|
-
pnpm test
|
|
531
|
-
|
|
532
|
-
# Run tests once
|
|
533
40
|
pnpm test:run
|
|
41
|
+
pnpm lint
|
|
42
|
+
pnpm typecheck
|
|
43
|
+
pnpm docs:build
|
|
534
44
|
```
|
|
535
45
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
Tests are organized in two categories:
|
|
539
|
-
|
|
540
|
-
- **Unit tests** (`tests/unit/`) — Test individual functions in isolation
|
|
541
|
-
- **Server tests** (`tests/server/`) — Integration tests that spin up a Nitro dev server with the c8y-nitro module
|
|
542
|
-
|
|
543
|
-
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>.
|
|
544
47
|
|
|
545
48
|
## License
|
|
546
49
|
|
|
547
|
-
|
|
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-
|
|
2
|
-
import { t as writeBootstrapCredentials } from "./env-file-
|
|
3
|
-
import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-
|
|
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";
|
|
2
|
+
import { t as writeBootstrapCredentials } from "./env-file-BrOTmVjA.mjs";
|
|
3
|
+
import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
|
|
4
4
|
import { defineCommand, runCommand } from "citty";
|
|
5
5
|
import { consola } from "consola";
|
|
6
6
|
//#region src/cli/commands/bootstrap.ts
|
|
@@ -12,11 +12,11 @@ var bootstrap_default = defineCommand({
|
|
|
12
12
|
args: {},
|
|
13
13
|
async run() {
|
|
14
14
|
consola.info("Loading configuration...");
|
|
15
|
-
const { env,
|
|
15
|
+
const { env, nitroConfig, configDir } = await loadC8yConfig();
|
|
16
16
|
consola.info("Validating environment variables...");
|
|
17
17
|
const envVars = validateBootstrapEnv(env);
|
|
18
18
|
consola.info("Building manifest...");
|
|
19
|
-
const manifest = await createC8yManifest(configDir,
|
|
19
|
+
const manifest = await createC8yManifest(configDir, nitroConfig);
|
|
20
20
|
consola.success(`Manifest created for: ${manifest.name} v${manifest.version}`);
|
|
21
21
|
const authHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
|
|
22
22
|
consola.info(`Checking if microservice "${manifest.name}" exists...`);
|
|
@@ -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-
|
|
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: [] });
|
|
56
56
|
}
|
|
57
57
|
consola.success("Bootstrap complete!");
|
|
58
58
|
}
|
|
@@ -6,6 +6,29 @@ const GENERATED_READINESS_ROUTE = "/_c8y_nitro/readiness";
|
|
|
6
6
|
const GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE = "/_c8y_nitro/invalidate-tenant-options";
|
|
7
7
|
//#endregion
|
|
8
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
|
+
* Decide whether Nitro will expose an OpenAPI spec in the deployed build,
|
|
13
|
+
* and if so return the configured route (with leading slash).
|
|
14
|
+
* Returns null when OpenAPI is not enabled for production.
|
|
15
|
+
* @param config - Nitro config (or options) to inspect
|
|
16
|
+
*/
|
|
17
|
+
function resolveNitroOpenApiRoute(config) {
|
|
18
|
+
if (!config.experimental?.openAPI) return null;
|
|
19
|
+
if (!config.openAPI?.production) return null;
|
|
20
|
+
return config.openAPI?.route || DEFAULT_OPENAPI_ROUTE;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Pull the parts of a Nitro config that feed manifest generation.
|
|
24
|
+
* @param config - Nitro config (or options) loaded from disk or from a Nitro instance
|
|
25
|
+
*/
|
|
26
|
+
function extractManifestInputs(config) {
|
|
27
|
+
return {
|
|
28
|
+
options: config.c8y?.manifest ?? {},
|
|
29
|
+
openApiSpec: resolveNitroOpenApiRoute(config) ?? void 0
|
|
30
|
+
};
|
|
31
|
+
}
|
|
9
32
|
function validateManifestSettings(options) {
|
|
10
33
|
const invalidSettings = options.settings?.filter((setting) => typeof setting.defaultValue !== "string" || setting.defaultValue.length === 0) ?? [];
|
|
11
34
|
if (invalidSettings.length === 0) return;
|
|
@@ -37,13 +60,18 @@ async function readPackageJsonFieldsForManifest(rootDir, logger) {
|
|
|
37
60
|
};
|
|
38
61
|
}
|
|
39
62
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
63
|
+
* Create a Cumulocity manifest from a project root and its Nitro config.
|
|
64
|
+
* Reads `package.json` for identity fields, takes user manifest options from
|
|
65
|
+
* `nitroConfig.c8y.manifest`, and auto-injects `openApiSpec` when Nitro
|
|
66
|
+
* OpenAPI is enabled for production.
|
|
67
|
+
*
|
|
68
|
+
* Used by both the Nitro module and the CLI commands.
|
|
69
|
+
* @param rootDir - Directory containing the project's `package.json`
|
|
70
|
+
* @param nitroConfig - Nitro config (or options) for this project
|
|
44
71
|
* @param logger - Optional logger for debug output
|
|
45
72
|
*/
|
|
46
|
-
async function createC8yManifest(rootDir,
|
|
73
|
+
async function createC8yManifest(rootDir, nitroConfig = {}, logger) {
|
|
74
|
+
const { options, openApiSpec } = extractManifestInputs(nitroConfig);
|
|
47
75
|
validateManifestSettings(options);
|
|
48
76
|
const { name, version, provider, ...restManifestFields } = await readPackageJsonFieldsForManifest(rootDir, logger);
|
|
49
77
|
const probeFields = {};
|
|
@@ -56,10 +84,17 @@ async function createC8yManifest(rootDir, options = {}, logger) {
|
|
|
56
84
|
httpGet: { path: GENERATED_READINESS_ROUTE }
|
|
57
85
|
};
|
|
58
86
|
const key = `${name}-key`;
|
|
87
|
+
let requiredRoles = options.requiredRoles ? [...options.requiredRoles] : void 0;
|
|
88
|
+
if (options.settings && options.settings.length > 0 && !requiredRoles?.includes(ROLE_OPTION_MANAGEMENT_READ)) {
|
|
89
|
+
requiredRoles = [...requiredRoles ?? [], ROLE_OPTION_MANAGEMENT_READ];
|
|
90
|
+
logger?.debug(`Auto-added ${ROLE_OPTION_MANAGEMENT_READ} to requiredRoles because manifest.settings are defined`);
|
|
91
|
+
}
|
|
59
92
|
const manifest = {
|
|
60
93
|
...restManifestFields,
|
|
61
94
|
...probeFields,
|
|
62
95
|
...options,
|
|
96
|
+
...requiredRoles !== void 0 ? { requiredRoles } : {},
|
|
97
|
+
...openApiSpec ? { openApiSpec } : {},
|
|
63
98
|
provider,
|
|
64
99
|
name,
|
|
65
100
|
version,
|
|
@@ -67,6 +102,7 @@ async function createC8yManifest(rootDir, options = {}, logger) {
|
|
|
67
102
|
key,
|
|
68
103
|
type: "MICROSERVICE"
|
|
69
104
|
};
|
|
105
|
+
if (openApiSpec) logger?.debug(`Auto-added openApiSpec="${openApiSpec}" to manifest (Nitro OpenAPI is enabled)`);
|
|
70
106
|
logger?.debug(`Created Cumulocity manifest: ${JSON.stringify(manifest, null, 2)}`);
|
|
71
107
|
return manifest;
|
|
72
108
|
}
|
|
@@ -76,7 +112,7 @@ async function createC8yManifest(rootDir, options = {}, logger) {
|
|
|
76
112
|
* @param nitro - The Nitro instance
|
|
77
113
|
*/
|
|
78
114
|
async function createC8yManifestFromNitro(nitro) {
|
|
79
|
-
return createC8yManifest(nitro.options.rootDir, nitro.options
|
|
115
|
+
return createC8yManifest(nitro.options.rootDir, nitro.options, nitro.logger);
|
|
80
116
|
}
|
|
81
117
|
//#endregion
|
|
82
118
|
//#region src/cli/utils/c8y-api.ts
|