@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 +9 -0
- package/dist/commands/group-grants.d.ts +1 -1
- package/dist/commands/group-grants.js +6 -6
- package/dist/commands/secrets.js +2 -1
- package/package.json +1 -1
- package/src/commands/group-grants.test.ts +41 -2
- package/src/commands/group-grants.ts +11 -8
- package/src/commands/secrets.test.ts +40 -0
- package/src/commands/secrets.ts +11 -2
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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
246
|
-
.
|
|
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();
|
package/dist/commands/secrets.js
CHANGED
|
@@ -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
|
@@ -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
|
|
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
|
|
192
|
+
groupId: string,
|
|
193
193
|
): Promise<GroupGrant[]> {
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
391
|
+
"List grants a source-company group holds on other companies",
|
|
389
392
|
)
|
|
390
|
-
.
|
|
393
|
+
.requiredOption(
|
|
391
394
|
"--group <groupId>",
|
|
392
|
-
"
|
|
395
|
+
"Group id to inspect",
|
|
393
396
|
)
|
|
394
|
-
.action(async (opts: { group
|
|
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
|
|
package/src/commands/secrets.ts
CHANGED
|
@@ -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: {
|
|
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
|
},
|