@indigoai-us/hq-cli 5.77.7 → 5.77.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.77.8]
6
+
7
+ ### Fixed
8
+
9
+ - `hq secrets script approve` now accepts `--remote-path <path>`, so an
10
+ operator can hash a local copy while approving the byte-identical script at
11
+ its fleet or other target runtime path. Existing same-path approvals are
12
+ unchanged. (#263)
13
+
5
14
  ## [5.77.7]
6
15
 
7
16
  ### Fixed
@@ -69,7 +69,7 @@ export declare function grantGroup(options: GrantGroupOptions): Promise<GroupGra
69
69
  /** POST /group-grants/revoke — remove a group's grant on a target company. */
70
70
  export declare function revokeGroupGrant(options: RevokeGroupGrantOptions): Promise<void>;
71
71
  /** GET /group-grants/outbound — grants a source company's group(s) hold. */
72
- export declare function listOutboundGrants(token: string, sourceCompanyUid: string, groupId?: string): Promise<GroupGrant[]>;
72
+ export declare function listOutboundGrants(token: string, sourceCompanyUid: string, groupId: string): Promise<GroupGrant[]>;
73
73
  /** GET /group-grants/inbound — grants other companies' groups hold on us. */
74
74
  export declare function listInboundGrants(token: string, companyUid: string): Promise<GroupGrant[]>;
75
75
  export declare function registerGroupGrantsCommand(program: Command): void;
@@ -123,13 +123,13 @@ export async function revokeGroupGrant(options) {
123
123
  }
124
124
  /** GET /group-grants/outbound — grants a source company's group(s) hold. */
125
125
  export async function listOutboundGrants(token, sourceCompanyUid, groupId) {
126
- const query = { sourceCompanyUid };
127
- if (groupId)
128
- query.groupId = groupId;
126
+ if (!GROUP_ID_PATTERN.test(groupId)) {
127
+ throw new Error(`Invalid group id '${groupId}': must match grp_<alphanumeric, underscore, hyphen>`);
128
+ }
129
129
  const res = await vaultApiFetch({
130
130
  token,
131
131
  path: "/group-grants/outbound",
132
- query,
132
+ query: { sourceCompanyUid, groupId },
133
133
  });
134
134
  if (!res.ok) {
135
135
  const err = (await res.json().catch(() => ({})));
@@ -242,8 +242,8 @@ export function registerGroupGrantsCommand(program) {
242
242
  });
243
243
  grants
244
244
  .command("outbound")
245
- .description("List grants the source company's groups hold on other companies")
246
- .option("--group <groupId>", "Filter to a single group id")
245
+ .description("List grants a source-company group holds on other companies")
246
+ .requiredOption("--group <groupId>", "Group id to inspect")
247
247
  .action(async (opts) => {
248
248
  try {
249
249
  const token = await ensureCognitoToken();
@@ -961,6 +961,7 @@ export function registerSecretsCommand(program) {
961
961
  .description("Approve a script for a secret path")
962
962
  .requiredOption("--id <scriptId>", "Stable script identifier")
963
963
  .requiredOption("--script <path>", "Path to the local script file")
964
+ .option("--remote-path <path>", "Script path reported by the target runtime (defaults to the local path)")
964
965
  .option("--attestation <level>", "Attestation level", "self-asserted-hash")
965
966
  .action(async (secretPath, opts) => {
966
967
  try {
@@ -979,7 +980,7 @@ export function registerSecretsCommand(program) {
979
980
  body: {
980
981
  path: secretPath,
981
982
  scriptId: usage.script?.scriptId,
982
- scriptPath: usage.script?.path,
983
+ scriptPath: opts.remotePath ?? usage.script?.path,
983
984
  sha256: usage.script?.sha256,
984
985
  attestationLevel: usage.script?.attestationLevel,
985
986
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.77.7",
3
+ "version": "5.77.8",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -15,14 +15,25 @@
15
15
  * yields an actionable cross-tenant permission message.
16
16
  */
17
17
 
18
+ import { Command } from "commander";
18
19
  import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest";
19
20
 
21
+ const { ensureCognitoTokenSpy } = vi.hoisted(() => ({
22
+ ensureCognitoTokenSpy: vi.fn(),
23
+ }));
24
+
25
+ vi.mock("../utils/cognito-session.js", async (importOriginal) => ({
26
+ ...(await importOriginal()),
27
+ ensureCognitoToken: ensureCognitoTokenSpy,
28
+ }));
29
+
20
30
  import {
21
31
  GrantHttpError,
22
32
  formatGrantHttpError,
23
33
  grantGroup,
24
34
  listInboundGrants,
25
35
  listOutboundGrants,
36
+ registerGroupGrantsCommand,
26
37
  revokeGroupGrant,
27
38
  } from "./group-grants.js";
28
39
 
@@ -41,8 +52,16 @@ beforeEach(() => {
41
52
 
42
53
  afterEach(() => {
43
54
  vi.restoreAllMocks();
55
+ ensureCognitoTokenSpy.mockReset();
44
56
  });
45
57
 
58
+ async function runCli(argv: string[]): Promise<void> {
59
+ const program = new Command();
60
+ program.exitOverride();
61
+ registerGroupGrantsCommand(program);
62
+ await program.parseAsync(["node", "hq", ...argv]);
63
+ }
64
+
46
65
  // ---------------------------------------------------------------------------
47
66
  // grantGroup — story e2e #1 (authorized) + validation
48
67
  // ---------------------------------------------------------------------------
@@ -242,7 +261,7 @@ describe("revokeGroupGrant", () => {
242
261
  // ---------------------------------------------------------------------------
243
262
 
244
263
  describe("listOutboundGrants", () => {
245
- it("GETs /group-grants/outbound with sourceCompanyUid and optional groupId", async () => {
264
+ it("GETs /group-grants/outbound with both required query parameters", async () => {
246
265
  fetchSpy.mockResolvedValueOnce(
247
266
  jsonResponse(200, {
248
267
  grants: [
@@ -267,7 +286,27 @@ describe("listOutboundGrants", () => {
267
286
 
268
287
  it("returns [] when the server returns no grants", async () => {
269
288
  fetchSpy.mockResolvedValueOnce(jsonResponse(200, {}));
270
- expect(await listOutboundGrants("test-token", "cmp_a")).toEqual([]);
289
+ expect(await listOutboundGrants("test-token", "cmp_a", "grp_eng")).toEqual([]);
290
+ });
291
+
292
+ it.each(["", "eng"]) (
293
+ "rejects invalid group id %j before calling the API",
294
+ async (groupId) => {
295
+ await expect(
296
+ listOutboundGrants("test-token", "cmp_a", groupId),
297
+ ).rejects.toThrow(/Invalid group id/);
298
+ expect(fetchSpy).not.toHaveBeenCalled();
299
+ },
300
+ );
301
+ });
302
+
303
+ describe("group-grants outbound command", () => {
304
+ it("rejects a missing --group before authentication or fetch", async () => {
305
+ await expect(runCli(["group-grants", "outbound"])).rejects.toMatchObject({
306
+ code: "commander.missingMandatoryOptionValue",
307
+ });
308
+ expect(ensureCognitoTokenSpy).not.toHaveBeenCalled();
309
+ expect(fetchSpy).not.toHaveBeenCalled();
271
310
  });
272
311
  });
273
312
 
@@ -189,15 +189,18 @@ export async function revokeGroupGrant(
189
189
  export async function listOutboundGrants(
190
190
  token: string,
191
191
  sourceCompanyUid: string,
192
- groupId?: string,
192
+ groupId: string,
193
193
  ): Promise<GroupGrant[]> {
194
- const query: Record<string, string> = { sourceCompanyUid };
195
- if (groupId) query.groupId = groupId;
194
+ if (!GROUP_ID_PATTERN.test(groupId)) {
195
+ throw new Error(
196
+ `Invalid group id '${groupId}': must match grp_<alphanumeric, underscore, hyphen>`,
197
+ );
198
+ }
196
199
 
197
200
  const res = await vaultApiFetch({
198
201
  token,
199
202
  path: "/group-grants/outbound",
200
- query,
203
+ query: { sourceCompanyUid, groupId },
201
204
  });
202
205
 
203
206
  if (!res.ok) {
@@ -385,13 +388,13 @@ export function registerGroupGrantsCommand(program: Command): void {
385
388
  grants
386
389
  .command("outbound")
387
390
  .description(
388
- "List grants the source company's groups hold on other companies",
391
+ "List grants a source-company group holds on other companies",
389
392
  )
390
- .option(
393
+ .requiredOption(
391
394
  "--group <groupId>",
392
- "Filter to a single group id",
395
+ "Group id to inspect",
393
396
  )
394
- .action(async (opts: { group?: string }) => {
397
+ .action(async (opts: { group: string }) => {
395
398
  try {
396
399
  const token = await ensureCognitoToken();
397
400
  const sourceSlug = grants.opts().company as string | undefined;
@@ -1723,6 +1723,46 @@ describe("secrets reveal and policy controls", () => {
1723
1723
  expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "LOCKED");
1724
1724
  });
1725
1725
 
1726
+ it("script approve hashes the local file while approving a remote runtime path", async () => {
1727
+ const scriptPath = join(tempDir, "approved.sh");
1728
+ const remotePath =
1729
+ "/home/ec2-user/hq-agent/companies/acme/scripts/approved.sh";
1730
+ const scriptBody = "#!/usr/bin/env bash\necho approved remotely\n";
1731
+ writeFileSync(scriptPath, scriptBody);
1732
+ const expectedSha = createHash("sha256").update(scriptBody).digest("hex");
1733
+ vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes({ ok: true }));
1734
+
1735
+ const program = buildProgram();
1736
+ await program.parseAsync([
1737
+ "node",
1738
+ "hq",
1739
+ "secrets",
1740
+ "script",
1741
+ "approve",
1742
+ "LOCKED",
1743
+ "--id",
1744
+ "deploy-script",
1745
+ "--script",
1746
+ scriptPath,
1747
+ "--remote-path",
1748
+ remotePath,
1749
+ ]);
1750
+
1751
+ expect(vaultApiFetch).toHaveBeenCalledWith({
1752
+ token: "test-token",
1753
+ path: "/secrets/prs_alice/policy/scripts",
1754
+ method: "POST",
1755
+ body: {
1756
+ path: "LOCKED",
1757
+ scriptId: "deploy-script",
1758
+ scriptPath: remotePath,
1759
+ sha256: expectedSha,
1760
+ attestationLevel: "self-asserted-hash",
1761
+ },
1762
+ });
1763
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "LOCKED");
1764
+ });
1765
+
1726
1766
  it("script revoke hits the revoke endpoint", async () => {
1727
1767
  vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes({ ok: true }));
1728
1768
 
@@ -1360,6 +1360,10 @@ export function registerSecretsCommand(program: Command): void {
1360
1360
  .description("Approve a script for a secret path")
1361
1361
  .requiredOption("--id <scriptId>", "Stable script identifier")
1362
1362
  .requiredOption("--script <path>", "Path to the local script file")
1363
+ .option(
1364
+ "--remote-path <path>",
1365
+ "Script path reported by the target runtime (defaults to the local path)",
1366
+ )
1363
1367
  .option(
1364
1368
  "--attestation <level>",
1365
1369
  "Attestation level",
@@ -1367,7 +1371,12 @@ export function registerSecretsCommand(program: Command): void {
1367
1371
  )
1368
1372
  .action(async (
1369
1373
  secretPath: string,
1370
- opts: { id: string; script: string; attestation: string },
1374
+ opts: {
1375
+ id: string;
1376
+ script: string;
1377
+ remotePath?: string;
1378
+ attestation: string;
1379
+ },
1371
1380
  ) => {
1372
1381
  try {
1373
1382
  rejectIfPersonal(secrets.opts(), "script approve");
@@ -1395,7 +1404,7 @@ export function registerSecretsCommand(program: Command): void {
1395
1404
  body: {
1396
1405
  path: secretPath,
1397
1406
  scriptId: usage.script?.scriptId,
1398
- scriptPath: usage.script?.path,
1407
+ scriptPath: opts.remotePath ?? usage.script?.path,
1399
1408
  sha256: usage.script?.sha256,
1400
1409
  attestationLevel: usage.script?.attestationLevel,
1401
1410
  },