@4da/mcp-server 6.0.2 → 6.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/CHANGELOG.md +433 -0
- package/README.md +26 -18
- package/dist/db.d.ts +73 -8
- package/dist/db.js +180 -43
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +15 -9
- package/dist/embeddings.js +19 -7
- package/dist/gitignore.js +9 -3
- package/dist/index.js +43 -4
- package/dist/live/cache.d.ts +31 -2
- package/dist/live/cache.js +113 -1
- package/dist/live/changelog-classify.d.ts +9 -3
- package/dist/live/changelog-classify.js +125 -12
- package/dist/live/changelog.d.ts +22 -3
- package/dist/live/changelog.js +132 -38
- package/dist/live/dev-scope.d.ts +2 -2
- package/dist/live/fix-paths.d.ts +118 -0
- package/dist/live/fix-paths.js +498 -0
- package/dist/live/github-releases.d.ts +69 -0
- package/dist/live/github-releases.js +252 -0
- package/dist/live/go-registry.d.ts +5 -0
- package/dist/live/go-registry.js +1 -1
- package/dist/live/go-releases.d.ts +48 -0
- package/dist/live/go-releases.js +196 -0
- package/dist/live/index.d.ts +34 -8
- package/dist/live/index.js +66 -7
- package/dist/live/lockfile-parents.d.ts +61 -0
- package/dist/live/lockfile-parents.js +360 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +31 -0
- package/dist/live/lockfile-parsers-pygo.js +178 -28
- package/dist/live/lockfile-parsers.d.ts +4 -1
- package/dist/live/lockfile-parsers.js +57 -24
- package/dist/live/lockfile-types.d.ts +12 -3
- package/dist/live/lockfile-types.js +2 -1
- package/dist/live/osv-ranges.d.ts +85 -0
- package/dist/live/osv-ranges.js +136 -0
- package/dist/live/osv-scanner.d.ts +1 -0
- package/dist/live/osv-scanner.js +85 -78
- package/dist/live/package-archive.d.ts +2 -0
- package/dist/live/package-archive.js +7 -4
- package/dist/live/pypi-releases.d.ts +44 -0
- package/dist/live/pypi-releases.js +103 -0
- package/dist/live/range-satisfies.d.ts +21 -0
- package/dist/live/range-satisfies.js +190 -0
- package/dist/live/rate-limiter.d.ts +6 -0
- package/dist/live/rate-limiter.js +5 -0
- package/dist/live/registry-health.js +2 -0
- package/dist/live/release-metadata.d.ts +17 -8
- package/dist/live/release-metadata.js +6 -3
- package/dist/live/resolution.d.ts +11 -1
- package/dist/live/resolution.js +16 -6
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +10 -2
- package/dist/live/types.d.ts +17 -1
- package/dist/live/upgrade-go.d.ts +62 -0
- package/dist/live/upgrade-go.js +130 -0
- package/dist/live/upgrade-pypi.d.ts +42 -0
- package/dist/live/upgrade-pypi.js +85 -0
- package/dist/live/upgrade-sources.d.ts +30 -11
- package/dist/live/upgrade-sources.js +93 -21
- package/dist/live/zip-archive.d.ts +22 -0
- package/dist/live/zip-archive.js +98 -0
- package/dist/memory-store.d.ts +19 -0
- package/dist/memory-store.js +8 -0
- package/dist/native-bindings.d.ts +7 -2
- package/dist/native-bindings.js +4 -0
- package/dist/project-tree.d.ts +50 -8
- package/dist/project-tree.js +121 -29
- package/dist/schema-registry.js +5 -5
- package/dist/server-instructions.d.ts +6 -0
- package/dist/server-instructions.js +20 -15
- package/dist/sqlite-driver.d.ts +134 -0
- package/dist/sqlite-driver.js +359 -0
- package/dist/tool-dispatch.d.ts +8 -1
- package/dist/tool-dispatch.js +17 -2
- package/dist/tools/agent-memory.js +10 -2
- package/dist/tools/app-plan.d.ts +11 -1
- package/dist/tools/app-plan.js +32 -6
- package/dist/tools/briefing-task-scope.d.ts +21 -2
- package/dist/tools/briefing-task-scope.js +97 -4
- package/dist/tools/decision-memory.js +9 -3
- package/dist/tools/dependency-check-signals.d.ts +23 -5
- package/dist/tools/dependency-check-signals.js +62 -19
- package/dist/tools/dependency-check-versions.d.ts +94 -0
- package/dist/tools/dependency-check-versions.js +213 -0
- package/dist/tools/dependency-check.d.ts +27 -11
- package/dist/tools/dependency-check.js +321 -74
- package/dist/tools/dependency-health.d.ts +1 -1
- package/dist/tools/dependency-health.js +5 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +1 -1
- package/dist/tools/knowledge-gap-ranges.js +1 -1
- package/dist/tools/knowledge-gap-scope.d.ts +2 -2
- package/dist/tools/project-scope.d.ts +45 -0
- package/dist/tools/project-scope.js +119 -0
- package/dist/tools/record-feedback.js +1 -1
- package/dist/tools/upgrade-impact-callsites.d.ts +1 -0
- package/dist/tools/upgrade-impact-callsites.js +23 -5
- package/dist/tools/upgrade-impact-changelog.d.ts +41 -0
- package/dist/tools/upgrade-impact-changelog.js +79 -0
- package/dist/tools/upgrade-impact-imports.d.ts +42 -0
- package/dist/tools/upgrade-impact-imports.js +260 -0
- package/dist/tools/upgrade-impact-literals.js +1 -1
- package/dist/tools/upgrade-impact-report.d.ts +23 -0
- package/dist/tools/upgrade-impact-report.js +45 -6
- package/dist/tools/upgrade-impact.d.ts +13 -4
- package/dist/tools/upgrade-impact.js +236 -85
- package/dist/tools/upgrade-planner-fixpaths.d.ts +40 -0
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +31 -3
- package/dist/tools/upgrade-planner.js +74 -19
- package/dist/tools/vulnerability-scan-concise.d.ts +13 -1
- package/dist/tools/vulnerability-scan-concise.js +15 -3
- package/dist/tools/vulnerability-scan-format.d.ts +58 -1
- package/dist/tools/vulnerability-scan-format.js +107 -11
- package/dist/tools/vulnerability-scan.d.ts +104 -13
- package/dist/tools/vulnerability-scan.js +27 -3
- package/dist/tools/what-should-i-know.d.ts +22 -2
- package/dist/tools/what-should-i-know.js +122 -12
- package/package.json +10 -32
|
@@ -3,53 +3,69 @@
|
|
|
3
3
|
* dependency_check tool
|
|
4
4
|
*
|
|
5
5
|
* A pre-flight verdict for ONE proposed change per item: adding a dependency
|
|
6
|
-
* at a version, or
|
|
7
|
-
*
|
|
8
|
-
* one-line reason, and the
|
|
6
|
+
* at a version, pinning one, or moving an installed one to another version.
|
|
7
|
+
* An agent calls this before it writes a version into a manifest and gets
|
|
8
|
+
* `proceed | wait | review | avoid | unknown`, a one-line reason, and the
|
|
9
|
+
* signals behind it, each with evidence. npm, crates.io, PyPI and Go.
|
|
10
|
+
*
|
|
11
|
+
* `to` is an exact version, "latest", or a constraint ("7.x", "^1.20",
|
|
12
|
+
* "~=3.1", "v1"). For a constraint the item also returns `range_check`: every
|
|
13
|
+
* release it admits judged against every advisory OSV.dev lists for the
|
|
14
|
+
* package, with the smallest and newest safe one, and the verdict is given
|
|
15
|
+
* for the version it recommends (the smallest safe upgrade above `from`, or
|
|
16
|
+
* for a new dependency the newest safe release). An exact `to` that an
|
|
17
|
+
* advisory affects gets the same `range_check` over its own release line, so
|
|
18
|
+
* the answer to "is 7.5.10 safe?" names 7.5.11.
|
|
9
19
|
*
|
|
10
20
|
* Signals (see dependency-check-signals.ts): known advisories on the target
|
|
11
21
|
* (OSV), release age (hold releases < 3 days unless they fix an advisory on
|
|
12
|
-
* the installed version), publish-trust drop
|
|
13
|
-
* (npm), newly introduced runtime dependencies that are
|
|
14
|
-
* old, yanked/deprecated/unpublished
|
|
22
|
+
* the installed version), publish-trust drop and newly added install scripts
|
|
23
|
+
* (npm; trust also crates.io), newly introduced runtime dependencies that are
|
|
24
|
+
* themselves < 30 days old (npm, crates.io), yanked / deprecated / unpublished
|
|
25
|
+
* / retracted targets, and the upgrade type. A signal a registry cannot
|
|
26
|
+
* supply says "not applicable", never a made-up value.
|
|
15
27
|
*
|
|
16
28
|
* Honesty contract: a registry or OSV that cannot be reached makes the item
|
|
17
29
|
* `unknown`, never `proceed`. A package missing from the public registry is
|
|
18
30
|
* `unknown` with that said (private package, or a misspelled/hallucinated
|
|
19
31
|
* name), never treated as clean.
|
|
20
32
|
*
|
|
21
|
-
* Privacy:
|
|
22
|
-
* target versions are picked out of the full release list
|
|
23
|
-
*
|
|
33
|
+
* Privacy: npm, crates.io and PyPI requests carry the package name only — the
|
|
34
|
+
* installed and target versions are picked out of the full release list
|
|
35
|
+
* locally. The Go proxy lists versions without dates, so the judged versions'
|
|
36
|
+
* `.info` is read from it. OSV receives name + version, as vulnerability_scan
|
|
37
|
+
* already does, and the package name for its advisory list.
|
|
24
38
|
*/
|
|
25
39
|
import { isMaintenanceNotice } from "../live/maintenance.js";
|
|
26
|
-
import {
|
|
27
|
-
import { advisoriesSignal, decide, installScriptSignal, introducedDependencies, newDependenciesSignal, publishTrustSignal, releaseAgeSignal, selectBaseline, upgradeTypeSignal, withdrawnSignal, } from "./dependency-check-signals.js";
|
|
40
|
+
import { goPathMajor, goVersionMajor } from "../live/upgrade-go.js";
|
|
41
|
+
import { advisoriesSignal, decide, installScriptSignal, introducedDependencies, newDependenciesSignal, publishTrustSignal, releaseAgeSignal, selectBaseline, upgradeTypeSignal, withdrawnSignal, WAIT_DAYS, } from "./dependency-check-signals.js";
|
|
42
|
+
import { checkRange, compareIn, isPrereleaseIn, lineOf, parseTarget, rangeAdmits } from "./dependency-check-versions.js";
|
|
28
43
|
const MAX_ITEMS = 25;
|
|
29
44
|
/** First-publish lookups for newly introduced dependencies, per call (each is a registry read). */
|
|
30
45
|
const MAX_NEW_DEP_LOOKUPS = 40;
|
|
31
46
|
const NPM_CONCURRENCY = 4;
|
|
32
|
-
const
|
|
33
|
-
|
|
47
|
+
const ECOSYSTEMS = ["npm", "crates.io", "PyPI", "Go"];
|
|
48
|
+
const DAY_MS = 86_400_000;
|
|
49
|
+
const PRIVACY_NOTE = "Registry requests carried package names only (npm full packument, crates.io versions API and sparse index, PyPI JSON API, Go proxy version list); " +
|
|
50
|
+
"installed and target versions were never sent to a registry, except the Go proxy's per-version .info for the versions judged (the list has no dates). " +
|
|
51
|
+
"OSV.dev received package names and versions, as vulnerability_scan does.";
|
|
34
52
|
const VERDICT_RULE = "avoid > review > unknown > wait > proceed. unknown = a registry or OSV.dev lookup failed (never read as proceed); " +
|
|
35
53
|
"a concrete avoid/review finding still wins over a missing source.";
|
|
36
54
|
export const dependencyCheckTool = {
|
|
37
55
|
name: "dependency_check",
|
|
38
56
|
description: [
|
|
39
|
-
"
|
|
57
|
+
"Is this dependency version safe to write? Call BEFORE you write any version into a manifest or lockfile: adding, pinning, upgrading, bumping or downgrading a package, or choosing a version (npm, crates.io, PyPI, Go). Pass the exact version you intend to write, or the constraint you were given (\"7.x\", \"^1.20\", \"~=3.1\", \"v1\") to get the smallest and newest safe release in it. Write only a version that came back `proceed`. Your memory of which release is safe is out of date; this reads the registry and OSV.dev now.",
|
|
40
58
|
"",
|
|
41
|
-
"Per item
|
|
42
|
-
"- advisories:
|
|
43
|
-
"- release_age:
|
|
44
|
-
"-
|
|
45
|
-
"-
|
|
46
|
-
"-
|
|
47
|
-
"- yanked_or_deprecated: `to` yanked, unpublished or deprecated -> avoid.",
|
|
48
|
-
"- upgrade_type: patch | minor | major (a 0.x minor counts as major) | downgrade | new_dependency. Information only.",
|
|
59
|
+
"Per item: `verdict` (proceed | wait | review | avoid | unknown), a one-line `reason`, `signals` (each with `id`, `value`, `evidence`, `effect`), and `range_check` when `to` is a constraint or an exact `to` has an advisory: `smallest_safe_in_range`, `newest_safe_in_range`, `safe_versions`, `smallest_safe_upgrade` (above `from`). For a constraint the verdict is for the version it picks: the smallest safe upgrade above `from`, else the newest safe release.",
|
|
60
|
+
"- advisories: OSV advisories affecting `to` (high/critical -> avoid, other -> review) and those this change fixes on `from`.",
|
|
61
|
+
"- release_age: under 3 days -> wait, unless `to` fixes an advisory affecting `from`, or every release on its line past the hold has an advisory and no safe one is past it (then proceed; tell the user its age).",
|
|
62
|
+
"- yanked_or_deprecated: yanked, unpublished, retracted (Go) or deprecated -> avoid.",
|
|
63
|
+
"- publish_trust (npm, crates.io): trusted publisher / provenance dropped -> review. install_script_added (npm): new install script -> review. new_dependencies (npm, crates.io): a new runtime dependency under 30 days old -> review. Not applicable elsewhere, and said so.",
|
|
64
|
+
"- upgrade_type: major (0.x minor counts) -> review: call upgrade_impact for what it breaks.",
|
|
49
65
|
"",
|
|
50
|
-
"
|
|
66
|
+
"`unknown` = a lookup failed or the package is not on the public registry (private, misspelled or hallucinated name); never a silent proceed.",
|
|
51
67
|
"",
|
|
52
|
-
"Privacy:
|
|
68
|
+
"Privacy: registries get the package name only (Go: also the judged versions' .info, as `go get` does); OSV.dev gets names and versions, as vulnerability_scan does. FOURDA_OFFLINE=true disables lookups (every item unknown).",
|
|
53
69
|
].join("\n"),
|
|
54
70
|
inputSchema: {
|
|
55
71
|
type: "object",
|
|
@@ -62,12 +78,15 @@ export const dependencyCheckTool = {
|
|
|
62
78
|
items: {
|
|
63
79
|
type: "object",
|
|
64
80
|
properties: {
|
|
65
|
-
ecosystem: { type: "string", enum: ["npm", "crates.io"], description: "Package registry." },
|
|
81
|
+
ecosystem: { type: "string", enum: ["npm", "crates.io", "PyPI", "Go"], description: "Package registry." },
|
|
66
82
|
package: {
|
|
67
83
|
type: "string",
|
|
68
|
-
description: 'Package name exactly as published (
|
|
84
|
+
description: 'Package name exactly as published ("axios", "@scope/pkg", "serde_json", "Jinja2", "golang.org/x/net", "github.com/labstack/echo/v4").',
|
|
85
|
+
},
|
|
86
|
+
to: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: 'The version you intend to write ("7.5.10", "3.1.4", "v0.23.0"), "latest", or the constraint it must satisfy ("7.x", "^1.20", "=0.17", "~=3.1", ">=2,<3", "v1") to get the safe releases in it.',
|
|
69
89
|
},
|
|
70
|
-
to: { type: "string", description: 'Exact version you intend to install (e.g. "1.14.1"), or "latest" for the newest stable release; not a range.' },
|
|
71
90
|
from: { type: "string", description: "Exact version currently installed. Omit when adding a new dependency." },
|
|
72
91
|
},
|
|
73
92
|
required: ["ecosystem", "package", "to"],
|
|
@@ -83,15 +102,28 @@ export async function executeDependencyCheck(_db, params, liveIntel, opts = {})
|
|
|
83
102
|
const checkedAt = now.toISOString();
|
|
84
103
|
if (!liveIntel || !liveIntel.isEnabled()) {
|
|
85
104
|
const reason = "Live lookups are disabled (FOURDA_OFFLINE=true); nothing about these versions could be checked.";
|
|
86
|
-
return finish(checkedAt, requested.map((
|
|
105
|
+
return finish(checkedAt, requested.map((r) => ({ ...head(provisional(r)), verdict: "unknown", reason, signals: [] })), { offline: true, osvCached: false });
|
|
87
106
|
}
|
|
88
107
|
const sources = liveIntel.getReleaseSources();
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
const items = requested.map((
|
|
94
|
-
|
|
108
|
+
const fixSources = liveIntel.getFixPathSources();
|
|
109
|
+
const indexes = await fetchIndexes(requested.map(provisional), sources, new Set(requested.filter(needsFullList).map(lookupKey)));
|
|
110
|
+
// Constraints are judged against the package's whole advisory list.
|
|
111
|
+
const advisoryLists = await packageAdvisoryLists(fixSources, requested.filter((r) => r.target.kind === "range" || r.target.kind === "exact_or_series"));
|
|
112
|
+
const items = requested.map((r) => resolveTarget(r, indexes.get(lookupKey(r)), advisoryLists.get(lookupKey(r)), now));
|
|
113
|
+
// Go dates come from per-version .info: read them for versions chosen from the list
|
|
114
|
+
// (a constraint, "latest", or `v2.0.0` listed as `v2.0.0+incompatible`).
|
|
115
|
+
const goRedo = items.filter((i) => {
|
|
116
|
+
const lookup = indexes.get(lookupKey(i));
|
|
117
|
+
return i.ecosystem === "Go" && !i.unresolved && lookup?.status === "ok" && lookup.data.releases[i.to] && !lookup.data.releases[i.to].publishedAt;
|
|
118
|
+
});
|
|
119
|
+
if (goRedo.length > 0) {
|
|
120
|
+
for (const [key, lookup] of await fetchIndexes(goRedo, sources, new Set()))
|
|
121
|
+
indexes.set(key, lookup);
|
|
122
|
+
await settleGoPicks(goRedo, indexes, sources.Go, now);
|
|
123
|
+
}
|
|
124
|
+
const osvDeps = items
|
|
125
|
+
.filter((i) => !i.unresolved)
|
|
126
|
+
.flatMap((item) => [item.to, item.from].filter((v) => Boolean(v)).map((version) => osvDep(item, version)));
|
|
95
127
|
const scan = await liveIntel.queryAdvisories(osvDeps);
|
|
96
128
|
const byInstance = new Map();
|
|
97
129
|
for (const v of scan.vulnerabilities) {
|
|
@@ -105,6 +137,20 @@ export async function executeDependencyCheck(_db, params, liveIntel, opts = {})
|
|
|
105
137
|
to: refs(byInstance.get(instanceKey(item.ecosystem, item.package, item.to))),
|
|
106
138
|
from: item.from ? refs(byInstance.get(instanceKey(item.ecosystem, item.package, item.from))) : [],
|
|
107
139
|
};
|
|
140
|
+
// An exact `to` an advisory affects: the safe releases on its own line.
|
|
141
|
+
const affectedExact = items.filter((i) => !i.unresolved && !i.rangeCheck && advisoriesFor(i).available && hasRealAdvisory(advisoriesFor(i)));
|
|
142
|
+
const moreLists = await packageAdvisoryLists(fixSources, affectedExact.filter((i) => !advisoryLists.has(lookupKey(i))));
|
|
143
|
+
for (const [k, v] of moreLists)
|
|
144
|
+
advisoryLists.set(k, v);
|
|
145
|
+
for (const item of affectedExact) {
|
|
146
|
+
const lookup = indexes.get(lookupKey(item));
|
|
147
|
+
const advisories = advisoryLists.get(lookupKey(item));
|
|
148
|
+
const line = lineOf(item.ecosystem, item.to);
|
|
149
|
+
if (lookup.status !== "ok" || !advisories || !line)
|
|
150
|
+
continue;
|
|
151
|
+
item.rangeCheck = checkRange({ ecosystem: item.ecosystem, index: lookup.data, range: line, basis: "same_line", advisories, from: item.from });
|
|
152
|
+
}
|
|
153
|
+
const holdWaivers = await findHoldWaivers(items, indexes, advisoryLists, fixSources, sources.Go, advisoriesFor, now);
|
|
108
154
|
// Phase 2: first-publish dates of newly introduced dependencies.
|
|
109
155
|
const plans = items.map((item) => planItem(item, indexes.get(lookupKey(item))));
|
|
110
156
|
const wantedCreated = new Map();
|
|
@@ -121,16 +167,20 @@ export async function executeDependencyCheck(_db, params, liveIntel, opts = {})
|
|
|
121
167
|
created.set(key, lookup.status === "ok" ? { status: "ok", createdAt: lookup.data } : lookup);
|
|
122
168
|
});
|
|
123
169
|
const results = plans.map((p) => {
|
|
170
|
+
const extra = p.item.rangeCheck ? { range_check: p.item.rangeCheck } : {};
|
|
124
171
|
if (p.blocked) {
|
|
125
172
|
// No release metadata: only the advisory lookup can still speak, and
|
|
126
173
|
// only a negative finding is worth more than "unknown".
|
|
174
|
+
if (p.item.unresolved)
|
|
175
|
+
return { ...head(p.item), verdict: "unknown", reason: p.blocked, signals: [], ...extra };
|
|
127
176
|
const advisories = advisoriesSignal(p.item, advisoriesFor(p.item));
|
|
128
177
|
const negative = advisories.effect === "avoid" || advisories.effect === "review";
|
|
129
178
|
return {
|
|
130
179
|
...head(p.item),
|
|
131
180
|
verdict: negative ? advisories.effect : "unknown",
|
|
132
|
-
reason: negative ? `advisories: ${advisories.evidence} | ${p.blocked}` : p.blocked,
|
|
181
|
+
reason: withAlternatives(negative ? `advisories: ${advisories.evidence} | ${p.blocked}` : p.blocked, p.item, negative),
|
|
133
182
|
signals: [advisories],
|
|
183
|
+
...extra,
|
|
134
184
|
};
|
|
135
185
|
}
|
|
136
186
|
const { item, index } = p;
|
|
@@ -145,17 +195,205 @@ export async function executeDependencyCheck(_db, params, liveIntel, opts = {})
|
|
|
145
195
|
}
|
|
146
196
|
const signals = [
|
|
147
197
|
advisories,
|
|
148
|
-
releaseAgeSignal(item, to, advisories, now),
|
|
198
|
+
releaseAgeSignal(item, to, advisories, now, holdWaivers.get(item)),
|
|
149
199
|
publishTrustSignal(item, to, base),
|
|
150
200
|
installScriptSignal(item, to, item.from ? index.releases[item.from] ?? null : null),
|
|
151
|
-
newDependenciesSignal(to, base, p.introduced, createdForItem),
|
|
201
|
+
newDependenciesSignal(item, to, base, p.introduced, createdForItem),
|
|
152
202
|
withdrawnSignal(item, to),
|
|
153
203
|
upgradeTypeSignal(item),
|
|
154
204
|
];
|
|
155
|
-
|
|
205
|
+
const decided = decide(signals);
|
|
206
|
+
const unsafe = decided.verdict === "avoid" || decided.verdict === "review";
|
|
207
|
+
return { ...head(item), verdict: decided.verdict, reason: withAlternatives(decided.reason, item, unsafe), signals, ...extra };
|
|
156
208
|
});
|
|
157
209
|
return finish(checkedAt, results, { offline: scan.offline, osvCached: scan.cached });
|
|
158
210
|
}
|
|
211
|
+
/** The reason, plus the safe releases to write instead when the verdict is against `to`. */
|
|
212
|
+
function withAlternatives(reason, item, unsafe) {
|
|
213
|
+
const rc = item.rangeCheck;
|
|
214
|
+
if (!rc)
|
|
215
|
+
return reason;
|
|
216
|
+
const where = rc.basis === "requested" ? `in ${rc.range}` : `on ${item.to}'s line (${rc.range})`;
|
|
217
|
+
if (rc.smallest_safe_in_range === null) {
|
|
218
|
+
const ids = rc.affecting_all ?? [];
|
|
219
|
+
const all = ids.length ? `: ${ids.slice(0, 3).join(", ")}${ids.length > 3 ? ` and ${ids.length - 3} more` : ""} affect every one` : "";
|
|
220
|
+
const outside = rc.newest_safe_outside_range ? ` Newest safe release outside it: ${rc.newest_safe_outside_range}.` : "";
|
|
221
|
+
return `${reason} | No release ${where} is free of known advisories${all}.${outside}`;
|
|
222
|
+
}
|
|
223
|
+
const upgrade = rc.smallest_safe_upgrade && rc.smallest_safe_upgrade !== rc.smallest_safe_in_range ? `, smallest above ${item.from} ${rc.smallest_safe_upgrade}` : "";
|
|
224
|
+
const safe = `Safe ${where}: smallest ${rc.smallest_safe_in_range}${upgrade}, newest ${rc.newest_safe_in_range}.`;
|
|
225
|
+
if (rc.basis === "same_line" && !unsafe)
|
|
226
|
+
return reason;
|
|
227
|
+
return `${reason} | ${safe}`;
|
|
228
|
+
}
|
|
229
|
+
function hasRealAdvisory(facts) {
|
|
230
|
+
return facts.available && facts.to.some((a) => !a.maintenance);
|
|
231
|
+
}
|
|
232
|
+
/** Before resolution: an exact `to` as is, anything else shown as the caller wrote it. */
|
|
233
|
+
function provisional(r) {
|
|
234
|
+
const to = r.target.kind === "exact" ? r.target.version : r.raw;
|
|
235
|
+
return { ecosystem: r.ecosystem, package: r.package, to, ...(r.from ? { from: r.from } : {}) };
|
|
236
|
+
}
|
|
237
|
+
function needsFullList(r) {
|
|
238
|
+
return r.target.kind !== "exact";
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* One version per item: an exact `to` (Go: `+incompatible` when that is how
|
|
242
|
+
* the proxy lists it), "latest" (newest stable, not withdrawn), or the
|
|
243
|
+
* version a constraint recommends.
|
|
244
|
+
*/
|
|
245
|
+
function resolveTarget(r, lookup, advisories, now) {
|
|
246
|
+
const base = provisional(r);
|
|
247
|
+
if (lookup.status !== "ok")
|
|
248
|
+
return base;
|
|
249
|
+
const index = lookup.data;
|
|
250
|
+
let target = r.target;
|
|
251
|
+
if (target.kind === "exact_or_series")
|
|
252
|
+
target = index.releases[target.version] ? { kind: "exact", version: target.version } : { kind: "range", range: target.range };
|
|
253
|
+
if (target.kind === "exact") {
|
|
254
|
+
let version = target.version;
|
|
255
|
+
if (r.ecosystem === "Go" && !index.releases[version] && index.releases[`${version}+incompatible`])
|
|
256
|
+
version = `${version}+incompatible`;
|
|
257
|
+
// PEP 440 spellings differ ("3.1.06", "1.0-post1"); use the registry's.
|
|
258
|
+
if (r.ecosystem === "PyPI" && !index.releases[version]) {
|
|
259
|
+
version = Object.keys(index.releases).find((v) => compareIn("PyPI", v, version) === 0) ?? version;
|
|
260
|
+
}
|
|
261
|
+
return { ...base, to: version };
|
|
262
|
+
}
|
|
263
|
+
if (target.kind === "latest") {
|
|
264
|
+
const newest = newestOf(r.ecosystem, Object.values(index.releases).filter((x) => !x.withdrawn).map((x) => x.version));
|
|
265
|
+
return newest ? { ...base, to: newest, requested: "latest" } : base;
|
|
266
|
+
}
|
|
267
|
+
const range = target.range;
|
|
268
|
+
const admitted = Object.values(index.releases)
|
|
269
|
+
.filter((x) => !x.withdrawn && rangeAdmits(r.ecosystem, range, x.version) === true)
|
|
270
|
+
.map((x) => x.version);
|
|
271
|
+
if (admitted.length === 0) {
|
|
272
|
+
return { ...base, requested: r.raw, unresolved: `No published, non-withdrawn release of ${r.package} matches ${range}.` };
|
|
273
|
+
}
|
|
274
|
+
if (!advisories) {
|
|
275
|
+
// Without the advisory list no release can be called safe; judge the newest one on its own advisories.
|
|
276
|
+
return { ...base, to: newestOf(r.ecosystem, admitted), requested: r.raw };
|
|
277
|
+
}
|
|
278
|
+
const rangeCheck = checkRange({ ecosystem: r.ecosystem, index, range, basis: "requested", advisories, from: r.from });
|
|
279
|
+
const safe = rangeCheck.safe_versions;
|
|
280
|
+
const settled = (v) => {
|
|
281
|
+
const at = index.releases[v]?.publishedAt;
|
|
282
|
+
return !at || (now.getTime() - Date.parse(at)) / DAY_MS >= WAIT_DAYS;
|
|
283
|
+
};
|
|
284
|
+
const pick = r.from
|
|
285
|
+
? rangeCheck.smallest_safe_upgrade
|
|
286
|
+
: ([...safe].reverse().find(settled) ?? rangeCheck.newest_safe_in_range);
|
|
287
|
+
return { ...base, to: pick ?? newestOf(r.ecosystem, admitted), requested: r.raw, rangeCheck };
|
|
288
|
+
}
|
|
289
|
+
/** `.info` reads spent looking for a Go release past the hold, per item. */
|
|
290
|
+
const GO_SETTLE_READS = 3;
|
|
291
|
+
/**
|
|
292
|
+
* Go lists carry no dates, so a constraint's newest safe release was picked
|
|
293
|
+
* blind to the 3-day hold. When it turns out to be inside it, step down the
|
|
294
|
+
* safe list (newest first) to the newest one that is not, reading `.info`
|
|
295
|
+
* for each step. A pick above `from` (the smallest fix) is left alone.
|
|
296
|
+
*/
|
|
297
|
+
async function settleGoPicks(items, indexes, go, now) {
|
|
298
|
+
const young = (at) => Boolean(at) && (now.getTime() - Date.parse(at)) / DAY_MS < WAIT_DAYS;
|
|
299
|
+
for (const item of items) {
|
|
300
|
+
const rc = item.rangeCheck;
|
|
301
|
+
const lookup = indexes.get(lookupKey(item));
|
|
302
|
+
if (!rc || item.from || rc.basis !== "requested" || lookup?.status !== "ok")
|
|
303
|
+
continue;
|
|
304
|
+
if (!young(lookup.data.releases[item.to]?.publishedAt))
|
|
305
|
+
continue;
|
|
306
|
+
const older = rc.safe_versions.filter((v) => compareIn("Go", v, item.to) < 0).reverse().slice(0, GO_SETTLE_READS);
|
|
307
|
+
for (const v of older) {
|
|
308
|
+
const next = await go.getReleases(item.package, [v, item.to]);
|
|
309
|
+
if (next.status !== "ok")
|
|
310
|
+
break;
|
|
311
|
+
if (next.data.releases[v])
|
|
312
|
+
lookup.data.releases[v] = next.data.releases[v];
|
|
313
|
+
if (!young(next.data.releases[v]?.publishedAt)) {
|
|
314
|
+
item.to = v;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** Newest release, stable ones first. */
|
|
321
|
+
function newestOf(ecosystem, versions) {
|
|
322
|
+
const stable = versions.filter((v) => !isPrereleaseIn(ecosystem, v));
|
|
323
|
+
const pool = stable.length > 0 ? stable : versions;
|
|
324
|
+
return pool.reduce((max, v) => (max === null || compareIn(ecosystem, v, max) > 0 ? v : max), null);
|
|
325
|
+
}
|
|
326
|
+
/** Go publish dates read per item to decide a hold waiver (the list has none). */
|
|
327
|
+
const GO_WAIVER_READS = 8;
|
|
328
|
+
function isYoung(at, now) {
|
|
329
|
+
return Boolean(at) && (now.getTime() - Date.parse(at)) / DAY_MS < WAIT_DAYS;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Clean `to` versions inside the release-age hold for which holding back
|
|
333
|
+
* leaves nothing safe to write: every safe release on the line is inside the
|
|
334
|
+
* hold as well, and the line has older releases, all with a known advisory.
|
|
335
|
+
* A package whose only releases are new keeps the hold, which is what it is for.
|
|
336
|
+
* An undated release (Go lists carry no dates) counts as old, never as safe.
|
|
337
|
+
*/
|
|
338
|
+
async function findHoldWaivers(items, indexes, advisoryLists, fixSources, go, advisoriesFor, now) {
|
|
339
|
+
const out = new Map();
|
|
340
|
+
const held = items.filter((i) => {
|
|
341
|
+
const lookup = indexes.get(lookupKey(i));
|
|
342
|
+
if (i.unresolved || lookup?.status !== "ok")
|
|
343
|
+
return false;
|
|
344
|
+
const facts = advisoriesFor(i);
|
|
345
|
+
return facts.available && !hasRealAdvisory(facts) && isYoung(lookup.data.releases[i.to]?.publishedAt, now);
|
|
346
|
+
});
|
|
347
|
+
if (held.length === 0)
|
|
348
|
+
return out;
|
|
349
|
+
const missing = held.filter((i) => !advisoryLists.has(lookupKey(i)));
|
|
350
|
+
for (const [k, v] of await packageAdvisoryLists(fixSources, missing))
|
|
351
|
+
advisoryLists.set(k, v);
|
|
352
|
+
for (const item of held) {
|
|
353
|
+
const lookup = indexes.get(lookupKey(item));
|
|
354
|
+
const advisories = advisoryLists.get(lookupKey(item));
|
|
355
|
+
const line = lineOf(item.ecosystem, item.to);
|
|
356
|
+
if (lookup?.status !== "ok" || !advisories || !line)
|
|
357
|
+
continue;
|
|
358
|
+
const index = lookup.data;
|
|
359
|
+
const rc = checkRange({ ecosystem: item.ecosystem, index, range: line, basis: "same_line", advisories, from: item.from });
|
|
360
|
+
// safe_versions holds the newest few; it must reach the smallest to cover them all.
|
|
361
|
+
if (rc.smallest_safe_in_range === null || !rc.safe_versions.includes(rc.smallest_safe_in_range))
|
|
362
|
+
continue;
|
|
363
|
+
if (item.ecosystem === "Go") {
|
|
364
|
+
const undated = rc.safe_versions.filter((v) => !index.releases[v]?.publishedAt).slice(0, GO_WAIVER_READS);
|
|
365
|
+
if (undated.length > 0) {
|
|
366
|
+
const read = await go.getReleases(item.package, undated);
|
|
367
|
+
if (read.status === "ok") {
|
|
368
|
+
for (const v of undated)
|
|
369
|
+
if (read.data.releases[v])
|
|
370
|
+
index.releases[v] = read.data.releases[v];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!rc.safe_versions.every((v) => isYoung(index.releases[v]?.publishedAt, now)))
|
|
375
|
+
continue;
|
|
376
|
+
const safe = new Set(rc.safe_versions);
|
|
377
|
+
const older = Object.values(index.releases).filter((r) => !r.withdrawn &&
|
|
378
|
+
!safe.has(r.version) &&
|
|
379
|
+
!isPrereleaseIn(item.ecosystem, r.version) &&
|
|
380
|
+
rangeAdmits(item.ecosystem, line, r.version) === true &&
|
|
381
|
+
!isYoung(r.publishedAt, now));
|
|
382
|
+
if (older.length > 0)
|
|
383
|
+
out.set(item, { line, olderVulnerable: older.length });
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
async function packageAdvisoryLists(fixSources, items) {
|
|
388
|
+
const out = new Map();
|
|
389
|
+
if (!fixSources)
|
|
390
|
+
return out;
|
|
391
|
+
const unique = new Map(items.map((i) => [lookupKey(i), i]));
|
|
392
|
+
await mapLimit([...unique.entries()], NPM_CONCURRENCY, async ([key, i]) => {
|
|
393
|
+
out.set(key, await fixSources.packageAdvisories(i.ecosystem, i.package).catch(() => null));
|
|
394
|
+
});
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
159
397
|
function planItem(item, lookup) {
|
|
160
398
|
if (lookup.status === "unreachable") {
|
|
161
399
|
return { item, blocked: `Registry could not be reached (${lookup.detail}); nothing about ${item.to} was verified.`, index: null, introduced: null };
|
|
@@ -168,14 +406,26 @@ function planItem(item, lookup) {
|
|
|
168
406
|
introduced: null,
|
|
169
407
|
};
|
|
170
408
|
}
|
|
409
|
+
if (item.unresolved)
|
|
410
|
+
return { item, blocked: item.unresolved, index: lookup.data, introduced: null };
|
|
171
411
|
const index = lookup.data;
|
|
172
412
|
const to = index.releases[item.to];
|
|
173
413
|
if (!to) {
|
|
174
|
-
return { item, blocked: `${item.package}@${item.to} is not a published version on ${item.ecosystem}
|
|
414
|
+
return { item, blocked: `${item.package}@${item.to} is not a published version on ${item.ecosystem}.${goMajorHint(item)}`, index, introduced: null };
|
|
175
415
|
}
|
|
176
416
|
return { item, blocked: null, index, introduced: introducedDependencies(to, selectBaseline(index, item).record) };
|
|
177
417
|
}
|
|
178
|
-
|
|
418
|
+
/** Go majors 2+ live at their own module path. */
|
|
419
|
+
function goMajorHint(item) {
|
|
420
|
+
if (item.ecosystem !== "Go")
|
|
421
|
+
return "";
|
|
422
|
+
const major = goVersionMajor(item.to);
|
|
423
|
+
const pathMajor = goPathMajor(item.package) ?? 1;
|
|
424
|
+
if (major === null || major < 2 || major === pathMajor)
|
|
425
|
+
return "";
|
|
426
|
+
return ` Go major ${major} is a different module: ${item.package.replace(/\/v\d+$/, "")}/v${major}.`;
|
|
427
|
+
}
|
|
428
|
+
async function fetchIndexes(items, sources, fullList) {
|
|
179
429
|
const wanted = new Map();
|
|
180
430
|
for (const item of items) {
|
|
181
431
|
const key = lookupKey(item);
|
|
@@ -187,7 +437,9 @@ async function fetchIndexes(items, sources) {
|
|
|
187
437
|
}
|
|
188
438
|
const out = new Map();
|
|
189
439
|
await mapLimit([...wanted.entries()], NPM_CONCURRENCY, async ([key, { item, versions }]) => {
|
|
190
|
-
|
|
440
|
+
// A reader that pages (crates.io) stops once it has the wanted versions; a constraint needs them all.
|
|
441
|
+
const list = item.ecosystem === "crates.io" && fullList.has(key) ? [] : [...versions];
|
|
442
|
+
out.set(key, await sources[item.ecosystem].getReleases(item.package, list));
|
|
191
443
|
});
|
|
192
444
|
return out;
|
|
193
445
|
}
|
|
@@ -198,39 +450,33 @@ function validateItems(raw) {
|
|
|
198
450
|
return raw.map((r, i) => {
|
|
199
451
|
const o = (r ?? {});
|
|
200
452
|
const where = `items[${i}]`;
|
|
201
|
-
|
|
202
|
-
|
|
453
|
+
const ecosystem = ECOSYSTEMS.find((e) => e === o.ecosystem || (typeof o.ecosystem === "string" && e.toLowerCase() === o.ecosystem.trim().toLowerCase()));
|
|
454
|
+
if (!ecosystem)
|
|
455
|
+
throw new Error(`${where}.ecosystem must be one of ${ECOSYSTEMS.map((e) => `"${e}"`).join(", ")}`);
|
|
203
456
|
if (typeof o.package !== "string" || !o.package.trim())
|
|
204
457
|
throw new Error(`${where}.package must be a non-empty string`);
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
|
|
458
|
+
const rawTo = typeof o.to === "string" ? o.to.trim() : "";
|
|
459
|
+
const target = parseTarget(ecosystem, rawTo);
|
|
460
|
+
if (!target) {
|
|
461
|
+
throw new Error(`${where}.to must be an exact version, "latest", or a ${ecosystem} version constraint such as ${EXAMPLES[ecosystem]} (got ${JSON.stringify(o.to)})`);
|
|
462
|
+
}
|
|
463
|
+
let from;
|
|
464
|
+
if (o.from !== undefined && o.from !== null && o.from !== "") {
|
|
465
|
+
const f = typeof o.from === "string" ? parseTarget(ecosystem, o.from) : null;
|
|
466
|
+
if (!f || (f.kind !== "exact" && f.kind !== "exact_or_series")) {
|
|
467
|
+
throw new Error(`${where}.from must be an exact version, the one installed (got ${JSON.stringify(o.from)}), not a range`);
|
|
468
|
+
}
|
|
469
|
+
from = f.version;
|
|
470
|
+
}
|
|
471
|
+
return { ecosystem, package: o.package.trim(), raw: rawTo, target, ...(from ? { from } : {}) };
|
|
208
472
|
});
|
|
209
473
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return s;
|
|
217
|
-
}
|
|
218
|
-
const LATEST = "latest";
|
|
219
|
-
/**
|
|
220
|
-
* `to: "latest"` -> the highest release that is neither a prerelease nor
|
|
221
|
-
* withdrawn (a deprecated latest stays: deprecation is a finding to report).
|
|
222
|
-
* Left as "latest" when the registry read failed or lists no release; the item
|
|
223
|
-
* is then blocked with the registry's own reason, or "not a published version".
|
|
224
|
-
*/
|
|
225
|
-
function resolveLatest(item, lookup) {
|
|
226
|
-
if (item.to !== LATEST || lookup.status !== "ok")
|
|
227
|
-
return item;
|
|
228
|
-
const candidates = Object.values(lookup.data.releases).filter((r) => !r.withdrawn && parseSemverPrecedence(r.version));
|
|
229
|
-
const stable = candidates.filter((r) => parseSemverPrecedence(r.version).prerelease.length === 0);
|
|
230
|
-
const pool = stable.length > 0 ? stable : candidates;
|
|
231
|
-
const newest = pool.reduce((max, r) => (max === null || (compareVersionPrecedence(r.version, max) ?? 0) > 0 ? r.version : max), null);
|
|
232
|
-
return newest ? { ...item, to: newest, requested: LATEST } : item;
|
|
233
|
-
}
|
|
474
|
+
const EXAMPLES = {
|
|
475
|
+
npm: '"7.x" or "^1.20.0"',
|
|
476
|
+
"crates.io": '"0.17" or "=0.23.x"',
|
|
477
|
+
PyPI: '"3.1.*", "~=3.1" or ">=2,<3"',
|
|
478
|
+
Go: '"v1" or "v0.23.x"',
|
|
479
|
+
};
|
|
234
480
|
function osvDep(item, version) {
|
|
235
481
|
return {
|
|
236
482
|
name: item.package,
|
|
@@ -253,8 +499,9 @@ function refs(entries) {
|
|
|
253
499
|
return [{ id: v.vulnId, severity: v.severity, summary: v.summary, maintenance: isMaintenanceNotice(v) }];
|
|
254
500
|
});
|
|
255
501
|
}
|
|
256
|
-
const instanceKey = (eco, name, version) => `${eco}\0${name}\0${version}`;
|
|
257
|
-
const
|
|
502
|
+
const instanceKey = (eco, name, version) => `${eco}\0${eco === "PyPI" ? normalizePypi(name) : name}\0${version}`;
|
|
503
|
+
const normalizePypi = (name) => name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
504
|
+
const lookupKey = (item) => `${item.ecosystem}\0${item.ecosystem === "PyPI" ? normalizePypi(item.package) : item.package}`;
|
|
258
505
|
const head = (item) => ({
|
|
259
506
|
ecosystem: item.ecosystem,
|
|
260
507
|
package: item.package,
|
|
@@ -270,10 +517,10 @@ function finish(checkedAt, results, meta) {
|
|
|
270
517
|
const parts = order.filter((v) => counts.has(v)).map((v) => `${counts.get(v)} ${v}`);
|
|
271
518
|
return {
|
|
272
519
|
checked_at: checkedAt,
|
|
273
|
-
summary: `${results.length} checked: ${parts.join(", ")}.
|
|
520
|
+
summary: `${results.length} checked: ${parts.join(", ")}. Write only "proceed" versions; for the rest use range_check's safe versions or report the evidence.`,
|
|
274
521
|
results,
|
|
275
522
|
_meta: {
|
|
276
|
-
sources: ["registry.npmjs.org", "crates.io", "index.crates.io", "osv.dev"],
|
|
523
|
+
sources: ["registry.npmjs.org", "crates.io", "index.crates.io", "pypi.org", "proxy.golang.org", "osv.dev"],
|
|
277
524
|
verdict_rule: VERDICT_RULE,
|
|
278
525
|
privacy: PRIVACY_NOTE,
|
|
279
526
|
offline: meta.offline,
|
|
@@ -41,4 +41,4 @@ export declare const dependencyHealthTool: {
|
|
|
41
41
|
};
|
|
42
42
|
};
|
|
43
43
|
};
|
|
44
|
-
export declare function executeDependencyHealth(_db: FourDADatabase, params: DependencyHealthParams, liveIntel: LiveIntelligence | null): Promise<DependencyHealthResult>;
|
|
44
|
+
export declare function executeDependencyHealth(_db: FourDADatabase | null, params: DependencyHealthParams, liveIntel: LiveIntelligence | null): Promise<DependencyHealthResult>;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { isActionableVulnerability } from "../live/maintenance.js";
|
|
10
10
|
import { driftFor } from "./install-drift-notes.js";
|
|
11
|
+
import { discoveryNote } from "../project-tree.js";
|
|
11
12
|
/** How long a health check waits for its own OSV scan before answering without CVE data (the scan keeps running). */
|
|
12
13
|
const HEALTH_SCAN_TIMEOUT_MS = 60_000;
|
|
13
14
|
export const dependencyHealthTool = {
|
|
@@ -209,6 +210,10 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
|
|
|
209
210
|
if (hidden > 0) {
|
|
210
211
|
parts.push(`${hidden} advisor${hidden !== 1 ? "ies" : "y"} not counted (platform-inactive or maintenance notices)`);
|
|
211
212
|
}
|
|
213
|
+
// "all healthy" over a partial walk must say it is partial.
|
|
214
|
+
const partial = discoveryNote(liveIntel.getDiscovery());
|
|
215
|
+
if (partial)
|
|
216
|
+
parts.push(partial.replace(/[.]$/, ""));
|
|
212
217
|
if (!vulnResult) {
|
|
213
218
|
parts.push(liveIntel.isEnabled()
|
|
214
219
|
? `CVE data not loaded: the OSV scan did not finish within ${HEALTH_SCAN_TIMEOUT_MS / 1000}s or OSV was unreachable. Call again to use it once it completes`
|
|
@@ -35,7 +35,7 @@ export declare function rangesContain(ranges: AdvisoryRangeEvent[][], installed:
|
|
|
35
35
|
* An advisory naming your dependency is only a gap if you are actually
|
|
36
36
|
* exposed: the live tool once graded three Hono CVEs `critical` on hono
|
|
37
37
|
* 4.13.2 when all three are fixed in 4.12.34, a version this repo had already
|
|
38
|
-
* pinned past via
|
|
38
|
+
* pinned past via pnpm overrides.
|
|
39
39
|
*
|
|
40
40
|
* Conservative by construction: a missing or unreadable installed version
|
|
41
41
|
* counts as AFFECTED. Never claim someone is safe on missing information.
|
|
@@ -105,7 +105,7 @@ export function rangesContain(ranges, installed) {
|
|
|
105
105
|
* An advisory naming your dependency is only a gap if you are actually
|
|
106
106
|
* exposed: the live tool once graded three Hono CVEs `critical` on hono
|
|
107
107
|
* 4.13.2 when all three are fixed in 4.12.34, a version this repo had already
|
|
108
|
-
* pinned past via
|
|
108
|
+
* pinned past via pnpm overrides.
|
|
109
109
|
*
|
|
110
110
|
* Conservative by construction: a missing or unreadable installed version
|
|
111
111
|
* counts as AFFECTED. Never claim someone is safe on missing information.
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* `temporal::{active_repo_roots, dep_within_active_root, shares_active_root}`
|
|
5
5
|
* and `knowledge_decay::{get_active_project_paths, linked_to}`.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import type { SqliteStatement } from "../sqlite-driver.js";
|
|
8
8
|
import type { FourDADatabase } from "../db.js";
|
|
9
9
|
/** `stmt.all(...)`, or [] when the statement cannot run: a read here never throws out of the tool. */
|
|
10
|
-
export declare function allRows<T>(stmt:
|
|
10
|
+
export declare function allRows<T>(stmt: SqliteStatement, ...args: unknown[]): T[];
|
|
11
11
|
/** Lowercase, forward slashes, no trailing slash (`knowledge_decay::normalize_project_path`). */
|
|
12
12
|
export declare function normPath(p: string): string;
|
|
13
13
|
/**
|