@indigoai-us/hq-cli 5.59.0 → 5.61.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.
- package/dist/commands/meetings.js +127 -14
- package/dist/commands/pack-install.d.ts +7 -1
- package/dist/commands/pack-install.js +86 -15
- package/dist/commands/packs.d.ts +2 -1
- package/dist/commands/packs.js +13 -8
- package/dist/commands/secrets.d.ts +5 -0
- package/dist/commands/secrets.js +134 -6
- package/dist/index.d.ts +5 -3
- package/dist/index.js +14 -240
- package/dist/main.d.ts +7 -0
- package/dist/main.js +247 -0
- package/dist/utils/sandbox-runner-client.d.ts +13 -0
- package/dist/utils/sandbox-runner-client.js +82 -5
- package/dist/utils/version-check.d.ts +6 -0
- package/dist/utils/version-check.js +78 -2
- package/package.json +1 -1
- package/src/commands/meetings.test.ts +116 -2
- package/src/commands/meetings.ts +206 -37
- package/src/commands/pack-install.ts +115 -18
- package/src/commands/pack-update-cache.test.ts +149 -0
- package/src/commands/packs.ts +28 -7
- package/src/commands/secrets.test.ts +407 -0
- package/src/commands/secrets.ts +203 -5
- package/src/index.test.ts +32 -0
- package/src/index.ts +11 -274
- package/src/main.ts +283 -0
- package/src/utils/sandbox-runner-client.test.ts +128 -0
- package/src/utils/sandbox-runner-client.ts +99 -3
- package/src/utils/version-check.test.ts +30 -0
- package/src/utils/version-check.ts +72 -0
package/src/commands/packs.ts
CHANGED
|
@@ -144,6 +144,7 @@ export function buildInstalledView(
|
|
|
144
144
|
pack: InstalledPack,
|
|
145
145
|
installedSources: Set<string>,
|
|
146
146
|
checkUpdates: boolean,
|
|
147
|
+
latestProbe?: LatestResult,
|
|
147
148
|
): InstalledPackView {
|
|
148
149
|
if (!pack.manifest) {
|
|
149
150
|
return {
|
|
@@ -178,7 +179,7 @@ export function buildInstalledView(
|
|
|
178
179
|
|
|
179
180
|
let updateAvailable: boolean | null = null;
|
|
180
181
|
if (checkUpdates && m.source) {
|
|
181
|
-
updateAvailable =
|
|
182
|
+
updateAvailable = latestProbe?.updateAvailable ?? null;
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
// US-005 — surface the pack's `initialization` block so the HQ Sync
|
|
@@ -214,15 +215,27 @@ export function buildInstalledView(
|
|
|
214
215
|
};
|
|
215
216
|
}
|
|
216
217
|
|
|
217
|
-
function buildListView(
|
|
218
|
+
async function buildListView(
|
|
219
|
+
hqRoot: string,
|
|
220
|
+
checkUpdates: boolean,
|
|
221
|
+
evalConditionals: boolean,
|
|
222
|
+
refreshUpdates: boolean,
|
|
223
|
+
): Promise<PacksListView> {
|
|
218
224
|
const hqVersion = readHqVersion(hqRoot);
|
|
219
225
|
const packs = listInstalledPacks(hqRoot);
|
|
220
226
|
const catalog = readRecommendedPackages(hqRoot);
|
|
221
227
|
const catalogSources = new Set(catalog.map((c) => c.source));
|
|
222
228
|
const warnings: string[] = [];
|
|
223
229
|
|
|
224
|
-
const
|
|
225
|
-
|
|
230
|
+
const latestProbes = await Promise.all(
|
|
231
|
+
packs.map((p) =>
|
|
232
|
+
checkUpdates && p.manifest?.source
|
|
233
|
+
? resolveLatest(p.manifest.source, p.manifest.version, { forceRefresh: refreshUpdates })
|
|
234
|
+
: Promise.resolve(undefined),
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
const installed = packs.map((p, i) =>
|
|
238
|
+
buildInstalledView(hqRoot, hqVersion, p, catalogSources, checkUpdates, latestProbes[i]),
|
|
226
239
|
);
|
|
227
240
|
for (const p of installed) {
|
|
228
241
|
if (p.error) warnings.push(`${p.name}: ${p.error}`);
|
|
@@ -303,6 +316,7 @@ interface UpdateCheck {
|
|
|
303
316
|
|
|
304
317
|
interface UpdateOpts extends CommonOpts {
|
|
305
318
|
checkOnly?: boolean;
|
|
319
|
+
refresh?: boolean;
|
|
306
320
|
yes?: boolean;
|
|
307
321
|
allowHooks?: boolean;
|
|
308
322
|
allowMcp?: boolean;
|
|
@@ -334,7 +348,7 @@ async function runUpdate(name: string | undefined, opts: UpdateOpts): Promise<Up
|
|
|
334
348
|
const probe: LatestResult =
|
|
335
349
|
safeClassify(source) === 'marketplace'
|
|
336
350
|
? await resolveLatestMarketplace(source, m.version)
|
|
337
|
-
: resolveLatest(source, m.version);
|
|
351
|
+
: await resolveLatest(source, m.version, { forceRefresh: true });
|
|
338
352
|
const base: UpdateCheck = {
|
|
339
353
|
name: pname,
|
|
340
354
|
transport: probe.transport,
|
|
@@ -517,11 +531,17 @@ export function registerPacksCommand(parent: Command): void {
|
|
|
517
531
|
.option('--json', 'Machine-readable JSON output')
|
|
518
532
|
.option('--hq-root <path>', 'HQ root (default: auto-detect)')
|
|
519
533
|
.option('--check-updates', 'Probe each pack for available updates (network I/O)')
|
|
534
|
+
.option('--refresh', 'Bypass cached update probes')
|
|
520
535
|
.option('--eval-conditionals', 'Evaluate catalog conditional predicates (runs bash)')
|
|
521
536
|
.action(
|
|
522
|
-
async (opts: CommonOpts & { checkUpdates?: boolean; evalConditionals?: boolean }) => {
|
|
537
|
+
async (opts: CommonOpts & { checkUpdates?: boolean; evalConditionals?: boolean; refresh?: boolean }) => {
|
|
523
538
|
try {
|
|
524
|
-
const view = buildListView(
|
|
539
|
+
const view = await buildListView(
|
|
540
|
+
resolveRoot(opts),
|
|
541
|
+
!!opts.checkUpdates,
|
|
542
|
+
!!opts.evalConditionals,
|
|
543
|
+
!!opts.refresh,
|
|
544
|
+
);
|
|
525
545
|
if (wantsJson(opts)) emitJson(view);
|
|
526
546
|
else printListHuman(view);
|
|
527
547
|
} catch (e) {
|
|
@@ -537,6 +557,7 @@ export function registerPacksCommand(parent: Command): void {
|
|
|
537
557
|
.option('--json', 'Machine-readable JSON output')
|
|
538
558
|
.option('--hq-root <path>', 'HQ root (default: auto-detect)')
|
|
539
559
|
.option('--check-only', 'Report availability without installing')
|
|
560
|
+
.option('--refresh', 'Bypass cached update probes (update refreshes by default)')
|
|
540
561
|
.option('-y, --yes', 'Non-interactive (implies --allow-hooks and --allow-mcp)')
|
|
541
562
|
.option('--allow-hooks', 'Install pack hooks without prompting')
|
|
542
563
|
.option('--allow-mcp', 'Register pack MCP servers without prompting')
|
|
@@ -264,6 +264,95 @@ describe("secrets sandbox", () => {
|
|
|
264
264
|
expect(stdoutSpy).toHaveBeenCalledWith("token [REDACTED]\nAPI_KEY=[REDACTED]\n");
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
it("terminates sandbox output with exactly one newline when output is present", async () => {
|
|
268
|
+
pollJobSpy.mockResolvedValueOnce({
|
|
269
|
+
jobId: "job_123",
|
|
270
|
+
status: "succeeded",
|
|
271
|
+
output: "123",
|
|
272
|
+
exitCode: 0,
|
|
273
|
+
success: true,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const program = buildProgram();
|
|
277
|
+
await program.parseAsync([
|
|
278
|
+
"node",
|
|
279
|
+
"hq",
|
|
280
|
+
"secrets",
|
|
281
|
+
"sandbox",
|
|
282
|
+
"--only",
|
|
283
|
+
"API_KEY",
|
|
284
|
+
"--",
|
|
285
|
+
"my-skill",
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
expect(stdoutSpy.mock.calls.flat().join("")).toBe("123\n");
|
|
289
|
+
|
|
290
|
+
stdoutSpy.mockClear();
|
|
291
|
+
pollJobSpy.mockResolvedValueOnce({
|
|
292
|
+
jobId: "job_123",
|
|
293
|
+
status: "succeeded",
|
|
294
|
+
output: "123\n",
|
|
295
|
+
exitCode: 0,
|
|
296
|
+
success: true,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await buildProgram().parseAsync([
|
|
300
|
+
"node",
|
|
301
|
+
"hq",
|
|
302
|
+
"secrets",
|
|
303
|
+
"sandbox",
|
|
304
|
+
"--only",
|
|
305
|
+
"API_KEY",
|
|
306
|
+
"--",
|
|
307
|
+
"my-skill",
|
|
308
|
+
]);
|
|
309
|
+
|
|
310
|
+
expect(stdoutSpy.mock.calls.flat().join("")).toBe("123\n");
|
|
311
|
+
|
|
312
|
+
stdoutSpy.mockClear();
|
|
313
|
+
pollJobSpy.mockResolvedValueOnce({
|
|
314
|
+
jobId: "job_123",
|
|
315
|
+
status: "succeeded",
|
|
316
|
+
output: "",
|
|
317
|
+
exitCode: 0,
|
|
318
|
+
success: true,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await buildProgram().parseAsync([
|
|
322
|
+
"node",
|
|
323
|
+
"hq",
|
|
324
|
+
"secrets",
|
|
325
|
+
"sandbox",
|
|
326
|
+
"--only",
|
|
327
|
+
"API_KEY",
|
|
328
|
+
"--",
|
|
329
|
+
"my-skill",
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
expect(stdoutSpy).not.toHaveBeenCalled();
|
|
333
|
+
|
|
334
|
+
stdoutSpy.mockClear();
|
|
335
|
+
pollJobSpy.mockResolvedValueOnce({
|
|
336
|
+
jobId: "job_123",
|
|
337
|
+
status: "succeeded",
|
|
338
|
+
exitCode: 0,
|
|
339
|
+
success: true,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await buildProgram().parseAsync([
|
|
343
|
+
"node",
|
|
344
|
+
"hq",
|
|
345
|
+
"secrets",
|
|
346
|
+
"sandbox",
|
|
347
|
+
"--only",
|
|
348
|
+
"API_KEY",
|
|
349
|
+
"--",
|
|
350
|
+
"my-skill",
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
expect(stdoutSpy).not.toHaveBeenCalled();
|
|
354
|
+
});
|
|
355
|
+
|
|
267
356
|
it("exits non-zero when the sandbox command fails", async () => {
|
|
268
357
|
pollJobSpy.mockResolvedValueOnce({
|
|
269
358
|
jobId: "job_123",
|
|
@@ -439,6 +528,324 @@ describe("US-003 — CLI refuses high-security secrets on local injection", () =
|
|
|
439
528
|
});
|
|
440
529
|
});
|
|
441
530
|
|
|
531
|
+
// secrets-proxy-per-secret-destination US-006: hq-cli creation-side parity.
|
|
532
|
+
// `hq secrets set NAME --high-security --destination <url> [--auth-style ...]`
|
|
533
|
+
// POSTs `highSecurity`/`destinations`/`injection` to the vault API — the CLI
|
|
534
|
+
// counterpart to the console create-form (US-005) and the server contract
|
|
535
|
+
// (US-001/US-004). Value input is exercised via --from-stdin (unaffected by
|
|
536
|
+
// this story) so these tests isolate the new flag-handling/validation.
|
|
537
|
+
describe("US-006 — hq secrets set --high-security --destination parity", () => {
|
|
538
|
+
let exitSpy: MockInstance<typeof process.exit>;
|
|
539
|
+
let stdinIsTTYSpy: MockInstance<() => boolean> | undefined;
|
|
540
|
+
|
|
541
|
+
beforeEach(() => {
|
|
542
|
+
exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
|
|
543
|
+
throw new Error("__exit__");
|
|
544
|
+
}) as never);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
afterEach(() => {
|
|
548
|
+
stdinIsTTYSpy?.mockRestore();
|
|
549
|
+
stdinIsTTYSpy = undefined;
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Drives the value through the existing --from-stdin path (unchanged by
|
|
553
|
+
// this story) by feeding a piped-stdin double: mocks isTTY=false and
|
|
554
|
+
// replaces the data/end listeners `readFromPipedStdin` attaches.
|
|
555
|
+
function stubPipedStdin(value: string): void {
|
|
556
|
+
Object.defineProperty(process.stdin, "isTTY", {
|
|
557
|
+
configurable: true,
|
|
558
|
+
value: false,
|
|
559
|
+
});
|
|
560
|
+
vi.spyOn(process.stdin, "on").mockImplementation(((
|
|
561
|
+
event: string,
|
|
562
|
+
cb: (...args: unknown[]) => void,
|
|
563
|
+
) => {
|
|
564
|
+
if (event === "data") cb(value);
|
|
565
|
+
if (event === "end") cb();
|
|
566
|
+
return process.stdin;
|
|
567
|
+
}) as never);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
it("with no flags, the POST body is byte-for-byte unchanged (just name+value)", async () => {
|
|
571
|
+
stubPipedStdin("plain-value");
|
|
572
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes({ ok: true }));
|
|
573
|
+
|
|
574
|
+
const program = buildProgram();
|
|
575
|
+
await program.parseAsync([
|
|
576
|
+
"node",
|
|
577
|
+
"hq",
|
|
578
|
+
"secrets",
|
|
579
|
+
"set",
|
|
580
|
+
"MY_KEY",
|
|
581
|
+
"--from-stdin",
|
|
582
|
+
]);
|
|
583
|
+
|
|
584
|
+
expect(vaultApiFetch).toHaveBeenCalledWith({
|
|
585
|
+
token: "test-token",
|
|
586
|
+
path: "/secrets/prs_alice",
|
|
587
|
+
method: "POST",
|
|
588
|
+
body: { name: "MY_KEY", value: "plain-value" },
|
|
589
|
+
});
|
|
590
|
+
expect(errSpy).not.toHaveBeenCalled();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("--high-security with a KNOWN destination auto-resolves the recipe (no --auth-style needed)", async () => {
|
|
594
|
+
stubPipedStdin("sk-secret-value");
|
|
595
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes({ ok: true }));
|
|
596
|
+
|
|
597
|
+
const program = buildProgram();
|
|
598
|
+
await program.parseAsync([
|
|
599
|
+
"node",
|
|
600
|
+
"hq",
|
|
601
|
+
"secrets",
|
|
602
|
+
"set",
|
|
603
|
+
"OPENAI_KEY",
|
|
604
|
+
"--from-stdin",
|
|
605
|
+
"--high-security",
|
|
606
|
+
"--destination",
|
|
607
|
+
"https://api.openai.com",
|
|
608
|
+
]);
|
|
609
|
+
|
|
610
|
+
expect(vaultApiFetch).toHaveBeenCalledWith({
|
|
611
|
+
token: "test-token",
|
|
612
|
+
path: "/secrets/prs_alice",
|
|
613
|
+
method: "POST",
|
|
614
|
+
body: {
|
|
615
|
+
name: "OPENAI_KEY",
|
|
616
|
+
value: "sk-secret-value",
|
|
617
|
+
highSecurity: true,
|
|
618
|
+
destinations: ["https://api.openai.com"],
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
expect(errSpy).not.toHaveBeenCalled();
|
|
622
|
+
expect(logSpy.mock.calls.flat().join(" ")).toMatch(/high-security/i);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("--auth-style bearer maps to {header: authorization, scheme: bearer}", async () => {
|
|
626
|
+
stubPipedStdin("bearer-token-value");
|
|
627
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes({ ok: true }));
|
|
628
|
+
|
|
629
|
+
const program = buildProgram();
|
|
630
|
+
await program.parseAsync([
|
|
631
|
+
"node",
|
|
632
|
+
"hq",
|
|
633
|
+
"secrets",
|
|
634
|
+
"set",
|
|
635
|
+
"SOME_BEARER_KEY",
|
|
636
|
+
"--from-stdin",
|
|
637
|
+
"--high-security",
|
|
638
|
+
"--destination",
|
|
639
|
+
"https://api.example.com",
|
|
640
|
+
"--auth-style",
|
|
641
|
+
"bearer",
|
|
642
|
+
]);
|
|
643
|
+
|
|
644
|
+
expect(vaultApiFetch).toHaveBeenCalledWith({
|
|
645
|
+
token: "test-token",
|
|
646
|
+
path: "/secrets/prs_alice",
|
|
647
|
+
method: "POST",
|
|
648
|
+
body: {
|
|
649
|
+
name: "SOME_BEARER_KEY",
|
|
650
|
+
value: "bearer-token-value",
|
|
651
|
+
highSecurity: true,
|
|
652
|
+
destinations: ["https://api.example.com"],
|
|
653
|
+
injection: { header: "authorization", scheme: "bearer" },
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("--auth-style x-api-key maps to {header: x-api-key, scheme: raw}", async () => {
|
|
659
|
+
stubPipedStdin("raw-key-value");
|
|
660
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes({ ok: true }));
|
|
661
|
+
|
|
662
|
+
const program = buildProgram();
|
|
663
|
+
await program.parseAsync([
|
|
664
|
+
"node",
|
|
665
|
+
"hq",
|
|
666
|
+
"secrets",
|
|
667
|
+
"set",
|
|
668
|
+
"SOME_RAW_KEY",
|
|
669
|
+
"--from-stdin",
|
|
670
|
+
"--high-security",
|
|
671
|
+
"--destination",
|
|
672
|
+
"https://api.example.com",
|
|
673
|
+
"--auth-style",
|
|
674
|
+
"x-api-key",
|
|
675
|
+
]);
|
|
676
|
+
|
|
677
|
+
expect(vaultApiFetch).toHaveBeenCalledWith({
|
|
678
|
+
token: "test-token",
|
|
679
|
+
path: "/secrets/prs_alice",
|
|
680
|
+
method: "POST",
|
|
681
|
+
body: {
|
|
682
|
+
name: "SOME_RAW_KEY",
|
|
683
|
+
value: "raw-key-value",
|
|
684
|
+
highSecurity: true,
|
|
685
|
+
destinations: ["https://api.example.com"],
|
|
686
|
+
injection: { header: "x-api-key", scheme: "raw" },
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it("--auth-style header:NAME maps to a custom raw header", async () => {
|
|
692
|
+
stubPipedStdin("custom-header-value");
|
|
693
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes({ ok: true }));
|
|
694
|
+
|
|
695
|
+
const program = buildProgram();
|
|
696
|
+
await program.parseAsync([
|
|
697
|
+
"node",
|
|
698
|
+
"hq",
|
|
699
|
+
"secrets",
|
|
700
|
+
"set",
|
|
701
|
+
"CUSTOM_HEADER_KEY",
|
|
702
|
+
"--from-stdin",
|
|
703
|
+
"--high-security",
|
|
704
|
+
"--destination",
|
|
705
|
+
"https://api.example.com",
|
|
706
|
+
"--auth-style",
|
|
707
|
+
"header:X-Custom-Key",
|
|
708
|
+
]);
|
|
709
|
+
|
|
710
|
+
expect(vaultApiFetch).toHaveBeenCalledWith({
|
|
711
|
+
token: "test-token",
|
|
712
|
+
path: "/secrets/prs_alice",
|
|
713
|
+
method: "POST",
|
|
714
|
+
body: {
|
|
715
|
+
name: "CUSTOM_HEADER_KEY",
|
|
716
|
+
value: "custom-header-value",
|
|
717
|
+
highSecurity: true,
|
|
718
|
+
destinations: ["https://api.example.com"],
|
|
719
|
+
injection: { header: "X-Custom-Key", scheme: "raw" },
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it("--high-security with an UNKNOWN host and no --auth-style errors clearly, client-side (no request sent)", async () => {
|
|
725
|
+
const program = buildProgram();
|
|
726
|
+
await expect(
|
|
727
|
+
program.parseAsync([
|
|
728
|
+
"node",
|
|
729
|
+
"hq",
|
|
730
|
+
"secrets",
|
|
731
|
+
"set",
|
|
732
|
+
"UNKNOWN_HOST_KEY",
|
|
733
|
+
"--from-stdin",
|
|
734
|
+
"--high-security",
|
|
735
|
+
"--destination",
|
|
736
|
+
"https://api.unknown-vendor.example",
|
|
737
|
+
]),
|
|
738
|
+
).rejects.toThrow("__exit__");
|
|
739
|
+
|
|
740
|
+
expect(exitSpy.mock.calls[0]?.[0]).toBe(1);
|
|
741
|
+
const errText = errSpy.mock.calls.flat().join(" ");
|
|
742
|
+
expect(errText).toMatch(/unknown destination host/i);
|
|
743
|
+
expect(errText).toMatch(/--auth-style/);
|
|
744
|
+
// Client-side rejection — never reaches the network.
|
|
745
|
+
expect(vaultApiFetch).not.toHaveBeenCalled();
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it("--high-security with no --destination errors clearly (non-zero exit, no request sent)", async () => {
|
|
749
|
+
const program = buildProgram();
|
|
750
|
+
await expect(
|
|
751
|
+
program.parseAsync([
|
|
752
|
+
"node",
|
|
753
|
+
"hq",
|
|
754
|
+
"secrets",
|
|
755
|
+
"set",
|
|
756
|
+
"MISSING_DEST_KEY",
|
|
757
|
+
"--from-stdin",
|
|
758
|
+
"--high-security",
|
|
759
|
+
]),
|
|
760
|
+
).rejects.toThrow("__exit__");
|
|
761
|
+
|
|
762
|
+
expect(exitSpy.mock.calls[0]?.[0]).toBe(1);
|
|
763
|
+
const errText = errSpy.mock.calls.flat().join(" ");
|
|
764
|
+
expect(errText).toMatch(/--high-security/);
|
|
765
|
+
expect(errText).toMatch(/--destination/);
|
|
766
|
+
expect(vaultApiFetch).not.toHaveBeenCalled();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it("--destination without --high-security errors clearly (non-zero exit, no request sent)", async () => {
|
|
770
|
+
const program = buildProgram();
|
|
771
|
+
await expect(
|
|
772
|
+
program.parseAsync([
|
|
773
|
+
"node",
|
|
774
|
+
"hq",
|
|
775
|
+
"secrets",
|
|
776
|
+
"set",
|
|
777
|
+
"STRAY_DEST_KEY",
|
|
778
|
+
"--from-stdin",
|
|
779
|
+
"--destination",
|
|
780
|
+
"https://api.openai.com",
|
|
781
|
+
]),
|
|
782
|
+
).rejects.toThrow("__exit__");
|
|
783
|
+
|
|
784
|
+
expect(exitSpy.mock.calls[0]?.[0]).toBe(1);
|
|
785
|
+
expect(vaultApiFetch).not.toHaveBeenCalled();
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("an invalid --destination URL (path segment) errors clearly, client-side", async () => {
|
|
789
|
+
const program = buildProgram();
|
|
790
|
+
await expect(
|
|
791
|
+
program.parseAsync([
|
|
792
|
+
"node",
|
|
793
|
+
"hq",
|
|
794
|
+
"secrets",
|
|
795
|
+
"set",
|
|
796
|
+
"BAD_URL_KEY",
|
|
797
|
+
"--from-stdin",
|
|
798
|
+
"--high-security",
|
|
799
|
+
"--destination",
|
|
800
|
+
"https://api.openai.com/v1",
|
|
801
|
+
]),
|
|
802
|
+
).rejects.toThrow("__exit__");
|
|
803
|
+
|
|
804
|
+
expect(exitSpy.mock.calls[0]?.[0]).toBe(1);
|
|
805
|
+
const errText = errSpy.mock.calls.flat().join(" ");
|
|
806
|
+
expect(errText).toMatch(/no path, query, or fragment/i);
|
|
807
|
+
expect(vaultApiFetch).not.toHaveBeenCalled();
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it("surfaces the server's rejection message when the backend 400s (e.g. unknown host drift)", async () => {
|
|
811
|
+
stubPipedStdin("value");
|
|
812
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
813
|
+
jsonRes(
|
|
814
|
+
{
|
|
815
|
+
error:
|
|
816
|
+
"unknown destination host api.newer-vendor.com: provide an injection recipe (header + scheme)",
|
|
817
|
+
},
|
|
818
|
+
400,
|
|
819
|
+
),
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
const program = buildProgram();
|
|
823
|
+
// A host absent from the CLI's own known-set copy but WOULD be flagged
|
|
824
|
+
// client-side too (this asserts the server-error surfacing path itself
|
|
825
|
+
// stays intact, using --auth-style to bypass the client-side check and
|
|
826
|
+
// reach the network).
|
|
827
|
+
await expect(
|
|
828
|
+
program.parseAsync([
|
|
829
|
+
"node",
|
|
830
|
+
"hq",
|
|
831
|
+
"secrets",
|
|
832
|
+
"set",
|
|
833
|
+
"DRIFT_KEY",
|
|
834
|
+
"--from-stdin",
|
|
835
|
+
"--high-security",
|
|
836
|
+
"--destination",
|
|
837
|
+
"https://api.newer-vendor.com",
|
|
838
|
+
"--auth-style",
|
|
839
|
+
"bearer",
|
|
840
|
+
]),
|
|
841
|
+
).rejects.toThrow("__exit__");
|
|
842
|
+
|
|
843
|
+
expect(exitSpy.mock.calls[0]?.[0]).toBe(1);
|
|
844
|
+
const errText = errSpy.mock.calls.flat().join(" ");
|
|
845
|
+
expect(errText).toMatch(/unknown destination host/i);
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
442
849
|
describe("secrets generate-link", () => {
|
|
443
850
|
it("mints one-time submission links for personal secrets", async () => {
|
|
444
851
|
const program = buildProgram();
|