@indigoai-us/hq-cli 5.77.11 → 5.77.12
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/api-keys.js +53 -10
- package/dist/commands/secrets.js +127 -21
- package/dist/utils/resolve-vault-credential.d.ts +30 -0
- package/dist/utils/resolve-vault-credential.js +48 -0
- package/package.json +1 -1
- package/src/commands/api-keys.test.ts +75 -1
- package/src/commands/api-keys.ts +86 -10
- package/src/commands/secrets.test.ts +133 -0
- package/src/commands/secrets.ts +172 -29
- package/src/utils/resolve-vault-credential.test.ts +69 -0
- package/src/utils/resolve-vault-credential.ts +60 -0
package/src/commands/api-keys.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
4
|
+
import { assertCognitoOnlyCommand } from "../utils/resolve-vault-credential.js";
|
|
4
5
|
import { getCompanyUid, vaultApiFetch } from "./secrets.js";
|
|
5
6
|
|
|
7
|
+
async function requireCognitoForApiKeys(label: string): Promise<string> {
|
|
8
|
+
assertCognitoOnlyCommand(label);
|
|
9
|
+
return ensureCognitoToken();
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
type ApiKeyPermission = "read" | "write" | "admin";
|
|
7
13
|
|
|
8
14
|
interface ApiKeyMetadata {
|
|
@@ -12,6 +18,10 @@ interface ApiKeyMetadata {
|
|
|
12
18
|
scope: {
|
|
13
19
|
allowedPrefixes: string[];
|
|
14
20
|
permission: ApiKeyPermission;
|
|
21
|
+
deploy?: {
|
|
22
|
+
apps: string[];
|
|
23
|
+
capabilities: string[];
|
|
24
|
+
};
|
|
15
25
|
};
|
|
16
26
|
status: string;
|
|
17
27
|
createdAt: string;
|
|
@@ -42,6 +52,7 @@ interface ApiKeysGroupOptions {
|
|
|
42
52
|
interface CreateApiKeyOptions {
|
|
43
53
|
name: string;
|
|
44
54
|
scope: string[];
|
|
55
|
+
deployApp: string[];
|
|
45
56
|
permission: string;
|
|
46
57
|
expires?: string;
|
|
47
58
|
}
|
|
@@ -58,6 +69,13 @@ function formatPrefixes(prefixes: string[]): string {
|
|
|
58
69
|
return prefixes.length > 0 ? prefixes.join(", ") : "-";
|
|
59
70
|
}
|
|
60
71
|
|
|
72
|
+
function formatDeployApps(
|
|
73
|
+
deploy?: { apps: string[]; capabilities: string[] },
|
|
74
|
+
): string {
|
|
75
|
+
if (!deploy?.apps?.length) return "-";
|
|
76
|
+
return deploy.apps.join(", ");
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
function parsePermission(value: string): ApiKeyPermission {
|
|
62
80
|
if (value === "read" || value === "write" || value === "admin") {
|
|
63
81
|
return value;
|
|
@@ -107,6 +125,7 @@ function renderApiKeysTable(apiKeys: ApiKeyMetadata[]): void {
|
|
|
107
125
|
name: apiKey.name,
|
|
108
126
|
permission: apiKey.scope.permission,
|
|
109
127
|
prefixes: formatPrefixes(apiKey.scope.allowedPrefixes),
|
|
128
|
+
deployApps: formatDeployApps(apiKey.scope.deploy),
|
|
110
129
|
status: apiKey.status,
|
|
111
130
|
lastUsedAt: formatMaybe(apiKey.lastUsedAt),
|
|
112
131
|
expiresAt: formatMaybe(apiKey.expiresAt),
|
|
@@ -119,6 +138,7 @@ function renderApiKeysTable(apiKeys: ApiKeyMetadata[]): void {
|
|
|
119
138
|
...rows.map((row) => row.permission.length),
|
|
120
139
|
);
|
|
121
140
|
const prefixesWidth = Math.max(8, ...rows.map((row) => row.prefixes.length));
|
|
141
|
+
const deployWidth = Math.max(6, ...rows.map((row) => row.deployApps.length));
|
|
122
142
|
const statusWidth = Math.max(6, ...rows.map((row) => row.status.length));
|
|
123
143
|
const lastUsedWidth = Math.max(
|
|
124
144
|
11,
|
|
@@ -134,6 +154,7 @@ function renderApiKeysTable(apiKeys: ApiKeyMetadata[]): void {
|
|
|
134
154
|
"NAME".padEnd(nameWidth),
|
|
135
155
|
"PERMISSION".padEnd(permissionWidth),
|
|
136
156
|
"PREFIXES".padEnd(prefixesWidth),
|
|
157
|
+
"DEPLOY".padEnd(deployWidth),
|
|
137
158
|
"STATUS".padEnd(statusWidth),
|
|
138
159
|
"LAST USED".padEnd(lastUsedWidth),
|
|
139
160
|
"EXPIRES".padEnd(expiresWidth),
|
|
@@ -147,6 +168,7 @@ function renderApiKeysTable(apiKeys: ApiKeyMetadata[]): void {
|
|
|
147
168
|
row.name.padEnd(nameWidth),
|
|
148
169
|
row.permission.padEnd(permissionWidth),
|
|
149
170
|
row.prefixes.padEnd(prefixesWidth),
|
|
171
|
+
row.deployApps.padEnd(deployWidth),
|
|
150
172
|
row.status.padEnd(statusWidth),
|
|
151
173
|
row.lastUsedAt.padEnd(lastUsedWidth),
|
|
152
174
|
row.expiresAt.padEnd(expiresWidth),
|
|
@@ -163,32 +185,42 @@ export function registerApiKeysCommand(program: Command): Command {
|
|
|
163
185
|
|
|
164
186
|
apiKeys
|
|
165
187
|
.command("create")
|
|
166
|
-
.description(
|
|
188
|
+
.description(
|
|
189
|
+
"Create a new API key (vault secrets and/or scoped deploy via --deploy-app)",
|
|
190
|
+
)
|
|
167
191
|
.requiredOption("--name <label>", "Human-readable label for the API key")
|
|
168
192
|
.option(
|
|
169
193
|
"--scope <prefix>",
|
|
170
|
-
"Allowed prefix (repeatable)",
|
|
194
|
+
"Allowed secret prefix (repeatable)",
|
|
195
|
+
collectRepeatedOption,
|
|
196
|
+
[],
|
|
197
|
+
)
|
|
198
|
+
.option(
|
|
199
|
+
"--deploy-app <id>",
|
|
200
|
+
"Deploy app id/slug allowed for publish (repeatable)",
|
|
171
201
|
collectRepeatedOption,
|
|
172
202
|
[],
|
|
173
203
|
)
|
|
174
204
|
.option(
|
|
175
205
|
"--permission <level>",
|
|
176
|
-
"
|
|
206
|
+
"Secret permission level: read | write | admin (required when --scope is set)",
|
|
177
207
|
"read",
|
|
178
208
|
)
|
|
179
209
|
.option("--expires <ISO8601>", "Optional ISO-8601 expiry timestamp")
|
|
180
210
|
.action(async (opts: CreateApiKeyOptions) => {
|
|
181
211
|
try {
|
|
182
|
-
if (opts.scope.length === 0) {
|
|
212
|
+
if (opts.scope.length === 0 && opts.deployApp.length === 0) {
|
|
183
213
|
console.error(
|
|
184
|
-
chalk.red(
|
|
214
|
+
chalk.red(
|
|
215
|
+
"Error: provide at least one --scope <prefix> and/or --deploy-app <id>.",
|
|
216
|
+
),
|
|
185
217
|
);
|
|
186
218
|
process.exit(1);
|
|
187
219
|
}
|
|
188
220
|
|
|
189
221
|
const permission = parsePermission(opts.permission);
|
|
190
222
|
const expiresAt = parseExpires(opts.expires);
|
|
191
|
-
const token = await
|
|
223
|
+
const token = await requireCognitoForApiKeys("api-keys create");
|
|
192
224
|
const companyUid = await getCompanyUid(
|
|
193
225
|
token,
|
|
194
226
|
(apiKeys.opts() as ApiKeysGroupOptions).company,
|
|
@@ -201,8 +233,20 @@ export function registerApiKeysCommand(program: Command): Command {
|
|
|
201
233
|
body: {
|
|
202
234
|
companyUid,
|
|
203
235
|
name: opts.name,
|
|
204
|
-
|
|
205
|
-
|
|
236
|
+
...(opts.scope.length > 0
|
|
237
|
+
? { allowedPrefixes: opts.scope, permission }
|
|
238
|
+
: {}),
|
|
239
|
+
...(opts.deployApp.length > 0
|
|
240
|
+
? {
|
|
241
|
+
deploy: {
|
|
242
|
+
apps: opts.deployApp,
|
|
243
|
+
capabilities: ["deploy:write"],
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
: {}),
|
|
247
|
+
...(opts.scope.length === 0 && opts.deployApp.length > 0
|
|
248
|
+
? { permission }
|
|
249
|
+
: {}),
|
|
206
250
|
...(expiresAt ? { expiresAt } : {}),
|
|
207
251
|
},
|
|
208
252
|
});
|
|
@@ -225,10 +269,42 @@ export function registerApiKeysCommand(program: Command): Command {
|
|
|
225
269
|
console.log(
|
|
226
270
|
` Prefixes: ${formatPrefixes(data.apiKey.scope.allowedPrefixes)}`,
|
|
227
271
|
);
|
|
272
|
+
console.log(
|
|
273
|
+
` Deploy apps: ${formatDeployApps(data.apiKey.scope.deploy)}`,
|
|
274
|
+
);
|
|
228
275
|
console.log(` Status: ${data.apiKey.status}`);
|
|
229
276
|
console.log(` Created: ${data.apiKey.createdAt}`);
|
|
230
277
|
console.log(` Last used: ${formatMaybe(data.apiKey.lastUsedAt)}`);
|
|
231
278
|
console.log(` Expires: ${formatMaybe(data.apiKey.expiresAt)}`);
|
|
279
|
+
console.log("");
|
|
280
|
+
console.log(chalk.bold("Usage"));
|
|
281
|
+
console.log(
|
|
282
|
+
" This key acts as you (Cognito identity), limited to the scopes above.",
|
|
283
|
+
);
|
|
284
|
+
console.log(" Export it for automation (never falls back to a session):");
|
|
285
|
+
console.log(`\n export HQ_API_KEY='${data.key.value}'\n`);
|
|
286
|
+
console.log(" Vault secrets:");
|
|
287
|
+
console.log(" hq secrets get <NAME> --reveal");
|
|
288
|
+
console.log(" hq secrets exec --only <NAME> -- <command>");
|
|
289
|
+
console.log(
|
|
290
|
+
" Or HTTP: POST /v1/keys/secrets/fetch with Authorization: Bearer <key>",
|
|
291
|
+
);
|
|
292
|
+
if (data.apiKey.scope.deploy?.apps?.length) {
|
|
293
|
+
console.log(" Deploy (scoped apps only):");
|
|
294
|
+
console.log(
|
|
295
|
+
" Authorization: Bearer <key> against the hq-deploy API",
|
|
296
|
+
);
|
|
297
|
+
console.log(
|
|
298
|
+
chalk.dim(
|
|
299
|
+
" Cannot change access-mode, password, or mint hqd_ keys.",
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
console.log(
|
|
304
|
+
chalk.dim(
|
|
305
|
+
" Unsupported under HQ_API_KEY: secrets list/set/share/acl (use a Cognito session).",
|
|
306
|
+
),
|
|
307
|
+
);
|
|
232
308
|
} catch (err) {
|
|
233
309
|
console.error(
|
|
234
310
|
chalk.red("Error:"),
|
|
@@ -243,7 +319,7 @@ export function registerApiKeysCommand(program: Command): Command {
|
|
|
243
319
|
.description("List API keys for a company")
|
|
244
320
|
.action(async () => {
|
|
245
321
|
try {
|
|
246
|
-
const token = await
|
|
322
|
+
const token = await requireCognitoForApiKeys("api-keys list");
|
|
247
323
|
const companyUid = await getCompanyUid(
|
|
248
324
|
token,
|
|
249
325
|
(apiKeys.opts() as ApiKeysGroupOptions).company,
|
|
@@ -280,7 +356,7 @@ export function registerApiKeysCommand(program: Command): Command {
|
|
|
280
356
|
.description("Revoke an API key")
|
|
281
357
|
.action(async (keyId: string) => {
|
|
282
358
|
try {
|
|
283
|
-
const token = await
|
|
359
|
+
const token = await requireCognitoForApiKeys("api-keys revoke");
|
|
284
360
|
const res = await vaultApiFetch({
|
|
285
361
|
token,
|
|
286
362
|
path: `/v1/api-keys/${encodeURIComponent(keyId)}/revoke`,
|
|
@@ -1902,3 +1902,136 @@ describe("secrets reveal and policy controls", () => {
|
|
|
1902
1902
|
);
|
|
1903
1903
|
});
|
|
1904
1904
|
});
|
|
1905
|
+
|
|
1906
|
+
describe("HQ_API_KEY consume path", () => {
|
|
1907
|
+
let exitSpy: MockInstance<typeof process.exit>;
|
|
1908
|
+
|
|
1909
|
+
beforeEach(() => {
|
|
1910
|
+
exitSpy = vi
|
|
1911
|
+
.spyOn(process, "exit")
|
|
1912
|
+
.mockImplementation(((code?: number) => {
|
|
1913
|
+
throw new Error(`__EXIT__:${code ?? 0}`);
|
|
1914
|
+
}) as never);
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
afterEach(() => {
|
|
1918
|
+
delete process.env.HQ_API_KEY;
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
it("rejects secrets list when HQ_API_KEY is set (no Cognito fallback)", async () => {
|
|
1922
|
+
process.env.HQ_API_KEY = "hqk_probe";
|
|
1923
|
+
const program = buildProgram();
|
|
1924
|
+
await expect(
|
|
1925
|
+
program.parseAsync(["node", "hq", "secrets", "list"]),
|
|
1926
|
+
).rejects.toThrow(/__EXIT__:1/);
|
|
1927
|
+
expect(ensureCognitoToken).not.toHaveBeenCalled();
|
|
1928
|
+
expect(vaultApiFetch).not.toHaveBeenCalled();
|
|
1929
|
+
const errText = errSpy.mock.calls
|
|
1930
|
+
.map((call) => call.map(String).join(" "))
|
|
1931
|
+
.join("\n");
|
|
1932
|
+
expect(errText).toMatch(/not supported for API keys/);
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
it("rejects invalid HQ_API_KEY prefix without Cognito fallback", async () => {
|
|
1936
|
+
process.env.HQ_API_KEY = "hqd_not_a_vault_key";
|
|
1937
|
+
const program = buildProgram();
|
|
1938
|
+
await expect(
|
|
1939
|
+
program.parseAsync(["node", "hq", "secrets", "get", "FOO"]),
|
|
1940
|
+
).rejects.toThrow(/__EXIT__:1/);
|
|
1941
|
+
expect(ensureCognitoToken).not.toHaveBeenCalled();
|
|
1942
|
+
const errText = errSpy.mock.calls
|
|
1943
|
+
.map((call) => call.map(String).join(" "))
|
|
1944
|
+
.join("\n");
|
|
1945
|
+
expect(errText).toMatch(/must start with 'hqk_'/);
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
it("gets a secret via /v1/keys/secrets/fetch when HQ_API_KEY is set", async () => {
|
|
1949
|
+
process.env.HQ_API_KEY = "hqk_valid_key";
|
|
1950
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
1951
|
+
jsonRes({
|
|
1952
|
+
secret: {
|
|
1953
|
+
name: "FOO",
|
|
1954
|
+
value: "secret-value",
|
|
1955
|
+
version: 1,
|
|
1956
|
+
tier: "standard",
|
|
1957
|
+
},
|
|
1958
|
+
}),
|
|
1959
|
+
);
|
|
1960
|
+
|
|
1961
|
+
const program = buildProgram();
|
|
1962
|
+
await program.parseAsync([
|
|
1963
|
+
"node",
|
|
1964
|
+
"hq",
|
|
1965
|
+
"secrets",
|
|
1966
|
+
"get",
|
|
1967
|
+
"FOO",
|
|
1968
|
+
"--reveal",
|
|
1969
|
+
]);
|
|
1970
|
+
|
|
1971
|
+
expect(ensureCognitoToken).not.toHaveBeenCalled();
|
|
1972
|
+
expect(vaultApiFetch).toHaveBeenCalledWith(
|
|
1973
|
+
expect.objectContaining({
|
|
1974
|
+
token: "hqk_valid_key",
|
|
1975
|
+
path: "/v1/keys/secrets/fetch",
|
|
1976
|
+
method: "POST",
|
|
1977
|
+
body: { name: "FOO" },
|
|
1978
|
+
}),
|
|
1979
|
+
);
|
|
1980
|
+
const printed = logSpy.mock.calls
|
|
1981
|
+
.map((call) => call.map(String).join(" "))
|
|
1982
|
+
.join("\n");
|
|
1983
|
+
expect(printed).toContain("FOO");
|
|
1984
|
+
expect(printed).toContain("secret-value");
|
|
1985
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
it("fails --reveal when API-key fetch omits secret.value", async () => {
|
|
1989
|
+
process.env.HQ_API_KEY = "hqk_valid_key";
|
|
1990
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
1991
|
+
jsonRes({
|
|
1992
|
+
secret: {
|
|
1993
|
+
name: "FOO",
|
|
1994
|
+
version: 1,
|
|
1995
|
+
tier: "standard",
|
|
1996
|
+
},
|
|
1997
|
+
}),
|
|
1998
|
+
);
|
|
1999
|
+
|
|
2000
|
+
const program = buildProgram();
|
|
2001
|
+
await expect(
|
|
2002
|
+
program.parseAsync([
|
|
2003
|
+
"node",
|
|
2004
|
+
"hq",
|
|
2005
|
+
"secrets",
|
|
2006
|
+
"get",
|
|
2007
|
+
"FOO",
|
|
2008
|
+
"--reveal",
|
|
2009
|
+
]),
|
|
2010
|
+
).rejects.toThrow(/__EXIT__:1/);
|
|
2011
|
+
const errText = errSpy.mock.calls
|
|
2012
|
+
.map((call) => call.map(String).join(" "))
|
|
2013
|
+
.join("\n");
|
|
2014
|
+
expect(errText).toMatch(/omitted secret\.value/);
|
|
2015
|
+
const printed = logSpy.mock.calls
|
|
2016
|
+
.map((call) => call.map(String).join(" "))
|
|
2017
|
+
.join("\n");
|
|
2018
|
+
expect(printed).not.toContain("[REDACTED]");
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
it("loadRevealedSecrets uses fetch endpoint for hqk_ tokens", async () => {
|
|
2022
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
2023
|
+
jsonRes({
|
|
2024
|
+
secret: { name: "A", value: "va" },
|
|
2025
|
+
}),
|
|
2026
|
+
);
|
|
2027
|
+
const out = await loadRevealedSecrets("hqk_tok", "cmp_x", ["A"]);
|
|
2028
|
+
expect(out.get("A")).toBe("va");
|
|
2029
|
+
expect(vaultApiFetch).toHaveBeenCalledWith(
|
|
2030
|
+
expect.objectContaining({
|
|
2031
|
+
path: "/v1/keys/secrets/fetch",
|
|
2032
|
+
method: "POST",
|
|
2033
|
+
body: { name: "A" },
|
|
2034
|
+
}),
|
|
2035
|
+
);
|
|
2036
|
+
});
|
|
2037
|
+
});
|
package/src/commands/secrets.ts
CHANGED
|
@@ -23,6 +23,11 @@ import {
|
|
|
23
23
|
getCompanyUid,
|
|
24
24
|
getEntityUid,
|
|
25
25
|
} from "../utils/vault-api.js";
|
|
26
|
+
import {
|
|
27
|
+
HQ_API_KEY_PREFIX,
|
|
28
|
+
assertCognitoOnlyCommand,
|
|
29
|
+
resolveVaultCredential,
|
|
30
|
+
} from "../utils/resolve-vault-credential.js";
|
|
26
31
|
import {
|
|
27
32
|
SandboxRunnerClient,
|
|
28
33
|
type SandboxRunnerJob,
|
|
@@ -30,6 +35,14 @@ import {
|
|
|
30
35
|
export type { VaultApiOptions } from "../utils/vault-api.js";
|
|
31
36
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
32
37
|
|
|
38
|
+
/** Cognito session for secrets commands that do not support HQ_API_KEY. */
|
|
39
|
+
async function requireCognitoTokenForSecrets(
|
|
40
|
+
commandLabel: string,
|
|
41
|
+
): Promise<string> {
|
|
42
|
+
assertCognitoOnlyCommand(commandLabel);
|
|
43
|
+
return ensureCognitoToken();
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
interface SecretsScopeOpts {
|
|
34
47
|
company?: string;
|
|
35
48
|
personal?: boolean;
|
|
@@ -654,12 +667,76 @@ function renderPolicyScripts(scripts: SecretPolicyScript[]): void {
|
|
|
654
667
|
// Requests are chunked at MAX_BATCH_NAMES and throw on the FIRST unresolved key
|
|
655
668
|
// with the same `Failed to fetch secret '<k>': <reason>` shape the per-key GET
|
|
656
669
|
// path used — never swallows a failure.
|
|
670
|
+
async function loadRevealedSecretsViaApiKey(
|
|
671
|
+
token: string,
|
|
672
|
+
keys: string[],
|
|
673
|
+
): Promise<Map<string, string>> {
|
|
674
|
+
const resolved = new Map<string, string>();
|
|
675
|
+
const requested = [...new Set(keys)];
|
|
676
|
+
const cacheScope = "__api_key__";
|
|
677
|
+
|
|
678
|
+
for (const name of requested) {
|
|
679
|
+
const res = await vaultApiFetch({
|
|
680
|
+
token,
|
|
681
|
+
path: "/v1/keys/secrets/fetch",
|
|
682
|
+
method: "POST",
|
|
683
|
+
body: { name },
|
|
684
|
+
signal: AbortSignal.timeout(SECRET_LOAD_TIMEOUT_MS),
|
|
685
|
+
});
|
|
686
|
+
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
687
|
+
|
|
688
|
+
if (!res.ok) {
|
|
689
|
+
if (res.status === 404) {
|
|
690
|
+
throw new Error(`Failed to fetch secret '${name}': Secret not found`);
|
|
691
|
+
}
|
|
692
|
+
if (res.status === 403) {
|
|
693
|
+
const message =
|
|
694
|
+
typeof body.error === "string"
|
|
695
|
+
? body.error
|
|
696
|
+
: typeof body.message === "string"
|
|
697
|
+
? body.message
|
|
698
|
+
: "No read permission";
|
|
699
|
+
if (body.highSecurity === true) {
|
|
700
|
+
throw new Error(highSecuritySandboxOnlyMessage(name));
|
|
701
|
+
}
|
|
702
|
+
throw new Error(`Failed to fetch secret '${name}': ${message}`);
|
|
703
|
+
}
|
|
704
|
+
if (res.status === 401) {
|
|
705
|
+
throw new Error(
|
|
706
|
+
`Failed to fetch secret '${name}': Invalid or missing API key`,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
throw new Error(
|
|
710
|
+
`Failed to fetch secret '${name}': ${extractApiMessage(body, res.statusText)}`,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const secret =
|
|
715
|
+
typeof body.secret === "object" && body.secret !== null
|
|
716
|
+
? (body.secret as { value?: unknown; name?: unknown })
|
|
717
|
+
: null;
|
|
718
|
+
if (typeof secret?.value !== "string") {
|
|
719
|
+
throw new Error(
|
|
720
|
+
`Failed to fetch secret '${name}': malformed fetch response`,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
removeCacheEntry(cacheScope, name);
|
|
724
|
+
resolved.set(name, secret.value);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return resolved;
|
|
728
|
+
}
|
|
729
|
+
|
|
657
730
|
export async function loadRevealedSecrets(
|
|
658
731
|
token: string,
|
|
659
732
|
companyUid: string,
|
|
660
733
|
keys: string[],
|
|
661
734
|
usage?: SecretUsage,
|
|
662
735
|
): Promise<Map<string, string>> {
|
|
736
|
+
if (token.startsWith(HQ_API_KEY_PREFIX)) {
|
|
737
|
+
return loadRevealedSecretsViaApiKey(token, keys);
|
|
738
|
+
}
|
|
739
|
+
|
|
663
740
|
const resolved = new Map<string, string>();
|
|
664
741
|
const requested = [...new Set(keys)];
|
|
665
742
|
try {
|
|
@@ -918,7 +995,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
918
995
|
process.exit(1);
|
|
919
996
|
}
|
|
920
997
|
|
|
921
|
-
const token = await
|
|
998
|
+
const token = await requireCognitoTokenForSecrets("secrets set");
|
|
922
999
|
const scope = scopeOpts(secrets.opts());
|
|
923
1000
|
const companyUid = await getEntityUid(token, scope);
|
|
924
1001
|
const scopeLabel = describeSecretsScope({
|
|
@@ -975,7 +1052,69 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
975
1052
|
.option("--reveal", "Include the decrypted secret value")
|
|
976
1053
|
.action(async (name: string, opts: { reveal?: boolean }) => {
|
|
977
1054
|
try {
|
|
978
|
-
const
|
|
1055
|
+
const cred = await resolveVaultCredential();
|
|
1056
|
+
|
|
1057
|
+
if (cred.kind === "api-key") {
|
|
1058
|
+
const res = await vaultApiFetch({
|
|
1059
|
+
token: cred.token,
|
|
1060
|
+
path: "/v1/keys/secrets/fetch",
|
|
1061
|
+
method: "POST",
|
|
1062
|
+
body: { name },
|
|
1063
|
+
});
|
|
1064
|
+
const body = (await res.json().catch(() => ({}))) as Record<
|
|
1065
|
+
string,
|
|
1066
|
+
unknown
|
|
1067
|
+
>;
|
|
1068
|
+
if (!res.ok) {
|
|
1069
|
+
if (res.status === 403 && body.highSecurity === true) {
|
|
1070
|
+
console.error(chalk.red(highSecuritySandboxOnlyMessage(name)));
|
|
1071
|
+
process.exit(1);
|
|
1072
|
+
}
|
|
1073
|
+
console.error(
|
|
1074
|
+
chalk.red(
|
|
1075
|
+
`Failed to get secret: ${extractApiMessage(body, res.statusText)}`,
|
|
1076
|
+
),
|
|
1077
|
+
);
|
|
1078
|
+
process.exit(1);
|
|
1079
|
+
}
|
|
1080
|
+
const secret =
|
|
1081
|
+
typeof body.secret === "object" && body.secret !== null
|
|
1082
|
+
? (body.secret as SecretGetResponse["secret"] & {
|
|
1083
|
+
value?: string;
|
|
1084
|
+
})
|
|
1085
|
+
: null;
|
|
1086
|
+
if (!secret || typeof secret.name !== "string") {
|
|
1087
|
+
console.error(chalk.red("Failed to get secret: malformed response"));
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
console.log(chalk.bold(`Secret: ${secret.name}`));
|
|
1091
|
+
if (secret.lastModifiedDate) {
|
|
1092
|
+
console.log(` Last Modified: ${secret.lastModifiedDate}`);
|
|
1093
|
+
}
|
|
1094
|
+
if (secret.version != null) {
|
|
1095
|
+
console.log(` Version: ${secret.version}`);
|
|
1096
|
+
}
|
|
1097
|
+
console.log(` Tier: ${normalizeSecretTier(secret.tier)}`);
|
|
1098
|
+
console.log(
|
|
1099
|
+
` Script Lock: ${normalizeScriptLockMode(secret.scriptLock?.mode)}`,
|
|
1100
|
+
);
|
|
1101
|
+
if (opts.reveal) {
|
|
1102
|
+
if (typeof secret.value !== "string") {
|
|
1103
|
+
console.error(
|
|
1104
|
+
chalk.red(
|
|
1105
|
+
"Failed to get secret: reveal requested but response omitted secret.value",
|
|
1106
|
+
),
|
|
1107
|
+
);
|
|
1108
|
+
process.exit(1);
|
|
1109
|
+
}
|
|
1110
|
+
console.log(` Value: ${secret.value}`);
|
|
1111
|
+
} else {
|
|
1112
|
+
console.log(` Value: ${chalk.dim("[REDACTED]")}`);
|
|
1113
|
+
}
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const token = cred.token;
|
|
979
1118
|
const companyUid = await getEntityUid(
|
|
980
1119
|
token,
|
|
981
1120
|
scopeOpts(secrets.opts()),
|
|
@@ -1061,7 +1200,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1061
1200
|
.option("--quiet", "Suppress the present/absent line (use the exit code only)")
|
|
1062
1201
|
.action(async (name: string, opts: { quiet?: boolean }) => {
|
|
1063
1202
|
try {
|
|
1064
|
-
const token = await
|
|
1203
|
+
const token = await requireCognitoTokenForSecrets("secrets exists");
|
|
1065
1204
|
const companyUid = await getEntityUid(
|
|
1066
1205
|
token,
|
|
1067
1206
|
scopeOpts(secrets.opts()),
|
|
@@ -1124,7 +1263,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1124
1263
|
normalizedPrefix = normalized;
|
|
1125
1264
|
}
|
|
1126
1265
|
|
|
1127
|
-
const token = await
|
|
1266
|
+
const token = await requireCognitoTokenForSecrets("secrets list");
|
|
1128
1267
|
const scope = scopeOpts(secrets.opts());
|
|
1129
1268
|
const companyUid = await getEntityUid(token, scope);
|
|
1130
1269
|
const scopeLabel = describeSecretsScope({
|
|
@@ -1226,7 +1365,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1226
1365
|
process.exit(1);
|
|
1227
1366
|
}
|
|
1228
1367
|
|
|
1229
|
-
const token = await
|
|
1368
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1230
1369
|
const companyUid = await getEntityUid(
|
|
1231
1370
|
token,
|
|
1232
1371
|
scopeOpts(secrets.opts()),
|
|
@@ -1309,7 +1448,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1309
1448
|
process.exit(1);
|
|
1310
1449
|
}
|
|
1311
1450
|
|
|
1312
|
-
const token = await
|
|
1451
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1313
1452
|
const companyUid = await getEntityUid(
|
|
1314
1453
|
token,
|
|
1315
1454
|
scopeOpts(secrets.opts()),
|
|
@@ -1392,7 +1531,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1392
1531
|
opts.id,
|
|
1393
1532
|
opts.attestation,
|
|
1394
1533
|
);
|
|
1395
|
-
const token = await
|
|
1534
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1396
1535
|
const companyUid = await getEntityUid(
|
|
1397
1536
|
token,
|
|
1398
1537
|
scopeOpts(secrets.opts()),
|
|
@@ -1442,7 +1581,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1442
1581
|
process.exit(1);
|
|
1443
1582
|
}
|
|
1444
1583
|
|
|
1445
|
-
const token = await
|
|
1584
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1446
1585
|
const companyUid = await getEntityUid(
|
|
1447
1586
|
token,
|
|
1448
1587
|
scopeOpts(secrets.opts()),
|
|
@@ -1485,7 +1624,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1485
1624
|
process.exit(1);
|
|
1486
1625
|
}
|
|
1487
1626
|
|
|
1488
|
-
const token = await
|
|
1627
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1489
1628
|
const companyUid = await getEntityUid(
|
|
1490
1629
|
token,
|
|
1491
1630
|
scopeOpts(secrets.opts()),
|
|
@@ -1546,7 +1685,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1546
1685
|
}
|
|
1547
1686
|
}
|
|
1548
1687
|
|
|
1549
|
-
const token = await
|
|
1688
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1550
1689
|
const companyUid = await getEntityUid(
|
|
1551
1690
|
token,
|
|
1552
1691
|
scopeOpts(secrets.opts()),
|
|
@@ -1606,7 +1745,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1606
1745
|
}
|
|
1607
1746
|
|
|
1608
1747
|
const keys = parseSecretNameList(opts.only);
|
|
1609
|
-
const token = await
|
|
1748
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1610
1749
|
const scope = scopeOpts(mergeScopeOpts(secrets.opts(), opts));
|
|
1611
1750
|
const companyUid = await getEntityUid(token, scope);
|
|
1612
1751
|
const client = new SandboxRunnerClient();
|
|
@@ -1673,17 +1812,19 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1673
1812
|
|
|
1674
1813
|
const keys = parseSecretNameList(_opts.only);
|
|
1675
1814
|
|
|
1676
|
-
const
|
|
1677
|
-
const companyUid =
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1815
|
+
const cred = await resolveVaultCredential();
|
|
1816
|
+
const companyUid =
|
|
1817
|
+
cred.kind === "api-key"
|
|
1818
|
+
? "__api_key__"
|
|
1819
|
+
: await getEntityUid(cred.token, scopeOpts(secrets.opts()));
|
|
1681
1820
|
|
|
1682
1821
|
const revealed = await loadRevealedSecrets(
|
|
1683
|
-
token,
|
|
1822
|
+
cred.token,
|
|
1684
1823
|
companyUid,
|
|
1685
1824
|
keys,
|
|
1686
|
-
|
|
1825
|
+
cred.kind === "cognito"
|
|
1826
|
+
? await buildSecretUsage("exec", _opts.script, _opts.scriptId)
|
|
1827
|
+
: undefined,
|
|
1687
1828
|
);
|
|
1688
1829
|
|
|
1689
1830
|
const secretEnv: Record<string, string> = {};
|
|
@@ -1744,17 +1885,19 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1744
1885
|
|
|
1745
1886
|
const keys = parseSecretNameList(opts.only);
|
|
1746
1887
|
|
|
1747
|
-
const
|
|
1748
|
-
const companyUid =
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1888
|
+
const cred = await resolveVaultCredential();
|
|
1889
|
+
const companyUid =
|
|
1890
|
+
cred.kind === "api-key"
|
|
1891
|
+
? "__api_key__"
|
|
1892
|
+
: await getEntityUid(cred.token, scopeOpts(secrets.opts()));
|
|
1752
1893
|
|
|
1753
1894
|
const revealed = await loadRevealedSecrets(
|
|
1754
|
-
token,
|
|
1895
|
+
cred.token,
|
|
1755
1896
|
companyUid,
|
|
1756
1897
|
keys,
|
|
1757
|
-
|
|
1898
|
+
cred.kind === "cognito"
|
|
1899
|
+
? await buildSecretUsage("env", opts.script, opts.scriptId)
|
|
1900
|
+
: undefined,
|
|
1758
1901
|
);
|
|
1759
1902
|
|
|
1760
1903
|
for (const key of keys) {
|
|
@@ -1799,7 +1942,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1799
1942
|
process.exit(1);
|
|
1800
1943
|
}
|
|
1801
1944
|
|
|
1802
|
-
const token = await
|
|
1945
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1803
1946
|
const companyUid = await getEntityUid(
|
|
1804
1947
|
token,
|
|
1805
1948
|
scopeOpts(secrets.opts()),
|
|
@@ -1865,7 +2008,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1865
2008
|
process.exit(1);
|
|
1866
2009
|
}
|
|
1867
2010
|
|
|
1868
|
-
const token = await
|
|
2011
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1869
2012
|
const companyUid = await getEntityUid(
|
|
1870
2013
|
token,
|
|
1871
2014
|
scopeOpts(secrets.opts()),
|
|
@@ -1929,7 +2072,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1929
2072
|
process.exit(1);
|
|
1930
2073
|
}
|
|
1931
2074
|
|
|
1932
|
-
const token = await
|
|
2075
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1933
2076
|
const companyUid = await getEntityUid(
|
|
1934
2077
|
token,
|
|
1935
2078
|
scopeOpts(secrets.opts()),
|
|
@@ -1985,7 +2128,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
1985
2128
|
process.exit(1);
|
|
1986
2129
|
}
|
|
1987
2130
|
|
|
1988
|
-
const token = await
|
|
2131
|
+
const token = await requireCognitoTokenForSecrets("secrets");
|
|
1989
2132
|
const companyUid = await getEntityUid(
|
|
1990
2133
|
token,
|
|
1991
2134
|
scopeOpts(secrets.opts()),
|