@indigoai-us/hq-cli 5.107.0 → 5.108.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/CHANGELOG.md +16 -0
- package/dist/commands/cloud.js +24 -15
- package/dist/commands/mcp-registration.d.ts +8 -0
- package/dist/commands/mcp-registration.js +29 -4
- package/dist/commands/pack-install.js +11 -3
- package/dist/commands/reindex.d.ts +15 -0
- package/dist/commands/reindex.js +120 -0
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +28 -2
- package/dist/lib/core-utils/timeout-guard.d.ts +4 -1
- package/dist/lib/core-utils/timeout-guard.js +12 -3
- package/dist/lib/flag-registry-worker.d.ts +3 -0
- package/dist/lib/flag-registry-worker.js +26 -0
- package/dist/lib/flag-registry.d.ts +44 -0
- package/dist/lib/flag-registry.js +85 -0
- package/dist/lib/narrow-hint-banner.d.ts +12 -1
- package/dist/lib/narrow-hint-banner.js +45 -3
- package/dist/lib/plan-limit-nag.d.ts +3 -0
- package/dist/lib/plan-limit-nag.js +18 -1
- package/dist/main.js +11 -2
- package/dist/run/hq-plugin.js +1 -1
- package/dist/utils/client-health-contract.d.ts +30 -0
- package/dist/utils/client-health-contract.js +37 -0
- package/dist/utils/client-health.d.ts +79 -2
- package/dist/utils/client-health.js +208 -6
- package/dist/utils/local-files-overview.d.ts +58 -0
- package/dist/utils/local-files-overview.js +170 -0
- package/dist/utils/secrets-cache.d.ts +8 -2
- package/dist/utils/secrets-cache.js +150 -34
- package/package.json +2 -1
|
@@ -6,9 +6,13 @@ const CACHE_DIR = path.join(os.homedir(), ".hq", "secrets-cache");
|
|
|
6
6
|
const KEY_PATH = path.join(CACHE_DIR, ".key");
|
|
7
7
|
const CACHE_FORMAT_MAGIC = Buffer.from("HQSC");
|
|
8
8
|
const CACHE_FORMAT_MAGIC_BYTES = CACHE_FORMAT_MAGIC.length;
|
|
9
|
+
const CACHE_VERSION_MARKER = Buffer.from("HQSCV2\0");
|
|
10
|
+
const LEGACY_CACHE_VERSION_MARKER = Buffer.from("HQSCV1\0");
|
|
11
|
+
const CACHE_VERSION_MARKER_BYTES = CACHE_VERSION_MARKER.length;
|
|
9
12
|
const LEGACY_TIMESTAMP_BYTES = 8;
|
|
10
13
|
const TIMESTAMP_BYTES = 8;
|
|
11
14
|
const TTL_BYTES = 8;
|
|
15
|
+
const CACHE_VERSION_BYTES = 8;
|
|
12
16
|
const ALGORITHM = "aes-256-gcm";
|
|
13
17
|
const IV_BYTES = 12;
|
|
14
18
|
const AUTH_TAG_BYTES = 16;
|
|
@@ -45,7 +49,94 @@ function getOrCreateKey() {
|
|
|
45
49
|
fs.renameSync(tmpPath, KEY_PATH);
|
|
46
50
|
return fs.readFileSync(KEY_PATH);
|
|
47
51
|
}
|
|
48
|
-
|
|
52
|
+
function isCacheVersion(value) {
|
|
53
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
54
|
+
}
|
|
55
|
+
function readCacheVersion(raw, offset) {
|
|
56
|
+
const value = Number(raw.readBigInt64BE(offset));
|
|
57
|
+
if (value === 0)
|
|
58
|
+
return undefined;
|
|
59
|
+
return isCacheVersion(value) ? value : null;
|
|
60
|
+
}
|
|
61
|
+
function cacheAad(header, companyUid, name) {
|
|
62
|
+
const company = Buffer.from(companyUid, "utf8");
|
|
63
|
+
const secretName = Buffer.from(name, "utf8");
|
|
64
|
+
const companyLength = Buffer.alloc(4);
|
|
65
|
+
const nameLength = Buffer.alloc(4);
|
|
66
|
+
companyLength.writeUInt32BE(company.length);
|
|
67
|
+
nameLength.writeUInt32BE(secretName.length);
|
|
68
|
+
return Buffer.concat([
|
|
69
|
+
Buffer.from("HQSC-AAD-V2\0"),
|
|
70
|
+
header,
|
|
71
|
+
companyLength,
|
|
72
|
+
company,
|
|
73
|
+
nameLength,
|
|
74
|
+
secretName,
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
function parseCacheHeader(raw) {
|
|
78
|
+
if (raw.subarray(0, CACHE_FORMAT_MAGIC_BYTES).equals(CACHE_FORMAT_MAGIC)) {
|
|
79
|
+
const baseHeaderBytes = CACHE_FORMAT_MAGIC_BYTES + TIMESTAMP_BYTES + TTL_BYTES;
|
|
80
|
+
if (raw.length < baseHeaderBytes + IV_BYTES + AUTH_TAG_BYTES)
|
|
81
|
+
return null;
|
|
82
|
+
let ivStart = baseHeaderBytes;
|
|
83
|
+
let version;
|
|
84
|
+
let identityAuthenticated = false;
|
|
85
|
+
if (raw.length >=
|
|
86
|
+
baseHeaderBytes + CACHE_VERSION_MARKER_BYTES + CACHE_VERSION_BYTES + IV_BYTES + AUTH_TAG_BYTES &&
|
|
87
|
+
raw
|
|
88
|
+
.subarray(baseHeaderBytes, baseHeaderBytes + CACHE_VERSION_MARKER_BYTES)
|
|
89
|
+
.equals(CACHE_VERSION_MARKER)) {
|
|
90
|
+
const parsedVersion = readCacheVersion(raw, baseHeaderBytes + CACHE_VERSION_MARKER_BYTES);
|
|
91
|
+
if (parsedVersion === null)
|
|
92
|
+
return null;
|
|
93
|
+
version = parsedVersion;
|
|
94
|
+
ivStart += CACHE_VERSION_MARKER_BYTES + CACHE_VERSION_BYTES;
|
|
95
|
+
identityAuthenticated = true;
|
|
96
|
+
}
|
|
97
|
+
else if (raw.length >=
|
|
98
|
+
baseHeaderBytes + LEGACY_CACHE_VERSION_MARKER.length + CACHE_VERSION_BYTES + IV_BYTES + AUTH_TAG_BYTES &&
|
|
99
|
+
raw
|
|
100
|
+
.subarray(baseHeaderBytes, baseHeaderBytes + LEGACY_CACHE_VERSION_MARKER.length)
|
|
101
|
+
.equals(LEGACY_CACHE_VERSION_MARKER)) {
|
|
102
|
+
const parsedVersion = readCacheVersion(raw, baseHeaderBytes + LEGACY_CACHE_VERSION_MARKER.length);
|
|
103
|
+
if (parsedVersion === null)
|
|
104
|
+
return null;
|
|
105
|
+
version = parsedVersion;
|
|
106
|
+
ivStart += LEGACY_CACHE_VERSION_MARKER.length + CACHE_VERSION_BYTES;
|
|
107
|
+
}
|
|
108
|
+
const authTagStart = ivStart + IV_BYTES;
|
|
109
|
+
const ciphertextStart = authTagStart + AUTH_TAG_BYTES;
|
|
110
|
+
if (raw.length < ciphertextStart)
|
|
111
|
+
return null;
|
|
112
|
+
return {
|
|
113
|
+
timestampMs: Number(raw.readBigInt64BE(CACHE_FORMAT_MAGIC_BYTES)),
|
|
114
|
+
ttlMs: Number(raw.readBigInt64BE(CACHE_FORMAT_MAGIC_BYTES + TIMESTAMP_BYTES)),
|
|
115
|
+
version,
|
|
116
|
+
identityAuthenticated,
|
|
117
|
+
ivStart,
|
|
118
|
+
authTagStart,
|
|
119
|
+
ciphertextStart,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const headerLen = LEGACY_TIMESTAMP_BYTES + IV_BYTES + AUTH_TAG_BYTES;
|
|
123
|
+
if (raw.length < headerLen)
|
|
124
|
+
return null;
|
|
125
|
+
return {
|
|
126
|
+
timestampMs: Number(raw.readBigInt64BE(0)),
|
|
127
|
+
ttlMs: DEFAULT_SECRETS_CACHE_TTL_MS,
|
|
128
|
+
identityAuthenticated: false,
|
|
129
|
+
ivStart: LEGACY_TIMESTAMP_BYTES,
|
|
130
|
+
authTagStart: LEGACY_TIMESTAMP_BYTES + IV_BYTES,
|
|
131
|
+
ciphertextStart: headerLen,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function isExpired(header, maxAgeMs) {
|
|
135
|
+
const ttlMs = maxAgeMs === undefined ? header.ttlMs : Math.min(header.ttlMs, maxAgeMs);
|
|
136
|
+
const ageMs = Date.now() - header.timestampMs;
|
|
137
|
+
return !Number.isFinite(ttlMs) || ttlMs <= 0 || ageMs < 0 || ageMs > ttlMs;
|
|
138
|
+
}
|
|
139
|
+
export function readCache(companyUid, name, expectedVersion, maxAgeMs) {
|
|
49
140
|
if (!validateInputs(companyUid, name))
|
|
50
141
|
return null;
|
|
51
142
|
const filePath = path.join(CACHE_DIR, companyUid, name);
|
|
@@ -56,40 +147,21 @@ export function readCache(companyUid, name) {
|
|
|
56
147
|
catch {
|
|
57
148
|
return null;
|
|
58
149
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
CACHE_FORMAT_MAGIC_BYTES + TIMESTAMP_BYTES + TTL_BYTES + IV_BYTES + AUTH_TAG_BYTES &&
|
|
66
|
-
raw.subarray(0, CACHE_FORMAT_MAGIC_BYTES).equals(CACHE_FORMAT_MAGIC)) {
|
|
67
|
-
timestampMs = Number(raw.readBigInt64BE(CACHE_FORMAT_MAGIC_BYTES));
|
|
68
|
-
ttlMs = Number(raw.readBigInt64BE(CACHE_FORMAT_MAGIC_BYTES + TIMESTAMP_BYTES));
|
|
69
|
-
ivStart = CACHE_FORMAT_MAGIC_BYTES + TIMESTAMP_BYTES + TTL_BYTES;
|
|
70
|
-
authTagStart = ivStart + IV_BYTES;
|
|
71
|
-
ciphertextStart = authTagStart + AUTH_TAG_BYTES;
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
const headerLen = LEGACY_TIMESTAMP_BYTES + IV_BYTES + AUTH_TAG_BYTES;
|
|
75
|
-
if (raw.length < headerLen)
|
|
76
|
-
return null;
|
|
77
|
-
timestampMs = Number(raw.readBigInt64BE(0));
|
|
78
|
-
ttlMs = DEFAULT_SECRETS_CACHE_TTL_MS;
|
|
79
|
-
ivStart = LEGACY_TIMESTAMP_BYTES;
|
|
80
|
-
authTagStart = ivStart + IV_BYTES;
|
|
81
|
-
ciphertextStart = authTagStart + AUTH_TAG_BYTES;
|
|
82
|
-
}
|
|
83
|
-
if (ttlMs <= 0 || Date.now() - timestampMs > ttlMs) {
|
|
150
|
+
const header = parseCacheHeader(raw);
|
|
151
|
+
if (!header)
|
|
152
|
+
return null;
|
|
153
|
+
if (!header.identityAuthenticated ||
|
|
154
|
+
isExpired(header, maxAgeMs) ||
|
|
155
|
+
(expectedVersion !== undefined && header.version !== expectedVersion)) {
|
|
84
156
|
try {
|
|
85
157
|
fs.unlinkSync(filePath);
|
|
86
158
|
}
|
|
87
159
|
catch { /* ok */ }
|
|
88
160
|
return null;
|
|
89
161
|
}
|
|
90
|
-
const iv = raw.subarray(ivStart, authTagStart);
|
|
91
|
-
const authTag = raw.subarray(authTagStart, ciphertextStart);
|
|
92
|
-
const ciphertext = raw.subarray(ciphertextStart);
|
|
162
|
+
const iv = raw.subarray(header.ivStart, header.authTagStart);
|
|
163
|
+
const authTag = raw.subarray(header.authTagStart, header.ciphertextStart);
|
|
164
|
+
const ciphertext = raw.subarray(header.ciphertextStart);
|
|
93
165
|
let key;
|
|
94
166
|
try {
|
|
95
167
|
key = getOrCreateKey();
|
|
@@ -99,6 +171,7 @@ export function readCache(companyUid, name) {
|
|
|
99
171
|
}
|
|
100
172
|
try {
|
|
101
173
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
174
|
+
decipher.setAAD(cacheAad(raw.subarray(0, header.ivStart), companyUid, name));
|
|
102
175
|
decipher.setAuthTag(authTag);
|
|
103
176
|
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
104
177
|
return decrypted.toString("utf8");
|
|
@@ -111,7 +184,35 @@ export function readCache(companyUid, name) {
|
|
|
111
184
|
return null;
|
|
112
185
|
}
|
|
113
186
|
}
|
|
114
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Returns whether an unexpired entry exists without decrypting its plaintext.
|
|
189
|
+
* Callers use this only to decide whether to reauthorize a possible cache hit;
|
|
190
|
+
* they must complete that authorization before calling {@link readCache}.
|
|
191
|
+
*/
|
|
192
|
+
export function hasCacheEntry(companyUid, name) {
|
|
193
|
+
if (!validateInputs(companyUid, name))
|
|
194
|
+
return false;
|
|
195
|
+
const filePath = path.join(CACHE_DIR, companyUid, name);
|
|
196
|
+
let raw;
|
|
197
|
+
try {
|
|
198
|
+
raw = fs.readFileSync(filePath);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const header = parseCacheHeader(raw);
|
|
204
|
+
if (!header)
|
|
205
|
+
return false;
|
|
206
|
+
if (!header.identityAuthenticated || isExpired(header)) {
|
|
207
|
+
try {
|
|
208
|
+
fs.unlinkSync(filePath);
|
|
209
|
+
}
|
|
210
|
+
catch { /* ok */ }
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
export function writeCache(companyUid, name, value, ttlMs = DEFAULT_SECRETS_CACHE_TTL_MS, version) {
|
|
115
216
|
try {
|
|
116
217
|
if (!validateInputs(companyUid, name))
|
|
117
218
|
return;
|
|
@@ -120,14 +221,29 @@ export function writeCache(companyUid, name, value, ttlMs = DEFAULT_SECRETS_CACH
|
|
|
120
221
|
ensureCacheDir(companyUid);
|
|
121
222
|
const key = getOrCreateKey();
|
|
122
223
|
const iv = crypto.randomBytes(IV_BYTES);
|
|
123
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
124
|
-
const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
125
|
-
const authTag = cipher.getAuthTag();
|
|
126
224
|
const timestamp = Buffer.alloc(8);
|
|
127
225
|
timestamp.writeBigInt64BE(BigInt(Date.now()));
|
|
128
226
|
const ttl = Buffer.alloc(8);
|
|
129
227
|
ttl.writeBigInt64BE(BigInt(ttlMs));
|
|
130
|
-
const
|
|
228
|
+
const versionBuffer = Buffer.alloc(CACHE_VERSION_BYTES);
|
|
229
|
+
versionBuffer.writeBigInt64BE(BigInt(isCacheVersion(version) ? version : 0));
|
|
230
|
+
const header = Buffer.concat([
|
|
231
|
+
CACHE_FORMAT_MAGIC,
|
|
232
|
+
timestamp,
|
|
233
|
+
ttl,
|
|
234
|
+
CACHE_VERSION_MARKER,
|
|
235
|
+
versionBuffer,
|
|
236
|
+
]);
|
|
237
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
238
|
+
cipher.setAAD(cacheAad(header, companyUid, name));
|
|
239
|
+
const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
240
|
+
const authTag = cipher.getAuthTag();
|
|
241
|
+
const out = Buffer.concat([
|
|
242
|
+
header,
|
|
243
|
+
iv,
|
|
244
|
+
authTag,
|
|
245
|
+
encrypted,
|
|
246
|
+
]);
|
|
131
247
|
const filePath = path.join(CACHE_DIR, companyUid, name);
|
|
132
248
|
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
133
249
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.108.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
32
32
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
33
33
|
"@indigoai-us/hq-cloud": "~6.16.6",
|
|
34
|
+
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
34
35
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
35
36
|
"@sentry/node": "^10.49.0",
|
|
36
37
|
"@tobilu/qmd": "2.5.3",
|