@indigoai-us/hq-cli 5.101.7 → 5.103.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 +57 -0
- package/dist/commands/agents.d.ts +43 -0
- package/dist/commands/agents.js +137 -0
- package/dist/commands/doctor.d.ts +10 -1
- package/dist/commands/doctor.js +7 -2
- package/dist/commands/integrations-api.d.ts +216 -0
- package/dist/commands/integrations-api.js +135 -0
- package/dist/commands/integrations-connect.d.ts +30 -0
- package/dist/commands/integrations-connect.js +583 -0
- package/dist/commands/integrations-core.d.ts +216 -0
- package/dist/commands/integrations-core.js +320 -0
- package/dist/commands/integrations-manage.d.ts +50 -0
- package/dist/commands/integrations-manage.js +556 -0
- package/dist/commands/integrations-oauth.d.ts +43 -0
- package/dist/commands/integrations-oauth.js +159 -0
- package/dist/commands/integrations.d.ts +32 -69
- package/dist/commands/integrations.js +42 -262
- package/dist/commands/reindex.js +1 -1
- package/dist/lib/doctor/checks/runtime-health.d.ts +100 -0
- package/dist/lib/doctor/checks/runtime-health.js +336 -0
- package/dist/lib/doctor/registry.js +6 -0
- package/dist/lib/doctor/types.d.ts +7 -0
- package/dist/utils/self-update.d.ts +2 -2
- package/dist/utils/self-update.js +19 -3
- package/dist/utils/version-gate.d.ts +34 -3
- package/dist/utils/version-gate.js +61 -4
- package/package.json +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared primitives for the `hq integrations` command group.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `integrations.ts` when the group grew past "use a connected
|
|
5
|
+
* app" into the full lifecycle (browse → connect → govern → disconnect). Every
|
|
6
|
+
* verb module imports its error taxonomy, HTTP guards, and connection
|
|
7
|
+
* resolution from here, so the classification rules that decide what reaches
|
|
8
|
+
* Sentry live in exactly one place. `integrations.ts` re-exports this module's
|
|
9
|
+
* public surface, so existing importers keep working unchanged.
|
|
10
|
+
*/
|
|
11
|
+
/** Write policy on a connection — mirrors hq-pro's `WritePolicy`. */
|
|
12
|
+
export type WritePolicy = "auto-allow" | "confirm" | "deny";
|
|
13
|
+
/** Who a grant points at — mirrors hq-pro's `AclGranteeType`. */
|
|
14
|
+
export type GranteeType = "person" | "group" | "email" | "company-wide";
|
|
15
|
+
/** Grant strength — mirrors hq-pro's `AclPermission`. */
|
|
16
|
+
export type Permission = "read" | "write" | "admin";
|
|
17
|
+
export interface WriteAllowlistEntry {
|
|
18
|
+
granteeType: GranteeType;
|
|
19
|
+
granteeId: string;
|
|
20
|
+
permission: Permission;
|
|
21
|
+
grantedBy?: string;
|
|
22
|
+
grantedAt?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface WriteAllowlistGrant {
|
|
25
|
+
toolName: string;
|
|
26
|
+
entries: WriteAllowlistEntry[];
|
|
27
|
+
updatedAt?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The factory installation joined onto a connection. `id` is the handle
|
|
31
|
+
* `hq integrations disconnect` needs — hq-pro's uninstall route keys on the
|
|
32
|
+
* INSTALLATION id, not the connection id.
|
|
33
|
+
*/
|
|
34
|
+
export interface FactoryInstallation {
|
|
35
|
+
id: string;
|
|
36
|
+
displayName?: string;
|
|
37
|
+
domain?: string;
|
|
38
|
+
status?: string;
|
|
39
|
+
surface?: {
|
|
40
|
+
kind?: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
url?: string;
|
|
43
|
+
authStatus?: string;
|
|
44
|
+
docs?: string;
|
|
45
|
+
};
|
|
46
|
+
trust?: {
|
|
47
|
+
lifecycleState?: string;
|
|
48
|
+
trustClaim?: string;
|
|
49
|
+
lastVerifiedAt?: string;
|
|
50
|
+
};
|
|
51
|
+
installedAt?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* One row of `GET /v1/integrations/admin`. Every field past `status` is
|
|
55
|
+
* optional on purpose: hq-pro redacts grant identities for non-managers and
|
|
56
|
+
* older deploys omit the newer access/installation joins entirely, so a
|
|
57
|
+
* missing field means UNKNOWN and must never be rendered as a value.
|
|
58
|
+
*/
|
|
59
|
+
export interface AdminConnection {
|
|
60
|
+
id: string;
|
|
61
|
+
provider: string;
|
|
62
|
+
status: string;
|
|
63
|
+
scopes?: string[];
|
|
64
|
+
createdBy?: string;
|
|
65
|
+
createdByName?: string | null;
|
|
66
|
+
createdAt?: string;
|
|
67
|
+
updatedAt?: string;
|
|
68
|
+
writePolicy?: WritePolicy;
|
|
69
|
+
writePolicyUpdatedAt?: string | null;
|
|
70
|
+
writeAllowlist?: WriteAllowlistGrant[];
|
|
71
|
+
access?: {
|
|
72
|
+
mode?: string;
|
|
73
|
+
grantCount?: number;
|
|
74
|
+
};
|
|
75
|
+
installation?: FactoryInstallation | null;
|
|
76
|
+
}
|
|
77
|
+
export interface AdminAuditEvent {
|
|
78
|
+
timestamp: string;
|
|
79
|
+
memberOrAgent: string;
|
|
80
|
+
memberOrAgentName?: string;
|
|
81
|
+
toolName: string;
|
|
82
|
+
outcome: string;
|
|
83
|
+
connectionId?: string;
|
|
84
|
+
provider?: string;
|
|
85
|
+
reason?: string;
|
|
86
|
+
queueId?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface AdminSurface {
|
|
89
|
+
companyUid: string;
|
|
90
|
+
factoryEnabled?: boolean;
|
|
91
|
+
viewer: {
|
|
92
|
+
personUid: string;
|
|
93
|
+
role: string;
|
|
94
|
+
canManageGovernance: boolean;
|
|
95
|
+
canManageIntegrations?: boolean;
|
|
96
|
+
};
|
|
97
|
+
connections: AdminConnection[];
|
|
98
|
+
audit: AdminAuditEvent[];
|
|
99
|
+
}
|
|
100
|
+
interface GatewayMessage {
|
|
101
|
+
result?: unknown;
|
|
102
|
+
error?: {
|
|
103
|
+
code?: number;
|
|
104
|
+
message?: string;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export declare class IntegrationsCliError extends Error {
|
|
108
|
+
/**
|
|
109
|
+
* True when the error is the caller's request/state/permission (a client 4xx
|
|
110
|
+
* or a local input/usage error) rather than an hq-cli defect. Expected errors
|
|
111
|
+
* are printed to the user but skipped for Sentry capture (HQ-CLI-6). Defaults
|
|
112
|
+
* to false so an unclassified error still reaches Sentry.
|
|
113
|
+
*/
|
|
114
|
+
readonly expected: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* hq-pro's machine code for the failure (e.g. `INTEGRATION_FACTORY_
|
|
117
|
+
* OAUTH_REQUIRED`), when the response carried one. Branch on THIS, never on
|
|
118
|
+
* the human message — the copy is free to change, the code is the contract.
|
|
119
|
+
*/
|
|
120
|
+
readonly code?: string;
|
|
121
|
+
/** HTTP status the failure came back with, when it came from a response. */
|
|
122
|
+
readonly status?: number;
|
|
123
|
+
constructor(message: string, opts?: {
|
|
124
|
+
expected?: boolean;
|
|
125
|
+
code?: string;
|
|
126
|
+
status?: number;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A client 4xx is the caller's request/state/permission (bad params, stale
|
|
131
|
+
* queueId, a non-owner approving) — expected and user-facing, not a bug. A 5xx
|
|
132
|
+
* (or a 2xx protocol violation) is a genuine server/unknown fault worth a Sentry
|
|
133
|
+
* crash report.
|
|
134
|
+
*/
|
|
135
|
+
export declare function isClientError(status: number): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Statuses that mean HQ's integration gateway (or the third-party provider
|
|
138
|
+
* behind it) could not serve this request right now — an UPSTREAM AVAILABILITY
|
|
139
|
+
* event, not an hq-cli defect and not something the caller did wrong.
|
|
140
|
+
*
|
|
141
|
+
* HQ-CLI-F: the 500/503s in Sentry correlate request-for-request with the
|
|
142
|
+
* hq-pro `IntegrationMcpFunction` Lambda hitting its 30s timeout while waiting
|
|
143
|
+
* on a provider (CloudWatch `integration_mcp_audit event=provider_error`, and
|
|
144
|
+
* the same spikes counted in the AWS/Lambda `Errors` metric). The event is
|
|
145
|
+
* already recorded first-party, in the project that owns the fix; mirroring it
|
|
146
|
+
* into hq-cli's tracker is duplicate, unactionable noise. 429 is included
|
|
147
|
+
* because a rate-limited call is the same "retry in a moment" outcome (it was
|
|
148
|
+
* already `expected` via `isClientError`; only its wording changes here).
|
|
149
|
+
*/
|
|
150
|
+
export declare function isUpstreamUnavailable(status: number): boolean;
|
|
151
|
+
/** Actionable wording for an upstream-availability status. */
|
|
152
|
+
export declare function upstreamUnavailableMessage(status: number): string;
|
|
153
|
+
/**
|
|
154
|
+
* Shared non-2xx guard for every integration-gateway call site. Raises the
|
|
155
|
+
* expected, actionable upstream-availability error when the status says the
|
|
156
|
+
* service is down or throttling; returns otherwise so the caller keeps its own
|
|
157
|
+
* status-specific message and `expected` classification unchanged.
|
|
158
|
+
*/
|
|
159
|
+
export declare function raiseIfUpstreamUnavailable(res: Response): void;
|
|
160
|
+
export declare function raiseIfUnauthorized(res: Response): void;
|
|
161
|
+
/**
|
|
162
|
+
* Single non-2xx funnel for the REST (non-JSON-RPC) integration routes. Applies
|
|
163
|
+
* the auth and upstream-availability guards in order, then raises the server's
|
|
164
|
+
* own `error` text — or `fallback` when the body carried none — with the right
|
|
165
|
+
* `expected` classification. Every factory/admin call site goes through this so
|
|
166
|
+
* one route cannot quietly drift into reporting its 4xx as crashes.
|
|
167
|
+
*/
|
|
168
|
+
export declare function raiseForResponse(res: Response, fallback: string): Promise<never>;
|
|
169
|
+
/** "factory:linear" → "linear"; mirrors hq-pro's factoryToolPrefix. */
|
|
170
|
+
export declare function toolPrefixForProvider(provider: string): string;
|
|
171
|
+
/** "factory:linear" → "linear", for display and for `--provider` echoes. */
|
|
172
|
+
export declare function bareProvider(provider: string): string;
|
|
173
|
+
/**
|
|
174
|
+
* Read the whole admin surface: connections with their governance state, the
|
|
175
|
+
* viewer's role, and the recent audit feed. Several verbs need more than the
|
|
176
|
+
* connection list, so this is the primitive and `fetchConnections` is the thin
|
|
177
|
+
* projection over it.
|
|
178
|
+
*/
|
|
179
|
+
export declare function fetchAdminSurface(token: string, companyUid: string): Promise<AdminSurface>;
|
|
180
|
+
export declare function fetchConnections(token: string, companyUid: string): Promise<AdminConnection[]>;
|
|
181
|
+
/**
|
|
182
|
+
* Resolve one connection by `--connection acct_…` or `--provider linear`
|
|
183
|
+
* (matches `factory:<slug>` and bare provider ids, case-insensitive). Errors
|
|
184
|
+
* list what IS connected so the fix is one command away.
|
|
185
|
+
*/
|
|
186
|
+
export declare function selectConnection(connections: AdminConnection[], opts: {
|
|
187
|
+
connection?: string;
|
|
188
|
+
provider?: string;
|
|
189
|
+
}): AdminConnection;
|
|
190
|
+
/**
|
|
191
|
+
* Resolve a connection the caller named positionally OR through the
|
|
192
|
+
* `--provider` / `--connection` flags. Every management verb takes an optional
|
|
193
|
+
* `<app>` argument for ergonomics (`hq integrations policy linear …`), which is
|
|
194
|
+
* matched exactly like `--provider` unless it looks like a connection id.
|
|
195
|
+
*/
|
|
196
|
+
export declare function resolveConnection(token: string, companyUid: string, app: string | undefined, opts: {
|
|
197
|
+
provider?: string;
|
|
198
|
+
connection?: string;
|
|
199
|
+
}): Promise<AdminConnection>;
|
|
200
|
+
export declare function callGateway(token: string, params: Record<string, unknown>): Promise<GatewayMessage>;
|
|
201
|
+
/**
|
|
202
|
+
* Gateway results arrive MCP-style: `{ content: [{ type: "text", text }] }`
|
|
203
|
+
* where `text` is the provider's JSON. Unwrap to the inner payload; fall back
|
|
204
|
+
* to the raw result when the shape differs.
|
|
205
|
+
*/
|
|
206
|
+
export declare function unwrapGatewayResult(result: unknown): unknown;
|
|
207
|
+
interface QueuedOutcome {
|
|
208
|
+
queuedForApproval: true;
|
|
209
|
+
queueId: string;
|
|
210
|
+
connectionId: string;
|
|
211
|
+
expiresAt?: string;
|
|
212
|
+
}
|
|
213
|
+
export declare function queuedOutcome(payload: unknown): QueuedOutcome | null;
|
|
214
|
+
export declare function printJson(value: unknown): void;
|
|
215
|
+
export {};
|
|
216
|
+
//# sourceMappingURL=integrations-core.d.ts.map
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared primitives for the `hq integrations` command group.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `integrations.ts` when the group grew past "use a connected
|
|
5
|
+
* app" into the full lifecycle (browse → connect → govern → disconnect). Every
|
|
6
|
+
* verb module imports its error taxonomy, HTTP guards, and connection
|
|
7
|
+
* resolution from here, so the classification rules that decide what reaches
|
|
8
|
+
* Sentry live in exactly one place. `integrations.ts` re-exports this module's
|
|
9
|
+
* public surface, so existing importers keep working unchanged.
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
13
|
+
import { AuthError } from "../utils/auth-error.js";
|
|
14
|
+
import { redactErrorText } from "../utils/redact-error-text.js";
|
|
15
|
+
export class IntegrationsCliError extends Error {
|
|
16
|
+
/**
|
|
17
|
+
* True when the error is the caller's request/state/permission (a client 4xx
|
|
18
|
+
* or a local input/usage error) rather than an hq-cli defect. Expected errors
|
|
19
|
+
* are printed to the user but skipped for Sentry capture (HQ-CLI-6). Defaults
|
|
20
|
+
* to false so an unclassified error still reaches Sentry.
|
|
21
|
+
*/
|
|
22
|
+
expected;
|
|
23
|
+
/**
|
|
24
|
+
* hq-pro's machine code for the failure (e.g. `INTEGRATION_FACTORY_
|
|
25
|
+
* OAUTH_REQUIRED`), when the response carried one. Branch on THIS, never on
|
|
26
|
+
* the human message — the copy is free to change, the code is the contract.
|
|
27
|
+
*/
|
|
28
|
+
code;
|
|
29
|
+
/** HTTP status the failure came back with, when it came from a response. */
|
|
30
|
+
status;
|
|
31
|
+
constructor(message, opts = {}) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "IntegrationsCliError";
|
|
34
|
+
this.expected = opts.expected ?? false;
|
|
35
|
+
if (opts.code)
|
|
36
|
+
this.code = opts.code;
|
|
37
|
+
if (opts.status !== undefined)
|
|
38
|
+
this.status = opts.status;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A client 4xx is the caller's request/state/permission (bad params, stale
|
|
43
|
+
* queueId, a non-owner approving) — expected and user-facing, not a bug. A 5xx
|
|
44
|
+
* (or a 2xx protocol violation) is a genuine server/unknown fault worth a Sentry
|
|
45
|
+
* crash report.
|
|
46
|
+
*/
|
|
47
|
+
export function isClientError(status) {
|
|
48
|
+
return status >= 400 && status < 500;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Statuses that mean HQ's integration gateway (or the third-party provider
|
|
52
|
+
* behind it) could not serve this request right now — an UPSTREAM AVAILABILITY
|
|
53
|
+
* event, not an hq-cli defect and not something the caller did wrong.
|
|
54
|
+
*
|
|
55
|
+
* HQ-CLI-F: the 500/503s in Sentry correlate request-for-request with the
|
|
56
|
+
* hq-pro `IntegrationMcpFunction` Lambda hitting its 30s timeout while waiting
|
|
57
|
+
* on a provider (CloudWatch `integration_mcp_audit event=provider_error`, and
|
|
58
|
+
* the same spikes counted in the AWS/Lambda `Errors` metric). The event is
|
|
59
|
+
* already recorded first-party, in the project that owns the fix; mirroring it
|
|
60
|
+
* into hq-cli's tracker is duplicate, unactionable noise. 429 is included
|
|
61
|
+
* because a rate-limited call is the same "retry in a moment" outcome (it was
|
|
62
|
+
* already `expected` via `isClientError`; only its wording changes here).
|
|
63
|
+
*/
|
|
64
|
+
export function isUpstreamUnavailable(status) {
|
|
65
|
+
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
66
|
+
}
|
|
67
|
+
/** Actionable wording for an upstream-availability status. */
|
|
68
|
+
export function upstreamUnavailableMessage(status) {
|
|
69
|
+
return status === 429
|
|
70
|
+
? `HQ's integration gateway is rate-limiting this request (HTTP 429). Wait a moment and retry.`
|
|
71
|
+
: `HQ's integration gateway is temporarily unavailable (HTTP ${status}). ` +
|
|
72
|
+
`This is a service-side hiccup, not a problem with your command — retry in a moment.`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Shared non-2xx guard for every integration-gateway call site. Raises the
|
|
76
|
+
* expected, actionable upstream-availability error when the status says the
|
|
77
|
+
* service is down or throttling; returns otherwise so the caller keeps its own
|
|
78
|
+
* status-specific message and `expected` classification unchanged.
|
|
79
|
+
*/
|
|
80
|
+
export function raiseIfUpstreamUnavailable(res) {
|
|
81
|
+
if (isUpstreamUnavailable(res.status)) {
|
|
82
|
+
throw new IntegrationsCliError(upstreamUnavailableMessage(res.status), {
|
|
83
|
+
expected: true,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// The integration gateway answers `POST /v1/integrations/mcp` JSON-RPC-style: a
|
|
88
|
+
// transport failure is a non-2xx HTTP status, but a GOVERNED refusal arrives as
|
|
89
|
+
// HTTP 200 carrying a JSON-RPC `error` object (mirrors hq-pro's
|
|
90
|
+
// integration-mcp/server.ts error mapping). These caller-side codes are the
|
|
91
|
+
// JSON-RPC analog of a client 4xx — the caller's request/state/permission,
|
|
92
|
+
// expected and actionable, not an hq-cli defect — so they are printed to the
|
|
93
|
+
// user and skipped for Sentry capture (HQ-CLI-B):
|
|
94
|
+
// -32003 UNAUTHORIZED — connection-level access denial, e.g. the
|
|
95
|
+
// "You do not have access to this integration. Ask its
|
|
96
|
+
// owner to share it with you." (IntegrationAccessDenied)
|
|
97
|
+
// that flooded Sentry, plus ConnectionNotFound / a
|
|
98
|
+
// read-only share rejecting a write.
|
|
99
|
+
// -32602 INVALID_PARAMS — an unknown tool or unsupported provider for the
|
|
100
|
+
// connection (a bad request the caller can correct).
|
|
101
|
+
// -32050 PROVIDER_ERROR — a THIRD-PARTY provider fault, surfaced verbatim.
|
|
102
|
+
// hq-pro mints this code for EVERY provider fault
|
|
103
|
+
// and for nothing else: `integration-mcp/server.ts`
|
|
104
|
+
// maps an `IntegrationMcpError` with status
|
|
105
|
+
// 'provider_error' to -32050, raised by
|
|
106
|
+
// `integration-mcp/dispatch.ts` for ProviderTimeout,
|
|
107
|
+
// ProviderRateLimited, ProviderParseError,
|
|
108
|
+
// ProviderWriteUnknown and TokenRefreshFailed.
|
|
109
|
+
// NOTE the remote server's OWN JSON-RPC code is
|
|
110
|
+
// embedded in the message TEXT ("Remote MCP request
|
|
111
|
+
// failed (-32602): …"); the wire code hq-cli sees is
|
|
112
|
+
// always -32050, so the codes above never match it.
|
|
113
|
+
// Every occurrence is already recorded first-party
|
|
114
|
+
// in hq-pro — `integration_mcp_audit
|
|
115
|
+
// event=provider_error` with reason/provider/tool,
|
|
116
|
+
// an `integration_mcp_health_signal` metric, and
|
|
117
|
+
// hq-pro's own Sentry project — so hq-cli reporting
|
|
118
|
+
// it again is duplicate noise in the wrong tracker,
|
|
119
|
+
// filed against a codebase that cannot fix it
|
|
120
|
+
// (HQ-CLI-F). Should hq-pro ever reuse -32050 for
|
|
121
|
+
// an hq-pro-side fault, the mapping site named above
|
|
122
|
+
// is where that change is traceable; -32603 below
|
|
123
|
+
// remains the code for hq-pro's own faults.
|
|
124
|
+
// Everything else stays unexpected so a genuine fault still reaches Sentry:
|
|
125
|
+
// INTERNAL_ERROR (-32603), CONFLICT (-32009, which the gateway also raises for
|
|
126
|
+
// a confirm queue being unavailable or an owner notification failing — real
|
|
127
|
+
// backend faults worth a report), METHOD_NOT_FOUND / PARSE_ERROR, and any
|
|
128
|
+
// absent or unrecognized code.
|
|
129
|
+
const EXPECTED_GATEWAY_ERROR_CODES = new Set([-32003, -32602, -32050]);
|
|
130
|
+
function isExpectedGatewayError(code) {
|
|
131
|
+
return code != null && EXPECTED_GATEWAY_ERROR_CODES.has(code);
|
|
132
|
+
}
|
|
133
|
+
// A 401 from ANY integration-gateway vault call means the caller's HQ session
|
|
134
|
+
// is expired or missing — an expected auth state fixed by `hq login`, not an
|
|
135
|
+
// hq-cli defect. Raise the same typed AuthError the vault company-resolution
|
|
136
|
+
// paths use (HQ-CLI-8) so the top-level handler prints one actionable message
|
|
137
|
+
// and skips Sentry, instead of surfacing the opaque, unactionable
|
|
138
|
+
// "Integration gateway request failed (HTTP 401)" that shipped as a fatal from
|
|
139
|
+
// `callGateway` (HQ-CLI-9). Non-401 statuses keep their existing behavior:
|
|
140
|
+
// other 4xx stay expected client errors, 5xx still report.
|
|
141
|
+
export function raiseIfUnauthorized(res) {
|
|
142
|
+
if (res.status === 401)
|
|
143
|
+
throw new AuthError();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Single non-2xx funnel for the REST (non-JSON-RPC) integration routes. Applies
|
|
147
|
+
* the auth and upstream-availability guards in order, then raises the server's
|
|
148
|
+
* own `error` text — or `fallback` when the body carried none — with the right
|
|
149
|
+
* `expected` classification. Every factory/admin call site goes through this so
|
|
150
|
+
* one route cannot quietly drift into reporting its 4xx as crashes.
|
|
151
|
+
*/
|
|
152
|
+
export async function raiseForResponse(res, fallback) {
|
|
153
|
+
raiseIfUnauthorized(res);
|
|
154
|
+
raiseIfUpstreamUnavailable(res);
|
|
155
|
+
const body = (await res.json().catch(() => ({})));
|
|
156
|
+
// hq-pro mints this error text, so it is untrusted input to a message we
|
|
157
|
+
// print verbatim — scrub credentials out before it can reach a terminal or a
|
|
158
|
+
// Sentry crash report (same reasoning as the gateway path below).
|
|
159
|
+
const detail = redactErrorText(body.error ?? "");
|
|
160
|
+
throw new IntegrationsCliError(detail || `${fallback} (HTTP ${res.status})`, {
|
|
161
|
+
expected: isClientError(res.status),
|
|
162
|
+
status: res.status,
|
|
163
|
+
...(body.code ? { code: body.code } : {}),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** "factory:linear" → "linear"; mirrors hq-pro's factoryToolPrefix. */
|
|
167
|
+
export function toolPrefixForProvider(provider) {
|
|
168
|
+
return provider
|
|
169
|
+
.replace(/^factory:/, "")
|
|
170
|
+
.trim()
|
|
171
|
+
.toLowerCase()
|
|
172
|
+
.replace(/[^a-z0-9]+/g, ".");
|
|
173
|
+
}
|
|
174
|
+
/** "factory:linear" → "linear", for display and for `--provider` echoes. */
|
|
175
|
+
export function bareProvider(provider) {
|
|
176
|
+
return provider.replace(/^factory:/, "");
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Read the whole admin surface: connections with their governance state, the
|
|
180
|
+
* viewer's role, and the recent audit feed. Several verbs need more than the
|
|
181
|
+
* connection list, so this is the primitive and `fetchConnections` is the thin
|
|
182
|
+
* projection over it.
|
|
183
|
+
*/
|
|
184
|
+
export async function fetchAdminSurface(token, companyUid) {
|
|
185
|
+
const res = await vaultApiFetch({
|
|
186
|
+
token,
|
|
187
|
+
path: "/v1/integrations/admin",
|
|
188
|
+
query: { companyUid },
|
|
189
|
+
});
|
|
190
|
+
if (!res.ok)
|
|
191
|
+
await raiseForResponse(res, "Failed to list integrations");
|
|
192
|
+
const data = (await res.json());
|
|
193
|
+
return {
|
|
194
|
+
companyUid: data.companyUid ?? companyUid,
|
|
195
|
+
...(data.factoryEnabled !== undefined ? { factoryEnabled: data.factoryEnabled } : {}),
|
|
196
|
+
viewer: data.viewer ?? {
|
|
197
|
+
personUid: "",
|
|
198
|
+
role: "member",
|
|
199
|
+
canManageGovernance: false,
|
|
200
|
+
},
|
|
201
|
+
connections: data.connections ?? [],
|
|
202
|
+
audit: data.audit ?? [],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export async function fetchConnections(token, companyUid) {
|
|
206
|
+
return (await fetchAdminSurface(token, companyUid)).connections;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Resolve one connection by `--connection acct_…` or `--provider linear`
|
|
210
|
+
* (matches `factory:<slug>` and bare provider ids, case-insensitive). Errors
|
|
211
|
+
* list what IS connected so the fix is one command away.
|
|
212
|
+
*/
|
|
213
|
+
export function selectConnection(connections, opts) {
|
|
214
|
+
const active = connections.filter((c) => c.status !== "revoked");
|
|
215
|
+
if (opts.connection) {
|
|
216
|
+
const match = connections.find((c) => c.id === opts.connection);
|
|
217
|
+
if (!match) {
|
|
218
|
+
throw new IntegrationsCliError(`No connection '${opts.connection}'. Run \`hq integrations list\` to see connected apps.`, { expected: true });
|
|
219
|
+
}
|
|
220
|
+
return match;
|
|
221
|
+
}
|
|
222
|
+
if (opts.provider) {
|
|
223
|
+
const want = opts.provider.trim().toLowerCase();
|
|
224
|
+
const match = active.find((c) => {
|
|
225
|
+
const bare = bareProvider(c.provider).toLowerCase();
|
|
226
|
+
return bare === want || c.provider.toLowerCase() === want;
|
|
227
|
+
});
|
|
228
|
+
if (!match) {
|
|
229
|
+
const available = active.map((c) => bareProvider(c.provider)).join(", ");
|
|
230
|
+
throw new IntegrationsCliError(`No connected app matches '${opts.provider}'.` +
|
|
231
|
+
(available ? ` Connected: ${available}.` : " Nothing is connected yet — connect apps with `hq integrations connect <app>`."), { expected: true });
|
|
232
|
+
}
|
|
233
|
+
return match;
|
|
234
|
+
}
|
|
235
|
+
if (active.length === 1)
|
|
236
|
+
return active[0];
|
|
237
|
+
if (active.length === 0) {
|
|
238
|
+
throw new IntegrationsCliError("No connected apps yet. Connect one with `hq integrations connect <app>`, then retry.", { expected: true });
|
|
239
|
+
}
|
|
240
|
+
throw new IntegrationsCliError(`Multiple apps are connected — pick one with --provider:\n` +
|
|
241
|
+
active.map((c) => ` --provider ${bareProvider(c.provider)}`).join("\n"), { expected: true });
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Resolve a connection the caller named positionally OR through the
|
|
245
|
+
* `--provider` / `--connection` flags. Every management verb takes an optional
|
|
246
|
+
* `<app>` argument for ergonomics (`hq integrations policy linear …`), which is
|
|
247
|
+
* matched exactly like `--provider` unless it looks like a connection id.
|
|
248
|
+
*/
|
|
249
|
+
export async function resolveConnection(token, companyUid, app, opts) {
|
|
250
|
+
const connections = await fetchConnections(token, companyUid);
|
|
251
|
+
if (app && !opts.provider && !opts.connection) {
|
|
252
|
+
return selectConnection(connections, app.startsWith("acct_") ? { connection: app } : { provider: app });
|
|
253
|
+
}
|
|
254
|
+
return selectConnection(connections, opts);
|
|
255
|
+
}
|
|
256
|
+
export async function callGateway(token, params) {
|
|
257
|
+
const res = await vaultApiFetch({
|
|
258
|
+
token,
|
|
259
|
+
path: "/v1/integrations/mcp",
|
|
260
|
+
method: "POST",
|
|
261
|
+
body: {
|
|
262
|
+
jsonrpc: "2.0",
|
|
263
|
+
id: `hq-cli-${randomUUID()}`,
|
|
264
|
+
method: "tools/call",
|
|
265
|
+
params,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
const message = (await res.json().catch(() => null));
|
|
269
|
+
if (!res.ok || !message) {
|
|
270
|
+
raiseIfUnauthorized(res);
|
|
271
|
+
raiseIfUpstreamUnavailable(res);
|
|
272
|
+
throw new IntegrationsCliError(`Integration gateway request failed (HTTP ${res.status}).`, { expected: isClientError(res.status) });
|
|
273
|
+
}
|
|
274
|
+
if (message.error) {
|
|
275
|
+
// The gateway's error text is minted UPSTREAM (hq-pro, the integration
|
|
276
|
+
// factory, and beyond it the third-party provider), so it is untrusted:
|
|
277
|
+
// scrub credentials and bound the length here, at the throw site, because
|
|
278
|
+
// an `expected` error is printed straight from `err.message` by the
|
|
279
|
+
// top-level handler and never passes through `unexpectedCliErrorMessage`.
|
|
280
|
+
// The chain is idempotent, so the unexpected path scrubbing again is a
|
|
281
|
+
// no-op. This preserves PR #298's user-visible diagnostic; it only makes
|
|
282
|
+
// it safe on the newly-expected path.
|
|
283
|
+
throw new IntegrationsCliError(redactErrorText(message.error.message ?? "") ||
|
|
284
|
+
"Integration gateway returned an error.", { expected: isExpectedGatewayError(message.error.code) });
|
|
285
|
+
}
|
|
286
|
+
return message;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Gateway results arrive MCP-style: `{ content: [{ type: "text", text }] }`
|
|
290
|
+
* where `text` is the provider's JSON. Unwrap to the inner payload; fall back
|
|
291
|
+
* to the raw result when the shape differs.
|
|
292
|
+
*/
|
|
293
|
+
export function unwrapGatewayResult(result) {
|
|
294
|
+
if (result && typeof result === "object" && Array.isArray(result.content)) {
|
|
295
|
+
const content = result.content;
|
|
296
|
+
const text = content.find((c) => c.type === "text")?.text;
|
|
297
|
+
if (typeof text === "string") {
|
|
298
|
+
try {
|
|
299
|
+
return JSON.parse(text);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return text;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
export function queuedOutcome(payload) {
|
|
309
|
+
if (payload &&
|
|
310
|
+
typeof payload === "object" &&
|
|
311
|
+
payload.queuedForApproval === true &&
|
|
312
|
+
typeof payload.queueId === "string") {
|
|
313
|
+
return payload;
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
export function printJson(value) {
|
|
318
|
+
console.log(JSON.stringify(value, null, 2));
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=integrations-core.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq integrations show | policy | grants | grant | ungrant | access | share |
|
|
3
|
+
* unshare | audit | pending | disconnect`.
|
|
4
|
+
*
|
|
5
|
+
* The govern-and-remove half of the lifecycle. Two different permission
|
|
6
|
+
* surfaces live here and are easy to confuse, so they get separate verbs:
|
|
7
|
+
*
|
|
8
|
+
* ACCESS (`access` / `share` / `unshare`) — who inside the company may use
|
|
9
|
+
* the connection at all. Managed by the connection's creator or a
|
|
10
|
+
* company admin.
|
|
11
|
+
* GRANTS (`grants` / `grant` / `ungrant`) — which of those people may call
|
|
12
|
+
* a specific WRITE tool without an approval round trip. Owner-only,
|
|
13
|
+
* and layered under the connection's write policy.
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
import { type GranteeType, type Permission, type WriteAllowlistGrant } from "./integrations-core.js";
|
|
17
|
+
interface Principal {
|
|
18
|
+
granteeType: GranteeType;
|
|
19
|
+
granteeId?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse a grantee from the one string a person would actually type.
|
|
23
|
+
*
|
|
24
|
+
* Accepts an explicit `person:`/`group:`/`email:` prefix, the word `everyone`,
|
|
25
|
+
* a bare email address, or a bare uid. Refuses anything ambiguous rather than
|
|
26
|
+
* guessing — a misparsed principal silently grants access to the wrong party.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parsePrincipal(raw: string): Principal;
|
|
29
|
+
export declare function registerManageCommands(integrations: Command): void;
|
|
30
|
+
/** Order-insensitive identity of an allowlist, for change detection. */
|
|
31
|
+
export declare function allowlistFingerprint(grants: WriteAllowlistGrant[]): string;
|
|
32
|
+
/**
|
|
33
|
+
* Add (or upgrade) one grantee's permission on one tool, returning the FULL
|
|
34
|
+
* allowlist to send back. Exported for tests: the merge is the part that would
|
|
35
|
+
* silently destroy other people's grants if it drifted.
|
|
36
|
+
*/
|
|
37
|
+
export declare function mergeGrant(current: WriteAllowlistGrant[], toolName: string, entry: {
|
|
38
|
+
granteeType: GranteeType;
|
|
39
|
+
granteeId: string;
|
|
40
|
+
permission: Permission;
|
|
41
|
+
}): WriteAllowlistGrant[];
|
|
42
|
+
/**
|
|
43
|
+
* Remove one grantee from one tool, returning the FULL allowlist to send back,
|
|
44
|
+
* or `null` when the grant was not there (so the caller can say "nothing to
|
|
45
|
+
* remove" instead of issuing a no-op write). A tool left with no entries is
|
|
46
|
+
* dropped entirely rather than persisted as an empty rule.
|
|
47
|
+
*/
|
|
48
|
+
export declare function removeGrant(current: WriteAllowlistGrant[], toolName: string, principal: Principal): WriteAllowlistGrant[] | null;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=integrations-manage.d.ts.map
|