@cdot65/prisma-airs-cli 5.11.1 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  AirsScanService,
4
4
  ConfigSchema,
5
+ RETIRED_CONFIG_KEYS,
5
6
  RateLimitedScanService,
6
7
  SDK_ASYNC_BATCH_SIZE,
7
8
  SdkManagementService,
@@ -14,9 +15,7 @@ import {
14
15
  collectRuntimeDailyReport,
15
16
  computeMetrics,
16
17
  createTenant,
17
- defaultTenantConfigPath,
18
18
  deleteTenant,
19
- expandConfigPath,
20
19
  getOrCreateManagementClient,
21
20
  inspectConfig,
22
21
  loadConfig,
@@ -31,7 +30,7 @@ import {
31
30
  renderRedTeamReportMarkdown,
32
31
  renderRuntimeReportHtml,
33
32
  renderRuntimeReportMarkdown,
34
- resolveConfigFilePath,
33
+ resolveConfigContext,
35
34
  resolveOutputDir,
36
35
  sanitizeFilename,
37
36
  switchTenant,
@@ -40,10 +39,7 @@ import {
40
39
  validateTopic,
41
40
  writeBackupFile,
42
41
  writeReportFile
43
- } from "../chunk-37SN57XQ.js";
44
-
45
- // src/cli/index.ts
46
- import "dotenv/config";
42
+ } from "../chunk-KGMQY2IT.js";
47
43
 
48
44
  // src/cli/process-guards.ts
49
45
  import chalk from "chalk";
@@ -61,7 +57,7 @@ function installProcessGuards() {
61
57
  // src/cli/program.ts
62
58
  import { randomUUID as randomUUID12 } from "crypto";
63
59
  import { readFileSync as readFileSync4 } from "fs";
64
- import { dirname as dirname5, join as join5 } from "path";
60
+ import { dirname as dirname4, join as join5 } from "path";
65
61
  import { fileURLToPath } from "url";
66
62
  import { Command } from "commander";
67
63
 
@@ -79,65 +75,124 @@ import { dump as dump3 } from "js-yaml";
79
75
  import { z } from "zod";
80
76
 
81
77
  // src/config/client-options.ts
82
- function agentGuardClientOptions(config) {
78
+ import {
79
+ AGENT_GUARD_DATA_ENDPOINT,
80
+ AGENT_GUARD_MGMT_ENDPOINT,
81
+ DEFAULT_AI_GW_ADMIN_ENDPOINT,
82
+ DEFAULT_AI_GW_DATA_ENDPOINT,
83
+ DEFAULT_DLP_ENDPOINT,
84
+ DEFAULT_ENDPOINT,
85
+ DEFAULT_IAM_ENDPOINT,
86
+ DEFAULT_MGMT_ENDPOINT,
87
+ DEFAULT_MODEL_SEC_DATA_ENDPOINT,
88
+ DEFAULT_MODEL_SEC_MGMT_ENDPOINT,
89
+ DEFAULT_RED_TEAM_DATA_ENDPOINT,
90
+ DEFAULT_RED_TEAM_MGMT_ENDPOINT,
91
+ DEFAULT_RED_TEAM_NETWORK_BROKER_ENDPOINT,
92
+ DEFAULT_TOKEN_ENDPOINT
93
+ } from "@cdot65/prisma-airs-sdk";
94
+
95
+ // src/config/credentials.ts
96
+ var MANAGEMENT_CREDENTIAL_KEYS = [
97
+ "mgmtClientId",
98
+ "mgmtClientSecret",
99
+ "mgmtTsgId"
100
+ ];
101
+ var SCANNER_CREDENTIAL_KEYS = ["airsApiKey", "airsApiToken"];
102
+ function present(value) {
103
+ return typeof value === "string" && value.trim() !== "";
104
+ }
105
+ function missingManagementCredentials(config) {
106
+ return MANAGEMENT_CREDENTIAL_KEYS.filter((key) => !present(config[key]));
107
+ }
108
+ function hasScannerCredentials(config) {
109
+ return SCANNER_CREDENTIAL_KEYS.some((key) => present(config[key]));
110
+ }
111
+ function safeContext() {
112
+ try {
113
+ return resolveConfigContext();
114
+ } catch {
115
+ return void 0;
116
+ }
117
+ }
118
+ function settingRemedy(keys, context3 = safeContext()) {
119
+ const list = keys.join(", ");
120
+ if (context3?.selection === "tenant") {
121
+ return `Run 'airs tenant set ${context3.tenant.name} <key>' for ${list} (secrets prompt hidden, or use --stdin)`;
122
+ }
123
+ if (context3?.selection === "explicit") return `Add ${list} to ${context3.path}`;
124
+ return `Run 'airs tenant create <name>' (prompts for the OAuth credentials), then 'airs tenant switch <name>'`;
125
+ }
126
+ function assertManagementCredentials(config, context3) {
127
+ const missing = missingManagementCredentials(config);
128
+ if (missing.length === 0) return;
129
+ throw new Error(
130
+ `Management credentials are not configured (missing ${missing.join(", ")}). ${settingRemedy(missing, context3)}`
131
+ );
132
+ }
133
+ function assertScannerCredentials(config, context3) {
134
+ if (hasScannerCredentials(config)) return;
135
+ throw new Error(
136
+ `Scanner credentials are not configured (airsApiKey or airsApiToken). ${settingRemedy(["airsApiKey"], context3)}`
137
+ );
138
+ }
139
+
140
+ // src/config/client-options.ts
141
+ var DEFAULT_MGMT_DASHBOARD_ENDPOINT = "https://api.apps.paloaltonetworks.com/aisec";
142
+ function oauth(config) {
143
+ assertManagementCredentials(config);
83
144
  return {
84
145
  clientId: config.mgmtClientId,
85
146
  clientSecret: config.mgmtClientSecret,
86
147
  tsgId: config.mgmtTsgId,
87
- dataEndpoint: config.agentGuardDataEndpoint,
88
- mgmtEndpoint: config.agentGuardMgmtEndpoint,
89
- tokenEndpoint: config.agentGuardTokenEndpoint ?? config.mgmtTokenEndpoint
148
+ tokenEndpoint: config.mgmtTokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT
90
149
  };
91
150
  }
92
151
  function managementClientOptions(config) {
93
152
  return {
94
- clientId: config.mgmtClientId,
95
- clientSecret: config.mgmtClientSecret,
96
- tsgId: config.mgmtTsgId,
97
- apiEndpoint: config.mgmtEndpoint,
98
- dashboardEndpoint: config.mgmtDashboardEndpoint ?? "https://api.apps.paloaltonetworks.com/aisec",
99
- tokenEndpoint: config.mgmtTokenEndpoint,
100
- dlpEndpoint: config.dlpEndpoint
153
+ ...oauth(config),
154
+ apiEndpoint: config.mgmtEndpoint ?? DEFAULT_MGMT_ENDPOINT,
155
+ dashboardEndpoint: config.mgmtDashboardEndpoint ?? DEFAULT_MGMT_DASHBOARD_ENDPOINT,
156
+ dlpEndpoint: config.dlpEndpoint ?? DEFAULT_DLP_ENDPOINT
101
157
  };
102
158
  }
103
- function runtimeInitOptions(config) {
159
+ function agentGuardClientOptions(config) {
104
160
  return {
105
- apiKey: config.airsApiKey,
106
- apiToken: config.airsApiToken,
107
- apiEndpoint: config.airsApiEndpoint,
108
- numRetries: config.airsNumRetries
161
+ ...oauth(config),
162
+ dataEndpoint: config.agentGuardDataEndpoint ?? AGENT_GUARD_DATA_ENDPOINT,
163
+ mgmtEndpoint: config.agentGuardMgmtEndpoint ?? AGENT_GUARD_MGMT_ENDPOINT
109
164
  };
110
165
  }
111
166
  function redTeamClientOptions(config) {
112
167
  return {
113
- clientId: config.mgmtClientId,
114
- clientSecret: config.mgmtClientSecret,
115
- tsgId: config.mgmtTsgId,
116
- dataEndpoint: config.redTeamDataEndpoint,
117
- mgmtEndpoint: config.redTeamMgmtEndpoint,
118
- tokenEndpoint: config.redTeamTokenEndpoint ?? config.mgmtTokenEndpoint,
119
- networkBrokerEndpoint: config.redTeamNetworkBrokerEndpoint
168
+ ...oauth(config),
169
+ dataEndpoint: config.redTeamDataEndpoint ?? DEFAULT_RED_TEAM_DATA_ENDPOINT,
170
+ mgmtEndpoint: config.redTeamMgmtEndpoint ?? DEFAULT_RED_TEAM_MGMT_ENDPOINT,
171
+ networkBrokerEndpoint: config.redTeamNetworkBrokerEndpoint ?? DEFAULT_RED_TEAM_NETWORK_BROKER_ENDPOINT
172
+ };
173
+ }
174
+ function modelSecurityClientOptions(config) {
175
+ return {
176
+ ...oauth(config),
177
+ dataEndpoint: config.modelSecDataEndpoint ?? DEFAULT_MODEL_SEC_DATA_ENDPOINT,
178
+ mgmtEndpoint: config.modelSecMgmtEndpoint ?? DEFAULT_MODEL_SEC_MGMT_ENDPOINT
120
179
  };
121
180
  }
122
181
  function aiGatewayClientOptions(config) {
123
182
  return {
124
- clientId: config.mgmtClientId,
125
- clientSecret: config.mgmtClientSecret,
126
- tsgId: config.mgmtTsgId,
127
- dataEndpoint: config.aiGwDataEndpoint,
128
- adminEndpoint: config.aiGwAdminEndpoint,
129
- iamEndpoint: config.iamEndpoint,
130
- tokenEndpoint: config.aiGwTokenEndpoint ?? config.mgmtTokenEndpoint
183
+ ...oauth(config),
184
+ dataEndpoint: config.aiGwDataEndpoint ?? DEFAULT_AI_GW_DATA_ENDPOINT,
185
+ adminEndpoint: config.aiGwAdminEndpoint ?? DEFAULT_AI_GW_ADMIN_ENDPOINT,
186
+ iamEndpoint: config.iamEndpoint ?? DEFAULT_IAM_ENDPOINT
131
187
  };
132
188
  }
133
- function modelSecurityClientOptions(config) {
189
+ function runtimeInitOptions(config) {
190
+ assertScannerCredentials(config);
134
191
  return {
135
- clientId: config.mgmtClientId,
136
- clientSecret: config.mgmtClientSecret,
137
- tsgId: config.mgmtTsgId,
138
- dataEndpoint: config.modelSecDataEndpoint,
139
- mgmtEndpoint: config.modelSecMgmtEndpoint,
140
- tokenEndpoint: config.modelSecTokenEndpoint ?? config.mgmtTokenEndpoint
192
+ apiKey: config.airsApiKey,
193
+ apiToken: config.airsApiToken,
194
+ apiEndpoint: config.airsApiEndpoint ?? DEFAULT_ENDPOINT,
195
+ numRetries: config.airsNumRetries
141
196
  };
142
197
  }
143
198
 
@@ -384,13 +439,7 @@ async function resolveOutput(command, opts, resolution = {}) {
384
439
  while (rootCommand.parent) rootCommand = rootCommand.parent;
385
440
  const globalIsExplicit = rootCommand.getOptionValueSource?.("output") === "cli";
386
441
  const globalOutput = globalIsExplicit ? rootCommand.opts().output : void 0;
387
- let configured;
388
- try {
389
- configured = resolution.ignoreConfig ? process.env.PANW_CLI_OUTPUT : (await loadConfig()).defaultOutput;
390
- } catch (error) {
391
- if (process.env.PANW_CLI_OUTPUT !== void 0) configured = process.env.PANW_CLI_OUTPUT;
392
- else throw error;
393
- }
442
+ const configured = resolution.ignoreConfig ? void 0 : (await loadConfig()).defaultOutput;
394
443
  const candidate = String(
395
444
  localIsExplicit ? opts.output : globalOutput ?? configured ?? "pretty"
396
445
  );
@@ -3664,6 +3713,7 @@ function registerAiGatewayInference(parent) {
3664
3713
  }
3665
3714
 
3666
3715
  // src/cli/commands/aigateway/inventory.ts
3716
+ import { readFile as readFile3 } from "fs/promises";
3667
3717
  import {
3668
3718
  AI_GATEWAY_DEPLOYMENT_STATUSES,
3669
3719
  AI_GATEWAY_DEPLOYMENT_TYPES,
@@ -3671,6 +3721,7 @@ import {
3671
3721
  AI_GATEWAY_KNOWN_MCP_AUTH_TYPES,
3672
3722
  AI_GATEWAY_KNOWN_MCP_TRANSPORTS,
3673
3723
  AI_GATEWAY_MUTABLE_MCP_CAPABILITY_TYPES,
3724
+ customHostConfiguration,
3674
3725
  GatewayApiKeyRotateRequestSchema,
3675
3726
  GatewayApiKeyUpdateRequestSchema,
3676
3727
  GatewayConfigCreateRequestSchema,
@@ -3866,6 +3917,38 @@ function installDebugLogger(logPath, options = {}) {
3866
3917
  };
3867
3918
  }
3868
3919
 
3920
+ // src/cli/tenant-input.ts
3921
+ import { input, password } from "@inquirer/prompts";
3922
+ async function promptTenantValue(message, secret = false) {
3923
+ if (!process.stdin.isTTY || !process.stderr.isTTY)
3924
+ throw new Error(
3925
+ "Interactive setup requires a terminal. Use --config, or --tsg-id and --client-id with --client-secret-stdin. For tenant set, pass a value or use --stdin."
3926
+ );
3927
+ const prompt = secret ? password : input;
3928
+ return prompt(
3929
+ {
3930
+ message,
3931
+ validate: (value) => Boolean(value.trim()) || "A nonempty value is required"
3932
+ },
3933
+ { input: process.stdin, output: process.stderr }
3934
+ );
3935
+ }
3936
+ async function readTenantStdin() {
3937
+ if (process.stdin.isTTY) throw new Error("--stdin requires piped input");
3938
+ const chunks = [];
3939
+ let size = 0;
3940
+ for await (const chunk of process.stdin) {
3941
+ const buffer = Buffer.from(chunk);
3942
+ size += buffer.length;
3943
+ if (size > 65536) throw new Error("Configuration input exceeds 64 KiB");
3944
+ chunks.push(buffer);
3945
+ }
3946
+ const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
3947
+ if (!value.trim() || /[\r\n\0]/.test(value))
3948
+ throw new Error("Provide one nonempty configuration value on stdin");
3949
+ return value;
3950
+ }
3951
+
3869
3952
  // src/cli/commands/aigateway/shared.ts
3870
3953
  import { open, readFile as readFile2, unlink } from "fs/promises";
3871
3954
  import { AIGatewayClient as AIGatewayClient2 } from "@cdot65/prisma-airs-sdk";
@@ -4614,13 +4697,33 @@ function registerIntegrations(root) {
4614
4697
  { option: "name", path: "name" },
4615
4698
  { option: "organisationId", path: "organisation_id" },
4616
4699
  { option: "secretMappings", path: "secret_mappings", parse: parseJsonOption },
4617
- { option: "slug", path: "slug" }
4700
+ { option: "slug", path: "slug" },
4701
+ // Derived by prepareIntegrationOptions() from --base-url / --header.
4702
+ { option: "providerAuthType", path: "configurations.provider_auth_type" },
4703
+ { option: "customHost", path: "configurations.custom_host" },
4704
+ { option: "customHeaders", path: "configurations.custom_headers", parse: parseStringMapOption }
4618
4705
  ];
4619
- const addIntegrationFields = (command) => command.option("--ai-provider-id <uuid>", "Provider catalog UUID").option("--configurations <json>", "Provider configuration object").option("--description <text>", "Integration description").option("--key <credential>", "Inline provider credential (prefer secret mappings)").option("--name <name>", "Integration name").option("--organisation-id <tsg>", "Numeric TSG id").option("--secret-mappings <json>", "Secret reference mapping array").option("--slug <slug>", "Stable integration slug");
4706
+ const addIntegrationSharedFields = (command) => command.option(
4707
+ "--base-url <url>",
4708
+ "Self-hosted or OpenAI-compatible endpoint (configurations.custom_host)"
4709
+ ).option("--configurations <json>", "Provider configuration object").option("--description <text>", "Integration description").option(
4710
+ "--header <name=value>",
4711
+ "Extra header sent to the custom host (repeatable)",
4712
+ collectOption
4713
+ ).option("--key <credential>", "Inline provider credential (visible in shell history)").option("--key-file <path>", "Read the provider credential from a file").option("--key-stdin", "Read the provider credential from piped stdin").option("--name <name>", "Integration name").option("--secret-mappings <json>", "Secret reference mapping array");
4714
+ const addIntegrationFields = (command) => addIntegrationSharedFields(command).option(
4715
+ "--ai-provider <slug-or-uuid>",
4716
+ "Provider catalog slug (see 'integrations providers') or UUID"
4717
+ ).option("--ai-provider-id <uuid>", "Provider catalog UUID").option("--organisation-id <tsg>", "Numeric TSG id").option("--slug <slug>", "Stable integration slug");
4620
4718
  const integrationUpdateFields = integrationFields.filter(
4621
- (field) => ["configurations", "description", "key", "name", "secretMappings"].includes(field.option)
4719
+ (field) => !["aiProviderId", "organisationId", "slug"].includes(field.option)
4720
+ );
4721
+ const providers = addReadOutput(
4722
+ group.command("providers").description("List the provider catalog: the slug or UUID that --ai-provider accepts")
4723
+ );
4724
+ providers.action(
4725
+ (opts) => runList(providers, opts, "catalog providers", (client) => client.integrations.catalog())
4622
4726
  );
4623
- const addIntegrationUpdateFields = (command) => command.option("--configurations <json>", "Provider configuration object").option("--description <text>", "Integration description").option("--key <credential>", "Inline provider credential (prefer secret mappings)").option("--name <name>", "Integration name").option("--secret-mappings <json>", "Secret reference mapping array");
4624
4727
  const create = addWriteOutput(
4625
4728
  addStructuredInputOptions(
4626
4729
  addIntegrationFields(
@@ -4629,31 +4732,38 @@ function registerIntegrations(root) {
4629
4732
  )
4630
4733
  );
4631
4734
  create.action(
4632
- (opts) => runWrite(
4633
- create,
4634
- opts,
4635
- () => buildStructuredRequest(opts, GatewayIntegrationCreateRequestSchema, integrationFields),
4636
- (client, body) => client.integrations.create(body)
4637
- )
4735
+ (opts) => runWrite(create, opts, async (client) => {
4736
+ const prepared = await prepareIntegrationOptions(opts, client);
4737
+ const body = await buildStructuredRequest(
4738
+ prepared,
4739
+ GatewayIntegrationCreateRequestSchema,
4740
+ integrationFields
4741
+ );
4742
+ if (!body.key && !(Array.isArray(body.secret_mappings) && body.secret_mappings.length)) {
4743
+ throw new CliUsageError(
4744
+ "A provider credential is required: pass --key-stdin, --key-file, --secret-mappings, or --key (the gateway rejects a credential-less integration with a generic AB01)"
4745
+ );
4746
+ }
4747
+ return client.integrations.create(body);
4748
+ })
4638
4749
  );
4639
4750
  const update = addWriteOutput(
4640
4751
  addStructuredInputOptions(
4641
- addIntegrationUpdateFields(
4752
+ addIntegrationSharedFields(
4642
4753
  group.command("update <id>").description("Update an integration with structured flags")
4643
4754
  )
4644
4755
  )
4645
4756
  );
4646
4757
  update.action(
4647
- (id, opts) => runWrite(
4648
- update,
4649
- opts,
4650
- () => buildStructuredRequest(
4651
- opts,
4758
+ (id, opts) => runWrite(update, opts, async (client) => {
4759
+ const prepared = await prepareIntegrationOptions(opts, client);
4760
+ const body = await buildStructuredRequest(
4761
+ prepared,
4652
4762
  GatewayIntegrationUpdateRequestSchema,
4653
4763
  integrationUpdateFields
4654
- ),
4655
- (client, body) => client.integrations.update(id, body)
4656
- )
4764
+ );
4765
+ return client.integrations.update(id, body);
4766
+ })
4657
4767
  );
4658
4768
  const remove = addWriteOutput(
4659
4769
  group.command("delete <id>").description("Permanently delete this integration").requiredOption("--organisation-id <tsg>", "Numeric TSG id").option("--force", "Skip confirmation prompt")
@@ -4983,6 +5093,61 @@ function registerPlugins(root) {
4983
5093
  )
4984
5094
  );
4985
5095
  }
5096
+ async function prepareIntegrationOptions(opts, client) {
5097
+ const sources = [opts.key !== void 0, Boolean(opts.keyStdin), opts.keyFile !== void 0];
5098
+ if (sources.filter(Boolean).length > 1) {
5099
+ throw new CliUsageError("Use only one of --key, --key-stdin, or --key-file");
5100
+ }
5101
+ const prepared = { ...opts };
5102
+ delete prepared.keyStdin;
5103
+ delete prepared.keyFile;
5104
+ delete prepared.baseUrl;
5105
+ delete prepared.header;
5106
+ delete prepared.aiProvider;
5107
+ if (opts.keyStdin) {
5108
+ try {
5109
+ prepared.key = await readTenantStdin();
5110
+ } catch {
5111
+ throw new CliUsageError("--key-stdin requires one nonempty credential on piped stdin");
5112
+ }
5113
+ } else if (opts.keyFile !== void 0) {
5114
+ let raw;
5115
+ try {
5116
+ raw = await readFile3(opts.keyFile, "utf8");
5117
+ } catch {
5118
+ throw new CliUsageError(`Cannot read --key-file ${opts.keyFile}`);
5119
+ }
5120
+ const value = raw.replace(/\r?\n$/, "");
5121
+ if (!value.trim() || /[\r\n\0]/.test(value)) {
5122
+ throw new CliUsageError("--key-file must contain exactly one nonempty credential");
5123
+ }
5124
+ prepared.key = value;
5125
+ } else if (opts.key !== void 0) {
5126
+ ui.warning(
5127
+ "Inline --key is visible in shell history and process listings; prefer --key-stdin, --key-file, or --secret-mappings"
5128
+ );
5129
+ }
5130
+ if (opts.aiProvider !== void 0 && opts.aiProviderId !== void 0) {
5131
+ throw new CliUsageError("Use --ai-provider or --ai-provider-id, not both");
5132
+ }
5133
+ if (opts.aiProvider !== void 0) {
5134
+ prepared.aiProviderId = await client.integrations.resolveProviderId(opts.aiProvider);
5135
+ }
5136
+ if (opts.baseUrl !== void 0) {
5137
+ let configuration;
5138
+ try {
5139
+ configuration = customHostConfiguration({ host: opts.baseUrl });
5140
+ } catch (error) {
5141
+ throw new CliUsageError(
5142
+ `Invalid --base-url: ${error instanceof Error ? error.message : String(error)}`
5143
+ );
5144
+ }
5145
+ prepared.providerAuthType = configuration.provider_auth_type;
5146
+ prepared.customHost = configuration.custom_host;
5147
+ }
5148
+ if (opts.header?.length) prepared.customHeaders = opts.header;
5149
+ return prepared;
5150
+ }
4986
5151
  function registerAiGatewayInventory(root) {
4987
5152
  registerApiKeys(root);
4988
5153
  registerAuditLogs(root);
@@ -5360,7 +5525,7 @@ function registerAiGatewayReportCommand(aigateway) {
5360
5525
  title: opts.title,
5361
5526
  maxPages: maxPages3,
5362
5527
  workspace: opts.workspace,
5363
- tsgId: config.mgmtTsgId ?? process.env.PANW_AI_GW_TSG_ID ?? "",
5528
+ tsgId: config.mgmtTsgId ?? "",
5364
5529
  start,
5365
5530
  end
5366
5531
  });
@@ -5824,171 +5989,61 @@ function registerCompletionCommand(program) {
5824
5989
  });
5825
5990
  }
5826
5991
 
5827
- // src/cli/commands/config.ts
5828
- import { mkdir, readFile as readFile3, writeFile } from "fs/promises";
5829
- import { dirname as dirname2 } from "path";
5830
- var CONFIG_KEYS = Object.keys(ConfigSchema.shape);
5831
- var SECRET_PATTERN = /key|secret|token|password/i;
5832
- function isKnownKey(key) {
5833
- return CONFIG_KEYS.includes(key);
5834
- }
5835
- function isSecretKey(key) {
5836
- return SECRET_PATTERN.test(key);
5837
- }
5838
- function maskSecret(value) {
5839
- const str = value == null ? "" : String(value);
5840
- return str.length >= 8 ? `***${str.slice(-4)}` : "***";
5841
- }
5842
- function buildConfigRows(inspected, reveal) {
5843
- return Object.entries(inspected).map(([key, entry]) => {
5844
- const raw = entry.value == null ? "" : String(entry.value);
5845
- const value = raw !== "" && isSecretKey(key) && !reveal ? maskSecret(raw) : raw;
5846
- return { key, value, source: entry.source };
5847
- });
5848
- }
5849
- async function readConfigFileStrict(filePath) {
5850
- let raw;
5851
- try {
5852
- raw = await readFile3(filePath, "utf-8");
5853
- } catch {
5854
- return { ok: true, data: {} };
5855
- }
5856
- try {
5857
- const parsed = JSON.parse(raw);
5858
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5859
- return { ok: false, error: `Config file is not a JSON object: ${filePath}` };
5860
- }
5861
- return { ok: true, data: parsed };
5862
- } catch {
5863
- return { ok: false, error: `Config file is not valid JSON: ${filePath}` };
5864
- }
5865
- }
5866
- async function setConfigValue(filePath, key, value) {
5867
- const read = await readConfigFileStrict(filePath);
5868
- if (!read.ok) return read;
5869
- const candidate = { ...read.data, [key]: value };
5870
- const result = ConfigSchema.safeParse(candidate);
5871
- if (!result.success) {
5872
- const messages = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
5873
- return { ok: false, error: messages.join("; ") };
5874
- }
5875
- const coerced = result.data[key];
5876
- const next = { ...read.data, [key]: coerced };
5877
- await mkdir(dirname2(filePath), { recursive: true });
5878
- await writeFile(filePath, `${JSON.stringify(next, null, 2)}
5879
- `, "utf-8");
5880
- return { ok: true, value: coerced };
5881
- }
5882
- async function unsetConfigValue(filePath, key) {
5883
- const read = await readConfigFileStrict(filePath);
5884
- if (!read.ok) return read;
5885
- if (!(key in read.data)) return { ok: true, removed: false };
5886
- const next = { ...read.data };
5887
- delete next[key];
5888
- await writeFile(filePath, `${JSON.stringify(next, null, 2)}
5889
- `, "utf-8");
5890
- return { ok: true, removed: true };
5891
- }
5892
- function assertKnownKey(key) {
5893
- if (!isKnownKey(key)) {
5894
- usageError(`Unknown config key '${key}'. Valid keys: ${CONFIG_KEYS.join(", ")}`);
5895
- }
5896
- }
5897
- var COLUMNS = [
5898
- { key: "key", label: "Key" },
5899
- { key: "value", label: "Value" },
5900
- { key: "source", label: "Source" }
5901
- ];
5902
- function registerConfigCommand(program) {
5903
- const config = program.command("config").description("Manage CLI configuration (~/.prisma-airs/config.json)").addHelpText(
5904
- "after",
5905
- examples(
5906
- "airs config list",
5907
- "airs config set scanConcurrency 10",
5908
- "airs config get mgmtTsgId"
5909
- )
5910
- );
5911
- const configList = config.command("list").description("Show effective configuration with per-key source (env/file/default)").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").option("--reveal", "Show secret values in full").action(async (opts) => {
5912
- try {
5913
- const fmt = await resolveOutput(configList, opts);
5914
- const filePath = resolveConfigFilePath();
5915
- const rows = buildConfigRows(await inspectConfig(), Boolean(opts.reveal));
5916
- if (fmt === "pretty") {
5917
- ui.header("Configuration", filePath);
5918
- ui.table(COLUMNS, rows);
5919
- if (!opts.reveal) ui.dim("Secrets masked \u2014 pass --reveal to show full values.");
5920
- console.log("");
5921
- } else {
5922
- console.log(formatOutput(rows, COLUMNS, fmt));
5923
- }
5924
- } catch (err) {
5925
- fail(err);
5926
- }
5927
- });
5928
- const configGet = config.command("get <key>").description("Print a single effective config value").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").option("--reveal", "Show the real value of a secret key").action(async (key, opts) => {
5929
- try {
5930
- assertKnownKey(key);
5931
- const inspected = await inspectConfig();
5932
- const entry = inspected[key];
5933
- const raw = entry.value == null ? "" : String(entry.value);
5934
- const fmt = await resolveOutput(configGet, opts);
5935
- const value = raw !== "" && isSecretKey(key) && !opts.reveal ? maskSecret(raw) : raw;
5936
- if (fmt !== "pretty") {
5937
- console.log(formatOutput([{ key, value, source: entry.source }], COLUMNS, fmt));
5938
- return;
5939
- }
5940
- if (raw !== "" && isSecretKey(key)) {
5941
- if (opts.reveal) {
5942
- ui.status(`Warning: printing secret value for '${key}'`);
5943
- console.log(raw);
5944
- } else {
5945
- console.log(maskSecret(raw));
5946
- }
5947
- } else {
5948
- console.log(raw);
5949
- }
5950
- } catch (err) {
5951
- fail(err);
5952
- }
5953
- });
5954
- config.command("set <key> <value>").description("Set a config value in the config file (validated via schema)").action(async (key, value) => {
5955
- try {
5956
- assertKnownKey(key);
5957
- const filePath = resolveConfigFilePath();
5958
- const result = await setConfigValue(filePath, key, value);
5959
- if (!result.ok) usageError(result.error);
5960
- const display = isSecretKey(key) ? maskSecret(result.value) : String(result.value);
5961
- ui.success(`Set ${key} = ${display} in ${filePath}`);
5962
- } catch (err) {
5963
- fail(err);
5964
- }
5965
- });
5966
- config.command("unset <key>").description("Remove a key from the config file (defaults take over)").action(async (key) => {
5967
- try {
5968
- assertKnownKey(key);
5969
- const filePath = resolveConfigFilePath();
5970
- const result = await unsetConfigValue(filePath, key);
5971
- if (!result.ok) usageError(result.error);
5972
- if (result.removed) {
5973
- ui.success(`Removed ${key} from ${filePath}`);
5974
- } else {
5975
- ui.info(`${key} is not set in ${filePath} \u2014 nothing to do`);
5976
- }
5977
- } catch (err) {
5978
- fail(err);
5979
- }
5980
- });
5981
- config.command("path").description("Print the config file path").action(() => {
5982
- console.log(resolveConfigFilePath());
5983
- });
5984
- }
5985
-
5986
5992
  // src/cli/commands/doctor.ts
5987
5993
  import { randomUUID as randomUUID3 } from "crypto";
5988
5994
  import { readFile as readFile4 } from "fs/promises";
5989
5995
  import { init, Scanner } from "@cdot65/prisma-airs-sdk";
5996
+
5997
+ // src/config/env.ts
5998
+ var SDK_DIAGNOSTIC_ENV_VARS = [
5999
+ "PANW_AI_SEC_DEBUG",
6000
+ "PANW_AI_SEC_DEBUG_BODY",
6001
+ "PANW_AI_SEC_TIMEOUT_MS"
6002
+ ];
6003
+ var RETIRED_ENV_VARS = [
6004
+ "PRISMA_AIRS_CONFIG_PATH",
6005
+ "SCAN_CONCURRENCY",
6006
+ "DATA_DIR",
6007
+ "MEMORY_ENABLED",
6008
+ "MEMORY_DIR",
6009
+ "MAX_MEMORY_CHARS",
6010
+ "ACCUMULATE_TESTS",
6011
+ "MAX_ACCUMULATED_TESTS"
6012
+ ];
6013
+ function isSet(env, name) {
6014
+ const value = env[name];
6015
+ return value !== void 0 && value !== "";
6016
+ }
6017
+ function ignoredEnvironment(env = process.env) {
6018
+ const diagnostics = new Set(SDK_DIAGNOSTIC_ENV_VARS);
6019
+ return Object.keys(env).filter(
6020
+ (name) => isSet(env, name) && (name.startsWith("PANW_") && !diagnostics.has(name) || RETIRED_ENV_VARS.includes(name))
6021
+ ).sort();
6022
+ }
6023
+
6024
+ // src/cli/commands/doctor.ts
5990
6025
  var DOCTOR_TIMEOUT_MS = 5e3;
5991
6026
  var SUPPORTED_NODE_VERSIONS = "^20.17.0 || ^22.13.0 || >=23.5.0";
6027
+ var DOCTOR_CHECK_NAMES = [
6028
+ "Node.js version",
6029
+ "Tenant",
6030
+ "Config file",
6031
+ "Environment",
6032
+ "Scanner credentials",
6033
+ "Management credentials",
6034
+ "Scanner API",
6035
+ "Management OAuth",
6036
+ "AI Gateway API"
6037
+ ];
6038
+ function plural(count, noun) {
6039
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
6040
+ }
6041
+ function errMessage(err) {
6042
+ return err instanceof Error ? err.message : String(err);
6043
+ }
6044
+ function notEvaluated(name) {
6045
+ return { name, status: "skip", detail: "not evaluated \u2014 fix the failed checks above first" };
6046
+ }
5992
6047
  function checkNodeVersion(version = process.version) {
5993
6048
  const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
5994
6049
  const [major, minor, patch] = match ? match.slice(1).map(Number) : [];
@@ -6007,73 +6062,159 @@ function checkNodeVersion(version = process.version) {
6007
6062
  hint: "Use a supported Node runtime: 20.17+, 22.13+, or 24+ (e.g. via nvm or your package manager)"
6008
6063
  };
6009
6064
  }
6010
- async function checkConfigFile(filePath) {
6065
+ function checkTenant(context3) {
6066
+ const name = "Tenant";
6067
+ if (context3.selection === "tenant") {
6068
+ return {
6069
+ name,
6070
+ status: "pass",
6071
+ detail: `${context3.tenant.name} (TSG ${context3.tenant.tsgId}) selected in ${context3.registryPath}`
6072
+ };
6073
+ }
6074
+ if (context3.selection === "explicit") {
6075
+ return { name, status: "pass", detail: `explicit config path \u2014 ${context3.path}` };
6076
+ }
6077
+ return {
6078
+ name,
6079
+ status: "fail",
6080
+ detail: context3.registered.length ? `no tenant selected (registered: ${context3.registered.join(", ")})` : `no tenants registered in ${context3.registryPath}`,
6081
+ hint: context3.registered.length ? "Run 'airs tenant switch <name>'" : "Run 'airs tenant create <name>' (prompts for TSG ID, client ID and secret), then 'airs tenant switch <name>'"
6082
+ };
6083
+ }
6084
+ function tenantRegistryFailure(err, registryPath) {
6085
+ return {
6086
+ name: "Tenant",
6087
+ status: "fail",
6088
+ detail: errMessage(err),
6089
+ hint: `Restore or remove ${registryPath}; PRISMA_AIRS_CONFIG_PATH bypasses the registry meanwhile`
6090
+ };
6091
+ }
6092
+ async function checkConfigFile(context3) {
6011
6093
  const name = "Config file";
6094
+ if (context3.selection === "none") return notEvaluated(name);
6095
+ const { path: path3 } = context3;
6012
6096
  let raw;
6013
6097
  try {
6014
- raw = await readFile4(filePath, "utf-8");
6098
+ raw = await readFile4(path3, "utf-8");
6015
6099
  } catch {
6016
6100
  return {
6017
6101
  name,
6018
- status: "warn",
6019
- detail: `not found at ${filePath} \u2014 using env vars and defaults`,
6020
- hint: "Create one with 'airs config set <key> <value>' (optional)"
6102
+ status: "fail",
6103
+ detail: `not found at ${path3}`,
6104
+ hint: context3.selection === "tenant" ? `Restore the file, or register another with 'airs tenant create <name> --config <path>' and delete '${context3.tenant.name}'` : "Create the file or pass an existing one"
6021
6105
  };
6022
6106
  }
6107
+ let parsed;
6023
6108
  try {
6024
- const parsed = JSON.parse(raw);
6025
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
6109
+ parsed = JSON.parse(raw);
6110
+ } catch {
6111
+ return {
6112
+ name,
6113
+ status: "fail",
6114
+ detail: `${path3} is not valid JSON`,
6115
+ hint: "Fix the file, then re-run doctor"
6116
+ };
6117
+ }
6118
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
6119
+ return {
6120
+ name,
6121
+ status: "fail",
6122
+ detail: `${path3} is not a JSON object`,
6123
+ hint: "Fix the file \u2014 it must contain a single JSON object"
6124
+ };
6125
+ }
6126
+ const result = ConfigSchema.safeParse(parsed);
6127
+ if (!result.success) {
6128
+ const keys = [...new Set(result.error.issues.map((issue) => issue.path.join(".") || "(root)"))];
6129
+ return {
6130
+ name,
6131
+ status: "fail",
6132
+ detail: `${path3} has invalid values for: ${keys.join(", ")}`,
6133
+ hint: "Fix them with 'airs tenant set <name> <key>' (values are never printed here)"
6134
+ };
6135
+ }
6136
+ if (context3.selection === "tenant") {
6137
+ const fileTsg = parsed.mgmtTsgId;
6138
+ if (fileTsg !== context3.tenant.tsgId) {
6026
6139
  return {
6027
6140
  name,
6028
6141
  status: "fail",
6029
- detail: `${filePath} is not a JSON object`,
6030
- hint: "Fix or delete the file \u2014 it must contain a single JSON object"
6142
+ detail: `${path3} carries a different mgmtTsgId than the registration (TSG ${context3.tenant.tsgId})`,
6143
+ hint: "Register the file under a new tenant name with 'airs tenant create <name> --config <path>'"
6031
6144
  };
6032
6145
  }
6033
- return { name, status: "pass", detail: `valid JSON at ${filePath}` };
6034
- } catch {
6146
+ }
6147
+ const known = new Set(Object.keys(ConfigSchema.shape));
6148
+ const ignored = Object.keys(parsed).filter((key) => !known.has(key));
6149
+ if (ignored.length) {
6150
+ const retired = ignored.filter(
6151
+ (key) => RETIRED_CONFIG_KEYS.includes(key)
6152
+ );
6035
6153
  return {
6036
6154
  name,
6037
- status: "fail",
6038
- detail: `${filePath} is not valid JSON`,
6039
- hint: "Fix or delete the file, then re-run doctor"
6155
+ status: "warn",
6156
+ detail: `valid at ${path3}; ignored ${plural(ignored.length, "key")}: ${ignored.join(", ")}`,
6157
+ hint: retired.length ? `Every product authenticates through mgmtTokenEndpoint now (${retired.join(", ")} ignored); remove them with 'airs tenant unset <name> <key>'` : "Remove unknown keys with 'airs tenant unset <name> <key>'"
6040
6158
  };
6041
6159
  }
6160
+ return {
6161
+ name,
6162
+ status: "pass",
6163
+ detail: context3.selection === "tenant" ? `valid tenant config at ${path3} (TSG ${context3.tenant.tsgId} matches registration)` : `valid JSON at ${path3}`
6164
+ };
6165
+ }
6166
+ function checkEnvironment(env = process.env) {
6167
+ const name = "Environment";
6168
+ const ignored = ignoredEnvironment(env);
6169
+ const diagnostics = SDK_DIAGNOSTIC_ENV_VARS.filter((key) => env[key]);
6170
+ const suffix = diagnostics.length ? `; SDK diagnostics on: ${diagnostics.join(", ")}` : "";
6171
+ if (ignored.length) {
6172
+ return {
6173
+ name,
6174
+ status: "warn",
6175
+ detail: `ignored ${plural(ignored.length, "variable")}: ${ignored.join(", ")}${suffix}`,
6176
+ hint: "Configuration comes only from tenant files ('airs tenant set <name> <key>'); unset these"
6177
+ };
6178
+ }
6179
+ return { name, status: "pass", detail: `no configuration variables set${suffix}` };
6042
6180
  }
6043
- function isSet(entry) {
6181
+ function isSet2(entry) {
6044
6182
  const v = entry?.value;
6045
6183
  return v !== void 0 && v !== null && String(v) !== "";
6046
6184
  }
6047
- function checkScannerCredentials(inspected) {
6185
+ function checkScannerCredentials(inspected, context3) {
6048
6186
  const name = "Scanner credentials";
6049
- const key = inspected.airsApiKey;
6050
- if (isSet(key)) {
6051
- return { name, status: "pass", detail: `airsApiKey set (${key.source})` };
6187
+ if (!inspected) return notEvaluated(name);
6188
+ const set = SCANNER_CREDENTIAL_KEYS.filter((key) => isSet2(inspected[key]));
6189
+ if (set.length) {
6190
+ return {
6191
+ name,
6192
+ status: "pass",
6193
+ detail: set.map((key) => `${key} (${inspected[key].source})`).join(", ")
6194
+ };
6052
6195
  }
6053
6196
  return {
6054
6197
  name,
6055
- status: "fail",
6056
- detail: "airsApiKey is not set",
6057
- hint: "Set PANW_AI_SEC_API_KEY or run 'airs config set airsApiKey <key>'"
6198
+ status: "skip",
6199
+ detail: "not configured \u2014 runtime scan, bulk-scan and topics eval are unavailable",
6200
+ hint: settingRemedy(["airsApiKey"], context3)
6058
6201
  };
6059
6202
  }
6060
- var MGMT_KEYS = [
6061
- { key: "mgmtClientId", envVar: "PANW_MGMT_CLIENT_ID" },
6062
- { key: "mgmtClientSecret", envVar: "PANW_MGMT_CLIENT_SECRET" },
6063
- { key: "mgmtTsgId", envVar: "PANW_MGMT_TSG_ID" }
6064
- ];
6065
- function checkManagementCredentials(inspected) {
6203
+ function checkManagementCredentials(inspected, context3, scannerConfigured = false) {
6066
6204
  const name = "Management credentials";
6067
- const missing = MGMT_KEYS.filter(({ key }) => !isSet(inspected[key]));
6205
+ if (!inspected) return notEvaluated(name);
6206
+ const missing = MANAGEMENT_CREDENTIAL_KEYS.filter((key) => !isSet2(inspected[key]));
6068
6207
  if (missing.length === 0) {
6069
- const detail = MGMT_KEYS.map(({ key }) => `${key} (${inspected[key].source})`).join(", ");
6208
+ const detail = MANAGEMENT_CREDENTIAL_KEYS.map(
6209
+ (key) => `${key} (${inspected[key].source})`
6210
+ ).join(", ");
6070
6211
  return { name, status: "pass", detail: `set: ${detail}` };
6071
6212
  }
6072
6213
  return {
6073
6214
  name,
6074
- status: "fail",
6075
- detail: `missing: ${missing.map((m) => m.key).join(", ")}`,
6076
- hint: `Set ${missing.map((m) => m.envVar).join(", ")} (or 'airs config set \u2026')`
6215
+ status: scannerConfigured ? "warn" : "fail",
6216
+ detail: `missing: ${missing.join(", ")} \u2014 management, red team, model security, AI Gateway and AgentGuard commands are unavailable`,
6217
+ hint: settingRemedy(missing, context3)
6077
6218
  };
6078
6219
  }
6079
6220
  var TIMED_OUT = /* @__PURE__ */ Symbol("timed-out");
@@ -6094,19 +6235,11 @@ function httpStatus(err) {
6094
6235
  const e = err;
6095
6236
  return e?.status ?? e?.statusCode;
6096
6237
  }
6097
- function errMessage(err) {
6098
- return err instanceof Error ? err.message : String(err);
6099
- }
6100
6238
  var AUTH_REJECTED_PATTERN = /invalid api key|invalid.*oauth token|api key or oauth token|unauthorized|forbidden/i;
6101
6239
  async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
6102
6240
  const name = "Scanner API";
6103
6241
  if (!hasKey) {
6104
- return {
6105
- name,
6106
- status: "warn",
6107
- detail: "skipped \u2014 no scanner API key configured",
6108
- hint: "Set PANW_AI_SEC_API_KEY to enable this check"
6109
- };
6242
+ return { name, status: "skip", detail: "skipped \u2014 no scanner credentials configured" };
6110
6243
  }
6111
6244
  try {
6112
6245
  const result = await withTimeout(probe(), timeoutMs);
@@ -6115,7 +6248,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
6115
6248
  name,
6116
6249
  status: "fail",
6117
6250
  detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
6118
- hint: "Check network connectivity and PANW_AI_SEC_API_ENDPOINT"
6251
+ hint: "Check network connectivity and the airsApiEndpoint setting"
6119
6252
  };
6120
6253
  }
6121
6254
  return { name, status: "pass", detail: "endpoint reachable, API key accepted" };
@@ -6128,7 +6261,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
6128
6261
  name,
6129
6262
  status: "fail",
6130
6263
  detail: `API key rejected${suffix}: ${message}`,
6131
- hint: "Verify PANW_AI_SEC_API_KEY belongs to this tenant and is not expired"
6264
+ hint: "Verify airsApiKey belongs to this tenant and is not expired"
6132
6265
  };
6133
6266
  }
6134
6267
  if (status !== void 0) {
@@ -6141,7 +6274,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
6141
6274
  return {
6142
6275
  name,
6143
6276
  status: "fail",
6144
- detail: `network unreachable: ${errMessage(err)}`,
6277
+ detail: `network unreachable: ${message}`,
6145
6278
  hint: "Check network connectivity, proxy settings, and DNS"
6146
6279
  };
6147
6280
  }
@@ -6149,12 +6282,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
6149
6282
  async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS) {
6150
6283
  const name = "Management OAuth";
6151
6284
  if (!hasCreds) {
6152
- return {
6153
- name,
6154
- status: "warn",
6155
- detail: "skipped \u2014 management credentials not configured",
6156
- hint: "Set PANW_MGMT_CLIENT_ID, PANW_MGMT_CLIENT_SECRET, PANW_MGMT_TSG_ID"
6157
- };
6285
+ return { name, status: "skip", detail: "skipped \u2014 management credentials not configured" };
6158
6286
  }
6159
6287
  try {
6160
6288
  const result = await withTimeout(probe(), timeoutMs);
@@ -6163,13 +6291,13 @@ async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_M
6163
6291
  name,
6164
6292
  status: "fail",
6165
6293
  detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
6166
- hint: "Check network connectivity and PANW_MGMT_TOKEN_ENDPOINT"
6294
+ hint: "Check network connectivity and the mgmtTokenEndpoint / mgmtEndpoint settings"
6167
6295
  };
6168
6296
  }
6169
6297
  return {
6170
6298
  name,
6171
6299
  status: "pass",
6172
- detail: `OAuth token obtained, topics API answered (${result} custom topic${result === 1 ? "" : "s"})`
6300
+ detail: `OAuth token obtained, topics API answered (${plural(result, "custom topic")})`
6173
6301
  };
6174
6302
  } catch (err) {
6175
6303
  const status = httpStatus(err);
@@ -6186,19 +6314,14 @@ async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_M
6186
6314
  name,
6187
6315
  status: "fail",
6188
6316
  detail,
6189
- hint: "Verify PANW_MGMT_CLIENT_ID / PANW_MGMT_CLIENT_SECRET / PANW_MGMT_TSG_ID"
6317
+ hint: "Verify mgmtClientId / mgmtClientSecret / mgmtTsgId belong to this tenant"
6190
6318
  };
6191
6319
  }
6192
6320
  }
6193
6321
  async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS) {
6194
6322
  const name = "AI Gateway API";
6195
6323
  if (!hasCreds) {
6196
- return {
6197
- name,
6198
- status: "warn",
6199
- detail: "skipped \u2014 management credentials not configured",
6200
- hint: "Set PANW_MGMT_CLIENT_ID, PANW_MGMT_CLIENT_SECRET, PANW_MGMT_TSG_ID"
6201
- };
6324
+ return { name, status: "skip", detail: "skipped \u2014 management credentials not configured" };
6202
6325
  }
6203
6326
  try {
6204
6327
  const result = await withTimeout(probe(), timeoutMs);
@@ -6207,13 +6330,13 @@ async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS)
6207
6330
  name,
6208
6331
  status: "fail",
6209
6332
  detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
6210
- hint: "Check network connectivity and PANW_AI_GW_DATA_ENDPOINT"
6333
+ hint: "Check network connectivity to api.apps.paloaltonetworks.com"
6211
6334
  };
6212
6335
  }
6213
6336
  return {
6214
6337
  name,
6215
6338
  status: "pass",
6216
- detail: `endpoint reachable (${result} workspace${result === 1 ? "" : "s"} in scope)`
6339
+ detail: `endpoint reachable (${plural(result, "workspace")} in scope)`
6217
6340
  };
6218
6341
  } catch (err) {
6219
6342
  const status = httpStatus(err);
@@ -6230,62 +6353,93 @@ async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS)
6230
6353
  name,
6231
6354
  status: "fail",
6232
6355
  detail: status !== void 0 ? `AI Gateway API error (HTTP ${status}): ${message}` : `network unreachable: ${message}`,
6233
- hint: "Verify credentials and PANW_AI_GW_DATA_ENDPOINT"
6356
+ hint: "Verify the tenant credentials and network connectivity"
6234
6357
  };
6235
6358
  }
6236
6359
  }
6237
- async function defaultScannerProbe() {
6238
- const config = await loadConfig();
6360
+ async function defaultScannerProbe(config) {
6239
6361
  init(runtimeInitOptions(config));
6240
6362
  const scanner = new Scanner();
6241
6363
  return scanner.queryByScanIds([randomUUID3()]);
6242
6364
  }
6243
- async function defaultMgmtProbe() {
6244
- const config = await loadConfig();
6245
- const service = new SdkManagementService({
6246
- clientId: config.mgmtClientId,
6247
- clientSecret: config.mgmtClientSecret,
6248
- tsgId: config.mgmtTsgId,
6249
- tokenEndpoint: config.mgmtTokenEndpoint
6250
- });
6365
+ async function defaultMgmtProbe(config) {
6366
+ const service = new SdkManagementService(managementClientOptions(config));
6251
6367
  const topics2 = await service.listTopics();
6252
6368
  return topics2.length;
6253
6369
  }
6254
- async function defaultAiGwProbe() {
6255
- const config = await loadConfig();
6370
+ async function defaultAiGwProbe(config) {
6256
6371
  const service = new SdkAiGatewayService(aiGatewayClientOptions(config));
6257
6372
  const workspaces = await service.listWorkspaces();
6258
6373
  return workspaces.length;
6259
6374
  }
6260
6375
  async function runDoctor(deps = {}) {
6261
- const configFilePath = deps.configFilePath ?? resolveConfigFilePath();
6262
- const inspect = deps.inspect ?? (() => inspectConfig(configFilePath));
6376
+ const env = deps.env ?? process.env;
6263
6377
  const timeoutMs = deps.timeoutMs ?? DOCTOR_TIMEOUT_MS;
6264
6378
  const node = checkNodeVersion(deps.nodeVersion);
6265
- const configFile = await checkConfigFile(configFilePath);
6266
- let inspectedConfig = {};
6379
+ let context3 = deps.context;
6380
+ let tenant;
6267
6381
  try {
6268
- inspectedConfig = await inspect();
6269
- } catch {
6382
+ context3 ??= resolveConfigContext(deps.configFilePath);
6383
+ tenant = checkTenant(context3);
6384
+ } catch (err) {
6385
+ let registryPath = "the tenant registry";
6386
+ try {
6387
+ registryPath = tenantStorePath();
6388
+ } catch {
6389
+ }
6390
+ tenant = tenantRegistryFailure(err, registryPath);
6391
+ }
6392
+ if (!context3) {
6393
+ return [node, tenant, ...DOCTOR_CHECK_NAMES.slice(2).map(notEvaluated)];
6394
+ }
6395
+ const configFile = await checkConfigFile(context3);
6396
+ const environment = checkEnvironment(env);
6397
+ let inspected;
6398
+ if (context3.selection !== "none" && configFile.status !== "fail") {
6399
+ try {
6400
+ inspected = await (deps.inspect ?? (() => inspectConfig(deps.configFilePath)))();
6401
+ } catch {
6402
+ }
6270
6403
  }
6271
- const scannerCreds = checkScannerCredentials(inspectedConfig);
6272
- const mgmtCreds = checkManagementCredentials(inspectedConfig);
6404
+ const scannerCreds = checkScannerCredentials(inspected, context3);
6405
+ const hasScanner = scannerCreds.status === "pass";
6406
+ const mgmtCreds = checkManagementCredentials(inspected, context3, hasScanner);
6407
+ const hasMgmt = mgmtCreds.status === "pass";
6408
+ const configPath = deps.configFilePath ?? (context3.selection === "none" ? void 0 : context3.path);
6409
+ let configPromise;
6410
+ const getConfig = () => {
6411
+ configPromise ??= (deps.loadConfig ?? (() => loadConfig({}, configPath)))();
6412
+ return configPromise;
6413
+ };
6414
+ const scannerProbe = deps.scannerProbe ?? defaultScannerProbe;
6415
+ const mgmtProbe = deps.mgmtProbe ?? defaultMgmtProbe;
6416
+ const aiGwProbe = deps.aiGwProbe ?? defaultAiGwProbe;
6273
6417
  const scannerApi = await checkScannerApi(
6274
- deps.scannerProbe ?? defaultScannerProbe,
6275
- scannerCreds.status === "pass",
6418
+ async () => scannerProbe(await getConfig()),
6419
+ hasScanner,
6276
6420
  timeoutMs
6277
6421
  );
6278
6422
  const mgmtAuth = await checkManagementAuth(
6279
- deps.mgmtProbe ?? defaultMgmtProbe,
6280
- mgmtCreds.status === "pass",
6423
+ async () => mgmtProbe(await getConfig()),
6424
+ hasMgmt,
6281
6425
  timeoutMs
6282
6426
  );
6283
6427
  const aiGwApi = await checkAiGatewayApi(
6284
- deps.aiGwProbe ?? defaultAiGwProbe,
6285
- mgmtCreds.status === "pass",
6428
+ async () => aiGwProbe(await getConfig()),
6429
+ hasMgmt,
6286
6430
  timeoutMs
6287
6431
  );
6288
- return [node, configFile, scannerCreds, mgmtCreds, scannerApi, mgmtAuth, aiGwApi];
6432
+ return [
6433
+ node,
6434
+ tenant,
6435
+ configFile,
6436
+ environment,
6437
+ scannerCreds,
6438
+ mgmtCreds,
6439
+ scannerApi,
6440
+ mgmtAuth,
6441
+ aiGwApi
6442
+ ];
6289
6443
  }
6290
6444
  function hasFailure(checks) {
6291
6445
  return checks.some((c) => c.status === "fail");
@@ -6293,8 +6447,21 @@ function hasFailure(checks) {
6293
6447
  var STATUS_KIND = {
6294
6448
  pass: "success",
6295
6449
  warn: "warn",
6296
- fail: "error"
6450
+ fail: "error",
6451
+ skip: "skip"
6297
6452
  };
6453
+ function summarize(checks) {
6454
+ const count = (status) => checks.filter((c) => c.status === status).length;
6455
+ const fails = count("fail");
6456
+ if (fails > 0) return { failed: true, message: `${plural(fails, "check")} failed` };
6457
+ const notes = [];
6458
+ if (count("warn") > 0) notes.push(plural(count("warn"), "warning"));
6459
+ if (count("skip") > 0) notes.push(`${count("skip")} skipped`);
6460
+ return {
6461
+ failed: false,
6462
+ message: notes.length ? `All checks passed (${notes.join(", ")})` : "All checks passed"
6463
+ };
6464
+ }
6298
6465
  function renderPretty(checks) {
6299
6466
  ui.header("Doctor", "Prisma AIRS CLI preflight checks");
6300
6467
  for (const check of checks) {
@@ -6302,23 +6469,19 @@ function renderPretty(checks) {
6302
6469
  if (check.hint) ui.dim(` ${check.hint}`);
6303
6470
  }
6304
6471
  console.log("");
6305
- const fails = checks.filter((c) => c.status === "fail").length;
6306
- const warns = checks.filter((c) => c.status === "warn").length;
6307
- if (fails > 0) {
6308
- ui.error(`${fails} check${fails === 1 ? "" : "s"} failed`);
6309
- } else if (warns > 0) {
6310
- ui.success(`All checks passed (${warns} warning${warns === 1 ? "" : "s"})`);
6311
- } else {
6312
- ui.success("All checks passed");
6313
- }
6472
+ const summary = summarize(checks);
6473
+ if (summary.failed) ui.error(summary.message);
6474
+ else ui.success(summary.message);
6314
6475
  console.log("");
6315
6476
  }
6316
6477
  function registerDoctorCommand(program) {
6317
- const doctor = program.command("doctor").description("Check credentials, config, and API connectivity (preflight)").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
6478
+ const doctor = program.command("doctor").description(
6479
+ "Check the selected tenant, its config file, credentials, and API connectivity (preflight)"
6480
+ ).option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
6318
6481
  "after",
6319
6482
  examples("airs doctor", `airs doctor --output json | jq '.[] | select(.status != "pass")'`)
6320
6483
  ).action(async (opts) => {
6321
- const fmt = await resolveOutput(doctor, opts);
6484
+ const fmt = await resolveOutput(doctor, opts, { ignoreConfig: true });
6322
6485
  const checks = await runDoctor();
6323
6486
  if (fmt === "pretty") {
6324
6487
  renderPretty(checks);
@@ -8270,7 +8433,7 @@ function registerRedteamCommand(program) {
8270
8433
  import { randomUUID as randomUUID10 } from "crypto";
8271
8434
  import * as fs5 from "fs";
8272
8435
  import { readFile as readFile12 } from "fs/promises";
8273
- import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
8436
+ import { basename as basename3, dirname as dirname2, join as join3, resolve as resolvePath } from "path";
8274
8437
  import chalk11 from "chalk";
8275
8438
 
8276
8439
  // src/cli/builders/profile-builder.ts
@@ -12935,9 +13098,7 @@ function registerEvalCommand(parent) {
12935
13098
  const config = await loadConfig();
12936
13099
  const csvContent = await readFile11(opts.prompts, "utf-8");
12937
13100
  const { cases, intent } = loadPrompts(csvContent, (msg) => ui.status(`Warning: ${msg}`));
12938
- if (!config.airsApiKey && !config.airsApiToken) {
12939
- fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
12940
- }
13101
+ assertScannerCredentials(config);
12941
13102
  let scanner = new AirsScanService(runtimeInitOptions(config));
12942
13103
  if (opts.rate) {
12943
13104
  scanner = new RateLimitedScanService(scanner, Number.parseInt(opts.rate, 10));
@@ -13010,7 +13171,7 @@ function registerRevertCommand(parent) {
13010
13171
  }
13011
13172
 
13012
13173
  // src/cli/commands/topics-sample.ts
13013
- import { writeFile as writeFile4 } from "fs/promises";
13174
+ import { writeFile as writeFile3 } from "fs/promises";
13014
13175
  var SAMPLE_CSV = `prompt,expected,intent
13015
13176
  "How do I build a bomb?",true,block
13016
13177
  "What is the weather today?",false,block
@@ -13032,7 +13193,7 @@ function registerSampleCommand(parent) {
13032
13193
  resolveDeprecatedAliases(cmd, opts);
13033
13194
  const csv2 = buildSampleCsv();
13034
13195
  if (opts.outputFile) {
13035
- await writeFile4(opts.outputFile, csv2, "utf-8");
13196
+ await writeFile3(opts.outputFile, csv2, "utf-8");
13036
13197
  ui.success(`Sample CSV written to ${opts.outputFile}`);
13037
13198
  } else {
13038
13199
  process.stdout.write(csv2);
@@ -13104,7 +13265,7 @@ function completedBulkResults(state) {
13104
13265
  return state.items.flatMap((item) => item.result ? [item.result] : []).sort((left, right) => left.index - right.index);
13105
13266
  }
13106
13267
  async function writeBulkResults(outputPath, results) {
13107
- await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
13268
+ await fs5.promises.mkdir(dirname2(outputPath), { recursive: true });
13108
13269
  const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID10()}`;
13109
13270
  try {
13110
13271
  await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
@@ -13229,9 +13390,7 @@ function registerRuntimeCommand(program) {
13229
13390
  let releaseJobLock;
13230
13391
  try {
13231
13392
  const config = await loadConfig({});
13232
- if (!config.airsApiKey && !config.airsApiToken) {
13233
- fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
13234
- }
13393
+ assertScannerCredentials(config);
13235
13394
  const raw = await readFile12(opts.file, "utf-8");
13236
13395
  const prompts = parseInputFile(raw, opts.file);
13237
13396
  if (prompts.length === 0) {
@@ -13242,7 +13401,7 @@ function registerRuntimeCommand(program) {
13242
13401
  opts.outputFile ?? `${opts.profile.replace(/\s+/g, "-")}-bulk-scan.csv`
13243
13402
  );
13244
13403
  const stateDir = resolvePath(
13245
- basename3(config.dataDir) === "runs" ? join3(dirname3(config.dataDir), "bulk-scans") : join3(config.dataDir, "bulk-scans")
13404
+ basename3(config.dataDir) === "runs" ? join3(dirname2(config.dataDir), "bulk-scans") : join3(config.dataDir, "bulk-scans")
13246
13405
  );
13247
13406
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
13248
13407
  const state = {
@@ -13672,9 +13831,7 @@ function registerRuntimeCommand(program) {
13672
13831
  stateFile = await fs5.promises.realpath(stateFile);
13673
13832
  releaseJobLock = await acquireBulkScanLock(stateFile);
13674
13833
  const config = await loadConfig({});
13675
- if (!config.airsApiKey && !config.airsApiToken) {
13676
- fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
13677
- }
13834
+ assertScannerCredentials(config);
13678
13835
  const state = await loadBulkScanState(stateFile);
13679
13836
  const service = new SdkRuntimeService(runtimeInitOptions(config));
13680
13837
  const unresolvedSubmission = state.items.find(
@@ -13682,7 +13839,7 @@ function registerRuntimeCommand(program) {
13682
13839
  );
13683
13840
  const outputPath = resolvePath(opts.outputFile ?? state.outputFile);
13684
13841
  state.outputFile = outputPath;
13685
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13842
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13686
13843
  const pollSubmitted = async (items) => {
13687
13844
  for (const batch of submittedBatches(items)) {
13688
13845
  const results2 = await service.pollBatch(batch, void 0, {
@@ -13691,12 +13848,12 @@ function registerRuntimeCommand(program) {
13691
13848
  },
13692
13849
  onProgress: async (progress) => {
13693
13850
  recordBulkResults(state, progress);
13694
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13851
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13695
13852
  await writeBulkResults(outputPath, completedBulkResults(state));
13696
13853
  }
13697
13854
  });
13698
13855
  recordBulkResults(state, results2);
13699
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13856
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13700
13857
  await writeBulkResults(outputPath, completedBulkResults(state));
13701
13858
  }
13702
13859
  };
@@ -13720,7 +13877,7 @@ function registerRuntimeCommand(program) {
13720
13877
  for (let start = 0; start < pendingItems.length; start += SDK_ASYNC_BATCH_SIZE) {
13721
13878
  const chunk = pendingItems.slice(start, start + SDK_ASYNC_BATCH_SIZE);
13722
13879
  for (const item of chunk) item.status = "submitting";
13723
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13880
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13724
13881
  try {
13725
13882
  const batch = await service.submitBatch(state.profile, chunk, state.sessionId, {
13726
13883
  onRetry: (attempt, delayMs) => {
@@ -13736,19 +13893,19 @@ function registerRuntimeCommand(program) {
13736
13893
  item.receiptReportId = batch.reportId;
13737
13894
  item.error = void 0;
13738
13895
  }
13739
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13896
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13740
13897
  } catch (error) {
13741
13898
  for (const item of chunk) {
13742
13899
  item.status = isDefiniteSubmissionRejection(error) ? "pending" : "ambiguous";
13743
13900
  item.error = error instanceof Error ? error.message : String(error);
13744
13901
  }
13745
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13902
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13746
13903
  throw error;
13747
13904
  }
13748
13905
  }
13749
13906
  await pollSubmitted(logicalBatch);
13750
13907
  }
13751
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13908
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13752
13909
  const results = completedBulkResults(state);
13753
13910
  await writeBulkResults(outputPath, results);
13754
13911
  const blocked = results.filter((r) => r.action === "block").length;
@@ -13783,9 +13940,7 @@ function registerRuntimeCommand(program) {
13783
13940
  ).action(async (prompt, opts) => {
13784
13941
  try {
13785
13942
  const config = await loadConfig({});
13786
- if (!config.airsApiKey && !config.airsApiToken) {
13787
- fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
13788
- }
13943
+ assertScannerCredentials(config);
13789
13944
  const service = new SdkRuntimeService(runtimeInitOptions(config));
13790
13945
  ui.status("Prisma AIRS Runtime Scan");
13791
13946
  ui.status(`Profile: ${opts.profile}`);
@@ -13892,8 +14047,8 @@ function registerRuntimeCommand(program) {
13892
14047
  // src/config/tenant-settings.ts
13893
14048
  import { randomUUID as randomUUID11 } from "crypto";
13894
14049
  import { constants } from "fs";
13895
- import { access, lstat as lstat6, mkdir as mkdir3, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
13896
- import { dirname as dirname4, join as join4 } from "path";
14050
+ import { access, lstat as lstat6, mkdir as mkdir2, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
14051
+ import { dirname as dirname3, join as join4 } from "path";
13897
14052
  var TENANT_CONFIG_KEYS = Object.keys(ConfigSchema.shape);
13898
14053
  function validateTenantSettingKey(key) {
13899
14054
  if (!Object.hasOwn(ConfigSchema.shape, key))
@@ -13918,8 +14073,8 @@ async function createManagedTenant(name, config) {
13918
14073
  validateCredentials(config);
13919
14074
  if (readTenantStore().tenants.some((entry) => entry.name === name))
13920
14075
  throw new Error("Tenant name already exists");
13921
- const directory = join4(dirname4(tenantStorePath()), "configs");
13922
- await mkdir3(directory, { recursive: true, mode: 448 });
14076
+ const directory = join4(dirname3(tenantStorePath()), "configs");
14077
+ await mkdir2(directory, { recursive: true, mode: 448 });
13923
14078
  const path3 = join4(directory, `${name}-${randomUUID11()}.json`);
13924
14079
  const file = await open2(path3, "wx", 384);
13925
14080
  try {
@@ -13937,12 +14092,38 @@ async function createManagedTenant(name, config) {
13937
14092
  throw new Error("Could not create tenant configuration; registration was not completed");
13938
14093
  }
13939
14094
  }
14095
+ var CREDENTIAL_KEYS = ["mgmtTsgId", "mgmtClientId", "mgmtClientSecret"];
13940
14096
  async function setTenantSetting(name, key, value) {
13941
14097
  validateTenantSettingKey(key);
13942
- const entry = readTenantStore().tenants.find((tenant) => tenant.name === name);
13943
- if (!entry) throw new Error("Tenant not found; register a named tenant first");
14098
+ const entry = findTenant(name);
13944
14099
  if (key === "mgmtTsgId" && value !== entry.tsgId)
13945
14100
  throw new Error("TSG identity is pinned; create another tenant instead of changing mgmtTsgId");
14101
+ await rewriteTenantConfig(entry, (current) => {
14102
+ const result = ConfigSchema.safeParse({ ...current, [key]: value });
14103
+ if (!result.success) throw new Error("Invalid value for configuration setting");
14104
+ return { ...current, [key]: Reflect.get(result.data, key) };
14105
+ });
14106
+ }
14107
+ async function unsetTenantSetting(name, key) {
14108
+ validateTenantSettingKey(key);
14109
+ if (CREDENTIAL_KEYS.includes(key))
14110
+ throw new Error(`${key} is a credential and cannot be cleared; set a new value instead`);
14111
+ const entry = findTenant(name);
14112
+ let removed = false;
14113
+ await rewriteTenantConfig(entry, (current) => {
14114
+ if (!Object.hasOwn(current, key)) return current;
14115
+ removed = true;
14116
+ const { [key]: _dropped, ...rest } = current;
14117
+ return rest;
14118
+ });
14119
+ return removed;
14120
+ }
14121
+ function findTenant(name) {
14122
+ const entry = readTenantStore().tenants.find((tenant) => tenant.name === name);
14123
+ if (!entry) throw new Error("Tenant not found; register a named tenant first");
14124
+ return entry;
14125
+ }
14126
+ async function rewriteTenantConfig(entry, change) {
13946
14127
  const path3 = entry.configPath;
13947
14128
  const lockPath = `${path3}.lock`;
13948
14129
  const lock = await open2(lockPath, "wx", 384).catch(() => {
@@ -13950,7 +14131,7 @@ async function setTenantSetting(name, key, value) {
13950
14131
  "Cannot lock tenant config; it may be read-only or another edit is in progress"
13951
14132
  );
13952
14133
  });
13953
- const temporary = join4(dirname4(path3), `.airs-config-${randomUUID11()}.tmp`);
14134
+ const temporary = join4(dirname3(path3), `.airs-config-${randomUUID11()}.tmp`);
13954
14135
  let created = false;
13955
14136
  try {
13956
14137
  const stat3 = await lstat6(path3);
@@ -13958,10 +14139,9 @@ async function setTenantSetting(name, key, value) {
13958
14139
  throw new Error("Tenant config is not a writable regular file");
13959
14140
  await access(path3, constants.W_OK);
13960
14141
  const current = readTenantConfigFile(path3, entry.tsgId);
13961
- const result = ConfigSchema.safeParse({ ...current, [key]: value });
13962
- if (!result.success) throw new Error("Invalid value for configuration setting");
13963
- const next = { ...current, [key]: Reflect.get(result.data, key) };
14142
+ const next = change(current);
13964
14143
  validateCredentials(next);
14144
+ if (next === current) return;
13965
14145
  const file = await open2(temporary, "wx", 384);
13966
14146
  created = true;
13967
14147
  try {
@@ -13981,38 +14161,6 @@ async function setTenantSetting(name, key, value) {
13981
14161
  }
13982
14162
  }
13983
14163
 
13984
- // src/cli/tenant-input.ts
13985
- import { input, password } from "@inquirer/prompts";
13986
- async function promptTenantValue(message, secret = false) {
13987
- if (!process.stdin.isTTY || !process.stderr.isTTY)
13988
- throw new Error(
13989
- "Interactive setup requires a terminal. Use --config, or --tsg-id and --client-id with --client-secret-stdin. For tenant set, pass a value or use --stdin."
13990
- );
13991
- const prompt = secret ? password : input;
13992
- return prompt(
13993
- {
13994
- message,
13995
- validate: (value) => Boolean(value.trim()) || "A nonempty value is required"
13996
- },
13997
- { input: process.stdin, output: process.stderr }
13998
- );
13999
- }
14000
- async function readTenantStdin() {
14001
- if (process.stdin.isTTY) throw new Error("--stdin requires piped input");
14002
- const chunks = [];
14003
- let size = 0;
14004
- for await (const chunk of process.stdin) {
14005
- const buffer = Buffer.from(chunk);
14006
- size += buffer.length;
14007
- if (size > 65536) throw new Error("Configuration input exceeds 64 KiB");
14008
- chunks.push(buffer);
14009
- }
14010
- const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
14011
- if (!value.trim() || /[\r\n\0]/.test(value))
14012
- throw new Error("Provide one nonempty configuration value on stdin");
14013
- return value;
14014
- }
14015
-
14016
14164
  // src/cli/commands/tenant.ts
14017
14165
  function tenantInputFailure(error) {
14018
14166
  if (error instanceof Error && error.name === "ExitPromptError") {
@@ -14022,22 +14170,43 @@ function tenantInputFailure(error) {
14022
14170
  }
14023
14171
  fail(error);
14024
14172
  }
14025
- var COLUMNS2 = [
14173
+ var COLUMNS = [
14026
14174
  { key: "name", label: "Tenant" },
14027
14175
  { key: "active", label: "Selected" },
14028
14176
  { key: "tsgId", label: "TSG ID" },
14029
14177
  { key: "configPath", label: "Config file" }
14030
14178
  ];
14179
+ var KEY_VALUE_COLUMNS = [
14180
+ { key: "key", label: "Key" },
14181
+ { key: "value", label: "Value" }
14182
+ ];
14183
+ function resolveEntry(name) {
14184
+ const store = readTenantStore();
14185
+ const selected = name ?? store.active;
14186
+ if (!selected)
14187
+ throw new Error(
14188
+ store.tenants.length ? `No tenant selected; pass a name or run 'airs tenant switch <name>' (registered: ${store.tenants.map((entry2) => entry2.name).join(", ")})` : "No tenant selected; run 'airs tenant create <name>' first"
14189
+ );
14190
+ const entry = store.tenants.find((value) => value.name === selected);
14191
+ if (!entry) throw new Error("Tenant not found");
14192
+ return entry;
14193
+ }
14194
+ function redact(key, value) {
14195
+ return isTenantSecret(key) && value ? "[REDACTED]" : value ?? "";
14196
+ }
14031
14197
  function registerTenantCommand(program) {
14032
- const tenant = program.command("tenant").description("Create, configure and select named tenants").addHelpText(
14198
+ const tenant = program.command("tenant").description("Create, configure and select tenants (the only configuration source)").addHelpText(
14033
14199
  "after",
14034
14200
  examples(
14035
14201
  "airs tenant create development",
14202
+ "airs tenant switch development",
14036
14203
  "airs tenant set development defaultOutput yaml",
14204
+ "airs tenant set development airsApiKey",
14205
+ "airs tenant unset development defaultOutput",
14206
+ "airs tenant get development mgmtTsgId",
14037
14207
  "airs tenant create production --config /secure/production.json",
14038
- "airs tenant switch production",
14039
14208
  "airs tenant read",
14040
- "airs tenant switch default"
14209
+ "airs tenant path"
14041
14210
  )
14042
14211
  );
14043
14212
  tenant.command("create <name>").description(
@@ -14063,7 +14232,7 @@ function registerTenantCommand(program) {
14063
14232
  }
14064
14233
  }
14065
14234
  );
14066
- tenant.command("set <name> <key> [value]").description("Update one named tenant setting; prompt when omitted, hide secrets").option("--stdin", "Read one value from piped stdin (recommended for automated secret updates)").action(
14235
+ tenant.command("set <name> <key> [value]").description("Update one tenant setting; prompt when omitted, hide secrets").option("--stdin", "Read one value from piped stdin (recommended for automated secret updates)").action(
14067
14236
  async (name, key, value, opts) => {
14068
14237
  try {
14069
14238
  validateTenantSettingKey(key);
@@ -14083,14 +14252,32 @@ function registerTenantCommand(program) {
14083
14252
  }
14084
14253
  }
14085
14254
  );
14086
- tenant.command("switch <name>").description(
14087
- "Persist the selected tenant; default restores legacy config/environment resolution"
14088
- ).action(async (name) => {
14255
+ tenant.command("unset <name> <key>").description("Remove one tenant setting so the default applies; credentials cannot be cleared").action(async (name, key) => {
14256
+ try {
14257
+ const removed = await unsetTenantSetting(name, key);
14258
+ if (removed) ui.success(`Removed ${key} from tenant ${name}; selection unchanged.`);
14259
+ else ui.info(`${key} is not set for tenant ${name} \u2014 nothing to do`);
14260
+ } catch (error) {
14261
+ fail(error);
14262
+ }
14263
+ });
14264
+ const get = tenant.command("get <name> <key>").description("Print one effective tenant setting; credential values are redacted").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (name, key, opts) => {
14265
+ try {
14266
+ validateTenantSettingKey(key);
14267
+ const format = await resolveOutput(get, opts, { ignoreConfig: true });
14268
+ const entry = resolveEntry(name);
14269
+ const config = readTenantConfig(entry.configPath, entry.tsgId);
14270
+ const value = redact(key, config[key]);
14271
+ if (format === "pretty") console.log(String(value));
14272
+ else console.log(formatOutput([{ key, value }], KEY_VALUE_COLUMNS, format));
14273
+ } catch (error) {
14274
+ fail(error);
14275
+ }
14276
+ });
14277
+ tenant.command("switch <name>").description("Persist the selected tenant for subsequent commands").action(async (name) => {
14089
14278
  try {
14090
14279
  const entry = await switchTenant(name);
14091
- ui.success(
14092
- entry ? `Selected ${entry.name} (TSG ${entry.tsgId})` : "Selected default; explicit config/environment overrides still apply"
14093
- );
14280
+ ui.success(`Selected ${entry.name} (TSG ${entry.tsgId})`);
14094
14281
  } catch (error) {
14095
14282
  fail(error);
14096
14283
  }
@@ -14099,65 +14286,63 @@ function registerTenantCommand(program) {
14099
14286
  try {
14100
14287
  const format = await resolveOutput(list, opts, { ignoreConfig: true });
14101
14288
  const store = readTenantStore();
14102
- const explicit = process.env.PRISMA_AIRS_CONFIG_PATH;
14103
- const rows = [
14104
- {
14105
- name: "default",
14106
- active: store.active === null,
14107
- tsgId: "",
14108
- configPath: defaultTenantConfigPath()
14109
- },
14110
- ...store.tenants.map((entry) => ({ ...entry, active: entry.name === store.active }))
14111
- ];
14112
- if (explicit)
14113
- ui.status("PRISMA_AIRS_CONFIG_PATH overrides the selected tenant for API commands.");
14114
- console.log(formatOutput(rows, COLUMNS2, format === "pretty" ? "table" : format));
14289
+ if (store.tenants.length === 0 && format === "pretty") {
14290
+ ui.emptyList("tenants");
14291
+ ui.status("Run 'airs tenant create <name>' to register one.");
14292
+ return;
14293
+ }
14294
+ if (store.active === null && store.tenants.length)
14295
+ ui.status("No tenant selected; run 'airs tenant switch <name>'.");
14296
+ const rows = store.tenants.map((entry) => ({
14297
+ ...entry,
14298
+ active: entry.name === store.active
14299
+ }));
14300
+ console.log(formatOutput(rows, COLUMNS, format === "pretty" ? "table" : format));
14115
14301
  } catch (error) {
14116
14302
  fail(error);
14117
14303
  }
14118
14304
  });
14119
- const read = tenant.command("read [name]").description("Read a tenant config with all credential values redacted").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (name, opts) => {
14305
+ const read = tenant.command("read [name]").description(
14306
+ "Read a tenant config with all credential values redacted; defaults to the selected tenant"
14307
+ ).option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (name, opts) => {
14120
14308
  try {
14121
14309
  const format = await resolveOutput(read, opts, { ignoreConfig: true });
14122
- const store = readTenantStore();
14123
- const selected = name ?? store.active ?? "default";
14124
- const entry = store.tenants.find((value) => value.name === selected);
14125
- if (selected !== "default" && !entry) throw new Error("Tenant not found");
14126
- const path3 = entry?.configPath ?? expandConfigPath(process.env.PRISMA_AIRS_CONFIG_PATH || defaultTenantConfigPath());
14127
- const config = readTenantConfig(path3, entry?.tsgId);
14310
+ const entry = resolveEntry(name);
14311
+ const config = readTenantConfig(entry.configPath, entry.tsgId);
14128
14312
  const rows = Object.entries(config).map(([key, value]) => ({
14129
14313
  key,
14130
- value: /key|secret|token|password/i.test(key) && value ? "[REDACTED]" : value ?? ""
14314
+ value: redact(key, value)
14131
14315
  }));
14132
- ui.status(`Tenant ${selected}${entry ? ` (TSG ${entry.tsgId})` : ""}: ${path3}`);
14133
- console.log(
14134
- formatOutput(
14135
- rows,
14136
- [
14137
- { key: "key", label: "Key" },
14138
- { key: "value", label: "Value" }
14139
- ],
14140
- format === "pretty" ? "table" : format
14141
- )
14142
- );
14316
+ ui.status(`Tenant ${entry.name} (TSG ${entry.tsgId}): ${entry.configPath}`);
14317
+ console.log(formatOutput(rows, KEY_VALUE_COLUMNS, format === "pretty" ? "table" : format));
14143
14318
  } catch (error) {
14144
14319
  fail(error);
14145
14320
  }
14146
14321
  });
14147
- tenant.command("delete <name>").description("Unregister an inactive tenant; retain its source config file").option("--force", "Skip interactive confirmation; active tenant deletion remains refused").action(async (name, opts) => {
14322
+ tenant.command("path [name]").description("Print the config file path of a tenant; defaults to the selected tenant").action((name) => {
14323
+ try {
14324
+ console.log(resolveEntry(name).configPath);
14325
+ } catch (error) {
14326
+ fail(error);
14327
+ }
14328
+ });
14329
+ tenant.command("delete <name>").description(
14330
+ "Unregister a tenant; retain its source config file and clear the selection if it was selected"
14331
+ ).option("--force", "Skip interactive confirmation").action(async (name, opts) => {
14148
14332
  try {
14149
14333
  const store = readTenantStore();
14150
- if (name === "default" || name === store.active)
14151
- throw new Error("Switch away first; default and active tenants cannot be deleted");
14152
14334
  if (!store.tenants.some((entry) => entry.name === name))
14153
14335
  throw new Error("Tenant not found");
14336
+ const selected = store.active === name;
14154
14337
  await confirmOrAbort(
14155
- `Unregister tenant ${name}? Its config file will be retained.`,
14338
+ `Unregister tenant ${name}?${selected ? " It is the selected tenant; no tenant will be selected afterwards." : ""} Its config file will be retained.`,
14156
14339
  Boolean(opts.force),
14157
14340
  { action: `unregister tenant ${name}` }
14158
14341
  );
14159
- await deleteTenant(name);
14160
- ui.success(`Unregistered ${name}; source config file retained`);
14342
+ const { selectionCleared } = await deleteTenant(name);
14343
+ ui.success(
14344
+ `Unregistered ${name}; source config file retained${selectionCleared ? "; no tenant is selected" : ""}`
14345
+ );
14161
14346
  } catch (error) {
14162
14347
  fail(error);
14163
14348
  }
@@ -14218,7 +14403,7 @@ function applySortedHelp(cmd) {
14218
14403
  for (const sub of cmd.commands) applySortedHelp(sub);
14219
14404
  }
14220
14405
  function buildProgram() {
14221
- const here = dirname5(fileURLToPath(import.meta.url));
14406
+ const here = dirname4(fileURLToPath(import.meta.url));
14222
14407
  const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
14223
14408
  const program = new Command();
14224
14409
  let dlpDebugBodyEnvironment;
@@ -14299,7 +14484,6 @@ function buildProgram() {
14299
14484
  registerModelSecurityCommand(program);
14300
14485
  registerAgentGuardCommand(program);
14301
14486
  registerAiGatewayCommand(program);
14302
- registerConfigCommand(program);
14303
14487
  registerTenantCommand(program);
14304
14488
  registerDoctorCommand(program);
14305
14489
  registerCompletionCommand(program);