@indigoai-us/hq-cli 5.345.170 → 5.345.171
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 +55 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +132 -5
- package/dist/command-catalog.generated.js +164 -6
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +112 -3
- package/dist/commands/agent-kit.d.ts +1 -1
- package/dist/commands/agent-kit.js +3 -1
- package/dist/commands/agents.js +2 -1
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +137 -14
- package/dist/commands/cloud.d.ts +58 -1
- package/dist/commands/cloud.js +219 -5
- package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
- package/dist/commands/core-company-index-scope-flag.js +9 -2
- package/dist/commands/core-delegate-w1b.js +5 -2
- package/dist/commands/core-dm-bind-native.js +19 -4
- package/dist/commands/core-rebuild-index-fast.js +24 -6
- package/dist/commands/core.js +18 -6
- package/dist/commands/dm.d.ts +17 -3
- package/dist/commands/dm.js +121 -26
- package/dist/commands/integrations-api.d.ts +4 -0
- package/dist/commands/integrations-api.js +18 -2
- package/dist/commands/integrations-connect.d.ts +10 -3
- package/dist/commands/integrations-connect.js +70 -6
- package/dist/commands/integrations-core.d.ts +23 -0
- package/dist/commands/integrations-core.js +100 -0
- package/dist/commands/integrations-manage.js +8 -1
- package/dist/commands/integrations.js +49 -5
- package/dist/commands/lanes-loop.d.ts +20 -0
- package/dist/commands/lanes-loop.js +69 -11
- package/dist/commands/lanes.js +9 -1
- package/dist/commands/rescue.d.ts +9 -0
- package/dist/commands/rescue.js +31 -0
- package/dist/commands/run.js +115 -40
- package/dist/commands/scan.d.ts +5 -1
- package/dist/commands/scan.js +16 -4
- package/dist/commands/secrets.js +50 -9
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2367 -2109
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
- package/dist/lib/agent-kit/inbox-state.js +41 -20
- package/dist/lib/agent-kit/kit-config.d.ts +2 -0
- package/dist/lib/agent-kit/kit-config.js +6 -0
- package/dist/lib/agent-kit/mcp/tools.js +12 -5
- package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -3
- package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
- package/dist/lib/agent-kit/run/inbox.js +42 -10
- package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
- package/dist/lib/agent-kit/run/request-timeout.js +51 -0
- package/dist/lib/bot/api.d.ts +1 -1
- package/dist/lib/bot/config.d.ts +10 -0
- package/dist/lib/bot/config.js +17 -1
- package/dist/lib/bot/fleet-local.d.ts +127 -0
- package/dist/lib/bot/fleet-local.js +638 -0
- package/dist/lib/bot/index.d.ts +1 -0
- package/dist/lib/bot/index.js +1 -0
- package/dist/lib/bot/local-config.d.ts +3 -2
- package/dist/lib/bot/local-config.js +5 -2
- package/dist/lib/bot/promote.d.ts +10 -0
- package/dist/lib/bot/promote.js +3 -2
- package/dist/lib/bot/runtime/index.js +8 -4
- package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
- package/dist/lib/bot/runtime/scratch-dir.js +41 -0
- package/dist/lib/caller-principal.d.ts +4 -0
- package/dist/lib/caller-principal.js +40 -0
- package/dist/lib/capacity/collectors/grok.js +8 -2
- package/dist/lib/cli-flag-keys.d.ts +2 -0
- package/dist/lib/cli-flag-keys.js +2 -0
- package/dist/lib/daemon/catalog.js +4 -1
- package/dist/lib/daemon/sync-core-canary.d.ts +3 -0
- package/dist/lib/daemon/sync-core-canary.js +8 -3
- package/dist/lib/doctor/checks/release-links.d.ts +62 -1
- package/dist/lib/doctor/checks/release-links.js +188 -2
- package/dist/lib/doctor/fix/apply.js +54 -0
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/index.d.ts +9 -0
- package/dist/lib/index-render/index.js +65 -5
- package/dist/lib/index-render/session-scope.d.ts +69 -0
- package/dist/lib/index-render/session-scope.js +279 -0
- package/dist/lib/lanes/admission.js +7 -20
- package/dist/lib/lanes/reconcile.js +13 -15
- package/dist/lib/lanes/stale-lock.d.ts +50 -0
- package/dist/lib/lanes/stale-lock.js +105 -0
- package/dist/lib/lanes/store.js +11 -76
- package/dist/lib/notify-recipient.d.ts +10 -4
- package/dist/lib/notify-recipient.js +16 -4
- package/dist/lib/secret-input-link.d.ts +42 -0
- package/dist/lib/secret-input-link.js +93 -0
- package/dist/lib/secret-scan/range.d.ts +57 -0
- package/dist/lib/secret-scan/range.js +266 -0
- package/dist/lib/secret-scan/repo-config.d.ts +18 -3
- package/dist/lib/secret-scan/repo-config.js +135 -2
- package/dist/run/script-bound-schema.d.ts +27 -0
- package/dist/run/script-bound-schema.js +97 -0
- package/dist/secrets-env-action.d.ts +2 -0
- package/dist/secrets-env-action.js +7 -2
- package/dist/utils/cognito-session.d.ts +3 -30
- package/dist/utils/cognito-session.js +2 -100
- package/dist/utils/machine-principal-state.d.ts +38 -0
- package/dist/utils/machine-principal-state.js +110 -0
- package/dist/utils/safe-link.js +9 -0
- package/dist/utils/script-bound-exec.d.ts +158 -0
- package/dist/utils/script-bound-exec.js +568 -0
- package/dist/utils/sync-watchdog.d.ts +92 -0
- package/dist/utils/sync-watchdog.js +250 -0
- package/package.json +8 -8
package/dist/commands/run.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { assertInjectedNamesAllowed, assertLiteralNamesAllowed, assertScriptUnchanged, planScriptBinding, selectChildLaunch } from '../utils/script-bound-exec.js';
|
|
2
|
+
import { parseStrictSchemaFiles } from '../run/script-bound-schema.js';
|
|
1
3
|
import { spawn } from 'node:child_process';
|
|
2
4
|
import * as path from 'node:path';
|
|
3
5
|
import * as fs from 'node:fs';
|
|
@@ -33,6 +35,47 @@ export async function buildRunUsage(scriptPath, scriptId) {
|
|
|
33
35
|
},
|
|
34
36
|
};
|
|
35
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* `hq run --script` resolution: read the schema files strictly, fetch only the
|
|
40
|
+
* hq() secrets, and return literals plus secret values. No varlock graph.
|
|
41
|
+
*/
|
|
42
|
+
async function resolveStrictSchema(paths, companyUid, usage, fetchBatch) {
|
|
43
|
+
const strict = parseStrictSchemaFiles(paths);
|
|
44
|
+
// Refuse proxy, CA/TLS, pager/editor, HOME/XDG and loader names before any
|
|
45
|
+
// fetch, so a schema literal cannot redirect what the bound script calls.
|
|
46
|
+
// Literals must be on the small --script allowlist; hq() names keep the
|
|
47
|
+
// denylist check (creating a secret needs vault write).
|
|
48
|
+
assertLiteralNamesAllowed(Object.keys(strict.statics));
|
|
49
|
+
assertInjectedNamesAllowed(strict.secrets.keys());
|
|
50
|
+
const names = [...new Set(strict.secrets.values())];
|
|
51
|
+
if (names.length > 100) {
|
|
52
|
+
throw new Error(`hq run supports at most 100 hq() resolvers per schema; got ${names.length}`);
|
|
53
|
+
}
|
|
54
|
+
const values = new Map();
|
|
55
|
+
if (names.length > 0) {
|
|
56
|
+
const data = await fetchBatch(companyUid, names, usage);
|
|
57
|
+
if (!Array.isArray(data?.secrets) || !Array.isArray(data?.errors)) {
|
|
58
|
+
throw new Error('Invalid secret load response from vault');
|
|
59
|
+
}
|
|
60
|
+
const firstError = data.errors[0];
|
|
61
|
+
if (firstError) {
|
|
62
|
+
throw new Error(`Failed to fetch secret '${firstError.name}': ${firstError.message ?? firstError.code}`);
|
|
63
|
+
}
|
|
64
|
+
for (const row of data.secrets) {
|
|
65
|
+
if (row && typeof row.name === 'string' && typeof row.value === 'string' && row.revokedAt == null) {
|
|
66
|
+
values.set(row.name, row.value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const env = { ...strict.statics };
|
|
71
|
+
for (const [key, name] of strict.secrets) {
|
|
72
|
+
const value = values.get(name);
|
|
73
|
+
if (value === undefined)
|
|
74
|
+
throw new Error(`Failed to fetch secret '${name}': not returned by vault`);
|
|
75
|
+
env[key] = value;
|
|
76
|
+
}
|
|
77
|
+
return env;
|
|
78
|
+
}
|
|
36
79
|
export function registerRunCommand(program) {
|
|
37
80
|
program
|
|
38
81
|
.command('run')
|
|
@@ -89,8 +132,19 @@ export function registerRunCommand(program) {
|
|
|
89
132
|
const uid = await getCompanyUid(token, slug, undefined, {
|
|
90
133
|
allowOfflineScopeFallback: true,
|
|
91
134
|
});
|
|
135
|
+
// feedback_0e6abcb8: with --script, script-locked secrets may reach
|
|
136
|
+
// only the approved script itself. Resolve the command now; a refusal
|
|
137
|
+
// is applied once the vault says which secrets are locked.
|
|
138
|
+
const scriptPlan = opts.check ? undefined : await planScriptBinding(opts.script, childArgs);
|
|
92
139
|
const usage = await buildRunUsage(opts.script, opts.scriptId);
|
|
140
|
+
if (scriptPlan?.bound) {
|
|
141
|
+
await assertScriptUnchanged(scriptPlan.bound, usage?.script?.sha256);
|
|
142
|
+
}
|
|
143
|
+
const requestedNames = new Set();
|
|
144
|
+
const lockModes = new Map();
|
|
93
145
|
const fetchBatch = async (companyUid, names, requestUsage) => withRunSecretReadTelemetry(companyUid, async () => {
|
|
146
|
+
for (const name of names)
|
|
147
|
+
requestedNames.add(name);
|
|
94
148
|
const res = await vaultApiFetch({
|
|
95
149
|
token,
|
|
96
150
|
path: `/secrets/${encodeURIComponent(companyUid)}/load`,
|
|
@@ -115,7 +169,12 @@ export function registerRunCommand(program) {
|
|
|
115
169
|
}
|
|
116
170
|
throw new Error(`Failed to batch-load secrets: ${message}`);
|
|
117
171
|
}
|
|
118
|
-
|
|
172
|
+
const data = await res.json();
|
|
173
|
+
for (const row of Array.isArray(data?.secrets) ? data.secrets : []) {
|
|
174
|
+
if (row && typeof row.name === 'string')
|
|
175
|
+
lockModes.set(row.name, row.scriptLock?.mode);
|
|
176
|
+
}
|
|
177
|
+
return data;
|
|
119
178
|
});
|
|
120
179
|
const pluginOpts = {
|
|
121
180
|
companyOverride: opts.company,
|
|
@@ -127,61 +186,77 @@ export function registerRunCommand(program) {
|
|
|
127
186
|
};
|
|
128
187
|
// LAST entry = highest precedence; .env.local files trail .env.schema files so any .env.local beats any schema regardless of depth.
|
|
129
188
|
const paths = [...schemaPaths, ...envLocalPaths];
|
|
130
|
-
let
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
skipCache: true,
|
|
138
|
-
afterInit: async (g) => { state = installHqPlugin(g, pluginOpts); },
|
|
139
|
-
});
|
|
140
|
-
// Refuse to resolve a graph varlock never finished loading (a malformed
|
|
141
|
-
// .env.schema / .env.local): resolving an unprocessed item whose key is
|
|
142
|
-
// also in process.env reaches varlock's `expected dataType to be set`
|
|
143
|
-
// invariant as a FLOATED, never-settling rejection — the CLI then exits
|
|
144
|
-
// 0 with the command never run (HQ-CLI-W). Run this BEFORE prewarm so a
|
|
145
|
-
// bad file can never trigger a secret fetch.
|
|
146
|
-
assertEnvGraphLoaded(graph);
|
|
147
|
-
await prewarmHqSecrets(graph, pluginOpts, state);
|
|
148
|
-
// Defence in depth: convert a floated resolution rejection into a thrown
|
|
149
|
-
// error instead of hanging on varlock's never-settling promise, in case
|
|
150
|
-
// the shape-dependent guard above ever silently degrades.
|
|
151
|
-
await resolveEnvValuesOrThrow(graph);
|
|
152
|
-
// varlock's item.errors includes warning-severity entries (isWarning),
|
|
153
|
-
// e.g. "Value is very short! Is it actually sensitive?" for a short
|
|
154
|
-
// sensitive value. Print those and carry on; only real errors are fatal.
|
|
155
|
-
const schemaItems = Object.entries(graph.configSchema);
|
|
156
|
-
const itemMessages = (wantWarnings) => schemaItems.flatMap(([k, item]) => (item.errors ?? [])
|
|
157
|
-
.filter((e) => Boolean(e?.isWarning) === wantWarnings)
|
|
158
|
-
.map((e) => ` ${k}: ${e?.message ?? String(e)}`));
|
|
159
|
-
const warningMsgs = itemMessages(true);
|
|
160
|
-
if (warningMsgs.length > 0) {
|
|
161
|
-
process.stderr.write(`Warning: env var checks reported warnings:\n${warningMsgs.join('\n')}\n`);
|
|
189
|
+
let resolvedEnv;
|
|
190
|
+
if (opts.script) {
|
|
191
|
+
// feedback_0e6abcb8: with --script, never build a varlock graph. Its
|
|
192
|
+
// exec()/plugin/import resolvers would run in this process while the
|
|
193
|
+
// fetched secrets are in memory, before the script binding is
|
|
194
|
+
// checked. The strict reader accepts only hq() items and literals.
|
|
195
|
+
resolvedEnv = await resolveStrictSchema(paths, uid, usage, fetchBatch);
|
|
162
196
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
197
|
+
else {
|
|
198
|
+
let state;
|
|
199
|
+
const graph = await internal.loadEnvGraph({
|
|
200
|
+
entryFilePaths: paths,
|
|
201
|
+
// hq run owns its short-lived in-memory resolution. Varlock's default
|
|
202
|
+
// cache auto-detection can write ~/.config/varlock and initialize
|
|
203
|
+
// the native local-encryption helper, neither of which belongs on
|
|
204
|
+
// this command path.
|
|
205
|
+
skipCache: true,
|
|
206
|
+
afterInit: async (g) => { state = installHqPlugin(g, pluginOpts); },
|
|
207
|
+
});
|
|
208
|
+
// Refuse to resolve a graph varlock never finished loading (a malformed
|
|
209
|
+
// .env.schema / .env.local): resolving an unprocessed item whose key is
|
|
210
|
+
// also in process.env reaches varlock's `expected dataType to be set`
|
|
211
|
+
// invariant as a FLOATED, never-settling rejection — the CLI then exits
|
|
212
|
+
// 0 with the command never run (HQ-CLI-W). Run this BEFORE prewarm so a
|
|
213
|
+
// bad file can never trigger a secret fetch.
|
|
214
|
+
assertEnvGraphLoaded(graph);
|
|
215
|
+
await prewarmHqSecrets(graph, pluginOpts, state);
|
|
216
|
+
// Defence in depth: convert a floated resolution rejection into a thrown
|
|
217
|
+
// error instead of hanging on varlock's never-settling promise, in case
|
|
218
|
+
// the shape-dependent guard above ever silently degrades.
|
|
219
|
+
await resolveEnvValuesOrThrow(graph);
|
|
220
|
+
// varlock's item.errors includes warning-severity entries (isWarning),
|
|
221
|
+
// e.g. "Value is very short! Is it actually sensitive?" for a short
|
|
222
|
+
// sensitive value. Print those and carry on; only real errors are fatal.
|
|
223
|
+
const schemaItems = Object.entries(graph.configSchema);
|
|
224
|
+
const itemMessages = (wantWarnings) => schemaItems.flatMap(([k, item]) => (item.errors ?? [])
|
|
225
|
+
.filter((e) => Boolean(e?.isWarning) === wantWarnings)
|
|
226
|
+
.map((e) => ` ${k}: ${e?.message ?? String(e)}`));
|
|
227
|
+
const warningMsgs = itemMessages(true);
|
|
228
|
+
if (warningMsgs.length > 0) {
|
|
229
|
+
process.stderr.write(`Warning: env var checks reported warnings:\n${warningMsgs.join('\n')}\n`);
|
|
230
|
+
}
|
|
231
|
+
const errorMsgs = itemMessages(false);
|
|
232
|
+
if (errorMsgs.length > 0) {
|
|
233
|
+
process.stderr.write(`Error: failed to resolve env vars:\n${errorMsgs.join('\n')}\n`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
resolvedEnv = graph.getResolvedEnvObject();
|
|
167
237
|
}
|
|
168
|
-
const resolvedEnv = graph.getResolvedEnvObject();
|
|
169
238
|
const varCount = Object.keys(resolvedEnv).length;
|
|
170
239
|
process.stderr.write(`Loaded ${varCount} env vars from .env.schema (company: ${slug})\n`);
|
|
171
240
|
if (opts.check) {
|
|
172
241
|
process.exit(0);
|
|
173
242
|
}
|
|
174
|
-
|
|
243
|
+
// Script-locked (or unknown) secrets go only to a private, re-hashed
|
|
244
|
+
// copy of the approved script; unlocked secrets keep the command as is.
|
|
245
|
+
const launch = selectChildLaunch(scriptPlan, [...requestedNames], lockModes, usage?.script?.sha256, childArgs, process.env, resolvedEnv);
|
|
246
|
+
const childCmd = launch.file;
|
|
247
|
+
const restArgs = launch.args;
|
|
175
248
|
const child = spawn(childCmd, restArgs, {
|
|
176
249
|
stdio: 'inherit',
|
|
177
|
-
env:
|
|
250
|
+
env: launch.env,
|
|
178
251
|
});
|
|
179
252
|
child.once('spawn', () => releaseChildWrapperPackageUseLeaseAfterStart("childWrapper", isHqCliChild(childCmd, restArgs)));
|
|
180
253
|
child.on('error', (err) => {
|
|
181
254
|
process.stderr.write(`Error: failed to start command '${childCmd}': ${err.message}\n`);
|
|
255
|
+
launch.cleanup();
|
|
182
256
|
process.exit(1);
|
|
183
257
|
});
|
|
184
258
|
child.on('close', (code, signal) => {
|
|
259
|
+
launch.cleanup();
|
|
185
260
|
if (signal) {
|
|
186
261
|
process.kill(process.pid, signal);
|
|
187
262
|
}
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* hq scan secrets --json findings as JSON
|
|
7
7
|
* hq scan secrets --repo <dir> [--snapshot-root <dir>] [--allowlist-rev <rev>] <file...>
|
|
8
8
|
* scan files with that repo's committed
|
|
9
|
-
* .gitleaks.toml allowlists
|
|
9
|
+
* .gitleaks.toml allowlists
|
|
10
|
+
* hq scan secrets --range <base>[..<head>] [--repo <dir>]
|
|
11
|
+
* scan the lines a branch added since <base>,
|
|
12
|
+
* every commit included (used by pr-check)
|
|
10
13
|
*
|
|
11
14
|
* Rules: the vendored gitleaks default rule set (MIT, vendor/gitleaks/) plus
|
|
12
15
|
* HQ's own token patterns. No network, no account, no quota. A staged scan also
|
|
@@ -24,6 +27,7 @@ interface ScanSecretsOptions {
|
|
|
24
27
|
repo?: string;
|
|
25
28
|
snapshotRoot?: string;
|
|
26
29
|
allowlistRev?: string;
|
|
30
|
+
range?: string;
|
|
27
31
|
}
|
|
28
32
|
export declare function runScanSecrets(paths: string[], opts: ScanSecretsOptions, cwd?: string): number;
|
|
29
33
|
export declare function registerScanCommand(program: Command): void;
|
package/dist/commands/scan.js
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* hq scan secrets --json findings as JSON
|
|
7
7
|
* hq scan secrets --repo <dir> [--snapshot-root <dir>] [--allowlist-rev <rev>] <file...>
|
|
8
8
|
* scan files with that repo's committed
|
|
9
|
-
* .gitleaks.toml allowlists
|
|
9
|
+
* .gitleaks.toml allowlists
|
|
10
|
+
* hq scan secrets --range <base>[..<head>] [--repo <dir>]
|
|
11
|
+
* scan the lines a branch added since <base>,
|
|
12
|
+
* every commit included (used by pr-check)
|
|
10
13
|
*
|
|
11
14
|
* Rules: the vendored gitleaks default rule set (MIT, vendor/gitleaks/) plus
|
|
12
15
|
* HQ's own token patterns. No network, no account, no quota. A staged scan also
|
|
@@ -18,6 +21,7 @@
|
|
|
18
21
|
* Output names the rule, file and line; secret values are never printed.
|
|
19
22
|
*/
|
|
20
23
|
import { ScanConfigError } from "../lib/secret-scan/repo-config.js";
|
|
24
|
+
import { scanRange } from "../lib/secret-scan/range.js";
|
|
21
25
|
import { ScanInputError, scanFiles, scanStaged } from "../lib/secret-scan/staged.js";
|
|
22
26
|
function report(findings, scope) {
|
|
23
27
|
if (!findings.length)
|
|
@@ -38,7 +42,14 @@ export function runScanSecrets(paths, opts, cwd = process.cwd()) {
|
|
|
38
42
|
throw new ScanInputError("--snapshot-root needs --repo");
|
|
39
43
|
if (opts.allowlistRev !== undefined && opts.repo === undefined)
|
|
40
44
|
throw new ScanInputError("--allowlist-rev needs --repo");
|
|
41
|
-
|
|
45
|
+
if (opts.range !== undefined) {
|
|
46
|
+
if (paths.length || opts.staged)
|
|
47
|
+
throw new ScanInputError("--range scans a commit range; do not pass files or --staged");
|
|
48
|
+
if (opts.snapshotRoot !== undefined || opts.allowlistRev !== undefined) {
|
|
49
|
+
throw new ScanInputError("--range reads .gitleaks.toml from the range base; drop --snapshot-root and --allowlist-rev");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
findings = opts.range !== undefined ? scanRange(opts.repo ?? cwd, opts.range) : paths.length && !opts.staged ? scanFiles(paths, { repo: opts.repo, snapshotRoot: opts.snapshotRoot, allowlistRev: opts.allowlistRev }) : scanStaged(cwd);
|
|
42
53
|
}
|
|
43
54
|
catch (err) {
|
|
44
55
|
const msg = err instanceof ScanInputError || err instanceof ScanConfigError ? err.message : `scan failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -51,7 +62,7 @@ export function runScanSecrets(paths, opts, cwd = process.cwd()) {
|
|
|
51
62
|
process.stdout.write(`${JSON.stringify({ ok: findings.length === 0, findings })}\n`);
|
|
52
63
|
}
|
|
53
64
|
else {
|
|
54
|
-
report(findings, paths.length && !opts.staged ? "the given files" : "staged changes");
|
|
65
|
+
report(findings, opts.range !== undefined ? `the lines added in ${opts.range}` : paths.length && !opts.staged ? "the given files" : "staged changes");
|
|
55
66
|
}
|
|
56
67
|
return findings.length ? 1 : 0;
|
|
57
68
|
}
|
|
@@ -63,9 +74,10 @@ export function registerScanCommand(program) {
|
|
|
63
74
|
.argument("[files...]", "Scan these files instead of the staged changes")
|
|
64
75
|
.option("--staged", "Scan the staged changes (default when no files are given)")
|
|
65
76
|
.option("--json", "Print findings as JSON")
|
|
66
|
-
.option("--repo <dir>", "With files: apply this repo's committed .gitleaks.toml allowlists, matching repo-relative paths")
|
|
77
|
+
.option("--repo <dir>", "With files: apply this repo's committed .gitleaks.toml allowlists, matching repo-relative paths. With --range: the repo to scan (default: the current directory)")
|
|
67
78
|
.option("--snapshot-root <dir>", "With --repo: files under <dir>/<commit>/<path> are matched as <path>")
|
|
68
79
|
.option("--allowlist-rev <rev>", "With --repo: read .gitleaks.toml from this commit instead of HEAD")
|
|
80
|
+
.option("--range <range>", "Scan the lines added in <base>..<head> (head defaults to HEAD), every commit included, with the base's .gitleaks.toml allowlists and the repo's [[rules]]")
|
|
69
81
|
.action((files, opts) => {
|
|
70
82
|
process.exitCode = runScanSecrets(files ?? [], opts);
|
|
71
83
|
});
|
package/dist/commands/secrets.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertScriptUnchanged, planScriptBinding, selectChildLaunch } from "../utils/script-bound-exec.js";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import * as yaml from "js-yaml";
|
|
@@ -77,6 +78,7 @@ import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, format
|
|
|
77
78
|
import { vaultApiFetch, getCompanyUid, getEntityUid, looksLikeCompanyUid, } from "../utils/vault-api.js";
|
|
78
79
|
import { formatApiKeyCapabilityDenial, peekHqApiKey, requireApiKeyCapability, resolveVaultCredentialForCapability, } from "../utils/resolve-vault-credential.js";
|
|
79
80
|
import { runSandboxJobAfterSubmission } from "../utils/secrets-sandbox-resident.js";
|
|
81
|
+
import { renderSecretInputLink, SECRET_LINK_FORMATS, } from "../lib/secret-input-link.js";
|
|
80
82
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
81
83
|
/**
|
|
82
84
|
* Mirrors `RESOURCE_SURFACES` in hq-pro
|
|
@@ -1370,6 +1372,10 @@ export function registerSecretsCommand(program) {
|
|
|
1370
1372
|
process.exit(1);
|
|
1371
1373
|
}
|
|
1372
1374
|
const keys = parseSecretNameList(_opts.only);
|
|
1375
|
+
// feedback_0e6abcb8: with --script, a script-locked secret may reach
|
|
1376
|
+
// only the approved script itself. Resolve the command now; a refusal
|
|
1377
|
+
// is applied once the vault says which secrets are locked.
|
|
1378
|
+
const scriptPlan = await planScriptBinding(_opts.script, childArgs);
|
|
1373
1379
|
const parentScope = { ...secrets.opts() };
|
|
1374
1380
|
const inheritedCompany = parentScope.company;
|
|
1375
1381
|
let requestedCompany = _opts.company ?? inheritedCompany;
|
|
@@ -1466,9 +1472,14 @@ export function registerSecretsCommand(program) {
|
|
|
1466
1472
|
secret_names: keys,
|
|
1467
1473
|
});
|
|
1468
1474
|
}
|
|
1469
|
-
const
|
|
1475
|
+
const execUsage = cred.kind === "cognito"
|
|
1470
1476
|
? await buildSecretUsage("exec", _opts.script, _opts.scriptId)
|
|
1471
|
-
: undefined
|
|
1477
|
+
: undefined;
|
|
1478
|
+
if (scriptPlan?.bound) {
|
|
1479
|
+
await assertScriptUnchanged(scriptPlan.bound, execUsage?.script?.sha256);
|
|
1480
|
+
}
|
|
1481
|
+
const lockModes = new Map();
|
|
1482
|
+
const readSecrets = async () => loadRevealedSecrets(cred.token, companyUid, keys, execUsage, { allowOfflineFallback: true, lockModes });
|
|
1472
1483
|
// API-key auth has no signed-in person identity for the CLI telemetry
|
|
1473
1484
|
// endpoint. Do not attribute that read to an unrelated cached session.
|
|
1474
1485
|
let revealed;
|
|
@@ -1489,18 +1500,24 @@ export function registerSecretsCommand(program) {
|
|
|
1489
1500
|
}
|
|
1490
1501
|
secretEnv[key] = value;
|
|
1491
1502
|
}
|
|
1492
|
-
|
|
1503
|
+
// Script-locked (or unknown) secrets go only to a private, re-hashed
|
|
1504
|
+
// copy of the approved script; unlocked secrets keep the command as is.
|
|
1505
|
+
const launch = selectChildLaunch(scriptPlan, keys, lockModes, execUsage?.script?.sha256, childArgs, process.env, secretEnv);
|
|
1506
|
+
const childCmd = launch.file;
|
|
1507
|
+
const childCmdArgs = launch.args;
|
|
1493
1508
|
const { spawn } = await import("node:child_process");
|
|
1494
1509
|
const child = spawn(childCmd, childCmdArgs, {
|
|
1495
1510
|
stdio: "inherit",
|
|
1496
|
-
env:
|
|
1511
|
+
env: launch.env,
|
|
1497
1512
|
});
|
|
1498
1513
|
child.once("spawn", () => releaseChildWrapperPackageUseLeaseAfterStart("childWrapper", isHqCliChild(childCmd, childCmdArgs)));
|
|
1499
1514
|
child.on("error", (err) => {
|
|
1500
1515
|
console.error(chalk.red(`Failed to start command '${childCmd}': ${err.message}`));
|
|
1516
|
+
launch.cleanup();
|
|
1501
1517
|
process.exit(1);
|
|
1502
1518
|
});
|
|
1503
1519
|
child.on("close", (code, signal) => {
|
|
1520
|
+
launch.cleanup();
|
|
1504
1521
|
if (signal) {
|
|
1505
1522
|
process.kill(process.pid, signal);
|
|
1506
1523
|
}
|
|
@@ -1525,8 +1542,19 @@ export function registerSecretsCommand(program) {
|
|
|
1525
1542
|
.command("generate-link <name>")
|
|
1526
1543
|
.description("Generate a one-time link for someone to submit a secret value")
|
|
1527
1544
|
.option("--expires <duration>", "Token expiry duration (e.g. 24h, 2d, 30m)", "24h")
|
|
1545
|
+
.option("--format <format>", "Output format: text (default; OSC-8 hyperlink on a supporting TTY, plain URL otherwise; FORCE_HYPERLINK=1/0 overrides detection on a TTY only), markdown (one Markdown link line on stdout, details on stderr), json")
|
|
1546
|
+
.option("--json", "Print { url, expiresAt, secretName } as JSON (same as --format json)")
|
|
1528
1547
|
.action(async (name, opts) => {
|
|
1529
1548
|
try {
|
|
1549
|
+
const format = (opts.json ? "json" : (opts.format ?? "text"));
|
|
1550
|
+
if (!SECRET_LINK_FORMATS.includes(format)) {
|
|
1551
|
+
console.error(chalk.red(`Invalid format '${opts.format}': must be one of ${SECRET_LINK_FORMATS.join(", ")}`));
|
|
1552
|
+
process.exit(1);
|
|
1553
|
+
}
|
|
1554
|
+
if (opts.json && opts.format !== undefined && opts.format !== "json") {
|
|
1555
|
+
console.error(chalk.red(`--json conflicts with --format ${opts.format}`));
|
|
1556
|
+
process.exit(1);
|
|
1557
|
+
}
|
|
1530
1558
|
if (!SECRET_NAME_PATTERN.test(name)) {
|
|
1531
1559
|
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)`));
|
|
1532
1560
|
process.exit(1);
|
|
@@ -1556,11 +1584,24 @@ export function registerSecretsCommand(program) {
|
|
|
1556
1584
|
process.exit(1);
|
|
1557
1585
|
}
|
|
1558
1586
|
const data = (await res.json());
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1587
|
+
const rendered = renderSecretInputLink(data, {
|
|
1588
|
+
format,
|
|
1589
|
+
isTTY: process.stdout.isTTY === true,
|
|
1590
|
+
env: process.env,
|
|
1591
|
+
});
|
|
1592
|
+
if (format === "text") {
|
|
1593
|
+
const [heading, link, ...meta] = rendered.stdout;
|
|
1594
|
+
console.log(chalk.green(heading));
|
|
1595
|
+
console.log(link);
|
|
1596
|
+
for (const line of meta)
|
|
1597
|
+
console.log(chalk.dim(line));
|
|
1598
|
+
}
|
|
1599
|
+
else {
|
|
1600
|
+
for (const line of rendered.stdout)
|
|
1601
|
+
console.log(line);
|
|
1602
|
+
for (const line of rendered.stderr)
|
|
1603
|
+
console.error(chalk.dim(line));
|
|
1604
|
+
}
|
|
1564
1605
|
}
|
|
1565
1606
|
catch (err) {
|
|
1566
1607
|
// Same shared plan-limit renderer as `secrets set` (see main.ts).
|