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.
@@ -1,222 +1,9 @@
1
- import { t as createError } from "../../logging-DYnYICoX.mjs";
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 };
@@ -1,4 +1,4 @@
1
- import { t as createError } from "../../logging-DYnYICoX.mjs";
1
+ import { t as createError } from "../../logging-BUYT8Z2V.mjs";
2
2
  import process from "node:process";
3
3
  import { defineMiddleware } from "nitro";
4
4
  //#region src/module/runtime/middlewares/c8y-client.ts
package/dist/types.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { a as C8YTenantOptionKeysCacheConfig, c as C8YZipOptions, d as C8YAPIClientOptions, i as C8YTenantOptionKey, l as C8YManifest, n as C8yNitroModuleOptions, o as C8YRoles, r as C8YSettingsCategory, s as C8yCacheOptions, t as C8yDevOptions, u as C8YManifestOptions } from "./index-ByDSMEvU.mjs";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8y-nitro",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "type": "module",
5
5
  "description": "Lightning fast Cumulocity IoT microservice development powered by Nitro",
6
6
  "keywords": [
@@ -1,3 +0,0 @@
1
- import "evlog/nitro/v3";
2
- import { createError } from "evlog";
3
- export { createError as t };