@indigoai-us/hq-cli 5.345.154 → 5.345.155
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 +80 -0
- package/dist/command-catalog.generated.d.ts +54 -6
- package/dist/command-catalog.generated.js +69 -6
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +92 -3
- package/dist/commands/library-add.d.ts +6 -2
- package/dist/commands/library-add.js +45 -6
- package/dist/commands/library-caption.d.ts +38 -1
- package/dist/commands/library-caption.js +304 -3
- package/dist/commands/library-link.d.ts +7 -3
- package/dist/commands/library-link.js +10 -4
- package/dist/commands/library-remove.d.ts +122 -0
- package/dist/commands/library-remove.js +681 -0
- package/dist/commands/library.d.ts +26 -1
- package/dist/commands/library.js +101 -14
- package/dist/commands/secrets.js +72 -5
- package/dist/commands/storage-offload.d.ts +22 -4
- package/dist/commands/storage-offload.js +196 -2
- package/dist/commands/storage.js +5 -0
- package/dist/lib/daemon/machine-login.d.ts +20 -0
- package/dist/lib/daemon/machine-login.js +53 -8
- package/dist/lib/daemon/machine-token-identity.d.ts +34 -0
- package/dist/lib/daemon/machine-token-identity.js +123 -0
- package/dist/lib/lanes/registry-flag.d.ts +23 -2
- package/dist/lib/lanes/registry-flag.js +256 -36
- package/dist/lib/personal-secrets-audit.d.ts +31 -0
- package/dist/lib/personal-secrets-audit.js +45 -0
- package/package.json +7 -7
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createFlagClient, readLocalOverride, } from "@indigoai-us/hq-flags-client";
|
|
2
|
-
import
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { ensureCognitoIdToken, loadCachedTokens, refreshActivePersonTokens, wantsMachineCognitoTokens, } from "../../utils/cognito-session.js";
|
|
3
5
|
import { FLAG_REGISTRY_DEFAULT_ENDPOINT, resolveFlagRegistryEndpoint, } from "../flag-registry-endpoint.js";
|
|
4
6
|
export const LANES_FLAG_API_DEFAULT_ENDPOINT = FLAG_REGISTRY_DEFAULT_ENDPOINT;
|
|
5
7
|
export const LANES_FLAG_READ_TIMEOUT_MS = 5_000;
|
|
@@ -15,15 +17,118 @@ class MissingFlagTokenError extends Error {
|
|
|
15
17
|
this.name = "MissingFlagTokenError";
|
|
16
18
|
}
|
|
17
19
|
}
|
|
20
|
+
export const LANES_FLAG_RETRY_BACKOFF_MS = 250;
|
|
21
|
+
export const LANES_FLAG_CACHE_FILE = "lanes-flag-cache.json";
|
|
22
|
+
const FLAG_CACHE_MAX_ENTRIES = 256;
|
|
23
|
+
/**
|
|
24
|
+
* Oldest last-known-good entry a failed read may serve. Every successful
|
|
25
|
+
* snapshot read restamps resolvedAt, so this bounds how long a value can
|
|
26
|
+
* outlive its last confirmation by the registry.
|
|
27
|
+
*/
|
|
28
|
+
export const LANES_FLAG_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
29
|
+
/**
|
|
30
|
+
* HQ state dir from the caller's env (`HQ_STATE_DIR`, else `<home>/.hq`).
|
|
31
|
+
* Returns null when the env names neither, so a hermetic env never touches
|
|
32
|
+
* the real home directory.
|
|
33
|
+
*/
|
|
34
|
+
function flagCacheDir(options) {
|
|
35
|
+
if (options.cacheDir !== undefined)
|
|
36
|
+
return options.cacheDir;
|
|
37
|
+
const env = options.env ?? process.env;
|
|
38
|
+
const stateDir = env.HQ_STATE_DIR?.trim();
|
|
39
|
+
if (stateDir)
|
|
40
|
+
return stateDir;
|
|
41
|
+
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
|
|
42
|
+
return home ? path.join(home, ".hq") : null;
|
|
43
|
+
}
|
|
44
|
+
function readFlagCacheFile(dir) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, LANES_FLAG_CACHE_FILE), "utf8"));
|
|
47
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
48
|
+
return {};
|
|
49
|
+
const entries = parsed.entries;
|
|
50
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries))
|
|
51
|
+
return {};
|
|
52
|
+
return entries;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function readCachedFlagValue(dir, key, now = Date.now()) {
|
|
59
|
+
const entry = readFlagCacheFile(dir)[key];
|
|
60
|
+
if (!entry || typeof entry.value !== "boolean" || typeof entry.resolvedAt !== "string")
|
|
61
|
+
return null;
|
|
62
|
+
const resolvedAt = Date.parse(entry.resolvedAt);
|
|
63
|
+
if (!Number.isFinite(resolvedAt))
|
|
64
|
+
return null;
|
|
65
|
+
const age = now - resolvedAt;
|
|
66
|
+
// A future timestamp (clock skew or a hand-edited file) is not trusted either.
|
|
67
|
+
if (age < 0 || age > LANES_FLAG_CACHE_MAX_AGE_MS)
|
|
68
|
+
return null;
|
|
69
|
+
return entry.value;
|
|
70
|
+
}
|
|
71
|
+
function writeCachedFlagValue(dir, key, value) {
|
|
72
|
+
let tmp;
|
|
73
|
+
try {
|
|
74
|
+
const entries = readFlagCacheFile(dir);
|
|
75
|
+
// Always restamp resolvedAt, even for an unchanged value: the max-age check
|
|
76
|
+
// measures time since the registry last confirmed it. Delete first so the
|
|
77
|
+
// key moves to the end and eviction drops the least recently confirmed.
|
|
78
|
+
delete entries[key];
|
|
79
|
+
entries[key] = { value, resolvedAt: new Date().toISOString() };
|
|
80
|
+
const keys = Object.keys(entries);
|
|
81
|
+
for (const stale of keys.slice(0, Math.max(0, keys.length - FLAG_CACHE_MAX_ENTRIES)))
|
|
82
|
+
delete entries[stale];
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
84
|
+
const target = path.join(dir, LANES_FLAG_CACHE_FILE);
|
|
85
|
+
tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
86
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ version: 1, entries })}\n`, { mode: 0o600 });
|
|
87
|
+
fs.renameSync(tmp, target);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
if (tmp) {
|
|
91
|
+
try {
|
|
92
|
+
fs.unlinkSync(tmp);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Nothing to clean up.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// The cache is best-effort; a read must never fail because it cannot be saved.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const TOKENISH_PATTERNS = [
|
|
102
|
+
/bearer\s+\S+/gi,
|
|
103
|
+
/eyJ[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*/g,
|
|
104
|
+
/\b(?:hq|sk|pk|ghp|gho|xox[a-z])[_-][A-Za-z0-9_-]{8,}/gi,
|
|
105
|
+
/[A-Za-z0-9+/_-]{32,}={0,2}/g,
|
|
106
|
+
];
|
|
107
|
+
/** Error message safe for stderr: token-like substrings removed, length bounded. */
|
|
108
|
+
export function sanitizeFlagErrorMessage(error) {
|
|
109
|
+
const raw = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
110
|
+
let message = raw;
|
|
111
|
+
for (const pattern of TOKENISH_PATTERNS)
|
|
112
|
+
message = message.replace(pattern, "[redacted]");
|
|
113
|
+
return message.length > 200 ? `${message.slice(0, 200)}...` : message;
|
|
114
|
+
}
|
|
18
115
|
function errorClass(error) {
|
|
19
116
|
return error instanceof Error ? error.name : "UnknownError";
|
|
20
117
|
}
|
|
21
|
-
function reportReadFailure(flagKey, error, fallback) {
|
|
118
|
+
function reportReadFailure(flagKey, error, fallback, diagnostics) {
|
|
22
119
|
const suffix = flagKey.replace(/^lanes\./, "").replaceAll(/[^a-zA-Z0-9]+/g, "_");
|
|
23
120
|
process.stderr.write(`${JSON.stringify({
|
|
24
121
|
event: `lanes.${suffix}.flag_read_failed`,
|
|
25
122
|
flag: flagKey,
|
|
26
123
|
error: errorClass(error),
|
|
124
|
+
error_message: sanitizeFlagErrorMessage(error),
|
|
125
|
+
...(diagnostics?.status !== undefined ? { status: diagnostics.status } : {}),
|
|
126
|
+
...(diagnostics ? {
|
|
127
|
+
duration_ms: diagnostics.durationMs,
|
|
128
|
+
attempts: diagnostics.attempts,
|
|
129
|
+
token_refreshed: diagnostics.tokenRefreshed,
|
|
130
|
+
cache_used: diagnostics.cacheUsed,
|
|
131
|
+
} : {}),
|
|
27
132
|
fallback,
|
|
28
133
|
})}\n`);
|
|
29
134
|
}
|
|
@@ -47,8 +152,11 @@ function cacheKey(flagKey, company, endpoint) {
|
|
|
47
152
|
}
|
|
48
153
|
/**
|
|
49
154
|
* Read a lanes flag once for each company identity in this CLI process. The
|
|
50
|
-
* cache
|
|
51
|
-
*
|
|
155
|
+
* in-process cache holds the in-flight or settled result. The last registry
|
|
156
|
+
* snapshot value per flag and company is also kept in the HQ state dir and
|
|
157
|
+
* used when a later read fails transiently (timeout, network error, 5xx) and
|
|
158
|
+
* the value was confirmed within LANES_FLAG_CACHE_MAX_AGE_MS. A local override
|
|
159
|
+
* is checked on every call and wins first.
|
|
52
160
|
*/
|
|
53
161
|
export async function readLanesRegistryFlag(flagKey, options = {}) {
|
|
54
162
|
const result = await readLanesRegistryFlagResult(flagKey, options);
|
|
@@ -72,7 +180,7 @@ async function readRegistryFlagOutcome(flagKey, options, failOpen) {
|
|
|
72
180
|
const endpoint = resolveFlagRegistryEndpoint(undefined, env);
|
|
73
181
|
const key = cacheKey(flagKey, options.company, endpoint);
|
|
74
182
|
const cached = registryReads.get(key);
|
|
75
|
-
const pending = cached ?? readRegistryValue(flagKey, endpoint, options);
|
|
183
|
+
const pending = cached ?? readRegistryValue(flagKey, endpoint, key, options);
|
|
76
184
|
if (!cached)
|
|
77
185
|
registryReads.set(key, pending);
|
|
78
186
|
const outcome = await pending;
|
|
@@ -108,42 +216,89 @@ export async function readLanesRegistryFlagFailOff(flagKey, options = {}, reader
|
|
|
108
216
|
export function _resetThrownFlagReadWarningsForTests() {
|
|
109
217
|
reportedThrownFlagReads.clear();
|
|
110
218
|
}
|
|
111
|
-
|
|
219
|
+
function isTimeoutError(error) {
|
|
220
|
+
if (!(error instanceof Error))
|
|
221
|
+
return false;
|
|
222
|
+
return error.name === "AbortError" || error.name === "TimeoutError" ||
|
|
223
|
+
error.message.startsWith("hq-flags: request timed out");
|
|
224
|
+
}
|
|
225
|
+
function classifyAttempt(attempt) {
|
|
226
|
+
if (!attempt.failure)
|
|
227
|
+
return "none";
|
|
228
|
+
const status = attempt.status;
|
|
229
|
+
if (status !== undefined && status >= 400) {
|
|
230
|
+
if (status === 401)
|
|
231
|
+
return "unauthorized";
|
|
232
|
+
if (status >= 500)
|
|
233
|
+
return "transient";
|
|
234
|
+
return "permanent";
|
|
235
|
+
}
|
|
236
|
+
if (attempt.networkError || isTimeoutError(attempt.failure.error))
|
|
237
|
+
return "transient";
|
|
238
|
+
return "permanent";
|
|
239
|
+
}
|
|
240
|
+
async function defaultRefreshToken() {
|
|
241
|
+
if (wantsMachineCognitoTokens()) {
|
|
242
|
+
return ensureCognitoIdToken({ interactive: false });
|
|
243
|
+
}
|
|
244
|
+
const cached = loadCachedTokens();
|
|
245
|
+
if (!cached)
|
|
246
|
+
throw new MissingFlagTokenError();
|
|
247
|
+
const refreshed = await refreshActivePersonTokens(cached);
|
|
248
|
+
return refreshed.idToken;
|
|
249
|
+
}
|
|
250
|
+
function defaultAcquireToken() {
|
|
251
|
+
return async () => {
|
|
252
|
+
try {
|
|
253
|
+
return await ensureCognitoIdToken({ interactive: false });
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (error instanceof Error &&
|
|
257
|
+
error.message.startsWith("No valid HQ session")) {
|
|
258
|
+
throw new MissingFlagTokenError();
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function requireToken(token) {
|
|
265
|
+
if (typeof token !== "string" || token.trim().length === 0) {
|
|
266
|
+
throw new MissingFlagTokenError();
|
|
267
|
+
}
|
|
268
|
+
return token;
|
|
269
|
+
}
|
|
270
|
+
async function readRegistryAttempt(flagKey, endpoint, options, acquireToken) {
|
|
112
271
|
const company = options.company;
|
|
113
272
|
const companyUid = company?.uid?.trim();
|
|
114
273
|
const companySlug = company?.slug?.trim();
|
|
274
|
+
const baseFetch = options.fetch ?? globalThis.fetch;
|
|
115
275
|
let client;
|
|
116
|
-
|
|
117
|
-
let failure;
|
|
276
|
+
const attempt = { networkError: false };
|
|
118
277
|
const rememberFailure = (error) => {
|
|
119
|
-
failure ??= { error };
|
|
278
|
+
attempt.failure ??= { error };
|
|
120
279
|
};
|
|
121
280
|
try {
|
|
122
|
-
const acquireToken = options.getToken ?? (async () => {
|
|
123
|
-
try {
|
|
124
|
-
return await ensureCognitoIdToken({ interactive: false });
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
if (error instanceof Error &&
|
|
128
|
-
error.message.startsWith("No valid HQ session")) {
|
|
129
|
-
throw new MissingFlagTokenError();
|
|
130
|
-
}
|
|
131
|
-
throw error;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
const getToken = async () => {
|
|
135
|
-
const token = await acquireToken();
|
|
136
|
-
if (typeof token !== "string" || token.trim().length === 0) {
|
|
137
|
-
throw new MissingFlagTokenError();
|
|
138
|
-
}
|
|
139
|
-
return token;
|
|
140
|
-
};
|
|
141
281
|
client = (options.createClient ?? createFlagClient)({
|
|
142
282
|
endpoint,
|
|
143
|
-
getToken,
|
|
283
|
+
getToken: async () => requireToken(await acquireToken()),
|
|
144
284
|
env: {},
|
|
145
285
|
refreshIntervalMs: 0,
|
|
146
286
|
requestTimeoutMs: LANES_FLAG_READ_TIMEOUT_MS,
|
|
287
|
+
...(baseFetch
|
|
288
|
+
? {
|
|
289
|
+
fetch: async (input, init) => {
|
|
290
|
+
try {
|
|
291
|
+
const res = await baseFetch(input, init);
|
|
292
|
+
attempt.status = res.status;
|
|
293
|
+
return res;
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
attempt.networkError = true;
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
: {}),
|
|
147
302
|
...(companyUid ? { companyUid } : {}),
|
|
148
303
|
...(companySlug ? { companyIdentifiers: [companySlug] } : {}),
|
|
149
304
|
onError: rememberFailure,
|
|
@@ -155,7 +310,7 @@ async function readRegistryValue(flagKey, endpoint, options) {
|
|
|
155
310
|
rememberFailure(error);
|
|
156
311
|
}
|
|
157
312
|
try {
|
|
158
|
-
resolution = client.explain(flagKey, { fallback: options.defaultValue ?? false });
|
|
313
|
+
attempt.resolution = client.explain(flagKey, { fallback: options.defaultValue ?? false });
|
|
159
314
|
}
|
|
160
315
|
catch (error) {
|
|
161
316
|
rememberFailure(error);
|
|
@@ -172,12 +327,77 @@ async function readRegistryValue(flagKey, endpoint, options) {
|
|
|
172
327
|
rememberFailure(error);
|
|
173
328
|
}
|
|
174
329
|
}
|
|
175
|
-
|
|
176
|
-
|
|
330
|
+
return attempt;
|
|
331
|
+
}
|
|
332
|
+
function isSnapshotResolution(resolution) {
|
|
333
|
+
return resolution?.source === "snapshot" || resolution?.source === "local-override";
|
|
334
|
+
}
|
|
335
|
+
async function readRegistryValue(flagKey, endpoint, key, options) {
|
|
336
|
+
const startedAt = Date.now();
|
|
337
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
338
|
+
let acquireToken = options.getToken ?? defaultAcquireToken();
|
|
339
|
+
let attempt = await readRegistryAttempt(flagKey, endpoint, options, acquireToken);
|
|
340
|
+
let attempts = 1;
|
|
341
|
+
let tokenRefreshed = false;
|
|
342
|
+
if (!isSnapshotResolution(attempt.resolution)) {
|
|
343
|
+
const kind = classifyAttempt(attempt);
|
|
344
|
+
if (kind === "transient") {
|
|
345
|
+
await sleep(options.retryBackoffMs ?? LANES_FLAG_RETRY_BACKOFF_MS);
|
|
346
|
+
attempt = await readRegistryAttempt(flagKey, endpoint, options, acquireToken);
|
|
347
|
+
attempts += 1;
|
|
348
|
+
}
|
|
349
|
+
else if (kind === "unauthorized") {
|
|
350
|
+
const refresh = options.refreshToken ?? (options.getToken ? options.getToken : defaultRefreshToken);
|
|
351
|
+
let refreshedToken;
|
|
352
|
+
try {
|
|
353
|
+
refreshedToken = requireToken(await refresh());
|
|
354
|
+
tokenRefreshed = true;
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Keep the original 401 as the reported failure.
|
|
358
|
+
}
|
|
359
|
+
if (refreshedToken !== undefined) {
|
|
360
|
+
const token = refreshedToken;
|
|
361
|
+
acquireToken = () => token;
|
|
362
|
+
attempt = await readRegistryAttempt(flagKey, endpoint, options, acquireToken);
|
|
363
|
+
attempts += 1;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const durationMs = Date.now() - startedAt;
|
|
368
|
+
const diagnostics = {
|
|
369
|
+
...(attempt.status !== undefined ? { status: attempt.status } : {}),
|
|
370
|
+
durationMs,
|
|
371
|
+
attempts,
|
|
372
|
+
tokenRefreshed,
|
|
373
|
+
cacheUsed: false,
|
|
374
|
+
};
|
|
375
|
+
const cacheDir = flagCacheDir(options);
|
|
376
|
+
if (isSnapshotResolution(attempt.resolution)) {
|
|
377
|
+
const value = attempt.resolution.value;
|
|
378
|
+
if (attempt.resolution.source === "snapshot" && cacheDir)
|
|
379
|
+
writeCachedFlagValue(cacheDir, key, value);
|
|
380
|
+
return {
|
|
381
|
+
result: { state: "resolved", value },
|
|
382
|
+
...(attempt.failure ? { failure: { ...attempt.failure, diagnostics } } : {}),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (attempt.failure) {
|
|
386
|
+
// Only a transient failure (timeout, network error, 5xx) may serve the
|
|
387
|
+
// last-known-good value. A 401 that survived the refresh, a 403 or other
|
|
388
|
+
// 4xx, and a missing token mean the registry (or the session) refused the
|
|
389
|
+
// read, so it fails closed to the caller's default instead.
|
|
390
|
+
const transient = classifyAttempt(attempt) === "transient";
|
|
391
|
+
const cachedValue = transient && cacheDir ? readCachedFlagValue(cacheDir, key) : null;
|
|
392
|
+
if (cachedValue !== null) {
|
|
393
|
+
return {
|
|
394
|
+
result: { state: "resolved", value: cachedValue },
|
|
395
|
+
failure: { ...attempt.failure, diagnostics: { ...diagnostics, cacheUsed: true } },
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return { result: { state: "unknown" }, failure: { ...attempt.failure, diagnostics } };
|
|
177
399
|
}
|
|
178
|
-
|
|
179
|
-
return { result: { state: "unknown" }, failure };
|
|
180
|
-
return { result: { state: "resolved", value: resolution?.value ?? false } };
|
|
400
|
+
return { result: { state: "resolved", value: attempt.resolution?.value ?? false } };
|
|
181
401
|
}
|
|
182
402
|
function reportRegistryFailure(flagKey, key, outcome, options, failOpen) {
|
|
183
403
|
if (!outcome.failure)
|
|
@@ -194,7 +414,7 @@ function reportRegistryFailure(flagKey, key, outcome, options, failOpen) {
|
|
|
194
414
|
reportFailure(outcome.failure.error);
|
|
195
415
|
return;
|
|
196
416
|
}
|
|
197
|
-
reportReadFailure(flagKey, outcome.failure.error, fallback);
|
|
417
|
+
reportReadFailure(flagKey, outcome.failure.error, fallback, outcome.failure.diagnostics);
|
|
198
418
|
}
|
|
199
419
|
export function _resetLanesRegistryFlagCacheForTests() {
|
|
200
420
|
registryReads.clear();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Personal vault use from a company-locked session.
|
|
3
|
+
*
|
|
4
|
+
* Owner decision dq_20261020234444_f1b1ea4185: a session locked to one company
|
|
5
|
+
* may read the caller's own personal-vault secrets through `hq secrets exec
|
|
6
|
+
* --personal`, read-only, and every use is audit-logged. All other personal
|
|
7
|
+
* vault operations stay refused in such a session.
|
|
8
|
+
*/
|
|
9
|
+
/** True for a person (prs_) or agent-self (agt_) vault uid used as a scope. */
|
|
10
|
+
export declare function isEntityScopeRef(ref: string): boolean;
|
|
11
|
+
/** Relative to the HQ root. One JSON object per line. */
|
|
12
|
+
export declare const PERSONAL_SECRETS_AUDIT_RELATIVE_PATH: string;
|
|
13
|
+
export declare const PERSONAL_SCOPE_READ_ONLY_MESSAGE = "Personal scope in a company-locked session is read-only: only `hq secrets exec --personal --only <NAMES> -- <command>` is allowed.";
|
|
14
|
+
export interface PersonalSecretsAuditEntry {
|
|
15
|
+
ts: string;
|
|
16
|
+
session_id: string;
|
|
17
|
+
locked_company: string;
|
|
18
|
+
command: "secrets exec";
|
|
19
|
+
scope: "personal";
|
|
20
|
+
secret_names: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function personalSecretsAuditPath(root: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Append one audit line before any value is read. Throws when the line cannot
|
|
25
|
+
* be written, so the caller fails closed instead of reading unlogged.
|
|
26
|
+
* Records secret names only, never values.
|
|
27
|
+
*/
|
|
28
|
+
export declare function appendPersonalSecretsAudit(root: string, entry: Omit<PersonalSecretsAuditEntry, "ts" | "command" | "scope"> & {
|
|
29
|
+
ts?: string;
|
|
30
|
+
}): PersonalSecretsAuditEntry;
|
|
31
|
+
//# sourceMappingURL=personal-secrets-audit.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Personal vault use from a company-locked session.
|
|
5
|
+
*
|
|
6
|
+
* Owner decision dq_20261020234444_f1b1ea4185: a session locked to one company
|
|
7
|
+
* may read the caller's own personal-vault secrets through `hq secrets exec
|
|
8
|
+
* --personal`, read-only, and every use is audit-logged. All other personal
|
|
9
|
+
* vault operations stay refused in such a session.
|
|
10
|
+
*/
|
|
11
|
+
/** True for a person (prs_) or agent-self (agt_) vault uid used as a scope. */
|
|
12
|
+
export function isEntityScopeRef(ref) {
|
|
13
|
+
return /^(prs|agt)_/.test(ref.trim());
|
|
14
|
+
}
|
|
15
|
+
/** Relative to the HQ root. One JSON object per line. */
|
|
16
|
+
export const PERSONAL_SECRETS_AUDIT_RELATIVE_PATH = path.join("workspace", "audit", "personal-secrets-use.jsonl");
|
|
17
|
+
export const PERSONAL_SCOPE_READ_ONLY_MESSAGE = "Personal scope in a company-locked session is read-only: only `hq secrets exec --personal --only <NAMES> -- <command>` is allowed.";
|
|
18
|
+
export function personalSecretsAuditPath(root) {
|
|
19
|
+
return path.join(root, PERSONAL_SECRETS_AUDIT_RELATIVE_PATH);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Append one audit line before any value is read. Throws when the line cannot
|
|
23
|
+
* be written, so the caller fails closed instead of reading unlogged.
|
|
24
|
+
* Records secret names only, never values.
|
|
25
|
+
*/
|
|
26
|
+
export function appendPersonalSecretsAudit(root, entry) {
|
|
27
|
+
const line = {
|
|
28
|
+
ts: entry.ts ?? new Date().toISOString(),
|
|
29
|
+
session_id: entry.session_id,
|
|
30
|
+
locked_company: entry.locked_company,
|
|
31
|
+
command: "secrets exec",
|
|
32
|
+
scope: "personal",
|
|
33
|
+
secret_names: [...entry.secret_names],
|
|
34
|
+
};
|
|
35
|
+
const file = personalSecretsAuditPath(root);
|
|
36
|
+
try {
|
|
37
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
38
|
+
fs.appendFileSync(file, `${JSON.stringify(line)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error(`Cannot write the personal secrets audit log (${PERSONAL_SECRETS_AUDIT_RELATIVE_PATH}); refusing to read personal secrets: ${error instanceof Error ? error.message : String(error)}`);
|
|
42
|
+
}
|
|
43
|
+
return line;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=personal-secrets-audit.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.155",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -114,11 +114,11 @@
|
|
|
114
114
|
"*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
|
|
115
115
|
},
|
|
116
116
|
"optionalDependencies": {
|
|
117
|
-
"@indigoai-us/hq-hook-win32-x64": "5.345.
|
|
118
|
-
"@indigoai-us/hq-hook-win32-arm64": "5.345.
|
|
119
|
-
"@indigoai-us/hq-hook-darwin-x64": "5.345.
|
|
120
|
-
"@indigoai-us/hq-hook-darwin-arm64": "5.345.
|
|
121
|
-
"@indigoai-us/hq-hook-linux-x64": "5.345.
|
|
122
|
-
"@indigoai-us/hq-hook-linux-arm64": "5.345.
|
|
117
|
+
"@indigoai-us/hq-hook-win32-x64": "5.345.155",
|
|
118
|
+
"@indigoai-us/hq-hook-win32-arm64": "5.345.155",
|
|
119
|
+
"@indigoai-us/hq-hook-darwin-x64": "5.345.155",
|
|
120
|
+
"@indigoai-us/hq-hook-darwin-arm64": "5.345.155",
|
|
121
|
+
"@indigoai-us/hq-hook-linux-x64": "5.345.155",
|
|
122
|
+
"@indigoai-us/hq-hook-linux-arm64": "5.345.155"
|
|
123
123
|
}
|
|
124
124
|
}
|