@indigoai-us/hq-cli 5.60.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/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 +129 -5
- 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/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 +318 -0
- package/src/commands/secrets.ts +198 -4
- 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
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
import { type KeyObject } from 'node:crypto';
|
|
37
37
|
import { type SecretResolver } from './mcp-registration.js';
|
|
38
38
|
import type { PackManifest } from '../types.js';
|
|
39
|
+
export interface ResolveLatestOptions {
|
|
40
|
+
forceRefresh?: boolean;
|
|
41
|
+
now?: number;
|
|
42
|
+
cacheTtlMs?: number;
|
|
43
|
+
fetchImpl?: typeof fetch;
|
|
44
|
+
}
|
|
39
45
|
export type Transport = 'npm' | 'git' | 'local' | 'marketplace';
|
|
40
46
|
/** Prefix that routes a source through the HQ marketplace transport (US-006). */
|
|
41
47
|
export declare const MARKETPLACE_PREFIX = "marketplace:";
|
|
@@ -181,7 +187,7 @@ export interface LatestResult {
|
|
|
181
187
|
* @param source the stamped `source:` from the installed package.yaml
|
|
182
188
|
* @param installedVersion the installed pack's manifest `version` (npm compare)
|
|
183
189
|
*/
|
|
184
|
-
export declare function resolveLatest(source: string, installedVersion?: string): LatestResult
|
|
190
|
+
export declare function resolveLatest(source: string, installedVersion?: string, opts?: ResolveLatestOptions): Promise<LatestResult>;
|
|
185
191
|
/**
|
|
186
192
|
* Async marketplace update probe (US-006): resolve the slug's latest approved
|
|
187
193
|
* listing version and compare it to the installed version. Reuses the same
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* from each pack's package.yaml; rationale lives in the layout-fix PR.)
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
37
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a90272ca-94a1-5905-85f8-af12a01cb58a")}catch(e){}}();
|
|
38
38
|
import * as fs from 'fs';
|
|
39
39
|
import * as os from 'os';
|
|
40
40
|
import * as path from 'path';
|
|
@@ -54,6 +54,58 @@ import { safeExtractTarball } from './safe-extract.js';
|
|
|
54
54
|
import { vaultApiFetchPublic } from '../utils/vault-api.js';
|
|
55
55
|
import { redactSecrets, SECRET_REDACTION, registerMcpServers, McpManifestError, } from './mcp-registration.js';
|
|
56
56
|
import { readCache, listSecretCacheScopes } from '../utils/secrets-cache.js';
|
|
57
|
+
const PACK_UPDATE_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
58
|
+
const PACK_UPDATE_FETCH_TIMEOUT_MS = 3_000;
|
|
59
|
+
const gitLsRemoteMemo = new Map();
|
|
60
|
+
function packUpdateCachePath() {
|
|
61
|
+
return path.join(os.homedir(), '.hq', 'pack-update-cache.json');
|
|
62
|
+
}
|
|
63
|
+
function readPackUpdateCache() {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(fs.readFileSync(packUpdateCachePath(), 'utf-8'));
|
|
66
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.entries || typeof parsed.entries !== 'object') {
|
|
67
|
+
return { entries: {} };
|
|
68
|
+
}
|
|
69
|
+
return { entries: parsed.entries };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { entries: {} };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function writePackUpdateCache(cache) {
|
|
76
|
+
try {
|
|
77
|
+
const file = packUpdateCachePath();
|
|
78
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
79
|
+
fs.writeFileSync(file, JSON.stringify(cache));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// best-effort; update checks must never fail because the cache is unwritable
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function cachedLatest(cacheKey, opts) {
|
|
86
|
+
if (opts.forceRefresh)
|
|
87
|
+
return undefined;
|
|
88
|
+
const entry = readPackUpdateCache().entries[cacheKey];
|
|
89
|
+
if (!entry || typeof entry.latest !== 'string' || typeof entry.fetchedAt !== 'number')
|
|
90
|
+
return undefined;
|
|
91
|
+
const now = opts.now ?? Date.now();
|
|
92
|
+
const ttl = opts.cacheTtlMs ?? PACK_UPDATE_CACHE_TTL_MS;
|
|
93
|
+
return now - entry.fetchedAt <= ttl ? entry.latest : undefined;
|
|
94
|
+
}
|
|
95
|
+
function storeCachedLatest(cacheKey, latest, opts) {
|
|
96
|
+
const cache = readPackUpdateCache();
|
|
97
|
+
cache.entries[cacheKey] = { latest, fetchedAt: opts.now ?? Date.now() };
|
|
98
|
+
writePackUpdateCache(cache);
|
|
99
|
+
}
|
|
100
|
+
async function latestWithDiskCache(cacheKey, opts, refresh) {
|
|
101
|
+
const cached = cachedLatest(cacheKey, opts);
|
|
102
|
+
if (cached)
|
|
103
|
+
return cached;
|
|
104
|
+
const latest = await refresh();
|
|
105
|
+
if (latest)
|
|
106
|
+
storeCachedLatest(cacheKey, latest, opts);
|
|
107
|
+
return latest;
|
|
108
|
+
}
|
|
57
109
|
/** Prefix that routes a source through the HQ marketplace transport (US-006). */
|
|
58
110
|
export const MARKETPLACE_PREFIX = 'marketplace:';
|
|
59
111
|
export function classify(source) {
|
|
@@ -489,13 +541,33 @@ function rsyncDir(src, dest) {
|
|
|
489
541
|
*/
|
|
490
542
|
function isNamedRef(url, ref) {
|
|
491
543
|
try {
|
|
492
|
-
const out =
|
|
544
|
+
const out = gitLsRemote(['--heads', '--tags', url, ref]);
|
|
493
545
|
return out.trim().length > 0;
|
|
494
546
|
}
|
|
495
547
|
catch {
|
|
496
548
|
return false;
|
|
497
549
|
}
|
|
498
550
|
}
|
|
551
|
+
function gitLsRemote(args) {
|
|
552
|
+
const key = args.join('\0');
|
|
553
|
+
const cached = gitLsRemoteMemo.get(key);
|
|
554
|
+
if (cached !== undefined)
|
|
555
|
+
return cached;
|
|
556
|
+
const out = execFileSync('git', ['ls-remote', ...args], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
557
|
+
gitLsRemoteMemo.set(key, out);
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
async function fetchLatestNpmVersion(pkg, opts) {
|
|
561
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
562
|
+
const res = await fetchImpl(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`, {
|
|
563
|
+
headers: { Accept: 'application/json' },
|
|
564
|
+
signal: AbortSignal.timeout(PACK_UPDATE_FETCH_TIMEOUT_MS),
|
|
565
|
+
});
|
|
566
|
+
if (!res.ok)
|
|
567
|
+
throw new Error(`registry returned ${res.status}`);
|
|
568
|
+
const body = (await res.json());
|
|
569
|
+
return typeof body.version === 'string' ? body.version : undefined;
|
|
570
|
+
}
|
|
499
571
|
/** Extract the ref (sha or named ref) recorded in a stamped git source. */
|
|
500
572
|
function gitRefFromSource(source) {
|
|
501
573
|
const { subpath, ref } = parseGitFragment(source);
|
|
@@ -504,6 +576,9 @@ function gitRefFromSource(source) {
|
|
|
504
576
|
void subpath;
|
|
505
577
|
return ref;
|
|
506
578
|
}
|
|
579
|
+
function isFullGitSha(ref) {
|
|
580
|
+
return /^[0-9a-f]{40}$/i.test(ref);
|
|
581
|
+
}
|
|
507
582
|
/**
|
|
508
583
|
* Probe whether a newer version of an already-installed pack is available,
|
|
509
584
|
* WITHOUT fetching or installing. Reuses the same git/npm primitives as the
|
|
@@ -513,7 +588,7 @@ function gitRefFromSource(source) {
|
|
|
513
588
|
* @param source the stamped `source:` from the installed package.yaml
|
|
514
589
|
* @param installedVersion the installed pack's manifest `version` (npm compare)
|
|
515
590
|
*/
|
|
516
|
-
export function resolveLatest(source, installedVersion) {
|
|
591
|
+
export async function resolveLatest(source, installedVersion, opts = {}) {
|
|
517
592
|
let transport;
|
|
518
593
|
try {
|
|
519
594
|
transport = classify(source);
|
|
@@ -542,15 +617,12 @@ export function resolveLatest(source, installedVersion) {
|
|
|
542
617
|
const pkg = stripVersion(source);
|
|
543
618
|
const current = installedVersion ?? (source.lastIndexOf('@') > 0 ? source.slice(source.lastIndexOf('@') + 1) : undefined);
|
|
544
619
|
try {
|
|
545
|
-
const latest =
|
|
546
|
-
encoding: 'utf-8',
|
|
547
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
548
|
-
}).trim();
|
|
620
|
+
const latest = await latestWithDiskCache(`npm:${pkg}`, opts, () => fetchLatestNpmVersion(pkg, opts));
|
|
549
621
|
const updateAvailable = current && latest ? semverGt(latest, current) : null;
|
|
550
622
|
return { transport, current, latest, updateAvailable };
|
|
551
623
|
}
|
|
552
624
|
catch (e) {
|
|
553
|
-
return { transport, current, updateAvailable: null, error: `npm
|
|
625
|
+
return { transport, current, updateAvailable: null, error: `npm registry check failed: ${e.message}` };
|
|
554
626
|
}
|
|
555
627
|
}
|
|
556
628
|
// git
|
|
@@ -565,13 +637,12 @@ export function resolveLatest(source, installedVersion) {
|
|
|
565
637
|
const current = gitRefFromSource(source);
|
|
566
638
|
// If install followed a named ref (branch/tag), compare that ref's tip;
|
|
567
639
|
// otherwise (default SHA-pin) compare the default branch HEAD.
|
|
568
|
-
const refArg = current && isNamedRef(url, current) ? current : 'HEAD';
|
|
640
|
+
const refArg = current && !isFullGitSha(current) && isNamedRef(url, current) ? current : 'HEAD';
|
|
569
641
|
try {
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
})
|
|
574
|
-
const latest = out.split(/\s+/)[0] || undefined;
|
|
642
|
+
const latest = await latestWithDiskCache(`git:${url}#${refArg}`, opts, () => {
|
|
643
|
+
const out = gitLsRemote([url, refArg]).trim();
|
|
644
|
+
return out.split(/\s+/)[0] || undefined;
|
|
645
|
+
});
|
|
575
646
|
const updateAvailable = current && latest ? !latest.startsWith(current) && !current.startsWith(latest) : null;
|
|
576
647
|
return { transport, current, latest, updateAvailable };
|
|
577
648
|
}
|
|
@@ -1629,4 +1700,4 @@ export async function installPack(source, opts = {}) {
|
|
|
1629
1700
|
}
|
|
1630
1701
|
}
|
|
1631
1702
|
//# sourceMappingURL=pack-install.js.map
|
|
1632
|
-
//# debugId=
|
|
1703
|
+
//# debugId=a90272ca-94a1-5905-85f8-af12a01cb58a
|
package/dist/commands/packs.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* Spec: knowledge/public/hq-core/package-yaml-spec.md.
|
|
19
19
|
*/
|
|
20
20
|
import { Command } from 'commander';
|
|
21
|
+
import { type LatestResult } from './pack-install.js';
|
|
21
22
|
import { type InstalledPack, type LinkStatus } from '../utils/pack-contributions.js';
|
|
22
23
|
import type { PackContributeKey } from '../types.js';
|
|
23
24
|
interface InstalledPackView {
|
|
@@ -48,7 +49,7 @@ interface InstalledPackView {
|
|
|
48
49
|
};
|
|
49
50
|
error?: string;
|
|
50
51
|
}
|
|
51
|
-
export declare function buildInstalledView(hqRoot: string, hqVersion: string | null, pack: InstalledPack, installedSources: Set<string>, checkUpdates: boolean): InstalledPackView;
|
|
52
|
+
export declare function buildInstalledView(hqRoot: string, hqVersion: string | null, pack: InstalledPack, installedSources: Set<string>, checkUpdates: boolean, latestProbe?: LatestResult): InstalledPackView;
|
|
52
53
|
export declare function registerPacksCommand(parent: Command): void;
|
|
53
54
|
export {};
|
|
54
55
|
//# sourceMappingURL=packs.d.ts.map
|
package/dist/commands/packs.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* Spec: knowledge/public/hq-core/package-yaml-spec.md.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
21
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f8ee3de6-a097-5e0f-8afa-63381f29032d")}catch(e){}}();
|
|
22
22
|
import * as fs from 'fs';
|
|
23
23
|
import * as path from 'path';
|
|
24
24
|
import * as readline from 'readline';
|
|
@@ -61,7 +61,7 @@ async function confirm(question) {
|
|
|
61
61
|
});
|
|
62
62
|
return /^(y|yes)$/i.test(answer.trim());
|
|
63
63
|
}
|
|
64
|
-
export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, checkUpdates) {
|
|
64
|
+
export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, checkUpdates, latestProbe) {
|
|
65
65
|
if (!pack.manifest) {
|
|
66
66
|
return {
|
|
67
67
|
name: pack.name,
|
|
@@ -95,7 +95,7 @@ export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, ch
|
|
|
95
95
|
const hqCoreSatisfied = hqVersion && requiresHqCore ? semverSatisfies(hqVersion, requiresHqCore) : null;
|
|
96
96
|
let updateAvailable = null;
|
|
97
97
|
if (checkUpdates && m.source) {
|
|
98
|
-
updateAvailable =
|
|
98
|
+
updateAvailable = latestProbe?.updateAvailable ?? null;
|
|
99
99
|
}
|
|
100
100
|
// US-005 — surface the pack's `initialization` block so the HQ Sync
|
|
101
101
|
// "Installed" panel can render its get-started affordance. `readPackManifest`
|
|
@@ -129,13 +129,16 @@ export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, ch
|
|
|
129
129
|
...(initialization ? { initialization } : {}),
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
|
-
function buildListView(hqRoot, checkUpdates, evalConditionals) {
|
|
132
|
+
async function buildListView(hqRoot, checkUpdates, evalConditionals, refreshUpdates) {
|
|
133
133
|
const hqVersion = readHqVersion(hqRoot);
|
|
134
134
|
const packs = listInstalledPacks(hqRoot);
|
|
135
135
|
const catalog = readRecommendedPackages(hqRoot);
|
|
136
136
|
const catalogSources = new Set(catalog.map((c) => c.source));
|
|
137
137
|
const warnings = [];
|
|
138
|
-
const
|
|
138
|
+
const latestProbes = await Promise.all(packs.map((p) => checkUpdates && p.manifest?.source
|
|
139
|
+
? resolveLatest(p.manifest.source, p.manifest.version, { forceRefresh: refreshUpdates })
|
|
140
|
+
: Promise.resolve(undefined)));
|
|
141
|
+
const installed = packs.map((p, i) => buildInstalledView(hqRoot, hqVersion, p, catalogSources, checkUpdates, latestProbes[i]));
|
|
139
142
|
for (const p of installed) {
|
|
140
143
|
if (p.error)
|
|
141
144
|
warnings.push(`${p.name}: ${p.error}`);
|
|
@@ -217,7 +220,7 @@ async function runUpdate(name, opts) {
|
|
|
217
220
|
// other transport keeps the existing synchronous probe unchanged.
|
|
218
221
|
const probe = safeClassify(source) === 'marketplace'
|
|
219
222
|
? await resolveLatestMarketplace(source, m.version)
|
|
220
|
-
: resolveLatest(source, m.version);
|
|
223
|
+
: await resolveLatest(source, m.version, { forceRefresh: true });
|
|
221
224
|
const base = {
|
|
222
225
|
name: pname,
|
|
223
226
|
transport: probe.transport,
|
|
@@ -374,10 +377,11 @@ export function registerPacksCommand(parent) {
|
|
|
374
377
|
.option('--json', 'Machine-readable JSON output')
|
|
375
378
|
.option('--hq-root <path>', 'HQ root (default: auto-detect)')
|
|
376
379
|
.option('--check-updates', 'Probe each pack for available updates (network I/O)')
|
|
380
|
+
.option('--refresh', 'Bypass cached update probes')
|
|
377
381
|
.option('--eval-conditionals', 'Evaluate catalog conditional predicates (runs bash)')
|
|
378
382
|
.action(async (opts) => {
|
|
379
383
|
try {
|
|
380
|
-
const view = buildListView(resolveRoot(opts), !!opts.checkUpdates, !!opts.evalConditionals);
|
|
384
|
+
const view = await buildListView(resolveRoot(opts), !!opts.checkUpdates, !!opts.evalConditionals, !!opts.refresh);
|
|
381
385
|
if (wantsJson(opts))
|
|
382
386
|
emitJson(view);
|
|
383
387
|
else
|
|
@@ -394,6 +398,7 @@ export function registerPacksCommand(parent) {
|
|
|
394
398
|
.option('--json', 'Machine-readable JSON output')
|
|
395
399
|
.option('--hq-root <path>', 'HQ root (default: auto-detect)')
|
|
396
400
|
.option('--check-only', 'Report availability without installing')
|
|
401
|
+
.option('--refresh', 'Bypass cached update probes (update refreshes by default)')
|
|
397
402
|
.option('-y, --yes', 'Non-interactive (implies --allow-hooks and --allow-mcp)')
|
|
398
403
|
.option('--allow-hooks', 'Install pack hooks without prompting')
|
|
399
404
|
.option('--allow-mcp', 'Register pack MCP servers without prompting')
|
|
@@ -463,4 +468,4 @@ export function registerPacksCommand(parent) {
|
|
|
463
468
|
});
|
|
464
469
|
}
|
|
465
470
|
//# sourceMappingURL=packs.js.map
|
|
466
|
-
//# debugId=
|
|
471
|
+
//# debugId=f8ee3de6-a097-5e0f-8afa-63381f29032d
|
|
@@ -36,6 +36,11 @@ export interface SecretLoadResponse {
|
|
|
36
36
|
message?: string;
|
|
37
37
|
}>;
|
|
38
38
|
}
|
|
39
|
+
export interface SecretInjectionRecipe {
|
|
40
|
+
header: string;
|
|
41
|
+
scheme: "raw" | "bearer";
|
|
42
|
+
extraHeaders?: Record<string, string>;
|
|
43
|
+
}
|
|
39
44
|
export declare function scrubSandboxOutput(text: string, secretNames?: string[]): string;
|
|
40
45
|
export declare function loadRevealedSecrets(token: string, companyUid: string, keys: string[], usage?: SecretUsage): Promise<Map<string, string>>;
|
|
41
46
|
export declare function registerSecretsCommand(program: Command): void;
|
package/dist/commands/secrets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="ac2acbfe-d6f8-52af-9e1a-38b43474a6d5")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import * as readline from "node:readline";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -153,6 +153,74 @@ function describeSecretAclPrincipal(principal) {
|
|
|
153
153
|
? "@all (entire company)"
|
|
154
154
|
: principal.granteeId;
|
|
155
155
|
}
|
|
156
|
+
// Mirrors hq-pro's server-authoritative KNOWN_DESTINATION_REGISTRY
|
|
157
|
+
// (src/vault-service/handlers/destination-registry.ts) — a KNOWN host's
|
|
158
|
+
// --auth-style is optional because the server resolves the recipe itself.
|
|
159
|
+
// This client-side copy exists purely so an UNKNOWN host with no
|
|
160
|
+
// --auth-style can be rejected immediately with a clear, actionable message
|
|
161
|
+
// instead of a round trip; the server remains the authoritative validator
|
|
162
|
+
// (this list drifting stale merely means one extra CLI round trip, not a
|
|
163
|
+
// security gap — the server still 400s an unrecognized host with no recipe).
|
|
164
|
+
const KNOWN_DESTINATION_HOSTS = new Set([
|
|
165
|
+
"api.anthropic.com",
|
|
166
|
+
"api.openai.com",
|
|
167
|
+
"api.stripe.com",
|
|
168
|
+
]);
|
|
169
|
+
// Parses `--auth-style` into the InjectionRecipe shape the server expects.
|
|
170
|
+
// Returns `null` (with a printed error) for an unrecognized value.
|
|
171
|
+
function parseAuthStyle(authStyle) {
|
|
172
|
+
if (authStyle === "bearer") {
|
|
173
|
+
return { header: "authorization", scheme: "bearer" };
|
|
174
|
+
}
|
|
175
|
+
if (authStyle === "x-api-key") {
|
|
176
|
+
return { header: "x-api-key", scheme: "raw" };
|
|
177
|
+
}
|
|
178
|
+
const headerMatch = authStyle.match(/^header:(.+)$/);
|
|
179
|
+
if (headerMatch) {
|
|
180
|
+
const headerName = headerMatch[1].trim();
|
|
181
|
+
if (!headerName) {
|
|
182
|
+
console.error(chalk.red(`Invalid --auth-style 'header:': must name a header, e.g. header:X-Custom-Key`));
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return { header: headerName, scheme: "raw" };
|
|
186
|
+
}
|
|
187
|
+
console.error(chalk.red(`Invalid --auth-style '${authStyle}': must be one of bearer, x-api-key, or header:NAME`));
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
// Validates `--destination` is a bare HTTPS scheme+host URL (no path, query,
|
|
191
|
+
// port). Mirrors hq-pro's `validateDestinations` server-side check
|
|
192
|
+
// (src/vault-service/handlers/secrets.ts) so a malformed URL is caught
|
|
193
|
+
// locally with an actionable message rather than a round trip — the server
|
|
194
|
+
// re-validates and remains authoritative.
|
|
195
|
+
function parseDestinationUrl(raw) {
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = new URL(raw);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
console.error(chalk.red(`Invalid --destination '${raw}': must be a valid URL`));
|
|
202
|
+
return { ok: false };
|
|
203
|
+
}
|
|
204
|
+
if (parsed.protocol !== "https:") {
|
|
205
|
+
console.error(chalk.red(`Invalid --destination '${raw}': must use https://`));
|
|
206
|
+
return { ok: false };
|
|
207
|
+
}
|
|
208
|
+
if (!parsed.hostname) {
|
|
209
|
+
console.error(chalk.red(`Invalid --destination '${raw}': missing hostname`));
|
|
210
|
+
return { ok: false };
|
|
211
|
+
}
|
|
212
|
+
if ((parsed.pathname !== "" && parsed.pathname !== "/") ||
|
|
213
|
+
parsed.search !== "" ||
|
|
214
|
+
parsed.hash !== "") {
|
|
215
|
+
console.error(chalk.red(`Invalid --destination '${raw}': must be a bare scheme+host URL with no path, query, or fragment (e.g. https://api.openai.com)`));
|
|
216
|
+
return { ok: false };
|
|
217
|
+
}
|
|
218
|
+
if (parsed.port !== "") {
|
|
219
|
+
console.error(chalk.red(`Invalid --destination '${raw}': must not specify a port`));
|
|
220
|
+
return { ok: false };
|
|
221
|
+
}
|
|
222
|
+
return { ok: true, url: `https://${parsed.hostname}`, hostname: parsed.hostname };
|
|
223
|
+
}
|
|
156
224
|
function normalizeSecretTier(tier) {
|
|
157
225
|
return tier === "sensitive" || tier === "nuclear" ? tier : "standard";
|
|
158
226
|
}
|
|
@@ -378,12 +446,56 @@ export function registerSecretsCommand(program) {
|
|
|
378
446
|
.command("set <name>")
|
|
379
447
|
.description("Create or update a secret")
|
|
380
448
|
.option("--from-stdin", "Read secret value from piped stdin")
|
|
449
|
+
.option("--high-security", "Mark the secret high-security: it can never be revealed or injected locally, only used through the HQ secret proxy (requires --destination)")
|
|
450
|
+
.option("--destination <https-url>", "Approved scheme+host HTTPS URL the proxy may forward this secret to (e.g. https://api.openai.com); required with --high-security")
|
|
451
|
+
.option("--auth-style <style>", "How the proxy attaches the key upstream: bearer | x-api-key | header:NAME. Optional for known destinations (auto-resolved server-side); required for unknown ones")
|
|
381
452
|
.action(async (name, opts) => {
|
|
382
453
|
try {
|
|
383
454
|
if (!SECRET_NAME_PATTERN.test(name)) {
|
|
384
455
|
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)`));
|
|
385
456
|
process.exit(1);
|
|
386
457
|
}
|
|
458
|
+
// secrets-proxy-per-secret-destination US-006: --high-security marks
|
|
459
|
+
// the secret so it can only ever be used through the server-side
|
|
460
|
+
// proxy (never revealed/injected locally — that refusal is the
|
|
461
|
+
// pre-existing consumption-side behavior in `get`/`exec`/`env` above,
|
|
462
|
+
// unchanged by this story). It REQUIRES a --destination: the proxy
|
|
463
|
+
// (hq-pro US-002) fails closed with no destination configured, so
|
|
464
|
+
// catching the missing pin here is a clear, immediate CLI error
|
|
465
|
+
// rather than a deferred proxy-time failure.
|
|
466
|
+
let destinations;
|
|
467
|
+
let injection;
|
|
468
|
+
if (opts.highSecurity) {
|
|
469
|
+
if (!opts.destination) {
|
|
470
|
+
console.error(chalk.red("Error: --high-security requires --destination <https-url> (e.g. --destination https://api.openai.com)."));
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
const destResult = parseDestinationUrl(opts.destination);
|
|
474
|
+
if (!destResult.ok) {
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
destinations = [destResult.url];
|
|
478
|
+
if (opts.authStyle) {
|
|
479
|
+
const recipe = parseAuthStyle(opts.authStyle);
|
|
480
|
+
if (!recipe) {
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
injection = recipe;
|
|
484
|
+
}
|
|
485
|
+
else if (!KNOWN_DESTINATION_HOSTS.has(destResult.hostname)) {
|
|
486
|
+
// Unknown host + no explicit recipe: the server would reject this
|
|
487
|
+
// 400 anyway (US-004 registry lookup only, never guesses) — fail
|
|
488
|
+
// fast locally with an actionable message instead of a round trip.
|
|
489
|
+
console.error(chalk.red(`Error: unknown destination host '${destResult.hostname}' — provide --auth-style <bearer|x-api-key|header:NAME> (known hosts auto-resolve: ${[...KNOWN_DESTINATION_HOSTS].join(", ")}).`));
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
// Known host + no --auth-style: leave `injection` undefined so the
|
|
493
|
+
// server (US-004) auto-resolves the recipe from its registry.
|
|
494
|
+
}
|
|
495
|
+
else if (opts.destination || opts.authStyle) {
|
|
496
|
+
console.error(chalk.red("Error: --destination/--auth-style require --high-security."));
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
387
499
|
let value;
|
|
388
500
|
if (opts.fromStdin) {
|
|
389
501
|
if (process.stdin.isTTY) {
|
|
@@ -419,15 +531,27 @@ export function registerSecretsCommand(program) {
|
|
|
419
531
|
token,
|
|
420
532
|
path: `/secrets/${encodeURIComponent(companyUid)}`,
|
|
421
533
|
method: "POST",
|
|
422
|
-
body: {
|
|
534
|
+
body: {
|
|
535
|
+
name,
|
|
536
|
+
value,
|
|
537
|
+
// Only present when --high-security was passed — an ordinary
|
|
538
|
+
// `set` with no flags sends exactly `{ name, value }`, byte-for-
|
|
539
|
+
// byte unchanged from before this story.
|
|
540
|
+
...(opts.highSecurity ? { highSecurity: true } : {}),
|
|
541
|
+
...(destinations ? { destinations } : {}),
|
|
542
|
+
...(injection ? { injection } : {}),
|
|
543
|
+
},
|
|
423
544
|
});
|
|
424
545
|
if (!res.ok) {
|
|
425
|
-
const body = await res.json().catch(() => ({}));
|
|
426
|
-
console.error(chalk.red(`Failed to set secret: ${body
|
|
546
|
+
const body = (await res.json().catch(() => ({})));
|
|
547
|
+
console.error(chalk.red(`Failed to set secret: ${extractApiMessage(body, res.statusText)}`));
|
|
427
548
|
process.exit(1);
|
|
428
549
|
}
|
|
429
550
|
removeCacheEntry(companyUid, name);
|
|
430
551
|
console.log(chalk.green(formatSecretSaved(name, scopeLabel)));
|
|
552
|
+
if (opts.highSecurity) {
|
|
553
|
+
console.log(chalk.dim(` High-security: destination pinned to ${destinations?.[0]}. This value can never be revealed or injected locally — only used through the HQ secret proxy.`));
|
|
554
|
+
}
|
|
431
555
|
}
|
|
432
556
|
catch (err) {
|
|
433
557
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -1258,4 +1382,4 @@ export function registerSecretsCommand(program) {
|
|
|
1258
1382
|
});
|
|
1259
1383
|
}
|
|
1260
1384
|
//# sourceMappingURL=secrets.js.map
|
|
1261
|
-
//# debugId=
|
|
1385
|
+
//# debugId=ac2acbfe-d6f8-52af-9e1a-38b43474a6d5
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* HQ CLI - Module management, package management, and cloud sync for HQ
|
|
4
|
-
*/
|
|
5
2
|
import "./node-preflight.js";
|
|
3
|
+
declare function isVersionRequest(argv: readonly string[]): boolean;
|
|
4
|
+
export declare const __test__: {
|
|
5
|
+
isVersionRequest: typeof isVersionRequest;
|
|
6
|
+
};
|
|
7
|
+
export {};
|
|
6
8
|
//# sourceMappingURL=index.d.ts.map
|