@indigoai-us/hq-cli 5.334.0 → 5.335.1-beta.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
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.335.1-beta.0] — 2026-10-03
|
|
6
|
+
- Client-health refresh failures now retain the bounded Cognito OAuth error class for accurate auth diagnostics.
|
|
7
|
+
- CI: Run main-push Linux ARM checks on the reserved self-hosted runner pool.
|
|
8
|
+
|
|
9
|
+
## [5.335.0] — 2026-10-03
|
|
10
|
+
- Keep expected invalid-refresh-token events countable at warning level and tag each rejection with its allowlisted Cognito OAuth error class.
|
|
11
|
+
- Keep the intentional HQ Anywhere flag-off refusal for `hq mcp login` out of Sentry while preserving its message and exit code.
|
|
12
|
+
|
|
5
13
|
## [5.334.0] — 2026-10-03
|
|
6
14
|
- Keep keep-alive audit verdicts for expired stops independent of the lane's current state, and report restarts recorded after the grace window separately.
|
|
7
15
|
|
|
@@ -5,6 +5,7 @@ import * as os from "node:os";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import open from "open";
|
|
7
7
|
import { resolveCognitoTokenSource } from "../../utils/cognito-session.js";
|
|
8
|
+
import { markExpectedUserError } from "../../utils/expected-cli-error.js";
|
|
8
9
|
import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
|
|
9
10
|
import { resolveHqMcpOAuthOrigin } from "./mcp-endpoint.js";
|
|
10
11
|
const MCP_SCOPE = "openid email profile hq-mcp/use";
|
|
@@ -128,7 +129,7 @@ function newPkce() {
|
|
|
128
129
|
async function runLogin(options) {
|
|
129
130
|
const env = options.env ?? process.env;
|
|
130
131
|
if (!(options.isEnabled ?? (() => anywhereRuntimeEnabled(env)))()) {
|
|
131
|
-
throw new McpOAuthError("HQ Anywhere is not enabled. Enable the hq-anywhere-runtime flag to use `hq mcp login`.");
|
|
132
|
+
throw markExpectedUserError(new McpOAuthError("HQ Anywhere is not enabled. Enable the hq-anywhere-runtime flag to use `hq mcp login`."));
|
|
132
133
|
}
|
|
133
134
|
if ((options.isMachine ?? (() => resolveCognitoTokenSource(env) === "machine"))()) {
|
|
134
135
|
throw new McpOAuthError("`hq mcp login` is for person accounts; this box uses machine credentials.");
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { CLI_VERSION } from "../cli-version.js";
|
|
2
2
|
import { CognitoRefreshError } from "@indigoai-us/hq-cloud";
|
|
3
|
+
const OAUTH_ERROR_CLASSES = new Set([
|
|
4
|
+
"invalid_grant",
|
|
5
|
+
"invalid_client",
|
|
6
|
+
"unauthorized_client",
|
|
7
|
+
"invalid_request",
|
|
8
|
+
"invalid_scope",
|
|
9
|
+
"NotAuthorizedException",
|
|
10
|
+
"other",
|
|
11
|
+
"unknown",
|
|
12
|
+
]);
|
|
3
13
|
/** Capture only fixed, aggregatable auth diagnostics; never attach tokens or identity. */
|
|
4
14
|
export async function reportNoninteractivePersonRefreshRejection(error) {
|
|
5
15
|
try {
|
|
@@ -11,6 +21,15 @@ export async function reportNoninteractivePersonRefreshRejection(error) {
|
|
|
11
21
|
const rejectionKind = error instanceof CognitoRefreshError
|
|
12
22
|
? "cognito_refresh_rejection"
|
|
13
23
|
: "classified_auth_error";
|
|
24
|
+
const rawOauthError = error instanceof CognitoRefreshError &&
|
|
25
|
+
"oauthError" in error &&
|
|
26
|
+
typeof error.oauthError === "string"
|
|
27
|
+
? error.oauthError
|
|
28
|
+
: "unknown";
|
|
29
|
+
const oauthError = OAUTH_ERROR_CLASSES.has(rawOauthError)
|
|
30
|
+
? rawOauthError
|
|
31
|
+
: "unknown";
|
|
32
|
+
const expectedReauthentication = oauthError === "invalid_grant" || oauthError === "NotAuthorizedException";
|
|
14
33
|
Sentry.withScope((scope) => {
|
|
15
34
|
scope.setUser(null);
|
|
16
35
|
scope.clearBreadcrumbs();
|
|
@@ -29,10 +48,11 @@ export async function reportNoninteractivePersonRefreshRejection(error) {
|
|
|
29
48
|
...(Number.isInteger(statusCode) ? { status_code: statusCode } : {}),
|
|
30
49
|
},
|
|
31
50
|
});
|
|
32
|
-
scope.setLevel("error");
|
|
51
|
+
scope.setLevel(expectedReauthentication ? "warning" : "error");
|
|
33
52
|
scope.setFingerprint(["hq-cli", "person-refresh-rejected"]);
|
|
34
53
|
scope.setTag("component", "person-session-refresh");
|
|
35
54
|
scope.setTag("auth.rejection_kind", rejectionKind);
|
|
55
|
+
scope.setTag("auth.oauth_error", oauthError);
|
|
36
56
|
scope.setTag("auth.fresh_cache_generation_found", "false");
|
|
37
57
|
scope.setTag("platform", process.platform);
|
|
38
58
|
scope.setTag("cli_version", CLI_VERSION);
|
|
@@ -531,6 +531,73 @@ function throwIfAborted(signal) {
|
|
|
531
531
|
if (signal.aborted)
|
|
532
532
|
throw signal.reason ?? new Error("Token acquisition was aborted");
|
|
533
533
|
}
|
|
534
|
+
const COGNITO_REFRESH_OAUTH_ERROR_CLASSES = new Set([
|
|
535
|
+
"invalid_grant",
|
|
536
|
+
"invalid_client",
|
|
537
|
+
"unauthorized_client",
|
|
538
|
+
"invalid_request",
|
|
539
|
+
"invalid_scope",
|
|
540
|
+
"NotAuthorizedException",
|
|
541
|
+
]);
|
|
542
|
+
const MAX_COGNITO_REFRESH_ERROR_BODY_BYTES = 4_096;
|
|
543
|
+
function classifyCognitoRefreshOAuthError(text) {
|
|
544
|
+
try {
|
|
545
|
+
const payload = JSON.parse(text);
|
|
546
|
+
if (!payload || typeof payload !== "object" || !("error" in payload))
|
|
547
|
+
return "unknown";
|
|
548
|
+
const oauthError = payload.error;
|
|
549
|
+
if (typeof oauthError !== "string")
|
|
550
|
+
return "unknown";
|
|
551
|
+
return COGNITO_REFRESH_OAUTH_ERROR_CLASSES.has(oauthError)
|
|
552
|
+
? oauthError
|
|
553
|
+
: "other";
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return "unknown";
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function readCognitoRefreshOAuthError(response, signal) {
|
|
560
|
+
throwIfAborted(signal);
|
|
561
|
+
const reader = response.body?.getReader();
|
|
562
|
+
if (!reader)
|
|
563
|
+
return "unknown";
|
|
564
|
+
const decoder = new TextDecoder();
|
|
565
|
+
let text = "";
|
|
566
|
+
let bytesRead = 0;
|
|
567
|
+
let bodyComplete = false;
|
|
568
|
+
const cancelRead = () => {
|
|
569
|
+
void reader.cancel(signal.reason ?? new Error("Token acquisition was aborted")).catch(() => undefined);
|
|
570
|
+
};
|
|
571
|
+
signal.addEventListener("abort", cancelRead, { once: true });
|
|
572
|
+
try {
|
|
573
|
+
while (bytesRead < MAX_COGNITO_REFRESH_ERROR_BODY_BYTES) {
|
|
574
|
+
throwIfAborted(signal);
|
|
575
|
+
const chunk = await reader.read();
|
|
576
|
+
throwIfAborted(signal);
|
|
577
|
+
if (chunk.done) {
|
|
578
|
+
bodyComplete = true;
|
|
579
|
+
text += decoder.decode();
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
const boundedChunk = chunk.value.subarray(0, MAX_COGNITO_REFRESH_ERROR_BODY_BYTES - bytesRead);
|
|
583
|
+
bytesRead += boundedChunk.byteLength;
|
|
584
|
+
text += decoder.decode(boundedChunk, { stream: true });
|
|
585
|
+
}
|
|
586
|
+
throwIfAborted(signal);
|
|
587
|
+
text += decoder.decode();
|
|
588
|
+
return classifyCognitoRefreshOAuthError(text);
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
throwIfAborted(signal);
|
|
592
|
+
return "unknown";
|
|
593
|
+
}
|
|
594
|
+
finally {
|
|
595
|
+
signal.removeEventListener("abort", cancelRead);
|
|
596
|
+
if (!bodyComplete)
|
|
597
|
+
cancelRead();
|
|
598
|
+
reader.releaseLock();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
534
601
|
function waitForRefreshLock(signal, delayMs) {
|
|
535
602
|
return new Promise((resolve, reject) => {
|
|
536
603
|
const finish = (error) => {
|
|
@@ -681,7 +748,16 @@ export async function refreshActivePersonTokensForClientHealth(cached, signal) {
|
|
|
681
748
|
continue;
|
|
682
749
|
const reauth = response.status === 401 ||
|
|
683
750
|
(response.status >= 400 && response.status < 500 && ![408, 429].includes(response.status));
|
|
684
|
-
|
|
751
|
+
const oauthError = reauth
|
|
752
|
+
? await readCognitoRefreshOAuthError(response, signal)
|
|
753
|
+
: "unknown";
|
|
754
|
+
const error = new CognitoRefreshError(`Cognito refresh failed (${response.status})`, reauth, response.status, fingerprint);
|
|
755
|
+
Object.defineProperty(error, "oauthError", {
|
|
756
|
+
configurable: true,
|
|
757
|
+
enumerable: true,
|
|
758
|
+
value: oauthError,
|
|
759
|
+
});
|
|
760
|
+
throw error;
|
|
685
761
|
}
|
|
686
762
|
if (!response?.ok) {
|
|
687
763
|
throw new CognitoRefreshError("Cognito refresh failed after retry", false, undefined, fingerprint);
|