@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/dist/commands/secrets.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d01cb1a1-b851-5ffe-9608-e505e55dcdc8")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import * as readline from "node:readline";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import * as nodePath from "node:path";
|
|
6
7
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
7
|
-
import { readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
|
|
8
|
+
import { DEFAULT_SECRETS_CACHE_TTL_MS, readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
|
|
9
|
+
import { computeSha256 } from "../utils/integrity.js";
|
|
8
10
|
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN } from "./_patterns.js";
|
|
9
11
|
import { vaultApiFetch, getCompanyUid, getEntityUid, } from "../utils/vault-api.js";
|
|
10
12
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
@@ -128,6 +130,86 @@ function promptSecretInteractively() {
|
|
|
128
130
|
// large --only list is chunked client-side rather than 400'd whole by the
|
|
129
131
|
// server (the legacy per-key GET path had no such cap).
|
|
130
132
|
const MAX_BATCH_NAMES = 100;
|
|
133
|
+
function normalizeSecretTier(tier) {
|
|
134
|
+
return tier === "sensitive" || tier === "nuclear" ? tier : "standard";
|
|
135
|
+
}
|
|
136
|
+
function normalizeScriptLockMode(mode) {
|
|
137
|
+
return mode === "enforced" ? "enforced" : "off";
|
|
138
|
+
}
|
|
139
|
+
function normalizeCacheTtlMs(cacheTtlMs) {
|
|
140
|
+
return typeof cacheTtlMs === "number"
|
|
141
|
+
? cacheTtlMs
|
|
142
|
+
: DEFAULT_SECRETS_CACHE_TTL_MS;
|
|
143
|
+
}
|
|
144
|
+
function extractApiMessage(body, fallback) {
|
|
145
|
+
const message = typeof body.message === "string" ? body.message : undefined;
|
|
146
|
+
const error = typeof body.error === "string" ? body.error : undefined;
|
|
147
|
+
return message ?? error ?? fallback;
|
|
148
|
+
}
|
|
149
|
+
async function buildSecretUsage(channel, scriptPath, scriptId, attestationLevel = "self-asserted-hash") {
|
|
150
|
+
if (!scriptPath) {
|
|
151
|
+
return { channel };
|
|
152
|
+
}
|
|
153
|
+
const resolvedPath = nodePath.resolve(scriptPath);
|
|
154
|
+
return {
|
|
155
|
+
channel,
|
|
156
|
+
script: {
|
|
157
|
+
scriptId: scriptId ?? resolvedPath,
|
|
158
|
+
path: resolvedPath,
|
|
159
|
+
sha256: await computeSha256(resolvedPath),
|
|
160
|
+
attestationLevel,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function normalizePolicyRecord(secretPath, data) {
|
|
165
|
+
const policy = data.policy ?? { path: secretPath };
|
|
166
|
+
const scripts = Array.isArray(policy.scripts)
|
|
167
|
+
? policy.scripts
|
|
168
|
+
: Array.isArray(data.scripts)
|
|
169
|
+
? data.scripts
|
|
170
|
+
: [];
|
|
171
|
+
return {
|
|
172
|
+
path: policy.path ?? secretPath,
|
|
173
|
+
tier: normalizeSecretTier(policy.tier),
|
|
174
|
+
scriptLock: {
|
|
175
|
+
mode: normalizeScriptLockMode(policy.scriptLock?.mode),
|
|
176
|
+
requiredAttestation: policy.scriptLock?.requiredAttestation,
|
|
177
|
+
},
|
|
178
|
+
scripts,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function renderPolicySummary(policy) {
|
|
182
|
+
console.log(chalk.bold(`Policy: ${policy.path}`));
|
|
183
|
+
console.log(` Tier: ${normalizeSecretTier(policy.tier)}`);
|
|
184
|
+
console.log(` Script Lock: ${normalizeScriptLockMode(policy.scriptLock?.mode)}`);
|
|
185
|
+
if (policy.scriptLock?.requiredAttestation) {
|
|
186
|
+
console.log(` Attestation: ${policy.scriptLock.requiredAttestation}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function renderPolicyScripts(scripts) {
|
|
190
|
+
if (scripts.length === 0) {
|
|
191
|
+
console.log(chalk.dim("No approved scripts."));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const idWidth = Math.max(2, ...scripts.map((script) => script.scriptId.length));
|
|
195
|
+
const pathWidth = Math.max(6, ...scripts.map((script) => script.scriptPath.length));
|
|
196
|
+
const attestationWidth = Math.max(11, ...scripts.map((script) => script.attestationLevel.length));
|
|
197
|
+
const header = [
|
|
198
|
+
"ID".padEnd(idWidth),
|
|
199
|
+
"SCRIPT".padEnd(pathWidth),
|
|
200
|
+
"ATTESTATION".padEnd(attestationWidth),
|
|
201
|
+
"SHA256",
|
|
202
|
+
].join(" ");
|
|
203
|
+
console.log(chalk.bold(header));
|
|
204
|
+
for (const script of scripts) {
|
|
205
|
+
console.log([
|
|
206
|
+
script.scriptId.padEnd(idWidth),
|
|
207
|
+
script.scriptPath.padEnd(pathWidth),
|
|
208
|
+
script.attestationLevel.padEnd(attestationWidth),
|
|
209
|
+
script.sha256,
|
|
210
|
+
].join(" "));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
131
213
|
// HQ-4H — load + decrypt secrets through the BATCH-LOAD endpoint
|
|
132
214
|
// (`POST /secrets/{companyUid}/load`) instead of one single-secret GET per key.
|
|
133
215
|
//
|
|
@@ -146,7 +228,7 @@ const MAX_BATCH_NAMES = 100;
|
|
|
146
228
|
// Cache-first (so warm keys cost no request), chunked at MAX_BATCH_NAMES, and
|
|
147
229
|
// throws on the FIRST unresolved key with the same `Failed to fetch secret
|
|
148
230
|
// '<k>': <reason>` shape the per-key GET path used — never swallows a failure.
|
|
149
|
-
export async function loadRevealedSecrets(token, companyUid, keys) {
|
|
231
|
+
export async function loadRevealedSecrets(token, companyUid, keys, usage) {
|
|
150
232
|
const resolved = new Map();
|
|
151
233
|
const missing = [];
|
|
152
234
|
for (const key of keys) {
|
|
@@ -164,18 +246,27 @@ export async function loadRevealedSecrets(token, companyUid, keys) {
|
|
|
164
246
|
token,
|
|
165
247
|
path: `/secrets/${encodeURIComponent(companyUid)}/load`,
|
|
166
248
|
method: "POST",
|
|
167
|
-
body: { names: chunk },
|
|
249
|
+
body: usage ? { names: chunk, usage } : { names: chunk },
|
|
168
250
|
});
|
|
169
251
|
if (!res.ok) {
|
|
170
252
|
const body = (await res.json().catch(() => ({})));
|
|
171
|
-
|
|
253
|
+
const message = extractApiMessage(body, res.statusText);
|
|
254
|
+
if (res.status >= 400 &&
|
|
255
|
+
res.status < 500 &&
|
|
256
|
+
typeof body.code === "string") {
|
|
257
|
+
throw new Error(message);
|
|
258
|
+
}
|
|
259
|
+
throw new Error(`Failed to batch-load secrets: ${message}`);
|
|
172
260
|
}
|
|
173
261
|
const data = (await res.json());
|
|
174
262
|
for (const s of data.secrets ?? []) {
|
|
175
263
|
if (s.value == null) {
|
|
176
264
|
throw new Error(`Secret '${s.name}' has no value (reveal may not be permitted).`);
|
|
177
265
|
}
|
|
178
|
-
|
|
266
|
+
const cacheTtlMs = normalizeCacheTtlMs(s.cacheTtlMs);
|
|
267
|
+
if (cacheTtlMs > 0) {
|
|
268
|
+
writeCache(companyUid, s.name, s.value, cacheTtlMs);
|
|
269
|
+
}
|
|
179
270
|
resolved.set(s.name, s.value);
|
|
180
271
|
}
|
|
181
272
|
const errorsByName = new Map();
|
|
@@ -267,21 +358,26 @@ export function registerSecretsCommand(program) {
|
|
|
267
358
|
try {
|
|
268
359
|
const token = await ensureCognitoToken();
|
|
269
360
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
270
|
-
const query = {};
|
|
271
|
-
if (opts.reveal) {
|
|
272
|
-
query.reveal = "true";
|
|
273
|
-
}
|
|
274
361
|
const res = await vaultApiFetch({
|
|
275
362
|
token,
|
|
276
363
|
path: buildSecretNamePath(companyUid, name),
|
|
277
|
-
query: Object.keys(query).length > 0 ? query : undefined,
|
|
278
364
|
});
|
|
279
365
|
if (!res.ok) {
|
|
280
|
-
const body = await res.json().catch(() => ({}));
|
|
281
|
-
console.error(chalk.red(`Failed to get secret: ${body
|
|
366
|
+
const body = (await res.json().catch(() => ({})));
|
|
367
|
+
console.error(chalk.red(`Failed to get secret: ${extractApiMessage(body, res.statusText)}`));
|
|
282
368
|
process.exit(1);
|
|
283
369
|
}
|
|
284
370
|
const data = (await res.json());
|
|
371
|
+
let revealedValue;
|
|
372
|
+
let revealError = null;
|
|
373
|
+
if (opts.reveal) {
|
|
374
|
+
try {
|
|
375
|
+
revealedValue = (await loadRevealedSecrets(token, companyUid, [name], await buildSecretUsage("reveal"))).get(name);
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
revealError = err instanceof Error ? err.message : String(err);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
285
381
|
const s = data.secret;
|
|
286
382
|
console.log(chalk.bold(`Secret: ${s.name}`));
|
|
287
383
|
if (s.lastModifiedDate) {
|
|
@@ -290,12 +386,18 @@ export function registerSecretsCommand(program) {
|
|
|
290
386
|
if (s.version != null) {
|
|
291
387
|
console.log(` Version: ${s.version}`);
|
|
292
388
|
}
|
|
293
|
-
|
|
294
|
-
|
|
389
|
+
console.log(` Tier: ${normalizeSecretTier(s.tier)}`);
|
|
390
|
+
console.log(` Script Lock: ${normalizeScriptLockMode(s.scriptLock?.mode)}`);
|
|
391
|
+
if (opts.reveal && revealedValue != null) {
|
|
392
|
+
console.log(` Value: ${revealedValue}`);
|
|
295
393
|
}
|
|
296
394
|
else {
|
|
297
395
|
console.log(` Value: ${chalk.dim("[REDACTED]")}`);
|
|
298
396
|
}
|
|
397
|
+
if (revealError) {
|
|
398
|
+
console.error(chalk.red(revealError));
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
299
401
|
}
|
|
300
402
|
catch (err) {
|
|
301
403
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -384,24 +486,244 @@ export function registerSecretsCommand(program) {
|
|
|
384
486
|
}
|
|
385
487
|
const nameWidth = Math.max(4, ...data.secrets.map((s) => s.name.length));
|
|
386
488
|
const hasPermission = data.secrets.some((s) => s.permission !== undefined);
|
|
489
|
+
const tierWidth = Math.max(4, ...data.secrets.map((s) => normalizeSecretTier(s.tier).length));
|
|
490
|
+
const scriptLockWidth = Math.max(11, ...data.secrets.map((s) => normalizeScriptLockMode(s.scriptLock?.mode).length));
|
|
387
491
|
if (hasPermission) {
|
|
388
492
|
const accessWidth = Math.max(6, ...data.secrets.map((s) => (s.permission ?? "-").length));
|
|
389
|
-
const header = `${"NAME".padEnd(nameWidth)} ${"ACCESS".padEnd(accessWidth)} LAST MODIFIED`;
|
|
493
|
+
const header = `${"NAME".padEnd(nameWidth)} ${"ACCESS".padEnd(accessWidth)} ${"TIER".padEnd(tierWidth)} ${"SCRIPT LOCK".padEnd(scriptLockWidth)} LAST MODIFIED`;
|
|
390
494
|
console.log(chalk.bold(header));
|
|
391
495
|
for (const s of data.secrets) {
|
|
392
496
|
const access = s.permission ?? "-";
|
|
497
|
+
const tier = normalizeSecretTier(s.tier);
|
|
498
|
+
const scriptLock = normalizeScriptLockMode(s.scriptLock?.mode);
|
|
393
499
|
const modified = s.lastModifiedDate ?? "-";
|
|
394
|
-
console.log(`${s.name.padEnd(nameWidth)} ${access.padEnd(accessWidth)} ${modified}`);
|
|
500
|
+
console.log(`${s.name.padEnd(nameWidth)} ${access.padEnd(accessWidth)} ${tier.padEnd(tierWidth)} ${scriptLock.padEnd(scriptLockWidth)} ${modified}`);
|
|
395
501
|
}
|
|
396
502
|
}
|
|
397
503
|
else {
|
|
398
|
-
const header = `${"NAME".padEnd(nameWidth)} LAST MODIFIED`;
|
|
504
|
+
const header = `${"NAME".padEnd(nameWidth)} ${"TIER".padEnd(tierWidth)} ${"SCRIPT LOCK".padEnd(scriptLockWidth)} LAST MODIFIED`;
|
|
399
505
|
console.log(chalk.bold(header));
|
|
400
506
|
for (const s of data.secrets) {
|
|
507
|
+
const tier = normalizeSecretTier(s.tier);
|
|
508
|
+
const scriptLock = normalizeScriptLockMode(s.scriptLock?.mode);
|
|
401
509
|
const modified = s.lastModifiedDate ?? "-";
|
|
402
|
-
console.log(`${s.name.padEnd(nameWidth)} ${modified}`);
|
|
510
|
+
console.log(`${s.name.padEnd(nameWidth)} ${tier.padEnd(tierWidth)} ${scriptLock.padEnd(scriptLockWidth)} ${modified}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
const policy = secrets
|
|
520
|
+
.command("policy")
|
|
521
|
+
.description("View or update secret access policy");
|
|
522
|
+
policy
|
|
523
|
+
.command("get <path>")
|
|
524
|
+
.description("Show the policy for a secret path")
|
|
525
|
+
.action(async (secretPath) => {
|
|
526
|
+
try {
|
|
527
|
+
rejectIfPersonal(secrets.opts(), "policy get");
|
|
528
|
+
if (!SECRET_NAME_PATTERN.test(secretPath)) {
|
|
529
|
+
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)`));
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
const token = await ensureCognitoToken();
|
|
533
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
534
|
+
const res = await vaultApiFetch({
|
|
535
|
+
token,
|
|
536
|
+
path: `/secrets/${encodeURIComponent(companyUid)}/policy`,
|
|
537
|
+
query: { path: secretPath },
|
|
538
|
+
});
|
|
539
|
+
if (res.status === 404) {
|
|
540
|
+
renderPolicySummary(normalizePolicyRecord(secretPath, { policy: { path: secretPath } }));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (!res.ok) {
|
|
544
|
+
const body = (await res.json().catch(() => ({})));
|
|
545
|
+
console.error(chalk.red(`Failed to get policy: ${extractApiMessage(body, res.statusText)}`));
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
const data = (await res.json());
|
|
549
|
+
renderPolicySummary(normalizePolicyRecord(secretPath, data));
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
policy
|
|
557
|
+
.command("set <path>")
|
|
558
|
+
.description("Set the policy for a secret path")
|
|
559
|
+
.option("--tier <tier>", "Secret tier: standard | sensitive | nuclear")
|
|
560
|
+
.option("--lock-script <mode>", "Script-lock mode: off | enforce")
|
|
561
|
+
.action(async (secretPath, opts) => {
|
|
562
|
+
try {
|
|
563
|
+
rejectIfPersonal(secrets.opts(), "policy set");
|
|
564
|
+
if (!SECRET_NAME_PATTERN.test(secretPath)) {
|
|
565
|
+
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)`));
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
const tier = opts.tier;
|
|
569
|
+
if (tier !== undefined &&
|
|
570
|
+
tier !== "standard" &&
|
|
571
|
+
tier !== "sensitive" &&
|
|
572
|
+
tier !== "nuclear") {
|
|
573
|
+
console.error(chalk.red(`Invalid tier '${tier}': must be one of standard, sensitive, nuclear`));
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
let scriptLock;
|
|
577
|
+
if (opts.lockScript !== undefined) {
|
|
578
|
+
if (opts.lockScript === "off") {
|
|
579
|
+
scriptLock = { mode: "off" };
|
|
580
|
+
}
|
|
581
|
+
else if (opts.lockScript === "enforce") {
|
|
582
|
+
scriptLock = { mode: "enforced" };
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
console.error(chalk.red(`Invalid script lock mode '${opts.lockScript}': must be one of off, enforce`));
|
|
586
|
+
process.exit(1);
|
|
403
587
|
}
|
|
404
588
|
}
|
|
589
|
+
if (tier === undefined && scriptLock === undefined) {
|
|
590
|
+
console.error(chalk.red("Error: provide at least one of --tier or --lock-script."));
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
const token = await ensureCognitoToken();
|
|
594
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
595
|
+
const res = await vaultApiFetch({
|
|
596
|
+
token,
|
|
597
|
+
path: `/secrets/${encodeURIComponent(companyUid)}/policy`,
|
|
598
|
+
method: "PUT",
|
|
599
|
+
body: {
|
|
600
|
+
path: secretPath,
|
|
601
|
+
...(tier !== undefined ? { tier } : {}),
|
|
602
|
+
...(scriptLock ? { scriptLock } : {}),
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
if (!res.ok) {
|
|
606
|
+
const body = (await res.json().catch(() => ({})));
|
|
607
|
+
console.error(chalk.red(`Failed to set policy: ${extractApiMessage(body, res.statusText)}`));
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
const data = (await res.json().catch(() => ({})));
|
|
611
|
+
console.log(chalk.green(`Policy updated for '${secretPath}'.`));
|
|
612
|
+
renderPolicySummary(normalizePolicyRecord(secretPath, data.policy ? data : { policy: { path: secretPath, tier, scriptLock } }));
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
const script = secrets
|
|
620
|
+
.command("script")
|
|
621
|
+
.description("Manage approved scripts for script-locked secrets");
|
|
622
|
+
script
|
|
623
|
+
.command("approve <path>")
|
|
624
|
+
.description("Approve a script for a secret path")
|
|
625
|
+
.requiredOption("--id <scriptId>", "Stable script identifier")
|
|
626
|
+
.requiredOption("--script <path>", "Path to the local script file")
|
|
627
|
+
.option("--attestation <level>", "Attestation level", "self-asserted-hash")
|
|
628
|
+
.action(async (secretPath, opts) => {
|
|
629
|
+
try {
|
|
630
|
+
rejectIfPersonal(secrets.opts(), "script approve");
|
|
631
|
+
if (!SECRET_NAME_PATTERN.test(secretPath)) {
|
|
632
|
+
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)`));
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
const usage = await buildSecretUsage("exec", opts.script, opts.id, opts.attestation);
|
|
636
|
+
const token = await ensureCognitoToken();
|
|
637
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
638
|
+
const res = await vaultApiFetch({
|
|
639
|
+
token,
|
|
640
|
+
path: `/secrets/${encodeURIComponent(companyUid)}/policy/scripts`,
|
|
641
|
+
method: "POST",
|
|
642
|
+
body: {
|
|
643
|
+
path: secretPath,
|
|
644
|
+
scriptId: usage.script?.scriptId,
|
|
645
|
+
scriptPath: usage.script?.path,
|
|
646
|
+
sha256: usage.script?.sha256,
|
|
647
|
+
attestationLevel: usage.script?.attestationLevel,
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
if (!res.ok) {
|
|
651
|
+
const body = (await res.json().catch(() => ({})));
|
|
652
|
+
console.error(chalk.red(`Failed to approve script: ${extractApiMessage(body, res.statusText)}`));
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
console.log(chalk.green(`Approved script '${opts.id}' for '${secretPath}'.`));
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
script
|
|
663
|
+
.command("revoke <path>")
|
|
664
|
+
.description("Revoke an approved script from a secret path")
|
|
665
|
+
.requiredOption("--id <scriptId>", "Stable script identifier")
|
|
666
|
+
.action(async (secretPath, opts) => {
|
|
667
|
+
try {
|
|
668
|
+
rejectIfPersonal(secrets.opts(), "script revoke");
|
|
669
|
+
if (!SECRET_NAME_PATTERN.test(secretPath)) {
|
|
670
|
+
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)`));
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
const token = await ensureCognitoToken();
|
|
674
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
675
|
+
const res = await vaultApiFetch({
|
|
676
|
+
token,
|
|
677
|
+
path: `/secrets/${encodeURIComponent(companyUid)}/policy/scripts`,
|
|
678
|
+
method: "DELETE",
|
|
679
|
+
body: { path: secretPath, scriptId: opts.id },
|
|
680
|
+
});
|
|
681
|
+
if (!res.ok) {
|
|
682
|
+
const body = (await res.json().catch(() => ({})));
|
|
683
|
+
console.error(chalk.red(`Failed to revoke script: ${extractApiMessage(body, res.statusText)}`));
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
console.log(chalk.green(`Revoked script '${opts.id}' for '${secretPath}'.`));
|
|
687
|
+
}
|
|
688
|
+
catch (err) {
|
|
689
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
script
|
|
694
|
+
.command("list <path>")
|
|
695
|
+
.description("List approved scripts for a secret path")
|
|
696
|
+
.action(async (secretPath) => {
|
|
697
|
+
try {
|
|
698
|
+
rejectIfPersonal(secrets.opts(), "script list");
|
|
699
|
+
if (!SECRET_NAME_PATTERN.test(secretPath)) {
|
|
700
|
+
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)`));
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
const token = await ensureCognitoToken();
|
|
704
|
+
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
705
|
+
const res = await vaultApiFetch({
|
|
706
|
+
token,
|
|
707
|
+
path: `/secrets/${encodeURIComponent(companyUid)}/policy`,
|
|
708
|
+
query: { path: secretPath },
|
|
709
|
+
});
|
|
710
|
+
if (res.status === 404) {
|
|
711
|
+
const policyRecord = normalizePolicyRecord(secretPath, {
|
|
712
|
+
policy: { path: secretPath },
|
|
713
|
+
});
|
|
714
|
+
renderPolicySummary(policyRecord);
|
|
715
|
+
renderPolicyScripts(policyRecord.scripts);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (!res.ok) {
|
|
719
|
+
const body = (await res.json().catch(() => ({})));
|
|
720
|
+
console.error(chalk.red(`Failed to list scripts: ${extractApiMessage(body, res.statusText)}`));
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
const data = (await res.json());
|
|
724
|
+
const policyRecord = normalizePolicyRecord(secretPath, data);
|
|
725
|
+
renderPolicySummary(policyRecord);
|
|
726
|
+
renderPolicyScripts(policyRecord.scripts);
|
|
405
727
|
}
|
|
406
728
|
catch (err) {
|
|
407
729
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -449,6 +771,7 @@ export function registerSecretsCommand(program) {
|
|
|
449
771
|
.command("exec")
|
|
450
772
|
.description("Run a command with secrets injected as env vars")
|
|
451
773
|
.requiredOption("--only <keys>", "Comma-separated list of secret names to inject (required)")
|
|
774
|
+
.option("--script <path>", "Attach local script identity for script-locked secrets")
|
|
452
775
|
.allowUnknownOption(true)
|
|
453
776
|
.action(async (_opts, cmd) => {
|
|
454
777
|
try {
|
|
@@ -478,7 +801,7 @@ export function registerSecretsCommand(program) {
|
|
|
478
801
|
}
|
|
479
802
|
const token = await ensureCognitoToken();
|
|
480
803
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
481
|
-
const revealed = await loadRevealedSecrets(token, companyUid, keys);
|
|
804
|
+
const revealed = await loadRevealedSecrets(token, companyUid, keys, await buildSecretUsage("exec", _opts.script));
|
|
482
805
|
const secretEnv = {};
|
|
483
806
|
for (const key of keys) {
|
|
484
807
|
const value = revealed.get(key);
|
|
@@ -514,6 +837,7 @@ export function registerSecretsCommand(program) {
|
|
|
514
837
|
.command("env")
|
|
515
838
|
.description("Print 'export KEY=VALUE' lines suitable for: source <(hq secrets env --only K1,K2)")
|
|
516
839
|
.requiredOption("--only <keys>", "Comma-separated list of secret names to print (required)")
|
|
840
|
+
.option("--script <path>", "Attach local script identity for script-locked secrets")
|
|
517
841
|
.action(async (opts) => {
|
|
518
842
|
try {
|
|
519
843
|
const redact = process.stdout.isTTY;
|
|
@@ -533,7 +857,7 @@ export function registerSecretsCommand(program) {
|
|
|
533
857
|
}
|
|
534
858
|
const token = await ensureCognitoToken();
|
|
535
859
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
536
|
-
const revealed = await loadRevealedSecrets(token, companyUid, keys);
|
|
860
|
+
const revealed = await loadRevealedSecrets(token, companyUid, keys, await buildSecretUsage("env", opts.script));
|
|
537
861
|
for (const key of keys) {
|
|
538
862
|
const value = revealed.get(key);
|
|
539
863
|
// loadRevealedSecrets throws on any unresolved key, so a miss here is
|
|
@@ -801,4 +1125,4 @@ export function registerSecretsCommand(program) {
|
|
|
801
1125
|
});
|
|
802
1126
|
}
|
|
803
1127
|
//# sourceMappingURL=secrets.js.map
|
|
804
|
-
//# debugId=
|
|
1128
|
+
//# debugId=d01cb1a1-b851-5ffe-9608-e505e55dcdc8
|
package/dist/run/hq-plugin.d.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
+
import type { SecretLoadResponse, SecretUsage } from '../commands/secrets.js';
|
|
1
2
|
export interface InstallHqPluginOpts {
|
|
2
3
|
companyOverride?: string;
|
|
3
4
|
resolveCompanyUid: (slug: string) => Promise<string>;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
name: string;
|
|
7
|
-
value: string;
|
|
8
|
-
}>;
|
|
9
|
-
errors: Array<{
|
|
10
|
-
name: string;
|
|
11
|
-
code: string;
|
|
12
|
-
message?: string;
|
|
13
|
-
}>;
|
|
14
|
-
}>;
|
|
5
|
+
usage?: SecretUsage;
|
|
6
|
+
fetchBatch: (uid: string, names: string[], usage?: SecretUsage) => Promise<SecretLoadResponse>;
|
|
15
7
|
}
|
|
16
8
|
export interface PluginState {
|
|
17
9
|
schemaCompanySlug: string | null;
|
|
@@ -20,6 +12,7 @@ export interface PluginState {
|
|
|
20
12
|
code: string;
|
|
21
13
|
message?: string;
|
|
22
14
|
}>;
|
|
15
|
+
loadedSecretsByName: Map<string, string>;
|
|
23
16
|
}
|
|
24
17
|
export declare function installHqPlugin(graph: any, opts: InstallHqPluginOpts): PluginState;
|
|
25
18
|
export declare function prewarmHqSecrets(graph: any, opts: InstallHqPluginOpts, state: PluginState): Promise<void>;
|
package/dist/run/hq-plugin.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="63aeb21a-b1e7-5bff-8c7d-560fad3cc0dd")}catch(e){}}();
|
|
3
3
|
import { ResolutionError } from 'varlock/plugin-lib';
|
|
4
|
-
import { readCache, writeCache } from '../utils/secrets-cache.js';
|
|
4
|
+
import { DEFAULT_SECRETS_CACHE_TTL_MS, readCache, writeCache, } from '../utils/secrets-cache.js';
|
|
5
|
+
function normalizeCacheTtlMs(cacheTtlMs) {
|
|
6
|
+
return typeof cacheTtlMs === 'number'
|
|
7
|
+
? cacheTtlMs
|
|
8
|
+
: DEFAULT_SECRETS_CACHE_TTL_MS;
|
|
9
|
+
}
|
|
5
10
|
export function installHqPlugin(graph /* EnvGraph */, opts) {
|
|
6
11
|
const pluginState = {
|
|
7
12
|
schemaCompanySlug: null,
|
|
8
13
|
uid: null,
|
|
9
14
|
errorsByName: new Map(),
|
|
15
|
+
loadedSecretsByName: new Map(),
|
|
10
16
|
};
|
|
11
17
|
// varlock@1.0.0's plugin-lib.js omits the Resolver export (d.ts/JS mismatch);
|
|
12
18
|
// extract it at runtime from any already-registered built-in resolver's prototype.
|
|
@@ -63,6 +69,10 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
|
|
|
63
69
|
if (pluginState.uid == null) {
|
|
64
70
|
throw new ResolutionError('Internal error: prewarmHqSecrets was not called before resolveEnvValues');
|
|
65
71
|
}
|
|
72
|
+
const inMemory = pluginState.loadedSecretsByName.get(secretName);
|
|
73
|
+
if (inMemory != null) {
|
|
74
|
+
return inMemory;
|
|
75
|
+
}
|
|
66
76
|
const cached = readCache(pluginState.uid, secretName); // string | null
|
|
67
77
|
if (cached == null) {
|
|
68
78
|
throw new ResolutionError(`Internal error: pre-warm did not populate cache for "${secretName}"`);
|
|
@@ -129,9 +139,16 @@ export async function prewarmHqSecrets(graph /* EnvGraph */, opts, state) {
|
|
|
129
139
|
if (uniqueNames.length > 100) {
|
|
130
140
|
throw new Error(`hq run supports at most 100 hq() resolvers per schema; got ${uniqueNames.length}`);
|
|
131
141
|
}
|
|
132
|
-
const result = await opts.fetchBatch(uid, uniqueNames);
|
|
142
|
+
const result = await opts.fetchBatch(uid, uniqueNames, opts.usage);
|
|
133
143
|
for (const s of result.secrets) {
|
|
134
|
-
|
|
144
|
+
if (s.value == null) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
state.loadedSecretsByName.set(s.name, s.value);
|
|
148
|
+
const cacheTtlMs = normalizeCacheTtlMs(s.cacheTtlMs);
|
|
149
|
+
if (cacheTtlMs > 0) {
|
|
150
|
+
writeCache(uid, s.name, s.value, cacheTtlMs);
|
|
151
|
+
}
|
|
135
152
|
}
|
|
136
153
|
const errorsByName = new Map();
|
|
137
154
|
for (const e of result.errors) {
|
|
@@ -141,4 +158,4 @@ export async function prewarmHqSecrets(graph /* EnvGraph */, opts, state) {
|
|
|
141
158
|
state.uid = uid;
|
|
142
159
|
}
|
|
143
160
|
//# sourceMappingURL=hq-plugin.js.map
|
|
144
|
-
//# debugId=
|
|
161
|
+
//# debugId=63aeb21a-b1e7-5bff-8c7d-560fad3cc0dd
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Integrity verification — SHA256 hash and RSA signature checks (US-005)
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Verify a file's SHA256 hash matches the expected value.
|
|
6
|
+
*/
|
|
7
|
+
export declare function computeSha256(filePath: string): Promise<string>;
|
|
4
8
|
/**
|
|
5
9
|
* Verify a file's SHA256 hash matches the expected value.
|
|
6
10
|
*/
|
package/dist/utils/integrity.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Integrity verification — SHA256 hash and RSA signature checks (US-005)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
5
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e47a47a7-a9cf-56f1-9522-983ebaa0e5cb")}catch(e){}}();
|
|
6
6
|
import * as crypto from 'crypto';
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
@@ -10,18 +10,22 @@ import { resolveDefaultHqRoot } from './cognito-session.js';
|
|
|
10
10
|
/**
|
|
11
11
|
* Verify a file's SHA256 hash matches the expected value.
|
|
12
12
|
*/
|
|
13
|
-
export async function
|
|
13
|
+
export async function computeSha256(filePath) {
|
|
14
14
|
return new Promise((resolve, reject) => {
|
|
15
15
|
const hash = crypto.createHash('sha256');
|
|
16
16
|
const stream = fs.createReadStream(filePath);
|
|
17
17
|
stream.on('data', (chunk) => hash.update(chunk));
|
|
18
|
-
stream.on('end', () =>
|
|
19
|
-
const computed = hash.digest('hex');
|
|
20
|
-
resolve(computed === expectedHash.toLowerCase());
|
|
21
|
-
});
|
|
18
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
22
19
|
stream.on('error', reject);
|
|
23
20
|
});
|
|
24
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Verify a file's SHA256 hash matches the expected value.
|
|
24
|
+
*/
|
|
25
|
+
export async function verifySha256(filePath, expectedHash) {
|
|
26
|
+
const computed = await computeSha256(filePath);
|
|
27
|
+
return computed === expectedHash.toLowerCase();
|
|
28
|
+
}
|
|
25
29
|
/**
|
|
26
30
|
* Verify an RSA signature of a SHA256 hash using the registry public key.
|
|
27
31
|
* The public key is expected at packages/.keys/registry-public.pem.
|
|
@@ -40,4 +44,4 @@ export function verifyRsaSignature(sha256Hash, signature, publicKeyPath) {
|
|
|
40
44
|
return verifier.verify(publicKey, Buffer.from(signature, 'base64'));
|
|
41
45
|
}
|
|
42
46
|
//# sourceMappingURL=integrity.js.map
|
|
43
|
-
//# debugId=
|
|
47
|
+
//# debugId=e47a47a7-a9cf-56f1-9522-983ebaa0e5cb
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export declare const DEFAULT_SECRETS_CACHE_TTL_MS: number;
|
|
1
2
|
export declare function readCache(companyUid: string, name: string): string | null;
|
|
2
|
-
export declare function writeCache(companyUid: string, name: string, value: string): void;
|
|
3
|
+
export declare function writeCache(companyUid: string, name: string, value: string, ttlMs?: number): void;
|
|
3
4
|
export declare function removeCacheEntry(companyUid: string, name: string): void;
|
|
4
5
|
export declare function clearAllCache(): {
|
|
5
6
|
removed: number;
|