@indigoai-us/hq-cli 5.47.9 → 5.47.11
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/dist/commands/members.js +38 -2
- package/dist/commands/run.js +34 -5
- package/dist/commands/secrets.d.ts +35 -1
- package/dist/commands/secrets.js +346 -22
- package/dist/run/hq-plugin.d.ts +4 -11
- package/dist/run/hq-plugin.js +22 -5
- package/dist/utils/integrity.d.ts +4 -0
- package/dist/utils/integrity.js +11 -7
- package/dist/utils/secrets-cache.d.ts +2 -1
- package/dist/utils/secrets-cache.js +42 -14
- package/package.json +1 -1
- package/src/commands/members.test.ts +92 -1
- package/src/commands/members.ts +61 -0
- package/src/commands/run.env-local.test.ts +5 -1
- package/src/commands/run.ts +40 -9
- package/src/commands/secrets.test.ts +394 -4
- package/src/commands/secrets.ts +574 -37
- package/src/run/hq-plugin.test.ts +31 -0
- package/src/run/hq-plugin.ts +33 -7
- package/src/utils/integrity.ts +13 -8
- package/src/utils/secrets-cache.ts +45 -11
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="fd3738da-3061-5784-bb7c-02021291aecc")}catch(e){}}();
|
|
3
3
|
import * as crypto from "node:crypto";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
const CACHE_DIR = path.join(os.homedir(), ".hq", "secrets-cache");
|
|
8
8
|
const KEY_PATH = path.join(CACHE_DIR, ".key");
|
|
9
|
-
const
|
|
9
|
+
const CACHE_FORMAT_MAGIC = Buffer.from("HQSC");
|
|
10
|
+
const CACHE_FORMAT_MAGIC_BYTES = CACHE_FORMAT_MAGIC.length;
|
|
11
|
+
const LEGACY_TIMESTAMP_BYTES = 8;
|
|
12
|
+
const TIMESTAMP_BYTES = 8;
|
|
13
|
+
const TTL_BYTES = 8;
|
|
10
14
|
const ALGORITHM = "aes-256-gcm";
|
|
11
15
|
const IV_BYTES = 12;
|
|
12
16
|
const AUTH_TAG_BYTES = 16;
|
|
17
|
+
export const DEFAULT_SECRETS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
13
18
|
function ensureCacheDir(companyUid) {
|
|
14
19
|
const dir = path.join(CACHE_DIR, companyUid);
|
|
15
20
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
@@ -53,21 +58,40 @@ export function readCache(companyUid, name) {
|
|
|
53
58
|
catch {
|
|
54
59
|
return null;
|
|
55
60
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
61
|
+
let timestampMs;
|
|
62
|
+
let ttlMs;
|
|
63
|
+
let ivStart;
|
|
64
|
+
let authTagStart;
|
|
65
|
+
let ciphertextStart;
|
|
66
|
+
if (raw.length >=
|
|
67
|
+
CACHE_FORMAT_MAGIC_BYTES + TIMESTAMP_BYTES + TTL_BYTES + IV_BYTES + AUTH_TAG_BYTES &&
|
|
68
|
+
raw.subarray(0, CACHE_FORMAT_MAGIC_BYTES).equals(CACHE_FORMAT_MAGIC)) {
|
|
69
|
+
timestampMs = Number(raw.readBigInt64BE(CACHE_FORMAT_MAGIC_BYTES));
|
|
70
|
+
ttlMs = Number(raw.readBigInt64BE(CACHE_FORMAT_MAGIC_BYTES + TIMESTAMP_BYTES));
|
|
71
|
+
ivStart = CACHE_FORMAT_MAGIC_BYTES + TIMESTAMP_BYTES + TTL_BYTES;
|
|
72
|
+
authTagStart = ivStart + IV_BYTES;
|
|
73
|
+
ciphertextStart = authTagStart + AUTH_TAG_BYTES;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const headerLen = LEGACY_TIMESTAMP_BYTES + IV_BYTES + AUTH_TAG_BYTES;
|
|
77
|
+
if (raw.length < headerLen)
|
|
78
|
+
return null;
|
|
79
|
+
timestampMs = Number(raw.readBigInt64BE(0));
|
|
80
|
+
ttlMs = DEFAULT_SECRETS_CACHE_TTL_MS;
|
|
81
|
+
ivStart = LEGACY_TIMESTAMP_BYTES;
|
|
82
|
+
authTagStart = ivStart + IV_BYTES;
|
|
83
|
+
ciphertextStart = authTagStart + AUTH_TAG_BYTES;
|
|
84
|
+
}
|
|
85
|
+
if (ttlMs <= 0 || Date.now() - timestampMs > ttlMs) {
|
|
62
86
|
try {
|
|
63
87
|
fs.unlinkSync(filePath);
|
|
64
88
|
}
|
|
65
89
|
catch { /* ok */ }
|
|
66
90
|
return null;
|
|
67
91
|
}
|
|
68
|
-
const iv = raw.subarray(
|
|
69
|
-
const authTag = raw.subarray(
|
|
70
|
-
const ciphertext = raw.subarray(
|
|
92
|
+
const iv = raw.subarray(ivStart, authTagStart);
|
|
93
|
+
const authTag = raw.subarray(authTagStart, ciphertextStart);
|
|
94
|
+
const ciphertext = raw.subarray(ciphertextStart);
|
|
71
95
|
let key;
|
|
72
96
|
try {
|
|
73
97
|
key = getOrCreateKey();
|
|
@@ -89,10 +113,12 @@ export function readCache(companyUid, name) {
|
|
|
89
113
|
return null;
|
|
90
114
|
}
|
|
91
115
|
}
|
|
92
|
-
export function writeCache(companyUid, name, value) {
|
|
116
|
+
export function writeCache(companyUid, name, value, ttlMs = DEFAULT_SECRETS_CACHE_TTL_MS) {
|
|
93
117
|
try {
|
|
94
118
|
if (!validateInputs(companyUid, name))
|
|
95
119
|
return;
|
|
120
|
+
if (ttlMs <= 0)
|
|
121
|
+
return;
|
|
96
122
|
ensureCacheDir(companyUid);
|
|
97
123
|
const key = getOrCreateKey();
|
|
98
124
|
const iv = crypto.randomBytes(IV_BYTES);
|
|
@@ -101,7 +127,9 @@ export function writeCache(companyUid, name, value) {
|
|
|
101
127
|
const authTag = cipher.getAuthTag();
|
|
102
128
|
const timestamp = Buffer.alloc(8);
|
|
103
129
|
timestamp.writeBigInt64BE(BigInt(Date.now()));
|
|
104
|
-
const
|
|
130
|
+
const ttl = Buffer.alloc(8);
|
|
131
|
+
ttl.writeBigInt64BE(BigInt(ttlMs));
|
|
132
|
+
const out = Buffer.concat([CACHE_FORMAT_MAGIC, timestamp, ttl, iv, authTag, encrypted]);
|
|
105
133
|
const filePath = path.join(CACHE_DIR, companyUid, name);
|
|
106
134
|
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
107
135
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
@@ -135,4 +163,4 @@ export function clearAllCache() {
|
|
|
135
163
|
return { removed };
|
|
136
164
|
}
|
|
137
165
|
//# sourceMappingURL=secrets-cache.js.map
|
|
138
|
-
//# debugId=
|
|
166
|
+
//# debugId=fd3738da-3061-5784-bb7c-02021291aecc
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for `hq members invite|list|revoke` (members.ts).
|
|
2
|
+
* Unit tests for `hq members invite|list|revoke|set-role` (members.ts).
|
|
3
3
|
*
|
|
4
4
|
* Coverage:
|
|
5
5
|
* - detectTarget — pure validation for email vs personUid vs invalid
|
|
@@ -7,10 +7,31 @@
|
|
|
7
7
|
* - inviteMember — email + personUid targets, --paths gating, HTTP errors
|
|
8
8
|
* - listPendingInvites — happy path + 403
|
|
9
9
|
* - revokeInvite — happy path + 404
|
|
10
|
+
* - set-role CLI surface — forwards membershipKey/newRole and surfaces 403
|
|
10
11
|
*/
|
|
11
12
|
|
|
13
|
+
import { Command } from "commander";
|
|
12
14
|
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest";
|
|
13
15
|
|
|
16
|
+
vi.mock("../utils/cognito-session.js", async (importOriginal) => {
|
|
17
|
+
const original =
|
|
18
|
+
await importOriginal<typeof import("../utils/cognito-session.js")>();
|
|
19
|
+
return {
|
|
20
|
+
...original,
|
|
21
|
+
ensureCognitoToken: vi.fn(async () => "test-token"),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
vi.mock("../utils/vault-api.js", async (importOriginal) => {
|
|
26
|
+
const original = await importOriginal<typeof import("../utils/vault-api.js")>();
|
|
27
|
+
return {
|
|
28
|
+
...original,
|
|
29
|
+
getCompanyUid: vi.fn(async () => "cmp_acme"),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
34
|
+
import { getCompanyUid } from "../utils/vault-api.js";
|
|
14
35
|
import {
|
|
15
36
|
InviteHttpError,
|
|
16
37
|
detectTarget,
|
|
@@ -18,6 +39,7 @@ import {
|
|
|
18
39
|
getCallerPersonUid,
|
|
19
40
|
inviteMember,
|
|
20
41
|
listPendingInvites,
|
|
42
|
+
registerMembersCommand,
|
|
21
43
|
resendInvite,
|
|
22
44
|
resolveRevokeTargetToMembershipKey,
|
|
23
45
|
revokeInvite,
|
|
@@ -31,15 +53,27 @@ function jsonResponse(status: number, body: unknown): Response {
|
|
|
31
53
|
}
|
|
32
54
|
|
|
33
55
|
let fetchSpy: MockInstance<typeof fetch>;
|
|
56
|
+
const mockEnsureCognitoToken = vi.mocked(ensureCognitoToken);
|
|
57
|
+
const mockGetCompanyUid = vi.mocked(getCompanyUid);
|
|
34
58
|
|
|
35
59
|
beforeEach(() => {
|
|
60
|
+
vi.clearAllMocks();
|
|
36
61
|
fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
62
|
+
mockEnsureCognitoToken.mockResolvedValue("test-token");
|
|
63
|
+
mockGetCompanyUid.mockResolvedValue("cmp_acme");
|
|
37
64
|
});
|
|
38
65
|
|
|
39
66
|
afterEach(() => {
|
|
40
67
|
vi.restoreAllMocks();
|
|
41
68
|
});
|
|
42
69
|
|
|
70
|
+
function buildMembersProgram(): Command {
|
|
71
|
+
const program = new Command();
|
|
72
|
+
program.name("hq").exitOverride();
|
|
73
|
+
registerMembersCommand(program);
|
|
74
|
+
return program;
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
// ---------------------------------------------------------------------------
|
|
44
78
|
// detectTarget
|
|
45
79
|
// ---------------------------------------------------------------------------
|
|
@@ -731,6 +765,63 @@ describe("revokeInvite", () => {
|
|
|
731
765
|
});
|
|
732
766
|
});
|
|
733
767
|
|
|
768
|
+
// ---------------------------------------------------------------------------
|
|
769
|
+
// registerMembersCommand set-role
|
|
770
|
+
// ---------------------------------------------------------------------------
|
|
771
|
+
|
|
772
|
+
describe("registerMembersCommand set-role", () => {
|
|
773
|
+
it("forwards membershipKey + newRole to /membership/role", async () => {
|
|
774
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse(200, {}));
|
|
775
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
776
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
777
|
+
|
|
778
|
+
await buildMembersProgram().parseAsync(
|
|
779
|
+
["members", "--company", "acme", "set-role", "agt_ops#cmp_acme", "admin"],
|
|
780
|
+
{ from: "user" },
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
expect(mockEnsureCognitoToken).toHaveBeenCalledTimes(1);
|
|
784
|
+
expect(mockGetCompanyUid).toHaveBeenCalledWith("test-token", "acme");
|
|
785
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
786
|
+
|
|
787
|
+
const call = fetchSpy.mock.calls[0];
|
|
788
|
+
expect(String(call[0])).toMatch(/\/membership\/role$/);
|
|
789
|
+
expect(call[1]?.method).toBe("POST");
|
|
790
|
+
const body = JSON.parse((call[1]?.body as string) ?? "{}");
|
|
791
|
+
expect(body).toEqual({
|
|
792
|
+
companyUid: "cmp_acme",
|
|
793
|
+
membershipKey: "agt_ops#cmp_acme",
|
|
794
|
+
newRole: "admin",
|
|
795
|
+
});
|
|
796
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
797
|
+
expect.stringContaining("Updated role for 'agt_ops#cmp_acme' to admin"),
|
|
798
|
+
);
|
|
799
|
+
expect(errSpy).not.toHaveBeenCalled();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it("surfaces a backend 403 verbatim and exits non-zero", async () => {
|
|
803
|
+
fetchSpy.mockResolvedValueOnce(
|
|
804
|
+
jsonResponse(403, { error: "Only owners can change member roles." }),
|
|
805
|
+
);
|
|
806
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
807
|
+
vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
808
|
+
vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
|
|
809
|
+
throw new Error(`__EXIT__:${code ?? 0}`);
|
|
810
|
+
}) as never);
|
|
811
|
+
|
|
812
|
+
await expect(
|
|
813
|
+
buildMembersProgram().parseAsync(
|
|
814
|
+
["members", "--company", "acme", "set-role", "agt_ops#cmp_acme", "admin"],
|
|
815
|
+
{ from: "user" },
|
|
816
|
+
),
|
|
817
|
+
).rejects.toThrow("__EXIT__:1");
|
|
818
|
+
|
|
819
|
+
expect(errSpy).toHaveBeenCalledWith(
|
|
820
|
+
expect.stringContaining("Only owners can change member roles."),
|
|
821
|
+
);
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
734
825
|
// ---------------------------------------------------------------------------
|
|
735
826
|
// resolveRevokeTargetToMembershipKey
|
|
736
827
|
// ---------------------------------------------------------------------------
|
package/src/commands/members.ts
CHANGED
|
@@ -6,8 +6,10 @@ import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
|
6
6
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
7
7
|
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
8
8
|
export const VALID_ROLES = new Set(["owner", "admin", "member", "guest"]);
|
|
9
|
+
const VALID_MEMBER_SET_ROLES = new Set(["admin", "member"]);
|
|
9
10
|
|
|
10
11
|
export type Role = "owner" | "admin" | "member" | "guest";
|
|
12
|
+
type MemberSetRole = "admin" | "member";
|
|
11
13
|
|
|
12
14
|
export interface PendingInvite {
|
|
13
15
|
membershipKey: string;
|
|
@@ -455,12 +457,71 @@ export async function revokeInvite(
|
|
|
455
457
|
}
|
|
456
458
|
}
|
|
457
459
|
|
|
460
|
+
async function setMemberRole(
|
|
461
|
+
token: string,
|
|
462
|
+
companyUid: string,
|
|
463
|
+
membershipKey: string,
|
|
464
|
+
newRole: MemberSetRole,
|
|
465
|
+
): Promise<void> {
|
|
466
|
+
if (!VALID_MEMBER_SET_ROLES.has(newRole)) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
`Invalid role '${newRole}': must be one of admin, member`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const res = await vaultApiFetch({
|
|
473
|
+
token,
|
|
474
|
+
path: "/membership/role",
|
|
475
|
+
method: "POST",
|
|
476
|
+
body: { companyUid, membershipKey, newRole },
|
|
477
|
+
});
|
|
478
|
+
if (!res.ok) {
|
|
479
|
+
const err = (await res.json().catch(() => ({}))) as Record<string, string>;
|
|
480
|
+
throw new InviteHttpError(
|
|
481
|
+
res.status,
|
|
482
|
+
err.message ?? err.error ?? res.statusText,
|
|
483
|
+
err.code,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
458
488
|
export function registerMembersCommand(program: Command): void {
|
|
459
489
|
const members = program
|
|
460
490
|
.command("members")
|
|
461
491
|
.description("Manage company memberships and invites")
|
|
462
492
|
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
463
493
|
|
|
494
|
+
members
|
|
495
|
+
.command("set-role <membershipKey> <role>")
|
|
496
|
+
.description("Change a member or agent role to admin or member")
|
|
497
|
+
.action(async (membershipKey: string, role: string) => {
|
|
498
|
+
try {
|
|
499
|
+
const token = await ensureCognitoToken();
|
|
500
|
+
const companySlug = members.opts().company as string | undefined;
|
|
501
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
502
|
+
|
|
503
|
+
await setMemberRole(
|
|
504
|
+
token,
|
|
505
|
+
companyUid,
|
|
506
|
+
membershipKey,
|
|
507
|
+
role as MemberSetRole,
|
|
508
|
+
);
|
|
509
|
+
console.log(
|
|
510
|
+
chalk.green(`Updated role for '${membershipKey}' to ${role}`),
|
|
511
|
+
);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
if (err instanceof InviteHttpError) {
|
|
514
|
+
console.error(chalk.red(err.message));
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
console.error(
|
|
518
|
+
chalk.red("Error:"),
|
|
519
|
+
err instanceof Error ? err.message : String(err),
|
|
520
|
+
);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
464
525
|
members
|
|
465
526
|
.command("invite <target>")
|
|
466
527
|
.description(
|
|
@@ -9,6 +9,7 @@ import { installHqPlugin, prewarmHqSecrets, type PluginState } from '../run/hq-p
|
|
|
9
9
|
vi.mock('../utils/secrets-cache.js', () => {
|
|
10
10
|
const store = new Map<string, string>();
|
|
11
11
|
return {
|
|
12
|
+
DEFAULT_SECRETS_CACHE_TTL_MS: 300000,
|
|
12
13
|
readCache: (uid: string, name: string): string | null =>
|
|
13
14
|
store.get(`${uid}\0${name}`) ?? null,
|
|
14
15
|
writeCache: (uid: string, name: string, value: string): void => {
|
|
@@ -46,7 +47,10 @@ test('.env.local overrides hq()-resolved value', async () => {
|
|
|
46
47
|
// fetchBatch returns "vault-value" — if .env.local is correctly wired,
|
|
47
48
|
// graph resolution should still surface "local-value" because the local
|
|
48
49
|
// file wins precedence.
|
|
49
|
-
fetchBatch: async () => ({
|
|
50
|
+
fetchBatch: async () => ({
|
|
51
|
+
secrets: [{ name: 'KEY', value: 'vault-value', cacheTtlMs: 0 }],
|
|
52
|
+
errors: [],
|
|
53
|
+
}),
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
let state!: PluginState;
|
package/src/commands/run.ts
CHANGED
|
@@ -4,9 +4,27 @@ import * as path from 'node:path';
|
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { internal } from 'varlock';
|
|
6
6
|
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
7
|
+
import { computeSha256 } from '../utils/integrity.js';
|
|
7
8
|
import { vaultApiFetch, getCompanyUid } from '../utils/vault-api.js';
|
|
8
9
|
import { discoverSchemas } from '../run/discover-schemas.js';
|
|
9
10
|
import { installHqPlugin, prewarmHqSecrets, type PluginState, type InstallHqPluginOpts } from '../run/hq-plugin.js';
|
|
11
|
+
import type { SecretLoadResponse, SecretUsage } from './secrets.js';
|
|
12
|
+
|
|
13
|
+
async function buildRunUsage(scriptPath?: string): Promise<SecretUsage | undefined> {
|
|
14
|
+
if (!scriptPath) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const resolvedPath = path.resolve(scriptPath);
|
|
18
|
+
return {
|
|
19
|
+
channel: 'run',
|
|
20
|
+
script: {
|
|
21
|
+
scriptId: resolvedPath,
|
|
22
|
+
path: resolvedPath,
|
|
23
|
+
sha256: await computeSha256(resolvedPath),
|
|
24
|
+
attestationLevel: 'self-asserted-hash',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
10
28
|
|
|
11
29
|
export function registerRunCommand(program: Command): void {
|
|
12
30
|
program
|
|
@@ -14,9 +32,10 @@ export function registerRunCommand(program: Command): void {
|
|
|
14
32
|
.description('Load secrets from .env.schema and run a command with them injected')
|
|
15
33
|
.option('--company <slug>', 'Company slug (overrides @hqCompany in schema)')
|
|
16
34
|
.option('--schema <path>', 'Explicit schema path (skips walk-up discovery)')
|
|
35
|
+
.option('--script <path>', 'Attach local script identity for script-locked secrets')
|
|
17
36
|
.option('--check', 'Resolve schema and validate vars without executing the command')
|
|
18
37
|
.allowUnknownOption(true)
|
|
19
|
-
.action(async (opts: { company?: string; schema?: string; check?: boolean }) => {
|
|
38
|
+
.action(async (opts: { company?: string; schema?: string; script?: string; check?: boolean }) => {
|
|
20
39
|
try {
|
|
21
40
|
const dashIndex = process.argv.indexOf('--');
|
|
22
41
|
const childArgs = dashIndex !== -1 ? process.argv.slice(dashIndex + 1) : [];
|
|
@@ -59,27 +78,39 @@ export function registerRunCommand(program: Command): void {
|
|
|
59
78
|
|
|
60
79
|
const token = await ensureCognitoToken();
|
|
61
80
|
const uid = await getCompanyUid(token, slug);
|
|
81
|
+
const usage = await buildRunUsage(opts.script);
|
|
62
82
|
|
|
63
|
-
const fetchBatch: InstallHqPluginOpts['fetchBatch'] = async (companyUid, names) => {
|
|
83
|
+
const fetchBatch: InstallHqPluginOpts['fetchBatch'] = async (companyUid, names, requestUsage) => {
|
|
64
84
|
const res = await vaultApiFetch({
|
|
65
85
|
token,
|
|
66
86
|
path: `/secrets/${encodeURIComponent(companyUid)}/load`,
|
|
67
87
|
method: 'POST',
|
|
68
|
-
body: { names },
|
|
88
|
+
body: requestUsage ? { names, usage: requestUsage } : { names },
|
|
69
89
|
});
|
|
70
90
|
if (!res.ok) {
|
|
71
|
-
const body = await res.json().catch(() => ({})) as Record<string,
|
|
72
|
-
|
|
91
|
+
const body = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
92
|
+
const message =
|
|
93
|
+
typeof body.message === 'string'
|
|
94
|
+
? body.message
|
|
95
|
+
: typeof body.error === 'string'
|
|
96
|
+
? body.error
|
|
97
|
+
: res.statusText;
|
|
98
|
+
if (
|
|
99
|
+
res.status >= 400 &&
|
|
100
|
+
res.status < 500 &&
|
|
101
|
+
typeof body.code === 'string'
|
|
102
|
+
) {
|
|
103
|
+
throw new Error(message);
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Failed to batch-load secrets: ${message}`);
|
|
73
106
|
}
|
|
74
|
-
return res.json() as Promise<
|
|
75
|
-
secrets: Array<{ name: string; value: string }>;
|
|
76
|
-
errors: Array<{ name: string; code: string; message?: string }>;
|
|
77
|
-
}>;
|
|
107
|
+
return res.json() as Promise<SecretLoadResponse>;
|
|
78
108
|
};
|
|
79
109
|
|
|
80
110
|
const pluginOpts: InstallHqPluginOpts = {
|
|
81
111
|
companyOverride: opts.company,
|
|
82
112
|
resolveCompanyUid: async () => uid,
|
|
113
|
+
usage,
|
|
83
114
|
fetchBatch,
|
|
84
115
|
};
|
|
85
116
|
|