@indigoai-us/hq-cli 5.103.26 → 5.103.28
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 +4 -0
- package/dist/commands/dm.js +3 -7
- package/dist/commands/integrations-core.d.ts +15 -3
- package/dist/commands/integrations-core.js +25 -3
- package/dist/commands/integrations-manage.js +4 -1
- package/dist/commands/integrations.js +2 -1
- package/dist/lib/doctor/checks/integrations.d.ts +8 -23
- package/dist/lib/doctor/checks/integrations.js +47 -150
- package/dist/lib/integrations/health.d.ts +78 -0
- package/dist/lib/integrations/health.js +255 -0
- package/dist/lib/integrations/provider-slug.d.ts +10 -0
- package/dist/lib/integrations/provider-slug.js +12 -0
- package/dist/lib/search-index/index.d.ts +44 -0
- package/dist/lib/search-index/index.js +111 -18
- package/dist/main.js +46 -2
- package/dist/utils/qmd-llm-disabled-error.d.ts +10 -0
- package/dist/utils/qmd-llm-disabled-error.js +87 -0
- package/dist/utils/qmd-module-missing-error.d.ts +41 -0
- package/dist/utils/qmd-module-missing-error.js +130 -0
- package/dist/utils/sentry-fingerprint.js +96 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dist/commands/dm.js
CHANGED
|
@@ -272,15 +272,11 @@ async function runGroupSend(recipients, message) {
|
|
|
272
272
|
for (const r of recipients) {
|
|
273
273
|
const rc = detectRecipient(r);
|
|
274
274
|
if (!rc) {
|
|
275
|
-
console.error(chalk.red(`Invalid recipient '${r}': each must be an email address
|
|
276
|
-
process.exit(1);
|
|
277
|
-
}
|
|
278
|
-
// Group DMs are channels — agents don't participate in channels (their
|
|
279
|
-
// DM surface is 1:1 via the durable box inbox). DM an agent directly.
|
|
280
|
-
if (rc.toPersonUid?.startsWith("agt_")) {
|
|
281
|
-
console.error(chalk.red(`Agents can't join group DMs yet — DM '${r}' directly: hq dm ${r} "<message>".`));
|
|
275
|
+
console.error(chalk.red(`Invalid recipient '${r}': each must be an email address, a personUid (prs_…), or an agentUid (agt_…).`));
|
|
282
276
|
process.exit(1);
|
|
283
277
|
}
|
|
278
|
+
// People (prs_) and agents (agt_) are both valid group participants —
|
|
279
|
+
// the server (handleCreateGroupDm) accepts agt_* uids like any other.
|
|
284
280
|
participants.push(rc.toEmail ?? rc.toPersonUid);
|
|
285
281
|
}
|
|
286
282
|
if (participants.length < 2) {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Sentry live in exactly one place. `integrations.ts` re-exports this module's
|
|
9
9
|
* public surface, so existing importers keep working unchanged.
|
|
10
10
|
*/
|
|
11
|
+
export { bareProvider } from "../lib/integrations/provider-slug.js";
|
|
11
12
|
/** Write policy on a connection — mirrors hq-pro's `WritePolicy`. */
|
|
12
13
|
export type WritePolicy = "auto-allow" | "confirm" | "deny";
|
|
13
14
|
/** Who a grant points at — mirrors hq-pro's `AclGranteeType`. */
|
|
@@ -199,8 +200,6 @@ export declare function raiseIfUnauthorized(res: Response): void;
|
|
|
199
200
|
export declare function raiseForResponse(res: Response, fallback: string): Promise<never>;
|
|
200
201
|
/** "factory:linear" → "linear"; mirrors hq-pro's factoryToolPrefix. */
|
|
201
202
|
export declare function toolPrefixForProvider(provider: string): string;
|
|
202
|
-
/** "factory:linear" → "linear", for display and for `--provider` echoes. */
|
|
203
|
-
export declare function bareProvider(provider: string): string;
|
|
204
203
|
/**
|
|
205
204
|
* Read the whole admin surface: connections with their governance state, the
|
|
206
205
|
* viewer's role, and the recent audit feed. Several verbs need more than the
|
|
@@ -264,5 +263,18 @@ interface QueuedOutcome {
|
|
|
264
263
|
}
|
|
265
264
|
export declare function queuedOutcome(payload: unknown): QueuedOutcome | null;
|
|
266
265
|
export declare function printJson(value: unknown): void;
|
|
267
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Print why a connection is unusable and how to repair it, directly beneath its
|
|
268
|
+
* status line.
|
|
269
|
+
*
|
|
270
|
+
* `list` and `show` used to print a bare `error`, leaving the diagnosis the
|
|
271
|
+
* control plane had already sent reachable only through a separate `hq doctor`
|
|
272
|
+
* run. Classification is shared with that check rather than duplicated, so the
|
|
273
|
+
* two surfaces cannot drift into disagreeing about the same row.
|
|
274
|
+
*
|
|
275
|
+
* Healthy and revoked rows print nothing: revoked tombstones carry their own
|
|
276
|
+
* re-add wording, and a repair path printed against a working connection sends
|
|
277
|
+
* a user to fix something that is not broken.
|
|
278
|
+
*/
|
|
279
|
+
export declare function printHealthNotice(connection: AdminConnection): void;
|
|
268
280
|
//# sourceMappingURL=integrations-core.d.ts.map
|
|
@@ -9,9 +9,13 @@
|
|
|
9
9
|
* public surface, so existing importers keep working unchanged.
|
|
10
10
|
*/
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
|
+
import chalk from "chalk";
|
|
12
13
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
13
14
|
import { AuthError } from "../utils/auth-error.js";
|
|
14
15
|
import { redactErrorText } from "../utils/redact-error-text.js";
|
|
16
|
+
import { connectionHealthNotice } from "../lib/integrations/health.js";
|
|
17
|
+
import { bareProvider } from "../lib/integrations/provider-slug.js";
|
|
18
|
+
export { bareProvider } from "../lib/integrations/provider-slug.js";
|
|
15
19
|
export class IntegrationsCliError extends Error {
|
|
16
20
|
/**
|
|
17
21
|
* True when the error is the caller's request/state/permission (a client 4xx
|
|
@@ -196,9 +200,6 @@ export function toolPrefixForProvider(provider) {
|
|
|
196
200
|
.replace(/[^a-z0-9]+/g, ".");
|
|
197
201
|
}
|
|
198
202
|
/** "factory:linear" → "linear", for display and for `--provider` echoes. */
|
|
199
|
-
export function bareProvider(provider) {
|
|
200
|
-
return provider.replace(/^factory:/, "");
|
|
201
|
-
}
|
|
202
203
|
/** Turn a factory display name into the human-friendly slug people type. */
|
|
203
204
|
function humanSlug(value) {
|
|
204
205
|
return value
|
|
@@ -441,4 +442,25 @@ export function queuedOutcome(payload) {
|
|
|
441
442
|
export function printJson(value) {
|
|
442
443
|
console.log(JSON.stringify(value, null, 2));
|
|
443
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* Print why a connection is unusable and how to repair it, directly beneath its
|
|
447
|
+
* status line.
|
|
448
|
+
*
|
|
449
|
+
* `list` and `show` used to print a bare `error`, leaving the diagnosis the
|
|
450
|
+
* control plane had already sent reachable only through a separate `hq doctor`
|
|
451
|
+
* run. Classification is shared with that check rather than duplicated, so the
|
|
452
|
+
* two surfaces cannot drift into disagreeing about the same row.
|
|
453
|
+
*
|
|
454
|
+
* Healthy and revoked rows print nothing: revoked tombstones carry their own
|
|
455
|
+
* re-add wording, and a repair path printed against a working connection sends
|
|
456
|
+
* a user to fix something that is not broken.
|
|
457
|
+
*/
|
|
458
|
+
export function printHealthNotice(connection) {
|
|
459
|
+
const notice = connectionHealthNotice(connection);
|
|
460
|
+
if (!notice)
|
|
461
|
+
return;
|
|
462
|
+
console.log(chalk.yellow(` ${notice.headline}`));
|
|
463
|
+
if (notice.remediation)
|
|
464
|
+
console.log(chalk.yellow(` ${notice.remediation}`));
|
|
465
|
+
}
|
|
444
466
|
//# sourceMappingURL=integrations-core.js.map
|
|
@@ -16,7 +16,7 @@ import readline from "node:readline";
|
|
|
16
16
|
import chalk from "chalk";
|
|
17
17
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
18
18
|
import { getCompanyUid } from "../utils/vault-api.js";
|
|
19
|
-
import { IntegrationsCliError, bareProvider, fetchConnections, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
|
|
19
|
+
import { IntegrationsCliError, bareProvider, printHealthNotice, fetchConnections, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
|
|
20
20
|
import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, purgeConnection, setReadSafe, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
|
|
21
21
|
const WRITE_POLICIES = ["auto-allow", "confirm", "deny"];
|
|
22
22
|
const PERMISSIONS = ["read", "write", "admin"];
|
|
@@ -238,6 +238,9 @@ export function registerManageCommands(integrations) {
|
|
|
238
238
|
if (install?.status === "needs_credentials") {
|
|
239
239
|
console.log(chalk.yellow(" Needs sign-in — run `hq integrations reconnect` to fix it."));
|
|
240
240
|
}
|
|
241
|
+
// Last, so the diagnosis and its fix path are the final thing on screen
|
|
242
|
+
// rather than buried above the sharing and ownership lines.
|
|
243
|
+
printHealthNotice(connection);
|
|
241
244
|
});
|
|
242
245
|
integrations
|
|
243
246
|
.command("policy [app]")
|
|
@@ -52,7 +52,7 @@ import { randomUUID } from "node:crypto";
|
|
|
52
52
|
import chalk from "chalk";
|
|
53
53
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
54
54
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
55
|
-
import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
55
|
+
import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printHealthNotice, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
56
56
|
import { registerConnectCommands } from "./integrations-connect.js";
|
|
57
57
|
import { registerImportCommands } from "./integrations-import.js";
|
|
58
58
|
import { registerManageCommands } from "./integrations-manage.js";
|
|
@@ -190,6 +190,7 @@ export function registerIntegrationsCommand(program) {
|
|
|
190
190
|
.join(" · ");
|
|
191
191
|
console.log(`${chalk.bold(connectionLabel(c))} ${chalk.dim(flags)}`);
|
|
192
192
|
console.log(chalk.dim(` connection: ${c.id}`));
|
|
193
|
+
printHealthNotice(c);
|
|
193
194
|
}
|
|
194
195
|
});
|
|
195
196
|
registerImportCommands(integrations);
|
|
@@ -7,41 +7,26 @@
|
|
|
7
7
|
* stored health signals only: a recorded rejection overrides a misleading
|
|
8
8
|
* `connected` status, but a clean connected row is not live-provider proof.
|
|
9
9
|
*/
|
|
10
|
-
import { type
|
|
10
|
+
import { type IntegrationConnection } from "../../integrations/health.js";
|
|
11
11
|
import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Health classification is shared with `hq integrations list`/`show` and lives
|
|
14
|
+
* in `lib/integrations/health.ts`. Re-exported here so the doctor family stays
|
|
15
|
+
* the single import surface for callers that already depend on it.
|
|
16
|
+
*/
|
|
17
|
+
export { classifyConnection } from "../../integrations/health.js";
|
|
18
|
+
export type { IntegrationConnection } from "../../integrations/health.js";
|
|
12
19
|
export declare const INTEGRATIONS_FAMILY_ID = "integrations";
|
|
13
20
|
export declare const INTEGRATIONS_PREFIX = "integrations";
|
|
14
|
-
/** Additive health fields returned by newer control planes. */
|
|
15
|
-
export interface IntegrationConnection extends AdminConnection {
|
|
16
|
-
errorReason?: string;
|
|
17
|
-
degradedReason?: string;
|
|
18
|
-
needsReauthReason?: string;
|
|
19
|
-
/** Server-derived remediation flow; optional for older control planes. */
|
|
20
|
-
fix_path?: string;
|
|
21
|
-
/** Server-derived remediation class; optional for older control planes. */
|
|
22
|
-
fix_kind?: string;
|
|
23
|
-
}
|
|
24
21
|
export interface IntegrationsDoctorDeps {
|
|
25
22
|
ensureToken?: () => Promise<string>;
|
|
26
23
|
resolveCompany?: (token: string, company: string | undefined) => Promise<string>;
|
|
27
24
|
listConnections?: (token: string, companyUid: string) => Promise<IntegrationConnection[]>;
|
|
28
25
|
}
|
|
29
|
-
type FindingKind = "reconnect" | "re-add" | "contact-admin" | "provider-blocked" | "retryable" | "hq-configuration";
|
|
30
|
-
interface Finding {
|
|
31
|
-
provider: string;
|
|
32
|
-
connectionId: string;
|
|
33
|
-
kind: FindingKind;
|
|
34
|
-
message: string;
|
|
35
|
-
/** Optional HQ-authored remediation returned by the control plane. */
|
|
36
|
-
remediation?: string;
|
|
37
|
-
}
|
|
38
26
|
/**
|
|
39
27
|
* Expected session/company prerequisites degrade to NA instead of crashing or
|
|
40
28
|
* becoming a false connection failure. An unreadable inventory remains UNKNOWN.
|
|
41
29
|
*/
|
|
42
30
|
export declare function checkIntegrations(context: CheckContext, deps?: IntegrationsDoctorDeps): Promise<CheckResult[]>;
|
|
43
|
-
/** Classify without echoing untrusted provider text, which may contain secrets. */
|
|
44
|
-
export declare function classifyConnection(connection: IntegrationConnection): Finding[];
|
|
45
31
|
export declare const integrationsFamily: CheckFamily;
|
|
46
|
-
export {};
|
|
47
32
|
//# sourceMappingURL=integrations.d.ts.map
|
|
@@ -11,20 +11,16 @@ import { ensureCognitoIdToken } from "../../../utils/cognito-session.js";
|
|
|
11
11
|
import { isAuthError } from "../../../utils/auth-error.js";
|
|
12
12
|
import { isCompanySelectionError } from "../../../utils/company-selection-error.js";
|
|
13
13
|
import { getCompanyUid } from "../../../utils/vault-api.js";
|
|
14
|
-
import {
|
|
14
|
+
import { fetchConnections, IntegrationsCliError } from "../../../commands/integrations-core.js";
|
|
15
|
+
import { classifyConnection } from "../../integrations/health.js";
|
|
16
|
+
/**
|
|
17
|
+
* Health classification is shared with `hq integrations list`/`show` and lives
|
|
18
|
+
* in `lib/integrations/health.ts`. Re-exported here so the doctor family stays
|
|
19
|
+
* the single import surface for callers that already depend on it.
|
|
20
|
+
*/
|
|
21
|
+
export { classifyConnection } from "../../integrations/health.js";
|
|
15
22
|
export const INTEGRATIONS_FAMILY_ID = "integrations";
|
|
16
23
|
export const INTEGRATIONS_PREFIX = "integrations";
|
|
17
|
-
const RECONNECT_REASON_CODES = new Set([
|
|
18
|
-
"oauth_refresh_invalid_grant",
|
|
19
|
-
"oauth_refresh_unavailable",
|
|
20
|
-
"token_refresh_failed",
|
|
21
|
-
"credentials_rejected",
|
|
22
|
-
]);
|
|
23
|
-
const RETRYABLE_REASON_CODES = new Set([
|
|
24
|
-
"oauth_refresh_transient",
|
|
25
|
-
"oauth_refresh_write_conflict",
|
|
26
|
-
]);
|
|
27
|
-
const HQ_CONFIGURATION_REASON_CODE = "oauth_client_secret_unavailable";
|
|
28
24
|
/**
|
|
29
25
|
* Expected session/company prerequisites degrade to NA instead of crashing or
|
|
30
26
|
* becoming a false connection failure. An unreadable inventory remains UNKNOWN.
|
|
@@ -93,144 +89,6 @@ export async function checkIntegrations(context, deps = {}) {
|
|
|
93
89
|
}
|
|
94
90
|
return groupFindings(findings, context.company);
|
|
95
91
|
}
|
|
96
|
-
/** Classify without echoing untrusted provider text, which may contain secrets. */
|
|
97
|
-
export function classifyConnection(connection) {
|
|
98
|
-
if (connection.status === "revoked")
|
|
99
|
-
return [];
|
|
100
|
-
const provider = bareProvider(connection.provider);
|
|
101
|
-
const reason = recordedReason(connection);
|
|
102
|
-
const connectedWithRecordedFailure = connection.status === "connected" && reason !== "";
|
|
103
|
-
const knownReasonCode = knownReasonCodeFor(connection);
|
|
104
|
-
if (knownReasonCode) {
|
|
105
|
-
const finding = findingForKnownReasonCode(connection, provider, knownReasonCode);
|
|
106
|
-
// Retryable and HQ-configuration reason codes diagnose conditions that a
|
|
107
|
-
// caller-specific remediation cannot change. Reconnect-class codes defer
|
|
108
|
-
// to the server's credential/role-aware remediation classification.
|
|
109
|
-
return [withServerRemediation(connection, RECONNECT_REASON_CODES.has(knownReasonCode)
|
|
110
|
-
? findingForServerFixKind(finding, connection.fix_kind)
|
|
111
|
-
: finding)];
|
|
112
|
-
}
|
|
113
|
-
// A stale remediation value does not make a clean connected row unhealthy.
|
|
114
|
-
if (!connectedWithRecordedFailure && connection.status === "connected")
|
|
115
|
-
return [];
|
|
116
|
-
if (isProviderBlocked(reason) || connection.status === "degraded") {
|
|
117
|
-
return [withServerRemediation(connection, {
|
|
118
|
-
provider,
|
|
119
|
-
connectionId: connection.id,
|
|
120
|
-
kind: "provider-blocked",
|
|
121
|
-
message: connectedWithRecordedFailure
|
|
122
|
-
? "reports connected, but recorded provider health says access is blocked upstream"
|
|
123
|
-
: "provider-side access is blocked or unavailable",
|
|
124
|
-
})];
|
|
125
|
-
}
|
|
126
|
-
const fallback = genericReconnectFinding(connection, provider);
|
|
127
|
-
const serverClassified = findingForServerFixKind(fallback, connection.fix_kind);
|
|
128
|
-
if (serverClassified.kind !== "reconnect") {
|
|
129
|
-
return [withServerRemediation(connection, serverClassified)];
|
|
130
|
-
}
|
|
131
|
-
if (isTokenRefreshFailure(reason) || isCredentialFailure(reason)) {
|
|
132
|
-
const problem = isTokenRefreshFailure(reason)
|
|
133
|
-
? "token refresh failed"
|
|
134
|
-
: connectedWithRecordedFailure
|
|
135
|
-
? "reports connected, but the provider rejected the stored credentials"
|
|
136
|
-
: "the provider rejected the stored credentials";
|
|
137
|
-
return [withServerRemediation(connection, {
|
|
138
|
-
provider,
|
|
139
|
-
connectionId: connection.id,
|
|
140
|
-
kind: "reconnect",
|
|
141
|
-
message: problem,
|
|
142
|
-
})];
|
|
143
|
-
}
|
|
144
|
-
if (connection.status === "needs-reauth" || connection.status === "error") {
|
|
145
|
-
return [withServerRemediation(connection, fallback)];
|
|
146
|
-
}
|
|
147
|
-
if (connection.status !== "connected") {
|
|
148
|
-
return [withServerRemediation(connection, {
|
|
149
|
-
provider,
|
|
150
|
-
connectionId: connection.id,
|
|
151
|
-
kind: "reconnect",
|
|
152
|
-
message: `reports an unrecognized non-healthy status (${connection.status})`,
|
|
153
|
-
})];
|
|
154
|
-
}
|
|
155
|
-
return [];
|
|
156
|
-
}
|
|
157
|
-
function genericReconnectFinding(connection, provider) {
|
|
158
|
-
return {
|
|
159
|
-
provider,
|
|
160
|
-
connectionId: connection.id,
|
|
161
|
-
kind: "reconnect",
|
|
162
|
-
message: connection.status === "needs-reauth"
|
|
163
|
-
? "needs re-authentication"
|
|
164
|
-
: "is in an error state",
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/** Only values emitted by hq-pro affect the classification; future values fall back. */
|
|
168
|
-
function findingForServerFixKind(fallback, fixKind) {
|
|
169
|
-
switch (fixKind) {
|
|
170
|
-
case "re-add":
|
|
171
|
-
return {
|
|
172
|
-
...fallback,
|
|
173
|
-
kind: "re-add",
|
|
174
|
-
message: "requires its API key to be re-entered",
|
|
175
|
-
};
|
|
176
|
-
case "contact-admin":
|
|
177
|
-
return {
|
|
178
|
-
...fallback,
|
|
179
|
-
kind: "contact-admin",
|
|
180
|
-
message: "must be repaired by a company owner or admin",
|
|
181
|
-
};
|
|
182
|
-
case "reconnect":
|
|
183
|
-
default:
|
|
184
|
-
return fallback;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* A fix path is server-authored, role- and credential-aware wording. It cannot
|
|
189
|
-
* make a connection unhealthy: the caller must first have classified a health
|
|
190
|
-
* signal, so a stale fix path on an otherwise clean connected row is ignored.
|
|
191
|
-
*/
|
|
192
|
-
function withServerRemediation(connection, finding) {
|
|
193
|
-
return connection.fix_path
|
|
194
|
-
? { ...finding, remediation: connection.fix_path }
|
|
195
|
-
: finding;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* These are a closed hq-pro contract. Exact matching deliberately comes before
|
|
199
|
-
* the legacy text heuristics, which remain below for old and unknown rows.
|
|
200
|
-
*/
|
|
201
|
-
function knownReasonCodeFor(connection) {
|
|
202
|
-
return [connection.errorReason, connection.needsReauthReason, connection.degradedReason]
|
|
203
|
-
.find((value) => (typeof value === "string" &&
|
|
204
|
-
(RECONNECT_REASON_CODES.has(value) ||
|
|
205
|
-
RETRYABLE_REASON_CODES.has(value) ||
|
|
206
|
-
value === HQ_CONFIGURATION_REASON_CODE)));
|
|
207
|
-
}
|
|
208
|
-
function findingForKnownReasonCode(connection, provider, code) {
|
|
209
|
-
if (RECONNECT_REASON_CODES.has(code)) {
|
|
210
|
-
return {
|
|
211
|
-
provider,
|
|
212
|
-
connectionId: connection.id,
|
|
213
|
-
kind: "reconnect",
|
|
214
|
-
message: "stored credentials need re-authentication",
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
if (RETRYABLE_REASON_CODES.has(code)) {
|
|
218
|
-
return {
|
|
219
|
-
provider,
|
|
220
|
-
connectionId: connection.id,
|
|
221
|
-
kind: "retryable",
|
|
222
|
-
message: code === "oauth_refresh_write_conflict"
|
|
223
|
-
? "token refresh lost a concurrent write race; the stored credential remains intact"
|
|
224
|
-
: "token refresh is temporarily unavailable; the stored credential remains intact",
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
return {
|
|
228
|
-
provider,
|
|
229
|
-
connectionId: connection.id,
|
|
230
|
-
kind: "hq-configuration",
|
|
231
|
-
message: "the HQ OAuth client secret is unavailable",
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
92
|
function groupFindings(findings, company) {
|
|
235
93
|
const groups = new Map();
|
|
236
94
|
for (const finding of findings) {
|
|
@@ -288,6 +146,15 @@ function resultForGroup(entries, company) {
|
|
|
288
146
|
...(serverRemediation ? { remediation: serverRemediation } : {}),
|
|
289
147
|
};
|
|
290
148
|
}
|
|
149
|
+
if (first.kind === "wait") {
|
|
150
|
+
return {
|
|
151
|
+
status: "WARN",
|
|
152
|
+
checkId: `${INTEGRATIONS_PREFIX}.wait.${first.provider}`,
|
|
153
|
+
target: namedConnections,
|
|
154
|
+
message: `${first.provider}: ${count} ${plural} ${first.message}. Wait for it to complete, then retry the operation.`,
|
|
155
|
+
remediation: serverRemediation ?? "Wait for the Factory installation to complete, then retry the operation.",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
291
158
|
if (first.kind === "hq-configuration") {
|
|
292
159
|
return {
|
|
293
160
|
status: "FAIL",
|
|
@@ -298,6 +165,36 @@ function resultForGroup(entries, company) {
|
|
|
298
165
|
};
|
|
299
166
|
}
|
|
300
167
|
const companyArg = company ? ` --company ${company}` : "";
|
|
168
|
+
if (first.kind === "undiagnosed") {
|
|
169
|
+
// The control plane reported a fault and explained none of it. Printing the
|
|
170
|
+
// reconnect command here would be the same guess `classifyConnection`
|
|
171
|
+
// refuses to make: it may cost the reader a working credential and, when it
|
|
172
|
+
// does not help, leaves them with nothing else to try. Name what HQ knows,
|
|
173
|
+
// point at the one command that can show more, and let a human decide.
|
|
174
|
+
//
|
|
175
|
+
// One `show` per listed connection, not just the first. Grouping keys on
|
|
176
|
+
// the message, and every undiagnosed row carries the same HQ-authored
|
|
177
|
+
// message whatever its unrecognized reason code was — so a single command
|
|
178
|
+
// would leave the rest of the group uninspected with their differing
|
|
179
|
+
// hidden reasons unread. `show` resolves exactly one connection.
|
|
180
|
+
const inspect = preview
|
|
181
|
+
.map((id) => `hq integrations show --connection ${id}${companyArg}`)
|
|
182
|
+
.join("; ");
|
|
183
|
+
return {
|
|
184
|
+
status: "FAIL",
|
|
185
|
+
checkId: `${INTEGRATIONS_PREFIX}.undiagnosed.${first.provider}`,
|
|
186
|
+
target: namedConnections,
|
|
187
|
+
// The caution belongs only where nothing supplied a fix. With a
|
|
188
|
+
// server-authored remediation printed below, warning that reconnecting
|
|
189
|
+
// may not help would contradict the very instruction being given.
|
|
190
|
+
message: serverRemediation
|
|
191
|
+
? `${first.provider}: ${count} ${plural} ${first.message}.`
|
|
192
|
+
: `${first.provider}: ${count} ${plural} ${first.message}. HQ has no diagnosis for this, so reconnecting may not be the fix.`,
|
|
193
|
+
remediation: serverRemediation ?? (overflow > 0
|
|
194
|
+
? `Inspect the listed connections, then repeat for the remaining ${overflow}: ${inspect}`
|
|
195
|
+
: inspect),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
301
198
|
const remediation = preview
|
|
302
199
|
.map((id) => `hq integrations reconnect --connection ${id}${companyArg}`)
|
|
303
200
|
.join("; ");
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection health classification, shared by every surface that has to explain
|
|
3
|
+
* why an integration is not usable.
|
|
4
|
+
*
|
|
5
|
+
* This lived inside the `hq doctor` integrations check until `list`/`show` had
|
|
6
|
+
* to answer the same question. It is deliberately presentation-free and makes
|
|
7
|
+
* no network calls: it classifies stored health signals only, so a caller can
|
|
8
|
+
* render a finding without deciding what the underlying codes mean. Keeping one
|
|
9
|
+
* copy is the point — `hq doctor` and `hq integrations show` disagreeing about
|
|
10
|
+
* whether a row needs a reconnect is exactly the confusion this family exists
|
|
11
|
+
* to remove.
|
|
12
|
+
*
|
|
13
|
+
* It never echoes untrusted provider text: upstream messages can carry tokens or
|
|
14
|
+
* secret-bearing URLs, so every user-visible string here is HQ-authored, and the
|
|
15
|
+
* only externally-sourced string that reaches a user is the server's own
|
|
16
|
+
* `fix_path`.
|
|
17
|
+
*/
|
|
18
|
+
import type { AdminConnection } from "../../commands/integrations-core.js";
|
|
19
|
+
/** Additive health fields returned by newer control planes. */
|
|
20
|
+
export interface IntegrationConnection extends AdminConnection {
|
|
21
|
+
errorReason?: string;
|
|
22
|
+
degradedReason?: string;
|
|
23
|
+
needsReauthReason?: string;
|
|
24
|
+
/** Server-derived remediation flow; optional for older control planes. */
|
|
25
|
+
fix_path?: string;
|
|
26
|
+
/** Server-derived remediation class; optional for older control planes. */
|
|
27
|
+
fix_kind?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Remediation class — WHO has to act and WHAT they have to do.
|
|
31
|
+
*
|
|
32
|
+
* `undiagnosed` is the console's twin member (`_connection-health.ts` `FixKind`)
|
|
33
|
+
* and lands here to close that divergence. Before it, an unhealthy row the
|
|
34
|
+
* server described with neither a recognised reason code NOR a `fix_kind` fell
|
|
35
|
+
* back to `reconnect`, so `hq doctor` told a person to sign in again on the
|
|
36
|
+
* strength of no diagnosis at all — advice that can make them discard a
|
|
37
|
+
* credential that was never the problem, and that leaves them no better off
|
|
38
|
+
* when it does not help. An admission is more useful than a guess.
|
|
39
|
+
*
|
|
40
|
+
* A server that DID say `fix_kind: "reconnect"` still classifies as
|
|
41
|
+
* `reconnect`: that is a diagnosis, not an absence of one.
|
|
42
|
+
*/
|
|
43
|
+
export type FindingKind = "reconnect" | "re-add" | "contact-admin" | "wait" | "provider-blocked" | "retryable" | "hq-configuration" | "undiagnosed";
|
|
44
|
+
export interface Finding {
|
|
45
|
+
provider: string;
|
|
46
|
+
connectionId: string;
|
|
47
|
+
kind: FindingKind;
|
|
48
|
+
message: string;
|
|
49
|
+
/** Optional HQ-authored remediation returned by the control plane. */
|
|
50
|
+
remediation?: string;
|
|
51
|
+
}
|
|
52
|
+
/** Classify without echoing untrusted provider text, which may contain secrets. */
|
|
53
|
+
export declare function classifyConnection(connection: IntegrationConnection): Finding[];
|
|
54
|
+
/**
|
|
55
|
+
* What a human should be told about one unhealthy connection.
|
|
56
|
+
*
|
|
57
|
+
* `null` means there is nothing to say: the row is healthy, or it is a revoked
|
|
58
|
+
* tombstone, which every surface already explains through its own re-add path.
|
|
59
|
+
*/
|
|
60
|
+
export interface ConnectionHealthNotice {
|
|
61
|
+
kind: FindingKind;
|
|
62
|
+
/** HQ-authored, provider-prefixed sentence — safe to print verbatim. */
|
|
63
|
+
headline: string;
|
|
64
|
+
/** The server's role- and credential-aware fix path, when it sent one. */
|
|
65
|
+
remediation?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A status alone ("error") tells a user nothing they can act on. The control
|
|
69
|
+
* plane already sends the diagnosis and the fix path on every non-healthy row;
|
|
70
|
+
* this turns those into the two lines a surface should print next to the status.
|
|
71
|
+
*
|
|
72
|
+
* Revoked rows return `null` on purpose. They are tombstones rather than broken
|
|
73
|
+
* connections, and `revokedConnectionDetails` already produces the correct
|
|
74
|
+
* re-add wording for them — classifying them here would print two competing
|
|
75
|
+
* recovery paths for the same row.
|
|
76
|
+
*/
|
|
77
|
+
export declare function connectionHealthNotice(connection: IntegrationConnection): ConnectionHealthNotice | null;
|
|
78
|
+
//# sourceMappingURL=health.d.ts.map
|