@indigoai-us/hq-cli 5.97.1 → 5.97.2

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,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.97.2]
6
+
7
+ ### Fixed
8
+
9
+ - `hq secrets env` no longer leaks processes that never exit. Two independent
10
+ defects combined: the identity/company resolution lookups the command runs
11
+ before its real work used unbounded `fetch` calls, so a vault gateway that
12
+ accepted the connection but never responded left the request in-flight
13
+ forever; and because the CLI exits by draining the event loop rather than
14
+ calling `process.exit`, that one referenced socket kept the whole process
15
+ alive. Callers wrap the command in `source <(…)` / `$(…)`, which bash never
16
+ reaps, so each wedged process was orphaned to PID 1 — on one build box 592 of
17
+ them accumulated, holding ~23.5 GB and starving unrelated work. Every vault
18
+ identity lookup now carries a 15s timeout (a stuck backend becomes a normal,
19
+ catchable error), and `secrets env` now writes its output in one flush and
20
+ exits deterministically in the flush callback (no truncation of a piped
21
+ consumer, no reliance on the event loop draining).
22
+
5
23
  ## [5.97.1]
6
24
 
7
25
  ### Fixed
@@ -1397,6 +1397,7 @@ export function registerSecretsCommand(program) {
1397
1397
  const revealed = await loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
1398
1398
  ? await buildSecretUsage("env", opts.script, opts.scriptId)
1399
1399
  : undefined);
1400
+ let payload = "";
1400
1401
  for (const key of keys) {
1401
1402
  const value = revealed.get(key);
1402
1403
  // loadRevealedSecrets throws on any unresolved key, so a miss here is
@@ -1405,8 +1406,18 @@ export function registerSecretsCommand(program) {
1405
1406
  throw new Error(`Failed to fetch secret '${key}': not returned by vault`);
1406
1407
  }
1407
1408
  const out = redact ? "[REDACTED]" : value;
1408
- process.stdout.write(`export ${key}=${shellSingleQuote(out)}\n`);
1409
- }
1409
+ payload += `export ${key}=${shellSingleQuote(out)}\n`;
1410
+ }
1411
+ // Deterministically terminate once stdout has flushed. Unlike `exec`
1412
+ // (which exits on its child's close), `env` has nothing to exit on and
1413
+ // otherwise relies on the event loop draining — so a single lingering
1414
+ // handle (e.g. a slow keep-alive socket to the vault) would keep this
1415
+ // process alive forever, the mechanism behind the orphaned-process leak.
1416
+ // Exit inside the write callback, which fires only after the bytes reach
1417
+ // the pipe/file, so a `source <(hq secrets env …)` consumer never loses
1418
+ // the export lines — a bare process.exit() would truncate buffered
1419
+ // stdout on a pipe.
1420
+ process.stdout.write(payload, () => process.exit(0));
1410
1421
  }
1411
1422
  catch (err) {
1412
1423
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -4,6 +4,19 @@ import { AuthError } from './auth-error.js';
4
4
  import { CompanySelectionError } from './company-selection-error.js';
5
5
  import { recordPlanLimitStatus } from '../lib/plan-limit-nag.js';
6
6
  import { networkTransportErrorCode } from './network-transport-error.js';
7
+ /**
8
+ * Identity / company resolution lookups must never hang forever. These small
9
+ * GETs run BEFORE a command does its real work (e.g. `hq secrets env` resolves
10
+ * the caller's person/company uid first). Without a bound, a vault gateway that
11
+ * accepts the TCP connection but never sends a response leaves the fetch
12
+ * in-flight, and because the CLI exits by draining the event loop (not
13
+ * `process.exit`), that one referenced socket keeps the whole process alive
14
+ * indefinitely — the mechanism behind the orphaned `hq secrets --personal env`
15
+ * process leak. Bounding each resolver fetch turns an immortal hang into a
16
+ * normal, catchable error. Scoped to identity lookups on purpose: this is NOT a
17
+ * whole-process watchdog.
18
+ */
19
+ const IDENTITY_LOOKUP_TIMEOUT_MS = 15_000;
7
20
  /**
8
21
  * Best-effort peek of a 2xx JSON body for plan-limit status (US-016).
9
22
  *
@@ -234,6 +247,7 @@ async function resolveCompanyByUid(token, uid) {
234
247
  const res = await vaultApiFetch({
235
248
  token,
236
249
  path: `/entity/${encodeURIComponent(uid)}`,
250
+ signal: AbortSignal.timeout(IDENTITY_LOOKUP_TIMEOUT_MS),
237
251
  });
238
252
  if (!res.ok) {
239
253
  raiseIfUnauthorized(res);
@@ -266,6 +280,7 @@ async function resolveSlugInCallerNamespace(token, slug) {
266
280
  token,
267
281
  path: '/entity/check-slug/me',
268
282
  query: { type: 'company', slug },
283
+ signal: AbortSignal.timeout(IDENTITY_LOOKUP_TIMEOUT_MS),
269
284
  });
270
285
  if (!res.ok) {
271
286
  raiseIfUnauthorized(res);
@@ -300,6 +315,7 @@ async function resolveCompanyUid(token, ref) {
300
315
  const res = await vaultApiFetch({
301
316
  token,
302
317
  path: `/entity/by-slug/company/${encodeURIComponent(ref)}`,
318
+ signal: AbortSignal.timeout(IDENTITY_LOOKUP_TIMEOUT_MS),
303
319
  });
304
320
  if (!res.ok) {
305
321
  raiseIfUnauthorized(res);
@@ -326,6 +342,7 @@ async function resolveCompanyFromMemberships(token) {
326
342
  const res = await vaultApiFetch({
327
343
  token,
328
344
  path: '/membership/me',
345
+ signal: AbortSignal.timeout(IDENTITY_LOOKUP_TIMEOUT_MS),
329
346
  });
330
347
  if (!res.ok) {
331
348
  throw new Error("Failed to fetch memberships — run `hq login` and try again");
@@ -370,6 +387,7 @@ export async function resolveCallerPersonUid(token, baseUrl) {
370
387
  token,
371
388
  path: '/entity/by-type/person',
372
389
  baseUrl,
390
+ signal: AbortSignal.timeout(IDENTITY_LOOKUP_TIMEOUT_MS),
373
391
  });
374
392
  if (!res.ok) {
375
393
  throw new Error("Failed to fetch person entity — run `hq login` and try again");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.97.1",
3
+ "version": "5.97.2",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {