@indigoai-us/hq-cli 5.75.0 → 5.77.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.
Files changed (49) hide show
  1. package/dist/commands/agents.js +8 -3
  2. package/dist/commands/files.d.ts +61 -0
  3. package/dist/commands/files.js +274 -0
  4. package/dist/commands/mcp-registration.d.ts +4 -5
  5. package/dist/commands/mcp-registration.js +5 -4
  6. package/dist/commands/outposts.d.ts +20 -4
  7. package/dist/commands/outposts.js +79 -10
  8. package/dist/commands/pack-install.d.ts +14 -17
  9. package/dist/commands/pack-install.js +53 -29
  10. package/dist/commands/pkg-install.js +3 -1
  11. package/dist/commands/run.d.ts +2 -0
  12. package/dist/commands/run.js +9 -3
  13. package/dist/commands/secrets.js +189 -87
  14. package/dist/run/hq-plugin.js +94 -31
  15. package/dist/utils/billing-gate.d.ts +15 -0
  16. package/dist/utils/billing-gate.js +35 -0
  17. package/dist/utils/sandbox-runner-client.d.ts +1 -0
  18. package/dist/utils/sandbox-runner-client.js +1 -0
  19. package/dist/utils/secrets-cache.d.ts +4 -5
  20. package/dist/utils/secrets-cache.js +5 -8
  21. package/package.json +3 -2
  22. package/pnpm-workspace.yaml +2 -0
  23. package/src/commands/agents.test.ts +41 -0
  24. package/src/commands/agents.ts +7 -3
  25. package/src/commands/files-recovery.test.ts +361 -0
  26. package/src/commands/files.ts +410 -0
  27. package/src/commands/mcp-registration.ts +9 -9
  28. package/src/commands/outposts.test.ts +155 -24
  29. package/src/commands/outposts.ts +199 -45
  30. package/src/commands/pack-install-secret-authorization.test.ts +115 -0
  31. package/src/commands/pack-install.test.ts +5 -1
  32. package/src/commands/pack-install.ts +67 -29
  33. package/src/commands/pkg-install.ts +3 -1
  34. package/src/commands/run.test.ts +45 -0
  35. package/src/commands/run.ts +20 -4
  36. package/src/commands/secrets.test.ts +366 -25
  37. package/src/commands/secrets.ts +222 -96
  38. package/src/run/hq-plugin.test.ts +186 -10
  39. package/src/run/hq-plugin.ts +102 -32
  40. package/src/utils/__fixtures__/scan-packages.generated-block.sh +23 -0
  41. package/src/utils/billing-gate.ts +46 -0
  42. package/src/utils/pack-contributions.test.ts +90 -31
  43. package/src/utils/sandbox-runner-client.test.ts +28 -0
  44. package/src/utils/sandbox-runner-client.ts +2 -0
  45. package/src/utils/secrets-cache.ts +5 -8
  46. package/test/commands/signals.test.ts +2 -2
  47. package/test/commands/sources.test.ts +2 -2
  48. package/test/helpers/vault-service-mock.ts +76 -17
  49. package/test/sources-signals/smoke.test.ts +2 -2
@@ -1,17 +1,32 @@
1
1
  /**
2
- * vault-service-mock.ts — fetch stub for vault-service HTTP endpoints.
2
+ * vault-service-mock.ts — vault-service test double.
3
3
  *
4
- * Intercepts globalThis.fetch and handles the subset of vault-service routes
5
- * needed by entity-resolver and STS tests. Returns the original fetch restore
6
- * function so callers can clean up in afterEach/finally.
4
+ * Two collaborating pieces:
5
+ * 1. A `globalThis.fetch` stub for the vault-service JSON API (membership,
6
+ * entity lookup, STS vend, files list/presign).
7
+ * 2. A real ephemeral localhost HTTP server that serves the presigned-object
8
+ * GETs. As of hq-cloud >=6.14.x, `PresignObjectIO` fetches presigned URLs
9
+ * through Node's core HTTP client (`nodePresignedGet`), NOT
10
+ * `globalThis.fetch`, to bypass the runtime-bundled undici parser — so a
11
+ * fetch stub alone never intercepts them and the request escapes to real
12
+ * DNS (`ENOTFOUND`). A real (loopback) server is transport-agnostic: it
13
+ * works for both the fetch and node-http code paths, and future ones.
7
14
  *
8
- * Handled endpoints (URL path suffixes):
15
+ * `mockVaultService` is async (it awaits the server's `listen`) and returns a
16
+ * restore function for `afterEach` that both un-installs the fetch stub and
17
+ * closes the server.
18
+ *
19
+ * Handled JSON endpoints (URL path suffixes):
9
20
  * GET .../membership/me returns { memberships: [...] }
10
21
  * GET .../entity/by-slug/{type}/{slug} returns { entity: {...} }
11
22
  * GET .../entity/{uid} returns { entity: {...} } (direct UID lookup)
12
- * POST .../sts/vend returns { credentials, expiresAt } (canonical company STS endpoint)
23
+ * GET .../v1/files/list returns { objects, cursor, truncated }
24
+ * POST .../v1/files/presign returns { results, expiresAt } (URLs point at the local server)
25
+ * POST .../sts/vend returns { credentials, expiresAt }
13
26
  */
14
27
 
28
+ import http from "node:http";
29
+ import type { AddressInfo } from "node:net";
15
30
  import type { Membership, EntityInfo } from "@indigoai-us/hq-cloud";
16
31
 
17
32
  export interface MockEntity {
@@ -56,8 +71,6 @@ export interface MockVaultOptions {
56
71
  files?: MockVaultFile[];
57
72
  }
58
73
 
59
- /** Host for mock presigned URLs minted by POST /v1/files/presign. */
60
- const PRESIGN_URL_HOST = "https://presigned.test";
61
74
  const DEFAULT_FILE_DATE = new Date("2026-01-01T00:00:00Z");
62
75
 
63
76
  const DEFAULT_STS: MockStsCredentials = {
@@ -67,14 +80,56 @@ const DEFAULT_STS: MockStsCredentials = {
67
80
  };
68
81
 
69
82
  /**
70
- * Installs a fetch stub for vault-service endpoints.
83
+ * Start a loopback HTTP server that serves presigned-object GETs from the
84
+ * in-memory `files`. Each minted presign URL is `${origin}/obj?key=<key>`;
85
+ * the handler resolves the file (200 + body) or 404s (→ NoSuchKey in the
86
+ * reader). Returns the server + its `http://127.0.0.1:<port>` origin.
87
+ */
88
+ async function startPresignedFileServer(
89
+ files: MockVaultFile[],
90
+ ): Promise<{ server: http.Server; origin: string }> {
91
+ const server = http.createServer((req, res) => {
92
+ const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
93
+ const key = requestUrl.searchParams.get("key") ?? "";
94
+ const file = files.find((f) => f.key === key);
95
+ if (!file) {
96
+ res.statusCode = 404;
97
+ res.setHeader("content-length", "0");
98
+ res.end();
99
+ return;
100
+ }
101
+ const body = Buffer.from(file.content, "utf-8");
102
+ res.writeHead(200, {
103
+ "content-type": "text/markdown",
104
+ "content-length": String(body.byteLength),
105
+ "last-modified": (file.lastModified ?? DEFAULT_FILE_DATE).toUTCString(),
106
+ etag: '"mock-etag"',
107
+ });
108
+ res.end(body);
109
+ });
110
+ await new Promise<void>((resolve) => {
111
+ server.listen(0, "127.0.0.1", resolve);
112
+ });
113
+ // Don't keep the event loop (or vitest) alive on this listener.
114
+ server.unref();
115
+ const { port } = server.address() as AddressInfo;
116
+ return { server, origin: `http://127.0.0.1:${port}` };
117
+ }
118
+
119
+ /**
120
+ * Installs the vault-service test double (fetch stub + presigned-file server).
71
121
  *
72
- * @returns A restore function — call it in afterEach to un-install the stub.
122
+ * @returns A restore function — call it in afterEach to un-install the stub and
123
+ * close the server.
73
124
  */
74
- export function mockVaultService(opts: MockVaultOptions): () => void {
125
+ export async function mockVaultService(
126
+ opts: MockVaultOptions,
127
+ ): Promise<() => void> {
75
128
  const { entities, stsCredentials = DEFAULT_STS, files = [] } = opts;
76
129
  const originalFetch = globalThis.fetch as typeof fetch | undefined;
77
130
 
131
+ const { server, origin: presignOrigin } = await startPresignedFileServer(files);
132
+
78
133
  globalThis.fetch = async (
79
134
  input: RequestInfo | URL,
80
135
  init?: RequestInit,
@@ -89,9 +144,10 @@ export function mockVaultService(opts: MockVaultOptions): () => void {
89
144
  (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
90
145
 
91
146
  // ── Presigned-URL transport (HQ-59 company reads) ──────────────────────
92
- // A GET against a URL minted by POST /v1/files/presign below. Serve the
93
- // file body (or 404) so PresignObjectIO.getObject resolves like real S3.
94
- if (url.startsWith(`${PRESIGN_URL_HOST}/`)) {
147
+ // Production fetches presigned URLs over node http (served by the loopback
148
+ // server above), but keep a fetch branch too so any fetch-based transport
149
+ // resolves identically without escaping to the network.
150
+ if (url.startsWith(`${presignOrigin}/`)) {
95
151
  const key = new URL(url).searchParams.get("key") ?? "";
96
152
  const file = files.find((f) => f.key === key);
97
153
  if (!file) return new Response("", { status: 404 });
@@ -122,8 +178,9 @@ export function mockVaultService(opts: MockVaultOptions): () => void {
122
178
  }
123
179
 
124
180
  // POST /v1/files/presign (VaultClient.presign) — mint a per-key URL that
125
- // the GET handler above resolves. Unknown keys still get a URL → that GET
126
- // 404s, which the reader normalizes to NoSuchKey (the not-found path).
181
+ // the loopback server (and the GET branch above) resolves. Unknown keys
182
+ // still get a URL → that GET 404s, which the reader normalizes to
183
+ // NoSuchKey (the not-found path).
127
184
  if (method === "POST" && /\/v1\/files\/presign$/.test(url)) {
128
185
  const body = init?.body ? JSON.parse(init.body.toString()) : {};
129
186
  const keys = (body.keys ?? []) as Array<{ key: string; op?: string }>;
@@ -134,7 +191,7 @@ export function mockVaultService(opts: MockVaultOptions): () => void {
134
191
  const results = keys.map((k) => ({
135
192
  key: k.key,
136
193
  op: k.op ?? "get",
137
- url: `${PRESIGN_URL_HOST}/obj?key=${encodeURIComponent(k.key)}`,
194
+ url: `${presignOrigin}/obj?key=${encodeURIComponent(k.key)}`,
138
195
  }));
139
196
  return json({
140
197
  results,
@@ -225,6 +282,8 @@ export function mockVaultService(opts: MockVaultOptions): () => void {
225
282
  } else {
226
283
  delete (globalThis as Record<string, unknown>).fetch;
227
284
  }
285
+ server.closeAllConnections?.();
286
+ server.close();
228
287
  };
229
288
  }
230
289
 
@@ -176,7 +176,7 @@ describe("mockVaultService — membership endpoint", () => {
176
176
  });
177
177
 
178
178
  it("GET /membership/me returns memberships for all configured entities", async () => {
179
- restore = mockVaultService({
179
+ restore = await mockVaultService({
180
180
  entities: [
181
181
  { uid: "cmp_indigo_001", slug: "indigo", bucketName: "hq-indigo-bucket" },
182
182
  { uid: "prs_personal_001", slug: "personal", bucketName: "hq-personal-bucket" },
@@ -194,7 +194,7 @@ describe("mockVaultService — membership endpoint", () => {
194
194
  });
195
195
 
196
196
  it("POST /sts/vend returns credentials", async () => {
197
- restore = mockVaultService({
197
+ restore = await mockVaultService({
198
198
  entities: [{ uid: "cmp_indigo_001", slug: "indigo", bucketName: "hq-indigo-bucket" }],
199
199
  });
200
200