c8y-nitro 0.7.0 → 0.7.2
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/autoBootstrap-Drk7ltXK.mjs +212 -0
- package/dist/{bootstrap-ObaEF7rN.mjs → bootstrap-C1Qly0J_.mjs} +2 -2
- package/dist/{c8y-api-BFjK6AYb.mjs → c8y-api-DscUs-Kd.mjs} +22 -18
- package/dist/cli/index.mjs +4 -4
- package/dist/client-D0Tdq7H1.mjs +280 -0
- package/dist/{index-ByDSMEvU.d.mts → index-vu4xaFkz.d.mts} +51 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +44 -183
- package/dist/logging-BUYT8Z2V.mjs +57 -0
- package/dist/options-CmLpZ3Qf.mjs +179 -0
- package/dist/{package-BhFLRPOb.mjs → package-BHOzdhHp.mjs} +1 -1
- package/dist/{roles-B58Fguop.mjs → roles-B-DNuScZ.mjs} +1 -1
- package/dist/runtime/handlers/invalidateTenantOptions.mjs +2 -215
- package/dist/runtime/handlers/openapi.mjs +57 -0
- package/dist/runtime/handlers/rewriteRequestUrl.mjs +38 -0
- package/dist/runtime/middlewares/c8y-client.mjs +1 -1
- package/dist/types.d.mts +2 -2
- package/dist/utils.d.mts +80 -3
- package/dist/utils.mjs +91 -2
- package/package.json +1 -1
- package/dist/logging-DYnYICoX.mjs +0 -3
- package/dist/options-B7_yb_wr.mjs +0 -147
|
@@ -1,222 +1,9 @@
|
|
|
1
|
-
import { t as createError } from "../../logging-
|
|
1
|
+
import { t as createError } from "../../logging-BUYT8Z2V.mjs";
|
|
2
|
+
import { t as useDeployedTenantClient } from "../../client-D0Tdq7H1.mjs";
|
|
2
3
|
import { c8yTenantOptionKeys } from "c8y-nitro/runtime";
|
|
3
|
-
import { BasicAuth, Client, MicroserviceClientRequestAuth } from "@c8y/client";
|
|
4
4
|
import { defineCachedFunction } from "nitro/cache";
|
|
5
|
-
import { Buffer } from "node:buffer";
|
|
6
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
7
|
-
import process from "node:process";
|
|
8
5
|
import { useRuntimeConfig } from "nitro/runtime-config";
|
|
9
|
-
import { useNitroHooks } from "nitro/app";
|
|
10
6
|
import { defineEventHandler, getQuery } from "nitro/h3";
|
|
11
|
-
//#region src/utils/internal/common.ts
|
|
12
|
-
/**
|
|
13
|
-
* Converts undici Request headers to the format expected by MicroserviceClientRequestAuth.\
|
|
14
|
-
* Extracts the following headers from the request:
|
|
15
|
-
* - `authorization`: Used for Basic Auth or Bearer token authentication
|
|
16
|
-
* - `cookie`: Used to extract XSRF-TOKEN and authorization token from cookies
|
|
17
|
-
*
|
|
18
|
-
* The MicroserviceClientRequestAuth class will automatically:
|
|
19
|
-
* - Extract XSRF-TOKEN from cookies for CSRF protection
|
|
20
|
-
* - Extract authorization token from cookies (prioritized over header auth)
|
|
21
|
-
* - Fall back to Authorization header if no cookie-based auth is present
|
|
22
|
-
*
|
|
23
|
-
* @param request - The HTTP request containing headers
|
|
24
|
-
* @returns Headers object compatible with \@c8y/client's MicroserviceClientRequestAuth
|
|
25
|
-
*/
|
|
26
|
-
function convertRequestHeadersToC8yFormat(request) {
|
|
27
|
-
const headers = {};
|
|
28
|
-
const authorization = request.headers.get("authorization");
|
|
29
|
-
if (authorization) headers.authorization = authorization;
|
|
30
|
-
const cookie = request.headers.get("cookie");
|
|
31
|
-
if (cookie) headers.cookie = cookie;
|
|
32
|
-
return headers;
|
|
33
|
-
}
|
|
34
|
-
//#endregion
|
|
35
|
-
//#region src/utils/internal/tenant.ts
|
|
36
|
-
const USER_TENANT_CACHE_SALT = randomBytes(32).toString("hex");
|
|
37
|
-
function getCookieValue(cookieHeader, name) {
|
|
38
|
-
try {
|
|
39
|
-
const value = cookieHeader?.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`);
|
|
40
|
-
return value ? value.pop() : void 0;
|
|
41
|
-
} catch {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function getCurrentUserTenantCacheKeyMaterial(requestOrEvent) {
|
|
46
|
-
const request = "req" in requestOrEvent ? requestOrEvent.req : requestOrEvent;
|
|
47
|
-
const cookieAuth = getCookieValue(request.headers.get("cookie"), "authorization");
|
|
48
|
-
if (cookieAuth) return `cookie:${cookieAuth}`;
|
|
49
|
-
const authorization = request.headers.get("authorization");
|
|
50
|
-
if (authorization) return `header:${authorization}`;
|
|
51
|
-
}
|
|
52
|
-
function createCurrentUserTenantCacheKey(requestOrEvent) {
|
|
53
|
-
const material = getCurrentUserTenantCacheKeyMaterial(requestOrEvent);
|
|
54
|
-
if (!material) throw new Error("Cannot create current user tenant cache key without auth material");
|
|
55
|
-
return createHash("sha256").update(USER_TENANT_CACHE_SALT).update(":").update(material).digest("hex");
|
|
56
|
-
}
|
|
57
|
-
function tryGetTenantFromBasicAuth(requestOrEvent) {
|
|
58
|
-
const authorization = ("req" in requestOrEvent ? requestOrEvent.req : requestOrEvent).headers.get("authorization");
|
|
59
|
-
if (!authorization?.startsWith("Basic ")) return;
|
|
60
|
-
try {
|
|
61
|
-
const decoded = Buffer.from(authorization.slice(6), "base64").toString("utf8");
|
|
62
|
-
const separatorIndex = decoded.indexOf(":");
|
|
63
|
-
const userPart = separatorIndex === -1 ? decoded : decoded.slice(0, separatorIndex);
|
|
64
|
-
const slashIndex = userPart.indexOf("/");
|
|
65
|
-
if (slashIndex <= 0) return;
|
|
66
|
-
return userPart.slice(0, slashIndex);
|
|
67
|
-
} catch {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
defineCachedFunction(async (requestOrEvent) => {
|
|
72
|
-
const basicTenant = tryGetTenantFromBasicAuth(requestOrEvent);
|
|
73
|
-
if (basicTenant) return basicTenant;
|
|
74
|
-
return (await useUserClient(requestOrEvent).tenant.current()).data.name;
|
|
75
|
-
}, {
|
|
76
|
-
maxAge: 60,
|
|
77
|
-
name: "_c8y_nitro_get_current_user_tenant_id",
|
|
78
|
-
group: "c8y_nitro",
|
|
79
|
-
swr: false,
|
|
80
|
-
getKey: (requestOrEvent) => createCurrentUserTenantCacheKey(requestOrEvent),
|
|
81
|
-
shouldBypassCache: (requestOrEvent) => !getCurrentUserTenantCacheKeyMaterial(requestOrEvent)
|
|
82
|
-
});
|
|
83
|
-
//#endregion
|
|
84
|
-
//#region src/utils/credentials.ts
|
|
85
|
-
let prevCredentials = null;
|
|
86
|
-
function shouldEmitTenantCredentialsUpdated(prev, next) {
|
|
87
|
-
if (!prev) return true;
|
|
88
|
-
const prevTenantIds = Object.keys(prev);
|
|
89
|
-
const nextTenantIds = Object.keys(next);
|
|
90
|
-
if (prevTenantIds.length !== nextTenantIds.length) return true;
|
|
91
|
-
return new Set(nextTenantIds).symmetricDifference(new Set(prevTenantIds)).size > 0;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Fetches credentials for all tenants subscribed to this microservice.\
|
|
95
|
-
* Uses bootstrap credentials from runtime config to query the microservice subscriptions API.\
|
|
96
|
-
* Results are cached based on the configured TTL (default: 10 minutes).\
|
|
97
|
-
* @returns Object mapping tenant IDs to their respective credentials
|
|
98
|
-
* @config Cache TTL can be configured via:
|
|
99
|
-
* - `c8y.cache.credentialsTTL` in the Nitro config (value in seconds)
|
|
100
|
-
* - `NITRO_C8Y_CREDENTIALS_CACHE_TTL` environment variable
|
|
101
|
-
* @example
|
|
102
|
-
* // Get all subscribed tenant credentials:
|
|
103
|
-
* const credentials = await useSubscribedTenantCredentials()
|
|
104
|
-
* console.log(Object.keys(credentials)) // ['t12345', 't67890']
|
|
105
|
-
*
|
|
106
|
-
* // Access specific tenant:
|
|
107
|
-
* const tenant1Creds = credentials['t12345']
|
|
108
|
-
*
|
|
109
|
-
* // Invalidate cache:
|
|
110
|
-
* await useSubscribedTenantCredentials.invalidate()
|
|
111
|
-
*
|
|
112
|
-
* // Force refresh:
|
|
113
|
-
* const freshCreds = await useSubscribedTenantCredentials.refresh()
|
|
114
|
-
*/
|
|
115
|
-
const cachedSubscribedTenantCredentials = defineCachedFunction(async () => {
|
|
116
|
-
const newCredentials = (await Client.getMicroserviceSubscriptions({
|
|
117
|
-
tenant: process.env.C8Y_BOOTSTRAP_TENANT,
|
|
118
|
-
user: process.env.C8Y_BOOTSTRAP_USER,
|
|
119
|
-
password: process.env.C8Y_BOOTSTRAP_PASSWORD
|
|
120
|
-
}, process.env.C8Y_BASEURL)).reduce((acc, cred) => {
|
|
121
|
-
if (cred.tenant) acc[cred.tenant] = cred;
|
|
122
|
-
return acc;
|
|
123
|
-
}, {});
|
|
124
|
-
if (shouldEmitTenantCredentialsUpdated(prevCredentials, newCredentials)) useNitroHooks().callHook("c8y:tenantCredentialsUpdated", prevCredentials, newCredentials);
|
|
125
|
-
prevCredentials = newCredentials;
|
|
126
|
-
return newCredentials;
|
|
127
|
-
}, {
|
|
128
|
-
maxAge: useRuntimeConfig().c8yCredentialsCacheTTL ?? 600,
|
|
129
|
-
name: "_c8y_nitro_get_subscribed_tenant_credentials",
|
|
130
|
-
group: "c8y_nitro",
|
|
131
|
-
swr: false
|
|
132
|
-
});
|
|
133
|
-
const useSubscribedTenantCredentials = Object.assign(async () => {
|
|
134
|
-
const credentials = await cachedSubscribedTenantCredentials();
|
|
135
|
-
prevCredentials = credentials;
|
|
136
|
-
return credentials;
|
|
137
|
-
}, {
|
|
138
|
-
invalidate: cachedSubscribedTenantCredentials.invalidate,
|
|
139
|
-
refresh: async () => {
|
|
140
|
-
await cachedSubscribedTenantCredentials.invalidate();
|
|
141
|
-
const credentials = await cachedSubscribedTenantCredentials();
|
|
142
|
-
prevCredentials = credentials;
|
|
143
|
-
return credentials;
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
/**
|
|
147
|
-
* Fetches credentials for the tenant where this microservice is deployed.\
|
|
148
|
-
* Uses the C8Y_BOOTSTRAP_TENANT environment variable to identify the deployed tenant.\
|
|
149
|
-
* Returns credentials from the subscribed tenant credentials cache (cached based on configured TTL, default: 10 minutes).
|
|
150
|
-
* @returns Credentials for the deployed tenant
|
|
151
|
-
* @throws {EvlogError} If no credentials found for the deployed tenant
|
|
152
|
-
* @example
|
|
153
|
-
* // Get deployed tenant credentials:
|
|
154
|
-
* const creds = await useDeployedTenantCredentials()
|
|
155
|
-
* console.log(creds.tenant, creds.user)
|
|
156
|
-
*
|
|
157
|
-
* // Invalidate cache:
|
|
158
|
-
* await useDeployedTenantCredentials.invalidate()
|
|
159
|
-
*
|
|
160
|
-
* // Force refresh:
|
|
161
|
-
* const freshCreds = await useDeployedTenantCredentials.refresh()
|
|
162
|
-
* @note This function is not cached separately. It uses the cache of `useSubscribedTenantCredentials()`. Invalidating or refreshing one will refresh `useDeployedTenantCredentials()`s cache.
|
|
163
|
-
*/
|
|
164
|
-
const useDeployedTenantCredentials = Object.assign(async () => {
|
|
165
|
-
const tenant = process.env.C8Y_BOOTSTRAP_TENANT;
|
|
166
|
-
const allCredsPromise = await useSubscribedTenantCredentials();
|
|
167
|
-
if (!allCredsPromise[tenant]) throw createError({
|
|
168
|
-
message: `No credentials found for tenant deployed tenant '${tenant}'`,
|
|
169
|
-
status: 500
|
|
170
|
-
});
|
|
171
|
-
return allCredsPromise[tenant];
|
|
172
|
-
}, {
|
|
173
|
-
invalidate: useSubscribedTenantCredentials.invalidate,
|
|
174
|
-
refresh: async () => {
|
|
175
|
-
await useSubscribedTenantCredentials.refresh();
|
|
176
|
-
return await useDeployedTenantCredentials();
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
//#endregion
|
|
180
|
-
//#region src/utils/client.ts
|
|
181
|
-
/**
|
|
182
|
-
* Creates a Cumulocity client authenticated with the current user's credentials.\
|
|
183
|
-
* Extracts credentials from the Authorization header of the current request.
|
|
184
|
-
* @param requestOrEvent - The H3Event or ServerRequest from the current request
|
|
185
|
-
* @returns A configured Cumulocity Client instance
|
|
186
|
-
* @example
|
|
187
|
-
* // In a request handler:
|
|
188
|
-
* const client = useUserClient(event)
|
|
189
|
-
* const { data } = await client.inventory.list()
|
|
190
|
-
*/
|
|
191
|
-
function useUserClient(requestOrEvent) {
|
|
192
|
-
const request = "req" in requestOrEvent ? requestOrEvent.req : requestOrEvent;
|
|
193
|
-
if (request.context?.["c8y_user_client"]) return request.context["c8y_user_client"];
|
|
194
|
-
const client = new Client(new MicroserviceClientRequestAuth(convertRequestHeadersToC8yFormat(request)), process.env.C8Y_BASEURL);
|
|
195
|
-
request.context ??= {};
|
|
196
|
-
request.context["c8y_user_client"] = client;
|
|
197
|
-
return client;
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Creates a Cumulocity client for the tenant where this microservice is deployed.\
|
|
201
|
-
* Uses the bootstrap tenant ID from runtime config to identify the deployed tenant.\
|
|
202
|
-
* @returns A configured Cumulocity Client instance for the deployed tenant
|
|
203
|
-
* @example
|
|
204
|
-
* // Get client for the tenant hosting this microservice:
|
|
205
|
-
* const client = await useDeployedTenantClient()
|
|
206
|
-
* const { data } = await client.application.list()
|
|
207
|
-
*/
|
|
208
|
-
async function useDeployedTenantClient() {
|
|
209
|
-
const creds = await useSubscribedTenantCredentials();
|
|
210
|
-
const tenant = process.env.C8Y_BOOTSTRAP_TENANT;
|
|
211
|
-
if (!creds[tenant]) throw createError({
|
|
212
|
-
message: `No subscribed tenant credentials found for tenant '${tenant}'`,
|
|
213
|
-
status: 500
|
|
214
|
-
});
|
|
215
|
-
const client = new Client(new BasicAuth(creds[tenant]), process.env.C8Y_BASEURL);
|
|
216
|
-
client.core.tenant = tenant;
|
|
217
|
-
return client;
|
|
218
|
-
}
|
|
219
|
-
//#endregion
|
|
220
7
|
//#region src/utils/internal/tenantOptionFetchers.ts
|
|
221
8
|
/**
|
|
222
9
|
* Internal storage for cached option fetchers, keyed by
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { r as tryGetUserTenantDomainFast } from "../../client-D0Tdq7H1.mjs";
|
|
2
|
+
import { c8yManifest } from "c8y-nitro/runtime";
|
|
3
|
+
import { useRuntimeConfig } from "nitro/runtime-config";
|
|
4
|
+
import { getRequestURL } from "nitro/h3";
|
|
5
|
+
import { defineMiddleware } from "nitro";
|
|
6
|
+
//#region src/module/runtime/handlers/openapi.ts
|
|
7
|
+
function isOpenAPIDocument(value) {
|
|
8
|
+
return Boolean(value) && typeof value === "object" && "openapi" in value;
|
|
9
|
+
}
|
|
10
|
+
function joinURLSegments(origin, ...segments) {
|
|
11
|
+
const path = segments.map((segment) => segment?.replace(/^\/+|\/+$/g, "") ?? "").filter((segment) => segment.length > 0).join("/");
|
|
12
|
+
return path ? `${origin}/${path}` : origin;
|
|
13
|
+
}
|
|
14
|
+
async function resolveServer(event) {
|
|
15
|
+
const baseURL = useRuntimeConfig().app?.baseURL;
|
|
16
|
+
const contextPath = c8yManifest.contextPath ?? c8yManifest.name;
|
|
17
|
+
if (!import.meta.dev) {
|
|
18
|
+
const domain = await tryGetUserTenantDomainFast(event);
|
|
19
|
+
if (domain) return {
|
|
20
|
+
url: joinURLSegments(`https://${domain}`, "service", contextPath, baseURL),
|
|
21
|
+
description: "Cumulocity microservice endpoint"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const requestUrl = getRequestURL(event, {
|
|
25
|
+
xForwardedHost: true,
|
|
26
|
+
xForwardedProto: true
|
|
27
|
+
});
|
|
28
|
+
if (event.req.headers.has("x-forwarded-host")) return {
|
|
29
|
+
url: joinURLSegments(requestUrl.origin, "service", contextPath, baseURL),
|
|
30
|
+
description: "Cumulocity microservice endpoint"
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
url: joinURLSegments(requestUrl.origin, baseURL),
|
|
34
|
+
description: "Direct server endpoint"
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Transforms the OpenAPI document generated by Nitro for Cumulocity microservices:
|
|
39
|
+
* strips internal routes (probes, `/_c8y_nitro/**`, the spec/UI routes themselves)
|
|
40
|
+
* and rewrites the advertised server URL to the public tenant endpoint instead of
|
|
41
|
+
* the internal (e.g. cluster-local) host the service listens on.
|
|
42
|
+
*/
|
|
43
|
+
var openapi_default = defineMiddleware(async (event, next) => {
|
|
44
|
+
const runtimeConfig = useRuntimeConfig();
|
|
45
|
+
const specRoute = runtimeConfig.nitro?.openAPI?.route || "/_openapi.json";
|
|
46
|
+
const specPath = `${(runtimeConfig.app?.baseURL ?? "/").replace(/\/+$/, "")}${specRoute}`;
|
|
47
|
+
const rewrittenSpecPath = `/service/${c8yManifest.contextPath ?? c8yManifest.name}${specPath}`;
|
|
48
|
+
if (event.url.pathname !== specPath && event.url.pathname !== rewrittenSpecPath) return next();
|
|
49
|
+
const body = await next();
|
|
50
|
+
if (!isOpenAPIDocument(body)) return body;
|
|
51
|
+
const excludedPrefixes = runtimeConfig.c8yOpenApiExcludeRoutes ?? [];
|
|
52
|
+
if (body.paths && excludedPrefixes.length > 0) body.paths = Object.fromEntries(Object.entries(body.paths).filter(([path]) => !excludedPrefixes.some((prefix) => path.startsWith(prefix))));
|
|
53
|
+
body.servers = [await resolveServer(event)];
|
|
54
|
+
return body;
|
|
55
|
+
});
|
|
56
|
+
//#endregion
|
|
57
|
+
export { openapi_default as default };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { n as buildPublicRequestUrl, r as tryGetUserTenantDomainFast } from "../../client-D0Tdq7H1.mjs";
|
|
2
|
+
import { c8yManifest } from "c8y-nitro/runtime";
|
|
3
|
+
import { defineMiddleware } from "nitro";
|
|
4
|
+
//#region src/module/runtime/handlers/rewriteRequestUrl.ts
|
|
5
|
+
/**
|
|
6
|
+
* Rewrites the incoming request URL to the public tenant endpoint.
|
|
7
|
+
*
|
|
8
|
+
* When deployed, the platform proxy forwards requests with the internal
|
|
9
|
+
* cluster host (e.g. `service-scope-t123.<cluster>.svc.cluster.local`) and the
|
|
10
|
+
* `/service/<contextPath>` prefix stripped. Any handler that derives absolute
|
|
11
|
+
* URLs from the request (`event.url`, `getRequestURL(event)`) therefore leaks
|
|
12
|
+
* internal cluster URLs into responses. This middleware swaps `event.url` for
|
|
13
|
+
* the public one (`https://<tenant domain>/service/<contextPath>/...`) before
|
|
14
|
+
* route handlers run, so request-derived URLs come out publicly reachable.
|
|
15
|
+
*
|
|
16
|
+
* The tenant domain is resolved from the requesting user's credentials and
|
|
17
|
+
* memoized in-process, so the per-request overhead after the first resolution
|
|
18
|
+
* is a hash and a Map lookup. Unauthenticated requests (probes, preflights)
|
|
19
|
+
* are passed through untouched.
|
|
20
|
+
*
|
|
21
|
+
* Registered after the scanned runtime middlewares (see registerRuntime) so
|
|
22
|
+
* they still observe the original URL, and before the OpenAPI transform,
|
|
23
|
+
* which reuses the tenant domain resolved here from its cache.
|
|
24
|
+
*/
|
|
25
|
+
var rewriteRequestUrl_default = defineMiddleware(async (event, next) => {
|
|
26
|
+
if (import.meta.dev) return next();
|
|
27
|
+
const domain = await tryGetUserTenantDomainFast(event);
|
|
28
|
+
if (domain && event.url.host !== domain) {
|
|
29
|
+
event.url = buildPublicRequestUrl(domain, c8yManifest.contextPath ?? c8yManifest.name, event.url);
|
|
30
|
+
try {
|
|
31
|
+
event.req.headers.set("x-forwarded-host", domain);
|
|
32
|
+
event.req.headers.set("x-forwarded-proto", "https");
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
return next();
|
|
36
|
+
});
|
|
37
|
+
//#endregion
|
|
38
|
+
export { rewriteRequestUrl_default as default };
|
package/dist/types.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as C8YTenantOptionKey, c as C8yDockerOptions, d as C8YManifest, f as C8YManifestOptions, i as C8YSettingsCategory, l as C8yCacheOptions, n as C8yNitroModuleOptions, o as C8YTenantOptionKeysCacheConfig, p as C8YAPIClientOptions, r as C8yOpenAPIOptions, s as C8YRoles, t as C8yDevOptions, u as C8YZipOptions } from "./index-vu4xaFkz.mjs";
|
|
2
2
|
import { t as TenantCredentials } from "./credentials-eKnJ7m9Y.mjs";
|
|
3
|
-
export { C8YAPIClientOptions, type C8YManifest, type C8YManifestOptions, C8YRoles, C8YSettingsCategory, C8YTenantOptionKey, C8YTenantOptionKeysCacheConfig, type C8YZipOptions, type C8yCacheOptions, C8yDevOptions, C8yNitroModuleOptions, type TenantCredentials };
|
|
3
|
+
export { C8YAPIClientOptions, type C8YManifest, type C8YManifestOptions, C8YRoles, C8YSettingsCategory, C8YTenantOptionKey, C8YTenantOptionKeysCacheConfig, type C8YZipOptions, type C8yCacheOptions, C8yDevOptions, type C8yDockerOptions, C8yNitroModuleOptions, C8yOpenAPIOptions, type TenantCredentials };
|
package/dist/utils.d.mts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { t as TenantCredentials } from "./credentials-eKnJ7m9Y.mjs";
|
|
2
|
-
import { useLogger } from "evlog/nitro/v3";
|
|
3
2
|
import { Client, ICredentials, ICurrentUser } from "@c8y/client";
|
|
4
|
-
import { createError, createLogger } from "evlog";
|
|
5
|
-
import { EventHandler, H3Event } from "nitro/h3";
|
|
3
|
+
import { createError as createError$1, createLogger } from "evlog";
|
|
4
|
+
import { EventHandler, H3Event, HTTPEvent } from "nitro/h3";
|
|
6
5
|
import { ServerRequest, TaskContext, TaskPayload } from "nitro/types";
|
|
7
6
|
import { C8YRoles, C8YSettingsCategory, C8YTenantOptionKey } from "c8y-nitro/types";
|
|
8
7
|
//#region src/utils/client.d.ts
|
|
@@ -425,6 +424,84 @@ interface UseTenantOptions {
|
|
|
425
424
|
*/
|
|
426
425
|
declare const useTenantOptions: UseTenantOptions;
|
|
427
426
|
//#endregion
|
|
427
|
+
//#region src/utils/logging.d.ts
|
|
428
|
+
/**
|
|
429
|
+
* Returns the request-scoped wide-event logger for the current request.
|
|
430
|
+
*
|
|
431
|
+
* Accepts **either** an `H3Event` (as received by a route handler) **or** the
|
|
432
|
+
* `ServerRequest` returned by Nitro's `useRequest()` — both carry the same
|
|
433
|
+
* request context, so `useLogger(event)` and `useLogger(useRequest())` are
|
|
434
|
+
* equivalent.
|
|
435
|
+
*
|
|
436
|
+
* evlog's own `useLogger` only accepts an `H3Event` and reads the logger from
|
|
437
|
+
* `event.req.context.log`; `useRequest()` returns that `ServerRequest` directly
|
|
438
|
+
* (no `.req`), which would throw. This wrapper normalizes both shapes to the
|
|
439
|
+
* `{ req }` event evlog expects, restoring the accept-both behavior.
|
|
440
|
+
*
|
|
441
|
+
* @param eventOrRequest - The current `H3Event` or `ServerRequest` (`useRequest()`).
|
|
442
|
+
* @param service - Optional service name to override the default.
|
|
443
|
+
* @example
|
|
444
|
+
* // In a route handler:
|
|
445
|
+
* const log = useLogger(event)
|
|
446
|
+
*
|
|
447
|
+
* // Deeper in the call stack with experimental.asyncContext enabled:
|
|
448
|
+
* import { useRequest } from 'nitro/context'
|
|
449
|
+
* const log = useLogger(useRequest())
|
|
450
|
+
*/
|
|
451
|
+
declare function useLogger<T extends object = Record<string, unknown>>(eventOrRequest: HTTPEvent | ServerRequest, service?: string): import("evlog").AuditableLogger<T>;
|
|
452
|
+
/**
|
|
453
|
+
* Create a structured error for a Cumulocity microservice. Always prefer this
|
|
454
|
+
* over Nitro/h3's built-in `createError`: the returned error is captured in the
|
|
455
|
+
* wide log event **and** serialized into the JSON response under a `data` key.
|
|
456
|
+
*
|
|
457
|
+
* ## What the caller sees vs what stays on the server
|
|
458
|
+
*
|
|
459
|
+
* Every field **except `internal`** is sent to the client in the HTTP response.
|
|
460
|
+
* `internal` is stripped from the response and only ever appears in your logs.
|
|
461
|
+
*
|
|
462
|
+
* | Field | Sent to client? | Purpose |
|
|
463
|
+
* | ---------- | :-------------: | ------------------------------------------------------------- |
|
|
464
|
+
* | `message` | yes | Short, safe summary of what went wrong. |
|
|
465
|
+
* | `status` | yes | HTTP status code (default `500`). |
|
|
466
|
+
* | `code` | yes | Stable machine-readable id (e.g. `'PAYMENT_DECLINED'`). |
|
|
467
|
+
* | `why` | yes | Human-readable cause — safe to expose, no secrets. |
|
|
468
|
+
* | `fix` | yes | What the caller can do about it. |
|
|
469
|
+
* | `link` | yes | Docs URL with more detail. |
|
|
470
|
+
* | `cause` | no (logged) | The original `Error` you caught. |
|
|
471
|
+
* | `internal` | no (logged) | Backend-only diagnostics — never reaches the client. |
|
|
472
|
+
*
|
|
473
|
+
* ## Do NOT leak internal system errors
|
|
474
|
+
*
|
|
475
|
+
* Put anything sensitive or diagnostic (raw Cumulocity core responses, DB
|
|
476
|
+
* errors, tokens, tenant internals) in `internal`, **not** in `message`/`why`.
|
|
477
|
+
* `internal` is logged for debugging but never serialized to the response.
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* // User-facing error — every field here is visible to the caller:
|
|
481
|
+
* throw createError({
|
|
482
|
+
* code: 'PAYMENT_DECLINED',
|
|
483
|
+
* message: 'Payment failed',
|
|
484
|
+
* status: 402,
|
|
485
|
+
* why: 'Card declined by issuer',
|
|
486
|
+
* fix: 'Try a different payment method',
|
|
487
|
+
* link: 'https://docs.example.com/payments/declined',
|
|
488
|
+
* })
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* // Wrapping an upstream failure — keep the raw payload server-side only:
|
|
492
|
+
* catch (cause) {
|
|
493
|
+
* throw createError({
|
|
494
|
+
* status: 502,
|
|
495
|
+
* message: 'Upstream request failed', // safe, generic — what the caller sees
|
|
496
|
+
* cause: cause as Error,
|
|
497
|
+
* internal: { upstreamStatus: cause?.res?.status, upstreamBody: cause?.data },
|
|
498
|
+
* })
|
|
499
|
+
* }
|
|
500
|
+
*
|
|
501
|
+
* @see https://www.evlog.dev
|
|
502
|
+
*/
|
|
503
|
+
declare const createError: typeof createError$1;
|
|
504
|
+
//#endregion
|
|
428
505
|
//#region src/utils/schedule.d.ts
|
|
429
506
|
type ScheduledTaskPayload = TaskPayload;
|
|
430
507
|
type ScheduledTaskContext = TaskContext;
|
package/dist/utils.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { useLogger } from "evlog/nitro/v3";
|
|
4
|
+
import { useLogger as useLogger$1 } from "evlog/nitro/v3";
|
|
5
5
|
import { BasicAuth, Client, MicroserviceClientRequestAuth } from "@c8y/client";
|
|
6
6
|
import { defineCachedFunction } from "nitro/cache";
|
|
7
|
-
import { createError, createLogger } from "evlog";
|
|
7
|
+
import { createError as createError$1, createLogger } from "evlog";
|
|
8
8
|
import { useRuntimeConfig } from "nitro/runtime-config";
|
|
9
9
|
import { useNitroHooks } from "nitro/app";
|
|
10
10
|
import { defineHandler } from "nitro/h3";
|
|
@@ -83,6 +83,95 @@ const getCurrentUserTenantId = defineCachedFunction(async (requestOrEvent) => {
|
|
|
83
83
|
getKey: (requestOrEvent) => createCurrentUserTenantCacheKey(requestOrEvent),
|
|
84
84
|
shouldBypassCache: (requestOrEvent) => !getCurrentUserTenantCacheKeyMaterial(requestOrEvent)
|
|
85
85
|
});
|
|
86
|
+
defineCachedFunction(async (requestOrEvent, _tenantId) => {
|
|
87
|
+
return (await useUserClient(requestOrEvent).tenant.current()).data.domainName;
|
|
88
|
+
}, {
|
|
89
|
+
maxAge: 86400,
|
|
90
|
+
name: "_c8y_nitro_get_user_tenant_domain",
|
|
91
|
+
group: "c8y_nitro",
|
|
92
|
+
swr: false,
|
|
93
|
+
getKey: (_requestOrEvent, tenantId) => tenantId
|
|
94
|
+
});
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/utils/logging.ts
|
|
97
|
+
/**
|
|
98
|
+
* Returns the request-scoped wide-event logger for the current request.
|
|
99
|
+
*
|
|
100
|
+
* Accepts **either** an `H3Event` (as received by a route handler) **or** the
|
|
101
|
+
* `ServerRequest` returned by Nitro's `useRequest()` — both carry the same
|
|
102
|
+
* request context, so `useLogger(event)` and `useLogger(useRequest())` are
|
|
103
|
+
* equivalent.
|
|
104
|
+
*
|
|
105
|
+
* evlog's own `useLogger` only accepts an `H3Event` and reads the logger from
|
|
106
|
+
* `event.req.context.log`; `useRequest()` returns that `ServerRequest` directly
|
|
107
|
+
* (no `.req`), which would throw. This wrapper normalizes both shapes to the
|
|
108
|
+
* `{ req }` event evlog expects, restoring the accept-both behavior.
|
|
109
|
+
*
|
|
110
|
+
* @param eventOrRequest - The current `H3Event` or `ServerRequest` (`useRequest()`).
|
|
111
|
+
* @param service - Optional service name to override the default.
|
|
112
|
+
* @example
|
|
113
|
+
* // In a route handler:
|
|
114
|
+
* const log = useLogger(event)
|
|
115
|
+
*
|
|
116
|
+
* // Deeper in the call stack with experimental.asyncContext enabled:
|
|
117
|
+
* import { useRequest } from 'nitro/context'
|
|
118
|
+
* const log = useLogger(useRequest())
|
|
119
|
+
*/
|
|
120
|
+
function useLogger(eventOrRequest, service) {
|
|
121
|
+
return useLogger$1("req" in eventOrRequest ? eventOrRequest : { req: eventOrRequest }, service);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create a structured error for a Cumulocity microservice. Always prefer this
|
|
125
|
+
* over Nitro/h3's built-in `createError`: the returned error is captured in the
|
|
126
|
+
* wide log event **and** serialized into the JSON response under a `data` key.
|
|
127
|
+
*
|
|
128
|
+
* ## What the caller sees vs what stays on the server
|
|
129
|
+
*
|
|
130
|
+
* Every field **except `internal`** is sent to the client in the HTTP response.
|
|
131
|
+
* `internal` is stripped from the response and only ever appears in your logs.
|
|
132
|
+
*
|
|
133
|
+
* | Field | Sent to client? | Purpose |
|
|
134
|
+
* | ---------- | :-------------: | ------------------------------------------------------------- |
|
|
135
|
+
* | `message` | yes | Short, safe summary of what went wrong. |
|
|
136
|
+
* | `status` | yes | HTTP status code (default `500`). |
|
|
137
|
+
* | `code` | yes | Stable machine-readable id (e.g. `'PAYMENT_DECLINED'`). |
|
|
138
|
+
* | `why` | yes | Human-readable cause — safe to expose, no secrets. |
|
|
139
|
+
* | `fix` | yes | What the caller can do about it. |
|
|
140
|
+
* | `link` | yes | Docs URL with more detail. |
|
|
141
|
+
* | `cause` | no (logged) | The original `Error` you caught. |
|
|
142
|
+
* | `internal` | no (logged) | Backend-only diagnostics — never reaches the client. |
|
|
143
|
+
*
|
|
144
|
+
* ## Do NOT leak internal system errors
|
|
145
|
+
*
|
|
146
|
+
* Put anything sensitive or diagnostic (raw Cumulocity core responses, DB
|
|
147
|
+
* errors, tokens, tenant internals) in `internal`, **not** in `message`/`why`.
|
|
148
|
+
* `internal` is logged for debugging but never serialized to the response.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* // User-facing error — every field here is visible to the caller:
|
|
152
|
+
* throw createError({
|
|
153
|
+
* code: 'PAYMENT_DECLINED',
|
|
154
|
+
* message: 'Payment failed',
|
|
155
|
+
* status: 402,
|
|
156
|
+
* why: 'Card declined by issuer',
|
|
157
|
+
* fix: 'Try a different payment method',
|
|
158
|
+
* link: 'https://docs.example.com/payments/declined',
|
|
159
|
+
* })
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* // Wrapping an upstream failure — keep the raw payload server-side only:
|
|
163
|
+
* catch (cause) {
|
|
164
|
+
* throw createError({
|
|
165
|
+
* status: 502,
|
|
166
|
+
* message: 'Upstream request failed', // safe, generic — what the caller sees
|
|
167
|
+
* cause: cause as Error,
|
|
168
|
+
* internal: { upstreamStatus: cause?.res?.status, upstreamBody: cause?.data },
|
|
169
|
+
* })
|
|
170
|
+
* }
|
|
171
|
+
*
|
|
172
|
+
* @see https://www.evlog.dev
|
|
173
|
+
*/
|
|
174
|
+
const createError = createError$1;
|
|
86
175
|
//#endregion
|
|
87
176
|
//#region src/utils/credentials.ts
|
|
88
177
|
let prevCredentials = null;
|
package/package.json
CHANGED