@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 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("--permission <level>", "Permission level: read | write | admin", "read")
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> is required."));
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 ensureCognitoToken();
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
- allowedPrefixes: opts.scope,
117
- permission,
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 ensureCognitoToken();
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 ensureCognitoToken();
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:
@@ -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 = resolveRevokeTargetToMembershipKey(target, companyUid);
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
- const token = await ensureCognitoToken();
393
- const companySlug = members.opts().company;
394
- const companyUid = await getCompanyUid(token, companySlug);
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
- // The target is ALREADY an active member (409 MEMBERSHIP_ALREADY_EXISTS).
515
- // Terminal, not a failure: the person is already in the company, so
516
- // there's nothing to invite or retry. Print a plain note and exit 0 —
517
- // re-attempting just re-fires the server's (correctly captured) 409,
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
  }
@@ -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 ensureCognitoToken();
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 token = await ensureCognitoToken();
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 ensureCognitoToken();
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 ensureCognitoToken();
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 ensureCognitoToken();
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 ensureCognitoToken();
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 ensureCognitoToken();
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 ensureCognitoToken();
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 ensureCognitoToken();
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 ensureCognitoToken();
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 ensureCognitoToken();
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 token = await ensureCognitoToken();
1188
- const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
1189
- const revealed = await loadRevealedSecrets(token, companyUid, keys, await buildSecretUsage("exec", _opts.script, _opts.scriptId));
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 token = await ensureCognitoToken();
1235
- const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
1236
- const revealed = await loadRevealedSecrets(token, companyUid, keys, await buildSecretUsage("env", opts.script, opts.scriptId));
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 ensureCognitoToken();
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 ensureCognitoToken();
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 ensureCognitoToken();
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 ensureCognitoToken();
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