@indigoai-us/hq-cli 5.254.0 → 5.256.0
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 +12 -0
- package/dist/commands/integrations-import.js +3 -1
- package/dist/commands/lanes-shared.d.ts +27 -0
- package/dist/commands/lanes-watch.d.ts +7 -0
- package/dist/commands/lanes-watch.js +215 -0
- package/dist/commands/lanes.js +14 -211
- package/dist/commands/pack-install.js +3 -1
- package/dist/commands/run.js +10 -2
- package/dist/commands/secrets.d.ts +4 -1
- package/dist/commands/secrets.js +56 -19
- package/dist/run/hq-plugin.d.ts +4 -0
- package/dist/run/hq-plugin.js +30 -3
- package/dist/utils/cognito-session.d.ts +2 -0
- package/dist/utils/cognito-session.js +5 -0
- package/dist/utils/secret-load-availability.d.ts +8 -0
- package/dist/utils/secret-load-availability.js +54 -0
- package/dist/utils/secrets-cache.d.ts +7 -3
- package/dist/utils/secrets-cache.js +162 -14
- package/dist/utils/vault-api.d.ts +5 -1
- package/dist/utils/vault-api.js +82 -23
- package/package.json +1 -1
|
@@ -67,10 +67,14 @@ export declare function getCompanyUid(token: string, companySlug: string | undef
|
|
|
67
67
|
* pass it when the caller is bound to another API (an enrolled agent's
|
|
68
68
|
* `apiBaseUrl`) so its token never goes to the default host.
|
|
69
69
|
*/
|
|
70
|
-
baseUrl?: string
|
|
70
|
+
baseUrl?: string, options?: {
|
|
71
|
+
allowOfflineScopeFallback?: boolean;
|
|
72
|
+
}): Promise<string>;
|
|
71
73
|
export declare function resolveCallerPersonUid(token: string, baseUrl?: string): Promise<string>;
|
|
72
74
|
export declare function getEntityUid(token: string, opts: {
|
|
73
75
|
personal?: boolean;
|
|
74
76
|
companySlug?: string;
|
|
77
|
+
}, options?: {
|
|
78
|
+
allowOfflineScopeFallback?: boolean;
|
|
75
79
|
}): Promise<string>;
|
|
76
80
|
//# sourceMappingURL=vault-api.d.ts.map
|
package/dist/utils/vault-api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_VAULT_API_URL } from './cognito-session.js';
|
|
1
|
+
import { DEFAULT_VAULT_API_URL, cognitoCachePrincipalUid } from './cognito-session.js';
|
|
2
2
|
import { CLI_VERSION } from '../cli-version.js';
|
|
3
3
|
import { Sentry } from '../sentry.js';
|
|
4
4
|
import { AuthError } from './auth-error.js';
|
|
@@ -8,6 +8,8 @@ import { networkTransportErrorCode } from './network-transport-error.js';
|
|
|
8
8
|
import { planGateErrorFromResponse } from './plan-gate-error.js';
|
|
9
9
|
import { markExpectedUserError } from './expected-cli-error.js';
|
|
10
10
|
import { redactErrorText } from './redact-error-text.js';
|
|
11
|
+
import { isSecretLoadUnavailable, isUnavailableSecretLoadHttpStatus, SecretLoadUnavailableError } from './secret-load-availability.js';
|
|
12
|
+
import { readCachedScopeUid, writeCachedScopeUid } from './secrets-cache.js';
|
|
11
13
|
/**
|
|
12
14
|
* Identity / company resolution lookups must never hang forever. These small
|
|
13
15
|
* GETs run BEFORE a command does its real work (e.g. `hq secrets env` resolves
|
|
@@ -47,8 +49,9 @@ function identityLookupTimeoutMs() {
|
|
|
47
49
|
* call .json()/.text()/.arrayBuffer(). Non-JSON and non-ok responses are
|
|
48
50
|
* returned untouched (streamed binary downloads must never be buffered).
|
|
49
51
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
+
* On inspection errors, return the original response by default to preserve
|
|
53
|
+
* existing behavior. Secret-load callers opt into rethrowing body read errors
|
|
54
|
+
* so the load path can classify a broken stream for offline-cache fallback.
|
|
52
55
|
*
|
|
53
56
|
* Why not response.clone()? Cloning tees the undici body stream; when a
|
|
54
57
|
* caller never consumes the original Response body (many commands only check
|
|
@@ -56,7 +59,7 @@ function identityLookupTimeoutMs() {
|
|
|
56
59
|
* referenced and the process (or vitest worker / spawned CLI child) never
|
|
57
60
|
* exits on Linux CI. Reading once + re-wrapping fully drains the stream.
|
|
58
61
|
*/
|
|
59
|
-
async function peekPlanLimitStatus(response) {
|
|
62
|
+
async function peekPlanLimitStatus(response, options = {}) {
|
|
60
63
|
try {
|
|
61
64
|
if (!response.ok)
|
|
62
65
|
return response;
|
|
@@ -64,14 +67,7 @@ async function peekPlanLimitStatus(response) {
|
|
|
64
67
|
if (!ct.includes('application/json') && !ct.includes('+json')) {
|
|
65
68
|
return response;
|
|
66
69
|
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
buf = await response.arrayBuffer();
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// Body may be locked/errored; original is the only thing we can return.
|
|
73
|
-
return response;
|
|
74
|
-
}
|
|
70
|
+
const buf = await response.arrayBuffer();
|
|
75
71
|
try {
|
|
76
72
|
const text = new TextDecoder().decode(buf);
|
|
77
73
|
try {
|
|
@@ -92,9 +88,11 @@ async function peekPlanLimitStatus(response) {
|
|
|
92
88
|
headers: response.headers,
|
|
93
89
|
});
|
|
94
90
|
}
|
|
95
|
-
catch {
|
|
96
|
-
//
|
|
97
|
-
//
|
|
91
|
+
catch (err) {
|
|
92
|
+
// Only secret-load callers need the body-read error for offline fallback.
|
|
93
|
+
// Other endpoints retain the pre-existing best-effort peek behavior.
|
|
94
|
+
if (options.throwOnBodyReadError)
|
|
95
|
+
throw err;
|
|
98
96
|
return response;
|
|
99
97
|
}
|
|
100
98
|
}
|
|
@@ -238,7 +236,21 @@ export async function vaultApiFetch(opts) {
|
|
|
238
236
|
throw planGate.error;
|
|
239
237
|
return planGate.response;
|
|
240
238
|
}
|
|
241
|
-
|
|
239
|
+
const isSecretLoad = method === "POST" && /^\/secrets\/[^/]+\/load$/.test(path);
|
|
240
|
+
try {
|
|
241
|
+
return await peekPlanLimitStatus(response, {
|
|
242
|
+
throwOnBodyReadError: isSecretLoad,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
// A successful /load response whose JSON stream breaks is a transient
|
|
247
|
+
// transport failure for offline-cache purposes, even when undici has
|
|
248
|
+
// already reduced its cause to a generic body-reader TypeError.
|
|
249
|
+
if (isSecretLoad) {
|
|
250
|
+
throw new SecretLoadUnavailableError("Failed to read the secret load response body", { cause: err });
|
|
251
|
+
}
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
242
254
|
}
|
|
243
255
|
/**
|
|
244
256
|
* Public (NONE-auth) GET against the vault API — no bearer token. The
|
|
@@ -324,6 +336,9 @@ async function resolveCompanyByUid(token, uid, baseUrl) {
|
|
|
324
336
|
if (!res.ok) {
|
|
325
337
|
raiseIfUnauthorized(res);
|
|
326
338
|
const body = (await res.json().catch(() => ({})));
|
|
339
|
+
if (isUnavailableSecretLoadHttpStatus(res.status)) {
|
|
340
|
+
throw new SecretLoadUnavailableError(`Failed to resolve company '${uid}': ${formatSlugLookupReason(res, body)}`);
|
|
341
|
+
}
|
|
327
342
|
throw new Error(`Failed to resolve company '${uid}': ${formatSlugLookupReason(res, body)}`);
|
|
328
343
|
}
|
|
329
344
|
const data = (await res.json());
|
|
@@ -492,6 +507,9 @@ async function resolveCompanyUid(token, ref, baseUrl) {
|
|
|
492
507
|
? " This is a temporary lookup failure, not a missing company. Retry the command."
|
|
493
508
|
: "";
|
|
494
509
|
const error = new Error(redactedSlugLookupMessage(`Failed to resolve company slug '${ref}': ${reason}.${hint}`));
|
|
510
|
+
if (retryable && (mine.kind === "transient" || isUnavailableSecretLoadHttpStatus(res.status))) {
|
|
511
|
+
throw new SecretLoadUnavailableError(error.message);
|
|
512
|
+
}
|
|
495
513
|
throw retryable ? error : markExpectedUserError(error);
|
|
496
514
|
}
|
|
497
515
|
const data = (await res.json());
|
|
@@ -506,6 +524,9 @@ async function resolveCompanyFromMemberships(token, baseUrl) {
|
|
|
506
524
|
});
|
|
507
525
|
if (!res.ok) {
|
|
508
526
|
const error = new Error("Failed to fetch memberships — run `hq login` and try again");
|
|
527
|
+
if (isUnavailableSecretLoadHttpStatus(res.status)) {
|
|
528
|
+
throw new SecretLoadUnavailableError(error.message);
|
|
529
|
+
}
|
|
509
530
|
throw res.status === 401 ? markExpectedUserError(error) : error;
|
|
510
531
|
}
|
|
511
532
|
const data = (await res.json());
|
|
@@ -529,7 +550,7 @@ export async function getCompanyUid(token, companySlug,
|
|
|
529
550
|
* pass it when the caller is bound to another API (an enrolled agent's
|
|
530
551
|
* `apiBaseUrl`) so its token never goes to the default host.
|
|
531
552
|
*/
|
|
532
|
-
baseUrl) {
|
|
553
|
+
baseUrl, options = {}) {
|
|
533
554
|
// Vault API keys are company-bound. Entity slug resolution has no keyed
|
|
534
555
|
// route, so under hqk_ we accept an explicit cmp_ uid (or HQ_COMPANY_UID)
|
|
535
556
|
// and otherwise fall through to keyed GET /v1/keys/membership/me.
|
|
@@ -544,10 +565,29 @@ baseUrl) {
|
|
|
544
565
|
}
|
|
545
566
|
return resolveCompanyFromMemberships(token, baseUrl);
|
|
546
567
|
}
|
|
547
|
-
if (
|
|
548
|
-
return
|
|
568
|
+
if (!options.allowOfflineScopeFallback) {
|
|
569
|
+
return companySlug
|
|
570
|
+
? resolveCompanyUid(token, companySlug, baseUrl)
|
|
571
|
+
: resolveCompanyFromMemberships(token, baseUrl);
|
|
572
|
+
}
|
|
573
|
+
const scopeKey = `company:${companySlug ?? "<active-membership>"}`;
|
|
574
|
+
const principalUid = cognitoCachePrincipalUid(token);
|
|
575
|
+
try {
|
|
576
|
+
const uid = companySlug
|
|
577
|
+
? await resolveCompanyUid(token, companySlug, baseUrl)
|
|
578
|
+
: await resolveCompanyFromMemberships(token, baseUrl);
|
|
579
|
+
if (principalUid)
|
|
580
|
+
writeCachedScopeUid(scopeKey, principalUid, uid);
|
|
581
|
+
return uid;
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
if (!principalUid || !isSecretLoadUnavailable(err))
|
|
585
|
+
throw err;
|
|
586
|
+
const cachedUid = readCachedScopeUid(scopeKey, principalUid);
|
|
587
|
+
if (!cachedUid)
|
|
588
|
+
throw err;
|
|
589
|
+
return cachedUid;
|
|
549
590
|
}
|
|
550
|
-
return resolveCompanyFromMemberships(token, baseUrl);
|
|
551
591
|
}
|
|
552
592
|
// Same selection rule as the backend's `resolveCallerPersonUid`: ascending by
|
|
553
593
|
// createdAt, tie-break by uid ascending. Returns the `prs_*` UID.
|
|
@@ -559,6 +599,9 @@ export async function resolveCallerPersonUid(token, baseUrl) {
|
|
|
559
599
|
signal: AbortSignal.timeout(identityLookupTimeoutMs()),
|
|
560
600
|
});
|
|
561
601
|
if (!res.ok) {
|
|
602
|
+
if (isUnavailableSecretLoadHttpStatus(res.status)) {
|
|
603
|
+
throw new SecretLoadUnavailableError("Failed to fetch person entity — run `hq login` and try again");
|
|
604
|
+
}
|
|
562
605
|
throw new Error("Failed to fetch person entity — run `hq login` and try again");
|
|
563
606
|
}
|
|
564
607
|
const data = (await res.json());
|
|
@@ -578,10 +621,26 @@ export async function resolveCallerPersonUid(token, baseUrl) {
|
|
|
578
621
|
// Resolves the scope UID (cmp_* or prs_*) for a secrets command. Precedence:
|
|
579
622
|
// `--personal` → caller's canonical person entity; else `--company <slug>` →
|
|
580
623
|
// resolved company UID; else fallback to single active company membership.
|
|
581
|
-
export async function getEntityUid(token, opts) {
|
|
624
|
+
export async function getEntityUid(token, opts, options = {}) {
|
|
582
625
|
if (opts.personal) {
|
|
583
|
-
|
|
626
|
+
if (!options.allowOfflineScopeFallback)
|
|
627
|
+
return resolveCallerPersonUid(token);
|
|
628
|
+
const principalUid = cognitoCachePrincipalUid(token);
|
|
629
|
+
try {
|
|
630
|
+
const uid = await resolveCallerPersonUid(token);
|
|
631
|
+
if (principalUid)
|
|
632
|
+
writeCachedScopeUid("personal", principalUid, uid);
|
|
633
|
+
return uid;
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
if (!principalUid || !isSecretLoadUnavailable(err))
|
|
637
|
+
throw err;
|
|
638
|
+
const cachedUid = readCachedScopeUid("personal", principalUid);
|
|
639
|
+
if (!cachedUid)
|
|
640
|
+
throw err;
|
|
641
|
+
return cachedUid;
|
|
642
|
+
}
|
|
584
643
|
}
|
|
585
|
-
return getCompanyUid(token, opts.companySlug);
|
|
644
|
+
return getCompanyUid(token, opts.companySlug, undefined, options);
|
|
586
645
|
}
|
|
587
646
|
//# sourceMappingURL=vault-api.js.map
|