c8y-nitro 0.3.0 → 0.4.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.
Files changed (54) hide show
  1. package/dist/bootstrap-CGOe2HxK.mjs +61 -0
  2. package/dist/{cli/utils/c8y-api.mjs → c8y-api-BBSKRwKs.mjs} +73 -3
  3. package/dist/cli/index.mjs +5 -7
  4. package/dist/{cli/utils/config.mjs → config-Dqi-ttQi.mjs} +1 -3
  5. package/dist/{cli/utils/env-file.mjs → env-file-B0BK-uZW.mjs} +1 -3
  6. package/dist/{types/manifest.d.mts → index-B6HtYHU0.d.mts} +94 -1
  7. package/dist/index.d.mts +1 -1
  8. package/dist/index.mjs +542 -11
  9. package/dist/{cli/commands/options.mjs → options-CuGdGP4l.mjs} +27 -30
  10. package/dist/{package.mjs → package-BAjMvZYS.mjs} +2 -3
  11. package/dist/{cli/commands/roles.mjs → roles-DrJsxUG-.mjs} +11 -14
  12. package/dist/runtime/handlers/liveness-readiness.d.mts +8 -0
  13. package/dist/runtime/handlers/liveness-readiness.mjs +7 -0
  14. package/dist/runtime/middlewares/dev-user.d.mts +6 -0
  15. package/dist/runtime/middlewares/dev-user.mjs +23 -0
  16. package/dist/runtime/plugins/c8y-variables.d.mts +6 -0
  17. package/dist/runtime/plugins/c8y-variables.mjs +17 -0
  18. package/dist/types.d.mts +2 -25
  19. package/dist/types.mjs +1 -1
  20. package/dist/utils.d.mts +292 -6
  21. package/dist/utils.mjs +444 -8
  22. package/package.json +10 -10
  23. package/dist/cli/commands/bootstrap.mjs +0 -64
  24. package/dist/module/apiClient.mjs +0 -207
  25. package/dist/module/autoBootstrap.mjs +0 -54
  26. package/dist/module/c8yzip.mjs +0 -66
  27. package/dist/module/constants.mjs +0 -6
  28. package/dist/module/docker.mjs +0 -101
  29. package/dist/module/manifest.mjs +0 -72
  30. package/dist/module/probeCheck.mjs +0 -30
  31. package/dist/module/register.mjs +0 -58
  32. package/dist/module/runtime/handlers/liveness-readiness.ts +0 -7
  33. package/dist/module/runtime/middlewares/dev-user.ts +0 -25
  34. package/dist/module/runtime/plugins/c8y-variables.ts +0 -24
  35. package/dist/module/runtime.mjs +0 -38
  36. package/dist/module/runtimeConfig.mjs +0 -20
  37. package/dist/types/apiClient.d.mts +0 -16
  38. package/dist/types/cache.d.mts +0 -28
  39. package/dist/types/roles.d.mts +0 -4
  40. package/dist/types/tenantOptions.d.mts +0 -13
  41. package/dist/types/zip.d.mts +0 -22
  42. package/dist/utils/client.d.mts +0 -52
  43. package/dist/utils/client.mjs +0 -90
  44. package/dist/utils/credentials.d.mts +0 -71
  45. package/dist/utils/credentials.mjs +0 -120
  46. package/dist/utils/internal/common.mjs +0 -26
  47. package/dist/utils/logging.d.mts +0 -3
  48. package/dist/utils/logging.mjs +0 -4
  49. package/dist/utils/middleware.d.mts +0 -89
  50. package/dist/utils/middleware.mjs +0 -62
  51. package/dist/utils/resources.d.mts +0 -30
  52. package/dist/utils/resources.mjs +0 -49
  53. package/dist/utils/tenantOptions.d.mts +0 -65
  54. package/dist/utils/tenantOptions.mjs +0 -127
package/dist/utils.mjs CHANGED
@@ -1,8 +1,444 @@
1
- import { useDeployedTenantCredentials, useSubscribedTenantCredentials, useUserTenantCredentials } from "./utils/credentials.mjs";
2
- import { useDeployedTenantClient, useSubscribedTenantClients, useUserClient, useUserTenantClient } from "./utils/client.mjs";
3
- import { useUser, useUserRoles } from "./utils/resources.mjs";
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";
7
-
8
- export { createError, createLogger, hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant, useDeployedTenantClient, useDeployedTenantCredentials, useLogger, useSubscribedTenantClients, useSubscribedTenantCredentials, useTenantOption, useUser, useUserClient, useUserRoles, useUserTenantClient, useUserTenantCredentials };
1
+ import process from "node:process";
2
+ import { useLogger } from "evlog/nitro/v3";
3
+ import { BasicAuth, Client, MicroserviceClientRequestAuth } from "@c8y/client";
4
+ import { defineCachedFunction } from "nitro/cache";
5
+ import { HTTPError, defineHandler } from "nitro/h3";
6
+ import { useStorage } from "nitro/storage";
7
+ import { useRuntimeConfig } from "nitro/runtime-config";
8
+ import { createError, createLogger } from "evlog";
9
+ //#region src/utils/internal/common.ts
10
+ /**
11
+ * Converts undici Request headers to the format expected by MicroserviceClientRequestAuth.\
12
+ * Extracts the following headers from the request:
13
+ * - `authorization`: Used for Basic Auth or Bearer token authentication
14
+ * - `cookie`: Used to extract XSRF-TOKEN and authorization token from cookies
15
+ *
16
+ * The MicroserviceClientRequestAuth class will automatically:
17
+ * - Extract XSRF-TOKEN from cookies for CSRF protection
18
+ * - Extract authorization token from cookies (prioritized over header auth)
19
+ * - Fall back to Authorization header if no cookie-based auth is present
20
+ *
21
+ * @param request - The HTTP request containing headers
22
+ * @returns Headers object compatible with \@c8y/client's MicroserviceClientRequestAuth
23
+ */
24
+ function convertRequestHeadersToC8yFormat(request) {
25
+ const headers = {};
26
+ const authorization = request.headers.get("authorization");
27
+ if (authorization) headers.authorization = authorization;
28
+ const cookie = request.headers.get("cookie");
29
+ if (cookie) headers.cookie = cookie;
30
+ return headers;
31
+ }
32
+ //#endregion
33
+ //#region src/utils/credentials.ts
34
+ /**
35
+ * Fetches credentials for all tenants subscribed to this microservice.\
36
+ * Uses bootstrap credentials from runtime config to query the microservice subscriptions API.\
37
+ * Results are cached based on the configured TTL (default: 10 minutes).\
38
+ * @returns Object mapping tenant IDs to their respective credentials
39
+ * @config Cache TTL can be configured via:
40
+ * - `c8y.cache.credentialsTTL` in the Nitro config (value in seconds)
41
+ * - `NITRO_C8Y_CACHE_CREDENTIALS_TTL` environment variable
42
+ * @example
43
+ * // Get all subscribed tenant credentials:
44
+ * const credentials = await useSubscribedTenantCredentials()
45
+ * console.log(Object.keys(credentials)) // ['t12345', 't67890']
46
+ *
47
+ * // Access specific tenant:
48
+ * const tenant1Creds = credentials['t12345']
49
+ *
50
+ * // Invalidate cache:
51
+ * await useSubscribedTenantCredentials.invalidate()
52
+ *
53
+ * // Force refresh:
54
+ * const freshCreds = await useSubscribedTenantCredentials.refresh()
55
+ */
56
+ const useSubscribedTenantCredentials = Object.assign(defineCachedFunction(async () => {
57
+ return (await Client.getMicroserviceSubscriptions({
58
+ tenant: process.env.C8Y_BOOTSTRAP_TENANT,
59
+ user: process.env.C8Y_BOOTSTRAP_USER,
60
+ password: process.env.C8Y_BOOTSTRAP_PASSWORD
61
+ }, process.env.C8Y_BASEURL)).reduce((acc, cred) => {
62
+ if (cred.tenant) acc[cred.tenant] = cred;
63
+ return acc;
64
+ }, {});
65
+ }, {
66
+ maxAge: useRuntimeConfig().c8yCredentialsCacheTTL ?? 600,
67
+ name: "_c8y_nitro_get_subscribed_tenant_credentials",
68
+ group: "c8y_nitro",
69
+ swr: false
70
+ }), {
71
+ invalidate: async () => {
72
+ await useStorage("cache").removeItem(`c8y_nitro:functions:_c8y_nitro_get_subscribed_tenant_credentials.json`);
73
+ },
74
+ refresh: async () => {
75
+ await useSubscribedTenantCredentials.invalidate();
76
+ return await useSubscribedTenantCredentials();
77
+ }
78
+ });
79
+ /**
80
+ * Fetches credentials for the tenant where this microservice is deployed.\
81
+ * Uses the C8Y_BOOTSTRAP_TENANT environment variable to identify the deployed tenant.\
82
+ * Returns credentials from the subscribed tenant credentials cache (cached based on configured TTL, default: 10 minutes).
83
+ * @returns Credentials for the deployed tenant
84
+ * @throws {HTTPError} If no credentials found for the deployed tenant
85
+ * @example
86
+ * // Get deployed tenant credentials:
87
+ * const creds = await useDeployedTenantCredentials()
88
+ * console.log(creds.tenant, creds.user)
89
+ *
90
+ * // Invalidate cache:
91
+ * await useDeployedTenantCredentials.invalidate()
92
+ *
93
+ * // Force refresh:
94
+ * const freshCreds = await useDeployedTenantCredentials.refresh()
95
+ * @note This function is not cached separately. It uses the cache of `useSubscribedTenantCredentials()`. Invalidating or refreshing one will refresh `useDeployedTenantCredentials()`s cache.
96
+ */
97
+ const useDeployedTenantCredentials = Object.assign(async () => {
98
+ const tenant = process.env.C8Y_BOOTSTRAP_TENANT;
99
+ const allCredsPromise = await useSubscribedTenantCredentials();
100
+ if (!allCredsPromise[tenant]) throw new HTTPError({
101
+ message: `No credentials found for tenant deployed tenant '${tenant}'`,
102
+ status: 500,
103
+ statusText: "Internal Server Error"
104
+ });
105
+ return allCredsPromise[tenant];
106
+ }, {
107
+ invalidate: useSubscribedTenantCredentials.invalidate,
108
+ refresh: async () => {
109
+ await useDeployedTenantCredentials.invalidate();
110
+ return await useDeployedTenantCredentials();
111
+ }
112
+ });
113
+ /**
114
+ * Fetches credentials for the tenant of the current user making the request.\
115
+ * Extracts the user's tenant ID from the request headers and returns corresponding credentials.\
116
+ * Results are cached in the request context for subsequent calls within the same request.
117
+ * @param requestOrEvent - The H3Event or ServerRequest from the current request
118
+ * @returns Credentials for the user's tenant
119
+ * @throws {HTTPError} If no subscribed tenant credentials found for the user's tenant
120
+ * @example
121
+ * // In a request handler:
122
+ * const userCreds = await useUserTenantCredentials(event)
123
+ * console.log(userCreds.tenant, userCreds.user)
124
+ *
125
+ * // Credentials are automatically cached for the request duration
126
+ * const sameCreds = await useUserTenantCredentials(event) // Uses cached value
127
+ */
128
+ async function useUserTenantCredentials(requestOrEvent) {
129
+ const request = "req" in requestOrEvent ? requestOrEvent.req : requestOrEvent;
130
+ if (request.context?.["c8y_user_tenant_credentials"]) return request.context["c8y_user_tenant_credentials"];
131
+ const tenantId = useUserClient(requestOrEvent).core.tenant;
132
+ const userTenantCreds = (await useSubscribedTenantCredentials())[tenantId];
133
+ if (!userTenantCreds) throw new HTTPError({
134
+ message: `No subscribed tenant credentials found for user tenant '${tenantId}'`,
135
+ status: 500,
136
+ statusText: "Internal Server Error"
137
+ });
138
+ request.context ??= {};
139
+ request.context["c8y_user_tenant_credentials"] = userTenantCreds;
140
+ return userTenantCreds;
141
+ }
142
+ //#endregion
143
+ //#region src/utils/client.ts
144
+ /**
145
+ * Creates a Cumulocity client authenticated with the current user's credentials.\
146
+ * Extracts credentials from the Authorization header of the current request.
147
+ * @param requestOrEvent - The H3Event or ServerRequest from the current request
148
+ * @returns A configured Cumulocity Client instance
149
+ * @example
150
+ * // In a request handler:
151
+ * const client = useUserClient(event)
152
+ * const { data } = await client.inventory.list()
153
+ */
154
+ function useUserClient(requestOrEvent) {
155
+ const request = "req" in requestOrEvent ? requestOrEvent.req : requestOrEvent;
156
+ if (request.context?.["c8y_user_client"]) return request.context["c8y_user_client"];
157
+ const client = new Client(new MicroserviceClientRequestAuth(convertRequestHeadersToC8yFormat(request)), process.env.C8Y_BASEURL);
158
+ request.context ??= {};
159
+ request.context["c8y_user_client"] = client;
160
+ return client;
161
+ }
162
+ /**
163
+ * Creates a Cumulocity client for the tenant of the current user.\
164
+ * Uses the tenant's service user credentials rather than the user's own credentials.
165
+ * @param requestOrEvent - The H3Event or ServerRequest from the current request
166
+ * @returns A configured Cumulocity Client instance for the user's tenant
167
+ * @example
168
+ * // In a request handler:
169
+ * const tenantClient = await useUserTenantClient(event)
170
+ * const { data } = await tenantClient.inventory.list()
171
+ */
172
+ async function useUserTenantClient(requestOrEvent) {
173
+ const request = "req" in requestOrEvent ? requestOrEvent.req : requestOrEvent;
174
+ if (request.context?.["c8y_user_tenant_client"]) return request.context["c8y_user_tenant_client"];
175
+ const tenantId = useUserClient(requestOrEvent).core.tenant;
176
+ const creds = await useSubscribedTenantCredentials();
177
+ if (!creds[tenantId]) throw new HTTPError({
178
+ message: `No subscribed tenant credentials found for user tenant '${tenantId}'`,
179
+ status: 500,
180
+ statusText: "Internal Server Error"
181
+ });
182
+ const tenantClient = new Client(new BasicAuth(creds[tenantId]), process.env.C8Y_BASEURL);
183
+ request.context ??= {};
184
+ request.context["c8y_user_tenant_client"] = tenantClient;
185
+ return tenantClient;
186
+ }
187
+ /**
188
+ * Creates Cumulocity clients for all tenants subscribed to this microservice.\
189
+ * Each client is authenticated with that tenant's service user credentials.\
190
+ * @returns Object mapping tenant IDs to their respective Client instances
191
+ * @example
192
+ * // Get clients for all subscribed tenants:
193
+ * const clients = await useSubscribedTenantClients()
194
+ * for (const [tenant, client] of Object.entries(clients)) {
195
+ * const { data } = await client.inventory.list()
196
+ * console.log(`Tenant ${tenant} has ${data.length} inventory items`)
197
+ * }
198
+ */
199
+ async function useSubscribedTenantClients() {
200
+ const creds = await useSubscribedTenantCredentials();
201
+ const clients = {};
202
+ for (const [tenant, tenantCreds] of Object.entries(creds)) clients[tenant] = new Client(new BasicAuth(tenantCreds), process.env.C8Y_BASEURL);
203
+ return clients;
204
+ }
205
+ /**
206
+ * Creates a Cumulocity client for the tenant where this microservice is deployed.\
207
+ * Uses the bootstrap tenant ID from runtime config to identify the deployed tenant.\
208
+ * @returns A configured Cumulocity Client instance for the deployed tenant
209
+ * @example
210
+ * // Get client for the tenant hosting this microservice:
211
+ * const client = await useDeployedTenantClient()
212
+ * const { data } = await client.application.list()
213
+ */
214
+ async function useDeployedTenantClient() {
215
+ const creds = await useSubscribedTenantCredentials();
216
+ const tenant = process.env.C8Y_BOOTSTRAP_TENANT;
217
+ if (!creds[tenant]) throw new HTTPError({
218
+ message: `No subscribed tenant credentials found for tenant '${tenant}'`,
219
+ status: 500,
220
+ statusText: "Internal Server Error"
221
+ });
222
+ return new Client(new BasicAuth(creds[tenant]), process.env.C8Y_BASEURL);
223
+ }
224
+ //#endregion
225
+ //#region src/utils/resources.ts
226
+ /**
227
+ * Fetches the current user from Cumulocity using credentials extracted from the current request's headers.
228
+ * This is a non-cached version - fetches fresh data on every call.
229
+ * @param requestOrEvent - The H3Event or ServerRequest from the current request
230
+ * @returns The current user object from Cumulocity
231
+ * @example
232
+ * // In a request handler:
233
+ * const user = await useUser(event)
234
+ * console.log(user.userName, user.email)
235
+ */
236
+ async function useUser(requestOrEvent) {
237
+ const request = "req" in requestOrEvent ? requestOrEvent.req : requestOrEvent;
238
+ if (request.context?.["c8y_user"]) return request.context["c8y_user"];
239
+ const { res, data: user } = await useUserClient(requestOrEvent).user.currentWithEffectiveRoles();
240
+ if (!res.ok) throw new HTTPError({
241
+ message: `Failed to fetch current user`,
242
+ status: res.status,
243
+ statusText: res.statusText
244
+ });
245
+ request.context ??= {};
246
+ request.context["c8y_user"] = user;
247
+ return user;
248
+ }
249
+ /**
250
+ * Fetches the roles of the current user from Cumulocity.
251
+ * Internally calls `useUser()` and extracts role IDs from the user object.
252
+ * This is a non-cached version - fetches fresh data on every call.
253
+ * @param requestOrEvent - The H3Event or ServerRequest from the current request
254
+ * @returns Array of role ID strings assigned to the current user
255
+ * @example
256
+ * // In a request handler:
257
+ * const roles = await useUserRoles(event)
258
+ * console.log(roles) // ['ROLE_INVENTORY_READ', 'ROLE_INVENTORY_ADMIN']
259
+ */
260
+ async function useUserRoles(requestOrEvent) {
261
+ const request = "req" in requestOrEvent ? requestOrEvent.req : requestOrEvent;
262
+ if (request.context?.["c8y_user_roles"]) return request.context["c8y_user_roles"];
263
+ const userRoles = (await useUser(requestOrEvent)).effectiveRoles?.map((role) => role.name) ?? [];
264
+ request.context ??= {};
265
+ request.context["c8y_user_roles"] = userRoles;
266
+ return userRoles;
267
+ }
268
+ //#endregion
269
+ //#region src/utils/middleware.ts
270
+ function hasUserRequiredRole(roleOrRoles) {
271
+ return defineHandler(async (event) => {
272
+ const requiredRoles = Array.isArray(roleOrRoles) ? roleOrRoles : [roleOrRoles];
273
+ const userRoles = await useUserRoles(event);
274
+ if (!requiredRoles.some((role) => userRoles.includes(role))) throw new HTTPError({
275
+ status: 403,
276
+ statusText: "Forbidden",
277
+ message: `User does not have required role(s) to access this resource: ${requiredRoles.join(", ")}`
278
+ });
279
+ });
280
+ }
281
+ function isUserFromAllowedTenant(tenantIdOrIds) {
282
+ return defineHandler(async (event) => {
283
+ const allowedTenants = Array.isArray(tenantIdOrIds) ? tenantIdOrIds : [tenantIdOrIds];
284
+ const userTenantId = useUserClient(event).core.tenant;
285
+ if (!allowedTenants.includes(userTenantId)) throw new HTTPError({
286
+ status: 403,
287
+ statusText: "Forbidden",
288
+ message: `User's tenant '${userTenantId}' is not allowed to access this resource. Allowed tenants: ${allowedTenants.join(", ")}`
289
+ });
290
+ });
291
+ }
292
+ /**
293
+ * Middleware to check if the current user belongs to the deployed tenant.\
294
+ * The deployed tenant is where this microservice is hosted (C8Y_BOOTSTRAP_TENANT).\
295
+ * If the user's tenant doesn't match the deployed tenant, throws a 403 Forbidden error.\
296
+ * Must be used within a request handler context.\
297
+ * @returns Event handler that validates user is from deployed tenant
298
+ * @example
299
+ * // Only allow users from the deployed tenant:
300
+ * export default defineHandler({
301
+ * middleware: [isUserFromDeployedTenant()],
302
+ * handler: async () => {
303
+ * return { message: 'You have access' }
304
+ * }
305
+ * })
306
+ */
307
+ function isUserFromDeployedTenant() {
308
+ return defineHandler(async (event) => {
309
+ const userTenantId = useUserClient(event).core.tenant;
310
+ const deployedTenantId = process.env.C8Y_BOOTSTRAP_TENANT;
311
+ if (!deployedTenantId) throw new HTTPError({
312
+ status: 500,
313
+ statusText: "Internal Server Error",
314
+ message: "C8Y_BOOTSTRAP_TENANT environment variable is not set"
315
+ });
316
+ if (userTenantId !== deployedTenantId) throw new HTTPError({
317
+ status: 403,
318
+ statusText: "Forbidden",
319
+ message: `Only users from tenant '${deployedTenantId}' can access this resource.`
320
+ });
321
+ });
322
+ }
323
+ //#endregion
324
+ //#region src/utils/tenantOptions.ts
325
+ /**
326
+ * Gets the cache TTL for a specific tenant option key.
327
+ * Uses per-key override if defined, otherwise falls back to default TTL.
328
+ * @param key - The tenant option key
329
+ */
330
+ function getTenantOptionCacheTTL(key) {
331
+ const config = useRuntimeConfig();
332
+ return config.c8yTenantOptionsPerKeyTTL?.[key] ?? config.c8yDefaultTenantOptionsTTL ?? 600;
333
+ }
334
+ /**
335
+ * Internal storage for cached functions per key
336
+ */
337
+ const tenantOptionFetchers = {};
338
+ /**
339
+ * Factory function that creates a cached fetcher for a specific tenant option key.
340
+ * @param key - The tenant option key
341
+ */
342
+ function createCachedTenantOptionFetcher(key) {
343
+ const cacheName = `_c8y_nitro_tenant_option_${key.replace(/\./g, "_")}`;
344
+ const fetcher = defineCachedFunction(async () => {
345
+ const client = await useDeployedTenantClient();
346
+ const category = useRuntimeConfig().c8ySettingsCategory;
347
+ const apiKey = key.replace(/^credentials\./, "");
348
+ try {
349
+ return (await client.options.tenant.detail({
350
+ key: apiKey,
351
+ category
352
+ })).data.value;
353
+ } catch (error) {
354
+ if (error?.res?.status === 404 || error?.status === 404) return;
355
+ throw error;
356
+ }
357
+ }, {
358
+ maxAge: getTenantOptionCacheTTL(key),
359
+ name: cacheName,
360
+ group: "c8y_nitro",
361
+ swr: false
362
+ });
363
+ return Object.assign(fetcher, {
364
+ invalidate: async () => {
365
+ const completeKey = `c8y_nitro:functions:${cacheName}.json`;
366
+ await useStorage("cache").removeItem(completeKey);
367
+ },
368
+ refresh: async () => {
369
+ const completeKey = `c8y_nitro:functions:${cacheName}.json`;
370
+ await useStorage("cache").removeItem(completeKey);
371
+ return await fetcher();
372
+ }
373
+ });
374
+ }
375
+ /**
376
+ * Gets or creates a cached fetcher for a specific tenant option key.
377
+ * @param key - The tenant option key
378
+ */
379
+ function getOrCreateFetcher(key) {
380
+ let fetcher = tenantOptionFetchers[key];
381
+ if (!fetcher) {
382
+ fetcher = createCachedTenantOptionFetcher(key);
383
+ tenantOptionFetchers[key] = fetcher;
384
+ }
385
+ return fetcher;
386
+ }
387
+ /**
388
+ * Fetches a tenant option value by key.\
389
+ * Uses the deployed tenant's service user credentials to access the Options API.\
390
+ * Results are cached based on the configured TTL (default: 10 minutes).
391
+ *
392
+ * @param key - The tenant option key to fetch (as defined in `manifest.settings`)
393
+ * @returns The option value as a string
394
+ *
395
+ * @config Cache TTL can be configured via:
396
+ * - `c8y.cache.defaultTenantOptionsTTL` — Default TTL for all keys (in seconds)
397
+ * - `c8y.cache.tenantOptions` — Per-key TTL overrides
398
+ * - `NITRO_C8Y_DEFAULT_TENANT_OPTIONS_TTL` — Environment variable for default TTL
399
+ *
400
+ * @note For encrypted options (keys starting with `credentials.`), the value is automatically
401
+ * decrypted by Cumulocity if this microservice is the owner of the option (category matches
402
+ * the microservice's settingsCategory/contextPath/name). The `credentials.` prefix is
403
+ * automatically stripped when calling the API.
404
+ *
405
+ * @example
406
+ * // Fetch a tenant option:
407
+ * const value = await useTenantOption('myOption')
408
+ *
409
+ * // Fetch an encrypted secret:
410
+ * const secret = await useTenantOption('credentials.apiKey')
411
+ *
412
+ * // Invalidate cache for a specific key:
413
+ * await useTenantOption.invalidate('myOption')
414
+ *
415
+ * // Force refresh a specific key:
416
+ * const fresh = await useTenantOption.refresh('myOption')
417
+ *
418
+ * // Invalidate all tenant option caches:
419
+ * await useTenantOption.invalidateAll()
420
+ *
421
+ * // Refresh all tenant options:
422
+ * const all = await useTenantOption.refreshAll()
423
+ */
424
+ const useTenantOption = Object.assign(async (key) => {
425
+ return await getOrCreateFetcher(key)();
426
+ }, {
427
+ invalidate: async (key) => {
428
+ const fetcher = tenantOptionFetchers[key];
429
+ if (fetcher) await fetcher.invalidate();
430
+ },
431
+ refresh: async (key) => {
432
+ return await getOrCreateFetcher(key).refresh();
433
+ },
434
+ invalidateAll: async () => {
435
+ await Promise.all(Object.values(tenantOptionFetchers).map((fetcher) => fetcher?.invalidate()));
436
+ },
437
+ refreshAll: async () => {
438
+ const entries = Object.entries(tenantOptionFetchers);
439
+ const values = await Promise.all(entries.map(([, fetcher]) => fetcher?.refresh()));
440
+ return Object.fromEntries(entries.map(([key], i) => [key, values[i]]));
441
+ }
442
+ });
443
+ //#endregion
444
+ 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.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Lightning fast Cumulocity IoT microservice development powered by Nitro",
6
6
  "keywords": [
@@ -48,29 +48,29 @@
48
48
  "dist"
49
49
  ],
50
50
  "dependencies": {
51
- "c12": "^4.0.0-beta.2",
52
- "citty": "^0.2.0",
51
+ "c12": "^4.0.0-beta.3",
52
+ "citty": "^0.2.1",
53
53
  "consola": "^3.4.2",
54
- "evlog": "^2.1.0",
54
+ "evlog": "^2.6.0",
55
55
  "jszip": "^3.10.1",
56
56
  "pathe": "^2.0.3",
57
57
  "pkg-types": "^2.3.0",
58
58
  "spinnies": "^0.5.1",
59
- "tinyexec": "^1.0.2"
59
+ "tinyexec": "^1.0.4"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@schplitt/eslint-config": "^1.3.1",
63
63
  "@types/spinnies": "^0.5.3",
64
64
  "bumpp": "^10.4.1",
65
- "eslint": "^10.0.0",
66
- "memfs": "^4.56.10",
67
- "tsdown": "^0.20.3",
65
+ "eslint": "^10.0.3",
66
+ "memfs": "^4.56.11",
67
+ "tsdown": "^0.21.2",
68
68
  "typescript": "^5.9.3",
69
- "vitest": "^4.0.18"
69
+ "vitest": "^4.1.0"
70
70
  },
71
71
  "peerDependencies": {
72
72
  "@c8y/client": ">=1021",
73
- "nitro": "v3.0.1-alpha.2"
73
+ "nitro": "3.0.260311-beta"
74
74
  },
75
75
  "engines": {
76
76
  "node": ">=24.0.0"
@@ -1,64 +0,0 @@
1
- import { createC8yManifest } from "../../module/manifest.mjs";
2
- import { createBasicAuthHeader, createMicroservice, findMicroserviceByName, getBootstrapCredentials, subscribeToApplication, updateMicroservice } from "../utils/c8y-api.mjs";
3
- import { writeBootstrapCredentials } from "../utils/env-file.mjs";
4
- import { loadC8yConfig, validateBootstrapEnv } from "../utils/config.mjs";
5
- import { defineCommand, runCommand } from "citty";
6
- import { consola } from "consola";
7
-
8
- //#region src/cli/commands/bootstrap.ts
9
- var bootstrap_default = defineCommand({
10
- meta: {
11
- name: "bootstrap",
12
- description: "Bootstrap your microservice to the development tenant"
13
- },
14
- args: {},
15
- async run() {
16
- consola.info("Loading configuration...");
17
- const { env, c8yOptions, configDir } = await loadC8yConfig();
18
- consola.info("Validating environment variables...");
19
- const envVars = validateBootstrapEnv(env);
20
- consola.info("Building manifest...");
21
- const manifest = await createC8yManifest(configDir, c8yOptions?.manifest);
22
- consola.success(`Manifest created for: ${manifest.name} v${manifest.version}`);
23
- const authHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
24
- consola.info(`Checking if microservice "${manifest.name}" exists...`);
25
- const existingApp = await findMicroserviceByName(envVars.C8Y_BASEURL, manifest.name, authHeader);
26
- let appId;
27
- if (existingApp) {
28
- consola.warn(`Microservice "${manifest.name}" already exists on development tenant (ID: ${existingApp.id})`);
29
- if (!await consola.prompt("Do you want to update the existing microservice?", {
30
- type: "confirm",
31
- cancel: "reject"
32
- })) {
33
- consola.info("Bootstrap cancelled.");
34
- return;
35
- }
36
- consola.info("Updating microservice...");
37
- appId = (await updateMicroservice(envVars.C8Y_BASEURL, existingApp.id, manifest, authHeader)).id;
38
- consola.success(`Microservice updated successfully (ID: ${appId})`);
39
- } else {
40
- consola.info("Creating microservice...");
41
- appId = (await createMicroservice(envVars.C8Y_BASEURL, manifest, authHeader)).id;
42
- consola.success(`Microservice created successfully (ID: ${appId})`);
43
- }
44
- consola.info("Subscribing tenant to application...");
45
- await subscribeToApplication(envVars.C8Y_BASEURL, envVars.C8Y_DEVELOPMENT_TENANT, appId, authHeader);
46
- consola.success("Tenant subscribed to application");
47
- consola.info("Fetching bootstrap credentials...");
48
- const credentials = await getBootstrapCredentials(envVars.C8Y_BASEURL, appId, authHeader);
49
- consola.info("Writing bootstrap credentials...");
50
- const envFileName = await writeBootstrapCredentials(configDir, {
51
- C8Y_BOOTSTRAP_TENANT: credentials.tenant,
52
- C8Y_BOOTSTRAP_USER: credentials.name,
53
- C8Y_BOOTSTRAP_PASSWORD: credentials.password
54
- });
55
- consola.success(`Bootstrap credentials written to ${envFileName}`);
56
- if (manifest.roles && manifest.roles.length > 0) {
57
- if (await consola.prompt("Do you want to manage microservice roles for your development user?", { type: "confirm" })) await runCommand(await import("./roles.mjs").then((r) => r.default), { rawArgs: [] });
58
- }
59
- consola.success("Bootstrap complete!");
60
- }
61
- });
62
-
63
- //#endregion
64
- export { bootstrap_default as default };