c8y-nitro 0.7.1 → 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-DxmbyZju.mjs → client-D0Tdq7H1.mjs} +45 -1
- package/dist/index.mjs +20 -173
- package/dist/options-CmLpZ3Qf.mjs +179 -0
- package/dist/{package-D5xRshsx.mjs → package-BHOzdhHp.mjs} +1 -1
- package/dist/{roles-B58Fguop.mjs → roles-B-DNuScZ.mjs} +1 -1
- package/dist/runtime/handlers/invalidateTenantOptions.mjs +1 -1
- package/dist/runtime/handlers/openapi.mjs +7 -6
- package/dist/runtime/handlers/rewriteRequestUrl.mjs +38 -0
- package/package.json +1 -1
- package/dist/options-B7_yb_wr.mjs +0 -147
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { a as findMicroserviceByName, d as updateMicroservice, l as subscribeToApplication, n as createBasicAuthHeader, o as getBootstrapCredentials, p as createC8yManifest, r as createMicroservice } from "./c8y-api-DscUs-Kd.mjs";
|
|
2
|
+
import { t as writeBootstrapCredentials } from "./env-file-BrOTmVjA.mjs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
//#region src/module/manifestCache.ts
|
|
8
|
+
/**
|
|
9
|
+
* Directory (inside the project's `node_modules`) where c8y-nitro persists
|
|
10
|
+
* local dev state such as the last-synced manifest hash. Lives in
|
|
11
|
+
* `node_modules` so it is naturally gitignored and scoped per-checkout.
|
|
12
|
+
*/
|
|
13
|
+
const CACHE_DIR_SEGMENTS = ["node_modules", ".c8y-nitro"];
|
|
14
|
+
const CACHE_FILE_NAME = "bootstrap-state.json";
|
|
15
|
+
const CACHE_SCHEMA_VERSION = 1;
|
|
16
|
+
function getCacheDir(rootDir) {
|
|
17
|
+
return join(rootDir, ...CACHE_DIR_SEGMENTS);
|
|
18
|
+
}
|
|
19
|
+
function getCacheFile(rootDir) {
|
|
20
|
+
return join(getCacheDir(rootDir), CACHE_FILE_NAME);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compute a hash over the manifest and the deployment target it will be synced
|
|
24
|
+
* to. Folding the target in means switching tenant/baseURL invalidates the
|
|
25
|
+
* cached hash even if the manifest itself is unchanged.
|
|
26
|
+
*
|
|
27
|
+
* @param manifest - The generated Cumulocity manifest
|
|
28
|
+
* @param target - The deployment target identity
|
|
29
|
+
*/
|
|
30
|
+
function hashManifest(manifest, target) {
|
|
31
|
+
const payload = JSON.stringify({
|
|
32
|
+
manifest,
|
|
33
|
+
target
|
|
34
|
+
});
|
|
35
|
+
return createHash("sha256").update(payload).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Read the persisted bootstrap state, or `null` when absent, unreadable, or
|
|
39
|
+
* from an incompatible schema version.
|
|
40
|
+
* @param rootDir - Project root directory (containing `node_modules`)
|
|
41
|
+
*/
|
|
42
|
+
async function readManifestCache(rootDir) {
|
|
43
|
+
try {
|
|
44
|
+
const content = await readFile(getCacheFile(rootDir), "utf-8");
|
|
45
|
+
const state = JSON.parse(content);
|
|
46
|
+
if (state.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
|
|
47
|
+
return state;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Persist the bootstrap state for future dev restarts.
|
|
54
|
+
* @param rootDir - Project root directory (containing `node_modules`)
|
|
55
|
+
* @param state - The state to persist (schema version is filled in automatically)
|
|
56
|
+
*/
|
|
57
|
+
async function writeManifestCache(rootDir, state) {
|
|
58
|
+
await mkdir(getCacheDir(rootDir), { recursive: true });
|
|
59
|
+
const payload = {
|
|
60
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
61
|
+
...state
|
|
62
|
+
};
|
|
63
|
+
await writeFile(getCacheFile(rootDir), JSON.stringify(payload, null, 2), "utf-8");
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/module/autoBootstrap.ts
|
|
67
|
+
const BOOTSTRAP_ENV_VARS = [
|
|
68
|
+
"C8Y_BOOTSTRAP_TENANT",
|
|
69
|
+
"C8Y_BOOTSTRAP_USER",
|
|
70
|
+
"C8Y_BOOTSTRAP_PASSWORD"
|
|
71
|
+
];
|
|
72
|
+
const DEV_ENV_VARS = [
|
|
73
|
+
"C8Y_BASEURL",
|
|
74
|
+
"C8Y_DEVELOPMENT_TENANT",
|
|
75
|
+
"C8Y_DEVELOPMENT_USER",
|
|
76
|
+
"C8Y_DEVELOPMENT_PASSWORD"
|
|
77
|
+
];
|
|
78
|
+
/**
|
|
79
|
+
* Reads bootstrap credentials from an env object (e.g. a loaded `.env` file or
|
|
80
|
+
* `process.env`). Returns `undefined` unless all three variables are present.
|
|
81
|
+
* @param env - Environment variables object
|
|
82
|
+
*/
|
|
83
|
+
function readBootstrapCredentialsFromEnv(env) {
|
|
84
|
+
const tenant = env.C8Y_BOOTSTRAP_TENANT;
|
|
85
|
+
const name = env.C8Y_BOOTSTRAP_USER;
|
|
86
|
+
const password = env.C8Y_BOOTSTRAP_PASSWORD;
|
|
87
|
+
if (!tenant || !name || !password) return;
|
|
88
|
+
return {
|
|
89
|
+
tenant,
|
|
90
|
+
name,
|
|
91
|
+
password
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Verifies the microservice on the development tenant and keeps it in sync with
|
|
96
|
+
* the local manifest, bootstrapping (or re-bootstrapping) when needed.
|
|
97
|
+
*
|
|
98
|
+
* Unlike a purely env-var-based check, this always verifies against the tenant,
|
|
99
|
+
* so it also handles:
|
|
100
|
+
* - the manifest changing between runs (auto-updates a placeholder), and
|
|
101
|
+
* - the application being deleted on the tenant while stale bootstrap env vars
|
|
102
|
+
* still linger locally (re-creates it).
|
|
103
|
+
*
|
|
104
|
+
* A previously *deployed* microservice (one with an active version) is never
|
|
105
|
+
* overwritten automatically - we only warn about the drift.
|
|
106
|
+
*
|
|
107
|
+
* @param options - See {@link SyncBootstrappedMicroserviceOptions}
|
|
108
|
+
* @returns The bootstrap credentials for the microservice (freshly fetched when
|
|
109
|
+
* they were refreshed, otherwise the ones already present in `env`)
|
|
110
|
+
*/
|
|
111
|
+
async function syncBootstrappedMicroservice(options) {
|
|
112
|
+
const { rootDir, baseUrl, developmentTenant, authHeader, manifest, logger } = options;
|
|
113
|
+
const env = options.env ?? process.env;
|
|
114
|
+
const target = {
|
|
115
|
+
baseUrl,
|
|
116
|
+
developmentTenant,
|
|
117
|
+
name: manifest.name
|
|
118
|
+
};
|
|
119
|
+
const currentHash = hashManifest(manifest, target);
|
|
120
|
+
const existingApp = await findMicroserviceByName(baseUrl, manifest.name, authHeader);
|
|
121
|
+
if (!existingApp) {
|
|
122
|
+
const createdApp = await createMicroservice(baseUrl, manifest, authHeader);
|
|
123
|
+
logger.debug(`Microservice "${manifest.name}" created (ID: ${createdApp.id})`);
|
|
124
|
+
const credentials = await finalizeBootstrap({
|
|
125
|
+
rootDir,
|
|
126
|
+
logger,
|
|
127
|
+
baseUrl,
|
|
128
|
+
developmentTenant,
|
|
129
|
+
appId: createdApp.id,
|
|
130
|
+
authHeader,
|
|
131
|
+
target,
|
|
132
|
+
manifestHash: currentHash,
|
|
133
|
+
forceCredentials: true
|
|
134
|
+
});
|
|
135
|
+
logger.success(`Microservice "${manifest.name}" created on the development tenant.`);
|
|
136
|
+
return credentials;
|
|
137
|
+
}
|
|
138
|
+
const appId = existingApp.id;
|
|
139
|
+
const isRealService = Boolean(existingApp.activeVersionId);
|
|
140
|
+
const cache = await readManifestCache(rootDir);
|
|
141
|
+
if (!cache || cache.manifestHash !== currentHash || cache.appId !== appId) if (isRealService) logger.warn(`Local manifest for "${manifest.name}" differs from the deployed microservice on the development tenant. The running service was left untouched. Redeploy or run \`npx c8y-nitro bootstrap\` to update it explicitly.`);
|
|
142
|
+
else {
|
|
143
|
+
await updateMicroservice(baseUrl, appId, manifest, authHeader);
|
|
144
|
+
logger.success(`Updated microservice "${manifest.name}" with changed manifest.`);
|
|
145
|
+
}
|
|
146
|
+
const hasBootstrapCreds = BOOTSTRAP_ENV_VARS.every((v) => env[v]);
|
|
147
|
+
const appChanged = cache?.appId !== appId;
|
|
148
|
+
return await finalizeBootstrap({
|
|
149
|
+
rootDir,
|
|
150
|
+
logger,
|
|
151
|
+
baseUrl,
|
|
152
|
+
developmentTenant,
|
|
153
|
+
appId,
|
|
154
|
+
authHeader,
|
|
155
|
+
target,
|
|
156
|
+
manifestHash: currentHash,
|
|
157
|
+
forceCredentials: !hasBootstrapCreds || appChanged
|
|
158
|
+
}) ?? readBootstrapCredentialsFromEnv(env);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Verifies the microservice on the development tenant during dev startup.
|
|
162
|
+
* Runs silently unless a bootstrap/sync was performed or an error occurs.
|
|
163
|
+
* @param nitro - Nitro instance
|
|
164
|
+
*/
|
|
165
|
+
async function autoBootstrap(nitro) {
|
|
166
|
+
try {
|
|
167
|
+
if (DEV_ENV_VARS.filter((v) => !process.env[v]).length > 0) return;
|
|
168
|
+
const baseUrl = process.env.C8Y_BASEURL.endsWith("/") ? process.env.C8Y_BASEURL.slice(0, -1) : process.env.C8Y_BASEURL;
|
|
169
|
+
const authHeader = createBasicAuthHeader(process.env.C8Y_DEVELOPMENT_TENANT, process.env.C8Y_DEVELOPMENT_USER, process.env.C8Y_DEVELOPMENT_PASSWORD);
|
|
170
|
+
const manifest = await createC8yManifest(nitro.options.rootDir, nitro.options, nitro.logger);
|
|
171
|
+
await syncBootstrappedMicroservice({
|
|
172
|
+
rootDir: nitro.options.rootDir,
|
|
173
|
+
baseUrl,
|
|
174
|
+
developmentTenant: process.env.C8Y_DEVELOPMENT_TENANT,
|
|
175
|
+
authHeader,
|
|
176
|
+
manifest,
|
|
177
|
+
logger: nitro.logger
|
|
178
|
+
});
|
|
179
|
+
} catch (error) {
|
|
180
|
+
nitro.logger.warn("Auto-bootstrap failed:", error instanceof Error ? error.message : String(error));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Subscribe the tenant, optionally (re)write bootstrap credentials, and persist
|
|
185
|
+
* the synced state to the local cache.
|
|
186
|
+
* @param opts - Bootstrap finalization inputs
|
|
187
|
+
* @returns The freshly fetched credentials when `forceCredentials` was set
|
|
188
|
+
*/
|
|
189
|
+
async function finalizeBootstrap(opts) {
|
|
190
|
+
await subscribeToApplication(opts.baseUrl, opts.developmentTenant, opts.appId, opts.authHeader);
|
|
191
|
+
let credentials;
|
|
192
|
+
if (opts.forceCredentials) {
|
|
193
|
+
credentials = await getBootstrapCredentials(opts.baseUrl, opts.appId, opts.authHeader);
|
|
194
|
+
const envFileName = await writeBootstrapCredentials(opts.rootDir, {
|
|
195
|
+
C8Y_BOOTSTRAP_TENANT: credentials.tenant,
|
|
196
|
+
C8Y_BOOTSTRAP_USER: credentials.name,
|
|
197
|
+
C8Y_BOOTSTRAP_PASSWORD: credentials.password
|
|
198
|
+
});
|
|
199
|
+
process.env.C8Y_BOOTSTRAP_TENANT = credentials.tenant;
|
|
200
|
+
process.env.C8Y_BOOTSTRAP_USER = credentials.name;
|
|
201
|
+
process.env.C8Y_BOOTSTRAP_PASSWORD = credentials.password;
|
|
202
|
+
opts.logger.debug(`Bootstrap credentials written to ${envFileName}`);
|
|
203
|
+
}
|
|
204
|
+
await writeManifestCache(opts.rootDir, {
|
|
205
|
+
manifestHash: opts.manifestHash,
|
|
206
|
+
target: opts.target,
|
|
207
|
+
appId: opts.appId
|
|
208
|
+
});
|
|
209
|
+
return credentials;
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
export { readBootstrapCredentialsFromEnv as n, syncBootstrappedMicroservice as r, autoBootstrap as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as findMicroserviceByName, d as updateMicroservice, l as subscribeToApplication, n as createBasicAuthHeader, o as getBootstrapCredentials, p as createC8yManifest, r as createMicroservice } from "./c8y-api-
|
|
1
|
+
import { a as findMicroserviceByName, d as updateMicroservice, l as subscribeToApplication, n as createBasicAuthHeader, o as getBootstrapCredentials, p as createC8yManifest, r as createMicroservice } from "./c8y-api-DscUs-Kd.mjs";
|
|
2
2
|
import { t as writeBootstrapCredentials } from "./env-file-BrOTmVjA.mjs";
|
|
3
3
|
import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
|
|
4
4
|
import { defineCommand, runCommand } from "citty";
|
|
@@ -52,7 +52,7 @@ var bootstrap_default = defineCommand({
|
|
|
52
52
|
});
|
|
53
53
|
consola.success(`Bootstrap credentials written to ${envFileName}`);
|
|
54
54
|
if (manifest.roles && manifest.roles.length > 0) {
|
|
55
|
-
if (await consola.prompt("Do you want to manage microservice roles for your development user?", { type: "confirm" })) await runCommand(await import("./roles-
|
|
55
|
+
if (await consola.prompt("Do you want to manage microservice roles for your development user?", { type: "confirm" })) await runCommand(await import("./roles-B-DNuScZ.mjs").then((r) => r.default), { rawArgs: [] });
|
|
56
56
|
}
|
|
57
57
|
consola.success("Bootstrap complete!");
|
|
58
58
|
}
|
|
@@ -283,14 +283,20 @@ async function unassignUserRole(baseUrl, tenantId, userId, roleId, authHeader) {
|
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
/**
|
|
286
|
-
*
|
|
286
|
+
* Fetches the current application's settings (the tenant options of the
|
|
287
|
+
* microservice's own settings category) via
|
|
288
|
+
* `GET /application/currentApplication/settings`.
|
|
289
|
+
*
|
|
290
|
+
* Must be called with microservice credentials (bootstrap or service user).
|
|
291
|
+
* Unlike the Options API — which returns `credentials.*` values encrypted for
|
|
292
|
+
* regular users and rejects the bootstrap user with `403` — this endpoint
|
|
293
|
+
* returns encrypted `credentials.*` options **decrypted**.
|
|
287
294
|
* @param baseUrl - The Cumulocity base URL
|
|
288
|
-
* @param
|
|
289
|
-
* @
|
|
290
|
-
* @returns Record of key-value pairs for the category
|
|
295
|
+
* @param authHeader - Basic Auth header of the bootstrap (or service) user
|
|
296
|
+
* @returns Flat key-value map of the microservice's settings
|
|
291
297
|
*/
|
|
292
|
-
async function
|
|
293
|
-
const url = `${baseUrl}/
|
|
298
|
+
async function getCurrentApplicationSettings(baseUrl, authHeader) {
|
|
299
|
+
const url = `${baseUrl}/application/currentApplication/settings`;
|
|
294
300
|
const response = await fetch(url, {
|
|
295
301
|
method: "GET",
|
|
296
302
|
headers: {
|
|
@@ -298,23 +304,21 @@ async function getTenantOptionsByCategory(baseUrl, category, authHeader) {
|
|
|
298
304
|
Accept: "application/json"
|
|
299
305
|
}
|
|
300
306
|
});
|
|
301
|
-
if (response.status === 404) return {};
|
|
302
307
|
if (!response.ok) {
|
|
303
308
|
const errorText = await response.text();
|
|
304
|
-
throw new Error(`Failed to get
|
|
309
|
+
throw new Error(`Failed to get current application settings: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
305
310
|
}
|
|
306
311
|
return await response.json();
|
|
307
312
|
}
|
|
308
313
|
/**
|
|
309
|
-
* Gets a specific
|
|
314
|
+
* Gets all tenant options for a specific category.
|
|
310
315
|
* @param baseUrl - The Cumulocity base URL
|
|
311
|
-
* @param category - The category
|
|
312
|
-
* @param key - The option key
|
|
316
|
+
* @param category - The category to fetch options for
|
|
313
317
|
* @param authHeader - The Basic Auth header
|
|
314
|
-
* @returns
|
|
318
|
+
* @returns Record of key-value pairs for the category
|
|
315
319
|
*/
|
|
316
|
-
async function
|
|
317
|
-
const url = `${baseUrl}/tenant/options/${encodeURIComponent(category)}
|
|
320
|
+
async function getTenantOptionsByCategory(baseUrl, category, authHeader) {
|
|
321
|
+
const url = `${baseUrl}/tenant/options/${encodeURIComponent(category)}`;
|
|
318
322
|
const response = await fetch(url, {
|
|
319
323
|
method: "GET",
|
|
320
324
|
headers: {
|
|
@@ -322,12 +326,12 @@ async function getTenantOption(baseUrl, category, key, authHeader) {
|
|
|
322
326
|
Accept: "application/json"
|
|
323
327
|
}
|
|
324
328
|
});
|
|
325
|
-
if (response.status === 404) return;
|
|
329
|
+
if (response.status === 404) return {};
|
|
326
330
|
if (!response.ok) {
|
|
327
331
|
const errorText = await response.text();
|
|
328
|
-
throw new Error(`Failed to get tenant
|
|
332
|
+
throw new Error(`Failed to get tenant options for category ${category}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
329
333
|
}
|
|
330
|
-
return
|
|
334
|
+
return await response.json();
|
|
331
335
|
}
|
|
332
336
|
/**
|
|
333
337
|
* Creates a tenant option.
|
|
@@ -420,4 +424,4 @@ async function deleteTenantOption(baseUrl, category, key, authHeader) {
|
|
|
420
424
|
}
|
|
421
425
|
}
|
|
422
426
|
//#endregion
|
|
423
|
-
export { GENERATED_READINESS_ROUTE as _, findMicroserviceByName as a, getTenantOptionsByCategory as c, updateMicroservice as d, upsertTenantOption as f, GENERATED_LIVENESS_ROUTE as g, GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE as h, deleteTenantOption as i, subscribeToApplication as l, createC8yManifestFromNitro as m, createBasicAuthHeader as n, getBootstrapCredentials as o, createC8yManifest as p, createMicroservice as r,
|
|
427
|
+
export { GENERATED_READINESS_ROUTE as _, findMicroserviceByName as a, getTenantOptionsByCategory as c, updateMicroservice as d, upsertTenantOption as f, GENERATED_LIVENESS_ROUTE as g, GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE as h, deleteTenantOption as i, subscribeToApplication as l, createC8yManifestFromNitro as m, createBasicAuthHeader as n, getBootstrapCredentials as o, createC8yManifest as p, createMicroservice as r, getCurrentApplicationSettings as s, assignUserRole as t, unassignUserRole as u };
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as name, r as version, t as description } from "../package-
|
|
1
|
+
import { n as name, r as version, t as description } from "../package-BHOzdhHp.mjs";
|
|
2
2
|
import { defineCommand, runMain } from "citty";
|
|
3
3
|
//#region src/cli/index.ts
|
|
4
4
|
runMain(defineCommand({
|
|
@@ -8,9 +8,9 @@ runMain(defineCommand({
|
|
|
8
8
|
description
|
|
9
9
|
},
|
|
10
10
|
subCommands: {
|
|
11
|
-
bootstrap: () => import("../bootstrap-
|
|
12
|
-
roles: () => import("../roles-
|
|
13
|
-
options: () => import("../options-
|
|
11
|
+
bootstrap: () => import("../bootstrap-C1Qly0J_.mjs").then((r) => r.default),
|
|
12
|
+
roles: () => import("../roles-B-DNuScZ.mjs").then((r) => r.default),
|
|
13
|
+
options: () => import("../options-CmLpZ3Qf.mjs").then((r) => r.default)
|
|
14
14
|
}
|
|
15
15
|
}));
|
|
16
16
|
//#endregion
|
|
@@ -96,6 +96,50 @@ async function getUserTenantDomain(requestOrEvent) {
|
|
|
96
96
|
const tenantId = await getCurrentUserTenantId(requestOrEvent);
|
|
97
97
|
return cachedTenantDomain(requestOrEvent, tenantId);
|
|
98
98
|
}
|
|
99
|
+
const FAST_DOMAIN_SUCCESS_TTL = 6e4;
|
|
100
|
+
const FAST_DOMAIN_FAILURE_TTL = 1e4;
|
|
101
|
+
const FAST_DOMAIN_MAX_ENTRIES = 1e3;
|
|
102
|
+
const fastDomainCache = /* @__PURE__ */ new Map();
|
|
103
|
+
/**
|
|
104
|
+
* Best-effort variant of {@link getUserTenantDomain} for per-request hot paths:
|
|
105
|
+
* never throws (returns `undefined` for unauthenticated requests or failed
|
|
106
|
+
* lookups) and memoizes results in-process per auth material. Failures are
|
|
107
|
+
* cached briefly so requests with unresolvable credentials don't trigger a
|
|
108
|
+
* platform call each time.
|
|
109
|
+
* @param requestOrEvent - The H3Event or ServerRequest from the current request
|
|
110
|
+
* @param resolveDomain - Domain resolver, only overridden in tests
|
|
111
|
+
*/
|
|
112
|
+
async function tryGetUserTenantDomainFast(requestOrEvent, resolveDomain = getUserTenantDomain) {
|
|
113
|
+
if (!getCurrentUserTenantCacheKeyMaterial(requestOrEvent)) return;
|
|
114
|
+
const key = createCurrentUserTenantCacheKey(requestOrEvent);
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
const cached = fastDomainCache.get(key);
|
|
117
|
+
if (cached && cached.expiresAt > now) return cached.domain;
|
|
118
|
+
let domain;
|
|
119
|
+
try {
|
|
120
|
+
domain = await resolveDomain(requestOrEvent);
|
|
121
|
+
} catch {
|
|
122
|
+
domain = void 0;
|
|
123
|
+
}
|
|
124
|
+
if (!fastDomainCache.has(key) && fastDomainCache.size >= FAST_DOMAIN_MAX_ENTRIES) fastDomainCache.delete(fastDomainCache.keys().next().value);
|
|
125
|
+
fastDomainCache.set(key, {
|
|
126
|
+
domain,
|
|
127
|
+
expiresAt: now + (domain ? FAST_DOMAIN_SUCCESS_TTL : FAST_DOMAIN_FAILURE_TTL)
|
|
128
|
+
});
|
|
129
|
+
return domain;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Builds the public URL a request is reachable at through the platform proxy:
|
|
133
|
+
* `https://<tenant domain>/service/<contextPath><pathname><search>`.
|
|
134
|
+
* The proxy strips the `/service/<contextPath>` prefix before the request
|
|
135
|
+
* reaches the microservice, so it is re-added here.
|
|
136
|
+
* @param tenantDomain - Public domain of the tenant (e.g. `tenant.cumulocity.com`)
|
|
137
|
+
* @param contextPath - Context path of the microservice
|
|
138
|
+
* @param url - URL the request was received with (only pathname and search are used)
|
|
139
|
+
*/
|
|
140
|
+
function buildPublicRequestUrl(tenantDomain, contextPath, url) {
|
|
141
|
+
return new URL(`https://${tenantDomain}/service/${contextPath}${url.pathname}${url.search}`);
|
|
142
|
+
}
|
|
99
143
|
//#endregion
|
|
100
144
|
//#region src/utils/credentials.ts
|
|
101
145
|
let prevCredentials = null;
|
|
@@ -233,4 +277,4 @@ async function useDeployedTenantClient() {
|
|
|
233
277
|
return client;
|
|
234
278
|
}
|
|
235
279
|
//#endregion
|
|
236
|
-
export {
|
|
280
|
+
export { buildPublicRequestUrl as n, tryGetUserTenantDomainFast as r, useDeployedTenantClient as t };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { _ as GENERATED_READINESS_ROUTE,
|
|
2
|
-
import { t as
|
|
3
|
-
import { n as name } from "./package-
|
|
1
|
+
import { _ as GENERATED_READINESS_ROUTE, g as GENERATED_LIVENESS_ROUTE, h as GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE, m as createC8yManifestFromNitro, p as createC8yManifest } from "./c8y-api-DscUs-Kd.mjs";
|
|
2
|
+
import { t as autoBootstrap } from "./autoBootstrap-Drk7ltXK.mjs";
|
|
3
|
+
import { n as name } from "./package-BHOzdhHp.mjs";
|
|
4
4
|
import { basename, dirname, join, relative } from "node:path";
|
|
5
|
-
import { mkdir,
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
6
|
import { x } from "tinyexec";
|
|
7
7
|
import { join as join$1 } from "pathe";
|
|
8
8
|
import { mkdir as mkdir$1, readFile as readFile$1, stat, writeFile as writeFile$1 } from "fs/promises";
|
|
@@ -11,8 +11,6 @@ import Spinnies from "spinnies";
|
|
|
11
11
|
import { colors } from "consola/utils";
|
|
12
12
|
import { mkdirSync, writeFileSync } from "fs";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { createHash } from "node:crypto";
|
|
15
|
-
import process from "node:process";
|
|
16
14
|
import evlog from "evlog/nitro/v3";
|
|
17
15
|
//#region src/module/apiClient.ts
|
|
18
16
|
/**
|
|
@@ -465,6 +463,22 @@ function registerRuntime(nitro, options = {}) {
|
|
|
465
463
|
handler,
|
|
466
464
|
middleware: true
|
|
467
465
|
})));
|
|
466
|
+
/**
|
|
467
|
+
* Request URL rewrite middleware (always on in production builds).
|
|
468
|
+
* Rewrites `event.url` to the public tenant endpoint so handlers that build
|
|
469
|
+
* absolute URLs from the request don't leak internal cluster URLs.
|
|
470
|
+
* Registered after the scanned middlewares (which observe the original
|
|
471
|
+
* cluster-local URL) and before the OpenAPI transform, which reuses the
|
|
472
|
+
* resolved tenant domain from its cache.
|
|
473
|
+
*/
|
|
474
|
+
if (!isNitroDev) {
|
|
475
|
+
nitro.options.handlers.push({
|
|
476
|
+
route: "/**",
|
|
477
|
+
handler: join$1(thisFilePath, "./runtime/handlers/rewriteRequestUrl"),
|
|
478
|
+
middleware: true
|
|
479
|
+
});
|
|
480
|
+
nitro.logger.debug("Registered public request URL rewrite middleware");
|
|
481
|
+
}
|
|
468
482
|
if (Boolean(nitro.options.experimental?.openAPI && (isNitroDev || nitro.options.openAPI?.production))) if (!isNitroDev && nitro.options.openAPI?.production === "prerender") nitro.logger.warn("c8y-nitro cannot transform a prerendered OpenAPI document (internal routes stay listed and the server URL is baked in at build time). Use `openAPI: { production: true }` to serve the document at runtime instead.");
|
|
469
483
|
else {
|
|
470
484
|
nitro.options.handlers.push({
|
|
@@ -535,173 +549,6 @@ function checkProbeEndpoint(nitro, probe, probeType) {
|
|
|
535
549
|
}
|
|
536
550
|
}
|
|
537
551
|
//#endregion
|
|
538
|
-
//#region src/module/manifestCache.ts
|
|
539
|
-
/**
|
|
540
|
-
* Directory (inside the project's `node_modules`) where c8y-nitro persists
|
|
541
|
-
* local dev state such as the last-synced manifest hash. Lives in
|
|
542
|
-
* `node_modules` so it is naturally gitignored and scoped per-checkout.
|
|
543
|
-
*/
|
|
544
|
-
const CACHE_DIR_SEGMENTS = ["node_modules", ".c8y-nitro"];
|
|
545
|
-
const CACHE_FILE_NAME = "bootstrap-state.json";
|
|
546
|
-
const CACHE_SCHEMA_VERSION = 1;
|
|
547
|
-
function getCacheDir(rootDir) {
|
|
548
|
-
return join(rootDir, ...CACHE_DIR_SEGMENTS);
|
|
549
|
-
}
|
|
550
|
-
function getCacheFile(rootDir) {
|
|
551
|
-
return join(getCacheDir(rootDir), CACHE_FILE_NAME);
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Compute a hash over the manifest and the deployment target it will be synced
|
|
555
|
-
* to. Folding the target in means switching tenant/baseURL invalidates the
|
|
556
|
-
* cached hash even if the manifest itself is unchanged.
|
|
557
|
-
*
|
|
558
|
-
* @param manifest - The generated Cumulocity manifest
|
|
559
|
-
* @param target - The deployment target identity
|
|
560
|
-
*/
|
|
561
|
-
function hashManifest(manifest, target) {
|
|
562
|
-
const payload = JSON.stringify({
|
|
563
|
-
manifest,
|
|
564
|
-
target
|
|
565
|
-
});
|
|
566
|
-
return createHash("sha256").update(payload).digest("hex");
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* Read the persisted bootstrap state, or `null` when absent, unreadable, or
|
|
570
|
-
* from an incompatible schema version.
|
|
571
|
-
* @param rootDir - Project root directory (containing `node_modules`)
|
|
572
|
-
*/
|
|
573
|
-
async function readManifestCache(rootDir) {
|
|
574
|
-
try {
|
|
575
|
-
const content = await readFile(getCacheFile(rootDir), "utf-8");
|
|
576
|
-
const state = JSON.parse(content);
|
|
577
|
-
if (state.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
|
|
578
|
-
return state;
|
|
579
|
-
} catch {
|
|
580
|
-
return null;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Persist the bootstrap state for future dev restarts.
|
|
585
|
-
* @param rootDir - Project root directory (containing `node_modules`)
|
|
586
|
-
* @param state - The state to persist (schema version is filled in automatically)
|
|
587
|
-
*/
|
|
588
|
-
async function writeManifestCache(rootDir, state) {
|
|
589
|
-
await mkdir(getCacheDir(rootDir), { recursive: true });
|
|
590
|
-
const payload = {
|
|
591
|
-
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
592
|
-
...state
|
|
593
|
-
};
|
|
594
|
-
await writeFile(getCacheFile(rootDir), JSON.stringify(payload, null, 2), "utf-8");
|
|
595
|
-
}
|
|
596
|
-
//#endregion
|
|
597
|
-
//#region src/module/autoBootstrap.ts
|
|
598
|
-
const BOOTSTRAP_ENV_VARS = [
|
|
599
|
-
"C8Y_BOOTSTRAP_TENANT",
|
|
600
|
-
"C8Y_BOOTSTRAP_USER",
|
|
601
|
-
"C8Y_BOOTSTRAP_PASSWORD"
|
|
602
|
-
];
|
|
603
|
-
const DEV_ENV_VARS = [
|
|
604
|
-
"C8Y_BASEURL",
|
|
605
|
-
"C8Y_DEVELOPMENT_TENANT",
|
|
606
|
-
"C8Y_DEVELOPMENT_USER",
|
|
607
|
-
"C8Y_DEVELOPMENT_PASSWORD"
|
|
608
|
-
];
|
|
609
|
-
/**
|
|
610
|
-
* Verifies the microservice on the development tenant and keeps it in sync with
|
|
611
|
-
* the local manifest, bootstrapping (or re-bootstrapping) when needed.
|
|
612
|
-
*
|
|
613
|
-
* Unlike a purely env-var-based check, this always verifies against the tenant
|
|
614
|
-
* when development credentials are available, so it also handles:
|
|
615
|
-
* - the manifest changing between dev restarts (auto-updates a placeholder), and
|
|
616
|
-
* - the application being deleted on the tenant while stale bootstrap env vars
|
|
617
|
-
* still linger locally (re-creates it).
|
|
618
|
-
*
|
|
619
|
-
* A previously *deployed* microservice (one with an active version) is never
|
|
620
|
-
* overwritten automatically - we only warn about the drift.
|
|
621
|
-
*
|
|
622
|
-
* Runs silently unless a bootstrap/sync was performed or an error occurs.
|
|
623
|
-
* @param nitro - Nitro instance
|
|
624
|
-
*/
|
|
625
|
-
async function autoBootstrap(nitro) {
|
|
626
|
-
try {
|
|
627
|
-
const rootDir = nitro.options.rootDir;
|
|
628
|
-
if (DEV_ENV_VARS.filter((v) => !process.env[v]).length > 0) return;
|
|
629
|
-
const baseUrl = process.env.C8Y_BASEURL.endsWith("/") ? process.env.C8Y_BASEURL.slice(0, -1) : process.env.C8Y_BASEURL;
|
|
630
|
-
const developmentTenant = process.env.C8Y_DEVELOPMENT_TENANT;
|
|
631
|
-
const authHeader = createBasicAuthHeader(developmentTenant, process.env.C8Y_DEVELOPMENT_USER, process.env.C8Y_DEVELOPMENT_PASSWORD);
|
|
632
|
-
const manifest = await createC8yManifest(rootDir, nitro.options, nitro.logger);
|
|
633
|
-
const target = {
|
|
634
|
-
baseUrl,
|
|
635
|
-
developmentTenant,
|
|
636
|
-
name: manifest.name
|
|
637
|
-
};
|
|
638
|
-
const currentHash = hashManifest(manifest, target);
|
|
639
|
-
const existingApp = await findMicroserviceByName(baseUrl, manifest.name, authHeader);
|
|
640
|
-
if (!existingApp) {
|
|
641
|
-
const createdApp = await createMicroservice(baseUrl, manifest, authHeader);
|
|
642
|
-
nitro.logger.debug(`Microservice "${manifest.name}" created (ID: ${createdApp.id})`);
|
|
643
|
-
await finalizeBootstrap(nitro, {
|
|
644
|
-
baseUrl,
|
|
645
|
-
developmentTenant,
|
|
646
|
-
appId: createdApp.id,
|
|
647
|
-
authHeader,
|
|
648
|
-
target,
|
|
649
|
-
manifestHash: currentHash,
|
|
650
|
-
forceCredentials: true
|
|
651
|
-
});
|
|
652
|
-
nitro.logger.success(`Auto-bootstrap complete! Microservice "${manifest.name}" created on tenant.`);
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
const appId = existingApp.id;
|
|
656
|
-
const isRealService = Boolean(existingApp.activeVersionId);
|
|
657
|
-
const cache = await readManifestCache(rootDir);
|
|
658
|
-
if (!cache || cache.manifestHash !== currentHash || cache.appId !== appId) if (isRealService) nitro.logger.warn(`Local manifest for "${manifest.name}" differs from the deployed microservice on the development tenant. The running service was left untouched. Redeploy or run \`npx c8y-nitro bootstrap\` to update it explicitly.`);
|
|
659
|
-
else {
|
|
660
|
-
await updateMicroservice(baseUrl, appId, manifest, authHeader);
|
|
661
|
-
nitro.logger.success(`Auto-bootstrap: updated microservice "${manifest.name}" with changed manifest.`);
|
|
662
|
-
}
|
|
663
|
-
const hasBootstrapCreds = BOOTSTRAP_ENV_VARS.every((v) => process.env[v]);
|
|
664
|
-
const appChanged = cache?.appId !== appId;
|
|
665
|
-
await finalizeBootstrap(nitro, {
|
|
666
|
-
baseUrl,
|
|
667
|
-
developmentTenant,
|
|
668
|
-
appId,
|
|
669
|
-
authHeader,
|
|
670
|
-
target,
|
|
671
|
-
manifestHash: currentHash,
|
|
672
|
-
forceCredentials: !hasBootstrapCreds || appChanged
|
|
673
|
-
});
|
|
674
|
-
} catch (error) {
|
|
675
|
-
nitro.logger.warn("Auto-bootstrap failed:", error instanceof Error ? error.message : String(error));
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
/**
|
|
679
|
-
* Subscribe the tenant, optionally (re)write bootstrap credentials, and persist
|
|
680
|
-
* the synced state to the local cache.
|
|
681
|
-
* @param nitro
|
|
682
|
-
* @param opts
|
|
683
|
-
*/
|
|
684
|
-
async function finalizeBootstrap(nitro, opts) {
|
|
685
|
-
await subscribeToApplication(opts.baseUrl, opts.developmentTenant, opts.appId, opts.authHeader);
|
|
686
|
-
if (opts.forceCredentials) {
|
|
687
|
-
const credentials = await getBootstrapCredentials(opts.baseUrl, opts.appId, opts.authHeader);
|
|
688
|
-
const envFileName = await writeBootstrapCredentials(nitro.options.rootDir, {
|
|
689
|
-
C8Y_BOOTSTRAP_TENANT: credentials.tenant,
|
|
690
|
-
C8Y_BOOTSTRAP_USER: credentials.name,
|
|
691
|
-
C8Y_BOOTSTRAP_PASSWORD: credentials.password
|
|
692
|
-
});
|
|
693
|
-
process.env.C8Y_BOOTSTRAP_TENANT = credentials.tenant;
|
|
694
|
-
process.env.C8Y_BOOTSTRAP_USER = credentials.name;
|
|
695
|
-
process.env.C8Y_BOOTSTRAP_PASSWORD = credentials.password;
|
|
696
|
-
nitro.logger.debug(`Bootstrap credentials written to ${envFileName}`);
|
|
697
|
-
}
|
|
698
|
-
await writeManifestCache(nitro.options.rootDir, {
|
|
699
|
-
manifestHash: opts.manifestHash,
|
|
700
|
-
target: opts.target,
|
|
701
|
-
appId: opts.appId
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
//#endregion
|
|
705
552
|
//#region src/index.ts
|
|
706
553
|
function c8y() {
|
|
707
554
|
return {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { c as getTenantOptionsByCategory, f as upsertTenantOption, i as deleteTenantOption, n as createBasicAuthHeader, p as createC8yManifest, s as getCurrentApplicationSettings } from "./c8y-api-DscUs-Kd.mjs";
|
|
2
|
+
import { n as readBootstrapCredentialsFromEnv, r as syncBootstrappedMicroservice } from "./autoBootstrap-Drk7ltXK.mjs";
|
|
3
|
+
import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
import { consola } from "consola";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
//#region src/cli/commands/options.ts
|
|
9
|
+
var options_default = defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: "options",
|
|
12
|
+
description: "Manage tenant options on the development tenant"
|
|
13
|
+
},
|
|
14
|
+
args: {},
|
|
15
|
+
async run() {
|
|
16
|
+
consola.info("Loading configuration...");
|
|
17
|
+
const { env, nitroConfig, configDir } = await loadC8yConfig();
|
|
18
|
+
consola.info("Validating environment variables...");
|
|
19
|
+
const envVars = validateBootstrapEnv(env);
|
|
20
|
+
consola.info("Loading manifest...");
|
|
21
|
+
const manifest = await createC8yManifest(configDir, nitroConfig);
|
|
22
|
+
const category = manifest.settingsCategory || manifest.contextPath || manifest.name;
|
|
23
|
+
if (!manifest.settings || manifest.settings.length === 0) throw new Error("No settings defined in manifest. Add settings to your c8y.manifest configuration.");
|
|
24
|
+
consola.success(`Using category: ${category}`);
|
|
25
|
+
const devAuthHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
|
|
26
|
+
let bootstrapCredentials;
|
|
27
|
+
try {
|
|
28
|
+
consola.info("Checking microservice on development tenant...");
|
|
29
|
+
bootstrapCredentials = await syncBootstrappedMicroservice({
|
|
30
|
+
rootDir: configDir,
|
|
31
|
+
baseUrl: envVars.C8Y_BASEURL,
|
|
32
|
+
developmentTenant: envVars.C8Y_DEVELOPMENT_TENANT,
|
|
33
|
+
authHeader: devAuthHeader,
|
|
34
|
+
manifest,
|
|
35
|
+
logger: consola,
|
|
36
|
+
env
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
consola.warn(`Could not sync microservice with the development tenant: ${error instanceof Error ? error.message : String(error)}`);
|
|
40
|
+
bootstrapCredentials = readBootstrapCredentialsFromEnv(env);
|
|
41
|
+
}
|
|
42
|
+
const bootstrapAuthHeader = bootstrapCredentials ? createBasicAuthHeader(bootstrapCredentials.tenant, bootstrapCredentials.name, bootstrapCredentials.password) : void 0;
|
|
43
|
+
if (!bootstrapAuthHeader) consola.warn("No bootstrap credentials available - reading with the development user. Encrypted credentials.* options cannot be decrypted.");
|
|
44
|
+
consola.info("Fetching current tenant options...");
|
|
45
|
+
const currentOptions = await getTenantOptionsByCategory(envVars.C8Y_BASEURL, category, devAuthHeader);
|
|
46
|
+
if (bootstrapAuthHeader) try {
|
|
47
|
+
const decryptedSettings = await getCurrentApplicationSettings(envVars.C8Y_BASEURL, bootstrapAuthHeader);
|
|
48
|
+
for (const [key, value] of Object.entries(decryptedSettings)) if (currentOptions[key] !== void 0 || key.startsWith("credentials.")) currentOptions[key] = value;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
consola.warn(`Could not read decrypted settings with the bootstrap user: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
}
|
|
52
|
+
const availableKeys = manifest.settings.map((s) => s.key);
|
|
53
|
+
const setManifestKeys = availableKeys.filter((k) => currentOptions[k] !== void 0);
|
|
54
|
+
consola.success(`Found ${Object.keys(currentOptions).length} options set on tenant (${setManifestKeys.length} of ${availableKeys.length} manifest settings)`);
|
|
55
|
+
const ctx = {
|
|
56
|
+
baseUrl: envVars.C8Y_BASEURL,
|
|
57
|
+
category,
|
|
58
|
+
devAuthHeader,
|
|
59
|
+
bootstrapAuthHeader,
|
|
60
|
+
availableKeys,
|
|
61
|
+
currentOptions
|
|
62
|
+
};
|
|
63
|
+
const action = await consola.prompt("What do you want to do?", {
|
|
64
|
+
type: "select",
|
|
65
|
+
options: [
|
|
66
|
+
{
|
|
67
|
+
label: "Read option value",
|
|
68
|
+
value: "read"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: "Update/Create option",
|
|
72
|
+
value: "update"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "Delete option(s)",
|
|
76
|
+
value: "delete"
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
});
|
|
80
|
+
if (action === "read") await handleRead(ctx);
|
|
81
|
+
else if (action === "update") await handleUpdate(ctx);
|
|
82
|
+
else if (action === "delete") await handleDelete(ctx);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Handle reading options: prints all options at once (decrypted where
|
|
87
|
+
* possible), then - after the user confirms with Enter - rewrites the output
|
|
88
|
+
* in place with `credentials.*` values masked, so secrets don't linger in the
|
|
89
|
+
* terminal scrollback.
|
|
90
|
+
* @param ctx - The shared options context
|
|
91
|
+
*/
|
|
92
|
+
async function handleRead(ctx) {
|
|
93
|
+
const { bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
|
|
94
|
+
if (!bootstrapAuthHeader && availableKeys.some((k) => k.startsWith("credentials."))) consola.warn("Encrypted credentials.* options cannot be read in decrypted form without bootstrap credentials. Run `npx c8y-nitro bootstrap` first.");
|
|
95
|
+
const keys = [.../* @__PURE__ */ new Set([...availableKeys, ...Object.keys(currentOptions)])];
|
|
96
|
+
const padWidth = Math.max(...keys.map((k) => k.length));
|
|
97
|
+
const renderLine = (key, maskCredentials) => {
|
|
98
|
+
const value = currentOptions[key];
|
|
99
|
+
const shownValue = value === void 0 ? "(not set)" : maskCredentials && key.startsWith("credentials.") ? "****" : value;
|
|
100
|
+
const suffix = availableKeys.includes(key) ? "" : " (not in manifest)";
|
|
101
|
+
return ` ${key.padEnd(padWidth)} = ${shownValue}${suffix}`;
|
|
102
|
+
};
|
|
103
|
+
const revealedLines = keys.map((key) => renderLine(key, false));
|
|
104
|
+
process.stdout.write(`${revealedLines.join("\n")}\n`);
|
|
105
|
+
const rl = createInterface({
|
|
106
|
+
input: process.stdin,
|
|
107
|
+
output: process.stdout
|
|
108
|
+
});
|
|
109
|
+
await rl.question("Press <Enter> to hide credential values...");
|
|
110
|
+
rl.close();
|
|
111
|
+
if (process.stdout.isTTY) {
|
|
112
|
+
const columns = process.stdout.columns || 80;
|
|
113
|
+
const rowsFor = (line) => Math.max(1, Math.ceil(line.length / columns));
|
|
114
|
+
const rowsToErase = revealedLines.reduce((rows, line) => rows + rowsFor(line), 1);
|
|
115
|
+
process.stdout.write(`\x1B[${rowsToErase}A\x1B[0J`);
|
|
116
|
+
process.stdout.write(`${keys.map((key) => renderLine(key, true)).join("\n")}\n`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Handle updating options (with loop for multiple updates)
|
|
121
|
+
* @param ctx - The shared options context
|
|
122
|
+
*/
|
|
123
|
+
async function handleUpdate(ctx) {
|
|
124
|
+
const { baseUrl, category, devAuthHeader, bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
|
|
125
|
+
let continueUpdating = true;
|
|
126
|
+
while (continueUpdating) {
|
|
127
|
+
const key = await consola.prompt("Select option to update:", {
|
|
128
|
+
type: "select",
|
|
129
|
+
options: availableKeys.map((k) => ({
|
|
130
|
+
label: currentOptions[k] !== void 0 ? `${k} (current: ${currentOptions[k]})` : k.startsWith("credentials.") && !bootstrapAuthHeader ? `${k} (current: unknown)` : `${k} (not set)`,
|
|
131
|
+
value: k
|
|
132
|
+
})),
|
|
133
|
+
cancel: "reject"
|
|
134
|
+
});
|
|
135
|
+
const currentValue = currentOptions[key];
|
|
136
|
+
const newValue = await consola.prompt("Enter new value:", {
|
|
137
|
+
type: "text",
|
|
138
|
+
default: currentValue,
|
|
139
|
+
cancel: "reject"
|
|
140
|
+
});
|
|
141
|
+
consola.info(`Updating option: ${key}`);
|
|
142
|
+
await upsertTenantOption(baseUrl, category, key, newValue, devAuthHeader);
|
|
143
|
+
currentOptions[key] = newValue;
|
|
144
|
+
consola.success(`Option '${key}' updated successfully`);
|
|
145
|
+
if (!await consola.prompt("Update another option?", {
|
|
146
|
+
type: "confirm",
|
|
147
|
+
initial: false,
|
|
148
|
+
cancel: "reject"
|
|
149
|
+
})) continueUpdating = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Handle deleting multiple options
|
|
154
|
+
* @param ctx - The shared options context
|
|
155
|
+
*/
|
|
156
|
+
async function handleDelete(ctx) {
|
|
157
|
+
const { baseUrl, category, devAuthHeader, bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
|
|
158
|
+
const allKeys = bootstrapAuthHeader ? availableKeys.filter((k) => currentOptions[k] !== void 0) : [...availableKeys.filter((k) => !k.startsWith("credentials.") && currentOptions[k] !== void 0), ...availableKeys.filter((k) => k.startsWith("credentials."))];
|
|
159
|
+
if (allKeys.length === 0) {
|
|
160
|
+
consola.warn("No options are currently set");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const keysToDelete = await consola.prompt("Select option(s) to delete:", {
|
|
164
|
+
type: "multiselect",
|
|
165
|
+
options: allKeys,
|
|
166
|
+
required: true,
|
|
167
|
+
cancel: "reject"
|
|
168
|
+
});
|
|
169
|
+
consola.info(`Deleting ${keysToDelete.length} option(s)...`);
|
|
170
|
+
for (const key of keysToDelete) {
|
|
171
|
+
consola.info(`Deleting option: ${key}`);
|
|
172
|
+
await deleteTenantOption(baseUrl, category, key, devAuthHeader);
|
|
173
|
+
delete currentOptions[key];
|
|
174
|
+
consola.success(`✓ Deleted: ${key}`);
|
|
175
|
+
}
|
|
176
|
+
consola.success("Delete operation completed");
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
export { options_default as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as createBasicAuthHeader, p as createC8yManifest, t as assignUserRole, u as unassignUserRole } from "./c8y-api-
|
|
1
|
+
import { n as createBasicAuthHeader, p as createC8yManifest, t as assignUserRole, u as unassignUserRole } from "./c8y-api-DscUs-Kd.mjs";
|
|
2
2
|
import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { consola } from "consola";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as createError } from "../../logging-BUYT8Z2V.mjs";
|
|
2
|
-
import { t as useDeployedTenantClient } from "../../client-
|
|
2
|
+
import { t as useDeployedTenantClient } from "../../client-D0Tdq7H1.mjs";
|
|
3
3
|
import { c8yTenantOptionKeys } from "c8y-nitro/runtime";
|
|
4
4
|
import { defineCachedFunction } from "nitro/cache";
|
|
5
5
|
import { useRuntimeConfig } from "nitro/runtime-config";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as tryGetUserTenantDomainFast } from "../../client-D0Tdq7H1.mjs";
|
|
2
2
|
import { c8yManifest } from "c8y-nitro/runtime";
|
|
3
3
|
import { useRuntimeConfig } from "nitro/runtime-config";
|
|
4
4
|
import { getRequestURL } from "nitro/h3";
|
|
@@ -14,13 +14,13 @@ function joinURLSegments(origin, ...segments) {
|
|
|
14
14
|
async function resolveServer(event) {
|
|
15
15
|
const baseURL = useRuntimeConfig().app?.baseURL;
|
|
16
16
|
const contextPath = c8yManifest.contextPath ?? c8yManifest.name;
|
|
17
|
-
if (!import.meta.dev)
|
|
18
|
-
const domain = await
|
|
17
|
+
if (!import.meta.dev) {
|
|
18
|
+
const domain = await tryGetUserTenantDomainFast(event);
|
|
19
19
|
if (domain) return {
|
|
20
20
|
url: joinURLSegments(`https://${domain}`, "service", contextPath, baseURL),
|
|
21
21
|
description: "Cumulocity microservice endpoint"
|
|
22
22
|
};
|
|
23
|
-
}
|
|
23
|
+
}
|
|
24
24
|
const requestUrl = getRequestURL(event, {
|
|
25
25
|
xForwardedHost: true,
|
|
26
26
|
xForwardedProto: true
|
|
@@ -43,8 +43,9 @@ async function resolveServer(event) {
|
|
|
43
43
|
var openapi_default = defineMiddleware(async (event, next) => {
|
|
44
44
|
const runtimeConfig = useRuntimeConfig();
|
|
45
45
|
const specRoute = runtimeConfig.nitro?.openAPI?.route || "/_openapi.json";
|
|
46
|
-
const
|
|
47
|
-
|
|
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();
|
|
48
49
|
const body = await next();
|
|
49
50
|
if (!isOpenAPIDocument(body)) return body;
|
|
50
51
|
const excludedPrefixes = runtimeConfig.c8yOpenApiExcludeRoutes ?? [];
|
|
@@ -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/package.json
CHANGED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { c as getTenantOptionsByCategory, f as upsertTenantOption, i as deleteTenantOption, n as createBasicAuthHeader, p as createC8yManifest, s as getTenantOption } from "./c8y-api-BFjK6AYb.mjs";
|
|
2
|
-
import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
|
|
3
|
-
import { defineCommand } from "citty";
|
|
4
|
-
import { consola } from "consola";
|
|
5
|
-
//#region src/cli/commands/options.ts
|
|
6
|
-
var options_default = defineCommand({
|
|
7
|
-
meta: {
|
|
8
|
-
name: "options",
|
|
9
|
-
description: "Manage tenant options on the development tenant"
|
|
10
|
-
},
|
|
11
|
-
args: {},
|
|
12
|
-
async run() {
|
|
13
|
-
consola.info("Loading configuration...");
|
|
14
|
-
const { env, nitroConfig, configDir } = await loadC8yConfig();
|
|
15
|
-
consola.info("Validating environment variables...");
|
|
16
|
-
const envVars = validateBootstrapEnv(env);
|
|
17
|
-
consola.info("Loading manifest...");
|
|
18
|
-
const manifest = await createC8yManifest(configDir, nitroConfig);
|
|
19
|
-
const category = manifest.settingsCategory || manifest.contextPath || manifest.name;
|
|
20
|
-
if (!manifest.settings || manifest.settings.length === 0) throw new Error("No settings defined in manifest. Add settings to your c8y.manifest configuration.");
|
|
21
|
-
consola.success(`Using category: ${category}`);
|
|
22
|
-
const authHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
|
|
23
|
-
consola.info("Fetching current tenant options...");
|
|
24
|
-
const currentOptions = await getTenantOptionsByCategory(envVars.C8Y_BASEURL, category, authHeader);
|
|
25
|
-
const availableKeys = manifest.settings.map((s) => s.key);
|
|
26
|
-
consola.success(`Found ${Object.keys(currentOptions).length} options set on tenant`);
|
|
27
|
-
const action = await consola.prompt("What do you want to do?", {
|
|
28
|
-
type: "select",
|
|
29
|
-
options: [
|
|
30
|
-
{
|
|
31
|
-
label: "Read option value",
|
|
32
|
-
value: "read"
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
label: "Update/Create option",
|
|
36
|
-
value: "update"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
label: "Delete option(s)",
|
|
40
|
-
value: "delete"
|
|
41
|
-
}
|
|
42
|
-
]
|
|
43
|
-
});
|
|
44
|
-
if (action === "read") await handleRead(envVars.C8Y_BASEURL, category, authHeader, availableKeys, currentOptions);
|
|
45
|
-
else if (action === "update") await handleUpdate(envVars.C8Y_BASEURL, category, authHeader, availableKeys, currentOptions);
|
|
46
|
-
else if (action === "delete") await handleDelete(envVars.C8Y_BASEURL, category, authHeader, availableKeys, currentOptions);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
/**
|
|
50
|
-
* Handle reading a single option
|
|
51
|
-
* @param baseUrl - The Cumulocity base URL
|
|
52
|
-
* @param category - The tenant option category
|
|
53
|
-
* @param authHeader - The Basic Auth header
|
|
54
|
-
* @param availableKeys - List of available option keys
|
|
55
|
-
* @param currentOptions - Current option values from tenant
|
|
56
|
-
*/
|
|
57
|
-
async function handleRead(baseUrl, category, authHeader, availableKeys, currentOptions) {
|
|
58
|
-
const setKeys = availableKeys.filter((k) => !k.startsWith("credentials.") && currentOptions[k] !== void 0);
|
|
59
|
-
const credentialsKeys = availableKeys.filter((k) => k.startsWith("credentials."));
|
|
60
|
-
const allKeys = [...setKeys, ...credentialsKeys];
|
|
61
|
-
if (allKeys.length === 0) {
|
|
62
|
-
consola.warn("No options are currently set");
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (credentialsKeys.length > 0) consola.warn("Encrypted credentials.* options cannot be read in decrypted form with the development user in this CLI.");
|
|
66
|
-
const key = await consola.prompt("Select option to read:", {
|
|
67
|
-
type: "select",
|
|
68
|
-
options: allKeys.map((k) => ({
|
|
69
|
-
label: k.startsWith("credentials.") ? `${k} (unknown)` : k,
|
|
70
|
-
value: k
|
|
71
|
-
})),
|
|
72
|
-
cancel: "reject"
|
|
73
|
-
});
|
|
74
|
-
consola.info(`Reading option: ${key}`);
|
|
75
|
-
const value = await getTenantOption(baseUrl, category, key, authHeader);
|
|
76
|
-
if (value === void 0) consola.warn(`Option '${key}' is not set`);
|
|
77
|
-
else consola.success(`Value: ${value}`);
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Handle updating options (with loop for multiple updates)
|
|
81
|
-
* @param baseUrl - The Cumulocity base URL
|
|
82
|
-
* @param category - The tenant option category
|
|
83
|
-
* @param authHeader - The Basic Auth header
|
|
84
|
-
* @param availableKeys - List of available option keys
|
|
85
|
-
* @param currentOptions - Current option values from tenant
|
|
86
|
-
*/
|
|
87
|
-
async function handleUpdate(baseUrl, category, authHeader, availableKeys, currentOptions) {
|
|
88
|
-
let continueUpdating = true;
|
|
89
|
-
while (continueUpdating) {
|
|
90
|
-
const key = await consola.prompt("Select option to update:", {
|
|
91
|
-
type: "select",
|
|
92
|
-
options: availableKeys.map((k) => ({
|
|
93
|
-
label: currentOptions[k] !== void 0 ? `${k} (current: ${currentOptions[k]})` : k.startsWith("credentials.") ? `${k} (current: unknown)` : `${k} (not set)`,
|
|
94
|
-
value: k
|
|
95
|
-
})),
|
|
96
|
-
cancel: "reject"
|
|
97
|
-
});
|
|
98
|
-
const currentValue = currentOptions[key];
|
|
99
|
-
const newValue = await consola.prompt("Enter new value:", {
|
|
100
|
-
type: "text",
|
|
101
|
-
default: currentValue,
|
|
102
|
-
cancel: "reject"
|
|
103
|
-
});
|
|
104
|
-
consola.info(`Updating option: ${key}`);
|
|
105
|
-
await upsertTenantOption(baseUrl, category, key, newValue, authHeader);
|
|
106
|
-
currentOptions[key] = newValue;
|
|
107
|
-
consola.success(`Option '${key}' updated successfully`);
|
|
108
|
-
if (!await consola.prompt("Update another option?", {
|
|
109
|
-
type: "confirm",
|
|
110
|
-
initial: false,
|
|
111
|
-
cancel: "reject"
|
|
112
|
-
})) continueUpdating = false;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Handle deleting multiple options
|
|
117
|
-
* @param baseUrl - The Cumulocity base URL
|
|
118
|
-
* @param category - The tenant option category
|
|
119
|
-
* @param authHeader - The Basic Auth header
|
|
120
|
-
* @param availableKeys - List of available option keys
|
|
121
|
-
* @param currentOptions - Current option values from tenant
|
|
122
|
-
*/
|
|
123
|
-
async function handleDelete(baseUrl, category, authHeader, availableKeys, currentOptions) {
|
|
124
|
-
const setKeys = availableKeys.filter((k) => !k.startsWith("credentials.") && currentOptions[k] !== void 0);
|
|
125
|
-
const credentialsKeys = availableKeys.filter((k) => k.startsWith("credentials."));
|
|
126
|
-
const allKeys = [...setKeys, ...credentialsKeys];
|
|
127
|
-
if (allKeys.length === 0) {
|
|
128
|
-
consola.warn("No options are currently set");
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
const keysToDelete = await consola.prompt("Select option(s) to delete:", {
|
|
132
|
-
type: "multiselect",
|
|
133
|
-
options: allKeys,
|
|
134
|
-
required: true,
|
|
135
|
-
cancel: "reject"
|
|
136
|
-
});
|
|
137
|
-
consola.info(`Deleting ${keysToDelete.length} option(s)...`);
|
|
138
|
-
for (const key of keysToDelete) {
|
|
139
|
-
consola.info(`Deleting option: ${key}`);
|
|
140
|
-
await deleteTenantOption(baseUrl, category, key, authHeader);
|
|
141
|
-
delete currentOptions[key];
|
|
142
|
-
consola.success(`✓ Deleted: ${key}`);
|
|
143
|
-
}
|
|
144
|
-
consola.success("Delete operation completed");
|
|
145
|
-
}
|
|
146
|
-
//#endregion
|
|
147
|
-
export { options_default as default };
|