@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.
Files changed (172) hide show
  1. package/CHANGELOG.md +667 -0
  2. package/README.md +66 -35
  3. package/dist/build-staleness.d.ts +35 -0
  4. package/dist/build-staleness.js +95 -0
  5. package/dist/db.d.ts +53 -2
  6. package/dist/db.js +242 -36
  7. package/dist/doctor.js +27 -7
  8. package/dist/embeddings.js +7 -1
  9. package/dist/freshness-markers.d.ts +34 -0
  10. package/dist/freshness-markers.js +74 -0
  11. package/dist/gitignore.d.ts +37 -0
  12. package/dist/gitignore.js +130 -0
  13. package/dist/index.js +127 -44
  14. package/dist/live/bun-lockfile.d.ts +26 -0
  15. package/dist/live/bun-lockfile.js +123 -0
  16. package/dist/live/cargo-platform.d.ts +38 -0
  17. package/dist/live/cargo-platform.js +178 -0
  18. package/dist/live/changelog-classify.d.ts +41 -0
  19. package/dist/live/changelog-classify.js +207 -0
  20. package/dist/live/changelog.d.ts +64 -0
  21. package/dist/live/changelog.js +318 -0
  22. package/dist/live/crates-registry.js +3 -1
  23. package/dist/live/crates-versions.d.ts +47 -0
  24. package/dist/live/crates-versions.js +201 -0
  25. package/dist/live/dependency-set.d.ts +13 -0
  26. package/dist/live/dependency-set.js +95 -0
  27. package/dist/live/dev-scope.d.ts +32 -0
  28. package/dist/live/dev-scope.js +102 -0
  29. package/dist/live/file-signature.d.ts +18 -0
  30. package/dist/live/file-signature.js +43 -0
  31. package/dist/live/go-registry.js +3 -1
  32. package/dist/live/hn-fetcher.js +9 -3
  33. package/dist/live/index.d.ts +105 -2
  34. package/dist/live/index.js +266 -139
  35. package/dist/live/install-state.d.ts +43 -0
  36. package/dist/live/install-state.js +150 -0
  37. package/dist/live/js-lockfile-readers.d.ts +46 -0
  38. package/dist/live/js-lockfile-readers.js +181 -0
  39. package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
  40. package/dist/live/lockfile-parsers-pygo.js +274 -0
  41. package/dist/live/lockfile-parsers.d.ts +69 -0
  42. package/dist/live/lockfile-parsers.js +311 -0
  43. package/dist/live/lockfile-types.d.ts +34 -0
  44. package/dist/live/lockfile-types.js +42 -0
  45. package/dist/live/maintenance.d.ts +33 -0
  46. package/dist/live/maintenance.js +37 -0
  47. package/dist/live/npm-packument.d.ts +47 -0
  48. package/dist/live/npm-packument.js +150 -0
  49. package/dist/live/npm-registry.js +67 -53
  50. package/dist/live/osv-scanner.d.ts +31 -1
  51. package/dist/live/osv-scanner.js +119 -10
  52. package/dist/live/package-archive.d.ts +49 -0
  53. package/dist/live/package-archive.js +186 -0
  54. package/dist/live/pypi-registry.js +3 -1
  55. package/dist/live/registry-health.d.ts +16 -0
  56. package/dist/live/registry-health.js +74 -0
  57. package/dist/live/release-metadata.d.ts +67 -0
  58. package/dist/live/release-metadata.js +16 -0
  59. package/dist/live/resolution.d.ts +43 -0
  60. package/dist/live/resolution.js +62 -0
  61. package/dist/live/semver-precedence.d.ts +30 -0
  62. package/dist/live/semver-precedence.js +83 -0
  63. package/dist/live/severity-scope.d.ts +57 -0
  64. package/dist/live/severity-scope.js +89 -0
  65. package/dist/live/types.d.ts +83 -0
  66. package/dist/live/upgrade-sources.d.ts +71 -0
  67. package/dist/live/upgrade-sources.js +198 -0
  68. package/dist/live/version-compare.d.ts +39 -0
  69. package/dist/live/version-compare.js +130 -0
  70. package/dist/live/version-resolver.d.ts +14 -1
  71. package/dist/live/version-resolver.js +93 -303
  72. package/dist/native-bindings.d.ts +19 -0
  73. package/dist/native-bindings.js +39 -0
  74. package/dist/project-manifests.d.ts +40 -0
  75. package/dist/project-manifests.js +193 -0
  76. package/dist/project-scanner.js +38 -48
  77. package/dist/project-tree.d.ts +41 -0
  78. package/dist/project-tree.js +110 -0
  79. package/dist/prompts.d.ts +28 -0
  80. package/dist/prompts.js +54 -0
  81. package/dist/schema-registry.d.ts +41 -29
  82. package/dist/schema-registry.js +98 -105
  83. package/dist/server-instructions.d.ts +11 -0
  84. package/dist/server-instructions.js +36 -0
  85. package/dist/tool-args.d.ts +28 -0
  86. package/dist/tool-args.js +93 -0
  87. package/dist/tool-dispatch.d.ts +1 -1
  88. package/dist/tool-dispatch.js +42 -8
  89. package/dist/tools/app-plan.d.ts +92 -0
  90. package/dist/tools/app-plan.js +155 -0
  91. package/dist/tools/briefing-stored-advisories.d.ts +20 -0
  92. package/dist/tools/briefing-stored-advisories.js +58 -0
  93. package/dist/tools/briefing-task-scope.d.ts +73 -0
  94. package/dist/tools/briefing-task-scope.js +197 -0
  95. package/dist/tools/briefing-wisdom.d.ts +31 -0
  96. package/dist/tools/briefing-wisdom.js +148 -0
  97. package/dist/tools/decision-enforcement.d.ts +0 -5
  98. package/dist/tools/decision-enforcement.js +0 -4
  99. package/dist/tools/decision-memory.js +18 -66
  100. package/dist/tools/dependency-check-signals.d.ts +74 -0
  101. package/dist/tools/dependency-check-signals.js +264 -0
  102. package/dist/tools/dependency-check.d.ts +98 -0
  103. package/dist/tools/dependency-check.js +295 -0
  104. package/dist/tools/dependency-health.js +74 -21
  105. package/dist/tools/developer-dna.d.ts +3 -0
  106. package/dist/tools/developer-dna.js +16 -15
  107. package/dist/tools/ecosystem-pulse.d.ts +6 -2
  108. package/dist/tools/ecosystem-pulse.js +53 -14
  109. package/dist/tools/get-actionable-signals.d.ts +26 -4
  110. package/dist/tools/get-actionable-signals.js +214 -251
  111. package/dist/tools/get-context.d.ts +2 -0
  112. package/dist/tools/get-context.js +9 -1
  113. package/dist/tools/get-relevant-content.d.ts +3 -1
  114. package/dist/tools/get-relevant-content.js +34 -3
  115. package/dist/tools/index.d.ts +4 -2
  116. package/dist/tools/index.js +4 -2
  117. package/dist/tools/install-drift-notes.d.ts +51 -0
  118. package/dist/tools/install-drift-notes.js +172 -0
  119. package/dist/tools/knowledge-gap-citations.d.ts +55 -0
  120. package/dist/tools/knowledge-gap-citations.js +132 -0
  121. package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
  122. package/dist/tools/knowledge-gap-exposure.js +160 -0
  123. package/dist/tools/knowledge-gap-grading.d.ts +91 -0
  124. package/dist/tools/knowledge-gap-grading.js +157 -0
  125. package/dist/tools/knowledge-gap-installs.d.ts +65 -0
  126. package/dist/tools/knowledge-gap-installs.js +134 -0
  127. package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
  128. package/dist/tools/knowledge-gap-ranges.js +146 -0
  129. package/dist/tools/knowledge-gap-scope.d.ts +63 -0
  130. package/dist/tools/knowledge-gap-scope.js +118 -0
  131. package/dist/tools/knowledge-gaps.d.ts +12 -3
  132. package/dist/tools/knowledge-gaps.js +172 -121
  133. package/dist/tools/package-presence.d.ts +24 -0
  134. package/dist/tools/package-presence.js +35 -0
  135. package/dist/tools/record-feedback.d.ts +0 -1
  136. package/dist/tools/record-feedback.js +7 -8
  137. package/dist/tools/signal-classifier.d.ts +25 -0
  138. package/dist/tools/signal-classifier.js +40 -0
  139. package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
  140. package/dist/tools/upgrade-impact-callsites.js +226 -0
  141. package/dist/tools/upgrade-impact-literals.d.ts +45 -0
  142. package/dist/tools/upgrade-impact-literals.js +118 -0
  143. package/dist/tools/upgrade-impact-report.d.ts +80 -0
  144. package/dist/tools/upgrade-impact-report.js +178 -0
  145. package/dist/tools/upgrade-impact.d.ts +84 -0
  146. package/dist/tools/upgrade-impact.js +305 -0
  147. package/dist/tools/upgrade-planner.d.ts +41 -8
  148. package/dist/tools/upgrade-planner.js +201 -53
  149. package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
  150. package/dist/tools/vulnerability-scan-concise.js +97 -0
  151. package/dist/tools/vulnerability-scan-format.d.ts +137 -0
  152. package/dist/tools/vulnerability-scan-format.js +276 -0
  153. package/dist/tools/vulnerability-scan.d.ts +338 -18
  154. package/dist/tools/vulnerability-scan.js +84 -125
  155. package/dist/tools/what-should-i-know.d.ts +86 -19
  156. package/dist/tools/what-should-i-know.js +277 -283
  157. package/dist/types.d.ts +4 -1
  158. package/package.json +27 -30
  159. package/dist/schemas/agent-memory.json +0 -49
  160. package/dist/schemas/decision-enforcement.json +0 -22
  161. package/dist/schemas/decision-memory.json +0 -54
  162. package/dist/schemas/dependency-health.json +0 -27
  163. package/dist/schemas/developer-dna.json +0 -219
  164. package/dist/schemas/ecosystem-pulse.json +0 -17
  165. package/dist/schemas/get-actionable-signals.json +0 -29
  166. package/dist/schemas/get-context.json +0 -17
  167. package/dist/schemas/get-relevant-content.json +0 -26
  168. package/dist/schemas/knowledge-gaps.json +0 -14
  169. package/dist/schemas/record-feedback.json +0 -24
  170. package/dist/schemas/upgrade-planner.json +0 -22
  171. package/dist/schemas/vulnerability-scan.json +0 -26
  172. 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
- const cacheKey = `npm-reg:${name}`;
26
- const cached = this.cache.get(cacheKey);
27
- if (cached !== null)
28
- return cached;
29
- if (!this.rateLimiter.canProceed("npm")) {
30
- const stale = this.cache.getStale(cacheKey);
31
- if (stale)
32
- return stale.data;
33
- return errorResult(name, currentVersion, isDev, "Rate limited");
34
- }
35
- try {
36
- this.rateLimiter.consume("npm");
37
- const meta = await fetchJson(`${NPM_REGISTRY_URL}/${encodeURIComponent(name)}`, { headers: { Accept: "application/vnd.npm.install-v1+json" } }, NPM_TIMEOUT_MS);
38
- const latestTag = meta["dist-tags"]?.latest ?? null;
39
- const versions = Object.keys(meta.versions || {});
40
- // Latest stable = last version that is not a prerelease
41
- const latestStable = findLatestStable(versions);
42
- // Check deprecation on the latest version entry
43
- const latestVersionEntry = latestTag ? meta.versions[latestTag] : null;
44
- const deprecated = !!latestVersionEntry?.deprecated;
45
- const deprecationMessage = latestVersionEntry?.deprecated ?? null;
46
- // Semver distance between current and latest stable (or latest tag)
47
- let versionsBehind = null;
48
- if (currentVersion && (latestStable || latestTag)) {
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
- // Semver-max, not last-published: maintenance releases of older lines are
131
- // published after newer lines (see maxStableSemver).
132
- function findLatestStable(versions) {
133
- return maxStableSemver(versions);
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;