@indigoai-us/hq-cli 5.109.2 → 5.109.3

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,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.109.3] — 2026-09-09
6
+
7
+ ### Fixed
8
+
9
+ - A provider that refuses the sign-in request is no longer reported as you
10
+ declining it. `hq integrations connect` treated every `?error=` on the
11
+ callback as "Sign-in was declined", so a scope the provider will not grant,
12
+ a misconfigured client, or a provider outage all read as a choice you made —
13
+ and, because declines are treated as expected, none of them reached error
14
+ reporting. Only `access_denied` is a decline now; anything else reports as a
15
+ provider refusal and is visible. The error code is matched against the
16
+ RFC 6749 set and reported as `other` when it is outside it, so nothing the
17
+ provider sends is echoed back.
18
+
5
19
  ## [5.109.2] — 2026-09-09
6
20
 
7
21
  ### Fixed
@@ -19,12 +19,32 @@
19
19
  * as `OAUTH_REDIRECT_URI_NOT_ALLOWED` at connect time.
20
20
  */
21
21
  export declare const LOOPBACK_CALLBACK_PATH = "/hq/integrations/oauth/callback";
22
+ /**
23
+ * Turn an `?error=` redirect into what to tell the user, and whether it is
24
+ * their doing.
25
+ *
26
+ * Every `?error=` used to be reported as "Sign-in was declined", marked
27
+ * expected, and therefore skipped for Sentry capture. Only `access_denied` is
28
+ * the person declining. `invalid_scope` is HQ or the catalog asking for
29
+ * something the provider does not grant; `unauthorized_client` and
30
+ * `invalid_request` are a misconfigured client; `server_error` is the provider
31
+ * failing. Reporting those as a decision the user made both misdirects the
32
+ * user — there is nothing for them to do differently — and hid the real,
33
+ * fixable causes from error reporting, because "expected" suppresses capture.
34
+ * So: `access_denied` stays expected and quiet; everything else is a provider
35
+ * rejection that reaches Sentry.
36
+ */
37
+ export declare function authorizeErrorOutcome(raw: string): {
38
+ message: string;
39
+ expected: boolean;
40
+ };
22
41
  export interface LoopbackListener {
23
42
  /** The redirect URI to hand hq-pro — includes the OS-assigned port. */
24
43
  redirectUri: string;
25
44
  /**
26
45
  * Resolves once the authorization server redirects back. Rejects on timeout,
27
- * on an `?error=` response (the person clicked Deny), or on a state mismatch.
46
+ * on an `?error=` response, or on a state mismatch. Only `access_denied`
47
+ * means the person clicked Deny — see {@link authorizeErrorOutcome}.
28
48
  */
29
49
  waitForCode(expectedState: string): Promise<string>;
30
50
  close(): void;
@@ -23,6 +23,48 @@ import { IntegrationsCliError } from "./integrations-core.js";
23
23
  export const LOOPBACK_CALLBACK_PATH = "/hq/integrations/oauth/callback";
24
24
  /** How long to wait for the browser round trip before giving the port back. */
25
25
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
26
+ /**
27
+ * The authorization-endpoint error codes RFC 6749 §4.1.2.1 defines. Anything
28
+ * outside this set is reported as `other`: the value arrives in a redirect
29
+ * query string a third party controls, so it is matched against a closed list
30
+ * and never echoed. `error_description` is deliberately never read at all.
31
+ */
32
+ const OAUTH_AUTHORIZE_ERROR_CODES = new Set([
33
+ "invalid_request",
34
+ "unauthorized_client",
35
+ "access_denied",
36
+ "unsupported_response_type",
37
+ "invalid_scope",
38
+ "server_error",
39
+ "temporarily_unavailable",
40
+ ]);
41
+ /**
42
+ * Turn an `?error=` redirect into what to tell the user, and whether it is
43
+ * their doing.
44
+ *
45
+ * Every `?error=` used to be reported as "Sign-in was declined", marked
46
+ * expected, and therefore skipped for Sentry capture. Only `access_denied` is
47
+ * the person declining. `invalid_scope` is HQ or the catalog asking for
48
+ * something the provider does not grant; `unauthorized_client` and
49
+ * `invalid_request` are a misconfigured client; `server_error` is the provider
50
+ * failing. Reporting those as a decision the user made both misdirects the
51
+ * user — there is nothing for them to do differently — and hid the real,
52
+ * fixable causes from error reporting, because "expected" suppresses capture.
53
+ * So: `access_denied` stays expected and quiet; everything else is a provider
54
+ * rejection that reaches Sentry.
55
+ */
56
+ export function authorizeErrorOutcome(raw) {
57
+ const code = raw.trim().toLowerCase();
58
+ if (code === "access_denied") {
59
+ return { message: "Sign-in was declined.", expected: true };
60
+ }
61
+ const known = OAUTH_AUTHORIZE_ERROR_CODES.has(code) ? code : "other";
62
+ return {
63
+ message: `The provider refused the sign-in request (${known}). ` +
64
+ "Nothing was connected.",
65
+ expected: false,
66
+ };
67
+ }
26
68
  /**
27
69
  * Escapes text destined for the callback page.
28
70
  *
@@ -180,7 +222,10 @@ export async function startLoopbackListener(opts = {}) {
180
222
  timer = null;
181
223
  const { code, state, error } = result;
182
224
  if (error) {
183
- reject(new IntegrationsCliError(`Sign-in was declined (${error}).`, { expected: true }));
225
+ const outcome = authorizeErrorOutcome(error);
226
+ reject(new IntegrationsCliError(outcome.message, {
227
+ expected: outcome.expected,
228
+ }));
184
229
  return;
185
230
  }
186
231
  if (!code || !state) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.109.2",
3
+ "version": "5.109.3",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {