@westbayberry/dg 2.1.0 → 2.2.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/README.md +216 -226
- package/dist/agents/claude-code.js +113 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +113 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +285 -0
- package/dist/agents/registry.js +127 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/windsurf.js +93 -0
- package/dist/api/analyze.js +1 -1
- package/dist/audit/detectors.js +0 -11
- package/dist/audit/events.js +5 -21
- package/dist/audit-ui/AuditApp.js +2 -0
- package/dist/audit-ui/components/AuditResultsView.js +55 -92
- package/dist/audit-ui/export.js +0 -4
- package/dist/audit-ui/format.js +0 -3
- package/dist/audit-ui/launch.js +10 -1
- package/dist/auth/device-login.js +4 -5
- package/dist/auth/login-app.js +7 -7
- package/dist/auth/store.js +8 -3
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +231 -0
- package/dist/commands/audit.js +22 -3
- package/dist/commands/config.js +15 -7
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +23 -9
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/licenses.js +10 -22
- package/dist/commands/logout.js +4 -11
- package/dist/commands/router.js +6 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +197 -27
- package/dist/commands/types.js +1 -0
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +14 -44
- package/dist/decisions/remember-prompt.js +5 -10
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +2 -2
- package/dist/install-ui/block-render.js +1 -1
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/launcher/agent-check.js +466 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +31 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +17 -6
- package/dist/launcher/env.js +71 -24
- package/dist/launcher/install-preflight.js +51 -14
- package/dist/launcher/live-install.js +25 -5
- package/dist/launcher/output-redaction.js +7 -4
- package/dist/launcher/preflight-prompt.js +14 -1
- package/dist/launcher/run.js +89 -86
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +18 -5
- package/dist/policy/evaluate.js +5 -6
- package/dist/policy/pypi-name.js +17 -0
- package/dist/presentation/mode.js +3 -2
- package/dist/presentation/package-page.js +9 -0
- package/dist/presentation/theme.js +7 -7
- package/dist/project/dgfile.js +145 -6
- package/dist/proxy/auth.js +42 -0
- package/dist/proxy/ca.js +29 -9
- package/dist/proxy/cooldown-exemptions-file.js +33 -0
- package/dist/proxy/enforcement.js +55 -16
- package/dist/proxy/metadata-map.js +41 -3
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +448 -49
- package/dist/proxy/worker.js +16 -1
- package/dist/publish-set/collect.js +1 -4
- package/dist/publish-set/npm.js +8 -5
- package/dist/publish-set/pack.js +9 -3
- package/dist/runtime/cli.js +0 -4
- package/dist/runtime/fatal.js +31 -0
- package/dist/runtime/first-run.js +12 -11
- package/dist/runtime/node-version.js +43 -6
- package/dist/runtime/nudges.js +35 -2
- package/dist/sbom/cyclonedx.js +211 -0
- package/dist/sbom-ui/SbomApp.js +158 -0
- package/dist/sbom-ui/components/SbomHeader.js +32 -0
- package/dist/sbom-ui/components/SbomList.js +52 -0
- package/dist/sbom-ui/inventory.js +128 -0
- package/dist/sbom-ui/launch.js +51 -0
- package/dist/sbom-ui/run.js +55 -0
- package/dist/sbom-ui/store.js +26 -0
- package/dist/scan/collect.js +10 -6
- package/dist/scan/command.js +17 -10
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +28 -4
- package/dist/scan/scanner-report.js +15 -9
- package/dist/scan/staged.js +2 -1
- package/dist/scan-ui/LegacyApp.js +9 -13
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
- package/dist/scan-ui/components/ProgressBar.js +3 -14
- package/dist/scan-ui/components/ProjectSelector.js +1 -1
- package/dist/scan-ui/components/ScoreHeader.js +2 -3
- package/dist/scan-ui/components/SetupBanner.js +0 -6
- package/dist/scan-ui/format-helpers.js +61 -5
- package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
- package/dist/scan-ui/hooks/useScan.js +16 -2
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +11 -4
- package/dist/scripts/detect.js +7 -2
- package/dist/scripts/gate.js +1 -1
- package/dist/service/state.js +27 -8
- package/dist/service/trust-refresh.js +92 -0
- package/dist/service/worker.js +23 -1
- package/dist/setup/activate-shell.js +28 -0
- package/dist/setup/git-hook.js +49 -4
- package/dist/setup/plan.js +73 -39
- package/dist/setup-ui/gate.js +39 -0
- package/dist/setup-ui/offer.js +42 -0
- package/dist/setup-ui/selector.js +27 -0
- package/dist/setup-ui/tasks.js +56 -0
- package/dist/setup-ui/wizard.js +225 -0
- package/dist/state/cooldown-held.js +66 -0
- package/dist/state/index.js +1 -0
- package/dist/state/locks.js +4 -2
- package/dist/state/store.js +2 -1
- package/dist/util/external-tool.js +25 -0
- package/dist/util/git.js +10 -3
- package/dist/util/report-writer.js +57 -0
- package/dist/verify/local.js +240 -42
- package/dist/verify/package-check.js +84 -28
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +6 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -117
- package/dist/commands/explain.js +0 -236
- package/dist/commands/unavailable.js +0 -11
- package/dist/scripts/rebuild.js +0 -28
- package/dist/telemetry/events.js +0 -40
package/dist/launcher/env.js
CHANGED
|
@@ -1,53 +1,100 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { proxyUrlWithAuth, readProxyAuthToken } from "../proxy/auth.js";
|
|
3
|
+
// The client read timeout must exceed the proxy's per-artifact verdict ceiling
|
|
4
|
+
// (DG_INSTALL_VERDICT_TIMEOUT_MS, default 240s) plus download headroom, so the
|
|
5
|
+
// package manager never gives up on a legitimately-slow verify. Overridable for
|
|
6
|
+
// constrained environments via DG_CLIENT_READ_TIMEOUT_S.
|
|
7
|
+
function clientReadTimeoutSeconds(env) {
|
|
8
|
+
const override = Number(env.DG_CLIENT_READ_TIMEOUT_S);
|
|
9
|
+
if (Number.isFinite(override) && override > 0) {
|
|
10
|
+
return Math.ceil(override);
|
|
11
|
+
}
|
|
12
|
+
const verdictMs = Number(env.DG_INSTALL_VERDICT_TIMEOUT_MS);
|
|
13
|
+
const verdictSeconds = Number.isFinite(verdictMs) && verdictMs > 0 ? verdictMs / 1000 : 240;
|
|
14
|
+
return Math.ceil(verdictSeconds + 60);
|
|
15
|
+
}
|
|
1
16
|
export function buildProxyChildEnv(options) {
|
|
17
|
+
const authToken = readProxyAuthToken(dirname(options.caBundlePath));
|
|
18
|
+
const proxyUrl = authToken ? proxyUrlWithAuth(options.proxyUrl, authToken) : options.proxyUrl;
|
|
2
19
|
const env = {
|
|
3
20
|
...options.baseEnv,
|
|
4
21
|
DG_PROXY_ACTIVE: "1"
|
|
5
22
|
};
|
|
6
|
-
|
|
23
|
+
// The wrapped package manager and every lifecycle/postinstall script it runs
|
|
24
|
+
// are untrusted. The dg account credential must never reach them; only the
|
|
25
|
+
// trusted proxy worker (which makes the authenticated API calls) keeps it.
|
|
26
|
+
delete env.DG_API_KEY;
|
|
27
|
+
delete env.DG_API_TOKEN;
|
|
28
|
+
// dg fully controls NO_PROXY (loopback only). An inherited NO_PROXY that names
|
|
29
|
+
// the registry (or a `*`/`.org` glob) would route the manager straight past the
|
|
30
|
+
// firewall, so the inherited value is dropped, not merged.
|
|
31
|
+
const noProxy = (options.noProxyHosts ?? ["127.0.0.1", "localhost"]).join(",");
|
|
32
|
+
env.NO_PROXY = noProxy;
|
|
33
|
+
env.no_proxy = noProxy;
|
|
34
|
+
const readTimeoutSeconds = clientReadTimeoutSeconds(options.baseEnv);
|
|
7
35
|
if (["npm", "npx", "pnpm", "pnpx", "yarn"].includes(options.manager)) {
|
|
8
|
-
env.HTTP_PROXY =
|
|
9
|
-
env.HTTPS_PROXY =
|
|
10
|
-
env.npm_config_proxy =
|
|
11
|
-
env.npm_config_https_proxy =
|
|
12
|
-
env.NO_PROXY = noProxy;
|
|
36
|
+
env.HTTP_PROXY = proxyUrl;
|
|
37
|
+
env.HTTPS_PROXY = proxyUrl;
|
|
38
|
+
env.npm_config_proxy = proxyUrl;
|
|
39
|
+
env.npm_config_https_proxy = proxyUrl;
|
|
13
40
|
env.NODE_EXTRA_CA_CERTS = options.caBundlePath;
|
|
41
|
+
// The proxy withholds every artifact byte until its verdict resolves, so the
|
|
42
|
+
// package manager sees no response data until then. Its own network read
|
|
43
|
+
// timeout must outlast the proxy's worst-case verdict wait or it aborts a
|
|
44
|
+
// legitimate install mid-verify (pip's 15s default is what made large cold
|
|
45
|
+
// installs fail). Set it explicitly so a user-lowered global config can't
|
|
46
|
+
// undercut the proxy.
|
|
47
|
+
env.npm_config_fetch_timeout = String(readTimeoutSeconds * 1000);
|
|
48
|
+
if (options.manager === "yarn") {
|
|
49
|
+
env.YARN_NETWORK_TIMEOUT = String(readTimeoutSeconds * 1000);
|
|
50
|
+
}
|
|
51
|
+
if (options.manager === "yarn") {
|
|
52
|
+
// Yarn Berry (2+) ignores HTTP(S)_PROXY/npm_config_*; without its own
|
|
53
|
+
// proxy + CA config it fetches straight from the registry, bypassing the
|
|
54
|
+
// firewall. Classic Yarn ignores these YARN_ vars, so set them for both.
|
|
55
|
+
env.YARN_HTTP_PROXY = proxyUrl;
|
|
56
|
+
env.YARN_HTTPS_PROXY = proxyUrl;
|
|
57
|
+
env.YARN_HTTPS_CA_FILE_PATH = options.caBundlePath;
|
|
58
|
+
}
|
|
14
59
|
if (options.cacheDir) {
|
|
15
60
|
env.npm_config_cache = options.cacheDir;
|
|
16
61
|
if (options.manager === "yarn") {
|
|
62
|
+
// Both: YARN_CACHE_FOLDER is Berry's per-project cache, but Berry 2+
|
|
63
|
+
// defaults to a shared global cache (~/.yarn/berry) that a warm install
|
|
64
|
+
// serves from without ever fetching, so redirect that too.
|
|
17
65
|
env.YARN_CACHE_FOLDER = options.cacheDir;
|
|
66
|
+
env.YARN_GLOBAL_FOLDER = options.cacheDir;
|
|
18
67
|
}
|
|
19
68
|
}
|
|
20
69
|
return env;
|
|
21
70
|
}
|
|
22
71
|
if (options.manager === "pip" || options.manager === "pipx") {
|
|
23
|
-
env.http_proxy =
|
|
24
|
-
env.https_proxy =
|
|
25
|
-
env.no_proxy = noProxy;
|
|
72
|
+
env.http_proxy = proxyUrl;
|
|
73
|
+
env.https_proxy = proxyUrl;
|
|
26
74
|
env.REQUESTS_CA_BUNDLE = options.caBundlePath;
|
|
27
75
|
env.PIP_CERT = options.caBundlePath;
|
|
28
76
|
env.PIP_NO_CACHE_DIR = "1";
|
|
77
|
+
env.PIP_DEFAULT_TIMEOUT = String(readTimeoutSeconds);
|
|
29
78
|
return env;
|
|
30
79
|
}
|
|
31
80
|
if (options.manager === "uv" || options.manager === "uvx") {
|
|
32
|
-
env.HTTP_PROXY =
|
|
33
|
-
env.HTTPS_PROXY =
|
|
34
|
-
env.ALL_PROXY =
|
|
35
|
-
env.NO_PROXY = noProxy;
|
|
81
|
+
env.HTTP_PROXY = proxyUrl;
|
|
82
|
+
env.HTTPS_PROXY = proxyUrl;
|
|
83
|
+
env.ALL_PROXY = proxyUrl;
|
|
36
84
|
env.SSL_CERT_FILE = options.caBundlePath;
|
|
37
85
|
env.UV_NO_CACHE = "1";
|
|
86
|
+
env.UV_HTTP_TIMEOUT = String(readTimeoutSeconds);
|
|
38
87
|
return env;
|
|
39
88
|
}
|
|
40
|
-
env.HTTPS_PROXY =
|
|
41
|
-
env.https_proxy =
|
|
42
|
-
env.http_proxy =
|
|
43
|
-
env.NO_PROXY = noProxy;
|
|
89
|
+
env.HTTPS_PROXY = proxyUrl;
|
|
90
|
+
env.https_proxy = proxyUrl;
|
|
91
|
+
env.http_proxy = proxyUrl;
|
|
44
92
|
env.CARGO_HTTP_CAINFO = options.caBundlePath;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
values.add(host);
|
|
93
|
+
env.CARGO_HTTP_TIMEOUT = String(readTimeoutSeconds);
|
|
94
|
+
if (options.cacheDir) {
|
|
95
|
+
// cargo serves a cached crate from ~/.cargo with zero network, skipping the
|
|
96
|
+
// firewall; pointing CARGO_HOME at the throwaway dir forces a re-fetch.
|
|
97
|
+
env.CARGO_HOME = options.cacheDir;
|
|
51
98
|
}
|
|
52
|
-
return
|
|
99
|
+
return env;
|
|
53
100
|
}
|
|
@@ -6,24 +6,26 @@ import { matchDecision, packageKey } from "../decisions/apply.js";
|
|
|
6
6
|
import { offerRememberOnIo } from "../decisions/remember-prompt.js";
|
|
7
7
|
import { provenanceDowngradeLine } from "../presentation/provenance.js";
|
|
8
8
|
import { defaultPromptIo } from "../install-ui/prompt.js";
|
|
9
|
-
import { cooldownRequestParam, formatCooldownDuration, formatPackageAge, isCooldownExempt } from "../policy/cooldown.js";
|
|
9
|
+
import { cooldownRequestParam, formatCooldownDuration, formatPackageAge, isCooldownExempt, isCooldownExemptByDgFile } from "../policy/cooldown.js";
|
|
10
10
|
import { findProjectRoot, loadDgFile, resolveAcceptedBy, warnUnreadableDgFile } from "../project/dgfile.js";
|
|
11
11
|
import { parsePipReportInstallCount, parsePipReportInstallSet } from "./pip-report.js";
|
|
12
|
-
|
|
12
|
+
import { resolveSpawnInvocation } from "./spawn-invocation.js";
|
|
13
|
+
export function resolvePreflightCooldown(env, ecosystem) {
|
|
13
14
|
try {
|
|
14
15
|
const config = loadUserConfig(env);
|
|
15
|
-
const param = cooldownRequestParam(config, env,
|
|
16
|
-
return param ? { param, exempt: config.cooldown.exempt } : undefined;
|
|
16
|
+
const param = cooldownRequestParam(config, env, ecosystem, "");
|
|
17
|
+
return param ? { param, exempt: config.cooldown.exempt, ecosystem } : undefined;
|
|
17
18
|
}
|
|
18
19
|
catch {
|
|
19
20
|
return undefined;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
|
-
function isQuarantined(pkg, context) {
|
|
23
|
+
function isQuarantined(pkg, context, dgExemptions = []) {
|
|
23
24
|
if (!context || !pkg.cooldown) {
|
|
24
25
|
return false;
|
|
25
26
|
}
|
|
26
|
-
if (isCooldownExempt(pkg.name, context.exempt,
|
|
27
|
+
if (isCooldownExempt(pkg.name, context.exempt, context.ecosystem)
|
|
28
|
+
|| isCooldownExemptByDgFile(pkg.name, context.ecosystem, dgExemptions)) {
|
|
27
29
|
return false;
|
|
28
30
|
}
|
|
29
31
|
return pkg.cooldown.status === "quarantine"
|
|
@@ -70,6 +72,15 @@ export function cachedPipResolution(binary, args) {
|
|
|
70
72
|
function pipResolutionKey(binary, args) {
|
|
71
73
|
return JSON.stringify([binary, ...args]);
|
|
72
74
|
}
|
|
75
|
+
// pip's dry-run resolve downloads sdists/wheels it cannot get PEP 658
|
|
76
|
+
// metadata for, so big dependency trees legitimately take minutes on a cold
|
|
77
|
+
// pip cache. A short timeout here silently skips the preflight (and with it
|
|
78
|
+
// the batched analyze + preverified handoff), pushing every package onto the
|
|
79
|
+
// slow per-artifact verdict path.
|
|
80
|
+
function pipResolveTimeoutMs(env) {
|
|
81
|
+
const raw = Number(env.DG_PIP_RESOLVE_TIMEOUT_MS);
|
|
82
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 120_000;
|
|
83
|
+
}
|
|
73
84
|
function resolvePipInstallSet(binary, args, env) {
|
|
74
85
|
if (!binary)
|
|
75
86
|
return Promise.resolve(UNRESOLVED);
|
|
@@ -87,9 +98,11 @@ function resolvePipInstallSet(binary, args, env) {
|
|
|
87
98
|
};
|
|
88
99
|
let child;
|
|
89
100
|
try {
|
|
90
|
-
|
|
101
|
+
const invocation = resolveSpawnInvocation(binary, [...args, "--dry-run", "--report", "-", "--quiet"]);
|
|
102
|
+
child = spawn(invocation.command, [...invocation.args], {
|
|
91
103
|
env,
|
|
92
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
104
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
105
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
93
106
|
});
|
|
94
107
|
}
|
|
95
108
|
catch {
|
|
@@ -102,7 +115,7 @@ function resolvePipInstallSet(binary, args, env) {
|
|
|
102
115
|
}
|
|
103
116
|
catch { /* already exited */ }
|
|
104
117
|
finish(UNRESOLVED);
|
|
105
|
-
},
|
|
118
|
+
}, pipResolveTimeoutMs(env));
|
|
106
119
|
child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
107
120
|
child.on("error", () => finish(UNRESOLVED));
|
|
108
121
|
child.on("close", (code) => finish(code === 0
|
|
@@ -154,7 +167,7 @@ export function renderProvenanceDowngrades(packages, out) {
|
|
|
154
167
|
out.write(` ⚠ ${pkg.name}@${pkg.version}: ${line} (display only, verdict unchanged)\n`);
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
|
-
export async function runInstallPreflight(manager, binary, childArgs, env, cwd = process.cwd()) {
|
|
170
|
+
export async function runInstallPreflight(manager, binary, childArgs, env, cwd = process.cwd(), beforeInteraction) {
|
|
158
171
|
if (manager !== "pip" || !binary) {
|
|
159
172
|
return PROCEED;
|
|
160
173
|
}
|
|
@@ -165,18 +178,42 @@ export async function runInstallPreflight(manager, binary, childArgs, env, cwd =
|
|
|
165
178
|
pipResolutions.set(key, resolution);
|
|
166
179
|
}
|
|
167
180
|
const set = resolution.set;
|
|
168
|
-
if (!set
|
|
181
|
+
if (!set) {
|
|
182
|
+
process.stderr.write(" dg: could not resolve the install set ahead of time — verifying during install instead\n");
|
|
183
|
+
return PROCEED;
|
|
184
|
+
}
|
|
185
|
+
if (set.length === 0) {
|
|
169
186
|
return PROCEED;
|
|
170
187
|
}
|
|
171
|
-
const cooldownContext = resolvePreflightCooldown(env);
|
|
188
|
+
const cooldownContext = resolvePreflightCooldown(env, "pypi");
|
|
172
189
|
let verdicts;
|
|
173
190
|
try {
|
|
174
191
|
verdicts = await analyzePackages(set.map((pkg) => ({ name: pkg.name, version: pkg.version })), { ecosystem: "pypi", env, ...(cooldownContext ? { cooldown: cooldownContext.param } : {}) });
|
|
175
192
|
}
|
|
176
193
|
catch {
|
|
194
|
+
process.stderr.write(" dg: pre-install check unavailable — verifying during install instead\n");
|
|
177
195
|
return PROCEED;
|
|
178
196
|
}
|
|
179
|
-
|
|
197
|
+
beforeInteraction?.();
|
|
198
|
+
const decision = await decideFromVerdicts(verdicts.packages, defaultPromptIo(), cooldownContext, resolvePreflightDecisions("pypi", cwd, env));
|
|
199
|
+
if (!decision.proceed || decision.forceOverride) {
|
|
200
|
+
return decision;
|
|
201
|
+
}
|
|
202
|
+
const preverified = preverifiedEntries(verdicts.packages, cooldownContext !== undefined);
|
|
203
|
+
return preverified.length > 0 ? { ...decision, preverified } : decision;
|
|
204
|
+
}
|
|
205
|
+
export function preverifiedEntries(packages, cooldownRequested) {
|
|
206
|
+
return packages
|
|
207
|
+
.filter((pkg) => pkg.action === "pass" || pkg.action === "warn")
|
|
208
|
+
.map((pkg) => ({
|
|
209
|
+
ecosystem: "pypi",
|
|
210
|
+
name: pkg.name,
|
|
211
|
+
version: pkg.version,
|
|
212
|
+
action: pkg.action,
|
|
213
|
+
...(pkg.reasons[0] ? { reason: pkg.reasons[0] } : {}),
|
|
214
|
+
...(typeof pkg.artifactSha256 === "string" && pkg.artifactSha256.length > 0 ? { scannedSha256: pkg.artifactSha256 } : {}),
|
|
215
|
+
cooldownEvaluated: cooldownRequested && pkg.cooldown !== undefined
|
|
216
|
+
}));
|
|
180
217
|
}
|
|
181
218
|
export async function decideFromVerdicts(packages, io, cooldownContext, decisions = null) {
|
|
182
219
|
if (io.isTTY) {
|
|
@@ -185,7 +222,7 @@ export async function decideFromVerdicts(packages, io, cooldownContext, decision
|
|
|
185
222
|
const covered = [];
|
|
186
223
|
const entries = packages
|
|
187
224
|
.map((pkg) => {
|
|
188
|
-
const viaCooldown = isQuarantined(pkg, cooldownContext) && pkg.action !== "block";
|
|
225
|
+
const viaCooldown = isQuarantined(pkg, cooldownContext, decisions?.file.cooldownExemptions ?? []) && pkg.action !== "block";
|
|
189
226
|
const action = viaCooldown ? "block" : pkg.action ?? "pass";
|
|
190
227
|
return { pkg, action, viaCooldown };
|
|
191
228
|
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { createLaunchPlan, runWithProductionProxyLive, EXIT_INSTALL_BLOCKED } from "./run.js";
|
|
1
|
+
import { createLaunchPlan, prepareProxyWorker, runWithProductionProxyLive, EXIT_INSTALL_BLOCKED } from "./run.js";
|
|
2
2
|
import { runInstallPreflight } from "./install-preflight.js";
|
|
3
3
|
import { isSupportedPackageManager } from "./classify.js";
|
|
4
4
|
import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
|
|
5
|
+
import { startPrepSpinner } from "../install-ui/prep-spinner.js";
|
|
5
6
|
const FALL_THROUGH = { handled: false };
|
|
6
7
|
export async function maybeRunLiveInstall(args, options = {}) {
|
|
7
8
|
const env = options.env ?? process.env;
|
|
@@ -17,24 +18,43 @@ export async function maybeRunLiveInstall(args, options = {}) {
|
|
|
17
18
|
if (plan.classification.kind !== "protected" || !plan.realBinary.path) {
|
|
18
19
|
return FALL_THROUGH;
|
|
19
20
|
}
|
|
21
|
+
const appLoading = import("../install-ui/live-install-app.js");
|
|
22
|
+
appLoading.catch(() => undefined);
|
|
23
|
+
let prepared = forceOverride ? undefined : prepareProxyWorker(plan.classification, env);
|
|
20
24
|
let effectiveOverride = forceOverride;
|
|
25
|
+
let preverified;
|
|
21
26
|
if (!forceOverride) {
|
|
22
|
-
const
|
|
27
|
+
const spinner = startPrepSpinner("DG preparing…");
|
|
28
|
+
let preflight;
|
|
29
|
+
try {
|
|
30
|
+
preflight = await runInstallPreflight(manager, plan.realBinary.path, childArgs, env, process.cwd(), spinner.stop);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
spinner.stop();
|
|
34
|
+
}
|
|
23
35
|
if (!preflight.proceed) {
|
|
36
|
+
prepared?.discard();
|
|
24
37
|
return { handled: true, result: { exitCode: EXIT_INSTALL_BLOCKED, stdout: "", stderr: " Install cancelled.\n" } };
|
|
25
38
|
}
|
|
26
39
|
effectiveOverride = preflight.forceOverride;
|
|
40
|
+
preverified = preflight.preverified;
|
|
41
|
+
if (effectiveOverride) {
|
|
42
|
+
prepared?.discard();
|
|
43
|
+
prepared = undefined;
|
|
44
|
+
}
|
|
27
45
|
}
|
|
28
46
|
const runOptions = {
|
|
29
47
|
env,
|
|
30
|
-
...(effectiveOverride ? { forceOverride: effectiveOverride } : {})
|
|
48
|
+
...(effectiveOverride ? { forceOverride: effectiveOverride } : {}),
|
|
49
|
+
...(preverified && preverified.length > 0 && !effectiveOverride ? { preverified } : {})
|
|
31
50
|
};
|
|
32
|
-
const { renderLiveInstall } = await import("../install-ui/live-install-app.js");
|
|
33
51
|
try {
|
|
34
|
-
const
|
|
52
|
+
const { renderLiveInstall } = await appLoading;
|
|
53
|
+
const result = await renderLiveInstall((onView) => runWithProductionProxyLive(plan, childArgs, runOptions, onView, prepared));
|
|
35
54
|
return { handled: true, result };
|
|
36
55
|
}
|
|
37
56
|
catch (error) {
|
|
57
|
+
prepared?.discard();
|
|
38
58
|
return {
|
|
39
59
|
handled: true,
|
|
40
60
|
result: {
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
const credentialUrlPattern = /\b([a-z][a-z0-9+.-]{0,31}:\/\/)([^/\s:@]+):([^/\s@]+)@/gi;
|
|
2
2
|
const authHeaderPattern = /\b(proxy-authorization|authorization):\s*[^\r\n]+/gi;
|
|
3
|
-
|
|
3
|
+
// Matches both KEY=value and colon-form (KEY: value, YAML) secret assignments;
|
|
4
|
+
// the separator and any opening quote are preserved in the replacement.
|
|
5
|
+
const tokenAssignmentPattern = /(?<![A-Za-z0-9])([A-Za-z0-9_-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret))(\s*[:=]\s*)("?)([^\s"]+)/gi;
|
|
4
6
|
const npmrcAuthPattern = /(_authToken|_auth|_password)\s*=\s*("[^"]*"|[^\s;,]+)/gi;
|
|
5
7
|
const jsonSecretPattern = /("[A-Za-z0-9_.$-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret)"\s*:\s*)"(?:[^"\\]|\\.)*"/gi;
|
|
6
8
|
const bearerPattern = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/g;
|
|
7
|
-
|
|
9
|
+
// Bare tokens (not in URL userinfo or KEY=value form), by their published shape.
|
|
10
|
+
const knownTokenShapePattern = /\b(npm_[A-Za-z0-9]{36}|gh[pousr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{22,255}|pypi-[A-Za-z0-9_-]{20,}|glpat-[A-Za-z0-9_-]{20,}|hf_[A-Za-z0-9]{30,}|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|sk_(?:live|test)_[A-Za-z0-9]{20,}|rk_live_[A-Za-z0-9]{20,}|sk-(?:proj-)?[A-Za-z0-9_-]{20,}|xox[abdeprs]-[A-Za-z0-9-]{10,})\b/g;
|
|
8
11
|
export function redactSecrets(text) {
|
|
9
12
|
return text
|
|
10
13
|
.replace(credentialUrlPattern, "$1<redacted>@")
|
|
11
14
|
.replace(authHeaderPattern, (_match, header) => `${header}: <redacted>`)
|
|
12
15
|
.replace(npmrcAuthPattern, "$1=<redacted>")
|
|
13
16
|
.replace(jsonSecretPattern, '$1"<redacted>"')
|
|
14
|
-
.replace(tokenAssignmentPattern, "$1
|
|
17
|
+
.replace(tokenAssignmentPattern, "$1$2$3<redacted>")
|
|
15
18
|
.replace(bearerPattern, "Bearer <redacted>")
|
|
16
19
|
.replace(knownTokenShapePattern, "<redacted>");
|
|
17
20
|
}
|
|
18
21
|
const STREAM_FLUSH_QUIET_MS = 80;
|
|
19
22
|
const SECRET_TAIL_SCAN_CHARS = 80;
|
|
20
|
-
const secretTailPattern = /(Bearer\s+[\w.~+/=-]*|npm_[A-Za-z0-9]*|gh[pousr]_[A-Za-z0-9]*|pypi-[\w-]*|glpat-[\w-]*|xox[
|
|
23
|
+
const secretTailPattern = /(Bearer\s+[\w.~+/=-]*|npm_[A-Za-z0-9]*|gh[pousr]_[A-Za-z0-9]*|github_pat_[A-Za-z0-9_]*|pypi-[\w-]*|glpat-[\w-]*|hf_[A-Za-z0-9]*|AKIA[0-9A-Z]*|ASIA[0-9A-Z]*|AIza[\w-]*|sk_(?:live|test)_[A-Za-z0-9]*|rk_live_[A-Za-z0-9]*|sk-(?:proj-)?[\w-]*|xox[abdeprs]-[\w-]*|[\w-]*(?:_authToken|_auth|_password|secret[_-]key|access[_-]key|api[_-]key|token|password|secret)\s*[:=]\s*"?\S*|"[\w.$-]*(?:secret[_-]key|access[_-]key|api[_-]key|token|password|secret)"\s*:\s*"?[^"\n]*)$/i;
|
|
21
24
|
export function createStreamRedactor(emit) {
|
|
22
25
|
let pending = "";
|
|
23
26
|
let timer;
|
|
@@ -4,11 +4,13 @@ import { matchDecision } from "../decisions/apply.js";
|
|
|
4
4
|
import { offerRememberOnIo } from "../decisions/remember-prompt.js";
|
|
5
5
|
import { renderInstallDecision } from "../install-ui/block-render.js";
|
|
6
6
|
import { defaultPromptIo } from "../install-ui/prompt.js";
|
|
7
|
+
import { normalizePypiName } from "../policy/pypi-name.js";
|
|
7
8
|
import { resolveAcceptedBy } from "../project/dgfile.js";
|
|
8
9
|
import { enforceProtectedInstall } from "../proxy/enforcement.js";
|
|
9
10
|
import { classifyPackageManagerInvocation, isSupportedPackageManager } from "./classify.js";
|
|
10
11
|
import { actionRank, promptPreflightYesNo, recordPreflightApprovals, renderCoveredWarns, renderProvenanceDowngrades, resolvePreflightDecisions } from "./install-preflight.js";
|
|
11
12
|
import { redactSecrets } from "./output-redaction.js";
|
|
13
|
+
import { startPrepSpinner } from "../install-ui/prep-spinner.js";
|
|
12
14
|
const ECOSYSTEM_BY_MANAGER = {
|
|
13
15
|
npm: "npm",
|
|
14
16
|
pnpm: "npm",
|
|
@@ -44,11 +46,13 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
|
|
|
44
46
|
const decisions = options.decisionsCwd ? resolvePreflightDecisions(ecosystem, options.decisionsCwd, env) : null;
|
|
45
47
|
const flagged = [];
|
|
46
48
|
const covered = [];
|
|
49
|
+
const spinner = startPrepSpinner("DG preparing…");
|
|
47
50
|
try {
|
|
48
51
|
const response = await (options.analyze ?? analyzePackages)(specs.map((spec) => ({ name: spec.name, version: spec.version })), { ecosystem, env });
|
|
52
|
+
spinner.stop();
|
|
49
53
|
renderProvenanceDowngrades(response.packages, io.output);
|
|
50
54
|
for (const spec of specs) {
|
|
51
|
-
const pkg = response.packages.find((entry) => entry.name
|
|
55
|
+
const pkg = response.packages.find((entry) => sameSpecName(entry.name, spec.name, ecosystem) && entry.version === spec.version);
|
|
52
56
|
const action = pkg?.action ?? "pass";
|
|
53
57
|
if (action === "pass") {
|
|
54
58
|
continue;
|
|
@@ -68,6 +72,9 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
|
|
|
68
72
|
catch {
|
|
69
73
|
return FALL_THROUGH;
|
|
70
74
|
}
|
|
75
|
+
finally {
|
|
76
|
+
spinner.stop();
|
|
77
|
+
}
|
|
71
78
|
if (covered.length > 0) {
|
|
72
79
|
renderCoveredWarns(covered, decisions, io.output);
|
|
73
80
|
recordPreflightApprovals(covered.map((pkg) => ({ name: pkg.name, version: pkg.version, action: "warn" })));
|
|
@@ -131,6 +138,12 @@ export async function maybePreflightInstallPrompt(args, options = {}) {
|
|
|
131
138
|
result: { exitCode: 1, stdout: "", stderr: "Declined. Nothing was installed.\n" }
|
|
132
139
|
};
|
|
133
140
|
}
|
|
141
|
+
function sameSpecName(entryName, specName, ecosystem) {
|
|
142
|
+
if (ecosystem === "pypi") {
|
|
143
|
+
return normalizePypiName(entryName) === normalizePypiName(specName);
|
|
144
|
+
}
|
|
145
|
+
return entryName === specName;
|
|
146
|
+
}
|
|
134
147
|
function asFlaggedPackage(entry) {
|
|
135
148
|
return {
|
|
136
149
|
name: entry.spec.name,
|