@indigoai-us/hq-cli 5.77.4 → 5.77.5
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 +15 -0
- package/dist/commands/meetings.js +37 -0
- package/dist/commands/secrets.js +25 -19
- package/package.json +1 -1
- package/src/commands/meetings.test.ts +56 -0
- package/src/commands/meetings.ts +44 -0
- package/src/commands/secrets.test.ts +55 -6
- package/src/commands/secrets.ts +30 -31
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.77.5]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Cross-platform meeting-bot invites.** Run
|
|
10
|
+
`hq meetings invite <meeting-url>` to schedule the meeting bot from any
|
|
11
|
+
terminal. Add `--company <slug-or-uid>` to route the transcript to a company
|
|
12
|
+
vault; omitting it keeps the invite in your personal vault. (#252)
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- High-security secret refusals now clearly direct callers to `hq secrets
|
|
17
|
+
sandbox` or the HQ secret proxy, including sandbox-only denials and
|
|
18
|
+
`--script` attempts. (#253)
|
|
19
|
+
|
|
5
20
|
## [5.77.4]
|
|
6
21
|
|
|
7
22
|
### Fixed
|
|
@@ -208,6 +208,43 @@ export function registerMeetingsCommand(program) {
|
|
|
208
208
|
process.exit(1);
|
|
209
209
|
}
|
|
210
210
|
});
|
|
211
|
+
// ── hq meetings invite <meeting-url> ──────────────────────────────
|
|
212
|
+
meetings
|
|
213
|
+
.command("invite <meetingUrl>")
|
|
214
|
+
.description("Invite the meeting bot to a Google Meet, Zoom, or Teams URL")
|
|
215
|
+
.action(async (meetingUrl) => {
|
|
216
|
+
try {
|
|
217
|
+
const token = await ensureCognitoToken();
|
|
218
|
+
const companySlug = meetings.opts().company;
|
|
219
|
+
const query = {};
|
|
220
|
+
if (companySlug)
|
|
221
|
+
query.companyId = await getCompanyUid(token, companySlug);
|
|
222
|
+
const res = await vaultApiFetch({
|
|
223
|
+
token,
|
|
224
|
+
method: "POST",
|
|
225
|
+
path: "/v1/bot/invite",
|
|
226
|
+
query,
|
|
227
|
+
body: { meetingUrl },
|
|
228
|
+
});
|
|
229
|
+
if (!res.ok)
|
|
230
|
+
await handleApiError(res);
|
|
231
|
+
const data = (await res.json());
|
|
232
|
+
if (meetings.opts().json) {
|
|
233
|
+
console.log(JSON.stringify(data, null, 2));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
console.log(chalk.green(`\n✓ Meeting bot invited to ${chalk.cyan(data.meetingUrl ?? meetingUrl)}.`));
|
|
237
|
+
if (data.botId)
|
|
238
|
+
console.log(chalk.dim(` Bot: ${data.botId}`));
|
|
239
|
+
if (data.status)
|
|
240
|
+
console.log(chalk.dim(` Status: ${data.status}`));
|
|
241
|
+
console.log();
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
211
248
|
// ── hq meetings get <id> ──────────────────────────────────────────
|
|
212
249
|
meetings
|
|
213
250
|
.command("get <meetingId>")
|
package/dist/commands/secrets.js
CHANGED
|
@@ -131,6 +131,12 @@ function promptSecretInteractively() {
|
|
|
131
131
|
// server (the legacy per-key GET path had no such cap).
|
|
132
132
|
const MAX_BATCH_NAMES = 100;
|
|
133
133
|
const SECRET_LOAD_TIMEOUT_MS = 30_000;
|
|
134
|
+
function highSecuritySandboxOnlyMessage(secretName) {
|
|
135
|
+
const sandboxExample = secretName
|
|
136
|
+
? `hq secrets sandbox --only ${secretName} -- <command>`
|
|
137
|
+
: "hq secrets sandbox --only <secret> -- <command>";
|
|
138
|
+
return `High-security secrets can run only in the sandbox (use \`${sandboxExample}\`) or through the HQ secret proxy. Local injection and script locks (\`--script\`) are not supported.`;
|
|
139
|
+
}
|
|
134
140
|
function parseSecretAclPrincipal(principal) {
|
|
135
141
|
const p = principal.trim();
|
|
136
142
|
if (p === "@all") {
|
|
@@ -435,10 +441,13 @@ export async function loadRevealedSecrets(token, companyUid, keys, usage) {
|
|
|
435
441
|
if (!res.ok) {
|
|
436
442
|
const body = (await res.json().catch(() => ({})));
|
|
437
443
|
const message = extractApiMessage(body, res.statusText);
|
|
438
|
-
// High-security
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
444
|
+
// High-security refusal surfaced at the batch level (rather than
|
|
445
|
+
// per-name): point the caller at the sandbox/proxy and never leak
|
|
446
|
+
// plaintext. A local script attestation is not an override.
|
|
447
|
+
if (body.code === "high_security_denied" ||
|
|
448
|
+
body.code === "high_security_sandbox_only" ||
|
|
449
|
+
body.highSecurity === true) {
|
|
450
|
+
throw new Error(highSecuritySandboxOnlyMessage());
|
|
442
451
|
}
|
|
443
452
|
if (res.status >= 400 &&
|
|
444
453
|
res.status < 500 &&
|
|
@@ -508,14 +517,13 @@ export async function loadRevealedSecrets(token, companyUid, keys, usage) {
|
|
|
508
517
|
continue;
|
|
509
518
|
removeCacheEntry(companyUid, key);
|
|
510
519
|
const err = errorsByName.get(key);
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
firstFailure ??= new Error(`Secret '${key}' is high-security and cannot be injected locally — it can only be used through the HQ secret proxy, which keeps the plaintext server-side.`);
|
|
520
|
+
// Every caller of loadRevealedSecrets injects or prints the plaintext
|
|
521
|
+
// locally (`secrets get --reveal`, `secrets exec`, `secrets env`). Both
|
|
522
|
+
// high-security denial shapes therefore receive the same explicit
|
|
523
|
+
// sandbox/proxy guidance, even if the caller supplied `--script`.
|
|
524
|
+
if (err?.code === "high_security_denied" ||
|
|
525
|
+
err?.code === "high_security_sandbox_only") {
|
|
526
|
+
firstFailure ??= new Error(highSecuritySandboxOnlyMessage(key));
|
|
519
527
|
continue;
|
|
520
528
|
}
|
|
521
529
|
const reason = err?.code === "not_found"
|
|
@@ -655,7 +663,7 @@ export function registerSecretsCommand(program) {
|
|
|
655
663
|
removeCacheEntry(companyUid, name);
|
|
656
664
|
console.log(chalk.green(formatSecretSaved(name, scopeLabel)));
|
|
657
665
|
if (opts.highSecurity) {
|
|
658
|
-
console.log(chalk.dim(` High-security: destination pinned to ${destinations?.[0]}. This value can
|
|
666
|
+
console.log(chalk.dim(` High-security: destination pinned to ${destinations?.[0]}. This value can run only in the sandbox (hq secrets sandbox) or through the HQ secret proxy; local injection and script locks are unsupported.`));
|
|
659
667
|
}
|
|
660
668
|
}
|
|
661
669
|
catch (err) {
|
|
@@ -677,13 +685,11 @@ export function registerSecretsCommand(program) {
|
|
|
677
685
|
});
|
|
678
686
|
if (!res.ok) {
|
|
679
687
|
const body = (await res.json().catch(() => ({})));
|
|
680
|
-
// High-security
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
// 4xx — the value can ONLY be used through the server-side proxy.
|
|
688
|
+
// High-security secrets cannot be revealed locally. Surface the
|
|
689
|
+
// hosted execution path rather than a raw 4xx; `--script` never
|
|
690
|
+
// overrides this boundary.
|
|
684
691
|
if (res.status === 403 && body.highSecurity === true) {
|
|
685
|
-
console.error(chalk.red(
|
|
686
|
-
console.error(chalk.dim(" It can only be used through the HQ secret proxy, which keeps the plaintext server-side."));
|
|
692
|
+
console.error(chalk.red(highSecuritySandboxOnlyMessage(name)));
|
|
687
693
|
process.exit(1);
|
|
688
694
|
}
|
|
689
695
|
console.error(chalk.red(`Failed to get secret: ${extractApiMessage(body, res.statusText)}`));
|
package/package.json
CHANGED
|
@@ -32,6 +32,7 @@ vi.mock("../utils/vault-api.js", async (importOriginal) => {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
import { registerMeetingsCommand } from "./meetings.js";
|
|
35
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
35
36
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
36
37
|
|
|
37
38
|
let logSpy: MockInstance<typeof console.log>;
|
|
@@ -145,6 +146,61 @@ describe("meetings get — short id resolution", () => {
|
|
|
145
146
|
});
|
|
146
147
|
});
|
|
147
148
|
|
|
149
|
+
describe("meetings invite", () => {
|
|
150
|
+
it("authenticates and posts the meeting URL with the optional company routing", async () => {
|
|
151
|
+
const meetingUrl = "https://meet.google.com/abc-defg-hij";
|
|
152
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
153
|
+
jsonRes({
|
|
154
|
+
botId: "bot_123",
|
|
155
|
+
meetingUrl,
|
|
156
|
+
platform: "google_meet",
|
|
157
|
+
status: "scheduled",
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const program = buildProgram();
|
|
162
|
+
await program.parseAsync([
|
|
163
|
+
"node",
|
|
164
|
+
"hq",
|
|
165
|
+
"meetings",
|
|
166
|
+
"--company",
|
|
167
|
+
"indigo",
|
|
168
|
+
"invite",
|
|
169
|
+
meetingUrl,
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
expect(ensureCognitoToken).toHaveBeenCalledOnce();
|
|
173
|
+
expect(getCompanyUid).toHaveBeenCalledWith("test-token", "indigo");
|
|
174
|
+
expect(vaultApiFetch).toHaveBeenCalledWith({
|
|
175
|
+
token: "test-token",
|
|
176
|
+
method: "POST",
|
|
177
|
+
path: "/v1/bot/invite",
|
|
178
|
+
query: { companyId: "cmp_indigo" },
|
|
179
|
+
body: { meetingUrl },
|
|
180
|
+
});
|
|
181
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Meeting bot invited"));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("omits company routing so the backend uses the caller's personal vault", async () => {
|
|
185
|
+
const meetingUrl = "https://zoom.us/j/123456789";
|
|
186
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
187
|
+
jsonRes({ botId: "bot_123", meetingUrl, status: "scheduled" }),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const program = buildProgram();
|
|
191
|
+
await program.parseAsync(["node", "hq", "meetings", "invite", meetingUrl]);
|
|
192
|
+
|
|
193
|
+
expect(getCompanyUid).not.toHaveBeenCalled();
|
|
194
|
+
expect(vaultApiFetch).toHaveBeenCalledWith({
|
|
195
|
+
token: "test-token",
|
|
196
|
+
method: "POST",
|
|
197
|
+
path: "/v1/bot/invite",
|
|
198
|
+
query: {},
|
|
199
|
+
body: { meetingUrl },
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
148
204
|
describe("meetings set-company", () => {
|
|
149
205
|
it("POSTs the resolved company id and applies to the recurring series by default", async () => {
|
|
150
206
|
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
package/src/commands/meetings.ts
CHANGED
|
@@ -101,6 +101,13 @@ interface MeetingDocument {
|
|
|
101
101
|
notes: MeetingNotes | null;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
interface MeetingBotInviteResponse {
|
|
105
|
+
botId?: string;
|
|
106
|
+
meetingUrl?: string;
|
|
107
|
+
platform?: string;
|
|
108
|
+
status?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
104
111
|
function formatDuration(seconds: number): string {
|
|
105
112
|
const h = Math.floor(seconds / 3600);
|
|
106
113
|
const m = Math.floor((seconds % 3600) / 60);
|
|
@@ -344,6 +351,43 @@ export function registerMeetingsCommand(program: Command): void {
|
|
|
344
351
|
}
|
|
345
352
|
});
|
|
346
353
|
|
|
354
|
+
// ── hq meetings invite <meeting-url> ──────────────────────────────
|
|
355
|
+
|
|
356
|
+
meetings
|
|
357
|
+
.command("invite <meetingUrl>")
|
|
358
|
+
.description("Invite the meeting bot to a Google Meet, Zoom, or Teams URL")
|
|
359
|
+
.action(async (meetingUrl: string) => {
|
|
360
|
+
try {
|
|
361
|
+
const token = await ensureCognitoToken();
|
|
362
|
+
const companySlug = meetings.opts().company as string | undefined;
|
|
363
|
+
const query: Record<string, string> = {};
|
|
364
|
+
if (companySlug) query.companyId = await getCompanyUid(token, companySlug);
|
|
365
|
+
|
|
366
|
+
const res = await vaultApiFetch({
|
|
367
|
+
token,
|
|
368
|
+
method: "POST",
|
|
369
|
+
path: "/v1/bot/invite",
|
|
370
|
+
query,
|
|
371
|
+
body: { meetingUrl },
|
|
372
|
+
});
|
|
373
|
+
if (!res.ok) await handleApiError(res);
|
|
374
|
+
const data = (await res.json()) as MeetingBotInviteResponse;
|
|
375
|
+
|
|
376
|
+
if (meetings.opts().json) {
|
|
377
|
+
console.log(JSON.stringify(data, null, 2));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log(chalk.green(`\n✓ Meeting bot invited to ${chalk.cyan(data.meetingUrl ?? meetingUrl)}.`));
|
|
382
|
+
if (data.botId) console.log(chalk.dim(` Bot: ${data.botId}`));
|
|
383
|
+
if (data.status) console.log(chalk.dim(` Status: ${data.status}`));
|
|
384
|
+
console.log();
|
|
385
|
+
} catch (err) {
|
|
386
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
347
391
|
// ── hq meetings get <id> ──────────────────────────────────────────
|
|
348
392
|
|
|
349
393
|
meetings
|
|
@@ -138,6 +138,29 @@ describe("secrets sandbox", () => {
|
|
|
138
138
|
expect(channel).toBe("sandbox");
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
it("runs a high-security secret in the sandbox without a local script attestation", async () => {
|
|
142
|
+
const program = buildProgram();
|
|
143
|
+
await program.parseAsync([
|
|
144
|
+
"node",
|
|
145
|
+
"hq",
|
|
146
|
+
"secrets",
|
|
147
|
+
"sandbox",
|
|
148
|
+
"--only",
|
|
149
|
+
"HIGH_SECURITY_KEY",
|
|
150
|
+
"--",
|
|
151
|
+
"env",
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
expect(startJobSpy).toHaveBeenCalledWith("test-token", {
|
|
155
|
+
companyUid: "prs_alice",
|
|
156
|
+
secretNames: ["HIGH_SECURITY_KEY"],
|
|
157
|
+
command: "env",
|
|
158
|
+
});
|
|
159
|
+
expect(vaultApiFetch).not.toHaveBeenCalledWith(
|
|
160
|
+
expect.objectContaining({ path: expect.stringContaining("/load") }),
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
141
164
|
it("parses --company, --only, and joins args after -- into a command", async () => {
|
|
142
165
|
const program = buildProgram();
|
|
143
166
|
await program.parseAsync([
|
|
@@ -534,9 +557,9 @@ describe("secrets exists (HQ-4H HEAD probe)", () => {
|
|
|
534
557
|
|
|
535
558
|
// US-003 (secrets-server-proxy): the CLI surfaces the SERVER's refusal of a
|
|
536
559
|
// high-security ("nuclear") secret on the local-injection path as a clear,
|
|
537
|
-
// actionable error
|
|
538
|
-
//
|
|
539
|
-
//
|
|
560
|
+
// actionable sandbox/proxy error — and never prints the value. The server-side
|
|
561
|
+
// deny is the real control (it returns 403 + highSecurity:true and NO
|
|
562
|
+
// plaintext); these tests assert the CLI's surfacing behavior.
|
|
540
563
|
describe("US-003 — CLI refuses high-security secrets on local injection", () => {
|
|
541
564
|
// The server's 403 refusal shape for a high-security secret.
|
|
542
565
|
function highSecurityDenied(): Response {
|
|
@@ -550,6 +573,20 @@ describe("US-003 — CLI refuses high-security secrets on local injection", () =
|
|
|
550
573
|
);
|
|
551
574
|
}
|
|
552
575
|
|
|
576
|
+
function highSecuritySandboxOnly(): Response {
|
|
577
|
+
return jsonRes({
|
|
578
|
+
secrets: [],
|
|
579
|
+
errors: [
|
|
580
|
+
{
|
|
581
|
+
name: "ANTHROPIC_API_KEY",
|
|
582
|
+
code: "high_security_sandbox_only",
|
|
583
|
+
message:
|
|
584
|
+
"High-security-tier secrets can run only in the sandbox (`hq secrets sandbox`) or through the secret proxy. Local injection and script locks (`--script`) are not supported.",
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
553
590
|
let exitSpy: MockInstance<typeof process.exit>;
|
|
554
591
|
beforeEach(() => {
|
|
555
592
|
exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
|
|
@@ -576,17 +613,22 @@ describe("US-003 — CLI refuses high-security secrets on local injection", () =
|
|
|
576
613
|
const exitCode = exitSpy.mock.calls[0]?.[0] as number | undefined;
|
|
577
614
|
|
|
578
615
|
expect(exitCode).toBe(1);
|
|
579
|
-
// A clear, actionable error
|
|
616
|
+
// A clear, actionable error names the sandbox/proxy boundary and rules
|
|
617
|
+
// out script-lock bypasses.
|
|
580
618
|
const errText = errSpy.mock.calls.flat().join(" ");
|
|
581
619
|
expect(errText).toMatch(/high-security/i);
|
|
620
|
+
expect(errText).toMatch(/hq secrets sandbox/i);
|
|
582
621
|
expect(errText).toMatch(/proxy/i);
|
|
622
|
+
expect(errText).toMatch(/--script/);
|
|
583
623
|
// The value is NEVER printed — no "Value:" line carrying plaintext.
|
|
584
624
|
const logText = logSpy.mock.calls.flat().join(" ");
|
|
585
625
|
expect(logText).not.toMatch(/sk-ant/i);
|
|
586
626
|
});
|
|
587
627
|
|
|
588
|
-
it("E2E: `secrets exec --
|
|
589
|
-
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
628
|
+
it("E2E: `secrets exec --script <path>` is denied — sandbox guidance, command not run", async () => {
|
|
629
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(highSecuritySandboxOnly());
|
|
630
|
+
const scriptPath = join(tempDir, "deploy.sh");
|
|
631
|
+
writeFileSync(scriptPath, "#!/usr/bin/env bash\necho deploy\n");
|
|
590
632
|
|
|
591
633
|
const program = buildProgram();
|
|
592
634
|
try {
|
|
@@ -597,6 +639,10 @@ describe("US-003 — CLI refuses high-security secrets on local injection", () =
|
|
|
597
639
|
"exec",
|
|
598
640
|
"--only",
|
|
599
641
|
"ANTHROPIC_API_KEY",
|
|
642
|
+
"--script",
|
|
643
|
+
scriptPath,
|
|
644
|
+
"--script-id",
|
|
645
|
+
"script.deploy",
|
|
600
646
|
"--",
|
|
601
647
|
"env",
|
|
602
648
|
]);
|
|
@@ -608,7 +654,10 @@ describe("US-003 — CLI refuses high-security secrets on local injection", () =
|
|
|
608
654
|
expect(exitCode).toBe(1);
|
|
609
655
|
const errText = errSpy.mock.calls.flat().join(" ");
|
|
610
656
|
expect(errText).toMatch(/high-security/i);
|
|
657
|
+
expect(errText).toMatch(/hq secrets sandbox/i);
|
|
611
658
|
expect(errText).toMatch(/proxy/i);
|
|
659
|
+
expect(errText).toMatch(/--script/);
|
|
660
|
+
expect(spawn).not.toHaveBeenCalled();
|
|
612
661
|
});
|
|
613
662
|
});
|
|
614
663
|
|
package/src/commands/secrets.ts
CHANGED
|
@@ -212,6 +212,13 @@ export interface SecretLoadResponse {
|
|
|
212
212
|
errors: Array<{ name: string; code: string; message?: string }>;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
function highSecuritySandboxOnlyMessage(secretName?: string): string {
|
|
216
|
+
const sandboxExample = secretName
|
|
217
|
+
? `hq secrets sandbox --only ${secretName} -- <command>`
|
|
218
|
+
: "hq secrets sandbox --only <secret> -- <command>";
|
|
219
|
+
return `High-security secrets can run only in the sandbox (use \`${sandboxExample}\`) or through the HQ secret proxy. Local injection and script locks (\`--script\`) are not supported.`;
|
|
220
|
+
}
|
|
221
|
+
|
|
215
222
|
interface SecretGetResponse {
|
|
216
223
|
secret: {
|
|
217
224
|
name: string;
|
|
@@ -669,12 +676,15 @@ export async function loadRevealedSecrets(
|
|
|
669
676
|
if (!res.ok) {
|
|
670
677
|
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
671
678
|
const message = extractApiMessage(body, res.statusText);
|
|
672
|
-
// High-security
|
|
673
|
-
//
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
679
|
+
// High-security refusal surfaced at the batch level (rather than
|
|
680
|
+
// per-name): point the caller at the sandbox/proxy and never leak
|
|
681
|
+
// plaintext. A local script attestation is not an override.
|
|
682
|
+
if (
|
|
683
|
+
body.code === "high_security_denied" ||
|
|
684
|
+
body.code === "high_security_sandbox_only" ||
|
|
685
|
+
body.highSecurity === true
|
|
686
|
+
) {
|
|
687
|
+
throw new Error(highSecuritySandboxOnlyMessage());
|
|
678
688
|
}
|
|
679
689
|
if (
|
|
680
690
|
res.status >= 400 &&
|
|
@@ -752,16 +762,15 @@ export async function loadRevealedSecrets(
|
|
|
752
762
|
if (resolved.has(key)) continue;
|
|
753
763
|
removeCacheEntry(companyUid, key);
|
|
754
764
|
const err = errorsByName.get(key);
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
);
|
|
765
|
+
// Every caller of loadRevealedSecrets injects or prints the plaintext
|
|
766
|
+
// locally (`secrets get --reveal`, `secrets exec`, `secrets env`). Both
|
|
767
|
+
// high-security denial shapes therefore receive the same explicit
|
|
768
|
+
// sandbox/proxy guidance, even if the caller supplied `--script`.
|
|
769
|
+
if (
|
|
770
|
+
err?.code === "high_security_denied" ||
|
|
771
|
+
err?.code === "high_security_sandbox_only"
|
|
772
|
+
) {
|
|
773
|
+
firstFailure ??= new Error(highSecuritySandboxOnlyMessage(key));
|
|
765
774
|
continue;
|
|
766
775
|
}
|
|
767
776
|
const reason =
|
|
@@ -947,7 +956,7 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
947
956
|
if (opts.highSecurity) {
|
|
948
957
|
console.log(
|
|
949
958
|
chalk.dim(
|
|
950
|
-
` High-security: destination pinned to ${destinations?.[0]}. This value can
|
|
959
|
+
` High-security: destination pinned to ${destinations?.[0]}. This value can run only in the sandbox (hq secrets sandbox) or through the HQ secret proxy; local injection and script locks are unsupported.`,
|
|
951
960
|
),
|
|
952
961
|
);
|
|
953
962
|
}
|
|
@@ -979,21 +988,11 @@ export function registerSecretsCommand(program: Command): void {
|
|
|
979
988
|
|
|
980
989
|
if (!res.ok) {
|
|
981
990
|
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
982
|
-
// High-security
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
// 4xx — the value can ONLY be used through the server-side proxy.
|
|
991
|
+
// High-security secrets cannot be revealed locally. Surface the
|
|
992
|
+
// hosted execution path rather than a raw 4xx; `--script` never
|
|
993
|
+
// overrides this boundary.
|
|
986
994
|
if (res.status === 403 && body.highSecurity === true) {
|
|
987
|
-
console.error(
|
|
988
|
-
chalk.red(
|
|
989
|
-
`Secret '${name}' is high-security and cannot be revealed locally.`,
|
|
990
|
-
),
|
|
991
|
-
);
|
|
992
|
-
console.error(
|
|
993
|
-
chalk.dim(
|
|
994
|
-
" It can only be used through the HQ secret proxy, which keeps the plaintext server-side.",
|
|
995
|
-
),
|
|
996
|
-
);
|
|
995
|
+
console.error(chalk.red(highSecuritySandboxOnlyMessage(name)));
|
|
997
996
|
process.exit(1);
|
|
998
997
|
}
|
|
999
998
|
console.error(
|