@indigoai-us/hq-cli 5.5.1 → 5.5.2-sentry-rc.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 (127) hide show
  1. package/dist/bin/hq-auth-refresh.js +22 -12
  2. package/dist/bin/hq-auth-refresh.js.map +1 -1
  3. package/dist/commands/_patterns.d.ts +3 -0
  4. package/dist/commands/_patterns.d.ts.map +1 -0
  5. package/dist/commands/_patterns.js +3 -0
  6. package/dist/commands/_patterns.js.map +1 -0
  7. package/dist/commands/add.d.ts.map +1 -1
  8. package/dist/commands/add.js +2 -1
  9. package/dist/commands/add.js.map +1 -1
  10. package/dist/commands/auth.d.ts +0 -3
  11. package/dist/commands/auth.d.ts.map +1 -1
  12. package/dist/commands/auth.js +0 -3
  13. package/dist/commands/auth.js.map +1 -1
  14. package/dist/commands/groups.d.ts +3 -0
  15. package/dist/commands/groups.d.ts.map +1 -0
  16. package/dist/commands/groups.js +341 -0
  17. package/dist/commands/groups.js.map +1 -0
  18. package/dist/commands/list.d.ts.map +1 -1
  19. package/dist/commands/list.js +18 -1
  20. package/dist/commands/list.js.map +1 -1
  21. package/dist/commands/login.d.ts +1 -1
  22. package/dist/commands/login.d.ts.map +1 -1
  23. package/dist/commands/login.js +28 -9
  24. package/dist/commands/login.js.map +1 -1
  25. package/dist/commands/logout.d.ts +1 -1
  26. package/dist/commands/logout.d.ts.map +1 -1
  27. package/dist/commands/logout.js +8 -3
  28. package/dist/commands/logout.js.map +1 -1
  29. package/dist/commands/pack-install.d.ts +44 -0
  30. package/dist/commands/pack-install.d.ts.map +1 -0
  31. package/dist/commands/pack-install.js +513 -0
  32. package/dist/commands/pack-install.js.map +1 -0
  33. package/dist/commands/pkg-install.d.ts.map +1 -1
  34. package/dist/commands/pkg-install.js +20 -8
  35. package/dist/commands/pkg-install.js.map +1 -1
  36. package/dist/commands/pkg-list.js +4 -4
  37. package/dist/commands/pkg-list.js.map +1 -1
  38. package/dist/commands/pkg-update.js +3 -3
  39. package/dist/commands/pkg-update.js.map +1 -1
  40. package/dist/commands/secrets.d.ts +12 -0
  41. package/dist/commands/secrets.d.ts.map +1 -0
  42. package/dist/commands/secrets.js +756 -0
  43. package/dist/commands/secrets.js.map +1 -0
  44. package/dist/commands/sync.d.ts.map +1 -1
  45. package/dist/commands/sync.js +10 -2
  46. package/dist/commands/sync.js.map +1 -1
  47. package/dist/commands/team-sync.d.ts.map +1 -1
  48. package/dist/commands/team-sync.js +6 -6
  49. package/dist/commands/team-sync.js.map +1 -1
  50. package/dist/commands/whoami.d.ts +1 -1
  51. package/dist/commands/whoami.d.ts.map +1 -1
  52. package/dist/commands/whoami.js +23 -7
  53. package/dist/commands/whoami.js.map +1 -1
  54. package/dist/index.js +21 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/sentry-before-send.d.ts +3 -0
  57. package/dist/sentry-before-send.d.ts.map +1 -0
  58. package/dist/sentry-before-send.js +216 -0
  59. package/dist/sentry-before-send.js.map +1 -0
  60. package/dist/sentry-dsn.generated.d.ts +2 -0
  61. package/dist/sentry-dsn.generated.d.ts.map +1 -0
  62. package/dist/sentry-dsn.generated.js.map +1 -0
  63. package/dist/sentry.d.ts +4 -0
  64. package/dist/sentry.d.ts.map +1 -0
  65. package/dist/sentry.js +19 -0
  66. package/dist/sentry.js.map +1 -0
  67. package/dist/strategies/link.d.ts +2 -2
  68. package/dist/strategies/link.d.ts.map +1 -1
  69. package/dist/strategies/link.js.map +1 -1
  70. package/dist/strategies/merge.d.ts +2 -2
  71. package/dist/strategies/merge.d.ts.map +1 -1
  72. package/dist/strategies/merge.js.map +1 -1
  73. package/dist/types.d.ts +37 -3
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js +6 -0
  76. package/dist/types.js.map +1 -1
  77. package/dist/utils/cognito-session.js +3 -3
  78. package/dist/utils/cognito-session.js.map +1 -1
  79. package/dist/utils/manifest.d.ts +13 -0
  80. package/dist/utils/manifest.d.ts.map +1 -1
  81. package/dist/utils/manifest.js +24 -1
  82. package/dist/utils/manifest.js.map +1 -1
  83. package/dist/utils/secrets-cache.d.ts +7 -0
  84. package/dist/utils/secrets-cache.d.ts.map +1 -0
  85. package/dist/utils/secrets-cache.js +135 -0
  86. package/dist/utils/secrets-cache.js.map +1 -0
  87. package/package.json +9 -5
  88. package/scripts/generate-dsn.mjs +26 -0
  89. package/src/bin/hq-auth-refresh.ts +23 -16
  90. package/src/commands/_patterns.ts +2 -0
  91. package/src/commands/add.ts +7 -6
  92. package/src/commands/auth.ts +0 -3
  93. package/src/commands/groups.ts +357 -0
  94. package/src/commands/list.ts +20 -1
  95. package/src/commands/login.ts +28 -9
  96. package/src/commands/logout.ts +8 -3
  97. package/src/commands/pack-install.ts +667 -0
  98. package/src/commands/pkg-install.ts +34 -16
  99. package/src/commands/pkg-list.ts +4 -4
  100. package/src/commands/pkg-update.ts +3 -3
  101. package/src/commands/secrets.ts +950 -0
  102. package/src/commands/sync.ts +20 -4
  103. package/src/commands/team-sync.ts +7 -8
  104. package/src/commands/whoami.ts +22 -7
  105. package/src/index.ts +22 -2
  106. package/src/sentry-before-send.test.ts +359 -0
  107. package/src/sentry-before-send.ts +221 -0
  108. package/src/sentry.test.ts +24 -0
  109. package/src/sentry.ts +19 -0
  110. package/src/strategies/link.ts +2 -2
  111. package/src/strategies/merge.ts +2 -2
  112. package/src/types.ts +54 -4
  113. package/src/utils/cognito-session.ts +3 -3
  114. package/src/utils/manifest.test.ts +148 -0
  115. package/src/utils/manifest.ts +22 -1
  116. package/src/utils/secrets-cache.ts +130 -0
  117. package/tsconfig.json +2 -1
  118. package/dist/utils/auth.d.ts +0 -27
  119. package/dist/utils/auth.d.ts.map +0 -1
  120. package/dist/utils/auth.js +0 -155
  121. package/dist/utils/auth.js.map +0 -1
  122. package/dist/utils/token-store.d.ts +0 -28
  123. package/dist/utils/token-store.d.ts.map +0 -1
  124. package/dist/utils/token-store.js +0 -68
  125. package/dist/utils/token-store.js.map +0 -1
  126. package/src/utils/auth.ts +0 -193
  127. package/src/utils/token-store.ts +0 -83
@@ -0,0 +1,756 @@
1
+ import chalk from "chalk";
2
+ import * as readline from "node:readline";
3
+ import { spawn } from "node:child_process";
4
+ import { ensureCognitoToken, DEFAULT_VAULT_API_URL, } from "../utils/cognito-session.js";
5
+ import { readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
6
+ import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN } from "./_patterns.js";
7
+ function shellSingleQuote(value) {
8
+ return "'" + value.replace(/'/g, "'\\''") + "'";
9
+ }
10
+ // API Gateway v2 URL-decodes `%2F` before route matching, so a full-name
11
+ // `encodeURIComponent` would collapse structural `/` separators into a single
12
+ // segment the `/name/{proxy+}` route can't match.
13
+ function buildSecretNamePath(companyUid, name) {
14
+ const encodedName = name
15
+ .split("/")
16
+ .map(encodeURIComponent)
17
+ .join("/");
18
+ return `/secrets/${encodeURIComponent(companyUid)}/name/${encodedName}`;
19
+ }
20
+ export async function vaultApiFetch(opts) {
21
+ const url = new URL(opts.path, DEFAULT_VAULT_API_URL);
22
+ if (opts.query) {
23
+ for (const [k, v] of Object.entries(opts.query)) {
24
+ url.searchParams.set(k, v);
25
+ }
26
+ }
27
+ return fetch(url.toString(), {
28
+ method: opts.method ?? "GET",
29
+ headers: {
30
+ Authorization: `Bearer ${opts.token}`,
31
+ "Content-Type": "application/json",
32
+ },
33
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
34
+ });
35
+ }
36
+ async function resolveCompanyUid(token, slug) {
37
+ const res = await vaultApiFetch({
38
+ token,
39
+ path: `/entity/by-slug/company/${encodeURIComponent(slug)}`,
40
+ });
41
+ if (!res.ok) {
42
+ const body = await res.json().catch(() => ({}));
43
+ throw new Error(`Failed to resolve company slug '${slug}': ${body.error ?? res.statusText}`);
44
+ }
45
+ const data = (await res.json());
46
+ return data.entity.uid;
47
+ }
48
+ async function resolveCompanyFromMemberships(token) {
49
+ const res = await vaultApiFetch({
50
+ token,
51
+ path: "/membership/me",
52
+ });
53
+ if (!res.ok) {
54
+ throw new Error("Failed to fetch memberships — run `hq login` and try again");
55
+ }
56
+ const data = (await res.json());
57
+ const active = data.memberships.filter((m) => m.status === "active");
58
+ if (active.length === 0) {
59
+ throw new Error("No active company memberships found. Use --company <slug> to specify.");
60
+ }
61
+ if (active.length === 1) {
62
+ return active[0].companyUid;
63
+ }
64
+ const uids = active.map((m) => m.companyUid).join(", ");
65
+ throw new Error(`Multiple companies found (${uids}). Use --company <slug> to specify which one.`);
66
+ }
67
+ export async function getCompanyUid(token, companySlug) {
68
+ if (companySlug) {
69
+ return resolveCompanyUid(token, companySlug);
70
+ }
71
+ return resolveCompanyFromMemberships(token);
72
+ }
73
+ function parseDuration(input) {
74
+ const match = input.match(/^(\d+)(m|h|d)$/);
75
+ if (!match)
76
+ return null;
77
+ const value = parseInt(match[1], 10);
78
+ const unit = match[2];
79
+ if (unit === "m")
80
+ return value * 60 * 1000;
81
+ if (unit === "h")
82
+ return value * 60 * 60 * 1000;
83
+ if (unit === "d")
84
+ return value * 24 * 60 * 60 * 1000;
85
+ return null;
86
+ }
87
+ function confirmPrompt(message) {
88
+ const rl = readline.createInterface({
89
+ input: process.stdin,
90
+ output: process.stdout,
91
+ });
92
+ return new Promise((resolve) => {
93
+ rl.question(`${message} [y/N] `, (answer) => {
94
+ rl.close();
95
+ resolve(answer.trim().toLowerCase() === "y");
96
+ });
97
+ });
98
+ }
99
+ function readFromPipedStdin() {
100
+ return new Promise((resolve, reject) => {
101
+ let data = "";
102
+ process.stdin.setEncoding("utf8");
103
+ process.stdin.on("data", (chunk) => {
104
+ data += chunk;
105
+ });
106
+ process.stdin.on("end", () => resolve(data.replace(/\n$/, "")));
107
+ process.stdin.on("error", reject);
108
+ });
109
+ }
110
+ function promptSecretInteractively() {
111
+ return new Promise((resolve, reject) => {
112
+ process.stdout.write("Enter secret value: ");
113
+ if (process.stdin.isTTY) {
114
+ process.stdin.setRawMode(true);
115
+ }
116
+ let value = "";
117
+ process.stdin.setEncoding("utf8");
118
+ const cleanup = () => {
119
+ if (process.stdin.isTTY) {
120
+ process.stdin.setRawMode(false);
121
+ }
122
+ process.stdin.removeListener("data", onData);
123
+ process.stdin.removeListener("end", onEnd);
124
+ process.stdin.pause();
125
+ };
126
+ const onEnd = () => {
127
+ cleanup();
128
+ process.stdout.write("\n");
129
+ resolve(value);
130
+ };
131
+ const onData = (ch) => {
132
+ for (const c of ch) {
133
+ const code = c.codePointAt(0);
134
+ if (c === "\n" || c === "\r" || code === 4) {
135
+ cleanup();
136
+ process.stdout.write("\n");
137
+ resolve(value);
138
+ return;
139
+ }
140
+ else if (code === 3) {
141
+ cleanup();
142
+ reject(new Error("Aborted"));
143
+ return;
144
+ }
145
+ else if (code === 0x1b) {
146
+ return;
147
+ }
148
+ else if (code === 127) {
149
+ if (value.length > 0) {
150
+ value = value.slice(0, -1);
151
+ }
152
+ }
153
+ else if (code >= 0x20) {
154
+ value += c;
155
+ }
156
+ }
157
+ };
158
+ process.stdin.on("data", onData);
159
+ process.stdin.on("end", onEnd);
160
+ process.stdin.resume();
161
+ });
162
+ }
163
+ export function registerSecretsCommand(program) {
164
+ const secrets = program
165
+ .command("secrets")
166
+ .description("Manage secrets in HQ vault (SSM Parameter Store)")
167
+ .option("--company <slug>", "Company slug (resolves to companyUid)");
168
+ secrets
169
+ .command("set <name>")
170
+ .description("Create or update a secret")
171
+ .option("--from-stdin", "Read secret value from piped stdin")
172
+ .action(async (name, opts) => {
173
+ try {
174
+ if (!SECRET_NAME_PATTERN.test(name)) {
175
+ console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
176
+ process.exit(1);
177
+ }
178
+ let value;
179
+ if (opts.fromStdin) {
180
+ if (process.stdin.isTTY) {
181
+ console.error(chalk.red("Error: --from-stdin requires piped input (e.g. echo 'val' | hq secrets set NAME --from-stdin)"));
182
+ process.exit(1);
183
+ }
184
+ value = await readFromPipedStdin();
185
+ }
186
+ else {
187
+ if (!process.stdin.isTTY) {
188
+ console.error(chalk.red("Error: stdin is not a terminal. Use --from-stdin for piped input."));
189
+ process.exit(1);
190
+ }
191
+ value = await promptSecretInteractively();
192
+ }
193
+ if (!value) {
194
+ console.error(chalk.red("Error: secret value cannot be empty."));
195
+ process.exit(1);
196
+ }
197
+ if (Buffer.byteLength(value, "utf8") > 4096) {
198
+ console.error(chalk.red(`Secret value exceeds 4096-byte SSM limit (got ${Buffer.byteLength(value, "utf8")} bytes).`));
199
+ process.exit(1);
200
+ }
201
+ const token = await ensureCognitoToken();
202
+ const companySlug = secrets.opts().company;
203
+ const companyUid = await getCompanyUid(token, companySlug);
204
+ const res = await vaultApiFetch({
205
+ token,
206
+ path: `/secrets/${encodeURIComponent(companyUid)}`,
207
+ method: "POST",
208
+ body: { name, value },
209
+ });
210
+ if (!res.ok) {
211
+ const body = await res.json().catch(() => ({}));
212
+ console.error(chalk.red(`Failed to set secret: ${body.error ?? res.statusText}`));
213
+ process.exit(1);
214
+ }
215
+ removeCacheEntry(companyUid, name);
216
+ console.log(chalk.green(`Secret '${name}' saved.`));
217
+ }
218
+ catch (err) {
219
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
220
+ process.exit(1);
221
+ }
222
+ });
223
+ secrets
224
+ .command("get <name>")
225
+ .description("Get a secret's metadata (use --reveal for value)")
226
+ .option("--reveal", "Include the decrypted secret value")
227
+ .action(async (name, opts) => {
228
+ try {
229
+ const token = await ensureCognitoToken();
230
+ const companySlug = secrets.opts().company;
231
+ const companyUid = await getCompanyUid(token, companySlug);
232
+ const query = {};
233
+ if (opts.reveal) {
234
+ query.reveal = "true";
235
+ }
236
+ const res = await vaultApiFetch({
237
+ token,
238
+ path: buildSecretNamePath(companyUid, name),
239
+ query: Object.keys(query).length > 0 ? query : undefined,
240
+ });
241
+ if (!res.ok) {
242
+ const body = await res.json().catch(() => ({}));
243
+ console.error(chalk.red(`Failed to get secret: ${body.error ?? res.statusText}`));
244
+ process.exit(1);
245
+ }
246
+ const data = (await res.json());
247
+ const s = data.secret;
248
+ console.log(chalk.bold(`Secret: ${s.name}`));
249
+ if (s.lastModifiedDate) {
250
+ console.log(` Last Modified: ${s.lastModifiedDate}`);
251
+ }
252
+ if (s.version != null) {
253
+ console.log(` Version: ${s.version}`);
254
+ }
255
+ if (opts.reveal && s.value != null) {
256
+ console.log(` Value: ${s.value}`);
257
+ }
258
+ else {
259
+ console.log(` Value: ${chalk.dim("[REDACTED]")}`);
260
+ }
261
+ }
262
+ catch (err) {
263
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
264
+ process.exit(1);
265
+ }
266
+ });
267
+ secrets
268
+ .command("list")
269
+ .description("List all secrets for the company (including nested path-based names)")
270
+ .option("--prefix <path>", "Filter by path prefix (e.g. DEV or DEV/SUB); omit or pass empty string for all secrets")
271
+ .action(async (opts) => {
272
+ try {
273
+ // MUST match SECRET_PREFIX_PATTERN in hq-pro/src/vault-service/handlers/secrets.ts
274
+ let normalizedPrefix;
275
+ if (opts.prefix) {
276
+ const normalized = opts.prefix.replace(/^\/+|\/+$/g, "");
277
+ if (normalized.length === 0 ||
278
+ !SECRET_NAME_PATTERN.test(normalized)) {
279
+ console.error(chalk.red(`Invalid prefix '${opts.prefix}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. DEV or DEV/SUB)`));
280
+ process.exit(1);
281
+ }
282
+ normalizedPrefix = normalized;
283
+ }
284
+ const token = await ensureCognitoToken();
285
+ const companySlug = secrets.opts().company;
286
+ const companyUid = await getCompanyUid(token, companySlug);
287
+ const query = {};
288
+ if (normalizedPrefix) {
289
+ query.prefix = normalizedPrefix;
290
+ }
291
+ const res = await vaultApiFetch({
292
+ token,
293
+ path: `/secrets/${encodeURIComponent(companyUid)}`,
294
+ query: Object.keys(query).length > 0 ? query : undefined,
295
+ });
296
+ if (!res.ok) {
297
+ const body = await res.json().catch(() => ({}));
298
+ console.error(chalk.red(`Failed to list secrets: ${body.error ?? res.statusText}`));
299
+ process.exit(1);
300
+ }
301
+ const data = (await res.json());
302
+ if (data.secrets.length === 0) {
303
+ console.log(chalk.dim("No secrets found."));
304
+ return;
305
+ }
306
+ const nameWidth = Math.max(4, ...data.secrets.map((s) => s.name.length));
307
+ const hasPermission = data.secrets.some((s) => s.permission !== undefined);
308
+ if (hasPermission) {
309
+ const accessWidth = Math.max(6, ...data.secrets.map((s) => (s.permission ?? "-").length));
310
+ const header = `${"NAME".padEnd(nameWidth)} ${"ACCESS".padEnd(accessWidth)} LAST MODIFIED`;
311
+ console.log(chalk.bold(header));
312
+ for (const s of data.secrets) {
313
+ const access = s.permission ?? "-";
314
+ const modified = s.lastModifiedDate ?? "-";
315
+ console.log(`${s.name.padEnd(nameWidth)} ${access.padEnd(accessWidth)} ${modified}`);
316
+ }
317
+ }
318
+ else {
319
+ const header = `${"NAME".padEnd(nameWidth)} LAST MODIFIED`;
320
+ console.log(chalk.bold(header));
321
+ for (const s of data.secrets) {
322
+ const modified = s.lastModifiedDate ?? "-";
323
+ console.log(`${s.name.padEnd(nameWidth)} ${modified}`);
324
+ }
325
+ }
326
+ }
327
+ catch (err) {
328
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
329
+ process.exit(1);
330
+ }
331
+ });
332
+ secrets
333
+ .command("delete <name>")
334
+ .description("Delete a secret")
335
+ .option("--force", "Skip confirmation prompt")
336
+ .action(async (name, opts) => {
337
+ try {
338
+ if (!SECRET_NAME_PATTERN.test(name)) {
339
+ console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
340
+ process.exit(1);
341
+ }
342
+ if (!opts.force) {
343
+ const confirmed = await confirmPrompt(`Delete secret '${name}'? This cannot be undone.`);
344
+ if (!confirmed) {
345
+ console.log(chalk.dim("Aborted."));
346
+ return;
347
+ }
348
+ }
349
+ const token = await ensureCognitoToken();
350
+ const companySlug = secrets.opts().company;
351
+ const companyUid = await getCompanyUid(token, companySlug);
352
+ const res = await vaultApiFetch({
353
+ token,
354
+ path: buildSecretNamePath(companyUid, name),
355
+ method: "DELETE",
356
+ });
357
+ if (!res.ok) {
358
+ const body = await res.json().catch(() => ({}));
359
+ console.error(chalk.red(`Failed to delete secret: ${body.error ?? res.statusText}`));
360
+ process.exit(1);
361
+ }
362
+ removeCacheEntry(companyUid, name);
363
+ console.log(chalk.green(`Secret '${name}' deleted.`));
364
+ }
365
+ catch (err) {
366
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
367
+ process.exit(1);
368
+ }
369
+ });
370
+ secrets
371
+ .command("exec")
372
+ .description("Run a command with secrets injected as env vars")
373
+ .requiredOption("--only <keys>", "Comma-separated list of secret names to inject (required)")
374
+ .allowUnknownOption(true)
375
+ .action(async (_opts, cmd) => {
376
+ try {
377
+ const rawArgs = cmd.args;
378
+ const dashIndex = process.argv.indexOf("--");
379
+ let childArgs;
380
+ if (dashIndex !== -1) {
381
+ childArgs = process.argv.slice(dashIndex + 1);
382
+ }
383
+ else {
384
+ childArgs = rawArgs;
385
+ }
386
+ if (childArgs.length === 0) {
387
+ console.error(chalk.red("Error: no command specified. Usage: hq secrets exec --only KEY1,KEY2 -- <command>"));
388
+ process.exit(1);
389
+ }
390
+ const keys = _opts.only.split(",").map((k) => k.trim()).filter(Boolean);
391
+ if (keys.length === 0) {
392
+ console.error(chalk.red("Error: --only requires at least one secret name."));
393
+ process.exit(1);
394
+ }
395
+ for (const key of keys) {
396
+ if (!SECRET_NAME_PATTERN.test(key)) {
397
+ console.error(chalk.red(`Invalid secret name '${key}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$`));
398
+ process.exit(1);
399
+ }
400
+ }
401
+ const token = await ensureCognitoToken();
402
+ const companySlug = secrets.opts().company;
403
+ const companyUid = await getCompanyUid(token, companySlug);
404
+ const revealed = await Promise.all(keys.map(async (key) => {
405
+ const cached = readCache(companyUid, key);
406
+ if (cached !== null) {
407
+ return { key, value: cached };
408
+ }
409
+ const res = await vaultApiFetch({
410
+ token,
411
+ path: buildSecretNamePath(companyUid, key),
412
+ query: { reveal: "true" },
413
+ });
414
+ if (!res.ok) {
415
+ const body = await res.json().catch(() => ({}));
416
+ throw new Error(`Failed to fetch secret '${key}': ${body.error ?? res.statusText}`);
417
+ }
418
+ const data = (await res.json());
419
+ if (data.secret.value == null) {
420
+ throw new Error(`Secret '${key}' has no value (reveal may not be permitted).`);
421
+ }
422
+ writeCache(companyUid, key, data.secret.value);
423
+ return { key, value: data.secret.value };
424
+ }));
425
+ const secretEnv = {};
426
+ for (const { key, value } of revealed) {
427
+ secretEnv[key] = value;
428
+ }
429
+ const [childCmd, ...childCmdArgs] = childArgs;
430
+ const child = spawn(childCmd, childCmdArgs, {
431
+ stdio: "inherit",
432
+ env: { ...process.env, ...secretEnv },
433
+ });
434
+ child.on("error", (err) => {
435
+ console.error(chalk.red(`Failed to start command '${childCmd}': ${err.message}`));
436
+ process.exit(1);
437
+ });
438
+ child.on("close", (code, signal) => {
439
+ if (signal) {
440
+ process.kill(process.pid, signal);
441
+ }
442
+ process.exit(code ?? 1);
443
+ });
444
+ }
445
+ catch (err) {
446
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
447
+ process.exit(1);
448
+ }
449
+ });
450
+ secrets
451
+ .command("env")
452
+ .description("Print 'export KEY=VALUE' lines suitable for: source <(hq secrets env --only K1,K2)")
453
+ .requiredOption("--only <keys>", "Comma-separated list of secret names to print (required)")
454
+ .action(async (opts) => {
455
+ try {
456
+ const redact = process.stdout.isTTY;
457
+ if (redact) {
458
+ console.error(chalk.yellow("stdout is a terminal — values redacted. Use: source <(hq secrets env --only KEY1,KEY2)"));
459
+ }
460
+ const keys = opts.only.split(",").map((k) => k.trim()).filter(Boolean);
461
+ if (keys.length === 0) {
462
+ console.error(chalk.red("Error: --only requires at least one secret name."));
463
+ process.exit(1);
464
+ }
465
+ for (const key of keys) {
466
+ if (!SECRET_NAME_PATTERN.test(key)) {
467
+ console.error(chalk.red(`Invalid secret name '${key}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$`));
468
+ process.exit(1);
469
+ }
470
+ }
471
+ const token = await ensureCognitoToken();
472
+ const companySlug = secrets.opts().company;
473
+ const companyUid = await getCompanyUid(token, companySlug);
474
+ const revealed = await Promise.all(keys.map(async (key) => {
475
+ const cached = readCache(companyUid, key);
476
+ if (cached !== null) {
477
+ return { key, value: cached };
478
+ }
479
+ const res = await vaultApiFetch({
480
+ token,
481
+ path: buildSecretNamePath(companyUid, key),
482
+ query: { reveal: "true" },
483
+ });
484
+ if (!res.ok) {
485
+ const body = await res.json().catch(() => ({}));
486
+ throw new Error(`Failed to fetch secret '${key}': ${body.error ?? res.statusText}`);
487
+ }
488
+ const data = (await res.json());
489
+ if (data.secret.value == null) {
490
+ throw new Error(`Secret '${key}' has no value (reveal may not be permitted).`);
491
+ }
492
+ writeCache(companyUid, key, data.secret.value);
493
+ return { key, value: data.secret.value };
494
+ }));
495
+ for (const { key, value } of revealed) {
496
+ const out = redact ? "[REDACTED]" : value;
497
+ process.stdout.write(`export ${key}=${shellSingleQuote(out)}\n`);
498
+ }
499
+ }
500
+ catch (err) {
501
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
502
+ process.exit(1);
503
+ }
504
+ });
505
+ secrets
506
+ .command("generate-link <name>")
507
+ .description("Generate a one-time link for someone to submit a secret value")
508
+ .option("--expires <duration>", "Token expiry duration (e.g. 24h, 2d, 30m)", "24h")
509
+ .action(async (name, opts) => {
510
+ try {
511
+ if (!SECRET_NAME_PATTERN.test(name)) {
512
+ console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
513
+ process.exit(1);
514
+ }
515
+ const expiresInMs = parseDuration(opts.expires);
516
+ if (expiresInMs === null) {
517
+ console.error(chalk.red(`Invalid duration '${opts.expires}'. Use formats like 30m, 12h, 2d.`));
518
+ process.exit(1);
519
+ }
520
+ const MAX_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
521
+ if (expiresInMs > MAX_EXPIRY_MS) {
522
+ console.error(chalk.red("Maximum expiry is 7 days (7d)."));
523
+ process.exit(1);
524
+ }
525
+ const token = await ensureCognitoToken();
526
+ const companySlug = secrets.opts().company;
527
+ const companyUid = await getCompanyUid(token, companySlug);
528
+ const res = await vaultApiFetch({
529
+ token,
530
+ path: buildSecretNamePath(companyUid, name),
531
+ method: "POST",
532
+ body: { expiresInMs },
533
+ query: { action: "generate-token" },
534
+ });
535
+ if (!res.ok) {
536
+ const body = await res.json().catch(() => ({}));
537
+ console.error(chalk.red(`Failed to generate link: ${body.error ?? res.statusText}`));
538
+ process.exit(1);
539
+ }
540
+ const data = (await res.json());
541
+ console.log(chalk.green("Secret input link generated:"));
542
+ console.log(`\n ${data.url}\n`);
543
+ console.log(chalk.dim(` Secret: ${data.secretName}`));
544
+ console.log(chalk.dim(` Expires: ${data.expiresAt}`));
545
+ console.log(chalk.dim(" One-time use — link is invalidated after first submission."));
546
+ }
547
+ catch (err) {
548
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
549
+ process.exit(1);
550
+ }
551
+ });
552
+ secrets
553
+ .command("share <path>")
554
+ .description("Share a secret with a person or group")
555
+ .requiredOption("--with <principal>", "Email address or group id to share with")
556
+ .requiredOption("--permission <level>", "Permission level: read | write | admin")
557
+ .action(async (path, opts) => {
558
+ try {
559
+ if (!SECRET_NAME_PATTERN.test(path)) {
560
+ console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
561
+ process.exit(1);
562
+ }
563
+ if (!["read", "write", "admin"].includes(opts.permission)) {
564
+ console.error(chalk.red(`Invalid permission '${opts.permission}': must be one of read, write, admin`));
565
+ process.exit(1);
566
+ }
567
+ const isEmail = /^[^\s]+@[^\s]+$/.test(opts.with);
568
+ if (!isEmail && !GROUP_ID_PATTERN.test(opts.with)) {
569
+ console.error(chalk.red(`Invalid principal '${opts.with}': must be an email address or a group id matching grp_<alphanumeric>`));
570
+ process.exit(1);
571
+ }
572
+ const granteeType = isEmail ? "email" : "group";
573
+ const granteeId = opts.with;
574
+ const token = await ensureCognitoToken();
575
+ const companySlug = secrets.opts().company;
576
+ const companyUid = await getCompanyUid(token, companySlug);
577
+ const res = await vaultApiFetch({
578
+ token,
579
+ path: `/secrets/${encodeURIComponent(companyUid)}/acl/grant`,
580
+ method: "POST",
581
+ body: { path, granteeType, granteeId, permission: opts.permission },
582
+ });
583
+ if (!res.ok) {
584
+ const body = await res.json().catch(() => ({}));
585
+ if (res.status === 401) {
586
+ console.error(chalk.red("Not authenticated — please run `hq login`"));
587
+ }
588
+ else if (res.status === 403) {
589
+ console.error(chalk.red("Not authorized to share this secret"));
590
+ }
591
+ else if (res.status === 404) {
592
+ console.error(chalk.red("ACL record not found for this path — has the secret been created?"));
593
+ }
594
+ else if (res.status === 409) {
595
+ console.error(chalk.red("Concurrent modification — please retry"));
596
+ }
597
+ else if (res.status >= 500) {
598
+ console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
599
+ }
600
+ else {
601
+ console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
602
+ }
603
+ process.exit(1);
604
+ }
605
+ console.log(chalk.green(`Shared '${path}' with ${opts.with} (${opts.permission})`));
606
+ }
607
+ catch (err) {
608
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
609
+ process.exit(1);
610
+ }
611
+ });
612
+ secrets
613
+ .command("unshare <path>")
614
+ .description("Remove a grant from a secret")
615
+ .requiredOption("--from <principal>", "Email address or group id to remove")
616
+ .action(async (path, opts) => {
617
+ try {
618
+ if (!SECRET_NAME_PATTERN.test(path)) {
619
+ console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
620
+ process.exit(1);
621
+ }
622
+ const isEmailFrom = /^[^\s]+@[^\s]+$/.test(opts.from);
623
+ if (!isEmailFrom && !GROUP_ID_PATTERN.test(opts.from)) {
624
+ console.error(chalk.red(`Invalid principal '${opts.from}': must be an email address or a group id matching grp_<alphanumeric>`));
625
+ process.exit(1);
626
+ }
627
+ const granteeType = isEmailFrom ? "email" : "group";
628
+ const granteeId = opts.from;
629
+ const token = await ensureCognitoToken();
630
+ const companySlug = secrets.opts().company;
631
+ const companyUid = await getCompanyUid(token, companySlug);
632
+ const res = await vaultApiFetch({
633
+ token,
634
+ path: `/secrets/${encodeURIComponent(companyUid)}/acl/revoke`,
635
+ method: "POST",
636
+ body: { path, granteeType, granteeId },
637
+ });
638
+ if (!res.ok) {
639
+ const body = await res.json().catch(() => ({}));
640
+ if (res.status === 401) {
641
+ console.error(chalk.red("Not authenticated — please run `hq login`"));
642
+ }
643
+ else if (res.status === 403) {
644
+ console.error(chalk.red("Not authorized to modify this secret's ACL"));
645
+ }
646
+ else if (res.status === 404) {
647
+ console.log(chalk.green(`Grant already absent for '${path}' / ${opts.from}`));
648
+ return;
649
+ }
650
+ else if (res.status >= 500) {
651
+ console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
652
+ }
653
+ else {
654
+ console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
655
+ }
656
+ process.exit(1);
657
+ }
658
+ console.log(chalk.green(`Removed grant for ${opts.from} on '${path}'`));
659
+ }
660
+ catch (err) {
661
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
662
+ process.exit(1);
663
+ }
664
+ });
665
+ secrets
666
+ .command("acl <path>")
667
+ .description("Show the ACL (access control list) for a secret path")
668
+ .action(async (path) => {
669
+ try {
670
+ if (!SECRET_NAME_PATTERN.test(path)) {
671
+ console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
672
+ process.exit(1);
673
+ }
674
+ const token = await ensureCognitoToken();
675
+ const companySlug = secrets.opts().company;
676
+ const companyUid = await getCompanyUid(token, companySlug);
677
+ const secretPath = path;
678
+ const res = await vaultApiFetch({
679
+ token,
680
+ path: `/secrets/${encodeURIComponent(companyUid)}/acl`,
681
+ query: { path: secretPath },
682
+ });
683
+ if (!res.ok) {
684
+ const body = await res.json().catch(() => ({}));
685
+ if (res.status === 401) {
686
+ console.error(chalk.red("Not authenticated — please run `hq login`"));
687
+ }
688
+ else if (res.status === 403) {
689
+ console.error(chalk.red("Not authorized to view this secret's ACL"));
690
+ }
691
+ else if (res.status === 404) {
692
+ console.error(chalk.red(`No ACL record exists for '${path}'`));
693
+ }
694
+ else if (res.status >= 500) {
695
+ console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
696
+ }
697
+ else {
698
+ console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
699
+ }
700
+ process.exit(1);
701
+ }
702
+ const data = (await res.json());
703
+ const acl = data.acl;
704
+ const aclStatus = acl.open ? "open" : "restricted";
705
+ console.log(chalk.green(`ACL for ${acl.path} (${aclStatus})`));
706
+ console.log(`Creator: ${acl.creatorUid}`);
707
+ if (acl.entries.length === 0) {
708
+ if (acl.open) {
709
+ console.log(chalk.gray("Open ACL — all active members have read access."));
710
+ }
711
+ else {
712
+ console.log(chalk.gray("No explicit grants — only creator has access."));
713
+ }
714
+ return;
715
+ }
716
+ console.log("Entries:");
717
+ const TYPE_W = Math.max(4, ...acl.entries.map((e) => e.granteeType.length));
718
+ const GRANTEE_W = Math.max(7, ...acl.entries.map((e) => e.granteeId.length));
719
+ const PERM_W = Math.max(10, ...acl.entries.map((e) => e.permission.length));
720
+ const BY_W = Math.max(10, ...acl.entries.map((e) => e.grantedBy.length));
721
+ const tableHeader = [
722
+ "TYPE".padEnd(TYPE_W),
723
+ "GRANTEE".padEnd(GRANTEE_W),
724
+ "PERMISSION".padEnd(PERM_W),
725
+ "GRANTED_BY".padEnd(BY_W),
726
+ "GRANTED_AT",
727
+ ].join(" ");
728
+ console.log(chalk.bold(tableHeader));
729
+ for (const e of acl.entries) {
730
+ const grantedAt = e.grantedAt.slice(0, 10);
731
+ console.log([
732
+ e.granteeType.padEnd(TYPE_W),
733
+ e.granteeId.padEnd(GRANTEE_W),
734
+ e.permission.padEnd(PERM_W),
735
+ e.grantedBy.padEnd(BY_W),
736
+ grantedAt,
737
+ ].join(" "));
738
+ }
739
+ }
740
+ catch (err) {
741
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
742
+ process.exit(1);
743
+ }
744
+ });
745
+ const cache = secrets
746
+ .command("cache")
747
+ .description("Manage the local secrets cache");
748
+ cache
749
+ .command("clear")
750
+ .description("Clear all cached secrets")
751
+ .action(async () => {
752
+ clearAllCache();
753
+ console.log(chalk.green("Secrets cache cleared."));
754
+ });
755
+ }
756
+ //# sourceMappingURL=secrets.js.map