@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.
@@ -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