@indigoai-us/hq-cli 5.47.10 → 5.47.11
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/commands/members.js +38 -2
- package/dist/commands/run.js +34 -5
- package/dist/commands/secrets.d.ts +35 -1
- package/dist/commands/secrets.js +346 -22
- package/dist/run/hq-plugin.d.ts +4 -11
- package/dist/run/hq-plugin.js +22 -5
- package/dist/utils/integrity.d.ts +4 -0
- package/dist/utils/integrity.js +11 -7
- package/dist/utils/secrets-cache.d.ts +2 -1
- package/dist/utils/secrets-cache.js +42 -14
- package/package.json +1 -1
- package/src/commands/members.test.ts +92 -1
- package/src/commands/members.ts +61 -0
- package/src/commands/run.env-local.test.ts +5 -1
- package/src/commands/run.ts +40 -9
- package/src/commands/secrets.test.ts +394 -4
- package/src/commands/secrets.ts +574 -37
- package/src/run/hq-plugin.test.ts +31 -0
- package/src/run/hq-plugin.ts +33 -7
- package/src/utils/integrity.ts +13 -8
- package/src/utils/secrets-cache.ts +45 -11
package/src/commands/secrets.ts
CHANGED
|
@@ -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,
|
|
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: ${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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:
|
|
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(
|
|
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
|
-
.
|
|
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(
|
|
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);
|