@indigoai-us/hq-cli 5.5.2 → 5.5.3
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/bin/hq-auth-refresh.js +26 -13
- package/dist/commands/_patterns.d.ts +3 -0
- package/dist/commands/_patterns.js +6 -0
- package/dist/commands/add.js +4 -1
- package/dist/commands/auth.js +4 -1
- package/dist/commands/cloud.js +4 -1
- package/dist/commands/groups.d.ts +3 -0
- package/dist/commands/groups.js +344 -0
- package/dist/commands/list.js +4 -1
- package/dist/commands/login.js +4 -1
- package/dist/commands/logout.js +4 -1
- package/dist/commands/onboard.js +4 -1
- package/dist/commands/pack-install.js +4 -1
- package/dist/commands/pkg-install.js +4 -1
- package/dist/commands/pkg-list.js +4 -1
- package/dist/commands/pkg-remove.js +4 -1
- package/dist/commands/pkg-update.js +4 -1
- package/dist/commands/secrets.d.ts +9 -0
- package/dist/commands/secrets.js +263 -25
- package/dist/commands/sync.js +4 -1
- package/dist/commands/team-sync.js +4 -1
- package/dist/commands/update.js +4 -1
- package/dist/commands/whoami.js +4 -1
- package/dist/index.js +21 -2
- package/dist/sentry-before-send.d.ts +3 -0
- package/dist/sentry-before-send.js +219 -0
- package/dist/sentry-dsn.generated.d.ts +2 -0
- package/dist/sentry-dsn.generated.js +5 -0
- package/dist/sentry.d.ts +4 -0
- package/dist/sentry.js +22 -0
- package/dist/strategies/link.js +4 -1
- package/dist/strategies/merge.js +4 -1
- package/dist/types.js +4 -1
- package/dist/utils/cognito-session.js +4 -1
- package/dist/utils/git.js +4 -1
- package/dist/utils/hq-root.js +4 -1
- package/dist/utils/integrity.js +4 -1
- package/dist/utils/manifest.js +4 -1
- package/dist/utils/registry-client.js +4 -1
- package/dist/utils/registry.js +4 -1
- package/dist/utils/secrets-cache.js +6 -2
- package/package.json +4 -3
- package/scripts/generate-dsn.mjs +26 -0
- package/src/bin/hq-auth-refresh.ts +23 -16
- package/src/commands/_patterns.ts +2 -0
- package/src/commands/groups.ts +357 -0
- package/src/commands/secrets.ts +306 -26
- package/src/index.ts +17 -1
- package/src/sentry-before-send.test.ts +359 -0
- package/src/sentry-before-send.ts +221 -0
- package/src/sentry.test.ts +24 -0
- package/src/sentry.ts +19 -0
- package/src/utils/secrets-cache.ts +2 -1
- package/dist/bin/hq-auth-refresh.d.ts.map +0 -1
- package/dist/bin/hq-auth-refresh.js.map +0 -1
- package/dist/commands/add.d.ts.map +0 -1
- package/dist/commands/add.js.map +0 -1
- package/dist/commands/auth.d.ts.map +0 -1
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/cloud.d.ts.map +0 -1
- package/dist/commands/cloud.js.map +0 -1
- package/dist/commands/list.d.ts.map +0 -1
- package/dist/commands/list.js.map +0 -1
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts.map +0 -1
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/onboard.d.ts.map +0 -1
- package/dist/commands/onboard.js.map +0 -1
- package/dist/commands/pack-install.d.ts.map +0 -1
- package/dist/commands/pack-install.js.map +0 -1
- package/dist/commands/pkg-install.d.ts.map +0 -1
- package/dist/commands/pkg-install.js.map +0 -1
- package/dist/commands/pkg-list.d.ts.map +0 -1
- package/dist/commands/pkg-list.js.map +0 -1
- package/dist/commands/pkg-remove.d.ts.map +0 -1
- package/dist/commands/pkg-remove.js.map +0 -1
- package/dist/commands/pkg-update.d.ts.map +0 -1
- package/dist/commands/pkg-update.js.map +0 -1
- package/dist/commands/secrets.d.ts.map +0 -1
- package/dist/commands/secrets.js.map +0 -1
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.js.map +0 -1
- package/dist/commands/team-sync.d.ts.map +0 -1
- package/dist/commands/team-sync.js.map +0 -1
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/whoami.d.ts.map +0 -1
- package/dist/commands/whoami.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/strategies/link.d.ts.map +0 -1
- package/dist/strategies/link.js.map +0 -1
- package/dist/strategies/merge.d.ts.map +0 -1
- package/dist/strategies/merge.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils/auth.d.ts +0 -27
- package/dist/utils/auth.d.ts.map +0 -1
- package/dist/utils/auth.js +0 -155
- package/dist/utils/auth.js.map +0 -1
- package/dist/utils/cognito-session.d.ts.map +0 -1
- package/dist/utils/cognito-session.js.map +0 -1
- package/dist/utils/git.d.ts.map +0 -1
- package/dist/utils/git.js.map +0 -1
- package/dist/utils/hq-root.d.ts.map +0 -1
- package/dist/utils/hq-root.js.map +0 -1
- package/dist/utils/integrity.d.ts.map +0 -1
- package/dist/utils/integrity.js.map +0 -1
- package/dist/utils/manifest.d.ts.map +0 -1
- package/dist/utils/manifest.js.map +0 -1
- package/dist/utils/registry-client.d.ts.map +0 -1
- package/dist/utils/registry-client.js.map +0 -1
- package/dist/utils/registry.d.ts.map +0 -1
- package/dist/utils/registry.js.map +0 -1
- package/dist/utils/secrets-cache.d.ts.map +0 -1
- package/dist/utils/secrets-cache.js.map +0 -1
- package/dist/utils/token-store.d.ts +0 -28
- package/dist/utils/token-store.d.ts.map +0 -1
- package/dist/utils/token-store.js +0 -68
- package/dist/utils/token-store.js.map +0 -1
package/src/commands/secrets.ts
CHANGED
|
@@ -12,8 +12,9 @@ import {
|
|
|
12
12
|
removeCacheEntry,
|
|
13
13
|
clearAllCache,
|
|
14
14
|
} from "../utils/secrets-cache.js";
|
|
15
|
+
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN } from "./_patterns.js";
|
|
15
16
|
|
|
16
|
-
interface VaultApiOptions {
|
|
17
|
+
export interface VaultApiOptions {
|
|
17
18
|
token: string;
|
|
18
19
|
path: string;
|
|
19
20
|
method?: string;
|
|
@@ -25,7 +26,18 @@ function shellSingleQuote(value: string): string {
|
|
|
25
26
|
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
// API Gateway v2 URL-decodes `%2F` before route matching, so a full-name
|
|
30
|
+
// `encodeURIComponent` would collapse structural `/` separators into a single
|
|
31
|
+
// segment the `/name/{proxy+}` route can't match.
|
|
32
|
+
function buildSecretNamePath(companyUid: string, name: string): string {
|
|
33
|
+
const encodedName = name
|
|
34
|
+
.split("/")
|
|
35
|
+
.map(encodeURIComponent)
|
|
36
|
+
.join("/");
|
|
37
|
+
return `/secrets/${encodeURIComponent(companyUid)}/name/${encodedName}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function vaultApiFetch(opts: VaultApiOptions): Promise<Response> {
|
|
29
41
|
const url = new URL(opts.path, DEFAULT_VAULT_API_URL);
|
|
30
42
|
if (opts.query) {
|
|
31
43
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -91,7 +103,7 @@ async function resolveCompanyFromMemberships(
|
|
|
91
103
|
);
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
async function getCompanyUid(
|
|
106
|
+
export async function getCompanyUid(
|
|
95
107
|
token: string,
|
|
96
108
|
companySlug: string | undefined,
|
|
97
109
|
): Promise<string> {
|
|
@@ -205,8 +217,8 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
205
217
|
.option("--from-stdin", "Read secret value from piped stdin")
|
|
206
218
|
.action(async (name: string, opts: { fromStdin?: boolean }) => {
|
|
207
219
|
try {
|
|
208
|
-
if (
|
|
209
|
-
console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*$ (e.g. MY_API_KEY)`));
|
|
220
|
+
if (!SECRET_NAME_PATTERN.test(name)) {
|
|
221
|
+
console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
|
|
210
222
|
process.exit(1);
|
|
211
223
|
}
|
|
212
224
|
|
|
@@ -282,7 +294,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
282
294
|
|
|
283
295
|
const res = await vaultApiFetch({
|
|
284
296
|
token,
|
|
285
|
-
path:
|
|
297
|
+
path: buildSecretNamePath(companyUid, name),
|
|
286
298
|
query: Object.keys(query).length > 0 ? query : undefined,
|
|
287
299
|
});
|
|
288
300
|
|
|
@@ -329,16 +341,44 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
329
341
|
|
|
330
342
|
secrets
|
|
331
343
|
.command("list")
|
|
332
|
-
.description("List all secrets for the company")
|
|
333
|
-
.
|
|
344
|
+
.description("List all secrets for the company (including nested path-based names)")
|
|
345
|
+
.option(
|
|
346
|
+
"--prefix <path>",
|
|
347
|
+
"Filter by path prefix (e.g. DEV or DEV/SUB); omit or pass empty string for all secrets",
|
|
348
|
+
)
|
|
349
|
+
.action(async (opts: { prefix?: string }) => {
|
|
334
350
|
try {
|
|
351
|
+
// MUST match SECRET_PREFIX_PATTERN in hq-pro/src/vault-service/handlers/secrets.ts
|
|
352
|
+
let normalizedPrefix: string | undefined;
|
|
353
|
+
if (opts.prefix) {
|
|
354
|
+
const normalized = opts.prefix.replace(/^\/+|\/+$/g, "");
|
|
355
|
+
if (
|
|
356
|
+
normalized.length === 0 ||
|
|
357
|
+
!SECRET_NAME_PATTERN.test(normalized)
|
|
358
|
+
) {
|
|
359
|
+
console.error(
|
|
360
|
+
chalk.red(
|
|
361
|
+
`Invalid prefix '${opts.prefix}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. DEV or DEV/SUB)`,
|
|
362
|
+
),
|
|
363
|
+
);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
normalizedPrefix = normalized;
|
|
367
|
+
}
|
|
368
|
+
|
|
335
369
|
const token = await ensureCognitoToken();
|
|
336
370
|
const companySlug = secrets.opts().company as string | undefined;
|
|
337
371
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
338
372
|
|
|
373
|
+
const query: Record<string, string> = {};
|
|
374
|
+
if (normalizedPrefix) {
|
|
375
|
+
query.prefix = normalizedPrefix;
|
|
376
|
+
}
|
|
377
|
+
|
|
339
378
|
const res = await vaultApiFetch({
|
|
340
379
|
token,
|
|
341
380
|
path: `/secrets/${encodeURIComponent(companyUid)}`,
|
|
381
|
+
query: Object.keys(query).length > 0 ? query : undefined,
|
|
342
382
|
});
|
|
343
383
|
|
|
344
384
|
if (!res.ok) {
|
|
@@ -350,7 +390,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
350
390
|
}
|
|
351
391
|
|
|
352
392
|
const data = (await res.json()) as {
|
|
353
|
-
secrets: { name: string; lastModifiedDate?: string; version?: number }[];
|
|
393
|
+
secrets: { name: string; lastModifiedDate?: string; version?: number; permission?: "admin" | "write" | "read" }[];
|
|
354
394
|
};
|
|
355
395
|
|
|
356
396
|
if (data.secrets.length === 0) {
|
|
@@ -359,11 +399,23 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
359
399
|
}
|
|
360
400
|
|
|
361
401
|
const nameWidth = Math.max(4, ...data.secrets.map((s) => s.name.length));
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
console.log(
|
|
402
|
+
const hasPermission = data.secrets.some((s) => s.permission !== undefined);
|
|
403
|
+
if (hasPermission) {
|
|
404
|
+
const accessWidth = Math.max(6, ...data.secrets.map((s) => (s.permission ?? "-").length));
|
|
405
|
+
const header = `${"NAME".padEnd(nameWidth)} ${"ACCESS".padEnd(accessWidth)} LAST MODIFIED`;
|
|
406
|
+
console.log(chalk.bold(header));
|
|
407
|
+
for (const s of data.secrets) {
|
|
408
|
+
const access = s.permission ?? "-";
|
|
409
|
+
const modified = s.lastModifiedDate ?? "-";
|
|
410
|
+
console.log(`${s.name.padEnd(nameWidth)} ${access.padEnd(accessWidth)} ${modified}`);
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
const header = `${"NAME".padEnd(nameWidth)} LAST MODIFIED`;
|
|
414
|
+
console.log(chalk.bold(header));
|
|
415
|
+
for (const s of data.secrets) {
|
|
416
|
+
const modified = s.lastModifiedDate ?? "-";
|
|
417
|
+
console.log(`${s.name.padEnd(nameWidth)} ${modified}`);
|
|
418
|
+
}
|
|
367
419
|
}
|
|
368
420
|
} catch (err) {
|
|
369
421
|
console.error(
|
|
@@ -380,8 +432,8 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
380
432
|
.option("--force", "Skip confirmation prompt")
|
|
381
433
|
.action(async (name: string, opts: { force?: boolean }) => {
|
|
382
434
|
try {
|
|
383
|
-
if (
|
|
384
|
-
console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*$ (e.g. MY_API_KEY)`));
|
|
435
|
+
if (!SECRET_NAME_PATTERN.test(name)) {
|
|
436
|
+
console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
|
|
385
437
|
process.exit(1);
|
|
386
438
|
}
|
|
387
439
|
|
|
@@ -401,7 +453,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
401
453
|
|
|
402
454
|
const res = await vaultApiFetch({
|
|
403
455
|
token,
|
|
404
|
-
path:
|
|
456
|
+
path: buildSecretNamePath(companyUid, name),
|
|
405
457
|
method: "DELETE",
|
|
406
458
|
});
|
|
407
459
|
|
|
@@ -452,8 +504,8 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
452
504
|
}
|
|
453
505
|
|
|
454
506
|
for (const key of keys) {
|
|
455
|
-
if (
|
|
456
|
-
console.error(chalk.red(`Invalid secret name '${key}': must match ^[A-Z][A-Z0-9_]*$`));
|
|
507
|
+
if (!SECRET_NAME_PATTERN.test(key)) {
|
|
508
|
+
console.error(chalk.red(`Invalid secret name '${key}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$`));
|
|
457
509
|
process.exit(1);
|
|
458
510
|
}
|
|
459
511
|
}
|
|
@@ -470,7 +522,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
470
522
|
}
|
|
471
523
|
const res = await vaultApiFetch({
|
|
472
524
|
token,
|
|
473
|
-
path:
|
|
525
|
+
path: buildSecretNamePath(companyUid, key),
|
|
474
526
|
query: { reveal: "true" },
|
|
475
527
|
});
|
|
476
528
|
if (!res.ok) {
|
|
@@ -543,8 +595,8 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
543
595
|
}
|
|
544
596
|
|
|
545
597
|
for (const key of keys) {
|
|
546
|
-
if (
|
|
547
|
-
console.error(chalk.red(`Invalid secret name '${key}': must match ^[A-Z][A-Z0-9_]*$`));
|
|
598
|
+
if (!SECRET_NAME_PATTERN.test(key)) {
|
|
599
|
+
console.error(chalk.red(`Invalid secret name '${key}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$`));
|
|
548
600
|
process.exit(1);
|
|
549
601
|
}
|
|
550
602
|
}
|
|
@@ -561,7 +613,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
561
613
|
}
|
|
562
614
|
const res = await vaultApiFetch({
|
|
563
615
|
token,
|
|
564
|
-
path:
|
|
616
|
+
path: buildSecretNamePath(companyUid, key),
|
|
565
617
|
query: { reveal: "true" },
|
|
566
618
|
});
|
|
567
619
|
if (!res.ok) {
|
|
@@ -600,8 +652,8 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
600
652
|
.option("--expires <duration>", "Token expiry duration (e.g. 24h, 2d, 30m)", "24h")
|
|
601
653
|
.action(async (name: string, opts: { expires: string }) => {
|
|
602
654
|
try {
|
|
603
|
-
if (
|
|
604
|
-
console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*$ (e.g. MY_API_KEY)`));
|
|
655
|
+
if (!SECRET_NAME_PATTERN.test(name)) {
|
|
656
|
+
console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
|
|
605
657
|
process.exit(1);
|
|
606
658
|
}
|
|
607
659
|
|
|
@@ -623,9 +675,10 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
623
675
|
|
|
624
676
|
const res = await vaultApiFetch({
|
|
625
677
|
token,
|
|
626
|
-
path:
|
|
678
|
+
path: buildSecretNamePath(companyUid, name),
|
|
627
679
|
method: "POST",
|
|
628
680
|
body: { expiresInMs },
|
|
681
|
+
query: { action: "generate-token" },
|
|
629
682
|
});
|
|
630
683
|
|
|
631
684
|
if (!res.ok) {
|
|
@@ -656,6 +709,233 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
656
709
|
}
|
|
657
710
|
});
|
|
658
711
|
|
|
712
|
+
secrets
|
|
713
|
+
.command("share <path>")
|
|
714
|
+
.description("Share a secret with a person or group")
|
|
715
|
+
.requiredOption("--with <principal>", "Email address or group id to share with")
|
|
716
|
+
.requiredOption("--permission <level>", "Permission level: read | write | admin")
|
|
717
|
+
.action(async (path: string, opts: { with: string; permission: string }) => {
|
|
718
|
+
try {
|
|
719
|
+
if (!SECRET_NAME_PATTERN.test(path)) {
|
|
720
|
+
console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (!["read", "write", "admin"].includes(opts.permission)) {
|
|
725
|
+
console.error(chalk.red(`Invalid permission '${opts.permission}': must be one of read, write, admin`));
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const isEmail = /^[^\s]+@[^\s]+$/.test(opts.with);
|
|
730
|
+
if (!isEmail && !GROUP_ID_PATTERN.test(opts.with)) {
|
|
731
|
+
console.error(chalk.red(`Invalid principal '${opts.with}': must be an email address or a group id matching grp_<alphanumeric>`));
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
const granteeType = isEmail ? "email" : "group";
|
|
735
|
+
const granteeId = opts.with;
|
|
736
|
+
|
|
737
|
+
const token = await ensureCognitoToken();
|
|
738
|
+
const companySlug = secrets.opts().company as string | undefined;
|
|
739
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
740
|
+
|
|
741
|
+
const res = await vaultApiFetch({
|
|
742
|
+
token,
|
|
743
|
+
path: `/secrets/${encodeURIComponent(companyUid)}/acl/grant`,
|
|
744
|
+
method: "POST",
|
|
745
|
+
body: { path, granteeType, granteeId, permission: opts.permission },
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
if (!res.ok) {
|
|
749
|
+
const body = await res.json().catch(() => ({})) as Record<string, string>;
|
|
750
|
+
if (res.status === 401) {
|
|
751
|
+
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
752
|
+
} else if (res.status === 403) {
|
|
753
|
+
console.error(chalk.red("Not authorized to share this secret"));
|
|
754
|
+
} else if (res.status === 404) {
|
|
755
|
+
console.error(chalk.red("ACL record not found for this path — has the secret been created?"));
|
|
756
|
+
} else if (res.status === 409) {
|
|
757
|
+
console.error(chalk.red("Concurrent modification — please retry"));
|
|
758
|
+
} else if (res.status >= 500) {
|
|
759
|
+
console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
|
|
760
|
+
} else {
|
|
761
|
+
console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
|
|
762
|
+
}
|
|
763
|
+
process.exit(1);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
console.log(chalk.green(`Shared '${path}' with ${opts.with} (${opts.permission})`));
|
|
767
|
+
} catch (err) {
|
|
768
|
+
console.error(
|
|
769
|
+
chalk.red("Error:"),
|
|
770
|
+
err instanceof Error ? err.message : String(err),
|
|
771
|
+
);
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
secrets
|
|
777
|
+
.command("unshare <path>")
|
|
778
|
+
.description("Remove a grant from a secret")
|
|
779
|
+
.requiredOption("--from <principal>", "Email address or group id to remove")
|
|
780
|
+
.action(async (path: string, opts: { from: string }) => {
|
|
781
|
+
try {
|
|
782
|
+
if (!SECRET_NAME_PATTERN.test(path)) {
|
|
783
|
+
console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const isEmailFrom = /^[^\s]+@[^\s]+$/.test(opts.from);
|
|
788
|
+
if (!isEmailFrom && !GROUP_ID_PATTERN.test(opts.from)) {
|
|
789
|
+
console.error(chalk.red(`Invalid principal '${opts.from}': must be an email address or a group id matching grp_<alphanumeric>`));
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
const granteeType = isEmailFrom ? "email" : "group";
|
|
793
|
+
const granteeId = opts.from;
|
|
794
|
+
|
|
795
|
+
const token = await ensureCognitoToken();
|
|
796
|
+
const companySlug = secrets.opts().company as string | undefined;
|
|
797
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
798
|
+
|
|
799
|
+
const res = await vaultApiFetch({
|
|
800
|
+
token,
|
|
801
|
+
path: `/secrets/${encodeURIComponent(companyUid)}/acl/revoke`,
|
|
802
|
+
method: "POST",
|
|
803
|
+
body: { path, granteeType, granteeId },
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
if (!res.ok) {
|
|
807
|
+
const body = await res.json().catch(() => ({})) as Record<string, string>;
|
|
808
|
+
if (res.status === 401) {
|
|
809
|
+
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
810
|
+
} else if (res.status === 403) {
|
|
811
|
+
console.error(chalk.red("Not authorized to modify this secret's ACL"));
|
|
812
|
+
} else if (res.status === 404) {
|
|
813
|
+
console.log(chalk.green(`Grant already absent for '${path}' / ${opts.from}`));
|
|
814
|
+
return;
|
|
815
|
+
} else if (res.status >= 500) {
|
|
816
|
+
console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
|
|
817
|
+
} else {
|
|
818
|
+
console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
|
|
819
|
+
}
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
console.log(chalk.green(`Removed grant for ${opts.from} on '${path}'`));
|
|
824
|
+
} catch (err) {
|
|
825
|
+
console.error(
|
|
826
|
+
chalk.red("Error:"),
|
|
827
|
+
err instanceof Error ? err.message : String(err),
|
|
828
|
+
);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
secrets
|
|
834
|
+
.command("acl <path>")
|
|
835
|
+
.description("Show the ACL (access control list) for a secret path")
|
|
836
|
+
.action(async (path: string) => {
|
|
837
|
+
try {
|
|
838
|
+
if (!SECRET_NAME_PATTERN.test(path)) {
|
|
839
|
+
console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const token = await ensureCognitoToken();
|
|
844
|
+
const companySlug = secrets.opts().company as string | undefined;
|
|
845
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
846
|
+
|
|
847
|
+
const secretPath = path;
|
|
848
|
+
const res = await vaultApiFetch({
|
|
849
|
+
token,
|
|
850
|
+
path: `/secrets/${encodeURIComponent(companyUid)}/acl`,
|
|
851
|
+
query: { path: secretPath },
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
if (!res.ok) {
|
|
855
|
+
const body = await res.json().catch(() => ({})) as Record<string, string>;
|
|
856
|
+
if (res.status === 401) {
|
|
857
|
+
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
858
|
+
} else if (res.status === 403) {
|
|
859
|
+
console.error(chalk.red("Not authorized to view this secret's ACL"));
|
|
860
|
+
} else if (res.status === 404) {
|
|
861
|
+
console.error(chalk.red(`No ACL record exists for '${path}'`));
|
|
862
|
+
} else if (res.status >= 500) {
|
|
863
|
+
console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
|
|
864
|
+
} else {
|
|
865
|
+
console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
|
|
866
|
+
}
|
|
867
|
+
process.exit(1);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const data = (await res.json()) as {
|
|
871
|
+
acl: {
|
|
872
|
+
itemType: "ACL";
|
|
873
|
+
companyUid: string;
|
|
874
|
+
path: string;
|
|
875
|
+
creatorUid: string;
|
|
876
|
+
open?: boolean;
|
|
877
|
+
entries: Array<{
|
|
878
|
+
granteeType: string;
|
|
879
|
+
granteeId: string;
|
|
880
|
+
permission: string;
|
|
881
|
+
grantedBy: string;
|
|
882
|
+
grantedAt: string;
|
|
883
|
+
}>;
|
|
884
|
+
createdAt: string;
|
|
885
|
+
updatedAt: string;
|
|
886
|
+
schemaVersion: number;
|
|
887
|
+
};
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
const acl = data.acl;
|
|
891
|
+
const aclStatus = acl.open ? "open" : "restricted";
|
|
892
|
+
|
|
893
|
+
console.log(chalk.green(`ACL for ${acl.path} (${aclStatus})`));
|
|
894
|
+
console.log(`Creator: ${acl.creatorUid}`);
|
|
895
|
+
|
|
896
|
+
if (acl.entries.length === 0) {
|
|
897
|
+
if (acl.open) {
|
|
898
|
+
console.log(chalk.gray("Open ACL — all active members have read access."));
|
|
899
|
+
} else {
|
|
900
|
+
console.log(chalk.gray("No explicit grants — only creator has access."));
|
|
901
|
+
}
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
console.log("Entries:");
|
|
906
|
+
const TYPE_W = Math.max(4, ...acl.entries.map((e) => e.granteeType.length));
|
|
907
|
+
const GRANTEE_W = Math.max(7, ...acl.entries.map((e) => e.granteeId.length));
|
|
908
|
+
const PERM_W = Math.max(10, ...acl.entries.map((e) => e.permission.length));
|
|
909
|
+
const BY_W = Math.max(10, ...acl.entries.map((e) => e.grantedBy.length));
|
|
910
|
+
const tableHeader = [
|
|
911
|
+
"TYPE".padEnd(TYPE_W),
|
|
912
|
+
"GRANTEE".padEnd(GRANTEE_W),
|
|
913
|
+
"PERMISSION".padEnd(PERM_W),
|
|
914
|
+
"GRANTED_BY".padEnd(BY_W),
|
|
915
|
+
"GRANTED_AT",
|
|
916
|
+
].join(" ");
|
|
917
|
+
console.log(chalk.bold(tableHeader));
|
|
918
|
+
for (const e of acl.entries) {
|
|
919
|
+
const grantedAt = e.grantedAt.slice(0, 10);
|
|
920
|
+
console.log(
|
|
921
|
+
[
|
|
922
|
+
e.granteeType.padEnd(TYPE_W),
|
|
923
|
+
e.granteeId.padEnd(GRANTEE_W),
|
|
924
|
+
e.permission.padEnd(PERM_W),
|
|
925
|
+
e.grantedBy.padEnd(BY_W),
|
|
926
|
+
grantedAt,
|
|
927
|
+
].join(" "),
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
} catch (err) {
|
|
931
|
+
console.error(
|
|
932
|
+
chalk.red("Error:"),
|
|
933
|
+
err instanceof Error ? err.message : String(err),
|
|
934
|
+
);
|
|
935
|
+
process.exit(1);
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
659
939
|
const cache = secrets
|
|
660
940
|
.command("cache")
|
|
661
941
|
.description("Manage the local secrets cache");
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
+
import { initSentry, Sentry } from "./sentry.js";
|
|
8
9
|
import { registerAddCommand } from "./commands/add.js";
|
|
9
10
|
import { registerSyncCommand } from "./commands/sync.js";
|
|
10
11
|
import { registerListCommand } from "./commands/list.js";
|
|
@@ -21,6 +22,9 @@ import { registerPackageListCommand } from "./commands/pkg-list.js";
|
|
|
21
22
|
import { registerTeamSyncCommand } from "./commands/team-sync.js";
|
|
22
23
|
import { registerAuthCommands } from "./commands/auth.js";
|
|
23
24
|
import { registerSecretsCommand } from "./commands/secrets.js";
|
|
25
|
+
import { registerGroupsCommand } from "./commands/groups.js";
|
|
26
|
+
|
|
27
|
+
initSentry();
|
|
24
28
|
|
|
25
29
|
const program = new Command();
|
|
26
30
|
|
|
@@ -74,7 +78,19 @@ registerAuthCommands(program);
|
|
|
74
78
|
// Secrets management (subcommand group — hq secrets set|get|list|delete|exec|generate-link|cache)
|
|
75
79
|
registerSecretsCommand(program);
|
|
76
80
|
|
|
81
|
+
// Groups management (subcommand group — hq groups create|delete|add|remove|list|members)
|
|
82
|
+
registerGroupsCommand(program);
|
|
83
|
+
|
|
77
84
|
// Onboarding (top-level — Cognito + vault-service provisioning)
|
|
78
85
|
registerOnboardCommand(program);
|
|
79
86
|
|
|
80
|
-
|
|
87
|
+
(async () => {
|
|
88
|
+
try {
|
|
89
|
+
await program.parseAsync();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
Sentry.captureException(err);
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
} finally {
|
|
94
|
+
await Sentry.flush(2000);
|
|
95
|
+
}
|
|
96
|
+
})();
|