@4da/mcp-server 5.0.3 → 6.0.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 +667 -0
- package/README.md +66 -35
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -2
- package/dist/db.js +242 -36
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +31 -1
- package/dist/live/osv-scanner.js +119 -10
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -1
- package/package.json +27 -30
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape every lockfile reader returns, shared by lockfile-parsers.ts,
|
|
3
|
+
* lockfile-parsers-pygo.ts and js-lockfile-readers.ts.
|
|
4
|
+
*/
|
|
5
|
+
/** One installed copy of a package. `dev` is undefined when the lockfile does not say. */
|
|
6
|
+
export interface PackageInstance {
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
dev?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Versions read from one directory, and the file they came from. */
|
|
12
|
+
export interface VersionSource {
|
|
13
|
+
/** The version a direct dependency of this name resolves to. */
|
|
14
|
+
versions: Map<string, string>;
|
|
15
|
+
/** Every installed copy, deduplicated by name@version. */
|
|
16
|
+
instances: PackageInstance[];
|
|
17
|
+
/** Absolute path of the file the versions were read from, or null when none exists. */
|
|
18
|
+
source: string | null;
|
|
19
|
+
/** `lockfile` = exact versions; `manifest` = read from a manifest (pins or specifier floors). */
|
|
20
|
+
kind: "lockfile" | "manifest" | null;
|
|
21
|
+
}
|
|
22
|
+
export declare function emptySource(): VersionSource;
|
|
23
|
+
/**
|
|
24
|
+
* Collects instances deduplicated by name@version. A copy seen as runtime
|
|
25
|
+
* anywhere stays runtime; dev is only claimed when every sighting says dev,
|
|
26
|
+
* and unknown when any sighting does not say.
|
|
27
|
+
*/
|
|
28
|
+
export declare class InstanceSet {
|
|
29
|
+
private byKey;
|
|
30
|
+
add(name: string, version: string, dev?: boolean): void;
|
|
31
|
+
get size(): number;
|
|
32
|
+
toArray(): PackageInstance[];
|
|
33
|
+
}
|
|
34
|
+
export declare function found(versions: Map<string, string>, instances: InstanceSet | PackageInstance[], source: string, kind: "lockfile" | "manifest"): VersionSource;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* The shape every lockfile reader returns, shared by lockfile-parsers.ts,
|
|
4
|
+
* lockfile-parsers-pygo.ts and js-lockfile-readers.ts.
|
|
5
|
+
*/
|
|
6
|
+
export function emptySource() {
|
|
7
|
+
return { versions: new Map(), instances: [], source: null, kind: null };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Collects instances deduplicated by name@version. A copy seen as runtime
|
|
11
|
+
* anywhere stays runtime; dev is only claimed when every sighting says dev,
|
|
12
|
+
* and unknown when any sighting does not say.
|
|
13
|
+
*/
|
|
14
|
+
export class InstanceSet {
|
|
15
|
+
byKey = new Map();
|
|
16
|
+
add(name, version, dev) {
|
|
17
|
+
const key = `${name}\0${version}`;
|
|
18
|
+
const existing = this.byKey.get(key);
|
|
19
|
+
if (!existing) {
|
|
20
|
+
this.byKey.set(key, dev === undefined ? { name, version } : { name, version, dev });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (existing.dev === undefined || dev === undefined)
|
|
24
|
+
delete existing.dev;
|
|
25
|
+
else
|
|
26
|
+
existing.dev = existing.dev && dev;
|
|
27
|
+
}
|
|
28
|
+
get size() {
|
|
29
|
+
return this.byKey.size;
|
|
30
|
+
}
|
|
31
|
+
toArray() {
|
|
32
|
+
return [...this.byKey.values()];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function found(versions, instances, source, kind) {
|
|
36
|
+
return {
|
|
37
|
+
versions,
|
|
38
|
+
instances: Array.isArray(instances) ? instances : instances.toArray(),
|
|
39
|
+
source,
|
|
40
|
+
kind,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maintenance-notice predicate, shared by every consumer of a vulnerability
|
|
3
|
+
* scan (vulnerability_scan, get_actionable_signals, what_should_i_know,
|
|
4
|
+
* dependency_health) so they all draw the same line.
|
|
5
|
+
*/
|
|
6
|
+
import type { VulnerabilityEntry } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Is this advisory a maintenance notice rather than a vulnerability?
|
|
9
|
+
*
|
|
10
|
+
* RustSec publishes "X is unmaintained" as an advisory so tooling can see it,
|
|
11
|
+
* and OSV carries it through with no severity and no fixed version. It is real
|
|
12
|
+
* information — an unmaintained dependency is a risk to plan around — but it is
|
|
13
|
+
* not something an attacker can exploit and not something you can upgrade away
|
|
14
|
+
* today, so it does not belong in the same list, the same count, or the same
|
|
15
|
+
* "Review X" recommendation as a live CVE.
|
|
16
|
+
*
|
|
17
|
+
* Live scan: 23 of 41 findings were these (the whole gtk-rs GTK3 family, all
|
|
18
|
+
* five `unic-*` crates, `paste`, `proc-macro-error`, `ttf-parser`), producing
|
|
19
|
+
* 27 "Review" lines around six genuinely actionable upgrades.
|
|
20
|
+
*
|
|
21
|
+
* Matched on the advisory's own wording, which RustSec keeps consistent, and
|
|
22
|
+
* deliberately NOT on "deprecated" — that word appears in plenty of real
|
|
23
|
+
* vulnerability summaries.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isMaintenanceNotice(v: Pick<VulnerabilityEntry, "summary">): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The advisories a scan can actually ask the user to act on today: built on
|
|
28
|
+
* this host AND exploitable (not a maintenance notice). This is the set
|
|
29
|
+
* `vulnerability_scan` reports as `vulnerabilities` / `total_vulnerable_packages`;
|
|
30
|
+
* every other tool that counts "vulnerable" must count the same set or the
|
|
31
|
+
* numbers disagree across tools over one scan.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isActionableVulnerability(v: Pick<VulnerabilityEntry, "summary" | "platformActive">): boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Maintenance-notice predicate, shared by every consumer of a vulnerability
|
|
4
|
+
* scan (vulnerability_scan, get_actionable_signals, what_should_i_know,
|
|
5
|
+
* dependency_health) so they all draw the same line.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Is this advisory a maintenance notice rather than a vulnerability?
|
|
9
|
+
*
|
|
10
|
+
* RustSec publishes "X is unmaintained" as an advisory so tooling can see it,
|
|
11
|
+
* and OSV carries it through with no severity and no fixed version. It is real
|
|
12
|
+
* information — an unmaintained dependency is a risk to plan around — but it is
|
|
13
|
+
* not something an attacker can exploit and not something you can upgrade away
|
|
14
|
+
* today, so it does not belong in the same list, the same count, or the same
|
|
15
|
+
* "Review X" recommendation as a live CVE.
|
|
16
|
+
*
|
|
17
|
+
* Live scan: 23 of 41 findings were these (the whole gtk-rs GTK3 family, all
|
|
18
|
+
* five `unic-*` crates, `paste`, `proc-macro-error`, `ttf-parser`), producing
|
|
19
|
+
* 27 "Review" lines around six genuinely actionable upgrades.
|
|
20
|
+
*
|
|
21
|
+
* Matched on the advisory's own wording, which RustSec keeps consistent, and
|
|
22
|
+
* deliberately NOT on "deprecated" — that word appears in plenty of real
|
|
23
|
+
* vulnerability summaries.
|
|
24
|
+
*/
|
|
25
|
+
export function isMaintenanceNotice(v) {
|
|
26
|
+
return /\b(unmaintained|no longer maintained)\b/i.test(v.summary || "");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The advisories a scan can actually ask the user to act on today: built on
|
|
30
|
+
* this host AND exploitable (not a maintenance notice). This is the set
|
|
31
|
+
* `vulnerability_scan` reports as `vulnerabilities` / `total_vulnerable_packages`;
|
|
32
|
+
* every other tool that counts "vulnerable" must count the same set or the
|
|
33
|
+
* numbers disagree across tools over one scan.
|
|
34
|
+
*/
|
|
35
|
+
export function isActionableVulnerability(v) {
|
|
36
|
+
return v.platformActive !== false && !isMaintenanceNotice(v);
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm full-packument reader for dependency_check.
|
|
3
|
+
*
|
|
4
|
+
* The abbreviated metadata the freshness reader uses carries no publish times,
|
|
5
|
+
* publishers, attestations, scripts or per-version dependencies, so this reads
|
|
6
|
+
* the FULL packument (`GET registry.npmjs.org/<name>`) and keeps only what the
|
|
7
|
+
* signals need. Some packuments are ~15 MB, so the projection is cached on
|
|
8
|
+
* disk (the live_cache table) together with the registry's ETag, and later
|
|
9
|
+
* reads revalidate with `If-None-Match` — an unchanged package costs a 304.
|
|
10
|
+
*
|
|
11
|
+
* Privacy: the request names the package and nothing else. The installed
|
|
12
|
+
* version is never sent; versions are picked out of the full list locally.
|
|
13
|
+
*/
|
|
14
|
+
import type { LiveCache } from "./cache.js";
|
|
15
|
+
import type { RateLimiter } from "./rate-limiter.js";
|
|
16
|
+
import type { PackageReleaseIndex, RegistryLookup, ReleaseMetadataSource } from "./release-metadata.js";
|
|
17
|
+
interface NpmVersionDoc {
|
|
18
|
+
_npmUser?: {
|
|
19
|
+
name?: string;
|
|
20
|
+
trustedPublisher?: {
|
|
21
|
+
id?: string;
|
|
22
|
+
} | null;
|
|
23
|
+
};
|
|
24
|
+
dist?: {
|
|
25
|
+
attestations?: unknown;
|
|
26
|
+
};
|
|
27
|
+
scripts?: Record<string, string>;
|
|
28
|
+
gypfile?: boolean;
|
|
29
|
+
hasInstallScript?: boolean;
|
|
30
|
+
dependencies?: Record<string, string>;
|
|
31
|
+
optionalDependencies?: Record<string, string>;
|
|
32
|
+
deprecated?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface NpmPackument {
|
|
35
|
+
versions?: Record<string, NpmVersionDoc>;
|
|
36
|
+
time?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
export declare class NpmPackumentReader implements ReleaseMetadataSource {
|
|
39
|
+
private cache;
|
|
40
|
+
private rateLimiter;
|
|
41
|
+
constructor(cache: LiveCache, rateLimiter: RateLimiter);
|
|
42
|
+
getReleases(name: string): Promise<RegistryLookup<PackageReleaseIndex>>;
|
|
43
|
+
getCreatedAt(name: string): Promise<RegistryLookup<string | null>>;
|
|
44
|
+
}
|
|
45
|
+
/** Keep only what dependency_check reads. Exported for tests. */
|
|
46
|
+
export declare function projectPackument(name: string, doc: NpmPackument): PackageReleaseIndex;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* npm full-packument reader for dependency_check.
|
|
4
|
+
*
|
|
5
|
+
* The abbreviated metadata the freshness reader uses carries no publish times,
|
|
6
|
+
* publishers, attestations, scripts or per-version dependencies, so this reads
|
|
7
|
+
* the FULL packument (`GET registry.npmjs.org/<name>`) and keeps only what the
|
|
8
|
+
* signals need. Some packuments are ~15 MB, so the projection is cached on
|
|
9
|
+
* disk (the live_cache table) together with the registry's ETag, and later
|
|
10
|
+
* reads revalidate with `If-None-Match` — an unchanged package costs a 304.
|
|
11
|
+
*
|
|
12
|
+
* Privacy: the request names the package and nothing else. The installed
|
|
13
|
+
* version is never sent; versions are picked out of the full list locally.
|
|
14
|
+
*/
|
|
15
|
+
import { fetchWithTimeout } from "./http-utils.js";
|
|
16
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
17
|
+
// Full packuments can be large; 8 s (the abbreviated reader's budget) is not enough.
|
|
18
|
+
const PACKUMENT_TIMEOUT_MS = 30_000;
|
|
19
|
+
/** Serve the cached projection without revalidating for this long. */
|
|
20
|
+
const FRESH_MS = 15 * 60 * 1000;
|
|
21
|
+
/** Keep the row (and its ETag) this long so revalidation survives restarts. */
|
|
22
|
+
const ROW_TTL_SECONDS = 7 * 86_400;
|
|
23
|
+
const INSTALL_HOOKS = ["preinstall", "install", "postinstall"];
|
|
24
|
+
export class NpmPackumentReader {
|
|
25
|
+
cache;
|
|
26
|
+
rateLimiter;
|
|
27
|
+
constructor(cache, rateLimiter) {
|
|
28
|
+
this.cache = cache;
|
|
29
|
+
this.rateLimiter = rateLimiter;
|
|
30
|
+
}
|
|
31
|
+
async getReleases(name) {
|
|
32
|
+
const key = `npm-packument:${name}`;
|
|
33
|
+
const cached = this.cache.getStale(key)?.data ?? null;
|
|
34
|
+
if (cached && Date.now() - cached.fetchedAtMs < FRESH_MS) {
|
|
35
|
+
return { status: "ok", data: cached.index };
|
|
36
|
+
}
|
|
37
|
+
if (!this.rateLimiter.consume("npm")) {
|
|
38
|
+
return { status: "unreachable", detail: "npm registry rate limit reached for this minute" };
|
|
39
|
+
}
|
|
40
|
+
const headers = { Accept: "application/json" };
|
|
41
|
+
if (cached?.etag)
|
|
42
|
+
headers["If-None-Match"] = cached.etag;
|
|
43
|
+
let response;
|
|
44
|
+
try {
|
|
45
|
+
response = await fetchWithTimeout(`${NPM_REGISTRY_URL}/${encodeURIComponent(name)}`, { headers }, PACKUMENT_TIMEOUT_MS);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return { status: "unreachable", detail: `npm registry: ${errorText(err)}` };
|
|
49
|
+
}
|
|
50
|
+
if (response.status === 304 && cached) {
|
|
51
|
+
this.cache.set(key, { ...cached, fetchedAtMs: Date.now() }, "npm", ROW_TTL_SECONDS);
|
|
52
|
+
return { status: "ok", data: cached.index };
|
|
53
|
+
}
|
|
54
|
+
if (response.status === 404) {
|
|
55
|
+
return { status: "not_found", detail: `"${name}" is not on the public npm registry` };
|
|
56
|
+
}
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
return { status: "unreachable", detail: `npm registry answered HTTP ${response.status}` };
|
|
59
|
+
}
|
|
60
|
+
let doc;
|
|
61
|
+
try {
|
|
62
|
+
doc = (await response.json());
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
return { status: "unreachable", detail: `npm registry: unreadable packument (${errorText(err)})` };
|
|
66
|
+
}
|
|
67
|
+
const index = projectPackument(name, doc);
|
|
68
|
+
this.cache.set(key, { etag: response.headers.get("etag"), fetchedAtMs: Date.now(), index }, "npm", ROW_TTL_SECONDS);
|
|
69
|
+
return { status: "ok", data: index };
|
|
70
|
+
}
|
|
71
|
+
async getCreatedAt(name) {
|
|
72
|
+
const lookup = await this.getReleases(name);
|
|
73
|
+
return lookup.status === "ok" ? { status: "ok", data: lookup.data.createdAt } : lookup;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Keep only what dependency_check reads. Exported for tests. */
|
|
77
|
+
export function projectPackument(name, doc) {
|
|
78
|
+
const time = doc.time ?? {};
|
|
79
|
+
const releases = {};
|
|
80
|
+
for (const [version, v] of Object.entries(doc.versions ?? {})) {
|
|
81
|
+
const { trust, trustEvidence } = npmTrust(v);
|
|
82
|
+
releases[version] = {
|
|
83
|
+
version,
|
|
84
|
+
publishedAt: typeof time[version] === "string" ? time[version] : null,
|
|
85
|
+
trust,
|
|
86
|
+
trustEvidence,
|
|
87
|
+
installScripts: installScriptsOf(v),
|
|
88
|
+
deprecated: typeof v.deprecated === "string" && v.deprecated.length > 0 ? v.deprecated : null,
|
|
89
|
+
withdrawn: null,
|
|
90
|
+
dependencies: [
|
|
91
|
+
...new Set([...Object.keys(v.dependencies ?? {}), ...Object.keys(v.optionalDependencies ?? {})]),
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// A version listed in `time` but absent from `versions` was unpublished —
|
|
96
|
+
// npm keeps the timestamp so the number can never be reused. The 2026-03-31
|
|
97
|
+
// axios 1.14.1 / 0.30.4 releases look exactly like this today.
|
|
98
|
+
const unpublished = new Set();
|
|
99
|
+
for (const k of Object.keys(time)) {
|
|
100
|
+
if (k !== "created" && k !== "modified" && k !== "unpublished" && !releases[k])
|
|
101
|
+
unpublished.add(k);
|
|
102
|
+
}
|
|
103
|
+
const whole = time.unpublished;
|
|
104
|
+
if (whole && Array.isArray(whole.versions)) {
|
|
105
|
+
for (const v of whole.versions)
|
|
106
|
+
if (typeof v === "string" && !releases[v])
|
|
107
|
+
unpublished.add(v);
|
|
108
|
+
}
|
|
109
|
+
for (const version of unpublished) {
|
|
110
|
+
releases[version] = {
|
|
111
|
+
version,
|
|
112
|
+
publishedAt: typeof time[version] === "string" ? time[version] : null,
|
|
113
|
+
trust: 0,
|
|
114
|
+
trustEvidence: "unknown (release unpublished; its metadata is gone)",
|
|
115
|
+
installScripts: null,
|
|
116
|
+
deprecated: null,
|
|
117
|
+
withdrawn: "unpublished",
|
|
118
|
+
dependencies: null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
ecosystem: "npm",
|
|
123
|
+
name,
|
|
124
|
+
createdAt: typeof time.created === "string" ? time.created : null,
|
|
125
|
+
releases,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function npmTrust(v) {
|
|
129
|
+
const tp = v._npmUser?.trustedPublisher;
|
|
130
|
+
if (tp)
|
|
131
|
+
return { trust: 2, trustEvidence: `trusted publisher (${tp.id ?? "OIDC"})` };
|
|
132
|
+
if (v.dist?.attestations)
|
|
133
|
+
return { trust: 1, trustEvidence: "provenance attestation, no trusted publisher" };
|
|
134
|
+
const who = v._npmUser?.name ? ` (published by npm user "${v._npmUser.name}")` : "";
|
|
135
|
+
return { trust: 0, trustEvidence: `no trusted publisher or provenance attestation${who}` };
|
|
136
|
+
}
|
|
137
|
+
function installScriptsOf(v) {
|
|
138
|
+
const found = INSTALL_HOOKS.filter((hook) => typeof v.scripts?.[hook] === "string");
|
|
139
|
+
// npm runs `node-gyp rebuild` as an implicit install script when the package
|
|
140
|
+
// ships a binding.gyp and declares no install/preinstall of its own.
|
|
141
|
+
if (v.gypfile && !found.includes("install") && !found.includes("preinstall")) {
|
|
142
|
+
found.push("install (implicit node-gyp rebuild)");
|
|
143
|
+
}
|
|
144
|
+
if (found.length === 0 && v.hasInstallScript)
|
|
145
|
+
found.push("install script (hasInstallScript)");
|
|
146
|
+
return found;
|
|
147
|
+
}
|
|
148
|
+
function errorText(err) {
|
|
149
|
+
return err instanceof Error ? err.message : String(err);
|
|
150
|
+
}
|
|
@@ -22,57 +22,33 @@ export class NpmRegistry {
|
|
|
22
22
|
this.rateLimiter = rateLimiter;
|
|
23
23
|
}
|
|
24
24
|
async getPackageInfo(name, currentVersion, isDev) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
versionsBehind = computeSemverDistance(currentVersion, latestStable || latestTag);
|
|
25
|
+
// The cache holds facts about the PACKAGE; the answer is computed for the
|
|
26
|
+
// installed version on every call. It used to cache the finished answer by
|
|
27
|
+
// name, so the second project to ask about a package got the first one's
|
|
28
|
+
// `currentVersion` and distance.
|
|
29
|
+
const cacheKey = `npm-facts:${name}`;
|
|
30
|
+
let facts = this.cache.get(cacheKey);
|
|
31
|
+
if (facts === null) {
|
|
32
|
+
if (!this.rateLimiter.canProceed("npm")) {
|
|
33
|
+
facts = this.cache.getStale(cacheKey)?.data ?? null;
|
|
34
|
+
if (!facts)
|
|
35
|
+
return errorResult(name, currentVersion, isDev, "Rate limited");
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
try {
|
|
39
|
+
this.rateLimiter.consume("npm");
|
|
40
|
+
const meta = await fetchJson(`${NPM_REGISTRY_URL}/${encodeURIComponent(name)}`, { headers: { Accept: "application/vnd.npm.install-v1+json" } }, NPM_TIMEOUT_MS);
|
|
41
|
+
facts = packageFacts(meta);
|
|
42
|
+
this.cache.set(cacheKey, facts, "npm", NPM_CACHE_TTL);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
facts = this.cache.getStale(cacheKey)?.data ?? null;
|
|
46
|
+
if (!facts)
|
|
47
|
+
return errorResult(name, currentVersion, isDev, err instanceof Error ? err.message : String(err));
|
|
48
|
+
}
|
|
50
49
|
}
|
|
51
|
-
const result = {
|
|
52
|
-
name,
|
|
53
|
-
ecosystem: "npm",
|
|
54
|
-
currentVersion,
|
|
55
|
-
latestVersion: latestTag,
|
|
56
|
-
latestStableVersion: latestStable,
|
|
57
|
-
versionsBehind,
|
|
58
|
-
deprecated,
|
|
59
|
-
deprecationMessage,
|
|
60
|
-
lastPublished: meta.modified ?? null,
|
|
61
|
-
license: null, // abbreviated metadata does not include license
|
|
62
|
-
weeklyDownloads: null, // fetched separately via getBulkDownloads
|
|
63
|
-
isDev,
|
|
64
|
-
fetchError: null,
|
|
65
|
-
};
|
|
66
|
-
this.cache.set(cacheKey, result, "npm", NPM_CACHE_TTL);
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
const stale = this.cache.getStale(cacheKey);
|
|
71
|
-
if (stale)
|
|
72
|
-
return stale.data;
|
|
73
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
74
|
-
return errorResult(name, currentVersion, isDev, message);
|
|
75
50
|
}
|
|
51
|
+
return infoFor(name, currentVersion, isDev, facts);
|
|
76
52
|
}
|
|
77
53
|
async getBulkDownloads(names) {
|
|
78
54
|
const result = new Map();
|
|
@@ -127,10 +103,48 @@ export class NpmRegistry {
|
|
|
127
103
|
return result;
|
|
128
104
|
}
|
|
129
105
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
106
|
+
function packageFacts(meta) {
|
|
107
|
+
const deprecations = {};
|
|
108
|
+
for (const [version, entry] of Object.entries(meta.versions || {})) {
|
|
109
|
+
if (entry?.deprecated)
|
|
110
|
+
deprecations[version] = entry.deprecated;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
latestTag: meta["dist-tags"]?.latest ?? null,
|
|
114
|
+
// Semver-max, not last-published: maintenance releases of older lines are
|
|
115
|
+
// published after newer lines (see maxStableSemver).
|
|
116
|
+
latestStable: maxStableSemver(Object.keys(meta.versions || {})),
|
|
117
|
+
modified: meta.modified ?? null,
|
|
118
|
+
deprecations,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The answer for one installed version. Deprecation is the INSTALLED
|
|
123
|
+
* version's first (that is what the project runs): `mkdirp@0.5.1` is
|
|
124
|
+
* deprecated on npm while its latest is not, and checking only the latest
|
|
125
|
+
* entry reported it healthy. A deprecated latest still marks the whole
|
|
126
|
+
* package deprecated.
|
|
127
|
+
*/
|
|
128
|
+
function infoFor(name, currentVersion, isDev, facts) {
|
|
129
|
+
const installedDeprecation = currentVersion ? facts.deprecations[currentVersion] : undefined;
|
|
130
|
+
const latestDeprecation = facts.latestTag ? facts.deprecations[facts.latestTag] : undefined;
|
|
131
|
+
const target = facts.latestStable || facts.latestTag;
|
|
132
|
+
const versionsBehind = currentVersion && target ? computeSemverDistance(currentVersion, target) : null;
|
|
133
|
+
return {
|
|
134
|
+
name,
|
|
135
|
+
ecosystem: "npm",
|
|
136
|
+
currentVersion,
|
|
137
|
+
latestVersion: facts.latestTag,
|
|
138
|
+
latestStableVersion: facts.latestStable,
|
|
139
|
+
versionsBehind,
|
|
140
|
+
deprecated: Boolean(installedDeprecation || latestDeprecation),
|
|
141
|
+
deprecationMessage: installedDeprecation ?? latestDeprecation ?? null,
|
|
142
|
+
lastPublished: facts.modified,
|
|
143
|
+
license: null, // abbreviated metadata does not include license
|
|
144
|
+
weeklyDownloads: null, // fetched separately via getBulkDownloads
|
|
145
|
+
isDev,
|
|
146
|
+
fetchError: null,
|
|
147
|
+
};
|
|
134
148
|
}
|
|
135
149
|
function errorResult(name, currentVersion, isDev, fetchError) {
|
|
136
150
|
return {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { LiveCache } from "./cache.js";
|
|
10
10
|
import type { RateLimiter } from "./rate-limiter.js";
|
|
11
|
-
import type { ResolvedDependency, VulnerabilityScanResult } from "./types.js";
|
|
11
|
+
import type { ResolvedDependency, OsvVulnerability, VulnerabilityEntry, VulnerabilityScanResult } from "./types.js";
|
|
12
12
|
export declare class OsvScanner {
|
|
13
13
|
private cache;
|
|
14
14
|
private rateLimiter;
|
|
@@ -24,7 +24,37 @@ export declare class OsvScanner {
|
|
|
24
24
|
private hydrateDetails;
|
|
25
25
|
private fetchDetail;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Overwrite the dep-context fields of a cached advisory entry with the CURRENT
|
|
29
|
+
* resolution's context. The advisory facts (id, severity, summary, fix version)
|
|
30
|
+
* are stable and cacheable; `platformActive`, `target`, `sourceDirs`, and the
|
|
31
|
+
* dev/direct flags are functions of TODAY's dependency resolution, not of the
|
|
32
|
+
* advisory. Serving them from cache bakes yesterday's context into today's
|
|
33
|
+
* scan — observed live 2026-08-30: a pre-platform-filter scan cached `glib`
|
|
34
|
+
* as platform-active and `sandbox` with no source dirs, and every scan for the
|
|
35
|
+
* next hour repeated both errors while the fixed resolver sat idle.
|
|
36
|
+
*/
|
|
37
|
+
export declare function restampDepContext(cached: VulnerabilityEntry, dep: ResolvedDependency): VulnerabilityEntry;
|
|
27
38
|
export declare function extractCvssScore(severity: Array<{
|
|
28
39
|
type: string;
|
|
29
40
|
score: string;
|
|
30
41
|
}> | undefined): number | null;
|
|
42
|
+
/**
|
|
43
|
+
* Pick the fix version for the OSV range that actually CONTAINS the installed
|
|
44
|
+
* version — never another release line's fix. The old code returned the first
|
|
45
|
+
* range's first `fixed` event, so a multi-range advisory (e.g. undici fixed at
|
|
46
|
+
* 6.x on the 6 line and 7.x on the 7 line) recommended a DOWNGRADE to 6.x for
|
|
47
|
+
* an installed 7.x.
|
|
48
|
+
*
|
|
49
|
+
* Selection order:
|
|
50
|
+
* 1. The `fixed` of the interval containing installed (introduced <= installed
|
|
51
|
+
* < fixed).
|
|
52
|
+
* 2. If that interval has no fix (or none contains installed), the smallest
|
|
53
|
+
* published fix strictly ABOVE installed across all ranges.
|
|
54
|
+
* 3. If the installed version is unparseable, the highest published fix (it
|
|
55
|
+
* resolves the advisory regardless of release line).
|
|
56
|
+
* 4. Otherwise null — omitting the recommendation beats emitting a downgrade.
|
|
57
|
+
*
|
|
58
|
+
* Hard guard: the returned version is never below the installed version.
|
|
59
|
+
*/
|
|
60
|
+
export declare function extractFixedVersion(affected: OsvVulnerability["affected"] | undefined, packageName: string, ecosystem: string, installedVersion: string | null): string | null;
|