@cdot65/prisma-airs-cli 5.4.1 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/{chunk-36Z6IPH6.js → chunk-XQOICLSH.js} +220 -25
- package/dist/cli/index.js +1063 -127
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
- **Unified automation output** — resource reads support `pretty`, `table`, `markdown`, `csv`, `json`, and `yaml`, with pipe-safe stdout; environment deliverables use HTML or Markdown
|
|
26
26
|
- **Complete pagination** — consistent `--limit`, `--offset`, and `--all` traversal with a configurable safety cap
|
|
27
27
|
- **`airs doctor`** — one-command diagnostics for environment, credentials, and API connectivity
|
|
28
|
-
-
|
|
28
|
+
- **Tenant configuration** — `airs tenant create|set|switch|list|read|delete` supports guided setup with hidden secret prompts, individual setting updates, and existing-file registration without changing read-only mounts
|
|
29
|
+
- **Profile migration** — `airs runtime profiles backup|restore` exports private JSON/YAML, previews cross-tenant restores, remaps topics and explicit DLP dependencies, and verifies restored policies ([guide](https://cdot65.github.io/prisma-airs-cli/runtime/profile-transfer/))
|
|
30
|
+
- **`airs config`** — manage the selected config file from the CLI (`list`, `get`, `set`, `unset`, `path`)
|
|
29
31
|
|
|
30
32
|
## Install
|
|
31
33
|
|
|
@@ -1454,7 +1454,7 @@ var SdkRedTeamService = class {
|
|
|
1454
1454
|
}));
|
|
1455
1455
|
}
|
|
1456
1456
|
async waitForCompletion(jobId, onProgress, intervalMs = 5e3) {
|
|
1457
|
-
const delay = (ms) => new Promise((
|
|
1457
|
+
const delay = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1458
1458
|
while (true) {
|
|
1459
1459
|
const job = await this.getScan(jobId);
|
|
1460
1460
|
onProgress?.(job);
|
|
@@ -1826,7 +1826,7 @@ var SdkRuntimeService = class {
|
|
|
1826
1826
|
const retryAfterMs = error.retryAfterMs;
|
|
1827
1827
|
const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryAttempt - 1);
|
|
1828
1828
|
retryOpts?.onRetry?.(retryAttempt, delayMs);
|
|
1829
|
-
await new Promise((
|
|
1829
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
1830
1830
|
}
|
|
1831
1831
|
}
|
|
1832
1832
|
return {
|
|
@@ -1871,7 +1871,7 @@ var SdkRuntimeService = class {
|
|
|
1871
1871
|
const retryAfterMs = err.retryAfterMs;
|
|
1872
1872
|
const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryLevel - 1);
|
|
1873
1873
|
retryOpts?.onRetry?.(retryLevel, delayMs);
|
|
1874
|
-
await new Promise((
|
|
1874
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
1875
1875
|
continue;
|
|
1876
1876
|
}
|
|
1877
1877
|
throw err;
|
|
@@ -1944,7 +1944,7 @@ var SdkRuntimeService = class {
|
|
|
1944
1944
|
const retryAfterMs = err.retryAfterMs;
|
|
1945
1945
|
const delayMs = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : baseDelay * 2 ** (retryLevel - 1);
|
|
1946
1946
|
retryOpts?.onRetry?.(retryLevel, delayMs);
|
|
1947
|
-
await new Promise((
|
|
1947
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
1948
1948
|
continue;
|
|
1949
1949
|
}
|
|
1950
1950
|
throw err;
|
|
@@ -1978,7 +1978,7 @@ var SdkRuntimeService = class {
|
|
|
1978
1978
|
);
|
|
1979
1979
|
}
|
|
1980
1980
|
if (resolved.size < batch.entries.length) {
|
|
1981
|
-
await new Promise((
|
|
1981
|
+
await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
|
|
1982
1982
|
}
|
|
1983
1983
|
}
|
|
1984
1984
|
return [...resolved.values()].sort((left, right) => left.index - right.index);
|
|
@@ -2026,7 +2026,7 @@ var SdkRuntimeService = class {
|
|
|
2026
2026
|
retryLevel++;
|
|
2027
2027
|
const delayMs = baseDelay * 2 ** (retryLevel - 1);
|
|
2028
2028
|
retryOpts?.onRetry?.(retryLevel, delayMs);
|
|
2029
|
-
await new Promise((
|
|
2029
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
2030
2030
|
sweepCompleted = false;
|
|
2031
2031
|
break;
|
|
2032
2032
|
}
|
|
@@ -2035,7 +2035,7 @@ var SdkRuntimeService = class {
|
|
|
2035
2035
|
this.processQueryResults(results, completed, pending);
|
|
2036
2036
|
if (b + 5 < pendingIds.length) {
|
|
2037
2037
|
const batchDelay = retryLevel > 0 ? baseDelay : Math.min(baseDelay, 1e3);
|
|
2038
|
-
await new Promise((
|
|
2038
|
+
await new Promise((resolve2) => setTimeout(resolve2, batchDelay));
|
|
2039
2039
|
}
|
|
2040
2040
|
}
|
|
2041
2041
|
if (sweepCompleted && retryLevel > 0) {
|
|
@@ -2043,7 +2043,7 @@ var SdkRuntimeService = class {
|
|
|
2043
2043
|
}
|
|
2044
2044
|
if (pending.size > 0) {
|
|
2045
2045
|
const sweepDelay = retryLevel > 0 ? baseDelay * 2 ** retryLevel : intervalMs;
|
|
2046
|
-
await new Promise((
|
|
2046
|
+
await new Promise((resolve2) => setTimeout(resolve2, sweepDelay));
|
|
2047
2047
|
}
|
|
2048
2048
|
}
|
|
2049
2049
|
return scanIds.map((id) => completed.get(id));
|
|
@@ -2164,7 +2164,7 @@ var RateLimitedScanService = class {
|
|
|
2164
2164
|
const oldest = this.timestamps[0];
|
|
2165
2165
|
const waitMs = 1e3 - (now - oldest);
|
|
2166
2166
|
if (waitMs > 0) {
|
|
2167
|
-
await new Promise((
|
|
2167
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitMs));
|
|
2168
2168
|
}
|
|
2169
2169
|
const after = Date.now();
|
|
2170
2170
|
this.timestamps = this.timestamps.filter((t) => after - t < 1e3);
|
|
@@ -2233,8 +2233,8 @@ function readBackupDir(dirPath, resourceType) {
|
|
|
2233
2233
|
|
|
2234
2234
|
// src/config/loader.ts
|
|
2235
2235
|
import { readFile } from "fs/promises";
|
|
2236
|
-
import { homedir } from "os";
|
|
2237
|
-
import { join } from "path";
|
|
2236
|
+
import { homedir as homedir2 } from "os";
|
|
2237
|
+
import { join as join2 } from "path";
|
|
2238
2238
|
|
|
2239
2239
|
// src/config/schema.ts
|
|
2240
2240
|
import { z } from "zod";
|
|
@@ -2280,9 +2280,186 @@ var ConfigSchema = z.object({
|
|
|
2280
2280
|
dataDir: z.string().default("~/.prisma-airs/runs")
|
|
2281
2281
|
});
|
|
2282
2282
|
|
|
2283
|
+
// src/config/tenants.ts
|
|
2284
|
+
import { randomUUID } from "crypto";
|
|
2285
|
+
import { lstatSync, readFileSync, realpathSync } from "fs";
|
|
2286
|
+
import { mkdir, open, rename, unlink } from "fs/promises";
|
|
2287
|
+
import { homedir } from "os";
|
|
2288
|
+
import { dirname, isAbsolute, join, resolve } from "path";
|
|
2289
|
+
import { z as z2 } from "zod";
|
|
2290
|
+
var NameSchema = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/).refine((v) => v !== "default");
|
|
2291
|
+
var EntrySchema = z2.object({
|
|
2292
|
+
name: NameSchema,
|
|
2293
|
+
configPath: z2.string().refine(isAbsolute),
|
|
2294
|
+
tsgId: z2.string().min(1)
|
|
2295
|
+
}).strict();
|
|
2296
|
+
var StoreSchema = z2.object({
|
|
2297
|
+
version: z2.literal(1),
|
|
2298
|
+
active: z2.string().nullable(),
|
|
2299
|
+
tenants: z2.array(EntrySchema).max(1e3)
|
|
2300
|
+
}).strict().superRefine((value, ctx) => {
|
|
2301
|
+
const names = new Set(value.tenants.map((entry) => entry.name));
|
|
2302
|
+
if (names.size !== value.tenants.length || value.active !== null && !names.has(value.active))
|
|
2303
|
+
ctx.addIssue({ code: z2.ZodIssueCode.custom, message: "Invalid tenant registry identities" });
|
|
2304
|
+
});
|
|
2305
|
+
function tenantStorePath() {
|
|
2306
|
+
const explicit = process.env.PRISMA_AIRS_TENANTS_PATH;
|
|
2307
|
+
if (explicit) return resolve(expandConfigPath(explicit));
|
|
2308
|
+
const state = process.env.XDG_STATE_HOME;
|
|
2309
|
+
if (state && !isAbsolute(state)) throw new Error("XDG_STATE_HOME must be an absolute path");
|
|
2310
|
+
return join(state || join(homedir(), ".local", "state"), "prisma-airs", "tenants.json");
|
|
2311
|
+
}
|
|
2312
|
+
function expandConfigPath(path2) {
|
|
2313
|
+
return path2 === "~" ? homedir() : path2.startsWith("~/") ? join(homedir(), path2.slice(2)) : path2;
|
|
2314
|
+
}
|
|
2315
|
+
function defaultTenantConfigPath() {
|
|
2316
|
+
return join(homedir(), ".prisma-airs", "config.json");
|
|
2317
|
+
}
|
|
2318
|
+
function missing(error) {
|
|
2319
|
+
return error?.code === "ENOENT";
|
|
2320
|
+
}
|
|
2321
|
+
function readTenantStore() {
|
|
2322
|
+
const path2 = tenantStorePath();
|
|
2323
|
+
try {
|
|
2324
|
+
const stat = lstatSync(path2);
|
|
2325
|
+
if (!stat.isFile() || stat.size > 1048576) throw new Error();
|
|
2326
|
+
return StoreSchema.parse(JSON.parse(readFileSync(path2, "utf8")));
|
|
2327
|
+
} catch (error) {
|
|
2328
|
+
if (missing(error)) return { version: 1, active: null, tenants: [] };
|
|
2329
|
+
throw new Error(
|
|
2330
|
+
`Cannot read tenant registry at ${path2}; restore a valid registry before proceeding`
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
function selectedTenant() {
|
|
2335
|
+
const store = readTenantStore();
|
|
2336
|
+
return store.tenants.find((entry) => entry.name === store.active);
|
|
2337
|
+
}
|
|
2338
|
+
function readTenantConfig(path2, expectedTsgId) {
|
|
2339
|
+
return ConfigSchema.parse(readTenantConfigFile(path2, expectedTsgId));
|
|
2340
|
+
}
|
|
2341
|
+
function readTenantConfigFile(path2, expectedTsgId) {
|
|
2342
|
+
let config;
|
|
2343
|
+
let parsed;
|
|
2344
|
+
try {
|
|
2345
|
+
const stat = lstatSync(realpathSync(path2));
|
|
2346
|
+
if (!stat.isFile() || stat.size > 1048576) throw new Error();
|
|
2347
|
+
parsed = JSON.parse(readFileSync(path2, "utf8"));
|
|
2348
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
|
|
2349
|
+
config = ConfigSchema.parse(parsed);
|
|
2350
|
+
} catch {
|
|
2351
|
+
throw new Error(`Cannot read a valid tenant config at ${path2}`);
|
|
2352
|
+
}
|
|
2353
|
+
if (expectedTsgId !== void 0 && config.mgmtTsgId !== expectedTsgId)
|
|
2354
|
+
throw new Error(
|
|
2355
|
+
"Tenant config TSG differs from its registered identity; register it under a new name"
|
|
2356
|
+
);
|
|
2357
|
+
return parsed;
|
|
2358
|
+
}
|
|
2359
|
+
function assertTenantEnvironment() {
|
|
2360
|
+
const conflicts = Object.keys(process.env).filter(
|
|
2361
|
+
(key) => /^PANW_(?:MGMT_|MODEL_SEC_|RED_TEAM_|AGENT_GUARD_|AI_GW_|DLP_|AI_SEC_API_)/.test(key) && Boolean(process.env[key])
|
|
2362
|
+
);
|
|
2363
|
+
if (conflicts.length)
|
|
2364
|
+
throw new Error(
|
|
2365
|
+
`Named tenant selection conflicts with environment overrides: ${conflicts.sort().join(", ")}. Unset them or switch to default.`
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
async function changeStore(change) {
|
|
2369
|
+
const path2 = tenantStorePath();
|
|
2370
|
+
await mkdir(dirname(path2), { recursive: true, mode: 448 });
|
|
2371
|
+
const lockPath = `${path2}.lock`;
|
|
2372
|
+
let lock;
|
|
2373
|
+
try {
|
|
2374
|
+
lock = await open(lockPath, "wx", 384);
|
|
2375
|
+
} catch {
|
|
2376
|
+
throw new Error(
|
|
2377
|
+
`Cannot lock tenant registry at ${lockPath}; another change may be in progress`
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
const temp = join(dirname(path2), `.airs-tenants-${randomUUID()}.tmp`);
|
|
2381
|
+
let created = false;
|
|
2382
|
+
try {
|
|
2383
|
+
const store = readTenantStore();
|
|
2384
|
+
change(store);
|
|
2385
|
+
StoreSchema.parse(store);
|
|
2386
|
+
const file = await open(temp, "wx", 384);
|
|
2387
|
+
created = true;
|
|
2388
|
+
try {
|
|
2389
|
+
await file.writeFile(`${JSON.stringify(store, null, 2)}
|
|
2390
|
+
`);
|
|
2391
|
+
await file.sync();
|
|
2392
|
+
} finally {
|
|
2393
|
+
await file.close();
|
|
2394
|
+
}
|
|
2395
|
+
await rename(temp, path2);
|
|
2396
|
+
created = false;
|
|
2397
|
+
return store;
|
|
2398
|
+
} finally {
|
|
2399
|
+
if (created) await unlink(temp).catch(() => {
|
|
2400
|
+
});
|
|
2401
|
+
await lock.close();
|
|
2402
|
+
await unlink(lockPath);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
function validateTenantName(name) {
|
|
2406
|
+
if (!NameSchema.safeParse(name).success)
|
|
2407
|
+
throw new Error(
|
|
2408
|
+
"Tenant name must be 1\u201364 letters, digits, hyphens or underscores, start with a letter/digit, and not be default"
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
async function createTenant(name, configPath) {
|
|
2412
|
+
validateTenantName(name);
|
|
2413
|
+
let path2;
|
|
2414
|
+
try {
|
|
2415
|
+
path2 = realpathSync(resolve(expandConfigPath(configPath)));
|
|
2416
|
+
} catch {
|
|
2417
|
+
throw new Error("Tenant config file does not exist or cannot be resolved");
|
|
2418
|
+
}
|
|
2419
|
+
const config = readTenantConfig(path2);
|
|
2420
|
+
if (!config.mgmtTsgId?.trim() || !config.mgmtClientId?.trim() || !config.mgmtClientSecret?.trim())
|
|
2421
|
+
throw new Error("Tenant config requires mgmtTsgId, mgmtClientId and mgmtClientSecret");
|
|
2422
|
+
const entry = { name, configPath: path2, tsgId: config.mgmtTsgId };
|
|
2423
|
+
await changeStore((store) => {
|
|
2424
|
+
if (store.tenants.some((tenant) => tenant.name === name))
|
|
2425
|
+
throw new Error("Tenant name already exists");
|
|
2426
|
+
store.tenants.push(entry);
|
|
2427
|
+
store.tenants.sort((a, b) => a.name.localeCompare(b.name));
|
|
2428
|
+
});
|
|
2429
|
+
return entry;
|
|
2430
|
+
}
|
|
2431
|
+
async function switchTenant(name) {
|
|
2432
|
+
let selected;
|
|
2433
|
+
await changeStore((store) => {
|
|
2434
|
+
if (name === "default") {
|
|
2435
|
+
store.active = null;
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
if (process.env.PRISMA_AIRS_CONFIG_PATH)
|
|
2439
|
+
throw new Error(
|
|
2440
|
+
"Unset PRISMA_AIRS_CONFIG_PATH before switching tenants; it overrides named selection"
|
|
2441
|
+
);
|
|
2442
|
+
assertTenantEnvironment();
|
|
2443
|
+
selected = store.tenants.find((entry) => entry.name === name);
|
|
2444
|
+
if (!selected) throw new Error("Tenant not found");
|
|
2445
|
+
readTenantConfig(selected.configPath, selected.tsgId);
|
|
2446
|
+
store.active = name;
|
|
2447
|
+
});
|
|
2448
|
+
return selected;
|
|
2449
|
+
}
|
|
2450
|
+
async function deleteTenant(name) {
|
|
2451
|
+
await changeStore((store) => {
|
|
2452
|
+
if (name === "default") throw new Error("The default tenant cannot be deleted");
|
|
2453
|
+
if (store.active === name)
|
|
2454
|
+
throw new Error("Switch to another tenant or default before deleting the active tenant");
|
|
2455
|
+
if (!store.tenants.some((entry) => entry.name === name)) throw new Error("Tenant not found");
|
|
2456
|
+
store.tenants = store.tenants.filter((entry) => entry.name !== name);
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2283
2460
|
// src/config/loader.ts
|
|
2284
2461
|
function expandHome(p) {
|
|
2285
|
-
return p.startsWith("~") ?
|
|
2462
|
+
return p.startsWith("~") ? join2(homedir2(), p.slice(1)) : p;
|
|
2286
2463
|
}
|
|
2287
2464
|
function fromEnv() {
|
|
2288
2465
|
const env = process.env;
|
|
@@ -2332,14 +2509,23 @@ function stripUndefined(obj) {
|
|
|
2332
2509
|
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0 && v !== ""));
|
|
2333
2510
|
}
|
|
2334
2511
|
function resolveConfigFilePath(configFilePath) {
|
|
2335
|
-
|
|
2512
|
+
return resolveConfigSource(configFilePath).path;
|
|
2513
|
+
}
|
|
2514
|
+
function resolveConfigSource(configFilePath) {
|
|
2515
|
+
if (configFilePath) return { path: expandHome(configFilePath) };
|
|
2336
2516
|
const envPath = process.env.PRISMA_AIRS_CONFIG_PATH;
|
|
2337
|
-
if (envPath) return expandHome(envPath);
|
|
2338
|
-
|
|
2517
|
+
if (envPath) return { path: expandHome(envPath) };
|
|
2518
|
+
const tenant = selectedTenant();
|
|
2519
|
+
return { path: tenant?.configPath ?? join2(homedir2(), ".prisma-airs", "config.json"), tenant };
|
|
2520
|
+
}
|
|
2521
|
+
async function readConfigSource(configFilePath) {
|
|
2522
|
+
const { path: path2, tenant } = resolveConfigSource(configFilePath);
|
|
2523
|
+
if (!tenant) return fromFile(path2);
|
|
2524
|
+
assertTenantEnvironment();
|
|
2525
|
+
return readTenantConfigFile(path2, tenant.tsgId);
|
|
2339
2526
|
}
|
|
2340
2527
|
async function loadConfig(cliOverrides = {}, configFilePath) {
|
|
2341
|
-
const
|
|
2342
|
-
const fileConfig = await fromFile(filePath);
|
|
2528
|
+
const fileConfig = await readConfigSource(configFilePath);
|
|
2343
2529
|
const envConfig = fromEnv();
|
|
2344
2530
|
const merged = {
|
|
2345
2531
|
...stripUndefined(fileConfig),
|
|
@@ -2353,8 +2539,7 @@ async function loadConfig(cliOverrides = {}, configFilePath) {
|
|
|
2353
2539
|
};
|
|
2354
2540
|
}
|
|
2355
2541
|
async function inspectConfig(configFilePath) {
|
|
2356
|
-
const
|
|
2357
|
-
const fileConfig = stripUndefined(await fromFile(filePath));
|
|
2542
|
+
const fileConfig = stripUndefined(await readConfigSource(configFilePath));
|
|
2358
2543
|
const envConfig = stripUndefined(fromEnv());
|
|
2359
2544
|
const merged = { ...fileConfig, ...envConfig };
|
|
2360
2545
|
const config = ConfigSchema.parse(merged);
|
|
@@ -2927,12 +3112,12 @@ function renderEnvironmentReportHtml(r) {
|
|
|
2927
3112
|
}
|
|
2928
3113
|
|
|
2929
3114
|
// src/reports/io.ts
|
|
2930
|
-
import { randomUUID } from "crypto";
|
|
2931
|
-
import { link, open, unlink } from "fs/promises";
|
|
2932
|
-
import { dirname, join as
|
|
3115
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3116
|
+
import { link, open as open2, unlink as unlink2 } from "fs/promises";
|
|
3117
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
2933
3118
|
async function writeReportFile(path2, content) {
|
|
2934
|
-
const temporary =
|
|
2935
|
-
const file = await
|
|
3119
|
+
const temporary = join3(dirname2(path2), `.airs-report-${randomUUID2()}.tmp`);
|
|
3120
|
+
const file = await open2(temporary, "wx", 384);
|
|
2936
3121
|
try {
|
|
2937
3122
|
await file.writeFile(content, "utf8");
|
|
2938
3123
|
await file.sync();
|
|
@@ -2940,7 +3125,7 @@ async function writeReportFile(path2, content) {
|
|
|
2940
3125
|
await link(temporary, path2);
|
|
2941
3126
|
} finally {
|
|
2942
3127
|
await file.close();
|
|
2943
|
-
await
|
|
3128
|
+
await unlink2(temporary);
|
|
2944
3129
|
}
|
|
2945
3130
|
}
|
|
2946
3131
|
|
|
@@ -4004,6 +4189,16 @@ export {
|
|
|
4004
4189
|
readBackupFile,
|
|
4005
4190
|
readBackupDir,
|
|
4006
4191
|
ConfigSchema,
|
|
4192
|
+
tenantStorePath,
|
|
4193
|
+
expandConfigPath,
|
|
4194
|
+
defaultTenantConfigPath,
|
|
4195
|
+
readTenantStore,
|
|
4196
|
+
readTenantConfig,
|
|
4197
|
+
readTenantConfigFile,
|
|
4198
|
+
validateTenantName,
|
|
4199
|
+
createTenant,
|
|
4200
|
+
switchTenant,
|
|
4201
|
+
deleteTenant,
|
|
4007
4202
|
resolveConfigFilePath,
|
|
4008
4203
|
loadConfig,
|
|
4009
4204
|
inspectConfig,
|