@indigoai-us/hq-cli 5.77.10 → 5.77.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.
- package/CHANGELOG.md +30 -0
- package/dist/commands/api-keys.js +53 -10
- package/dist/commands/members.d.ts +8 -0
- package/dist/commands/members.js +82 -9
- package/dist/commands/secrets.js +127 -21
- package/dist/utils/resolve-vault-credential.d.ts +30 -0
- package/dist/utils/resolve-vault-credential.js +48 -0
- package/package.json +1 -1
- package/src/commands/api-keys.test.ts +75 -1
- package/src/commands/api-keys.ts +86 -10
- package/src/commands/members.test.ts +195 -3
- package/src/commands/members.ts +124 -10
- package/src/commands/secrets.test.ts +133 -0
- package/src/commands/secrets.ts +172 -29
- package/src/utils/resolve-vault-credential.test.ts +69 -0
- package/src/utils/resolve-vault-credential.ts +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.77.12]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `HQ_API_KEY` fail-closed consume path: `hqk_…` keys route `hq secrets get`
|
|
10
|
+
/ `exec` / `env` through vault key fetch; Cognito-only commands hard-error.
|
|
11
|
+
- `hq api-keys create --deploy-app` (repeatable) and deploy-app column in list
|
|
12
|
+
output for identity-bound deploy keys. (#270)
|
|
13
|
+
|
|
14
|
+
## [5.77.11]
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `hq members promote <email>` now looks up the active member roster by
|
|
19
|
+
`personEmail` (and maps fleet-agent machine emails like revoke) instead of
|
|
20
|
+
synthesizing a pending-only `email:…#cmp` membership key that the role API
|
|
21
|
+
cannot find. (#269)
|
|
22
|
+
- Re-inviting an email that already has a pending invite no longer claims the
|
|
23
|
+
person is already a member; the CLI distinguishes pending vs active and
|
|
24
|
+
suggests `hq members invite <email> --company <slug> --resend` when
|
|
25
|
+
appropriate. (#269)
|
|
26
|
+
|
|
27
|
+
## [5.77.10]
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Updated `@indigoai-us/hq-cloud` to 6.14.27 so the sync watcher no longer
|
|
32
|
+
descends into directory-only ignores such as nested `node_modules/`, which
|
|
33
|
+
could exhaust host inotify watches. (#268)
|
|
34
|
+
|
|
5
35
|
## [5.77.9]
|
|
6
36
|
|
|
7
37
|
### Added
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
3
|
+
import { assertCognitoOnlyCommand } from "../utils/resolve-vault-credential.js";
|
|
3
4
|
import { getCompanyUid, vaultApiFetch } from "./secrets.js";
|
|
5
|
+
async function requireCognitoForApiKeys(label) {
|
|
6
|
+
assertCognitoOnlyCommand(label);
|
|
7
|
+
return ensureCognitoToken();
|
|
8
|
+
}
|
|
4
9
|
function collectRepeatedOption(value, previous) {
|
|
5
10
|
return [...previous, value];
|
|
6
11
|
}
|
|
@@ -10,6 +15,11 @@ function formatMaybe(value) {
|
|
|
10
15
|
function formatPrefixes(prefixes) {
|
|
11
16
|
return prefixes.length > 0 ? prefixes.join(", ") : "-";
|
|
12
17
|
}
|
|
18
|
+
function formatDeployApps(deploy) {
|
|
19
|
+
if (!deploy?.apps?.length)
|
|
20
|
+
return "-";
|
|
21
|
+
return deploy.apps.join(", ");
|
|
22
|
+
}
|
|
13
23
|
function parsePermission(value) {
|
|
14
24
|
if (value === "read" || value === "write" || value === "admin") {
|
|
15
25
|
return value;
|
|
@@ -51,6 +61,7 @@ function renderApiKeysTable(apiKeys) {
|
|
|
51
61
|
name: apiKey.name,
|
|
52
62
|
permission: apiKey.scope.permission,
|
|
53
63
|
prefixes: formatPrefixes(apiKey.scope.allowedPrefixes),
|
|
64
|
+
deployApps: formatDeployApps(apiKey.scope.deploy),
|
|
54
65
|
status: apiKey.status,
|
|
55
66
|
lastUsedAt: formatMaybe(apiKey.lastUsedAt),
|
|
56
67
|
expiresAt: formatMaybe(apiKey.expiresAt),
|
|
@@ -59,6 +70,7 @@ function renderApiKeysTable(apiKeys) {
|
|
|
59
70
|
const nameWidth = Math.max(4, ...rows.map((row) => row.name.length));
|
|
60
71
|
const permissionWidth = Math.max(10, ...rows.map((row) => row.permission.length));
|
|
61
72
|
const prefixesWidth = Math.max(8, ...rows.map((row) => row.prefixes.length));
|
|
73
|
+
const deployWidth = Math.max(6, ...rows.map((row) => row.deployApps.length));
|
|
62
74
|
const statusWidth = Math.max(6, ...rows.map((row) => row.status.length));
|
|
63
75
|
const lastUsedWidth = Math.max(11, ...rows.map((row) => row.lastUsedAt.length));
|
|
64
76
|
const expiresWidth = Math.max(10, ...rows.map((row) => row.expiresAt.length));
|
|
@@ -67,6 +79,7 @@ function renderApiKeysTable(apiKeys) {
|
|
|
67
79
|
"NAME".padEnd(nameWidth),
|
|
68
80
|
"PERMISSION".padEnd(permissionWidth),
|
|
69
81
|
"PREFIXES".padEnd(prefixesWidth),
|
|
82
|
+
"DEPLOY".padEnd(deployWidth),
|
|
70
83
|
"STATUS".padEnd(statusWidth),
|
|
71
84
|
"LAST USED".padEnd(lastUsedWidth),
|
|
72
85
|
"EXPIRES".padEnd(expiresWidth),
|
|
@@ -78,6 +91,7 @@ function renderApiKeysTable(apiKeys) {
|
|
|
78
91
|
row.name.padEnd(nameWidth),
|
|
79
92
|
row.permission.padEnd(permissionWidth),
|
|
80
93
|
row.prefixes.padEnd(prefixesWidth),
|
|
94
|
+
row.deployApps.padEnd(deployWidth),
|
|
81
95
|
row.status.padEnd(statusWidth),
|
|
82
96
|
row.lastUsedAt.padEnd(lastUsedWidth),
|
|
83
97
|
row.expiresAt.padEnd(expiresWidth),
|
|
@@ -91,20 +105,21 @@ export function registerApiKeysCommand(program) {
|
|
|
91
105
|
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
92
106
|
apiKeys
|
|
93
107
|
.command("create")
|
|
94
|
-
.description("Create a new API key")
|
|
108
|
+
.description("Create a new API key (vault secrets and/or scoped deploy via --deploy-app)")
|
|
95
109
|
.requiredOption("--name <label>", "Human-readable label for the API key")
|
|
96
|
-
.option("--scope <prefix>", "Allowed prefix (repeatable)", collectRepeatedOption, [])
|
|
97
|
-
.option("--
|
|
110
|
+
.option("--scope <prefix>", "Allowed secret prefix (repeatable)", collectRepeatedOption, [])
|
|
111
|
+
.option("--deploy-app <id>", "Deploy app id/slug allowed for publish (repeatable)", collectRepeatedOption, [])
|
|
112
|
+
.option("--permission <level>", "Secret permission level: read | write | admin (required when --scope is set)", "read")
|
|
98
113
|
.option("--expires <ISO8601>", "Optional ISO-8601 expiry timestamp")
|
|
99
114
|
.action(async (opts) => {
|
|
100
115
|
try {
|
|
101
|
-
if (opts.scope.length === 0) {
|
|
102
|
-
console.error(chalk.red("Error: at least one --scope <prefix>
|
|
116
|
+
if (opts.scope.length === 0 && opts.deployApp.length === 0) {
|
|
117
|
+
console.error(chalk.red("Error: provide at least one --scope <prefix> and/or --deploy-app <id>."));
|
|
103
118
|
process.exit(1);
|
|
104
119
|
}
|
|
105
120
|
const permission = parsePermission(opts.permission);
|
|
106
121
|
const expiresAt = parseExpires(opts.expires);
|
|
107
|
-
const token = await
|
|
122
|
+
const token = await requireCognitoForApiKeys("api-keys create");
|
|
108
123
|
const companyUid = await getCompanyUid(token, apiKeys.opts().company);
|
|
109
124
|
const res = await vaultApiFetch({
|
|
110
125
|
token,
|
|
@@ -113,8 +128,20 @@ export function registerApiKeysCommand(program) {
|
|
|
113
128
|
body: {
|
|
114
129
|
companyUid,
|
|
115
130
|
name: opts.name,
|
|
116
|
-
|
|
117
|
-
|
|
131
|
+
...(opts.scope.length > 0
|
|
132
|
+
? { allowedPrefixes: opts.scope, permission }
|
|
133
|
+
: {}),
|
|
134
|
+
...(opts.deployApp.length > 0
|
|
135
|
+
? {
|
|
136
|
+
deploy: {
|
|
137
|
+
apps: opts.deployApp,
|
|
138
|
+
capabilities: ["deploy:write"],
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
142
|
+
...(opts.scope.length === 0 && opts.deployApp.length > 0
|
|
143
|
+
? { permission }
|
|
144
|
+
: {}),
|
|
118
145
|
...(expiresAt ? { expiresAt } : {}),
|
|
119
146
|
},
|
|
120
147
|
});
|
|
@@ -131,10 +158,26 @@ export function registerApiKeysCommand(program) {
|
|
|
131
158
|
console.log(` Company: ${data.apiKey.companyUid}`);
|
|
132
159
|
console.log(` Permission: ${data.apiKey.scope.permission}`);
|
|
133
160
|
console.log(` Prefixes: ${formatPrefixes(data.apiKey.scope.allowedPrefixes)}`);
|
|
161
|
+
console.log(` Deploy apps: ${formatDeployApps(data.apiKey.scope.deploy)}`);
|
|
134
162
|
console.log(` Status: ${data.apiKey.status}`);
|
|
135
163
|
console.log(` Created: ${data.apiKey.createdAt}`);
|
|
136
164
|
console.log(` Last used: ${formatMaybe(data.apiKey.lastUsedAt)}`);
|
|
137
165
|
console.log(` Expires: ${formatMaybe(data.apiKey.expiresAt)}`);
|
|
166
|
+
console.log("");
|
|
167
|
+
console.log(chalk.bold("Usage"));
|
|
168
|
+
console.log(" This key acts as you (Cognito identity), limited to the scopes above.");
|
|
169
|
+
console.log(" Export it for automation (never falls back to a session):");
|
|
170
|
+
console.log(`\n export HQ_API_KEY='${data.key.value}'\n`);
|
|
171
|
+
console.log(" Vault secrets:");
|
|
172
|
+
console.log(" hq secrets get <NAME> --reveal");
|
|
173
|
+
console.log(" hq secrets exec --only <NAME> -- <command>");
|
|
174
|
+
console.log(" Or HTTP: POST /v1/keys/secrets/fetch with Authorization: Bearer <key>");
|
|
175
|
+
if (data.apiKey.scope.deploy?.apps?.length) {
|
|
176
|
+
console.log(" Deploy (scoped apps only):");
|
|
177
|
+
console.log(" Authorization: Bearer <key> against the hq-deploy API");
|
|
178
|
+
console.log(chalk.dim(" Cannot change access-mode, password, or mint hqd_ keys."));
|
|
179
|
+
}
|
|
180
|
+
console.log(chalk.dim(" Unsupported under HQ_API_KEY: secrets list/set/share/acl (use a Cognito session)."));
|
|
138
181
|
}
|
|
139
182
|
catch (err) {
|
|
140
183
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -146,7 +189,7 @@ export function registerApiKeysCommand(program) {
|
|
|
146
189
|
.description("List API keys for a company")
|
|
147
190
|
.action(async () => {
|
|
148
191
|
try {
|
|
149
|
-
const token = await
|
|
192
|
+
const token = await requireCognitoForApiKeys("api-keys list");
|
|
150
193
|
const companyUid = await getCompanyUid(token, apiKeys.opts().company);
|
|
151
194
|
const res = await vaultApiFetch({
|
|
152
195
|
token,
|
|
@@ -173,7 +216,7 @@ export function registerApiKeysCommand(program) {
|
|
|
173
216
|
.description("Revoke an API key")
|
|
174
217
|
.action(async (keyId) => {
|
|
175
218
|
try {
|
|
176
|
-
const token = await
|
|
219
|
+
const token = await requireCognitoForApiKeys("api-keys revoke");
|
|
177
220
|
const res = await vaultApiFetch({
|
|
178
221
|
token,
|
|
179
222
|
path: `/v1/api-keys/${encodeURIComponent(keyId)}/revoke`,
|
|
@@ -142,6 +142,14 @@ export declare class InviteHttpError extends Error {
|
|
|
142
142
|
export declare function formatInviteHttpError(status: number, fallback: string, code?: string): string;
|
|
143
143
|
export declare function listPendingInvites(token: string, companyUid: string): Promise<PendingInvite[]>;
|
|
144
144
|
export declare function listActiveMembers(token: string, companyUid: string): Promise<ActiveMember[]>;
|
|
145
|
+
/**
|
|
146
|
+
* Resolve a role-change target to an active membership key.
|
|
147
|
+
*
|
|
148
|
+
* Pending invites are email-keyed, but claimed active memberships are keyed by
|
|
149
|
+
* personUid. Consequently, role changes must look up email targets in the
|
|
150
|
+
* active member list instead of synthesizing an `email:...#company` key.
|
|
151
|
+
*/
|
|
152
|
+
export declare function resolveRoleChangeTarget(token: string, companyUid: string, target: string): Promise<string>;
|
|
145
153
|
/**
|
|
146
154
|
* Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
|
|
147
155
|
* the server requires. Accepts three input forms:
|
package/dist/commands/members.js
CHANGED
|
@@ -244,6 +244,39 @@ export async function listActiveMembers(token, companyUid) {
|
|
|
244
244
|
const data = (await res.json());
|
|
245
245
|
return data?.members ?? [];
|
|
246
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Resolve a role-change target to an active membership key.
|
|
249
|
+
*
|
|
250
|
+
* Pending invites are email-keyed, but claimed active memberships are keyed by
|
|
251
|
+
* personUid. Consequently, role changes must look up email targets in the
|
|
252
|
+
* active member list instead of synthesizing an `email:...#company` key.
|
|
253
|
+
*/
|
|
254
|
+
export async function resolveRoleChangeTarget(token, companyUid, target) {
|
|
255
|
+
if (target.includes("#"))
|
|
256
|
+
return target;
|
|
257
|
+
const detected = detectTarget(target);
|
|
258
|
+
if (detected?.type === "person" || detected?.type === "agent") {
|
|
259
|
+
return `${detected.value}#${companyUid}`;
|
|
260
|
+
}
|
|
261
|
+
if (detected?.type === "email") {
|
|
262
|
+
// Active fleet-agent guest memberships are keyed agt_…#cmp_…, not by
|
|
263
|
+
// personEmail enrichment. Map machine emails the same way revoke does
|
|
264
|
+
// before falling through to the human roster lookup.
|
|
265
|
+
if (detected.isAgent) {
|
|
266
|
+
const local = detected.value.split("@")[0] ?? "";
|
|
267
|
+
const m = local.match(/^agt-(.+)$/i);
|
|
268
|
+
if (m)
|
|
269
|
+
return `agt_${m[1].toUpperCase()}#${companyUid}`;
|
|
270
|
+
}
|
|
271
|
+
const members = await listActiveMembers(token, companyUid);
|
|
272
|
+
const member = members.find((candidate) => candidate.personEmail?.trim().toLowerCase() === detected.value);
|
|
273
|
+
if (member)
|
|
274
|
+
return member.membershipKey;
|
|
275
|
+
throw new Error(`No active member has email '${detected.value}' in this company. ` +
|
|
276
|
+
"Run `hq members list` to find the member, or use their prs_ personUid.");
|
|
277
|
+
}
|
|
278
|
+
throw new Error(`Invalid target '${target}': use an email, prs_ personUid, agt_ agentUid, or full membership key`);
|
|
279
|
+
}
|
|
247
280
|
/**
|
|
248
281
|
* Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
|
|
249
282
|
* the server requires. Accepts three input forms:
|
|
@@ -356,7 +389,7 @@ export function registerMembersCommand(program) {
|
|
|
356
389
|
const token = await ensureCognitoToken();
|
|
357
390
|
const companySlug = members.opts().company;
|
|
358
391
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
359
|
-
const membershipKey =
|
|
392
|
+
const membershipKey = await resolveRoleChangeTarget(token, companyUid, target);
|
|
360
393
|
await changeMemberRole(token, companyUid, membershipKey, role);
|
|
361
394
|
console.log(chalk.green(`Updated role for '${target}' to ${role}`));
|
|
362
395
|
if (role === "owner" || role === "admin") {
|
|
@@ -388,10 +421,13 @@ export function registerMembersCommand(program) {
|
|
|
388
421
|
.option("--no-send-email", "Skip the server-side Resend send — only create the pending DDB row. Useful when you're scripting bulk invites and will send the announcement out-of-band.")
|
|
389
422
|
.option("--resend", "Re-fire the invitation email against an existing pending row without creating a new row. Maps to hq-pro `resend: true` short-circuit. Email-keyed invites only.")
|
|
390
423
|
.action(async (target, opts) => {
|
|
424
|
+
let token;
|
|
425
|
+
let companyUid;
|
|
426
|
+
let companySlug;
|
|
391
427
|
try {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
428
|
+
token = await ensureCognitoToken();
|
|
429
|
+
companySlug = members.opts().company;
|
|
430
|
+
companyUid = await getCompanyUid(token, companySlug);
|
|
395
431
|
const callerUid = await getCallerPersonUid(token);
|
|
396
432
|
// --resend short-circuits to the server's re-fire path. No new row.
|
|
397
433
|
if (opts.resend === true) {
|
|
@@ -511,13 +547,50 @@ export function registerMembersCommand(program) {
|
|
|
511
547
|
}
|
|
512
548
|
}
|
|
513
549
|
catch (err) {
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
// and a bulk-invite script shouldn't abort on an already-member. HQ-11.
|
|
550
|
+
// A 409 may refer to either an active membership or an existing
|
|
551
|
+
// pending invite. Probe both views so email targets get actionable
|
|
552
|
+
// guidance while preserving the terminal exit-0 behavior for bulk
|
|
553
|
+
// invite scripts.
|
|
519
554
|
if (err instanceof InviteHttpError &&
|
|
520
555
|
err.code === "MEMBERSHIP_ALREADY_EXISTS") {
|
|
556
|
+
const detected = detectTarget(target);
|
|
557
|
+
const normalizedEmail = detected?.type === "email" ? detected.value : undefined;
|
|
558
|
+
if (normalizedEmail && token && companyUid) {
|
|
559
|
+
// Preserve --company in copy-paste hints so multi-company
|
|
560
|
+
// callers don't hit "Multiple active companies" on --resend.
|
|
561
|
+
const companyFlag = companySlug
|
|
562
|
+
? ` --company ${companySlug}`
|
|
563
|
+
: "";
|
|
564
|
+
const resendHint = `hq members invite ${normalizedEmail}${companyFlag} --resend`;
|
|
565
|
+
try {
|
|
566
|
+
const pending = await listPendingInvites(token, companyUid);
|
|
567
|
+
if (pending.some((invite) => invite.inviteeEmail?.trim().toLowerCase() ===
|
|
568
|
+
normalizedEmail)) {
|
|
569
|
+
console.log(chalk.yellow(`${normalizedEmail} already has a pending invite. ` +
|
|
570
|
+
`Use \`${resendHint}\` to re-send it.`));
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
// Pending-list visibility can vary by server version or role.
|
|
576
|
+
// Fall through to the active-member probe and generic copy.
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
const activeMembers = await listActiveMembers(token, companyUid);
|
|
580
|
+
if (activeMembers.some((member) => member.personEmail?.trim().toLowerCase() ===
|
|
581
|
+
normalizedEmail)) {
|
|
582
|
+
console.log(chalk.yellow(`${normalizedEmail} is already a member of this company — nothing to do.`));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// Preserve the original conflict as the source of truth and
|
|
588
|
+
// use copy that remains accurate when the probes are hidden.
|
|
589
|
+
}
|
|
590
|
+
console.log(chalk.yellow(`${normalizedEmail} already has a membership or pending invite for this company. ` +
|
|
591
|
+
`If the invite is pending, use \`${resendHint}\`.`));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
521
594
|
console.log(chalk.yellow(`${target.toLowerCase()} is already a member of this company — nothing to do.`));
|
|
522
595
|
return;
|
|
523
596
|
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -8,8 +8,14 @@ import { computeSha256 } from "../utils/integrity.js";
|
|
|
8
8
|
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
|
|
9
9
|
import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, formatSecretsListHeader, } from "./secrets-scope.js";
|
|
10
10
|
import { vaultApiFetch, getCompanyUid, getEntityUid, } from "../utils/vault-api.js";
|
|
11
|
+
import { HQ_API_KEY_PREFIX, assertCognitoOnlyCommand, resolveVaultCredential, } from "../utils/resolve-vault-credential.js";
|
|
11
12
|
import { SandboxRunnerClient, } from "../utils/sandbox-runner-client.js";
|
|
12
13
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
14
|
+
/** Cognito session for secrets commands that do not support HQ_API_KEY. */
|
|
15
|
+
async function requireCognitoTokenForSecrets(commandLabel) {
|
|
16
|
+
assertCognitoOnlyCommand(commandLabel);
|
|
17
|
+
return ensureCognitoToken();
|
|
18
|
+
}
|
|
13
19
|
function scopeOpts(opts) {
|
|
14
20
|
if (opts.personal && opts.company) {
|
|
15
21
|
console.error(chalk.red("Error: --personal cannot be combined with --company."));
|
|
@@ -424,7 +430,54 @@ function renderPolicyScripts(scripts) {
|
|
|
424
430
|
// Requests are chunked at MAX_BATCH_NAMES and throw on the FIRST unresolved key
|
|
425
431
|
// with the same `Failed to fetch secret '<k>': <reason>` shape the per-key GET
|
|
426
432
|
// path used — never swallows a failure.
|
|
433
|
+
async function loadRevealedSecretsViaApiKey(token, keys) {
|
|
434
|
+
const resolved = new Map();
|
|
435
|
+
const requested = [...new Set(keys)];
|
|
436
|
+
const cacheScope = "__api_key__";
|
|
437
|
+
for (const name of requested) {
|
|
438
|
+
const res = await vaultApiFetch({
|
|
439
|
+
token,
|
|
440
|
+
path: "/v1/keys/secrets/fetch",
|
|
441
|
+
method: "POST",
|
|
442
|
+
body: { name },
|
|
443
|
+
signal: AbortSignal.timeout(SECRET_LOAD_TIMEOUT_MS),
|
|
444
|
+
});
|
|
445
|
+
const body = (await res.json().catch(() => ({})));
|
|
446
|
+
if (!res.ok) {
|
|
447
|
+
if (res.status === 404) {
|
|
448
|
+
throw new Error(`Failed to fetch secret '${name}': Secret not found`);
|
|
449
|
+
}
|
|
450
|
+
if (res.status === 403) {
|
|
451
|
+
const message = typeof body.error === "string"
|
|
452
|
+
? body.error
|
|
453
|
+
: typeof body.message === "string"
|
|
454
|
+
? body.message
|
|
455
|
+
: "No read permission";
|
|
456
|
+
if (body.highSecurity === true) {
|
|
457
|
+
throw new Error(highSecuritySandboxOnlyMessage(name));
|
|
458
|
+
}
|
|
459
|
+
throw new Error(`Failed to fetch secret '${name}': ${message}`);
|
|
460
|
+
}
|
|
461
|
+
if (res.status === 401) {
|
|
462
|
+
throw new Error(`Failed to fetch secret '${name}': Invalid or missing API key`);
|
|
463
|
+
}
|
|
464
|
+
throw new Error(`Failed to fetch secret '${name}': ${extractApiMessage(body, res.statusText)}`);
|
|
465
|
+
}
|
|
466
|
+
const secret = typeof body.secret === "object" && body.secret !== null
|
|
467
|
+
? body.secret
|
|
468
|
+
: null;
|
|
469
|
+
if (typeof secret?.value !== "string") {
|
|
470
|
+
throw new Error(`Failed to fetch secret '${name}': malformed fetch response`);
|
|
471
|
+
}
|
|
472
|
+
removeCacheEntry(cacheScope, name);
|
|
473
|
+
resolved.set(name, secret.value);
|
|
474
|
+
}
|
|
475
|
+
return resolved;
|
|
476
|
+
}
|
|
427
477
|
export async function loadRevealedSecrets(token, companyUid, keys, usage) {
|
|
478
|
+
if (token.startsWith(HQ_API_KEY_PREFIX)) {
|
|
479
|
+
return loadRevealedSecretsViaApiKey(token, keys);
|
|
480
|
+
}
|
|
428
481
|
const resolved = new Map();
|
|
429
482
|
const requested = [...new Set(keys)];
|
|
430
483
|
try {
|
|
@@ -632,7 +685,7 @@ export function registerSecretsCommand(program) {
|
|
|
632
685
|
console.error(chalk.red(`Secret value exceeds 4096-byte SSM limit (got ${Buffer.byteLength(value, "utf8")} bytes).`));
|
|
633
686
|
process.exit(1);
|
|
634
687
|
}
|
|
635
|
-
const token = await
|
|
688
|
+
const token = await requireCognitoTokenForSecrets("secrets set");
|
|
636
689
|
const scope = scopeOpts(secrets.opts());
|
|
637
690
|
const companyUid = await getEntityUid(token, scope);
|
|
638
691
|
const scopeLabel = describeSecretsScope({
|
|
@@ -677,7 +730,52 @@ export function registerSecretsCommand(program) {
|
|
|
677
730
|
.option("--reveal", "Include the decrypted secret value")
|
|
678
731
|
.action(async (name, opts) => {
|
|
679
732
|
try {
|
|
680
|
-
const
|
|
733
|
+
const cred = await resolveVaultCredential();
|
|
734
|
+
if (cred.kind === "api-key") {
|
|
735
|
+
const res = await vaultApiFetch({
|
|
736
|
+
token: cred.token,
|
|
737
|
+
path: "/v1/keys/secrets/fetch",
|
|
738
|
+
method: "POST",
|
|
739
|
+
body: { name },
|
|
740
|
+
});
|
|
741
|
+
const body = (await res.json().catch(() => ({})));
|
|
742
|
+
if (!res.ok) {
|
|
743
|
+
if (res.status === 403 && body.highSecurity === true) {
|
|
744
|
+
console.error(chalk.red(highSecuritySandboxOnlyMessage(name)));
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
console.error(chalk.red(`Failed to get secret: ${extractApiMessage(body, res.statusText)}`));
|
|
748
|
+
process.exit(1);
|
|
749
|
+
}
|
|
750
|
+
const secret = typeof body.secret === "object" && body.secret !== null
|
|
751
|
+
? body.secret
|
|
752
|
+
: null;
|
|
753
|
+
if (!secret || typeof secret.name !== "string") {
|
|
754
|
+
console.error(chalk.red("Failed to get secret: malformed response"));
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
757
|
+
console.log(chalk.bold(`Secret: ${secret.name}`));
|
|
758
|
+
if (secret.lastModifiedDate) {
|
|
759
|
+
console.log(` Last Modified: ${secret.lastModifiedDate}`);
|
|
760
|
+
}
|
|
761
|
+
if (secret.version != null) {
|
|
762
|
+
console.log(` Version: ${secret.version}`);
|
|
763
|
+
}
|
|
764
|
+
console.log(` Tier: ${normalizeSecretTier(secret.tier)}`);
|
|
765
|
+
console.log(` Script Lock: ${normalizeScriptLockMode(secret.scriptLock?.mode)}`);
|
|
766
|
+
if (opts.reveal) {
|
|
767
|
+
if (typeof secret.value !== "string") {
|
|
768
|
+
console.error(chalk.red("Failed to get secret: reveal requested but response omitted secret.value"));
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
console.log(` Value: ${secret.value}`);
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
console.log(` Value: ${chalk.dim("[REDACTED]")}`);
|
|
775
|
+
}
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const token = cred.token;
|
|
681
779
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
682
780
|
const res = await vaultApiFetch({
|
|
683
781
|
token,
|
|
@@ -748,7 +846,7 @@ export function registerSecretsCommand(program) {
|
|
|
748
846
|
.option("--quiet", "Suppress the present/absent line (use the exit code only)")
|
|
749
847
|
.action(async (name, opts) => {
|
|
750
848
|
try {
|
|
751
|
-
const token = await
|
|
849
|
+
const token = await requireCognitoTokenForSecrets("secrets exists");
|
|
752
850
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
753
851
|
const res = await vaultApiFetch({
|
|
754
852
|
token,
|
|
@@ -791,7 +889,7 @@ export function registerSecretsCommand(program) {
|
|
|
791
889
|
}
|
|
792
890
|
normalizedPrefix = normalized;
|
|
793
891
|
}
|
|
794
|
-
const token = await
|
|
892
|
+
const token = await requireCognitoTokenForSecrets("secrets list");
|
|
795
893
|
const scope = scopeOpts(secrets.opts());
|
|
796
894
|
const companyUid = await getEntityUid(token, scope);
|
|
797
895
|
const scopeLabel = describeSecretsScope({
|
|
@@ -865,7 +963,7 @@ export function registerSecretsCommand(program) {
|
|
|
865
963
|
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)`));
|
|
866
964
|
process.exit(1);
|
|
867
965
|
}
|
|
868
|
-
const token = await
|
|
966
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
869
967
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
870
968
|
const res = await vaultApiFetch({
|
|
871
969
|
token,
|
|
@@ -926,7 +1024,7 @@ export function registerSecretsCommand(program) {
|
|
|
926
1024
|
console.error(chalk.red("Error: provide at least one of --tier or --lock-script."));
|
|
927
1025
|
process.exit(1);
|
|
928
1026
|
}
|
|
929
|
-
const token = await
|
|
1027
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
930
1028
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
931
1029
|
const res = await vaultApiFetch({
|
|
932
1030
|
token,
|
|
@@ -971,7 +1069,7 @@ export function registerSecretsCommand(program) {
|
|
|
971
1069
|
process.exit(1);
|
|
972
1070
|
}
|
|
973
1071
|
const usage = await buildSecretUsage("exec", opts.script, opts.id, opts.attestation);
|
|
974
|
-
const token = await
|
|
1072
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
975
1073
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
976
1074
|
const res = await vaultApiFetch({
|
|
977
1075
|
token,
|
|
@@ -1009,7 +1107,7 @@ export function registerSecretsCommand(program) {
|
|
|
1009
1107
|
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)`));
|
|
1010
1108
|
process.exit(1);
|
|
1011
1109
|
}
|
|
1012
|
-
const token = await
|
|
1110
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1013
1111
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
1014
1112
|
const res = await vaultApiFetch({
|
|
1015
1113
|
token,
|
|
@@ -1040,7 +1138,7 @@ export function registerSecretsCommand(program) {
|
|
|
1040
1138
|
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)`));
|
|
1041
1139
|
process.exit(1);
|
|
1042
1140
|
}
|
|
1043
|
-
const token = await
|
|
1141
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1044
1142
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
1045
1143
|
const res = await vaultApiFetch({
|
|
1046
1144
|
token,
|
|
@@ -1087,7 +1185,7 @@ export function registerSecretsCommand(program) {
|
|
|
1087
1185
|
return;
|
|
1088
1186
|
}
|
|
1089
1187
|
}
|
|
1090
|
-
const token = await
|
|
1188
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1091
1189
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
1092
1190
|
const res = await vaultApiFetch({
|
|
1093
1191
|
token,
|
|
@@ -1131,7 +1229,7 @@ export function registerSecretsCommand(program) {
|
|
|
1131
1229
|
process.exit(1);
|
|
1132
1230
|
}
|
|
1133
1231
|
const keys = parseSecretNameList(opts.only);
|
|
1134
|
-
const token = await
|
|
1232
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1135
1233
|
const scope = scopeOpts(mergeScopeOpts(secrets.opts(), opts));
|
|
1136
1234
|
const companyUid = await getEntityUid(token, scope);
|
|
1137
1235
|
const client = new SandboxRunnerClient();
|
|
@@ -1184,9 +1282,13 @@ export function registerSecretsCommand(program) {
|
|
|
1184
1282
|
process.exit(1);
|
|
1185
1283
|
}
|
|
1186
1284
|
const keys = parseSecretNameList(_opts.only);
|
|
1187
|
-
const
|
|
1188
|
-
const companyUid =
|
|
1189
|
-
|
|
1285
|
+
const cred = await resolveVaultCredential();
|
|
1286
|
+
const companyUid = cred.kind === "api-key"
|
|
1287
|
+
? "__api_key__"
|
|
1288
|
+
: await getEntityUid(cred.token, scopeOpts(secrets.opts()));
|
|
1289
|
+
const revealed = await loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
|
|
1290
|
+
? await buildSecretUsage("exec", _opts.script, _opts.scriptId)
|
|
1291
|
+
: undefined);
|
|
1190
1292
|
const secretEnv = {};
|
|
1191
1293
|
for (const key of keys) {
|
|
1192
1294
|
const value = revealed.get(key);
|
|
@@ -1231,9 +1333,13 @@ export function registerSecretsCommand(program) {
|
|
|
1231
1333
|
console.error(chalk.yellow("stdout is a terminal — values redacted. Use: source <(hq secrets env --only KEY1,KEY2)"));
|
|
1232
1334
|
}
|
|
1233
1335
|
const keys = parseSecretNameList(opts.only);
|
|
1234
|
-
const
|
|
1235
|
-
const companyUid =
|
|
1236
|
-
|
|
1336
|
+
const cred = await resolveVaultCredential();
|
|
1337
|
+
const companyUid = cred.kind === "api-key"
|
|
1338
|
+
? "__api_key__"
|
|
1339
|
+
: await getEntityUid(cred.token, scopeOpts(secrets.opts()));
|
|
1340
|
+
const revealed = await loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
|
|
1341
|
+
? await buildSecretUsage("env", opts.script, opts.scriptId)
|
|
1342
|
+
: undefined);
|
|
1237
1343
|
for (const key of keys) {
|
|
1238
1344
|
const value = revealed.get(key);
|
|
1239
1345
|
// loadRevealedSecrets throws on any unresolved key, so a miss here is
|
|
@@ -1270,7 +1376,7 @@ export function registerSecretsCommand(program) {
|
|
|
1270
1376
|
console.error(chalk.red("Maximum expiry is 7 days (7d)."));
|
|
1271
1377
|
process.exit(1);
|
|
1272
1378
|
}
|
|
1273
|
-
const token = await
|
|
1379
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1274
1380
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
1275
1381
|
const res = await vaultApiFetch({
|
|
1276
1382
|
token,
|
|
@@ -1316,7 +1422,7 @@ export function registerSecretsCommand(program) {
|
|
|
1316
1422
|
if (!principal) {
|
|
1317
1423
|
process.exit(1);
|
|
1318
1424
|
}
|
|
1319
|
-
const token = await
|
|
1425
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1320
1426
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
1321
1427
|
const res = await vaultApiFetch({
|
|
1322
1428
|
token,
|
|
@@ -1373,7 +1479,7 @@ export function registerSecretsCommand(program) {
|
|
|
1373
1479
|
if (!principal) {
|
|
1374
1480
|
process.exit(1);
|
|
1375
1481
|
}
|
|
1376
|
-
const token = await
|
|
1482
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1377
1483
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
1378
1484
|
const res = await vaultApiFetch({
|
|
1379
1485
|
token,
|
|
@@ -1422,7 +1528,7 @@ export function registerSecretsCommand(program) {
|
|
|
1422
1528
|
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)`));
|
|
1423
1529
|
process.exit(1);
|
|
1424
1530
|
}
|
|
1425
|
-
const token = await
|
|
1531
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1426
1532
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
1427
1533
|
const secretPath = path;
|
|
1428
1534
|
const res = await vaultApiFetch({
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Vault API keys issued by `hq api-keys create` (hq-pro). */
|
|
2
|
+
export declare const HQ_API_KEY_PREFIX = "hqk_";
|
|
3
|
+
export type VaultCredential = {
|
|
4
|
+
kind: "api-key";
|
|
5
|
+
token: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "cognito";
|
|
8
|
+
token: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Raw HQ_API_KEY from the environment, trimmed. Undefined when unset/empty.
|
|
12
|
+
* Does not validate prefix — use {@link resolveVaultCredential} for that.
|
|
13
|
+
*/
|
|
14
|
+
export declare function peekHqApiKey(): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve vault auth for CLI commands.
|
|
17
|
+
*
|
|
18
|
+
* When `HQ_API_KEY` is set it is authoritative: must be a vault key (`hqk_…`)
|
|
19
|
+
* and Cognito is never used as a fallback (fail-closed). When unset, uses the
|
|
20
|
+
* cached Cognito session (interactive login if needed).
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveVaultCredential(options?: {
|
|
23
|
+
interactive?: boolean;
|
|
24
|
+
}): Promise<VaultCredential>;
|
|
25
|
+
/**
|
|
26
|
+
* Throw when HQ_API_KEY is set but the command only supports Cognito sessions
|
|
27
|
+
* (list, set, ACL, api-keys admin, etc.).
|
|
28
|
+
*/
|
|
29
|
+
export declare function assertCognitoOnlyCommand(commandLabel: string): void;
|
|
30
|
+
//# sourceMappingURL=resolve-vault-credential.d.ts.map
|