@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 +18 -0
- package/dist/commands/secrets.js +13 -2
- package/dist/utils/vault-api.js +18 -0
- package/package.json +1 -1
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
|
package/dist/commands/secrets.js
CHANGED
|
@@ -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
|
-
|
|
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));
|
package/dist/utils/vault-api.js
CHANGED
|
@@ -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");
|