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.
- package/dist/bootstrap-CGOe2HxK.mjs +61 -0
- package/dist/{cli/utils/c8y-api.mjs → c8y-api-BBSKRwKs.mjs} +73 -3
- package/dist/cli/index.mjs +5 -7
- package/dist/{cli/utils/config.mjs → config-Dqi-ttQi.mjs} +1 -3
- package/dist/{cli/utils/env-file.mjs → env-file-B0BK-uZW.mjs} +1 -3
- package/dist/{types/manifest.d.mts → index-B6HtYHU0.d.mts} +94 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +542 -11
- package/dist/{cli/commands/options.mjs → options-CuGdGP4l.mjs} +27 -30
- package/dist/{package.mjs → package-BAjMvZYS.mjs} +2 -3
- package/dist/{cli/commands/roles.mjs → roles-DrJsxUG-.mjs} +11 -14
- package/dist/runtime/handlers/liveness-readiness.d.mts +8 -0
- package/dist/runtime/handlers/liveness-readiness.mjs +7 -0
- package/dist/runtime/middlewares/dev-user.d.mts +6 -0
- package/dist/runtime/middlewares/dev-user.mjs +23 -0
- package/dist/runtime/plugins/c8y-variables.d.mts +6 -0
- package/dist/runtime/plugins/c8y-variables.mjs +17 -0
- package/dist/types.d.mts +2 -25
- package/dist/types.mjs +1 -1
- package/dist/utils.d.mts +292 -6
- package/dist/utils.mjs +444 -8
- package/package.json +10 -10
- package/dist/cli/commands/bootstrap.mjs +0 -64
- package/dist/module/apiClient.mjs +0 -207
- package/dist/module/autoBootstrap.mjs +0 -54
- package/dist/module/c8yzip.mjs +0 -66
- package/dist/module/constants.mjs +0 -6
- package/dist/module/docker.mjs +0 -101
- package/dist/module/manifest.mjs +0 -72
- package/dist/module/probeCheck.mjs +0 -30
- package/dist/module/register.mjs +0 -58
- package/dist/module/runtime/handlers/liveness-readiness.ts +0 -7
- package/dist/module/runtime/middlewares/dev-user.ts +0 -25
- package/dist/module/runtime/plugins/c8y-variables.ts +0 -24
- package/dist/module/runtime.mjs +0 -38
- package/dist/module/runtimeConfig.mjs +0 -20
- package/dist/types/apiClient.d.mts +0 -16
- package/dist/types/cache.d.mts +0 -28
- package/dist/types/roles.d.mts +0 -4
- package/dist/types/tenantOptions.d.mts +0 -13
- package/dist/types/zip.d.mts +0 -22
- package/dist/utils/client.d.mts +0 -52
- package/dist/utils/client.mjs +0 -90
- package/dist/utils/credentials.d.mts +0 -71
- package/dist/utils/credentials.mjs +0 -120
- package/dist/utils/internal/common.mjs +0 -26
- package/dist/utils/logging.d.mts +0 -3
- package/dist/utils/logging.mjs +0 -4
- package/dist/utils/middleware.d.mts +0 -89
- package/dist/utils/middleware.mjs +0 -62
- package/dist/utils/resources.d.mts +0 -30
- package/dist/utils/resources.mjs +0 -49
- package/dist/utils/tenantOptions.d.mts +0 -65
- package/dist/utils/tenantOptions.mjs +0 -127
package/dist/utils.mjs
CHANGED
|
@@ -1,8 +1,444 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
"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.
|
|
52
|
-
"citty": "^0.2.
|
|
51
|
+
"c12": "^4.0.0-beta.3",
|
|
52
|
+
"citty": "^0.2.1",
|
|
53
53
|
"consola": "^3.4.2",
|
|
54
|
-
"evlog": "^2.
|
|
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.
|
|
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.
|
|
66
|
-
"memfs": "^4.56.
|
|
67
|
-
"tsdown": "^0.
|
|
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
|
|
69
|
+
"vitest": "^4.1.0"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"@c8y/client": ">=1021",
|
|
73
|
-
"nitro": "
|
|
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 };
|