@4da/mcp-server 6.1.0 → 6.2.1

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 (57) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/README.md +14 -9
  3. package/dist/db.d.ts +15 -0
  4. package/dist/db.js +37 -2
  5. package/dist/live/changelog-classify.d.ts +9 -3
  6. package/dist/live/changelog-classify.js +122 -12
  7. package/dist/live/changelog.d.ts +8 -4
  8. package/dist/live/changelog.js +67 -29
  9. package/dist/live/fix-paths.js +1 -1
  10. package/dist/live/github-releases.d.ts +69 -0
  11. package/dist/live/github-releases.js +252 -0
  12. package/dist/live/go-releases.d.ts +48 -0
  13. package/dist/live/go-releases.js +196 -0
  14. package/dist/live/index.d.ts +4 -5
  15. package/dist/live/index.js +7 -1
  16. package/dist/live/osv-ranges.d.ts +6 -0
  17. package/dist/live/osv-ranges.js +3 -0
  18. package/dist/live/package-archive.d.ts +2 -0
  19. package/dist/live/package-archive.js +7 -4
  20. package/dist/live/pypi-releases.d.ts +44 -0
  21. package/dist/live/pypi-releases.js +103 -0
  22. package/dist/live/release-metadata.d.ts +17 -8
  23. package/dist/live/release-metadata.js +6 -3
  24. package/dist/live/upgrade-go.d.ts +62 -0
  25. package/dist/live/upgrade-go.js +130 -0
  26. package/dist/live/upgrade-pypi.d.ts +42 -0
  27. package/dist/live/upgrade-pypi.js +85 -0
  28. package/dist/live/upgrade-sources.d.ts +28 -11
  29. package/dist/live/upgrade-sources.js +86 -19
  30. package/dist/live/zip-archive.d.ts +22 -0
  31. package/dist/live/zip-archive.js +98 -0
  32. package/dist/schema-registry.js +3 -3
  33. package/dist/server-instructions.d.ts +6 -0
  34. package/dist/server-instructions.js +20 -15
  35. package/dist/tool-dispatch.d.ts +2 -0
  36. package/dist/tool-dispatch.js +41 -1
  37. package/dist/tools/briefing-stored-advisories.js +1 -1
  38. package/dist/tools/briefing-task-scope.js +2 -2
  39. package/dist/tools/dependency-check-signals.d.ts +16 -5
  40. package/dist/tools/dependency-check-signals.js +46 -18
  41. package/dist/tools/dependency-check-versions.d.ts +94 -0
  42. package/dist/tools/dependency-check-versions.js +213 -0
  43. package/dist/tools/dependency-check.d.ts +26 -10
  44. package/dist/tools/dependency-check.js +321 -74
  45. package/dist/tools/knowledge-gap-citations.js +1 -1
  46. package/dist/tools/upgrade-impact-callsites.d.ts +1 -0
  47. package/dist/tools/upgrade-impact-callsites.js +23 -5
  48. package/dist/tools/upgrade-impact-changelog.d.ts +41 -0
  49. package/dist/tools/upgrade-impact-changelog.js +79 -0
  50. package/dist/tools/upgrade-impact-imports.d.ts +42 -0
  51. package/dist/tools/upgrade-impact-imports.js +260 -0
  52. package/dist/tools/upgrade-impact-report.d.ts +13 -0
  53. package/dist/tools/upgrade-impact-report.js +13 -4
  54. package/dist/tools/upgrade-impact.d.ts +13 -4
  55. package/dist/tools/upgrade-impact.js +235 -84
  56. package/dist/tools/what-should-i-know.js +1 -1
  57. package/package.json +6 -6
@@ -0,0 +1,196 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Go module release index for dependency_check, from the module proxy
4
+ * (proxy.golang.org, the host `go get` downloads from):
5
+ *
6
+ * - `/@v/list`: the tagged versions, `+incompatible` ones included (a module
7
+ * that predates modules and tags v2+ without a `/v2` path).
8
+ * - `/@v/<version>.info`: one release's time. The list carries no dates, so
9
+ * this is read for the versions being judged only (cached; a published
10
+ * version never changes). A pseudo-version (`v0.0.0-20240102150405-abcdef123456`)
11
+ * is not in the list; its `.info` both confirms it exists and dates it.
12
+ * - The latest version's `go.mod`: its `retract` directives withdraw versions
13
+ * (`go get` skips them), reported like a yank.
14
+ * - An untagged module: `@latest` gives its newest pseudo-version.
15
+ *
16
+ * Major versions 2+ are different module paths (`.../v5`); the proxy lists
17
+ * each path's own versions only.
18
+ *
19
+ * Not applicable to Go and reported so by the signals: publisher identity
20
+ * (integrity is checked against sum.golang.org instead), install hooks (Go
21
+ * runs no package code at download), per-release dependency lists.
22
+ */
23
+ import { fetchWithTimeout } from "./http-utils.js";
24
+ import { compareVersions } from "./version-compare.js";
25
+ import { escapeGoPath } from "./upgrade-go.js";
26
+ const GO_PROXY = "https://proxy.golang.org";
27
+ const TIMEOUT_MS = 15_000;
28
+ const LIST_TTL = 3600;
29
+ const INFO_TTL = 30 * 86_400;
30
+ /** `.info` reads per getReleases call. */
31
+ const MAX_INFO_READS = 6;
32
+ export const GO_NOT_APPLICABLE = "not applicable to Go modules: the proxy carries no publisher identity (downloads are checked against sum.golang.org)";
33
+ /** A Go version: release, prerelease, pseudo-version, optional `+incompatible`. */
34
+ export const GO_VERSION = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+incompatible)?$/;
35
+ const PSEUDO = /(?:^v\d+\.\d+\.\d+-|[.-])(?:0\.)?(\d{14})-[0-9a-f]{12}(?:\+incompatible)?$/;
36
+ /** `v`-prefixed spelling of a Go version ("1.2.3" -> "v1.2.3"). */
37
+ export function goVersion(raw) {
38
+ const v = raw.trim();
39
+ return /^\d/.test(v) ? `v${v}` : v;
40
+ }
41
+ /** The commit time a pseudo-version embeds, as ISO, or null when `version` is not one. */
42
+ export function pseudoVersionTime(version) {
43
+ const m = PSEUDO.exec(version);
44
+ if (!m)
45
+ return null;
46
+ const t = m[1];
47
+ return `${t.slice(0, 4)}-${t.slice(4, 6)}-${t.slice(6, 8)}T${t.slice(8, 10)}:${t.slice(10, 12)}:${t.slice(12, 14)}Z`;
48
+ }
49
+ /** `retract` directives of a go.mod: single versions, `[low, high]` intervals, and blocks of either. */
50
+ export function parseRetractions(goMod) {
51
+ const out = [];
52
+ const entry = (text) => {
53
+ const [body, ...comment] = text.split("//");
54
+ const rationale = comment.join("//").trim() || null;
55
+ const interval = /^\s*\[\s*(v\S+?)\s*,\s*(v\S+?)\s*\]\s*$/.exec(body);
56
+ if (interval)
57
+ out.push({ low: interval[1], high: interval[2], rationale });
58
+ else {
59
+ const single = /^\s*(v\S+)\s*$/.exec(body);
60
+ if (single)
61
+ out.push({ low: single[1], high: single[1], rationale });
62
+ }
63
+ };
64
+ const lines = goMod.split("\n");
65
+ for (let i = 0; i < lines.length; i++) {
66
+ const m = /^\s*retract\s+(.*)$/.exec(lines[i]);
67
+ if (!m)
68
+ continue;
69
+ if (m[1].trim().startsWith("(")) {
70
+ for (i++; i < lines.length && !/^\s*\)/.test(lines[i]); i++)
71
+ entry(lines[i]);
72
+ }
73
+ else {
74
+ entry(m[1]);
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+ export class GoReleasesReader {
80
+ cache;
81
+ rateLimiter;
82
+ constructor(cache, rateLimiter) {
83
+ this.cache = cache;
84
+ this.rateLimiter = rateLimiter;
85
+ }
86
+ async getReleases(module, wanted) {
87
+ const list = await this.list(module);
88
+ if (list.status !== "ok")
89
+ return list;
90
+ const { versions, retractions } = list.data;
91
+ const releases = {};
92
+ for (const version of versions)
93
+ releases[version] = record(version, null, retractions);
94
+ // Dates (and existence, for a pseudo-version) of the versions being judged.
95
+ const want = [...new Set(wanted.map(goVersion))].filter((v) => GO_VERSION.test(v)).slice(0, MAX_INFO_READS);
96
+ await Promise.all(want.map(async (version) => {
97
+ const info = await this.info(module, version);
98
+ // The proxy answers `v20.10.0` with the `v20.10.0+incompatible` release: file it under its own name.
99
+ if (info === null || (info.Version && info.Version !== version))
100
+ return;
101
+ const time = info.Time ?? pseudoVersionTime(version);
102
+ releases[version] = { ...(releases[version] ?? record(version, null, retractions)), publishedAt: time ?? null };
103
+ }));
104
+ return { status: "ok", data: { ecosystem: "Go", name: module, createdAt: null, releases } };
105
+ }
106
+ async getCreatedAt() {
107
+ return { status: "ok", data: null };
108
+ }
109
+ async list(module) {
110
+ const key = `go-releases:${module}`;
111
+ const hit = this.cache.get(key);
112
+ if (hit)
113
+ return { status: "ok", data: hit };
114
+ const base = `${GO_PROXY}/${escapeGoPath(module)}`;
115
+ let versions;
116
+ try {
117
+ const res = await this.get(`${base}/@v/list`);
118
+ // The proxy answers 404 / 410 for a path it cannot resolve.
119
+ if (res.status === 404 || res.status === 410) {
120
+ return { status: "not_found", detail: `"${module}" is not a module the Go proxy can resolve` };
121
+ }
122
+ if (!res.ok)
123
+ return { status: "unreachable", detail: `Go module proxy answered HTTP ${res.status}` };
124
+ versions = (await res.text()).split("\n").map((v) => v.trim()).filter((v) => GO_VERSION.test(v));
125
+ if (versions.length === 0) {
126
+ // Untagged: the newest pseudo-version.
127
+ const latest = await this.get(`${base}/@latest`);
128
+ if (latest.ok) {
129
+ const info = (await latest.json());
130
+ if (info.Version && GO_VERSION.test(info.Version)) {
131
+ versions = [info.Version];
132
+ if (info.Time)
133
+ this.cache.set(`go-info:${module}@${info.Version}`, info, "go", INFO_TTL);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ catch (err) {
139
+ return { status: "unreachable", detail: `Go module proxy: ${err instanceof Error ? err.message : String(err)}` };
140
+ }
141
+ // Retractions live in the newest version's go.mod.
142
+ let retractions = [];
143
+ let retractionsNote = null;
144
+ const newest = versions.reduce((max, v) => (max === null || compareVersions(v, max, "Go") > 0 ? v : max), null);
145
+ if (newest) {
146
+ try {
147
+ const res = await this.get(`${base}/@v/${escapeGoPath(newest)}.mod`);
148
+ if (res.ok)
149
+ retractions = parseRetractions(await res.text());
150
+ else
151
+ retractionsNote = `go.mod of ${newest}: HTTP ${res.status}`;
152
+ }
153
+ catch (err) {
154
+ retractionsNote = `go.mod of ${newest}: ${err instanceof Error ? err.message : String(err)}`;
155
+ }
156
+ }
157
+ const data = { versions, retractions, retractionsNote };
158
+ this.cache.set(key, data, "go", LIST_TTL);
159
+ return { status: "ok", data };
160
+ }
161
+ async info(module, version) {
162
+ const key = `go-info:${module}@${version}`;
163
+ const hit = this.cache.get(key);
164
+ if (hit)
165
+ return hit;
166
+ try {
167
+ const res = await this.get(`${GO_PROXY}/${escapeGoPath(module)}/@v/${escapeGoPath(version)}.info`);
168
+ if (!res.ok)
169
+ return null;
170
+ const info = (await res.json());
171
+ this.cache.set(key, info, "go", INFO_TTL);
172
+ return info;
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ }
178
+ async get(url) {
179
+ if (!this.rateLimiter.consume("go"))
180
+ throw new Error("Go module proxy rate limit reached for this minute");
181
+ return fetchWithTimeout(url, {}, TIMEOUT_MS);
182
+ }
183
+ }
184
+ function record(version, publishedAt, retractions) {
185
+ const hit = retractions.find((r) => compareVersions(version, r.low, "Go") >= 0 && compareVersions(version, r.high, "Go") <= 0);
186
+ return {
187
+ version,
188
+ publishedAt,
189
+ trust: 0,
190
+ trustEvidence: GO_NOT_APPLICABLE,
191
+ installScripts: null,
192
+ deprecated: hit?.rationale ? `retracted: ${hit.rationale}` : null,
193
+ withdrawn: hit ? "retracted" : null,
194
+ dependencies: null,
195
+ };
196
+ }
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { SqliteDatabase } from "../sqlite-driver.js";
11
11
  import { FixPathSources } from "./fix-paths.js";
12
- import type { ReleaseMetadataSource } from "./release-metadata.js";
12
+ import type { CheckEcosystem, ReleaseMetadataSource } from "./release-metadata.js";
13
13
  import { type ResolutionGroup } from "./resolution.js";
14
14
  import type { TreeDiscovery } from "../project-tree.js";
15
15
  import type { InstallDriftRecord, ResolutionProvenance, ResolvedDependency, RegistryPackageInfo, VulnerabilityScanResult, LiveHeadline, LiveIntelligenceStatus } from "./types.js";
@@ -26,6 +26,8 @@ export declare class LiveIntelligence {
26
26
  private goRegistry;
27
27
  private npmPackuments;
28
28
  private cratesVersions;
29
+ private pypiReleases;
30
+ private goReleases;
29
31
  private fixPathSources;
30
32
  private enabled;
31
33
  private lastVulnScan;
@@ -210,10 +212,7 @@ export declare class LiveIntelligence {
210
212
  isInitialized(): boolean;
211
213
  fetchRegistryHealth(deps: ResolvedDependency[]): Promise<RegistryPackageInfo[]>;
212
214
  /** Per-release registry readers for dependency_check (name-only requests). */
213
- getReleaseSources(): {
214
- npm: ReleaseMetadataSource;
215
- "crates.io": ReleaseMetadataSource;
216
- };
215
+ getReleaseSources(): Record<CheckEcosystem, ReleaseMetadataSource>;
217
216
  /**
218
217
  * OSV advisories for explicit (ecosystem, name, version) triples that need
219
218
  * not be in the project, e.g. a version an agent is about to install. Same
@@ -18,6 +18,8 @@ import { PyPIRegistry } from "./pypi-registry.js";
18
18
  import { GoRegistry } from "./go-registry.js";
19
19
  import { NpmPackumentReader } from "./npm-packument.js";
20
20
  import { CratesVersionsReader } from "./crates-versions.js";
21
+ import { PypiReleasesReader } from "./pypi-releases.js";
22
+ import { GoReleasesReader } from "./go-releases.js";
21
23
  import { FixPathSources } from "./fix-paths.js";
22
24
  import { fetchRegistryHealthFor } from "./registry-health.js";
23
25
  import { commonPathRoot, dedupeDependencies, emptyVulnResult } from "./dependency-set.js";
@@ -38,6 +40,8 @@ export class LiveIntelligence {
38
40
  goRegistry;
39
41
  npmPackuments;
40
42
  cratesVersions;
43
+ pypiReleases;
44
+ goReleases;
41
45
  fixPathSources;
42
46
  enabled;
43
47
  lastVulnScan = null;
@@ -93,6 +97,8 @@ export class LiveIntelligence {
93
97
  this.goRegistry = new GoRegistry(this.cache, this.rateLimiter);
94
98
  this.npmPackuments = new NpmPackumentReader(this.cache, this.rateLimiter);
95
99
  this.cratesVersions = new CratesVersionsReader(this.cache, this.rateLimiter);
100
+ this.pypiReleases = new PypiReleasesReader(this.cache, this.rateLimiter);
101
+ this.goReleases = new GoReleasesReader(this.cache, this.rateLimiter);
96
102
  this.fixPathSources = new FixPathSources(this.cache, this.rateLimiter);
97
103
  }
98
104
  /** Advisory, registry and parent-requirement lookups for fix paths; null when offline. */
@@ -446,7 +452,7 @@ export class LiveIntelligence {
446
452
  }
447
453
  /** Per-release registry readers for dependency_check (name-only requests). */
448
454
  getReleaseSources() {
449
- return { npm: this.npmPackuments, "crates.io": this.cratesVersions };
455
+ return { npm: this.npmPackuments, "crates.io": this.cratesVersions, PyPI: this.pypiReleases, Go: this.goReleases };
450
456
  }
451
457
  /**
452
458
  * OSV advisories for explicit (ecosystem, name, version) triples that need
@@ -25,6 +25,12 @@ export interface AdvisoryRanges {
25
25
  id: string;
26
26
  spans: AffectedSpan[];
27
27
  versions: string[];
28
+ /**
29
+ * A maintenance notice (RustSec "unmaintained" and other informational
30
+ * notices), not an exploitable vulnerability. Absent on entries cached
31
+ * before 6.2.
32
+ */
33
+ maintenance?: boolean;
28
34
  }
29
35
  /**
30
36
  * Flatten the matching package's version ranges into spans. OSV sorts events
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { compareVersions, isComparable, parsePep440, samePackageName } from "./version-compare.js";
15
15
  import { parseSemverPrecedence } from "./semver-precedence.js";
16
+ import { isMaintenanceNotice } from "./maintenance.js";
16
17
  /**
17
18
  * Flatten the matching package's version ranges into spans. OSV sorts events
18
19
  * within a range; each `introduced` opens a span and the next `fixed` or
@@ -69,10 +70,12 @@ function listedVersions(affected, packageName, ecosystem) {
69
70
  export function advisoryRanges(vuln, packageName, ecosystem) {
70
71
  if (vuln.withdrawn)
71
72
  return null;
73
+ const informational = vuln.database_specific?.informational;
72
74
  return {
73
75
  id: vuln.id,
74
76
  spans: collectAffectedSpans(vuln.affected, packageName, ecosystem),
75
77
  versions: listedVersions(vuln.affected, packageName, ecosystem),
78
+ maintenance: isMaintenanceNotice({ summary: vuln.summary ?? "" }) || informational === "unmaintained" || informational === "notice",
76
79
  };
77
80
  }
78
81
  /** True when `version` lies in a span (or the explicit list) of this advisory. */
@@ -44,6 +44,8 @@ export declare function iterateTar(tar: Buffer): Generator<TarEntry>;
44
44
  * files are skipped.
45
45
  */
46
46
  export declare function readRootTextFiles(gz: Buffer, accept: (basename: string) => boolean): Map<string, string>;
47
+ /** Download a registry archive, refusing at MAX_COMPRESSED_BYTES. */
48
+ export declare function fetchArchiveBytes(url: string, fetchFn?: FetchFn, headers?: Record<string, string>): Promise<Buffer>;
47
49
  /** Download a registry archive (size-capped) and return the accepted root-level text files. */
48
50
  export declare function fetchArchiveRootFiles(url: string, accept: (basename: string) => boolean, fetchFn?: FetchFn, headers?: Record<string, string>): Promise<Map<string, string>>;
49
51
  export {};
@@ -176,11 +176,14 @@ async function readCapped(response, cap) {
176
176
  }
177
177
  return Buffer.concat(chunks);
178
178
  }
179
- /** Download a registry archive (size-capped) and return the accepted root-level text files. */
180
- export async function fetchArchiveRootFiles(url, accept, fetchFn = defaultFetch, headers = {}) {
179
+ /** Download a registry archive, refusing at MAX_COMPRESSED_BYTES. */
180
+ export async function fetchArchiveBytes(url, fetchFn = defaultFetch, headers = {}) {
181
181
  const response = await fetchFn(url, { headers }, ARCHIVE_TIMEOUT_MS);
182
182
  if (!response.ok)
183
183
  throw new ArchiveError(`archive download failed: HTTP ${response.status}`);
184
- const gz = await readCapped(response, MAX_COMPRESSED_BYTES);
185
- return readRootTextFiles(gz, accept);
184
+ return readCapped(response, MAX_COMPRESSED_BYTES);
185
+ }
186
+ /** Download a registry archive (size-capped) and return the accepted root-level text files. */
187
+ export async function fetchArchiveRootFiles(url, accept, fetchFn = defaultFetch, headers = {}) {
188
+ return readRootTextFiles(await fetchArchiveBytes(url, fetchFn, headers), accept);
186
189
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * PyPI release index for dependency_check, from the JSON API
3
+ * (`GET pypi.org/pypi/<name>/json`): every release with its upload time and
4
+ * whether it is yanked.
5
+ *
6
+ * - A release's publish time is its EARLIEST file upload (later wheels for
7
+ * new platforms do not make it newer).
8
+ * - A release is yanked when every one of its files is (PEP 592); pip then
9
+ * skips it unless pinned with `==`.
10
+ * - A release with no files cannot be installed and is left out.
11
+ * - Versions are PEP 440 and are kept as the registry spells them.
12
+ *
13
+ * Not read, and reported as not applicable by the signals: publisher identity
14
+ * (the JSON API carries none), install-time hooks (PyPI exposes none in its
15
+ * index; an sdist-only release runs its build backend at install), and
16
+ * per-release dependency lists (one request per release).
17
+ *
18
+ * Privacy: the request names the package and nothing else.
19
+ */
20
+ import type { LiveCache } from "./cache.js";
21
+ import type { RateLimiter } from "./rate-limiter.js";
22
+ import type { PackageReleaseIndex, RegistryLookup, ReleaseMetadataSource } from "./release-metadata.js";
23
+ interface PyPIFile {
24
+ upload_time_iso_8601?: string;
25
+ yanked?: boolean;
26
+ yanked_reason?: string | null;
27
+ }
28
+ interface PyPIProject {
29
+ info?: {
30
+ name?: string;
31
+ };
32
+ releases?: Record<string, PyPIFile[] | null>;
33
+ }
34
+ export declare const PYPI_NOT_APPLICABLE = "not applicable on PyPI: its JSON API carries no publisher identity or install-hook metadata";
35
+ /** The index the signals read, from one JSON API document. */
36
+ export declare function projectPypiProject(name: string, doc: PyPIProject): PackageReleaseIndex;
37
+ export declare class PypiReleasesReader implements ReleaseMetadataSource {
38
+ private cache;
39
+ private rateLimiter;
40
+ constructor(cache: LiveCache, rateLimiter: RateLimiter);
41
+ getReleases(name: string): Promise<RegistryLookup<PackageReleaseIndex>>;
42
+ getCreatedAt(name: string): Promise<RegistryLookup<string | null>>;
43
+ }
44
+ export {};
@@ -0,0 +1,103 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * PyPI release index for dependency_check, from the JSON API
4
+ * (`GET pypi.org/pypi/<name>/json`): every release with its upload time and
5
+ * whether it is yanked.
6
+ *
7
+ * - A release's publish time is its EARLIEST file upload (later wheels for
8
+ * new platforms do not make it newer).
9
+ * - A release is yanked when every one of its files is (PEP 592); pip then
10
+ * skips it unless pinned with `==`.
11
+ * - A release with no files cannot be installed and is left out.
12
+ * - Versions are PEP 440 and are kept as the registry spells them.
13
+ *
14
+ * Not read, and reported as not applicable by the signals: publisher identity
15
+ * (the JSON API carries none), install-time hooks (PyPI exposes none in its
16
+ * index; an sdist-only release runs its build backend at install), and
17
+ * per-release dependency lists (one request per release).
18
+ *
19
+ * Privacy: the request names the package and nothing else.
20
+ */
21
+ import { fetchWithTimeout } from "./http-utils.js";
22
+ import { parsePep440 } from "./version-compare.js";
23
+ import { normalizePypiName } from "./upgrade-pypi.js";
24
+ const PYPI_URL = "https://pypi.org/pypi";
25
+ const TIMEOUT_MS = 30_000;
26
+ const TTL_SECONDS = 3600;
27
+ export const PYPI_NOT_APPLICABLE = "not applicable on PyPI: its JSON API carries no publisher identity or install-hook metadata";
28
+ /** The index the signals read, from one JSON API document. */
29
+ export function projectPypiProject(name, doc) {
30
+ const releases = {};
31
+ let createdAt = null;
32
+ for (const [version, rawFiles] of Object.entries(doc.releases ?? {})) {
33
+ const files = Array.isArray(rawFiles) ? rawFiles : [];
34
+ if (files.length === 0 || !parsePep440(version))
35
+ continue;
36
+ const times = files.map((f) => f.upload_time_iso_8601).filter((t) => typeof t === "string").sort();
37
+ const publishedAt = times[0] ?? null;
38
+ if (publishedAt && (!createdAt || publishedAt < createdAt))
39
+ createdAt = publishedAt;
40
+ const yanked = files.every((f) => f.yanked === true);
41
+ const reason = yanked ? files.find((f) => f.yanked_reason)?.yanked_reason ?? null : null;
42
+ releases[version] = {
43
+ version,
44
+ publishedAt,
45
+ trust: 0,
46
+ trustEvidence: PYPI_NOT_APPLICABLE,
47
+ installScripts: null,
48
+ deprecated: reason ? `yanked: ${reason}` : null,
49
+ withdrawn: yanked ? "yanked" : null,
50
+ dependencies: null,
51
+ };
52
+ }
53
+ return { ecosystem: "PyPI", name: doc.info?.name ?? name, createdAt, releases };
54
+ }
55
+ export class PypiReleasesReader {
56
+ cache;
57
+ rateLimiter;
58
+ constructor(cache, rateLimiter) {
59
+ this.cache = cache;
60
+ this.rateLimiter = rateLimiter;
61
+ }
62
+ async getReleases(name) {
63
+ const normalized = normalizePypiName(name);
64
+ const key = `pypi-releases:${normalized}`;
65
+ const hit = this.cache.get(key);
66
+ if (hit)
67
+ return { status: "ok", data: hit };
68
+ if (!this.rateLimiter.consume("pypi")) {
69
+ return { status: "unreachable", detail: "PyPI rate limit reached for this minute" };
70
+ }
71
+ let response;
72
+ try {
73
+ response = await fetchWithTimeout(`${PYPI_URL}/${encodeURIComponent(normalized)}/json`, { headers: { Accept: "application/json" } }, TIMEOUT_MS);
74
+ }
75
+ catch (err) {
76
+ return { status: "unreachable", detail: `PyPI: ${errorText(err)}` };
77
+ }
78
+ if (response.status === 404)
79
+ return { status: "not_found", detail: `"${name}" is not on PyPI` };
80
+ if (!response.ok)
81
+ return { status: "unreachable", detail: `PyPI answered HTTP ${response.status}` };
82
+ let doc;
83
+ try {
84
+ doc = (await response.json());
85
+ }
86
+ catch (err) {
87
+ return { status: "unreachable", detail: `PyPI: unreadable project document (${errorText(err)})` };
88
+ }
89
+ if (!doc.releases || typeof doc.releases !== "object") {
90
+ return { status: "unreachable", detail: `PyPI returned no release list for ${name}` };
91
+ }
92
+ const index = projectPypiProject(name, doc);
93
+ this.cache.set(key, index, "pypi", TTL_SECONDS);
94
+ return { status: "ok", data: index };
95
+ }
96
+ async getCreatedAt(name) {
97
+ const lookup = await this.getReleases(name);
98
+ return lookup.status === "ok" ? { status: "ok", data: lookup.data.createdAt } : lookup;
99
+ }
100
+ }
101
+ function errorText(err) {
102
+ return err instanceof Error ? err.message : String(err);
103
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Per-release registry metadata, normalised across npm and crates.io.
2
+ * Per-release registry metadata, normalised across npm, crates.io, PyPI and Go.
3
3
  *
4
4
  * `dependency_check` judges ONE proposed version against the one installed
5
5
  * (or, for a new dependency, against the previous release on the same line).
@@ -9,8 +9,11 @@
9
9
  * raw responses into this shape so the signal logic never sees a registry's
10
10
  * wire format.
11
11
  *
12
- * Privacy: every request behind this shape carries the package NAME only.
13
- * Versions are selected locally from the full release list.
12
+ * Privacy: every npm, crates.io and PyPI request behind this shape carries
13
+ * the package NAME only; versions are selected locally from the full release
14
+ * list. The Go module proxy lists versions without dates, so a Go release's
15
+ * date is read from its own `/@v/<version>.info` (the proxy `go get` itself
16
+ * downloads from), for the versions being judged only.
14
17
  */
15
18
  /**
16
19
  * Publish trust of one release.
@@ -27,17 +30,23 @@ export interface ReleaseRecord {
27
30
  trust: TrustLevel;
28
31
  /** Human label for the trust evidence, e.g. "trusted publisher (github)". */
29
32
  trustEvidence: string;
30
- /** npm: install-time lifecycle scripts declared (preinstall/install/postinstall, or a gypfile). Null for crates. */
33
+ /** npm: install-time lifecycle scripts declared (preinstall/install/postinstall, or a gypfile). Null elsewhere. */
31
34
  installScripts: string[] | null;
32
35
  /** npm deprecation message, or null. */
33
36
  deprecated: string | null;
34
- /** crates.io: yanked. npm: unpublished (listed in `time` but gone from `versions`). */
35
- withdrawn: "yanked" | "unpublished" | null;
36
- /** Runtime dependency NAMES (npm dependencies + optionalDependencies; crates normal + build). Null when unknown. */
37
+ /**
38
+ * crates.io / PyPI: yanked (PyPI: every file of the release). npm: unpublished
39
+ * (listed in `time` but gone from `versions`). Go: retracted by a `retract`
40
+ * directive in the module's latest go.mod.
41
+ */
42
+ withdrawn: "yanked" | "unpublished" | "retracted" | null;
43
+ /** Runtime dependency NAMES (npm dependencies + optionalDependencies; crates normal + build). Null when unknown or not read (PyPI, Go). */
37
44
  dependencies: string[] | null;
38
45
  }
46
+ /** The registries dependency_check reads. */
47
+ export type CheckEcosystem = "npm" | "crates.io" | "PyPI" | "Go";
39
48
  export interface PackageReleaseIndex {
40
- ecosystem: "npm" | "crates.io";
49
+ ecosystem: CheckEcosystem;
41
50
  name: string;
42
51
  /** When the package itself was first published, or null. */
43
52
  createdAt: string | null;
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
- * Per-release registry metadata, normalised across npm and crates.io.
3
+ * Per-release registry metadata, normalised across npm, crates.io, PyPI and Go.
4
4
  *
5
5
  * `dependency_check` judges ONE proposed version against the one installed
6
6
  * (or, for a new dependency, against the previous release on the same line).
@@ -10,7 +10,10 @@
10
10
  * raw responses into this shape so the signal logic never sees a registry's
11
11
  * wire format.
12
12
  *
13
- * Privacy: every request behind this shape carries the package NAME only.
14
- * Versions are selected locally from the full release list.
13
+ * Privacy: every npm, crates.io and PyPI request behind this shape carries
14
+ * the package NAME only; versions are selected locally from the full release
15
+ * list. The Go module proxy lists versions without dates, so a Go release's
16
+ * date is read from its own `/@v/<version>.info` (the proxy `go get` itself
17
+ * downloads from), for the versions being judged only.
15
18
  */
16
19
  export {};
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Go modules for `upgrade_impact`, from the module proxy (proxy.golang.org):
3
+ * `/@v/list` for the tagged versions, `/@v/<version>.info` for a release's
4
+ * time and VCS origin, and `/@v/<version>.zip` as the archive a changelog is
5
+ * read from (the zip holds the module's files under `<module>@<version>/`).
6
+ *
7
+ * Major versions 2 and up are DIFFERENT modules: `github.com/jackc/pgx/v5`
8
+ * is not a newer release of `github.com/jackc/pgx/v4`, and the proxy lists
9
+ * each path's own versions only. `goMajorPath` gives the path a major lives
10
+ * at, so an upgrade across majors is read from the target module and the
11
+ * report can say that every import path changes.
12
+ *
13
+ * `.info` carries `Origin` (VCS URL, the tag's ref, the module's subdirectory
14
+ * in a multi-module repository) for versions the proxy fetched recently.
15
+ * That, not a guess from the import path, names the GitHub repository and tag
16
+ * for the opt-in release-notes fallback: `go.uber.org/zap` lives at
17
+ * github.com/uber-go/zap, and aws-sdk-go-v2's `service/s3` tags its releases
18
+ * `service/s3/v1.50.0`.
19
+ */
20
+ import type { PublishedVersion, RegistryIndex, UpgradeNet } from "./upgrade-sources.js";
21
+ /** A module path: a dotted host, then path elements (case kept; the proxy escapes capitals). */
22
+ export declare const GO_MODULE: RegExp;
23
+ /** Proxy escaping: each capital letter becomes `!` + lowercase. */
24
+ export declare function escapeGoPath(path: string): string;
25
+ export interface GoInfo {
26
+ Version?: string;
27
+ Time?: string;
28
+ Origin?: {
29
+ VCS?: string;
30
+ URL?: string;
31
+ Subdir?: string;
32
+ Ref?: string;
33
+ };
34
+ }
35
+ /** The major version a module path carries: `/v5` -> 5, `gopkg.in/yaml.v3` -> 3, none -> null (0 or 1). */
36
+ export declare function goPathMajor(path: string): number | null;
37
+ /** The module path without its major suffix. */
38
+ export declare function goBasePath(path: string): string;
39
+ /** The path major `major` lives at: none for 0 and 1, `/vN` (or gopkg.in's `.vN`) from 2. */
40
+ export declare function goMajorPath(path: string, major: number): string;
41
+ export declare function goVersionMajor(version: string): number | null;
42
+ /** GitHub repository from a module path: `github.com/<owner>/<repo>/...`. */
43
+ export declare function githubRepoOfGoPath(path: string): string | null;
44
+ export declare function goInfo(net: UpgradeNet, module: string, version: string): Promise<GoInfo | null>;
45
+ export declare function loadGoIndex(net: UpgradeNet, module: string): Promise<RegistryIndex | null>;
46
+ /**
47
+ * Publish times for the versions an upgrade crosses (newest first, at most
48
+ * MAX_INFO_READS), and the target's VCS origin. Missing `.info` leaves a null date.
49
+ */
50
+ export declare function enrichGoVersions(net: UpgradeNet, module: string, versions: PublishedVersion[], target: PublishedVersion | null, budget?: number): Promise<{
51
+ origin: GoInfo["Origin"] | null;
52
+ }>;
53
+ /**
54
+ * The tag prefix a module's releases carry: `service/s3` for
55
+ * `refs/tags/service/s3/v1.50.0`, "" for a module at the repository root.
56
+ * From the proxy's recorded ref when there is one; otherwise from a
57
+ * github.com path (the elements after owner/repo, minus a `/vN` suffix,
58
+ * which a major-version subdirectory does not put in its tags).
59
+ */
60
+ export declare function goTagPrefix(module: string, version: string, origin: GoInfo["Origin"] | null): string;
61
+ /** The proxy's zip of one module version. */
62
+ export declare function goZipUrl(module: string, version: string): string;