@indigoai-us/hq-cli 5.47.10 → 5.47.12

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.
@@ -2,13 +2,16 @@ import { Command } from "commander";
2
2
  import chalk from "chalk";
3
3
  import * as readline from "node:readline";
4
4
  import { spawn } from "node:child_process";
5
+ import * as nodePath from "node:path";
5
6
  import { ensureCognitoToken } from "../utils/cognito-session.js";
6
7
  import {
8
+ DEFAULT_SECRETS_CACHE_TTL_MS,
7
9
  readCache,
8
10
  writeCache,
9
11
  removeCacheEntry,
10
12
  clearAllCache,
11
13
  } from "../utils/secrets-cache.js";
14
+ import { computeSha256 } from "../utils/integrity.js";
12
15
  import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN } from "./_patterns.js";
13
16
  import {
14
17
  vaultApiFetch,
@@ -157,6 +160,192 @@ function promptSecretInteractively(): Promise<string> {
157
160
  // server (the legacy per-key GET path had no such cap).
158
161
  const MAX_BATCH_NAMES = 100;
159
162
 
163
+ export type SecretTier = "standard" | "sensitive" | "nuclear";
164
+ export type SecretScriptLockMode = "off" | "enforced";
165
+ export type SecretUsageChannel =
166
+ | "run"
167
+ | "exec"
168
+ | "env"
169
+ | "reveal"
170
+ | "submit-link";
171
+
172
+ export interface SecretScriptUsage {
173
+ scriptId: string;
174
+ path: string;
175
+ sha256: string;
176
+ attestationLevel: string;
177
+ }
178
+
179
+ export interface SecretUsage {
180
+ channel: SecretUsageChannel;
181
+ reason?: string;
182
+ script?: SecretScriptUsage;
183
+ }
184
+
185
+ export interface SecretMetadata {
186
+ tier?: SecretTier;
187
+ scriptLock?: {
188
+ mode?: SecretScriptLockMode;
189
+ requiredAttestation?: string;
190
+ };
191
+ cacheTtlMs?: number;
192
+ }
193
+
194
+ export interface SecretLoadSuccessRow extends SecretMetadata {
195
+ name: string;
196
+ value?: string;
197
+ }
198
+
199
+ export interface SecretLoadResponse {
200
+ secrets: SecretLoadSuccessRow[];
201
+ errors: Array<{ name: string; code: string; message?: string }>;
202
+ }
203
+
204
+ interface SecretGetResponse {
205
+ secret: {
206
+ name: string;
207
+ companyUid: string;
208
+ type?: string;
209
+ lastModifiedDate?: string;
210
+ version?: number;
211
+ value?: string;
212
+ } & SecretMetadata;
213
+ }
214
+
215
+ interface SecretPolicyScript {
216
+ scriptId: string;
217
+ scriptPath: string;
218
+ sha256: string;
219
+ attestationLevel: string;
220
+ }
221
+
222
+ interface SecretPolicyRecord {
223
+ path: string;
224
+ tier?: SecretTier;
225
+ scriptLock?: {
226
+ mode?: SecretScriptLockMode;
227
+ requiredAttestation?: string;
228
+ };
229
+ scripts?: SecretPolicyScript[];
230
+ }
231
+
232
+ interface SecretPolicyResponse {
233
+ policy?: SecretPolicyRecord;
234
+ scripts?: SecretPolicyScript[];
235
+ }
236
+
237
+ function normalizeSecretTier(tier?: string): SecretTier {
238
+ return tier === "sensitive" || tier === "nuclear" ? tier : "standard";
239
+ }
240
+
241
+ function normalizeScriptLockMode(mode?: string): SecretScriptLockMode {
242
+ return mode === "enforced" ? "enforced" : "off";
243
+ }
244
+
245
+ function normalizeCacheTtlMs(cacheTtlMs?: number): number {
246
+ return typeof cacheTtlMs === "number"
247
+ ? cacheTtlMs
248
+ : DEFAULT_SECRETS_CACHE_TTL_MS;
249
+ }
250
+
251
+ function extractApiMessage(
252
+ body: Record<string, unknown>,
253
+ fallback: string,
254
+ ): string {
255
+ const message = typeof body.message === "string" ? body.message : undefined;
256
+ const error = typeof body.error === "string" ? body.error : undefined;
257
+ return message ?? error ?? fallback;
258
+ }
259
+
260
+ async function buildSecretUsage(
261
+ channel: SecretUsageChannel,
262
+ scriptPath?: string,
263
+ scriptId?: string,
264
+ attestationLevel = "self-asserted-hash",
265
+ ): Promise<SecretUsage> {
266
+ if (!scriptPath) {
267
+ return { channel };
268
+ }
269
+ const resolvedPath = nodePath.resolve(scriptPath);
270
+ return {
271
+ channel,
272
+ script: {
273
+ scriptId: scriptId ?? resolvedPath,
274
+ path: resolvedPath,
275
+ sha256: await computeSha256(resolvedPath),
276
+ attestationLevel,
277
+ },
278
+ };
279
+ }
280
+
281
+ function normalizePolicyRecord(
282
+ secretPath: string,
283
+ data: SecretPolicyResponse,
284
+ ): SecretPolicyRecord & { scripts: SecretPolicyScript[] } {
285
+ const policy = data.policy ?? { path: secretPath };
286
+ const scripts = Array.isArray(policy.scripts)
287
+ ? policy.scripts
288
+ : Array.isArray(data.scripts)
289
+ ? data.scripts
290
+ : [];
291
+ return {
292
+ path: policy.path ?? secretPath,
293
+ tier: normalizeSecretTier(policy.tier),
294
+ scriptLock: {
295
+ mode: normalizeScriptLockMode(policy.scriptLock?.mode),
296
+ requiredAttestation: policy.scriptLock?.requiredAttestation,
297
+ },
298
+ scripts,
299
+ };
300
+ }
301
+
302
+ function renderPolicySummary(policy: SecretPolicyRecord): void {
303
+ console.log(chalk.bold(`Policy: ${policy.path}`));
304
+ console.log(` Tier: ${normalizeSecretTier(policy.tier)}`);
305
+ console.log(
306
+ ` Script Lock: ${normalizeScriptLockMode(policy.scriptLock?.mode)}`,
307
+ );
308
+ if (policy.scriptLock?.requiredAttestation) {
309
+ console.log(
310
+ ` Attestation: ${policy.scriptLock.requiredAttestation}`,
311
+ );
312
+ }
313
+ }
314
+
315
+ function renderPolicyScripts(scripts: SecretPolicyScript[]): void {
316
+ if (scripts.length === 0) {
317
+ console.log(chalk.dim("No approved scripts."));
318
+ return;
319
+ }
320
+
321
+ const idWidth = Math.max(2, ...scripts.map((script) => script.scriptId.length));
322
+ const pathWidth = Math.max(
323
+ 6,
324
+ ...scripts.map((script) => script.scriptPath.length),
325
+ );
326
+ const attestationWidth = Math.max(
327
+ 11,
328
+ ...scripts.map((script) => script.attestationLevel.length),
329
+ );
330
+ const header = [
331
+ "ID".padEnd(idWidth),
332
+ "SCRIPT".padEnd(pathWidth),
333
+ "ATTESTATION".padEnd(attestationWidth),
334
+ "SHA256",
335
+ ].join(" ");
336
+ console.log(chalk.bold(header));
337
+ for (const script of scripts) {
338
+ console.log(
339
+ [
340
+ script.scriptId.padEnd(idWidth),
341
+ script.scriptPath.padEnd(pathWidth),
342
+ script.attestationLevel.padEnd(attestationWidth),
343
+ script.sha256,
344
+ ].join(" "),
345
+ );
346
+ }
347
+ }
348
+
160
349
  // HQ-4H — load + decrypt secrets through the BATCH-LOAD endpoint
161
350
  // (`POST /secrets/{companyUid}/load`) instead of one single-secret GET per key.
162
351
  //
@@ -179,6 +368,7 @@ export async function loadRevealedSecrets(
179
368
  token: string,
180
369
  companyUid: string,
181
370
  keys: string[],
371
+ usage?: SecretUsage,
182
372
  ): Promise<Map<string, string>> {
183
373
  const resolved = new Map<string, string>();
184
374
  const missing: string[] = [];
@@ -197,18 +387,23 @@ export async function loadRevealedSecrets(
197
387
  token,
198
388
  path: `/secrets/${encodeURIComponent(companyUid)}/load`,
199
389
  method: "POST",
200
- body: { names: chunk },
390
+ body: usage ? { names: chunk, usage } : { names: chunk },
201
391
  });
202
392
  if (!res.ok) {
203
- const body = (await res.json().catch(() => ({}))) as Record<string, string>;
393
+ const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
394
+ const message = extractApiMessage(body, res.statusText);
395
+ if (
396
+ res.status >= 400 &&
397
+ res.status < 500 &&
398
+ typeof body.code === "string"
399
+ ) {
400
+ throw new Error(message);
401
+ }
204
402
  throw new Error(
205
- `Failed to batch-load secrets: ${body.error ?? res.statusText}`,
403
+ `Failed to batch-load secrets: ${message}`,
206
404
  );
207
405
  }
208
- const data = (await res.json()) as {
209
- secrets: Array<{ name: string; value?: string }>;
210
- errors: Array<{ name: string; code: string; message?: string }>;
211
- };
406
+ const data = (await res.json()) as SecretLoadResponse;
212
407
 
213
408
  for (const s of data.secrets ?? []) {
214
409
  if (s.value == null) {
@@ -216,7 +411,10 @@ export async function loadRevealedSecrets(
216
411
  `Secret '${s.name}' has no value (reveal may not be permitted).`,
217
412
  );
218
413
  }
219
- writeCache(companyUid, s.name, s.value);
414
+ const cacheTtlMs = normalizeCacheTtlMs(s.cacheTtlMs);
415
+ if (cacheTtlMs > 0) {
416
+ writeCache(companyUid, s.name, s.value, cacheTtlMs);
417
+ }
220
418
  resolved.set(s.name, s.value);
221
419
  }
222
420
 
@@ -333,35 +531,32 @@ export function registerSecretsCommand(program: Command): void {
333
531
  scopeOpts(secrets.opts()),
334
532
  );
335
533
 
336
- const query: Record<string, string> = {};
337
- if (opts.reveal) {
338
- query.reveal = "true";
339
- }
340
-
341
534
  const res = await vaultApiFetch({
342
535
  token,
343
536
  path: buildSecretNamePath(companyUid, name),
344
- query: Object.keys(query).length > 0 ? query : undefined,
345
537
  });
346
538
 
347
539
  if (!res.ok) {
348
- const body = await res.json().catch(() => ({}));
540
+ const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
349
541
  console.error(
350
- chalk.red(`Failed to get secret: ${(body as Record<string, string>).error ?? res.statusText}`),
542
+ chalk.red(`Failed to get secret: ${extractApiMessage(body, res.statusText)}`),
351
543
  );
352
544
  process.exit(1);
353
545
  }
354
546
 
355
- const data = (await res.json()) as {
356
- secret: {
357
- name: string;
358
- companyUid: string;
359
- type?: string;
360
- lastModifiedDate?: string;
361
- version?: number;
362
- value?: string;
363
- };
364
- };
547
+ const data = (await res.json()) as SecretGetResponse;
548
+
549
+ let revealedValue: string | undefined;
550
+ let revealError: string | null = null;
551
+ if (opts.reveal) {
552
+ try {
553
+ revealedValue = (
554
+ await loadRevealedSecrets(token, companyUid, [name], await buildSecretUsage("reveal"))
555
+ ).get(name);
556
+ } catch (err) {
557
+ revealError = err instanceof Error ? err.message : String(err);
558
+ }
559
+ }
365
560
 
366
561
  const s = data.secret;
367
562
  console.log(chalk.bold(`Secret: ${s.name}`));
@@ -371,11 +566,19 @@ export function registerSecretsCommand(program: Command): void {
371
566
  if (s.version != null) {
372
567
  console.log(` Version: ${s.version}`);
373
568
  }
374
- if (opts.reveal && s.value != null) {
375
- console.log(` Value: ${s.value}`);
569
+ console.log(` Tier: ${normalizeSecretTier(s.tier)}`);
570
+ console.log(
571
+ ` Script Lock: ${normalizeScriptLockMode(s.scriptLock?.mode)}`,
572
+ );
573
+ if (opts.reveal && revealedValue != null) {
574
+ console.log(` Value: ${revealedValue}`);
376
575
  } else {
377
576
  console.log(` Value: ${chalk.dim("[REDACTED]")}`);
378
577
  }
578
+ if (revealError) {
579
+ console.error(chalk.red(revealError));
580
+ process.exit(1);
581
+ }
379
582
  } catch (err) {
380
583
  console.error(
381
584
  chalk.red("Error:"),
@@ -490,7 +693,14 @@ export function registerSecretsCommand(program: Command): void {
490
693
  }
491
694
 
492
695
  const data = (await res.json()) as {
493
- secrets: { name: string; lastModifiedDate?: string; version?: number; permission?: "admin" | "write" | "read" }[];
696
+ secrets: Array<
697
+ {
698
+ name: string;
699
+ lastModifiedDate?: string;
700
+ version?: number;
701
+ permission?: "admin" | "write" | "read";
702
+ } & SecretMetadata
703
+ >;
494
704
  };
495
705
 
496
706
  if (data.secrets.length === 0) {
@@ -500,21 +710,33 @@ export function registerSecretsCommand(program: Command): void {
500
710
 
501
711
  const nameWidth = Math.max(4, ...data.secrets.map((s) => s.name.length));
502
712
  const hasPermission = data.secrets.some((s) => s.permission !== undefined);
713
+ const tierWidth = Math.max(
714
+ 4,
715
+ ...data.secrets.map((s) => normalizeSecretTier(s.tier).length),
716
+ );
717
+ const scriptLockWidth = Math.max(
718
+ 11,
719
+ ...data.secrets.map((s) => normalizeScriptLockMode(s.scriptLock?.mode).length),
720
+ );
503
721
  if (hasPermission) {
504
722
  const accessWidth = Math.max(6, ...data.secrets.map((s) => (s.permission ?? "-").length));
505
- const header = `${"NAME".padEnd(nameWidth)} ${"ACCESS".padEnd(accessWidth)} LAST MODIFIED`;
723
+ const header = `${"NAME".padEnd(nameWidth)} ${"ACCESS".padEnd(accessWidth)} ${"TIER".padEnd(tierWidth)} ${"SCRIPT LOCK".padEnd(scriptLockWidth)} LAST MODIFIED`;
506
724
  console.log(chalk.bold(header));
507
725
  for (const s of data.secrets) {
508
726
  const access = s.permission ?? "-";
727
+ const tier = normalizeSecretTier(s.tier);
728
+ const scriptLock = normalizeScriptLockMode(s.scriptLock?.mode);
509
729
  const modified = s.lastModifiedDate ?? "-";
510
- console.log(`${s.name.padEnd(nameWidth)} ${access.padEnd(accessWidth)} ${modified}`);
730
+ console.log(`${s.name.padEnd(nameWidth)} ${access.padEnd(accessWidth)} ${tier.padEnd(tierWidth)} ${scriptLock.padEnd(scriptLockWidth)} ${modified}`);
511
731
  }
512
732
  } else {
513
- const header = `${"NAME".padEnd(nameWidth)} LAST MODIFIED`;
733
+ const header = `${"NAME".padEnd(nameWidth)} ${"TIER".padEnd(tierWidth)} ${"SCRIPT LOCK".padEnd(scriptLockWidth)} LAST MODIFIED`;
514
734
  console.log(chalk.bold(header));
515
735
  for (const s of data.secrets) {
736
+ const tier = normalizeSecretTier(s.tier);
737
+ const scriptLock = normalizeScriptLockMode(s.scriptLock?.mode);
516
738
  const modified = s.lastModifiedDate ?? "-";
517
- console.log(`${s.name.padEnd(nameWidth)} ${modified}`);
739
+ console.log(`${s.name.padEnd(nameWidth)} ${tier.padEnd(tierWidth)} ${scriptLock.padEnd(scriptLockWidth)} ${modified}`);
518
740
  }
519
741
  }
520
742
  } catch (err) {
@@ -526,6 +748,309 @@ export function registerSecretsCommand(program: Command): void {
526
748
  }
527
749
  });
528
750
 
751
+ const policy = secrets
752
+ .command("policy")
753
+ .description("View or update secret access policy");
754
+
755
+ policy
756
+ .command("get <path>")
757
+ .description("Show the policy for a secret path")
758
+ .action(async (secretPath: string) => {
759
+ try {
760
+ rejectIfPersonal(secrets.opts(), "policy get");
761
+
762
+ if (!SECRET_NAME_PATTERN.test(secretPath)) {
763
+ console.error(chalk.red(`Invalid secret path '${secretPath}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
764
+ process.exit(1);
765
+ }
766
+
767
+ const token = await ensureCognitoToken();
768
+ const companyUid = await getEntityUid(
769
+ token,
770
+ scopeOpts(secrets.opts()),
771
+ );
772
+ const res = await vaultApiFetch({
773
+ token,
774
+ path: `/secrets/${encodeURIComponent(companyUid)}/policy`,
775
+ query: { path: secretPath },
776
+ });
777
+
778
+ if (res.status === 404) {
779
+ renderPolicySummary(
780
+ normalizePolicyRecord(secretPath, { policy: { path: secretPath } }),
781
+ );
782
+ return;
783
+ }
784
+ if (!res.ok) {
785
+ const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
786
+ console.error(
787
+ chalk.red(`Failed to get policy: ${extractApiMessage(body, res.statusText)}`),
788
+ );
789
+ process.exit(1);
790
+ }
791
+
792
+ const data = (await res.json()) as SecretPolicyResponse;
793
+ renderPolicySummary(normalizePolicyRecord(secretPath, data));
794
+ } catch (err) {
795
+ console.error(
796
+ chalk.red("Error:"),
797
+ err instanceof Error ? err.message : String(err),
798
+ );
799
+ process.exit(1);
800
+ }
801
+ });
802
+
803
+ policy
804
+ .command("set <path>")
805
+ .description("Set the policy for a secret path")
806
+ .option("--tier <tier>", "Secret tier: standard | sensitive | nuclear")
807
+ .option("--lock-script <mode>", "Script-lock mode: off | enforce")
808
+ .action(async (
809
+ secretPath: string,
810
+ opts: { tier?: string; lockScript?: string },
811
+ ) => {
812
+ try {
813
+ rejectIfPersonal(secrets.opts(), "policy set");
814
+
815
+ if (!SECRET_NAME_PATTERN.test(secretPath)) {
816
+ console.error(chalk.red(`Invalid secret path '${secretPath}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
817
+ process.exit(1);
818
+ }
819
+
820
+ const tier = opts.tier;
821
+ if (
822
+ tier !== undefined &&
823
+ tier !== "standard" &&
824
+ tier !== "sensitive" &&
825
+ tier !== "nuclear"
826
+ ) {
827
+ console.error(chalk.red(`Invalid tier '${tier}': must be one of standard, sensitive, nuclear`));
828
+ process.exit(1);
829
+ }
830
+
831
+ let scriptLock:
832
+ | { mode: SecretScriptLockMode }
833
+ | undefined;
834
+ if (opts.lockScript !== undefined) {
835
+ if (opts.lockScript === "off") {
836
+ scriptLock = { mode: "off" };
837
+ } else if (opts.lockScript === "enforce") {
838
+ scriptLock = { mode: "enforced" };
839
+ } else {
840
+ console.error(chalk.red(`Invalid script lock mode '${opts.lockScript}': must be one of off, enforce`));
841
+ process.exit(1);
842
+ }
843
+ }
844
+
845
+ if (tier === undefined && scriptLock === undefined) {
846
+ console.error(chalk.red("Error: provide at least one of --tier or --lock-script."));
847
+ process.exit(1);
848
+ }
849
+
850
+ const token = await ensureCognitoToken();
851
+ const companyUid = await getEntityUid(
852
+ token,
853
+ scopeOpts(secrets.opts()),
854
+ );
855
+ const res = await vaultApiFetch({
856
+ token,
857
+ path: `/secrets/${encodeURIComponent(companyUid)}/policy`,
858
+ method: "PUT",
859
+ body: {
860
+ path: secretPath,
861
+ ...(tier !== undefined ? { tier } : {}),
862
+ ...(scriptLock ? { scriptLock } : {}),
863
+ },
864
+ });
865
+
866
+ if (!res.ok) {
867
+ const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
868
+ console.error(
869
+ chalk.red(`Failed to set policy: ${extractApiMessage(body, res.statusText)}`),
870
+ );
871
+ process.exit(1);
872
+ }
873
+
874
+ const data = (await res.json().catch(() => ({}))) as SecretPolicyResponse;
875
+ console.log(chalk.green(`Policy updated for '${secretPath}'.`));
876
+ renderPolicySummary(
877
+ normalizePolicyRecord(
878
+ secretPath,
879
+ data.policy ? data : { policy: { path: secretPath, tier, scriptLock } },
880
+ ),
881
+ );
882
+ } catch (err) {
883
+ console.error(
884
+ chalk.red("Error:"),
885
+ err instanceof Error ? err.message : String(err),
886
+ );
887
+ process.exit(1);
888
+ }
889
+ });
890
+
891
+ const script = secrets
892
+ .command("script")
893
+ .description("Manage approved scripts for script-locked secrets");
894
+
895
+ script
896
+ .command("approve <path>")
897
+ .description("Approve a script for a secret path")
898
+ .requiredOption("--id <scriptId>", "Stable script identifier")
899
+ .requiredOption("--script <path>", "Path to the local script file")
900
+ .option(
901
+ "--attestation <level>",
902
+ "Attestation level",
903
+ "self-asserted-hash",
904
+ )
905
+ .action(async (
906
+ secretPath: string,
907
+ opts: { id: string; script: string; attestation: string },
908
+ ) => {
909
+ try {
910
+ rejectIfPersonal(secrets.opts(), "script approve");
911
+
912
+ if (!SECRET_NAME_PATTERN.test(secretPath)) {
913
+ console.error(chalk.red(`Invalid secret path '${secretPath}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
914
+ process.exit(1);
915
+ }
916
+
917
+ const usage = await buildSecretUsage(
918
+ "exec",
919
+ opts.script,
920
+ opts.id,
921
+ opts.attestation,
922
+ );
923
+ const token = await ensureCognitoToken();
924
+ const companyUid = await getEntityUid(
925
+ token,
926
+ scopeOpts(secrets.opts()),
927
+ );
928
+ const res = await vaultApiFetch({
929
+ token,
930
+ path: `/secrets/${encodeURIComponent(companyUid)}/policy/scripts`,
931
+ method: "POST",
932
+ body: {
933
+ path: secretPath,
934
+ scriptId: usage.script?.scriptId,
935
+ scriptPath: usage.script?.path,
936
+ sha256: usage.script?.sha256,
937
+ attestationLevel: usage.script?.attestationLevel,
938
+ },
939
+ });
940
+
941
+ if (!res.ok) {
942
+ const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
943
+ console.error(
944
+ chalk.red(`Failed to approve script: ${extractApiMessage(body, res.statusText)}`),
945
+ );
946
+ process.exit(1);
947
+ }
948
+
949
+ console.log(chalk.green(`Approved script '${opts.id}' for '${secretPath}'.`));
950
+ } catch (err) {
951
+ console.error(
952
+ chalk.red("Error:"),
953
+ err instanceof Error ? err.message : String(err),
954
+ );
955
+ process.exit(1);
956
+ }
957
+ });
958
+
959
+ script
960
+ .command("revoke <path>")
961
+ .description("Revoke an approved script from a secret path")
962
+ .requiredOption("--id <scriptId>", "Stable script identifier")
963
+ .action(async (secretPath: string, opts: { id: string }) => {
964
+ try {
965
+ rejectIfPersonal(secrets.opts(), "script revoke");
966
+
967
+ if (!SECRET_NAME_PATTERN.test(secretPath)) {
968
+ console.error(chalk.red(`Invalid secret path '${secretPath}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
969
+ process.exit(1);
970
+ }
971
+
972
+ const token = await ensureCognitoToken();
973
+ const companyUid = await getEntityUid(
974
+ token,
975
+ scopeOpts(secrets.opts()),
976
+ );
977
+ const res = await vaultApiFetch({
978
+ token,
979
+ path: `/secrets/${encodeURIComponent(companyUid)}/policy/scripts`,
980
+ method: "DELETE",
981
+ body: { path: secretPath, scriptId: opts.id },
982
+ });
983
+
984
+ if (!res.ok) {
985
+ const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
986
+ console.error(
987
+ chalk.red(`Failed to revoke script: ${extractApiMessage(body, res.statusText)}`),
988
+ );
989
+ process.exit(1);
990
+ }
991
+
992
+ console.log(chalk.green(`Revoked script '${opts.id}' for '${secretPath}'.`));
993
+ } catch (err) {
994
+ console.error(
995
+ chalk.red("Error:"),
996
+ err instanceof Error ? err.message : String(err),
997
+ );
998
+ process.exit(1);
999
+ }
1000
+ });
1001
+
1002
+ script
1003
+ .command("list <path>")
1004
+ .description("List approved scripts for a secret path")
1005
+ .action(async (secretPath: string) => {
1006
+ try {
1007
+ rejectIfPersonal(secrets.opts(), "script list");
1008
+
1009
+ if (!SECRET_NAME_PATTERN.test(secretPath)) {
1010
+ console.error(chalk.red(`Invalid secret path '${secretPath}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
1011
+ process.exit(1);
1012
+ }
1013
+
1014
+ const token = await ensureCognitoToken();
1015
+ const companyUid = await getEntityUid(
1016
+ token,
1017
+ scopeOpts(secrets.opts()),
1018
+ );
1019
+ const res = await vaultApiFetch({
1020
+ token,
1021
+ path: `/secrets/${encodeURIComponent(companyUid)}/policy`,
1022
+ query: { path: secretPath },
1023
+ });
1024
+
1025
+ if (res.status === 404) {
1026
+ const policyRecord = normalizePolicyRecord(secretPath, {
1027
+ policy: { path: secretPath },
1028
+ });
1029
+ renderPolicySummary(policyRecord);
1030
+ renderPolicyScripts(policyRecord.scripts);
1031
+ return;
1032
+ }
1033
+ if (!res.ok) {
1034
+ const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
1035
+ console.error(
1036
+ chalk.red(`Failed to list scripts: ${extractApiMessage(body, res.statusText)}`),
1037
+ );
1038
+ process.exit(1);
1039
+ }
1040
+
1041
+ const data = (await res.json()) as SecretPolicyResponse;
1042
+ const policyRecord = normalizePolicyRecord(secretPath, data);
1043
+ renderPolicySummary(policyRecord);
1044
+ renderPolicyScripts(policyRecord.scripts);
1045
+ } catch (err) {
1046
+ console.error(
1047
+ chalk.red("Error:"),
1048
+ err instanceof Error ? err.message : String(err),
1049
+ );
1050
+ process.exit(1);
1051
+ }
1052
+ });
1053
+
529
1054
  secrets
530
1055
  .command("delete <name>")
531
1056
  .description("Delete a secret")
@@ -582,8 +1107,9 @@ export function registerSecretsCommand(program: Command): void {
582
1107
  .command("exec")
583
1108
  .description("Run a command with secrets injected as env vars")
584
1109
  .requiredOption("--only <keys>", "Comma-separated list of secret names to inject (required)")
1110
+ .option("--script <path>", "Attach local script identity for script-locked secrets")
585
1111
  .allowUnknownOption(true)
586
- .action(async (_opts: { only: string }, cmd: Command) => {
1112
+ .action(async (_opts: { only: string; script?: string }, cmd: Command) => {
587
1113
  try {
588
1114
  const rawArgs = cmd.args;
589
1115
  const dashIndex = process.argv.indexOf("--");
@@ -618,7 +1144,12 @@ export function registerSecretsCommand(program: Command): void {
618
1144
  scopeOpts(secrets.opts()),
619
1145
  );
620
1146
 
621
- const revealed = await loadRevealedSecrets(token, companyUid, keys);
1147
+ const revealed = await loadRevealedSecrets(
1148
+ token,
1149
+ companyUid,
1150
+ keys,
1151
+ await buildSecretUsage("exec", _opts.script),
1152
+ );
622
1153
 
623
1154
  const secretEnv: Record<string, string> = {};
624
1155
  for (const key of keys) {
@@ -661,7 +1192,8 @@ export function registerSecretsCommand(program: Command): void {
661
1192
  .command("env")
662
1193
  .description("Print 'export KEY=VALUE' lines suitable for: source <(hq secrets env --only K1,K2)")
663
1194
  .requiredOption("--only <keys>", "Comma-separated list of secret names to print (required)")
664
- .action(async (opts: { only: string }) => {
1195
+ .option("--script <path>", "Attach local script identity for script-locked secrets")
1196
+ .action(async (opts: { only: string; script?: string }) => {
665
1197
  try {
666
1198
  const redact = process.stdout.isTTY;
667
1199
  if (redact) {
@@ -691,7 +1223,12 @@ export function registerSecretsCommand(program: Command): void {
691
1223
  scopeOpts(secrets.opts()),
692
1224
  );
693
1225
 
694
- const revealed = await loadRevealedSecrets(token, companyUid, keys);
1226
+ const revealed = await loadRevealedSecrets(
1227
+ token,
1228
+ companyUid,
1229
+ keys,
1230
+ await buildSecretUsage("env", opts.script),
1231
+ );
695
1232
 
696
1233
  for (const key of keys) {
697
1234
  const value = revealed.get(key);