c8y-nitro 0.1.2 → 0.3.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 +206 -6
- package/dist/cli/commands/options.mjs +148 -0
- package/dist/cli/index.mjs +2 -1
- package/dist/cli/utils/c8y-api.mjs +91 -1
- package/dist/index.mjs +10 -5
- package/dist/module/manifest.mjs +1 -1
- package/dist/module/runtime.mjs +11 -4
- package/dist/module/runtimeConfig.mjs +20 -0
- package/dist/package.mjs +1 -1
- package/dist/types/cache.d.mts +28 -0
- package/dist/types/manifest.d.mts +1 -9
- package/dist/types/tenantOptions.d.mts +13 -0
- package/dist/types.d.mts +13 -1
- package/dist/utils/credentials.d.mts +5 -2
- package/dist/utils/credentials.mjs +7 -3
- package/dist/utils/logging.d.mts +3 -0
- package/dist/utils/logging.mjs +4 -0
- package/dist/utils/tenantOptions.d.mts +65 -0
- package/dist/utils/tenantOptions.mjs +127 -0
- package/dist/utils.d.mts +3 -1
- package/dist/utils.mjs +3 -1
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -103,6 +103,8 @@ C8Y_BOOTSTRAP_PASSWORD=<generated-password>
|
|
|
103
103
|
|
|
104
104
|
> **Manual Bootstrap**: For more control or troubleshooting, you can use the [CLI bootstrap command](#cli-commands) to manually register your microservice.
|
|
105
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
|
+
|
|
106
108
|
## Automatic Zip Creation
|
|
107
109
|
|
|
108
110
|
`c8y-nitro` automatically generates a ready-to-deploy microservice zip package after each build. The process includes:
|
|
@@ -134,6 +136,35 @@ For all available manifest options, see the [Cumulocity Microservice Manifest do
|
|
|
134
136
|
|
|
135
137
|
> **Note**: Health probe endpoints (`/_c8y_nitro/liveness` and `/_c8y_nitro/readiness`) are automatically injected if not manually defined.
|
|
136
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
|
+
|
|
137
168
|
## Development User Injection
|
|
138
169
|
|
|
139
170
|
During development, `c8y-nitro` automatically injects your development user credentials into all requests. This allows you to test authentication and authorization middlewares locally.
|
|
@@ -212,6 +243,109 @@ export class MyComponent {
|
|
|
212
243
|
|
|
213
244
|
> **Note**: The client regenerates automatically when routes change during development.
|
|
214
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
|
+
|
|
215
349
|
## Utilities
|
|
216
350
|
|
|
217
351
|
`c8y-nitro` provides several utility functions to simplify common tasks in Cumulocity microservices.
|
|
@@ -261,13 +395,66 @@ async function anotherFunction() {
|
|
|
261
395
|
|
|
262
396
|
### Credentials
|
|
263
397
|
|
|
264
|
-
| Function | Description
|
|
265
|
-
| ---------------------------------- |
|
|
266
|
-
| `useSubscribedTenantCredentials()` | Get credentials for all subscribed tenants (cached 10min) | ❌ |
|
|
267
|
-
| `useDeployedTenantCredentials()` | Get credentials for the deployed tenant (cached
|
|
268
|
-
| `useUserTenantCredentials()` | Get credentials for the current user's tenant
|
|
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 | ✅ |
|
|
269
403
|
|
|
270
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
|
+
```
|
|
452
|
+
|
|
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.
|
|
271
458
|
|
|
272
459
|
### Resources
|
|
273
460
|
|
|
@@ -299,6 +486,7 @@ async function anotherFunction() {
|
|
|
299
486
|
| ----------- | ------------------------------------------------------- |
|
|
300
487
|
| `bootstrap` | Manually register microservice and retrieve credentials |
|
|
301
488
|
| `roles` | Manage development user roles |
|
|
489
|
+
| `options` | Manage tenant options on development tenant |
|
|
302
490
|
|
|
303
491
|
For more information, run:
|
|
304
492
|
|
|
@@ -318,10 +506,22 @@ pnpm dev
|
|
|
318
506
|
# Build for production
|
|
319
507
|
pnpm build
|
|
320
508
|
|
|
321
|
-
# Run tests
|
|
509
|
+
# Run tests (watch mode)
|
|
322
510
|
pnpm test
|
|
511
|
+
|
|
512
|
+
# Run tests once
|
|
513
|
+
pnpm test:run
|
|
323
514
|
```
|
|
324
515
|
|
|
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.
|
|
524
|
+
|
|
325
525
|
## License
|
|
326
526
|
|
|
327
527
|
MIT
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createC8yManifest } from "../../module/manifest.mjs";
|
|
2
|
+
import { createBasicAuthHeader, deleteTenantOption, getTenantOption, getTenantOptionsByCategory, updateTenantOption } from "../utils/c8y-api.mjs";
|
|
3
|
+
import { loadC8yConfig, validateBootstrapEnv } from "../utils/config.mjs";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { consola } from "consola";
|
|
6
|
+
|
|
7
|
+
//#region src/cli/commands/options.ts
|
|
8
|
+
var options_default = defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: "options",
|
|
11
|
+
description: "Manage tenant options on the development tenant"
|
|
12
|
+
},
|
|
13
|
+
args: {},
|
|
14
|
+
async run() {
|
|
15
|
+
consola.info("Loading configuration...");
|
|
16
|
+
const { env, c8yOptions, configDir } = await loadC8yConfig();
|
|
17
|
+
consola.info("Validating environment variables...");
|
|
18
|
+
const envVars = validateBootstrapEnv(env);
|
|
19
|
+
consola.info("Loading manifest...");
|
|
20
|
+
const manifest = await createC8yManifest(configDir, c8yOptions?.manifest);
|
|
21
|
+
const category = manifest.settingsCategory || manifest.contextPath || manifest.name;
|
|
22
|
+
if (!manifest.settings || manifest.settings.length === 0) throw new Error("No settings defined in manifest. Add settings to your c8y.manifest configuration.");
|
|
23
|
+
consola.success(`Using category: ${category}`);
|
|
24
|
+
const authHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
|
|
25
|
+
consola.info("Fetching current tenant options...");
|
|
26
|
+
const currentOptions = await getTenantOptionsByCategory(envVars.C8Y_BASEURL, category, authHeader);
|
|
27
|
+
const availableKeys = manifest.settings.map((s) => s.key);
|
|
28
|
+
consola.success(`Found ${Object.keys(currentOptions).length} options set on tenant`);
|
|
29
|
+
const action = await consola.prompt("What do you want to do?", {
|
|
30
|
+
type: "select",
|
|
31
|
+
options: [
|
|
32
|
+
{
|
|
33
|
+
label: "Read option value",
|
|
34
|
+
value: "read"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: "Update/Create option",
|
|
38
|
+
value: "update"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: "Delete option(s)",
|
|
42
|
+
value: "delete"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
});
|
|
46
|
+
if (action === "read") await handleRead(envVars.C8Y_BASEURL, category, authHeader, availableKeys, currentOptions);
|
|
47
|
+
else if (action === "update") await handleUpdate(envVars.C8Y_BASEURL, category, authHeader, availableKeys, currentOptions);
|
|
48
|
+
else if (action === "delete") await handleDelete(envVars.C8Y_BASEURL, category, authHeader, availableKeys, currentOptions);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* Handle reading a single option
|
|
53
|
+
* @param baseUrl - The Cumulocity base URL
|
|
54
|
+
* @param category - The tenant option category
|
|
55
|
+
* @param authHeader - The Basic Auth header
|
|
56
|
+
* @param availableKeys - List of available option keys
|
|
57
|
+
* @param currentOptions - Current option values from tenant
|
|
58
|
+
*/
|
|
59
|
+
async function handleRead(baseUrl, category, authHeader, availableKeys, currentOptions) {
|
|
60
|
+
const setKeys = availableKeys.filter((k) => !k.startsWith("credentials.") && currentOptions[k] !== void 0);
|
|
61
|
+
const credentialsKeys = availableKeys.filter((k) => k.startsWith("credentials."));
|
|
62
|
+
const allKeys = [...setKeys, ...credentialsKeys];
|
|
63
|
+
if (allKeys.length === 0) {
|
|
64
|
+
consola.warn("No options are currently set");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const key = await consola.prompt("Select option to read:", {
|
|
68
|
+
type: "select",
|
|
69
|
+
options: allKeys.map((k) => ({
|
|
70
|
+
label: k.startsWith("credentials.") ? `${k} (unknown)` : k,
|
|
71
|
+
value: k
|
|
72
|
+
})),
|
|
73
|
+
cancel: "reject"
|
|
74
|
+
});
|
|
75
|
+
consola.info(`Reading option: ${key}`);
|
|
76
|
+
const value = await getTenantOption(baseUrl, category, key.startsWith("credentials.") ? key.replace(/^credentials\./, "") : key, authHeader);
|
|
77
|
+
if (value === void 0) consola.warn(`Option '${key}' is not set`);
|
|
78
|
+
else consola.success(`Value: ${value}`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Handle updating options (with loop for multiple updates)
|
|
82
|
+
* @param baseUrl - The Cumulocity base URL
|
|
83
|
+
* @param category - The tenant option category
|
|
84
|
+
* @param authHeader - The Basic Auth header
|
|
85
|
+
* @param availableKeys - List of available option keys
|
|
86
|
+
* @param currentOptions - Current option values from tenant
|
|
87
|
+
*/
|
|
88
|
+
async function handleUpdate(baseUrl, category, authHeader, availableKeys, currentOptions) {
|
|
89
|
+
let continueUpdating = true;
|
|
90
|
+
while (continueUpdating) {
|
|
91
|
+
const key = await consola.prompt("Select option to update:", {
|
|
92
|
+
type: "select",
|
|
93
|
+
options: availableKeys.map((k) => ({
|
|
94
|
+
label: currentOptions[k] !== void 0 ? `${k} (current: ${currentOptions[k]})` : k.startsWith("credentials.") ? `${k} (current: unknown)` : `${k} (not set)`,
|
|
95
|
+
value: k
|
|
96
|
+
})),
|
|
97
|
+
cancel: "reject"
|
|
98
|
+
});
|
|
99
|
+
const currentValue = currentOptions[key];
|
|
100
|
+
const newValue = await consola.prompt("Enter new value:", {
|
|
101
|
+
type: "text",
|
|
102
|
+
default: currentValue,
|
|
103
|
+
cancel: "reject"
|
|
104
|
+
});
|
|
105
|
+
consola.info(`Updating option: ${key}`);
|
|
106
|
+
await updateTenantOption(baseUrl, category, key.replace(/^credentials\./, ""), newValue, authHeader);
|
|
107
|
+
currentOptions[key] = newValue;
|
|
108
|
+
consola.success(`Option '${key}' updated successfully`);
|
|
109
|
+
if (!await consola.prompt("Update another option?", {
|
|
110
|
+
type: "confirm",
|
|
111
|
+
initial: false,
|
|
112
|
+
cancel: "reject"
|
|
113
|
+
})) continueUpdating = false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Handle deleting multiple options
|
|
118
|
+
* @param baseUrl - The Cumulocity base URL
|
|
119
|
+
* @param category - The tenant option category
|
|
120
|
+
* @param authHeader - The Basic Auth header
|
|
121
|
+
* @param availableKeys - List of available option keys
|
|
122
|
+
* @param currentOptions - Current option values from tenant
|
|
123
|
+
*/
|
|
124
|
+
async function handleDelete(baseUrl, category, authHeader, availableKeys, currentOptions) {
|
|
125
|
+
const setKeys = availableKeys.filter((k) => !k.startsWith("credentials.") && currentOptions[k] !== void 0);
|
|
126
|
+
const credentialsKeys = availableKeys.filter((k) => k.startsWith("credentials."));
|
|
127
|
+
const allKeys = [...setKeys, ...credentialsKeys];
|
|
128
|
+
if (allKeys.length === 0) {
|
|
129
|
+
consola.warn("No options are currently set");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const keysToDelete = await consola.prompt("Select option(s) to delete:", {
|
|
133
|
+
type: "multiselect",
|
|
134
|
+
options: allKeys,
|
|
135
|
+
required: true,
|
|
136
|
+
cancel: "reject"
|
|
137
|
+
});
|
|
138
|
+
consola.info(`Deleting ${keysToDelete.length} option(s)...`);
|
|
139
|
+
for (const key of keysToDelete) {
|
|
140
|
+
consola.info(`Deleting option: ${key}`);
|
|
141
|
+
await deleteTenantOption(baseUrl, category, key.startsWith("credentials.") ? key.replace(/^credentials\./, "") : key, authHeader);
|
|
142
|
+
consola.success(`✓ Deleted: ${key}`);
|
|
143
|
+
}
|
|
144
|
+
consola.success("Delete operation completed");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
export { options_default as default };
|
package/dist/cli/index.mjs
CHANGED
|
@@ -10,7 +10,8 @@ runMain(defineCommand({
|
|
|
10
10
|
},
|
|
11
11
|
subCommands: {
|
|
12
12
|
bootstrap: () => import("./commands/bootstrap.mjs").then((r) => r.default),
|
|
13
|
-
roles: () => import("./commands/roles.mjs").then((r) => r.default)
|
|
13
|
+
roles: () => import("./commands/roles.mjs").then((r) => r.default),
|
|
14
|
+
options: () => import("./commands/options.mjs").then((r) => r.default)
|
|
14
15
|
}
|
|
15
16
|
}));
|
|
16
17
|
|
|
@@ -167,6 +167,96 @@ async function unassignUserRole(baseUrl, tenantId, userId, roleId, authHeader) {
|
|
|
167
167
|
throw new Error(`Failed to unassign role ${roleId}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Gets all tenant options for a specific category.
|
|
172
|
+
* @param baseUrl - The Cumulocity base URL
|
|
173
|
+
* @param category - The category to fetch options for
|
|
174
|
+
* @param authHeader - The Basic Auth header
|
|
175
|
+
* @returns Record of key-value pairs for the category
|
|
176
|
+
*/
|
|
177
|
+
async function getTenantOptionsByCategory(baseUrl, category, authHeader) {
|
|
178
|
+
const url = `${baseUrl}/tenant/options/${encodeURIComponent(category)}`;
|
|
179
|
+
const response = await fetch(url, {
|
|
180
|
+
method: "GET",
|
|
181
|
+
headers: {
|
|
182
|
+
Authorization: authHeader,
|
|
183
|
+
Accept: "application/json"
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
if (response.status === 404) return {};
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
const errorText = await response.text();
|
|
189
|
+
throw new Error(`Failed to get tenant options for category ${category}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
190
|
+
}
|
|
191
|
+
return await response.json();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Gets a specific tenant option value.
|
|
195
|
+
* @param baseUrl - The Cumulocity base URL
|
|
196
|
+
* @param category - The category of the option
|
|
197
|
+
* @param key - The option key
|
|
198
|
+
* @param authHeader - The Basic Auth header
|
|
199
|
+
* @returns The option value, or undefined if not set
|
|
200
|
+
*/
|
|
201
|
+
async function getTenantOption(baseUrl, category, key, authHeader) {
|
|
202
|
+
const url = `${baseUrl}/tenant/options/${encodeURIComponent(category)}/${encodeURIComponent(key)}`;
|
|
203
|
+
const response = await fetch(url, {
|
|
204
|
+
method: "GET",
|
|
205
|
+
headers: {
|
|
206
|
+
Authorization: authHeader,
|
|
207
|
+
Accept: "application/json"
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
if (response.status === 404) return;
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
const errorText = await response.text();
|
|
213
|
+
throw new Error(`Failed to get tenant option ${category}/${key}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
214
|
+
}
|
|
215
|
+
return (await response.json()).value;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Updates or creates a tenant option.
|
|
219
|
+
* @param baseUrl - The Cumulocity base URL
|
|
220
|
+
* @param category - The category of the option
|
|
221
|
+
* @param key - The option key
|
|
222
|
+
* @param value - The new value to set
|
|
223
|
+
* @param authHeader - The Basic Auth header
|
|
224
|
+
*/
|
|
225
|
+
async function updateTenantOption(baseUrl, category, key, value, authHeader) {
|
|
226
|
+
const url = `${baseUrl}/tenant/options/${encodeURIComponent(category)}/${encodeURIComponent(key)}`;
|
|
227
|
+
const response = await fetch(url, {
|
|
228
|
+
method: "PUT",
|
|
229
|
+
headers: {
|
|
230
|
+
"Authorization": authHeader,
|
|
231
|
+
"Content-Type": "application/json",
|
|
232
|
+
"Accept": "application/json"
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify({ value })
|
|
235
|
+
});
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
const errorText = await response.text();
|
|
238
|
+
throw new Error(`Failed to update tenant option ${category}/${key}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Deletes a tenant option.
|
|
243
|
+
* @param baseUrl - The Cumulocity base URL
|
|
244
|
+
* @param category - The category of the option
|
|
245
|
+
* @param key - The option key to delete
|
|
246
|
+
* @param authHeader - The Basic Auth header
|
|
247
|
+
*/
|
|
248
|
+
async function deleteTenantOption(baseUrl, category, key, authHeader) {
|
|
249
|
+
const url = `${baseUrl}/tenant/options/${encodeURIComponent(category)}/${encodeURIComponent(key)}`;
|
|
250
|
+
const response = await fetch(url, {
|
|
251
|
+
method: "DELETE",
|
|
252
|
+
headers: { Authorization: authHeader }
|
|
253
|
+
});
|
|
254
|
+
if (response.status === 404) return;
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
const errorText = await response.text();
|
|
257
|
+
throw new Error(`Failed to delete tenant option ${category}/${key}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
170
260
|
|
|
171
261
|
//#endregion
|
|
172
|
-
export { assignUserRole, createBasicAuthHeader, createMicroservice, findMicroserviceByName, getBootstrapCredentials, subscribeToApplication, unassignUserRole, updateMicroservice };
|
|
262
|
+
export { assignUserRole, createBasicAuthHeader, createMicroservice, deleteTenantOption, findMicroserviceByName, getBootstrapCredentials, getTenantOption, getTenantOptionsByCategory, subscribeToApplication, unassignUserRole, updateMicroservice, updateTenantOption };
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { createC8yManifestFromNitro } from "./module/manifest.mjs";
|
|
1
2
|
import { writeAPIClient } from "./module/apiClient.mjs";
|
|
2
3
|
import { createC8yZip } from "./module/c8yzip.mjs";
|
|
3
4
|
import { setupRuntime } from "./module/runtime.mjs";
|
|
5
|
+
import { setupRuntimeConfig } from "./module/runtimeConfig.mjs";
|
|
4
6
|
import { registerRuntime } from "./module/register.mjs";
|
|
5
7
|
import { checkProbes } from "./module/probeCheck.mjs";
|
|
6
8
|
import { autoBootstrap } from "./module/autoBootstrap.mjs";
|
|
7
9
|
import { name } from "./package.mjs";
|
|
10
|
+
import evlog from "evlog/nitro/v3";
|
|
8
11
|
|
|
9
12
|
//#region src/index.ts
|
|
10
13
|
function c8y() {
|
|
@@ -16,8 +19,8 @@ function c8y() {
|
|
|
16
19
|
nitro.options.typescript.tsConfig = {};
|
|
17
20
|
nitro.options.typescript.tsConfig.include = ["./**/*.d.ts"];
|
|
18
21
|
nitro.options.typescript.tsConfig.exclude = [];
|
|
19
|
-
nitro.options.noExternals = nitro.options.noExternals
|
|
20
|
-
...nitro.options.noExternals
|
|
22
|
+
nitro.options.noExternals = nitro.options.noExternals === true ? true : [
|
|
23
|
+
...Array.isArray(nitro.options.noExternals) ? nitro.options.noExternals : [],
|
|
21
24
|
name,
|
|
22
25
|
"@c8y/client"
|
|
23
26
|
];
|
|
@@ -25,7 +28,10 @@ function c8y() {
|
|
|
25
28
|
nitro.logger.error(`Unsupported preset "${nitro.options.preset}" for c8y-nitro module, only node presets are supported.`);
|
|
26
29
|
throw new Error("Unsupported preset for c8y-nitro module");
|
|
27
30
|
}
|
|
28
|
-
await
|
|
31
|
+
const { setup: setupEvlog } = evlog({ env: { service: (await createC8yManifestFromNitro(nitro)).name } });
|
|
32
|
+
await setupEvlog(nitro);
|
|
33
|
+
if (!options.skipBootstrap) await autoBootstrap(nitro);
|
|
34
|
+
await setupRuntimeConfig(nitro, options);
|
|
29
35
|
setupRuntime(nitro, options.manifest);
|
|
30
36
|
nitro.hooks.hook("dev:reload", async () => {
|
|
31
37
|
setupRuntime(nitro, options.manifest);
|
|
@@ -50,7 +56,6 @@ function c8y() {
|
|
|
50
56
|
}
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
|
-
var src_default = c8y;
|
|
54
59
|
|
|
55
60
|
//#endregion
|
|
56
|
-
export { c8y,
|
|
61
|
+
export { c8y, c8y as default };
|
package/dist/module/manifest.mjs
CHANGED
package/dist/module/runtime.mjs
CHANGED
|
@@ -5,26 +5,33 @@ import { mkdirSync, writeFileSync } from "fs";
|
|
|
5
5
|
function setupRuntime(nitro, manifestOptions = {}) {
|
|
6
6
|
nitro.logger.debug("Setting up C8Y nitro runtime");
|
|
7
7
|
const roles = manifestOptions.roles ?? [];
|
|
8
|
+
const settingKeys = (manifestOptions.settings ?? []).map((s) => s.key);
|
|
8
9
|
const completeTypesDir = join(nitro.options.rootDir, nitro.options.typescript.generatedTypesDir ?? "node_modules/.nitro/types");
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
const typesFile = join(completeTypesDir, "c8y-nitro.d.ts");
|
|
11
|
+
const typesContent = `// generated by c8y-nitro
|
|
11
12
|
declare module 'c8y-nitro/types' {
|
|
12
13
|
interface C8YRoles {
|
|
13
14
|
${roles.map((role) => ` '${role}': '${role}';`).join("\n")}
|
|
14
15
|
}
|
|
16
|
+
type C8YTenantOptionKey = ${settingKeys.length > 0 ? `${settingKeys.map((k) => `'${k}'`).join(" | ")}` : "never"}
|
|
17
|
+
type C8YTenantOptionKeysCacheConfig = Partial<Record<C8YTenantOptionKey, number>>;
|
|
15
18
|
}
|
|
19
|
+
|
|
16
20
|
declare module 'c8y-nitro/runtime' {
|
|
17
21
|
import type { C8YRoles } from 'c8y-nitro/types';
|
|
18
22
|
export const c8yRoles: C8YRoles;
|
|
23
|
+
export const c8yTenantOptionKeys: readonly [${settingKeys.map((key) => `'${key}'`).join(", ")}];
|
|
19
24
|
}`;
|
|
20
25
|
nitro.options.virtual["c8y-nitro/runtime"] = `
|
|
21
26
|
export const c8yRoles = {
|
|
22
27
|
${roles.map((role) => ` '${role}': '${role}',`).join("\n")}
|
|
23
28
|
}
|
|
29
|
+
|
|
30
|
+
export const c8yTenantOptionKeys = [${settingKeys.map((key) => `'${key}'`).join(", ")}]
|
|
24
31
|
`;
|
|
25
32
|
mkdirSync(completeTypesDir, { recursive: true });
|
|
26
|
-
writeFileSync(
|
|
27
|
-
nitro.logger.debug(`Written C8Y
|
|
33
|
+
writeFileSync(typesFile, typesContent, { encoding: "utf-8" });
|
|
34
|
+
nitro.logger.debug(`Written C8Y types to ${typesFile}`);
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
//#endregion
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createC8yManifest } from "./manifest.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/module/runtimeConfig.ts
|
|
4
|
+
/**
|
|
5
|
+
* Sets up runtime configuration values from module options.
|
|
6
|
+
* These can be overridden by environment variables.
|
|
7
|
+
* @param nitro - The Nitro instance
|
|
8
|
+
* @param options - The c8y-nitro module options
|
|
9
|
+
*/
|
|
10
|
+
async function setupRuntimeConfig(nitro, options) {
|
|
11
|
+
nitro.logger.debug("Setting up C8Y runtime config");
|
|
12
|
+
nitro.options.runtimeConfig.c8yCredentialsCacheTTL = options.cache?.credentialsTTL ?? 600;
|
|
13
|
+
nitro.options.runtimeConfig.c8yDefaultTenantOptionsTTL = options.cache?.defaultTenantOptionsTTL ?? 600;
|
|
14
|
+
nitro.options.runtimeConfig.c8yTenantOptionsPerKeyTTL = options.cache?.tenantOptions ?? {};
|
|
15
|
+
const manifest = await createC8yManifest(nitro.options.rootDir, options.manifest, nitro.logger);
|
|
16
|
+
nitro.options.runtimeConfig.c8ySettingsCategory = options.manifest?.settingsCategory ?? manifest.contextPath ?? manifest.name;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { setupRuntimeConfig };
|
package/dist/package.mjs
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { C8YTenantOptionKeysCacheConfig } from "c8y-nitro/types";
|
|
2
|
+
|
|
3
|
+
//#region src/types/cache.d.ts
|
|
4
|
+
interface C8yCacheOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Cache TTL for subscribed tenant credentials in seconds.
|
|
7
|
+
* @default 600 (10 minutes)
|
|
8
|
+
*/
|
|
9
|
+
credentialsTTL?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Default cache TTL for tenant options in seconds.
|
|
12
|
+
* Applied to all keys unless overridden in `tenantOptions`.
|
|
13
|
+
* @default 600 (10 minutes)
|
|
14
|
+
*/
|
|
15
|
+
defaultTenantOptionsTTL?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Per-key cache TTL overrides for tenant options in seconds.
|
|
18
|
+
* Keys should match those defined in `manifest.settings[].key`.
|
|
19
|
+
* @example
|
|
20
|
+
* {
|
|
21
|
+
* 'myOption': 300, // 5 minutes
|
|
22
|
+
* 'credentials.secret': 60, // 1 minute
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
tenantOptions?: C8YTenantOptionKeysCacheConfig;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { C8yCacheOptions };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/types/manifest.d.ts
|
|
2
|
-
type C8YManifestOptions =
|
|
2
|
+
type C8YManifestOptions = Omit<C8YManifest, 'name' | 'version' | 'apiVersion' | 'key' | 'type' | 'provider'>;
|
|
3
3
|
interface C8YManifest {
|
|
4
4
|
/**
|
|
5
5
|
* API version (e.g., "v2", "2"). Version 2 required for enhanced container security.
|
|
@@ -253,10 +253,6 @@ interface ExecAction {
|
|
|
253
253
|
* TCP socket probe.
|
|
254
254
|
*/
|
|
255
255
|
interface TCPSocketAction {
|
|
256
|
-
/**
|
|
257
|
-
* Hostname or IP to connect to.
|
|
258
|
-
*/
|
|
259
|
-
host: string;
|
|
260
256
|
/**
|
|
261
257
|
* Port number to connect to.
|
|
262
258
|
* @default 80
|
|
@@ -267,10 +263,6 @@ interface TCPSocketAction {
|
|
|
267
263
|
* HTTP GET probe.
|
|
268
264
|
*/
|
|
269
265
|
interface HTTPGetAction {
|
|
270
|
-
/**
|
|
271
|
-
* Hostname to connect to.
|
|
272
|
-
*/
|
|
273
|
-
host?: string;
|
|
274
266
|
/**
|
|
275
267
|
* URL path to request.
|
|
276
268
|
* @example "/health"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/types/tenantOptions.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Per-key cache TTL configuration for tenant options.
|
|
4
|
+
* Overwritten by generated types from manifest settings (manifest.settings[].key).
|
|
5
|
+
*/
|
|
6
|
+
type C8YTenantOptionKeysCacheConfig = Partial<Record<C8YTenantOptionKey, number>>;
|
|
7
|
+
/**
|
|
8
|
+
* Type for tenant option keys.
|
|
9
|
+
* Overwritten by generated types from manifest settings (manifest.settings[].key).
|
|
10
|
+
*/
|
|
11
|
+
type C8YTenantOptionKey = string;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { C8YTenantOptionKey, C8YTenantOptionKeysCacheConfig };
|
package/dist/types.d.mts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { C8YAPIClientOptions } from "./types/apiClient.mjs";
|
|
2
2
|
import { C8YManifestOptions } from "./types/manifest.mjs";
|
|
3
3
|
import { C8YZipOptions } from "./types/zip.mjs";
|
|
4
|
+
import { C8yCacheOptions } from "./types/cache.mjs";
|
|
4
5
|
import { C8YRoles } from "./types/roles.mjs";
|
|
6
|
+
import { C8YTenantOptionKey, C8YTenantOptionKeysCacheConfig } from "./types/tenantOptions.mjs";
|
|
5
7
|
|
|
6
8
|
//#region src/types/index.d.ts
|
|
7
9
|
interface C8yNitroModuleOptions {
|
|
8
10
|
manifest?: C8YManifestOptions;
|
|
9
11
|
apiClient?: C8YAPIClientOptions;
|
|
10
12
|
zip?: C8YZipOptions;
|
|
13
|
+
cache?: C8yCacheOptions;
|
|
14
|
+
/**
|
|
15
|
+
* Disable auto-bootstrap during development.
|
|
16
|
+
* When true, the module will not automatically register the microservice
|
|
17
|
+
* or retrieve bootstrap credentials on startup.
|
|
18
|
+
*
|
|
19
|
+
* Useful for CI/CD pipelines or manual bootstrap management.
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
skipBootstrap?: boolean;
|
|
11
23
|
}
|
|
12
24
|
//#endregion
|
|
13
|
-
export { C8YAPIClientOptions, type C8YManifestOptions, C8YRoles, C8YZipOptions, C8yNitroModuleOptions };
|
|
25
|
+
export { C8YAPIClientOptions, type C8YManifestOptions, C8YRoles, C8YTenantOptionKey, C8YTenantOptionKeysCacheConfig, C8YZipOptions, type C8yCacheOptions, C8yNitroModuleOptions };
|
|
@@ -6,8 +6,11 @@ import { ServerRequest } from "nitro/types";
|
|
|
6
6
|
/**
|
|
7
7
|
* Fetches credentials for all tenants subscribed to this microservice.\
|
|
8
8
|
* Uses bootstrap credentials from runtime config to query the microservice subscriptions API.\
|
|
9
|
-
* Results are cached
|
|
9
|
+
* Results are cached based on the configured TTL (default: 10 minutes).\
|
|
10
10
|
* @returns Object mapping tenant IDs to their respective credentials
|
|
11
|
+
* @config Cache TTL can be configured via:
|
|
12
|
+
* - `c8y.cache.credentialsTTL` in the Nitro config (value in seconds)
|
|
13
|
+
* - `NITRO_C8Y_CACHE_CREDENTIALS_TTL` environment variable
|
|
11
14
|
* @example
|
|
12
15
|
* // Get all subscribed tenant credentials:
|
|
13
16
|
* const credentials = await useSubscribedTenantCredentials()
|
|
@@ -29,7 +32,7 @@ declare const useSubscribedTenantCredentials: (() => Promise<Record<string, ICre
|
|
|
29
32
|
/**
|
|
30
33
|
* Fetches credentials for the tenant where this microservice is deployed.\
|
|
31
34
|
* Uses the C8Y_BOOTSTRAP_TENANT environment variable to identify the deployed tenant.\
|
|
32
|
-
* Returns credentials from the subscribed tenant credentials cache (cached
|
|
35
|
+
* Returns credentials from the subscribed tenant credentials cache (cached based on configured TTL, default: 10 minutes).
|
|
33
36
|
* @returns Credentials for the deployed tenant
|
|
34
37
|
* @throws {HTTPError} If no credentials found for the deployed tenant
|
|
35
38
|
* @example
|
|
@@ -4,13 +4,17 @@ import { Client } from "@c8y/client";
|
|
|
4
4
|
import { defineCachedFunction } from "nitro/cache";
|
|
5
5
|
import { HTTPError } from "nitro/h3";
|
|
6
6
|
import { useStorage } from "nitro/storage";
|
|
7
|
+
import { useRuntimeConfig } from "nitro/runtime-config";
|
|
7
8
|
|
|
8
9
|
//#region src/utils/credentials.ts
|
|
9
10
|
/**
|
|
10
11
|
* Fetches credentials for all tenants subscribed to this microservice.\
|
|
11
12
|
* Uses bootstrap credentials from runtime config to query the microservice subscriptions API.\
|
|
12
|
-
* Results are cached
|
|
13
|
+
* Results are cached based on the configured TTL (default: 10 minutes).\
|
|
13
14
|
* @returns Object mapping tenant IDs to their respective credentials
|
|
15
|
+
* @config Cache TTL can be configured via:
|
|
16
|
+
* - `c8y.cache.credentialsTTL` in the Nitro config (value in seconds)
|
|
17
|
+
* - `NITRO_C8Y_CACHE_CREDENTIALS_TTL` environment variable
|
|
14
18
|
* @example
|
|
15
19
|
* // Get all subscribed tenant credentials:
|
|
16
20
|
* const credentials = await useSubscribedTenantCredentials()
|
|
@@ -35,7 +39,7 @@ const useSubscribedTenantCredentials = Object.assign(defineCachedFunction(async
|
|
|
35
39
|
return acc;
|
|
36
40
|
}, {});
|
|
37
41
|
}, {
|
|
38
|
-
maxAge: 600,
|
|
42
|
+
maxAge: useRuntimeConfig().c8yCredentialsCacheTTL ?? 600,
|
|
39
43
|
name: "_c8y_nitro_get_subscribed_tenant_credentials",
|
|
40
44
|
group: "c8y_nitro",
|
|
41
45
|
swr: false
|
|
@@ -51,7 +55,7 @@ const useSubscribedTenantCredentials = Object.assign(defineCachedFunction(async
|
|
|
51
55
|
/**
|
|
52
56
|
* Fetches credentials for the tenant where this microservice is deployed.\
|
|
53
57
|
* Uses the C8Y_BOOTSTRAP_TENANT environment variable to identify the deployed tenant.\
|
|
54
|
-
* Returns credentials from the subscribed tenant credentials cache (cached
|
|
58
|
+
* Returns credentials from the subscribed tenant credentials cache (cached based on configured TTL, default: 10 minutes).
|
|
55
59
|
* @returns Credentials for the deployed tenant
|
|
56
60
|
* @throws {HTTPError} If no credentials found for the deployed tenant
|
|
57
61
|
* @example
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { C8YTenantOptionKey } from "c8y-nitro/types";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/tenantOptions.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Fetches a tenant option value by key.\
|
|
6
|
+
* Uses the deployed tenant's service user credentials to access the Options API.\
|
|
7
|
+
* Results are cached based on the configured TTL (default: 10 minutes).
|
|
8
|
+
*
|
|
9
|
+
* @param key - The tenant option key to fetch (as defined in `manifest.settings`)
|
|
10
|
+
* @returns The option value as a string
|
|
11
|
+
*
|
|
12
|
+
* @config Cache TTL can be configured via:
|
|
13
|
+
* - `c8y.cache.defaultTenantOptionsTTL` — Default TTL for all keys (in seconds)
|
|
14
|
+
* - `c8y.cache.tenantOptions` — Per-key TTL overrides
|
|
15
|
+
* - `NITRO_C8Y_DEFAULT_TENANT_OPTIONS_TTL` — Environment variable for default TTL
|
|
16
|
+
*
|
|
17
|
+
* @note For encrypted options (keys starting with `credentials.`), the value is automatically
|
|
18
|
+
* decrypted by Cumulocity if this microservice is the owner of the option (category matches
|
|
19
|
+
* the microservice's settingsCategory/contextPath/name). The `credentials.` prefix is
|
|
20
|
+
* automatically stripped when calling the API.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Fetch a tenant option:
|
|
24
|
+
* const value = await useTenantOption('myOption')
|
|
25
|
+
*
|
|
26
|
+
* // Fetch an encrypted secret:
|
|
27
|
+
* const secret = await useTenantOption('credentials.apiKey')
|
|
28
|
+
*
|
|
29
|
+
* // Invalidate cache for a specific key:
|
|
30
|
+
* await useTenantOption.invalidate('myOption')
|
|
31
|
+
*
|
|
32
|
+
* // Force refresh a specific key:
|
|
33
|
+
* const fresh = await useTenantOption.refresh('myOption')
|
|
34
|
+
*
|
|
35
|
+
* // Invalidate all tenant option caches:
|
|
36
|
+
* await useTenantOption.invalidateAll()
|
|
37
|
+
*
|
|
38
|
+
* // Refresh all tenant options:
|
|
39
|
+
* const all = await useTenantOption.refreshAll()
|
|
40
|
+
*/
|
|
41
|
+
declare const useTenantOption: ((key: C8YTenantOptionKey) => Promise<string | undefined>) & {
|
|
42
|
+
/**
|
|
43
|
+
* Invalidate the cache for a specific tenant option key.
|
|
44
|
+
* @param key - The tenant option key to invalidate
|
|
45
|
+
*/
|
|
46
|
+
invalidate: (key: C8YTenantOptionKey) => Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Force refresh a specific tenant option key (invalidates and re-fetches).
|
|
49
|
+
* @param key - The tenant option key to refresh
|
|
50
|
+
*/
|
|
51
|
+
refresh: (key: C8YTenantOptionKey) => Promise<string | undefined>;
|
|
52
|
+
/**
|
|
53
|
+
* Invalidate all tenant option caches that have been accessed.
|
|
54
|
+
* Only invalidates keys that have been fetched at least once.
|
|
55
|
+
*/
|
|
56
|
+
invalidateAll: () => Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Refresh all tenant options that have been accessed.
|
|
59
|
+
* Only refreshes keys that have been fetched at least once.
|
|
60
|
+
* @returns Object mapping keys to their refreshed values
|
|
61
|
+
*/
|
|
62
|
+
refreshAll: () => Promise<Record<string, string | undefined>>;
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
export { useTenantOption };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useDeployedTenantClient } from "./client.mjs";
|
|
2
|
+
import { defineCachedFunction } from "nitro/cache";
|
|
3
|
+
import { useStorage } from "nitro/storage";
|
|
4
|
+
import { useRuntimeConfig } from "nitro/runtime-config";
|
|
5
|
+
|
|
6
|
+
//#region src/utils/tenantOptions.ts
|
|
7
|
+
/**
|
|
8
|
+
* Gets the cache TTL for a specific tenant option key.
|
|
9
|
+
* Uses per-key override if defined, otherwise falls back to default TTL.
|
|
10
|
+
* @param key - The tenant option key
|
|
11
|
+
*/
|
|
12
|
+
function getTenantOptionCacheTTL(key) {
|
|
13
|
+
const config = useRuntimeConfig();
|
|
14
|
+
return config.c8yTenantOptionsPerKeyTTL?.[key] ?? config.c8yDefaultTenantOptionsTTL ?? 600;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Internal storage for cached functions per key
|
|
18
|
+
*/
|
|
19
|
+
const tenantOptionFetchers = {};
|
|
20
|
+
/**
|
|
21
|
+
* Factory function that creates a cached fetcher for a specific tenant option key.
|
|
22
|
+
* @param key - The tenant option key
|
|
23
|
+
*/
|
|
24
|
+
function createCachedTenantOptionFetcher(key) {
|
|
25
|
+
const cacheName = `_c8y_nitro_tenant_option_${key.replace(/\./g, "_")}`;
|
|
26
|
+
const fetcher = defineCachedFunction(async () => {
|
|
27
|
+
const client = await useDeployedTenantClient();
|
|
28
|
+
const category = useRuntimeConfig().c8ySettingsCategory;
|
|
29
|
+
const apiKey = key.replace(/^credentials\./, "");
|
|
30
|
+
try {
|
|
31
|
+
return (await client.options.tenant.detail({
|
|
32
|
+
key: apiKey,
|
|
33
|
+
category
|
|
34
|
+
})).data.value;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error?.res?.status === 404 || error?.status === 404) return;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}, {
|
|
40
|
+
maxAge: getTenantOptionCacheTTL(key),
|
|
41
|
+
name: cacheName,
|
|
42
|
+
group: "c8y_nitro",
|
|
43
|
+
swr: false
|
|
44
|
+
});
|
|
45
|
+
return Object.assign(fetcher, {
|
|
46
|
+
invalidate: async () => {
|
|
47
|
+
const completeKey = `c8y_nitro:functions:${cacheName}.json`;
|
|
48
|
+
await useStorage("cache").removeItem(completeKey);
|
|
49
|
+
},
|
|
50
|
+
refresh: async () => {
|
|
51
|
+
const completeKey = `c8y_nitro:functions:${cacheName}.json`;
|
|
52
|
+
await useStorage("cache").removeItem(completeKey);
|
|
53
|
+
return await fetcher();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Gets or creates a cached fetcher for a specific tenant option key.
|
|
59
|
+
* @param key - The tenant option key
|
|
60
|
+
*/
|
|
61
|
+
function getOrCreateFetcher(key) {
|
|
62
|
+
let fetcher = tenantOptionFetchers[key];
|
|
63
|
+
if (!fetcher) {
|
|
64
|
+
fetcher = createCachedTenantOptionFetcher(key);
|
|
65
|
+
tenantOptionFetchers[key] = fetcher;
|
|
66
|
+
}
|
|
67
|
+
return fetcher;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Fetches a tenant option value by key.\
|
|
71
|
+
* Uses the deployed tenant's service user credentials to access the Options API.\
|
|
72
|
+
* Results are cached based on the configured TTL (default: 10 minutes).
|
|
73
|
+
*
|
|
74
|
+
* @param key - The tenant option key to fetch (as defined in `manifest.settings`)
|
|
75
|
+
* @returns The option value as a string
|
|
76
|
+
*
|
|
77
|
+
* @config Cache TTL can be configured via:
|
|
78
|
+
* - `c8y.cache.defaultTenantOptionsTTL` — Default TTL for all keys (in seconds)
|
|
79
|
+
* - `c8y.cache.tenantOptions` — Per-key TTL overrides
|
|
80
|
+
* - `NITRO_C8Y_DEFAULT_TENANT_OPTIONS_TTL` — Environment variable for default TTL
|
|
81
|
+
*
|
|
82
|
+
* @note For encrypted options (keys starting with `credentials.`), the value is automatically
|
|
83
|
+
* decrypted by Cumulocity if this microservice is the owner of the option (category matches
|
|
84
|
+
* the microservice's settingsCategory/contextPath/name). The `credentials.` prefix is
|
|
85
|
+
* automatically stripped when calling the API.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // Fetch a tenant option:
|
|
89
|
+
* const value = await useTenantOption('myOption')
|
|
90
|
+
*
|
|
91
|
+
* // Fetch an encrypted secret:
|
|
92
|
+
* const secret = await useTenantOption('credentials.apiKey')
|
|
93
|
+
*
|
|
94
|
+
* // Invalidate cache for a specific key:
|
|
95
|
+
* await useTenantOption.invalidate('myOption')
|
|
96
|
+
*
|
|
97
|
+
* // Force refresh a specific key:
|
|
98
|
+
* const fresh = await useTenantOption.refresh('myOption')
|
|
99
|
+
*
|
|
100
|
+
* // Invalidate all tenant option caches:
|
|
101
|
+
* await useTenantOption.invalidateAll()
|
|
102
|
+
*
|
|
103
|
+
* // Refresh all tenant options:
|
|
104
|
+
* const all = await useTenantOption.refreshAll()
|
|
105
|
+
*/
|
|
106
|
+
const useTenantOption = Object.assign(async (key) => {
|
|
107
|
+
return await getOrCreateFetcher(key)();
|
|
108
|
+
}, {
|
|
109
|
+
invalidate: async (key) => {
|
|
110
|
+
const fetcher = tenantOptionFetchers[key];
|
|
111
|
+
if (fetcher) await fetcher.invalidate();
|
|
112
|
+
},
|
|
113
|
+
refresh: async (key) => {
|
|
114
|
+
return await getOrCreateFetcher(key).refresh();
|
|
115
|
+
},
|
|
116
|
+
invalidateAll: async () => {
|
|
117
|
+
await Promise.all(Object.values(tenantOptionFetchers).map((fetcher) => fetcher?.invalidate()));
|
|
118
|
+
},
|
|
119
|
+
refreshAll: async () => {
|
|
120
|
+
const entries = Object.entries(tenantOptionFetchers);
|
|
121
|
+
const values = await Promise.all(entries.map(([, fetcher]) => fetcher?.refresh()));
|
|
122
|
+
return Object.fromEntries(entries.map(([key], i) => [key, values[i]]));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
export { useTenantOption };
|
package/dist/utils.d.mts
CHANGED
|
@@ -2,4 +2,6 @@ import { useDeployedTenantClient, useSubscribedTenantClients, useUserClient, use
|
|
|
2
2
|
import { hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant } from "./utils/middleware.mjs";
|
|
3
3
|
import { useUser, useUserRoles } from "./utils/resources.mjs";
|
|
4
4
|
import { useDeployedTenantCredentials, useSubscribedTenantCredentials, useUserTenantCredentials } from "./utils/credentials.mjs";
|
|
5
|
-
|
|
5
|
+
import { useTenantOption } from "./utils/tenantOptions.mjs";
|
|
6
|
+
import { createError, createLogger, useLogger } from "./utils/logging.mjs";
|
|
7
|
+
export { createError, createLogger, hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant, useDeployedTenantClient, useDeployedTenantCredentials, useLogger, useSubscribedTenantClients, useSubscribedTenantCredentials, useTenantOption, useUser, useUserClient, useUserRoles, useUserTenantClient, useUserTenantCredentials };
|
package/dist/utils.mjs
CHANGED
|
@@ -2,5 +2,7 @@ import { useDeployedTenantCredentials, useSubscribedTenantCredentials, useUserTe
|
|
|
2
2
|
import { useDeployedTenantClient, useSubscribedTenantClients, useUserClient, useUserTenantClient } from "./utils/client.mjs";
|
|
3
3
|
import { useUser, useUserRoles } from "./utils/resources.mjs";
|
|
4
4
|
import { hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant } from "./utils/middleware.mjs";
|
|
5
|
+
import { useTenantOption } from "./utils/tenantOptions.mjs";
|
|
6
|
+
import { createError, createLogger, useLogger } from "./utils/logging.mjs";
|
|
5
7
|
|
|
6
|
-
export { hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant, useDeployedTenantClient, useDeployedTenantCredentials, useSubscribedTenantClients, useSubscribedTenantCredentials, useUser, useUserClient, useUserRoles, useUserTenantClient, useUserTenantCredentials };
|
|
8
|
+
export { createError, createLogger, hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant, useDeployedTenantClient, useDeployedTenantCredentials, useLogger, useSubscribedTenantClients, useSubscribedTenantCredentials, useTenantOption, useUser, useUserClient, useUserRoles, useUserTenantClient, useUserTenantCredentials };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8y-nitro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lightning fast Cumulocity IoT microservice development powered by Nitro",
|
|
6
6
|
"keywords": [
|
|
@@ -48,9 +48,10 @@
|
|
|
48
48
|
"dist"
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"c12": "^
|
|
51
|
+
"c12": "^4.0.0-beta.2",
|
|
52
52
|
"citty": "^0.2.0",
|
|
53
53
|
"consola": "^3.4.2",
|
|
54
|
+
"evlog": "^2.1.0",
|
|
54
55
|
"jszip": "^3.10.1",
|
|
55
56
|
"pathe": "^2.0.3",
|
|
56
57
|
"pkg-types": "^2.3.0",
|
|
@@ -58,12 +59,12 @@
|
|
|
58
59
|
"tinyexec": "^1.0.2"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
|
-
"@schplitt/eslint-config": "^1.
|
|
62
|
+
"@schplitt/eslint-config": "^1.3.1",
|
|
62
63
|
"@types/spinnies": "^0.5.3",
|
|
63
|
-
"bumpp": "^10.4.
|
|
64
|
-
"eslint": "^
|
|
64
|
+
"bumpp": "^10.4.1",
|
|
65
|
+
"eslint": "^10.0.0",
|
|
65
66
|
"memfs": "^4.56.10",
|
|
66
|
-
"tsdown": "^0.20.
|
|
67
|
+
"tsdown": "^0.20.3",
|
|
67
68
|
"typescript": "^5.9.3",
|
|
68
69
|
"vitest": "^4.0.18"
|
|
69
70
|
},
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
"typecheck": "tsc --noEmit",
|
|
83
84
|
"prerelease": "eslint && tsc --noEmit && tsdown && vitest run",
|
|
84
85
|
"release": "bumpp",
|
|
85
|
-
"test": "vitest"
|
|
86
|
+
"test": "vitest",
|
|
87
|
+
"test:run": "vitest run"
|
|
86
88
|
}
|
|
87
89
|
}
|